乐于分享
好东西不私藏

AES 加密算法详解:从原理到安卓逆向实战

AES 加密算法详解:从原理到安卓逆向实战

本文在原理讲解基础上,增加了安卓逆向视角:如何识别 AES、如何还原密钥,以及各变种算法介绍。


目录

  1. AES 简介
  2. 密钥编排
  3. 明文运算
  4. 密钥分析
  5. 故障对密文的影响
  6. 差分故障分析(DFA)
  7. 安卓逆向中识别 AES 算法
  8. 安卓逆向中还原 AES 密钥
  9. AES 变种算法介绍

一、AES 简介

AES(Advanced Encryption Standard,高级加密标准)是一种对称加密算法,被广泛应用于数据加密领域。它是美国国家标准与技术研究院(NIST)于 2001 年指定的新加密标准,用来替代原有的 DES(Data Encryption Standard)。

AES 支持三种密钥长度:

版本
密钥长度
加密轮数
AES-128
128 位(16 字节)
10 轮
AES-192
192 位(24 字节)
12 轮
AES-256
256 位(32 字节)
14 轮

本文重点讨论 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 加密流程图如下:

AES加密流程图

整个过程分为两个主要部分:明文处理密钥编排

  • 密钥编排将 16 字节主密钥扩展为 11 组轮密钥 ,每组 16 字节
  • 明文处理包含一次初始轮密钥加,加上 10 轮主运算

二、密钥编排

密钥扩展将主密钥 2b7e151628aed2a6abf7158809cf4f3c 扩展为 176 字节序列,包含 11 个子密钥 ,共 44 个四字节单元( 数组):

W数组结构

主密钥被切分为  数组的前四个元素:

