首页
社区
课程
招聘
[原创] 一次由 DLL 静态依赖顺序引发的 Windows 7 崩溃排查
发表于: 10小时前 304

[原创] 一次由 DLL 静态依赖顺序引发的 Windows 7 崩溃排查

10小时前
304

在软件保护和授权校验场景中,对目标程序添加加密外壳(shell)是常见做法。然而,外壳处理可能引入某些操作系统版本上的兼容性问题。本文记录了一起实际案例:某 Windows 应用程序在添加授权校验外壳后,于 Windows 7 上启动即崩溃,而未添加授权校验的版本运行正常;且该问题在 Windows 10 及更高版本上无法复现。经逐步调试分析,最终定位问题根源为 Windows 7 加载器在处理静态 DLL 依赖顺序时的一个缺陷,导致系统 DLL setupapi.dll 的入口点被跳过,进而引发后续 API 调用失败。本文将详细还原排查过程、使用的调试技巧以及最终解决方案,以期为类似问题的处理提供参考。

在 Windows 7 虚拟机中运行授权校验版本,崩溃现象稳定复现。使用 Visual Studio 附加到进程进行调试,发现堆栈回溯信息异常:崩溃指令的地址不属于任何已加载模块的内存区域。这是因为授权校验模块采用匿名加载方式——外壳通过 VirtualAlloc 分配内存、写入机器码并修改入口点,并未调用 LoadLibrary 等标准 API 进行模块注册。因此,该模块的代码段在内核中无对应映像文件,调试器无法将其映射为模块符号,导致堆栈显示为“未知地址”,无法直接追溯调用关系。

客户配合提供了更多信息:只有当加密特定的用户 DLL(如 ModuleA.dllModuleB.dll)时才会触发崩溃,且加密选项中已关闭所有可能干扰的附加功能(如压缩、反调试),以排除这些因素对崩溃的影响。这些 DLL 正是授权校验逻辑所依赖的模块,暗示问题与授权代码的引入密切相关。

为了获得清晰的调用关系,我们临时修改了外壳代码,将授权模块的加载方式从匿名加载改为使用 LoadLibrary 显式加载。经测试,该改动仅改变模块的加载方式,程序执行路径与崩溃行为均未变化,因此可作为获取符号信息的有效手段。调试器能够解析该模块的符号,从而获得了完整的崩溃堆栈:

注意,崩溃发生在 RtlAllocateHeap 内部,其第一个参数 HeapHandle 的值为 NULL,表明调用者向堆分配函数传递了一个无效的堆句柄。

通过逆向分析 setupapi.dll 中的 AllocateDeviceInfoSet 函数,发现它内部调用了 RtlAllocateHeap,而所使用的堆句柄来源于该 DLL 数据段中的一个全局变量(下文称 g_hSetupAPIHeap)。该变量位于 .data 节,初始值为 0。

为追踪 g_hSetupAPIHeap 的写入时机,我们在 x64dbg 中对该变量设置硬件写入断点,然后分别运行正常程序(未添加授权校验)与授权校验版本,观察断点触发情况:

可见,该变量在 setupapi.dllDllMain(具体位于 ProcessAttach 分支)中被初始化,而 DllMain 的调用由系统加载器通过 LdrpRunInitializeRoutines 触发。

在分析陷入僵局时,我们尝试了多种非常规手段,其中包括手动调整 PE 文件的导入表顺序——将系统 DLL(如 SETUPAPI.dll)的导入项提前到用户 DLL 之前。结果发现,这一修改绕过了 Windows 7 上运行崩溃的问题。

这一偶然发现提示:问题很可能与 EXE 的导入表中 DLL 的静态依赖顺序有关。进一步检查发现,在崩溃版本中,用户 DLL(如 ModuleA.dll)的导入项出现在系统 DLL(SETUPAPI.dll)之前;而通过手动调整顺序后,问题消失。

Windows 加载器在创建进程时,会解析主模块的导入表,按照导入项的顺序依次加载所需的 DLL。在所有 DLL 加载完毕后,加载器通过 LdrpRunInitializeRoutines加载顺序依次调用每个 DLL 的入口点(DllMain)。理想情况下,每个 DLL 的 DllMain 应在其所依赖的其他 DLL 初始化之后被调用。

然而,Windows 7 的加载器在处理静态依赖顺序时存在一个缺陷:如果某个系统 DLL 在导入表中的位置晚于某些用户 DLL,而该用户 DLL 的 DllMain 中又直接或间接调用了该系统 DLL 中的函数,就可能导致系统 DLL 的入口点尚未执行,其全局变量处于未初始化状态。正如《Windows Internals》第7版第3章所述,当导入表顺序引发复杂依赖关系时,加载器构建初始化列表时可能发生遗漏,致使某些 DLL 的入口点未被加入调用队列,从而从未执行。本案例中 setupapi.dll 的入口点即属于此情况。

