news 2026/7/27 2:44:17

Frida Hook JNI动态注册函数:Android Native层逆向分析实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Frida Hook JNI动态注册函数:Android Native层逆向分析实战

1. 项目概述:为什么需要Hook JNI动态注册函数?

在Android逆向与安全分析的日常工作中,我们经常会遇到一个棘手的问题:应用的核心逻辑被编译进了原生(Native)层,也就是那些用C/C++写的.so库文件。这些库通过JNI(Java Native Interface)与Java层交互。其中,JNI函数的注册方式有两种——静态注册和动态注册。静态注册的函数名遵循特定的命名规则(如Java_com_example_MyClass_myMethod),相对容易被定位。而动态注册,则是开发者在Native代码中,通过一个JNINativeMethod结构体数组,手动将Java方法与C函数指针进行绑定。这种方式隐蔽性极强,函数名在编译后可能被混淆或完全无关,传统的基于字符串搜索的方法基本失效。

这就引出了我们今天的实战主题:如何用Frida的Hook技术,精准监控Android应用中的JNI动态注册函数。这不仅仅是逆向分析中的一个“高级技巧”,更是深入理解应用底层行为、分析加密算法、追踪敏感数据流(如密钥生成、网络请求签名)的必经之路。想象一下,一个金融类App的登录密码加密函数,或者一个游戏的核心校验逻辑,很可能就藏在这些动态注册的JNI函数里。如果你无法定位和监控它们,整个分析工作就可能止步于Java层,触及不到真正的核心。

我遇到过不少案例,应用在Java层只是做简单的参数组装,真正的加密、签名、协议封包全部在动态注册的Native函数里完成。不搞定它们,你连数据包都构造不出来。因此,掌握这套方法,相当于拿到了一把打开Native层黑盒的钥匙。接下来,我将从一个实战者的角度,带你一步步拆解原理,并附上我打磨过无数次的完整脚本,让你能直接上手,复现整个监控过程。

2. 核心原理与前置知识拆解

2.1 JNI动态注册机制深度剖析

要Hook,必须先理解它是怎么“长”出来的。动态注册的核心发生在JNI_OnLoad函数或某个初始化函数中。我们来看一个典型的代码片段:

// 假设这是Native层实现的函数 jstring native_hello(JNIEnv* env, jobject thiz) { return (*env)->NewStringUTF(env, "Hello from Dynamic JNI!"); } // 定义方法映射表 static JNINativeMethod gMethods[] = { {"helloFromJNI", "()Ljava/lang/String;", (void*)native_hello} }; // 在JNI_OnLoad中注册 jint JNI_OnLoad(JavaVM* vm, void* reserved) { JNIEnv* env = NULL; if ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) { return JNI_ERR; } jclass clazz = (*env)->FindClass(env, "com/example/myapp/NativeHelper"); (*env)->RegisterNatives(env, clazz, gMethods, 1); // 关键注册调用 return JNI_VERSION_1_6; }

这里的关键是RegisterNatives这个JNI函数。它接受一个Java类引用、一个JNINativeMethod结构体数组及其长度作为参数。JNINativeMethod结构体包含三个字段:

  1. name: Java方法名(如"helloFromJNI")。
  2. signature: JNI方法签名,描述参数和返回值类型(如"()Ljava/lang/String;")。
  3. fnPtr: 指向实际Native函数实现的指针(如(void*)native_hello)。

我们的Hook目标,就是这个fnPtr指向的函数地址。但问题在于,这个地址是在运行时,由RegisterNatives调用时才确定并绑定到Java方法上的。我们无法像Hook Java方法那样,直接通过类名和方法名去拦截。

2.2 Frida的介入策略:从源头拦截

既然动态注册的过程发生在Native层,我们的思路就很明确了:RegisterNatives函数被调用时进行拦截,从而“捕获”到那个关键的JNINativeMethod数组,进而拿到每个Native函数的指针地址。

Frida提供了强大的Interceptor.attach功能,允许我们附加到任意Native函数地址上。因此,整个方案的核心步骤就清晰了:

  1. 定位RegisterNatives函数:它在libart.so(Android运行时库)或目标应用自身的Native库中导出。
  2. HookRegisterNatives函数:当它被调用时,我们就能获取到其参数,特别是那个包含函数指针的JNINativeMethod数组。
  3. 解析并二次Hook:从数组中提取出每个fnPtr,然后使用Frida对这些具体的Native函数实现进行Hook。

注意:不同Android版本、不同厂商ROM的libart.so中,RegisterNatives的函数签名(参数顺序、类型)可能略有差异。我们的脚本需要具备一定的兼容性,不能写死偏移量或假设参数顺序。

2.3 工具与环境准备

工欲善其事,必先利其器。开始实战前,请确保你的环境已经就绪:

  1. 一部已Root的Android设备或模拟器:这是使用Frida进行深入Native Hook的前提。推荐使用官方Android Studio自带的x86_64镜像模拟器,兼容性好,调试方便。
  2. Frida环境
    • PC端:通过pip安装pip install frida-tools
    • 设备端:根据设备CPU架构(arm,arm64,x86_64)下载对应的frida-server,推送到设备并以后台进程运行。这是Frida的核心服务。
  3. 目标应用:选择一个你拥有测试权限的应用。对于学习,可以自己写一个包含动态注册JNI函数的Demo App。这样你能完全掌控内部逻辑,方便验证Hook是否成功。
  4. 代码编辑器:任意你喜欢的即可,用于编写和修改Frida JavaScript脚本。

3. 实战:分步构建监控脚本

理论讲完,我们进入最核心的实操环节。我将把完整的脚本拆解开,逐一讲解每个部分的意图和关键代码。

3.1 第一步:定位并Hook RegisterNatives

首先,我们需要找到RegisterNatives函数。在Android系统中,它通常由libart.so导出。我们可以使用Frida的Module.findExportByName来获取其地址。

// 1. 定义我们需要的关键函数指针和结构体(根据JNI头文件) const RegisterNativesAddr = Module.findExportByName('libart.so', '_ZN3art3JNI15RegisterNativesEP7_JNIEnvP7_jclassPK15JNINativeMethodi'); if (RegisterNativesAddr) { console.log(`[+] Found RegisterNatives at ${RegisterNativesAddr}`); } else { // 某些版本或ROM可能在别处,尝试其他常见名称或模块 console.log(`[-] Failed to find RegisterNatives in libart.so, trying alternative...`); // 可以尝试遍历所有模块查找 Process.enumerateModules().forEach(m => { let addr = m.findExportByName('RegisterNatives'); if (addr) { console.log(`[+] Found in ${m.name} at ${addr}`); RegisterNativesAddr = addr; } }); }

找到地址后,我们使用Interceptor.attach进行挂钩。这里最大的难点是参数解析。RegisterNatives的原型是:jint RegisterNatives(JNIEnv* env, jclass clazz, const JNINativeMethod* methods, jint nMethods)

我们需要读取第三个参数methods,它是一个指向JNINativeMethod数组的指针。

Interceptor.attach(RegisterNativesAddr, { onEnter: function(args) { // args[0] 是 JNIEnv* // args[1] 是 jclass clazz this.clazz = args[1]; // args[2] 是 const JNINativeMethod* methods this.methodsPtr = args[2]; // args[3] 是 jint nMethods this.nMethods = args[3].toInt32(); console.log(`\n[RegisterNatives Called]`); console.log(` Class: ${this.clazz}`); console.log(` Methods Ptr: ${this.methodsPtr}`); console.log(` Number of Methods: ${this.nMethods}`); // 临时保存,用于onLeave时或直接在此处解析 this._nativeMethods = []; }, onLeave: function(retval) { // 注册完成后,我们可以解析方法数组 // 注意:onEnter时数组内容可能还未完全准备好,有时在onLeave解析更稳妥 if (this.methodsPtr && this.nMethods > 0) { parseJNINativeMethods(this.methodsPtr, this.nMethods, this.clazz); } } });

3.2 第二步:解析JNINativeMethod结构体

这是脚本中最精细的部分。我们需要在内存中正确地“爬取”这个结构体数组。JNINativeMethod在内存中的布局通常是三个连续指针(在32位下是3个4字节,64位下是3个8字节),分别对应name,signature,fnPtr

function parseJNINativeMethods(methodsPtr, nMethods, clazz) { const ptrSize = Process.pointerSize; // 获取当前进程指针大小,兼容32/64位 const methodSize = ptrSize * 3; // 每个JNINativeMethod结构体的大小 for (let i = 0; i < nMethods; i++) { let methodEntry = methodsPtr.add(i * methodSize); // 读取三个指针 let namePtr = methodEntry.readPointer(); let sigPtr = methodEntry.add(ptrSize).readPointer(); let fnPtr = methodEntry.add(ptrSize * 2).readPointer(); // 将指针转换为字符串(C字符串) let name = namePtr.readCString(); let signature = sigPtr.readCString(); console.log(` [${i}] ${name} ${signature} --> NativePtr: ${fnPtr}`); // 记录到全局对象,以便后续Hook let methodInfo = { javaName: name, javaSig: signature, nativePtr: fnPtr, owningClass: clazz }; gHookedMethods.push(methodInfo); // 立即对这个Native函数进行Hook! hookNativeFunction(methodInfo); } }

实操心得readCString()可能会因为指针无效而抛出异常。在实际对抗中,应用可能会故意传入假指针或已释放的内存来干扰分析。因此,在生产脚本中,这部分代码需要放在try-catch块中,并增加对指针有效性的基础判断(例如,检查是否在可读的内存页内)。

3.3 第三步:对捕获的Native函数实现进行Hook

现在,我们拿到了梦寐以求的Native函数指针fnPtr。接下来就可以像Hook普通Native函数一样Hook它了。但是,我们不知道这个函数的参数个数和类型(签名只对Java层有意义)。这里有两种策略:

  1. 通用参数打印:适用于快速监控函数是否被调用、获取大致参数值。我们可以读取前几个参数(通常是JNIEnv*jclass/jobject)以及可能的后续参数。
function hookNativeFunction(methodInfo) { let address = methodInfo.nativePtr; console.log(`[*] Attempting to hook native function at ${address} for ${methodInfo.javaName}`); try { Interceptor.attach(address, { onEnter: function(args) { console.log(`\n=== [Native Call: ${methodInfo.javaName}] ===`); // args[0] 通常是 JNIEnv* // args[1] 通常是 jclass (静态方法) 或 jobject (实例方法) console.log(` JNIEnv*: ${args[0]}`); console.log(` this/class: ${args[1]}`); // 尝试打印可能的参数(从第三个开始,具体取决于函数签名) // 这是一个示例,实际参数解析极其复杂,需要根据签名来 for (let i = 2; i < 6; i++) { // 假设最多看前4个可能参数 if (i < args.length) { // 防止越界 let val = args[i]; // 简单判断:如果是指针,可能是字符串或对象引用 if (val.isNull()) { console.log(` arg${i}: NULL`); } else { // 尝试当作C字符串读取,如果不是会抛异常,我们捕获即可 try { let str = val.readCString(); console.log(` arg${i}: "${str}" (string)`); } catch (e) { // 不是字符串,直接打印指针值 console.log(` arg${i}: ${val} (pointer)`); // 还可以尝试读取为整数等,这里省略 } } } } // 记录调用栈,对于分析调用链非常有用 // console.log(Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join('\n')); }, onLeave: function(retval) { // 尝试打印返回值 if (!retval.isNull()) { // 根据方法签名猜测返回值类型,这里简化处理 if (methodInfo.javaSig.includes('String')) { try { // 对于jstring,需要通过JNIEnv函数转换,这里直接打印指针 console.log(` retval (jstring): ${retval}`); } catch(e) {} } else if (methodInfo.javaSig.includes('I') || methodInfo.javaSig.includes('J')) { console.log(` retval (int/long): ${retval.toInt32()}`); } else { console.log(` retval: ${retval}`); } } else { console.log(` retval: NULL`); } console.log(`=== [End: ${methodInfo.javaName}] ===\n`); } }); console.log(`[+] Successfully hooked ${methodInfo.javaName}`); } catch (e) { console.log(`[-] Failed to hook ${methodInfo.javaName}: ${e}`); } }
  1. 基于签名的精确参数解析:这是终极方案,但实现极其复杂。你需要完整解析JNI类型签名(如"(ILjava/lang/String;[B)J"),然后在onEnter中根据签名,使用Frida的Memory.readByteArrayMemory.readInt等API,结合JNIEnv的函数(如GetStringUTFChars)来正确读取参数值。这通常需要为每种JNI类型编写专门的解析函数,工作量巨大,一般只在针对特定关键函数进行深度分析时使用。

3.4 第四步:脚本整合与优化

将上述所有步骤整合,并增加一些健壮性和实用性功能。

// frida_hook_jni_dynamic.js // 全局存储已Hook的方法信息 var gHookedMethods = []; // 主逻辑 function hookDynamicJNIRegistration() { console.log("[*] Starting JNI Dynamic Registration Hook..."); let targetModules = ['libart.so', 'libandroid_runtime.so']; let registerNativesAddr = null; // 1. 寻找RegisterNatives for (let modName of targetModules) { let addr = Module.findExportByName(modName, 'RegisterNatives'); if (addr) { registerNativesAddr = addr; console.log(`[+] Found RegisterNatives in ${modName} at ${addr}`); break; } } if (!registerNativesAddr) { console.log('[-] Could not find RegisterNatives. Trying broader search...'); // 更暴力的搜索方式,遍历所有模块 Process.enumerateModules().forEach(m => { let exports = m.enumerateExports(); for (let exp of exports) { if (exp.name.includes('RegisterNatives')) { // 模糊匹配 registerNativesAddr = exp.address; console.log(`[+] Found via search: ${exp.name} in ${m.name} at ${exp.address}`); return false; // 跳出forEach循环 } } }); } if (!registerNativesAddr) { console.log('[-] Fatal: RegisterNatives not found. Exiting.'); return; } // 2. Hook RegisterNatives Interceptor.attach(registerNativesAddr, { onEnter: function(args) { this.methodsPtr = args[2]; this.nMethods = args[3].toInt32(); this.clazz = args[1]; // 可以在这里记录,但解析放在onLeave更安全 }, onLeave: function(retval) { if (this.methodsPtr && this.nMethods > 0) { console.log(`\n[+] Intercepted RegisterNatives for class ${this.clazz}, registering ${this.nMethods} method(s).`); parseAndHookMethods(this.methodsPtr, this.nMethods, this.clazz); } } }); console.log('[+] RegisterNatives hook installed. Waiting for calls...\n'); } // 解析并Hook方法的函数 function parseAndHookMethods(methodsPtr, nMethods, clazz) { // ... 同上一节的 parseJNINativeMethods 函数 ... } // 对单个Native函数进行Hook的函数 function hookNativeFunction(methodInfo) { // ... 同上一节的 hookNativeFunction 函数 ... // 可以增加一个过滤,只Hook我们感兴趣的函数名 let targetNames = ['encrypt', 'decrypt', 'sign', 'check', 'init']; // 示例关键词 for (let kw of targetNames) { if (methodInfo.javaName.toLowerCase().includes(kw)) { console.log(`[!] Key function "${methodInfo.javaName}" hooked.`); // 这里可以触发更详细的Hook逻辑 break; } } } // 延迟执行,确保目标库已加载 setTimeout(hookDynamicJNIRegistration, 1000); // 导出一些实用函数,方便在REPL中调用 rpc.exports = { listHookedMethods: function() { return gHookedMethods.map(m => `${m.javaName} @ ${m.nativePtr}`); }, // 可以添加手动Hook指定地址的函数 };

4. 运行脚本与结果分析

将上述脚本保存为hook_jni.js。在确保frida-server已在设备上运行后,在电脑终端执行:

frida -U -f com.example.targetapp -l hook_jni.js --no-pause
  • -U: 连接到USB设备。
  • -f com.example.targetapp: 启动目标应用。
  • -l hook_jni.js: 加载我们的脚本。
  • --no-pause: 立即启动主线程。

如果应用已经运行,你可以使用其进程名或PID来附加:

frida -U com.example.targetapp -l hook_jni.js

当应用启动并执行到JNI_OnLoad或任何调用RegisterNatives的地方时,你的控制台就会输出捕获到的信息。

示例输出可能如下:

[*] Starting JNI Dynamic Registration Hook... [+] Found RegisterNatives in libart.so at 0x7a12c3d4a0 [+] RegisterNatives hook installed. Waiting for calls... [+] Intercepted RegisterNatives for class 0xdf2a, registering 3 method(s). [0] nativeEncrypt (Ljava/lang/String;)[B --> NativePtr: 0x7a8f1b2c [1] nativeGetKey ()Ljava/lang/String; --> NativePtr: 0x7a8f1b8 [2] nativeVerify (I[B)Z --> NativePtr: 0x7a8f1c04 [*] Attempting to hook native function at 0x7a8f1b2c for nativeEncrypt [+] Successfully hooked nativeEncrypt [*] Attempting to hook native function at 0x7a8f1b8 for nativeGetKey [+] Successfully hooked nativeGetKey [*] Attempting to hook native function at 0x7a8f1c04 for nativeVerify [+] Successfully hooked nativeVerify === [Native Call: nativeEncrypt] === JNIEnv*: 0x7a8e4000 this/class: 0xdf2a arg2: "HelloWorld" (string) arg3: 0x16 (pointer) === [End: nativeEncrypt] === retval: 0x7a8f4a00 (pointer) // 这是一个jbyteArray的指针

从输出中,你可以清晰地看到:

  1. 动态注册发生时,捕获了3个方法。
  2. 成功Hook了这三个方法对应的Native函数地址。
  3. 当Java层调用nativeEncrypt("HelloWorld")时,我们的Hook被触发,打印出了参数。

5. 高级技巧与疑难问题排查

5.1 对抗反调试与Frida检测

在实际分析中,尤其是安全要求较高的应用,可能会检测Frida或反调试。我们的Hook行为本身也可能触发这些机制。

  • Frida检测:应用可能通过检查进程内存中是否存在frida-agent字符串、特定端口(如27042)是否被监听、或/proc/self/maps中是否存在frida相关库来检测。
    • 应对:使用Frida的frida-compile将脚本编译成二进制,或使用frida-gumMemory.protectAPI修改相关特征字符串。也可以尝试使用frida--debug模式配合spawn方式启动,有时能绕过简单检测。
  • 反调试:Native层可能使用ptrace、检查TracerPid、或利用定时器检查执行时间差等方式进行反调试。
    • 应对:Hook这些反调试函数(如ptrace,fork,syscall)并修改其返回值。这需要更深入的Native逆向知识。

5.2 处理多线程与并发注册

应用可能在多个线程中并发调用RegisterNatives。我们的脚本需要保证gHookedMethods数组的线程安全(虽然Frida JavaScript运行在主线程,但回调可能来自不同线程)。简单的做法是在修改全局数组时,使用锁或原子操作,但在JS中较难实现。一个更实用的方法是允许重复Hook,并在hookNativeFunction函数开始时检查该地址是否已被Hook过。

var gHookedAddresses = {}; // 使用对象作为简单Set function hookNativeFunction(methodInfo) { if (gHookedAddresses[methodInfo.nativePtr]) { return; // 已Hook,跳过 } gHookedAddresses[methodInfo.nativePtr] = true; // ... 原有的Hook逻辑 ... }

5.3 参数与返回值的深度解析难题

如前所述,通用地解析所有JNI函数参数几乎是不可能的。对于关键函数,你需要进行手动逆向分析。

  1. 使用IDA Pro/Ghidra反编译目标so库,找到fnPtr对应的函数,分析其参数和返回值类型。
  2. 编写针对性的Hook脚本。例如,如果你知道某个函数接收一个jstring和一个jint,你可以这样精确读取:
    onEnter: function(args) { let jniEnv = args[0]; let jstringArg = args[2]; // 假设是第三个参数 // 调用JNIEnv函数转换jstring到C字符串 let getStringUTFChars = new NativeFunction(Module.findExportByName('libart.so', '_ZN3art3JNI12GetStringUTFCharsEP7_JNIEnvP8_jstringPh'), 'pointer', ['pointer', 'pointer', 'pointer']); let cStrPtr = getStringUTFChars(jniEnv, jstringArg, NULL); let inputStr = cStrPtr.readCString(); console.log(`Input String: ${inputStr}`); // 记得后续要调用ReleaseStringUTFChars,这里省略 }
    这需要对JNI API非常熟悉,并且小心处理内存管理。

5.4 脚本性能优化

如果注册的函数非常多(几十上百个),全部Hook可能会对应用性能产生明显影响,甚至导致崩溃。

  • 选择性Hook:在parseAndHookMethods函数中,根据方法名(javaName)或签名(javaSig)进行过滤,只Hook你关心的函数(如包含crypt,sign,key,token等关键词的)。
  • 精简日志:在onEnter/onLeave中减少console.log的输出,尤其是在高频调用的函数上。可以将日志写入文件,或仅在某些条件触发时输出。

6. 完整脚本与使用指南

以下是我在实际工作中使用的增强版脚本框架,它包含了基本的健壮性处理和过滤功能。你可以以此为起点,根据你的具体目标进行修改。

// frida_hook_jni_dynamic_enhanced.js var gHookedMethods = []; var gHookedAddressMap = {}; function main() { console.log("[*] ===== JNI Dynamic Registration Hooking Script v1.2 ====="); let resolvedAddr = resolveRegisterNatives(); if (!resolvedAddr) { console.log("[-] Critical: Failed to resolve RegisterNatives. Exiting."); return; } installHook(resolvedAddr); console.log("[+] Hook installed successfully. Monitoring for JNI registrations...\n"); } function resolveRegisterNatives() { let commonPaths = [ 'libart.so', 'libandroid_runtime.so', 'libnativehelper.so' ]; for (let lib of commonPaths) { let addr = Module.findExportByName(lib, 'RegisterNatives'); if (addr) { console.log(`[+] Resolved RegisterNatives in ${lib} @ ${addr}`); return addr; } } console.log("[-] Not found in common libs, performing broad search..."); // 遍历模块搜索(略,见前文) return null; } function installHook(registerNativesAddr) { Interceptor.attach(registerNativesAddr, { onEnter: function(args) { this.registrationArgs = { clazz: args[1], methodsPtr: args[2], nMethods: args[3].toInt32() }; }, onLeave: function(retval) { let ra = this.registrationArgs; if (ra.methodsPtr && ra.nMethods > 0) { console.log(`\n[>] RegisterNatives Invoked: Class=${ra.clazz}, Count=${ra.nMethods}`); processMethodsArray(ra.methodsPtr, ra.nMethods, ra.clazz); } } }); } function processMethodsArray(ptr, count, clazz) { const PTR_SIZE = Process.pointerSize; const STRUCT_SIZE = PTR_SIZE * 3; for (let i = 0; i < count; i++) { let entry = ptr.add(i * STRUCT_SIZE); try { let namePtr = entry.readPointer(); let sigPtr = entry.add(PTR_SIZE).readPointer(); let fnPtr = entry.add(PTR_SIZE * 2).readPointer(); if (namePtr.isNull() || sigPtr.isNull() || fnPtr.isNull()) { console.log(` [${i}] <Invalid Entry>`); continue; } let name = namePtr.readCString() || '<unnamed>'; let sig = sigPtr.readCString() || '<no sig>'; let methodInfo = { name: name, signature: sig, nativePtr: fnPtr, classPtr: clazz, id: `${name}@${fnPtr}` }; // 过滤:只Hook感兴趣的函数 if (shouldHookMethod(methodInfo)) { console.log(` [${i}] HOOKING: ${name} ${sig} -> ${fnPtr}`); safeAttachHook(methodInfo); } else { console.log(` [${i}] Skipped: ${name} ${sig}`); } } catch (e) { console.log(` [${i}] Error parsing entry: ${e}`); } } } function shouldHookMethod(info) { // 自定义过滤逻辑 let keywords = ['encrypt', 'decrypt', 'sign', 'verify', 'key', 'secret', 'token', 'auth', 'init', 'check']; let lowerName = info.name.toLowerCase(); for (let kw of keywords) { if (lowerName.includes(kw)) { return true; } } // 或者根据签名过滤,例如只Hook返回String或byte[]的函数 // if (info.signature.includes('String') || info.signature.includes('[')) { // return true; // } return false; // 默认全部Hook,生产环境建议设为false并配置白名单 } function safeAttachHook(info) { if (gHookedAddressMap[info.nativePtr]) { return; // 避免重复Hook } try { Interceptor.attach(info.nativePtr, { onEnter: function(args) { console.log(`\n[Call] ${info.name}`); // 基础参数日志 // 可以在这里添加更精细的参数解析 logBasicArgs(args, info.signature); }, onLeave: function(retval) { // 基础返回值日志 logBasicRetval(retval, info.signature); console.log(`[End] ${info.name}\n`); } }); gHookedAddressMap[info.nativePtr] = true; gHookedMethods.push(info); } catch (e) { console.log(`[!] Failed to hook ${info.name}: ${e}`); } } function logBasicArgs(args, signature) { // 简化版:只打印前几个参数的指针值 for (let i = 0; i < Math.min(args.length, 4); i++) { console.log(` arg[${i}]: ${args[i]}`); } } function logBasicRetval(retval, signature) { if (!retval.isNull()) { console.log(` retval: ${retval}`); } else { console.log(` retval: null`); } } // 延迟启动,确保目标库加载 setTimeout(main, 800); // RPC接口,方便交互 rpc.exports = { get_hooked_list: () => gHookedMethods.map(m => m.id), get_method_info: (ptr) => gHookedMethods.find(m => m.nativePtr.equals(ptr)) };

使用指南:

  1. 将脚本保存为jni_hook.js
  2. 启动目标应用:frida -U -f com.target.app -l jni_hook.js --no-pause
  3. 观察控制台输出,动态注册发生时,符合过滤条件的方法会被自动Hook并打印调用信息。
  4. 你可以在Frida的REPL中调用rpc.exports.get_hooked_list()来查看已Hook的函数列表。

这个脚本提供了一个坚实的起点。真正的战场在于,如何根据具体的应用,调整过滤策略、深化参数解析、并应对各种保护措施。Hook动态注册的JNI函数,就像在程序的启动阶段埋下了一颗颗监听器,让你能洞察那些最深层的秘密。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/27 2:41:43

GitHub界面汉化终极指南:3分钟让英文GitHub变中文

GitHub界面汉化终极指南&#xff1a;3分钟让英文GitHub变中文 【免费下载链接】github-chinese GitHub 汉化插件&#xff0c;GitHub 中文化界面。 (GitHub Translation To Chinese) 项目地址: https://gitcode.com/gh_mirrors/gi/github-chinese 你是否因为GitHub的英文…

作者头像 李华
网站建设 2026/7/27 2:40:45

Linux硬件时钟管理:clock命令详解与实践

1. 认识clock命令&#xff1a;系统时间的守护者在Linux系统中&#xff0c;时间管理从来都不是简单的"看看表"这么简单。作为系统管理员&#xff0c;我经常需要处理各种时间相关的问题&#xff1a;为什么日志时间戳对不上&#xff1f;为什么定时任务提前执行了&#x…

作者头像 李华
网站建设 2026/7/27 2:33:40

AI Agent如何革新礼品包装定制行业

1. AI Agent如何重塑礼品包装定制行业礼品包装行业正面临前所未有的转型压力。作为从业十年的包装设计师&#xff0c;我亲眼见证了客户需求从"标准化"到"高度个性化"的演变过程。三年前&#xff0c;我们工作室接到的订单中&#xff0c;约70%是标准化的节日…

作者头像 李华
网站建设 2026/7/27 2:31:57

7.1.1.1 空口物理信道和信号的基本功能和特征

7.1.1.1 空口物理信道和信号的基本功能和特征 在 5G NR 协议体系中&#xff0c;物理层&#xff08;Layer 1&#xff09;是所有通信行为的物理基石。它是协议栈最底层的实现&#xff0c;负责将复杂的上层配置和业务需求转化为无线电波在空间中传输。理解物理信道与信号的区别与…

作者头像 李华
网站建设 2026/7/27 2:31:42

空间数字谜题如何训练程序员的模式识别与工程思维

那天下午&#xff0c;我正对着屏幕发呆&#xff0c;试图从一堆待办事项里找回一点专注力。一个链接跳了出来——“Sequence&#xff0c;一个每日空间数字谜题”。坦白说&#xff0c;这类“每日一题”的小游戏我见过不少&#xff0c;大部分玩几次就腻了。但点开后的五分钟&#…

作者头像 李华