奇怪的函數(shù)調(diào)用
早期文章
整理移動(dòng)硬盤(pán)時(shí),發(fā)現(xiàn)一個(gè)名為 attack 的目錄,進(jìn)去以后發(fā)現(xiàn)原來(lái)是一段簡(jiǎn)單的 C 語(yǔ)言代碼。代碼如下:
void Attack(){while (1){printf("Attack...\r\n");}getchar();}int main(int argc, char* argv[]){int arr[5] = { 0 };arr[7] = (int)Attack;return 0;}
看代碼猜測(cè),應(yīng)該是死循環(huán)輸出 Attack 字符串,因?yàn)楫吘故菙?shù)組的越界訪(fǎng)問(wèn)(很多一些演示棧溢出的程序,都會(huì)用到數(shù)組的越界訪(fǎng)問(wèn)、字符串的拷貝等)。直接打開(kāi) VS 2015 進(jìn)行編譯、連接、運(yùn)行,發(fā)現(xiàn)運(yùn)行后什么結(jié)果都沒(méi)有輸出。當(dāng)然了,這應(yīng)該是被 VS 2015 的編譯連接選項(xiàng)所導(dǎo)致的。進(jìn)行一番設(shè)置,然后再進(jìn)行執(zhí)行。果然是死循環(huán)輸出 Attack 字符串。
設(shè)置編譯連接選項(xiàng)
類(lèi)似這樣的程序,在之前 VC 6 的編譯環(huán)境下比較簡(jiǎn)單,到了高版本的 VS 下就需要設(shè)置相應(yīng)的項(xiàng)目、編譯、連接選項(xiàng)了,否則默認(rèn)的安全選項(xiàng)會(huì)導(dǎo)致測(cè)試失敗。不過(guò)好在這樣的選項(xiàng)不多。這里逐一來(lái)進(jìn)行設(shè)置。
在項(xiàng)目名上點(diǎn)擊鼠標(biāo)右鍵,在彈出的對(duì)話(huà)框上選擇“屬性”。
在“屬性頁(yè)”的“常規(guī)”選項(xiàng)中將字符集設(shè)置為“未設(shè)置”,如下圖所示。

在 C/C++ 的“代碼生成”選項(xiàng)中,將“安全檢查”設(shè)置為“禁用安全檢查(/GS-)”,如下圖所示。

設(shè)置“連接器”下的“高級(jí)”選項(xiàng),“隨機(jī)地址”設(shè)置為“否”,“數(shù)據(jù)執(zhí)行保護(hù)(DEP)”設(shè)置為“否”,如下圖所示。

再次進(jìn)行編譯運(yùn)行,發(fā)現(xiàn)死循環(huán)的測(cè)試成功了。如下圖所示。注意觀(guān)察右側(cè)的滾動(dòng)條,往下滾動(dòng)速度很快。

原因分析
為什么會(huì)產(chǎn)生這樣的顯現(xiàn)呢?原因就是數(shù)組越界的賦值,代碼如下:
arr[7] = (int)Attack; 在 C 語(yǔ)言中,函數(shù)名的名稱(chēng)就是函數(shù)的首地址。上面的賦值語(yǔ)句是將 arr[7] 的位置賦值為了 Attack 函數(shù)的地址。而 arr[7] 又是何物呢?在了解 arr[7] 之前,需要了解的是函數(shù)調(diào)用與函數(shù)的棧幀。
C 語(yǔ)言在調(diào)用函數(shù)時(shí),根據(jù)函數(shù)的調(diào)用約定(C 語(yǔ)言的調(diào)用約定為 _cdcel)先將參數(shù)從右至左依次入棧,然后將返回地址壓入棧中。當(dāng)進(jìn)入被調(diào)用的函數(shù)后,會(huì)先將 EBP 寄存器入棧,然后將 ESP 寄存器賦值給 EBP,最后通過(guò) sub esp 來(lái)抬高棧頂,當(dāng)作被調(diào)用函數(shù)的??臻g。EBP 作為基址指針,對(duì)當(dāng)前函數(shù)(被調(diào)用函數(shù))中的局部變量通過(guò) [EBP - 0xXXX] 來(lái)進(jìn)行訪(fǎng)問(wèn),而對(duì)于調(diào)用時(shí)棧中的參數(shù),則通過(guò) [EBP + 0xXXX] 來(lái)進(jìn)行訪(fǎng)問(wèn)。通常,[EBP + 4] 是返回地址,[EBP + 8] 是第一個(gè)參數(shù)的(如果有參數(shù)的話(huà))。當(dāng)函數(shù)返回時(shí),通過(guò) add esp 來(lái)收回??臻g,然后在執(zhí)行 retn 指令時(shí),會(huì)把棧中的保存的返回地址賦值給 EIP 寄存器,然后從返回地址繼續(xù)執(zhí)行代碼。
有了上面的知識(shí)點(diǎn),我們來(lái)看一下,上面程序的反匯編代碼,代碼如下:
004117F0 55 PUSH EBP004117F1 8BEC MOV EBP,ESP004117F3 81EC DC000000 SUB ESP,0DC004117F9 53 PUSH EBX004117FA 56 PUSH ESI004117FB 57 PUSH EDI004117FC 8DBD 24FFFFFF LEA EDI,DWORD PTR SS:[EBP-DC]00411802 B9 37000000 MOV ECX,3700411807 B8 CCCCCCCC MOV EAX,CCCCCCCC0041180C F3:AB REP STOS DWORD PTR ES:[EDI]0041180E C745 E8 0000000>MOV DWORD PTR SS:[EBP-18],000411815 33C0 XOR EAX,EAX00411817 8945 EC MOV DWORD PTR SS:[EBP-14],EAX0041181A 8945 F0 MOV DWORD PTR SS:[EBP-10],EAX0041181D 8945 F4 MOV DWORD PTR SS:[EBP-C],EAX00411820 8945 F8 MOV DWORD PTR SS:[EBP-8],EAX00411823 B8 04000000 MOV EAX,400411828 6BC8 07 IMUL ECX,EAX,70041182B C7440D E8 5A104>MOV DWORD PTR SS:[EBP+ECX-18],test.0041105A00411833 33C0 XOR EAX,EAX00411835 52 PUSH EDX00411836 8BCD MOV ECX,EBP00411838 50 PUSH EAX00411839 8D15 50184100 LEA EDX,DWORD PTR DS:[411850]0041183F E8 19FAFFFF CALL test.0041125D00411844 58 POP EAX00411845 5A POP EDX00411846 5F POP EDI00411847 5E POP ESI00411848 5B POP EBX00411849 8BE5 MOV ESP,EBP0041184B 5D POP EBP0041184C C3 RETN
以上反匯編代碼來(lái)自 OD,如下圖所示。

