1. 程式人生 > >黑客攻防入門(一)緩衝區(堆疊)溢位

黑客攻防入門(一)緩衝區(堆疊)溢位

1. 概說


緩衝區溢位又叫堆疊溢位(還有許許多的稱呼),這是計算機程式難以避免的漏洞,除非有新的設計方式將程式執行的堆疊設計取代。

溢位的目的是重寫程式的執行堆疊,使呼叫返回堆疊包含一個跳向預設好的程式的程式(程式碼),這個程式通常稱為shellcode,通過這個shellcode就能獲得如期的shell,更有可能獲得root。


2. 緩衝區溢位的原理

計算機中每一個執行中的程式都有相同的記憶體佈局(邏輯佈局),Linux/Unix的程式佈局大體如下:

儲存器佈局

緩衝區溢位就是利用這個佈局中的堆疊段來作文章的。

堆: 通常用來作為動態儲存分配,如C標準庫函式 malloc 就是在堆裡申請記憶體空間的
棧: 自動變數和每次函式呼叫時所需儲存的資訊存放的地方。棧是自頂向下生長的,棧還有一個特別的地方,就是先進後出,往棧放資料就好比往洞裡塞東西,當拿東西的時候只能先把最外面的拿走。

其中,最重要的一點: 棧中儲存了函式呼叫時的返回地址。

緩衝區溢位的目的就是要將棧中儲存的返回地址篡改成成溢位的資料,這樣就間接修改了函式的返回地址,當函式返回時,就能跳轉到預設的地址中,執行植入的程式碼。


3. 緩衝區溢位需要掌握的知識

  1. 程式執行時的堆疊佈局,請看上圖。

  2. C語言基礎知識,這個可以看C語言相關的入門書籍,如《The C Programming language》。

  3. 彙編基礎,可以看入門的書籍,如《Assembly.Language.Step-by-Step》,下面簡單介紹一下:

    1. 暫存器: 通用暫存器有 AX, BX, CX,DX, DI, SI, BP, SP 共有8個,x86_64bit的cpu新增了八個通用暫存器,分別是r8,r9…r15, 8-15共8個。
    2. 非通用暫存器,即專用暫存器,最重要的是IP, 它總是指向cpu要執行的下一條指令的地址。這個很重要,緩衝區溢位的目的就是要修改ip的值。
    3. 通常暫存器名稱前都有修飾符,主要用來區分暫存器所代表的值的位數,32位的暫存器前面有E,如EAX、EBX等,64位的暫存器前面則有R,如RAX,RBX等。
    4. BP和SP雖然是通用暫存器,但它專用為棧的基址(BP)指向棧的底部,SP指向棧的頂部。
    5. 64位CPU因為通用暫存器比32位的多,所以函式的引數分別用di,si,dx,cx,r8,r9去儲存。
    6. AX寄傳器通常用來儲存函式返回值。

    這裡寫圖片描述

    這裡寫圖片描述


4. 見證緩衝區溢位

4.1 測試程式碼

通過下面的一個小程式,我們一起來見證一下緩衝區溢位。

#include <stdio.h>
#include <string.h>

#define BUFSIZE 10

void foo()
{
    printf("Exploit\n");
}

int main(int argc, char *argv[])
{
    char buf[BUFSIZE];
    strcpy(buf, argv[1]);
    printf("Buf: %s\n", buf);
    return 0;
}

對照上圖的記憶體佈局圖,上面這段小程式中,argv字串陣列就是命令列引數,環境變數(environ)預設不需要顯式寫出來。

接下來,通過下面的命令,我們將程式碼編譯成二進位制可執行檔案。

gcc -g -o stack1 stack1.c

gcc帶-g引數方便gdb除錯。


4.2 執行測試

我們定義了buf的大小是10,下圖是測試向buf複製10位元組、20位元組、24位元組、23位元組的情況:
這裡寫圖片描述

上圖中,我用centos 64bit系統測試,當向buf傳入24位元組時,程式產生了段錯誤(segmentation fault)。

那麼這有問題來了:

  1. buf的定義是 10 位元組,為什麼可以傳入大於10位元組的資料而不出錯?
  2. 當存入24位元組時,程式為什麼出現段錯誤?
  3. 這和緩衝區溢位有關係嗎?

先解釋第3個問題,上圖所參生的錯誤就是緩衝區溢位造成,我們成功的製造了一次緩衝區溢位案例。


4.3 緩衝區的空間估算

