常見的幾種DLL注入技術
簡介
這次實驗是在WIN7 X86系統上進程,使用的編譯器是VS2017。
所謂的DLL注入,其實就是在其他的進程中把我們編寫的DLL加載進去。如下圖所示:

而加載Dll的API就是LoadLibrary,它的參數是保存要加載的DLL的路徑的地址。所以DLL注入的核心就是把要注入的DLL的路徑寫到目標進程中,然后在目標進程中調用LoadLibrary函數,并且指定參數為保存了DLL路徑的地址。
要實現DLL注入,首先就要創建一個用來注入的DLL。在VS2017中要生成一個DLL項目,只需要向下圖這樣創建一個DLL工程就好。

在生成的文件中,有個dllmain.cpp,打開以后內容如下:

當DLL的狀態發生變化的時候,就會調用DllMain函數。而傳遞的ul_reason_for_call這個參數代表了4種不同的狀態變化的情況,我們就可以根據這四種不同的狀態根據需要來寫出相應的代碼,就會讓注入的DLL執行我們需要的功能。
ul_reason_for_call的值代表的狀態DLL_PROCESS_ATTACHDll剛剛映射到進程空間中DLL_THREAD_ATTACH進程中有新線程創建DLL_THREAD_DETACH進程中有新線程銷毀DLL_PROCESS_DETACH Dll從進程空間中接觸映射 |
不過在實現DLL注入的時候用的DLL幾乎都是在Dll剛剛映射到進程空間的時候就執行相關的代碼。比如像下面這樣,創建一個新線程來執行代碼,這里在桌面打開一個文件來并寫入加載這個DLL的進程的完成路徑名。由于是獨占方式打開,此時如果多個線程同時打開這個文件,CreateFile就會出錯,錯誤碼就會是32,根據這個來對線程進行休眠,等其他線程使用完了,再次打開文件進行操作。
// dllmain.cpp : 定義 DLL 應用程序的入口點。#include #include #pragma comment(lib, "shell32.lib") #define FILE_NAME "result.txt" DWORD WINAPI ThreadProc(LPVOID lpParameter){ HANDLE hFile = NULL; CHAR szDesktopFile[MAX_PATH] = { 0 }; //保存系統桌面路徑 CHAR szFullFilePath[MAX_PATH] = { 0 }; //保存完成的加載DLL文件的文件路徑 DWORD dwRetLen = 0, dwFileLen = 0; BOOL bRet = TRUE; //獲取桌面路徑 bRet = SHGetSpecialFolderPath(NULL, szDesktopFile, CSIDL_DESKTOP, TRUE); if (bRet) { strcat(szDesktopFile, "\\"); strcat(szDesktopFile, FILE_NAME); while (TRUE) { hFile = CreateFile( szDesktopFile, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if (hFile == INVALID_HANDLE_VALUE) //打開文件錯誤 { if (GetLastError() == 32) //錯誤碼是不是其他進程正在使用這個文件,是的話等待一會在繼續打開 { Sleep(200); continue; } else break; } else { GetModuleFileName(NULL, szFullFilePath, MAX_PATH); //獲取加載DLL的進程的完整路徑 dwFileLen = strlen(szFullFilePath); szFullFilePath[dwFileLen] = '\r'; //由于是在WIN7運行,換行符是\r szFullFilePath[dwFileLen + 1] = ''; SetFilePointer(hFile, 0, NULL, FILE_END); WriteFile(hFile, szFullFilePath, dwFileLen + 2, &dwRetLen, NULL); if (hFile) CloseHandle(hFile); break; } } } return 0;} BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ){ switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: { HANDLE hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL); if (hThread) CloseHandle(hThread); break; } case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE;}
點擊生成解決方案以后就可以在項目目錄下找到相應的DLL文件,如下圖。這個文件就是用來注入到其他進程的DLL。

