AES 加密算法详解:从原理到安卓逆向实战
本文在原理讲解基础上,增加了安卓逆向视角:如何识别 AES、如何还原密钥,以及各变种算法介绍。
目录
-
AES 简介 -
密钥编排 -
明文运算 -
密钥分析 -
故障对密文的影响 -
差分故障分析(DFA) -
安卓逆向中识别 AES 算法 -
安卓逆向中还原 AES 密钥 -
AES 变种算法介绍
一、AES 简介
AES(Advanced Encryption Standard,高级加密标准)是一种对称加密算法,被广泛应用于数据加密领域。它是美国国家标准与技术研究院(NIST)于 2001 年指定的新加密标准,用来替代原有的 DES(Data Encryption Standard)。
AES 支持三种密钥长度:
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
本文重点讨论 AES-128 版本。
1.1 工作原理
虽然 AES 有许多复杂的工作模式(如 ECB、CBC、CFB 等)以及填充方式来适应不同长度的数据块,但这些不在本节讨论范围内。这里主要关注最基本的 AES-128 加解密过程。
-
输入:16 字节(128 位)的明文 -
密钥:同样为 16 字节的密钥 -
输出:经过一系列变换后,得到 16 字节的密文
1.2 填充方式
当处理明文数据时,如果原始数据长度恰好是 16 字节的整数倍,除了 NoPadding 之外,其他填充方法都会在数据尾部额外添加一个 16 字节块。
1.2.1 NoPadding(不填充)
不对明文做任何填充,前提是明文长度必须是 16 字节的整数倍。
1.2.2 PKCS5Padding / PKCS7Padding(固定值填充)
在末尾追加若干字节,每个补充字节的值等于补充的字节数。
例如:明文缺少 6 个字节时,追加 6 个值为 0x06 的字节:
xx xx xx xx xx xx xx xx xx xx 06 06 06 06 06 06
这是 Android/Java 中最常见的填充方式。PKCS5Padding 和 PKCS7Padding 在 16 字节块的情况下行为完全一致。
1.2.3 ISO10126Padding(随机 + 固定值混合填充)
末尾填充随机字节,但最后一个字节固定为实际需要填充的字节数:
xx xx xx xx xx xx xx xx xx xx r r r r r 06
其中 r 为随机字节,最后的 06 表示补充了 6 个字节。
1.2.4 ANSI X.923(零填充 + 固定值)
末尾补零(0x00),最后一个字节为需要填充的字节数:
xx xx xx xx xx xx xx xx xx xx 00 00 00 00 00 06
1.3 加密流程
假设参数如下:
# 明文(十六进制)
input_bytes = 0x00112233445566778899aabbccddeeff
# 密钥(十六进制)
key = 0x2b7e151628aed2a6abf7158809cf4f3c
AES-128 加密流程图如下:

整个过程分为两个主要部分:明文处理和密钥编排。
-
密钥编排将 16 字节主密钥扩展为 11 组轮密钥 ,每组 16 字节 -
明文处理包含一次初始轮密钥加,加上 10 轮主运算
二、密钥编排
密钥扩展将主密钥 2b7e151628aed2a6abf7158809cf4f3c 扩展为 176 字节序列,包含 11 个子密钥 ,共 44 个四字节单元( 数组):

主密钥被切分为 数组的前四个元素:
2.1 黄色区域()
每隔 4 个元素的列(图中黄色),计算规则需要用到 函数:
函数三个步骤:
第一步:循环左移一字节(RotWord)
将最左边的字节移到最右边:

第二步:S 盒替换(SubWord)
S 盒是预先设计好的 16×16 查询表(256 个元素),将字节值作为索引取出对应替换值:


第三步:与轮常数 Rcon 异或
将上一步结果的最高字节与 Rcon 表中对应轮次的值异或:
Rcon 表:

异或过程:

