AES基本概念
AES基本概念
本文适合
已经了解 XOR 和基础编码,准备处理 AES 模式、IV、nonce 和 padding oracle 题的 CTF 学习者。学完你能:根据题目给出的 key、iv、nonce、mode、密文和接口响应,判断 AES 题的可利用点,并写出 ECB、CBC、CTR/GCM 相关的最小验证脚本。
AES 是一种对称分组密码。对称的意思是加密和解密使用同一个密钥。分组的意思是它一次处理固定长度的数据块。
AES 处理多大的块
AES 的分组长度固定是 128 bit,也就是 16 字节。
密钥长度可以是 128、192 或 256 bit,分别对应 16、24、32 字节。
这两个数字不要混淆:块大小固定 16 字节,密钥大小可以不同。
为什么需要工作模式
真实消息通常不止 16 字节。AES 本体只处理一个块,所以需要工作模式把很多块组织起来。
常见模式包括:
ECB:每个明文块独立加密。
CBC:每个明文块先和前一个密文块异或,再加密。
CTR:把计数器加密成密钥流,再和明文异或。
GCM:在加密之外还提供认证,能检查数据是否被篡改。
IV 和 nonce 是什么
IV 是初始化向量。nonce 是一次性数字。它们通常不是密钥,但会影响加密结果。
CBC 里 IV 会影响第一块明文。
CTR 和 GCM 里 nonce 不能重复。重复可能导致密钥流复用,严重时可以恢复明文或伪造数据。
Padding 是什么
如果明文长度不是 16 字节的整数倍,一些模式需要填充。常见填充方式是 PKCS#7。
例如缺 5 个字节,就补 5 个 0x05。如果刚好是 16 字节整数倍,会额外补一整块 16 个 0x10,这样解密端才能确定地去掉填充。
Padding 看似是格式问题,但在 CTF 中经常变成 padding oracle,也就是通过错误信息泄露解密过程。服务端返回"padding 错误"和"解密失败"是不同信息,攻击者可以逐字节猜出明文。
Padding Oracle 攻击原理
Padding Oracle 攻击的核心是:服务端对 padding 正确与否给出了不同响应。攻击者可以篡改密文块的对应字节,观察服务端反应,逐字节还原明文。
CBC 模式下,第 N 块密文会影响第 N+1 块明文的解密。通过修改 C[N] 的最后一字节并发送,如果服务端说 padding 正确,就能推断出 P[N+1] 最后一字节的值。
整个攻击复杂度是 O(256 * 块数 * 块长度),对 16 字节块来说非常快。
CTF 中怎么看 AES
先判断题目给了什么:密文、密钥、IV、nonce、加密脚本、解密接口、错误信息。
再判断模式:ECB、CBC、CTR、GCM 或自定义变体。
最后看安全条件有没有被破坏:ECB 泄露块模式,CBC 可被 bit flipping 或 padding oracle 利用,CTR/GCM 害怕 nonce 复用。
代码示例
AES-ECB 加密解密
from Crypto.Cipher import AES
# ECB 模式:每个块独立加密,相同明文块产生相同密文块
key = b'0123456789abcdef' # 16 字节 = 128 bit
cipher = AES.new(key, AES.MODE_ECB)
# 明文必须是 16 字节的整数倍,否则需要 padding
plaintext = b'Hello CTF player!' # 17 字节
# PKCS#7 填充到 32 字节
pad_len = 16 - len(plaintext) % 16
padded = plaintext + bytes([pad_len] * pad_len)
ciphertext = cipher.encrypt(padded)
print(f"密文: {ciphertext.hex()}")
# 解密
decipher = AES.new(key, AES.MODE_ECB)
result = decipher.decrypt(ciphertext)
# 去掉 padding
print(f"明文: {result[:-result[-1]]}")AES-CBC 加密解密
from Crypto.Cipher import AES
import os
key = b'0123456789abcdef'
iv = os.urandom(16) # CBC 需要 IV,必须随机且不可预测
cipher = AES.new(key, AES.MODE_CBC, iv=iv)
plaintext = b'flag{cbc_mode_is_fun}'
pad_len = 16 - len(plaintext) % 16
padded = plaintext + bytes([pad_len] * pad_len)
ciphertext = cipher.encrypt(padded)
print(f"IV: {iv.hex()}")
print(f"密文: {ciphertext.hex()}")
# 解密时需要同样的 IV
decipher = AES.new(key, AES.MODE_CBC, iv=iv)
result = decipher.decrypt(ciphertext)
print(f"明文: {result[:-result[-1]]}")AES-CTR 模式(nonce 复用演示)
from Crypto.Cipher import AES
import os
key = b'0123456789abcdef'
nonce = os.urandom(8)
# 用同一个 nonce 加密两条消息 -> 危险!
cipher1 = AES.new(key, AES.MODE_CTR, nonce=nonce)
cipher2 = AES.new(key, AES.MODE_CTR, nonce=nonce)
msg1 = b'AAAAAAAAAAAAAAA' # 已知明文
msg2 = b'flag{secret_msg}'
ct1 = cipher1.encrypt(msg1)
ct2 = cipher2.encrypt(msg2)
# CTR nonce 复用 = 两次密钥流相同 = 异或可还原
# ct1 ^ ct2 = msg1 ^ msg2, 因为 msg1 已知,可以恢复 msg2
recovered_msg2 = bytes(a ^ b ^ c for a, b, c in zip(ct1, ct2, msg1))
print(f"恢复的明文: {recovered_msg2}")CBC Bit-Flipping 攻击演示
from Crypto.Cipher import AES
key = b'0123456789abcdef'
iv = b'AAAAAAAAAAAAAAAA' # 固定 IV 方便演示
# 原始明文: "role=guest;user=A"
# 目标: 把 'g' 改成 'a',变成 "role=admi n;user=A"
plaintext = b'role=guest;user=A'
cipher = AES.new(key, AES.MODE_CBC, iv=iv)
ciphertext = cipher.encrypt(plaintext)
# CBC 解密: P[i] = Decrypt(C[i]) XOR C[i-1]
# 修改 C[0] 的第 5 字节,会影响 P[1] 的第 5 字节
ct_list = bytearray(ciphertext)
# 'g' XOR 'a' XOR 原始密文字节 = 新密文字节
ct_list[5] ^= ord('g') ^ ord('a')
modified_ct = bytes(ct_list)
decipher = AES.new(key, AES.MODE_CBC, iv=iv)
result = decipher.decrypt(modified_ct)
print(f"修改后的明文: {result}")PKCS#7 Padding 验证
def pkcs7_unpad(data):
"""验证并移除 PKCS#7 填充"""
pad_len = data[-1]
if pad_len < 1 or pad_len > 16:
raise ValueError("无效的 padding")
if data[-pad_len:] != bytes([pad_len] * pad_len):
raise ValueError("padding 不匹配")
return data[:-pad_len]
# 正确 padding
print(pkcs7_unpad(b'hello\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b'))
# 错误 padding 会抛异常 -- padding oracle 攻击就是利用这个差异
try:
print(pkcs7_unpad(b'hello\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0c'))
except ValueError as e:
print(f"捕获错误: {e}")常见误区
- 看到 Base64 就以为是 AES。
- 把 IV 当成密钥。
- 以为 AES 本身被“爆破”了。
- 忽略模式,直接套解密脚本。
- 不检查明文长度和 padding。
一句话判断
题目出现 16 字节块、AES、ECB/CBC/CTR/GCM、IV、nonce、padding、解密接口或密文块重复时,就按 AES 模式和使用条件分析。
CTF 里很少是“破解 AES 算法本身”,更多是工作模式、nonce/IV、padding、认证或接口错误。
题目中常见信号
- 密文长度是 16 字节的整数倍。
- 提供
key、iv、nonce、ciphertext、tag。 - 加密脚本使用
AES.MODE_ECB/CBC/CTR/GCM。 - 相同明文块对应相同密文块,怀疑 ECB。
- 服务端返回
padding error、decrypt failed等不同错误。 - 多条 CTR/GCM 密文使用相同 nonce。
- 明文结构可预测,例如
role=user、admin=false、JSON、Cookie。
核心概念
AES 本体只负责 16 字节块变换。安全性取决于模式和使用方式:
关键条件:不应直接加密重复结构
常见 CTF 问题:块替换、图案泄露
关键条件:IV 不可预测,padding 错误不应泄露
常见 CTF 问题:bit flipping、padding oracle
关键条件:nonce 不能复用
常见 CTF 问题:密钥流复用,异或恢复
关键条件:nonce 不能复用,tag 必须验证
常见 CTF 问题:伪造、明文恢复、认证绕过
先判断模式,再判断哪个安全条件被破坏。
最小分析流程
- 记录题目给了哪些字段:密文、密钥、IV、nonce、tag、接口。
- 看密文长度是否按 16 字节分块。
- 如果有源码,直接确认 AES 模式。
- 没源码时检查密文块是否重复,优先判断 ECB。
- 检查 CBC 是否有 padding oracle 或 bit flipping 条件。
- 检查 CTR/GCM nonce 是否复用。
- 构造最小脚本验证假设。
- 解出明文或伪造目标 token 后回到服务端验证。
最小验证示例
检测 ECB 重复块:
ct = bytes.fromhex(cipher_hex)
blocks = [ct[i:i+16] for i in range(0, len(ct), 16)]
print(len(blocks), len(set(blocks)))
if len(blocks) != len(set(blocks)):
print("可能是 ECB 或明文块重复严重")检测 CTR nonce 复用:
def xor(a, b):
return bytes(x ^ y for x, y in zip(a, b))
ct1 = bytes.fromhex(ct1_hex)
ct2 = bytes.fromhex(ct2_hex)
known = b"known plaintext prefix"
recovered = xor(xor(ct1, ct2), known)
print(recovered)如果能恢复另一条明文对应位置,说明两条密文复用了同一密钥流。
常见利用 / 解题路线
路线总览:
路线一:ECB 块替换
适合明文可控,目标字段能对齐到独立块。
构造 admin 块 -> 从密文中剪切 -> 替换到目标 token -> 服务端解密通过路线二:CBC bit flipping
适合服务端解密后信任明文,但没有认证。
想改 P[i] 的某字节 -> 修改 C[i-1] 对应字节 -> 解密后明文翻转常见目标:
role=user -> role=admin
admin=0 -> admin=1路线三:CBC padding oracle
适合服务端区分 padding 错误和其他错误。
逐字节修改前一块 -> 观察 padding 是否正确 -> 还原后一块明文路线四:CTR/GCM nonce 复用
适合同 key 同 nonce 加密多条消息。
ct1 xor ct2 = pt1 xor pt2
已知 pt1 局部 -> 恢复 pt2 局部路线五:GCM tag 未校验
适合代码解密后不验证 tag,或忽略验证异常。
篡改密文 -> 服务端仍使用明文 -> 认证失效常见失败原因
- 模式没判断就写脚本:ECB、CBC、CTR 的利用完全不同。
- IV/nonce 当密钥:IV 和 nonce 通常公开,但安全条件不同。
- padding 没处理:PKCS#7 刚好整块时还会补一整块。
- 密文编码层没拆:hex、base64、URL 编码要先还原成 bytes。
- bit flipping 改错块:CBC 中修改
C[i-1]才影响P[i]。 - CTR 复用只异或密文不利用已知明文:需要结合可预测明文恢复内容。
- GCM 忽略 tag 字段:tag 是认证核心,不是可有可无的附件。
迷你案例
题目给加密 Cookie,明文格式可预测:
uid=1001;role=user;expires=...服务端使用 AES-CBC,返回的 token 是:
iv || ciphertext目标是把 role=user 改成 role=admin。如果 user 位于第 2 个明文块,就修改第 1 个密文块对应位置:
token = bytes.fromhex(token_hex)
iv = token[:16]
ct = bytearray(token[16:])
old = b"user"
new = b"admin"[:4] # 实际要结合字段长度设计
pos = 5 # old 在目标明文块中的偏移
for i, (a, b) in enumerate(zip(old, new)):
ct[pos + i] ^= a ^ b
print((iv + bytes(ct)).hex())如果服务端解密后身份变高,说明漏洞是 CBC 未认证导致的可控翻转。WP 中要写清楚:
CBC 解密 P[i] = Dec(C[i]) xor C[i-1]
修改前一块密文会定点改变后一块明文
服务端没有 MAC/GCM tag 校验