在本案例中,授权模块位于用户 DLL 中,其初始化代码在 DllMain 阶段执行,此时调用了 setupapi.dll!SetupDiGetClassDevsA,该函数内部需要用到 g_hSetupAPIHeap。由于 setupapi.dllDllMain 未被调用,该变量保持为 0,最终导致 RtlAllocateHeap(NULL, ...) 崩溃。

将 EXE 导入表中的所有系统 DLL 移动到用户 DLL 之前,可以确保系统 DLL 优先加载并完成初始化,从而避免上述依赖顺序缺陷。这一调整仅修改导入项的顺序,不改变任何代码逻辑,因此理论上不会引入新的问题。

我们编写了一个名为 reorder_imports 的小工具,用于自动重排 PE 文件的导入表。其核心步骤如下:

工具源码附于文末(附录 A)。需在 Visual Studio 开发人员命令提示符下编译,或使用任意支持 C++11 的编译器。编译后使用命令 reorder_imports <input.exe> <output.exe> 即可生成调整后的文件。

客户使用该工具处理授权校验版本的 EXE 文件,在 Windows 7 上运行测试,程序启动正常,未再出现崩溃。后续在 Windows 10 和 Windows 11 上也验证了程序功能完整,无副作用。

该问题无法在 Windows 8、Windows 10 及 Windows 11 上复现,这与微软后续对加载器的改进密切相关。根据《Windows Internals》及相关微软文档,主要改进包括:

这些改进共同消除了 Windows 7 中存在的静态依赖顺序缺陷。

希望本文的分享能为读者在处理类似 Windows 7 下的 DLL 初始化异常时提供一条清晰的排查路径。

ntdll.dll!RtlAllocateHeap
setupapi.dll!AllocateDeviceInfoSet
setupapi.dll!SetupDiCreateDeviceInfoListExW
setupapi.dll!SetupDiGetClassDevsExW
setupapi.dll!SetupDiGetClassDevsA
授权模块!授权函数
...
ntdll.dll!RtlAllocateHeap
setupapi.dll!AllocateDeviceInfoSet
setupapi.dll!SetupDiCreateDeviceInfoListExW
setupapi.dll!SetupDiGetClassDevsExW
setupapi.dll!SetupDiGetClassDevsA
授权模块!授权函数
...
setupapi.dll!ProcessAttach()  - 0x1171 bytes   
setupapi.dll!DllMain()  + 0x1616 bytes 
setupapi.dll!_CRT_INIT()  - 0x42 bytes 
ntdll.dll!LdrpRunInitializeRoutines()  + 0x1fe bytes   
ntdll.dll!LdrpLoadDll()  + 0x594 bytes 
ntdll.dll!LdrLoadDll()  + 0xed bytes   
KernelBase.dll!LoadLibraryExW()  + 0xea bytes  
stub.exe!start(void * hModule=0x0000000000000000, unsigned long ul_reason_for_call=0x00000000, void * lpReserved=0x0000000000000000)  Line 20   C++
kernel32.dll!BaseThreadInitThunk()  + 0xd bytes
ntdll.dll!RtlUserThreadStart()  + 0x1d bytes   
setupapi.dll!ProcessAttach()  - 0x1171 bytes   
setupapi.dll!DllMain()  + 0x1616 bytes 
setupapi.dll!_CRT_INIT()  - 0x42 bytes 
ntdll.dll!LdrpRunInitializeRoutines()  + 0x1fe bytes   
ntdll.dll!LdrpLoadDll()  + 0x594 bytes 
ntdll.dll!LdrLoadDll()  + 0xed bytes   
KernelBase.dll!LoadLibraryExW()  + 0xea bytes  
stub.exe!start(void * hModule=0x0000000000000000, unsigned long ul_reason_for_call=0x00000000, void * lpReserved=0x0000000000000000)  Line 20   C++
kernel32.dll!BaseThreadInitThunk()  + 0xd bytes
ntdll.dll!RtlUserThreadStart()  + 0x1d bytes   
// reorder_imports.cpp
// 编译:cl /EHsc reorder_imports.cpp
#include <Windows.h>
#include <algorithm>
#include <fstream>
#include <iostream>
#include <iterator>
#include <vector>
 
// 判断 DLL 是否为系统 DLL:尝试从系统目录加载
bool is_system_dll(const char* name) {
    char sysPath[MAX_PATH];
    GetSystemDirectoryA(sysPath, MAX_PATH);
    strcat_s(sysPath, "\\");
    strcat_s(sysPath, name);
    HMODULE handle = LoadLibraryA(sysPath);
    if (handle) {
        FreeLibrary(handle);
        return true;
    }
    return false;
}
 