以 为例,完整计算如下:
2.2 红色区域(其余列)
去掉 函数,直接与前四个元素异或:
例如:
三、明文运算
AES 算法中,数据以 state(状态矩阵) 的形式进行计算和传输。明文按从上到下、从左到右的顺序填入 4×4 的状态矩阵:

3.1 轮密钥加(AddRoundKeys)
将当前轮密钥与 state 逐字节异或:

初始轮密钥加使用 (即主密钥本身):

十轮运算流程如下(注意第 10 轮没有列混淆):

3.2 字节替换(SubBytes)
与密钥编排中的 S 盒替换完全一致,对 state 中每个字节用 S 盒做查表替换:

3.3 行移位(ShiftRows)
循环左移,目的是实现字节在行方向上的扩散:
-
第 1 行:不移位 -
第 2 行:循环左移 1 字节 -
第 3 行:循环左移 2 字节 -
第 4 行:循环左移 3 字节

3.4 列混淆(MixColumns)
列混淆是最复杂的步骤,涉及两块知识:矩阵乘法和伽罗瓦域(GF(2^8))中的加法与乘法。
AES 列混淆使用如下固定矩阵左乘 state:
其中”加法”是异或运算,”乘法”是伽罗瓦域内的乘法。
对于 AES 中仅出现的三种乘法情况:
:结果为 本身
defmul_by_01(num):
return num # 乘以 1,不变
:左移 1 位,最高位为 1 时额外异或 0x1B(对应多项式模约简)
defmul_by_02(num):
if num < 0x80:
# 最高位为 0,直接左移
res = (num << 1)
else:
# 最高位为 1,左移后需模约简:异或 GF(2^8) 生成多项式 0x1B
res = (num << 1) ^ 0x1b
return res % 0x100
defmul_by_03(num):
# x*3 = x*2 XOR x(在 GF(2^8) 中)
return mul_by_02(num) ^ num
四、密钥分析
接下来讨论密钥编排更深层的问题,核心问题:可以从轮密钥逆推主密钥吗?
完整的 11 个轮密钥如下:
K00: 2b7e151628aed2a6abf7158809cf4f3c
K01: a0fafe1788542cb123a339392a6c7605
K02: f2c295f27a96b9435935807a7359f67f
K03: 3d80477d4716fe3e1e237e446d7a883b
K04: ef44a541a8525b7fb671253bdb0bad00
K05: d4d1c6f87c839d87caf2b8bc11f915bc
K06: 6d88a37a110b3efddbf98641ca0093fd
K07: 4e54f70e5f5fc9f384a64fb24ea6dc4f
K08: ead27321b58dbad2312bf5607f8d292f
K09: ac7766f319fadc2128d12941575c006e
K10: d014f9a8c9ee2589e13f0cc8b6630ca6
4.1 K10 分析
由 组成。已知 ,可以逆推 :
将 切成四块后计算:
>>> hex(0xd014f9a8 ^ 0xc9ee2589)
'0x19fadc21'# W37
>>> hex(0xc9ee2589 ^ 0xe13f0cc8)
'0x28d12941'# W38
>>> hex(0xe13f0cc8 ^ 0xb6630ca6)
'0x575c006e'# W39
即求出了 的后三个字,加上通过 函数逆算的 ,即可得到完整 。
4.2 K09 分析
下面验证 (即 的第一个字)。:
-
循环左移: 575c006e→5c006e57 -
S 盒替换(逐字节查表):
>>> Sbox = (0x63, 0x7C, 0x77, 0x7B, ...)
>>> hex(Sbox[0x5c]) # '0x4a'
>>> hex(Sbox[0x00]) # '0x63'
>>> hex(Sbox[0x6e]) # '0x9f'
>>> hex(Sbox[0x57]) # '0x5b'
-
首字节与 Rcon 第 10 个元素 0x36异或:
>>> hex(0x4a ^ 0x36)
'0x7c'
-
函数结果为 7c639f5b,与 异或:
>>> hex(0x7c639f5b ^ 0xd014f9a8)
'0xac7766f3'# 正是 W36
结论:AES-128 可以通过任意一轮轮密钥逆推出主密钥。AES-192 需要一轮半,AES-256 需要两轮。
五、故障对密文的影响
了解轮密钥可以逆推主密钥后,问题变为:如何获取轮密钥?
在 AES 加密过程中,如果在某时刻故意修改 state 中的某一个字节(通过 Frida、IDA、SO patching 等手段),会对最终密文产生有规律的影响。这类技术称为差分故障注入(Differential Fault Attack, DFA)。

