一、注册表方式

1.1 注入方法

如题,通过注册表的方式来实现 DLL 注入,我们只需要针对特定的注册表项进行修改即可,有一点需要注意的是:如果被注入的进程是 64 位进程,则注入的 DLL 也需要是 64 位的。同理,注入到 32 位的进程也需要是 32 位的 DLL。

另外,根据被注入目标进程的位数(32 或 64)不同,注册表的位置也不同。

1.1.1 注入 64 位系统上的 32 位进程

在64位Windows系统上,将DLL注入到32位进程,步骤如下:

  1. 将被注入的 DLL 名称填入到AppInit_DLLs注册表项:

    1
    HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows NT\CurrentVersion\Windows\AppInit_DLLs

    AppInit_DLLs中的文件名通过逗号或空格来分割,在文件名中要避免使用空格。

    另外AppInit_DLLs中的第一个文件可以包含路径,而后面的文件的路径则将被忽略。出于这个原因,我们最好将 DLL 文件放到 Windows 的系统目录中。

  2. 并将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 位进程”类似,区别在于:

  1. 注册表位置不一样,注入 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
  2. 被注入 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:

  1. lpfn 函数的代码位于本进程内时。
  2. 只需要勾住本进程的消息时,即 dwThreadId 参数指定的线程位于当前进程。

dwThreadId参数:线程 ID,用于指定勾住哪个线程的消息。如果传 0,则表示勾住所有线程的指定消息。

SetWindowsHookEx 详细的参数解释可以参考 MSDN

2.1.2 CallNextHookEx

当我们的钩子处理函数(由lpfn参数指定)将消息处理完之后,我们可以选择将消息丢弃,不让钩子链后面的钩子进行处理;也可以在钩子处理函数的最后调用CallNextHookEx函数,让消息继续传递下去,从而让其他钩子有处理的机会。

2.1.3 UnhookWindowsHookEx

UnhookWindowsHookEx函数用于将指定钩子从钩子链中移除。

即使不调用UnhookWindowsHookEx,在调用SetWindowsHookEx的进程退出后,钩子也将被自动移除。

2.2 钩子实例

SetWindowsHookEx函数返回一个HHOOK类型的钩子句柄,CallNextHookExUnhookWindowsHookEx函数都需要使用这个句柄作为参数。

如果我们将“注入逻辑”放在独立的 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;
}



// 提权函数
// 参考:https://blog.csdn.net/china_jeffery/article/details/79173417
//
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;
}

// 获取DLL自身的句柄
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 注入主要是在lpStartAddresslpParameter这 2 个参数上面做文章。

lpStartAddress参数是函数指针类型,为远程线程的处理过程函数,函数原型分别如下:

1
DWORD WINAPI ThreadProc(LPVOID lpParameter);

我们知道加载 DLL 使用的 API 是LoadLibraryALoadLibraryW,原型如下:

1
HMODULE WINAPI LoadLibrary(LPCTSTR lpFileName);

对比LoadLibrary和线程处理函数(LPTHREAD_START_ROUTINE)的原型,不难发现两者的函数的原型基本相同。虽然不是完全相同,但都是接收一个指针参数,而且都是返回一个值,并且调用约定也都是WINAPI

因此,我们完全可以利用它们之间的相似性,把线程处理函数的地址设为LoadLibraryALoadLibraryW,如:

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 函数地址

我们不能向上面的代码那样直接把LoadLibraryALoadLibraryW作为第 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
// dwProcessID为被注入目标进程的进程ID
hTargeProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessID);
if (!hTargeProcess) {
return;
}

SIZE_T dllPathSize = strlen(pszDllPath); // pszDllPath存储了DLL的路径
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 实例

本实例中的InjectDllByRemoteThreadEjectDllByRemoteThread两个函数使用远程线程的方式分别实现了注入和取消注入的功能。

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, // For CreateRemoteThread
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;
// 使用注册表方式和CreateRemoteThread方式注入时,一般在此处创建线程
//

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