一、注册表方式
1.1 注入方法
如题,通过注册表的方式来实现 DLL 注入,我们只需要针对特定的注册表项进行修改即可,有一点需要注意的是:如果被注入的进程是 64 位进程,则注入的 DLL 也需要是 64 位的。同理,注入到 32 位的进程也需要是 32 位的 DLL。
另外,根据被注入目标进程的位数(32 或 64)不同,注册表的位置也不同。
1.1.1 注入 64 位系统上的 32 位进程
在64位Windows系统上,将DLL注入到32位进程,步骤如下:
将被注入的 DLL 名称填入到AppInit_DLLs注册表项:
1
| HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows NT\CurrentVersion\Windows\AppInit_DLLs
|
AppInit_DLLs中的文件名通过逗号或空格来分割,在文件名中要避免使用空格。
另外AppInit_DLLs中的第一个文件可以包含路径,而后面的文件的路径则将被忽略。出于这个原因,我们最好将 DLL 文件放到 Windows 的系统目录中。
并将LoadAppInit_DLLs注册表项的值修改为1:
1
| HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows NT\CurrentVersion\Windows\LoadAppInit_DLLs
|
1.1.2 注入 64 位进程
在64位Windows系统上,将DLL注入到64位进程,步骤与“注入 64 位系统上的 32 位进程”类似,区别在于:
注册表位置不一样,注入 64 位进程的注册表分别位于:
1 2 3
| HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows\AppInit_DLLs
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows\LoadAppInit_DLLs
|
被注入 DLL 需要是 64 位版本。
1.2 原理
为什么可以通过上述方式实现DLL注入呢?
当系统的User32.dll被加载到新进程时,会在DLL_PROCESS_ATTACH通知处理过程中读取AppInit_DLLs注册表值,并依次加载该项中的每个 DLL。
1.3 弊端
这么做有什么弊端?
依据上面一节的介绍,我们可以知道被注入的 DLL 是通过 User32.dll 加载到目标进程中去的,这也就要求被注入的目标进程必须使用了 User32.dll,虽然基于 GUI 的程序都会使用这个 DLL 文件,但命令行程序一般不会加载 User32.dll,所以可能无法通过这种方式被注入。
而且这种方式会导致系统上所有使用了 User32.dll 的程序都会被注入DLL,很多时候这也并不是我们所期望的。
二、消息钩子方式
2.1 钩子技术
Windows 提供了三个 API 让我们可以很方便使用钩子技术将 DLL 文件注入到进程之中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| HHOOK SetWindowsHookEx(int idHook, HOOKPROC lpfn, HINSTANCE hMod, DWORD dwThreadId );
BOOL UnhookWindowsHookEx(HHOOK hhk );
LRESULT CallNextHookEx(HHOOK hhk, int nCode, WPARAM wParam, LPARAM lParam );
|
Hook中文名“钩子”,我们可以把它想象成一个“鱼钩”,用来钩住指定类型的消息。我们可以指定“钩子”需要钩住哪个线程的消息,可以是当前线程,也可以是所有线程。
当“指定线程”的“指定消息”被钩住时,系统就会将我们的 DLL(如果钩子的处理过程位于 DLL 中)加载到该线程所属进程的地址空间中,并且在该地址空间中调用我们的 DLL 中的钩子处理过程函数,从而实现注入功能。
我们通过SetWindowsHookEx函数来安装一个钩子,操作系统同时也允许开发人员为“同一个线程”的“同一个消息类型”指定多个钩子,这样就形成了一个“钩子链”(Hook Chain)。
2.1.1 SetWindowsHookEx
1 2 3 4 5 6
| HHOOK SetWindowsHookExA( [in] int idHook, [in] HOOKPROC lpfn, [in] HINSTANCE hmod, [in] DWORD dwThreadId );
|
idHook参数:指定我们需要勾住的消息类型;
lpfn参数:函数指针。当 idHook 指定的消息触发时,系统将会调用 lpfn 函数指针。
hMod参数:lpfn 函数指针所在 DLL 的句柄。有 2 种情况下这个参数需要传 NULL:
- lpfn 函数的代码位于本进程内时。
- 只需要勾住本进程的消息时,即 dwThreadId 参数指定的线程位于当前进程。
dwThreadId参数:线程 ID,用于指定勾住哪个线程的消息。如果传 0,则表示勾住所有线程的指定消息。
SetWindowsHookEx 详细的参数解释可以参考 MSDN。
2.1.2 CallNextHookEx
当我们的钩子处理函数(由lpfn参数指定)将消息处理完之后,我们可以选择将消息丢弃,不让钩子链后面的钩子进行处理;也可以在钩子处理函数的最后调用CallNextHookEx函数,让消息继续传递下去,从而让其他钩子有处理的机会。
2.1.3 UnhookWindowsHookEx
UnhookWindowsHookEx函数用于将指定钩子从钩子链中移除。
即使不调用UnhookWindowsHookEx,在调用SetWindowsHookEx的进程退出后,钩子也将被自动移除。
2.2 钩子实例
SetWindowsHookEx函数返回一个HHOOK类型的钩子句柄,CallNextHookEx和UnhookWindowsHookEx函数都需要使用这个句柄作为参数。
如果我们将“注入逻辑”放在独立的 exe 中,将“钩子处理过程”放到独立的 dll 中,那么为了在“钩子处理过程”中调用CallNextHookEx时能够拿到钩子的句柄,我们需要通过其他途径将该句柄从 exe 传递到 dll 中。
所以为了避免传递“钩子句柄”的麻烦,我们将“注入逻辑”和“钩子处理过程”都写入到一个 DLL 之中。我们只需要调用这个 DLL 的导出函数就可以将这个 DLL 注入到指定线程所属的进程中。
示例 DLL 名称为InjectDLL,用于勾住指定窗口的WH_GETMESSAGE消息,我们也可以指定其他的消息类型,如键盘消息等。完整的消息类型可以参考 MSDN 上关于SetWindowsHookEx函数的解释。
InjectDLL示例完整代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
| #include <stdio.h> #include <windows.h> #include <tchar.h>
HHOOK g_hook = NULL;
BOOL APIENTRY DllMain(HMODULE hModule, DWORD fdwReason, LPVOID lpReserved) { switch(fdwReason) { case DLL_PROCESS_ATTACH: { break; } case DLL_THREAD_ATTACH: { break; } case DLL_THREAD_DETACH: { break; } case DLL_PROCESS_DETACH: { break; } } return TRUE; }
BOOL EnablePrivilege(LPCTSTR szPrivilege, BOOL fEnable) { BOOL fOk = FALSE; HANDLE hToken = NULL;
if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken)) { TOKEN_PRIVILEGES tp; tp.PrivilegeCount = 1; LookupPrivilegeValue(NULL, szPrivilege, &tp.Privileges[0].Luid); tp.Privileges->Attributes = fEnable ? SE_PRIVILEGE_ENABLED : 0; AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), NULL, NULL); fOk = (GetLastError() == ERROR_SUCCESS);
CloseHandle(hToken); }
return fOk; }
LRESULT CALLBACK HookProc_GetMsg(int code, WPARAM wParam, LPARAM lParam) { char szMsg[512] = { 0 }; sprintf_s(szMsg, 512, "code: %d, wParam: %d, lParam: %d", code, wParam, lParam); OutputDebugStringA(szMsg);
return CallNextHookEx(g_hook, code, wParam, lParam);; }
HHOOK InjectDllByHook(HWND hwnd) { DWORD dwThreadId = 0; HHOOK hHook = NULL;
__try { if (!EnablePrivilege(SE_DEBUG_NAME, TRUE)) { __leave; }
dwThreadId = GetWindowThreadProcessId(hwnd, NULL); if (dwThreadId == 0) { __leave; }
HMODULE hModule = NULL; GetModuleHandleExW( GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, (LPCWSTR)InjectDllByHook, &hModule);
hHook = SetWindowsHookEx(WH_GETMESSAGE, (HOOKPROC)HookProc_GetMsg, hModule, dwThreadId); } __finally {
}
return (hHook); }
BOOL EjectDllByHook(HHOOK hook) { return UnhookWindowsHookEx(hook); }
|
直接调用该 DLL 的导出函数InjectDllByHook,实现安装钩子并Hook指定窗口的指定消息。当指定窗口消息被勾住时,我们的 DLL 也就会被加载到了窗口所属的进程地址空间中,从而实现注入。
三、远程线程方式
3.1 远程线程原理
“注册表注入方式”由于不能精确指定需要注入的目标进程,而且只能注入到 GUI 程序中,灵活性较差;
“钩子注入方式”虽然能够精确指定被注入的目标线程,但只能针对特定类型的窗口消息进行 Hook,对于类似 Windows 服务这样的非GUI程序就束手无策了。
本节介绍的“远程线程的注入方式”是在实际中使用较为广泛的一种注入方式,它既可以精确指定需要注入的进程,又可以注入到非 GUI 程序中。
远程线程注入方式使用的关键系统 API 为CreateRemoteThread,原型如下:
1 2 3 4 5 6 7 8 9
| 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 );
|
CreateRemoteThread的参数和我们平时创建本地线程使用的CreateThread的参数类似,新增了hProcess句柄参数用于指定在哪个进程创建远程线程,通过OpenProcess可以获取到进程句柄,但需要注意权限问题,可能由于权限不足,获取进程句柄失败。
通过远程线程方式实现 DLL 注入主要是在lpStartAddress和lpParameter这 2 个参数上面做文章。
lpStartAddress参数是函数指针类型,为远程线程的处理过程函数,函数原型分别如下:
1
| DWORD WINAPI ThreadProc(LPVOID lpParameter);
|
我们知道加载 DLL 使用的 API 是LoadLibraryA或LoadLibraryW,原型如下:
1
| HMODULE WINAPI LoadLibrary(LPCTSTR lpFileName);
|
对比LoadLibrary和线程处理函数(LPTHREAD_START_ROUTINE)的原型,不难发现两者的函数的原型基本相同。虽然不是完全相同,但都是接收一个指针参数,而且都是返回一个值,并且调用约定也都是WINAPI。
因此,我们完全可以利用它们之间的相似性,把线程处理函数的地址设为LoadLibraryA或LoadLibraryW,如:
1 2 3 4 5 6 7 8
| HANDLE hThread = CreateRemoteThread(hProcessRemote, NULL, 0, LoadLibraryA, "C:\\InjectDll.dll", 0, NULL );
|
当CreateRemoteThread所创建的线程在远程进程地址空间中被创建的时候,就会立即调用LoadLibraryA函数,并传入 DLL 路径的地址作为其参数,从而实现DLL加载。但这还没完,请继续看下面章节。
3.2 注意事项
按照上面的介绍的方法很容易就能实现远程线程注入,实际上也的确是很容易实现,只是还有几个地方需要注意:
- LoadLibrary 函数地址
- DLL 路径字符串地址
- 取消注入
LoadLibrary 函数地址
我们不能向上面的代码那样直接把LoadLibraryA或LoadLibraryW作为第 4 个参数传给CreateRemoteThread函数。这涉及模块的导入段等问题,如果在调用CreateRemoteThread时直接引用LoadLibraryA函数地址,该引用会被解析为我们被注入 DLL 的导入段中的LoadLibraryA函数地址,如果把这个函数地址作为远程线程的起始地址传入,其结果很可能是访问违规。
我们必须通过 GetProcAddress 来得到LoadLibraryA的确切地址,如:
1 2
| HMODULE hKernel32 = GetModuleHandle(TEXT("kernel32.dll")); LPVOID pLoadLibraryAAddr = (LPVOID)GetProcAddress(hKernel32, "LoadLibraryA");
|
DLL 路径字符串地址
DLL 路径字符串"C:\\InjectDll.dll"的内存地址位于调用进程的地址空间中,并不位于被注入的进程的地址空间中。所以,当LoadLibraryA用该地址访问被注入进程地址空间时,会导致访问违规。
如果对进程的地址空间不了解,可以参考:系列文章。
为了解决这个问题,我们需要把 DLL 的路径字符串存储到被注入进程的地址空间中。Windows 提供的VirtualAllocEx函数可以实现在其他进程的地址空间中分配内存块,实现过程大致如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| hTargeProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessID); if (!hTargeProcess) { return; }
SIZE_T dllPathSize = strlen(pszDllPath); pVM4DllPath = VirtualAllocEx(hTargeProcess, NULL, dllPathSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (!pVM4DllPath) { return; }
if (!WriteProcessMemory(hTargeProcess, pVM4DllPath, pszDllPath, dllPathSize, NULL)) { return; }
|
取消注入
取消注入就是将 DLL 从目标进程卸载,卸载 DLL 所用的 API 是FreeLibrary,但我们不能直接调用这个函数,因为直接调用的话是在我们的进程中卸载 DLL,而不是目标进程中卸载,很显然这样达不到卸载的目的。我们需要和加载 DLL 时一样,将FreeLibrary的地址作为第 4 个参数传给CreateRemoteThread函数,但同样需要通过GetProcAddress来得到FreeLibrary的确切地址:
1 2 3 4
| PTHREAD_START_ROUTINE pfnThreadRtn = (PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "FreeLibrary"); if(pfnThreadRtn) { hThread = CreateRemoteThread(hProcess, NULL, 0, pfnThreadRtn, me.modBaseAddr, 0, NULL); }
|
3.3 实例
本实例中的InjectDllByRemoteThread和EjectDllByRemoteThread两个函数使用远程线程的方式分别实现了注入和取消注入的功能。
3.3.1 InjectDllByRemoteThread 函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| BOOL InjectDllByRemoteThread(DWORD dwProcessID, const char* pszDllPath) { BOOL bRet = FALSE; const DWORD dwThreadSize = 50 * 1024; HANDLE hTargeProcess = NULL; HANDLE hRemoteThread = NULL; PVOID pVM4LoadLibrary = NULL; PVOID pVM4DllPath = NULL;
__try { if (!EnablePrivilege(SE_DEBUG_NAME, TRUE)) { __leave; }
hTargeProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessID); if (!hTargeProcess) { __leave; }
SIZE_T dllPathSize = strlen(pszDllPath); pVM4DllPath = VirtualAllocEx(hTargeProcess, NULL, dllPathSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); if (!pVM4DllPath) { __leave; }
if (!WriteProcessMemory(hTargeProcess, pVM4DllPath, pszDllPath, dllPathSize, NULL)) { __leave; }
HMODULE hKernel32 = GetModuleHandle(TEXT("kernel32.dll")); LPVOID pLoadLibraryAAddr = (LPVOID)GetProcAddress(hKernel32, "LoadLibraryA");
hRemoteThread = CreateRemoteThread(hTargeProcess, NULL, 0, (DWORD(WINAPI *)(LPVOID))pLoadLibraryAAddr, pVM4DllPath, 0, NULL); if (!hRemoteThread) { __leave; }
WaitForSingleObject(hRemoteThread, INFINITE);
DWORD dwExitCode = 0; BOOL B = GetExitCodeThread(hRemoteThread, &dwExitCode);
bRet = TRUE; } __finally { if (hTargeProcess && pVM4DllPath) { VirtualFreeEx(hTargeProcess, pVM4DllPath, dwThreadSize, MEM_RELEASE); }
if (hRemoteThread) { CloseHandle(hRemoteThread); }
if (hTargeProcess) { CloseHandle(hTargeProcess); } }
return bRet; }
|
3.3.2 EjectDllByRemoteThread 函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| BOOL EjectDllByRemoteThread(DWORD dwProcessID, LPCWSTR pszDllPath) { BOOL bOk = FALSE; HANDLE hTHSnapshot = NULL; HANDLE hProcess = NULL, hThread = NULL;
__try { hTHSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwProcessID); if(hTHSnapshot == INVALID_HANDLE_VALUE) { __leave; }
MODULEENTRY32W me = {sizeof(me)}; BOOL bFound = FALSE; BOOL bMoreMods = Module32FirstW(hTHSnapshot, &me); for(; bMoreMods; bMoreMods = Module32NextW(hTHSnapshot, &me)) { bFound = (_wcsicmp(me.szModule, pszDllPath) == 0) || (_wcsicmp(me.szExePath, pszDllPath) == 0); if(bFound) break; }
if(!bFound) { __leave; }
hProcess = OpenProcess( PROCESS_QUERY_INFORMATION | PROCESS_CREATE_THREAD | PROCESS_VM_OPERATION, FALSE, dwProcessID); if(hProcess == NULL) { __leave; }
PTHREAD_START_ROUTINE pfnThreadRtn = (PTHREAD_START_ROUTINE) GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "FreeLibrary"); if(pfnThreadRtn == NULL) { __leave; }
hThread = CreateRemoteThread(hProcess, NULL, 0, pfnThreadRtn, me.modBaseAddr, 0, NULL); if(hThread == NULL) { __leave; }
WaitForSingleObject(hThread, INFINITE);
bOk = TRUE; } __finally { if(hTHSnapshot != NULL) { CloseHandle(hTHSnapshot); } if(hThread != NULL) { CloseHandle(hThread); } if(hProcess != NULL) { CloseHandle(hProcess); } }
return(bOk); }
|
3.3.3 DllMain 函数
使用远程线程的方式进行 DLL 注入时,我们一般在 DllMain 的DLL_PROCESS_ATTACH条件分支开始业务逻辑(通常会另外创建一个子线程,将业务逻辑放到子线程中处理),在DLL_PROCESS_DETACH条件分支处结束业务逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| BOOL APIENTRY DllMain(HMODULE hModule, DWORD fdwReason, LPVOID lpReserved) { HANDLE hThread = NULL;
switch(fdwReason) { case DLL_PROCESS_ATTACH: { g_hDllModule = hModule;
hThread = (HANDLE)_beginthreadex(NULL, 0, PluginProc, NULL, 0, NULL); if (hThread) { CloseHandle(hThread); } break; } case DLL_THREAD_ATTACH: { break; } case DLL_THREAD_DETACH: { break; } case DLL_PROCESS_DETACH: { break; } } return TRUE; }
|
四、APC方式
TODO