当前位置:首页>安卓APP>安卓逆向系列 05:Frida Native 层 Hook 实战(SO 文件)

安卓逆向系列 05:Frida Native 层 Hook 实战(SO 文件)

  • 2026-11-04 01:54:15
安卓逆向系列 05:Frida Native 层 Hook 实战(SO 文件)

越来越多 App 把核心逻辑下沉到 Native 层(C/C++),编译成 .so 文件。Java 层什么都看不到——但 Frida 同样可以 Hook。



一、为什么要 Hook Native 层?

  • 加密算法、签名计算越来越多放在 SO 里

  • Java 层只传递加密后的结果,无法在 Java 层拿到明文

  • VMP 加固的代码运行在 Native 的私有虚拟机中

  • 反调试、Root 检测逻辑通常在 Native 实现


二、SO 文件结构(ELF 格式)

如图所示,.so 是标准 ELF 格式文件,关键段:

段名内容逆向关注点
.text可执行代码Hook 函数实现
.rodata只读数据(字符串常量等)找 key/salt
.dynsym动态符号表(导出函数列表)定位函数地址
.got / .plt函数跳转表PLT Hook 点

三、定位 Native 函数的三种方式

方式一:按导出符号名(最简单)

// 函数有导出符号(未被 strip)varaddr=Module.findExportByName("libcrypto.so", "AES_encrypt");console.log("AES_encrypt @ "+addr);

方式二:按偏移地址

// 先找到模块基址,再加上偏移量// 偏移量从 IDA Pro / Ghidra 静态分析获得varbase=Module.findBaseAddress("libnative.so");varoffset=0x1234;  // IDA 中的函数地址varfuncAddr=base.add(offset);console.log("目标函数 @ "+funcAddr);

方式三:符号扫描

// 枚举模块所有导出符号,找包含关键字的Module.enumerateExports("libnative.so").forEach(function(exp) {if (exp.name.includes("sign") ||exp.name.includes("encrypt")) {console.log(exp.name+" @ "+exp.address);    }});

四、Interceptor.attach(最常用)

varaddr=Module.findExportByName("libcrypto.so", "AES_encrypt");Interceptor.attach(addr, {onEnter: function(args) {// args[0], args[1], args[2] 是函数参数(指针)console.log("[AES_encrypt] 调用");// 读取第一个参数(输入数据指针)// AES_encrypt(const unsigned char *in, unsigned char *out, const AES_KEY *key)varinPtr=args[0];varlen=16;  // AES 块大小console.log("[AES_encrypt] 输入:");console.log(hexdump(inPtr, { length: len }));// 保存参数供 onLeave 使用this.outPtr=args[1];    },onLeave: function(retval) {// 读取输出数据(加密结果)console.log("[AES_encrypt] 输出:");console.log(hexdump(this.outPtr, { length: 16 }));    }});

五、Interceptor.replace(完整替换)

// 完全替换函数实现varcheckRootAddr=Module.findExportByName("libsecurity.so", "checkRootStatus");// 保存原始函数,供新实现调用varoriginalCheck=newNativeFunction(checkRootAddr, 'int', []);Interceptor.replace(checkRootAddr, newNativeCallback(function() {console.log("[checkRoot] 被调用,强制返回 0");return0;  // 0 = 未 Root    },'int',  // 返回值类型    []      // 参数类型列表));

六、读取和修改内存

// 读取字符串varstrAddr=ptr("0x7f001234");console.log(Memory.readUtf8String(strAddr));console.log(Memory.readCString(strAddr));  // C 风格字符串// 读取整数console.log(Memory.readS32(addr));   // 有符号 32 位console.log(Memory.readU32(addr));   // 无符号 32 位console.log(Memory.readU64(addr));   // 64 位// 读取字节数组varbytes=Memory.readByteArray(addr, 32);console.log(hexdump(bytes));// 写入(危险!修改前确保有写权限)Memory.protect(addr, 16, 'rwx');     // 先改权限Memory.writeU32(addr, 0x12345678);   // 再写值Memory.writeByteArray(addr, [0x90, 0x90]);  // NOP 指令

七、hexdump 调试技巧

// hexdump 是 Native 层调试最常用工具Interceptor.attach(targetAddr, {onEnter: function(args) {// 完整 hexdumpconsole.log(hexdump(args[0], {offset: 0,       // 起始偏移length: 64,      // 读取字节数header: true,    // 显示地址列ansi: false// 终端颜色(不支持时关掉)        }));    }});

八、ARM 汇编速查(10 条最常见)

指令含义示例
MOV赋值MOV X0, #0 → X0 = 0
LDR从内存读取LDR X0, [X1] → X0 = *X1
STR写入内存STR X0, [X1] → *X1 = X0
BL调用函数BL 0x1234 → call 0x1234
RET函数返回RET
CMP比较CMP X0, #0
B.EQ相等时跳转B.EQ label
ADD加法ADD X0, X0, #4
SUB减法SUB SP, SP, #16
STP/LDP保存/恢复寄存器对STP X29, X30, [SP, #-16]!

寄存器约定(ARM64): X0-X7 传递参数和返回值;X8 = 间接结果地址;SP = 栈指针;X29 = 帧指针;X30 = 链接寄存器(返回地址)


九、实战:Hook libssl 抓明文数据

// Hook SSL_read / SSL_write 拿到明文Java.perform(function() {// 找到 libssl.sovarSSL_read=Module.findExportByName("libssl.so", "SSL_read");varSSL_write=Module.findExportByName("libssl.so", "SSL_write");if (SSL_read) {Interceptor.attach(SSL_read, {onLeave: function(retval) {varlen=retval.toInt32();if (len>0) {// args[1] 是数据缓冲区vardata=Memory.readByteArray(this.context.x1, len);console.log("[SSL_read] "+len+" bytes:");console.log(hexdump(data, { length: Math.min(len, 256) }));                }            },onEnter: function(args) {this.context= {x1: args[1]                };            }        });    }if (SSL_write) {Interceptor.attach(SSL_write, {onEnter: function(args) {varlen=args[2].toInt32();if (len>0) {vardata=Memory.readByteArray(args[1], len);console.log("[SSL_write] "+len+" bytes:");console.log(hexdump(data, { length: Math.min(len, 256) }));                }            }        });    }});

十、常见问题

问题原因解决方案
findExportByName 返回 null符号被 strip 掉用 IDA 静态分析找偏移地址
Hook 成功但 onEnter 不触发可能有多个同名 soProcess.enumerateModules() 找到正确的
hexdump 读取崩溃指针无效或长度越界先用 ptr.isNull() 判空
Process terminatedApp 检测到 Hook见第 09 篇反检测方案

下一篇:砸壳全攻略

最新文章

随机文章