WebServer端口重定向后門(mén)的研究
前段時(shí)間有朋友問(wèn)到了我一個(gè)關(guān)于“無(wú)端口可用”的問(wèn)題。說(shuō)在如下圖所示的內(nèi)網(wǎng)環(huán)境中,firewall只允許Web Server的80端口建立網(wǎng)絡(luò)連接,并且Web Server上的80端口已經(jīng)被IIS、Apache等軟件占用了的情況下,怎么建立一個(gè)RAT后門(mén)。

早些時(shí)候的著名后門(mén)byshell就考慮到了這個(gè)問(wèn)題,于是使用了一種非常挫的方式去解決??蛻舳藢?shù)據(jù)發(fā)送到80端口,服務(wù)端將IIS的進(jìn)程打開(kāi),循環(huán)遍歷IIS進(jìn)程的整個(gè)內(nèi)存去尋找數(shù)據(jù)標(biāo)記。顯然這種方法從效率和穩(wěn)定性上來(lái)講,都是不可取的。這里姑且不把這種方法當(dāng)做方法。
過(guò)去有人提出過(guò)一種端口復(fù)用的方法去建立的后門(mén),這個(gè)方法使用了setsockopt()這個(gè)API,這個(gè)API在MSDN里說(shuō)是用來(lái)設(shè)置套接字的選項(xiàng)的。原型如下。
view source
print?1.int setsockopt(
2. SOCKET s,
3. int level,
4. int optname,
5. const char FAR* optval,
6. int optlen
7.);
我們這里只關(guān)心它的第三個(gè)參數(shù),這個(gè)參數(shù)用來(lái)設(shè)置套接字狀態(tài)。這個(gè)參數(shù)有一個(gè)取值為SO_REUSEADDR,MSDN對(duì)這個(gè)參數(shù)的解釋如下。
The state of the SO_REUSEADDR socket option determines whether the local transport address to which a socket will be bound is always shared with other sockets. This socket option applies only to listening sockets, datagram sockets, and connection-oriented sockets.
也就是說(shuō),當(dāng)?shù)谌齻€(gè)參數(shù)的取值設(shè)置為SO_REUSEADDR時(shí),套接字的端口是可以共享復(fù)用的。具體共享細(xì)節(jié)為后來(lái)居上,后建立該參數(shù)鏈接的套接字先拿到數(shù)據(jù)。此方法目前對(duì)Apache和IIS5.0及以下版本有效。那為什么IIS6.0及以上就不行了呢?之后會(huì)做解釋。
通過(guò)逆向和查閱開(kāi)源代碼可以得知,Apache和IIS5.0及以下版本使用了應(yīng)用層的IOCP模型進(jìn)行通信,盡管框架比較復(fù)雜,但是依然在應(yīng)用層創(chuàng)建了套接字。到這里你是否有新的想法呢?沒(méi)錯(cuò),我想到了可以使用遠(yuǎn)程線程注入一個(gè)DLL進(jìn)行Api Hook例如WSARecv()、WSASend()這樣的API獲取套接字和異步IO的緩沖區(qū)指針,再使用getpeername()函數(shù)對(duì)比客戶端信息,緊接著用套接字進(jìn)行IO。
或者也可以使用更簡(jiǎn)單粗暴的方法,直接使用SPI安裝一個(gè)LSP,也可以抓到數(shù)據(jù),但是就比較難再做通信了。我們將這種在應(yīng)用層建立套接字通信轉(zhuǎn)接通信過(guò)程的方法總結(jié)為下圖,紅線表示可以利用的地方。

這里可能就有同學(xué)會(huì)問(wèn)到,為什么不能直接上Rootkit呢?如果從ring0層去考慮這個(gè)問(wèn)題理論上是非常容易的。我們可以使用TDI或者NDIS的過(guò)濾驅(qū)動(dòng)直接過(guò)濾所有IO網(wǎng)卡的流量,不過(guò)寫(xiě)個(gè)這么重量級(jí)的后門(mén),的確有“殺雞焉用宰牛刀”的意味。再者,也可以使用SSDT HOOK和對(duì)TCP驅(qū)動(dòng)下IRP HOOK去解決問(wèn)題。但是為什么不去做呢?因?yàn)槭褂抿?qū)動(dòng)目前已經(jīng)不是Windows木馬編程潮流所在。
還記得之前提到的IIS6.0和以上版本的問(wèn)題嗎?這里一個(gè)新問(wèn)題出現(xiàn)了,從IIS6.0開(kāi)始,微軟可能考慮到了安全性和穩(wěn)定性以及數(shù)據(jù)處理的效率的問(wèn)題,將網(wǎng)絡(luò)通信的過(guò)程封裝在了ring0層,使用了http.sys這個(gè)驅(qū)動(dòng)來(lái)直接進(jìn)行網(wǎng)絡(luò)通信。如下圖所示。
這樣一來(lái),應(yīng)用層就沒(méi)有了套接字,我們就不能使用上述方法去解決這個(gè)問(wèn)題了。那怎么辦呢?難道應(yīng)用層因此就再?zèng)]有可以利用的地方了嗎?這么想并不符合Geek的風(fēng)格。于是,經(jīng)過(guò)對(duì)W3wp.exe的初步的逆向,又發(fā)現(xiàn)了一些可以利用的地方。

