百趣云 百趣云的博客

JNI 动态注册:通过 RegisterNatives 定位 native 函数地址

为什么静态符号找不到 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 里打开后:

  1. 找到 JNI_OnLoad,往里看 RegisterNatives 的调用
  2. 它的第三个参数就是方法表指针,双击跟过去
  3. 方法表在 .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 跳转,直达函数实现。

精髓:三个实战要点

  1. 时机问题:RegisterNatives 在 SO 加载时执行,Frida attach 晚了就错过。用 spawn 模式(frida -f)启动,或者 hook dlopen/android_dlopen_ext 等目标 SO 加载时再挂 hook。
  2. 函数表偏移 215 是 JNI 规范定死的(RegisterNatives 在 JNINativeInterface 里的索引),所有 Android 版本通用,放心硬编码。
  3. 多次注册:有的 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 把它识别为函数

总结

  1. 动态注册让导出符号消失,但 RegisterNatives 调用本身就是路标
  2. 静态:IDA 跟 JNI_OnLoad 里的方法表;动态:Frida hook 函数表第 215 项
  3. spawn 模式或 hook dlopen 解决时机问题
  4. 输出清单转 IDA 脚本,批量重命名效率翻倍

交流微信:run1255

By 百趣云 阅读量:2 On