IO_FILE學習(一)
2020-08-22 14:01:55 hawk
因為參加的2020年全國大學生信息安全競賽創新實踐賽時,因為自己十分的菜,pwn題僅僅痛苦的做出了幾道。之后學校大佬分享了一下他們的wp,仔細查看部分題目的wp,解法涉及到了之前的盲區——IO_FILE(比如其中的一道題,我傻乎乎的將strdup的got表轉換為printf的plt地址,然后再leak對應的調用strdup的函數棧的基址,最后使用格式化字符串漏洞leak程序的libc基址並修改strdup為system,太笨了。。;而當后面出現了FULL RELRO和PIE保護機制時,這個方法就失效了,我也就不會了。。。),因此這里特點學習一下IO_FILE相關的知識,填補一下知識短板。
IO_FILE概述
眾所周知,Linux將一切都當作文件進行操作,因此實際上,對於程序的IO來說,也是如此。而顧名思義,IO_FILE就是和描述IO的文件結構體,我們首先查看一下相關的源代碼(我的是glibc2.23,不同版本內容可能會有一定差別),其中IO_FILE相關的源代碼位於glibc源代碼的libio/libioP.h文件中,如下所示
/* We always allocate an extra word following an _IO_FILE. This contains a pointer to the function jump table used. This is for compatibility with C++ streambuf; the word can be used to smash to a pointer to a virtual function table. */ struct _IO_FILE_plus { _IO_FILE file; const struct _IO_jump_t *vtable; };
實際上我們最終描述文件流文件的數據結構是_IO_FILE_plus,其中有_IO_FILE結構體和常量_IO_jump_t(內容不可被修改),而根據成員的名稱,我們大概可以推測出不同成員的作用——file成員應該包含的是該文件的一些關鍵數據;而vtable,也就是virtual table,虛表,即各種操作函數的指針。
下面我們再分別查看一個各自成員的組成,首先是_IO_FILE,其源代碼位於libio/libio.h文件中,如下所示
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 }; struct _IO_FILE_complete { struct _IO_FILE _file; #endif #if defined _G_IO_IO_FILE_VERSION && _G_IO_IO_FILE_VERSION == 0x20001 _IO_off64_t _offset; # if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T /* Wide character stream stuff. */ struct _IO_codecvt *_codecvt; struct _IO_wide_data *_wide_data; struct _IO_FILE *_freeres_list; void *_freeres_buf; # else void *__pad1; void *__pad2; void *__pad3; void *__pad4; # endif size_t __pad5; int _mode; /* Make sure we don't get into trouble again. */ char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)]; #endif };
該數據結構大概如此,其他大小通過輸出sizeof可以判斷(64/0xd8, 32/0x94)。我們根據源代碼中的注釋,就很容易注意到中間的那些指針應該就和輸入、輸出數據有相當大的關系了。這些我們會在后面在進行分析。
下面我們再介紹一下_IO_FILE_plus的另一個重要的成員結構,即_IO_jump_t結構,其源代碼位於libio/libioP.h文件中,如下所示
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 };
我們簡單列出來對應的數據的索引,方便之后進行查詢,如下所示
0,size_t, __dummy 1,size_t, __dummy2 2,_IO_finish_t, __finish 3,_IO_overflow_t, __overflow 4,_IO_underflow_t, __underflow 5,_IO_underflow_t, __uflow 6,_IO_pbackfail_t, __pbackfail 7,_IO_xsputn_t, __xsputn 8,_IO_xsgetn_t, __xsgetn 9,_IO_seekoff_t, __seekoff 10,_IO_seekpos_t, __seekpos 11,_IO_setbuf_t, __setbuf 12,_IO_sync_t, __sync 13,_IO_doallocate_t, __doallocate 14,_IO_read_t, __read 15,_IO_write_t, __write 16,_IO_seek_t, __seek 17,_IO_close_t, __close 18,_IO_stat_t, __stat 19,_IO_showmanyc_t, __showmanyc 20,_IO_imbue_t, __imbue
而我們在程序中經常會聽到如下一些關鍵字stdin、stdout等,實際上其也就是上面提到的結構,如下所示
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_;
可以看到,實際上我們所說的stdin、stdout以及stderr等,都是IO_FILE數據結構進行組織的。這樣子,我們就基本完成了IO_FILE相關知識的總體概括。而實際上單純對於IO_FILE結構來說,很難展開去講——因為涉及的方面過多,但是我們在ctf比賽或者利用的時候,並不需要那么多,因此下面我將結合功能進行講解。
puts分析
這里我們首先結合puts函數(往往可以用來leak地址),puts函數是由_IO_puts實現的(網絡資料),而_IO_puts的功能主要會調用_IO_sputn,而_IO_sputn是_IO_new_file_xsputn的包裝。當然這中間的分析極其復雜,我嘗試從頭到尾分析一遍,最后放棄了,因為過於龐大。但就我有限的嘗試來看,實際上前者都是對后者的一些包裝,也就是通過各種情況的判斷,從而提高對於后者的調用效率,因此我們只需要直接對於最后的_IO_new_file_overflow函數進行分析即可。_IO_new_file_xsputn位於libio/fileops.c文件中,因為代碼過長,我這里放置一個經過優化的源代碼,如下所示
_IO_size_t _IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n) { const char *s = (const char *) data; _IO_size_t to_do = n; int must_flush = 0; _IO_size_t count = 0; if (n <= 0) return 0; /* This is an optimized implementation. If the amount to be written straddles a block boundary (or the filebuf is unbuffered), use sys_write directly. */ /* First figure out how much space is available in the buffer. */ if ((f->_flags & _IO_LINE_BUF) && (f->_flags & _IO_CURRENTLY_PUTTING)) ... } else if (f->_IO_write_end > f->_IO_write_ptr) count = f->_IO_write_end - f->_IO_write_ptr; /* Space available. */
/* Then fill the buffer. */ if (count > 0) { if (count > to_do) count = to_do; #ifdef _LIBC f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count); #else memcpy (f->_IO_write_ptr, s, count); f->_IO_write_ptr += count; #endif s += count; to_do -= count; }
if (to_do + must_flush > 0) { _IO_size_t block_size, do_write; /* Next flush the (full) buffer. */ if (_IO_OVERFLOW (f, EOF) == EOF) /* If nothing else has to be written we must not signal the caller that everything has been written. */ return to_do == 0 ? EOF : n - to_do; /* Try to maintain alignment: write a whole number of blocks. */ block_size = f->_IO_buf_end - f->_IO_buf_base; do_write = to_do - (block_size >= 128 ? to_do % block_size : 0); if (do_write) { count = new_do_write (f, s, do_write); to_do -= count; if (count < do_write) return n - to_do; } /* Now write out the remainder. Normally, this will fit in the buffer, but it's somewhat messier for line-buffered files, so we let _IO_default_xsputn handle the general case. */ if (to_do) to_do -= _IO_default_xsputn (f, s+do_write, to_do); } return n - to_do; }
實際上我們可以根據源代碼中的注釋,大體了解程序的流程——首先根據情況獲取可用空間大小,然后將字符串復制到_IO_write_ptr所指向的地址處,最后使用_IO_OVERFLOW、new_do_write等進行刷新即可。而實際上_IO_OVERFLOW是對於_IO_new_file_overflow函數的包裝,最后仍然引用到了new_do_write,這里我放置一下其他博主https://n0va-scy.github.io/2019/09/21/IO_FILE/優化過的代碼,如下所示
int _IO_new_file_overflow (_IO_FILE *f, int ch) { if (f->_flags & _IO_NO_WRITES) /* SET ERROR */ { ... } /* If currently reading or no buffer allocated. */ if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL){ ... } if (ch == EOF) return _IO_do_write (f, f->_IO_write_base, f->_IO_write_ptr - f->_IO_write_base); //進入目標 if (f->_IO_write_ptr == f->_IO_buf_end ) /* Buffer is really full */ if (_IO_do_flush (f) == EOF) return EOF; *f->_IO_write_ptr++ = ch; if ((f->_flags & _IO_UNBUFFERED) || ((f->_flags & _IO_LINE_BUF) && ch == '\n')) if (_IO_do_write (f, f->_IO_write_base, f->_IO_write_ptr - f->_IO_write_base) == EOF) return EOF; return (unsigned char) ch; }
可以看出來,其會有兩個判斷,判斷成功執行的代碼都是設置錯誤並退出,我們在執行時需要繞過,其中對應的標志位宏定義位於libio/libio.h中,可以具體查看。可以看到,最后都可以歸納為對於_IO_do_write的調用,而_IO_do_write實際上又是new_do_write的包裝,同樣位於libio/fileops.c文件中,同樣借鑒一下上面博主整理的代碼,方便閱讀,如下所示
static _IO_size_t new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do) { ... _IO_size_t count; if (fp->_flags & _IO_IS_APPENDING) fp->_offset = _IO_pos_BAD; else if (fp->_IO_read_end != fp->_IO_write_base) { _IO_off64_t new_pos = _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1); if (new_pos == _IO_pos_BAD) return 0; fp->_offset = new_pos; } // 調用函數輸出輸出緩沖區 count = _IO_SYSWRITE (fp, data, to_do); //最終輸出 ... return count; }
同樣類似於_IO_new_file_overflow,而我們想要的及時調用的_IO_SYSWRITE,中間的變化到會導致一些不可控的因素,因此我們需要讓兩個判斷都不成立即可。如果我們要讓第二個不成立,根據網絡資料,程序可能會崩潰,因此我們選擇進入第一個判斷,也就是讓fp->offset & _IO_IS_APPENDING !=0即可。最后總結即可知,實際上打印的數據地址是fp->_IO_write_base,並且f->_flags需要滿足如下條件
f->_flags必須包含魔數(結構體注釋)
f->_flags & _IO_NO_WRITES = 0 f->_flags & _IO_CURRENTLY_PUTTING != 0 f->_flags & _IO_IS_APPENDING != 0
其各個宏常量定義在libio/libio.h文件中,最后我們可以得出f->_flags字段的值
#define _IO_MAGIC 0xFBAD0000 /* Magic number * #define _IO_NO_WRITES 8 /* Writing not allowd */ #define _IO_CURRENTLY_PUTTING 0x800 #define _IO_IS_APPENDING 0x1000 f->_flags = (~_IO_NO_WRITES) | (_IO_CURRENTLY_PUTTING) | (_IO_IS_APPENDIGN) =0xfbad0000 | 0x800 | 0x1000 & (0xfffffff7) = 0xfbad1800
因此,如果我們修改了stdout的flags和_IO_write_base成員,並且將_IO_write_base減小,則根據上面源代碼易知,我們將輸出_IO_write_base地址,長度為_IO_write_ptr-_IO_write_base個字節的信息,從而會輸出多一些的相關信息,這中間很容易包含glibc地址。
下面說一下常用的地址泄露用法——如果我們要泄露地址,則一定需要要輸出函數,這里就是puts函數,因此我們需要修改stdout對應的IO_FILE結構,也就是我們需要修改stdout的flags和_IO_write_base結構,從而獲取glibc的地址。那么我們通常會在stdout地址附近,構造一個fake chunk並分配,從而可以修改stdout對應的結構體,完成地址泄露。
我們首先gdb觀察一下stdout附近有沒有現成的fake chunk,從而方便進行分配,如下所示

