第41章 DLL注入技术(一):远程线程注入、SetWindowsHookEx注入

DLL注入,说白了就是让一个进程强行加载你写的DLL。这招在逆向工程里太常用了——你想hook某个API、监控程序行为、或者给目标进程打补丁,都得先把代码塞进去。

我刚开始搞逆向那会儿,觉得注入这事儿挺神秘的。后来做多了才发现,原理其实不复杂。Windows提供了多种注入方式,这一章我们先聊两个最经典的:远程线程注入SetWindowsHookEx注入

41.1 远程线程注入

远程线程注入,是我个人用得最多的一种方式。它的核心思路就一句话:在目标进程里创建一个线程,让这个线程去加载你的DLL

具体怎么做?Windows提供了几个关键API:

  • OpenProcess — 打开目标进程,拿到句柄
  • VirtualAllocEx — 在目标进程里分配内存
  • WriteProcessMemory — 把DLL路径写进去
  • CreateRemoteThread — 在目标进程里创建线程
  • LoadLibraryA — 让线程执行这个函数,加载DLL

嗯,这里要注意:CreateRemoteThread的线程函数地址,我们传的是LoadLibraryA的地址。为什么能这么干?因为kernel32.dll在每个进程里的加载地址基本一样(除非开启了ASLR但没开重定位)。

核心原理:远程线程注入的本质,就是利用CreateRemoteThread让目标进程执行LoadLibrary,从而加载你的DLL。

代码实现

我写个简单的例子,你一看就明白:

// 远程线程注入示例
BOOL InjectDLL(DWORD dwProcessId, const char* szDllPath) {
    HANDLE hProcess = NULL;
    HANDLE hThread = NULL;
    LPVOID pRemoteBuf = NULL;
    HMODULE hKernel32 = NULL;
    LPTHREAD_START_ROUTINE pLoadLibrary = NULL;

    // 1. 打开目标进程
    hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);
    if (!hProcess) return FALSE;

    // 2. 在目标进程分配内存
    int nPathLen = strlen(szDllPath) + 1;
    pRemoteBuf = VirtualAllocEx(hProcess, NULL, nPathLen,
                                MEM_COMMIT, PAGE_READWRITE);
    if (!pRemoteBuf) {
        CloseHandle(hProcess);
        return FALSE;
    }

    // 3. 写入DLL路径
    WriteProcessMemory(hProcess, pRemoteBuf,
                       (LPVOID)szDllPath, nPathLen, NULL);

    // 4. 获取LoadLibraryA地址
    hKernel32 = GetModuleHandleA("kernel32.dll");
    pLoadLibrary = (LPTHREAD_START_ROUTINE)
                   GetProcAddress(hKernel32, "LoadLibraryA");

    // 5. 创建远程线程
    hThread = CreateRemoteThread(hProcess, NULL, 0,
                                 pLoadLibrary, pRemoteBuf, 0, NULL);
    if (!hThread) {
        VirtualFreeEx(hProcess, pRemoteBuf, 0, MEM_RELEASE);
        CloseHandle(hProcess);
        return FALSE;
    }

    // 6. 等待线程结束,清理
    WaitForSingleObject(hThread, INFINITE);
    CloseHandle(hThread);
    CloseHandle(hProcess);
    return TRUE;
}

这段代码我用了很多年,基本没出过问题。但有一点要提醒你:目标进程的权限。如果你要注入系统进程或者高权限进程,你的程序也得有相应权限。我曾经在注入svchost.exe时翻过车,就是因为忘了提权。

小技巧:如果目标进程是32位的,你的注入器也得是32位的。同理,64位对64位。跨位数注入?嗯,那得用其他方法,后面章节会讲。

远程线程注入的优缺点

优点 缺点
实现简单,代码量少 容易被安全软件检测(CreateRemoteThread是敏感API)
兼容性好,Windows全版本支持 需要目标进程有PROCESS_CREATE_THREAD权限
注入成功率高 会留下明显的线程痕迹

41.2 SetWindowsHookEx注入

另一种经典注入方式,是利用Windows的钩子机制。你想想看,Windows的消息处理系统本身就支持钩子——你可以安装一个全局钩子,拦截键盘、鼠标等消息。而钩子函数必须放在DLL里,系统会自动把DLL注入到所有相关进程。

说白了,SetWindowsHookEx注入就是利用系统机制,让Windows帮你完成注入。你只需要提供一个DLL,里面包含钩子处理函数,然后调用SetWindowsHookEx注册钩子就行了。

