[二進位制漏洞]棧(Stack)溢位漏洞 Linux篇
前言
我們在學習棧溢位漏洞之前,最好都要懂一些開發,還有一些彙編知識,因為不管是安全還是逆向,這些都是基於開發的,有了開發紮實的基礎在後續中才會突破瓶頸。
堆疊
推薦大家可以先去看看《王爽彙編》,或者直接看Bilibili的堆疊是個啥?
堆疊(Stack)概念
首先來了解下什麼是堆疊?我們得從CPU開始說起,CPU中有個模組叫ALU
,專門用來處理資料運算。
學過彙編的小夥伴們都知道,CPU中有多個暫存器,不過是固定的,比如eax、ebx、ecx、edx、ebp、esp、edi、esi、eip
等,當處理的資料過多或者過大時候,暫存器都不夠用了,這時候怎麼辦?
增加CPU的暫存器嗎?不行那樣成本太大了,所以就需要找另外的地方存資料,那麼硬體中讀取速度除了CPU,也就記憶體條速度最快了。
所以CPU招募了記憶體條,來用來儲存資料,在記憶體條中還專門找了個區域用來存資料即:堆疊(stack)
,說白了堆疊就是一塊記憶體
。
堆疊資料儲存方式
我簡單的畫了一個堆疊示意圖,堆疊是一個自高地址向下增長的記憶體空間
,從圖中可以看到我們的高地址,也就是棧底,而低地址大概4個空格的位置是棧頂。
也就是記住地址越低是棧頂,而且堆疊中要新增資料,地址要往跟低的地址移動。
接下來我們繼續來看看,如何在堆疊中存取和讀取資料,他既然是塊記憶體,那麼我們關注的肯定是存取和讀取,首先堆疊中存入資料叫push
,讀取資料叫pop
。
堆疊管理資料的方式是先進後出
,即存取進去的資料,會在堆底,最後存取進去的資料會在棧頂,所以最先拿出來的資料也是最後放進去的即棧頂。
這裡有個需要注意的地方,就是很多人以為pop資料後,堆疊裡面的資料就清空了,其實並不是。
之前說過堆疊其實就是一塊記憶體,當我們pop後,其實知識把棧頂往下移了而已,記憶體裡面的資料還是在的,並沒有被清除掉,只是對於堆疊而言,那資料被彈出。
當要push新資料,push很多個資料,或者pop很多個資料,都按照圖示以此類推。
函式呼叫
函式呼叫C語言程式碼
學習堆疊最重要的應該就是函式呼叫了,當我們呼叫完一個函式後,程式碼都會往下繼續執行下一句程式碼,那麼這一步在底層是如何實現的呢?CPU怎麼知道接下來要執行你函式呼叫完後的下一句程式碼?
這一部分其實稍微學過彙編的都應該知道。
當我們呼叫個函式的時候,在彙編層是叫Call myfunction
,而呼叫函式的時候就會用到堆疊,傳入的引數即:push
。
#include <stdio.h>
/*自己的函式*/
void myfunction(int a,int b)
{
int c = a+b;
printf("%d\n",c);
}
int main()
{
myfunction(1,2);
printf("函式呼叫完畢!");
return 0;
}
函式呼叫過程GDB除錯
接著我們將上面程式碼編譯出來,並且關閉stack保護,編譯成32位,命令gcc test.c -m32 -fno-stack-protector -o test
。
接下來用pwndbg
進行除錯,詳細的看下,函式呼叫與堆疊中的關係。gdb test , b main ,r
。
斷點斷到如上的位置,然後再單步n執行到call myfunction
處,此時注意觀察堆疊,可以看到堆疊中壓入了資料2
,1
。
而我們程式碼是 myfunction(1,2);
第一個引數是1
,第而個引數是2
,因為堆疊的先進後出的特性,所以先把最後的資料入棧。
函式Call返回原理
接著最重要的一步,需要注意!
目前我們處在call myfunction
函式上,我們先記一下call myfunction的下一句彙編地址是多少,我這裡是0x565555ac
,然後接著我們輸入si
,單步步入進行除錯,跳轉到myfunction
函式的內部,然後此時注意觀察你的堆疊有什麼變化!
此時我們觀察堆疊發現,之前我們call的下一句地址0x565555ac
被壓棧了。
當我們一直單步步過myfunction
函式中的彙編程式碼,直到他的最後一句這裡,發現彙編程式碼是一句ret
,ret的彙編程式碼其實就是pop eip
,
也就是將堆疊中的資料彈出到eip
,eip我們都知道是彙編中的PC指標,修改eip,那麼當前CPU就會指向那地方開始執行程式碼。
而當前的堆疊資料就是我們呼叫myfunction
函式時壓入的下一條指令的地址
,所以將其彈到eip,CPU就會指向那地方執行程式碼。
所以底層利用這種call 函式
時將下一條指令地址壓棧的方式,然後執行完函式後再彈棧到eip
的方式跳過到呼叫完函式後的下一條程式碼。
函式棧幀
函式棧幀描述
棧幀也叫過程活動記錄
,是編譯器用來實現過程/函式呼叫
的一種資料結構
。
當一個函式在執行時,需要為它在堆疊
中建立一個棧幀
(stack frame)用來記錄執行時產生的相關資訊
,因此每個函式在執行前都會建立一個棧幀,在它返回時會銷燬該棧幀。
所以說函式棧幀
就是一種資料結構,也是塊記憶體裡的資料。
函式棧幀除錯
我們繼續用之前的程式碼做例子,然後用pwndbg
除錯來詳細的分析函式棧幀。
如上圖,當我們準備呼叫call myfunction
的時候,其實在C語言中是當我們執行myfunction(1,2)
的時候就會生成一個棧幀,那麼在彙編層具體是什麼時候建立呢?
然後當我們進入到myfunction函式內部,然後看到第一條彙編語句是push ebp
,將ebp
暫存器壓入堆疊。
EBP暫存器又被稱為幀指標(Frame Pointer) 【指向當前棧幀的底部】
ESP暫存器又被稱為棧指標(Stack Pointer) 【永遠指向棧幀的頂部】
然後接著的一句彙編程式碼是mov ebp,esp
,這一句彙編指向完後,才開始真正的建立棧幀。
此時棧幀的資料結構差不多是這樣: (現在我們就可以用ebp來進行定址了,當我們要用到第一個引數那麼用ebp+8即可,第二個引數ebp+0xC)
[ebp+0] -----> 棧幀底 ,也是當前的棧頂 【ebp】【esp】
[ebp+4] --> 呼叫完Call函式後下一條指令地址
[ebp+8] --> 1(引數1)
[ebp+0xC] --> 2(引數2)
在我們程式碼中myfunction
裡面還有計算a+b的值賦值給c的程式碼
,我們繼續除錯看彙編且關注棧幀中對資料的處理。
當執行完棧幀建立後的彙編程式碼後,第一句的彙編程式碼是sub esp,0x10
,我們之前講過esp永遠為棧頂,當esp-16代表的是,esp要向上移動16位元組,用來存放資料。
一般來說這種sub esp,xxx
或者add esp,-xxx
,都是用來建立臨時變數 ,存放臨時變數資料的。我們這裡的臨時變數就一個那就是int c
,那麼int
佔用4個位元組,這裡開闢了
16位元組空間,可能是gcc的優化為了對齊什麼的吧,Windows的話多少個臨時變數空間就開闢多少空間。
那麼此時的棧幀結構如下所示:
[ebp-0x10] 棧頂 [esp]
[ebp-0xC]
[ebp-8]
[ebp-4]
[ebp+0] -----> ebp 棧幀底 ,之前棧頂
[ebp+4] --> 呼叫完Call函式後下一條指令地址
[ebp+8] --> 1(引數1)
[ebp+0xC] --> 2(引數2)
【可以看到我們可以利用ebp這種方式來進行對臨時變數的一個定位,因為ebp永遠是棧底,所以可以用來尋找不同的資料,當ebp-代表的是臨時變數,ebp+代表的是函式引數】
當我們繼續單步執行程式碼,執行到如下圖所示的地方,可以看到果然是利用【ebp+偏移】進行函式引數的定位,然後利用【ebp-偏移】進行臨時變數的定位。
OK很好,到這裡我們基本已經瞭解了函式呼叫棧幀
的一個詳細原理了,這裡再考考大家,那我在這個myfunction函式裡要怎麼知道返回後下一條程式碼的地址呢?
這個在之前說過了,當執行到ret彙編程式碼的時候,會把堆疊裡面資料彈給eip。
那麼現在我們用了函式呼叫幀的概念,是不是就很好懂了,當我們執行到ret的時候,這時候棧幀也就全部結束了,所以此時堆疊中的資料就是返回地址了。
也可以用[ebp+4]來代表返回地址。
最後從其他文章裡面偷來的圖片,方便理解函式棧幀概念。
棧溢位漏洞實戰
要求實現棧溢位來執行沒有被呼叫的hack
函式。
要求:不允許使用pwntools工具
#include <stdio.h>
void hack()
{
printf("Hack Success!!!!\n");
}
int main()
{
printf("Hello,Please Start Hack!\n");
char buf[20];
scanf("%s",buf);
return 0;
}
首先我們執行程式,然後輸入>=20位元組,程式會崩潰(緩衝區溢位)!
pwndbg除錯
接下來老規矩,pwndbg
開始除錯。
首先來找到返回地址,正常情況下[ebp+4]就是ret的返回地址,但是main函式可能不太一樣。
除錯下來發現,[ebp+20]才是返回地址,這個實際情況還是以ret語句時候堆疊裡面的資料為準。
在這裡我們可以手動用命令set *地址=值
來把return地址改成其他的,這裡我們改成hack
函式。
開始Hack
OK上面我沒通過偵錯程式修改數值,直接將堆疊的值改成了hack函式的地址,讓他在return的時候直接返回到hack函式,從而成功輸出Hack Success!!!!
。
接下來我們用溢位來構造流程,讓程式執行hack函式。
思路:
char buf[20]; 是20個位元組的空間,因為他是個臨時變數,所以他應該是用ebp-xxx來定位。
假設 [ebp-xxx] = buf地址
那麼我們需要覆蓋到的是返回地址,一般是在[ebp+4]
而這裡strcpy允許我們任意的輸入任何長度的字串(造成漏洞的原因)
我們這裡只要把[ebp+4]給覆蓋了就行,所以我們在輸入20個字串後,再繼續輸入4個字串會把[ebp+0]覆蓋掉,因為溢位。
接著繼續輸入4個字串,(28個字串),就會把[ebp+4]也給覆蓋掉,就覆蓋到返回地址了。
程式ret的時候,就能跳到我們28個字串中最後4個字串構造的地址中去了。
因為我們這裡除錯出來是[ebp+20]
才是返回地址,而且這裡buf是[ebp-0x1c]
,0x1c=28
,所以28位元組剛好覆蓋到ebp,那麼再加20就覆蓋到返回地址,所以長度是28+20=48
。
覆蓋前
溢位覆蓋後,溢位字串1111111111111111111111111111111111111111111111112222
。
哈哈哈,一開始我還以為開心的結束了能hack到了,結果狗日的...有坑啊這玩意。
;這裡把[ebp=8]地址設為棧頂,除錯發現[ebp-8],剛好是char [20]位元組後的資料,也就是溢位後的第一個位元組地址。
0x565555e2 <main+74> lea esp, [ebp - 8]
;然後這裡把棧頂彈給ecx暫存器
0x565555e5 <main+77> pop ecx
0x565555e6 <main+78> pop ebx
0x565555e7 <main+79> pop ebp
;這裡又把[ecx-4],也就是[ebp-8]棧頂-4位置堆疊裡面的 值 ,設定為新的esp,然後ret返回。
0x565555e8 <main+80> lea esp, [ecx - 4]
0x565555eb <main+83> ret
所以這裡的思路是,我們可以來控制ecx暫存器,因為ecx暫存器是由[ebp-8]地址的值賦值過去的,這裡剛好是我們溢位覆蓋到的最開始4個位元組,所以我們可以控制這個地址,然後讓這個地址指向偏移-4位置,然後這位置裡面的值是hack函式地址,即可hack成功!
哈哈,因為我自己出的題目,要求不能用pwntools工具,所以只能用ASCII碼
來構造,構造來構造去發現ecx的堆疊地址是0xFF這種開頭的,這種ASCII碼對不上,超過能顯示正常字元的ASCII碼了,所以最後放棄了,我重新把題目程式碼改了下,改成了下面的樣子。
題目要求:不能使用pwntools,讓程式執行hack函式。
#include <stdio.h>
int _a = 1;
int _b = 2;
int _c = 3;
int _d = 4;
int _e = 5;
int _f = 6;
int _g = 7;
int _h = 0x5655556d;
int _i = 8;
int _j = 9;
void hack()
{
asm("mov esp,0xffffd57c\n");
printf("Hack Success!!!!\n");
asm("mov ebx,0\n");
asm("mov eax,1\n");
asm("int 0x80\n");
}
int main()
{
printf("Hello,Please Start Hack!\n");
char buf[20];
scanf("%s",buf);
return 0;
}
解題思路:
這題目不同電腦可能執行效果不一樣,因為我把地址寫死了,我這裡把hack函式地址寫到了全域性變數,而且故意是第8個全域性變數,因為這位置剛好是 .data段中地址是 可以用ASCII碼來顯示的,然後我在hack函式開頭用了一個彙編設定了棧頂,因為不設定的話呼叫printf函式會失敗,最後用匯編呼叫int 80(中斷),功能號1 exit來強制退出程式,讓其能顯示出Hack Suucess字串。
因為構造中是要[ecx-4]
才是返回地址,所以我們要填入的地址是0x56557028
,字串是VUp(
因為記憶體中是大端儲存,我們要反過來,改成(pUV
。
最後加上20個字串用來做溢位,payload如下。
Payload:
11111111111111111111(pUV
除錯圖:
Pwn菜雞小分隊
最後感謝大家的閱讀,本菜雞也是剛學,文章中如有錯誤請及時指出。
大家也可以來群裡罵我哈哈哈,群裡有PWN、RE、WEB大佬,歡迎交流