18、注入技术(上):DLL注入原理与实战

大家好,欢迎来到逆向工程的第十八讲。今天咱们聊点硬核的——DLL注入。说实话,这玩意儿在安全圈里名声不太好,但作为安全研究员,你必须要懂。不懂注入,你就没法理解恶意软件是怎么潜伏的,也没法做深入的内存分析。

我最早接触DLL注入,是在分析一个勒索软件样本的时候。那个样本把自己伪装成系统更新程序,通过注入把核心逻辑塞进了explorer.exe。当时我花了整整两天才把它的注入链理清楚。从那以后,我就下定决心,必须把注入技术吃透。

核心概念:DLL注入的本质,就是让一个目标进程主动加载我们指定的DLL。一旦DLL被加载,它就在目标进程的地址空间内执行,拥有和该进程相同的权限和资源访问能力。

18.1 为什么需要DLL注入?

你可能会问:我直接写个exe跑起来不就行了?为什么要费劲去注入?

原因其实很简单。很多安全软件、游戏反作弊系统、甚至操作系统本身,都会限制外部进程的访问权限。但如果你能成为目标进程的一部分——比如成为notepad.exe的一个线程——那你就能绕过很多限制。

我举个例子。你在分析一个恶意软件时,想hook它的某个API调用。如果你从外部做,很容易被检测到。但如果你注入一个DLL进去,在内部做hook,那就隐蔽得多。

常见的应用场景包括:

  • 安全软件:监控进程行为、拦截恶意API调用
  • 游戏外挂:读取内存数据、修改游戏逻辑(嗯,这个不提倡)
  • 调试工具:注入hook DLL进行函数追踪
  • 恶意软件:窃取信息、持久化驻留

18.2 两种经典注入方式

注入方式有很多种,但最经典、最基础的,就是下面这两种。我个人建议初学者先把这两个吃透,其他的都是变种。

18.2.1 CreateRemoteThread 注入

这是最直接的方式。说白了,就是在目标进程里创建一个远程线程,让这个线程去调用LoadLibrary加载你的DLL。

具体步骤是这样的:

  1. OpenProcess打开目标进程,获取句柄
  2. VirtualAllocEx在目标进程里分配一块内存
  3. WriteProcessMemory把DLL路径写进去
  4. CreateRemoteThread在目标进程里启动一个线程
  5. 线程入口点设为LoadLibrary,参数就是刚才写的DLL路径

代码实现大概长这样:

// 获取目标进程句柄
HANDLE hProcess = OpenProcess(
    PROCESS_ALL_ACCESS, FALSE, dwProcessId);

