安卓 JNI / Native Bridge 逆向:别卡在 Java 层,要顺着桥往下走
Android 逆向里有个特别典型的断点:Java 层明明已经追到一个很可疑的方法,结果点进去只看到 native,很多人就在这里开始掉线。其实问题不在于 JNI 多神秘,而在于你没把它当成一座“桥”来看——桥的上面、桥本身、桥下面,都要连起来。
先说结论:JNI 逆向不是跳层,是连层
很多人把 Java 层和 so 层当成两个世界。实际上更好的理解是:
Java 入口 -> native 声明 -> JNI 注册/绑定 -> 参数转换 -> Native 核心逻辑你如果只盯 Java,看不到真正算法;只盯 so,又很容易不知道谁在什么场景下调用它。真正有用的是把整条桥连起来。
第一步:先确认这座桥怎么连的
JNI 绑定通常有两种典型路径:
- 静态注册
- 动态注册
不先分清这点,后面很容易找错入口。
静态注册的信号
- 函数名里直接带 Java 包名/类名/方法名
- 导出符号相对直白
- 从 Java 声明到 native 实现能直接对上
动态注册的信号
- JNI_OnLoad 可疑
- 有 RegisterNatives 调用
- native 函数名本身不直观
- Java 层声明很简洁,但 so 层实现藏得深
第二步:别急着进算法,先看参数怎么过桥
JNI 场景里特别容易误判的一点是:你以为你看到的是“算法入口”,其实你看到的可能只是参数搬运层。
常见情况包括:
- jstring 转 char*
- jbyteArray 转 native buffer
- Java 对象字段被拆出来重新组装
- 上下文对象被缓存成全局引用
如果你不先搞清 Java 参数到了 native 层后变成什么,后面看算法很容易对不上号。
第三步:区分“桥”和“核心”
很多 native 方法其实只干两件事:
- 做参数转换
- 再调真正核心函数
这时候最关键的问题就不是“这个 JNI 方法看不看得懂”,而是:
- 它把参数交给谁了?
- 谁才是真正做计算/校验/加密的地方?
如果你只停在桥上,很容易把包装层误当核心层。
第四步:回到 Java 层确认调用场景
为什么同一个 native 函数,有时候看起来很复杂?因为它可能被多个场景共用。
所以你还得回头看 Java 层:
- 谁调用它
- 什么时候调用
- 传进来的参数有没有前置加工
- 调用后结果去哪了
一旦场景不清楚,你在 native 层看到的很多分支就会显得很乱。
特别高频的几个逆向目标
1. 找签名/加密逻辑
很多 Android 应用会把关键签名、摘要、加密逻辑下沉到 so 层。你要重点看:
- 参数拼接是否在 Java 做完
- 真正 hash / crypto 是在 native 还是 Java
- key/盐值是写在 so 里还是运行时传入
2. 找设备信息采集
有些字段表面在 Java 层拿,真正整理和混淆在 native 层做。别只停留在 Java API 名字上。
3. 找校验放行点
很多人会在 Java 层 patch 布尔值,但更稳的方法往往是先看 native 返回值和错误码生成位置。
最容易犯的错误
- 看到 native 声明就直接跳 so,不回头看 Java 调用场景
- 把 JNI 包装函数误认成算法核心
- 没有跟清 RegisterNatives,导致函数对错位
- 动态抓到了参数,却不知道它在 Java 侧原本是什么语义
我更推荐的排查顺序
- 先在 Java 层锁定可疑调用点
- 确认是静态注册还是动态注册
- 把 Java 参数和 native 参数一一对齐
- 找到桥下面真正的核心函数
- 最后再决定静态深挖还是动态 hook
这套顺序的核心不是复杂,而是稳:先把桥接关系理顺,再深挖实现细节。
给新手的一句最实在的话
如果你现在卡在一个 native 方法上,先别急着感叹“so 好难”。先问自己:
- 这个方法是怎么注册进去的?
- Java 参数到了 native 层后变成了什么?
- 我看到的是桥接层,还是核心计算层?
结尾
Android 的 JNI / Native Bridge 逆向,说到底不是“Java 不会 so、so 不会 Java”的问题,而是你要学会把两边连成一条完整链路。
一旦你把这条链路理清,很多本来很像迷宫的问题,都会落成几个特别具体的工程问题:
- 谁调它?
- 怎么绑上去?
- 参数怎么变?
- 真正干活的是谁?
到这个颗粒度,JNI 也就不再是玄学桥段,而是正常可拆的工作流。