探秘“棧”之旅(II):結語、金絲雀和緩衝區溢位

Gustavo Duarte發表於2018-06-09

上一週我們講解了 棧是如何工作的 以及在函式的序言prologue上棧幀是如何被構建的。今天,我們來看一下它的相反的過程,在函式結語epilogue中棧幀是如何被銷燬的。重新回到我們的 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 ,主要部分如下:

mainadd 中複製返回值到本地變數 answer,然後,執行它自己的結語epilogue,這一點和其它的函式是一樣的。在 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 位元組為界進行多次重複,它們就會更好地覆寫棧上的原始返回地址。

幸虧,現代作業系統有了 防止緩衝區溢位 的一系列保護措施,包括不可執行的棧和棧內金絲雀stack canary。這個 “金絲雀canary” 名字來自 煤礦中的金絲雀canary in a coal mine 中的表述(LCTT 譯註:指在過去煤礦工人下井時會帶一隻金絲雀,因為金絲雀對煤礦中的瓦斯氣體非常敏感,如果進入煤礦後,金絲雀死亡,說明瓦斯超標,礦工會立即撤出煤礦。金絲雀做為煤礦中瓦斯預警器來使用),這是對電腦科學詞彙的補充,用 Steve McConnell 的話解釋如下:

電腦科學擁有比其它任何領域都豐富多彩的語言,在其它的領域中你進入一個無菌室,小心地將溫度控制在 68°F,然後,能找到病毒、特洛伊木馬、蠕蟲、臭蟲(bug)、炸彈(邏輯炸彈)、崩潰、爆發(口水戰)、扭曲的變性者(雙絞線轉換頭),以及致命錯誤嗎?

—— Steve McConnell 《程式碼大全 2》

不管怎麼說,這裡所謂的“棧金絲雀”應該看起來是這個樣子的:

金絲雀是透過彙編來實現的。例如,由於 GCC 的 棧保護器 選項的原因使金絲雀能被用於任何可能有漏洞的函式上。函式序言載入一個魔法值到金絲雀的位置,並且在函式結語時確保這個值完好無損。如果這個值發生了變化,那就表示發生了一個緩衝區溢位(或者 bug),這時,程式透過 __stack_chk_fail 被終止執行。由於金絲雀處於棧的關鍵位置上,它使得棧緩衝區溢位的漏洞挖掘變得非常困難。

深入棧的探秘之旅結束了。我並不想過於深入。下一週我將深入遞迴、尾呼叫以及其它相關內容。或許要用到谷歌的 V8 引擎。作為函式的序言和結語的討論的結束,我引述了美國國家檔案館紀念雕像上的一句名言:(凡是過去 皆為序章what is past is prologue)。


via:https://manybutfinite.com/post/epilogues-canaries-buffer-overflows/

作者:Gustavo Duarte 譯者:qhwdw 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出

相關文章