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

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

前言

我們在學習棧溢出漏洞之前,最好都要懂一些開發,還有一些彙編知識,因為不管是安全還是逆向,這些都是基於開發的,有了開發紮實的基礎在後續中才會突破瓶頸。

堆棧

推薦大家可以先去看看《王爽彙編》,或者直接看Bilibili的堆棧是個啥?

image-20220617134927429

堆棧(Stack)概念

首先來了解下什麼是堆棧?我們得從CPU開始說起,CPU中有個模組叫ALU,專門用來處理數據運算。

學過彙編的小夥伴們都知道,CPU中有多個暫存器,不過是固定的,比如eax、ebx、ecx、edx、ebp、esp、edi、esi、eip等,當處理的數據過多或者過大時候,暫存器都不夠用了,這時候怎麼辦?

增加CPU的暫存器嗎?不行那樣成本太大了,所以就需要找另外的地方存數據,那麼硬體中讀取速度除了CPU,也就記憶體條速度最快了。

所以CPU招募了記憶體條,來用來存儲數據,在記憶體條中還專門找了個區域用來存數據即:堆棧(stack),說白了堆棧就是一塊記憶體。

堆棧數據存儲方式

我簡單的畫了一個堆棧示意圖,堆棧是一個自高地址向下增長的記憶體空間,從圖中可以看到我們的高地址,也就是棧底,而低地址大概4個空格的位置是棧頂。

也就是記住地址越低是棧頂,而且堆棧中要添加數據,地址要往跟低的地址移動。

image-20220617143333004

接下來我們繼續來看看,如何在堆棧中存取和讀取數據,他既然是塊記憶體,那麼我們關注的肯定是存取和讀取,首先堆棧中存入數據叫push,讀取數據叫pop。

堆棧管理數據的方式是先進後出,即存取進去的數據,會在堆底,最後存取進去的數據會在棧頂,所以最先拿出來的數據也是最後放進去的即棧頂。

image-20220617144702914

這裡有個需要注意的地方,就是很多人以為pop數據後,堆棧裡面的數據就清空了,其實並不是。

之前說過堆棧其實就是一塊記憶體,當我們pop後,其實知識把棧頂往下移了而已,記憶體裡面的數據還是在的,並沒有被清除掉,只是對於堆棧而言,那數據被彈出。

當要push新數據,push很多個數據,或者pop很多個數據,都按照圖示以此類推。

image-20220617145414921

函數調用

函數調用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。

image-20220617154234257

斷點斷到如上的位置,然後再單步n執行到call myfunction處,此時注意觀察堆棧,可以看到堆棧中壓入了數據2,1。

而我們程式碼是 myfunction(1,2);第一個參數是1,第而個參數是2,因為堆棧的先進後出的特性,所以先把最後的數據入棧。

image-20220617154431218

函數Call返回原理

接著最重要的一步,需要注意!

目前我們處在call myfunction函數上,我們先記一下call myfunction的下一句彙編地址是多少,我這裡是0x565555ac,然後接著我們輸入si,單步步入進行調試,跳轉到myfunction函數的內部,然後此時注意觀察你的堆棧有什麼變化!

此時我們觀察堆棧發現,之前我們call的下一句地址0x565555ac被壓棧了。

image-20220617155152479

當我們一直單步步過myfunction函數中的彙編程式碼,直到他的最後一句這裡,發現彙編程式碼是一句ret,ret的彙編程式碼其實就是pop eip,

也就是將堆棧中的數據彈出到eip,eip我們都知道是彙編中的PC指針,修改eip,那麼當前CPU就會指向那地方開始執行程式碼。

而當前的堆棧數據就是我們調用myfunction函數時壓入的下一條指令的地址,所以將其彈到eip,CPU就會指向那地方執行程式碼。

image-20220617155447844

所以底層利用這種call 函數時將下一條指令地址壓棧的方式,然後執行完函數後再彈棧到eip的方式跳過到調用完函數後的下一條程式碼。

image-20220617160107736

函數棧幀

函數棧幀描述

棧幀也叫過程活動記錄,是編譯器用來實現過程/函數調用的一種數據結構。

當一個函數在運行時,需要為它在堆棧中創建一個棧幀(stack frame)用來記錄運行時產生的相關資訊,因此每個函數在執行前都會創建一個棧幀,在它返回時會銷毀該棧幀。

所以說函數棧幀就是一種數據結構,也是塊記憶體里的數據。

函數棧幀調試

我們繼續用之前的程式碼做例子,然後用pwndbg調試來詳細的分析函數棧幀。

image-20220617164713531

如上圖,當我們準備調用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)

image-20220617181933836

在我們程式碼中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的話多少個臨時變數空間就開闢多少空間。

image-20220617182016232

那麼此時的棧幀結構如下所示:

[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-偏移】進行臨時變數的定位。

image-20220617183223102

OK很好,到這裡我們基本已經了解了函數調用棧幀的一個詳細原理了,這裡再考考大家,那我在這個myfunction函數里要怎麼知道返回後下一條程式碼的地址呢?

這個在之前說過了,當執行到ret彙編程式碼的時候,會把堆棧裡面數據彈給eip。
那麼現在我們用了函數調用幀的概念,是不是就很好懂了,當我們執行到ret的時候,這時候棧幀也就全部結束了,所以此時堆棧中的數據就是返回地址了。
也可以用[ebp+4]來代表返回地址。

最後從其他文章裡面偷來的圖片,方便理解函數棧幀概念。

img

棧溢出漏洞實戰

要求實現棧溢出來執行沒有被調用的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位元組,程式會崩潰(緩衝區溢出)!

image-20220617190111564

pwndbg調試

接下來老規矩,pwndbg開始調試。

首先來找到返回地址,正常情況下[ebp+4]就是ret的返回地址,但是main函數可能不太一樣。

image-20220617193144547

調試下來發現,[ebp+20]才是返回地址,這個實際情況還是以ret語句時候堆棧裡面的數據為準。

image-20220617204342983

在這裡我們可以手動用命令set *地址=值來把return地址改成其他的,這裡我們改成hack函數。

image-20220617204941416

image-20220617205103779

開始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。

覆蓋前

image-20220619220038503

溢出覆蓋後,溢出字元串1111111111111111111111111111111111111111111111112222。

image-20220619220553737

哈哈哈,一開始我還以為開心的結束了能hack到了,結果狗日的...有坑啊這玩意。

image-20220619220910799

;這裡把[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字元串。

image-20220619223032330

因為構造中是要[ecx-4]才是返回地址,所以我們要填入的地址是0x56557028,字元串是VUp(

image-20220619223204751

因為記憶體中是大端存儲,我們要反過來,改成(pUV。

最後加上20個字元串用來做溢出,payload如下。

Payload:

11111111111111111111(pUV

image-20220619223529706

調試圖:

image-20220619223628759

image-20220619223727379

image-20220619223818300

Pwn菜雞小分隊

最後感謝大家的閱讀,本菜雞也是剛學,文章中如有錯誤請及時指出。

大家也可以來群里罵我哈哈哈,群里有PWN、RE、WEB大佬,歡迎交流

img