LOADING

加载过慢请开启缓存 浏览器默认开启

AES-CBC 模式与 PKCS#7 填充实践

AES-CBC 模式与 PKCS#7 填充实践

这篇文章记录 Cryptopals Set 2 中几道和 AES 分组密码有关的题。重点不是背概念,而是通过真实数据把下面几件事跑通:

PKCS#7 padding 怎么补
AES-CBC 如何自己实现链接过程
ECB 和 CBC 怎么通过重复块检测
padding validation 为什么要严格检查
CBC bit flipping 为什么能改下一块明文

实验数据来自 Cryptopals 平台,尤其是 Challenge 10 的真实密文文件:

https://cryptopals.com/static/challenge-data/10.txt

1. 为什么分组密码需要 padding

AES 的 block size 是 16 字节。CBC、ECB 等模式都要按块处理数据。

如果明文长度不是 16 的倍数,就需要填充。Cryptopals Challenge 9 要求实现 PKCS#7 padding。

题目样例:

YELLOW SUBMARINE

填充到 20 字节:

YELLOW SUBMARINE\x04\x04\x04\x04

因为原文长度是 16,目标长度是 20,所以补 4 个字节,每个字节的值都是 0x04

实现:

def pkcs7_pad(data: bytes, block_size: int) -> bytes:
if block_size <= 0 or block_size > 255:
raise ValueError("block size must be in 1..255")
pad_len = block_size - (len(data) % block_size)
return data + bytes([pad_len]) * pad_len

运行截图:

PKCS#7 padding 运行截图

输出里可以看到:

Output hex: 59454c4c4f57205355424d4152494e4504040404
Verification: PASS

2. PKCS#7 去填充必须校验

去 padding 时不能只看最后一个字节,然后直接切掉。必须确认最后 pad_len 个字节都等于 pad_len

正确写法:

def pkcs7_unpad(data: bytes, block_size: int = 16) -> bytes:
if not data or len(data) % block_size:
raise ValueError("invalid PKCS#7 length")
pad_len = data[-1]
if pad_len < 1 or pad_len > block_size:
raise ValueError("invalid PKCS#7 padding")
if data[-pad_len:] != bytes([pad_len]) * pad_len:
raise ValueError("invalid PKCS#7 padding")
return data[:-pad_len]

Cryptopals Challenge 15 的三个样例:

"ICE ICE BABY\x04\x04\x04\x04" -> valid
"ICE ICE BABY\x05\x05\x05\x05" -> invalid
"ICE ICE BABY\x01\x02\x03\x04" -> invalid

运行截图:

PKCS#7 validation 运行截图

3. CBC 模式原理

CBC 是 Cipher Block Chaining。它的关键是:每一块都依赖前一块密文。

加密:

C_i = AES_ENC(P_i xor C_{i-1}, K)

解密:

P_i = AES_DEC(C_i, K) xor C_{i-1}

第一块没有前一块密文,所以使用 IV:

C_0 = IV

Cryptopals Challenge 10 给出的参数:

key = YELLOW SUBMARINE
IV = 00000000000000000000000000000000

4. 自己实现 AES-CBC 解密

可以使用库函数做 AES 单块 ECB 解密,但 CBC 链接过程自己写,这样更容易理解公式。

def xor_bytes(left: bytes, right: bytes) -> bytes:
return bytes(a ^ b for a, b in zip(left, right))


def aes_cbc_decrypt(data: bytes, key: bytes, iv: bytes) -> bytes:
if len(iv) != 16 or len(data) % 16:
raise ValueError("invalid CBC input length")

prev = iv
plaintext_blocks = []

for i in range(0, len(data), 16):
block = data[i : i + 16]
decrypted = aes_decrypt_block(block, key)
plaintext_blocks.append(xor_bytes(decrypted, prev))
prev = block

return b"".join(plaintext_blocks)

运行:

python work\experiment_02_aes_modes\02_decrypt_aes_cbc_mode.py

真实 VSCode 截图:

AES-CBC 解密运行截图

关键输出:

Ciphertext length: 2880 bytes
Key: YELLOW SUBMARINE
IV : 00000000000000000000000000000000
Plaintext preview:
I'm back and I'm ringin' the bell

这说明我们自己写的 CBC 链接过程和平台数据匹配。

5. ECB/CBC oracle 检测

Challenge 11 要求写一个加密 oracle,它随机选择 ECB 或 CBC,然后我们根据密文判断模式。

检测 ECB 的方法是看重复密文块:

def detect_ecb(ciphertext: bytes, block_size: int = 16) -> bool:
blocks = [ciphertext[i : i + block_size] for i in range(0, len(ciphertext), block_size)]
return len(blocks) != len(set(blocks))

为什么可行?

ECB: 相同明文块 -> 相同密文块
CBC: 每块会和上一密文块 XOR,相同明文块通常不会产生相同密文块

所以输入很多重复的 A

actual, ciphertext = encryption_oracle(b"A" * 64)
detected = "ECB" if detect_ecb(ciphertext) else "CBC"

运行截图:

ECB/CBC oracle 运行截图

本次真实运行检测准确率为:

12/12

6. CBC bit flipping 为什么可行

CBC 解密公式:

P_i = AES_DEC(C_i) xor C_{i-1}

如果攻击者修改 C_{i-1} 的某些 bit,那么解密后的 P_i 对应 bit 也会改变。

这就是 Challenge 16 的 bit flipping 攻击。

目标是在明文中构造:

;admin=true;

但是输入会过滤 ;=。做法是先输入安全字符,再在上一块密文中翻转差值。

核心思路:

delta = original_byte ^ target_byte
ciphertext[previous_block_offset + index] ^= delta

运行截图:

CBC bit flipping 运行截图

结果:

Admin before bit flip: False
Admin after bit flip : True

7. 单元测试

实验二的单元测试覆盖:

PKCS#7 padding
AES-CBC 解密真实平台文件
byte-at-a-time ECB
ECB cut-and-paste
padding validation
CBC bit flipping
MTC3 ePassport

截图:

实验二单元测试截图

8. 常见错误

8.1 解密时把 XOR 顺序写错

CBC 解密是:

AES_DEC(C_i) xor C_{i-1}

不是先把 C_iC_{i-1} XOR 再 AES 解密。

8.2 忘记更新 prev

每处理完一个块,prev 要更新为当前密文块:

prev = block

不能更新成明文块。

8.3 padding 只切不验

错误 padding 必须抛异常,否则后续 padding oracle 或格式绕过类问题会更容易出现。

8.4 把 IV 当成密钥

IV 参与第一块 CBC 运算,但 IV 不是 key。key 要保密,IV 通常可以公开但不能随便复用到危险场景里。

9. 小结

这组 AES/CBC 实验可以按下面顺序掌握:

先写 PKCS#7 padding
再写 CBC 解密链
用真实 challenge-data/10.txt 验证
用重复块检测 ECB
严格验证 padding
理解 CBC bit flipping 的可控改位

分组密码的安全不只取决于 AES 算法本身,也取决于模式、填充和输入处理是否正确。

参考资料