内存加载带有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
相关推荐
zskj_zhyl1 小时前
智绅科技:以科技为翼,构建养老安全守护网
人工智能·科技·安全
zsq1 小时前
【网络与系统安全】域类实施模型DTE
网络·安全·系统安全
csdn_aspnet2 小时前
在 Windows 机器上安装和配置 RabbitMQ
windows·rabbitmq
csdn_aspnet2 小时前
Windows Server 上的 RabbitMQ 安装和配置
windows·rabbitmq
缘友一世4 小时前
网安系列【4】之OWASP与OWASP Top 10:Web安全入门指南
安全·web安全
HMS Core5 小时前
HarmonyOS免密认证方案 助力应用登录安全升级
安全·华为·harmonyos
Gauss松鼠会6 小时前
GaussDB权限管理:从RBAC到精细化控制的企业级安全实践
大数据·数据库·安全·database·gaussdb
热爱生活的猴子7 小时前
Poetry 在 Linux 和 Windows 系统中的安装步骤
linux·运维·windows
小赖同学啊8 小时前
基于区块链的物联网(IoT)安全通信与数据共享的典型实例
物联网·安全·区块链
R-sz10 小时前
java流式计算 获取全量树形数据,非懒加载树,递归找儿
java·开发语言·windows