让 Camille 在 Android 16 + Frida 17.x 上重新工作
文章使用AI辅助生成,AI生成的部分已使用 details 标签折叠
背景
之前一直使用Camille 来进行APP的隐私获取行为检测。但是测试机升级到 Android16 以后,之前的frida-server-16.6.4版本启动提示
libsepol.avtab_read_item: more than one specifier
libsepol.avtab_read: failed on entry 1247 of 94491
Unable to load SELinux policy from the kernel: unsupported policy database format
虽然服务已经启动,但 camille 插件无法正常捕获进程。于是开始升级 frida 到最新的 17.16.4 版本,并配套升级 camille 的 frida 版本。
紧接着问题就一个又一个的出现了。
问题一:Frida 17.x 不再自动注入 Java Bridge
升级到 Frida 17.x 后,直接运行 camille 报错:
ReferenceError: 'Java' is not defined
搜索相关词条,找到了这样一篇帖子 关于Frida 17+版本Python API调用JS引发Bridge故障的解析
原因是 Frida 17.x 的 Python API 不再自动包含 Java bridge。在 Frida 16.x 及之前,session.create_script() 注入的脚本运行时中会自动存在一个全局 Java 对象,脚本可以直接调用 Java.perform()、Java.use() 等方法。但 Frida 17.x 取消了这个自动捆绑机制,导致所有依赖 Java 全局对象的脚本全部失效。
帖子的核心思路是:
- 创建一个 Agent 工程,通过 npm 安装
frida-java-bridge依赖 - 在 TypeScript 入口中显式引入该桥接模块
- 使用
frida-compile将 TypeScript 编译为 JS,再由 Python 脚本加载
移植 frida-compile 方案
按照帖子的思路,在 camille 项目中创建了 `agent` 目录:agent/
├── agent/
│ ├── index.ts # 入口:引入 frida-java-bridge 并挂载到全局
│ └── script.js # camille 原始 hook 脚本(从项目根目录同步)
├── patches/
│ ├── 01-android-stripped-libart.patch
│ └── 02-class-factory-transition-ref.patch
├── apply-patches.sh
├── package.json
└── tsconfig.json
入口文件 index.ts
import Java from 'frida-java-bridge';
(globalThis as any).Java = Java;
require('./script.js');
第一行导入 frida-java-bridge,第二行将其挂载到全局对象,第三行加载 camille 的 hook 脚本。这样当 script.js 执行时,Java 已经作为全局变量存在了。
编译命令:
cd agent
npm install
frida-compile agent/index.ts -o ../script_compiled.js -c
编译产物 script_compiled.js 将 frida-java-bridge 和 script.js 打包在一起,camille 启动时自动优先加载这个文件。
同时改进了 camille.py 的脚本加载逻辑:优先加载 script_compiled.js,并通过 recv('start') 模式替代原来的 setTimeout/setImmediate 控制启动时机,避免时序问题。
到这里,Java is not defined 的问题解决了。
问题二:Android 16 JNI 崩溃
Java bridge 加载成功后,hook 开始执行,但一碰到 Java 方法的 hook 就崩溃了:
JNI DETECTED ERROR IN APPLICATION: jobject is an invalid JNI transition frame reference
这是 Android 14+ (API 34+) 引入的一个变化:ART 在向 native 方法传递 this 和对象参数时,不再直接传递对象指针,而是传递 JNI transition frame reference(IndirectRefKind kJniTransition,低 2 位为 00)。
这些引用不是真正的对象指针,而是指向调用者 quick frame 中压缩引用槽位的指针。在 CheckJNI 启用的构建上(userdebug/eng 系统在 zygote 启动时强制开启),任何校验型 JNI 调用——NewLocalRef、IsInstanceOf、CallNonvirtual*Method 等——都会拒绝这种引用。
根本原因:IsJniTransitionReference() 需要遍历托管栈来验证引用,但 Frida 的 trampoline 帧对这个遍历过程是不透明的,所以校验始终失败。
修复方案:手动解码 + AddGlobalRef
这个问题需要对 `frida-java-bridge` 的 `class-factory.js` 打补丁,核心思路是将 transition reference "提升"为全局引用。整个修复分三部分:
1. 获取 ART 堆基址
压缩引用是相对于堆基址的偏移量,要还原完整指针需要 base + compressed。获取堆基址的方法很巧妙:
- 通过
env.findClass('java/lang/Class')找到java.lang.Class并创建全局引用 - 切换到 Runnable 状态(因为
DecodeJObject需要 shared mutator lock) - 调用
art::Thread::DecodeJObject获取java.lang.Class的原始 mirror 指针 - 读取前 32 位字段——由于
java.lang.Class的klass_字段是自身的压缩引用,所以base = raw - compressed
2. 提升 Transition Reference
从槽位读取压缩引用后,通过 art::JavaVMExt::AddGlobalRef 创建全局引用。关键在于 AddGlobalRef 只获取 globals lock,不需要 mutator lock,因此可以在 kNative 状态下安全调用——这正是 JNI NewGlobalRef 的内部实现。而且它不执行 CheckJNI 校验,所以接受重建后的原始指针。
3. 集成到方法调用流程
在 handleMethodInvocation 中,对 selfHandle(实例方法的 this)和每个对象类型参数执行提升。提升后的全局引用在方法调用结束后通过 env.deleteGlobalRef 释放,避免泄漏。
问题三:Stripped-libart.so-符号查找
JNI 崩溃修好了,但 Android 16 还有一个坑:libart.so 的 .symtab 被 strip 掉了。
frida-java-bridge 的 android.js 在 Android 16 上需要查找 VisiblyInitializedCallback 相关的符号(如 MakeVisible、Run、MarkVisiblyInitialized)。这些符号以 C++ name mangling 形式存在:
_ZN3art11ClassLinker26VisiblyInitializedCallback11MakeVisibleEPNS_6ThreadE
_ZN3art11ClassLinker26VisiblyInitializedCallback3RunEPNS_6ThreadE
_ZN3art11ClassLinker26VisiblyInitializedCallback22MarkVisiblyInitializedEPNS_6ThreadE
Frida 的 findSymbolByName() 只解析 .symtab/.dynsym,strip 后返回 null。但这些符号仍然存在于 LZMA 压缩的 .gnu_debugdata mini-debuginfo 段中。
解决方案:改用 `enumerateSymbols()`
这个方法会解析 `.gnu_debugdata`。将所有符号构建为 `Mapconst symbolsByName = new Map();
for (const symbol of art.enumerateSymbols()) {
symbolsByName.set(symbol.name, symbol.address);
}
同时,Android 16 重构了 VisiblyInitializedCallback 的回调流程,原有的字节签名匹配模式不再可靠。补丁将原来匹配失败后直接 return 改为 continue,让循环尝试下一个签名模式,并在全部失败后走符号查找 fallback。
问题四:JIT 编译竞争
即使 JNI 补丁全部就位,userdebug 系统上仍偶发 SIGSEGV,崩溃栈指向 JIT 线程:
art::HBasicBlockBuilder::Build
art::optimizing_compiler.cc
原因:Frida 修改 ArtMethod(设置 kAccNative + kAccCompileDontBother + quick generic JNI trampoline)后,如果 JIT 线程在这之前已经将该方法加入编译队列,JIT 会编译被修改后的 ArtMethod,读到清零的 CompilerMetadata,导致崩溃。
这是 Frida 与 ART 的已知竞争问题,不是补丁引入的。临时解决方案:
adb shell "setprop dalvik.vm.usejit false"
adb shell stop && adb shell start
关闭 JIT 后不再复现。
补丁管理
cd agent
npm install
sh apply-patches.sh
frida-compile agent/index.ts -o ../script_compiled.js -c
最终成果
经过上述一系列修复,camille 终于在 Android 16 + Frida 17.x 上稳定运行了。
Fork 的仓库地址地址
https://github.com/MaYiFei1995/Camille_Android16
新增特性
- 场景控制系统:移除原屏幕截图/模拟点击机制,改为运行时键盘输入切换 5 个检测场景(同意隐私政策前/后、IDLE、初始化中、请求业务中),所有告警标注当前场景标签。
- 检测项扩展:补充 MSA SDK OAID、DRM 设备 ID、GAID、NAI、账户信息、传感器注册监听等检测项。
- 模块配置系统:新增
utlis/modules.json配置表和-mc/--module-config参数,支持检测项级别的启用/禁用控制,与-u/-nu模块级控制正交组合。 - 前台应用自动检测:新增
-FU/--front-most参数,自动获取设备当前前台运行应用,免手动输入包名或查 PID。
参考资料
- zhengjim/camille — 原项目作者
- 看雪论坛:关于Frida 17+版本Python API调用JS引发Bridge故障的解析(作者:逆向小呆呆)— Frida 17.x 无 Java bridge 修复方案的参考
- frida-java-bridge — Frida Java bridge 项目
本文涉及的代码修改由 AI 辅助生成并经实际设备验证(OnePlus GM1911, Android 16 userdebug, frida-server 17.16.4)。
浙公网安备 33010602011771号