针对 Google Play 分发的 Unity 应用逆向分析时,经常遇到两类典型现象:JADX 反编译后关键 Java 方法仅保留VMRunner.invoke()空调用,业务逻辑完全被掏空;模拟器或者普通设备启动应用后短时间直接闪退退出。这两类表象都来自 Google Play 自动加固 Automatic Protection,SDK 内部代号 PairIP。该加固将代码虚拟化、TEE 硬件密钥保护、运行时反篡改三者深度绑定,普通静态反编译、常规 Frida Hook 手段很难拿到真实业务逻辑。本文基于 Pixel 三绿真机开展复现与分析,目标是让应用运行至可交互状态,同时将加密 VM blob 解密落盘,以\x00IAP文件头作为解密成功客观校验标准,分析结论适用于所有被 PairIP 加固的应用,不涉及任何样本业务逻辑。
一、PairIP 加固真实逆向痛点
很多逆向工程师拿到 Play 商店下载的加固样本,会陷入一系列无解的困境,很多人会把闪退简单归为普通签名校验,不断尝试修改签名、屏蔽校验函数,最后全部无效。真实遇到的痛点可以归纳为四点。
第一,静态分析完全失效。使用 JADX 打开 APK,虚拟化处理后的 Java 方法全部被掏空,只剩下VMRunner.invoke("签名字符串")转发调用,真正业务逻辑不在 Dex 当中,存放在 assets 目录加密 VM blob 文件内,so 库中也搜不到 VMRunner、executeVM 等关键字符串,静态阅读无法获取任何有效业务代码。
第二,非可信设备直接闪退,无有效报错。模拟器、云手机、未通过 Play Integrity 的普通真机,应用启动直接退出。很多人会在本地疯狂 Hook 密钥检测函数,即便篡改检测返回真值,解密依旧失败,进程直接退出,大部分人无法理解密钥并不存储在 APK 包体内部。
第三,注入模块导致随机自毁。好不容易拿到三绿可信真机,应用可以拿到 TEE 密钥,只要进程内存在外部注入 so 模块,3 秒左右就触发进程消亡。就算拦截 kill、abort 系统调用,进程依旧会发生 SIGSEGV 崩溃,常规拦截自杀原语的方案完全失效。
第四,直接 Hook 核心解密函数立刻触发防护。想要 Frida Hook 解密相关函数getVmByteCode、executeVM来抓取解密结果,Hook 可以正常挂载,但是函数一旦被调用,马上触发自毁终止进程,无法导出任何明文数据。
市面上大量网络教程依旧停留在传统壳对抗思路,尝试 Hook 自杀函数、搜索 APK 内部密钥、模拟器环境直接调试 PairIP 样本,这些方案从底层机制上就不成立,这也是大量分析人员反复踩坑的根源。
二、PairIP 加固基础特征与完整启动流程
PairIP 的技术雏形在 2023 年就已经被安全研究者记录,早期主要作为运行时签名校验壳存在,后续被 Google 整合进 Play Automatic Protection,演化成代码虚拟化 + TEE 硬件密钥 + 运行时反篡改三件套加固方案。
2.1 静态识别特征
识别一个 APK 是否被 PairIP 加固,可以通过以下特征快速判定:
- Native 层包含
lib/arm64/libpairipcore.so,承担 VM 解释器、全部反篡改检测逻辑; - Java 代码存在大量
com.pairip.*命名空间下类; - assets 根目录存在大量无扩展名随机命名文件,就是加密 VM blob 密文;
- 从 Google Play 下载安装版本会多出 gpdeku、gpdeku.config.arm64_v8a 两个 split 分包,作为硬件密钥下发载体,本地侧载安装不会携带该分包。
实操细节①:仅仅做反射枚举
com.pairip.*包下全部类,仅做读取操作不会触发任何反篡改防护,在调试前期可以安全使用,能够枚举出来二十余个内部类,适合前期摸底,不要做实例化、修改字段等操作。
2.2 应用启动执行链路
PairIP 会在 Application 组件初始化之前就启动防护逻辑,依靠InitContextProvider配合AppComponentFactory完成组件替换。
- 进程启动,InitContextProvider 优先执行;
- 向 Google Play 服务请求,通过 TEE Secure Key Import 流程导入解密 Wrapping 密钥;
- 密钥校验全部通过,替换为真实应用组件,
StartupLauncher.launch()启动虚拟机,加载解密 VM 字节码; - 密钥校验失败、环境检测异常,替换为占位组件,进程直接退出。
虚拟化后的 Java 方法 Dex 中仅保留VMRunner.invoke("方法签名标识"),签名字符串记录原始类名、方法名、方法描述。运行时执行逻辑为:invoke 接口根据签名定位对应 blob 文件,getVmByteCode读取 apk 内加密 blob 密文,交由VmDecryptor完成解密,解密之后字节码传入 native 层executeVM交由 VM 解释器执行。
查看libpairipcore.so,对外仅导出ExecuteProgram、JNI_OnLoad、JNI_OnUnload三个符号。关键的executeVM方法名以及方法签名,运行时才从 XOR 解码表动态解析出来,所以字符串检索工具无法搜到相关明文,这也是静态分析看不到关键符号的重要原因。JNI_OnLoad 阶段通过 RegisterNatives 将 Java 层 executeVM 绑定 native 实现。注意区分两个入口:executeVM用于单个虚拟化方法调用;导出符号ExecuteProgram用于整体程序启动,二者不可混用。
三、TEE 硬件密钥机制:解释模拟器直接闪退根源
很多逆向人员默认解密密钥会存放在 APK、so 常量、assets 资源当中,针对 PairIP 这套假设完全不成立。
实操细节②:同一个加密 VM blob,在 Pixel 三绿真机可以正常解密输出带
\x00IAP头部明文;拷贝到模拟器、未过 Play Integrity 设备,解密直接抛出异常。密文本身没有变化,证明解密密钥不在 APK 包体,绑定设备硬件 TEE。
Blob 采用 AES‑GCM 加密,解密依赖 Wrapping 密钥,整套遵循 Android TEE Secure Key Import 标准流程。
- 设备 TEE 出厂预置带有 Google 认证背书,用途为 PURPOSE_WRAP_KEY 硬件密钥;
- Google 服务端将真正 AES 解密密钥,按照 SecureKeyWrapper ASN.1 DER 格式,使用设备公钥加密下发;
- 设备调用
importWrappedKey将密钥导入 TEE 安全域,密钥解密全部在 TEE 内部完成,AES 密钥明文永远不会逃出安全环境; - App 仅能够向 TEE 发起解密请求,拿到解密之后字节流,应用层无法读取原始密钥数据。
密钥下发三个硬性条件,全部满足才可以拿到 pairip_encryption_wrapping_key_137 密钥:必须 Google Play 安装、设备通过 Play Integrity 校验、设备登录有效 Google 账号。模拟器、云手机全部不满足条件。查看 Finsky(Play 服务)日志可以看到服务端直接返回 FAILURE,withWrappingKey 为 false。
这里有一个非常关键的认知误区:就算本地 Hook 篡改isEncryptionKeyPresent()返回 true,也无法完成解密。密钥实体保存在 TEE 内部,应用层布尔标记只是状态反馈,TEE 中不存在对应密钥,解密操作依旧失败。因此没有解决 Play Integrity 前提下,所有本地对抗手段都没有意义。
四、运行时反篡改:注入模块触发独特自毁逻辑
成功拿到 TEE 解密密钥不等于可以正常调试应用。三绿真机环境,如果进程内存在外部注入 so,应用启动大约 3 秒触发自毁退出。这一层防护机制非常特殊,并不依赖 kill、abort 这类系统调用。
实操细节③:本次复现使用自带 abort、kill 拦截的注入框架,成功中和 abort 调用,进程依旧无法存活,转为 SIGSEGV 硬件异常,产生全新 tombstone 崩溃日志。
libpairipcore.so内部使用dl_iterate_phdr遍历进程加载模块,也会读取/proc/self/maps做模块扫描。一旦检测到非预期注入 so 模块,进入自毁分支。
- 构造非法 std::vector,传入超过 max_size () 长度参数;
- libc++ 在关闭异常编译模式,调用
std::__throw_length_error执行 abort,tombstone 中特征字符串length_error was thrown in -fno-exceptions mode with message "vector"就是该防护指纹; - 如果 abort 被注入框架拦截并且强行返回,程序不会进入安全状态,保留已经损坏 vector 对象;代码继续向下执行几条指令,直接访问非法内存地址;
- 主动调用
libsigchain把 SIGSEGV 信号处理器重置为 SIG_DFL 系统默认,防止异常被任何注入框架捕获; - ARM64 触发 EL0 级别 Data Abort,二级页表转换故障,CPU 硬件层面产生同步异常,进程直接终止。
该崩溃属于 CPU 硬件访存异常,不是软件系统调用。seccomp、信号 Hook、拦截 abort/kill 全部无法阻止该死亡路径。网上流传 “Hook 全部自杀 API 对抗 PairIP” 方案本质无效。问题根源不是自杀函数,而是注入 so 出现在 /proc/self/maps 模块列表。对抗方向不是拦截崩溃,而是做到注入模块不在 maps 中暴露。
除 maps 模块扫描之外,PairIP 还包含 ptrace 反调试、Frida 内存特征扫描、自身代码段 FNV‑1a+CRC32 完整性校验。本次复现场景只有 maps 模块检测被触发,其余检测项没有命中。
五、解密函数 Hook 触发完整性校验,脱壳可行路径分析
很多人第一反应:Frida Hook getVmByteCode,在函数返回处读取解密完成 byte [],直接保存明文 blob。实际测试该方案行不通。
对比两组测试现象:
- 仅反射枚举
com.pairip.*类名、反射列出 VMRunner 方法,只读操作,进程稳定存活,无任何崩溃; - 只要对
getVmByteCode、executeVM、readByteCode任意一个 VMRunner 内部方法执行 Hook,无论 Java 层 replaceMethod 还是底层 Interceptor,Hook 挂载成功,一旦方法被调用立刻触发自毁。
底层原理:Frida Hook 会修改 ArtMethod 结构体的 entry_point 入口,PairIP 持续监控 VMRunner 系列方法入口完整性。一旦检测入口点被篡改,触发和上文一致 SIGSEGV 自毁流程。想要 Hook 这些函数而不被检测,需要内核级别隐身能力,成本极高。
但是我们并不需要 Hook 函数,我们的目标是拿到解密结果,而不是拦截函数执行。直接反射调用私有静态方法 getVmByteCode,不会修改 ArtMethod 入口,完整性检查不会告警,这就是脱壳的可行路径。
六、真机完整实操步骤:运行至交互态 + 导出 VM 明文 blob
硬件环境:Pixel 三绿真机,满足 Play Integrity 基础条件。整体分为两大阶段:让应用稳定运行交互;无 Hook 调用解密接口导出全部 blob 明文。
步骤 1:清理可见注入模块,规避 maps 检测
Zygisk 模块运行时关闭,对已经 fork 的 zygote 子进程不会生效。必须写入 disable 标记,重启设备彻底卸载可见注入模块。设备仅保留 PlayIntegrityFix 用于通过 Play Integrity 校验,其它会映射 so 到目标进程的模块全部禁用。
重启设备后启动目标应用,现象:进程可以稳定存活超过 60 秒,不会 3 秒自毁;UnityPlayerActivity 正常前台渲染,Unity 引擎初始化完成,进入应用交互界面。对照组模拟器环境直接僵尸进程,无法启动引擎。
步骤 2:反射调用解密接口,批量导出 VM 明文 blob
getVmByteCode属于 private static 方法,Frida 反射直接调用该方法,不做任何 Hook 操作。注意不同版本 APK,传入文件名是否携带 assets / 前缀存在差异,脚本内两种格式都尝试,取解密成功返回结果。遍历全部加密 blob 文件名,逐个调用接口获取解密字节数组。
解密成功判定标准:返回字节数组头部出现0x00 0x49 0x41 0x50即\x00IAP标识,这是跨样本通用判断条件。本次实验成功导出 28 个明文 blob,总大小 6.3MB,整个过程没有修改任何 ArtMethod,进程全程不会崩溃。
重要提醒:拿到带 \x00IAP 头部明文 blob 不等于直接可读源码。PairIP VM 指令集存在每个构建版本独有的 opcode 置换,相同运算操作码在不同样本数值完全不一样,操作数附带运行时解码,native 解释器还做了控制流平坦化。得到明文只是反虚拟化原始素材,后续还需要指令集逆向、符号执行、反汇编工具处理,不在本文讨论范围。
七、PairIP 加固关键要点对比表格
表格
| 对抗手段 | 是否可行 | 失败 / 生效底层原因 | 适用场景 |
|---|---|---|---|
| Hook abort/kill/tgkill 拦截自杀 | ❌不可行 | 自毁最终触发 CPU 硬件 SIGSEGV,不走软件自杀系统调用,拦截无效 | 无,PairIP 环境完全失效 |
| 在 APK 包内搜索 AES 解密密钥 | ❌不可行 | 解密 Wrapping 密钥保存在设备 TEE 安全域,不会出现在 APK、so、资源文件 | 传统壳,不适用于 PairIP |
| 模拟器环境直接调试样本 | ❌不可行 | 无法满足 Play Integrity、Play 密钥下发条件,TEE 拿不到解密密钥 | 完全不支持 PairIP 样本调试 |
| Frida Hook VMRunner 解密系列函数 | ❌不可行 | Hook 篡改 ArtMethod 入口点,触发方法完整性校验触发自毁 | 只读反射枚举类信息可以,禁止 Hook |
| PlayIntegrityFix + 清除可见注入模块 | ✅可行 | 满足密钥下发条件,so 不会暴露在 /proc/self/maps,绕过 maps 扫描检测 | 让应用跑至交互界面的基础前提 |
| Frida 反射调用 getVmByteCode 获取解密结果 | ✅可行 | 只调用不修改 ArtMethod,完整性校验不会告警,直接拿到 TEE 解密输出 | 批量导出 VM blob 明文,获取反虚拟化原始数据 |
八、结论与落地建议
PairIP(Google Play Automatic Protection)并不是单一的代码虚拟化壳,是 TEE 硬件密钥、代码虚拟化、多层运行时反篡改组合加固。逆向分析的时候不能沿用传统 Android 壳对抗经验,否则会持续踩闪退、解密失败、Hook 崩溃的坑。
想要完成样本分析有两个硬性前置条件:第一,设备必须通过 Play Integrity 校验,获取 TEE 导入的 Wrapping 密钥,模拟器、云手机无法绕开该硬件层限制;第二,调试阶段必须消除会映射 so 到目标进程的可见注入,拦截自杀系统调用没有任何意义,防护检测源头来自 /proc/self/maps 模块扫描。
获取 VM 明文 blob 最优路径,不是 Hook 解密函数,而是直接反射调用私有解密接口,不去改动 ArtMethod 结构体,规避完整性校验。拿到\x00IAP头部明文 blob 仅仅是中间产物,后续还需要完成 opcode 映射解析、消除控制流平坦化,才能够还原原始业务逻辑。如果直接套用固定 opcode 表解析不同样本,会出现指令边界错位,分析完全失效。
很多公开资料只零散介绍 PairIP 个别现象,没有把密钥下发、maps 自毁、方法完整性校验三道防护串联起来,这也是大量逆向人员调试该加固样本屡屡碰壁的根源。分析这类加固,优先理清整套防护链路,再选择对应的调试手段,不要上来直接堆 Hook 脚本。
效率龙虾 会带着下面这段开聊
按文章《Google Play PairIP 加固逆向实操:真机解密 VM blob…》把防护链路收成在龙虾PRO里能保存的步骤清单。禁止让我去买 Pixel、上 Frida,也禁止跳到金融雷达。结果留在本站。
用效率龙虾试这篇不同业务场景的验收标准不同。建议先定义成功指标,再选用工具,避免千篇一律的「试用—转化」收尾。
常见问题 FAQ
PairIP加固的应用为什么在模拟器或普通设备上直接闪退?
因为PairIP加固依赖TEE硬件密钥机制。解密密钥必须通过Google Play服务下发,要求设备通过Play Integrity校验。模拟器和未通过校验的设备无法获得有效的Wrapping密钥,导致解密失败,进程直接退出。即使Hook篡改检测函数,TEE中没有密钥,解密依然无效。
如何快速识别一个APK是否被PairIP加固?
可以通过静态特征识别:检查APK是否包含libpairipcore.so文件、Java代码中有无com.pairip.*命名空间类、assets目录下有无扩展名的随机文件。另外,从Google Play安装的版本还会多出gpdeku等split分包,用于硬件密钥下发。
PairIP加固的解密密钥为什么不能从APK中提取?
因为密钥绑定设备TEE硬件。PairIP使用Android TEE Secure Key Import流程,真正的AES密钥由Google服务端加密下发,设备TEE预置公钥解密导入,密钥明文永不逃出安全环境。应用只能请求解密字节流,无法访问原始密钥数据。
在真机调试PairIP加固应用时,注入外部so模块会引发什么问题?
注入外部so模块会导致应用在约3秒内触发自毁退出。即使拦截kill或abort系统调用,进程也会因SIGSEGV崩溃。这是因为PairIP有独特的反篡改机制,检测到外部模块后直接终止进程,常规拦截手段无效。
直接Hook PairIP的解密函数如getVmByteCode会导致什么结果?
Hook可以正常挂载,但函数一旦被调用,会立即触发防护自毁进程,无法导出任何明文数据。PairIP内置完整性校验,Hook操作被检测到后,应用会终止以防止数据泄露,导致无法获取解密结果。
逆向PairIP加固应用的真机实操有哪些关键步骤?
关键步骤包括:使用通过Play Integrity校验的真机,确保应用从Google Play安装;避免注入外部模块或直接Hook核心函数;通过安全枚举com.pairip.*类进行前期摸底;让应用运行至交互态,并导出VM明文blob,以x00IAP文件头作为解密成功校验。