目錄
漏洞原理
二次釋放
如何在第二次釋放前修改函數地址
fastbin的特性
修改函數指針流程
如何獲得進程的加載基址
格式化字符串漏洞
確定printf函數在代碼段中偏移
printf函數輸出想要的地址
如何獲得system函數的地址
尋找一個被fheap進程調用並且和system函數處於同一個so庫的函數A
通過讀取函數A在got.plt中相應位置的值獲得函數A的地址
通過讀取dynsym段中的信息計算system相對於函數A在so庫中的偏移
實際運行效果
小結
參考資料
漏洞原理
程序自己實現了一套管理字符串的體系,但是在釋放的時候用指針是否為空來判斷該索引代表地方是否存放有字符串,如果指針不空,表示可以釋放。但是釋放完后,沒有將指針置空,因此導致可以二次釋放,多次釋放。
圖1 漏洞觸發原理
如何在第二次釋放前修改函數地址
fastbin特性
根據[參考資料一]的介紹,fastbin維護的chunk分九個檔次,大小從16字節到80字節,每8個字節一個檔次。那我們要求的0x20(32)個字節,屬於48字節的檔次(因為每個chunk還要加上16字節的管理區),所以我們申請0x20空間后釋放的chunk被歸到fastbin[5]這個鏈表中了。
修改函數指針流程
創建四個字符串之后的內存布局
圖2 創建四個字符串之后的內存布局
在heap中分配的8個塊是從上到下在heap中依次排列的。之所以中間沒有連續排列,是因為每個塊前面都有16個字節的chunk管理區沒有畫出來。
刪除四個字符串之后的內存布局
圖3 刪除四個字符創后內存布局
由於glibc中釋放內存是將16到80個空間的chunk直接添加到fastbin數組中的鏈表里面,所以能在fastbin[5]代表的鏈表中找到我們釋放的內存塊。而fastbin中的鏈表的進出規則是先進后出,所以最先釋放的str0相關的塊在鏈表尾,而最后釋放的str3相關的塊在鏈表頭的位置。詳情見圖3。
修改release_ptr方式
在釋放完字符串后,再創建一個0x80大小的字符串,由於str_manage的大小就是0x20,所以str_manage存放空間還是從fastbin中分配,此時的str0_manage就和原來的str3_manage重合了,但是由於其需要存放字符的空間是0x80,所以這次存放數據的chunk塊就沒有從fastbin中獲取了,而是接着上述8個塊之后再分配的。這樣,我們再創建的字符串str1_manage就和原來的string3重合,string1就和原來的str2_manage重合了,那我們此時寫入的字符串str1就會覆蓋原來的str2_manage結構,那么我們再次釋放str2時候,依然會將str2_info_ptr指向地址的后8個字節解釋為函數指針並且調用它。我們寫入想執行的函數,delete str2就會執行我們的函數。
圖4 修改release_ptr方式
如何獲得進程的加載基址
此時我們已經可以讓程序執行我們需要的函數了。但是我們要執行的函數地址是多少呢?首先說明,release_ptr處填的值是fheap程序的函數,也就是這個值是在代碼段中的。我們先看看進程運行起來后的內存分布圖。
圖5 fheap運行后的內存區域分布
可以看到,此時堆還沒有分配,而libc庫加載到0x7FFFF7A0E000處,但是我們能保證libc庫每次都加載到相同的地址嗎。不能,那現在我們有什么信息呢?我們有源程序,我們知道代碼段中每條指令相對於進程基址的偏移,如果我們只修改release_ptr處的低位字節,那么我們可以調到代碼段中任意一條指令處。
那么我們讓程序執行到哪里比較有用呢?或者說用什么方式能獲得程序的加載基址,以便后續工作開展呢?[參考資料二]的作者使用格式化字符串方式獲取程序基址。
格式化字符串漏洞
[參考資料三]總有很詳細的介紹,此處不再贅述,簡單說一下原理。我們在使用printf時候,往往會按照正常形式先傳遞一個格式化字符串,再傳遞我們要打印的數據作為參數。但是,如果我們只傳遞格式化字符串,而不傳遞參數呢?見下圖。
圖6 格式化字符串漏洞示意
會發生什么事呢?printf會按照格式字符解釋arg1之后的數據。
此處,我們僅僅是讀取了棧上的數據,至於如何讀任意地址的數據,還是見[參考資料三],原理是一樣的。只不過不適合我們這個實例。
確定printf函數在代碼段中偏移
由於位置無關代碼(PIC)技術的使用,printf這種實現在程序外的動態庫中的函數都被編譯器在本地.plt段添加了一個代理函數,具體原理見[參考資料四]。所以我們要找到print在本地的代理函數。見下圖。
圖7 printf_plt
我們將0x9D0填充到release_ptr的低兩個字節,就可以讓程序控制流到printf_plt了。從而可以完成讀取任意地址處的數據。
printf函數輸出想要的地址
那我們怎么獲取進程加載基址呢?回想一下,當程序控制傳遞到release函數時候,此時是在delete 函數中,而delete函數是在main中被調用,所以棧中肯定有返回main中的地址。此時我們需要的僅僅是動態調試一次,看看執行release函數之前棧中保存返回main函數地址的位置。
圖8 執行release函數之前堆棧中的信息
可以發現,刪除之前我們輸入的確認字符串”yes.aaaabbbbbbbbcccccccc”已經放在棧中了。剩下的就是計算要獲取的ret_mian_addr在棧中的位置。可以看到ret_mian_addr在棧中0x6F8處,而棧頂在0x5D0處。相當於偏移了多少個參數呢?偏移了(0x6F8-0x5D0)/8=37個參數。
但是,這是在64位機器上的程序,不要忘了,調用函數的規則還包括前六個參數保存在六個寄存器中。所以總數還要加上6,即37+6=43。
那么,我們要打印43個參數才能得到我們要的數據嗎?還好,[參考資料三]中已經介紹了一種方法,打印指定位置的值,即:”%參數位置$格式”。例如,我們要以地址的格式打印第43個參數,可以這樣寫:“%43$p”,確實很方便。
這樣,我們能獲取ret_main_addr。
那怎么用這個地址呢?這個地址處於main函數的范圍,我們在ida里面可以看到這個地址距離進程起始地址偏移為0xCF2。
所以進程加載基址就是ret_main_addr-0xCF2。
如何獲得system函數的地址
我們要打開一個shell,就要獲得system函數的地址。system函數是在libc庫中。但是這個庫每次加載的地址可不是唯一的,我們如何獲得libc庫中函數的地址呢?
要是我們能夠通過進程本身,得到libc庫中任意變量或者函數的地址,我們就能通過查看libc符號表知道所有變量或者函數的地址了。
尋找一個被fheap進程調用並且和system函數處於同一個so庫的函數A
也許read這個函數可以聯系進程和庫(此處我也是學習者,積累技巧)。
通過讀取函數A在got.plt中相應位置的值獲得函數A的地址
那么怎么獲取read函數的地址呢?位置無關代碼(PIC)技術的使用,使得我們能夠實現需求。PIC技術要通過編譯器在elf文件中加入GOT表來實現。見[參考資料四],寫的真好,還有實例。GOT表中存放的是什么呢?就是程序調用的動態庫中函數的地址。對於read而言,放的就是read函數的實際地址。那么read在GOT表中什么位置呢?由於read是函數,所以它的位置在got.plt中的某一個位置offset,而在加載時候,offset處填充的是read函數在本地的代理read_plt函數的第二條指令的地址。不是read在libc中實際地址,所以它會被動態加載器在合適(見[參考資料四]講解合適的意思)的時候再次修訂。那么在重定位信息中,必然有read在got.plt的地址。我們查看重定位信息。
見下圖。
圖9 read函數的重定位信息
可以看到,在相對進程加載基址0x202058處會被動態加載器填上read函數的實際地址。我們獲得了進程的加載基址,那就能知道代碼段和數據段中的任意數據。
通過讀取dynsym段中的信息計算system地址
此時我們已經知道了read函數的地址,那么怎么獲取system函數的地址呢?read和system的距離肯定是一定,知道了相對距離,就可以知道system的地址。
在動態庫的.dynsym段中記錄了每個可以被其他程序或者庫調用的的符號地址,這個地址被寫成相對於邏輯地址0的偏移。就是用來方便動態加載器修改GOT表的。我們看看read和system在.dynsym中的信息。
圖10 read函數在dynsym中的信息
圖11 system函數在dynsym中的信息
由這兩個信息,可以計算出system相對於read的偏移是-0xb12e0。
實際運行效果
有了system地址,我們通過創建字符串的方式修改release函數指針,而字符串其實地址會被傳遞過去,這樣,創建的字符串開始部分可以寫成”/bin/sh”,當做system開啟shell的參數。當我們再次delete str2的時候,就會執行system函數,進而開始一個shell。
圖12 成功開啟shell
小結
和別人的差距挺大的,花費了15個小時才明白[參考資料二]中作者的意圖。之前只是擁有正向的知識和理解,我感覺分析了這一題,不僅將以往學習的理論知識運用了一下,也好像打開了另一種思想世界的大門。
本次涉及到知識點:
-
- 二次釋放
- glibc內存管理概要
- 格式化字符串漏洞、打印指定位置處的值
- 動態重定位相關知識
- GDB調試
參考資料
[1] glibc內存管理介紹:
[2] fheap漏洞利用程序:
http://bobao.360.cn/ctf/detail/179.html
[3] 格式化字符串漏洞:
[4] 《深度探索Linux操作系統:系統構建和原理解析》王柏生