1. 项目概述:为什么AES是逆向工程师的必修课?
如果你在逆向分析一个软件、一个网络协议,或者一个固件镜像时,看到一堆看似毫无规律的十六进制数据流,或者程序在内存中对某段数据进行了一连串复杂的“变换”操作,那么你大概率已经撞上了加密算法。而在这些加密算法中,AES(Advanced Encryption Standard,高级加密标准)的出现频率,高到几乎可以称之为“行业标配”。无论是桌面软件的注册验证、移动App的通信保护、游戏资源的加密打包,还是物联网设备的固件加密,AES的身影无处不在。
因此,对于从事安全研究、逆向工程、漏洞挖掘甚至只是对软件内部机制好奇的开发者而言,深入理解AES不再是一个“加分项”,而是一项“生存技能”。仅仅知道AES是“一种对称加密算法”是远远不够的。你需要清楚地知道它是如何一步步把明文变成密文的(加密原理),在逆向时如何从二进制代码或内存数据中识别出它(特征识别),以及面对被AES保护的数据时,有哪些思路可以尝试去还原或绕过它(实战应用)。这就是我们这次要深入探讨的核心: 拆解AES的算法原理,并聚焦于其在逆向实战中的具体应用场景与分析方法。 本系列的第一部分,我们将夯实基础,彻底搞懂AES的“内功心法”。
2. AES加密算法核心原理深度拆解
要逆向分析一个东西,你必须先知道它是怎么正向工作的。AES算法是一个结构清晰、步骤明确的迭代分组密码。我们抛开最复杂的数学证明,用工程师的视角来理解它的每一步。
2.1 算法基础与核心概念
AES加密的对象是“分组”,每个分组固定为 128位(16字节) 。密钥长度则可以是128位、192位或256位,分别对应AES-128, AES-192, AES-256。密钥越长,安全性理论上越高,加解密的轮数也越多(10轮、12轮、14轮)。在逆向场景中,AES-128最为常见。
它的加密过程,可以看作对16字节的“状态矩阵”进行多轮(Round)的变换。每一轮都包含四个基本操作(最后一轮略有不同):
- SubBytes(字节替换) :一个非线性变换,通过一个称为S盒的查找表,将状态矩阵中的每一个字节替换成另一个字节。这是AES混淆性的主要来源。
- ShiftRows(行移位) :状态矩阵有4行4列。这个操作将矩阵的每一行进行循环左移,第0行不移,第1行左移1字节,第2行左移2字节,第3行左移3字节。这一步增加了扩散性。
- MixColumns(列混合) :将状态矩阵的每一列视为一个向量,与一个固定的矩阵在有限域GF(2^8)上进行乘法运算。这一步是算法中数学性最强的部分,极大地增强了扩散效果。
- AddRoundKey(轮密钥加) :将当前的状态矩阵与一个本轮专用的“轮密钥”进行逐字节的异或(XOR)操作。轮密钥是由初始密钥通过密钥扩展算法派生出来的。
加密开始时,会先进行一次初始的AddRoundKey(使用第0个轮密钥)。然后进行N-1轮完整的上述四步操作,最后一轮则省略MixColumns操作。解密过程就是加密过程的逆序,使用逆变换和逆轮密钥。
注意 :对于逆向分析,你不需要手算一遍列混合。关键是要理解这些操作在代码和内存中的“痕迹”。例如,一个256字节的常量数组(S盒)的访问、对16字节数据块进行的固定模式的移位、以及一系列异或和乘法操作,都是强烈的AES特征。
2.2 密钥扩展算法:轮密钥的生成奥秘
初始密钥只有128/192/256位,但每一轮都需要一个128位的轮密钥。密钥扩展算法就是负责“生产”这些轮密钥的工厂。它的核心也是一个迭代过程,涉及字节的循环移位、S盒替换以及与轮常数的异或。
对于AES-128,初始密钥被分成4个32位的字(W[0], W[1], W[2], W[3])。后续的轮密钥字W[i]由前面的字推导而来。具体规则是:对于i是4的倍数时,W[i] = W[i-4] ⊕ SubWord(RotWord(W[i-1])) ⊕ Rcon[i/4];否则,W[i] = W[i-4] ⊕ W[i-1]。其中RotWord是循环左移,SubWord是用S盒替换每个字节,Rcon是轮常数。
为什么逆向时要关注密钥扩展? 因为在实际软件中,为了效率,程序可能会选择“预计算”所有轮密钥并存储在内存或全局变量中。如果你在静态分析时,发现程序初始化阶段生成了一个远长于初始密钥的字节数组(对于AES-128,10轮需要11个轮密钥,共176字节),这几乎就是AES的铁证。动态调试时,在加密函数调用前下断点,观察传入的密钥数据附近的内存,也常常能找到扩展后的轮密钥,这为后续的密钥提取或推断提供了可能。
2.3 工作模式:算法如何应对大量数据?
AES本身只能加密16字节的块。要加密一个文件、一段消息,就需要选择一种“工作模式”。不同的模式在逆向中会呈现出不同的数据流特征。
- ECB(电子密码本) :最简单的模式,每个16字节块独立加密。 致命缺点 :相同的明文块会产生相同的密文块。在逆向中,如果你发现密文数据中存在大量重复的16字节片段,那很可能就是ECB模式。例如,加密了一张BMP图片的纯色背景区域,在密文中就能看到明显的规律性图案残留。
- CBC(密码分组链接) :最常用的模式之一。每个明文块在加密前,先与前一个密文块进行异或(第一个块与一个初始化向量IV异或)。 逆向要点 :你需要同时找到**密钥(Key) 和 初始化向量(IV)**才能正确解密。IV可能硬编码在代码里,也可能在数据流头部传递。
- CTR(计数器) :一种将分组密码转换为流密码的模式。它加密一个计数器序列,然后将结果与明文异或。 逆向特点 :加解密过程对称,无需反向算法。关键点是找到**计数器(Nonce/IV)**和其生成规律。
- 其他模式 :如CFB、OFB等,原理类似,都是通过反馈机制将分组密码转化为流式加密。
在实战中,识别工作模式是解密的第一步。通过观察数据块之间的依赖关系、寻找可能的IV、分析代码中是否包含异或反馈逻辑,可以做出判断。
3. 逆向实战中识别AES算法的关键特征
当面对一个未知二进制文件时,如何快速判断它是否使用了AES加密?以下是一些在静态分析和动态调试中非常实用的“指纹”特征。
3.1 静态分析特征:代码与数据中的“蛛丝马迹”
-
常量S盒与逆S盒
:这是最显著的特征。AES的S盒和逆S盒是固定的256字节常量数组。在IDA Pro、Ghidra等反编译工具中,如果你在数据段(.rdata, .data)发现大段的、看起来随机但固定的256字节数组,极有可能就是S盒。你可以尝试搜索这些数组的字节序列(例如,S盒的前几个字节是
0x63, 0x7C, 0x77, 0x7B...),互联网上有现成的特征码可供匹配。 -
轮常数Rcon
:另一个固定的小数组,通常有10个或更多32位整数。其值序列(
0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1B, 0x36...)也是很好的识别标志。 - 特定的运算组合 :在函数中观察到密集的字节替换(查表)、32位整数的循环移位(特别是左移1、2、3位)、以及异或操作,这些操作组合在一起,高度提示了AES的ShiftRows和MixColumns的优化实现(例如使用查表优化的T-boxes)。
- 密钥扩展代码 :寻找一个循环结构,它读取密钥数组,进行移位、查表(S盒)、异或操作,并输出一个更长的密钥数组。这段代码相对独立,常被单独作为一个初始化函数。
-
函数名与字符串
:在未剥离符号的二进制文件(如某些Linux动态库)或调试版本中,可能会直接出现
AES_encrypt,AES_set_encrypt_key,SubBytes,MixColumns等函数名或字符串。使用开源库(如OpenSSL, Crypto++)的程序,其函数调用模式也有规律可循。
3.2 动态调试特征:运行时行为的“现场取证”
- 内存中的轮密钥 :在加密/解密函数调用前设置断点,观察传入的“密钥”参数指向的数据。如果该数据长度超过16/24/32字节(例如176字节对应AES-128扩展密钥),那么你很可能找到了扩展后的轮密钥。这比只找到原始密钥更有价值。
- 固定的数据块操作 :单步跟踪时,注意观察程序是否以16字节为单位循环处理输入缓冲区。每次循环内部,对一块16字节的数据进行一系列复杂操作后,再处理下一块。这是分组密码的典型行为。
- S盒访问模式 :在调试器中,你可以监视对疑似S盒数组的内存访问。如果看到程序在循环中,以明文字节或中间状态字节为索引,反复读取一个固定的256字节数组,这几乎可以实锤。
- 工作模式痕迹 :对于CBC模式,观察在加密第一个块之前,是否有一个16字节的数据(IV)与明文块进行异或。对于CTR模式,可能会观察到一个计数器在不断递增,并被加密后用于异或。
3.3 工具辅助识别
-
IDA Pro插件
:如
FindCrypt、IDA-Signsrch,它们内置了各种加密算法(包括AES的S盒、轮常数)的签名库,能自动扫描二进制文件并提示可能的位置。 -
二进制分析框架
:如
radare2、Binary Ninja,也具备类似的模式识别功能。 -
熵值分析
:如果一段数据经过AES加密,其熵值(随机性)会非常高。使用
binwalk -E或ent工具分析文件,高熵段可能指示加密区域。但这只是一个辅助线索,压缩数据熵值也高。
4. 实战场景剖析:从内存DUMP到密钥提取
理论说再多,不如看一个简化但典型的实战场景。假设我们有一个Windows桌面软件,它的VIP功能相关数据在内存中被加密了。我们的目标是找到密钥并解密它。
4.1 场景建立与初步分析
我们通过调试器附加到目标进程。我们知道,当点击“查看VIP信息”按钮时,程序会从服务器或本地文件读取一段密文,解密后显示。我们的突破口就在这个解密函数。
-
定位解密函数 :
- 字符串交叉引用 :如果软件有“解密错误”、“数据损坏”等提示字符串,在IDA中查找这些字符串,并回溯引用它们的函数。
-
API监控
:对
ReadFile、InternetReadFile等读取数据的API下断点,获取密文缓冲区地址。然后对该内存地址设置硬件写入断点,跟踪后续是哪个函数读取并处理了它。 - 行为推测 :解密函数必然在显示函数之前被调用。在显示VIP信息的UI代码附近下断点,然后反向单步跟踪,找到数据处理逻辑。
-
识别加密算法 : 进入可疑函数后,我们进行静态和动态结合的分析:
- 观察函数开头,是否有一个16/24/32字节的密钥被加载或传入。
-
在函数内部数据区,搜索
0x63, 0x7C, 0x77...序列,确认S盒存在。 -
动态调试时,输入一个自定义的16字节测试数据(如全零
0x00),单步执行,观察输出。同时,用Python的pycryptodome库写一个AES-ECB加密全零的脚本。如果调试器里计算出的中间状态(例如第一轮轮密钥加之后的状态)与我们脚本计算的一致,那么算法和密钥就都确认了。
4.2 密钥的存储与获取方式
软件不会明文存储密钥。常见的保护方式有:
- 硬编码(最简单) :密钥以字节数组形式直接写在代码的.data或.rdata段。用IDA查看字符串或交叉引用可能找到。可能是原始密钥,也可能是经过简单变换(如Base64编码、与固定值异或)后的形式。
- 运行时计算(中等) :密钥由多个字符串片段拼接、或通过某种算法(如哈希)从用户输入、机器特征(硬盘序列号、MAC地址)派生而来。这需要逆向密钥生成算法。
- 白盒加密(困难) :将密钥与算法本身深度混淆,密钥被编码在庞大的查找表中,与算法执行过程融为一体。这是专门的对抗逆向技术,分析难度极大。在移动应用(尤其是金融类App)和游戏保护中常见。
在我们的假设场景中,最可能的是方式1或2。假设我们发现密钥是硬编码的,但被一个简单的
XOR 0xAA
处理过。我们在内存中找到了处理后的密钥字节数组
byte_407030
。通过调试,我们发现解密函数在调用AES解密前,先对这个数组的每个字节进行
xor 0xAA
操作。那么,我们只需要在Python中模拟这个操作,就能得到真实密钥。
# 从IDA或内存dump出的混淆后密钥
obfuscated_key = bytes.fromhex('DE AD BE EF ... ') # 示例
real_key = bytes([b ^ 0xAA for b in obfuscated_key])
print(f"Real Key: {real_key.hex()}")
4.3 解密流程还原与验证
拿到密钥后,还需要确定工作模式和IV。
- 确定模式 :观察解密函数。如果它在一个循环中,每次处理16字节,且当前块的解密结果直接输出,没有与下一个密文块进行异或,可能是ECB。如果发现解密后的块与前一个密文块进行了异或,那就是CBC,并且前一个密文块(对第一个块而言就是IV)需要找到。
-
寻找IV
:对于CBC模式,IV可能:
- 硬编码在密钥附近。
- 存放在加密数据文件的头部。
- 由某个固定值(如全零)派生。 在动态调试中,在解密函数开始处,查看与第一个密文块进行异或的数据是什么,那就是IV。
-
编写解密脚本
:使用
pycryptodome或cryptography库进行最终解密。
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad # 如果加密时用了填充
# 假设我们已获得
key = b'ThisIsASecretKey' # 16字节 for AES-128
iv = b'InitializationV' # 16字节 for CBC
ciphertext = open('encrypted_data.bin', 'rb').read()
# 创建解密器
cipher = AES.new(key, AES.MODE_CBC, iv)
# 解密并去除填充(例如PKCS7)
plaintext = unpad(cipher.decrypt(ciphertext), AES.block_size)
print(plaintext.decode('utf-8', errors='ignore'))
实操心得
:很多时候,解密出来的数据可能仍是二进制格式(如序列化的结构体、压缩数据等)。不要期望总是得到可读字符串。用
file
命令或
binwalk
分析解密后的数据,判断其真实类型,可能是下一步逆向的开始。
5. 逆向分析中的常见问题与高级技巧
即使找到了AES,逆向之路也未必一帆风顺。下面是一些常见坑点和进阶思路。
5.1 典型问题排查清单
| 问题现象 | 可能原因 | 排查思路 |
|---|---|---|
| 找到S盒,但解密失败 |
1. 密钥错误(混淆、派生)
2. 工作模式判断错误 3. IV错误或缺失 4. 填充方式不匹配 |
1. 动态调试验证密钥使用过程。
2. 尝试ECB、CBC等常见模式。 3. 在内存或数据流中搜索16字节常量作为IV。 4. 尝试无填充、PKCS7、ZeroPadding等。 |
| 解密出乱码,但部分可读 |
1. 密钥正确但模式/IV有误(CBC错一位全盘皆输)
2. 解密后数据还需进一步处理(如解压缩、反序列化) |
1. 检查IV是否正确,尝试交换CBC和ECB模式。
2. 对解密输出做熵分析,或用
binwalk
、
xxd
查看头部特征。
|
| 无法在代码中找到标准S盒 |
1. 使用了白盒AES实现
2. S盒被动态生成或加密 3. 使用了自定义或修改的S盒(罕见) |
1. 寻找庞大的、看似随机的查找表(T-boxes)。
2. 在初始化函数下断点,观察S盒数组是如何被填充的。 3. 动态跟踪输入输出,尝试暴力匹配算法特征。 |
| 算法识别工具未报告AES |
1. 算法被严重混淆或自定义实现
2. 使用了不常见的库或内联汇编优化 |
1. 关注16字节分组处理、异或、查表、移位操作组合。
2. 使用动态污点分析(如Triton, angr),跟踪数据流。 |
5.2 对抗混淆与自定义实现
- 内联与展开 :编译器优化可能将AES的轮函数内联展开,消除了明显的循环结构,使代码看起来是一大串线性操作。此时需要寻找 轮密钥加 的痕迹——即数据与一个常量数组进行异或。这个常量数组可能就是轮密钥。找到多个这样的异或操作(对应多轮),就能勾勒出算法轮廓。
- 比特切片实现 :一种高性能实现方式,它一次性并行加密多个块,操作对象是比特位而非字节。代码看起来完全不同,充满了位运算(AND, OR, XOR, SHIFT)。识别难度大,需要结合输入输出测试来验证。
- 动态生成S盒/轮密钥 :程序可能在启动时通过一个种子计算生成S盒和轮密钥,而非使用标准常量。你需要找到这个生成函数,并提取其逻辑或直接dump出运行时内存中的结果。
5.3 当没有密钥时:侧信道与暴力破解的思路
在无法直接提取密钥的极端情况下,可以考虑:
- 已知明文攻击 :如果你知道某段密文对应的明文(比如软件界面上固定的标题、错误信息),你就可以利用这一点。在调试器中,在加密函数处断点,输入已知明文,观察其生成的密文。虽然不能直接得到密钥,但可以极大地帮助你验证对算法和模式的判断,甚至可能利用某些弱点(如ECB模式)推断其他部分。
- 暴力破解 :仅对短密钥(如8字符以下)或弱密钥可行。AES-128的密钥空间是2^128,完全不可暴力破解。但如果密钥是来自一个字典(常见单词、短语)或派生自简单规则(如手机号),可以尝试字典攻击。
- 故障注入 :一种高级硬件攻击方法,通过物理手段(如电压毛刺、时钟抖动)使芯片在计算中出错,通过分析错误输出来推断密钥信息。这属于专业硬件安全领域,远超普通软件逆向范畴。
对于绝大多数软件逆向场景,我们的目标不是破解AES算法本身,而是 找到程序自身存储或使用的那个密钥 。因此,分析的重点应始终放在程序的逻辑上:密钥从哪里来、如何被处理、在哪里被使用。
6. 工具链与自动化辅助分析
工欲善其事,必先利其器。除了经典的调试器(x64dbg/OllyDbg, GDB)和反编译器(IDA Pro, Ghidra, Binary Ninja),还有一些专门针对加密算法的工具和脚本可以提升效率。
- CyberChef :一个强大的Web端“密码学厨房”。当你提取到一段疑似密文和密钥时,可以快速在浏览器里尝试各种AES模式、填充、编码进行解密测试,无需编写脚本。它的“魔方”功能还能自动尝试多种组合。
-
Python Cryptography库
:
pycryptodome或cryptography是必不可少的后端工具。用于编写自动化的解密脚本、验证密钥、模拟算法步骤。 -
Frida
:动态插桩框架。你可以编写Frida脚本,Hook目标程序中的加密/解密函数,直接dump出调用时的参数(密钥、IV、输入、输出)。这对于快速验证猜测和批量提取数据非常有效。
// 示例:Hook一个假设的 encrypt_data 函数 Interceptor.attach(Module.findExportByName(null, "encrypt_data"), { onEnter: function(args) { console.log("[*] encrypt_data called"); // 假设密钥是第一个参数(指针) var key_ptr = args[0]; var key = key_ptr.readByteArray(16); console.log("Key: " + key); // 假设数据是第二个参数 var data_ptr = args[1]; var data_len = args[2].toInt32(); var data = data_ptr.readByteArray(data_len); console.log("Data len: " + data_len); // 可以在这里将 key 和 data 发送到外部文件 } }); - 自定义IDA Python脚本 :可以编写脚本在IDA中自动搜索S盒、轮常数特征,标记可能的加密函数,甚至尝试匹配已知的加密库函数签名。
逆向分析AES加密,是一个从“特征识别”到“逻辑理解”再到“数据提取”的完整过程。它考验的不仅仅是密码学知识,更是对程序行为的洞察力和系统性的调试技巧。掌握了其原理和常见模式,你就能在纷繁复杂的二进制世界中,撕开许多软件自我保护的第一道面纱。在下一部分,我们将探讨更复杂的场景,包括网络协议中的AES、白盒AES的初步分析,以及如何利用模拟执行来辅助分析复杂的密钥派生过程。

2025

被折叠的 条评论
为什么被折叠?



