TEESimulator注入技术深度解析:ptrace远程加载从SCM_RIGHTS传fd到信号返回的完整过程
【免费下载链接】TEESimulatorSoftware simulation for Android hardware-backed key pairs with key attestation项目地址: https://gitcode.com/gh_mirrors/te/TEESimulator
TEESimulator 是一款针对 Android 硬件级密钥认证(Key Attestation)的软件模拟器,它通过一个基于 ptrace 的注入工具inject,把拦截库远程加载进系统的 keystore 守护进程,从而在进程内部接管 KeyMint 请求。本文面向想搞懂 Android 进程注入原理的读者,完整拆解这条链路:从PTRACE_ATTACH附加进程、用SCM_RIGHTS跨进程传递文件描述符(fd),到利用受控返回地址触发信号、把远程调用的结果"接"回手里。
一、为什么 TEESimulator 必须做 ptrace 注入?
TEESimulator 的拦截逻辑必须运行在 keystore 守护进程(Android 12+ 的keystore2,10/11 的keystore)自己的地址空间里,才能挂钩它的 Binder 调用。但这类系统服务有两个"护身符",让常规手段全部失效:
- 无法重编译:它是系统预置二进制,你改不了它的代码。
- 受限的链接器命名空间:动态链接器会拒绝
dlopen任何/data路径下的库——命名空间允许的搜索路径里根本不含/data,检查发生在打开文件之前,文件权限再大也没用。
所以 TEESimulator 选择最"原始"的路子:用 injector/main.cpp 中的inject工具,ptrace附加到正在运行的守护进程,替它调用dlopen加载我们的.so,再调用库导出的entry()完成挂钩,最后脱离现场——守护进程照常运行,仿佛什么都没发生。
二、注入前的三步准备:附加、备份寄存器、让出红区
整个注入由 injector/main.cpp 中的inject_library编排,开头用 RAII 对象把"顺序依赖"写成了真正的控制流:
PTRACE_ATTACH附加:目标进程收到SIGSTOP停下,注入器用waitpid确认它确实停下了(injector/utils.cpp 的wait_for_trace还会处理EINTR重试)。- 备份全部寄存器:
get_regs拍一张 CPU 寄存器的快照存入backup_regs,由RegisterRestorer负责在作用域退出时原样写回——目标进程会在被中断的那条指令上精确续跑。 - x86_64 额外下探 128 字节:跳过被中断函数可能仍在使用的"红区"(red zone),避免往栈上写数据时踩坏现场。
随后lsplt::MapInfo::Scan解析/proc/<pid>/maps,得到目标进程中libc.so等模块的基址。所有远程函数地址都不直接写死,而是用 injector/utils.cpp 的find_func_addr解决:在本地dlopen同一个libc.so拿到符号偏移,再加到目标进程中的模块基址上——ASLR 随机化的是加载基址,不随机化库内部布局,所以偏移量在两个进程里完全一致。
三、SCM_RIGHTS 传 fd:为什么"递文件句柄"是整个方案的灵魂
既然不能把库路径递给keystore2,那就换个思路:不递路径,递一个已经打开的文件描述符。Android 提供了android_dlopen_ext+ANDROID_DLEXT_USE_LIBRARY_FD组合——给链接器一个 fd,它就直接从 fd 加载 ELF,彻底绕开路径与命名空间检查。
坑在于:fd 必须有效于目标进程的文件表。这正是SCM_RIGHTS的用武之地:它是 Linux 内核原生的 fd 传递机制,通过 Unix 域套接字的控制消息(cmsg),把发送方打开的文件在内核里复制一份到接收方的 fd 表中。
injector/main.cpp 中transfer_fd_to_remote的完整"舞蹈"如下:
| 步骤 | 动作 | 关键点 |
|---|---|---|
| ① | 远程调用socket(AF_UNIX, SOCK_DGRAM) | 套接字由目标进程自己创建,活在 keystore 自己的 SELinux 域里,传输才合法 |
| ② | 生成 16 字符随机 magic,bind 到抽象地址 | 抽象 socket 没有文件系统节点,不存在"目录被 SELinux 拒绝穿越"的问题 |
| ③ | push_memory把cmsghdr缓冲区和msghdr压到目标栈上 | 栈空间由REG_SP下移腾出,16 字节对齐 |
| ④ | remote_pre_call发起recvmsg(fd, …, MSG_WAITALL) | 目标进程阻塞在 recvmsg 内部,等数据报到达 |
| ⑤ | 注入器本地sendmsg,附带库文件的 fd(SCM_RIGHTS) | 内核在目标进程 fd 表中安装副本 |
| ⑥ | remote_post_call收尾,read_proc读回控制缓冲区 | 解析 cmsg,得到目标进程中那个 fd 的编号 |
最后remote_dlopen拿这个编号配上ANDROID_DLEXT_USE_LIBRARY_FD,远程调用android_dlopen_ext完成加载。用完的 fd 由RemoteLibraryHandle析构时远程调用close清理,防止在守护进程里泄漏。
四、信号返回:用一次"受控崩溃"换回函数结果
远程调用(remote_call,injector/utils.cpp)是整篇文章最值得细品的部分,它是所有远程动作的原语:在别的进程里调用任意函数并取回返回值。
做法是改写目标寄存器:参数写入 ABI 约定的参数寄存器(x86_64 的rdi/rsi/…,arm64 的x0–x7),超出部分压栈,指令指针REG_IP指向目标函数,然后PTRACE_CONT放它跑。
最关键的一步是设置一个我们控制的返回地址:find_module_return_addr扫描目标 maps,返回libc.so中一段可读但不可执行页面的起始地址。x86_64 把它压栈(模拟call),arm64 则装入链接寄存器x30。
当远程函数执行完、跳回这个地址时,CPU 试图从不可执行页取指,触发SIGSEGV——故障地址恰好等于我们设置的返回地址。这个信号不是错误,而是约定的"完成信号":waitpid因之返回,remote_post_call校验REG_IP == 期望返回地址(任何别的停点都说明函数在真实位置崩了,此时取PTRACE_GETSIGINFO记录崩溃细节并放弃),最后从返回寄存器REG_RET(rax/x0)里直接读出函数结果。
一句话总结这个技巧:页面必须"存在但跑不了",因为故障才是信号,而不是事故。
remote_pre_call/remote_post_call拆成两半的用武之地正是上文第④步——让目标停在recvmsg内部,注入器从外部完成sendmsg,再回来收尾。
五、Staging 兜底与 entry() 契约
如果 fd 传递失败(比如 seccomp 过滤了recvmsg),injector/main.cpp 会走inject_via_staging兜底:把库拷到/data/local/tmp/lib<随机8位>.so、chmod 0644、远程调用普通dlopen,随后ScopedFileDeleter立即unlink文件——inode 因目标已映射而继续存活,文件却从磁盘上消失。注意它只在命名空间宽松的场景有效(Android 10/11 的keystore可以,keystore2不行,后者只能靠 fd 传递)。
加载成功后,remote_find_entry在目标里dlsym出entry符号并远程调用,参数就是dlopen句柄。两个值得记住的细节:
- 从不
dlclose:库常驻目标进程,钩子才持续有效。 - "注入成功"≠"钩子生效":
remote_call_entry只要调用完成就返回 true。所以守护进程会另行验证——等拦截库通过控制 socket(/data/misc/keystore/.teesim-ctl,藏在 keystore 自己的 0700 数据目录里)报到,见 Const.kt 与 module/sepolicy.rule 中对应 SELinux 授权;长时间不到报就打日志告警(提示查avc: denied)。
六、控制面:守护进程如何驱动注入循环
Kotlin 侧的 Injector.kt 把一切串起来:启动后开线程轮询/proc找 keystore 进程名对应的 pid,等 Binder 服务注册完毕(避免注入半初始化的进程),然后执行inject <pid> <lib.so> entry。keystore 守护进程会频繁重启,每次重启都会丢掉我们的库,所以循环监听 pid 变化并重新注入;连续失败 3 次后把退避从 2 秒拉长到 10 秒,避免死磕一个卡死的服务。
SELinux 层面,module/sepolicy.rule 授予crash_dump域对 keystore 的process *权限(即 ptrace 附加),并允许 keystore 读取/data/adb(adb_data_file)下的拦截库文件——enforcing 策略的设备上这两条缺一不可。
七、关键文件索引
| 路径 | 作用 |
|---|---|
| injector/main.cpp | 注入总编排:transfer_fd_to_remote、remote_dlopen、inject_via_staging与 RAII 守卫 |
| injector/utils.cpp | ptrace 原语:寄存器读写、process_vm_readv内存搬运、remote_call三件套 |
| injector/include/utils.hpp | 各架构寄存器宏(REG_SP/REG_IP/REG_RET)与 API 声明 |
| injector/README.md | 注入器设计文档:fd 传递六步舞、staging 兜底、entry 契约 |
| app/src/main/java/org/matrix/teesim/Injector.kt | 控制面注入循环:找 pid、判服务就绪、重注入与确认 |
| module/sepolicy.rule | ptrace 附加与库文件读取的 SELinux 授权 |
| CMakeLists.txt | inject目标的构建定义(按 ABI 交叉编译) |
八、写在最后
TEESimulator 的注入器把操作系统教材里的几块拼图——ptrace寄存器操控、process_vm_readv跨进程内存访问、SCM_RIGHTSfd 传递、不可执行页故障——组装成了一条完整可用的"远程dlopen"流水线。理解它,等于同时理解了 Android 链接器命名空间的限制与绕过方式、以及"用故障当信号"这一类精巧的系统编程技巧。若想动手复现,建议从 injector/README.md 开始,再对照 injector/main.cpp 顶部的 ASCII 流程图逐段走读源码。
【免费下载链接】TEESimulatorSoftware simulation for Android hardware-backed key pairs with key attestation项目地址: https://gitcode.com/gh_mirrors/te/TEESimulator
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考