這個(gè)過(guò)程中,ring0無(wú)法處理的HTTP請(qǐng)求,都下發(fā)由w3wp.exe進(jìn)程處理。針對(duì)HTTP請(qǐng)求考慮,我想到了如下幾點(diǎn)可以轉(zhuǎn)接網(wǎng)絡(luò)通信過(guò)程的地方。
1.GET或者POST對(duì)硬盤(pán)中的文件做訪問(wèn),有可能需要通過(guò)應(yīng)用層CreateFile()、ReadFile()、WriteFile()去完成。(API HOOK?)
2.對(duì)于HTTP這種大部分內(nèi)容由ANSI字符串解析的協(xié)議,難道用不到標(biāo)準(zhǔn)字符串處理函數(shù)嗎?(還是API HOOK?)
3.對(duì)ASP、PHP、JSP腳本的解釋?zhuān)赡苄枰獙?shù)據(jù)提交給解釋引擎去完成。(inline hook?)
基于以上幾點(diǎn),我也一一做了驗(yàn)證。這里逆向要注意了,OllyDbg對(duì)低權(quán)限進(jìn)程的Attach調(diào)試穩(wěn)定性并不好,經(jīng)常崩掉。所以我換了非常難看的Windbg。微軟自家的調(diào)試器調(diào)自家的程序非常穩(wěn)定。首先bp了CreateFileW(),得到了一個(gè)比較驚喜的效果。如圖所示。

Windbg攔下了CreateFileW(),并且在第一個(gè)參數(shù)里,我們看到了我們提交的“fuckyou1234”,這里我們就完全可以用if(wcsncmp(s1,L”fuckyou1234”,11))這樣取得后門(mén)命令了。
而我通過(guò)對(duì)多個(gè)字符串處理函數(shù)的bp,同樣也發(fā)現(xiàn)了可以利用的地方。例如wcsstr()函數(shù)的斷點(diǎn),成功的抓到了cookies和一些其他與HTTP協(xié)議相關(guān)的信息。cookies可以比URL提交跟多內(nèi)容,并且可以繞過(guò)一些log和過(guò)濾。如下圖所示。

這是對(duì)前兩個(gè)問(wèn)題逆向的初步成果,至于第三個(gè)問(wèn)題,暫時(shí)先留著好了。現(xiàn)在遇到了一個(gè)新的問(wèn)題,當(dāng)拿到需要執(zhí)行的命令,我們至少不能讓他去w3wp.exe這個(gè)低權(quán)限的進(jìn)程去實(shí)施。那么怎么讓他在高權(quán)限的進(jìn)程去實(shí)施呢?還有,我們執(zhí)行完的結(jié)果,該怎么返回給客戶端呢?
這里我考慮到了留一個(gè)宿主進(jìn)程,建立事件對(duì)象和郵槽或者管道接受由w3wp.exe發(fā)來(lái)的命令。至于把數(shù)據(jù)反饋到客戶端,我們可以重定向CreateFileW()的第一個(gè)參數(shù),將他指向管道也好,指向某個(gè)硬盤(pán)里的輸出文件也好,當(dāng)緊接著去ReadFile()的時(shí)候,就可以順利將我們的執(zhí)行結(jié)果反饋到客戶端去了。
這里涉及到高低權(quán)限進(jìn)程通信的一個(gè)問(wèn)題,高權(quán)限進(jìn)程創(chuàng)建的內(nèi)核對(duì)象必須設(shè)置安全屬性為低權(quán)限可繼承句柄,并且設(shè)置安全描述符和DACL。事件、郵槽、管道的等內(nèi)核對(duì)象才能被低權(quán)限進(jìn)程打開(kāi)。具體代碼如下。最后,我用上述所訴內(nèi)容,寫(xiě)了如下的一個(gè)后門(mén)。
view source
print?01.HANDLE secCreateEventPort(WCHAR* szNameEvent)
02.{
03. SECURITY_DESCRIPTOR SecDescriptor = {0};
04. SECURITY_ATTRIBUTES SecurityAttributes = {0};
05. if (InitializeSecurityDescriptor(&SecDescriptor,SECURITY_DESCRIPTOR_REVISION) == FALSE)
06. return INVALID_HANDLE_VALUE;
07. if(SetSecurityDescriptorDacl(&SecDescriptor,TRUE, NULL, FALSE) == 0)
08. return INVALID_HANDLE_VALUE;
09. SecurityAttributes.bInheritHandle = TRUE;
10. SecurityAttributes.lpSecurityDescriptor = &SecDescriptor;
11. SecurityAttributes.nLength = sizeof(SECURITY_ATTRIBUTES);
12. return CreateEvent(&SecurityAttributes,TRUE,FALSE,szNameEvent);
13.}
