1. 项目概述为什么逆向工程师需要掌握Frida与加密算法分析在移动应用和桌面软件的安全评估、漏洞挖掘以及协议分析领域加密算法的逆向分析一直是个硬骨头。无论是App与服务器通信的HTTPS流量还是本地存储的加密数据AES、DES、RSA这些算法就像一道道坚固的锁。传统的静态分析面对混淆后的代码和复杂的密钥管理逻辑常常让人无从下手。这时候Frida这个动态插桩框架就成了我们手中的“万能钥匙”。它允许我们在目标进程运行时动态地注入JavaScript代码去窥探、修改甚至劫持函数的执行流程和内存数据。对于逆向工程师来说掌握用Frida分析加密算法意味着你能直接看到明文和密文转换的瞬间定位密钥生成逻辑甚至绕过整个加密流程这远比静态反编译猜测要高效和准确得多。这篇文章就是为你——无论是刚入门的逆向爱好者还是有一定经验但想系统提升加密分析能力的安全研究员——准备的一份实战指南。我不会只讲理论而是会结合具体的场景比如分析一个社交App的发帖API或者一个支付SDK的签名过程带你一步步拆解如何使用Frida来对付AES、DES和RSA。你会学到如何定位加密函数、如何Hook关键参数、如何导出密钥以及如何应对常见的反调试和检测机制。整个过程就像在程序的运行时世界里进行一次精密的外科手术。2. 核心思路与工具准备构建你的动态分析环境2.1 为什么选择Frida进行加密算法分析在逆向工程中分析加密算法无非几个目标识别算法类型、获取加密密钥、理解加密模式、最终实现加解密过程的复现或绕过。静态分析工具如IDA Pro、Ghidra能帮你理清代码逻辑但面对运行时生成的密钥、来自服务器的动态盐值、或者高度混淆的算法实现时就显得力不从心。Frida的核心优势在于“动态”和“实时”。它不需要你完全理解所有代码只需要找到加解密函数的入口点然后挂上钩子Hook就能在函数被调用时捕获到最关键的输入明文、密钥、IV和输出密文。举个例子一个App使用AES-CBC模式加密用户数据。静态分析你可能找到了Cipher.getInstance(“AES/CBC/PKCS5Padding”)这行代码但密钥从哪里来IV是否固定这些在运行时才确定。用Frida Hook这个Cipher对象的init和doFinal方法你就能直接打印出密钥字节数组和IV向量一切一目了然。对于RSA你可以Hook密钥对的生成过程或者拦截使用公钥加密的环节直接拿到加密前的原始数据。这种“所见即所得”的能力是静态分析无法比拟的。2.2 基础环境搭建与避坑指南工欲善其事必先利其器。Frida的环境搭建看似简单但新手常在这里踩坑。整个环境分为两部分Frida服务端运行在目标设备上和Frida客户端你的分析主机。服务端安装对于Android最稳定的是下载对应架构arm, arm64, x86的frida-server文件通过adb推送到设备赋权并运行。这里第一个坑就是版本匹配。你的PC上的frida Python包版本必须与frida-server版本一致否则会出现连接失败。我习惯用pip install frida-tools来安装客户端然后用frida --version和下载的server文件版本核对。注意不要在Root过的真机上随意运行从不明来源下载的frida-server存在安全风险。建议在模拟器如Genymotion、Android Studio AVD或专用的测试机上操作。许多App会检测frida-server的默认端口27042或特征你可以通过frida-server -l 0.0.0.0:8080来更改监听端口。客户端脚本编写这是核心。你将主要使用JavaScript来编写注入代码。Frida提供了强大的API如Interceptor.attach用于Hook函数。我强烈建议在VSCode等编辑器中编写并利用Frida的frida-compile工具或简单的模板来管理代码。一个常见的误区是试图在Hook的回调函数里做太多复杂的同步操作这可能导致应用崩溃或Hook失败。记住回调函数里的代码应尽量快速、简单复杂的处理逻辑最好通过send()函数将数据发回给Python端处理。基础检查安装后在命令行执行frida-ps -U如果能列出设备上的进程列表说明连接成功。如果失败首先检查adb连接是否正常adb devices其次检查frida-server进程是否在设备上运行ps | grep frida最后检查防火墙或网络设置。3. 定位与Hook加密函数的通用方法论在动手Hook具体的AES或RSA之前我们必须先找到它们。茫茫代码海如何精准定位这需要结合静态分析和动态试探。3.1 静态分析寻找线索首先你需要对目标应用进行初步的静态分析。使用Apktool、Jadx-GUI针对Android或直接反编译二进制文件搜索一些特征字符串或类名。AES/DES相关搜索关键词如“AES”、“DES”、“Cipher”、“SecretKeySpec”、“IvParameterSpec”。在Java层加解密通常通过javax.crypto.Cipher类完成。找到Cipher.getInstance()的调用处就能看到算法和模式比如AES/CBC/PKCS5Padding。RSA相关搜索“RSA”、“KeyFactory”、“KeyPairGenerator”、“Cipher”、“RSAPublicKey”。特别注意Cipher.getInstance(“RSA/ECB/PKCS1Padding”)这样的调用。此外从网络库如OkHttp的拦截器或证书加载代码入手也常有收获。原生层Native如果加密在.so库中实现你需要用IDA Pro分析native代码。搜索字符串“AES_encrypt”、“RSA_public_encrypt”等OpenSSL函数名或者一些固定的魔数Magic Number。3.2 动态追踪与模糊Hook静态分析找到的可能是多个候选函数或者代码被混淆得面目全非。这时就需要动态追踪来验证和缩小范围。1. 堆栈跟踪法这是最有效的方法之一。写一个Frida脚本Hook你认为可能相关的函数甚至是所有Cipher类的doFinal方法然后在回调函数里打印堆栈信息Thread.backtrace。当你触发一个加密操作如点击登录时观察是哪个Hook被触发并通过堆栈回溯看到完整的调用链从而理解加密在业务逻辑中的上下文。这能帮你确认这个函数是否是你关心的以及它的上层调用者是谁。2. 参数监控法初步Hook后不要急于求成去拿密钥。先打印函数的参数args[0],args[1]...。对于Cipher.init方法第一个参数是操作模式加密1/解密2第二个参数就是密钥Key。观察这些参数是对象还是基本类型如果是对象尝试用Frida的Java.cast将其转换为具体的类如SecretKeySpec然后访问其内部的字节数组字段。3. 模糊Hook与Stalker对于高度混淆或自实现的算法你可能连函数名都找不到。Frida的Stalker功能可以跟踪一个线程的指令执行虽然开销大且会产生海量数据但在绝望时是最后一根稻草。你可以先通过其他方式如监控文件读写、网络流量定位到大概的代码区域然后在该区域使用Stalker进行细粒度跟踪分析其指令模式是否与标准加密算法如AES的查表操作相似。实操心得定位过程往往是“静态分析 - 动态验证 - 调整思路 - 再次静态分析”的循环。不要指望一次成功。我通常会准备一个“侦察脚本”里面预置了多个常见加密函数和系统API的Hook通过frida -U -f com.target.app -l scout.js --no-pause启动应用并加载脚本然后在App内进行操作观察控制台输出快速筛选出目标。4. 实战剖析Hook AES加解密的多种姿势AES高级加密标准是目前应用最广泛的对称加密算法。在移动端它常被用于本地数据加密如SharedPreferences、网络请求体加密或资源文件加密。下面我们针对不同场景拆解几种典型的Hook方法。4.1 场景一Hook Java层的Cipher类这是最常见、最直接的情况。Android应用使用Java Cryptography Architecture (JCA) 进行加解密。Java.perform(function () { var Cipher Java.use(javax.crypto.Cipher); // Hook Cipher.getInstance() 来了解使用了什么算法和模式 Cipher.getInstance.overload(java.lang.String).implementation function (transformation) { console.log([] Cipher.getInstance called: transformation); var result this.getInstance(transformation); // 可以在这里把返回的Cipher对象实例也Hook住关联起来 return result; }; // Hook Cipher.init() 来获取密钥和IV Cipher.init.overload(int, java.security.Key, java.security.spec.AlgorithmParameterSpec).implementation function (opmode, key, params) { console.log(\n[] Cipher.init called:); console.log( OpMode: opmode (1ENCRYPT_MODE, 2DECRYPT_MODE)); // 获取密钥 if (key) { var keyBytes key.getEncoded(); console.log( Key Hex: bytesToHex(keyBytes)); console.log( Key Base64: bytesToBase64(keyBytes)); } // 获取IV如果存在 if (params) { var IvParamSpec Java.use(javax.crypto.spec.IvParameterSpec); if (params.getClass().getName().indexOf(IvParameterSpec) ! -1) { var iv Java.cast(params, IvParamSpec); var ivBytes iv.getIV(); console.log( IV Hex: bytesToHex(ivBytes)); } } return this.init(opmode, key, params); }; // Hook Cipher.doFinal() 来获取输入数据和输出结果 Cipher.doFinal.overload([B).implementation function (input) { console.log(\n[] Cipher.doFinal called with input:); console.log( Input Hex: bytesToHex(input)); // 先调用原函数获取结果 var result this.doFinal(input); console.log( Output Hex: bytesToHex(result)); // 尝试解码为字符串方便阅读 try { var inputStr bytesToString(input); var outputStr bytesToString(result); console.log( Input as String: inputStr); console.log( Output as String: outputStr); } catch(e) {} return result; }; // 一些工具函数 function bytesToHex(bytes) { return Array.from(bytes, byte (0 (byte 0xFF).toString(16)).slice(-2)).join(); } function bytesToBase64(bytes) { return Java.use(android.util.Base64).encodeToString(bytes, 0); } function bytesToString(bytes) { return String.fromCharCode.apply(null, bytes); } });这个脚本能帮你捕获算法类型、操作模式、密钥、IV、输入明文/密文和输出密文/明文。关键点在于init方法可能被重载多次我们Hook的是最常见的一个。你需要根据实际情况调整overload签名。另外密钥对象可能是SecretKeySpec通过getEncoded()就能拿到原始字节。4.2 场景二处理自定义KeyGenerator或密钥派生有些应用不会硬编码密钥而是通过KeyGenerator、PBKDF2基于口令的密钥派生或从服务器获取种子来动态生成。这时你需要扩大Hook范围。// Hook KeyGenerator来获取生成的密钥 var KeyGenerator Java.use(javax.crypto.KeyGenerator); KeyGenerator.getInstance.overload(java.lang.String).implementation function (algorithm) { console.log([] KeyGenerator.getInstance: algorithm); var kg this.getInstance(algorithm); // 也可以进一步Hook kg.generateKey()方法 return kg; }; // Hook SecretKeySpec的构造函数这是密钥被包装的常见地方 var SecretKeySpec Java.use(javax.crypto.spec.SecretKeySpec); SecretKeySpec.$init.overload([B, java.lang.String).implementation function (keyBytes, algorithm) { console.log(\n[] SecretKeySpec created:); console.log( Algorithm: algorithm); console.log( Key Bytes Hex: bytesToHex(keyBytes)); return this.$init(keyBytes, algorithm); };如果应用使用PBEKeySpec口令加密你需要去Hook相关类。思路是一样的找到密钥材料口令、盐、迭代次数变成最终密钥的地方。4.3 场景三深入Native层Hook OpenSSL的AES函数高性能或核心的加密逻辑可能放在Native层使用OpenSSL或BoringSSL库。这需要一些C/C的知识。假设你已经通过静态分析在libcrypto.so中找到了AES_encrypt函数。// 首先枚举模块找到libcrypto.so的基地址 var libcrypto Process.getModuleByName(libcrypto.so); console.log(libcrypto base: libcrypto.base); // 计算目标函数的绝对地址。你需要知道函数在so中的偏移量通过IDA查看 // 例如AES_encrypt的偏移量是0x12345 var aes_encrypt_addr libcrypto.base.add(0x12345); // 使用Interceptor.attach Hook这个Native函数 Interceptor.attach(aes_encrypt_addr, { onEnter: function (args) { // AES_encrypt签名: void AES_encrypt(const unsigned char *in, unsigned char *out, const AES_KEY *key) // args[0]: in (明文输入指针) // args[1]: out (密文输出指针) // args[2]: key (AES_KEY结构体指针) console.log(\n[] AES_encrypt called); // 读取输入明文AES块大小16字节 var in_ptr args[0]; var input in_ptr.readByteArray(16); console.log( Plaintext Hex: bytesToHex(input)); // 读取AES_KEY结构体中的轮密钥rd_key来推测原始密钥 // 注意AES_KEY结构复杂直接提取原始密钥需要理解其内存布局这里只是示例 var key_ptr args[2]; // 假设轮密钥数组在结构体开头对于AES-128有11个轮密钥每个4字节 // 这只是一个演示实际提取需要精确的偏移量 // var round_key key_ptr.readByteArray(176); // AES-128: 11*16176字节 // console.log( Round Key (partial): bytesToHex(round_key.slice(0, 16))); // 只看第一轮 }, onLeave: function (retval) { // 函数执行后输出缓冲区已经被写入密文 // 但我们在onEnter时没有保存输出指针这里无法直接读取。 // 更好的做法是在onEnter保存args[1]到this.context中。 } });Hook Native函数难度更大你需要准确知道函数签名和参数含义。理解参数的数据类型指针、结构体并正确读取内存。找到正确的函数偏移量。可以通过导出函数名AES_encrypt直接解析如果被去符号表了就需要通过特征码或偏移来定位。注意事项直接Hook像AES_encrypt这样的底层函数可能会因为函数被内联调用或编译器优化而失败。有时Hook更高层的函数如OpenSSL的EVP_EncryptUpdate会更稳定。另外频繁的Native Hook可能影响程序稳定性建议在关键点触发时再启用。5. 实战剖析Hook RSA加解密与密钥管理RSA作为非对称加密算法的代表常用于密钥交换、数字签名和数据加密。在逆向中我们更关心如何获取公钥、拦截加密前的数据或者获取私钥进行解密。5.1 场景一拦截RSA公钥加密过程很多App在登录或提交敏感数据时会用固定的或从服务器获取的RSA公钥加密密码或对称密钥。Java.perform(function () { var Cipher Java.use(javax.crypto.Cipher); // 重点Hook RSA相关的Cipher初始化 Cipher.init.overload(int, java.security.Key).implementation function (opmode, key) { // 只关心加密模式且是RSA公钥 if (opmode 1) { // ENCRYPT_MODE var keyClass key.$className; if (keyClass.indexOf(RSAPublicKey) ! -1 || keyClass.indexOf(PublicKey) ! -1) { console.log(\n[] RSA Public Key Encryption Init Detected!); console.log( Key Class: keyClass); // 尝试获取公钥的模数(n)和指数(e) try { var rsaPubKey Java.cast(key, Java.use(java.security.interfaces.RSAPublicKey)); var modulus rsaPubKey.getModulus(); var exponent rsaPubKey.getPublicExponent(); console.log( Modulus (n) Hex: modulus.toString(16)); console.log( Public Exponent (e) Hex: exponent.toString(16)); // 可以将n和e保存下来用于后续构造公钥 } catch (e) { console.log( Failed to extract key details: e); } } } return this.init(opmode, key); }; // Hook doFinal捕获被公钥加密前的数据 Cipher.doFinal.overload([B).implementation function (input) { // 我们可以通过检查当前Cipher实例的算法来确认是否是RSA // 但更简单的方法在init时给这个实例打个“标记”这里为了简化我们直接打印所有doFinal的输入然后人工筛选 console.log([?] Cipher.doFinal input length: input.length bytes); if (input.length 128) { // RSA加密的输入通常较短受密钥长度限制 console.log( Input Hex: bytesToHex(input)); try { console.log( Input as String: String.fromCharCode.apply(null, input)); } catch(e) {} } var result this.doFinal(input); console.log( Output (Ciphertext) Hex: bytesToHex(result)); return result; }; });这个脚本能帮你捕获RSA公钥的组成部分n, e以及被加密的原始数据。有了n和e你就能在Python或其他工具中完全复现加密过程。关键点RSA加密的输入数据长度有限例如2048位密钥最多加密245字节明文通常用于加密一个随机生成的AES会话密钥。5.2 场景二Hook密钥对生成或私钥加载如果应用在本地生成RSA密钥对例如用于客户端签名或者从本地文件加载私钥这就是获取私钥的绝佳机会。// Hook KeyPairGenerator生成密钥对 var KeyPairGenerator Java.use(java.security.KeyPairGenerator); KeyPairGenerator.genKeyPair.implementation function () { console.log(\n[] KeyPairGenerator.genKeyPair called!); var keyPair this.genKeyPair(); var privateKey keyPair.getPrivate(); var publicKey keyPair.getPublic(); // 提取私钥信息PKCS8格式 if (privateKey.$className.indexOf(RSAPrivate) ! -1) { try { var rsaPrivateKey Java.cast(privateKey, Java.use(java.security.interfaces.RSAPrivateKey)); var modulus rsaPrivateKey.getModulus(); var privateExponent rsaPrivateKey.getPrivateExponent(); console.log( [PRIVATE KEY] Modulus (n): modulus.toString(16).substring(0, 64) ...); console.log( [PRIVATE KEY] Private Exponent (d): privateExponent.toString(16).substring(0, 64) ...); // 警告私钥极其敏感仅用于安全研究切勿泄露。 } catch(e) { console.log( Failed to extract private key details.); } } return keyPair; }; // Hook KeyFactory或KeyStore加载密钥 var KeyFactory Java.use(java.security.KeyFactory); KeyFactory.generatePrivate.implementation function (keySpec) { console.log([] KeyFactory.generatePrivate called with spec: keySpec.$className); // 可以进一步检查keySpec的内容例如PKCS8EncodedKeySpec包含了私钥的DER编码字节 if (keySpec.$className.indexOf(PKCS8EncodedKeySpec) ! -1) { var encoded keySpec.getEncoded(); console.log( PKCS8 Private Key DER (Base64): bytesToBase64(encoded)); } return this.generatePrivate(keySpec); };重要警告获取到的私钥是最高机密。你的分析环境必须是完全离线的并且所有研究成果不得包含真实的私钥。这些技术仅用于授权范围内的安全评估。5.3 场景三处理前端RSA加密如WebView在一些Hybrid App中前端JavaScript可能会用RSA加密数据后再提交。你可以通过Frida Hook WebView的JavaScript执行环境或者更简单地直接Hook Android WebView中用于JavaScript与Java交互的WebView.evaluateJavascript或JavascriptInterface注解的方法来捕获前端准备加密的数据。另一种思路是如果前端使用了固定的公钥你可以直接替换掉WebView加载的JavaScript文件将公钥替换成你自己生成的密钥对中的公钥从而解密所有通信。这属于更高级的中间人攻击思路需要结合其他工具。6. 实战剖析Hook DES/3DES算法DES和3DES虽然逐渐被AES取代但在一些老旧系统或特定协议中仍有出现。Hook思路与AES高度相似因为它们都通过JCA的Cipher类实现。// Hook DES/3DES的关键在于识别算法名称 Java.perform(function () { var Cipher Java.use(javax.crypto.Cipher); Cipher.getInstance.overload(java.lang.String).implementation function (transformation) { if (transformation.indexOf(DES) ! -1) { console.log([] DES/3DES Cipher detected: transformation); // 可以在这里保存这个transformation用于关联后续的init和doFinal this._transformation transformation; // 给实例添加自定义属性 } return this.getInstance(transformation); }; Cipher.init.overload(int, java.security.Key).implementation function (opmode, key) { if (this._transformation this._transformation.indexOf(DES) ! -1) { console.log(\n[] Initializing DES/3DES Cipher); console.log( Mode: (opmode 1 ? ENCRYPT : DECRYPT)); var keyBytes key.getEncoded(); console.log( Key Hex: bytesToHex(keyBytes)); console.log( Key Length: (keyBytes.length * 8) bits); // DES64位3DES192位 } return this.init(opmode, key); }; });DES的密钥是64位8字节但实际使用56位。3DES的密钥是192位24字节。通过Hook你可以轻松验证密钥长度是否符合预期。一个常见漏洞是开发者错误地使用了弱密钥或固定的IV通过Frida抓取几次加解密过程对比IV值就能快速判断。7. 进阶技巧与对抗反调试7.1 处理复杂参数与对象结构有时你Hook到的参数是复杂的对象直接打印是[object Object]。你需要用Frida的Java.choose或反射来遍历其字段。// 假设你Hook到一个不明对象params Cipher.init.overload(int, java.security.Key, java.security.AlgorithmParameters).implementation function (opmode, key, params) { if (params) { console.log(AlgorithmParameters class: params.$className); // 使用反射获取其编码后的字节这通常包含了IV等信息 var encodedBytes params.getEncoded(); console.log(Parameters encoded: bytesToHex(encodedBytes)); } return this.init(opmode, key, params); };7.2 绕过Frida检测越来越多的应用会检测Frida的存在常见手段包括检测端口扫描27042等默认端口。检测进程名查找frida-server、frida-helper等进程。检测文件特征检查/proc/self/maps或/proc/self/task/.../status中是否包含frida相关字符串。检测线程名Frida会创建特定名称的线程。对抗策略修改端口启动frida-server时使用非默认端口./frida-server -l 0.0.0.0:8080连接时指定端口frida -H 192.168.1.5:8080 ...。重命名与隐藏将frida-server文件重命名为一个不起眼的名字如libandroid_servers并修改其内存映射中的字符串特征需要修改Frida源码重新编译门槛较高。使用定制工具使用如objection基于Frida这类工具它有时能绕过一些简单检测。Patch检测代码直接使用Frida去Hook应用自身的检测函数使其永远返回false。这需要你先逆向找到检测代码的位置。// 示例Hook一个常见的检测函数假设你通过逆向找到了它 var DetectionClass Java.use(com.example.security.AntiFrida); DetectionClass.checkFrida.implementation function () { console.log([] AntiFrida.checkFrida() called, returning false.); return false; // 永远返回未检测到 };7.3 性能优化与稳定Hook精准Hook尽量Hook具体的、重载的函数避免使用太宽泛的Hook如Hook所有java.lang.String的构造函数这会导致性能急剧下降和应用崩溃。延迟Hook有些类可能在Hook之后才被加载。使用Java.choose()或setImmediate()来确保在类加载后再执行Hook操作。错误处理在implementation函数内部做好try-catch避免因为意外的对象类型转换错误导致脚本崩溃进而使目标应用崩溃。8. 常见问题排查与实战心得在实际操作中你肯定会遇到各种问题。下面是一个快速排查表问题现象可能原因排查步骤与解决方案TypeError: cannot read property ‘$className’ of undefinedHook的时机不对目标对象还未初始化或为null。1. 确保Hook代码在Java.perform内。2. 使用Java.choose在堆上查找已存在的实例进行Hook。3. 检查函数重载overload签名是否完全匹配。Error: access violation accessing 0x...(Native Hook)传入的地址错误或内存不可读。1. 确认模块基地址和函数偏移量计算正确。2. 使用Process.getModuleByName()确保模块已加载。3. 在onEnter中谨慎使用readByteArray先检查指针是否有效。Frida连接被拒绝或超时frida-server未运行、版本不匹配、端口被占用或应用有反调试。1.adb shell进入设备psHook后应用闪退Hook的函数不稳定或在回调函数中执行了耗时操作。1. 简化Hook回调函数内的逻辑只做最简单的记录和发送。2. 尝试Hook更上层的函数。3. 使用setTimeout将复杂操作异步化。抓不到预期的函数调用函数名或类名因混淆而改变函数在Native层调用路径非预期。1. 使用更宽泛的Hook如Hook所有javax.crypto.Cipher的doFinal重载。2. 结合静态分析查看调用链尝试Hook更前端的业务函数。3. 使用frida-trace进行模糊跟踪。获取到的密钥全是0或乱码Hook的时机晚了密钥可能已经被清除或覆盖或者密钥对象不是预期的类型。1. 尝试Hook密钥生成或传入的更早环节。2. 打印对象的完整类名和所有字段进行调试。3. 检查是否在onLeave中才去读取数据而数据在函数执行中被修改。最后分享几点个人心得 第一耐心比技术更重要。逆向加密算法很少能一击即中需要反复猜测、验证、调整脚本。第二做好记录。每次Hook到的数据哪怕看起来是乱码也保存下来交叉对比不同操作下的数据变化往往能发现规律。第三理解业务逻辑。加密不是为了加密而加密它服务于“登录”、“支付”、“发帖”这些具体业务。从业务入口点开始分析往往比直接扎进加密库更容易找到突破口。第四工具是死的思路是活的。Frida只是工具之一结合Xposed、IDA动态调试、网络抓包Charles/Fiddler一起使用才能构建完整的分析能力。当你能够熟练运用Frida窥探运行时的一切秘密时那些看似坚固的加密堡垒在你面前也就有了清晰的脉络。