1.4 編寫簡(jiǎn)易ShellCode彈窗
在前面的章節(jié)中相信讀者已經(jīng)學(xué)會(huì)了使用Metasploit
工具生成自己的ShellCode
代碼片段了,本章將繼續(xù)深入探索關(guān)于ShellCode
的相關(guān)知識(shí)體系,ShellCode 通常是指一個(gè)原始的可執(zhí)行代碼的有效載荷,攻擊者通常會(huì)使用這段代碼來獲得被攻陷系統(tǒng)上的交互Shell的訪問權(quán)限,而現(xiàn)在用于描述一段自包含的獨(dú)立的可執(zhí)行代碼片段。ShellCode代碼的編寫有多種方式,通常會(huì)優(yōu)先使用匯編語言實(shí)現(xiàn),這得益于匯編語言的可控性。
ShellCode 通常會(huì)與漏洞利用并肩使用,或是被惡意代碼用于執(zhí)行進(jìn)程代碼的注入,通常情況下ShellCode
代碼無法獨(dú)立運(yùn)行,必須依賴于父進(jìn)程或是Windows
文件加載器的加載才能夠被運(yùn)行,本章將通過一個(gè)簡(jiǎn)單的彈窗(MessageBox)來實(shí)現(xiàn)一個(gè)簡(jiǎn)易版的彈窗功能,并以此來加深讀者對(duì)匯編語言的理解。
1.4.1 尋找DLL庫函數(shù)地址
在編寫ShellCode
之前,我們需要查找一個(gè)函數(shù)地址,由于我們需要調(diào)用MessageBoxA()
這個(gè)函數(shù),所以需要獲取該函數(shù)的內(nèi)存動(dòng)態(tài)地址,根據(jù)微軟的官方定義可知,該函數(shù)默認(rèn)放在了User32.dll
庫中,為了能夠了解壓棧時(shí)需要傳入?yún)?shù)的類型,我們還需要查詢一下函數(shù)的原型;
在微軟定義中MessageBoxA
函數(shù)的原型如下:
int?MessageBoxA(
??HWND?hWnd,
??LPCSTR?lpText,
??LPCSTR?lpCaption,
??UINT?uType
);
參數(shù)說明:
??hWnd:消息框的父窗口句柄。
??lpText:消息框中顯示的文本。
??lpCaption:消息框的標(biāo)題欄文本。
??uType:消息框的類型,可以指定消息框包含的按鈕以及圖標(biāo)等。
需要注意的是,由于我們調(diào)用的是MessageBoxA
,而此函數(shù)為ASCII模式,需要讀者自行修改解決方案,在配置屬性的常規(guī)選項(xiàng)卡,修改字符集(使用多字節(jié)字符集)即可,如下圖所示;

