看板 C_and_CPP 關於我們 聯絡資訊
這是去年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