堆疊溢出
堆疊溢出就是不顧堆疊中分配的局部
數據塊大小,向該數據塊寫入了過多的數據,導致數據越界,結果覆蓋了別的數據。 可以理解為 在長字元串中嵌入一段代碼,並將過程的
返回地址覆蓋為這段代碼的地址,這樣當過程返回時,程式就轉而開始執行這段自編的代碼了。
比如如下這段程式:
#include<stdio.h>
int main()
{
char name[8];
printf("Please type your name:");
gets(name);
printf("Hello.%s!",name);
return 0;
}
編譯並且執行,輸入ipxodiAAAAAAAAAAAAAAAA,執行完gets(name)之後,堆疊如下:
記憶體底部 記憶體頂部
name EBP ret
<-------[ipxodiAA][AAAA][AAAA]............
^&name
堆疊頂部 堆疊底部
由於我們輸入的name字元串太長,name
數組容納不下,只好向記憶體頂部繼續寫'A',如果提前申請動態記憶體就可以避免堆疊溢出。而此例由於堆疊的生長方向與記憶體的生長方向相反,這些'A’覆蓋了堆疊的老的元素。'EBP ret’都被'A'覆蓋了。在main返回的時候,就會把'AAAA'的ASCII碼:0x41414141作為返回地址,CPU會試圖執行0x41414141處的指令,結果出現錯誤。這就是一次堆疊溢出!
解決措施
能夠監視malloc,memset,memcpy,free這四個函式的行為(棧就不檢測了,一般棧溢出的情況比較少,也好查。另外new和delete由於水平有限,無法對其監視)。 如果發現越界操作,列印出來,繼續執行。也就是說該檢測工具不影響程式的行為。
堆疊區域
堆疊是一塊保存數據的連續記憶體。 一個名為
堆疊指針(SP)的
暫存器指向堆疊的頂部。 堆疊的底部在一個固定的地址。 堆疊的大小在運行時由核心動態地調整。
CPU實現指令 PUSH和POP, 向堆疊中添加元素和從中移去元素。 堆疊由邏輯堆疊幀組成。 當調用函式時邏輯堆疊幀被壓入棧中, 當函式返回時邏輯堆疊幀被從棧中彈出。 堆疊幀包括函式的參數, 函式地
局部變數, 以及恢復前一個堆疊幀所需要的數據, 其中包括在
函式調用時指令
指針(IP)的值。 堆疊既可以向下增長(向記憶體低地址)也可以向上增長, 這依賴於具體的實現。 在我們的例子中, 堆疊是向下增長的。 這是很多計算機的實現方式, 包括Intel, Motorola, SPARC和
MIPS處理器。 堆疊指針(SP)也是依賴於具體實現的。 它可以指向堆疊的最後地址, 或者指向堆疊之後的下一個空閒可用地址。 在我們的討論當中, SP指向堆疊的最後地址。 除了
堆疊指針(SP指向堆疊頂部的的低地址)之外, 為了使用方便還有指向幀內固定 地址的指針叫做幀指針(FP)。 有些文章把它叫做局部基
指針(LB-local base pointer)。 從理論上來說,
局部變數可以用SP加
偏移量來引用。 然而, 當有字被壓棧和
出棧後, 這 些
偏移量就變了。 儘管在某些情況下
編譯器能夠跟蹤棧中的字操作, 由此可以修正偏移 量, 但是在某些情況下不能。 而且在所有情況下, 要引入可觀的管理開銷。 而且在有些 機器上, 比如Intel處理器, 由SP加
偏移量訪問一個變數需要多條指令才能實現。 因此, 許多
編譯器使用第二個
暫存器, FP, 對於
局部變數和函式參數都可以引用, 因為它們到FP的距離不會受到PUSH和POP操作的影響。 在Intel CPU中, BP(EBP)用於這 個目的。 在Motorola CPU中, 除了A7(
堆疊指針SP)之外的任何
地址暫存器都可以做FP。 考慮到我們堆疊的增長方向, 從FP的位置開始計算, 函式參數的
偏移量是正值, 而局部 變數的偏移量是負值。 當一個例程被調用時所必須做的第一件事是保存前一個FP(這樣當例程退出時就可以 恢復)。 然後它把SP複製到FP, 創建新的FP, 把SP向前移動為
局部變數保留空間。 這稱為 例程的序幕(prolog)工作。 當例程退出時, 堆疊必須被清除乾淨, 這稱為例程的收尾 (epilog)工作。 Intel的ENTER和LEAVE指令, Motorola的LINK和UNLINK指令, 都可以用於 有效地序幕和收尾工作。
堆疊溢出攻擊
利用JMP ESP的方式
其利用格式是NNNNNNRSSSSS,這裡N=NOP,R=RET(jmp esp的地址),S=ShellCode。就是把緩衝區一直覆蓋成NOP(空指令,什麼都不做),直到原來的EIP位置時,我們填入系統中某個核心dll中的jmp esp的地址,緊跟後面才是我們的ShellCode。 正常情況下,函式返回時,執行RET指令,這等於POP EIP,會把保存的原來程式的EIP的值恢復,從而完成中斷的返回。但在這裡,我們把保存的EIP的值覆蓋了,改寫成了jmp esp的地址。這樣,POP EIP後,EIP = jmp esp的地址,而堆疊指針ESP會往下走,指向ShellCode的開始。程式繼續執行,此時EIP里的內容是jmp esp,系統執行jmp esp,就正好就跳到我們的ShellCode的地方了.
如果ShellCode是開個連線埠,那我們就可以遠程連上去;如果ShellCode是下載執行,那我們就可以讓目標機在網頁上下個檔案並執行,只要你想到達的功能,都可以想辦法實現。
利用JMP EBX的方式
其利用格式是NNNNN JESSSSSS。這裡N = NOP, J = Jmp 04,E = jmp ebx的地址,S = ShellCode。 這裡的J和E的位置是關鍵,E是在出錯處理的入口位置,而J在其前面。 在第一種方式中,我們知道將返回地址覆蓋成另一個地址。但如果是個無效的地址呢?那裡指向的數據或許不能讀,或許不能執行,那會怎么樣呢?其實相信大家都遇到過,那就是系統會彈出個對話框報錯,我們點確定,就會終止運行。 這是因為作為一個系統級的程式,內部有健全的出錯處理機制。簡單的說,如果運行時有錯誤產生,windows就會跳到一個專門處理錯誤的地方,對應不同的錯誤,執行不同的代碼。上面執行的代碼就是彈出個對話框報錯。所以這裡我們故意把返回的地址覆蓋成一個錯誤的地址。這樣出錯時,windows就會跳到處理錯誤的入口,而ebx指向入口前4個位元組的地方!那我們把錯誤入口處覆蓋為jmp ebx的地址,就會跳到前4個位元組,怎么跳到ShellCode呢?在這裡我們寫入jmp 04,哈哈,往後跳4個位元組,正好跳過覆蓋值,達到我們的ShellCode。