实现步骤

  1. 编写一个DLL,里面实现钩子处理函数(比如KeyboardProc
  2. 在DLL的DllMain里写你的注入逻辑(比如启动一个线程)
  3. 调用SetWindowsHookEx注册钩子,指定DLL句柄
  4. 系统会把你的DLL注入到所有满足条件的进程中

这里有个关键点:钩子类型决定了注入范围。比如WH_KEYBOARD_LL是低级键盘钩子,不需要注入到目标进程,而是在你的进程里处理。但WH_KEYBOARD(非LL版本)就需要注入到目标进程。

注意:全局钩子会影响系统性能。每个消息都要经过你的DLL处理,如果处理慢了,整个系统都会卡。我曾经在生产环境里见过有人用全局钩子做监控,结果用户反馈电脑变慢,最后查出来是钩子DLL里有个死循环。

代码示例

DLL端的代码大概长这样:

// HookDLL.cpp
#include <windows.h>

HHOOK g_hHook = NULL;

// 钩子处理函数
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) {
    // 这里可以写你的注入逻辑
    // 比如启动一个线程做其他事情
    return CallNextHookEx(g_hHook, nCode, wParam, lParam);
}

// 安装钩子
extern "C" __declspec(dllexport)
BOOL InstallHook() {
    HMODULE hMod = GetModuleHandleA("HookDLL.dll");
    g_hHook = SetWindowsHookEx(WH_KEYBOARD, KeyboardProc,
                               hMod, 0);
    return (g_hHook != NULL);
}

// 卸载钩子
extern "C" __declspec(dllexport)
BOOL UninstallHook() {
    if (g_hHook) {
        UnhookWindowsHookEx(g_hHook);
        g_hHook = NULL;
        return TRUE;
    }
    return FALSE;
}

// DllMain
BOOL APIENTRY DllMain(HMODULE hModule,
                      DWORD ul_reason_for_call,
                      LPVOID lpReserved) {
    if (ul_reason_for_call == DLL_PROCESS_ATTACH) {
        // DLL被注入到目标进程时触发
        // 在这里写你的注入后逻辑
        MessageBox(NULL, "DLL Injected!", "Success", MB_OK);
    }
    return TRUE;
}

调用端就简单了:

// 加载DLL,调用InstallHook
HMODULE hDll = LoadLibraryA("HookDLL.dll");
if (hDll) {
    typedef BOOL (*InstallHookFunc)();
    InstallHookFunc InstallHook = (InstallHookFunc)
        GetProcAddress(hDll, "InstallHook");
    if (InstallHook) InstallHook();
}

SetWindowsHookEx注入的优缺点

优点 缺点
系统自动注入,不需要手动创建远程线程 只能注入有消息队列的GUI进程
相对隐蔽,不容易被简单检测 全局钩子影响系统性能
可以同时注入多个进程 钩子DLL必须导出钩子处理函数

41.3 两种注入方式的对比

我个人的经验是:远程线程注入适合精确打击——你知道目标进程的PID,只想注入这一个进程。SetWindowsHookEx注入适合广撒网——你想监控所有键盘输入,或者注入所有记事本进程。

但说实话,现在很多安全软件对这两种方式都有检测。远程线程的CreateRemoteThread是重点监控对象,而全局钩子也会被一些HIPS(主机入侵防御系统)拦截。

避坑指南:我曾经在写一个游戏辅助工具时,用了远程线程注入。结果游戏的反作弊系统直接检测到了CreateRemoteThread调用,把我进程干掉了。后来我改用APC注入才绕过去。所以,没有万能的注入方式,要根据场景选

41.4 知识体系图

下面这张图帮你理清两种注入方式的脉络:

DLL注入技术(一)知识体系 DLL注入 远程线程注入 SetWindowsHookEx注入 OpenProcess VirtualAllocEx CreateRemoteThread 权限要求高 易被检测 精确注入 SetWindowsHookEx DLL导出函数 消息队列 系统自动注入 影响性能 仅GUI进程 选择建议:精确注入用远程线程,批量注入用Hook 注意权限、位数匹配、安全软件检测

41.5 写在最后

这两种注入方式,是逆向工程里最基础也最实用的。远程线程注入就像一把手术刀,精准但容易被发现。SetWindowsHookEx注入更像一张网,覆盖面广但有限制。

我个人建议你先把远程线程注入练熟,因为它能帮你理解Windows进程和线程的核心机制。等你把基础打牢了,再去研究更高级的注入方式——比如APC注入、线程劫持、或者利用Windows漏洞的注入。

嗯,这一章就到这里。代码你可以直接拿去用,但记得在合法范围内使用。搞逆向的,底线还是要有的。


公众号:蓝海资料掘金营,微信deep3321