JNI 层是 Java 与 SO 的边境检查站
分析带 native 签名的 App 时,最有价值的信息都发生在 JNI 边界:Java 层把什么参数传进了 SO,SO 又把什么结果还了回来。jnitrace 这个 Frida 工具就是干这个的——自动 hook 所有 JNI 函数调用,打印参数和返回值。
pip install jnitrace
jnitrace -l libtarget.so com.target.app-l 指定要跟踪的 SO。跑起来后,目标 SO 的每一次 JNI 调用(FindClass、GetMethodID、CallObjectMethod、NewStringUTF……)全部实时打印。
输出怎么读
/* TID 12345 */
12345 ms JNIEnv->FindClass("com/xxx/SignUtils")
12345 ms JNIEnv->GetMethodID(0x1234, "getSign", "(Ljava/lang/String;)Ljava/lang/String;")
12346 ms JNIEnv->NewStringUTF("timestamp=1787968800&data=...")
12346 ms JNIEnv->CallObjectMethod(0x1234, 0x5678, 0x9abc)
12347 ms JNIEnv->GetStringUTFChars(0x9abc) => "a8f5c3...签名结果"这一小段就讲完了整个故事:Java 层传了 timestamp=...&data=... 进 native,native 返回了签名结果。签名算法的输入输出格式不用逆 SO 就拿到了。
精髓一:用 jnitrace 做"算法黑盒分析"
拿到输入输出后,立刻做对照实验:
- 固定输入,多次调用看输出是否变化(判断有没有随机数/时间戳参与)
- 改输入的一个字符,看输出变化范围(雪崩效应明显 = 标准哈希/加密;只变几位 = 自定义简单变换)
- 输入全 0、全 F 等特征值,看输出模式(识别算法家族)
这三板斧下来,不用打开 IDA 就能判断算法类型,决定是"手搓还原"还是"Frida 直调"。
精髓二:过滤噪音
jnitrace 全量输出非常吵(App 的 JNI 调用每秒几百次),过滤是关键:
# 只看指定方法相关的调用
jnitrace -l libtarget.so -m "getSign|check" com.target.app
# 忽略某些 JNI 函数(字符串操作类最吵)
jnitrace -l libtarget.so -i "GetStringUTFChars" -i "NewStringUTF" com.target.app
# 输出到文件慢慢分析
jnitrace -l libtarget.so -o trace.log com.target.app实战节奏:先全量跑一遍摸清调用规模,然后逐步加过滤条件收敛到目标方法附近。
精髓三:配合 RegisterNatives 定位
jnitrace 还能抓动态注册——输出里的 RegisterNatives 调用会列出所有方法名和地址。这和我之前讲的手动 hook RegisterNatives 是同一个信息,但 jnitrace 零成本:
12350 ms JNIEnv->RegisterNatives(0x1234, 0x7f8abc, 3)
method[0] = getSign (Ljava/lang/String;)Ljava/lang/String; @ 0x7f12345000
method[1] = check (I)Z @ 0x7f12346000拿到地址后 模块基址 + 偏移 算出 IDA 里的位置,直接跳过去看实现。
与其他工具的组合拳
- jnitrace 拿输入输出 → 判断算法类型
- Frida hook 具体 native 函数 → 验证猜测(改输入看输出)
- IDA 静态分析 → 彻底理解算法(需要手搓时)
- unidbg 模拟执行 → 需要脱离设备批量调用时
jnitrace 是这条链路的侦察兵,花十分钟跑一遍,后面每一步的方向都清晰了。
总结
- jnitrace 自动 hook 全部 JNI 调用,Java↔SO 边界的参数返回值全透明
- 黑盒分析三板斧:固定输入看稳定性、改输入看雪崩、特征输入看模式
- 用 -m/-i 过滤收敛噪音,先全量后聚焦
- RegisterNatives 输出直接给 native 函数地址,衔接 IDA
交流微信:run1255