可以看到,貌似在0x7ffff7dd25e0(具體地址根據實際調試情況而定)處可以有偽造的chunk,這里看起來不太方便,我們更換成熟悉的chunk,如下所示

這樣子的話,就非常像一個chunk了,也不用擔心萬一不同電腦不一樣怎么辦,因為實際上這里是固定存在的(具體數值可能不太一樣而已)。除此之外,我們注意到該fake chunk的大小字段的值為0x7f,這並不是一個很理想的值——因為一般chunk分配比較大小的時候,不考慮第三位,都是0x10對齊的(64位),因此這樣的話我們不能通過一般unsorted bin進行分配(否則malloc內部比較大小時會不一樣),因此我們不妨通過fast bin類型進行分配,因為fast bin比較大小是通過idx,即自動忽略了最后四位,因此這實際上0x7ffff7dd25dd是一個合法的fast bin。
下面的問題就在於如何通過malloc分配到這個fake chunk。一般我們通過修改fast bin的fd字段為該fake chunk的地址,從而獲取該fake chunk。但問題就在於我們不知道這個fake chunk的地址,因此我們沒有辦法直接修改fd字段的值來進行分配。一般遇到這種問題,我們的思路都是通過相對偏移進行解決——如果我們能獲取到這個fake chunk附近的地址,我們通過覆蓋后幾位,從而添加了相對偏移,從而將fd字段的值修正為該chunk即可。實際上,獲取該fake chunk周邊的地址是非常容易得,因為該fake chunk周邊即是main_arena,如圖所示

