44、DLL注入技术(四):DLL劫持、侧加载攻击

说实话,前面几讲我们聊的都是主动注入——你写代码,调用API,把DLL塞进目标进程。但这一讲要聊的东西,思路完全反过来。我们不去“塞”,而是让目标进程自己“请”我们进去。

听起来有点玄乎?其实原理很简单:Windows在加载DLL时,有一套固定的搜索顺序。如果我们能在这条搜索路径上,提前放一个同名但内容不同的DLL,系统就会傻乎乎地把我们的DLL加载进去。这就是DLL劫持,也叫侧加载攻击。

我在做渗透测试时,遇到过好几次这种情况。明明系统补丁都打全了,杀软也开着,但攻击者就是靠一个放在临时目录下的恶意DLL,轻松拿到了系统权限。嗯,这招确实防不胜防。

DLL搜索顺序:劫持的根基

要理解劫持,先得搞清楚Windows加载DLL时到底去哪找。说白了,就是一套优先级规则:

  1. 程序所在目录——这是最优先的
  2. 系统目录(C:\Windows\System32)
  3. 16位系统目录(C:\Windows\System)
  4. Windows目录(C:\Windows)
  5. 当前工作目录
  6. PATH环境变量中的目录

你想想看,如果一个程序在启动时,用相对路径加载一个DLL,而它自己的目录里又没有这个DLL……那系统就会按这个顺序一路找下去。这时候,如果我们能在程序目录里放一个同名DLL,劫持就成功了。

关键点:劫持的核心不是“注入”,而是“欺骗”。我们让系统以为加载的是合法DLL,实际上加载的是我们的恶意代码。

实战:劫持一个系统DLL

我记得有一次,我在分析一个老旧的桌面软件。它每次启动都会加载 version.dll,但用的是相对路径。我一看,这不就是典型的劫持机会吗?

具体怎么做呢?我们写一个假的 version.dll,导出和原版一模一样的函数。然后在程序目录下放这个DLL。程序启动时,系统会优先加载我们的DLL,而不是去System32里找。

下面是一个简单的劫持DLL示例:

// 劫持 version.dll 的示例
#include <windows.h>

// 声明原始DLL的句柄
HMODULE g_hOriginalDll = NULL;

// 导出函数:GetFileVersionInfoA
BOOL WINAPI GetFileVersionInfoA(
    LPCSTR lptstrFilename,
    DWORD dwHandle,
    DWORD dwLen,
    LPVOID lpData
) {
    // 第一次调用时,加载原始DLL
    if (!g_hOriginalDll) {
        g_hOriginalDll = LoadLibraryA("C:\\Windows\\System32\\version.dll");
    }
    
    // 获取原始函数地址并调用
    typedef BOOL (WINAPI *pGetFileVersionInfoA)(
        LPCSTR, DWORD, DWORD, LPVOID);
    pGetFileVersionInfoA original = (pGetFileVersionInfoA)
        GetProcAddress(g_hOriginalDll, "GetFileVersionInfoA");
    
    // 在这里插入恶意代码
    MessageBoxA(NULL, "DLL劫持成功!", "警告", MB_OK);
    
    return original(lptstrFilename, dwHandle, dwLen, lpData);
}

// DLL入口点
BOOL APIENTRY DllMain(HMODULE hModule,
                      DWORD  ul_reason_for_call,
                      LPVOID lpReserved) {
    switch (ul_reason_for_call) {
        case DLL_PROCESS_ATTACH:
            // 劫持初始化代码
            DisableThreadLibraryCalls(hModule);
            break;
        case DLL_THREAD_ATTACH:
        case DLL_THREAD_DETACH:
        case DLL_PROCESS_DETACH:
            break;
    }
    return TRUE;
}

这段代码的核心思路是:我们导出了和原始DLL完全相同的函数名,但内部先执行恶意代码,再转发到原始DLL。这样程序既不会崩溃,我们的代码也执行了。

注意:劫持DLL时,必须导出所有原始DLL的导出函数,否则程序调用未导出的函数时会崩溃。我建议用工具(如Dependency Walker)先分析原始DLL的导出表。

侧加载攻击:更隐蔽的变种

侧加载攻击,说白了就是DLL劫持的一个变种。区别在于:劫持通常针对系统DLL,而侧加载针对的是第三方软件的DLL。

举个例子:很多软件会加载自己的插件DLL,比如 plugin.dll。如果这个插件DLL的加载路径不安全,攻击者就可以放一个恶意版本进去。

我曾经分析过一个案例:某款视频播放器会从当前工作目录加载 ffmpeg.dll。攻击者把恶意DLL放在U盘里,用户一打开U盘里的视频文件,播放器就自动加载了恶意DLL。整个过程用户毫无察觉。

如何防御?

作为安全研究员,我们不能光会攻击,还得知道怎么防。下面是我总结的几个防御要点:

防御措施 说明 优先级
使用绝对路径加载DLL 调用 LoadLibrary 时指定完整路径,不走搜索顺序
启用 SafeDllSearchMode 注册表设置,改变搜索顺序,优先搜索系统目录
数字签名验证 加载DLL前验证其数字签名是否合法
移除当前目录 从搜索路径中移除当前工作目录
使用 KnownDLLs 机制 将常用DLL注册到 KnownDLLs 列表,绕过搜索顺序
我的建议:开发软件时,尽量用绝对路径加载DLL。如果必须用相对路径,至少把DLL放在程序自己的目录下,别依赖系统搜索。我曾经见过一个软件,DLL放在共享目录里,结果被局域网内的攻击者轻松劫持。

知识体系图

下面这张图展示了DLL劫持和侧加载攻击的完整知识结构:

DLL劫持与侧加载攻击知识体系 DLL劫持/侧加载 搜索顺序劫持 系统DLL vs 第三方DLL 防御措施 程序目录优先 相对路径加载 缺少SafeDllSearchMode version.dll劫持 comctl32.dll劫持 第三方插件DLL 绝对路径加载 数字签名验证 KnownDLLs机制

避坑指南

做DLL劫持实验时,有几个坑我踩过,分享给你:

  • 导出函数必须完整——少导出一个函数,程序就可能崩溃。我曾经漏了一个导出函数,结果目标程序启动就蓝屏,排查了半天才发现问题。
  • 注意DLL重定向——有些程序会启用DLL重定向机制,劫持会失效。遇到这种情况,得换思路。
  • 杀软会拦截——现在的杀软对DLL劫持很敏感。我建议在实验环境里测试,别在生产环境乱搞。
  • 64位和32位要分清——64位进程只能加载64位DLL,32位同理。混着用会加载失败。
一句话总结:DLL劫持的本质是利用Windows的搜索顺序漏洞,让目标进程主动加载恶意DLL。防御的关键是使用绝对路径和数字签名验证。

好了,这一讲的内容就到这里。DLL劫持是个老技术,但至今仍然有效。很多老旧软件和系统组件都存在这个问题。作为安全研究员,理解它不仅能帮你做渗透测试,更能帮你写出更安全的代码。


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