在前面的章節中相信讀者已經學會了使用Metasploit
工具生成自己的ShellCode
代碼片段了,本章將繼續深入探索關于ShellCode
的相關知識體系,ShellCode 通常是指一個原始的可執行代碼的有效載荷,攻擊者通常會使用這段代碼來獲得被攻陷系統上的互動Shell的訪問權限,而現在用于描述一段自包含的獨立的可執行代碼片段,ShellCode代碼的撰寫有多種方式,通常會優先使用匯編語言實作,這得益于匯編語言的可控性,
ShellCode 通常會與漏洞利用并肩使用,或是被惡意代碼用于執行行程代碼的注入,通常情況下ShellCode
代碼無法獨立運行,必須依賴于父行程或是Windows
檔案加載器的加載才能夠被運行,本章將通過一個簡單的彈窗(MessageBox)來實作一個簡易版的彈窗功能,并以此來加深讀者對匯編語言的理解,
1.4.1 尋找DLL庫函式地址
在撰寫ShellCode
之前,我們需要查找一個函式地址,由于我們需要呼叫MessageBoxA()
這個函式,所以需要獲取該函式的記憶體動態地址,根據微軟的官方定義可知,該函式默認放在了User32.dll
庫中,為了能夠了解壓堆疊時需要傳入引數的型別,我們還需要查詢一下函式的原型;
在微軟定義中MessageBoxA
函式的原型如下:
int MessageBoxA(
HWND hWnd,
LPCSTR lpText,
LPCSTR lpCaption,
UINT uType
);
引數說明:
- hWnd:訊息框的父視窗句柄,
- lpText:訊息框中顯示的文本,
- lpCaption:訊息框的標題欄文本,
- uType:訊息框的型別,可以指定訊息框包含的按鈕以及圖示等,
需要注意的是,由于我們呼叫的是MessageBoxA
,而此函式為ASCII模式,需要讀者自行修改解決方案,在配置屬性的常規選項卡,修改字符集(使用多位元組字符集)即可,如下圖所示;
讀者可以通過撰寫一段簡單的代碼來獲取所需資料,首先通過LoadLibrary
函式加載名為user32.dll
的元件,并將其基地址存盤在HINSTANCE
型別的變數LibAddr
中,然后,使用GetProcAddress
函式獲取 MessageBoxA
函式的地址,并將其存盤在MYPROC
型別的變數ProcAddr
中,最后輸出所需結果;
#include <windows.h>
#include <iostream>
typedef void(*MYPROC)(LPTSTR);
int main(int argc, char *argv[])
{
HINSTANCE LibAddr,KernelAddr;
MYPROC ProcAddr;
// 獲取User32.dll基地址
LibAddr = LoadLibrary("user32.dll");
printf("user32.dll 動態庫基地址 = 0x%x \n", LibAddr);
// 獲取kernel32.dll基地址
KernelAddr = LoadLibrary("kernel32.dll");
printf("kernel32.dll 動態庫基地址 = 0x%x \n", KernelAddr);
// 獲取MessageBox基地址
ProcAddr = (MYPROC)GetProcAddress(LibAddr, "MessageBoxA");
printf("MessageBoxA 函式相對地址 = 0x%x \n", ProcAddr);
// 獲取ExitProcess基地址
ProcAddr = (MYPROC)GetProcAddress(KernelAddr, "ExitProcess");
printf("ExitProcess 函式相對地址 = 0x%x \n", ProcAddr);
system("pause");
return 0;
}
上方的代碼經過編譯運行后會得到兩個回傳結果,如下圖所示,其中User32.dll
的基地址是0x75a40000
而該模塊內的MessageBoxA
函式在當前系統中的地址為0x75ac0ba0
,當然這兩個模塊地址在每次系統啟動時都會發生幻化,讀者電腦中的地址肯定與筆者不相同,這都是正常現象,之所以會出現這種情況是因為,系統中存在一種ASLR機制,
擴展知識:ASLR(Address Space Layout Randomization)機制的核心是用于隨機化系統中程式和資料的記憶體地址分布,從而增加攻擊者攻擊系統的難度,在啟用了ASLR機制的系統下,每次運行程式時,程式和系統組件(例如DLL、驅動程式等)都會被分配不同的記憶體地址,而不是固定的記憶體地址,這樣可以使得攻擊者難以利用已知的記憶體地址漏洞進行攻擊,因為攻擊者需要先找到正確的記憶體地址才能利用漏洞,ASLR的隨機化是根據作業系統的一些隨機因素進行計算的,例如啟動時間、行程 ID 等等,
由于如上機制的存在,導致user32.dll
模塊地址不確定,也就會導致其地址內部的API函式地址也會發生一定的變化,下圖僅作為參考圖;
在獲取到MessageBoxA
函式的記憶體地址以后,我們接著需要獲取一個ExitProecess
函式的地址,這個API函式的作用是讓程式正常退出,這是因為我們注入代碼以后,原始的堆疊地址會被破壞,堆疊失衡后會導致程式崩潰,所以為了穩妥起見我們還是添加一行正常退出為好,函式ExitProcess
的原型如下:
VOID WINAPI ExitProcess(
UINT uExitCode
);
其中引數uExitCode
指定了行程的退出代碼,表示行程成功退出或者發生了錯誤,如果uExitCode
為0,表示行程成功退出,其他的非0值則表示行程發生了錯誤,不同的非0值可以用于表示不同的錯誤型別,
1.4.2 探討STDCALL呼叫約定
既然獲取到了相應的記憶體地址,那么接下來就需要通過匯編來撰寫可執行代碼片段了,在撰寫這段代碼之前,先來了解一下匯編語言的呼叫約定,在匯編語言中,要想呼叫某個函式,需要使用CALL陳述句,而在CALL陳述句的后面,要跟上該函式在系統中的地址,前面我們已經獲取到了相應的記憶體地址了,所以在這里就可以通過CALL相應的地址來呼叫相應的函式,
我們以32位應用程式為例,在32位應用程式內通常使用STDCALL
呼叫約定,它定義了函式在被呼叫時,引數傳遞、回傳值傳遞以及堆疊的使用等方面的規則,該呼叫約定的規則如下所示:
- 引數傳遞:引數從右向左依次壓入堆疊中,由被呼叫者在回傳前清理堆疊,
- 回傳值傳遞:函式回傳時將回傳值存盤在EAX暫存器中,
- 堆疊的使用:函式被呼叫前,呼叫者將引數壓入堆疊中;被呼叫者在回傳前清理堆疊,以確保堆疊的平衡,
- 函式呼叫:在呼叫函式之前,呼叫者將回傳地址(Return Address)和EBP暫存器的值保存在堆疊中,并將ESP暫存器指向引數串列的最后一個元素;在函式回傳之后,呼叫者通過將之前保存的EBP和回傳地址彈出堆疊中,并將ESP暫存器恢復到最初的位置來恢復堆疊的狀態,
總之,stdcall呼叫約定將引數按照從右到左的順序壓入堆疊中,由被呼叫者清理堆疊,回傳值存盤在EAX暫存器中,函式呼叫者和被呼叫者都需要遵循一定的堆疊使用規則,這種約定的好處是引數傳遞簡單,可讀性高,并且在函式回傳時堆疊已經被清理,不需要額外的清理作業,
在實際的編程中,一般還是先將地址賦值給eax
暫存器,然后再CALL
呼叫相應的暫存器實作呼叫,比如現在筆者有一個lyshark(a,b,c,d)
函式,如果我們想要呼叫它,那么它的匯編代碼就應該撰寫為:
push d
push c
push b
push a
mov eax,AddressOflyshark // 獲取偏移地址
call eax // 間接呼叫
根據上方的呼叫方式,我們可以寫出ExitProcess()
函式的匯編版呼叫結構,如下;
xor ebx, ebx
push ebx
mov eax, 0x76c84100
call eax
接著撰寫MessageBox()
這個函式呼叫,與ExitProcess()
函式不同的是,這個API函式包含有四個引數,當然第一和第四個引數,我們可以賦給0值,但是中間兩個引數都包含有較長的字串,這個該如何解決呢?我們不妨先把所需要用到的字串轉換為ASCII碼值,轉換的方式有許多,如下代碼則是通過Python實作的轉換模式;
import os,sys
from LyScript32 import MyDebug
# 字串轉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標題
title = StringToAscii("alert")
for index in range(0,len(title)):
print(title[index],end="")
print()
# 輸出MsgBox內容
box = StringToAscii("hello lyshark")
for index in range(0,len(box)):
print(box[index],end="")
當Python
程式被運行,則用戶即可得到兩串通過編碼后的字串資料,
MsgBox標題:alert \x61\x6c\x65\x72\x74\x21
MsgBox內容:hello lyshark \x68\x65\x6c\x6c\x6f\x20\x6c\x79\x73\x68\x61\x72\x6b
由于我們使用的是32位匯編,所以上方的字串需要做一定的處理,我們分別將每四個字符為一組,進行分組,將不滿四個字符的,以空格0x20
進行填充,這是因為我們采用的存盤字串模式為堆疊傳遞,而一個暫存器為32位,所以就需要填充滿4位元組才可以平衡;
-------------------------------------------------------------
填充 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
進行填充,而不是0x00
進行填充,是因為strcpy
這個字串拷貝函式,默認只要一遇到0x00
就會認為我們的字串結束了,就不會再拷貝0x00
后的內容了,所以這里就不能使用0x00
進行填充了,這里要特別留意一下,
接著我們需要將這兩段字串分別壓入堆疊存盤,這里需要注意,由于我們的計算機是小端序
排列的,因此字符的入堆疊順序是從后往前不斷進堆疊的,上面的字串壓堆疊引數應該寫為:
小提示:小端序(Little Endian)是一種資料存盤方式,在匯編語言中,小端序的表示方式與高位位元組優先(Big Endian)相反,例如,對于一個16位的整數0x1234,它在小端序的存盤方式下,將會被存盤為0x340x12(低位位元組先存盤);而在高位位元組優先的存盤方式下,將會被存盤為0x120x34(高位位元組先存盤),
-------------------------------------------------------------
壓入字串 alert
-------------------------------------------------------------
push 0x20202174
push 0x72656c61
-------------------------------------------------------------
壓入字串 hello lyshark
-------------------------------------------------------------
push 0x2020206b
push 0x72616873
push 0x796c206f
push 0x6c6c6568
既然字串壓入堆疊的功能有了,那么下面問題來了,我們如何獲取這兩個字串的地址,從而讓其成為MessageBox()
的引數呢?
其實這個問題也不難,我們可以利用esp
指標,因為它始終指向的是堆疊頂的位置,我們將字符壓入堆疊后,堆疊頂位置就是我們所壓入的字符的位置,于是在每次字符壓堆疊后,可以加入如下指令,依次將第一個字串基地址保存至eax
暫存器中,將第二個基地址保存至ecx
暫存器中,
xor ebx,ebx // 清空暫存器
push 0x20202174 // 字串 alert
push 0x72656c61
mov eax,esp // 獲取第一個字串的地址
push ebx // 壓入00為了將兩個字串分開
push 0x2020206b // 字串 hello lyshark
push 0x72616873
push 0x796c206f
push 0x6c6c6568
mov ecx,esp // 獲取第二個字串的地址
上方匯編指令完成壓堆疊以后,接下來我們就可以呼叫MessageBoxA
函式了,其呼叫代碼如下,
push ebx // push 0
push eax // push "alert"
push ecx // push "hello lyshark !"
push ebx // push 0
mov eax,0x75ac0ba0 // 將MessageBox地址賦值給EAX
call eax // 呼叫 MessageBox
1.4.3 ShellCode提取與應用
通過上方的實作流程,我們的ShellCode
就算開發完成了,接下來讀者只需要將上方ShellCode
整理成一個可執行檔案并編譯即可,
#include <iostream>
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 截斷字串
push 0x2020206b
push 0x72616873
push 0x796c206f
push 0x6c6c6568 // 字串 hello lyshark
mov ecx, esp // 獲取第二個字串的地址
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;
}
接下來就是需要手動提取此處匯編指令的特征碼,本案例中我們可以通過x64dbg
中的LyScript
插件實作提取,首先載入被除錯行程,然后尋找到如下所示的特征位置,當遇到Call
時,則通過F7進入到內部,如下圖所示;
如下圖中所示,就是我們所需要的匯編指令集,也就是我們自己的ShellCode
代碼片段,記憶體地址為0x002D12A0
轉換為十進制為2953888
通過LyScript插件并撰寫如下腳本,并將EIP位置設定為eip = 2953888
運行這段代碼;
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()
則可輸出如下圖所示的完整特征碼,讀者可自行將此處特征碼格式化;
當然讀者通過在_asm
指令位置設定F9
斷點,并通過F5
啟動除錯,如下圖所示;
當除錯器被斷下時,通過按下Ctrl+Alt+D
跳轉至反匯編代碼位置,并點擊顯示代碼位元組,同樣可以實作提取,如下圖所示;
我們直接將上方的這些機器碼提取出來,從而撰寫出完整的ShellCode,最終測驗代碼如下,
#include <windows.h>
#include <stdio.h>
#include <string.h>
#pragma comment(linker,"/section:.data,RWE")
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;
}
上方代碼經過編譯以后,運行會彈出一個我們自己DIY
的MessageBox
提示框,輸出效果圖如下所示;
文章出處:https://www.cnblogs.com/LyShark/p/17520452.html
本博客所有文章除特別宣告外,均采用 BY-NC-SA 許可協議,轉載請注明出處!
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/556507.html
標籤:訊息安全
上一篇:css學習(一)
下一篇:返回列表