为什么静态符号找不到 native 方法
分析 SO 时经常遇到:Java 层声明了 public static native String getSign(...),但 IDA 的导出表里找不到 Java_com_xxx_getSign。原因是 JNI 动态注册——SO 在 JNI_OnLoad 里调用 RegisterNatives,把 Java 方法名和 native 函数地址运行时绑定,符号表里完全没有痕迹。
这是商业 App 的标配做法,目的就是增加静态分析难度。但绑定动作本身必须调用系统 API,这就是突破口。
RegisterNatives 的数据结构
动态注册的核心是一个 JNINativeMethod 结构体数组:
typedef struct {
const char* name; // Java 方法名,如 "getSign"
const char* signature; // JNI 签名,如 "(Ljava/lang/String;)Ljava/lang/String;"
void* fnPtr; // native 函数地址 ← 我们要的就是它
} JNINativeMethod;
// SO 里的典型代码
static JNINativeMethod methods[] = {
{"getSign", "(Ljava/lang/String;)Ljava/lang/String;", (void*)native_getSign},
{"check", "(I)Z", (void*)native_check},
};
JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env;
(*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6);
jclass cls = (*env)->FindClass(env, "com/xxx/SignUtils");
(*env)->RegisterNatives(env, cls, methods, 2); // 绑定发生在这里
}静态分析法:IDA 里找方法表
JNI_OnLoad 是导出函数(动态注册也必须导出它),在 IDA 里打开后:
- 找到
JNI_OnLoad,往里看RegisterNatives的调用 - 它的第三个参数就是方法表指针,双击跟过去
- 方法表在
.data或.rodata段,每项 12 字节(32 位)/24 字节(64 位):名字指针、签名指针、函数指针
64 位 SO 里 IDA 经常不能把方法表识别成结构体,手动按 24 字节一组解析:前 8 字节是名字字符串指针,中间 8 字节签名指针,最后 8 字节就是 native 函数地址。按 X 查函数指针的引用,直接跳到实现。
动态分析法:Frida hook RegisterNatives(推荐)
静态找表在方法表被加密/混淆时会失效,动态 hook 一劳永逸:
function hookRegisterNatives() {
// RegisterNatives 是 JNIEnv 函数表的成员,先拿函数表
Java.perform(function() {
var env = Java.vm.getEnv();
var registerNatives = env.handle.readPointer()
.add(215 * Process.pointerSize) // RegisterNatives 在函数表的第215项
.readPointer();
Interceptor.attach(registerNatives, {
onEnter: function(args) {
var className = args[1]; // jclass
var methods = args[2]; // JNINativeMethod*
var count = args[3].toInt32();
var env2 = Java.vm.getEnv();
var clsName = env2.getClassName ? '' : '';
for (var i = 0; i < count; i++) {
var item = methods.add(i * Process.pointerSize * 3);
var name = item.readPointer().readCString();
var sig = item.add(Process.pointerSize).readPointer().readCString();
var fnPtr = item.add(Process.pointerSize * 2).readPointer();
var module = Process.findModuleByAddress(fnPtr);
console.log('[RegisterNatives] ' + name + ' ' + sig +
' -> ' + fnPtr + ' (' + module.name + ' + ' +
fnPtr.sub(module.base) + ')');
}
}
});
});
}输出直接给出:Java 方法名、JNI 签名、native 函数地址、所在 SO 及偏移——拿着偏移去 IDA 里 G 跳转,直达函数实现。
精髓:三个实战要点
- 时机问题:RegisterNatives 在 SO 加载时执行,Frida attach 晚了就错过。用 spawn 模式(
frida -f)启动,或者 hookdlopen/android_dlopen_ext等目标 SO 加载时再挂 hook。 - 函数表偏移 215 是 JNI 规范定死的(RegisterNatives 在 JNINativeInterface 里的索引),所有 Android 版本通用,放心硬编码。
- 多次注册:有的 SO 分批注册(核心方法一批、普通方法一批),hook 要全程挂着,别拿到第一批就撤。
进阶:批量转 IDA 脚本
方法多的时候,把 Frida 输出的"函数名+偏移"清单转成 IDA Python 脚本批量重命名:
# ida_rename.py
import idc, idaapi
methods = [
(0x12340, "native_getSign"),
(0x1a2b0, "native_check"),
]
for off, name in methods:
ea = idaapi.get_imagebase() + off
idc.set_name(ea, name, idc.SN_CHECK)
idc.add_func(ea) # 确保 IDA 把它识别为函数总结
- 动态注册让导出符号消失,但 RegisterNatives 调用本身就是路标
- 静态:IDA 跟 JNI_OnLoad 里的方法表;动态:Frida hook 函数表第 215 项
- spawn 模式或 hook dlopen 解决时机问题
- 输出清单转 IDA 脚本,批量重命名效率翻倍
交流微信:run1255