Frida 实现 JNI 方法地址跟踪、反汇编、Patch
版权归作者所有,如有转发,请注明文章出处:https://cyrus-studio.github.io/blog/
Frida 反汇编
Frida 提供了反汇编 API,主要用于对目标进程的代码进行动态分析,例如:
获取函数的机器代码并将其反汇编为汇编指令。
分析内存中的指令流。
这些功能在 Frida 的 Instruction 类中实现,基于 Capstone 实现的反汇编功能。
https://frida.re/docs/javascript-api/#instruction
比如,可以通过 Instruction.parse 方法反汇编指定地址的汇编信息
[Remote::AndroidExample]-> Instruction.parse(ptr("0x7e7acdb9a0"));
{
"address": "0x7e7acdb9a0",
"groups": [],
"mnemonic": "sub",
"next": "0x7e7acdb9a4",
"opStr": "sp, sp, #0x10",
"operands": [
{
"type": "reg",
"value": "sp"
},
{
"type": "reg",
"value": "sp"
},
{
"type": "imm",
"value": "16"
}
],
"regsRead": [],
"regsWritten": [],
"size": 4
}
Android 示例代码
定义一个 native 函数 add,计算 1+ 1 + 1 并返回结果。
把 add 函数动态注册到 com.cyrus.example.frida.disassemble.FridaDisassembleActivity#add。
#include <jni.h>
#include <android/log.h>
#define LOG_TAG "FridaDisassemble"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
// 实现 add 方法:计算 1 + 1 + 1
jint native_add(JNIEnv *env, jobject obj) {
return 1 + 1 + 1;
}
// 定义 JNI 方法映射
static const JNINativeMethod methods[] = {
{"add", "()I", (void *)native_add} // 方法名, 方法签名, C++函数指针
};
// 动态注册 JNI 方法
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env;
if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
// 获取 Java 类
jclass clazz = env->FindClass("com/cyrus/example/frida/disassemble/FridaDisassembleActivity");
if (clazz == nullptr) {
LOGD("[-] Failed to find class FridaDisassembleActivity");
return JNI_ERR;
}
// 注册 native 方法
if (env->RegisterNatives(clazz, methods, sizeof(methods) / sizeof(methods[0])) < 0) {
LOGD("[-] Failed to register native methods");
return JNI_ERR;
}
LOGD("[+] Successfully registered native methods");
return JNI_VERSION_1_6;
}
在 kotlin 层调用 add 函数并 toast 结果。
// 调用 native 方法
val result = add()
// 显示 Toast
Toast.makeText(this, "add() returned: $result", Toast.LENGTH_LONG).show()
当点击按钮可以看到返回结果 3。
接下来通过 Frida 实现:
找到 add 函数动态注册的地址
反汇编 add 函数机器码
patch add 函数汇编指令修改返回结果
JNI 方法地址跟踪
1. 启动 Frida Server
# 启用超级管理员
adb root
# 进入命令行
adb shell
# 启动frida-server,并指定端口
/data/local/tmp/fs -l 0.0.0.0:1234
# 端口转发到本地
adb forward tcp:1234 tcp:1234
Frida 的详细使用可以介绍参考这篇文章:使用 Frida Hook Android App
2. hook RegisterNatives 方法
创建 RegisterNatives.js hook RegisterNatives 函数并打印每个动态注册的 jni 方法信息。
// 查找 libart.so 中的 RegisterNatives 地址
function findRegisterNatives() {
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("art") >= 0 &&
symbol.name.indexOf("JNI") >= 0 &&
symbol.name.indexOf("RegisterNatives") >= 0 &&
symbol.name.indexOf("CheckJNI") < 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); // 方法签名
let symbol = DebugSymbol.fromAddress(fnPtr); // 函数符号信息
// 打印每个 JNI 方法的详细信息
console.log("[RegisterNatives] Class:", className);
console.log(" Method: " + name);
console.log(" Signature: " + sig);
console.log(" Function Pointer: " + fnPtr);
console.log(" Function Offset: " + symbol);
console.log(" Return Address: " + this.returnAddress);
}
}
});
} else {
console.log("[!] Cannot hook RegisterNatives because the address is null.");
}
}
// 执行查找并 hook RegisterNatives
setImmediate(function() {
let addrRegisterNatives = findRegisterNatives();
hookRegisterNatives(addrRegisterNatives);
});
3. 附加到当前设备前台应用
frida -H 127.0.0.1:1234 -l RegisterNatives.js -F
输出如下:
____
/ _ | Frida 14.2.18 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at https://www.frida.re/docs/home/
[+] Found RegisterNatives symbol: _ZN3art3JNI15RegisterNativesEP7_JNIEnvP7_jclassPK15JNINativeMethodi at 0x7eecdf6d10
[Remote::AndroidExample]-> [RegisterNatives] method_count: 1
[RegisterNatives] Class: com.cyrus.example.frida.disassemble.FridaDisassembleActivity
Method: add
Signature: ()I
Function Pointer: 0x7eddb289a0
Function Offset: 0x7eddb289a0
Return Address: 0x7eecd7da74
找到目标方法地址为:0x7eddb289a0
反汇编 JNI 方法
反汇编并打印目标地址开始 10 条汇编指令。
// 定义函数:打印指定地址开始的指定数量的汇编指令
function printAssemblyInstructions(methodAddress, numInstructions) {
for (var i = 0; i < numInstructions; i++) {
// 解析当前地址的指令
var instruction = Instruction.parse(methodAddress);
// 打印当前指令的地址和汇编指令
console.log(instruction.address + ": " + instruction.toString());
// 获取下一条指令的地址,当前地址 + 指令的大小
methodAddress = methodAddress.add(instruction.size);
}
}
// 调用函数,传入方法地址和指令数量数量
printAssemblyInstructions(ptr("0x7eddb289a0"), 10);
执行脚本
frida -H 127.0.0.1:1234 -l instructions.js -F
输出如下:
0x7eddb289a0: sub sp, sp, #0x10
0x7eddb289a4: str x0, [sp, #8]
0x7eddb289a8: str x1, [sp]
0x7eddb289ac: mov w0, #3
0x7eddb289b0: add sp, sp, #0x10
0x7eddb289b4: ret
0x7eddb289b8: sub sp, sp, #0x50
0x7eddb289bc: stp x29, x30, [sp, #0x40]
0x7eddb289c0: add x29, sp, #0x40
0x7eddb289c4: mrs x8, tpidr_el0
Patch JNI 方法
把目标地址 0x7eddb289ac 的汇编指令
mov w0, #3
修改为:
mov w0, #2
汇编指令转换机器码:https://armconverter.com/?code=mov+w0,+%232
原始机器码:0x40008052。
小端字节序:反转字节顺序后的机器码是 0x52800040。
编码实现如下:
// 目标地址
var targetAddress = ptr("0x7eddb289ac");
// 获取目标地址所在内存区域的可访问权限,并修改为可读写
Memory.protect(targetAddress, 8, 'rwx'); // 将目标地址所在的内存页面设置为可读写执行
// 构造新的汇编指令:mov w0, #2
var newCode = [0x52800040]; // ARM64 `mov w0, #2` 的机器码
// 将新的机器码写入内存
Memory.writeByteArray(targetAddress, newCode);
patch 完成后,再点击按钮可以看到返回结果变成了 2 。
完整代码
创建 frida 脚本 patch_add_function.js,把上面所有流程连贯实现, 源码如下:
// 查找 libart.so 中的 RegisterNatives 地址
function findRegisterNatives() {
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("art") >= 0 &&
symbol.name.indexOf("JNI") >= 0 &&
symbol.name.indexOf("RegisterNatives") >= 0 &&
symbol.name.indexOf("CheckJNI") < 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, className, methodName, methodSig, customAction) {
if (addrRegisterNatives !== null) {
// 使用 Interceptor 附加到 RegisterNatives 地址
Interceptor.attach(addrRegisterNatives, {
onEnter: function (args) {
// 获取方法数量
let methodCount = args[3].toInt32();
console.log("[RegisterNatives] method_count:", methodCount);
// 获取 Java 类的类名
let javaClass = args[1];
let currentClassName = Java.vm.tryGetEnv().getClassName(javaClass); // 获取类名
// 打印每个 JNI 方法的详细信息
let methodsPtr = ptr(args[2]);
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); // 方法签名
let symbol = DebugSymbol.fromAddress(fnPtr); // 函数符号信息
// 打印每个 JNI 方法的详细信息
console.log("[RegisterNatives] Class: " + currentClassName);
console.log(" Method: " + name);
console.log(" Signature: " + sig);
console.log(" Function Pointer: " + fnPtr);
console.log(" Function Offset: " + symbol);
console.log(" Return Address: " + this.returnAddress);
// 如果指定了 className 且不匹配,则跳过
if (className && currentClassName !== className) {
continue;
}
// 如果指定了 methodName 且不匹配,则跳过
if (methodName && name !== methodName) {
continue;
}
// 如果指定了 methodSig 且不匹配,则跳过
if (methodSig && sig !== methodSig) {
continue;
}
// 执行自定义操作(传递函数的地址作为参数)
if (customAction) {
customAction(fnPtr);
}
}
}
});
} else {
console.log("[!] Cannot hook RegisterNatives because the address is null.");
}
}
// 定义函数:打印指定地址开始的指定数量的汇编指令
function printAssemblyInstructions(methodAddress, numInstructions) {
for (var i = 0; i < numInstructions; i++) {
// 解析当前地址的指令
var instruction = Instruction.parse(methodAddress);
// 打印当前指令的地址、助记符和操作数
console.log(instruction.address + ": " + instruction.toString());
// 获取下一条指令的地址,4 是指令的大小,ARM64 指令通常是 4 字节
methodAddress = methodAddress.add(instruction.size);
}
}
/**
* 修改指定地址的内存内容为给定的机器码
* @param {Pointer} targetAddress - 目标函数的内存地址
* @param {number} size - 目标函数的大小(字节数)
* @param {Uint8Array} newCode - 要写入的机器码(字节数组)
*/
function patchFunction(targetAddress, size, newCode) {
try {
// 获取目标地址所在内存区域的可访问权限,并修改为可读写执行
Memory.protect(targetAddress, size, 'rwx'); // 设置为可读、可写、可执行
// 将新的机器码写入内存
Memory.writeByteArray(targetAddress, newCode);
console.log("[+] Patched function at address: " + targetAddress);
} catch (e) {
console.log("[!] Error patching function at address: " + targetAddress + " - " + e.message);
}
}
// 执行查找并 hook RegisterNatives
setImmediate(function () {
// 查找 RegisterNatives 地址
let addrRegisterNatives = findRegisterNatives();
// 自定义的操作函数:接收 JNI 方法的地址
function customAction(jniMethodAddress) {
// 自定义操作:可以在这里通过 jniMethodAddress 进行特定操作
console.log("[Custom Action] Executing custom action for JNI method at address: " + jniMethodAddress);
// 打印汇编指令,传入方法地址和指令数量数量
printAssemblyInstructions(jniMethodAddress, 10);
// ARM64 `mov w0, #2` 的机器码
var newCode = [0x52800040];
// 计算目标地址:jniMethodAddress 开始第4条指令的地址
var targetAddress = jniMethodAddress.add(3 * 4); // 每条指令为 4 字节,3 * 4 即跳过前三条指令
// patch
patchFunction(targetAddress, 8, newCode);
}
// 调用 hookRegisterNatives,传入类名、方法名、方法签名及自定义操作
hookRegisterNatives(addrRegisterNatives, "com.cyrus.example.frida.disassemble.FridaDisassembleActivity", "add", "()I", customAction);
});
执行脚本
frida -H 127.0.0.1:1234 -l patch_add.js -F
输出如下:
[+] Found RegisterNatives symbol: _ZN3art3JNI15RegisterNativesEP7_JNIEnvP7_jclassPK15JNINativeMethodi at 0x7eecdf6d10
[Remote::AndroidExample]-> [RegisterNatives] method_count: 1
[RegisterNatives] Class: com.cyrus.example.frida.disassemble.FridaDisassembleActivity
Method: add
Signature: ()I
Function Pointer: 0x7edd6169a0
Function Offset: 0x7edd6169a0
Return Address: 0x7eecd7da74
[Custom Action] Executing custom action for JNI method at address: 0x7edd6169a0
0x7edd6169a0: sub sp, sp, #0x10
0x7edd6169a4: str x0, [sp, #8]
0x7edd6169a8: str x1, [sp]
0x7edd6169ac: mov w0, #3
0x7edd6169b0: add sp, sp, #0x10
0x7edd6169b4: ret
0x7edd6169b8: sub sp, sp, #0x50
0x7edd6169bc: stp x29, x30, [sp, #0x40]
0x7edd6169c0: add x29, sp, #0x40
0x7edd6169c4: mrs x8, tpidr_el0
[+] Patched function at address: 0x7edd6169ac
调用 printAssemblyInstructions 方法重新反汇编 add 函数地址可以看到汇编指令 mov w0, #3 已经成功被替换为 mov w0, #2
[Remote::AndroidExample]-> printAssemblyInstructions(ptr("0x7edd6169a0"), 10);
0x7edd6169a0: sub sp, sp, #0x10
0x7edd6169a4: str x0, [sp, #8]
0x7edd6169a8: str x1, [sp]
0x7edd6169ac: mov w0, #2
0x7edd6169b0: add sp, sp, #0x10
0x7edd6169b4: ret
0x7edd6169b8: sub sp, sp, #0x50
0x7edd6169bc: stp x29, x30, [sp, #0x40]
0x7edd6169c0: add x29, sp, #0x40
0x7edd6169c4: mrs x8, tpidr_el0
Android 示例程序完整源码:https://github.com/CYRUS-STUDIO/AndroidExample