Author: 堇姬Naup
glibc提供很多I/O操作,主要就是去打這些東西
printf
scanf
fopen
fread
fwrite
…
主要看的source code:
https://elixir.bootlin.com/glibc/glibc-2.31/source/libio/libio.h
每次用printf等I/O function都syscall一次有點浪費時間,加個buffer優化他,等到buffer積累到一個程度,就輸出
常看到的這兩行,是設定不要有buffer,這樣會IO會比較單純
這三個指向_IO_FILE_plus
這個struct
https://elixir.bootlin.com/glibc/glibc-2.31/source/libio/stdio.c#L33
https://elixir.bootlin.com/glibc/glibc-2.31/source/libio/libio.h#L149
_IO_FILE_plus
是個這樣的struct,裡面包含了FILE這個struct跟vtable
https://elixir.bootlin.com/glibc/glibc-2.31/source/libio/libioP.h#L324
去追FILE這個struct
https://elixir.bootlin.com/glibc/glibc-2.31/source/libio/bits/types/FILE.h#L7
裡面有很多的flag,並且有很多buffer,大致上有兩種
read base是buffer開始
read end是buffer結束
read ptr是buffer當前用到的位置
以此類推
繼續往下看看到struct _IO_FILE *_chain;
把IO_FILE串成一條chain
int _fileno
是個int
num | 代表 |
---|---|
0 | stdin |
1 | stdout |
2 | stderr |
https://elixir.bootlin.com/glibc/glibc-2.31/source/libio/bits/types/struct_FILE.h#L49
這邊可以看到他define了很多跟flag有關的東西,你可以看到很多magic number可以設定,像是IO file不能read等
https://elixir.bootlin.com/glibc/glibc-2.31/source/libio/libio.h#L67
再來去追vtable
簡單理解成一個指標table,存放個函數指標,要找對應函數就會到vtable查表
https://elixir.bootlin.com/glibc/glibc-2.31/source/libio/libioP.h#L293
我們在open新的file時候,fd=3,之後以此類推,原因是前面是stdin、stdout、stderr(0、1、2),他用IO_list_all
的單向鏈表儲存
這裡面就是IO_FILE這個struct
這邊來看一下,實際define的地方
https://elixir.bootlin.com/glibc/glibc-2.31/source/libio/stdfiles.c#L35
第一欄 -> 名字
第二欄 -> fd
第三欄 -> 鏈表 (_IO_list_all -> _IO_2_1_stderr_ -> _IO_2_1_stdout_ -> _IO_2_1_stdin_ -> 0)
第四欄 -> 各類FLAG
vtable被設為_IO_file_jumps
這邊明確定義了每個函數的指標
既然已經知道了FILE的結構,那接下來來看glibc提供的操作FILE的函數
(這部分有點複雜,可以直接看總結)
大致流程如下:
接下來來看source code
https://elixir.bootlin.com/glibc/glibc-2.31/source/libio/stdio.h#L242
實際定義fopen
https://elixir.bootlin.com/glibc/glibc-2.31/source/libio/iofopen.c#L83
他調用了 __fopen_internal
https://elixir.bootlin.com/glibc/glibc-2.31/source/libio/iofopen.c#L55
filename -> 要打開的文件。
mode -> 模式(如 "r", "w", "a")
is32 -> 32 bits or 64 bits
定義了一個鎖
包含了 FILE_plus(file跟vtable) 跟 _IO_wide_data (操作寬字符數據)
https://elixir.bootlin.com/glibc/glibc-2.31/source/libio/libio.h#L122
並且malloc了一塊chunk new_f,來存放該struct
接著調用 _IO_no_init (&new_f->fp.file, 0, 0, &new_f->wd, &_IO_wfile_jumps);
https://elixir.bootlin.com/glibc/glibc-2.31/source/libio/genops.c#L561
這部分將一個FILE結構進行初始化,並進入了_IO_old_init
_IO_file_jumps 就是vtable
https://elixir.bootlin.com/glibc/glibc-2.31/source/libio/fileops.c#L1433
接著調用_IO_new_file_init_internal
https://elixir.bootlin.com/glibc/glibc-2.31/source/libio/genops.c#L529
以上內容都是準備工作
接下來呼叫_IO_file_fopen
(是_IO_new_file_fopen
別稱),準備開始開檔案了
來看 _IO_new_file_fopen
,有點長,這邊分段看
https://elixir.bootlin.com/glibc/glibc-2.31/source/libio/fileops.c#L211
基本設定
確認文件是否已經開啟
根據mode進行設定
之後呼叫了_IO_file_open
使用syscall,去打開文件,並且fileno設為文件描述符,並將該struct鏈入
https://elixir.bootlin.com/glibc/glibc-2.31/source/libio/fileops.c#L180
會再做一次鏈入的原因是
剩下的不是很重要
總結一下
https://elixir.bootlin.com/glibc/glibc-2.31/source/libio/stdio.h#L646
宣告了fread的地方
接下來去找實際定義fread的部分
https://elixir.bootlin.com/glibc/glibc-2.31/source/libio/iofread.c#L29
buf -> 儲存文件的地方
size -> 讀的大小
count 是要讀取元素個數
FILE *fp -> 讀的文件pointer
_IO_acquire_lock、_IO_release_lock -> 鎖跟解鎖,防止race
CHECK_FILE -> 一個define -> 檢查文件指標的有效性
這邊順便補一下各種error
https://elixir.bootlin.com/glibc/glibc-2.31/source/sysdeps/mach/hurd/bits/errno.h#L254
這部分是實際上執行fread的部分,他調用了_IO_sgetn
調用 _IO_XSGETN
https://elixir.bootlin.com/glibc/glibc-2.31/source/libio/genops.c#L407
這幾段簡單來說就是要去抓到vtable中的__xsgetn
實際上__xsgetn是_IO_file_xsgetn
,這邊有寫
https://elixir.bootlin.com/glibc/glibc-2.31/source/libio/fileops.c#L1433
這邊追進去看_IO_file_xsgetn
https://elixir.bootlin.com/glibc/glibc-2.31/source/libio/fileops.c#L1272
這邊一樣慢慢看
首先是他會檢查是否已經分配buffer了,如果沒有分配就進入,先檢查有沒有backup buffer,如果沒有的話就直接進_IO_doallocbuf (fp)
,分配一塊
若有backup buffer就把他free掉
補一下backup buffer,他會被_IO_save_base
指向,若需要回退回原來buffer的狀況,就會根據backup來還原
_IO_doallocbuf
https://elixir.bootlin.com/glibc/glibc-2.31/source/libio/genops.c#L342
先檢查是否已經有分配buffer了,如果有就return
經過一些簡單檢查後會呼叫 _IO_DOALLOCATE
_IO_DOALLOCATE 實際上會是 _IO_file_doallocate
https://elixir.bootlin.com/glibc/glibc-2.31/source/libio/fileops.c#L1433
所以追進去
這裡會分配一塊buffer
_IO_SYSSTAT 會獲取該file狀態
調用malloc給一塊buffer
_IO_setb 會賦值給buffer pointer
https://elixir.bootlin.com/glibc/glibc-2.31/source/libio/genops.c#L327
以上是分配空間
接下來要開始讀取
want -> 想要讀取的data量
have -> 剩餘的空間(have = fp->_IO_read_end - fp->_IO_read_ptr)
如果剩餘空間>想讀取的data
直接把data放到buffer上
並把_IO_read_ptr往上加
想要讀的(want) > 剩餘的(have)
如果有空間,就先把buffer讀滿
並且檢查有沒有backup buffer
https://elixir.bootlin.com/glibc/glibc-2.31/source/libio/genops.c#L124
有的話就切會回來
如果大小不夠就看整個buffer大小夠不夠處理want,可以就進入underflow,進行刷新
呼叫 __underflow
https://elixir.bootlin.com/glibc/glibc-2.31/source/libio/genops.c#L268
經過簡單檢查後,會呼叫__underflow,這邊會去查vtable,發現對應到_IO_file_underflow
_IO_file_underflow實際上呼叫到_IO_new_file_overflow
總之就是檢查
syscall read
然後刷新buffer
以便繼續讀取資料
這部分是資料量太大的處理,詳情見總結
總結
其實基本上fwrite前面跟fread一樣,跳呼叫_IO_XSPUTN
https://elixir.bootlin.com/glibc/glibc-2.31/source/libio/iofwrite.c#L53
_IO_new_file_xsputn
https://elixir.bootlin.com/glibc/glibc-2.31/source/libio/fileops.c#L1196
f: 一個指向文件結構體的指標。
data: 需要寫入的數據指標。
n: 要寫入的數據大小
s: 將輸入的 data 轉換為字串來處理。
to_do: 要寫入的剩餘字節數,初始值為 n。
must_flush: 是否需要強制刷新buffer的標誌,初始化為 0。
count: 表示buffer中可以立即使用的空間大小。
這邊檢查讀入的大小
接下檢查否設置了_IO_LINE_BUF及當前是否處於寫入模式 (_IO_CURRENTLY_PUTTING)。
補充一下,可以分為三類
all buffer -> 填滿了buffer才輸出
line buffer -> 遇到\n才輸出
no buffer -> 沒有buffer,及時輸出
再來會計算當前buffer大小為count
如果buffer大小大於要寫入的剩餘字節數,就檢查是否遇到\n,如果遇到就調整count,並將刷新buffer的flag設為1,強制刷新buffer
如果沒有滿足前面設置的flag,就計算buffer大小
如果buffer有剩下,就將他填滿
將data複製到_IO_write_ptr ~ _IO_write_ptr+count
等待寫的data往後推count
需要寫的減count
如果還有剩餘,那就代表沒有分配到buffer,或是buffer不夠大,並且也看是否需要刷新buffer,有的話就進去刷新
接下來這部分就是刷新buffer
他呼叫的 _IO_OVERFLOW 實際上是 _IO_file_overflow
_IO_file_overflow 又是 _IO_new_file_overflow
https://elixir.bootlin.com/glibc/glibc-2.31/source/libio/fileops.c#L730
一樣一段一段看
f 是 FILE
ch 是 EOF
先判斷是否當前是是寫入,不是寫入就進入ERROR
當前不是寫入或是write base未設置就去設置(大蓋就是去malloc一塊或切換回來)
之後進do_write
從 f->_IO_write_base開始寫長度 f->_IO_write_ptr - f->_IO_write_base
_IO_do_write 是 _IO_new_do_write
https://elixir.bootlin.com/glibc/glibc-2.31/source/libio/fileops.c#L430
syscall write
然後
調用 _IO_setg 將 fp 的buffer指針重設為初始狀態。
將 fp->_IO_write_base 和 fp->_IO_write_ptr 設置為buffer的開始地址 fp->_IO_buf_base,表示buffer處於可以重新寫入的狀態。
根據文件的模式(如是否為行緩衝 _IO_LINE_BUF 或不使用緩衝 _IO_UNBUFFERED),調整 fp->_IO_write_end,使其指向buffer的適當結尾
寫入量超過一個block就會進入該處理
這邊還有剩餘資料處理
https://elixir.bootlin.com/glibc/glibc-2.31/source/libio/genops.c#L370
處理剩餘data
總結
n <= 0
當data n 小於等於 0 時,程式直接返回 0,不會進行任何操作。
data n 小於等於 buffer剩餘空間 (count)
看完上述的source code,其實大概就會有一些如何攻擊的想法,像是如果我們能夠改掉read跟write等pointer,是不是有機會構造任意讀或任意寫
改寫vtable pointer並偽造vtable來做任意跳轉
先看這個POC
他會輸出
為甚麼呢?
還需要繞過甚麼?
這邊回去看puts的source code,他會調用xsputn
首先是
flag會被設置,所以會進入到該if
並且計算count
這邊為了方便可以直接讓count=0
f->_IO_buf_end = f->_IO_write_ptr
count = 0 不會進去
to_do初始設為n,大於零(若進入到上方判斷,有機會讓to_do被讀完)
呼叫_IO_OVERFLOW
stdout 不會設置 _IO_NO_WRITES,不會進
_IO_CURRENTLY_PUTTING預設有設定,另外_IO_write_base在利用時候,不會為空,所以不會進以下if
進
_IO_IS_APPENDING 不會設定,不會進
下方的read跟write需要設定成一樣,這樣才不會因為進入該判斷式,去重新定位_IO_write_base,導致利用失敗
fp->_IO_read_end = fp->_IO_write_base
這樣就繞過
最後syscall就成功了
所以上方我把該設定的設好,該等於的設好,最後把p->_IO_write_base設定到要讀的地方,結束也設定好,就可以構造任意讀
我們要利用fwrite來做任意讀
fp是在stack上的一個FILE
並且我們有heap overflow,可以去覆蓋FILE *fp來做任意讀
那我們要怎麼偽造
p->_IO_read_end = 想要讀的address;
p->_IO_write_base = 想要讀的address;
p->_IO_write_ptr = 想要讀的address + strlen(想要讀的);
p->_IO_buf_end = 想要讀的address + strlen(想要讀的);
fileno=1 (stdout)
flag = 0x00000800 (_IO_CURRENTLY_PUTTING)
這樣偽造一個FILE struct
然後就是算要蓋多少
buf -> malloc(0x10)實際大小0x20
而open開了一個chunk在heap上,並緊鄰buf
觀察一下記憶體,就是FILE
當我設定完成
0x0 0x0
0x0 0x1e1
…
p->_IO_read_end = 想要讀的address;
p->_IO_write_base = 想要讀的address;
p->_IO_write_ptr = 想要讀的address + strlen(想要讀的);
p->_IO_buf_end = 想要讀的address + strlen(想要讀的);
fileno=1 (stdout)
flag = 0x00000800 (_IO_CURRENTLY_PUTTING)
後並送出
爛了,遇事不決,gdb開起來
他死在這裡rdi+0x8,是個指標
我一個個慢慢測候發現他是 _lock
我隨便將他設成一個合理的address就可以了
其實還有個方法就是不要蓋到_lock,我這邊選擇填一個合法的空address
另外要把vtable切掉,不然動到vtable pointer會吃保護然後crash
exploit
NAUP_filestructure_lib.py
這樣就成功任意讀了
我們輸入的東西被確實的寫到了target
一樣來看為甚麼這樣設定
目標是要進到underflow中call read syscall
需要bypass一些東西
首先我們不希望他malloc一塊,所以我們要把 fp->_IO_buf_base 賦值
把他設為要寫的地方
want是要讀取的數量,正常都會>0
have 是 fp->_IO_read_end - fp->_IO_read_ptr ,是剩餘buffer的數量
我們要進到underflow,所以把fp->_IO_read_end 和 fp->_IO_read_ptr 設為相等,讓have是0會方便很多
這樣不會進
if (want <= have)
if (have > 0)
兩條
backup基本上也都不會進
這邊的話之前已經設定過了fp->_IO_buf_base,所以會過
want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base)
這邊我們把他設定成我們要讀取的值長度左右就可以了
所以設成了
sizeof(buf) + 1
__underflow 內的東西都不用繞,出問題了再來檢查就好
進 _IO_UNDERFLOW -> _IO_file_underflow -> _IO_new_file_underflow
這部分也是有問題再繞,基本上都不會進
最後會call IO syscall read
另外我們把fileno設為了stdin,他會從你的stdin開始讀
這樣他就將我們的stdin讀到target了
一樣有heap overflow的問題,padding 一樣
設定該設定的
p->_IO_buf_base = target;
p->_IO_buf_end = target + sizeof(buf) + 1;
p->_fileno = 0;
並且一樣要設定_lock成合法的位置
一樣不要蓋到vtable
NAUP_filestructure_lib.py
腳本跑完後,多輸入幾個觸發EOF
就可以任意讀了
在glibc 2.24前,如果我去修改FILE pluse中指向vtable的pointer,不會做檢查
所以我可以嘗試偽造一個vtable在stack上或其他地方,然後指過去,vtable中可以將某些變數改為自己想要跳轉的地方
像是這樣
先做一個假的vtable,fake_vtable[7]是__xsputn,我把它改成backdoor,這樣當我們call put就會跳上去
開成功
按照剛剛打法直接噴了error
那是因為他檢查了vtable所在的位置
直接舉個例子當我們call _IO_sputn,他是一個macro,展開來後
macro展開後call IO_validate_vtable會檢查他在哪個區段
在 __stop___libc_IO_vtables ~ __start___libc_IO_vtables;
中就可以過
繞過不實際
可以直接改vtable上的pointer成如Onegadget等位置,然而需要Glibc 2.29後,因為2.29前放在不可寫段
在Glibc < 2.29時候
還有其他地方的vtable可以利用
這裡有 IO_str_jumps可以嘗試做利用
他是 IO_strfile 用的
如果我將stdout中的vtable指向IO_str_jumps,並讓stdout[7](也就是第八個)為IO_str_overflow,這樣puts就會call到IO_str_overflow(原本是xsputn)
他會在這個section中不會有問題
接下來我們依據glibc 2.27去追 _IO_str_overflow source code
https://elixir.bootlin.com/glibc/glibc-2.27/source/libio/strops.c#L80
我們目標是(char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size)
如果我們把 _s._allocate_buffer 寫成 system
new size 寫成 /bin/sh
就可以成功
以下是 PoC
那就會想說為甚麼不直接改vtable值為one gadget
因為這個section是ro段(蠻合理的,畢竟vtable是用來查function位置,不需要w權限),不過在glibc 2.29它變成不是ro,所以利用起來很簡單
(這邊我看得一臉矇,不知道為甚麼要改成可寫,不過後來又改回去了)
也可以通過hijack _IO_list_all 來偽造一整條chain
_IO_flush_all_lockp 會去把所有的file structure上東西flush(所以會call _IO_OVERFLOW)
main return、libc 記憶體error、call exit都會call到_IO_flush_all_lockp
https://tttang.com/archive/1345/
https://blog.csdn.net/qq_41202237/article/details/113845320
https://blog.wingszeng.top/pwn-glibc-file-struct-and-related-functions/
https://a1ex.online/2020/10/01/glibc-IO源码分析/
https://www.youtube.com/watch?v=_TYWsA8gEW0
https://github.com/u1f383/Software-Security-2021-2022/tree/master/2022/week3