緩衝區溢位攻擊之一(Buffer Overflow)

b3rm1nG
10 min readMar 30, 2017

--

什麼是Buffer Overflow?wiki的描述如下:

a buffer overflow, or buffer overrun, is an anomaly where a program, while writing data to a buffer, overruns the buffer’s boundary and overwrites adjacent memory locations.

通常是因為程式沒有做邊界檢查(boundary check)-例如C的矩陣,導致資料可以寫超過邊界,造成程式被破壞。但為什麼這樣做會破壞程式?以及實際上要如何做才行呢?讓我們來看個簡單的例子。

(以下程式都在x64環境下運行)

#include <stdio.h>void hacker()
{
printf("No, I'm a hacker!\n");
}
void nonSecure()
{
char name[16];
printf("What's your name?\n");
gets(name);
printf("Hey %s, you're harmless, aren't you?\n", name);
}
int main()
{
nonSecure();
return 0;
}

一支簡單的程式,nonSecure()函式裡面用gets()讀取使用者輸入,還有一個莫名其妙完全沒用到的function — hacker(),先不管它,將他存為overflow.c後用gcc compile。

gcc overflow.c -o overflow -fno-stack-protector

[Note] windows下如何使用gcc、gdb:

1. 到MinGW的安裝路徑,通常許多IDE都已經幫你裝了(Ex: Dev-Cpp),如果沒有可以自己到MinGW官網下載。

2. bin資料夾,shift+右鍵=>在此處開啟命令視窗

3. (Optional)可以到系統的環境變數PATH新增MinGW/bin的路徑,之後就可以直接使用。

輸入”Kevin”後看看執行結果:

那麼這次試著輸入”AAAAAAAAAAAAAAAAAAAAAAAA”看看結果如何:

Segmentation fault ! 看來溢位確實造成了程式崩壞了呢,但是為什麼呢?再繼續深入前必須先談談兩件事:

  1. 暫存器(register)
  2. 函式呼叫(function call)

以x86為例,暫存器有許許多多種,例如:EAX、EBX、ECX、EDX,但有幾個暫存器有特殊意義必須特別談談,他們分別是EIP(instruction pointer register)、EBP (base pointer)、以及ESP(stack pointer)。

p.s. x86和x64的暫存器就只有些微的差別,

例如"E"AX(x86, 32bit)=>"R"AX(x64, 64bit)

EIP指向目前要執行的指令的位址。而EBP(base)到ESP(top)的範圍為目前stack的框架。

什麼是框架(frame)?我們知道stack是一個共用的空間,假設有個function A先使用了一些stack的空間,然後程式流程跳轉到function B,此時B要怎麼確認它可以用stack上的哪些空間而不要覆寫、誤存到其他人的空間呢(例如剛剛A所使用掉的空間)? -答案是設一個指標記住之前stack用到哪裡(也就是B開始使用stack那一刻的stack top),並且把這裡當成新的stack base(EBP),從這裡到stack top(ESP)就明確表達出目前stack使用範圍,這就是目前程序的框架(frame)。

談完暫存器後讓我們談談function call時stack所發生的變化。

假如有個function長這樣:

void func(char a, char b)
{
char local_var, local_arr[16];
}

以x86為例,當func被呼叫時,stack上會發生如下的變化:

  1. push參數b、a (注意順序有差,由右到左依序push才能確保最左邊的參數在stack top,也就是a在top)
  2. push return address of func (跳轉到別處當然得要記錄回來的地方囉!也就是呼叫func處的下一行指令位址)
  3. push ebp
  4. 分配stack空間給local_var、local_arr[16]

p.s. x64下步驟 1有些不同,但不影響後續實作,若想了解的朋友可參考x86–64 calling conventions

完成之後stack會變成這個面貌:

在上面的第三步,為什麼要”push ebp”呢?其實這是function prologue的一部分,function prologue是什麼?其實就是function呼叫時的事前準備。

引述wiki:

the function prologue is a few lines of code at the beginning of a function, which prepare the stack and registers for use within the function.

As an example, here′s a typical x86 assembly language function prologue as produced by the GCC

push ebp
mov ebp, esp
sub esp, N

The N immediate value is the number of bytes reserved on the stack for local use.

在上面我們看到三行code的作用其實就是更改框架(frame),假如function A的框架為(ebp_A , esp_A),而A又呼叫了B,而B的框架為(ebp_B , esp_B),此時要如何從A框架改變成B框架呢?

  1. 把A的stack base存起來 (push ebp)
  2. 更改為B的stack base (mov ebp, esp)
  3. 更改stack top為B的stack top(sub esp, N),N取決於B要使用的空間(local variables for B)

既然function prologue是函式呼叫的事前準備,那應該也有函式返回時的準備吧?當然有,那就是function epilogue,一樣以x86 gcc為例:

mov esp, ebp
pop ebp
ret

與function prologue對照應該非常好理解,function epilogue只是做了與其相反的事情,也就是恢復原本的框架,然後回到返回位址(ret,等價於pop eip)。

在此做個小總結,function呼叫時會做四件事情,一、push參數,二、push返回位址,三、push ebp,四、分配空間給區域變數。

談完register以及function call後,現在就能了解為何溢位會讓程式崩潰的原因了。

#include <stdio.h>void hacker()
{
printf("No, I'm a hacker!\n");
}
void nonSecure()
{
char name[16];
printf("What's your name?\n");
gets(name);
printf("Hey %s, you're harmless, aren't you?\n", name);
}
int main()
{
nonSecure();
return 0;
}

這是執行gets(name)之前的stack:

(p.s. stack push是往低位址長,可注意圖上灰字標示)

這是執行gets(name)輸入”AAAAAAA…”之後的stack:

name、rbp、ret全部被A給覆寫,然後當nonSecure這個函式返回時,取用到的return address就是”AAAAAAAA”-也就是0x4141414141414141 (ASCII A : 0x41),而程式無法解析這個位址所放的指令,於是就發生Segmentation fault。

實際用gdb看一下,的確吻合上面的結果。

[Note] gdb常用指令

r => 執行

c => 繼續執行

disas [func name]=> 反組譯

b [*address]=> 斷點

i [r(egister)、f(rame)] => 資訊

我們可以操控程式流程(操控rip值),也就意味著我們能夠控制程式,如果覆蓋return address指向真的有實際指令的地方呢?讓我們來試試。

我們用gdb來找找那個沒用到的函式-hacker()的位址。

他的位址是0x00000000004005bd,把這個位址覆蓋在return address上,於是我寫個python script來產生payload。

16個覆蓋name、8個覆蓋rbp、和用來覆蓋ret的hacker的位址

(由於Little Endian的緣故,原本要寫成第四行那樣,但python的struct函式庫可以幫我們輕鬆處理這個問題。)

跑出了"No, I’m a hacker!”,看來我們成功執行了函式hacker了,接著離開gdb環境直接執行看看:

大功告成!

但到目前為止,我們也只能改變程式流程(跳轉到hacker函式),執行的仍然是現有的code,離任意的執行code還有一段距離。

而且我們目前為止仍舊是在很理想的環境執行,還記得一開始我們編譯時所加上的參數”-fno-stack-protector”吧?試著把他拿掉看看,重新編譯並執行,應該會發現失敗了,這是因為電腦的安全措施所致(Stack Canary),諸如此類的相關安全防護還有DEP、ASLR等等...

該如何實際的執行code,以及種種的安全保護,我們在下一次介紹。

--

--