// 在目标进程分配内存
LPVOID pRemoteMemory = VirtualAllocEx(
    hProcess, NULL, 256,
    MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

// 写入DLL路径
WriteProcessMemory(hProcess, pRemoteMemory,
    szDllPath, strlen(szDllPath) + 1, NULL);

// 获取LoadLibrary地址
LPTHREAD_START_ROUTINE pLoadLibrary =
    (LPTHREAD_START_ROUTINE)GetProcAddress(
        GetModuleHandle("kernel32.dll"),
        "LoadLibraryA");

// 创建远程线程
HANDLE hThread = CreateRemoteThread(
    hProcess, NULL, 0,
    pLoadLibrary, pRemoteMemory, 0, NULL);

// 等待线程结束
WaitForSingleObject(hThread, INFINITE);

// 清理
CloseHandle(hThread);
CloseHandle(hProcess);

注意:这种方法在x64系统上注入32位进程到64位进程时会失败。因为LoadLibrary的地址在不同位数进程间不通用。我曾经在这个坑里爬了整整一个下午。

18.2.2 SetWindowsHookEx 注入

这种方法更隐蔽一些。它利用Windows的消息钩子机制,当目标进程处理某个消息时,系统会自动把钩子DLL加载进去。

我个人的经验是,这种方法适合做键盘记录或者消息监控。但缺点也很明显——它依赖消息队列,如果目标进程没有消息循环,那就白搭。

实现步骤:

  1. 编写一个DLL,里面实现钩子过程(Hook Procedure)
  2. 在DLL中,DllMain里做你想做的事
  3. SetWindowsHookEx安装钩子
  4. 指定钩子类型(比如WH_KEYBOARD)和DLL句柄
// 安装全局钩子
HHOOK hHook = SetWindowsHookEx(
    WH_KEYBOARD_LL,           // 低级键盘钩子
    (HOOKPROC)HookProcedure,  // 钩子函数
    hInstance,                // DLL模块句柄
    0);                       // 0表示全局钩子

// 钩子过程
LRESULT CALLBACK HookProcedure(
    int nCode, WPARAM wParam, LPARAM lParam)
{
    if (nCode >= 0)
    {
        // 在这里处理按键事件
        // 或者执行你的注入逻辑
    }
    return CallNextHookEx(NULL, nCode, wParam, lParam);
}

小技巧SetWindowsHookEx注入的DLL会被映射到所有拥有消息队列的进程中。如果你只想注入特定进程,可以在钩子过程里检查当前进程ID,不是目标就跳过。

18.3 进程间通信(IPC)

DLL注入进去了,然后呢?你得和它通信吧?不然你都不知道它有没有成功加载,也不知道它干了什么。

这里我介绍两种最常用的IPC方式。

18.3.1 命名管道

管道就像一根水管,一头连着你(注入器),一头连着目标进程里的DLL。数据从一头流到另一头。

我比较喜欢用命名管道,因为它简单可靠。服务端(通常是注入器)创建管道,客户端(DLL)连接上来,然后就可以双向通信了。

// 服务端(注入器)
HANDLE hPipe = CreateNamedPipe(
    "\\\\.\\pipe\\MyInjectPipe",
    PIPE_ACCESS_DUPLEX,
    PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE,
    PIPE_UNLIMITED_INSTANCES,
    1024, 1024, 0, NULL);

// 等待客户端连接
ConnectNamedPipe(hPipe, NULL);

// 发送命令
char cmd[] = "DO_SOMETHING";
WriteFile(hPipe, cmd, strlen(cmd), &written, NULL);

// 客户端(DLL)
HANDLE hPipe = CreateFile(
    "\\\\.\\pipe\\MyInjectPipe",
    GENERIC_READ | GENERIC_WRITE,
    0, NULL, OPEN_EXISTING, 0, NULL);

// 读取命令
char buffer[1024];
ReadFile(hPipe, buffer, 1024, &read, NULL);

18.3.2 共享内存

共享内存更快,因为它不需要数据拷贝。说白了,就是两个进程共同映射同一块物理内存。

在Windows上,我们用CreateFileMappingMapViewOfFile来实现。

// 创建共享内存
HANDLE hMapFile = CreateFileMapping(
    INVALID_HANDLE_VALUE,
    NULL,
    PAGE_READWRITE,
    0,
    4096,  // 4KB共享内存
    "Global\\MySharedMemory");

// 映射到进程地址空间
LPVOID pSharedMem = MapViewOfFile(
    hMapFile,
    FILE_MAP_ALL_ACCESS,
    0, 0, 4096);

// 写入数据
strcpy((char*)pSharedMem, "Hello from injector!");

// 在DLL中读取
HANDLE hMapFile = OpenFileMapping(
    FILE_MAP_ALL_ACCESS,
    FALSE,
    "Global\\MySharedMemory");
LPVOID pSharedMem = MapViewOfFile(
    hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, 4096);
printf("%s", (char*)pSharedMem);

对比总结:管道适合小数据量、需要同步的场景;共享内存适合大数据量、追求性能的场景。我一般混合使用——用共享内存传数据,用管道传控制信号。

18.4 实战:编写一个简单的DLL注入器

好了,理论说完了,咱们动手写一个。这个注入器支持两种注入方式,并且通过命名管道和注入的DLL通信。

先看整体架构:

DLL注入器架构图 注入器 (Injector.exe) 1. OpenProcess 打开目标 2. VirtualAllocEx 分配内存 3. WriteProcessMemory 写路径 4. CreateRemoteThread 启动 目标进程 (Target.exe) 加载了我们的DLL DLL在目标进程内执行 拥有目标进程的权限 CreateRemoteThread 注入的DLL (Inject.dll) DllMain 中执行初始化 创建通信线程 命名管道 IPC 注入流程 通信流程

注入器的核心代码,我把它封装成了一个类:

class Injector
{
public:
    bool Inject(DWORD pid, const char* dllPath)
    {
        // 1. 打开目标进程
        HANDLE hProcess = OpenProcess(
            PROCESS_ALL_ACCESS, FALSE, pid);
        if (!hProcess) return false;

        // 2. 在目标进程分配内存
        size_t pathLen = strlen(dllPath) + 1;
        LPVOID pRemoteMem = VirtualAllocEx(
            hProcess, NULL, pathLen,
            MEM_COMMIT, PAGE_READWRITE);
        if (!pRemoteMem) {
            CloseHandle(hProcess);
            return false;
        }

        // 3. 写入DLL路径
        WriteProcessMemory(hProcess, pRemoteMem,
            dllPath, pathLen, NULL);

        // 4. 获取LoadLibrary地址
        HMODULE hKernel32 = GetModuleHandle("kernel32.dll");
        LPTHREAD_START_ROUTINE pLoadLib =
            (LPTHREAD_START_ROUTINE)GetProcAddress(
                hKernel32, "LoadLibraryA");

        // 5. 创建远程线程
        HANDLE hThread = CreateRemoteThread(
            hProcess, NULL, 0,
            pLoadLib, pRemoteMem, 0, NULL);

        if (!hThread) {
            VirtualFreeEx(hProcess, pRemoteMem, 0, MEM_RELEASE);
            CloseHandle(hProcess);
            return false;
        }

        // 6. 等待加载完成
        WaitForSingleObject(hThread, 5000);

        // 7. 清理
        CloseHandle(hThread);
        CloseHandle(hProcess);
        return true;
    }
};

对应的DLL代码也很简单:

BOOL APIENTRY DllMain(HMODULE hModule,
    DWORD reason, LPVOID lpReserved)
{
    if (reason == DLL_PROCESS_ATTACH)
    {
        // 创建通信线程
        CreateThread(NULL, 0,
            CommunicationThread, NULL, 0, NULL);
    }
    return TRUE;
}

DWORD WINAPI CommunicationThread(LPVOID lpParam)
{
    // 连接命名管道
    HANDLE hPipe = CreateFile(
        "\\\\.\\pipe\\InjectPipe",
        GENERIC_READ | GENERIC_WRITE,
        0, NULL, OPEN_EXISTING, 0, NULL);

    if (hPipe == INVALID_HANDLE_VALUE)
        return 1;

    // 循环处理命令
    char buffer[256];
    DWORD read;
    while (ReadFile(hPipe, buffer, 256, &read, NULL))
    {
        // 解析命令并执行
        if (strcmp(buffer, "GET_PROCESS_INFO") == 0)
        {
            // 返回进程信息
            char info[256];
            sprintf(info, "PID: %d, Threads: %d",
                GetCurrentProcessId(),
                GetCurrentThreadId());
            WriteFile(hPipe, info, strlen(info), &written, NULL);
        }
    }

    CloseHandle(hPipe);
    return 0;
}

避坑指南:我曾经在DLL里直接调用MessageBox做测试,结果目标进程是服务程序,没有桌面,消息框弹不出来,整个注入就卡死了。记住,在DLL里不要做任何UI操作,除非你确定目标进程有窗口。

18.5 检测与防御

作为安全研究员,光会注入还不够,你还得知道怎么防。这里我简单提几个检测点:

检测方法 原理 绕过思路
枚举进程模块 CreateToolhelp32Snapshot枚举所有加载的DLL 使用反射式注入,不经过LoadLibrary
检测远程线程 Thread32First检查线程入口点 使用APC注入或线程劫持
Hook关键API Hook CreateRemoteThread等函数 使用内核态注入或硬件断点
内存扫描 扫描可疑的内存区域 加密DLL内容,运行时解密

嗯,这里要注意,检测和绕过是一个猫鼠游戏。没有绝对安全的系统,也没有绝对隐蔽的注入。作为安全研究员,我们的目标是理解这些技术,而不是滥用它们。

好了,这一讲的内容就到这里。DLL注入是逆向工程中非常基础也非常重要的技术。我建议你亲手写一个注入器,注入到notepad.exe里试试。只有亲手踩过坑,才能真正理解这些API的细节。

课后练习:写一个DLL注入器,支持CreateRemoteThreadSetWindowsHookEx两种方式。注入的DLL通过共享内存向注入器报告目标进程的当前工作目录。试试看,能不能在10分钟内搞定?


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