什麼是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 ! 看來溢位確實造成了程式崩壞了呢,但是為什麼呢?再繼續深入前必須先談談兩件事:
以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上會發生如下的變化:
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, NThe 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框架呢?
既然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,以及種種的安全保護,我們在下一次介紹。