可以看到,實際上僅僅后16比特不同。而雖然main_arena、fake chunk的地址都是變化的,但是根據計算機獨有的特性,其內存按照頁分配,一個頁大小一般是1k,也就是后12比特是固定不變的,因此往往我們只需要爆破一些部分,即可將main_arena的值變換為fake chunk對應的地址。
那么現在的問題重新變換為了如果在fast bin的fd字段獲取main_arena周邊的地址,實際上這個就是house of Romanhttps://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/house_of_roman-zh/的攻擊,這里我簡單畫圖進行示意,方便進行理解。其大體思路如下所示
首先我們申請一個比較大的chunk,確保其釋放的時候可以釋放到unsorted bin中去,因為根據unsoretd bin的規則,其fd和bk的值就是main_arena附近的值。

然后我們將其釋放,確保其釋放到unsoretd bin中,而非和top chunk融合,此時其fd字段應該已經為main_arena + 88,這個偏移是固定的,不理解的可以閱讀一下malloc分配機制。當然,這個便宜只要不是特別大,是多少都無所謂,因為我們會覆蓋掉。結果如下所示

然后我們重新申請相同大小的chunk,這里根據malloc的分配機制,實際上獲取的一定是剛剛釋放掉的chunk,也就是除了該chunk的fd、bk字段的值進行了修改,其余基本沒有什么變化,如圖所示