5.1 起始处发生故障对密文的影响
初始轮密钥加阶段:故障仅限于一个字节:

第一轮字节替换:故障仍限于一个字节:

第一轮行移位:故障字节在第一行,无影响:

第一轮列混淆:由于矩阵乘法,一个字节的差异扩散到整列四个字节:

第一轮轮密钥加:不会进一步扩散差异:

一轮完整流程后,单一字节的错误扩展为四个字节的差异。进入第二轮:
第二轮字节替换:

第二轮行移位:四个差异字节分散到四个不同列:

第二轮列混淆:每列差异再次扩展,最终整个 state 全部受影响:

规律总结:
-
经过 2 次 MixColumns,起始处 1 字节的变化可扩展为全部 16 字节的差异 -
若省略 MixColumns,变化永远只影响 1 个字节 -
MixColumns 与 ShiftRows 共同实现了差分的有效扩散
5.2 倒数两个列混淆之间发生故障对密文的影响
DFA 攻击要求故障发生在倒数两次 MixColumns 之间,对应四个时机点:

这等价于分析”最后一次 MixColumns 前发生故障”的情况:

在后续五个处理步骤中,仅存在一次列混淆,因此故障导致四个特定字节的变化:
|
|
|
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|



故障影响范围总结:
-
倒数第二次 MixColumns 之前发生故障 → 影响全部 16 字节 -
倒数两次 MixColumns 之间发生故障 → 影响 4 字节(四种固定模式之一) -
最后一个 MixColumns 之后发生故障 → 只影响 1 字节
六、差分故障分析
6.1 数理分析
设最后一次 MixColumns 前 state 状态中,故障修改第一个字节(A 变为 X):

经过 MixColumns、轮密钥加 、SubBytes、ShiftRows、末轮轮密钥加 后,正常密文 和故障密文 的差异字节为:
6.2 差异点
对第一个字节的正常与故障值异或(注意这里”加”即异或):
令 ,(输入差分),化简得:
6.3 引入差分
-
输入差分:未知,来自故障注入 -
输出差分:已知(可观测)
类似地可以得到:
四个约束联立,可以缩小 的候选范围。以输出差分 0x10 为例:
# 遍历所有可能的 Z 和 Y0,检查是否满足输出差分约束
diff = 0x10
zlist = set()
for z in range(0x100):
for y0 in range(0x100):
# S(y0) XOR S(y0 XOR mul2(z)) == diff?
tmp = SBox[y0] ^ SBox[mul_by_02(z) ^ y0]
if tmp == diff:
zlist.add(z)
# 单个约束下满足条件的 Z 有 127 个
四个约束取交集后, 的候选值从 256 个缩小至 15 个:
zlist = set.intersection(z1list, z2list, z3list, z4list)
# result: 15 个候选 Z
# {193, 132, 68, 134, 7, 197, 105, 205, 109, 145, 82, 55, 59, 156, 61}
6.4 进一步探索
为何要求 Z? 因为当 候选范围缩小时,可以进一步约束 ,进而约束 :
单次注入后 候选有 30 个。关键在于:** 固定不变**,因此多次注入、取候选交集可不断缩小 的范围:
# 第一次注入(state[0][0] = 0)
klist1 = {131, 132, 11, 12, 19, 20, ...} # 30 个候选
# 第二次注入(state[0][0] = 1)
klist2 = {129, 130, 4, 7, 16, 19, ...} # 30 个候选
# 取交集
klist = set.intersection(klist1, klist2)
# result: {208, 19, 45} 只剩 3 个候选!
第三次注入后:
klist = set.intersection(klist1, klist2, klist3)
# result: {208} 即 0xd0,正是真实的 K10[0]
最终结论:
-
通过 8 次故障注入(4 列各 2 次),可在最优情况下完整还原 -
再通过密钥逆推,恢复主密钥 -
差分故障攻击是针对白盒 AES 实现的核心攻击手段
七、安卓逆向中识别 AES 算法
在实际安卓逆向中,识别目标 App 是否使用了 AES 加密,需要结合静态分析与动态分析两种手段。