代碼框架
由于要編寫的代碼中,只有注入功能不同,但是其他的輔助功能。比如,提權,獲取進程PID等等是一樣的,為了避免重復就先在這給出代碼的框架。后面的不同注入技術只需根據需要加進去就好。注意,如果想要提權成功,需要用管理員權限運行代碼。
#include #include #include #define PROCESS_NAME "taskmgr.exe" //要注入的進程名,這個是任務管理器的進程名#define DLL_NAME "InjectDll.dll" //要注入的DLL的名稱 BOOL InjectDll(DWORD dwPid, CHAR szDllName[]); //注入DLLDWORD GetPID(PCHAR pProName); //根據進程名獲取PIDVOID ShowError(PCHAR msg); //打印錯誤信息BOOL EnbalePrivileges(HANDLE hProcess, char *pszPrivilegesName); //提升進程權限 int main(){ CHAR szDllPath[MAX_PATH] = { 0 }; //保存要注入的DLL的路徑 DWORD dwPID = 0; //保存要注入的進程的PID // 提升當前進程令牌權限 if (!EnbalePrivileges(GetCurrentProcess(), SE_DEBUG_NAME)) { printf("權限提升失敗"); } dwPID = GetPID(PROCESS_NAME); if (dwPID == 0) { printf("沒有找到要注入的進程"); goto exit; } GetCurrentDirectory(MAX_PATH, szDllPath); //獲取程序的目錄 strcat(szDllPath, "\\"); strcat(szDllPath, DLL_NAME); //與DLL名字拼接得到DLL的完整路徑 printf("要注入的進程名:%s PID:%d", PROCESS_NAME, dwPID); printf("要注入的DLL的完整路徑%s", szDllPath); if (InjectDll(dwPID, szDllPath)) { printf("Dll注入成功"); }exit: system("pause"); return 0;} BOOL InjectDll(DWORD dwPid, CHAR szDllName[]){ BOOL bRet = TRUE; return bRet;} DWORD GetPID(PCHAR pProName){ PROCESSENTRY32 pe32 = { 0 }; HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); BOOL bRet = FALSE; DWORD dwPID = 0; if (hSnap == INVALID_HANDLE_VALUE) { printf("CreateToolhelp32Snapshot process %d", GetLastError()); goto exit; } pe32.dwSize = sizeof(pe32); bRet = Process32First(hSnap, &pe32); while (bRet) { if (lstrcmp(pe32.szExeFile, pProName) == 0) { dwPID = pe32.th32ProcessID; break; } bRet = Process32Next(hSnap, &pe32); } CloseHandle(hSnap);exit: return dwPID;} VOID ShowError(PCHAR msg){ printf("%s Error %d", msg, GetLastError());} BOOL EnbalePrivileges(HANDLE hProcess, char *pszPrivilegesName){ HANDLE hToken = NULL; LUID luidValue = { 0 }; TOKEN_PRIVILEGES tokenPrivileges = { 0 }; BOOL bRet = FALSE; DWORD dwRet = 0; // 打開進程令牌并獲取具有 TOKEN_ADJUST_PRIVILEGES 權限的進程令牌句柄 if (!OpenProcessToken(hProcess, TOKEN_ADJUST_PRIVILEGES, &hToken)) { ShowError("OpenProcessToken"); goto exit; } // 獲取本地系統的 pszPrivilegesName 特權的LUID值 if (!LookupPrivilegeValue(NULL, pszPrivilegesName, &luidValue)) { ShowError("LookupPrivilegeValue"); goto exit; } // 設置提升權限信息 tokenPrivileges.PrivilegeCount = 1; tokenPrivileges.Privileges[0].Luid = luidValue; tokenPrivileges.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; // 提升進程令牌訪問權限 if (!AdjustTokenPrivileges(hToken, FALSE, &tokenPrivileges, 0, NULL, NULL)) { ShowError("AdjustTokenPrivileges"); goto exit; } else { // 根據錯誤碼判斷是否特權都設置成功 dwRet = ::GetLastError(); if (ERROR_SUCCESS == dwRet) { bRet = TRUE; goto exit; } else if (ERROR_NOT_ALL_ASSIGNED == dwRet) { ShowError("ERROR_NOT_ALL_ASSIGNED"); goto exit; } }exit: return bRet;}
遠程線程注入
這種注入方式可以說是最常用的注入方式了,它的核心就是調用Windows提供的CreateRemoteThread函數。該函數可以在其他的進程空間中創建一個新的線程進行執行,該函數在文檔中的定義如下:
HANDLE WINAPI CreateRemoteThread( __in HANDLE hProcess, __in LPSECURITY_ATTRIBUTES lpThreadAttributes, __in SIZE_T dwStackSize, __in LPTHREAD_START_ROUTINE lpStartAddress, __in LPVOID lpParameter, __in DWORD dwCreationFlags, __out LPDWORD lpThreadId);
參數說明hProcess要創建線程的進程句柄lpThreadAttributes新線程的安全描述符dwStackSize堆棧起始大小,為0表示默認大小lpStartAddress表示要運行線程的起始地址lpParameter保存要傳遞給線程參數的地址dwCreationFlags控制線程創建的標志,為0表示創建后立即執行lpThreadId指向接收線程標識符變量的指針。為NULL表示不返回線程標識符
其中的關鍵三個參數分別是:
(1)hProcess用來指定在哪個進程中創建新線程。
(2)lpStartAddress用來指定將進程中的哪個地址開始作為新線程運行的起始地址。
(3)lpParameter保存的也是一個地址,這個地址中保存的就是新線程要用到的參數。
那也就是說只要我們指定了一個地址給lpStartAddress,那么我們就可以在其他進程中創建一個線程來執行程序。而再看加載DLL的LoadLibrary函數在文檔中的定義如下:
HMODULE WINAPI LoadLibrary(__in LPCTSTR lpFileName);
可以看到,這個函數同樣也只需要一個參數,這個參數是一個地址,而這個地址中保存的是我們要加載的DLL的名稱的字符串。
根據這些,不難想到,只要我們可以獲取新進程中的LoadLibrary函數的地址以及包含有要加載的DLL的字符串的地址就可以通過CreateRemoteThread函數來成功開起一個線程執行LoadLibrary函數來加載我們的DLL。
那么現在的問題就是如何獲得LoadLibrary函數的地址以及保存有要加載的DLL路徑的字符串的地址。
對于LoadLibrary函數,由于它是在常用的系統DLL,也就是KERNEL32.dll中,所以這個DLL是可以按照它的ImageBase成功裝載到每個進程的空間中。這樣的話Kernel32.dll在每個進程中的起始地址是一樣的,那么LoadLibrary函數的地址也就會一樣。那么我們就可以在本進程中查找LoadLibrary函數的地址,并且完全可以相信,在要注入DLL的進程中LoadLibrary的地址也是這個。
至于DLL名稱的字符串,我們可以通過在進程中申請一塊可以將DLL完整路徑寫入的內存,并在這個內存中將DLL的完整路徑寫入,將寫入到注入進程DLL完整路徑的內存地址作為參數就可以實現進程的注入。
具體代碼如下:
BOOL InjectDll(DWORD dwPid, CHAR szDllName[]){ BOOL bRet = TRUE; HANDLE hProcess = NULL, hRemoteThread = NULL; HMODULE hKernel32 = NULL; DWORD dwSize = 0; LPVOID pDllPathAddr = NULL; PVOID pLoadLibraryAddr = NULL; // 打開注入進程,獲取進程句柄 hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid); if (NULL == hProcess) { ShowError("OpenProcess"); bRet = FALSE; goto exit; } // 在注入進程中申請可以容納DLL完成路徑名的內存空間 dwSize = 1 + strlen(szDllName); pDllPathAddr = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE); if (!pDllPathAddr) { ShowError("VirtualAllocEx"); bRet = FALSE; goto exit; } // 把DLL完整路徑名寫入進程中 if (!WriteProcessMemory(hProcess, pDllPathAddr, szDllName, dwSize, NULL)) { ShowError("WriteProcessMemory"); bRet = FALSE; goto exit; } hKernel32 = LoadLibrary("kernel32.dll"); if (hKernel32 == NULL) { ShowError("LoadLibrary"); bRet = FALSE; goto exit; } // 獲取LoadLibraryA函數地址 pLoadLibraryAddr = GetProcAddress(hKernel32, "LoadLibraryA"); if (pLoadLibraryAddr == NULL) { ShowError("GetProcAddress "); bRet = FALSE; goto exit; } //創建遠程線程進行DLL注入 hRemoteThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pLoadLibraryAddr, pDllPathAddr, 0, NULL); if (hRemoteThread == NULL) { ShowError("CreateRemoteThread"); bRet = FALSE; goto exit; } exit: if (hKernel32) FreeLibrary(hKernel32); if (hProcess) CloseHandle(hProcess); if (hRemoteThread) CloseHandle(hRemoteThread); return bRet;}
加強版遠程線程注入
上面的方法雖然可以方便的注入DLL,但是在WIN7、WIN10系統上,會由于SESSION 0隔離機制從而導致只能成功注入普通的用戶進程,如果注入系統進程就會導致失敗。
而經過逆向分析發現,使用Kernel32.dll中的CreateRemoteThread進行注入的時候,程序會走到ntdll.dll中的ZwCreateThreadEx函數進行執行。這是一個未導出的函數,所以需要手動獲取函數地址來進行調用,相比于CreateRemoteThread更加底層。這個函數在64位和32位系統中的函數聲明也不相同,在32位中的聲明如下:
typedef DWORD(WINAPI *pFnZwCreateThreadEx)( 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位中的聲明如下:
typedef DWORD(WINAPI *pFnZwCreateThreadEx)( 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);
根據逆向分析的結果,在內核6.0(WIN7, WIN10)等系統上調用CreateRemoteThread的時候,當程序走到ZwCreateThreaEx的時候它第7個參數,也就是CreateThreadFlags會被設置為1,如下圖:

它會導致線程創建的時候就被掛起,隨后查看要運行的進程所在的會話層之后再決定是否要恢復線程的運行。所以要破解這種情況只需要將第7個參數設為0就可以,相應代碼如下:
typedef DWORD(WINAPI *pFnZwCreateThreadEx)(PHANDLE, ACCESS_MASK, LPVOID, HANDLE, LPTHREAD_START_ROUTINE, LPVOID, BOOL, DWORD, DWORD, DWORD, LPVOID); BOOL InjectDll(DWORD dwPid, CHAR szDllName[]){ BOOL bRet = TRUE; HANDLE hProcess = NULL, hRemoteThread = NULL; HMODULE hKernel32 = NULL, hNtDll = NULL; DWORD dwSize = 0; LPVOID pDllPathAddr = NULL; PVOID pLoadLibraryAddr = NULL; pFnZwCreateThreadEx ZwCreateThreadEx = NULL; // 打開注入進程,獲取進程句柄 hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid); if (NULL == hProcess) { ShowError("OpenProcess"); bRet = FALSE; goto exit; } // 在注入進程中申請可以容納DLL完成路徑名的內存空間 dwSize = 1 + strlen(szDllName); pDllPathAddr = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE); if (!pDllPathAddr) { ShowError("VirtualAllocEx"); bRet = FALSE; goto exit; } // 把DLL完成路徑名寫入進程中 if (!WriteProcessMemory(hProcess, pDllPathAddr, szDllName, dwSize, NULL)) { ShowError("WriteProcessMemory"); bRet = FALSE; goto exit; } hKernel32 = LoadLibrary("kernel32.dll"); if (hKernel32 == NULL) { ShowError("LoadLibrary kernel32"); bRet = FALSE; goto exit; } // 獲取LoadLibraryA函數地址 pLoadLibraryAddr = GetProcAddress(hKernel32, "LoadLibraryA"); if (pLoadLibraryAddr == NULL) { ShowError("GetProcAddress LoadLibraryA"); bRet = FALSE; goto exit; } hNtDll = LoadLibrary("ntdll.dll"); if (hNtDll == NULL) { ShowError("LoadLibrary ntdll"); bRet = FALSE; goto exit; } ZwCreateThreadEx = (pFnZwCreateThreadEx)GetProcAddress(hNtDll, "ZwCreateThreadEx"); if (!ZwCreateThreadEx) { ShowError("GetProcAddress ZwCreateThreadEx"); bRet = FALSE; goto exit; } ZwCreateThreadEx(&hRemoteThread, PROCESS_ALL_ACCESS, NULL, hProcess, (LPTHREAD_START_ROUTINE)pLoadLibraryAddr, pDllPathAddr, 0, 0, 0, 0, NULL); if (hRemoteThread == NULL) { ShowError("ZwCreateThreadEx"); bRet = FALSE; goto exit; }exit: if (hKernel32) FreeLibrary(hKernel32); if (hNtDll) FreeLibrary(hNtDll); if (hProcess) CloseHandle(hProcess); if (hRemoteThread) CloseHandle(hRemoteThread); return bRet;}
APC注入
在Windows系統中,每個線程都會維護一個自己的APC隊列,這個APC隊列中保存了要求線程執行的一些APC函數。對于用戶模式的APC隊列,當線程處在可警告狀態時,就會執行這些APC函數。而要往APC隊列中增加APC函數,需要通過QueueUserAPC函數來實現,這個函數在文檔中的定義如下:
DWORD WINAPI QueueUserAPC( __in PAPCFUNC pfnAPC, __in HANDLE hThread, __in ULONG_PTR dwData);
參數 說明 pfnAPC 當滿足條件時,要執行的APC函數的地址 hThread 指定增加APC函數的線程句柄 dwData 要執行的APC函數參數地址 |
可以看到pfnAPC和dwData這兩個參數和CreateRemoteThread中的lpStartAddress和lpParameter的作用是一樣的。不過這里是對線程進行操作,一個進程有多個線程。所以為了確保程序正確運行,所以需要遍歷所有線程,查看是否是要注入的進程的線程,依次獲得句柄插入APC函數。具體代碼如下:
BOOL InjectDll(DWORD dwPid, CHAR szDllName[]){ BOOL bRet = TRUE; HANDLE hProcess = NULL, hThread = NULL, hSnap = NULL; HMODULE hKernel32 = NULL; DWORD dwSize = 0; PVOID pDllPathAddr = NULL; PVOID pLoadLibraryAddr = NULL; THREADENTRY32 te32 = { 0 }; // 打開注入進程,獲取進程句柄 hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid); if (NULL == hProcess) { ShowError("OpenProcess"); bRet = FALSE; goto exit; } // 在注入進程中申請可以容納DLL完成路徑名的內存空間 dwSize = 1 + strlen(szDllName); pDllPathAddr = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE); if (!pDllPathAddr) { ShowError("VirtualAllocEx"); bRet = FALSE; goto exit; } // 把DLL完成路徑名寫入進程中 if (!WriteProcessMemory(hProcess, pDllPathAddr, szDllName, dwSize, NULL)) { ShowError("WriteProcessMemory"); bRet = FALSE; goto exit; } hKernel32 = LoadLibrary("kernel32.dll"); if (hKernel32 == NULL) { ShowError("LoadLibrary"); bRet = FALSE; goto exit; } // 獲取LoadLibraryA函數地址 pLoadLibraryAddr = GetProcAddress(hKernel32, "LoadLibraryA"); if (pLoadLibraryAddr == NULL) { ShowError("GetProcAddress"); bRet = FALSE; goto exit; } //獲得線程快照 hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); if (!hSnap) { ShowError("CreateToolhelp32Snapshot"); bRet = FALSE; goto exit; } //遍歷線程 te32.dwSize = sizeof(te32); if (Thread32First(hSnap, &te32)) { do { //這個線程的進程ID是不是要注入的進程的PID if (te32.th32OwnerProcessID == dwPid) { hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, te32.th32ThreadID); if (hThread) { QueueUserAPC((PAPCFUNC)pLoadLibraryAddr, hThread, (ULONG_PTR)pDllPathAddr); CloseHandle(hThread); hThread = NULL; } else { ShowError("OpenThread"); bRet = FALSE; goto exit; } } } while (Thread32Next(hSnap, &te32)); }exit: if (hKernel32) FreeLibrary(hKernel32); if (hProcess) CloseHandle(hProcess); if (hThread) CloseHandle(hThread); return bRet;}
AppInit_DLLs注入
這種注入方式主要是通過修改注冊表中HKEY_LOCAL_MACHINE\\Software\\Microsoft\\Windows NT\\CurrentVersion\\Windows中的AppInit_DLLs和LoadAppInit_Dlls,如下圖:

只要將AppInit_DLLs設置為要注入的DLL的路徑并且將LoadAppInit_DLLs的值改成1。那么,當程序重啟的時候,所有加載user32.dll的進程都會根據AppInit_Dlls中的DLL路徑加載指定的DLL。
所以這種DLL注入的實現代碼如下:
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;}
運行程序以后,會發現相應的鍵值已經被設置。


全局鉤子注入
Windows系統中的大多數應用都是基于消息機制的,也就是說它們都有一個消息過程函數,可以根據收到的不同消息來執行不同的代碼。基于這種消息機制,Windows維護了一個OS message queue以及為每個程序維護著一個application message queue。當發生各種事件的時候,比如敲擊鍵盤,點擊鼠標等等,操作系統會從OS message queue將消息取出給到相應的程序的application message queue。
而OS message queue和application message queue的中間有一個稱為鉤鏈的結果如下:

在這個鉤鏈中保存的就是設置的各種鉤子函數,而這些鉤子函數會比應用程序還早接收到消息并對消息進行處理。所以程序員可以通過在鉤子中設置鉤子函數,而要設置鉤子函數就需要使用SetWindowHookEx來將鉤子函數安裝到鉤鏈中,函數在文檔中的定義如下:
HHOOK SetWindowsHookEx(int idHook, HOOKPROC lpfn, HINSTANCE hMod, DWORD dwThreadId);
參數 說明 idHook 要安裝的鉤子類型,為了掛全局鉤子,這里選擇WH_GETMESSAGE。表示的是安裝一個掛鉤過程,它監視發送到消息隊列的消息 lpfn 表示的是鉤子的回調函數。如果dwThreadId為0,則lpfn指向的鉤子過程必須指向DLL中的鉤子過程 hMod 包含由lpfn參數執行的鉤子過程的DLL句柄 dwThreadId 與鉤子過程關聯的線程標識符,如果為0則表示與所有線程相關聯。 |
如果函數成功,則返回鉤子過程的句柄,否則為NULL。
根據上面的介紹可以得知,想要創建一個全局鉤子,就必須在DLL文件中創建。這是因為進程的地址空間是獨立的,發生對應事件的進程不能調用其他進程地址空間的鉤子函數。如果鉤子函數的實現代碼在DLL中,則在對應事件發生時,系統會把這個DLL加載到發生事件的進程地址空間中,使它可以調用鉤子函數進行處理。
所以只要在系統中安裝了全局鉤子,那么只要進程接收到可以發出鉤子的消息,全局鉤子的DLL就會被系統自動或者強行加載到進程空間中,這就可以實現DLL注入。
而這里之所以設置為WH_GETMESSAGE,是因為這種類型的鉤子會監視消息隊列,又因為Windows系統是基于消息驅動的,所以所有的進程都會有自己的一個消息隊列,都會加載WH_GETMESSAGE類型的全局鉤子。
由于設置全局鉤子的代碼需要在DLL文件中完成,所以首先需要新建一個InjectDll.cpp。

隨后在文件中寫入如下設置全局鉤子的函數:
extern HMODULE g_hDllModule;// 設置全局鉤子BOOL SetGlobalHook(){ g_hHook = SetWindowsHookEx(WH_GETMESSAGE, (HOOKPROC)GetMsgProc, g_hDllModule, 0); if (NULL == g_hHook) { return FALSE; } return TRUE;}
其中的回調函數的實現如下:
// 鉤子回調函數LRESULT GetMsgProc( int code, WPARAM wParam, LPARAM lParam){ return CallNextHookEx(g_hHook, code, wParam, lParam);}
這里只是簡單的調用CallNextHookEx函數表示將當前鉤子傳遞給鉤鏈中的下一個鉤子,第一個參數要指定當前鉤子的句柄。如果直接返回0,則表示中斷鉤子傳遞,這就實現了對鉤子進行攔截。
而g_hDllModule則是在DLL加載的時候被賦值的。

當鉤子不再使用,可以卸載掉全局鉤子,這樣此時已經包含鉤子回調函數的DLL模塊的進程就會釋放DLL模塊。卸載鉤子的代碼如下:
// 卸載鉤子BOOL UnSetGlobalHook(){ if (g_hHook) { UnhookWindowsHookEx(g_hHook); } return TRUE;}
上面的全局鉤子的設置,鉤子回調函數的實現以及全局鉤子的卸載都需要使用到全局鉤子的句柄。為了讓任意一個獨立的進程中對句柄的修改都可以影響到其他進程,就需要在DLL中使用共享內存的,來保證將DLL中加載到多個進程以后,一個進程對它的修改可以影響到其他進程。設置共享內存的方式如下:
// 共享內存#pragma data_seg("mydata")HHOOK g_hHook = NULL;#pragma data_seg()#pragma comment(linker, "/SECTION:mydata,RWS")
而為了調用設置鉤子和卸載鉤子的函數,就需要創建一個.def文件來將兩個函數導出。

此時使用PEID查看InjectDll.dll可以看到導出表有如下的導出函數:

接下來只要在代碼中將DLL引入并或者對應的函數對它們進行調用就好。
BOOL InjectDll(DWORD dwPid, CHAR szDllName[]){ BOOL bRet = TRUE; HMODULE hDll = NULL; pFnSetGlobalHook SetGlobalHook = NULL; pFnUnSetGlobalHook UnSetGlobalHook = NULL; hDll = LoadLibrary(szDllName); if (hDll == NULL) { ShowError("LoadLibrary"); bRet = FALSE; goto exit; } SetGlobalHook = (pFnSetGlobalHook)GetProcAddress(hDll, "SetGlobalHook"); if (SetGlobalHook == NULL) { ShowError("GetProcAddress SetGlobalHook"); bRet = FALSE; goto exit; } if (!SetGlobalHook()) { printf("鉤子安裝失敗"); bRet = FALSE; goto exit; } printf("鉤子安裝成功,按回車卸載鉤子"); system("pause"); UnSetGlobalHook = (pFnUnSetGlobalHook)GetProcAddress(hDll, "UnSetGlobalHook"); if (UnSetGlobalHook == NULL) { ShowError("GetProcAddress UnSetGlobalHook"); bRet = FALSE; goto exit; } if (UnSetGlobalHook()) { printf("已將全局鉤子卸載"); }exit: return bRet;}
實驗結果
將編譯好的exe文件和dll文件放到同一路徑中,運行exe以后會在桌面生成一個result.txt文件。打開文件以后會看到里面的內容是被注入的進程的完整的路徑名。