讀者可以通過編寫一段簡(jiǎn)單的代碼來獲取所需數(shù)據(jù),首先通過LoadLibrary
函數(shù)加載名為user32.dll
的動(dòng)態(tài)鏈接庫,并將其基地址存儲(chǔ)在HINSTANCE
類型的變量LibAddr
中。然后,使用GetProcAddress
函數(shù)獲取?MessageBoxA
函數(shù)的地址,并將其存儲(chǔ)在MYPROC
類型的變量ProcAddr
中。最后輸出所需結(jié)果;
typedef?void(*MYPROC)(LPTSTR);
int?main(int?argc,?char?*argv[])
{
????HINSTANCE?LibAddr,KernelAddr;
????MYPROC?ProcAddr;
????//?獲取User32.dll基地址
????LibAddr?=?LoadLibrary("user32.dll");
????printf("user32.dll?動(dòng)態(tài)庫基地址?=?0x%x?\n",?LibAddr);
????//?獲取kernel32.dll基地址
????KernelAddr?=?LoadLibrary("kernel32.dll");
????printf("kernel32.dll?動(dòng)態(tài)庫基地址?=?0x%x?\n",?KernelAddr);
????//?獲取MessageBox基地址
????ProcAddr?=?(MYPROC)GetProcAddress(LibAddr,?"MessageBoxA");
????printf("MessageBoxA?函數(shù)相對(duì)地址?=?0x%x?\n",?ProcAddr);
????//?獲取ExitProcess基地址
????ProcAddr?=?(MYPROC)GetProcAddress(KernelAddr,?"ExitProcess");
????printf("ExitProcess?函數(shù)相對(duì)地址?=?0x%x?\n",?ProcAddr);
????system("pause");
????return?0;
}
上方的代碼經(jīng)過編譯運(yùn)行后會(huì)得到兩個(gè)返回結(jié)果,如下圖所示,其中User32.dll
的基地址是0x75a40000
而該模塊內(nèi)的MessageBoxA
函數(shù)在當(dāng)前系統(tǒng)中的地址為0x75ac0ba0
,當(dāng)然這兩個(gè)模塊地址在每次系統(tǒng)啟動(dòng)時(shí)都會(huì)發(fā)生幻化,讀者電腦中的地址肯定與筆者不相同,這都是正?,F(xiàn)象,之所以會(huì)出現(xiàn)這種情況是因?yàn)?,系統(tǒng)中存在一種ASLR機(jī)制。
擴(kuò)展知識(shí):ASLR(Address Space Layout Randomization)機(jī)制的核心是用于隨機(jī)化系統(tǒng)中程序和數(shù)據(jù)的內(nèi)存地址分布,從而增加攻擊者攻擊系統(tǒng)的難度,在啟用了ASLR機(jī)制的系統(tǒng)下,每次運(yùn)行程序時(shí),程序和系統(tǒng)組件(例如DLL、驅(qū)動(dòng)程序等)都會(huì)被分配不同的內(nèi)存地址,而不是固定的內(nèi)存地址。這樣可以使得攻擊者難以利用已知的內(nèi)存地址漏洞進(jìn)行攻擊,因?yàn)楣粽咝枰日业秸_的內(nèi)存地址才能利用漏洞。ASLR的隨機(jī)化是根據(jù)操作系統(tǒng)的一些隨機(jī)因素進(jìn)行計(jì)算的,例如啟動(dòng)時(shí)間、進(jìn)程 ID 等等。
由于如上機(jī)制的存在,導(dǎo)致user32.dll
模塊地址不確定,也就會(huì)導(dǎo)致其地址內(nèi)部的API函數(shù)地址也會(huì)發(fā)生一定的變化,下圖僅作為參考圖;

