文章目录
-
- [1 前言](#1 前言)
-
- [1.1 对称加密:AES的企业级通用规范](#1.1 对称加密:AES的企业级通用规范)
- [2. Hook技术](#2. Hook技术)
-
- [2.1 核心源码](#2.1 核心源码)
- [2.2 Hook脚本](#2.2 Hook脚本)
- [2.3 脚本设计细节](#2.3 脚本设计细节)
-
- [2.3.1 辅助函数:字节数组处理与Base64兼容](#2.3.1 辅助函数:字节数组处理与Base64兼容)
- [2.3.2 Hook Cipher.doFinal():获取加密/解密的核心数据](#2.3.2 Hook Cipher.doFinal():获取加密/解密的核心数据)
- [2.3.3 Hook Base64.encodeToString():捕获最终传输的密文](#2.3.3 Hook Base64.encodeToString():捕获最终传输的密文)
- [3. 总结](#3. 总结)
⚠️本博文所涉安全渗透测试技术、方法及案例,仅用于网络安全技术研究与合规性交流,旨在提升读者的安全防护意识与技术能力。任何个人或组织在使用相关内容前,必须获得目标网络 / 系统所有者的明确且书面授权,严禁用于未经授权的网络探测、漏洞利用、数据获取等非法行为。
1 前言
在企业级应用中,数据安全是业务稳定运行的基石,加密技术作为数据安全的核心手段,对称加密因高效性被广泛应用。其中,AES(高级加密标准)是最主流的对称加密算法,凭借高强度、高兼容性的特点,成为支付信息传输、用户凭证存储、API通信加密等场景的首选方案。
1.1 对称加密:AES的企业级通用规范
AES加密解密使用相同密钥,运算效率远超非对称加密,特别适合处理大量数据。其企业级通用规范确保在各类场景下的安全性:
- 密钥管理:禁止硬编码在代码中,必须通过KMS(密钥管理系统)动态获取,且需定期轮换(降低密钥泄露后的风险);
- 工作模式:优先使用GCM(认证加密模式,同时保证机密性、完整性、真实性),禁用ECB(相同明文生成相同密文,易被攻击者分析规律);
- IV/Nonce(初始化向量):需12-16字节随机生成,同一密钥下绝对不可重复(避免通过密文对比泄露明文信息)。
2. Hook技术
以下是针对Java环境中AES加密体系的Hook脚本及详细解析,可通用监控各类应用的AES加密行为。
本章节使用的示例 APK、相关源码如下:
链接: https://pan.baidu.com/s/10yENJwouZqa41qkcB0QEAQ?pwd=vsu1 提取码: vsu1
2.1 核心源码
示例APK核心代码简要分析
以下是示例APK中AES加密工具类AESUtils的核心逻辑解析,该类采用企业主流的AES-256-GCM方案。
- 算法选型 :采用
AES/GCM/NoPadding,是企业级加密的首选组合------GCM模式同时保证机密性(加密)、完整性(防篡改)、真实性(防伪造),无需额外填充逻辑。 - 关键参数:12字节IV(Nonce)+ 256位密钥 + 128位认证标签,完全符合"随机IV+强密钥+认证校验"的安全规范。
- 数据结构 :加密后最终输出
Base64(IV+密文),这是Hook时需要重点捕获的"完整密文"------仅抓doFinal返回的密文(不含IV)无法解密,需通过Base64 Hook或IV提取获取完整数据。
java
package com.example.fridaapk
import java.security.SecureRandom
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.GCMParameterSpec
import android.util.Base64
object AESUtils {
private const val GCM_IV_LENGTH = 12
private const val GCM_TAG_LENGTH = 128
private const val ALGORITHM = "AES/GCM/NoPadding"
// 注意:在实际应用中,应通过 KMS 系统获取密钥而不是本地生成
fun generateKey(): SecretKey {
val keyGenerator = KeyGenerator.getInstance("AES")
keyGenerator.init(256)
return keyGenerator.generateKey()
}
fun encrypt(plaintext: String, key: SecretKey): String {
val cipher = Cipher.getInstance(ALGORITHM)
val iv = ByteArray(GCM_IV_LENGTH)
SecureRandom().nextBytes(iv)
val spec = GCMParameterSpec(GCM_TAG_LENGTH, iv)
cipher.init(Cipher.ENCRYPT_MODE, key, spec)
val ciphertext = cipher.doFinal(plaintext.toByteArray(Charsets.UTF_8))
val encryptedData = ByteArray(iv.size + ciphertext.size)
System.arraycopy(iv, 0, encryptedData, 0, iv.size) // 前12字节:IV
System.arraycopy(ciphertext, 0, encryptedData, iv.size, ciphertext.size)
return Base64.encodeToString(encryptedData, Base64.NO_WRAP)
}
fun decrypt(encryptedData: String, key: SecretKey): String {
val decodedData = Base64.decode(encryptedData, Base64.NO_WRAP)
val iv = ByteArray(GCM_IV_LENGTH)
val ciphertext = ByteArray(decodedData.size - GCM_IV_LENGTH)
System.arraycopy(decodedData, 0, iv, 0, GCM_IV_LENGTH)
System.arraycopy(decodedData, GCM_IV_LENGTH, ciphertext, 0, ciphertext.size)
val cipher = Cipher.getInstance(ALGORITHM)
val spec = GCMParameterSpec(GCM_TAG_LENGTH, iv)
cipher.init(Cipher.DECRYPT_MODE, key, spec)
val plaintext = cipher.doFinal(ciphertext)
return String(plaintext, Charsets.UTF_8)
}
}
2.2 Hook脚本
javascript
import Java from "frida-java-bridge";
// 字节数组转明文
function bytesToString(byteArr) {
if (!byteArr || byteArr.length === 0) return "空";
try {
const str = Java.use("java.lang.String").$new(byteArr, "UTF-8");
return str.toString();
} catch (e) {
return getHexBytes(byteArr);
}
}
// 字节数组转16进制
function getHexBytes(byteArr) {
if (!byteArr || byteArr.length === 0) return "空";
let hex = "";
for (let i = 0; i < byteArr.length; i++) {
const b = byteArr[i] & 0xFF;
hex += (b < 16 ? "0" : "") + b.toString(16);
}
return hex;
}
// Base64编码(兼容android.util.Base64和java.util.Base64)
function encodeBase64(data) {
try {
return Java.use("android.util.Base64").encodeToString(data, 0);
} catch (e) {
try {
const base64Encoder = Java.use("java.util.Base64").getEncoder();
return base64Encoder.encodeToString(data);
} catch (e2) {
return "<Base64编码失败>";
}
}
}
try {
Java.perform(() => {
const Cipher = Java.use("javax.crypto.Cipher");
const Base64Android = Java.use("android.util.Base64");
const Base64Java = Java.use("java.util.Base64");
// Hook doFinal 方法
Cipher.doFinal.overload('[B').implementation = function (input) {
const result = this.doFinal(input);
try {
const algorithm = this.getAlgorithm();
const mode = this.opmode.value;
if (algorithm && algorithm.includes("AES")) {
console.log(`=== 操作详情 ===`);
console.log(`[加密/解密] 算法名称: ${algorithm}`);
if (mode == 1) { // 加密模式
console.log(`[加密] 明文: ${bytesToString(input)}`);
console.log(`[加密] 密文(Base64): ${encodeBase64(result)}`);
} else if (mode == 2) { // 解密模式
console.log(`[解密] 密文(Base64): ${encodeBase64(input)}`);
console.log(`[解密] 明文: ${bytesToString(result)}`);
}
}
} catch (e) {
console.log(`[Cipher] 解析数据出错: ${e.message}`);
}
return result;
};
// Hook Base64.encodeToString 方法
Base64Android.encodeToString.overloads.forEach(func => {
func.implementation = function (data, flags) {
const result = this.encodeToString(data, flags);
console.log(`[Base64编码] 疑似密文: ${result}`);
return result;
};
});
// Hook java.util.Base64.encoder.encodeToString 方法
try {
const encoder = Base64Java.getEncoder();
encoder.encodeToString.overloads.forEach(func => {
func.implementation = function (data) {
const result = this.encodeToString(data);
console.log(`[Base64编码] 疑似密文: ${result}`);
return result;
};
});
} catch (e) {
// 如果不可用,忽略
}
});
} catch (e) {
console.log(`[Frida] 脚本执行异常: ${e.message}`);
}
启动脚本run.py和前面一样:
python
import frida
import sys
# 目标应用包名
PACKAGE_NAME = "com.example.fridaapk"
def main():
"""
启动目标应用并注入 Frida 脚本。
"""
try:
# 获取连接的 USB 设备
print("正在寻找 USB 设备...")
device = frida.get_usb_device(timeout=10)
print(f"成功连接到设备: {device.name}")
# 启动应用并附加
print(f"正在启动应用: {PACKAGE_NAME}...")
pid = device.spawn([PACKAGE_NAME])
session = device.attach(pid)
print(f"应用启动成功,进程 ID (PID): {pid}")
# 加载并注入 JS 脚本
print("正在注入 Frida 脚本...")
with open("./js/compiled_hook.js", "r", encoding="utf-8") as f:
js_code = f.read()
script = session.create_script(js_code)
script.load()
print("脚本注入成功!")
# 恢复应用运行
device.resume(pid)
print("应用已恢复运行,开始监控 Frida 输出...")
# 保持脚本运行,直到用户按下 Ctrl+C
try:
sys.stdin.read()
except KeyboardInterrupt:
print("用户中断,正在退出...")
except frida.TimedOutError:
print("错误:未找到连接的 USB 设备。请确保设备已连接并开启 USB 调试。")
except frida.ProcessNotFoundError:
print(f"错误:无法找到或启动应用 '{PACKAGE_NAME}'。请确保应用已安装。")
except FileNotFoundError:
print(f"错误:Frida 脚本文件未找到。请检查路径 './js/compiled_hook.js' 是否正确。")
except Exception as e:
print(f"发生未知错误: {e}")
finally:
print("程序已退出。")
if __name__ == "__main__":
main()
运行启动脚本后,操作示例 APK 的对称加密区域,输出结果如下:
可以看到AES加密密文,与 Hook 住的第2个 Base64 的密文不同,因为我在 APK 中输出的最终密文是随机 IV 和 AES密文的拼接,可以看到AES加密密文是最终密文的后半部分。

2.3 脚本设计细节
Java中所有AES加密/解密操作均通过javax.crypto.Cipher类完成,而密文通常会经过Base64编码后用于传输或存储。脚本通过监控关键节点,实现对AES加密全流程的可视化。
2.3.1 辅助函数:字节数组处理与Base64兼容
-
bytesToString 函数 :
作用:将加密/解密过程中的字节数组转为UTF-8字符串,方便直接查看明文(如用户输入的密码、API请求参数等)。
必要性:加密操作的输入输出都是字节数组,直接打印会显示乱码,通过该函数可直观呈现可读的明文内容。
-
getHexBytes 函数 :
作用:当字节数组无法转为UTF-8字符串(如二进制密文、随机生成的IV)时,转为十六进制字符串展示。
必要性:确保所有字节数据都能以人类可读的形式呈现,避免因编码问题导致关键信息丢失。
-
encodeBase64 函数 :
作用:兼容Android和Java平台的Base64编码逻辑,将字节数组转为Base64字符串。
必要性:实际应用中,密文常以Base64形式传输(如接口中的
encryptedData字段),且不同平台可能使用android.util.Base64(Android)或java.util.Base64(Java),兼容两者才能确保获取完整的最终密文(例如示例APK中可能采用"IV+密文"拼接后再Base64编码的格式,需通过该函数正确解析)。
2.3.2 Hook Cipher.doFinal():获取加密/解密的核心数据
作用:doFinal是AES实际执行加密/解密的方法------加密时输入明文字节数组、输出密文字节数组;解密时输入密文字节数组、输出明文字节数组。通过Hook该方法,可直接获取:
- 加密前的原始明文和加密后的密文(字节数组及Base64形式);
- 解密前的密文(字节数组及Base64形式)和解密后的原始明文。
必要性:这是加密流程的"结果输出口",不Hook此方法就无法获取实际参与传输/存储的密文和原始明文,无法验证加密逻辑是否正确(如明文密文是否匹配)。
2.3.3 Hook Base64.encodeToString():捕获最终传输的密文
作用:监控所有Base64编码操作,输出编码后的字符串(疑似密文)。
必要性:
- 多数场景下,AES生成的二进制密文会经过Base64编码后才用于传输(如HTTP请求体、数据库存储),仅Hook
Cipher只能拿到二进制密文,无法获取实际传输的"最终密文"; - 部分应用会对"IV+密文""密文+校验值"等组合数据进行Base64编码,Hook该方法可捕获完整的拼接后密文,便于还原加密数据结构;
- 兼容Android和Java的Base64实现,避免因平台差异漏掉关键密文(例如某些应用在不同版本中切换Base64工具类,不全面Hook会导致监控断层)。
3. 总结
该Hook脚本通过监控AES加密流程的核心节点,实现了对加密行为的全链路可视化:
- 借助
bytesToString和getHexBytes确保字节数据可读; - 通过
encodeBase64兼容多平台Base64编码,获取最终传输的密文; - Hook
Cipher.doFinal获取加密/解密的原始数据,验证明文密文对应关系; - 监控Base64编码过程,捕获实际传输的密文形式。
无论是验证AES是否符合"GCM模式+随机IV+动态密钥"的企业规范,还是调试加密逻辑中的数据 mismatch 问题,该脚本都能提供直观的监控数据,帮助开发者把控加密环节的安全性与正确性。