逆向 JNI 函数找不到入口?动态注册定位技巧全解析

版权归作者所有,如有转发,请注明文章出处:cyrus-studio.github.io/blog/

前言

使用 IDA Pro 静态分析时,JNI 函数并没有出现在导出表中,根本找不到函数实现的位置。

这是因为很多 App 为了安全性和混淆目的,采用了 JNI 动态注册(RegisterNatives) 的方式,绕开了传统的静态绑定机制。

那么遇到这种情况我们该怎么办?

通过源码分析JNI 函数调用流程

ArtMethod 是 ART 虚拟机中用于表示 Java 方法的底层结构体,Java 中每个方法在运行时都会对应一个 ArtMethod 实例,用于管理其执行入口、访问标志、Dex 信息等元数据。

entry_point_from_jni_ 是 ArtMethod 结构体的一个字段,专门用于存储 Java 方法对应的 JNI 函数地址。

在编译 JNI 方法时,ArtJniCompileMethodInternal 中通过调用 EntryPointFromJniOffset 方法获取 entry_point_from_jni_ 字段的偏移,并生成一条调用该 jni 函数的机器指令插入到编译结果中。

scss 复制代码
// 9. Plant call to native code associated with method.

// 计算 ArtMethod 中 JNI 入口地址的字段偏移(即 ptr_sized_fields_.data_ 偏移)
// 根据目标指令集(如 arm64、x86)传入对应的指针大小(PointerSize::k64 或 k32)
MemberOffset jni_entrypoint_offset =
    ArtMethod::EntryPointFromJniOffset(InstructionSetPointerSize(instruction_set));

// 这部分代码生成了一条机器指令,用于调用 ArtMethod::entry_point_from_jni 所指向的 native 函数:
__ Call(main_jni_conv->MethodStackOffset(),  // 栈中 ArtMethod* 相对偏移
        jni_entrypoint_offset,              // ArtMethod 内部 entry_point_from_jni 字段的偏移
        mr_conv->InterproceduralScratchRegister());  // 用于保存临时地址的寄存器

cs.android.com/android/pla...

EntryPointFromJniOffset 内部调用 DataOffset 获取 PtrSizedFields 结构的 data_ 字段偏移

scss 复制代码
  static constexpr MemberOffset DataOffset(PointerSize pointer_size) {
    return MemberOffset(PtrSizedFieldsOffset(pointer_size) + OFFSETOF_MEMBER(
        PtrSizedFields, data_) / sizeof(void*) * static_cast<size_t>(pointer_size));
  }

  static constexpr MemberOffset EntryPointFromJniOffset(PointerSize pointer_size) {
    return DataOffset(pointer_size);
  }

cs.android.com/android/pla...

在 android 9 以后,entry_point_from_jni_ 实际上是 ArtMethod 中 ptr_sized_fields_.data_

arduino 复制代码
  // Must be the last fields in the method.
  struct PtrSizedFields {
    // Depending on the method type, the data is
    //   - native method: pointer to the JNI function registered to this method
    //                    or a function to resolve the JNI function,
    //   - conflict method: ImtConflictTable,
    //   - abstract/interface method: the single-implementation if any,
    //   - proxy method: the original interface method or constructor,
    //   - other methods: the profiling data.
    void* data_;

    // Method dispatch from quick compiled code invokes this pointer which may cause bridging into
    // the interpreter.
    void* entry_point_from_quick_compiled_code_;
  } ptr_sized_fields_;

当 Java 代码调用一个 native 方法时,ART 会通过 GetEntryPointFromJni 找到对应的 JNI 函数入口,然后切换到 C/C++ 层执行该方法。

cs.android.com/android/pla...

实际返回的是 ArtMethod 中 ptr_sized_fields_.data_ 字段的值

scss 复制代码
  void* GetEntryPointFromJni() {
    DCHECK(IsNative());
    return GetEntryPointFromJniPtrSize(kRuntimePointerSize);
  }

  ALWAYS_INLINE void* GetEntryPointFromJniPtrSize(PointerSize pointer_size) {
    return GetDataPtrSize(pointer_size);
  }
  
  // 返回 ArtMethod 中 data_ 字段的值,该字段在 native 方法中就是 JNI 函数指针
  ALWAYS_INLINE void* GetDataPtrSize(PointerSize pointer_size) {
    DCHECK(IsImagePointerSize(pointer_size));
    return GetNativePointer<void*>(DataOffset(pointer_size), pointer_size);
  }

