DLL 注入技術的 N 種姿勢
DLL 注入技術的 N 種姿勢
本文中我將介紹DLL注入的相關知識。不算太糟的是,DLL注入技術可以被正常軟體用來新增/擴充套件其他程式,除錯或逆向工程的功能性;該技術也常被惡意軟體以多種方式利用。這意味著從安全形度來說,瞭解DLL注入的工作原理是十分必要的。
不久前在為攻擊方測試(目的是為了模擬不同型別的攻擊行為)開發定製工具的時候,我編寫了這個名為“injectAllTheThings”的小工程的大部分程式碼。如果你想看一下利用DLL注入實施的攻擊行為的若干示例,請參閱網址:https://attack.mitre.org/wiki/Technique/T1055
下圖為工具的輸出資訊,其中顯示了所有的選項和實現的技術。
網友@SubTee認為,DLL注入是多餘的(如下圖所示);我傾向於同意TA的觀點,然而DLL注入並不僅僅是載入DLL那麼簡單。
你確實可以利用簽名認證的微軟二進位制檔案來載入DLL,但你無法附加到一個特定的程序來干預其記憶體內容。為什麼大部分滲透測試師實際上不知道DLL注入是什麼,或者它是如何工作的?因為Metasploit平臺替他們包辦的太多了;他們一直盲目地使用它。我認為,學習這種“奇特的”記憶體操作技術的最好地點,實際上是遊戲黑客論壇。如果你正在進行攻擊方測試,那麼你就必須幹這些“髒”活兒,同時研究這些技術;除非你樂意僅僅使用別人隨意編寫的工具。
大部分時間,我們使用很複雜的技術開始一次攻擊方測試;如果我們未被發現,則開始降低複雜度。基本上這就是我們開始向磁碟投放二進位制檔案和應用DLL注入技術的時間點。
本文試圖以一種簡單而高階的方式縱覽DLL注入技術,同時為GitHub中的專案(網址為:https://github.com/fdiskyou/injectAllTheThings)提供“文件”支援。
簡介
DLL注入技術,一般來講是向一個正在執行的程序插入/注入程式碼的過程。我們注入的程式碼以動態連結庫(DLL)的形式存在。DLL檔案在執行時將按需載入(類似於UNIX系統中的共享庫)。本工程中,我將僅使用DLL檔案,然而實際上,我們可以以其他的多種形式“注入“程式碼(正如惡意軟體中所常見的,任意PE檔案,shellcode程式碼/程式集等)。
同時要記住,你需要合適的許可權級別來操控其他程序的記憶體空間。但我不會在此討論保護程序(相關網址:https://www.microsoftpressstore.com/articles/article.aspx?p=2233328&seqNum=2)和許可權級別(通過Vista系統介紹,相關網址:https://msdn.microsoft.com/en-gb/library/windows/desktop/bb648648(v=vs.85).aspx);這屬於完全不同的另一個主題。
再次強調一下,正如我之前所說,DLL注入技術可以被用於合法正當的用途。比如,反病毒軟體和端點安全解決方案使用這些技術來將其軟體的程式碼嵌入/攔截系統中“所有”正在執行的程序,這使得它們可以在其執行過程中監控每一個程序,從而更好地保護我們。同樣存在惡意的用途。一種經常被用到的通用技術是注入“lsass”程序來獲取口令雜湊值。我們之前都這麼幹過。很明顯,惡意程式碼同樣廣泛應用了程式碼注入技術:不管是執行shellcode程式碼,執行PE檔案,還是在另一個程序的記憶體空間中載入DLL檔案以隱藏自身,等等。
基礎知識
對於每一種技術,我們都將用到微軟Windows API,因為它為我們提供了大量的函式來附加和操縱其他程序。從微軟Windows作業系統的第一個版本開始,DLL檔案就是其基石。事實上,微軟Windows
API中的所有函式都包含於DLL檔案之中。其中,最重要的是“Kernel32.dll”(包含管理記憶體,程序和執行緒相關的函式),“User32.dll”(大部分是使用者介面函式),和“GDI32.dll”(繪製圖形和顯示文字相關的函式)。
你可能會有疑問,為什麼會有這些API介面,為什麼微軟為我們提供如此豐富的函式集來操縱和修改其他程序的記憶體空間?主要原因是為了擴充套件應用程式的功能。比如,一個公司建立了一款應用程式,並且允許其他公司來擴充套件或增強這個應用程式;如此,這就有了一個合法正當的用途。除此之外,DLL檔案還用於專案管理,記憶體保護,資源共享,等等。
下圖嘗試說明了幾乎每一種DLL注入技術的流程。
如上所見,我認為 DLL 注入共四個步驟:
1. 附加到目標/遠端程序
2. 在目標/遠端程序內分配記憶體
3. 將DLL檔案路徑,或者DLL檔案,複製到目標/遠端程序的記憶體空間
4. 控制程序執行DLL檔案
所有這些步驟是通過呼叫一系列指定的API函式來完成的。每種技術需要進行特定的設定和選項配置。我認為,每種技術都有其優點和缺點
(1)技術介紹
我們有多種方式可以控制程序執行我們的DLL檔案。最普通的應該是“CreateRemoteThread()”和“NtCreateThreadEx()”函式;然而,不可能僅僅向這些函式傳遞一個DLL檔案作為引數,我們必須提供一個包含執行起點的記憶體地址。為此,我們需要分配記憶體,使用“LoadLibrary()”載入我們的DLL檔案,複製記憶體,等等。
我稱之為“injectAllTheThings”的工程(因為我只是單純討厭“注入器”這個名字,加上GitHub上已經有太多的垃圾“注入器”,而我不想再多一個了)包含7種不同的技術;我並不是其中任何一種技術的原創作者,而是提煉總結了這七種技術(是的,還有更多)。其中某些已經有很多文件資料描述(像“CreateRemoteThread()”),而另一些則屬於未公開API函式(像“NtCreateThreadEx()”)。以下是所實現的技術的完整列表,其中每種都可以在32位和64位環境下生效。
•CreateRemoteThread()
•NtCreateThreadEx()
•QueueUserAPC
•SetWindowsHookEx()
•RtlCreateUserThread()
•利用SetThreadContext()找到的程式碼區域
•反射DLL
你可能通過其他的名字瞭解其中某些技術。以上並不是包含每一種DLL注入技術的完整列表;正如我所說的,還有更多技術,如果之後我在某個工程中需要對其接觸學習的話我會將它們新增進來。到目前為止,這就是我在某些工程中所用到的技術列表;其中某些可以穩定利用,某些不可以。需要注意的是,不能夠穩定利用的那些技術可能是由於我所編寫程式碼的自身問題。
(2)LoadLibrary()
正如MSDN中所述,“LoadLibrary()”函式“被用於向呼叫程序的地址空間載入指定模組,而該指定模組可能導致其他模組被載入”。函式原型與引數說明如下所示:
HMODULE WINAPI LoadLibrary(
_IN_ LPCRSTR lpFileName
);
lpFileName [輸入引數]
模組名稱。該模組可能是一個庫模組(.dll檔案),或者一個可執行模組(.exe檔案)
(…)
若字串指定了一個完全路徑,則函式只在該路徑下搜尋模組;
若字串指定了一個相對路徑或者無路徑的模組名稱,則函式使用標準搜尋策略來查詢模組。
(…)
若函式無法找到模組,則函式執行失敗。當指定路徑時,必須使用反斜線(\)而不是斜線(/)。
(…)
如果字串指定了一個無路徑的模組名稱並且無檔名字尾,則函式預設在模組名稱後面新增庫檔案字尾.dll。
換言之,該函式只需要一個檔名作為其唯一的引數。即,我們只需要為我們的DLL檔案路徑分配記憶體,將執行起點設定為“LoadLibrary()”函式的地址,之後將路徑的記憶體地址傳遞給函式作為引數。
正如你所知道(或不知道)的,最大的問題是“LoadLibrary()”會向程式註冊已載入的DLL模組;這意味著這種方法很容易被檢測到,但令人驚奇的是很多端點安全解決方案仍檢測不出。不管怎樣,正如我之前所說,DLL注入也有一些合法正當的用途,因此我們還要注意的是,如果一個DLL檔案已經用“LoadLibrary()”載入過了,則它不會再次執行。你可以試驗一下,但我沒有對任何一種技術試過。當然,使用反射DLL注入技術不會有這方面的問題,因為DLL模組並未被註冊。不同於使用“LoadLibrary()”,反射DLL注入技術將整個DLL檔案載入記憶體,然後通過確定DLL模組的入口點偏移來將其載入;這樣可以按照需求更隱蔽的對其進行呼叫。取證人員仍然能夠在記憶體中找到你的DLL,但會很艱難。Metasploit平臺大量使用了這項技術,而且大部分端點解決方案也還樂意始終使用它。如果你想查詢這方面的技術資料,或者你在攻防遊戲中處於防守方,可以參閱以下網址:
https://www.defcon.org/html/defcon-20/dc-20-speakers.html#King
https://github.com/aking1012/dc20
附註一下,如果你正在折騰你的端點安全軟體,而它很好地利用了以上所有這些技術,你可能需要使用以下攻防反欺騙引擎來試試(注意,我只是嘗試輕鬆的說法,以便你能理解)。某些反欺騙工具的反Rookit效能要比某些反病毒軟體要先進得多。reddit網站上有一本書你肯定讀過,叫“黑客遊戲”,它的作者Nick Cano對其有非常深入的研究;只需瞭解一下他的工作,你就會理解我所談論的內容。
附加到目標/遠端程序
首先,我們需要獲取我們想要互動的程序控制代碼;為此我們呼叫“OpenProcess()”API函式。函式原型如下:
HANDLE WINAPI OpenProcess(
_In_ DWORD dwDesiredAccess,
_In_ BOOL bInheritHandle,
_In_ DWORD dwProcessId
);
如果你讀過MSDN中相關的文件,那麼你應該知道我們需要請求獲取一系列特定的訪問許可權;訪問許可權的完整列表參閱網址:https://msdn.microsoft.com/en-gb/library/windows/desktop/ms684880(v=vs.85).aspx。
這些許可權隨微軟Windows作業系統版本不同而不同;以下呼叫程式碼可用於幾乎每一種技術之中:
HANDLE hProcess = OpenProcess(
PROCESS_QUERY_INFORMATION |
PROCESS_CREATE_THREAD |
PROCESS_VM_OPERATION |
PROCESS_VM_WRITE,
FALSE, dwProcessId );
在目標/遠端程序空間分配記憶體
我們呼叫“VirtualAllocEx()”函式為DLL路徑分配記憶體。正如MSDN中所述,“VirtualAllocEx()”函式“保留,提交或改變指定程序虛擬地址空間中的一塊記憶體區域的狀態;函式通過置零來初始化記憶體。”函式原型如下:
LPVOID WINAPI VirtualAllocEx(
_In_ HANDLE hProcess,
_In_opt_ LPVOID lpAddress,
_In_ SIZE_T dwSize,
_In_ DWORD flAllocationType,
_In_ DWORD flProtect
);
基本上,我們進行如下的呼叫操作:
// 計算DLL檔案路徑名稱所需的位元組數
DWORD dwSize = (lstrlenW(pszLibFile) + 1) * sizeof(wchar_t);
// 在目標/遠端程序中為路徑名稱分配空間
LPVOID pszLibFileRemote = (PWSTR)VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE);
或者你可以更聰明一點地呼叫“GetFullPathName()”API函式;然後,我在整個工程中都沒有呼叫這個API函式,僅僅是出於個人偏好或者是不夠聰明。
如果你想要為整個DLL檔案分配空間,就必須進行如下操作:
hFile = CreateFileW(pszLibFile, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
dwSize, = GetFileSize(hFile, NULL);
PVOID pszLibFileRemote = (PWSTR)VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE);
複製DLL檔案路徑(或者DLL檔案)到目標/遠端程序的記憶體空間中
現在,我們需要呼叫“WriteProcessMemory()”API函式,來將我們的DLL檔案路徑,或者整個DLL檔案,複製到目標/遠端程序中。函式原型如下所示:
BOOL WINAPI WriteProcessMemory(
_In_ HANDLE hProcess,
_In_ LPVOID lpBaseAddress,
_In_ LPCVOID lpBuffer,
_In_ SIZE_T nSize,
_Out_ SIZE_T *lpNumberOfBytesWritten
);
一般的呼叫程式碼如下所示:
DWORD n = WriteProcessMemory(hProcess, pszLibFileRemote, (PVOID)pszLibFile, dwSize, NULL);
如果我們想要像反射DLL注入技術所做的那樣複製整個DLL檔案,就需要更多的程式碼,因為在將其複製到目標/遠端程序之前我們需要將其讀入記憶體。具體程式碼如下所示:
lpBuffer = HeapAlloc(GetProcessHeap(), 0, dwLength)
ReadFile(hFile, lpBuffer, dwLength, &dwBytesRead, NULL)
WriteProcessMemory(hProcess, pszLibFileRemote, (PVOID)pszLibFile, dwSize, NULL);
正如之前所述,通過使用反射DLL注入技術,以及將DLL檔案複製到記憶體中,程序不會記錄該DLL模組。
但這樣會有一點複雜,因為當DLL模組載入到記憶體中時我們需要獲取其入口點;反射DLL工程的“LoadRemoteLibraryR()”函式部分為我們完成了這項工作。如有需要請參閱原始碼。
需要注意的是,我們將注入的DLL檔案需要使用適當的包含與選項來進行編譯,這樣它才能與ReflectiveDLLInjection方法相匹配。“InjectAllThings”工程中包含了一個名為“rdll_32.dll/rdll_64.dll”的DLL檔案,可用於練習。
控制程序來執行DLL檔案
(1)CreateRemoteThread()
可以說,“CreateRemoteThread()”是最傳統和最流行,以及最多文件資料介紹的DLL注入技術。
它包括以下幾個步驟:
1.使用OpenProcess()函式開啟目標程序
2.通過呼叫GetProAddress()函式找到LoadLibrary()函式的地址
3.通過呼叫VirtualAllocEx()函式在目標/遠端程序地址空間中為DLL檔案路徑開闢記憶體空間
4.呼叫WriteProcessMemory()函式在之前所分配的記憶體空間中寫入DLL檔案路徑
5.呼叫CreateRemoteThread()函式建立一個新的執行緒,新執行緒以DLL檔案路徑名稱作為引數來呼叫LoadLibrary()函式
如果你看過MSDN中關於“CreateRemoteThread()”函式的文件,那麼你應該知道,我們需要一個指標,“指向將由執行緒執行的,型別為‘LPTHREAD_START_ROUTINE’的應用程式定義函式,並且該指標代表遠端程序中執行緒的起始地址”。
這意味著要執行我們的DLL檔案,我們只需要控制程序來做就好(譯者注:由下文可知,應該是將“LoadLibrary()”函式作為執行緒的啟動函式,來載入待注入的DLL檔案)。很簡單。
以下程式碼即之前所列的全部基本步驟。
HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_CREATE_THREAD | PROCESS_VM_OPERATION | PROCESS_VM_WRITE, FALSE, dwProcessId);
// 在遠端程序中為路徑名稱分配空間
LPVOID pszLibFileRemote = (PWSTR)VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE);
// 將DLL路徑名稱複製到遠端程序地址空間
DWORD n = WriteProcessMemory(hProcess, pszLibFileRemote, (PVOID)pszLibFile, dwSize, NULL);
// 獲取Kernel32.dll中的LoadLibraryW函式的真正地址
PTHREAD_START_ROUTINE pfnThreadRtn = (PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryW");
// 建立遠端執行緒呼叫LoadLibraryW(DLLPathname)
HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, pfnThreadRtn, pszLibFileRemote, 0, NULL);
完整程式碼請參閱原始檔“t_CreateRemoteThread.cpp”。
(2)NtCreateThreadEx()
另一個選擇是使用“NtCreateThreadEx()”函式;這是一個未公開的“ntdll.dll”中的函式,未來可能會消失或改變。這種技術相比而言實現更加複雜,因為我們需要一個結構體(具體如下所示)來向函式傳遞引數,以及另一個結構體用於從函式接收資料。
struct NtCreateThreadExBuffer {
ULONG Size;
ULONG Unknown1;
ULONG Unknown2;
PULONG Unknown3;
ULONG Unknown4;
ULONG Unknown5;
ULONG Unknown6;
PULONG Unknown7;
ULONG Unknown8;
};
網址:http://securityxploded.com/ntcreatethreadex.php處的文章詳細介紹了該函式呼叫。設定部分與“CreateRemoteThread()”非常類似;然而,相較於直接呼叫“CreateRemoteThread()”函式,我們使用如下程式碼來呼叫“NtCreateThreadEx()”。
PTHREAD_START_ROUTINE ntCreateThreadExAddr = (PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(TEXT("ntdll.dll")), "NtCreateThreadEx");
LPFUN_NtCreateThreadEx funNtCreateThreadEx = (LPFUN_NtCreateThreadEx)ntCreateThreadExAddr;
NTSTATUS status = funNtCreateThreadEx(
&hRemoteThread,
0x1FFFFF,
NULL,
hProcess,
pfnThreadRtn,
(LPVOID)pszLibFileRemote,
FALSE,
NULL,
NULL,
NULL,
NULL
);
完整程式碼請參閱原始檔“t_NtCreateThreadEx.cpp”。
(3)QueueUserAPC()
除了之前介紹的方法還有一種選擇,不用在目標/遠端程序中建立一個新的執行緒,那就是“QueueUserAPC()”函式。
根據MSDN中的文件介紹,該函式“向指定執行緒的APC佇列中新增一個使用者態的非同步過程呼叫(APC)物件”。
函式原型與引數說明如下所示。
DWORD WINAPI QueueUserAPC(
_In_ PAPCFUNC pfnAPC,
_In_ HANDLE hThread,
_In_ ULONG_PTR dwData
);
pfnAPC [輸入引數]
指向應用程式提供的APC函式的指標,該函式在指定執行緒執行一個可喚醒等待操作的時候被呼叫。(…)
HThread [輸入引數]
執行緒控制代碼。該控制代碼必須具備THREAD_SET_CONTEXT訪問許可權。(…)
dwData [輸入引數]
傳遞給pfnAPC引數所指向的APC函式的一個值
因此,如果不想建立我們自己的執行緒,我們可以呼叫“QueueUserAPC()”函式來劫持目標/遠端程序中一個已存在的執行緒;即,呼叫該函式將在指定執行緒的APC佇列中新增一個非同步過程呼叫。
我們可以使用一個真實的APC回撥函式,而不使用“LoadLibrary()”。事實上引數可以是指向我們想要注入的DLL檔名稱的指標,具體程式碼如下所示。
DWORD dwResult = QueueUserAPC((PAPCFUNC)pfnThreadRtn, hThread, (ULONG_PTR)pszLibFileRemote);
如果你想試用這種技術,那麼有一點你可能注意到了,即它與微軟Windows作業系統執行APC的方式有關。沒有能夠檢視APC佇列的排程器,這意味著只有執行緒設定成可喚醒模式才能夠檢查佇列。
因此,我們基本上劫持每一個單獨的執行緒,具體程式碼如下所示。
BOOL bResult = Thread32First(hSnapshot, &threadEntry);
while (bResult)
{
bResult = Thread32Next(hSnapshot, &threadEntry);
if (bResult)
{
if (threadEntry.th32OwnerProcessID == dwProcessId)
{
threadId = threadEntry.th32ThreadID;
wprintf(TEXT("[+] Using thread: %i\n"), threadId);
HANDLE hThread = OpenThread(THREAD_SET_CONTEXT, FALSE, threadId);
if (hThread == NULL)
wprintf(TEXT("[-] Error: Can't open thread. Continuing to try other threads...\n"));
else
{
DWORD dwResult = QueueUserAPC((PAPCFUNC)pfnThreadRtn, hThread, (ULONG_PTR)pszLibFileRemote);
if (!dwResult)
wprintf(TEXT("[-] Error: Couldn't call QueueUserAPC on thread> Continuing to try othrt threads...\n"));
else
wprintf(TEXT("[+] Success: DLL injected via CreateRemoteThread().\n"));
CloseHandle(hThread);
}
}
}
}
這樣做的理由主要是期望其中一個執行緒會被設為可喚醒模式。
另外,使用“雙脈衝星”(網址:https://countercept.com/our-thinking/doublepulsar-usermode-analysis-generic-reflective-dll-loader/,DOUBLEPULSAR 使用者模式分析:通用反射DLL載入器)工具分析學習這項技術,是個很好的辦法。
完整程式碼請參閱原始檔“t_QueueUserAPC.cpp”。
(4)SetWindowsHookEx()
使用這項技術的首要工作是,我們需要理解在微軟Windows作業系統中劫持的工作原理。本質上,劫持技術是攔截並干預事件的一種方式。
正如你所猜想的那樣,有很多種不同型別的劫持技術。最通用的一種可能是WH_KEYBOARD和WH_MOUSE訊息攔截;沒錯,它們可被用於監控鍵盤與滑鼠的輸入。
函式“SetWindowsHookEx()”將一個應用程式定義的攔截例程安裝到一個攔截連結串列中。函式原型和引數定義如下所示。
HHOOK WINAPI SetWindowsHookEx(
_In_ int idHook,
_In_ HOOKPROC lpfn,
_In_ HINSTANCE hMod,
_In_ DWORD dwThreadId
);
idHook [輸入引數]
型別:整型(int)
待安裝攔截例程的型別。(…)
lpfn [輸入引數]
型別:攔截例程函式型別(HOOKPROC)
指向攔截例程的指標。(…)
hMod [輸入引數]
型別:例項控制代碼(HINSTANCE)
一個DLL模組的控制代碼,該DLL模組中包含lpfn引數所指向的攔截例程。(…)
dwThreadId [輸入引數]
型別:雙字(DWORD)
攔截例程所關聯的執行緒的識別符號。(…)
MSDN中有一段很有趣的備註如下:
“SetWindowsHookEx函式可被用於向另一個程序注入DLL檔案。一個32位的DLL檔案不能注入一個64位的程序,反之亦然。如果一個應用程式需要在其他程序中使用劫持技術,那麼就要求一個32(64)位的應用程式呼叫SetWindowsHookEx函式來將一個32(64)位的DLL檔案注入到一個32(64)位的程序中。32位和64位DLL檔案的名稱必須不同。”
記住以上內容。
以下程式碼是實現的簡要過程。
GetWindowThreadProcessId(targetWnd, &dwProcessId)
HHOOK handle = SetWindowsHookEx(WH_KEYBOARD, addr, dll, threadID);
我們需要理解的是,每一個發生的事件都要遍歷攔截連結串列,該連結串列包含一系列響應事件的例程。“SetWindowsHookEx()”函式的設定工作基本上就是如何將我們自己的攔截例程植入攔截連結串列中。
以上程式碼用到了待安裝的劫持訊息型別(WH_KEYBOARD)例程指標,包含例程的DLL模組控制代碼,以及劫持所關聯的執行緒標識號。
為了獲取例程指標,我們需要首先呼叫“LoadLibrary()”函式載入DLL檔案,然後呼叫“SetWindowsHookEx()”函式,並等待我們所需的事件發生(本文中該事件是指按鍵);一旦事件發生我們的DLL就會被執行。
注意,正如我們在維基解密(網址:https://wikileaks.org/ciav7p1/cms/page_6062133.html)中所看到的,就連聯邦調查局的人員也有可能用到“SetWindowsHookEx()”函式。
完整程式碼請參閱原始檔“t_SetWindowsHookEx.cpp”。
(5)RtlCreateUserThread()
“RtlCreateUserThread()”是一個未公開的API函式。它的設定工作幾乎和“CreateRemoteThread()”函式相同,相應的也和“NtCreateThreadEx()”函式相同。
實際上,“RtlCreateUserThread()”呼叫“NtCreateThreadEx()”,這意味著“RtlCreateUserThread()”是“NtCreateThreadEx()”的一個小型封裝函式;因此,這個函式並沒有新的內容。然而,我們可能只是單純地想使用“RtlCreateUserThread()”而不用“NtCreateThreadEx()”。哪怕之後發生變動,我們的“RtlCreateUserThread()”仍能正常工作。
正如你所知道的,不同於其他工具,mimikatz工具和Metasploit平臺都用到了“RtlCreateUserThread()”。如果你對此感興趣,請參閱網址:https://github.com/gentilkiwi/mimikatz/blob/d5676aa66cb3f01afc373b0a2f8fcc1a2822fd27/modules/kull_m_remotelib.c#L59和網址:https://github.com/rapid7/meterpreter/blob/6d43284689240f4261cae44a47f0fb557c1dde27/source/common/arch/win/remote_thread.c。
因此,如果mimikatz工具和Metasploit平臺都使用“RtlCreateUserThread()”函式,那麼(是的,他們瞭解自己的處理物件)聽從他們的“建議”,使用“RtlCreateUserThread()”;特別是你計劃做一項相比於簡單的“injectAllTheThings”工程來說更認真的專案。
完整程式碼請參閱原始檔“t_RtlCreateUserThread.cpp”。
(6)SetThreadContext()
實際上這是一種非常酷的方法:通過在目標/遠端程序中分配一塊記憶體區域,向目標/遠端程序注入一段特別構造的程式碼,這段程式碼的用途是載入DLL模組。
以下是32位環境下的程式碼。
0x68, 0xCC, 0xCC, 0xCC, 0xCC, // push 0xDEADBEEF (為返回地址佔位)
0x9c, // pushfd (儲存標誌和暫存器)
0x60, // pushad
0x68, 0xCC, 0xCC, 0xCC, 0xCC, // push 0xDEADBEEF (為DLL路徑名稱佔位)
0xb8, 0xCC, 0xCC, 0xCC, 0xCC, // mov eax, 0xDEADBEEF (為LoadLibrary函式佔位)
0xff, 0xd0, // call eax (呼叫LoadLibrary函式)
0x61, // popad (恢復標誌和暫存器)
0x9d, // popfd
0xc3 // ret
對於64位環境,實際上我沒有找到任何完整的工作程式碼,因此我簡單寫了我自己的程式碼,如下所示。
0x50, // push rax (儲存RAX暫存器)
0x48, 0xB8, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, // mov rax, 0CCCCCCCCCCCCCCCCh (為返回地址佔位)
0x9c, // pushfq
0x51, // push rcx
0x52, // push rdx
0x53, // push rbx
0x55, // push rbp
0x56, // push rsi
0x57, // push rdi
0x41, 0x50, // push r8
0x41, 0x51, // push r9
0x41, 0x52, // push r10
0x41, 0x53, // push r11
0x41, 0x54, // push r12
0x41, 0x55, // push r13
0x41, 0x56, // push r14
0x41, 0x57, // push r15
0x68,0xef,0xbe,0xad,0xde, // fastcall呼叫約定
0x48, 0xB9, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, // mov rcx, 0CCCCCCCCCCCCCCCCh (為DLL路徑名稱佔位)
0x48, 0xB8, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, // mov rax, 0CCCCCCCCCCCCCCCCh (為LoadLibrary函式佔位)
0xFF, 0xD0, // call rax (呼叫LoadLibrary函式)
0x58, // pop dummy
0x41, 0x5F, // pop r15
0x41, 0x5E, // pop r14
0x41, 0x5D, // pop r13
0x41, 0x5C, // pop r12
0x41, 0x5B, // pop r11
0x41, 0x5A, // pop r10
0x41, 0x59, // pop r9
0x41, 0x58, // pop r8
0x5F, // pop rdi
0x5E, // pop rsi
0x5D, // pop rbp
0x5B, // pop rbx
0x5A, // pop rdx
0x59, // pop rcx
0x9D, // popfq
0x58, // pop rax
0xC3 // ret
在我們想目標程序注入這段程式碼之前,以下佔位符需要修改填充:
- ·返回地址(程式碼樁執行完畢之後,執行緒恢復應回到的地址)
- ·DLL路徑名稱
- ·LoadLibrary()函式地址
而這也是進行劫持,掛起,注入和恢復執行緒這一系列操作的時機。
我們需要附加到目標/遠端程序,之後當然是在目標/遠端程序中分配記憶體。注意,我們需要以讀寫許可權分配記憶體,以便操作DLL路徑名稱和用於載入DLL檔案的封裝程式碼。具體程式碼如下所示。
LPVOID lpDllAddr = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
stub = VirtualAllocEx(hProcess, NULL, stubLen, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
之後,我們需要獲取一個運行於目標/遠端程序之上的執行緒上下文(即我們將要注入封裝程式碼的目標執行緒)。
我們呼叫函式“getThreadID()”來找到執行緒,你可以在檔案“auxiliary.cpp”中找到該函式。
有了執行緒標識號之後,我們需要設定執行緒上下文。具體程式碼如下所示。
hThread = OpenThread((THREAD_GET_CONTEXT | THREAD_SET_CONTEXT | THREAD_SUSPEND_RESUME), false, threadID);
然後,我們需要掛起執行緒來獲取其上下文;一個執行緒的上下文是指其暫存器的狀態,我們格外關注的是EIP/RIP暫存器(根據需要也可以稱其為IP——instruction pointer,指令指標)。
由於執行緒已被掛起,所以我們可以改變EIP/RIP暫存器的值,控制執行緒在不同的路徑上(我們的程式碼區域)繼續執行。具體程式碼如下所示。
ctx.ContextFlags = CONTEXT_CONTROL;
GetThreadContext(hThread, &ctx);
DWORD64 oldIP = ctx.Rip;
ctx.Rip = (DWORD64)stub;
ctx.ContextFlags = CONTEXT_CONTROL;
WriteProcessMemory(hProcess, (void *)stub, &sc, stubLen, NULL); // write code cave
SetThreadContext(hThread, &ctx);
ResumeThread(hThread);
因此,我們中斷執行緒,獲取上下文,並從上下文中提取EIP/RIP值;儲存的舊值用於在注入程式碼執行完成時恢復執行緒的執行流程。新的EIP/RIP值設定為我們注入的程式碼位置。
然後我們用返回地址,DLL路徑名稱地址和“LoadLibrary”函式地址填充所有的佔位符。
執行緒開始執行的時候,我們的DLL檔案將被載入;而當注入程式碼執行完成時,執行流程將返回縣城掛起點,並從此恢復執行緒的正常執行流程。
如果你想要除錯這種技術方法來學習練習,以下是操作流程。啟動你想要注入的應用程式,在此我們以“notepad.exe”為例。使用“x64dbg”除錯工具來執行“injectAllTheThings_64.exe”,如下圖所示。
使用以下命令(根據你的實際環境來調整)。
"C:\Users\rui\Documents\Visual Studio 2013\Projects\injectAllTheThings\bin\injectAllTheThings_64.exe" -t 6 notepad.exe "c:\Users\rui\Documents\Visual Studio 2013\Projects\injectAllTheThings\bin\dllmain_64.dll"
在呼叫“WriteProcessMemory()”函式處設下斷點,如下圖所示。
繼續執行程式,當執行到斷點處時,注意暫存器RDX中的記憶體地址,如圖所示。如果你對為什麼這裡需要關注RDX有疑問,請去查閱x64環境下的呼叫約定;搞清楚再回來繼續學習。
單步步過(快捷鍵F8)呼叫“WriteProcessMemory()”函式的過程,開啟x64dbg工具的另一個例項,並將其附加到“notepad.exe”程序通過快捷鍵“Ctrl+g”調到之前複製的地址(即RDX暫存器中的記憶體地址)處,你將看到我們的程式碼區域程式集,如下圖所示。
很酷吧?現在在Shellcode程式碼起始處設下斷點。轉向“injectAllTheThings”除錯程序,並執行程式。我們的斷點被成功斷下,如下圖所示;現在我們可以步過程式碼,並分析這段程式碼的功能。
當我們呼叫“LoadLibrary()”函式時,我們的DLL檔案成功載入,如下圖所示。
太棒了~
我們的Shellcode程式碼將返回到之前儲存的RIP地址處,“notepad.exe”程序將恢復執行。
完整程式碼請參閱原始檔“t_suspendInjectResume.cpp”。
(7)反射DLL注入
我將StepheFewer(這項技術的先驅)的程式碼也整合到了這個“injectAllTheThings”工程中,同時還構建了一個反射DLL檔案用於這項技術。注意,我們要注入的DLL檔案必須使用適當的包含和選項來進行編譯,這樣它才能與反射DLL注入技術相匹配。
反射DLL注入技術通過將整個DLL檔案複製到記憶體中的方式來生效,因此它避免了向程序註冊DLL模組這一行為。所有的繁瑣工作都已完成。要在DLL模組載入到記憶體時獲取其入口點,我們只需要使用Stephen
Fewer的程式碼;他的工程中所包含的“LoadRemoteLibrary()”函式為我們完成這項工作。我們使用“GetReflectLoaderOffset()”函式來確定在我們程序記憶體中的偏移,然後我們將偏移加上目標/遠端程序(即我們寫入DLL模組的程序)的記憶體基址,將該結果作為執行起始點。
太複雜?好吧,可能有點兒;以下是實現上述過程的4個主要步驟。
1.將DLL檔案頭部寫入記憶體
2.將每個區塊寫入記憶體(通過解析區塊表)
3.檢查輸入表,並載入任何引用的DLL檔案
4.呼叫DLLMain函式的入口點
相比於其他方法,這種技術有很好的隱蔽性,主要被用於Metasploit平臺。
如果你想要了解更多,請前往官方GitHub庫;還想要閱讀Stephen http://www.harmonysecurity.com/files/HS-P005_ReflectiveDllInjection.pdf
還可以參閱“MemoryModule”專案的作者Joachim Bauch所寫的“從記憶體中載入DLL檔案”,以及一篇好文章“不呼叫LoadLibrary()函式‘手動’載入32位/64位DLL檔案”。
程式碼
還有一些模糊複雜的注入方法,因此我未來將對“injectAllTheThings”工程進行更新。其中某些最有趣的技術包括:
•“雙脈衝星”工具所用到的技術
•網友@zerosum0x0所編寫的工具,使用SetThreadContext()和NtContinue()實現的反射DLL注入,詳細描述參見網址:https://zerosum0x0.blogspot.co.uk/2017/07/threadcontinue-reflective-injection.html,可用程式碼詳見網址:https://github.com/zerosum0x0/ThreadContinue。
以上我所描述的所有技術,都在一個單獨的工程中實現了,我將其放在GitHub庫中;其中還包括每種技術所需的DLL檔案。為了便於理解,下表簡單介紹了所實現的方法和具體用法。
需要說明的是,從安全形度出發,應該堅持使用injectAllTheThings_32.exe注入32位程序,或者injectAllTheThings_64.exe來注入64位程序;儘管你也可以使用injectAllTheThings_64.exe來注入32位程序。而實際上我並沒有這樣實現,但可能之後我會嘗試一下,具體請參考以下網址:http://blog.rewolf.pl/blog/?p=102。參考網址中的技術基本上就是Metasploit平臺上“smart_migrate”工具所用到的,詳見網址:https://github.com/rapid7/meterpreter/blob/5e24206d510a48db284d5f399a6951cd1b4c754b/source/common/arch/win/i386/base_inject.c。
整個工程的程式碼(包括DLL檔案)都在GitHub庫中。程式碼以32位/64位環境編譯,包含或不包含除錯資訊都可以。
參考
·http://www.nologin.org/Downloads/Papers/remote-library-injection.pdf
·https://warroom.securestate.com/dll-injection-part-1-setwindowshookex/
·https://warroom.securestate.com/dll-injection-part-2-createremotethread-and-more/
·http://blog.opensecurityresearch.com/2013/01/windows-dll-injection-basics.html
·http://resources.infosecinstitute.com/using-createremotethread-for-dll-injection-on-windows/
·http://securityxploded.com/ntcreatethreadex.php
·https://www.codeproject.com/Tips/211962/Bit-Injection-Cave
·http://www.blizzhackers.cc/viewtopic.php?p=2483118
·http://resources.infosecinstitute.com/code-injection-techniques/
·Windows via C/C++ 5th Edition
本文由 看雪翻譯小組 木無聊偶 編譯 轉載請註明來自看雪社群