在上面 0041180E 到 0041181D 的位置處,是對(duì) arr 數(shù)組進(jìn)行初始化的過(guò)程。對(duì)應(yīng)代碼的如下:
int arr[5] = { 0 }; 可以看到,C 語(yǔ)言的一行代碼,對(duì)應(yīng)到匯編就有好多行。在 0041182B 處也是一行賦值語(yǔ)句,代碼如下:
MOV DWORD PTR SS:[EBP+ECX-18],test.0041105A EBP + ECX - 18,此處 ECX 的值為 1C,1C - 18 = 4,那么此處相當(dāng)于是如下代碼:
MOV DWORD PTR SS:[EBP + 4], test.0041105A回顧我們前面提到的,[EBP + 4] 的位置處保存著返回地址,也就是調(diào)用當(dāng)前函數(shù)的函數(shù)的下一條指令。比如,A 函數(shù)中調(diào)用了 B 函數(shù),當(dāng) B 函數(shù)執(zhí)行完成后,會(huì)接著執(zhí)行 A 函數(shù)中,調(diào)用 B 函數(shù)處的下一條指令。而此時(shí),返回地址被覆蓋為 0041105A,那么,這個(gè) 0041105A 是什么值?回顧上面的代碼,代碼如下:
arr[7] = (int)Attack; 0041105A 是 Attack 函數(shù)的首地址。那么當(dāng) main 函數(shù)返回時(shí),相當(dāng)于調(diào)用了 Attack 函數(shù)。而 Attack 函數(shù)中是一個(gè)死循環(huán)。
觀(guān)察內(nèi)存變化
看一下代碼執(zhí)行到 0041180E 時(shí) ebp 的情況,如下圖所示。

此時(shí),可以看到 [ebp + 4] 中的值是 00411FCE,然后再觀(guān)察 [ebp - 18] 到 [ebp - 8] 內(nèi)存中的值都為 cc。然后,將代碼執(zhí)行到 00411823 處,觀(guān)察 ebp 的情況,如下圖所示。

可以看到 [ebp - 18] 到 [ebp - 8] 的??臻g都被初始化為了 0。接著繼續(xù)執(zhí)行代碼,到 00411833 的地址處,再次觀(guān)察 ebp 的情況,如下圖所示。

可以看到,[ebp + 4] 的棧地址處的值被修改了,接著將代碼執(zhí)行向下執(zhí)行,執(zhí)行到 0041184C 后,也就是執(zhí)行完 retn 后觀(guān)察 EIP 寄存器的值,如下圖所示。

可以看到,此時(shí) EIP 的值為 0041105A,而反匯編代碼處是一個(gè)跳表的位置。在當(dāng)前位置接著在單步一下,如下圖所示。

從圖中可以看到,在注釋位置有一個(gè)“attack...”字符串的提示,從這點(diǎn)就可以看出,該段反匯編代碼是 Attack() 函數(shù)了。
到此,整個(gè)程序的執(zhí)行就說(shuō)清楚了。
總結(jié)
這種程序雖小,但是考察的是對(duì)函數(shù)調(diào)用時(shí)內(nèi)存結(jié)構(gòu)相關(guān)的知識(shí)。雖然簡(jiǎn)單的,但還是很有意思的。
更多文章
