引言

在上一篇文章中,我们介绍了如何使用 Frida Hook Android 应用中的 Java 函数。然而,在实际逆向分析中,很多关键逻辑是通过 Native 层(C/C++) 实现的,例如加密算法、网络通信、反调试机制等。

本文将带你了解如何使用 Frida 对 Native 函数进行 Hook,掌握基本的 Native Hook 技巧,并提供完整的示例代码和操作步骤。同时,我们将重点介绍两种常见场景:

  • 如何 Hook 导出函数

  • 如何 Hook 未导出函数


什么是 Native Hook?

Native Hook 指的是对运行在 C/C++ 层的函数进行动态拦截与修改。与 Java 层不同,Native 层的函数通常位于 .so 动态链接库中,且不暴露给 Java 虚拟机直接调用,因此 Hook 难度更高。

Frida 提供了强大的 Native Hook 支持,包括:

  • 查找函数地址

  • 注册 Hook 回调

  • 修改寄存器/内存值

  • 打印调用堆栈

  • 返回自定义值


环境准备

1. 安装 Frida 工具链(回顾)

请确保你已按照上一篇文章完成以下配置:

1
pip install frida-tools

或 Node.js 版本:

1
npm install -g frida

上传并启动 frida-server 到设备上(armeabi-v7a / arm64-v8a)

1
2
adb push frida-server /data/local/tmp/
adb shell chmod 755 frida-server && ./frida-server &

示例场景

假设我们的目标 App 中存在一个 Native 函数:

1
2
3
4
5
6
7
8
extern "C" JNIEXPORT jboolean JNICALL
Java_com_example_app_NativeLib_checkSN(JNIEnv *env, jobject /* this */, jstring sn, jstring key) {
const char *native_sn = env->GetStringUTFChars(sn, nullptr);
const char *native_key = env->GetStringUTFChars(key, nullptr);

bool result = verify(native_sn, native_key); // 关键验证逻辑
return result;
}

我们需要 Hook verify() 函数,查看其输入参数并篡改返回值。


编写 Frida Native Hook 脚本

下面是完整的 Frida Hook Native 函数脚本示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function hook_native_verify() {
// 获取 libnative-lib.so 基址
var baseAddr = Module.findBaseAddress("libnative-lib.so");

if (baseAddr) {
console.log("[*] Base address of libnative-lib.so: " + baseAddr);

// 假设 verify 函数偏移为 0x1234(可通过 IDA Pro 或 objdump 获取)
var verifyAddr = baseAddr.add(0x1234);

console.log("[*] Hooking verify function at address: " + verifyAddr);

Interceptor.attach(verifyAddr, {
onEnter: function(args) {
console.log("[*] Called verify()");
console.log("[*] Arg1 (sn): " + args[0].readUtf8String());
console.log("[*] Arg2 (key): " + args[1].readUtf8String());

// 可选:修改参数
// args[0] = ptr("0xdeadbeef");
},
onLeave: function(retval) {
console.log("[*] Return value: " + retval.toInt32());

// 修改返回值为 true(即 1)
retval.replace(1);
}
});
} else {
console.log("[!] Failed to find libnative-lib.so");
}
}

setTimeout(hook_native_verify, 0);

如何运行脚本

方法一:命令行执行

1
frida -U -n com.example.app -l hook_native.js

方法二:Python 控制脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import frida
import sys

def on_message(message, data):
print("[*] Message:", message)

device = frida.get_usb_device()
pid = device.spawn(["com.example.app"])
device.resume(pid)
session = device.attach(pid)

with open("hook_native.js") as f:
script = session.create_script(f.read())

script.on('message', on_message)
script.load()

sys.stdin.read()

技术点解析

1. Module.findBaseAddress("libname.so")

获取指定 .so 文件的加载基地址。这是定位函数地址的基础。

2. baseAddr.add(offset)

结合函数偏移地址,计算出函数的完整地址。

偏移地址可以通过 IDA Pro、Ghidra、objdump 等工具获得。

3. Interceptor.attach(address, callbacks)

注册对某个地址的 Hook 回调,支持 onEnteronLeave 两个阶段。

  • onEnter: 函数即将被执行时触发

  • onLeave: 函数即将返回时触发

4. args[n]

表示函数传入的第 n 个参数,是一个 NativePointer 类型对象,可读取内存内容:

  • readCString() / readUtf8String():读取字符串

  • toInt32():转为整数

  • add(offset):指针偏移

5. retval.replace(value)

强制修改函数返回值,实现绕过校验、激活功能等目的。


进阶技巧:Hook 导出函数 vs 未导出函数

在 Native Hook 中,根据函数是否被导出,我们可以分为两类处理方式:


✅ Hook 导出函数(Exported Function)

如果目标函数是导出函数(出现在 .plt 表中),可以直接通过名字查找:

示例:Hook JNI 函数 Java_a_b_k0801_MainActivity_stringFromJNI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function hook_exported_function() {
var funcPtr = Module.findExportByName("libk0801.so", "Java_a_b_k0801_MainActivity_stringFromJNI");
if (funcPtr) {
console.log("[*] Found exported function at address:", funcPtr);

Interceptor.attach(funcPtr, {
onEnter: function(args) {
console.log("[*] stringFromJNI called with arg3:", Java.vm.getEnv().getStringUtfChars(args[3]).readCString());
},
onLeave: function(retval) {
console.log("[*] Return value:", Java.vm.getEnv().getStringUtfChars(retval).readCString());
}
});
} else {
console.log("[!] Exported function not found!");
}
}

✅ Hook 未导出函数(Unexported Function)

未导出函数不会出现在 .plt 表中,需要通过静态分析找到其偏移地址后再进行 Hook。

示例:Hook 地址为 0x1E250 的未导出函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function hook_unexported_function() {
var baseAddr = Module.findBaseAddress("libk0801.so");
if (baseAddr) {
var funcAddr = baseAddr.add(0x1E250);
console.log("[*] Hooking unexported function at address:", funcAddr);

Interceptor.attach(funcAddr, {
onEnter: function(args) {
console.log("[*] Args:", args[0], args[1], args[2]);
console.log("[*] Read CString:", ptr(args[2]).readCString());
},
onLeave: function(retval) {
console.log("[*] Return value:", myReadStdString(retval));
}
});
} else {
console.log("[!] Failed to locate libk0801.so");
}
}

// 自定义读取 std::string 函数
function myReadStdString(str) {
const isTiny = (str.readU8() & 1) == 0;
if (isTiny) {
return str.add(1).readUtf8String();
}
return str.add(2 * Process.pointerSize).readPointer().readUtf8String();
}

进阶技巧汇总

✅ 枚举所有模块

列出当前进程加载的所有模块:

1
2
3
4
5
6
Process.enumerateModules({
onMatch: function(module) {
console.log(module.name + " - " + module.base + " - " + module.size);
},
onComplete: function() {}
});

✅ 内存 dump

打印指定地址附近的机器码:

1
console.log(hexdump(addr, { offset: 0, length: 32, header: true, ansi: true }));

✅ 使用 Memory.scan 查找特征码

当无法确定函数地址时,可以使用字节码特征匹配:

1
2
3
4
5
6
7
Memory.scan(baseAddr, size, "FF 00 ?? 11 ?? ?? 00 00", {
onMatch: function(address, size) {
console.log("Found pattern at: " + address);
},
onError: function(reason) {},
onComplete: function() {}
});

常见问题与解决方案

❓ Hook 地址无效?

  • 确保 .so 加载成功

  • 检查偏移是否正确(注意 PIE 偏移)

  • 使用 Memory.scan() 查找特定字节码特征

❓ 参数类型不确定?

  • 可尝试打印多个寄存器或指针偏移

  • 使用 Memory.read*() 直接读取内存

❓ Hook 失败?

  • 确保设备 root 并运行 frida-server

  • Frida 无法 Hook 未加载的函数,可以延迟 Hook 或监听模块加载事件


法律风险提示

本文内容仅用于合法授权的安全测试和研究,请勿用于非法用途。任何未经授权的系统入侵、数据篡改行为均属违法行为。


结语

通过本文,你已经掌握了 Frida 在 Native 层进行 Hook 的基础方法,包括查找函数地址、Hook 调用流程、修改参数和返回值等。这些技术对于分析 APK 中隐藏的加密算法、绕过反调试机制、提取密钥等任务至关重要。

下一期我们将介绍 Frida 高级技巧:Hook C++ STL 函数、处理 C++ 异常、Hook JNI 接口函数等,敬请期待!