7.1 Java/Kotlin 层静态特征
Java 层使用 AES 最常见的写法是通过 javax.crypto.Cipher 类。用 jadx 反编译 APK 后,搜索以下关键字符串:
// 最直接的标志:算法字符串
Cipher.getInstance("AES");
Cipher.getInstance("AES/CBC/PKCS5Padding");
Cipher.getInstance("AES/ECB/NoPadding");
Cipher.getInstance("AES/GCM/NoPadding");
// 密钥构造
new SecretKeySpec(keyBytes, "AES");
// IV(初始向量)
new IvParameterSpec(ivBytes);
jadx 中搜索方法: 在 Text search 中输入 "AES" 或 SecretKeySpec,即可定位加密代码。
常见 Android 加密工具类结构:
publicclassAESUtil{
// 密钥,通常硬编码或从 SharedPreferences / 服务端获取
privatestaticfinal String KEY = "0123456789abcdef";
// 初始向量
privatestaticfinal String IV = "fedcba9876543210";
publicstaticbyte[] encrypt(byte[] data) throws Exception {
// 注意这里的算法字符串是识别 AES 的关键
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKeySpec keySpec = new SecretKeySpec(KEY.getBytes(), "AES");
IvParameterSpec ivSpec = new IvParameterSpec(IV.getBytes());
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
return cipher.doFinal(data);
}
}
常见混淆规避:部分 App 会将算法字符串拆分或通过反射传入,例如:
// 字符串拼接混淆
String algo = "AES" + "/" + "CBC" + "/" + "PKCS5Padding";
Cipher c = Cipher.getInstance(algo);
// 反射混淆
Method m = Class.forName("javax.crypto.Cipher")
.getMethod("getInstance", String.class);
m.invoke(null, "AES/CBC/PKCS5Padding");
遇到此类混淆,静态分析难以直接定位,需结合动态 Hook。
7.2 Native 层静态特征
当 AES 在 Native 层(SO 文件)实现时,可以通过以下常量特征识别:
7.2.1 S 盒常量
AES S 盒第一行的字节序列是最显著的识别标志:
63 7C 77 7B F2 6B 6F C5 30 01 67 2B FE D7 AB 76
逆 S 盒(用于解密)第一行:
52 09 6A D5 30 36 A5 38 BF 40 A3 9E 81 F3 D7 FB
在 IDA 或 radare2 中直接搜索这两个字节序列,命中即可确认 AES。
7.2.2 Rcon 表
00 01 02 04 08 10 20 40 80 1B 36
7.2.3 0x1B 异或特征
列混淆中 运算有特征代码模式(xtime 函数):
// xtime 函数的典型汇编特征:与 0x1B 异或
uint8_txtime(uint8_t a){
return (a << 1) ^ (a & 0x80 ? 0x1B : 0x00);
}
在 IDA 中可以看到对常量 0x1B 的引用。
7.2.4 轮次循环结构
AES-128 有 10 轮主循环,搜索包含 10 次迭代的循环结构加上 SubBytes/ShiftRows/MixColumns 的组合调用,可以确认 AES。
7.2.5 常见库的 AES 函数名
# OpenSSL / BoringSSL(Android 常用)
AES_encrypt
AES_set_encrypt_key
EVP_EncryptInit_ex(参数中含 EVP_aes_128_cbc 等)
# 自定义实现(混淆后名称不确定,但仍有常量特征)
7.3 动态 Hook 识别(Frida)
动态分析是识别 AES 最稳定可靠的方式,不受混淆影响。
7.3.1 Hook Java 层 Cipher.getInstance
// Hook Cipher.getInstance,打印算法字符串
Java.perform(function () {
var Cipher = Java.use("javax.crypto.Cipher");
// Hook getInstance,捕获算法名
Cipher.getInstance.overload("java.lang.String").implementation = function (transformation) {
console.log("[*] Cipher.getInstance: " + transformation);
// 打印调用栈,定位代码位置
console.log(Java.use("android.util.Log")
.getStackTraceString(Java.use("java.lang.Exception").$new()));
returnthis.getInstance(transformation);
};
});
输出示例:
[*] Cipher.getInstance: AES/CBC/PKCS5Padding
7.3.2 Hook Cipher.init,获取密钥和 IV
Java.perform(function () {
var Cipher = Java.use("javax.crypto.Cipher");
var Arrays = Java.use("java.util.Arrays");
// init(mode, key, params) 重载
Cipher.init.overload(
"int",
"java.security.Key",
"java.security.spec.AlgorithmParameterSpec"
).implementation = function (opmode, key, params) {
// 提取密钥字节
var keyBytes = key.getEncoded();
console.log("[*] Cipher.init mode=" + opmode
+ " key=" + bytesToHex(keyBytes));
// 提取 IV(如果是 IvParameterSpec)
var IvParameterSpec = Java.use("javax.crypto.spec.IvParameterSpec");
if (params instanceof IvParameterSpec.$javaClass) {
var iv = Java.cast(params, IvParameterSpec).getIV();
console.log("[*] IV=" + bytesToHex(iv));
}
returnthis.init(opmode, key, params);
};
});
// 辅助:字节数组转十六进制字符串
functionbytesToHex(bytes) {
var hex = "";
for (var i = 0; i < bytes.length; i++) {
hex += ("0" + (bytes[i] & 0xFF).toString(16)).slice(-2);
}
return hex;
}
7.3.3 Hook doFinal,拦截明密文
Java.perform(function () {
var Cipher = Java.use("javax.crypto.Cipher");
Cipher.doFinal.overload("[B").implementation = function (input) {
console.log("[*] doFinal input (plaintext/ciphertext): "
+ bytesToHex(input));
var result = this.doFinal(input);
console.log("[*] doFinal output: " + bytesToHex(result));
return result;
};
});
7.4 常见 AES 特征常量速查
|
|
|
|---|---|
|
|
63 7C 77 7B |
|
|
52 09 6A D5 |
|
|
01 02 04 08 10 20 40 80 1B 36 |
|
|
0x1B |
|
|
|
|
|
|
|
|
|
|
|
|
八、安卓逆向中还原 AES 密钥
确认 AES 后,核心目标是获取密钥(key)和初始向量(IV)。

