code

2017年12月11日 星期一

Software Security 2 - Buffer Overflow attack

Low-level Security: Buffer Overflow (stack smashing)

這主要發生在C/C++,目前常使用的系統包括: OS kernels, servers (各種http server, memory cache等), embedded systems。

定義: access 一個buffer非合法的index範圍,就是一個buffer overflow attack。


Process 記憶體配置複習

首先一個program要執行時,會有一個process執行它,這個process被分配了某個大小的記憶體,例如在32bit系統上,記憶體位址為 0 ~ 2^32 - 1:


這記憶體位址當然是虛擬(logical address)的,不是真實的physical address (那什麼叫做physical address?),只對此process來說有意義,這個記憶體區塊又分為以下的堆疊:


Text就是code (e.g. function implementation),Heap被malloc管理,而Stack存放local variables和function calls,把Stack和Heap放置呈水平來看:



每當Stack push一個local variable,它會往memory address較低的方向push,而heap剛好相反,每當allocate一個新的memory,heap會往address較高的方向增加。

Compiler在runtime會透過指令去調整stack所占記憶體的大小(push / pop / return),malloc implementation則負責管理跟OS要(allocate)的記憶體,但是要到的記憶體則充分由malloc管理。


Stack and Function calls

假設有一個function有三個傳進來的參數,以及兩個local variables,定義如下:


當一個function的caller呼叫此function時,compiler會先把帶進的參數以相反順序push到stack上,而local variables "通常" 會以宣告的順序push到stack上:



那program要怎麼知道loc2在記憶體中的位置呢? 絕對位置無法得知,因為這個function可能被call很多次,每次呼叫都會push一個loc2到stack上,但是compiler可以知道相對於caller的位置。

先定義stack frame為每次此function被呼叫時使用到的stack範圍:


compiler使用一個稱為 frame pointer (i.e. ebp register),這指向了一個funciton的local variable的起始記憶體位置:

所以在32bit word機器上,loc2 address = %ebp - 2words = %ebp - 8 bytes


假設main()呼叫了func(),首先對compiler來說,main()跟func()沒什麼兩樣,所以先把main的parameters/local variables push到stack上面,就是上圖中的caller's data部分,當然對main()來說,它有儲存自己的frame pointer到ebp register (下圖綠色字眼):


compiler把func()的arg3, arg2, arg1 push到stack上,然後得要在ebp register中記錄func()的frame pointer (i.e. func()的local variable起始位置),可是這樣在func() return之後, 我們ebp register仍然保有的是func()的frame pointer,而不是main()的,這樣main()的接下來未執行的程式就無法找到main()的local variable相對位置,所以我們必須要把這個main()的 frame pointer也push到stack 上:



所以第一個問號就是caller (main())的frame pointer,這樣我們等一下可以直接用這個pointer指向的位置來restore ebp register成caller的frame pointer。

我們還需要push第二個問號: 目前的instruction pointer。
由於func()在text (code)的位置當然不同於main()的位置,所以呼叫func()代表instruction pointer要jump到不同的位置:



所以從func() return 回來的時候,我們不但restore了main()的frame pointer到ebp register,也restore了jump 之前的 instruction pointer到 eip register,這樣就完成了一個function call。



Buffer Overflow

以下這段程式碼,被strcpy造成了buffer overflow (i.e. access 不該access的address):


首先main()呼叫了func(),所以compiler執行過後的stack會長這樣:

strcpy會造成overflow,所以原本記錄著main()的frame pointer變成了:



所以當func() returns,這個錯誤的frame pointer會被複寫到ebp register,造成segmentation fault:



可是如果overflow複寫的不是ebp register,而是另一個local variable?


如果上面的authenticated是一個安全檢查的local variable,則這個buffer overflow造成了檢查漏洞:



Code Injection

buffer overflow既然有可能改寫contiguous memory上面的數值,且可以直接複寫不論多少bytes都可以,所以hacker面臨兩個主要挑戰:

1. 怎麼把自己的惡意的assembly code塞到memory?
2. 怎麼移動instruction pointer指到自已惡意的code來執行?

關於第一點,由於strcpy / sprintf 等有可能造成buffer overflow的function都會在遇到 '\0' byte的時候就停止operation,所以hacker要先避免自己的assembly code有NULL byte。另外當然自己的assembly code必須是完整的,不依賴loader去load其他位置的code。

這個injected code最好是能執行shell command,稱為shellcode,例如以下:

最後一行就是執行了shell command,也就是執行了 "bin/sh"。


再來關於第二點,怎麼移動instruction pointer到自己的code?
還記得呼叫一個funciton的時候,stack frame會長這樣:

所以我們只要把儲存caller instruction pointer的memory (上圖中的 藍色%eip位置),改寫成惡意code的起始位置0xbff即可。問題是我們怎麼知道我們惡意的code是在memory中的哪個位置?

推理,precondition:
(a) hacker不知道原本的source code,所以hacker不知道buffer的記憶體位置以及大小,所以無法知道自己透過overflow inject的code在記憶體的位置

(b) hacker可以access目前eip和ebp register,所以她知道目前這個callee的frame pointer是指向哪裡,所以可以知道存放caller的instruction pointer存放在哪個記憶體位置。

(b) 如果caller的instruction pointer填錯位置(亂猜)的話,則CPU jump後會看到invalid instruction,就panic當機。所以在惡意的code之前塞入一堆noop instruction,這是1-byte instruction,CPU看到這個只會停止一個cycle不做事,不會panic,所以可以提高亂猜卻抵達injected code機率:




所以要使得惡意inject code能被執行就是靠 "猜"一個可能的位置,存放在caller instruction pointer存放的位置,這在function return的時候會jump過去,然而由於是猜測的位置,所以要靠多放一些noops在惡意的code之前來提高jump過後不會panic的機率。

大致上是這樣:






沒有留言:

張貼留言