ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Frida动态插桩实战:自动化Hook移动端加密函数全指南

Frida动态插桩实战:自动化Hook移动端加密函数全指南 做移动端安全分析的人应该都有过这种经历拿到一个App抓包一看请求体全是密文签名串是几串不知道哪来的哈希值代码里搜了半天也找不到加密入口只能一层层往上追。我最早接触Frida就是在这样一个“一头雾水”的项目里。那时候网上资料零散得很中文教程更是一句一句拼出来的踩坑踩到怀疑人生。后来做多了才慢慢把“定位加密函数—看入参—看返回值—还原加解密过程”这条链整理成一套自动化Hook的打法现在一次分析的时间从半天压缩到十来分钟。这篇实战指南就是把我这些年用下来的思路、脚本和坑一次性讲清楚。适合刚入门逆向、想系统做移动端安全分析的读者也适合已经在用Frida但总觉得效率上不去的朋友。1. Frida到底是什么为什么移动端加密分析绕不开它1.1 一句话说透Frida的工作原理Frida本质上是动态插桩工具也就是在不修改目标App安装包的前提下把一段JavaScript引擎注入到App进程内部然后让脚本直接操作内存中的对象、方法、甚至native层的函数指针。通俗点说它像你在一个正在运转的流水线旁边临时搭了个观察台任何经过的零件都能抄下来看一眼不用停机也不用改图纸。这个机制对移动端加密分析的意义非常大。很多App的加密参数都是运行时才生成的静态反编译根本看不出来比如动态生成的密钥、秒级变化的签名串、从服务端拉下来的加密算法参数。用Frida你只需要在运行时把对应函数拦住输入输出直接打印出来加密逻辑瞬间透明。尤其是加解密这类确定性函数一旦你拿到“输入原文输出密文”的样例整个密码学过程基本上就摊在桌面上任你分析了。Frida的注入和脚本执行是热更新的改完脚本不用重装App、不用重启设备几秒钟就能再次重放和分析。这是Frida和其他工具拉开差距的核心优势也是自动化体系能建立起来的前提。我个人的习惯是拿到新目标先跑一轮通用脚本把所有常见加密API罩住再针对具体业务函数做定点分析整个流程一气呵成。1.2 为什么是Frida而不是Xposed或定制ROM提到Hook很多人第一反应是Xposed。我也用Xposed做过不少东西它确实很强但有一个致命伤必须重启设备才能生效而且对系统版本和框架兼容性要求高。定制ROM的方案就更麻烦了刷机、适配、中间还容易把设备搞成砖头。在加密函数分析这种需要高频试错、频繁改脚本反复跑的场景下这种“改一下就得重启五分钟”的节奏完全没法接受。Frida在这几个维度上的对比非常明显我做了一张表大家直观感受一下对比维度FridaXposed定制ROM动态生效秒级无需重启重启才能生效刷机才能生效安装包改动不需要改动不需要改动需要重打包系统批量自动化脚本化天然适合模块开发门槛高几乎不可能对App检测的暴露面相对可控特征明显系统级隐蔽性好新手上手成本中低文档多中等极高从实际工作效率来看Frida几乎是为分析场景量身定做的。你可以在一条命令里启动脚本、收集数据、退出进程整个过程完全可编程后面讲自动化Hook体系时你会看到这一点有多重要。至于定制ROM那类方案更多是用于对抗检测严重的场景日常加密分析完全用不上杀鸡的牛刀。1.3 基础环境搭建版本匹配决定成败先说一个最基础也最容易卡住新手的问题Frida版本不匹配导致的注入失败。Frida的客户端电脑端Python包和Server端手机或模拟器上跑的frida-server必须严格同版本哪怕只差一个小版本号也可能出现attach成功但脚本不执行、或者直接报“unable to connect to remote frida-server”这一类诡异问题。安装步骤非常直接电脑端执行pip install frida-tools然后在GitHub Releases页面下载对应平台的frida-server压缩包解压后推送到设备上。真机用ARM64版本模拟器则要看CPU架构。很多人在这里栽跟头雷电模拟器默认是x86架构的就得下载x86_64的frida-server而不是ARM版本如果你用的是ARM镜像的模拟器那才需要arm64。这事我踩过不只一次每次重新配置环境都先确认一件事——设备是什么架构别想当然。启动frida-server之前建议先给足权限adb shell进入设备后chmod 755然后以root权限运行。很多模拟器需要adb root才能拿到root权限这个步骤不做后续所有操作都会失败。搭好环境后用一条最简单的命令验证一下frida-ps -U如果能看到设备的进程列表说明注入通道已经畅通可以开始正事了。另外一个小建议把frida-server的启动命令写成一个批处理脚本每次用设备前双击启动省得频繁手敲尤其是配合自动化分析的时候环境一乱排查起来特别费神。2. Hook加密函数的完整思路与前置分析2.1 先分清“三层加密”数据加密、协议加密、签名防篡改刚接触逆向的人容易犯一个错误一上来就满代码库搜“AES”“MD5”这类关键字指望直接找到加密函数然后一把梭。实际上移动端的加密体系通常是分层嵌套的至少可以拆成三层来理解。第一层是数据加密最常见的就是AES或RSA用来保护业务字段内容比如请求体里的账号密码、手机号、身份证号这类敏感信息。第二层是协议加密更隐蔽一些很多App会自定义二进制格式或对整体报文再做一层DES/ChaCha20混淆这层的作用是防篡改和防协议回归分析。第三层是签名防篡改通常是基于消息摘要实现的比如MD5、SHA256或者更考究一点会用HMAC甚至叠加时间戳、随机数、设备指纹等动态因子。这三层的性质完全不同分析策略也必须分开。数据加密层核心目标是拿到密钥和初始向量从而能主动加解密消息协议加密层重点是还原封装和解封装的流程往往需要Hook到native层去看签名防篡改层则是要理解它的拼接规则和摘要方式然后复现出可用的签名机。你在动手Hook之前先通过抓包和反编译判断目标处于哪一层直接决定了后面的脚本怎么写。2.2 定位关键函数的三种实用姿势第一种是关键词定位法适合代码没怎么混淆或混淆强度不高的目标。用jadx或者GDA打开反编译后的代码搜“AES/CBC”“SecretKeySpec”“Cipher.getInstance”“MessageDigest”这类特征字符串基本能锁定加密入口。高级一点的还可以搜“IvParameterSpec”“PBEKeySpec”很多App会用基于口令的加密模式密钥派生函数里藏着大量可分析信息。第二种是调用栈回溯法适合抓到包但不知道字段生成位置的场景。在Frida脚本里先对比较泛的方法比如哈希摘要的update或final方法打上Hook并打印调用栈。Frida的Thread.backtrace()方法可以直接把调用栈吐出来瞬间就能看到是哪个类、哪个方法在触发加密逻辑然后顺着栈往上追业务代码就行了。这个方法比关键词搜索效率高很多因为它直接定位运行时真实调用关系不会被混淆代码和死代码干扰。第三种是全量枚举法也是自动化绕不开的一环。很多大型App会使用代码混淆、字符串加密、花指令等方式隐藏加密逻辑静态搜关键词基本失效。这时候要换个思路在运行时用Frida的反射枚举能力遍历所有已加载的类和方法过滤出包含“encrypt”“decrypt”“sign”“cipher”“aes”“des”“rsa”“md5”“hash”等特征的方法名。只要App运行到过相关逻辑方法就会被加载到内存枚举一定抓得住。Objection这个工具内部就是这套逻辑执行android hooking list activities或者配合自定义脚本扫描方法列表自动化程度非常高。2.3 从抓包到反编译再到动态验证的闭环流程我完整走一遍项目的时候很少直接上来就Hook而是先搭一个三层闭环的分析路径。第一步是抓包。这个环节Charles或者Burp Suite都行很多App做了证书校验那就在模拟器或真机上安装用户证书配合Frida绕过SSL Pinning。注意了绕过证书绑定不是用来干坏事的而是为了能看清App自己发出的明文请求结构确定哪些字段是动态生成、哪些字段是静态常量。第二步是反编译静态分析用jadx打开APK梳理出可疑的加密入口和密钥装配逻辑。第三步是动态验证针对静态分析找到的目标函数写Frida脚本打印输入输出确认猜测是否正确。这三步形成一个闭环抓包发现密文字段反编译找到加密代码Hook验证加密过程验证完再回抓包看结果是否对得上。这个闭环流程最关键的点在于“交叉验证”。有时候反编译看到的代码路径不一定被执行有时候抓包看到的密文并不是你以为的那个函数生成的。只有把抓包、静态、动态三者统一起来才能100%确认加密函数的真实逻辑。否则很容易被字符串搜索引入歧途对着一个根本不会被调用的死代码分析半天浪费时间。3. 实战Java层加密函数Hook全流程3.1 Hook MD5哈希函数的完整脚本多数App的签名逻辑第一站就是MD5或SHA系列这类消息摘要算法在Java层的入口非常固定都是java.security.MessageDigest类。我写了一个通用脚本直接打印消息输入和摘要输出作为所有项目的起手式Java.perform(function () { var MessageDigest Java.use(java.security.MessageDigest); MessageDigest.update.overload([B).implementation function (input) { var dataStr bytesToHex(input); console.log([MD5/update] invoke); console.log( input( input.length ) dataStr); return this.update(input); }; MessageDigest.digest.overload().implementation function () { var result this.digest(); console.log([MD5/digest] bytesToHex(result)); return result; }; function bytesToHex(bytes) { var hex ; for (var i 0; i bytes.length; i) { var b bytes[i] 0xff; hex (0 b.toString(16)).slice(-2); } return hex; } });这里有两个非常容易踩的坑需要提醒。第一MessageDigest.update有多种重载比如update(byte[] input, int offset, int len)你只Hook了最基础的[B版本其他重载调用时打印不到建议把所有重载都覆盖。第二bytesToHex里必须做 0xff处理Java的byte是有符号类型直接转十六进制会出现一堆ffffff80这种诡异输出新手排查半天不知道怎么回事。实际使用中我发现绝大多数App的MD5签名逻辑会多次调用update分多次输入不同的拼接片段最后统一digest。这就意味着日志里可能有十几条update记录真正关键的是每个update的输入顺序和内容。所以要重点关注“update的数据是哪里来的”——如果是从业务字段直接拼接过来的那签名规则就基本摸清了。3.2 Hook AES对称加密的进阶写法AES在Java层的核心类是javax.crypto.Cipher它的运作流程是先通过init方法传入加解密模式和密钥然后调用update或doFinal执行实际数据转换。要完整还原一个AES加密流程必须同时抓到这两个环节。Java.perform(function () { var Cipher Java.use(javax.crypto.Cipher); Cipher.init.overload(int, Java.use(java.security.Key)) .implementation function (opmode, key) { console.log([Cipher.init] opmode opmode); console.log( key class key.getClass().getName()); if (opmode 1) { console.log( [ENCRYPT_MODE] Initialized for encryption); } else if (opmode 2) { console.log( [DECRYPT_MODE] Initialized for decryption); } return this.init(opmode, key); }; Cipher.update.overload([B, int, int).implementation function (input, offset, len) { var dataHex bytesToHex(input); console.log([Cipher.update] input( len ) dataHex); var ret this.update(input, offset, len); if (ret ! null) { console.log([Cipher.update] out bytesToHex(ret)); } return ret; }; Cipher.doFinal.overload([B).implementation function (input) { var dataHex bytesToHex(input); console.log([Cipher.doFinal] input( input.length ) dataHex); var ret this.doFinal(input); console.log([Cipher.doFinal] out bytesToHex(ret)); return ret; }; });光看算法还不够AES真正值钱的是密钥。你可以写一个额外的Hook把javax.crypto.spec.SecretKeySpec的构造函数也罩住这样当App执行到密钥装配代码时你能直接看到密钥的字节内容var SecretKeySpec Java.use(javax.crypto.spec.SecretKeySpec); SecretKeySpec.$init.overload([B, java.lang.String).implementation function (keyBytes, algorithm) { console.log([SecretKeySpec.$init] algorithm algorithm); console.log([SecretKeySpec.$init] key bytesToHex(keyBytes)); return this.$init(keyBytes, algorithm); };有了密钥和算法参数再配合你抓到的密文理论上就能手工复现整个加密过程。即使密钥是动态生成的Hook记录也能告诉你每个会话密钥什么时候生成、基于什么种子生成顺藤摸瓜就能找到密钥交换协议。3.3 参数处理、返回值读取与类型转换避坑Java层Hook最常见的几个类型转换坑我集中说一遍。第一个是byte数组的打印。Java层函数的实参如果是[B类型Frida里拿到的是Java对象引用不能直接用JavaScript的数组方法必须通过索引去取。我上面的bytesToHex函数就是最朴素的写法实测稳定。有人喜欢用Java.array(byte, input)把Java数组转成JS数组再用遇到大数组时性能很差尤其是加解密函数处理几KB数据时一秒钟可能转几十次直接拖慢目标App甚至导致超时。建议直接用索引访问。第二个是byte[]和String之间的编码问题。App传参时如果是String类型Frida里可以直接调用myString.toString()拿到Java层的字符串原始内容但如果拿到的是byte数组别盲目转字符串因为加密函数处理的数据往往已经是二进制密文直接按UTF-8解码会得到一堆乱码和替换符正确的做法是统一以十六进制字符串形式输出后续要用时再转换。第三个是重载匹配问题。Java方法可以有多个签名Frida的overload必须严格匹配。我的经验是先通过Jadx查找目标方法的完整签名再在Frida脚本里精确声明。如果漏掉了某个重载它的调用会直接穿透你的Hook表现为“明明打了Hook但日志里只出现了一半记录”。排查时可以用Cipher.update.overloads来查看所有重载声明再逐一覆盖。第四个是返回值类型。Cipher.update和doFinal返回的是byte数组需要打印但有些重载接收byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset返回值是int表示输出长度这种情况下不能只打印返回值必须打印传入的output缓冲区因为真正的密文写在那个缓冲区里。很多人在这个点上栽过返回值打印了半天都是个数字加密结果永远看不到。4. 实战Native层加密函数的自动化Hook4.1 定位so库导出函数与内部函数Java层Hook再牛也架不住App把核心加密逻辑下沉到native层。很多大厂App会在so库里用OpenSSL或者自己封装的加密算法Java层只传进字节流所有关键计算都在C/C层完成。这种情况下需要先把目标so库加载进内存然后用Frida的Moudle API去定位关键函数位置。第一个手段是导出表查询。直接用Module.findExportByName(libxxx.so, EVP_EncryptUpdate)适用于目标so没有做导出表混淆的常规情况。但很多商业App会在编译时用visibility属性隐藏内部符号导出表里只有JNI_OnLoad、Java_xxx这类入口函数真正干活的自定义函数一个都看不见。这时就需要用Module.enumerateSymbols()枚举所有符号先摸一下so库里到底暴露了什么再决定后续方案。第二个手段是内部函数手工定位。静态反编译so库文件用IDA或者Ghidra找到目标函数相对于so基址的偏移量然后运行时用基址加偏移算出真实内存地址再Interceptor.attach到该地址上。这个方法在分析自定义魔改加密算法时几乎是唯一选择因为函数名都被strip掉了你只能通过交叉引用和字符串定位它的位置。有一次我分析一个游戏App的通信协议加密整个so库只有一个导出函数Java_com_game_xxx_nativeRSA实际内部至少有三层自定义编码就是靠Ghidra一个个函数盯出来的。第三个手段是JNI动态注册的Hook。现在很多App会在JNI_OnLoad里用RegisterNatives动态注册Java层和native层的对应关系这种情况下Java_com_xxx_这种静态导出名字根本不存在。对策是Hook住RegisterNatives本身当你调用目标Java方法时Frida就能在运行时打印出它实际对应的native函数指针拿到指针后再Interceptor.attach即可。4.2 用Interceptor对EVP_EncryptUpdate做批量拦截如果你确定目标App走的是OpenSSL加密链路那libssl和libcrypto库里的函数就是天然的Hook锚点。OpenSSL的加密入口是EVP_EncryptUpdate和EVP_DecryptUpdate它们在加解密数据时会被反复调用非常适合做批量拦截。下面是我常用的一段模板脚本var sslModule Process.findModuleByName(libssl.so); if (sslModule) { var encUpdate Module.findExportByName(libssl.so, EVP_EncryptUpdate); if (encUpdate) { Interceptor.attach(encUpdate, { onEnter: function (args) { var inBuf args[1]; var inLen args[3].toInt32(); console.log([EVP_EncryptUpdate] inLen inLen); var data Memory.readByteArray(inBuf, inLen); console.log(hexdump(data, { offset: 0, length: inLen, header: true, ansi: true })); }, onLeave: function (retval) { console.log([EVP_EncryptUpdate] ret retval); } }); console.log([] EVP_EncryptUpdate hooked); } }这里有个重要的细节OpenSSL的EVP_EncryptUpdate是缓冲型API数据有可能被拆分成多次调用也有可能多次调用凑齐一整块才做一次加密。所以在分析时不要只看单条调用记录要关注同一会话内连续多次调用之间的数据关系。配合EVP_EncryptInit_ex的Hook你还能拿到每次加密会话使用的密钥和IV。把这两组日志放在一起对照AES-CBC这类模式的加密基本透明。还有一个性能问题要提。如果你拦截的是高频调用比如每帧都加密的游戏协议hexdump的日志量会非常巨大容易导致App卡顿。建议在生产分析场景下先只打印长度和哈希特征比如计算输入数据的MD5值快速识别出哪些数据块是固定结构、哪些是动态变化的再针对关键数据块用完整hexdump二次分析。4.3 魔改算法的自动化定位思路市面上很多App对AES、RC4这类标准算法做了“魔改”比如改了S盒、改了轮数、改了初始向量拼接方式。这种时候直接找标准函数签名是找不到的因为它们根本没有使用标准库而是在so里自己写了一套实现。我的定位思路分三步。第一步在Java层用Cipher实现自己的加密调用在native层用字符串匹配或者交叉引用找到调用加密逻辑的发起函数通常Java层的入口点会有一个对应的native方法。第二步从native方法出发在Ghidra中追踪它的调用链找到循环结构和大量位运算、查表操作的代码块这些都是对称加密算法的强特征。第三步用Frida在可疑代码块的入口和出口分别打上日志比对输入输出和已知明文之间的差异反推出算法的具体变换规则。举例来说如果识别到某个函数每次处理16字节数据块内部有多个轮次的轮换、异或、S盒查表几乎可以肯定是类AES结构只是细节被魔改了。你可以用Frida动态修改S盒数组的值来验证猜测——如果修改后输出与标准AES一致说明你的理解是正确的。这种验证思路比纯静态逆向效率高一大截毕竟是动态环境改一个字节、跑一次函数、看结果远比在几百KB的so库里翻汇编来得快。自动化方面这种分析模式完全可以脚本化跑一轮输入输出样本算差值和特征再自动打点验证。熟练之后就算面对完全陌生的自定义加密算法也能在两三个小时内还原出基本结构。5. 搭建自动化Hook体系的思路与落地5.1 frida-trace与Objection的自动化三板斧单个脚本手动hook是基本功但实际项目里你不可能每次都手动写脚本。到我手里的目标App更新频繁上一版本的加密逻辑和新版本不一定一样每次都要重新分析。所以从最初我就开始搭建自动化的Hook套件。第一板斧是frida-trace它是Frida官方自带的批量跟踪工具一条命令就能监听一类函数调用frida-trace -U -m -[NSURL* requestWithURL:*] com.demo.appAndroid端可以监听Java方法frida-trace -U -i AES* -i MD5* com.demo.app-i参数支持通配符匹配相当于把一类加密相关的方法全部挂上探针自动打印调用参数和返回值。这个工具适合第一轮信息收集让你快速看到App运行时实际调用了哪些加密函数、调用频率和顺序如何。缺点是日志不够结构化需要配合后续精加工。第二板斧是Objection。这个工具基于Frida封装了一整套移动端动态检测能力我最常用的是android hooking list classes和android hooking list class java.security.MessageDigest。它可以在不写一行代码的情况下快速枚举类、枚举方法、hook方法。尤其适合那些Java层代码混淆严重、静态搜索无果的场景Objection的运行时枚举几乎不受混淆影响因为你枚举的是JVM实际加载的内存对象和方法表。第三板斧是我自己写的通用Hook脚本模板库。我把MD5、SHA、AES、RSA、DES、Base64这些常用算法的Hook脚本整理成单独文件按需加载。分析新App时先用frida-trace全局扫一遍再针对命中的算法用对应模板脚本做深度hook整个过程不超过五分钟。这套模板库我还写了注释和参数说明每次接手新项目都能直接复用省去大量重复劳动。5.2 用Python驱动批量自动化分析Frida最强大的地方在于它的Python绑定。你可以用Python脚本控制Frida启动、注入、接收日志、保存结果这意味着整个分析流程完全可以程序化。我在实际项目中写过一个批量分析框架核心流程是这样的import frida import sys import os def on_message(message, data): if message[type] send: print([HookLog] message[payload]) else: print([Error] str(message)) def analyze_apk(package_name, script_path): device frida.get_usb_device(timeout5) session device.attach(package_name) with open(script_path, r, encodingutf-8) as f: script_code f.read() script session.create_script(script_code) script.on(message, on_message) script.load() return session, script def kill_and_restart(package_name): os.system(fadb shell am force-stop {package_name}) os.system(fadb shell am start -n {package_name}/.MainActivity) if __name__ __main__: target sys.argv[1] if len(sys.argv) 1 else com.demo.app script_path sys.argv[2] if len(sys.argv) 2 else hooks/base_hooks.js session, script analyze_apk(target, script_path) kill_and_restart(target) print(f[] Attached to {target}, waiting for actions...)这个框架做到了三件事。第一自动挂到目标进程即使App做了多进程架构也能按进程名分别attach。第二脚本热加载分析过程中想换一组Hook函数不用断开会话直接调用script.load()重新加载即可。第三消息回调所有JavaScript侧的日志通过send发回Python端Python端统一记录到文件或标准输出方便后续程序化处理。批量场景更有用。比如你有一个目录放了10个待分析的App每个都要先扫一遍加密函数分布那就可以写一个外层循环依次启动每个App、attach、跑30秒通用Hook日志、保存结果、下一个。全程无人值守输出结构化日志文件。这个能力在处理防火墙类应用或SDK轮询检测时特别有价值——你可以一次性把所有加密调用点全部标记出来画出一张完整的加密调用拓扑图。5.3 输出结果处理与长期复用自动化Hook如果不做结果沉淀那就是白忙一场。我踩过这个坑第一次分析完某个App日志文件存在电脑桌面上几个月后项目复盘再回来找发现文件名是log_v3_final_2.log内容乱成一锅粥根本没法复用。现在我的做法是严格分级输出。第一级是原始日志来自Frida消息回调按时间戳和进程名分目录保存不做任何加工。第二级是结构化的Hook调用记录解析原始日志后提取关键字段——函数名、入参、出参、调用栈顶部两层——写入JSON或SQLite。第三级是结论性报告手写的加密流程说明、密钥获取路径、签名规则复现步骤。这三份数据分别对应三个使用场景复盘排查、批量搜索特征、团队知识沉淀。[ { timestamp: 2025-01-12 14:23:55.123, process: com.demo.app, class_method: javax.crypto.Cipher.doFinal([B), operation: ENCRYPT, algorithm: AES/CBC/PKCS5Padding, input_hex_length: 48, output_hex_length: 64, key_hex: e1a2b3c4d5e6f708192a3b4c5d6e7f80, iv_hex: 000102030405060708090a0b0c0d0e0f, call_stack_top: com.demo.core.crypto.CryptoHelper.encrypt(Native Method) - com.demo.network.RequestBuilder.sign(RequestBuilder.java:128) } ]长期复用的关键是不要只存结果要把“当时怎么分析出来的”这个过程也记录下来。我通常在每个项目的分析目录里放一个ANALYSIS_NOTES.md里面记录分析顺序、用过的Hook脚本版本、关键实验现象和判断依据。几个月后回来看等于拿到一本自己写的“解密日记”省下的时间远远大于记录的时间。6. 常见问题与排查技巧实录6.1 崩溃、闪退、直接exit的经典故障Frida使用过程中遇到最多的就是注入后目标App当场崩溃。这里面的原因五花八门但我在实际操作中总结出了几个高频触发点。第一个是frida-server和frida客户端版本不匹配。这是最常见也最基础的原因症状是attach成功后脚本还没执行App就挂掉或者设备直接断开连接。解决办法很简单用pip show frida查看电脑端版本然后去GitHub下载完全相同版本的frida-server覆盖设备上的二进制文件并重启。第二个是Hook了不该Hook的critical method。Frida官方文档里明确说了某些平台或JVM内部的方法比如ClassLoader相关的关键路径不能随便Hook一旦Hook就会触发ART运行时崩溃。我踩过一次Hook掉了java.lang.String的某个内部方法设备直接重启。经验是优先Hook业务层面的方法不要动JDK核心类的底层实现。第三个是多线程并发打印导致的崩溃。当Hook的函数被多个线程同时调用而你的日志打印逻辑里做了字符串拼接或文件写入Java层的console.log加上Python端的消息回传可能堆积成山最终导致ANR或内存溢出。对策是在脚本内部直接用简单的send方法发送消息不要做复杂的日志格式转换高频日志适当做采样比如只打印每秒前20条。6.2 Hook不上、方法找不到的排查路径“Hook不上”这个说法其实很笼统我把它拆成几种具体场景来讲。场景一方法签名写错了。Frida的overload对签名非常严格参数类型不匹配就直接抛异常。排查方法很简单在控制台打印目标类的所有方法列表以及每个方法的参数签名和返回类型确认后再写overload。这个操作在Objection里就是一条命令的事。场景二目标类还没有被加载。App的某些加密类可能只在特定业务路径下才会被ClassLoader加载App刚启动时你的脚本就尝试Java.use()自然找不到类。解决办法是给脚本加上等待逻辑或者主动触发目标业务路径比如登录、支付来确保类加载。也可以在Android的ClassLoader层次上做Hook等目标类加载完成后再执行Java.use()。Frida提供了Java.perform结合setTimeout的方式来做延迟初始化我一般在分析脚本开头预留一个可选延迟参数。场景三目标方法被混淆/内联/抽取。如果是代码抽取加固比如梆梆、爱加密这类加固壳方法体在运行前是加密存放的静态反编译看不到真实代码运行时Hook也要先等方法被decode到内存。这种情况下单纯Hook业务方法会失败需要先处理脱壳或等待。但如果是混淆改名方法名变了但类结构还在就可以通过类名和参数特征来匹配或者在运行时枚举方法名再做模糊筛选。场景四目标函数被inlineHook或反Frida检测保护。现在主流App几乎都有不同强度的Frida检测检测到注入后要么直接退出要么瘫痪功能。这个属于对抗范畴主流做法是用各种方式隐藏注入痕迹或延迟注入时机。我只能说这确实是一场持续的攻防博弈脚本尽量精简、降低注入特征能争取到更大的分析窗口。这里不展开讲具体绕过手段合规使用是底线。6.3 模拟器、真机、内核版本组合的坑我最后想说一下运行环境的选择。模拟器开发调试非常方便但加密分析场景下真机和模拟器的行为差别可能很大。模拟器的问题集中在架构上。像雷电模拟器默认是x86架构跑的是x86_64的Android系统而市面上的大多数App是ARM指令集的APK包运行在x86模拟器上会通过二进制翻译执行。二进制翻译后native层的函数地址和内存布局与真机有差异某些Hook点可能失效或行为诡异。所以我的建议是能做真机分析尽量用真机尤其是native层加密逻辑真机的arm64环境下分析结果最可靠。如果确实只有模拟器优先选择官方提供的ARM镜像或者雷电模拟器里安装ARM转换组件能大幅减少native层分析中的异常行为。系统版本方面也有讲究。Android 7及以上对JNI Hook和ClassLoader机制的约束更严格Android 10起对native内存访问做了更多限制Android 14的某些版本连ptrace都加了限制。如果你工作用的测试机是Android 13以上遇到莫名奇妙的Hook失败先怀疑是不是系统版本限制换一台Android 9或10的设备试试。我自己的主力分析设备就是一台Android 9的Pixel系统版本老却反而稳定能覆盖市面上绝大部分App的兼容性。另外一个容易被忽略的点是CPU架构选择。真机找arm64版本没错但很多模拟器上安装的frida-server是x86_64的如果App内部有so库的arm64版本和x86版本之分你Hook的so库可能是x86翻译过来的地址偏移与arm64不同。所以在模拟器上做native分析前先确认目标App加载的so库文件路径用adb shell cat /proc/pid/maps看一下实际加载的是哪个库再决定Hook策略。排查这些问题时我还有个习惯每遇到一个新环境组合先跑一遍最简单的“Hello World”级Hook脚本比如Hook一下java.lang.System.currentTimeMillis确认注入、脚本执行、日志回传三个环节都正常工作然后再上复杂分析脚本。这个简单的冒烟测试能帮你快速区分“环境问题”和“分析问题”省下大量定位时间。最后再分享一个长期项目中特别有用的小技巧给常用脚本做版本管理。Frida脚本更新迭代快同一个App的不同版本需要不同脚本同一个功能脚本也可能要针对不同环境微调。我一开始不重视这个后来发现脚本散落各处改来改去自己都分不清哪个能跑。现在所有生产脚本都纳入git管理每个脚本头部写了适用场景、依赖环境和测试日期。每次分析前Pull一下最新代码跑完确实有改进再Push上去。这套“脚本即代码”的流程配合Frida本身的动态能力基本上是我这几年移动端加密分析效率提升最大的一个变革点。
返回列表