2.2 修改鄰接變量
2.2.1 修改鄰接變量的原理
通過上一節,我們已經知道了函數調用的細節和棧中數據的分布情況。如圖2.1.8所示,函數的局部變量在棧中一個挨著一個排列。如果這些局部變量中有數組之類的緩沖區,并且程序中存在數組越界的缺陷,那么越界的數組元素就有可能破壞棧中相鄰變量的值,甚至破壞棧幀中所保存的EBP值、返回地址等重要數據。
題外話:大多數情況下,局部變量在棧中的分布是相鄰的,但也有可能出于編譯優化等需要而有所例外。具體情況我們需要在動態調試中具體對待,這里出于講述基本原理的目的,可以暫時認為局部變量在棧中是緊挨在一起的。
我們將用一個非常簡單的例子來說明破壞棧內局部變量對程序的安全性有何種影響。
#include <stdio.h> #define PASSWORD "1234567" int verify_password (char *password) { int authenticated; char buffer[8];// add local buffto be overflowed authenticated=strcmp(password,PASSWORD); strcpy(buffer,password);//over flowed here! return authenticated; } main() { int valid_flag=0; 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; } } }
上述代碼是第1章最后一節中Crack實驗的驗證程序修改而來的。請尤其注意以下兩處修改:
(1)verify_password()函數中的局部變量char buffer[8]的聲明位置。
(2)字符串比較之后的strcpy(buffer,password)。
這兩處修改實際上對程序的密碼驗證功能并沒有額外作用,這里加上它們只是為了人為制造一個棧溢出漏洞。
按照前面對系統棧工作原理的了解,我們不難想象出這段代碼執行到int verify_password(char *password)時的棧幀狀態如圖2.2.1所示。
題外話:這里只是給出了字符數組的緩沖區與局部變量authenticated在棧中的一種分布形式。出于編譯優化等目的,變量在棧中的存儲順序可能會有變化,需要在動態調試時具體問題具體分析。
可以看到,在verify_password 函數的棧幀中,局部變量int authenticated恰好位于緩沖區char buffer[8]的“下方”。

圖2.2.1 棧幀布局
authenticated為int類型,在內存中是一個DWORD,占4個字節。所以,如果能夠讓buffer數組越界,buffer[8]、buffer[9]、buffer[10]、buffer[11]將寫入相鄰的變量authenticated中。
觀察一下源代碼不難發現,authenticated變量的值來源于strcmp函數的返回值,之后會返回給main函數作為密碼驗證成功與否的標志變量:當authenticated為0時,表示驗證成功;反之,驗證不成功。
如果我們輸入的密碼超過了7個字符(注意:字符串截斷符NULL將占用一個字節),則越界字符的ASCII碼會修改掉authenticated的值。如果這段溢出數據恰好把authenticated改為0,則程序流程將被改變。本節實驗要做的就是研究怎樣用非法的超長密碼去修改buffer的鄰接變量authenticated從而繞過密碼驗證程序這樣一件有趣的事情。
2.2.2 突破密碼驗證程序
實驗環境要求如表2-2-1所示。
表2-2-1 實驗環境
說明:如果完全采用實驗指導所推薦的實驗環境,將精確地重現指導中所有的細節;否則需要根據具體情況重新調試。
請您在開始實驗前務必先確定實驗環境是否符合要求。
按照程序的設計思路,只有輸入了正確的密碼“1234567”之后才能通過驗證。程序運行情況如圖2.2.2所示。

圖2.2.2 程序正常運行時的情況
假如我們輸入的密碼為7個英文字母“q”,按照字符串的序關系“qqqqqqq”>“1234567”,strcmp應該返回1,即authenticated為1。OllyDbg動態調試的實際內存情況如圖2.2.3所示。

