news 2026/6/18 14:45:16

安卓APP加固后怎么搞?手把手教你用Frida Hook无导出So函数(附Arm64偏移计算)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
安卓APP加固后怎么搞?手把手教你用Frida Hook无导出So函数(附Arm64偏移计算)

安卓So层无导出函数Hook实战:从特征定位到Arm64偏移计算

在移动安全领域,对抗加固和混淆的技术始终是一场攻防双方的拉锯战。当开发者将关键函数隐藏在So层并抹去导出表信息时,传统的Hook方法往往束手无策。本文将深入探讨如何结合静态分析与动态调试技术,精准定位无导出函数的内存地址,并针对Arm32/Arm64架构差异提供完整的Frida Hook解决方案。

1. So层Hook技术基础

1.1 有导出与无导出函数的本质区别

在ELF文件格式中,导出表(.dynsym节)就像函数的"通讯录",记录了符号名与地址的映射关系。有导出函数可以直接通过Module.findExportByName定位,而无导出函数则如同"隐形人",需要更精细的追踪手段:

// 典型的有导出函数声明 extern "C" void exposed_func() { LOGD("This function is exported"); } // 典型的无导出函数声明 __attribute__((visibility("hidden"))) void hidden_func() { LOGD("This function is hidden"); }

两种函数在IDA中的表现差异明显:

特征有导出函数无导出函数
IDA视图显示函数名显示为sub_XXXX
符号表可见性.dynsym节可见仅.symtab节可能保留
定位方式直接名称查询需特征匹配或偏移计算

1.2 Frida的Native Hook原理

Frida通过动态二进制插桩(DBI)技术实现So层Hook,其核心流程分为三步:

  1. 基址定位:获取目标So模块的加载基地址
  2. 地址计算:基址 + 函数偏移 = 绝对内存地址
  3. 插桩执行:通过Interceptor替换原函数执行流
// 基本Hook模板 Interceptor.attach(targetAddress, { onEnter: function(args) { console.log("Entering function at:", this.returnAddress); }, onLeave: function(retval) { console.log("Returning:", retval); } });

2. 无导出函数定位技术

2.1 静态特征分析法

当函数没有导出符号时,我们需要在IDA中通过以下特征进行人工定位:

  • 字符串引用:查找函数内使用的独特字符串
  • 交叉引用:分析调用该函数的上级函数
  • 指令模式:识别特定的汇编指令序列
  • 参数特征:观察寄存器/栈的使用方式

以实际案例说明,假设我们需要定位一个校验函数:

  1. 在IDA Strings窗口搜索"Verification failed"等关键字符串
  2. 右键跳转到引用该字符串的代码位置
  3. 向上追溯函数入口点(通常以STP/LDP指令开头)

2.2 动态行为追踪法

当静态分析受阻时,可结合动态调试:

// 监控So模块的所有函数调用 const module = Process.getModuleByName('libtarget.so'); const ranges = module.enumerateRanges('r-x'); ranges.forEach(range => { Memory.scan(range.base, range.size, "ff 43 ? d1", { onMatch: function(address, size) { console.log("Found potential function prologue at:", address); } }); });

常用Arm64函数特征码:

指令类型特征字节码说明
函数开头ff 43 ? d1STP指令常见组合
字符串引用? ? ? 58LDR指令加载字符串
函数结尾c0 03 5f d6RET指令

3. Arm64架构下的偏移计算

3.1 基址获取与偏移修正

在Android系统中,So模块的加载地址会受ASLR影响每次不同:

// 获取模块基址的正确方式 const moduleBase = Module.findBaseAddress('libtarget.so'); console.log("Module base:", moduleBase); // 计算绝对地址(Arm64需注意指针长度) function calculateAbsoluteOffset(offset) { return moduleBase.add(offset); }

不同架构的指针处理差异:

架构指针长度Frida指针类型地址计算方式
Arm324字节NativePointerptr.add(offset)
Arm648字节UInt64/NativePointerptr.add(offset)
Thumb2/4字节需+1处理ptr.add(offset|1)

3.2 实战案例:Hook加密函数

假设我们需要Hook一个Arm64下的AES加密函数:

  1. 在IDA中确定函数偏移为0x7A3C
  2. 处理Thumb模式下的地址对齐
const encryptFuncOffset = 0x7A3C; const moduleBase = Module.findBaseAddress('libcrypto.so'); // Arm64不需要Thumb模式处理 const absoluteAddr = moduleBase.add(encryptFuncOffset); Interceptor.attach(absoluteAddr, { onEnter: function(args) { console.log("AES key:", args[0].readByteArray(32)); console.log("Input data:", args[1].readByteArray(args[2].toInt32())); }, onLeave: function(retval) { console.log("Output cipher:", retval.readByteArray(16)); } });

4. 对抗加固的高级技巧

4.1 动态加载So的Hook时机

许多加固方案会延迟加载关键So,需要监听模块加载事件:

const dlopen = Module.findExportByName(null, "dlopen"); Interceptor.attach(dlopen, { onEnter: function(args) { const soName = args[0].readCString(); if (soName.includes("libtarget.so")) { this.shouldHook = true; } }, onLeave: function(retval) { if (this.shouldHook) { setTimeout(() => { hookHiddenFunctions(); }, 500); // 等待初始化完成 } } });

4.2 指令修复技术

某些加固会修改函数入口指令,需要原始指令恢复:

function hookWithFixup(targetAddr, originalOpcodes) { const originalFunc = new NativeFunction(targetAddr, 'void', []); // 备份原始指令 const backup = Memory.alloc(Process.pageSize); backup.writeByteArray(originalOpcodes); Interceptor.replace(targetAddr, new NativeCallback(() => { console.log("Before original function"); originalFunc(); console.log("After original function"); }, 'void', [])); return { restore: () => { Memory.protect(targetAddr, originalOpcodes.length, 'rwx'); targetAddr.writeByteArray(originalOpcodes); } }; }

5. 完整实战:Hook校验函数

综合应用上述技术,我们来看一个完整的无导出函数Hook案例:

  1. 静态分析阶段

    • 使用IDA定位到校验函数偏移为0x8F24
    • 发现函数引用字符串"Invalid license"
    • 确认函数原型为:bool verify(const char* input)
  2. 动态Hook脚本

function hookLicenseCheck() { const soName = "libsecurity.so"; const checkOffset = 0x8F24; const moduleBase = Module.findBaseAddress(soName); if (!moduleBase) { console.error("Module not loaded, waiting..."); return false; } const checkFunc = moduleBase.add(checkOffset); console.log("License check function at:", checkFunc); Interceptor.attach(checkFunc, { onEnter: function(args) { this.input = args[0].readCString(); console.log(`Verify called with: ${this.input}`); }, onLeave: function(retval) { console.log(`Original return: ${retval.toInt32()}`); retval.replace(1); // 强制验证通过 } }); return true; } // 处理延迟加载情况 if (!hookLicenseCheck()) { const dlopen = Module.findExportByName(null, "dlopen"); Interceptor.attach(dlopen, { onEnter: function(args) { const soName = args[0].readCString(); if (soName && soName.includes("libsecurity.so")) { this.targetSo = soName; } }, onLeave: function(retval) { if (this.targetSo) { setTimeout(hookLicenseCheck, 300); } } }); }
  1. 运行效果验证
    • 输入无效许可证时原始返回0
    • Hook后强制返回1绕过验证
    • 控制台输出完整调用日志

在实际对抗更复杂的加固方案时,可能需要结合更多技术:

  • 内存扫描:搜索特征字节序列定位关键函数
  • 指令跟踪:使用Stalker分析执行流程
  • 环境检测:绕过反调试和Frida检测机制

通过本方案的技术组合,即使面对没有导出符号的So层函数,也能实现精准定位和可靠Hook。不同Android版本和芯片架构可能需要调整偏移计算方式,建议在实际设备上验证后再部署到生产环境。

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

【毕业设计】基于springboot+微信小程序的母猪生猪养殖信息化管理系统基于SpringBoot猪场管理系统(源码+文档+远程调试,全bao定制等)

博主介绍:✌️码农一枚 ,专注于大学生项目实战开发、讲解和毕业🚢文撰写修改等。全栈领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围:&am…

作者头像 李华
网站建设 2026/6/6 8:22:03

不止于终端:挖掘MobaXterm的日志记录与文件传输(Zmodem)隐藏功能

不止于终端:挖掘MobaXterm的日志记录与文件传输(Zmodem)隐藏功能 在远程办公和跨系统运维成为常态的今天,高效的工具选择往往能决定工作效率的天花板。MobaXterm作为一款集成了多种实用功能的终端工具,其价值远不止于基…

作者头像 李华
网站建设 2026/6/6 8:15:49

智慧树自动刷课插件:高效学习终极指南

智慧树自动刷课插件:高效学习终极指南 【免费下载链接】zhihuishu 智慧树刷课插件,自动播放下一集、1.5倍速度、无声 项目地址: https://gitcode.com/gh_mirrors/zh/zhihuishu 智慧树刷课插件是一款专为智慧树在线教育平台设计的Chrome浏览器扩展…

作者头像 李华
网站建设 2026/6/6 8:15:13

KiCad导出的Gerber文件,后缀名GBL和.gbl到底有啥区别?一次讲清所有层

KiCad导出的Gerber文件:GBL与.gbl的区别及各层功能详解 刚接触PCB设计的新手在首次导出Gerber文件时,往往会对着满屏的.GTL、.GBL、.gbl等文件后缀感到困惑。这些看似相似却大小写不同的文件名究竟代表什么?它们会影响电路板的生产吗&#x…

作者头像 李华
网站建设 2026/6/8 15:21:25

避开这些坑!MAX17043锂电池电量计与STC8的I2C通信实战避坑指南

MAX17043锂电池电量计与STC8深度开发实战:从寄存器操作到低功耗设计在嵌入式设备开发中,精确的电池电量管理往往是决定产品用户体验的关键因素。MAX17043作为一款成熟的锂电池电量计芯片,以其高精度和低功耗特性广泛应用于各类便携设备。然而…

作者头像 李华