iOS/Macos C++ thread_local 具体实现分析

116 阅读3分钟

示例如下: image.png

直接断点运行查看汇编实现

由于我们对 thread_local tls_variable 变量进行了 ++ 操作,因此在汇编中大概率会有一个 add x?, x?, #1 的指令,因此通过观察下图划线的三条指令,可以得知 x8 寄存器中存储的地址就是获取 tls_variable 变量的 dyld 函数 tlv_get_addr

image.pngtlv_get_addr 进行符号断点分析发现:

  1. TPIDRRO_EL0 寄存器对应内存中存在 pthread_key_t key 对应的值,则直接返回内存地址 ( 函数 instantiateTLVs_thunk 的第一个参数的签名为 pthread_key_t )
  2. 如果不符合 1,则执行 dyld instantiateTLVs_thunk 以及 RuntimeState::_instantiateTLVs

image.png tlv_get_addr 函数的源码也可通过 dyld 的 threadLocalHelpers.s 文件查看

instantiateTLVs_thunk 的实现主要是对 RuntimeState::_instantiateTLVs 的包装 image.png

RuntimeState::_instantiateTLVs 实现如下: image.png 针对单个 pthread_key_t 的 lazy 实现,使用 libsystem 的 malloc 开辟相关的内存,再保存到 pthread 的 tsd 数组中

libpthread 中 _pthread_setspecific 的实现如下: image.png

基本流程了解后,目前未解决的问题有如下:

  1. 变量 thread_local int tls_variable 是如何访问到的?
  2. tlv_get_addr 函数是如何被设置到 x8 寄存器对应内存?其中偏移值为 #0x8 #0x10 的内存具体有什么含义?
  3. TPIDRRO_EL0 寄存器是何时被赋值的?

问题一:

tls_variable 变量是如何访问到的?

image.png 注意这里的 adrp   x0, 5 指令,代表 ( 当前 pc 寄存器值 & page_size ) + 5 * page_size 的结果赋值到 x0 寄存器。由于在 Macos 下 page_size 是 4K,因此这里的计算方式为 x0 = (0x1000030a4 & 0x1000) + 5 * 0x1000 = 0x100008000

image.png

同时该内存在进程中所在的 section 为 __DATA,__thread_vars,我们的进程中有两个 thread_local 变量,此 section 的大小却为 0x30,因此推断每个变量在 Section 中占用 0x18 字节,同时也能和汇编中的 #0x8, #0x10 的偏移量访问对应。同时 thread_local 变量的初始值是通过 __DATA,__thread_data__DATA,__thread_bss 两个 Section 来初始化的(相关代码可以在 ld64 和 dyld 中找到) image.png

问题二:

tlv_get_addr 函数是如何被设置到 x8 寄存器对应内存?其中偏移值为 #0x8 #0x10 的内存具体有什么含义?

image.png

arm64 dyld 在进程启动时,forEachThreadLocalVariable函数会以单次 0x18 (struct TLV_Info) 字节大小遍历 __DATA,__thread_vars,同时在 #0x0 设置 tlv_get_addr 函数指针,#0x8 设置 pthread_key_t,#0x10 代表 offset。TLV_Info 结构体如下:

struct TLV_Thunk
{
    void*   (*thunk)(TLV_Thunk*);
    size_t	  key;
    size_t	  offset;
};

因此 #0x0 指的是此处的 thunk, #0x8 是 pthread_key,#0x16 是 offset 变量

问题三: TPIDRRO_EL0 寄存器是何时被赋值的?

明确一个结论:用户态下 TPIDRRO_EL0 是无法被设置的,只有在内核态才能。

默认情况下, libpthread 在初始化线程时将会使用 struct phthread_s 成员变量 tsd 的起始地址作为 TPIDRRO_EL0 寄存器的值

image.png

最终在内核态的 xnu/osfmk/arm/machdep_call.c 设置 TPIDRRO_EL0 寄存器 image.png

因此,如果我们能使用用户态 API 直接设置 TPIDRRO_EL0 寄存器,即可伪造指定线程的 TLS