8.1 Java 层密钥提取(Frida)
最简单直接的方式是 Hook SecretKeySpec 构造函数:
Java.perform(function () {
var SecretKeySpec = Java.use("javax.crypto.spec.SecretKeySpec");
// Hook SecretKeySpec(byte[] key, String algorithm)
SecretKeySpec.$init.overload("[B", "java.lang.String")
.implementation = function (keyBytes, algorithm) {
console.log("[*] SecretKeySpec algorithm=" + algorithm);
console.log("[*] Key bytes: " + bytesToHex(keyBytes));
// 打印调用栈定位代码
console.log(Java.use("android.util.Log")
.getStackTraceString(
Java.use("java.lang.Exception").$new()));
returnthis.$init(keyBytes, algorithm);
};
});
如果密钥通过 KeyGenerator 动态生成,则 Hook:
Java.perform(function () {
var KeyGenerator = Java.use("javax.crypto.KeyGenerator");
KeyGenerator.generateKey.implementation = function () {
var key = this.generateKey();
console.log("[*] Generated key: "
+ bytesToHex(key.getEncoded()));
return key;
};
});
8.2 Native 层密钥提取
8.2.1 Hook OpenSSL / BoringSSL
Android 的 libcrypto.so 是 BoringSSL(Google fork of OpenSSL),可直接 Hook AES 相关函数:
// Hook AES_set_encrypt_key(userKey, bits, key)
var libcrypto = Module.findExportByName("libcrypto.so",
"AES_set_encrypt_key");
if (libcrypto) {
Interceptor.attach(libcrypto, {
onEnter: function (args) {
var keyLen = args[1].toInt32() / 8; // bits -> bytes
// args[0] 是 userKey 指针
console.log("[*] AES_set_encrypt_key key="
+ hexdump(args[0], { length: keyLen }));
}
});
}
// Hook EVP_EncryptInit_ex(ctx, type, impl, key, iv)
var evpInit = Module.findExportByName("libcrypto.so",
"EVP_EncryptInit_ex");
if (evpInit) {
Interceptor.attach(evpInit, {
onEnter: function (args) {
// args[3] 是 key 指针,args[4] 是 iv 指针
// 需要先确认 type 是 AES(args[1] 不为 NULL 时检查)
if (!args[3].isNull()) {
console.log("[*] EVP key="
+ hexdump(args[3], { length: 16 }));
}
if (!args[4].isNull()) {
console.log("[*] EVP IV="
+ hexdump(args[4], { length: 16 }));
}
}
});
}
8.2.2 Hook 自定义 AES 实现
若 SO 中包含自定义 AES,可先定位关键函数(通过 S 盒地址交叉引用),再 Hook 入参:
// 通过模块基址 + 函数偏移的方式 Hook 自定义 AES
var base = Module.findBaseAddress("libnative.so");
var aesEncryptOffset = 0x12345; // 从 IDA/Ghidra 中找到的偏移
Interceptor.attach(base.add(aesEncryptOffset), {
onEnter: function (args) {
// 根据函数签名解析 key/plaintext 指针
console.log("[*] custom AES key: "
+ hexdump(args[1], { length: 16 }));
}
});
8.2.3 内存 Dump 轮密钥
如果直接 Hook 困难,可以在 AES 加密过程中 Dump 内存。由于轮密钥扩展后驻留在内存中,可扫描内存特征:
# Frida 脚本:搜索内存中的 AES 轮密钥特征
# S 盒前 4 字节的十六进制:63 7C 77 7B
import frida
deffind_aes_keys(session):
script = session.create_script("""
// 遍历所有内存段,查找 S 盒特征
Process.enumerateRanges('r--').forEach(function(range) {
try {
var mem = Memory.readByteArray(range.base, range.size);
var arr = new Uint8Array(mem);
for (var i = 0; i < arr.length - 4; i++) {
if (arr[i]==0x63 && arr[i+1]==0x7C
&& arr[i+2]==0x77 && arr[i+3]==0x7B) {
console.log("[*] Possible S-box at: "
+ range.base.add(i));
}
}
} catch(e) {}
});
""")
script.load()
8.3 白盒 AES 场景下的密钥还原(DFA)
白盒 AES 是将密钥”融入”查找表的实现方式,无法直接提取密钥,但可通过 DFA 攻击还原 ,进而逆推主密钥。
操作步骤(参考第六章的数理分析):
-
定位 AES 加密函数,找到第 9 轮 MixColumns 前的 state 写入点 -
用 Frida 修改该时机处 state 的某一字节(故障注入) -
多次注入不同故障值,收集正常密文和故障密文对 -
运行 DFA 分析脚本,求各 字节候选值的交集 -
得到 后,按第四章的逆推流程还原主密钥
关键 Frida 注入代码:
// 在第 9 轮 MixColumns 前注入故障
// offset_state: state 数组在内存中的地址(由 IDA 分析确定)
Interceptor.attach(ptr(target_address), {
onEnter: function (args) {
// 修改 state[0][0] 为指定故障值
var statePtr = this.context.x0; // 根据架构调整寄存器
Memory.writeU8(statePtr, fault_value);
}
});
九、AES 变种算法介绍
9.1 按密钥长度:AES-128 / AES-192 / AES-256
|
|
|
|
|
|
|---|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
三者的核心结构(SubBytes / ShiftRows / MixColumns / AddRoundKeys)完全一致,区别仅在密钥长度和轮数。
安卓逆向识别技巧:可通过循环轮数区分版本——10 轮为 AES-128,12 轮为 AES-192,14 轮为 AES-256。
密钥逆推差异:
-
AES-128:任意 1 轮轮密钥即可逆推主密钥 -
AES-192:需要 1.5 轮轮密钥 -
AES-256:需要 2 轮轮密钥
9.2 按工作模式:ECB / CBC / CFB / OFB / CTR / GCM
工作模式决定了多个 16 字节块如何连接,以及是否需要 IV。
9.2.1 ECB(电子密码本模式)
最简单的模式,每个 16 字节块独立加密,相同明文块产生相同密文块。

