Windows堆疊溢位全面解析

2021-04-18 22:43:44 字數 2962 閱讀 6522

關於堆疊溢位,前面寫的多期文章都是關於具體漏洞分析和shellcode的編寫技術,而有朋友希望我能寫點入門級的文章。今天,我就和大家一起,來全面解析windows下堆疊溢位的具體細節及利用過程。過程雖然簡單,但自己又走了一遍,才發現裡面的確有一些值得注意的地方。廢話少說,我們直接切入正題!

基礎知識

首先簡單講兩個基礎知識,一是函式呼叫時堆疊的變化;二是函式呼叫約定對函式呼叫及返回時堆疊變化的影響。

我們用高階語言編寫程式的時候,函式呼叫是很常見的事情,但是我們卻很少去觀察函式呼叫時彙編究竟是如何實現的。其實,函式呼叫實現的過程很簡單。父函式將函式的實參按照從右至左順序壓入堆疊;接下來,系統將父函式中函式呼叫指令call ***x的下一條指令位址eip壓入堆疊;然後,父函式通過push ebp指令將基址指標ebp壓入堆疊,並通過mov ebp, esp將當前堆疊指標esp賦予ebp;最後就是通過sub esp, m(m 是位元組數)為存放函式中區域性變數開闢的記憶體。函式在執行的時候如果需要訪問實參或者區域性變數,都是通過ebp指標來完成的。

在windows系統下,我們常見的函式呼叫約定有兩種:c/c++的函式呼叫方式_cdecl,以及__stdcall呼叫方式。在vc、.net等環境下,我們編寫命令列程式時的main或者_tmain函式,以及我們自己定義的很多函式,在不宣告呼叫約定的情況下預設都是__cdecl的方式。我們在通過mfc編寫圖形化程式的時候,其主函式的宣告為:extern "c" int winapi _twinmain(引數),該函式的呼叫約定就是_stdcall,winapi、pascal等名稱就是__stdcall的巨集定義,都乙個意思;此外,我們呼叫的api函式,絕大多數也是__stdcall的呼叫方式。

對於_cdecl呼叫方式的函式,其父函式在呼叫該函式的時候,會先將它的實參按照從右至左順序壓入堆疊;函式返回之後,父函式通過sub esp, n(n = 函式實參個數 * 4)指令來負責恢復堆疊。對於__stdcall呼叫約定函式,函式呼叫時實參入棧順序也是從右至左,但堆疊恢復是該函式返回時自己通過ret n(n = 函式實參個數 * 4)指令來完成的。

如果以前大家沒有注意這些基礎知識,可能理解起來不是很清晰。不過沒關係,下面我們再結合具體例子仔細講講,之所以要在這裡提出來,是因為這些知識對我們溢位漏洞的利用有比較明顯的影響。

例項解析

#include "windows.h"

#include "stdio.h"

#include "string.h"

int fun(char *szin, int ntest)

int main()

編譯、執行,彈出異常對話方塊:

顯然是發生了堆疊溢位,這是從直觀的角度進行了解。大家花1分鐘時間看下程式**,然後我們來仔細看看這個溢位的發生細節。

好了,我們繼續。在debug模式下, 按f10,進入單步除錯狀態,再按下alt+8,進入彙編狀態:

高階語言第15行**char sz_in = "1234567890abcdefghijklmn"前面是main函式的啟動**,其實大家從這裡就可以看到在呼叫主函式時堆疊的變化。一直按f10,直到第16行高階語言**fun(sz_in, 888)停下來,這裡是我們開始重點看的地方:

大家現在可以看到,main函式(前面講的父函式)通過call @ilt+0(fun) (00401005)指令呼叫fun函式,在該指令之前還有兩個push 指令操作。不難看出,這兩條push 指令就是將fun函式的兩個實參按照從右往左的順序壓入堆疊。繼續按f10,直到call指令停下,用f11跟到fun函式裡面去,跳到了00401005處,該處指令是乙個跳轉指令jmp fun (00401020),00401020就是函式fun開始的地方:

進入fun函式的第一條指令就是push ebp,即儲存基址指標ebp。按f10執行完這條指令之前,我們觀察當前堆疊頂部4位元組是0x004010b6,也就是我們剛才呼叫fun函式的call指令下一條指令add esp, 8的位址,即儲存了返回位址eip。現在繼續往單步執行,mov ebp, esp 使當前ebp指標指向父函式ebp指標儲存的位置,並用於函式執行過程中實參和區域性變數的訪問。sub esp, 48h指令是為fun函式的區域性變數預分配堆疊空間,後面的szbuf就位於這片預分配空間裡。繼續單步執行完00401051的call strcpy (004010e0)指令後停下,這裡說一句,strcpy函式的呼叫約定是__cdecl, 因此呼叫完成後,fun函式作為其父函式要通過add esp, 8來恢復堆疊。好了,資料覆蓋也完成了,這時候我們看看ebp+4指向的堆疊位址,即函式返回位址eip的值為0x66656463,就是我們覆蓋的資料:

函式返回之後將跳轉到該位址來執行指令,而該位址上的指令無效,因此會發生前面圖1中的讀錯誤。到這一步,我們既驗證了前面的基礎知識,有了解了堆疊溢位的過程,雖然簡單,但是卻具有代表性。

下面我們來談談堆疊溢位漏洞的利用。堆疊溢位漏洞的利用方式有兩種:jmp esp和覆蓋seh結構,其實這兩種方法我在前面的文章都提到很多,大家完全可以參考。這裡我既然講基礎的東西,那我們就講jmp esp。前面的示例1中,如果我們把覆蓋eip指標的四個位元組改為jmp esp指令位址,再在後面填充我們的shellcode,那麼函式返回之後就是執行jmp esp指令,繼而跳轉到我們的shellcode中來執行了。為了幫大家驗證這個說法,我準備了示例**2,其中shellcode的功能是建立使用者x,而跳轉位址是通用jmp esp指令位址0x7ffa4512大家可以試試。

至於shellcode的編寫技術,也有太多現成的東西,我這裡也不多講了。到這裡,我們的內容似乎就該告一段落了,但細心的朋友肯定要問了,這些和前面提到的函式呼叫約定有什麼關係嗎?當然有!大家不要忘了,前面的示例中函式是用的__cdecl呼叫約定。返回圖3中,當函式fun返回之後,main函式也是通過add esp, 8來恢復堆疊。如果將fun函式改為__stdcall的呼叫約定,在新增shellcode的時候大家就得注意了。因為fun函式在返回的時候,是自己通過ret 8指令來恢復堆疊的。當cpu跳轉到jmp esp指令時,此時的esp已經是在原來ebp + 16的位置,而不是ebp + 8,那麼覆蓋資料中shellcode的位置,也應該順理成章地往後走8位元組,中間多出來的8位元組就用90h填充吧。針對這類情況,我準備了示例**3

堆疊溢位檢測

維持的每個任務,都有它自己的堆疊。任務堆疊占用的記憶體,當任務建立時自動分配了 堆疊的尺寸引數通過xtaskcreate api確定。堆疊溢位是常見事件,由應用程式不確定引起的。freertos.org?因此提供了提供了兩個可供選擇的機制,可用來協助察覺和糾正這種事件發生。使用的選項由configc...

堆 棧 記憶體溢位

堆與棧的區別,遞迴沒有退出條件會怎樣,系統怎麼判定棧溢位?1,管理方式不同 棧編譯器自動管理,無需程式設計師手工控制 而堆空間的申請釋放工作由程式設計師控制,容易產生記憶體洩漏。2,空間大小不同 棧是一塊連續的記憶體,棧頂的位址和棧底的位址是系統預先規定好的,當申請空間大於剩餘空間,提示溢位 堆是不...

堆疊溢位及其原因

在乙個程式中,分配給堆疊的記憶體是有一定限度的。由於c語言系列沒有內建檢查機制來確保複製到緩衝區的資料不得大於緩衝區的大小,因此當這個資料足夠大的時候,將會溢位緩衝區的範圍。堆疊溢位就是不顧堆疊中分配的區域性資料塊大小,向該資料塊寫入了過多的資料,導致資料越界,結果覆蓋了別的資料。堆疊溢位時會有意想...