棧溢位——鄰接變數
一般而言,區域性變數在棧中的分佈是相鄰的(但也可能出於編譯優化等需要而出現例外)。如果這些區域性變數中有陣列之類的緩衝區,並且程式確實沒有防護陣列越界,那麼越界的陣列元素就有可能破壞棧中相鄰變數的值,甚至破壞棧幀中所儲存的EBP的值、返回地址等重要資料。
0x00 原始碼
又是一份密碼驗證功能的C程式程式碼:
#include <stdio.h> #include <string.h> #define PASSWD "qazwsxe" int verify_password(char* password) { int authenticated; char buffer[8]; authenticated = strcmp(password, PASSWD); strcpy(buffer, password); return authenticated; } void main() { int valid_flag; char password[1024]; while(1) { printf("[*] Please input password: "); scanf("%s", password); valid_flag = verify_password(password); if(valid_flag) { printf("[-] Incorrect password!\n\n"); } else { printf("[*] Congratulation! You have passed the verification!\n"); break; } } }
程式的“初衷”是驗證密碼“qazwsxe”,如果輸入錯誤將迴圈等待下次輸入,只有輸入正確了才能跳出迴圈。
程式使用TDM-GCC 9.2.0 32-bit Debug進行編譯;務必避開Visual Studio系列編譯的GS選項,這個選項會規避棧溢位。
0x01 分析
函式verify_password()新增了局部變數char buffer[8],注意是緊鄰著變數authenticated的,這個宣告位置很重要。另外,在字串比較後有函式strcpy(buffer, password),沒有發揮實現驗證功能的作用,非常違和,僅僅是為了構成棧溢位漏洞。
當程式執行到呼叫函式verify_password()時,其棧幀狀態如下所示:
char buffer[0~3](ASCII編碼的輸入前4位) |
char buffer[4~7](ASCII編碼的輸入第5到8位,期望第8位為字串截斷符) |
int authenticated |
返回地址 |
上一個棧幀的EBP |
形參:password |
…… |
authenticated是int型別,在記憶體中佔用一個DWORD,即4個位元組。如果能讓buffer陣列越界,buffer[8]、buffer[9]、buffer[10]、buffer[11]將寫入相鄰變數authenticated中。
authenticated承接的是函式strcmp()的返回值,之後會返回給main函式作為密碼驗證成功與否的標誌變數。按照strcmp()的設計,authenticated為0表示驗證成功;任何非0值都表示不成功。
如果輸入超過了7個字元(本來buffer第8位是用來存放字串截斷符NULL的,截斷符必須佔用1個位元組),則越界字元的ASCII碼會修改掉authenticated的值:恰好為0,則程式流程就會發生改變。
0x02 除錯
先通過IDA獲得函式strcpy()的VA:
使用x96dbg開啟程式,在這個VA處下斷點:
輸入錯誤密碼“zzzzzzz”,按照ASCII編碼,有字串的序關係“zzzzzzz”>“qazwsxe”,則strcmp()函式應當返回1,即預計authenticated會被賦值1。實際情況符合預期(斷點後進行一次單步步過):
0x7A
是小寫字母“z”的ASCII編碼,下方的0x00000001
正是authenticated的值。棧幀資料分佈情況一目瞭然。
區域性變數名 | 記憶體地址 | 偏移3處的值 | 偏移2處的值 | 偏移1處的值 | 偏移0處的值 |
---|---|---|---|---|---|
buffer[0~3] | 0x0065FA94 |
0x7A ('z') |
0x7A ('z') |
0x7A ('z') |
0x7A ('z') |
buffer[4~7] | 0x0065FA98 |
NULL | 0x7A ('z') |
0x7A ('z') |
0x7A ('z') |
authenticated | 0x0065FA9C |
0x00 |
0x00 |
0x00 |
0x01 |
注意:實際上,變數authenticated在記憶體中儲存為0x01000000
(小端儲存),由偵錯程式自行反轉顯示以便閱讀。所以偏移量(相對左邊給出的記憶體地址的距離)從左至右依次為3、2、1、0。
0x03 溢位
下面嘗試輸入超過7個字元,看看越界資料能否寫入authenticated的資料區。輸入“zzzzzzzzyxw”(“w”、“x”、“y”、“z”的ASCII碼依次遞增1):
與預期一致,從第9個字元開始的資料被依次寫入authenticated的資料區,即authenticated的值變為了0x00777879
。
此時的棧幀資料分佈情況為:
區域性變數名 | 記憶體地址 | 偏移3處的值 | 偏移2處的值 | 偏移1處的值 | 偏移0處的值 |
---|---|---|---|---|---|
buffer[0~3] | 0x0065FA94 |
0x7A ('z') |
0x7A ('z') |
0x7A ('z') |
0x7A ('z') |
buffer[4~7] | 0x0065FA98 |
0x7A ('z') |
0x7A ('z') |
0x7A ('z') |
0x7A ('z') |
authenticated(被覆蓋前) | 0x0065FA9C |
0x00 |
0x00 |
0x00 |
0x01 |
authenticated(被覆蓋後) | 0x0065FA9C |
NULL | 0x77 ('w') |
0x78 ('x') |
0x79 ('y') |
現已知溢位資料確實能夠覆寫authenticated,只需要使authenticated為0就能通過程式驗證了。
輸入8個字元時,第9位的截斷符NULL(0x00
)將會被寫入記憶體0x0065FA9C
,即下一個雙字的低位位元組,恰好將authenticated從0x00000001
變為0x00000000
。
此時棧幀資料為:
區域性變數名 | 記憶體地址 | 偏移3處的值 | 偏移2處的值 | 偏移1處的值 | 偏移0處的值 |
---|---|---|---|---|---|
buffer[0~3] | 0x0065FA94 |
0x7A ('z') |
0x7A ('z') |
0x7A ('z') |
0x7A ('z') |
buffer[4~7] | 0x0065FA98 |
0x7A ('z') |
0x7A ('z') |
0x7A ('z') |
0x7A ('z') |
authenticated(被覆蓋前) | 0x0065FA9C |
0x00 |
0x00 |
0x00 |
0x01 |
authenticated(被覆蓋後) | 0x0065FA9C |
0x00 |
0x00 |
0x00 |
0x00 (NULL) |
結論就是非常簡單,輸入8位字元即可,讓截斷符去覆蓋0x01
。
0x04 補充
並不是所有的8位字串都可以繞過驗證。
如果輸入的字串按照ASCII編碼的序關係小於正確密碼“qazwsxe”,如“aaaaaaa”<“qazwsxe”,strcmp()函式會返回-1,此時authenticated儲存著補碼形式的-1,即0xFFFFFFFF
。這樣NULL覆寫後也只能變成0xFFFFFF00
。