[二進位制漏洞]棧(Stack)溢位漏洞 Linux篇

2022-06-20 06:03:48

[二進位制漏洞]棧(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大佬,歡迎交流