安全缺陷:相同明文总产生相同密文,无语义安全,不推荐用于加密图像等有规律的数据。逆向特征:无 IV,加密 16 字节整数倍数据,模式字符串为 "AES/ECB/...".
9.2.2 CBC(密码块链接模式)
最常用的模式,每块明文在加密前先与前一块密文异或(第一块与 IV 异或):

特点:需要 IV,且 IV 应随机生成;解密可并行,加密串行。逆向特征:有 16 字节 IV,常见于 "AES/CBC/PKCS5Padding"。
9.2.3 CFB(密码反馈模式)
将 AES 用作流密码,以密文作为下一块加密的输入:

特点:可加密非整数倍长度的数据,误差传播有限。
9.2.4 OFB(输出反馈模式)
与 CFB 类似,但每次用密钥流而非密文作为下一轮输入:

9.2.5 CTR(计数器模式)
将计数器值(Nonce + Counter)加密后产生密钥流,再与明文异或:

特点:加解密均可完全并行,无需填充,是高性能场景的首选。逆向特征:通常有 8 字节 Nonce + 8 字节 Counter,模式字符串 "AES/CTR/NoPadding".
9.2.6 GCM(Galois/Counter Mode,伽罗华计数器模式)
CTR 模式 + GHASH 认证,同时提供加密和完整性校验(MAC):

