内存加载带有TLS的DLL的问题分析

背景

最近在做内存加载DLL时,碰到了一个崩溃,涉及到TLS(线程局部存储),所以记录一下对问题的分析。

问题分析

内存加载原理

内存加载[1],又称自加载、反射加载[2],是指不使用系统的LoadLibrary类函数加载DLL,而是模拟DLL加载过程,手动将DLL加载到内存中。

内存加载的一般过程为:

  1. 读DLL文件,解析PE头
  2. 申请DLL image空间:OptionalHeader.SizeOfImage
  3. 逐个拷贝section内容到image内存
  4. 解析导入函数,填充IAT表
  5. 修复重定位
  6. 调用TLS回调函数
  7. 调用DLL入口点

存在的问题

上述过程只是模拟了将DLL加载到内存中,并简化了DLL的初始化(只调用了入口点),这对于一般DLL来说已经足够了,但对于带TLS的DLL,就会有问题:

  1. DllMain和TLS回调函数不会得到线程事件通知:自加载DLL不在模块列表中,线程创建或退出等事件不会通知它[3]
  2. 静态TLS变量未初始化:模块的TLS变量内存块是加载时系统动态分配的,然后从文件中的TLS模板数据拷贝过去来对简单类型的TLS变量进行初始化[4],同时会更新该模块的_tls_index值,通过TEB.ThreadLocalStoragePointer + _tls_index来得到TLS内存块的地址

第一点还好说,一般DLL中没有TLS回调函数,DllMain中也没有特殊操作,线程事件不通知问题也不大。但是第二点就比较严重了,如果DLL中存在静态TLS变量,因为没有申请TLS内存块,也没有更新_tls_index,DLL中使用到TLS变量时,结果会出错或崩溃。

问题重现与分析

来看个例子:

cpp 复制代码
// 静态TLS变量
__declspec(thread) int v0;
__declspec(thread) int v1 = 1;
__declspec(thread) string v2("hello");

extern "C" __declspec(DLLexport) void Test(int n) {
  static int v3 = 3; // 局部static变量,简单类型,已初始化
  static int v4 = n; // 局部static变量,简单类型,需运行时初始化
  static string v5("world"); // 局部static变量,复杂类型,调用string构造函数动态初始化

  printf("global: v0=%d, v1=%d, v2=%s\n", v0, v1, v2.c_str());
  printf("local : v3=%d, v4=%d, v5=%s\n", v3, v4, v5.c_str());
}

在测试程序中先使用系统加载DLL,然后使用内存加载DLL,调用DLL的导出函数Test(4),输出结果如下:

ini 复制代码
System load:
global: v0=0, v1=1, v2=hello
local : v3=3, v4=4, v5=world

Memory load:
global: v0=0, v1=0, v2=hello
local : v3=3, v4=0, v5=

系统加载的情况下,输出结果符合预期;但是内存加载的情况下,程序大概率会崩溃,上面没有崩溃的输出是使用debug增量编译的,方便看一下不同变量的细微差别。

总的来说,由于内存加载中没有为TLS变量申请空间,也没有更新DLL的_tls_index_tls_index是默认值0,然后基于_tls_index获取该DLL的TLS内存块时,得到的其实是另一个模块的TLS内存块,读写都会有问题。我的测试环境是win10 22H2,测试程序中_tls_index为0对应的是kernelbase.DLL的TLS内存块。