int parse_import(const IMAGE_SECTION_HEADER& section, char* base, std::size_t vaddr) {
    auto pred = [&](const IMAGE_IMPORT_DESCRIPTOR& desc) {
        return is_system_dll(&base[desc.Name - section.VirtualAddress]);
    };
    auto first = reinterpret_cast<IMAGE_IMPORT_DESCRIPTOR*>(&base[vaddr - section.VirtualAddress]);
    auto last = first;
    for (auto iter = first; iter->Name; ++iter, ++last)
        std::cout << "name: " << &base[iter->Name - section.VirtualAddress] << '\n';
    std::stable_partition(first, last, pred);
    std::cout << "\nAfter reordering:\n";
    for (auto iter = first; iter->Name; ++iter)
        std::cout << "name: " << &base[iter->Name - section.VirtualAddress] << '\n';
    return 0;
}
 
int main(int argc, char* argv[]) try {
    if (argc < 3) {
        std::cout << "Usage: " << argv[0] << " <input> <output>\n";
        return 1;
    }
    std::ifstream is{argv[1], std::ios::binary | std::ios::ate};
    is.exceptions(std::ifstream::failbit);
    const auto& count = is.tellg();
    if (!count) throw std::runtime_error{"empty file"};
    std::vector<char> buff(count);
    is.seekg(0, std::ios::beg).read(&buff[0], count);
 
    auto& Dos = reinterpret_cast<IMAGE_DOS_HEADER&>(buff[0]);
    if (Dos.e_magic != IMAGE_DOS_SIGNATURE) throw std::runtime_error{"invalid DOS signature"};
    auto& Nt = reinterpret_cast<IMAGE_NT_HEADERS&>(buff[Dos.e_lfanew]);
    if (Nt.Signature != IMAGE_NT_SIGNATURE) throw std::runtime_error{"invalid NT signature"};
 
    std::size_t vaddr;
    if (Nt.OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC) {
        auto& Nt64 = reinterpret_cast<IMAGE_NT_HEADERS64&>(buff[Dos.e_lfanew]);
        vaddr = Nt64.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
    } else {
        auto& Nt32 = reinterpret_cast<IMAGE_NT_HEADERS32&>(buff[Dos.e_lfanew]);
        vaddr = Nt32.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
    }
 
    auto sections = IMAGE_FIRST_SECTION(&Nt);
    for (std::size_t i = 0; i < Nt.FileHeader.NumberOfSections; ++i) {
        auto offset = vaddr - sections[i].VirtualAddress;
        if (!vaddr || vaddr < sections[i].VirtualAddress || offset > sections[i].Misc.VirtualSize)
            continue;
        parse_import(sections[i], &buff[sections[i].PointerToRawData], vaddr);
        break; // 假设导入表位于单个节内
    }
 
    std::ofstream os{argv[2], std::ios::binary};
    os.exceptions(std::ofstream::failbit);
    os.write(&buff[0], buff.size());
    std::cout << "Successfully reordered imports and wrote to " << argv[2] << "\n";
    return 0;
} catch (const std::exception& e) {
    std::cerr << "error: " << e.what() << '\n';
    return 1;
}
// reorder_imports.cpp
// 编译:cl /EHsc reorder_imports.cpp
#include <Windows.h>
#include <algorithm>
#include <fstream>
#include <iostream>
#include <iterator>
#include <vector>
 
// 判断 DLL 是否为系统 DLL:尝试从系统目录加载
bool is_system_dll(const char* name) {
    char sysPath[MAX_PATH];
    GetSystemDirectoryA(sysPath, MAX_PATH);
    strcat_s(sysPath, "\\");
    strcat_s(sysPath, name);
    HMODULE handle = LoadLibraryA(sysPath);
    if (handle) {
        FreeLibrary(handle);
        return true;
    }
    return false;
}
 
int parse_import(const IMAGE_SECTION_HEADER& section, char* base, std::size_t vaddr) {
    auto pred = [&](const IMAGE_IMPORT_DESCRIPTOR& desc) {
        return is_system_dll(&base[desc.Name - section.VirtualAddress]);
    };
    auto first = reinterpret_cast<IMAGE_IMPORT_DESCRIPTOR*>(&base[vaddr - section.VirtualAddress]);
    auto last = first;
    for (auto iter = first; iter->Name; ++iter, ++last)
        std::cout << "name: " << &base[iter->Name - section.VirtualAddress] << '\n';
    std::stable_partition(first, last, pred);
    std::cout << "\nAfter reordering:\n";
    for (auto iter = first; iter->Name; ++iter)
        std::cout << "name: " << &base[iter->Name - section.VirtualAddress] << '\n';
    return 0;
}
 

[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!

收藏
免费 1
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回