分析并提取 Android 应用的加密密钥
📚 前置知识
本配方涉及以下核心技术,建议先阅读相关章节:
问题场景
你遇到了什么问题?
- App 的 API 请求参数被加密了,看不懂内容
- 想知道 App 使用了什么加密算法
- 需要提取加密密钥来解密数据
- 想重现 App 的加密/签名逻辑用于自动化
- 需要绕过加密验证或签名检查
本配方教你:识别加密算法、定位密钥位置、使用 Frida 动态提取密钥。
核心理念:
💡 密码学逆向的关键不是破解算法,而是找到密钥
- 不要试图"破解" AES/RSA 等成熟算法(几乎不可能)
- 用动态分析直接提取密钥
- 或直接调用 App 自己的加密函数(利用已有密钥)
预计用时: 30-60 分钟
工具清单
必需工具
- jadx-gui - Java 层静态分析
- Frida - 动态 Hook 提取密钥
- Android 设备(已 Root)
可选工具
前置条件
✅ 确认清单
1
2
3
4
5
6
7
8
| # 1. Frida 正常运行
frida-ps -U
# 2. jadx-gui 已安装
jadx-gui --version
# 3. 抓包环境已配置(可选)
# 参考: network_sniffing.md
|
解决方案
第 1 步:识别加密算法(5 分钟)
1.1 搜索特征字符串
用 jadx-gui 打开 APK,全局搜索:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| # 对称加密
AES
DES
3DES
# 非对称加密
RSA
ECC
# 哈希算法
MD5
SHA
SHA256
HMAC
# 加密模式
ECB
CBC
CTR
GCM
# Padding
PKCS5Padding
PKCS7Padding
# 编码
Base64
|
1.2 搜索 Java 加密 API
1
2
3
4
5
6
7
8
9
10
11
| // Java 层加密 API
javax.crypto.Cipher
javax.crypto.spec.SecretKeySpec
javax.crypto.spec.IvParameterSpec
javax.crypto.Mac
java.security.Signature
java.security.MessageDigest
// Base64 编码
android.util.Base64
java.util.Base64
|
1.3 检查 Native 层加密
1
2
3
4
5
6
7
8
| # 解压 APK
unzip app.apk -d app_unzipped
# 搜索 .so 文件中的加密库
strings app_unzipped/lib/*/lib*.so | grep -i -E "openssl|crypto|encrypt|aes|rsa"
# 或使用 rabin2 分析
rabin2 -z app_unzipped/lib/arm64-v8a/libnative.so | grep -i encrypt
|
第 2 步:定位加密代码(10 分钟)
2.1 跟踪加密字符串
假设你搜到了 AES/CBC/PKCS5Padding:
- 在 jadx 中点击这个字符串
- 查看交叉引用(
X 键或右键 → Find Usage) - 跳转到使用这个字符串的函数
典型代码模式:
1
2
3
4
5
6
7
8
9
| // 你可能会看到类似这样的代码
public static String encrypt(String plaintext) {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKeySpec key = new SecretKeySpec(KEY_BYTES, "AES");
IvParameterSpec iv = new IvParameterSpec(IV_BYTES);
cipher.init(Cipher.ENCRYPT_MODE, key, iv);
byte[] encrypted = cipher.doFinal(plaintext.getBytes());
return Base64.encodeToString(encrypted, Base64.DEFAULT);
}
|
记录关键信息:
1
2
3
| 密钥变量: KEY_BYTES
IV 变量: IV_BYTES
加密函数: com.example.app.CryptoUtils.encrypt()
|
第 3 步:找到密钥位置(10 分钟)
📍 位置 1: Java 代码硬编码(难度:低)
查找方法:
1
2
3
4
| // 搜索关键词
SecretKeySpec
byte[] key
private static final byte[]
|
示例:
1
2
3
4
5
| private static final byte[] KEY = {
0x12, 0x34, 0x56, 0x78,
0x9a, 0xbc, 0xde, 0xf0,
// ... 16/24/32 字节
};
|
密钥长度:
- AES-128: 16 字节
- AES-192: 24 字节
- AES-256: 32 字节
📍 位置 2: 资源文件(难度:低)
查找路径:
1
2
3
4
5
6
7
8
| # 检查 assets 目录
ls app_unzipped/assets/
# 检查 res/raw
ls app_unzipped/res/raw/
# 搜索二进制文件
find app_unzipped -type f -exec file {} \; | grep data
|
常见文件名:
secret.keyconfig.datlicense.bin
📍 位置 3: Native (.so) 硬编码(难度:中)
IDA Pro 分析:
- 打开
.so 文件 - 跳转到 Strings 窗口 (
Shift+F12) - 搜索关键字符串
- 查看交叉引用找到使用密钥的函数
Ghidra 分析:
- 导入
.so 文件 - 搜索 → For Strings
- 筛选长度为 16/24/32 的可疑字符串
📍 位置 4: 动态生成(难度:高)
特征:密钥通过算法计算,常见方式:
1
2
3
4
5
6
7
| // 基于设备信息生成
String deviceId = getDeviceId();
byte[] key = MD5(deviceId + SALT);
// 基于时间戳
long timestamp = System.currentTimeMillis();
byte[] key = HMACSHA256(timestamp, SECRET);
|
从服务器获取:
对策:
- 抓包查看密钥传输
- Hook 网络请求获取密钥
- 或直接 Hook 加密函数(密钥已在内存中)
第 4 步:动态提取密钥(15 分钟)
终极方法:无论密钥藏在哪,只要加密函数被调用,Hook 就能抓到
4.1 Hook Java 层加密
通用 AES Hook 脚本 dump_aes_key.js:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
| Java.perform(function () {
console.log("\n[Crypto Hook] 启动\n");
// Hook Cipher.init
var Cipher = Java.use("javax.crypto.Cipher");
Cipher.init.overload(
"int",
"java.security.Key",
"java.security.spec.AlgorithmParameterSpec"
).implementation = function (opmode, key, spec) {
console.log("\n🔐 [Cipher.init] 捕获!");
// 模式
var mode = opmode == 1 ? "ENCRYPT" : "DECRYPT";
console.log(" 模式: " + mode);
// 算法
console.log(" 算法: " + this.getAlgorithm());
// 提取密钥
try {
var secretKey = Java.cast(
key,
Java.use("javax.crypto.spec.SecretKeySpec")
);
var keyBytes = secretKey.getEncoded();
var Base64 = Java.use("android.util.Base64");
console.log(" 密钥 (Base64): " + Base64.encodeToString(keyBytes, 0));
console.log(" 密钥 (Hex): " + bytesToHex(keyBytes));
} catch (e) {
console.log(" 密钥类型: " + key.$className);
}
// 提取 IV
if (spec) {
try {
var ivSpec = Java.cast(
spec,
Java.use("javax.crypto.spec.IvParameterSpec")
);
var ivBytes = ivSpec.getIV();
console.log(" IV (Hex): " + bytesToHex(ivBytes));
} catch (e) {}
}
return this.init(opmode, key, spec);
};
// Hook Cipher.doFinal
Cipher.doFinal.overload("[B").implementation = function (input) {
var result = this.doFinal(input);
console.log("\n📦 [Cipher.doFinal] 捕获!");
console.log(" 输入长度: " + input.length);
console.log(" 输出长度: " + result.length);
console.log(" 输入数据 (前32字节): " + bytesToHex(input.slice(0, 32)));
console.log(" 输出数据 (前32字节): " + bytesToHex(result.slice(0, 32)));
return result;
};
function bytesToHex(bytes) {
var hex = [];
for (var i = 0; i < bytes.length && i < 32; i++) {
hex.push(("0" + (bytes[i] & 0xff).toString(16)).slice(-2));
}
return hex.join(" ");
}
console.log("✅ [Crypto Hook] 配置完成\n");
});
|
运行脚本:
1
| frida -U -f com.example.app -l dump_aes_key.js
|
预期输出:
1
2
3
4
5
6
7
8
9
10
11
12
| 🔐 [Cipher.init] 捕获!
模式: ENCRYPT
算法: AES/CBC/PKCS5Padding
密钥 (Base64): MTIzNDU2Nzg5MGFiY2RlZg==
密钥 (Hex): 31 32 33 34 35 36 37 38 39 30 61 62 63 64 65 66
IV (Hex): 66 65 64 63 62 61 30 39 38 37 36 35 34 33 32 31
📦 [Cipher.doFinal] 捕获!
输入长度: 128
输出长度: 144
输入数据 (前32字节): 7b 22 75 73 65 72 6e 61 6d 65 22 3a ...
输出数据 (前32字节): a3 b2 c1 d0 e4 f5 ...
|
4.2 Hook Native 层加密
查找 Native 加密函数:
1
2
3
4
5
6
| # 使用 nm 查看函数
nm -D libnative.so | grep -i encrypt
# 使用 Frida
frida -U -f com.example.app
> Module.enumerateExports('libnative.so').filter(e => e.name.includes('encrypt'))
|
Hook Native 函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| Interceptor.attach(
Module.findExportByName("libnative.so", "Java_com_example_Crypto_encrypt"),
{
onEnter: function (args) {
console.log("\n[Native Encrypt] 调用!");
// args[0] = JNIEnv*
// args[1] = jclass
// args[2] = 第一个参数(通常是明文)
// args[3] = 第二个参数(可能是密钥)
// 读取字符串参数
var plaintext = Java.vm
.getEnv()
.getStringUtfChars(args[2], null)
.readCString();
console.log(" 明文: " + plaintext);
// 保存指针用于后续读取
this.keyPtr = args[3];
},
onLeave: function (retval) {
// retval 是返回值(密文)
console.log(" 返回值: " + retval);
},
}
);
|
第 5 步:验证密钥(5 分钟)
5.1 使用 CyberChef 验证
- 打开 https://gchq.github.io/CyberChef/
- 选择操作:
AES Decrypt - 输入:
1
2
3
4
5
6
| | 项目 | 说明 |
| --- | --- |
| **Key** (Hex) | `31 32 33 34 ...` |
| **IV** (Hex) | `66 65 64 63 ...` |
| **Mode** | `CBC` |
| **Input** | 密文(Base64 或 Hex) |
|
- 点击 Bake!
如果解密成功,说明密钥正确!
5.2 Python 脚本验证
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
import base64
# 从 Frida 获取的密钥和 IV(Hex 转 bytes)
key = bytes.fromhex('31323334353637383930616263646566')
iv = bytes.fromhex('66656463626130393837363534333231')
# 从抓包获取的密文
ciphertext = base64.b64decode('YWJjZGVmZ2hpamtsbW5vcA==')
# 解密
cipher = AES.new(key, AES.MODE_CBC, iv)
plaintext = unpad(cipher.decrypt(ciphertext), AES.block_size)
print("解密结果:", plaintext.decode())
|
5.3 直接调用 App 的加密函数
1
2
3
4
5
6
7
8
9
10
11
| Java.perform(function () {
var CryptoUtils = Java.use("com.example.app.CryptoUtils");
// 调用加密函数
var encrypted = CryptoUtils.encrypt("Hello World");
console.log("加密结果: " + encrypted);
// 调用解密函数
var decrypted = CryptoUtils.decrypt(encrypted);
console.log("解密结果: " + decrypted);
});
|
工作原理
常见加密算法对照表
| 算法类型 | 算法 | 密钥长度 | 用途 |
|---|
| 对称加密 | AES | 128/192/256 bit | 加密数据 |
| DES | 56 bit | 旧标准(不安全) |
| 3DES | 168 bit | DES 增强版 |
| 非对称加密 | RSA | 1024/2048/4096 bit | 密钥交换、签名 |
| ECC | 256/384/521 bit | RSA 的高效替代 |
| 哈希 | MD5 | 128 bit 输出 | 校验(不安全) |
| SHA-256 | 256 bit 输出 | 安全哈希 |
| HMAC | 可变 | 带密钥的哈希 |
AES 加密流程
1
2
3
4
5
6
7
8
9
| 明文数据
↓
[AES Encrypt] ← 使用密钥 + IV
↓
密文数据
↓
[Base64 Encode] ← 便于传输
↓
最终密文
|
常见问题
❌ 问题 1: Hook 脚本不生效
症状:运行 Frida 脚本后没有任何输出
检查:
- 确认加密函数被调用了吗?
1
2
3
4
| Java.use("javax.crypto.Cipher").$init.overload().implementation = function () {
console.log("[TEST] Cipher 实例化");
return this.$init();
};
|
- 加密是否在 Native 层?
- 类名可能被混淆
1
2
3
4
5
6
7
8
| Java.enumerateLoadedClasses({
onMatch: function (className) {
if (className.toLowerCase().includes("cipher")) {
console.log(className);
}
},
onComplete: function () {},
});
|
❌ 问题 2: 解密失败
可能原因:
- IV 不正确
- 确认是否使用了 IV
- 某些实现会将 IV 附加在密文开头
- Padding 不匹配
- 尝试不同的 Padding:
PKCS5Padding, PKCS7Padding, NoPadding
- 编码问题
1
2
3
4
| # 尝试不同编码
ciphertext = base64.b64decode(data) # Base64
ciphertext = bytes.fromhex(data) # Hex
ciphertext = data.encode() # UTF-8
|
- 密钥派生
- 可能使用了 PBKDF2 等密钥派生函数
- Hook
SecretKeyFactory.generateSecret() 查看
❌ 问题 3: Native 函数找不到
症状:Module.findExportByName() 返回 null
解决:
- 函数可能未导出
1
2
| # 查看所有符号(包括未导出)
readelf -s libnative.so | grep encrypt
|
- 使用地址偏移 Hook
1
2
3
| var baseAddr = Module.findBaseAddress('libnative.so');
var funcAddr = baseAddr.add(0x1234); // 从 IDA 获取偏移
Interceptor.attach(funcAddr, { ... });
|
- 动态注册的 JNI 方法
1
2
3
4
5
6
7
8
9
10
11
| // Hook RegisterNatives
var RegisterNatives = Module.findExportByName(
"libart.so",
"_ZN3art3JNI15RegisterNativesEP7_JNIEnvP7_jclassPK15JNINativeMethodi"
);
Interceptor.attach(RegisterNatives, {
onEnter: function (args) {
var methods = ptr(args[2]);
console.log("Register JNI Method:", methods.readCString());
},
});
|
❌ 问题 4: 密钥是字符串而非字节数组
症状:
1
2
| // 看到类似这样的代码
SecretKeySpec key = new SecretKeySpec("MyPassword123".getBytes(), "AES");
|
解决方案:
1. 使用密钥派生函数(KDF)
1
2
3
4
5
| from Crypto.Protocol.KDF import PBKDF2
password = "MyPassword123"
salt = b"somesalt" # 需要从代码中找到
key = PBKDF2(password, salt, dkLen=16) # 16 字节 AES-128
|
说明:
PBKDF2 会将任意长度的密码派生为固定长度的密钥salt 通常在代码中硬编码或从服务器获取dkLen 决定输出密钥长度:16 (AES-128) / 24 (AES-192) / 32 (AES-256)
2. Hook 密钥派生函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| var SecretKeyFactory = Java.use("javax.crypto.SecretKeyFactory");
SecretKeyFactory.generateSecret.implementation = function (keySpec) {
var key = this.generateSecret(keySpec);
console.log("[密钥派生] 算法:", this.getAlgorithm());
console.log("[密钥派生] 密钥 (Hex):", bytesToHex(key.getEncoded()));
// 尝试获取 salt(如果是 PBEKeySpec)
try {
var PBEKeySpec = Java.use("javax.crypto.spec.PBEKeySpec");
var pbeSpec = Java.cast(keySpec, PBEKeySpec);
console.log("[密钥派生] Salt:", bytesToHex(pbeSpec.getSalt()));
console.log("[密钥派生] 迭代次数:", pbeSpec.getIterationCount());
} catch (e) {}
return key;
};
|
3. 直接使用字符串密钥
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
import hashlib
# 从代码中找到的字符串密码
password = "MyPassword123"
# 方法 1: 直接使用前 16 字节
key = password.encode()[:16].ljust(16, b'\0')
# 方法 2: MD5 哈希(常见做法,输出正好 16 字节)
key = hashlib.md5(password.encode()).digest()
# 方法 3: SHA256 前 16 字节
key = hashlib.sha256(password.encode()).digest()[:16]
# 然后用于解密
cipher = AES.new(key, AES.MODE_CBC, iv)
plaintext = unpad(cipher.decrypt(ciphertext), AES.block_size)
|
延伸阅读
相关配方
工具深入
案例分析
💡 思路一句话: 抓包拿到加密请求 → 在 jadx 中搜索加密相关类名(Cipher, MessageDigest, Mac)→ hook 加解密方法获取明文和密钥 → 用 Python 重写加密逻辑验证。
理论基础
快速参考
Hook 脚本模板库
1. RSA Hook
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| var Cipher = Java.use("javax.crypto.Cipher");
Cipher.init.overload("int", "java.security.Key").implementation = function (
opmode,
key
) {
console.log("[RSA] 模式:", opmode == 1 ? "ENCRYPT" : "DECRYPT");
console.log("[RSA] 密钥类型:", key.$className);
// 获取公钥/私钥
if (key.$className.includes("PublicKey")) {
console.log("[RSA] 公钥:", bytesToHex(key.getEncoded()));
} else if (key.$className.includes("PrivateKey")) {
console.log("[RSA] 私钥:", bytesToHex(key.getEncoded()));
}
return this.init(opmode, key);
};
|
2. HMAC Hook
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| var Mac = Java.use("javax.crypto.Mac");
Mac.init.implementation = function (key) {
console.log("[HMAC] 算法:", this.getAlgorithm());
var secretKey = Java.cast(key, Java.use("javax.crypto.spec.SecretKeySpec"));
console.log("[HMAC] 密钥:", bytesToHex(secretKey.getEncoded()));
return this.init(key);
};
Mac.doFinal.overload("[B").implementation = function (data) {
var result = this.doFinal(data);
console.log("[HMAC] 输入:", bytesToHex(data));
console.log("[HMAC] 输出:", bytesToHex(result));
return result;
};
|
3. Base64 Hook
1
2
3
4
5
6
7
8
9
10
11
| var Base64 = Java.use("android.util.Base64");
Base64.decode.overload("java.lang.String", "int").implementation = function (
str,
flags
) {
var result = this.decode(str, flags);
console.log("[Base64] 解码:");
console.log(" 输入:", str.substring(0, 50) + "...");
console.log(" 输出 (Hex):", bytesToHex(result));
return result;
};
|
OpenSSL 命令速查
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| # AES 加密
echo "Hello" | openssl enc -aes-128-cbc -K 31323334353637383930616263646566 -iv 66656463626130393837363534333231 -base64
# AES 解密
echo "密文" | base64 -d | openssl enc -d -aes-128-cbc -K 31323334353637383930616263646566 -iv 66656463626130393837363534333231
# 生成 MD5
echo -n "text" | openssl md5
# 生成 SHA256
echo -n "text" | openssl sha256
# RSA 密钥生成
openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -pubout -out public.pem
|