1. 项目概述:从AES加解密到Hook拦截的逆向工程实践
最近在搞一个安全评估项目,客户那边有个Android应用,核心业务逻辑里用了AES来加密一些本地的配置数据和通信报文。我们的任务不是破解它的密钥——那太粗暴了,而且不总是合法合规的。我们的思路是,能不能在应用运行时,“偷看”一下它解密后的明文是什么?这就引出了今天要聊的这个经典组合技:先实现一个完整的AES加解密流程,然后用Hook技术拦截它的解密函数,实时获取解密结果。这招在安全分析、逆向工程、甚至是自动化测试里都特别有用。比如,你想分析某个App的网络协议,但它数据全加密了,直接逆向算法可能很复杂,这时候动态Hook解密函数,往往能事半功倍。
简单来说,这个项目会分成两大块。第一块是“建设”,我们会用Java写一个标准、可运行的AES加解密示例,把密钥、加密模式、填充方式这些参数都固定下来,模拟一个真实的应用场景。第二块是“潜入”,我们会使用目前移动端逆向里最强大的动态插桩工具之一——Frida,来编写一个Hook脚本。这个脚本会像特工一样,潜伏到目标应用进程中,精确地在AES解密函数被执行的那一刻介入,把传入的密文、计算出的明文,甚至当时的调用堆栈都给打印出来,整个过程对应用来说几乎是透明的。
无论你是对加密算法实现感兴趣的开发者,还是想学习移动安全、逆向分析的安全研究员,甚至是需要处理加密数据的测试工程师,这套方法都能给你提供一个清晰的、可动手实践的路径。我们不会停留在理论,而是会一步步写出代码,看到实际效果。下面,我就把自己趟过路的详细过程、踩过的坑和总结的技巧,毫无保留地分享出来。
2. 核心原理与工具选型:为何是AES与Frida?
在动手敲代码之前,我们得先搞清楚两件事:第一,为什么选AES?第二,为什么用Frida来Hook?理解这些背后的“为什么”,能让你在遇到变种情况时,自己也能灵活应对。
2.1 AES加解密:对称加密的业界标杆
AES(Advanced Encryption Standard,高级加密标准)是目前应用最广泛的对称加密算法。对称加密的意思是,加密和解密用的是同一把密钥。它的特点是速度快,适合加密大量数据。在咱们这个示例里,我们选择AES,主要是因为它太常见了,你遇到的App十有八九都用它,搞明白它极具实战价值。
AES有几个关键参数需要确定,这直接决定了我们后续Hook时要找的目标:
- 密钥长度:可以是128位、192位或256位。128位(16字节)最常用,我们示例就用这个。
- 工作模式:这是决定如何用密钥和算法来加密数据块的方式。常见的有ECB、CBC、CFB等。ECB模式最简单,但安全性差,因为相同的明文块会加密成相同的密文块,容易暴露出数据模式。所以实际项目里多用CBC(密码块链接)模式,它需要一个额外的参数——初始化向量(IV),来确保即使相同明文加密出的密文也不同。我们的示例将采用更规范的CBC模式。
- 填充方式:AES是块加密算法,一次处理一个固定长度(128位,16字节)的数据块。如果明文长度不是16字节的整数倍,就需要填充。常用的是PKCS5Padding(在Java里叫PKCS7Padding,两者在AES语境下等价)。
在Java中,实现AES加解密通常使用javax.crypto.Cipher这个类。我们会像下面这样获取一个Cipher实例:Cipher cipher = Cipher.getInstance(“AES/CBC/PKCS5Padding”);这个字符串“AES/CBC/PKCS5Padding”就完整指定了算法、模式和填充。Hook的关键,就是要定位到执行这个Cipher.doFinal()方法的调用。
注意:不同平台、不同库的AES实现可能略有差异。比如在Android上,除了标准Java API,有些应用可能使用Bouncy Castle库或者自定义的Native代码(C/C++)来实现加密。我们的示例先从最普遍的Java层
Cipher类入手,这也是Frida最容易Hook的层次。
2.2 Hook技术与Frida:动态分析的瑞士军刀
Hook(钩子)技术,简单说就是在程序运行时,拦截并改变函数或API的执行流程。我们可以让它先执行我们的代码(比如打印参数),再执行原函数,或者干脆替换掉原函数。
为什么选择Frida?因为它有这几个碾压性优势:
- 跨平台:支持Android、iOS、Windows、macOS、Linux。我们搞Android,它是最佳选择。
- 无需修改目标应用:不像Xposed需要修改系统或应用,Frida通过注入一个JavaScript运行时到目标进程来实现Hook,对应用本身无侵入。
- 开发效率极高:用JavaScript写Hook脚本,比写C/C++的Native Hook代码快太多了。修改脚本后几乎可以实时重载,调试起来非常方便。
- 功能强大:不仅能Hook Java层函数,还能Hook Native层(SO库)的函数,内存搜索、调用堆栈查看都不在话下。
Frida的架构是C/S模式。我们在电脑上运行Python脚本(客户端),它负责启动Frida服务、上传Hook脚本。手机上(或模拟器里)运行着一个守护进程frida-server(服务端),负责注入和执行脚本。我们接下来的操作,就是写一个Python程序和一个JavaScript脚本。
工具准备清单:
- 一台已Root的Android手机或一台Android模拟器(如雷电模拟器):这是运行被测试应用和Frida服务端的基础。模拟器调试更方便,推荐初学者使用。
- 安装Frida:在电脑上通过pip安装:
pip install frida-tools。这会同时安装frida和frida-ps等命令行工具。 - 下载对应版本的frida-server:从Frida官网的GitHub Releases页面,根据你手机或模拟器的CPU架构(通常是
arm64或x86_64)和Android系统版本,下载对应的frida-server文件。比如frida-server-16.1.11-android-arm64.xz。 - 将frida-server推送到设备并运行:
# 解压下载的.xz文件得到可执行文件 # 推送至设备 adb push frida-server /data/local/tmp/ adb shell su cd /data/local/tmp chmod 755 frida-server ./frida-server & - 保持设备连接,在电脑终端运行
frida-ps -U,如果能看到设备上的进程列表,说明Frida环境搭建成功。
3. 构建靶子:一个完整的Java AES加解密示例
我们要Hook,首先得有个明确的目标。我们来写一个简单的Java控制台程序,它模拟一个应用的核心加密操作。为了后续Hook演示更清晰,我们会特意把加解密逻辑写在一个单独的类里。
3.1 核心加解密类实现
创建一个名为AESDemo.java的文件。
import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.util.Base64; public class AESDemo { // 定义固定的密钥和IV。在实际应用中,这些信息可能来自配置文件、网络或代码混淆。 // AES-128,密钥长度16字节 private static final String SECRET_KEY = “1234567890123456”; // CBC模式需要的初始化向量,长度16字节 private static final String INIT_VECTOR = “abcdefghijklmnop”; // 加密算法/模式/填充 的完整描述 private static final String TRANSFORMATION = “AES/CBC/PKCS5Padding”; /** * AES加密方法 * @param plainText 待加密的明文 * @return Base64编码后的密文字符串 */ public static String encrypt(String plainText) { try { // 1. 根据密钥字节数组和算法名称,生成SecretKeySpec对象 SecretKeySpec keySpec = new SecretKeySpec(SECRET_KEY.getBytes(“UTF-8”), “AES”); // 2. 根据IV字节数组,生成IvParameterSpec对象 IvParameterSpec ivSpec = new IvParameterSpec(INIT_VECTOR.getBytes(“UTF-8”)); // 3. 获取Cipher实例,指定算法/模式/填充 Cipher cipher = Cipher.getInstance(TRANSFORMATION); // 4. 初始化Cipher为加密模式,传入密钥和IV cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec); // 5. 执行加密,得到字节数组 byte[] encryptedBytes = cipher.doFinal(plainText.getBytes(“UTF-8”)); // 6. 将加密后的字节数组用Base64编码,方便传输和存储 return Base64.getEncoder().encodeToString(encryptedBytes); } catch (Exception e) { e.printStackTrace(); return null; } } /** * AES解密方法 * @param encryptedText Base64编码的密文字符串 * @return 解密后的明文字符串 */ public static String decrypt(String encryptedText) { try { SecretKeySpec keySpec = new SecretKeySpec(SECRET_KEY.getBytes(“UTF-8”), “AES”); IvParameterSpec ivSpec = new IvParameterSpec(INIT_VECTOR.getBytes(“UTF-8”)); Cipher cipher = Cipher.getInstance(TRANSFORMATION); // 初始化Cipher为解密模式 cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); // 先将Base64字符串解码成字节数组 byte[] encryptedBytes = Base64.getDecoder().decode(encryptedText); // 执行解密 byte[] decryptedBytes = cipher.doFinal(encryptedBytes); return new String(decryptedBytes, “UTF-8”); } catch (Exception e) { e.printStackTrace(); return null; } } /** * 一个模拟的业务方法,内部调用了解密逻辑。 * 这模拟了真实应用中,解密函数被层层调用的场景。 * @param encryptedData 加密的业务数据 * @return 处理后的业务结果(这里简单返回解密内容) */ public static String processSecureData(String encryptedData) { System.out.println(“[AESDemo] 开始处理加密数据...”); // 这里是业务逻辑,我们假设它最终需要解密数据 String result = decrypt(encryptedData); System.out.println(“[AESDemo] 数据处理完毕。”); return result; } }代码要点解析:
TRANSFORMATION字符串“AES/CBC/PKCS5Padding”是我们的核心标识,Hook时就要找使用这个字符串的Cipher.getInstance()调用或直接找decrypt方法。- 我们将加解密所需的
SECRET_KEY和INIT_VECTOR硬编码在类中。这在实际生产环境中是严重的安全隐患,但为了示例清晰,我们这样做。真实应用可能从服务器动态获取,或做白盒加密处理。 processSecureData方法模拟了一个业务场景,它内部调用了decrypt。这有助于我们演示如何Hook一个被间接调用的方法。
3.2 主程序与测试
再创建一个Main.java文件,用于测试我们的加解密类。
public class Main { public static void main(String[] args) { String originalText = “这是一段需要加密的敏感信息,比如用户令牌或者配置数据!”; System.out.println(“=== AES加解密演示 ===”); System.out.println(“原始明文:” + originalText); // 加密 String encryptedText = AESDemo.encrypt(originalText); System.out.println(“加密后 (Base64):” + encryptedText); // 直接解密 String decryptedText = AESDemo.decrypt(encryptedText); System.out.println(“直接解密结果:” + decryptedText); System.out.println(“\n=== 模拟业务调用 ==="); // 通过业务方法解密 String result = AESDemo.processSecureData(encryptedText); System.out.println(“业务方法解密结果:” + result); // 验证 if (originalText.equals(decryptedText) && originalText.equals(result)) { System.out.println(“\n✅ 加解密验证成功!”); } else { System.out.println(“\n❌ 加解密验证失败!”); } } }编译并运行这两个Java文件:
javac AESDemo.java Main.java java Main你应该能看到成功的加解密输出,这证明我们的“靶子”程序工作正常。记住输出的密文(Base64字符串),等下Hook的时候我们会用到它。
实操心得:在真实逆向中,你面对的往往不是这么清晰的
AESDemo.decrypt()调用。更常见的是在庞大的代码库中,一个Cipher对象在某个地方被初始化,然后在另一个地方调用doFinal。因此,我们的Hook思路也要灵活,既可以Hook具体的自定义方法(如decrypt),也可以Hook更底层的、通用的Android API(如Cipher.doFinal)。后者覆盖面更广。
4. 将靶子部署到Android环境
为了用Frida进行Hook,我们需要一个运行在Android环境下的目标。有两个选择:1. 将上面的Java代码改造成一个简单的Android App;2. 在Android设备上运行一个Java命令行程序。为了方便和通用,我们选择第一种,创建一个最简单的Android应用。
4.1 创建Android测试应用
使用Android Studio创建一个新的Empty Activity项目,语言选Java。在MainActivity中,我们做如下修改:
package com.example.aeshookdemo; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.util.Log; import android.widget.TextView; // 注意:需要将之前写的AESDemo类复制到这个项目中,或者将其代码内联。 import com.example.aeshookdemo.AESDemo; public class MainActivity extends AppCompatActivity { private static final String TAG = “HookDemo”; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); TextView tv = findViewById(R.id.sample_text); String originalText = “Hello from Android! 这是安卓端的秘密。”; Log.d(TAG, “原始明文:” + originalText); // 加密 String encryptedText = AESDemo.encrypt(originalText); Log.d(TAG, “加密后:” + encryptedText); tv.setText(“密文:” + encryptedText); // 延迟2秒后,模拟一个业务操作触发解密 tv.postDelayed(new Runnable() { @Override public void run() { Log.d(TAG, “触发业务逻辑处理...”); // 通过业务方法触发解密 String result = AESDemo.processSecureData(encryptedText); Log.d(TAG, “业务处理结果(明文):” + result); tv.append(“\n\n解密结果:” + result); } }, 2000); } }同时,确保AESDemo类被正确添加到项目中。布局文件activity_main.xml只需一个TextView即可。
关键点:我们在onCreate中加密一段文本并显示密文,然后通过一个延迟任务调用AESDemo.processSecureData来触发解密。这个延迟模拟了用户交互或网络回调等异步触发场景,让我们的Hook更有真实感。
4.2 编译安装与运行
连接你的已Root真机或启动安卓模拟器(确保adb已连接)。 在Android Studio中点击运行,或者使用命令行:
./gradlew installDebug adb shell am start -n com.example.aeshookdemo/.MainActivity查看Logcat,你应该能看到加密的日志,以及2秒后解密的日志。记下应用显示的密文字符串。
注意事项:如果目标应用发布的是Release版本(开启了混淆),那么类名和方法名可能会变成
a.a,b.b这种无意义的名字。这时,直接Hook类名AESDemo就会失败。我们需要通过分析APK,找到混淆后的实际类名和方法名。对于这个演示,我们运行的是Debug版本,类名方法名都是清晰的。
5. Frida Hook实战:拦截解密过程
环境准备好了,靶子也立起来了,现在轮到我们的“特工”——Frida Hook脚本上场了。我们将编写一个JavaScript脚本,注入到我们刚刚安装的App进程中。
5.1 Hook脚本编写思路
我们的目标很明确:拦截解密过程,获取输入(密文)和输出(明文)。有几个潜在的Hook点:
- Hook我们自定义的
AESDemo.decrypt方法:最直接,但通用性不强,换一个App就不行了。 - Hook
AESDemo.processSecureData方法:可以观察业务层如何调用解密。 - Hook Android系统的
javax.crypto.Cipher.doFinal方法:这是最通用、最强大的方法。几乎所有Java层的AES解密最终都会调用它。我们重点演示这个。
5.2 通用Hook脚本:拦截Cipher.doFinal
创建一个名为hook_aes.js的文件。
console.log(“[*] Frida脚本启动,开始Hook AES加解密...”); // Hook Java层的Cipher类 Java.perform(function () { // 定位到javax.crypto.Cipher类 var Cipher = Java.use(“javax.crypto.Cipher”); // Hook Cipher类的doFinal方法。 // 这里Hook有多个重载版本的doFinal,我们选择最常用的 byte[] 参数和返回值的版本。 Cipher[“doFinal”].overload(‘[B’).implementation = function (input) { console.log(“\n=== Cipher.doFinal被调用 ==="); // 1. 打印调用堆栈,帮助定位是哪里发起的解密 console.log(“[*] 调用堆栈:”); var stackTrace = Java.use(“android.util.Log”).getStackTraceString(Java.use(“java.lang.Exception”).$new()); console.log(stackTrace); // 2. 获取并打印传入的密文(input参数) console.log(“[*] 输入参数 (密文字节数组): ”); // 将字节数组转换成十六进制字符串,便于查看 var hexInput = Array.from(input).map(b => (‘0’ + (b & 0xFF).toString(16)).slice(-2)).join(‘:’); console.log(“ Hex: ” + hexInput); // 也可以尝试以Base64或字符串形式打印(如果是文本的话) try { var base64Input = Java.use(“android.util.Base64”).encodeToString(input, 0); console.log(“ Base64: ” + base64Input); } catch(e) {} // 3. 调用原函数,获取解密后的明文结果 var result = this.doFinal(input); console.log(“[*] 解密结果 (明文字节数组): ”); // 4. 打印解密后的明文 var hexOutput = Array.from(result).map(b => (‘0’ + (b & 0xFF).toString(16)).slice(-2)).join(‘:’); console.log(“ Hex: ” + hexOutput); try { var plainText = Java.use(“java.lang.String”).$new(result); console.log(“ String: ” + plainText); } catch(e) { // 如果解密结果不是合法字符串,可能只是二进制数据 console.log(“ (结果无法转换为字符串,可能是二进制数据)”); } // 5. 打印当前Cipher实例的一些信息(可选) console.log(“[*] Cipher算法信息: ” + this.getAlgorithm()); // 6. 返回原函数的结果,确保程序正常运行 return result; }; console.log(“[*] Hook设置完成,等待Cipher.doFinal被调用...\n”); });脚本逐行解析:
Java.perform:确保在Java虚拟机上下文中执行我们的Hook代码。Java.use(“javax.crypto.Cipher”):获取对Cipher类的引用。Cipher[“doFinal”].overload(‘[B’):指定HookdoFinal方法,且参数类型是字节数组([B是JNI签名,表示byte[])。.overload用于区分重载方法。.implementation = function (input) {...}:替换该方法的实现。我们定义的新函数会在原方法被调用时执行。- 打印堆栈:这是逆向中极其重要的一步。它能告诉你这个解密调用是从应用代码的哪一行发起的,帮你快速定位到关键的业务逻辑代码位置。
- 参数与结果处理:我们将输入的密文字节数组和输出的明文字节数组,分别以十六进制和可读字符串(如果可能)的形式打印出来。
this.doFinal(input):在Hook函数内部,通过this调用原方法,获取其返回值。这是Frida Hook的常见模式,确保不破坏原程序逻辑。return result:将原方法的结果返回,这样调用者收到的就是正常的解密数据,应用行为不受影响。
5.3 运行Hook脚本
首先,确保你的Android设备上frida-server正在运行,并且电脑可以连接(frida-ps -U正常)。
然后,我们需要知道目标应用的进程名或PID。我们的应用包名是com.example.aeshookdemo,其进程名通常也是这个。
编写一个Python脚本来加载我们的JS脚本,hook_runner.py:
import frida import sys def on_message(message, data): if message[‘type’] == ‘send’: print(f“[+] {message[‘payload’]}”) else: print(f“[!] {message}”) # 连接设备 device = frida.get_usb_device() # 附加到目标进程(应用需要先启动) # 方式一:通过包名附加(推荐) try: session = device.attach(“com.example.aeshookdemo”) except frida.ProcessNotFoundError: print(“目标进程未找到,请确保应用已启动。”) sys.exit(1) # 方式二:启动应用并附加(如果应用未启动) # pid = device.spawn([“com.example.aeshookdemo”]) # session = device.attach(pid) # device.resume(pid) # 恢复进程执行 # print(f“应用已启动,PID: {pid}”) # 读取Hook脚本 with open(“hook_aes.js”, “r”, encoding=“utf-8”) as f: js_code = f.read() # 创建脚本并加载 script = session.create_script(js_code) script.on(‘message’, on_message) print(“[*] 正在加载Hook脚本...”) script.load() # 保持脚本运行,等待输入退出 print(“[*] Hook脚本加载成功!正在监听AES解密调用...”) print(“[*] 按Ctrl+C退出。”) sys.stdin.read()运行这个Python脚本:
python hook_runner.py现在,启动或操作你的Android测试应用。当延迟2秒后,processSecureData被调用,进而触发decrypt和最终的Cipher.doFinal时,你会在电脑端的终端看到Frida脚本输出的信息。
预期输出示例:
[*] Frida脚本启动,开始Hook AES加解密... [*] Hook设置完成,等待Cipher.doFinal被调用... === Cipher.doFinal被调用 === [*] 调用堆栈: at javax.crypto.Cipher.doFinal(Native Method) at com.example.aeshookdemo.AESDemo.decrypt(AESDemo.java:48) at com.example.aeshookdemo.AESDemo.processSecureData(AESDemo.java:62) at com.example.aeshookdemo.MainActivity$1.run(MainActivity.java:30) ... [*] 输入参数 (密文字节数组): Hex: 1a:2b:3c:4d:5e:... (很长一串) Base64: L0MxQnRqW... (与App显示的密文一致) [*] 解密结果 (明文字节数组): Hex: 48:65:6c:6c:6f:20... String: Hello from Android! 这是安卓端的秘密。 [*] Cipher算法信息: AES大功告成!你成功拦截了一次AES解密操作,看到了原始的密文和解密后的明文。调用堆栈清晰地显示了从MainActivity到processSecureData,再到decrypt,最后到Cipher.doFinal的完整链路。
6. 进阶技巧与问题排查
掌握了基础Hook后,我们来看看一些更实战化的技巧和可能遇到的问题。
6.1 如何Hook重载方法
Cipher.doFinal有多个重载,比如doFinal(byte[], int, int)。我们的脚本只Hook了其中一个。为了更全面,可以同时Hook多个:
// Hook doFinal(byte[]) Cipher[“doFinal”].overload(‘[B’).implementation = function (input) { ... }; // Hook doFinal(byte[], int, int) Cipher[“doFinal”].overload(‘[B’, ‘int’, ‘int’).implementation = function (input, inputOffset, inputLen) { console.log(“[*] doFinal(byte[], int, int) 被调用”); console.log(“ Offset: ” + inputOffset + “, Len: ” + inputLen); // 提取指定范围的字节数组 var relevantInput = Java.array(‘byte’, input.slice(inputOffset, inputOffset + inputLen)); // ... 打印等相关操作 return this.doFinal(input, inputOffset, inputLen); };6.2 如何Hook构造函数和init方法
有时候,密钥和IV不是在代码里写死的,而是在Cipher.init()方法中传入的。Hook这个方法来获取密钥和IV是更根本的做法。
var Cipher = Java.use(“javax.crypto.Cipher”); var SecretKeySpec = Java.use(“javax.crypto.spec.SecretKeySpec”); var IvParameterSpec = Java.use(“javax.crypto.spec.IvParameterSpec”); // Hook Cipher.init 方法,这里以 init(int opmode, Key key, AlgorithmParameterSpec params) 为例 Cipher[“init”].overload(‘int’, ‘java.security.Key’, ‘java.security.spec.AlgorithmParameterSpec’).implementation = function (opmode, key, params) { console.log(“\n=== Cipher.init 被调用 ==="); console.log(“[*] 操作模式: ” + opmode + “ (1=加密, 2=解密)”); // 获取密钥信息 if (key) { console.log(“[*] 密钥算法: ” + key.getAlgorithm()); var keyBytes = key.getEncoded(); // 获取密钥字节 if (keyBytes) { var keyHex = Array.from(keyBytes).map(b => (‘0’ + (b & 0xFF).toString(16)).slice(-2)).join(‘:’); console.log(“[*] 密钥字节 (Hex): ” + keyHex); } } // 获取IV信息 if (params) { // 判断是否是IvParameterSpec if (Java.cast(params, IvParameterSpec)) { var ivParams = Java.cast(params, IvParameterSpec); var ivBytes = ivParams.getIV(); var ivHex = Array.from(ivBytes).map(b => (‘0’ + (b & 0xFF).toString(16)).slice(-2)).join(‘:’); console.log(“[*] IV字节 (Hex): ” + ivHex); } } console.log(“[*] 算法: ” + this.getAlgorithm()); // 调用原init方法 return this.init(opmode, key, params); };通过Hookinit,你可以在加解密发生之前,就拿到最关键的密钥和IV参数,这对于完全未知的加密算法分析至关重要。
6.3 常见问题与排查技巧
Hook失败,脚本没输出
- 检查Frida连接:
frida-ps -U是否能列出进程?确保frida-server在设备上运行且版本与电脑端frida库匹配。 - 检查进程名:确保附加的进程名正确。对于Android应用,通常是包名。可以用
frida-ps -U | grep your.app.package查找。 - 检查脚本语法:JS脚本是否有语法错误?Frida会在加载时报错。
- 目标方法是否被调用:你的应用真的执行到
Cipher.doFinal了吗?确认业务逻辑已被触发。
- 检查Frida连接:
报错:
TypeError: cannot read property ‘overload’ of undefined- 这通常是因为类路径不对。在Hook之前,先用
Java.use尝试获取类,并打印看看是否成功。console.log(Java.use(“javax.crypto.Cipher”));。如果返回undefined,可能是类加载器问题。可以尝试枚举所有类加载器来查找类:Java.enumerateClassLoaders({ onMatch: function(loader) { try { if (loader.findClass(“javax.crypto.Cipher”)) { console.log(“[*] 找到Cipher类的加载器: ” + loader); Java.classFactory.loader = loader; // 切换类加载器 } } catch(e) {} }, onComplete: function() {} });
- 这通常是因为类路径不对。在Hook之前,先用
应用崩溃或行为异常
- 确保调用原方法:在Hook函数的最后,一定要调用原方法(
this.doFinal(...))并返回其结果,除非你 intentionally 想改变程序行为。 - 异常处理:在原方法调用前后做好
try-catch,避免你的Hook代码抛出异常导致应用崩溃。 - 性能影响:Hook函数不要执行太耗时的操作(比如网络请求),否则可能导致应用无响应。
- 确保调用原方法:在Hook函数的最后,一定要调用原方法(
面对混淆
- 如果类名和方法名被混淆,你需要先进行静态分析。使用反编译工具(如JADX-GUI)打开APK,搜索关键词如“AES”、“Cipher”、“doFinal”。即使类名是
a.a,方法名是a(),你也可以通过其调用的系统API或字符串常量来定位。然后,在Frida脚本中使用混淆后的名字进行Hook。
- 如果类名和方法名被混淆,你需要先进行静态分析。使用反编译工具(如JADX-GUI)打开APK,搜索关键词如“AES”、“Cipher”、“doFinal”。即使类名是
Hook Native层(SO库)
- 如果加密逻辑在C/C++编写的SO库中,就需要Hook Native函数。这更复杂,需要分析SO的导出函数或通过偏移地址来Hook。Frida同样支持,使用
Interceptor.attach函数。这需要一定的逆向工程基础。
// 示例:Hook libnative-lib.so 中的 aes_decrypt 函数 var baseAddr = Module.findBaseAddress(“libnative-lib.so”); var decryptFuncAddr = baseAddr.add(0x1234); // 函数偏移地址,需通过IDA等工具分析得到 Interceptor.attach(decryptFuncAddr, { onEnter: function(args) { console.log(“[*] aes_decrypt 被调用”); // args[0]可能是密文指针,args[1]可能是明文输出指针 var ciphertext = Memory.readByteArray(args[0], 16); // 假设读取16字节 console.log(hexdump(ciphertext)); }, onLeave: function(retval) { // 可以在这里读取解密结果 } });- 如果加密逻辑在C/C++编写的SO库中,就需要Hook Native函数。这更复杂,需要分析SO的导出函数或通过偏移地址来Hook。Frida同样支持,使用
7. 总结与安全思考
通过这个从构建到拦截的完整流程,我们不仅实现了一个AES加解密工具,更重要的是掌握了使用Frida进行动态运行时分析的核心方法。这套方法的价值远不止于AES,它可以应用于任何你感兴趣的函数调用上——网络请求、文件操作、数据库访问、权限检查等等。
回顾一下关键步骤:
- 明确目标:确定要Hook的类和方法(这里是
javax.crypto.Cipher.doFinal)。 - 编写靶程序:一个包含目标方法的可运行程序,用于验证Hook效果。
- 搭建Frida环境:设备端运行
frida-server,电脑端安装frida-tools。 - 开发Hook脚本:用JavaScript编写拦截逻辑,重点打印参数、返回值、调用堆栈。
- 运行与调试:通过Python脚本将JS脚本注入目标进程,观察输出。
安全与合规提醒:本文所述技术仅用于安全研究、学术探讨和对自己拥有合法权限的应用程序进行测试。未经授权对他人软件进行逆向、Hook或篡改,可能违反法律法规和软件许可协议,请务必在合法合规的范围内使用这些技术。
最后,我个人在实际的逆向工作中发现,耐心和细心比任何高级技巧都重要。一个复杂的应用可能有多层加密、混淆和反调试机制。从最外层的API开始Hook,逐步深入,结合静态分析(阅读反编译代码)和动态分析(Frida Hook),像剥洋葱一样一层层解开它的防护,这个过程本身就是一种极大的乐趣和挑战。当你第一次成功拦截到核心数据时,那种成就感是无与伦比的。希望这个详细的示例能成为你探索移动安全与逆向工程世界的一块坚实垫脚石。