作者fon909 (峰)
看板C_and_CPP
標題Re: [問題] Buffer overflow 和 Instruction address
時間Thu Sep 8 15:03:05 2011
這是去年12月的老題了,搜尋overflow看到堆疊溢位,就覺得有點心癢想來玩一下。
原PO的超連結還在:
http://isis.poly.edu/courses/cs392-f2010/labs/HW6.pdf
不知是否2011年還會有類似的題目可以玩看看。
題目有兩小題,第1題是要改func內容,使得main函式內的printf("x is 1");被跳過,也就是說,
程式只會印出x is 0的字串,原來的程式碼如下:
//BEGIN:A program to illustrate buffer overflow
//vulnerability
void func(char *str) {
char buffer[24];
int *ret;
strcpy(buffer,str);
}
int main(int argc, char **argv) {
int x;
x = 0;
func(argv[1]);
x = 1;
printf("x is 1");
printf("x is 0");
}
遊戲規則是必須修改func的內容,使得程式只會印出x is 0來。
第2題是要透過另一個程式去呼叫原來的程式,同樣也是要讓printf("x is 1");被跳過,也就是說,
原來的程式不變,但是另一支程式去呼叫它的時候,可以傳入參數argv[1],透過傳入的參數修改原程式
的執行順序,使得printf("x is 1");被跳過,程式也是只會印出x is 0。
為了解釋我一步一步怎麼操作的,下面文章有點長,如果你對堆疊溢位有興趣請再繼續往下看,我當然也
可以直接PO解答,幾行就沒了,但是那意義就不大了。
第1題滿簡單的,甚至算不上堆疊溢位,這小題答案很明顯的一定跟編譯器有關,用不同的編譯器做出來
的答案就不一樣,我用Windows XP SP3和Windows 7 SP1試驗過,只要用的編譯器和設定都一樣,
在這兩個OS答案是一樣的,我以下提供兩種解答,分別針對萬年Beta版的Dev-C++ 4.9.9.2,以及
VC++ 2010 Express兩者。
針對Dev-C++的第1小題解答:
void func(char *str) {
char buffer[24];
int *ret = (int*)(buffer + 0x2C);
strcpy(buffer,str);
*ret += 0x13;
}
要理解這小題,要先知道在堆疊內的架構,我這裡大概簡述一下,一般在呼叫函式的時候,例如在main
函式中的第3行func(argv[1]);當程式執行到這一行,程式會把參數argv[1]推入到堆疊內,並且把之後
main函式的位置推入堆疊內(可以想就是main第3行的下一行的位置,也就是第4行),並進入到func內部,
再推入frame pointer(以下稱EBP)當作一個"柵欄",這裡先不管EBP是哪來的其原本的值為何,只要知
道推入堆疊的順序即可,接下把EBP設為堆疊目前最上方的位置,然後堆疊繼續長,為func內部的區域變數
留空間,繼續有東西被推到堆疊裏面,而EBP一直維持不變,所以當func要結束的時候,從EBP到最後堆疊
的位置,中間所有的空間,就是在func中被推進堆疊的內容,不管被推進去了多少,到func結束的時候,
都應該被吐(pop)出來,包括"柵欄"本身,全部pop出來之後,剩下來堆疊最上面的第一個,就是之前一開
始有被推到堆疊裡的main函式第4行的位置,然後程式會把堆疊的最上方的位置讀進來,也就是main函式
第4行的位置,並且跳到那個位置去執行,我們姑且把這個位置暱稱作RET,而且在堆疊中,RET會在EBP
(柵欄)下面,也就是EBP+4(堆疊是往記憶體位置低的地方堆,所以堆疊往下數是加,往上堆是減),而且,
只要修改了[RET],程式就會跳到我們修改的地方去執行,換言之,我們可以透過修改[RET]控制程式執行
的流程。
記住:RET是EBP+4
雖然不是每次都這樣,但是在這個題目裡頭func就是這樣,和C的__cdecl呼叫方式有關。
我用大寫RET和程式碼裏面的ret變數做區別。
這裡容我先省略了暫存器的功能以及圖示堆疊,或許未來會有機會特別來講這些,可以更圖文並茂一點。
如果理解上述堆疊內的概念,就可以知道我們只要修改RET,就可以控制程式執行的流程,而在func函式
內,我們是有機會可以透過相對位置找到RET,並進而修改RET的值,以至於func結束回到main的時候,
應該由main第4行開始執行下去,變得由第6行開始執行。
到這裡都可以理解的話,我們要做的事情有兩件,第1件是要在func內找到RET在哪裡,第2件是要把RET
的值從原本的main第4行,改成第6行,以讓func結束回到main的時候,從第6行開始執行。
到這裡要打開debugger了,在Windows下我常用Immunity,有時候用OllyDbg,比較少用Windbg,
隨便哪個都好,至少要熟一個就是了,我很少用Windbg原因是它在我的VM上會crash(我不想要debug
the debugger...)。
下面我將只用文字來解釋OllyDbg的操作,抱歉沒有ASCII圖。
OllyDbg網站超連結:
http://www.ollydbg.de/
先用Dev-C++把原始程式編譯出來,假設產生出來的叫作original.exe檔案,打開OllyDbg選
File|Open把original.exe載入,載入視窗下方Arguments欄位隨便填一個字串"meaningless",
為了要應付argv[1]用的,這裡先不用理會argv[1],讓它有值就好。
打開之後,OllyDbg預設的是CPU view,所以你應該會看到顯示組合語言的視窗,因為程式很小,所以
在那個視窗往下拉一點就可以看到strcpy這個函式被呼叫的地方,我貼我看到的在下面:
00401290 /$ 55 PUSH EBP
00401291 |. 89E5 MOV EBP,ESP
00401293 |. 83EC 48 SUB ESP,48
00401296 |. 8B45 08 MOV EAX,DWORD PTR SS:[EBP+8] ; |
00401299 |. 894424 04 MOV DWORD PTR SS:[ESP+4],EAX ; |
0040129D |. 8D45 D8 LEA EAX,DWORD PTR SS:[EBP-28] ; |
004012A0 |. 890424 MOV DWORD PTR SS:[ESP],EAX ; |
004012A3 |. E8 A8050000 CALL <JMP.&msvcrt.strcpy> ; \strcpy
004012A8 |. C9 LEAVE
004012A9 \. C3 RETN
留意,你看到的可能和我這裡貼的有些微的差異,記憶體位置可能不一樣,總之,找到strcpy,可以
看到是上面004012A3的位置,OllyDbg很方便的幫我們抓出整個func,並且把func用記號括起來,從我
的00401290到004012A9這中間,就是函式func,可以看到在004012A3那裡strcpy被呼叫之前有4行要
注意,strcpy的宣告是char* strcpy(char* dest, const char* src),有兩個參數dest和src,所以
很自然在程式於004012A3呼叫strcpy以前,必須要把這兩個參數準備好,準備的方式就是會把它們推
到堆疊裡,src會被推到[ESP+4]的位置,dest會被推到[ESP]的位置,ESP代表堆疊目前位置,從
004012A0看到MOV DWORD PTR SS:[ESP],EAX,代表把暫存器EAX的值推入[ESP],而前一行0040129D
是在預備EAX,LEA是把[EBP-0x28]的值拷貝到EAX,所以綜合0040129D和004012A0這兩行來看,程式把
[EBP-0x28]的值拷貝到[ESP],所以[EBP-0x28]就是dest。
好,回到原始程式碼,可以看到strcpy的dest是char buffer[24],又從剛剛知道[EBP-0x28]是
dest,所以buffer指標就是EBP-0x28,換言之,buffer+0x28就是EBP,而記得RET等於EBP+4嗎?
所以buffer+0x28+0x4 = buffer+0x2C就是RET的位置,我們在func中找到RET了。
所以我的解答第2行:
int *ret = (int*)(buffer + 0x2C);
接下來我們要找main的第4行和第6行,從剛剛的OllyDbg往下繼續拉一點,會看到兩處呼叫printf
的位置,這就是main函式庫(程式真的很小,所以往下拉一點就看到了),我貼我看到的在下面,留意,
你看到的記憶體位置可能會些許不一樣:
004012E6 |. E8 A5FFFFFF CALL BBS-Proj.00401290
004012EB |. C745 FC 010000>MOV DWORD PTR SS:[EBP-4],1 ;
004012F2 |. C70424 0030400>MOV DWORD PTR SS:[ESP],xxxx.00403000 ; "x is 1"
004012F9 |. E8 42050000 CALL <JMP.&msvcrt.printf> ; \printf
004012FE |. C70424 0730400>MOV DWORD PTR SS:[ESP],xxxx.00403007 ; "x is 0"
00401305 |. E8 36050000 CALL <JMP.&msvcrt.printf> ; \printf
從004012E6可以看到CALL xxxx.00401290,這裡是呼叫函式func,所以上面你可以看到func的
起始位置是00401290,當func結束之後,程式會回到main函式,就會繼續由004012EB開始跑,以
至於到004012F9的時候呼叫第一次printf印出"x is 1",所以我們只要把RET指向的值從原本的
004012EB改成004012FE,就會跳過第一次printf的呼叫,用小算盤工程師模式算一下十六進位
004012FE-004012EB = 13(十六進位),所以我只需要將指標RET指向的內容平移加上0x13即可,
所以我的解答第4行:
*ret += 0x13;
原題的第1小題解開了。
運用同樣的手法,針對VC++ 2010 Express版本的解答:
void func(char *str) {
char buffer[24];
int *ret = (int*)(buffer+0x24);
strcpy(buffer,str);
*ret += 0x14;
}
我在VC++ 2010 裏面是開Empty Project,然後新增一個.c檔案,把程式碼貼入編譯,其他選項都
沒動,可以正確解答問題。
別忘了執行程式的時候要丟入參數argv[1],否則根本沒得玩,例如編譯為original.exe,命令列
呼叫:
> original.exe "meaningless"
"meaningless"字串是沒有特別意義的,只是讓程式切到argv[1]有東西而已,又不會讓buffer[]
爆掉。
轉移目標到第2小題。
我們需要透過另一支程式呼叫原始程式,並且讓它同樣發生第一次printf呼叫被跳過的情況。
這題牽涉到VC++的時候可以小題大作,不過我們先看簡單的Dev-C++ 4.9.9.2。
針對Dev-C++的解答是:
#include <string>
#include <sstream>
#include <cstdlib>
using namespace std;
int main(int argc, char **argv) {
string junk(0x2C, 'A');
string ret("\xFE\x12\x40\x00");
ostringstream sout;
sout << argv[1] << ' ' << junk << ret;
system(sout.str().c_str());
}
假設原始被攻擊的程式是original.exe,而上面這支程式是attack.exe,執行方式就是:
> attack.exe original.exe
(要注意路徑,還有記得我這裡的作業系統是XP SP3,關於不同作業系統的影響請繼續讀下去)
記得第1小題我針對Dev-C++的解答是先ret = (int*)(buffer+0x2C);也就先跳到程式流程RET
的位置,因為透過另一個程式呼叫不可能直接這樣跳,所以要透過strcpy來幫助我們,把buffer塞到
0x2C的長度,下一個DWORD(4 bytes)就是RET,所以我設定junk為0x2C個'A'字元,然後就是RET,
把字串ret設為"\xFE\x12\x40\x00"是考慮到little-endian,其實這值就是0x004012FE,最後
一個字元是\x00無所謂,反正我後面也沒有要貼別的東西了,還記得0x004012FE嗎?往上面看一下原
程式main函式的OllyDbg的輸出,我們在第1小題就是把[RET]平移到004012FE,所以這裡其實就是直
接指定絕對位置。
這種指定絕對位置的手法,在Dev-C++這類"穩定"的編譯器所編譯出來的程式,相當得配合,把以上的
程式拿到Windows 7上面編譯或者執行,還是可以跑出解答。
接下來針對VC++來做第2小題,準備要拉扯出一堆堆疊溢位保護的機制了。
在VC++ 2010中,預設有security cookie等保護機制,所以這個直接賦值的手法是不能用的,而從
XP SP3到Windows 7之間又多了rebase、ASLR(Address Space Layout Randomization)、以
及Permanent DEP這些大關卡,為了完整的緣故我還是講一下,先從最簡單的沒有這些保護的機制講起,
首先要用XP SP3,這樣確定我們的程式不會rebase,然後手動拿掉VC++的一些設定,先開一個Empty
Project,新增.c檔案,把原始程式碼貼入存檔,要編譯之前,到Project | C/C++ | Code
Generation | Buffer Security Check那裡,設為 No (/GS-),Basic Runtime Checks
設為 (/RTCu),存檔,編譯,這樣出來的原始程式就跟Dev-C++編譯出來的程式一樣是可以直接用RET
覆蓋手法攻擊,編譯好成為original.exe,針對這個VC++編譯出來的程式去攻擊,另一支程式的程式
碼如下:
#include <string>
#include <sstream>
#include <cstdlib>
using namespace std;
int main(int argc, char **argv) {
string junk(0x1C, 'A');
string eip("\xD0\x13\x41\x00");
ostringstream sout;
sout << argv[1] << ' ' << junk << eip;
system(sout.str().c_str());
}
要注意我的作業系統是XP SP3,不同的作業系統編譯出來的original.exe,上面magic values
(0x1C、"\xD0\x13\x41\x00"這兩個值)會不一樣。
前面有提到這第2小題可以小題大作,原因就是因為VC++的Buffer Securty Check等等機制,還有
就是指定絕對位置畢竟是很粗糙的手法,第1題雖然我們也是直接賦值,但還是用相對位置,比這裡給一
個絕對位置好多了。
小題大作的手法就是透過去覆蓋例外處理的位置(exception handling),然後寫入過多的資料到
buffer裏面以至於產生access violation的例外,然後跳到我們所覆蓋的例外處理位置,例外處理
的裏面,還需要針對rebase的緣故去動態找到main函式中第2次呼叫printf在哪裡,因為Windows 7
上VC++編譯的程式會rebase,所以我們不能夠依靠絕對位置,解決的手法就是去找"x is 0"的字串,
然後找到在哪裡用到這個字串,那裡就是我們第2次呼叫printf的地方,所以需要執行動態去搜尋記憶體
的shellcode。
總結來說,這個大手術就是先透過buffer覆蓋例外處理的位置,然後繼續讓過多的buffer產生例外,
當程式去呼叫例外處理的程式碼的時候,因為已經被我們覆蓋掉了,所以我們可以改變程式執行的流程,
去執行別的地方的程式碼,這時候我們的shellcode上場,我們讓程式去執行我們的shellcode,在
shellcode中做的事情,就是去記憶體裏面動態的找"x is 0"字串,找到字串存放的位置,在去找使
用這個位置的地方,那個地方就是我們要跳過去執行的位置。
大概是這樣,但是要在Windows 7下面VC++編譯的程式中執行shellcode,還得過一個大魔頭
Permanent DEP,一般在記憶體中的shellcode是無法被執行的,需要使用ret-to-libc手法或是ROP
(return-oriented programming),而我們的目的是要跳到第2次的printf,而不是要任意執行其他
程式碼,所以ret-to-libc也不適用這個情況(就我所知),必須用ROP的手法,而ROP又必須仰賴足夠
的DLL數量,原始程式超簡單幾乎沒有什麼DLL被載入,能否成功使用ROP還是未知數。零零總總扯了
很多,所以才說這可以是個大手術,還不一定會成功。
或許我會找時間更多的解釋大手術的這一塊。
以上,分享我的心得給對堆疊溢位有興趣的朋友。
內文頗多,難免有疏忽遺漏之處,多多包含。
--
※ 發信站: 批踢踢實業坊(ptt.cc)
◆ From: 111.249.168.145
推 kasase:有趣,推 09/08 18:06
推 purpose:Immunity,連看雪都很少聽到有人提的軟體,感覺很陌生 09/08 18:06
推 VictorTom:推; 話說有沒有可能讓printf還是照印, 只是把它導去印 09/08 23:13
→ VictorTom:空字串....XD 09/08 23:13
→ firejox:第1題 把func 用#define 寫不是比較簡單 XD 09/08 23:54
→ firejox:反正條件是更改func內容XD 09/08 23:54
→ fon909:@VictorTom:可,但要執行shellcode改程式碼 09/09 00:59
→ fon909:再想一下,應該是不行,因為那塊記憶體唯讀 09/09 14:40
推 VictorTom:改變第一次call printf時傳入的arg參數為NULL之類的? 09/10 01:00
→ fon909:我也想過,但傳入參數是00403000,是一個唯讀的位置 09/10 09:36
→ fon909:請看004012F2: MOV DWORD PTR SS:[ESP],xxxx.00403000 09/10 09:37
推 VictorTom:這點小弟倒是忘記了, 有debugger在的時候都習慣硬改XD 09/11 20:06
→ fon909:轉念又想,其實還是"可能"突破唯讀限制寫進去 09/16 00:09
→ fon909:但這題buffer太小,不知道行不行,或許有時間再來試試看 09/16 00:10