此時,這里需要通過特殊的一些方法(可以是Off By One,可以是UAF等手段)對該chunk進行修改,從而使其成為大小為0x71的free過的fake chunk,並且其fd指向我們想要的stdout附近的那個fake chunk,這樣子我們相當於構造了一個假的fast bin鏈,如果我們將其鏈接到真的fast bin鏈上,則基本完成了目標,從而可以修改stdout的結構。如圖所示

最后我們就是將其鏈接到真的fast bin上,一般我們通過在創建兩個chunk,並且使其連接關系如下,如圖所示

即address C處的chunk->address B處的chunk,一般address A、address B和address C是物理連續的chunk,因此如果我們同樣使用一些特殊方法(Off By One、UAF等)技術覆蓋掉address C的fd的后幾個字節,從而使其指向address A,這樣子我們就完成分配一個chunk在stdout附近,從而覆蓋掉stdout的成員,完成地址leak。如圖所示

最后,假如我們已經成功完成了這些,剩下就是單純的泄露了,這里給出一下填充的內容模板,如下所示
def leak(addr): r.send('a' * 51 + p64(0xfbad1800) + p64(0) * 3 + p64(addr))
這里稍微解釋一下,其中51是根據前面fake chunk和stdout的間隔計算出來的,如下所

0x7ffff7dd2620 - 0x7ffff7dd25dd - 0x10 = 51
而后面的填充是根據stdout數據結構的偏移決定的,如下所示
truct _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. */ ... }
這里p64(0xfbad1800)對應的是_flags,其余三個p64(0)對應的是_IO_read_ptr、_IO_read_ptr和_IO_read_base。這里唯一可能的問題在於為什么int類型的_flags對應的是64比特——因為字節對齊,c語言的特性,后邊都是64比特的指針,所以這里被迫對齊到64比特,其余就沒什么了。