至於第一個問題,是因為所有記憶體存放資料都遵循約定的方式:儲存的資料必需是4、8、16、32和64的倍數,這種方式叫記憶體對齊。

為什麼要對齊呢?

這和cpu存取資料的效率有關,資料對齊方便存取,就和東西擺放得整齊方便尋找一個道理。

所以,雖然定義了buf的大小是10,但向其填充大於10的資料,只要在一定範圍內,容忍度還是有的。

那麼,為什麼它的容忍度不是30,不是20,而偏偏是24呢?因為24就是資料對齊的邊界,本來是可以容忍24個位元組的,但是符串的結尾有一個空字元’【/0】’,例子中存入24個A時,實際上存入了25個字元,超過了24的邊界,越界了,所以就出問題啦!


文字的說服力不如圖片,下面請看圖:

這裡寫圖片描述

上圖是main函式的反彙編程式碼,我們擷取一小部分來看。

紅色框部分就是當前棧空間和main的引數傳遞。

mov %rsp, %rbp 設定當前棧指標地址為基址
sub %0x20, %rsp 新的棧指標
上面兩條語句作用是將一段新的記憶體空間設定為新的棧段,棧的空間大小是0x20(32位元組)
mov %edi, -0x14(%rbp) 引數1,距離棧的基址只有0x14(20位元組)
mov %rsi, -0x20(%rbp) 引數2


引數1是main的argc, 引數2就是argv字串陣列(其中argv[0]是/root/stack/stack1,argv[1]是我們將要存入buf的資料),我們來驗證一下是否正確:

這裡寫圖片描述

上圖我們啟動stack1程式,用perl列印20個A作為引數傳遞,可以看到%rdi = 2, 是argc, %rsi 則是一個雙重指標,正符合*argv[]的定義,我們再看看這個指標的資料是什麼?

這裡寫圖片描述

  1. 上圖顯示的正是我們所預期的。
  2. 在這裡,也可以複習一下什麼是雙重指標,如**ptr, arry[][]諸類的定義,它們所指的資料,都要經過兩層間接才能接觸到。同理如果是***ptr這些定義,則要經過三層間接才找到最終資料。

言歸正轉,說說buf為什麼只能容納24個位元組

這裡寫圖片描述

1.對照上圖可知,buf的地址在當前的棧空間內,距離棧基RBP只有0x10(16位元組),即是說buf至少可以容納16位元組,這是記憶體資料對齊到8,buf定義是10byte,為了對齊,需要分配16byte。
2.下面再看rbp上面的內容。
這裡寫圖片描述
可以看到在rbp上的高地址裡有八個位元組多餘的,這也是為了對齊而分配的。
加起來就剛好是24位元組,0x7fffffffe328裡放的資料就是main函式返回地址。


4.4 main函式返回地址

我們再看下面的圖,這是main函式執行時的棧資料

這裡寫圖片描述

上圖紅色框部分就是main函式儲存在棧的返回地址,當main函式執行完畢後,CPU就會跳到這個地址裡執行指令。

那麼這個地址(0x7ffff7a3ab15) 儲存在哪裡呢?

根據程式記憶體佈局,可以肯定,它是儲存在棧段裡!


下面我們先看看當前的棧空間:

這裡寫圖片描述

紅色框裡指示了棧的空間位置,我們再敲入指令看看這個棧空間包含了什麼資料

這裡寫圖片描述

對照紅色框裡的資料,並沒有0x7ffff7a3ab15,即是說main的返回地址並沒有儲存在當前棧空間裡,那麼是不是我們的肯定過於堅定了呢?

非也!我們再看:

這裡寫圖片描述

0x7ffff7a3ab15原來躲在了更高的地址裡,這個也在棧段範圍內,所以說上面的肯定並沒有錯。

bp和sp代表的是當前的棧空間,程式的執行週期裡會在約定的棧段裡使用不同的棧空間。


4.5 緩衝區溢位

上述一系列說明,不難看出,我們最終的目的就是在buf裡溢位資料去覆蓋main的返回地址。

上面分析,我們只要向buf寫入大於24位元組的資料就可以到達到儲存main返回地址的空間,測試也證明了這一點。

當寫入大於24位元組後,程式為什麼會出現Segmentation fault (core dumped)這錯誤呢?

這是因為,我們覆蓋main地址的資料並不是一個main有效的返回地址。

為了達到溢位的真正目的(執行shell,取得root許可權),我們需要精心構建溢位資料。

首先我們要學會構建shellcode,那麼shellcode是如何構建的呢?請看下回分解!