AIGC标识 让 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 全局对象的脚本全部失效。

帖子的核心思路是:

  1. 创建一个 Agent 工程,通过 npm 安装 frida-java-bridge 依赖
  2. 在 TypeScript 入口中显式引入该桥接模块
  3. 使用 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.jsfrida-java-bridgescript.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 referenceIndirectRefKind kJniTransition,低 2 位为 00)。

这些引用不是真正的对象指针,而是指向调用者 quick frame 中压缩引用槽位的指针。在 CheckJNI 启用的构建上(userdebug/eng 系统在 zygote 启动时强制开启),任何校验型 JNI 调用——NewLocalRefIsInstanceOfCallNonvirtual*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.Classklass_ 字段是自身的压缩引用,所以 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-bridgeandroid.js 在 Android 16 上需要查找 VisiblyInitializedCallback 相关的符号(如 MakeVisibleRunMarkVisiblyInitialized)。这些符号以 C++ name mangling 形式存在:

_ZN3art11ClassLinker26VisiblyInitializedCallback11MakeVisibleEPNS_6ThreadE
_ZN3art11ClassLinker26VisiblyInitializedCallback3RunEPNS_6ThreadE
_ZN3art11ClassLinker26VisiblyInitializedCallback22MarkVisiblyInitializedEPNS_6ThreadE

Frida 的 findSymbolByName() 只解析 .symtab/.dynsym,strip 后返回 null。但这些符号仍然存在于 LZMA 压缩的 .gnu_debugdata mini-debuginfo 段中。

解决方案:改用 `enumerateSymbols()` 这个方法会解析 `.gnu_debugdata`。将所有符号构建为 `Map`,作为 `findSymbolByName` 的 fallback:
const 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 后不再复现。

补丁管理

为了方便维护,我将两个补丁保存为标准 diff 格式,并编写了 `apply-patches.sh` 脚本。每次 `npm install` 重新安装 `frida-java-bridge` 后,执行以下命令即可重新应用补丁:
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。

参考资料


本文涉及的代码修改由 AI 辅助生成并经实际设备验证(OnePlus GM1911, Android 16 userdebug, frida-server 17.16.4)。

posted on 2026-07-24 16:45  maiiiiii  阅读(33)  评论(0)    收藏  举报

导航