一種處理棧越界的方法

在linux下,棧越界寫壞返回地址會導致調用棧無法回溯,這就導致我們直接使用bt沒有辦法查看崩潰時調用棧,今天我講一下我最近研究出來的一種方法(雖然是原創,但可能互聯網上早有人發佈過此種方法,只不過我沒有查到而已).

廢話少說,步入正題,首先我寫了個簡單的程序來構造一個棧溢出的情況,爲了使效果更加明顯,我使用了一些遞歸來增加調用棧的深度,代碼如下:

不要吐槽命名方式,我也知道很醜,棧都能越界的程序,一定漂亮不到哪去,哈哈!

簡單描述下這個代碼的功能,func2裏遞歸調用了func2,這樣能有效增加調用棧深度,然後調用func3的時候,由於func3裏寫buf越界了,導致棧被破壞了,然後段錯誤崩潰.崩潰後生成了core文件,我們用gdb打開,輸入bt,效果如下:

由於棧破壞有很多種方式,bt也有可能顯示出一排??,總之棧破壞很有可能導致bt無法回溯就是了.那我們如何應對呢?我們首先來看一下調用棧的一些知識:

一般情況下,在調用函數之前,(部分)參數會放入棧內,然後執行彙編指令call, 執行call後會自動將返回地址壓入棧中,然後執行被調用函數,被調用函數開頭的兩條彙編指令很可能是:
push rbp
mov  rbp,rsp
這兩條指令的作用就是把rbp壓入棧中,把棧頂指針rsp賦值給rbp,這樣在棧內就會形成一個鏈表,以便我們回溯調用棧.
注意:
在開啓優化的時候,默認是 -fomit-frame-pointer,這樣可能導致很多函數開頭不會出現那兩條彙編指令,-fno-omit-frame-pointer選項開啓後就會生成以上兩條指令.

好了,詳細的棧資料請大家自行查閱資料學習,我就不再贅述了.你只要知道調用棧在棧內是以鏈表的方式保存即可.那棧越界寫壞的地方我們可以認爲是鏈表的頭部,由於鏈表的頭部被寫壞了,導致gdb的bt指令無法回溯調用棧了.

既然如此,那我們可以再找一個節點作爲鏈表的頭部.只不過回溯的調用棧可能比"完美"的調用棧少那麼一兩條,不過半個麪包總比沒有好,說幹就幹:

上圖的代碼是gdb的擴展,我擴展了一個bts(backtrace stack)指令,其作用是打印給定addr後的count條內容.gdb中可以使用source指令來加載擴展,也可以在home路徑下新建.gdbinit文件,將腳本內容寫入,這樣在gdb啓動時就會自動生效,我們使用source .gdbinit來加載一個這個bts擴展指令.然後在gdb裏輸入i r rsp指令:

,rsp的值一般情況下是棧頂,不過不排除這個值是不對的,只是不對的概率比較小而已,然後鍵入指令:

(gdb) set pagination off
(gdb) set loggin on ./log
Copying output to ./log.
(gdb)bts 0x7fff81f7c250 1000

第一條指令的作用是關閉"超過一屏內容等待鍵入回車"功能,第二條是打開log,這樣gdb裏的輸出就會寫入log文件內,第三條就是我們寫的擴展指令了,執行一下,等待結果寫入到文件中吧.由於我們的測試程序很短,棧也沒用多大,所以1000應該就可以了,實際程序中這個1000可能要變得很大,可能要跑幾分鐘,不過我很享受這幾分鐘,因爲我就喜歡敲入一條命令然後屏幕刷刷滾的感覺,逼格高,哈哈!!

跑題了,好了,輸出結束我們去看看./log文件,執行head ./log命令,結果如下:

這正是我們想要的內容,第一列是棧地址,第二列是該地址的內容,既然調用棧在棧內是鏈表,那我們就可以寫個代碼把棧內所有的鏈表都暴力搜出來,然後看下哪個最可能是調用棧.

這是我寫的暴力搜索棧中鏈表的程序,其實大家完全可以用腳本寫,比c++方便多了.好了,g++ stack.cpp -o stack編譯一下,然後執行:

./stack log 100 > symbol

log就是我們的log文件了,100呢是調用棧的深度,當你指定100的時候,會把所有調用棧深度爲100的鏈表打印出來,由於我們遞歸超過100次了,所以這裏我就指定100了,如果你在實際應用中,100沒有結果,那可以嘗試逐漸減小這個數值,然後我們看看symbol裏的內容:

大概是這樣的,還有好多條,我只截取了部分,之所以有info symbol,是因爲我要在gdb中加載這個symbol文件,加載後會自動執行info symbol address,作用就是打印出這條地址附近的符號:

(gdb)source ./symbol

此處應該有掌聲!!!

從下往上看,依次是__libc_start_main()->main()->func1()->func2()...,這的確是我們程序中的調用棧,只不過丟失了func3而已.

最後總結一下,此片專欄只是提供一種解決方法而已,具體能否成功,要看運氣了.我一直覺得調試找bug是要看運氣的,尤其是那些偶然出現的crash,在你不知道原因無法重現時只能從core dump裏尋找蛛絲馬跡了.

經驗:一般棧越界很有可能是字符串越界,此時可以查看rsp附近的內存,說不定有很明顯的字符串,一下就定位問題了呢.

由於水平有限,文章中有錯誤在所難免,請大家包含並指教,謝謝!

-----------------------------------------------

更新一下暴力搜索調用棧的那個代碼,其中有一處bug可能導致在遍歷調用棧的時候死循環.

更新後如上圖所示,這樣就不怕棧裏有迴路了,還有一處修改,這個地方用sizeof(void*),就兼容32bit 64bit的程序了,以前寫的8只支持64bit的程序
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章