在獲取到MessageBoxA
函數(shù)的內(nèi)存地址以后,我們接著需要獲取一個(gè)ExitProecess
函數(shù)的地址,這個(gè)API函數(shù)的作用是讓程序正常退出,這是因?yàn)槲覀冏⑷氪a以后,原始的堆棧地址會(huì)被破壞,堆棧失衡后會(huì)導(dǎo)致程序崩潰,所以為了穩(wěn)妥起見我們還是添加一行正常退出為好。函數(shù)ExitProcess
的原型如下:
VOID?WINAPI?ExitProcess(
??UINT?uExitCode
);
其中參數(shù)uExitCode
指定了進(jìn)程的退出代碼,表示進(jìn)程成功退出或者發(fā)生了錯(cuò)誤。如果uExitCode
為0,表示進(jìn)程成功退出,其他的非0值則表示進(jìn)程發(fā)生了錯(cuò)誤,不同的非0值可以用于表示不同的錯(cuò)誤類型。
1.4.2 探討STDCALL調(diào)用約定
既然獲取到了相應(yīng)的內(nèi)存地址,那么接下來就需要通過匯編來編寫可執(zhí)行代碼片段了,在編寫這段代碼之前,先來了解一下匯編語言的調(diào)用約定,在匯編語言中,要想調(diào)用某個(gè)函數(shù),需要使用CALL語句,而在CALL語句的后面,要跟上該函數(shù)在系統(tǒng)中的地址,前面我們已經(jīng)獲取到了相應(yīng)的內(nèi)存地址了,所以在這里就可以通過CALL相應(yīng)的地址來調(diào)用相應(yīng)的函數(shù)。
我們以32位應(yīng)用程序?yàn)槔?,?2位應(yīng)用程序內(nèi)通常使用STDCALL
調(diào)用約定,它定義了函數(shù)在被調(diào)用時(shí),參數(shù)傳遞、返回值傳遞以及棧的使用等方面的規(guī)則,該調(diào)用約定的規(guī)則如下所示:
??參數(shù)傳遞:參數(shù)從右向左依次壓入棧中,由被調(diào)用者在返回前清理?xiàng)!?/p>
??返回值傳遞:函數(shù)返回時(shí)將返回值存儲(chǔ)在EAX寄存器中。
??棧的使用:函數(shù)被調(diào)用前,調(diào)用者將參數(shù)壓入棧中;被調(diào)用者在返回前清理?xiàng)?,以確保棧的平衡。
??函數(shù)調(diào)用:在調(diào)用函數(shù)之前,調(diào)用者將返回地址(Return Address)和EBP寄存器的值保存在棧中,并將ESP寄存器指向參數(shù)列表的最后一個(gè)元素;在函數(shù)返回之后,調(diào)用者通過將之前保存的EBP和返回地址彈出棧中,并將ESP寄存器恢復(fù)到最初的位置來恢復(fù)棧的狀態(tài)。
總之,stdcall調(diào)用約定將參數(shù)按照從右到左的順序壓入棧中,由被調(diào)用者清理?xiàng)?,返回值存?chǔ)在EAX寄存器中,函數(shù)調(diào)用者和被調(diào)用者都需要遵循一定的棧使用規(guī)則。這種約定的好處是參數(shù)傳遞簡(jiǎn)單,可讀性高,并且在函數(shù)返回時(shí)棧已經(jīng)被清理,不需要額外的清理工作。
在實(shí)際的編程中,一般還是先將地址賦值給eax
寄存器,然后再CALL
調(diào)用相應(yīng)的寄存器實(shí)現(xiàn)調(diào)用,比如現(xiàn)在筆者有一個(gè)lyshark(a,b,c,d)
函數(shù),如果我們想要調(diào)用它,那么它的匯編代碼就應(yīng)該編寫為:
push?d
push?c
push?b
push?a
mov?eax,AddressOflyshark????//?獲取偏移地址
call?eax????????????????????//?間接調(diào)用
根據(jù)上方的調(diào)用方式,我們可以寫出ExitProcess()
函數(shù)的匯編版調(diào)用結(jié)構(gòu),如下;
xor?ebx,?ebx
push?ebx
mov?eax,?0x76c84100
call?eax
接著編寫MessageBox()
這個(gè)函數(shù)調(diào)用。與ExitProcess()
函數(shù)不同的是,這個(gè)API函數(shù)包含有四個(gè)參數(shù),當(dāng)然第一和第四個(gè)參數(shù),我們可以賦給0值,但是中間兩個(gè)參數(shù)都包含有較長(zhǎng)的字符串,這個(gè)該如何解決呢?我們不妨先把所需要用到的字符串轉(zhuǎn)換為ASCII碼值,轉(zhuǎn)換的方式有許多,如下代碼則是通過Python實(shí)現(xiàn)的轉(zhuǎn)換模式;
import?os,sys
from?LyScript32?import?MyDebug
#?字符串轉(zhuǎn)ascii
def?StringToAscii(string):
????ref?=?[]
????for?index?in?range(0,len(string)):
????????hex_str?=?str(hex(ord(string[index])))
????????ref.append(hex_str.replace("0x","\\x"))
????return?ref
if?__name__?==?"__main__":
????#?輸出MsgBox標(biāo)題
????title?=?StringToAscii("alert")
????for?index?in?range(0,len(title)):
????????print(title[index],end="")
????print()
????#?輸出MsgBox內(nèi)容
????box?=?StringToAscii("hello?lyshark")
????for?index?in?range(0,len(box)):
????????print(box[index],end="")
當(dāng)Python
程序被運(yùn)行,則用戶即可得到兩串通過編碼后的字符串?dāng)?shù)據(jù)。
MsgBox標(biāo)題:alert??????????????\x61\x6c\x65\x72\x74\x21
MsgBox內(nèi)容:hello?lyshark??????\x68\x65\x6c\x6c\x6f\x20\x6c\x79\x73\x68\x61\x72\x6b
由于我們使用的是32位匯編,所以上方的字符串需要做一定的處理,我們分別將每四個(gè)字符為一組,進(jìn)行分組,將不滿四個(gè)字符的,以空格0x20
進(jìn)行填充,這是因?yàn)槲覀儾捎玫拇鎯?chǔ)字符串模式為棧傳遞,而一個(gè)寄存器為32位,所以就需要填充滿4字節(jié)才可以平衡;
-------------------------------------------------------------
填充?alert
-------------------------------------------------------------
\x61\x6c\x65\x72
\x74\x21\x20\x20
-------------------------------------------------------------
填充?hello?lyshark
-------------------------------------------------------------
\x68\x65\x6c\x6c
\x6f\x20\x6c\x79
\x73\x68\x61\x72
\x6b\x20\x20\x20
上方的空位置之所以需要以0x20
進(jìn)行填充,而不是0x00
進(jìn)行填充,是因?yàn)?code>strcpy這個(gè)字符串拷貝函數(shù),默認(rèn)只要一遇到0x00
就會(huì)認(rèn)為我們的字符串結(jié)束了,就不會(huì)再拷貝0x00
后的內(nèi)容了,所以這里就不能使用0x00
進(jìn)行填充了,這里要特別留意一下。
接著我們需要將這兩段字符串分別壓入堆棧存儲(chǔ),這里需要注意,由于我們的計(jì)算機(jī)是小端序
排列的,因此字符的入棧順序是從后往前不斷進(jìn)棧的,上面的字符串壓棧參數(shù)應(yīng)該寫為:
小提示:小端序(Little Endian)是一種數(shù)據(jù)存儲(chǔ)方式,在匯編語言中,小端序的表示方式與高位字節(jié)優(yōu)先(Big Endian)相反。例如,對(duì)于一個(gè)16位的整數(shù)0x1234,它在小端序的存儲(chǔ)方式下,將會(huì)被存儲(chǔ)為0x340x12(低位字節(jié)先存儲(chǔ));而在高位字節(jié)優(yōu)先的存儲(chǔ)方式下,將會(huì)被存儲(chǔ)為0x120x34(高位字節(jié)先存儲(chǔ))。
-------------------------------------------------------------
壓入字符串?alert
-------------------------------------------------------------
push?0x20202174
push?0x72656c61
-------------------------------------------------------------
壓入字符串?hello?lyshark
-------------------------------------------------------------
push?0x2020206b
push?0x72616873
push?0x796c206f
push?0x6c6c6568
既然字符串壓入堆棧的功能有了,那么下面問題來了,我們?nèi)绾潍@取這兩個(gè)字符串的地址,從而讓其成為MessageBox()
的參數(shù)呢?
其實(shí)這個(gè)問題也不難,我們可以利用esp
指針,因?yàn)樗冀K指向的是棧頂?shù)奈恢?,我們將字符壓入堆棧后,棧頂位置就是我們所壓入的字符的位置,于是在每次字符壓棧后,可以加入如下指令,依次將第一個(gè)字符串基地址保存至eax
寄存器中,將第二個(gè)基地址保存至ecx
寄存器中。
xor?ebx,ebx?????????????????//?清空寄存器
push?0x20202174?????????????//?字符串?alert?
push?0x72656c61
mov?eax,esp?????????????????//?獲取第一個(gè)字符串的地址
push?ebx????????????????????//?壓入00為了將兩個(gè)字符串分開
push?0x2020206b?????????????//?字符串?hello?lyshark
push?0x72616873
push?0x796c206f
push?0x6c6c6568
mov?ecx,esp?????????????????//?獲取第二個(gè)字符串的地址
上方匯編指令完成壓棧以后,接下來我們就可以調(diào)用MessageBoxA
函數(shù)了,其調(diào)用代碼如下。
push?ebx?????????????????????????????//?push?0
push?eax?????????????????????????????//?push?"alert"
push?ecx?????????????????????????????//?push?"hello?lyshark?!"
push?ebx?????????????????????????????//?push?0
mov?eax,0x75ac0ba0???????????????????//?將MessageBox地址賦值給EAX
call?eax?????????????????????????????//?調(diào)用?MessageBox
1.4.3 ShellCode提取與應(yīng)用
通過上方的實(shí)現(xiàn)流程,我們的ShellCode
就算開發(fā)完成了,接下來讀者只需要將上方ShellCode
整理成一個(gè)可執(zhí)行文件并編譯即可。
int?main(int?argc,?char?*argv[])
{
????_asm
????{
????????sub?esp,?0x50??????????//?抬高棧頂,防止沖突
????????xor?ebx,?ebx???????????//?清空ebx
????????push?ebx
????????push?0x20202174
????????push?0x72656c61????????//?字符串?"alert"
????????mov?eax,?esp???????????//?獲取棧頂
????????push?ebx???????????????//?填充00?截?cái)嘧址?/span>
????????push?0x2020206b
????????push?0x72616873
????????push?0x796c206f
????????push?0x6c6c6568?????????//?字符串?hello?lyshark
????????mov?ecx,?esp????????????//?獲取第二個(gè)字符串的地址
????????push?ebx
????????push?eax
????????push?ecx
????????push?ebx
????????mov?eax,?0x75ac0ba0????//?獲取MessageBox地址
????????call?eax???????????????//?call?MessageBox
????????push?ebx
????????mov?eax,?0x76c84100???//?獲取ExitProcess地址
????????call?eax??????????????//?call?ExitProcess
????}
????return?0;
}
接下來就是需要手動(dòng)提取此處匯編指令的特征碼,本案例中我們可以通過x64dbg
中的LyScript
插件實(shí)現(xiàn)提取,首先載入被調(diào)試進(jìn)程,然后尋找到如下所示的特征位置,當(dāng)遇到Call
時(shí),則通過F7進(jìn)入到內(nèi)部,如下圖所示;