圖2.2.3 棧幀布局
也就是說,棧幀數據分布情況如表2-2-2所示。
表2-2-2 棧幀數據分布情況
在觀察內存的時候應當注意“內存數據”與“數值數據”的區別。在我們的調試環境中,內存由低到高分布,您可以簡單地把這種情形理解成Win32系統在內存中由低位向高位存儲一個4字節的雙字(DWORD),但在作為“數值”應用的時候,卻是按照由高位字節向低位字節進行解釋。這樣一來,在我們的調試環境中,“內存數據”中的DWORD和我們邏輯上使用的“數值數據”是按字節序逆序過的。
例如,變量authenticated在內存中存儲為0x 01 00 00 00,這個“內存數據”的雙字會被計算機由高位向低位按字節解釋成“數值數據”0x 00 00 00 01。出于便于閱讀的目的,OllyDbg在棧區顯示的時候已經將內存中雙字的字節序反轉了,也就是說,棧區欄顯示的是“數值數據”,而不是原始的“內存數據”,所以,在棧內看數據時,從左向右對于左邊地址的偏移依次為3、2、1、0。請您在實驗中注意這一細節。
下面我們試試輸入超過7個字符,看看超過buffer[8]邊界的數據能不能寫進authenticated變量的數據區。為了便于區分溢出的數據,這次我們輸入的密碼為“qqqqqqqqrst”(‘q’、‘r’、‘s’、‘t’的ASCII碼相差1),結果如圖2.2.4所示。

圖2.2.4 覆蓋鄰接變量
棧中的情況和我們分析的一樣,從輸入的第9個字符開始,將依次寫入authenticated變量。按照我們的輸入“qqqqqqqqrst”,最終authenticated的值應該是字符‘r’、‘s’、‘t’和用于截斷字符串的null所對應的ASCII碼0x00747372。
這時的棧幀數據如表2-2-3所示。
表2-2-3 棧幀數據
authenticated變量的值來源于strcmp函數的返回值,之后會返回給main函數作為密碼驗證成功與否的標志變量。當authenticated為0時,表示驗證成功;反之,驗證不成功。
我們已經知道越過數組buffer[8]的邊界的后續數據可以改寫變量authenticated,那么如果我們用這段溢出數據恰好把authenticated改為0,是不是就可以直接通過驗證了呢?
字符串數據最后都有作為結束標志的NULL(0),當我們輸入8個‘q’的時候,按照前邊的分析,buffer所擁有的8個字節將全部被‘q’的ASCII碼0x71填滿,而字符串的第9個字符——作為結尾的NULL將剛好寫入內存0x0012FB20處,即下一個雙字的低位字節,恰好將authenticated從0x 00 00 00 01改成 0x 00 00 00 00,如圖2.2.5所示。

圖2.2.5 修改鄰接變量
這時系統棧內的變化過程如表2-2-4所示。
表2-2-4 棧幀數據
經過上述分析和動態調試,我們知道即使不知道正確的密碼“1234567”,只要輸入一個為8個字符的字符串,那么字符串中隱藏的第9個截斷符NULL就應該能夠將authenticated低字節中的1覆蓋成0,從而繞過驗證程序!修改鄰接變量成功的界面如圖2.2.6所示。

圖2.2.6 修改鄰接變量成功
題外話:嚴格說來,并不是任何8個字符的字符串都能沖破上述驗證程序。由代碼中的authenticated=strcmp(password,PASSWORD),我們知道authenticated的值來源于字符串比較函數strcmp的返回值。按照字符串的序關系,當輸入的字符串大于“1234567”時,返回1,這時authenticated在內存中的值為0x00000001,可以用字串的截斷符NULL淹沒authenticated的低位字節而突破驗證;當輸入字符串小于“1234567”時(例如,“0123”等字符串),函數返回-1,這時authenticated在內存中的值按照雙字-1的補碼存放,為0xFFFFFFFF,如果這時也輸入8個字符的字符串,截斷符淹沒authenticated低字節后,其值變為0xFFFFFF00,所以這時是不能沖破驗證程序的。圖2.2.6所示的“01234567”輸入就屬于這種情形。如果您感興趣,可以嘗試進一步調試研究這種情況。