具体来说,不同类型的变量有些差异:

  1. v0: 简单类型无初值,使用默认初值0,其输出结果是kernelbase的TLS内存块对应位置的值

  2. v1: 简单类型有初值,初值在TLS模板中,但是内存加载时没有处理TLS初始化,所以输出结果也是kernelbase的TLS内存块对应位置的值

  3. v2: 复杂类型有初值,需要动态初始化的TLS变量,其初始化函数会在DllMain中被调用,所以内存加载时会执行初始化,但是初始化的位置是kernelbase的TLS内存块,会覆盖掉kernelbase的TLS变量。从下图可以看出,系统加载该DLL时,v2初始化函数的执行时机在DLL入口中。

  4. v3: 局部static变量,简单类型,静态初始化,不需要运行时初始化,不涉及TLS,输出结果正确

  5. v4: 局部static变量,简单类型,需要运行时初始化,为了保证局部static变量只在第一次运行时初始化,编译器额外为v4后面添加了一个$TSS0变量标识初始化状态,默认为0,并引入了TLS变量_Init_thread_epoch,初值是INT_MIN,当$TSS0 > _Init_thread_epoch时,才会执行动态初始化,初始化完成后会将$TSS0置为_Init_thread_epoch,这样后续的线程就不会重复执行初始化了。但内存加载场景下,读取的_Init_thread_epoch是kernelbase TLS内存块中的某个位置,可能是0,导致条件不满足,不走v4的初始化。

    cpp 复制代码
    // 摘自vcruntime的thread_safe_statics.cpp,从wdk中可以找到
    static int const epoch_start = INT_MIN; // #define INT_MIN  (-2147483647 - 1)
    extern "C"
    {
        int _Init_global_epoch = epoch_start;
        __declspec(thread) int _Init_thread_epoch = epoch_start;
    }
  6. v5: 局部static变量,复杂类型,需要运行时初始化,和v4一样,编译器添加了$TSS1变量标识其初始化状态,也会引入TLS变量_Init_thread_epoch

    注意,v4v5,包括编译器添加的$TSS0$TSS1本身还是静态变量,它们都位于静态数据区(.data),不在TLS内存块中。

在日常开发中,对于显式的使用TLS我们是有意识的,但对于复杂类型的局部static变量引入的隐式TLS,就比较容易忽略,就容易出错。而最常见引入方式,大概就是使用Meyers' Singleton单例模式了[5],我在文章开头的崩溃,就是因为使用了这种单例模式写法。

cpp 复制代码
// Meyer's Singleton
class Singleton {
public:
    static Singleton& getInstance() {
        // 复杂类型局部static变量,引入TLS
        static Singleton instance;
        return instance;
    }

    // 删除拷贝构造函数和赋值运算符
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

private:
    // 私有构造函数和析构函数
    Singleton() = default;
    ~Singleton() = default;

    // 类中含有复杂类型
    string a;
    vector b;
}

解决方案

  1. 去除DLL中使用的TLS变量即可,要特别留意复杂类型的局部static变量,会隐式引入TLS,可以考虑改成全局static,但要注意全局static的初始化顺序问题[5]
  2. 如果有实在不能去除的TLS变量,就放弃使用内存加载,改用系统加载吧

参考

  1. MemoryModule, https://github.com/fancycode/MemoryModule/blob/master/MemoryModule.c
  2. ReflectiveDLLInjection, https://github.com/stephenfewer/ReflectiveDLLInjection/blob/master/DLL/src/ReflectiveLoader.c
  3. 线程初始化过程PK加载DLL过程, https://bbs.kanxue.com/thread-185786.htm
  4. Windows线程局部存储机制解析, https://mp.weixin.qq.com/s/_vpyCjBAllzG6x1cFTh4mA?token=507367694\&lang=zh_CN
  5. 为什么Meyers' Singleton是线程安全的, https://blog.csdn.net/m0_59680769/article/details/140094001
相关推荐
用户962377954481 天前
VulnHub DC-3 靶机渗透测试笔记
安全
叶落阁主2 天前
Tailscale 完全指南:从入门到私有 DERP 部署
运维·安全·远程工作
用户962377954484 天前
DVWA 靶场实验报告 (High Level)
安全
数据智能老司机4 天前
用于进攻性网络安全的智能体 AI——在 n8n 中构建你的第一个 AI 工作流
人工智能·安全·agent
数据智能老司机4 天前
用于进攻性网络安全的智能体 AI——智能体 AI 入门
人工智能·安全·agent
用户962377954484 天前
DVWA 靶场实验报告 (Medium Level)
安全
red1giant_star4 天前
S2-067 漏洞复现:Struts2 S2-067 文件上传路径穿越漏洞
安全
用户962377954484 天前
DVWA Weak Session IDs High 的 Cookie dvwaSession 为什么刷新不出来?
安全
阿白的白日梦4 天前
winget基础管理---更新/修改源为国内源
windows
cipher6 天前
ERC-4626 通胀攻击:DeFi 金库的"捐款陷阱"
前端·后端·安全