1. 程式人生 > 其它 >棧溢位——鄰接變數

棧溢位——鄰接變數

修改鄰接變數

一般而言,區域性變數在棧中的分佈是相鄰的(但也可能出於編譯優化等需要而出現例外)。如果這些區域性變數中有陣列之類的緩衝區,並且程式確實沒有防護陣列越界,那麼越界的陣列元素就有可能破壞棧中相鄰變數的值,甚至破壞棧幀中所儲存的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