cs.android.com/android/pla...

所以只要拿到 ArtMethod 和 EntryPointFromJniOffset 就可以得到 jni 方法的入口地址。

如何拿到 ArtMethod

我们可以通过 java 的反射技术,getDeclaredMethods 方法拿到类的所有 Method

Method 继承 Executable,Executable 中的 getArtMethod 方法可以拿到 ArtMethod 的地址

如何得到 EntryPointFromJniOffset

动态推导出 ArtMethod 结构中 entry_point_from_jni_ 字段的偏移量,用于后续从 ArtMethod* 中取出 native 函数地址。

  1. 选择一个已知的 Java native 方法作为"参考样本"。 该方法需要在系统中真实存在并且能找到其 native 地址。

  2. 调用 findNativeAddress 查找对应的 native 函数地址

  3. 获取类中所有方法并筛选 native 方法

  4. 获取 ArtMethod 指针并逐字节扫描字段

  5. 比较每个字段的值是否等于我们前面获取到的 native 函数地址,找到即返回偏移

代码实现如下:

javascript 复制代码
/**
 * 查找指定类和方法名对应的 native 函数地址(排除 CheckJNI 包装版本)
 *
 * @param {string} soName             - 包含目标 native 方法的 so 库名(如 "libandroid_runtime.so")
 * @param {string} javaClassName      - Java 类名(如 "android.os.Process")
 * @param {string} targetMethodName   - 方法名(如 "getUidForName")
 * @returns {NativePointer|null}      - 找到的 native 函数地址或 null
 */
function findNativeAddress(soName, javaClassName, targetMethodName) {
    // 获取指定 so 中的导出符号列表
    const exports = Module.enumerateExportsSync(soName);

    // 将 Java 类名转为 native C++ 符号格式(如 android_os_Process)
    const lowerClassName = javaClassName.replace(/\./g, "_");

    // 遍历导出符号,寻找符合条件的 native 方法
    for (let exp of exports) {
        if (
            exp.type === "function" &&
            exp.name.indexOf(lowerClassName) !== -1 &&         // 包含类名
            exp.name.indexOf(targetMethodName) !== -1 &&       // 包含方法名
            exp.name.indexOf("CheckJNI") === -1     // 排除 CheckJNI 包装版本
        ) {
            console.log(`[+] Found native method: ${exp.name} @ ${exp.address}`);
            return ptr(exp.address);  // 找到匹配的符号地址
        }
    }

    // 未找到匹配的符号
    return null;
}


/**
 * 找到 entry_point_from_jni_ 在 ArtMethod 结构体中的偏移量(根据 Android 版本不同可能会变化)
 *
 * @returns {number} 返回 entry_point_from_jni_ 的偏移量,若未找到返回 -1
 */
function entryPointFromJniOffset() {

    // 1. 选择一个已知的 Java native 方法作为"参考样本"。 该方法需要在系统中真实存在并且能找到其 native 地址。
    const soName = "libandroid_runtime.so";
    const className = "android.os.Process";
    const methodName = "getUidForName";

    // 2. 查找对应的 native 函数地址
    const native_addr = findNativeAddress(soName, className, methodName);
    if (native_addr === null) {
        console.log("[-] Native function not found.");
        return -1;
    }

    let clazz = Java.use(className).class;
    let methods = clazz.getDeclaredMethods();

    // 3. 获取类中所有方法并筛选 native 方法
    for (let i = 0; i < methods.length; i++) {

        // 获取方法签名
        let methodName = methods[i].toString();

        // 获取方法的修饰符,如 public、private、static、native 等
        let flags = methods[i].getModifiers();

        // 256 代表 native 修饰符
        if (flags & 256) {

            // 如果方法名中包含 methodName 说明找到了目标方法
            if (methodName.indexOf("getUidForName") != -1) {

                // 4. 获取 ArtMethod 指针并逐字节扫描字段
                let art_method = methods[i].getArtMethod();

                for (let j = 0; j < 30; j = j + 1) {

                    let jni_entrypoint_offset = Memory.readPointer(ptr(art_method + j));

                    // 比较每个字段的值是否等于我们前面获取到的 native 函数地址。
                    if (native_addr.equals(jni_entrypoint_offset)) {
                        // 找到即返回偏移
                        return j;
                    }
                }
            }
        }
    }

    // 未找到 JNI 方法对应的偏移量,返回 -1
    return -1;
}

打印指定类的所有 JNI 函数地址

得到当前系统中 entry_point_from_jni_ 的偏移量后,通过 ArtMethod 的内存地址 + entry_point_from_jni_ 的偏移量 就可以得到 JNI 函数地址。

遍历类中的 native 方法,打印 JNI 函数地址、所属模块信息,结构化输出。

javascript 复制代码
/**
 * 遍历类中的 native 方法,打印 JNI 函数地址、所属模块信息,结构化输出。
 *
 * @param {string} className - Java 类名(如 "android.os.Process")
 */
function getJniMethodAddr(className) {
    Java.perform(function () {
        const obj = Java.use(className);
        const clazz = obj.class;
        const jni_entrypoint_offset = entryPointFromJniOffset();

        console.log("========== [ JNI Method Info Dump ] ==========");
        console.log("[*] Target class: " + className);
        console.log("[*] entry_point_from_jni_ offset = " + jni_entrypoint_offset + " bytes\n");

        const methods = clazz.getDeclaredMethods();
        let count = 0;

        for (let i = 0; i < methods.length; i++) {
            const m = methods[i];
            const flags = m.getModifiers();
            const methodName = m.toString();

            // 256 表示 native 方法
            if ((flags & 256) !== 0) {
                count++;
                const art_method = m.getArtMethod();
                const native_addr = Memory.readPointer(ptr(art_method).add(jni_entrypoint_offset));

                const module = Process.getModuleByAddress(native_addr);
                const offset = module ? native_addr.sub(module.base) : ptr(0);

                // 结构化打印信息
                console.log("------------ [ #" + count + " Native Method ] ------------");
                console.log("Method Name     : " + methodName);
                console.log("ArtMethod Ptr   : " + ptr(art_method));
                console.log("Native Addr     : " + native_addr);
                if (module) {
                    console.log("Module Name     : " + module.name);
                    console.log("Module Offset   : 0x" + offset.toString(16));
                    console.log("Module Base     : " + module.base);
                    console.log("Module Size     : " + module.size + " bytes");
                    console.log("Module Path     : " + module.path);
                } else {
                    console.log("Module Info     : [ Not Found ]");
                }
                console.log("------------------------------------------------\n");
            }
        }

        if (count === 0) {
            console.log("[-] No native methods found in class: " + className);
        } else {
            console.log("[*] Total native methods found: " + count);
        }

        console.log("===============================================");
    });
}

在 python 中调用

通过 rpc 把 getJniMethodAddr 暴露给 Python 调用

ini 复制代码
// 暴露给 Python 调用(注意:exports中函数名需要全部小写,而且不能有下划线,不然会找不到方法)
rpc.exports.getjnimethodaddr = getJniMethodAddr

在 python 中加载 frida js 并调用 getJniMethodAddr 函数打印指定类的所有 native 方法地址

scss 复制代码
import frida


def read_frida_js_source(script):
    with open(script, "r", encoding='utf-8') as f:
        return f.read()


def on_message(message, data):
    print(f"消息: {message['type']}, 数据: {message['payload']}")


def main():
    class_name = "lte.NCall"
    # class_name = "com.cyrus.example.MainActivity"

    device = frida.get_device_manager().add_remote_device("127.0.0.1:1234")
    pid = device.get_frontmost_application().pid
    session: frida.core.Session = device.attach(pid)
    script = session.create_script(read_frida_js_source("jni_addr.js"))
    script.on('message', on_message)
    script.load()

    script.exports.getjnimethodaddr(class_name)

    # 退出
    session.detach()


if __name__ == "__main__":
    main()

关于 Frida 的详细使用可以参考这篇文章:一文搞懂如何使用 Frida Hook Android App

调用示例

比如,获取 lte.NCall 类中 native 方法地址

ini 复制代码
class_name = "lte.NCall"

首先打开 APP,再运行 python 脚本,输出如下

yaml 复制代码
[+] Found native method: _Z32android_os_Process_getUidForNameP7_JNIEnvP8_jobjectP8_jstring @ 0x7c65c0d648
========== [ JNI Method Info Dump ] ==========
[*] Target class: lte.NCall
[*] entry_point_from_jni_ offset = 24 bytes

------------ [ #1 Native Method ] ------------
Method Name     : public static native byte lte.NCall.IB(java.lang.Object[])
ArtMethod Ptr   : 0x9f63a590
Native Addr     : 0x7b6b454fa8
Module Name     : libGameVMP.so
Module Offset   : 0xdfa8
Module Base     : 0x7b6b447000
Module Size     : 462848 bytes
Module Path     : /data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/lib/arm64/libGameVMP.so
------------------------------------------------

------------ [ #2 Native Method ] ------------
Method Name     : public static native char lte.NCall.IC(java.lang.Object[])
ArtMethod Ptr   : 0x9f63a5b8
Native Addr     : 0x7b6b454fa8
Module Name     : libGameVMP.so
Module Offset   : 0xdfa8
Module Base     : 0x7b6b447000
Module Size     : 462848 bytes
Module Path     : /data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/lib/arm64/libGameVMP.so
------------------------------------------------

------------ [ #3 Native Method ] ------------
Method Name     : public static native double lte.NCall.ID(java.lang.Object[])
ArtMethod Ptr   : 0x9f63a5e0
Native Addr     : 0x7b6b455028
Module Name     : libGameVMP.so
Module Offset   : 0xe028
Module Base     : 0x7b6b447000
Module Size     : 462848 bytes
Module Path     : /data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/lib/arm64/libGameVMP.so
------------------------------------------------

------------ [ #4 Native Method ] ------------
Method Name     : public static native float lte.NCall.IF(java.lang.Object[])
ArtMethod Ptr   : 0x9f63a608
Native Addr     : 0x7b6b454fe8
Module Name     : libGameVMP.so
Module Offset   : 0xdfe8
Module Base     : 0x7b6b447000
Module Size     : 462848 bytes
Module Path     : /data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/lib/arm64/libGameVMP.so
------------------------------------------------

------------ [ #5 Native Method ] ------------
Method Name     : public static native int lte.NCall.II(java.lang.Object[])
ArtMethod Ptr   : 0x9f63a630
Native Addr     : 0x7b6b454fa8
Module Name     : libGameVMP.so
Module Offset   : 0xdfa8
Module Base     : 0x7b6b447000
Module Size     : 462848 bytes
Module Path     : /data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/lib/arm64/libGameVMP.so
------------------------------------------------

------------ [ #6 Native Method ] ------------
Method Name     : public static native long lte.NCall.IJ(java.lang.Object[])
ArtMethod Ptr   : 0x9f63a658
Native Addr     : 0x7b6b454fa8
Module Name     : libGameVMP.so
Module Offset   : 0xdfa8
Module Base     : 0x7b6b447000
Module Size     : 462848 bytes
Module Path     : /data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/lib/arm64/libGameVMP.so
------------------------------------------------

------------ [ #7 Native Method ] ------------
Method Name     : public static native java.lang.Object lte.NCall.IL(java.lang.Object[])
ArtMethod Ptr   : 0x9f63a680
Native Addr     : 0x7b6b454fa8
Module Name     : libGameVMP.so
Module Offset   : 0xdfa8
Module Base     : 0x7b6b447000
Module Size     : 462848 bytes
Module Path     : /data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/lib/arm64/libGameVMP.so
------------------------------------------------

------------ [ #8 Native Method ] ------------
Method Name     : public static native short lte.NCall.IS(java.lang.Object[])
ArtMethod Ptr   : 0x9f63a6a8
Native Addr     : 0x7b6b454fa8
Module Name     : libGameVMP.so
Module Offset   : 0xdfa8
Module Base     : 0x7b6b447000
Module Size     : 462848 bytes
Module Path     : /data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/lib/arm64/libGameVMP.so
------------------------------------------------

------------ [ #9 Native Method ] ------------
Method Name     : public static native void lte.NCall.IV(java.lang.Object[])
ArtMethod Ptr   : 0x9f63a6d0
Native Addr     : 0x7b6b454fa8
Module Name     : libGameVMP.so
Module Offset   : 0xdfa8
Module Base     : 0x7b6b447000
Module Size     : 462848 bytes
Module Path     : /data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/lib/arm64/libGameVMP.so
------------------------------------------------

------------ [ #10 Native Method ] ------------
Method Name     : public static native boolean lte.NCall.IZ(java.lang.Object[])
ArtMethod Ptr   : 0x9f63a6f8
Native Addr     : 0x7b6b454fa8
Module Name     : libGameVMP.so
Module Offset   : 0xdfa8
Module Base     : 0x7b6b447000
Module Size     : 462848 bytes
Module Path     : /data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/lib/arm64/libGameVMP.so
------------------------------------------------

------------ [ #11 Native Method ] ------------
Method Name     : public static native int lte.NCall.dI(int)
ArtMethod Ptr   : 0x9f63a720
Native Addr     : 0x7b6b45293c
Module Name     : libGameVMP.so
Module Offset   : 0xb93c
Module Base     : 0x7b6b447000
Module Size     : 462848 bytes
Module Path     : /data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/lib/arm64/libGameVMP.so
------------------------------------------------

------------ [ #12 Native Method ] ------------
Method Name     : public static native long lte.NCall.dL(long)
ArtMethod Ptr   : 0x9f63a748
Native Addr     : 0x7b6b452ad0
Module Name     : libGameVMP.so
Module Offset   : 0xbad0
Module Base     : 0x7b6b447000
Module Size     : 462848 bytes
Module Path     : /data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/lib/arm64/libGameVMP.so
------------------------------------------------

------------ [ #13 Native Method ] ------------
Method Name     : public static native java.lang.String lte.NCall.dS(java.lang.String)
ArtMethod Ptr   : 0x9f63a770
Native Addr     : 0x7b6b452aec
Module Name     : libGameVMP.so
Module Offset   : 0xbaec
Module Base     : 0x7b6b447000
Module Size     : 462848 bytes
Module Path     : /data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/lib/arm64/libGameVMP.so
------------------------------------------------

[*] Total native methods found: 13
===============================================

比如,native 方法 lte.NCall.IL 在 libGameVMP.so 中偏移量为 0xdfa8。

在 IDA Pro 中跳转到 0xdfa8 就是 native 方法的入口点。

相关推荐
传奇开心果编程41 分钟前
【Compose Multiplatform 跨端开发学与练】第9课 测试与调试
android·学习·macos·ios·kotlin·web·composer
传奇开心果编程1 小时前
【Compose Multiplatform 跨端开发学与练】第4课 导航与路由
android·windows·学习·ui·ios·kotlin·composer
传奇开心果编程1 小时前
【Compose Multiplatform 跨端开发学与练】第6课 状态管理与架构
android·学习·ui·ios·架构·kotlin·composer
事圆则缓1 小时前
Android AOSP 定制常见概念:源码目录、系统镜像与刷机流程
android
传奇开心果编程1 小时前
【Compose Multiplatform 跨端开发学与练】第2课 Compose 基础语法
android·windows·学习·ui·ios·kotlin·composer
supabc1232 小时前
Celium:连接 Windows、Mac、Linux 与 Android,让远程访问和设备管理更简单
android·linux·windows·macos·远程访问·网络管理·celium
Dovis(誓平步青云)2 小时前
导览音频切换太快,旧讲解不能覆盖新展品
android·前端·javascript·ecmascript·音视频·宠物
五彩小白19 小时前
自回归和上下文
android
事圆则缓21 小时前
Android 多渠道打包实战:Product Flavors、签名、AAB 与发包校验
android
树下困觉眯1 天前
View绘制流程学习--获取WindowManager的三种方式
android·系统架构·view design