如下圖中所示,就是我們所需要的匯編指令集,也就是我們自己的ShellCode
代碼片段,內(nèi)存地址為0x002D12A0
轉(zhuǎn)換為十進(jìn)制為2953888

通過LyScript插件并編寫如下腳本,并將EIP位置設(shè)置為eip = 2953888
運(yùn)行這段代碼;
from?LyScript32?import?MyDebug
if?__name__?==?"__main__":
????dbg?=?MyDebug()
????dbg.connect()
????ShellCode?=?[]
????eip?=?2953888
????for?index?in?range(0,?100?-?1):
????????read_code?=?dbg.read_memory_byte(eip?+?index)
????????ShellCode.append(str(hex(read_code)))
????for?index?in?ShellCode:
????????print(index.replace("0x","\\x"),end="")
????dbg.close()
則可輸出如下圖所示的完整特征碼,讀者可自行將此處特征碼格式化;

當(dāng)然讀者通過在_asm
指令位置設(shè)置F9
斷點(diǎn),并通過F5
啟動(dòng)調(diào)試,如下圖所示;

當(dāng)調(diào)試器被斷下時(shí),通過按下Ctrl+Alt+D
跳轉(zhuǎn)至反匯編代碼位置,并點(diǎn)擊顯示代碼字節(jié),同樣可以實(shí)現(xiàn)提取,如下圖所示;

