堆疊溢出

堆疊溢出

堆疊是一個在計算機科學中經常使用的抽象數據類型。堆疊中的物體具有一個特性: 最後一個放入堆疊中的物體總是被最先拿出來, 這個特性通常稱為後進先出(LIFO)佇列。 堆疊中定義了一些操作。 兩個最重要的是PUSH和POP。 PUSH操作在堆疊的頂部加入一 個元素。POP操作相反, 在堆疊頂部移去一個元素, 並將堆疊的大小減一。

堆疊溢出的產生是由於過多的函式調用,導致調用堆疊無法容納這些調用的返回地址,一般在遞歸中產生。堆疊溢出很可能由無限遞歸(Infinite recursion)產生,但也可能僅僅是過多的堆疊層級。

基本介紹

  • 中文名:堆疊溢出
  • 套用學科:計算機科學
  • 類別:高級語言
  • 技術:過程和函式
  • 記憶體:連續記憶體
  • 地址:固定地址
  • 領域:計算機安全
堆疊溢出,解決措施,堆疊區域,堆疊溢出攻擊,利用JMP ESP的方式,利用JMP EBX的方式,

堆疊溢出

堆疊溢出就是不顧堆疊中分配的局部數據塊大小,向該數據塊寫入了過多的數據,導致數據越界,結果覆蓋了別的數據。 可以理解為 在長字元串中嵌入一段代碼,並將過程的返回地址覆蓋為這段代碼的地址,這樣當過程返回時,程式就轉而開始執行這段自編的代碼了。
比如如下這段程式:
#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。

相關詞條

熱門詞條

聯絡我們