一、說明
本來是想直接寫一個緩沖區溢出的例子,但是一是當前編譯器和操作系統有溢出的保護措施沒有完全弄清怎么取消,二是strcpy等遇到00會截斷需要進行編碼這比較難搞,所以最終沒有實現。
但已經雙看了一陣函數的調用過程,如果全然就此放棄那以后再研究緩沖區溢出又得從0開始研究函數的調用,所以就記些東西下來,免得以后雙得從0開始。
在緩沖區溢出中堆棧變化是最為關鍵的,本文從堆棧入手。
二、函數調用過程中的堆棧變化
2.1 使用程序
本文使用程序源代碼如下(編寫時我創建了一個StackChange工程,將源代碼保存為StackChange.c),使用vc++ 6.0編譯,使用olldbg逆向。
#include <stdio.h> int get_sum(int a, int b) { int sum; sum = a + b; printf("get_sum: calc sum success, now will return\n"); return sum; } int main(int argc, char **argv) { int a = 1; int b = 2; int sum; sum = get_sum(a,b); printf("main: the sum is %d\n", sum); return 1; }
2.2 堆棧在內存地址空間中的位置
今早等公交的時候看到一個問題大意是,如果堆棧不可執行那么程序為什么還可以執行,從概念上說堆棧段和代碼段沒關系所以堆棧段不可執行代碼段還可以執行。
但我想這位小哥根本上是想知道,堆棧段和代碼段分別在內存的什么位置,為什么堆棧段不可執行代碼段還可執行。
將2.1中的程序編譯后,使用olldbg載入exe,然后打開內存窗口(M),如下圖:
圖中各種信息都很明了了,具體到堆棧段和代碼段的位置,堆棧段為0x0018e000-0x00190000(因為size為0x2000),代碼段為0x00401000-0x00422000(因為size為0x2100)。所以堆棧段不可執行,不影向代碼段可不可執行。
2.3 函數調用在匯編上的過程
main函數匯編解析,說明看其中的注釋:
get_sum函數匯編解析如下,流程和main函數是類似的(應該說所有函數的流程框架都是這樣的)不重復注釋,需要注意的就是獲取參數是使用ebp+8的形式回頭獲取的
2.4 函數調用過程中的堆棧變化
應該很多人和我一樣2.3中的函數調用過程聽了一萬遍了,只是下次還是記不住;想以此去理解緩沖區溢出更是浮沙築台,一知半解,過后又忘。
我想了想其症結在於我們總嘗試去死記硬背這個過程,重點放在代碼段上。
而如果我們從整個程序內存空間在程序執行過程中的變化情況,就會發現代碼段區域和數據段區域內容都是不變的,只有堆棧段內容才會變(當然寄存器也是變化的但寄存器不在內存中)。
或者換言之,整個0x00000000-0xffffffff內存地址空間,從程序運行到進程結束,只有堆棧部分的內存即0x0018e000-0x00190000是會變的,其他部分在初始化后都是不變的。
所以也許重點放在堆棧段上,也許我們能更好更解函數調用過程。
(此時想起兩年前去面試,面試官問輸入的用戶名密碼存到哪了,回答存內存了,得到基礎薄弱的評價,當時是有些不忿的;現在想來內存是0x00000000-0xffffffff,而別人想要的是0x0018e000-0x00190000這么一小段,差距確實是有點大)
2.4.1 堆棧在函數層次的變化
以下是olldbg進入get_sum函數的printf函數內部時的各函數堆棧情況(0x0018fe6c-0x0018ff4c):
以下是olldbg進入main函數的printf函數內部時各函數的堆棧情況(0x0018fe6c-0x0018ff4c):
從以上兩圖中的變化我們可以部結出以下幾點:
1. 棧從高地址向低地址生長。我們前邊說過棧地址空間為0x0018e000-0x00190000,這里main並沒有從0x00190000開始是因為main函數其實是被系統函數調用啟動的,並不能一開始就是main函數。見下圖。
2. 層次為父子關系的函數(比如這里的main和get_sum、get_sum和其中的printf),父函數的堆棧在高地址子函數的堆棧在緊接父函數堆棧的低地址。
3. 層次為兄弟關系的函數(比如這里的get_sum和main函數的中的printf),前一函數調用完后其堆棧被釋放歸回未使用,后一函數執行時使用的堆棧和前一函數一樣(開始地址一樣,結束地址一般不一樣,畢竟各自局部變量需要的空間不一樣)
4.堆棧在使用時初始化在釋放時不初始化。比如上邊我們已進入到main函數的printf,但get_sum未被占用的那部份和get_sum中的printf的堆棧共保留的內容還和原來一樣。
2.4.2 單個函數內部的堆棧空間分析
我們使用前邊“olldbg進入get_sum函數的printf函數內部時的各函數堆棧情況”時的圖來看get_sum函數堆棧的內容
我們將get_sum堆棧空間轉化為以下表格:
對應圖中地址 | 內容 | 說明 |
0x0018FE84 | ebp(get_sum函數的) | 這其實是printf函數中的push ebp |
0x0018FE88 | eip(printf返回get_sum的) | 這是get_sum函數中call printf壓入棧的 |
0x0018FE8C | 形參(printf的) | |
0x0018FE90 | edi(main函數的) | 如果不調用參數,或調用參數返回后,esp指向此處 |
0x0018FE94 | esi(main函數的) | |
0x0018FE98 | ebx(main函數的) | |
0x0018FE9C-0x0018FED8 | 預留空間 | 編譯器總會給比參數需要的空間多一些的空間,malloc的話會從0x0018FE9C往0x0018FED8方向使用 |
0x0018FEDC | 局部變量已使用空間 | 所謂緩沖區溢出,就是此處的參數長度溢出,向后覆蓋0x0018FEE4處的eip |
0x0018FEE0 | ebp(main函數的) | 這是get_sum函數形頭push ebp壓入棧的 |
0x0018FEE4 | eip(get_sum返回main的) | 這是main函數中的call get_sum壓入棧的,不屬於get_sum的堆棧空間 |
三、緩沖區溢出
3.1 緩沖區溢出利用的原理
緩沖區溢出:給本函數局部變量賦的值其長度超過了變量定義的長度
結合2.4.2最后的表格中我們可以得到緩沖區溢出利用的原理就是:給本函數局部變量賦長度超過其定義長度的值,使本函數返回上層函數的eip值被覆蓋成自己想去執行的語句的地址。
如果這個定義還感覺不是很明了,那我們舉個例子:main函數調用了vuln_fun函數,vuln_fun函數調用了strcpy,strcpy沒注意長度會引發緩沖區溢出。此時溢出發生在vuln_fun函數的堆棧中,而被覆蓋的eip是vuln_fun執行完返回main的eip。
所以可以給緩沖區溢出漏洞下一個更簡單的定義:緩沖區溢出發生在調用strcpy/strcat/sprintf/vsprintf/gets/scanf的函數中,被覆蓋的eip是該函數返回上層函數的eip。
3.2 緩沖區溢出利用的難點
3.2.1 不考慮系統與編譯器無保護機制時的難點
在給變量賦的超長字符串中包含以下三部份內容:填充數據、注入的要執行的匯編語句、注入的要執行的匯編語句的地址。
填充數據,隨便點就行了不是難點。
注入的要執行的匯編語句,在不考慮編譯器和系統保護機制的情況下,大概是把自己要執行的語句寫成c程序,然后編譯成exe,然后再把語句對應的十六進制dump出來就行了;不過strcpy等函數遇到00會認為字符串結束,而期望dump出的十六進制剛好沒有00是不太現實的,需要對其進行編碼處理。
注入的要執行的匯編語句的地址,這又有兩個難點:一是要確定變量到eip的距離,以便剛好能在eip的位置寫入想要執行的代碼的地址;二是要確定注入的、想要執行的代碼的地址是多少。
3.2.2 編譯器與操作系統的保護措施
編譯器有棧不可執行、棧保護兩種措施,編譯器與操作系統聯動則還有內存布局隨機化。更具體內容可參考:https://www.jianshu.com/p/47d484b9227e
3.2.3 為什么很多overflow的cve沒有exp
長期以來,我都將存在緩沖區溢出漏洞等同於系統命令執行、等同於系統淪陷。但很多overflow類型的cve都只是評分“很低”的dos而不是execute code,而且只有極少數才有exp,這很令人不解。
而基於以上難點的討論,這種現像就好理解了,緩沖區溢出到導致程序運行出錯所以基本都能dos,但由於編寫shellcode本身就比較困難再加上各種保護機制,有溢出不是必然就有exp的。
參考:
https://www.jianshu.com/p/47d484b9227e
https://www.shiyanlou.com/courses/231
https://www.cnblogs.com/yejianyong/p/7506465.html
https://blog.csdn.net/nicholas199109/article/details/8560988