一、注册表方式
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