特点:同时提供机密性和完整性保证(AEAD),现代通信协议首选(TLS 1.3 默认使用)。逆向特征:密文后附有 16 字节认证 Tag,API 为 "AES/GCM/NoPadding",初始化参数使用 GCMParameterSpec.
// GCM 模式 Java 示例
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec paramSpec = new GCMParameterSpec(128, iv); // 128-bit tag
cipher.init(Cipher.ENCRYPT_MODE, secretKey, paramSpec);
byte[] ciphertext = cipher.doFinal(plaintext);
// 输出 = 真正密文 + 16 字节 Tag
工作模式对比总览
|
|
|
|
|
|
|
|
|
|---|---|---|---|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
是 | 首选 |
9.3 白盒 AES(WBAES,White-Box AES)
标准 AES 的安全前提是密钥保密,但在白盒攻击模型中,攻击者可以完整观察和控制执行环境(调试器、内存读取等)。白盒 AES 的目标是在密钥完全暴露于攻击者可见的执行环境下,仍然保护密钥安全。
实现原理(Chow 等人 2002 年方案):
将 AES 的所有操作(SubBytes、MixColumns、AddRoundKeys)合并成巨大的查找表(Look-Up Table),使密钥与操作融合,无法直接从表中提取:
传统 AES:
SubBytes(state[i]) XOR K[i] → 独立的 S 盒 + 密钥异或
白盒 AES:
T[state[i]] → 密钥已融入表中,无法分离
白盒 AES 的安全问题:
尽管白盒方案提高了攻击门槛,但仍可通过以下方法破解:
-
DFA(差分故障攻击):如第六章所述,通过注入故障还原 -
DCA(差分计算分析):统计分析白盒实现中的中间值泄漏 -
BGE 攻击:针对 Chow 方案的代数攻击,可直接还原等价密钥
安卓逆向中的特征:
-
出现大量(GB 级别)的查找表数据段 -
无明显的 S 盒常量(已融入表中) -
加密函数可能包含数千个 Table Lookup 操作 -
常见于 DRM 保护(如 Widevine)、金融 App 核心加密
识别方法:
-
通过 Frida Hook 输入输出(黑盒分析),观察是否符合 AES 行为 -
使用开源工具 Deadpool 进行 DFA 自动化攻击
9.4 魔改 AES(Custom S-Box AES)
部分安全意识较强的 App 会对标准 AES 进行魔改,最常见的是替换 S 盒:
# 标准 AES S 盒(开头几个字节)
SBOX_STD = [0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, ...]
# 魔改 S 盒(开头字节已不同,无法用标准常量识别)
SBOX_CUSTOM = [0x52, 0x09, 0x6A, 0xD5, 0x30, ...]
其他常见魔改方式:
-
修改 Rcon 表 -
改变 MixColumns 的固定矩阵 -
增减加密轮数 -
调整 ShiftRows 的移位量
安卓逆向识别思路:
1. 标准 S 盒常量搜索失败 → 怀疑魔改
2. 搜索 256 字节的查找表(大小特征不变)
3. 验证:找到疑似 S 盒后,检查其是否为全排列(每个 0-255 恰好出现一次)
4. 若是全排列 → 基本确认是魔改 AES 的自定义 S 盒
5. 提取自定义 S 盒后,用它替换标准 S 盒进行解密
# 验证是否为有效 S 盒(全排列性质)
defis_valid_sbox(data: bytes) -> bool:
if len(data) != 256:
returnFalse
return sorted(data) == list(range(256))
9.5 SM4(类 AES 国密算法)
SM4 是中国国家密码局制定的分组加密标准(GB/T 32907-2016),在结构上与 AES 非常相似,但并非 AES。
与 AES 的相似之处:
-
同为分组密码,块大小均为 128 位(16 字节) -
均使用 S 盒替换、行/列扩散操作 -
密钥扩展机制类似
主要区别:
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
32 轮 |
|
|
|
|
|
|
|
|
|
|
|
不同
D6 90 E9 FE CC |
|
|
|
|
|
|
|
|
SM4 S 盒识别特征:
D6 90 E9 FE CC E1 3D B7 16 B6 14 C2 28 FB 2C 05
安卓逆向中的 SM4:
-
使用 BouncyCastle库时,算法字符串为"SM4"或"SM4/CBC/PKCS5Padding" -
使用国密 SDK(如腾讯 TENCENT_SM4、BaiduSM4)时,可能封装在 Native 层 -
因为 32 轮循环结构,Native 实现中容易通过循环次数(32 次)识别 -
SM4 S 盒同样满足全排列性质,可用上述方法验证
总结:本文从 AES 基础原理出发,深入分析了密钥编排、明文运算、密钥逆推、差分故障攻击,并在此基础上从安卓逆向视角补充了:识别 AES 的静态与动态方法、各类密钥提取技术,以及 AES 各变种(ECB/CBC/GCM/白盒/魔改/SM4)的特征和逆向要点。差分故障攻击(DFA)是针对白盒 AES 还原密钥的核心技术,其后续可进一步研究白盒算法的完整攻击链。
夜雨聆风