拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

TEESimulator注入技术深度解析:ptrace远程加载从SCM_RIGHTS传fd到信号返回的完整过程

TEESimulator注入技术深度解析:ptrace远程加载从SCM_RIGHTS传fd到信号返回的完整过程

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 对象把"顺序依赖"写成了真正的控制流:

  1. PTRACE_ATTACH附加:目标进程收到SIGSTOP停下,注入器用waitpid确认它确实停下了(injector/utils.cpp 的wait_for_trace还会处理EINTR重试)。
  2. 备份全部寄存器:get_regs拍一张 CPU 寄存器的快照存入backup_regs,由RegisterRestorer负责在作用域退出时原样写回——目标进程会在被中断的那条指令上精确续跑。
  3. 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.cppptrace 原语:寄存器读写、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.ruleptrace 附加与库文件读取的 SELinux 授权
CMakeLists.txtinject目标的构建定义(按 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),仅供参考

返回列表