我們直接將上方的這些機(jī)器碼提取出來,從而編寫出完整的ShellCode,最終測(cè)試代碼如下。
unsigned?char?shellcode[]?=?"\x83\xec\x50"
"\x33\xdb"
"\x53"
"\x68\x74\x21\x20\x20"
"\x68\x61\x6c\x65\x72"
"\x8b\xc4"
"\x53"
"\x68\x6b\x20\x20\x20"
"\x68\x73\x68\x61\x72"
"\x68\x6f\x20\x6c\x79"
"\x68\x68\x65\x6c\x6c"
"\x8b\xcc"
"\x53"
"\x50"
"\x51"
"\x53"
"\xb8\xa0\x0b\xac\x75"
"\xff\xd0"
"\x53"
"\xb8\x00\x41\xc8\x76"
"\xff\xd0";
int?main(int?argc,?char?**argv)
{
????LoadLibrary("user32.dll");
????__asm
????{
????????lea?eax,?shellcode
????????call?eax
????}
????return?0;
}
上方代碼經(jīng)過編譯以后,運(yùn)行會(huì)彈出一個(gè)我們自己DIY
的MessageBox
提示框,輸出效果圖如下所示;

本文作者: 王瑞 本文鏈接: https://www.lyshark.com/post/f7242d3c.html 版權(quán)聲明: 本博客所有文章除特別聲明外,均采用 BY-NC-SA 許可協(xié)議。轉(zhuǎn)載請(qǐng)注明出處!