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

berming
berming
Mar 30, 2017 · 10 min read

什麼是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,以及種種的安全保護,我們在下一次介紹。

berming

Written by

berming

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade