Frida Native 层 Hook 技巧:JNI 函数调用、字符串解析、so 加载

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

RegisterNatives

RegisterNatives 是 JNI(Java Native Interface)的一部分,用于在 Java 类和本地 C/C++ 代码之间注册本地方法。其原型如下:

jint RegisterNatives(JNIEnv* env, jclass clazz, const JNINativeMethod* methods, jint method_count);

参数说明:

  • env:JNIEnv 指针。

  • clazz:Java 类的 jclass 句柄。

  • methods:指向 JNINativeMethod 结构体数组的指针。

  • method_count:要注册的方法数量。

其中,methods 结构体的定义如下:

typedef struct {
    const char* name;       // 方法名称(指向字符串)
    const char* signature;  // 方法签名(指向字符串)
    void* fnPtr;            // 方法的本地实现(指向本地函数)
} JNINativeMethod;

可以看到,每个 JNINativeMethod 结构体由 三个指针 组成:

  1. name(方法名指针)

  2. signature(方法签名指针)

  3. fnPtr(本地方法指针)

一般会有两个 RegisterNatives 函数,CheckJNI 版本只有在调试选项打开时才会调用,我们一般用 JNI 的那个就行。

[+] Found RegisterNatives symbol: _ZN3art12_GLOBAL__N_18CheckJNI15RegisterNativesEP7_JNIEnvP7_jclassPK15JNINativeMethodi at 0x780d7757a8
[+] Found RegisterNatives symbol: _ZN3art3JNI15RegisterNativesEP7_JNIEnvP7_jclassPK15JNINativeMethodi at 0x780d7eed10

通过 hook RegisterNatives 实现监控 app 中动态注册的 JNI 函数。代码如下:

RegisterNatives.js

// 查找 libart.so 中的 RegisterNatives 地址
function findRegisterNativesAddr() {
    let symbols = Module.enumerateSymbolsSync("libart.so");
    let addrRegisterNatives = null;

    // 遍历所有符号,查找非 CheckJNI 版本的 RegisterNatives
    for (let i = 0; i < symbols.length; i++) {
        let symbol = symbols[i];

        // 确认符合 RegisterNatives 标准的符号(非 CheckJNI 版本)
        if (symbol.name.indexOf("CheckJNI") < 0 &&
            symbol.name.indexOf("RegisterNatives") >= 0) {
            addrRegisterNatives = symbol.address;
            console.log("[+] Found RegisterNatives symbol: " + symbol.name + " at " + symbol.address);
            break;  // 找到第一个匹配的符号即可
        }
    }

    // 如果没有找到 RegisterNatives 地址,返回 null
    if (addrRegisterNatives === null) {
        console.log("[!] No non-CheckJNI RegisterNatives symbol found!");
    }
    return addrRegisterNatives;
}

// Hook RegisterNatives 函数,打印方法相关信息
function hookRegisterNatives(addrRegisterNatives) {
    if (addrRegisterNatives !== null) {
        // 使用 Interceptor 附加到 RegisterNatives 地址
        Interceptor.attach(addrRegisterNatives, {
            onEnter: function (args) {

                // 获取 java 类和方法列表
                let javaClass = args[1];
                let className = Java.vm.tryGetEnv().getClassName(javaClass); // 获取类名

                // 打印 RegisterNatives 的方法参数
                let methodsPtr = ptr(args[2]);

                let methodCount = args[3].toInt32();
                console.log("[RegisterNatives] method_count:", methodCount);

                // 遍历注册的每个 JNI 方法
                for (let i = 0; i < methodCount; i++) {
                    let methodPtr = methodsPtr.add(i * Process.pointerSize * 3); // 获取每个方法的指针

                    // 读取每个方法的名称、签名和函数指针
                    let namePtr = Memory.readPointer(methodPtr);
                    let sigPtr = Memory.readPointer(methodPtr.add(Process.pointerSize));
                    let fnPtr = Memory.readPointer(methodPtr.add(Process.pointerSize * 2));

                    let name = Memory.readCString(namePtr); // 方法名称
                    let sig = Memory.readCString(sigPtr
<think>我们被要求使用Frida Hook Java Native函数注册方法。在Android中,Java Native函数通过JNI(Java Native Interface)注册,有两种方式:静态注册和动态注册。动态注册是通过JNI_OnLoad函数中调用RegisterNatives函数来注册的。我们想要Hook的是动态注册的过程,特别是RegisterNatives函数。 根据引用[1]和[2],我们知道Frida可以Hook Native函数。我们需要找到RegisterNatives函数的地址,然后使用Interceptor.attach来附加钩子。 步骤: 1. 确定目标函数:RegisterNatives,它位于libart.so(或不同Android版本对应的lib)中。 2. 使用Frida的Module.getExportByName获取函数地址。 3. 使用Interceptor.attach附加钩子,在回调函数中打印或处理参数。 RegisterNatives函数原型: jint RegisterNatives(JNIEnv *env, jclass clazz, const JNINativeMethod *methods, jint nMethods); 其中,JNINativeMethod结构体包含: const char* name; const char* signature; void* fnPtr; 因此,在Hook时,我们可以读取第三个参数(methods数组)和第四个参数(nMethods)来获取注册的本地方法信息。 注意:由于不同Android版本中so库的名称和函数符号可能不同,我们需要根据实际情况调整。 下面是一个示例脚本,用于Hook RegisterNatives函数: ```javascript Java.perform(function() { // 获取RegisterNatives函数的地址 var registerNativesAddr = Module.getExportByName('libart.so', 'RegisterNatives'); // 如果找不到,尝试其他库名,例如不同Android版本可能为libart.so, libdvm.so等 if (!registerNativesAddr) { console.log("Failed to find RegisterNatives address."); return; } // 附加钩子 Interceptor.attach(registerNativesAddr, { onEnter: function(args) { // args[0]是JNIEnv* // args[1]是jclass // args[2]是JNINativeMethod*,即方法数组 // args[3]是jint,即方法数量 var methodsPtr = ptr(args[2]); var methodCount = args[3].toInt32(); console.log("RegisterNatives called with " + methodCount + " methods."); // 遍历方法数组 for (var i = 0; i < methodCount; i++) { // JNINativeMethod结构体的大小,在32位上是12字节(3个指针),64位上是24字节(3个指针,每个8字节) var method = methodsPtr.add(i * Process.pointerSize * 3); // 读取name(char*) var namePtr = method.readPointer(); var signaturePtr = method.add(Process.pointerSize).readPointer(); var fnPtr = method.add(Process.pointerSize * 2).readPointer(); var methodName = namePtr.readCString(); var signature = signaturePtr.readCString(); console.log("Method [" + i + "]:"); console.log(" name: " + methodName); console.log(" signature: " + signature); console.log(" function pointer: " + fnPtr); } } }); }); ``` 注意:上述脚本在32位和64位上的结构体大小不同,我们通过Process.pointerSize来适配。每个指针的大小为Process.pointerSize。 但是,需要注意的是,不同Android版本中JNINativeMethod结构体的布局可能一致(都是三个连续的指针),但为了兼容性,我们按照标准布局处理。 另外,由于RegisterNatives可能在JNI_OnLoad中被调用,因此我们需要在脚本加载时尽早Hook。 如果目标应用在加载时已经注册了Native方法,我们可能需要通过setTimeout或立即执行来尽早Hook,或者使用Frida的早期注入(如frida -f 包名 --no-pause)来在应用启动时注入。 此外,引用[4]中展示了读取字符串参数的方法,我们这里也使用了readCString来读取方法名和签名。 这个脚本将打印出每次调用RegisterNatives时注册的所有本地方法的信息,包括方法名、签名和函数指针。 但是,请注意,有些应用可能会在运行时动态注册,因此我们可能需要在整个生命周期中监控。 另外,我们也可以Hook JNI_OnLoad函数,因为动态注册通常发生在这里。 参考引用[1]中提到的“Hook JNI函数”,这个案例就是直接Hook RegisterNatives。 因此,我们可以使用上述脚本来实现需求。 相关问题: 1. 如何Hook其他JNI函数,比如FindClass或GetMethodID? 2. 如何通过Frida调用一个已经注册的Native函数? 3. 如何Hook一个已经注册的Native函数的具体实现(即替换函数指针)? 下面我们尝试回答这些问题: 问题1:Hook其他JNI函数 类似地,我们可以找到其他JNI函数的地址,然后使用Interceptor.attach。例如,Hook FindClass: ```javascript var findClassAddr = Module.getExportByName('libart.so', 'FindClass'); Interceptor.attach(findClassAddr, { onEnter: function(args) { var classNamePtr = args[1]; // 第一个参数是JNIEnv*,第二个是const char* var className = classNamePtr.readCString(); console.log("FindClass: " + className); } }); ``` 问题2:调用已经注册的Native函数 引用[2]中提到,我们可以使用NativeFunction来调用本地函数。假设我们有一个函数指针,我们可以这样: ```javascript // 假设我们有一个函数指针fnPtr,函数原型为返回int,参数为两个int var func = new NativeFunction(fnPtr, 'int', ['int', 'int']); var result = func(1, 2); console.log("Result: " + result); ``` 问题3:替换已经注册的Native函数的实现 在Hook RegisterNatives时,我们可以修改传入的函数指针,或者在获取到函数指针后,使用Interceptor.replace来替换该函数指针指向的代码。 但是,注意:在RegisterNatives时,我们无法修改传入的methods数组(因为它是只读的?),但我们可以修改结构体中的函数指针指向的内容?实际上,在onEnter时,args[2]指向的数组是可写的吗?这取决于调用者传入的是否是可写内存。通常,这个数组是调用者分配的,可能是栈上或堆上,但修改它可能不安全。 另一种方法是,在注册完成后,我们直接Hook每个具体的函数。例如,在打印出函数指针后,我们可以对每个函数指针进行Hook: ```javascript Interceptor.attach(fnPtr, { onEnter: function(args) { console.log("Native method called: " + methodName); } }); ``` 或者,我们可以替换函数: ```javascript var originalFunc = new NativeFunction(fnPtr, 'void', ['pointer', 'pointer']); // 假设返回void,参数两个指针(JNIEnv*, jobject) Interceptor.replace(fnPtr, new NativeCallback(function(env, obj) { console.log("Hooked native method"); return originalFunc(env, obj); // 调用原函数 }, 'void', ['pointer', 'pointer'])); ``` 但是,请注意,替换函数需要知道确切的函数签名(返回类型和参数类型)。因此,我们需要在Hook RegisterNatives时记录每个方法的签名,然后根据签名创建对应的NativeCallback。 由于签名解析比较复杂,这里不展开。 因此,我们回到最初的问题,已经通过示例脚本展示了如何Hook Java Native函数注册方法。 如果用户需要更详细的信息,可以进一步提供具体的Android版本或需求。</think>### Frida Hook Java Native函数注册方法 在Android开发中,Java Native函数通过JNI(Java Native Interface)动态注册时,会调用`RegisterNatives()`函数。使用Frida Hook函数可以捕获所有动态注册的Native方法信息,以下是完整实现方案: #### 1. Hook RegisterNatives函数 ```javascript Java.perform(function() { // 获取RegisterNatives函数地址 const registerNativesAddr = Module.getExportByName('libart.so', 'RegisterNatives'); Interceptor.attach(registerNativesAddr, { onEnter: function(args) { // args[0]: JNIEnv* // args[1]: jclass // args[2]: JNINativeMethod* (方法数组) // args[3]: jint (方法数量) const methodCount = args[3].toInt32(); const methodsPtr = ptr(args[2]); console.log(`[+] RegisterNatives called (${methodCount} methods)`); // 解析每个JNINativeMethod结构体 for (let i = 0; i < methodCount; i++) { const method = methodsPtr.add(i * Process.pointerSize * 3); // 读取方法名指针 const namePtr = method.readPointer(); // 读取签名指针 const sigPtr = method.add(Process.pointerSize).readPointer(); // 读取函数指针 const fnPtr = method.add(Process.pointerSize * 2).readPointer(); // 读取字符串内容 const methodName = namePtr.readCString(); const signature = sigPtr.readCString(); console.log(`├── Method ${i+1}:`); console.log(`│ ├── Name: ${methodName}`); console.log(`│ ├── Signature: ${signature}`); console.log(`│ └── Function Pointer: ${fnPtr}`); } } }); }); ``` #### 2. 关键技术解析 1. **JNINativeMethod结构体** ```c typedef struct { const char* name; // 方法名 const char* signature; // JNI签名 void* fnPtr; // 函数指针 } JNINativeMethod; ``` 内存布局:每个结构体占用`3 * pointerSize`字节(32位:12字节,64位:24字节) 2. **Hook原理** - 通过`libart.so`导出符号定位`RegisterNatives` - 解析第三个参数`JNINativeMethod*`指向的方法数组 - 遍历读取每个方法的名称、签名和函数指针 - 使用`readCString()`获取字符串内容[^2] 3. **多版本适配** ```javascript // 兼容不同Android版本的库名 const libNames = ['libart.so', 'libdvm.so']; let registerNativesAddr = null; libNames.forEach(lib => { if (!registerNativesAddr) { registerNativesAddr = Module.getExportByName(lib, 'RegisterNatives'); } }); ``` #### 3. 实战应用场景 1. **动态注册监控** ```javascript // 记录所有注册的Native方法 const nativeMethods = {}; onEnter: function(args) { // ...解析过程 nativeMethods[methodName] = { signature: signature, address: fnPtr, jclass: args[1] }; } ``` 2. **方法替换** ```javascript // 替换目标函数实现 Interceptor.replace(fnPtr, new NativeCallback( (env, clazz) => { console.log(`[!] ${methodName} called`); return 0; }, 'int', ['pointer', 'pointer'] )); ``` 3. **JNI函数调用追踪** ```javascript // Hook具体Native函数 Interceptor.attach(fnPtr, { onEnter: function(args) { console.log(`[→] Enter ${methodName}`); }, onLeave: function(retval) { console.log(`[←] Exit ${methodName}`); } }); ``` #### 4. 注意事项 1. **时序问题**:需在`JNI_OnLoad`执行前注入,建议使用`frida -f com.app --no-pause` 2. **字符串编码**:非ASCII方法名需使用`readUtf8String()`替代 3. **内存保护**:Hook前检查内存权限`Memory.protect(addr, size, 'rwx')` 4. **64位适配**:使用`Process.pointerSize`确定指针大小 > 通过Hook `RegisterNatives`,我们可以完整掌握JNI动态注册过程,为后续的Native分析和Hook奠定基础[^1][^2]。实际测试中建议结合`frida-trace`进行调用栈追踪: > `frida-trace -U com.app -i "RegisterNatives"`
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值