探秘「棧」之旅(II):結語、金絲雀和緩衝區溢出
上一周我們講解了 棧是如何工作的 以及在函數的 序言 上棧幀是如何被構建的。今天,我們來看一下它的相反的過程,在函數 結語 中棧幀是如何被銷毀的。重新回到我們的 add.c
上:
int add(int a, int b)
{
int result = a + b;
return result;
}
int main(int argc)
{
int answer;
answer = add(40, 2);
}
簡單的一個做加法的程序 - add.c
在運行到第 4 行時,在把 a + b
值賦給 result
後,這時發生了什麼:
第一個指令是有些多餘而且有點傻的,因為我們知道 eax
已經等於 result
了,但這就是關閉優化時得到的結果。leave
指令接著運行,這一小段做了兩個任務:重置 esp
並將它指向到當前棧幀開始的地方,另一個是恢復在 ebp
中保存的值。這兩個操作在邏輯上是獨立的,因此,在圖中將它們分開來說,但是,如果你使用一個調試器去跟蹤,你就會發現它們都是自動發生的。
在 leave
運行後,恢復了前一個棧幀。add
調用唯一留下的東西就是在棧頂部的返回地址。它包含了運行完 add
之後在 main
中必須運行的指令的地址。ret
指令用來處理它:它彈出返回地址到 eip
寄存器(LCTT 譯註:32 位的指令寄存器),這個寄存器指向下一個要執行的指令。現在程序將返回到 main
,主要部分如下:
main
從 add
中拷貝返回值到本地變數 answer
,然後,運行它自己的 結語 ,這一點和其它的函數是一樣的。在 main
中唯一的怪異之處是,保存在 ebp
中的是 null
值,因為它是我們的代碼中的第一個棧幀。最後一步執行的是,返回到 C 運行時庫(libc
),它將退回到操作系統中。這裡為需要的人提供了一個 完整的返回順序 的圖。
現在,你已經理解了棧是如何運作的,所以我們現在可以來看一下,一直以來最臭名昭著的黑客行為:利用緩衝區溢出。這是一個有漏洞的程序:
void doRead()
{
char buffer[28];
gets(buffer);
}
int main(int argc)
{
doRead();
}
有漏洞的程序 - buffer.c
上面的代碼中使用了 gets 從標準輸入中去讀取內容。gets
持續讀取直到一個新行或者文件結束。下圖是讀取一個字元串之後棧的示意圖:
在這裡存在的問題是,gets
並不知道緩衝區(buffer
)大小:它毫無查覺地持續讀取輸入內容,並將讀取的內容填入到緩衝區那邊的棧,清除保存在 ebp
中的值、返回地址,下面的其它內容也是如此。對於利用這種行為,攻擊者製作一個精密的載荷並將它「喂」給程序。在這個時候,棧應該是下圖所示的樣子,然後去調用 gets
:
基本的思路是提供一個惡意的彙編代碼去運行,通過覆寫棧上的返回地址指向到那個代碼。這有點像病毒侵入一個細胞,顛覆它,然後引入一些 RNA 去達到它的目的。
和病毒一樣,挖掘者的載荷有許多特別的功能。它以幾個 nop
指令開始,以提升成功利用的可能性。這是因為返回的地址是一個絕對的地址,需要猜測,而攻擊者並不知道保存它的代碼的棧的準確位置。但是,只要它們進入一個 nop
,這個漏洞利用就成功了:處理器將運行 nop
指令,直到命中它希望去運行的指令。
exec /bin/sh
表示運行一個 shell 的原始彙編指令(假設漏洞是在一個網路程序中,因此,這個漏洞可能提供一個訪問系統的 shell)。將一個命令或用戶輸入以原始彙編指令的方式嵌入到一個程序中的思路是很可怕的,但是,那只是讓安全研究如此有趣且「腦洞大開」的一部分而已。對於防範這個怪異的 get
,給你提供一個思路,有時候,在有漏洞的程序上,讓它的輸入轉換為小寫或者大寫,將迫使攻擊者寫的彙編指令的完整位元組不屬於小寫或者大寫的 ascii 字母的範圍內。
最後,攻擊者重複猜測幾次返回地址,這將再次提升他們的勝算。以 4 位元組為界進行多次重複,它們就會更好地覆寫棧上的原始返回地址。
幸虧,現代操作系統有了 防止緩衝區溢出 的一系列保護措施,包括不可執行的棧和 棧內金絲雀 。這個 「 金絲雀 」 名字來自 煤礦中的金絲雀 中的表述(LCTT 譯註:指在過去煤礦工人下井時會帶一隻金絲雀,因為金絲雀對煤礦中的瓦斯氣體非常敏感,如果進入煤礦後,金絲雀死亡,說明瓦斯超標,礦工會立即撤出煤礦。金絲雀做為煤礦中瓦斯預警器來使用),這是對計算機科學辭彙的補充,用 Steve McConnell 的話解釋如下:
計算機科學擁有比其它任何領域都豐富多彩的語言,在其它的領域中你進入一個無菌室,小心地將溫度控制在 68°F,然後,能找到病毒、特洛伊木馬、蠕蟲、臭蟲(bug)、炸彈(邏輯炸彈)、崩潰、爆發(口水戰)、扭曲的變性者(雙絞線轉換頭),以及致命錯誤嗎?
—— Steve McConnell 《代碼大全 2》
不管怎麼說,這裡所謂的「棧金絲雀」應該看起來是這個樣子的:
金絲雀是通過彙編來實現的。例如,由於 GCC 的 棧保護器 選項的原因使金絲雀能被用於任何可能有漏洞的函數上。函數序言載入一個魔法值到金絲雀的位置,並且在函數結語時確保這個值完好無損。如果這個值發生了變化,那就表示發生了一個緩衝區溢出(或者 bug),這時,程序通過 __stack_chk_fail
被終止運行。由於金絲雀處於棧的關鍵位置上,它使得棧緩衝區溢出的漏洞挖掘變得非常困難。
深入棧的探秘之旅結束了。我並不想過於深入。下一周我將深入遞歸、尾調用以及其它相關內容。或許要用到谷歌的 V8 引擎。作為函數的序言和結語的討論的結束,我引述了美國國家檔案館紀念雕像上的一句名言:( 凡是過去 皆為序章 )。
via:https://manybutfinite.com/post/epilogues-canaries-buffer-overflows/
作者:Gustavo Duarte 譯者:qhwdw 校對:wxy
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive