14、Native层逆向(三):Unicorn引擎模拟执行、Frida对Native函数的Hook
好,咱们继续聊Native层逆向。前两章我们把ELF文件结构和ARM汇编基础过了一遍,算是把地基打牢了。这一章,我们来点真刀真枪的——怎么让Native代码“跑起来”,以及怎么“拦截”它。
我个人觉得,Native逆向最头疼的地方就是:代码是死的,你得让它动起来才能看出门道。静态分析看半天,不如动态跑一次。所以这一章,我重点讲两个工具:Unicorn引擎和Frida。一个负责模拟执行,一个负责动态Hook。两者配合,基本能覆盖90%的Native层分析场景。
14.1 Unicorn引擎:让Native代码“活”过来
Unicorn是什么?说白了,它是一个轻量级的CPU模拟器。它能模拟ARM、ARM64、x86等多种架构的指令执行。你不需要真机,不需要root,甚至不需要一个完整的操作系统,就能跑一段Native代码。
我最早接触Unicorn,是在分析一个混淆得很厉害的so文件时。静态分析完全看不懂,IDA里全是花指令。后来我试着把关键函数抠出来,用Unicorn模拟执行,结果一下子就看清了它的逻辑。嗯,那感觉就像在黑屋子里突然开了灯。
14.1.1 Unicorn的核心概念
Unicorn的使用其实不复杂,核心就几个概念:
- 引擎(Engine):负责初始化和执行代码
- 内存(Memory):你需要手动映射内存区域,包括代码段、栈、堆
- 寄存器(Register):可以读写CPU寄存器,比如R0-R12、SP、PC等
- 钩子(Hook):可以在指令执行、内存访问等时机插入回调函数
你想想看,这就像搭了一个微型计算机。你给它代码,给它内存,告诉它从哪里开始执行,然后它就能一步步跑下去。
14.1.2 一个简单的Unicorn示例
咱们直接看代码。假设有一段ARM32的汇编代码,功能很简单:把两个数相加。
// 汇编代码(ARM32)
// R0 = 1, R1 = 2
// ADD R2, R0, R1
// BX LR
unsigned char code[] = {
0x01, 0x00, 0x80, 0xE3, // MOV R0, #1
0x02, 0x10, 0x81, 0xE3, // MOV R1, #2
0x02, 0x20, 0x80, 0xE0, // ADD R2, R0, R1
0x1E, 0xFF, 0x2F, 0xE1 // BX LR
};
用Unicorn模拟执行这段代码:
from unicorn import *
from unicorn.arm_const import *
# 初始化ARM32引擎
mu = Uc(UC_ARCH_ARM, UC_MODE_ARM)
# 映射内存:代码段 + 栈
ADDRESS = 0x10000
mu.mem_map(ADDRESS, 2 * 1024 * 1024) # 2MB
mu.mem_write(ADDRESS, bytes(code))
# 设置栈指针
mu.reg_write(UC_ARM_REG_SP, ADDRESS + 0x1000)
# 开始执行
mu.emu_start(ADDRESS, ADDRESS + len(code))
# 读取结果
r2 = mu.reg_read(UC_ARM_REG_R2)
print(f"R2 = {r2}") # 输出:R2 = 3
看到了吗?就这么几行代码,我们模拟执行了一段ARM指令,并且拿到了结果。R2的值是3,完全正确。
14.1.3 Unicorn在逆向中的典型场景
我个人习惯把Unicorn用在以下几个地方:
- 脱壳:很多壳会在运行时解密代码,你可以用Unicorn模拟执行解密过程,然后dump出解密后的代码
- 分析混淆算法:遇到控制流平坦化、指令替换等混淆,静态分析很痛苦,但模拟执行可以绕过这些
- 模拟系统调用:有些Native代码会调用系统API,你可以用Hook来模拟这些调用,返回你想要的值
我曾经遇到过一个so文件,它会在执行前先校验环境。如果检测到是模拟器或调试器,就直接退出。我用Unicorn模拟执行时,通过Hook拦截了它的检测函数,直接返回假值,顺利绕过了校验。
14.2 Frida对Native函数的Hook
如果说Unicorn是“让代码跑起来”,那Frida就是“让代码听你的话”。Frida是一个动态插桩工具,它可以在运行时修改函数的行为。对于Native层,Frida可以Hook so文件中的导出函数,甚至是内部函数。
Frida的原理其实不复杂:它通过ptrace或者注入的方式,把JavaScript引擎(V8或Duktape)注入到目标进程中。然后你写的JS代码就可以在目标进程里执行,拦截函数调用、修改参数、篡改返回值。
14.2.1 Hook Native函数的基本写法
咱们直接上代码。假设目标so文件叫libnative.so,里面有一个导出函数Java_com_example_app_MainActivity_stringFromJNI。我们想Hook它,看看它返回了什么。
// Frida JavaScript代码
Interceptor.attach(Module.findExportByName("libnative.so",
"Java_com_example_app_MainActivity_stringFromJNI"), {
onEnter: function(args) {
console.log("stringFromJNI called!");
console.log("JNIEnv: " + args[0]);
console.log("thiz: " + args[1]);
},
onLeave: function(retval) {
console.log("Original return value: " + retval);
// 修改返回值
retval.replace(ptr(0x12345678));
}
});
这段代码做了三件事:
- 找到
libnative.so中的目标函数地址 - 在函数进入时打印参数
- 在函数返回时打印原始返回值,并把它替换成一个固定值
你想想看,这有多强大。你可以随意修改函数的输入输出,甚至跳过函数体直接返回。
14.2.2 Hook非导出函数
很多时候,我们要Hook的函数并不是导出的。比如一些内部辅助函数。这时候怎么办?
方法一:通过偏移量计算地址。先用IDA找到函数的偏移量,然后加上so的基址。
var base = Module.findBaseAddress("libnative.so");
var func_addr = base.add(0x1234); // 假设偏移是0x1234
Interceptor.attach(func_addr, {
onEnter: function(args) {
console.log("Internal function called");
}
});
方法二:通过特征码搜索。如果so被加固或动态加载,偏移量可能变化。这时候可以用Memory.scan来搜索特征码。
var pattern = "01 02 03 04 ?? 06 07 08"; // 问号表示通配
Memory.scan(Module.findBaseAddress("libnative.so"),
0x100000, pattern, {
onMatch: function(address, size) {
console.log("Found at: " + address);
Interceptor.attach(address, {
onEnter: function(args) {
// ...
}
});
},
onComplete: function() {
console.log("Scan complete");
}
});
Module.findBaseAddress获取基址,再加上偏移量。或者直接用Module.getExportByName,如果函数是导出的。
14.2.3 Frida与Unicorn的配合
这两个工具不是互斥的,而是互补的。我经常这样用:
- 先用Frida在真机上动态分析,看看函数的调用时机、参数特征
- 然后把关键函数抠出来,用Unicorn模拟执行,深入分析算法细节
- 最后再用Frida验证修改后的逻辑是否正确
举个例子。有一次我分析一个支付相关的so,它有一个签名校验函数。我用Frida Hook了它,发现它接收三个参数,返回一个字符串。但算法太复杂,静态看不太懂。于是我把这个函数的字节码导出来,用Unicorn模拟执行,一步步跟踪寄存器的变化,终于搞清楚了它的加密流程。
14.3 本章知识体系
下面这张图,是我自己总结的Native层动态分析的知识体系。你可以把它当作一个路线图。
这张图把Unicorn和Frida的核心能力以及应用场景串在了一起。你可以看到,两者各有侧重,但最终都指向同一个目标:让Native代码变得可观测、可控制。
14.4 实战中的一些体会
最后,我想分享几点实战中的体会:
- 不要迷信工具:Unicorn和Frida都很强大,但它们不是万能的。遇到反调试、反Hook机制时,还是得靠经验和耐心。
- 日志是最好的朋友:无论是Unicorn的Hook回调,还是Frida的console.log,多打日志能帮你快速定位问题。
- 先理解,再动手:我见过很多人一上来就Hook所有函数,结果被海量日志淹没了。先静态分析一下,找到关键函数,再有针对性地动态分析,效率会高很多。
好了,这一章就到这里。Unicorn和Frida这两个工具,值得你花时间去熟练掌握。它们就像瑞士军刀,在Native逆向的战场上,能帮你解决大部分问题。