2.1 黄色区域(

每隔 4 个元素的列(图中黄色),计算规则需要用到  函数:

 函数三个步骤:

第一步:循环左移一字节(RotWord)

将最左边的字节移到最右边:

循环左移

第二步:S 盒替换(SubWord)

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

S盒
S盒替换过程

第三步:与轮常数 Rcon 异或

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

Rcon 表:

Rcon表

异或过程:

字节异或过程

以  为例,完整计算如下:

2.2 红色区域(其余列)

去掉  函数,直接与前四个元素异或:

例如:


三、明文运算

AES 算法中,数据以 state(状态矩阵) 的形式进行计算和传输。明文按从上到下、从左到右的顺序填入 4×4 的状态矩阵:

明文转state

3.1 轮密钥加(AddRoundKeys)

将当前轮密钥与 state 逐字节异或:

轮密钥加示意

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

初始轮密钥加K0

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

十轮运算流程

3.2 字节替换(SubBytes)

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

SubBytes

3.3 行移位(ShiftRows)

循环左移,目的是实现字节在行方向上的扩散:

  • 第 1 行:不移位
  • 第 2 行:循环左移 1 字节
  • 第 3 行:循环左移 2 字节
  • 第 4 行:循环左移 3 字节
ShiftRows

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 分析

下面验证 (即  的第一个字)。

  1. 循环左移:575c006e → 5c006e57
  2. S 盒替换(逐字节查表):
>>> Sbox = (0x630x7C0x770x7B, ...)
>>> hex(Sbox[0x5c])  # '0x4a'
>>> hex(Sbox[0x00])  # '0x63'
>>> hex(Sbox[0x6e])  # '0x9f'
>>> hex(Sbox[0x57])  # '0x5b'
  1. 首字节与 Rcon 第 10 个元素 0x36 异或:
>>> hex(0x4a ^ 0x36)
'0x7c'
  1.  函数结果为 7c639f5b,与  异或:
>>> hex(0x7c639f5b ^ 0xd014f9a8)
'0xac7766f3'# 正是 W36

结论:AES-128 可以通过任意一轮轮密钥逆推出主密钥。AES-192 需要一轮半,AES-256 需要两轮。


五、故障对密文的影响

了解轮密钥可以逆推主密钥后,问题变为:如何获取轮密钥?

在 AES 加密过程中,如果在某时刻故意修改 state 中的某一个字节(通过 Frida、IDA、SO patching 等手段),会对最终密文产生有规律的影响。这类技术称为差分故障注入(Differential Fault Attack, DFA)

DFA攻击示意

5.1 起始处发生故障对密文的影响

初始轮密钥加阶段:故障仅限于一个字节:

初始轮密钥加故障

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

第一轮字节替换故障

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

行移位故障

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

列混淆扩散

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

轮密钥加

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

第二轮字节替换

第二轮字节替换

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

第二轮行移位

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

第二轮列混淆

规律总结

  • 经过 2 次 MixColumns,起始处 1 字节的变化可扩展为全部 16 字节的差异
  • 若省略 MixColumns,变化永远只影响 1 个字节
  • MixColumns 与 ShiftRows 共同实现了差分的有效扩散

5.2 倒数两个列混淆之间发生故障对密文的影响

DFA 攻击要求故障发生在倒数两次 MixColumns 之间,对应四个时机点:

四个攻击时机

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

最后一次列混淆前故障

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

故障发生列
最终受影响的密文字节
第 1 列
字节 1、8、11、14
第 2 列
字节 2、5、12、15
第 3 列
字节 3、6、9、16
第 4 列
字节 4、7、10、13
第一列故障
第三列故障
第四列故障

故障影响范围总结

  • 倒数第二次 MixColumns 之前发生故障 → 影响全部 16 字节
  • 倒数两次 MixColumns 之间发生故障 → 影响 4 字节(四种固定模式之一)
  • 最后一个 MixColumns 之后发生故障 → 只影响 1 字节

六、差分故障分析

6.1 数理分析

设最后一次 MixColumns 前 state 状态中,故障修改第一个字节(A 变为 X):

故障注入state

经过 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 = {13113211121920, ...}   # 30 个候选

# 第二次注入(state[0][0] = 1)
klist2 = {129130471619, ...}     # 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 加密,需要结合静态分析与动态分析两种手段。

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 特征常量速查

特征
S 盒起始 4 字节
63 7C 77 7B
逆 S 盒起始 4 字节
52 09 6A D5
Rcon 表
01 02 04 08 10 20 40 80 1B 36
xtime 模约简常数
0x1B
AES-128 主循环次数
10
AES-192 主循环次数
12
AES-256 主循环次数
14
块大小
16 字节

八、安卓逆向中还原 AES 密钥

确认 AES 后,核心目标是获取密钥(key)初始向量(IV)

AES密钥还原方法

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, {
onEnterfunction (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, {
onEnterfunction (args{
// args[3] 是 key 指针,args[4] 是 iv 指针
// 需要先确认 type 是 AES(args[1] 不为 NULL 时检查)
if (!args[3].isNull()) {
console.log("[*] EVP key="
                            + hexdump(args[3], { length16 }));
            }
if (!args[4].isNull()) {
console.log("[*] EVP IV="
                            + hexdump(args[4], { length16 }));
            }
        }
    });
}

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), {
onEnterfunction (args{
// 根据函数签名解析 key/plaintext 指针
console.log("[*] custom AES key: "
                    + hexdump(args[1], { length16 }));
    }
});

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 攻击还原 ,进而逆推主密钥。

操作步骤(参考第六章的数理分析):

  1. 定位 AES 加密函数,找到第 9 轮 MixColumns 前的 state 写入点
  2. 用 Frida 修改该时机处 state 的某一字节(故障注入)
  3. 多次注入不同故障值,收集正常密文和故障密文对
  4. 运行 DFA 分析脚本,求各  字节候选值的交集
  5. 得到  后,按第四章的逆推流程还原主密钥

关键 Frida 注入代码:

// 在第 9 轮 MixColumns 前注入故障
// offset_state: state 数组在内存中的地址(由 IDA 分析确定)
Interceptor.attach(ptr(target_address), {
onEnterfunction (args{
// 修改 state[0][0] 为指定故障值
var statePtr = this.context.x0;  // 根据架构调整寄存器
        Memory.writeU8(statePtr, fault_value);
    }
});

九、AES 变种算法介绍

9.1 按密钥长度:AES-128 / AES-192 / AES-256

版本
密钥长度
加密轮数
扩展密钥长度
安全性
AES-128
128 位(16 B)
10
176 字节
AES-192
192 位(24 B)
12
208 字节
很高
AES-256
256 位(32 B)
14
240 字节
最高

三者的核心结构(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 字节块独立加密,相同明文块产生相同密文块。

ECB模式原理图

安全缺陷:相同明文总产生相同密文,无语义安全,不推荐用于加密图像等有规律的数据。逆向特征:无 IV,加密 16 字节整数倍数据,模式字符串为 "AES/ECB/...".

9.2.2 CBC(密码块链接模式)

最常用的模式,每块明文在加密前先与前一块密文异或(第一块与 IV 异或):

CBC模式原理图

特点:需要 IV,且 IV 应随机生成;解密可并行,加密串行。逆向特征:有 16 字节 IV,常见于 "AES/CBC/PKCS5Padding"

9.2.3 CFB(密码反馈模式)

将 AES 用作流密码,以密文作为下一块加密的输入:

CFB模式原理图

特点:可加密非整数倍长度的数据,误差传播有限。

9.2.4 OFB(输出反馈模式)

与 CFB 类似,但每次用密钥流而非密文作为下一轮输入:

OFB模式原理图

9.2.5 CTR(计数器模式)

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

CTR模式原理图

特点:加解密均可完全并行,无需填充,是高性能场景的首选。逆向特征:通常有 8 字节 Nonce + 8 字节 Counter,模式字符串 "AES/CTR/NoPadding".

9.2.6 GCM(Galois/Counter Mode,伽罗华计数器模式)

CTR 模式 + GHASH 认证,同时提供加密完整性校验(MAC)

GCM模式原理图

特点:同时提供机密性和完整性保证(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

工作模式对比总览

模式
需要 IV
可并行加密
可并行解密
误差传播
需要填充
提供认证
推荐度
ECB
不推荐
CBC
有限
常用
CFB
有限
一般
OFB
一般
CTR
推荐
GCM
首选

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 的安全问题

尽管白盒方案提高了攻击门槛,但仍可通过以下方法破解:

  1. DFA(差分故障攻击):如第六章所述,通过注入故障还原 
  2. DCA(差分计算分析):统计分析白盒实现中的中间值泄漏
  3. 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 = [0x630x7C0x770x7B0xF20x6B, ...]

# 魔改 S 盒(开头字节已不同,无法用标准常量识别)
SBOX_CUSTOM = [0x520x090x6A0xD50x30, ...]

其他常见魔改方式

  • 修改 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 盒替换、行/列扩散操作
  • 密钥扩展机制类似

主要区别

特性
AES-128
SM4
密钥长度
128/192/256 位
仅 128 位
加密轮数
10 轮
32 轮
块大小
128 位
128 位
S 盒大小
16×16(256 项)
16×16(256 项)
S 盒内容
不同
不同

,起始字节:D6 90 E9 FE CC
线性变换
MixColumns(矩阵乘法)
32 位线性变换 L
Rcon
无(使用 FK/CK 系统密钥常数)

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 还原密钥的核心技术,其后续可进一步研究白盒算法的完整攻击链。

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » AES 加密算法详解:从原理到安卓逆向实战

猜你喜欢

  • 暂无文章