📚 第一部分:核心理论梳理
4.1 对称加密基础
核心思想:加密与解密使用同一把密钥( Kenc=KdecKenc=Kdec )。
| 特性 | 分组密码 | 流密码 |
|---|---|---|
| 处理方式 | 将明文切分为固定长度的块(如128位),逐块加密 | 生成密钥流,与明文逐字节/逐位异或 |
| 典型算法 | AES, DES, 3DES | RC4, ChaCha20, Salsa20 |
| 填充 | 需要(若明文长度不是块大小倍数) | 不需要 |
| 速度 | 较慢(涉及复杂的置换和代换) | 通常更快(硬件实现效率极高) |
4.2 AES 算法详解
AES (Advanced Encryption Standard)
- 标准 :分组长度固定为 128位(16字节)。
- 密钥长度:128/192/256位。
- 轮数:AES-128 (10轮), AES-192 (12轮), AES-256 (14轮)。
四大操作步骤(每轮):
- SubBytes (字节替换):非线性变换,通过S-Box替换字节。
- ShiftRows (行移位):状态矩阵的行进行循环左移。
- MixColumns (列混淆):列的线性变换(最后一轮省略)。
- AddRoundKey (轮密钥加):与轮密钥进行异或。
常见工作模式对比:
| 模式 | 全称 | 特点 | 安全性 |
|---|---|---|---|
| ECB | 电子密码本 | 相同明文块 →→ 相同密文块 | 极差 (暴露数据模式) |
| CBC | 密码分组链接 | 引入IV,块间异或,串行加密 | 良好 (需防填充攻击) |
| CTR | 计数器模式 | 变分组为流密码,支持并行 | 良好 (无需填充) |
| GCM | 伽罗瓦/计数器 | CTR + 认证 (GMAC) | 最佳 (推荐用于生产) |
🛠️ 第二部分:实战攻击代码与工具库
我将你提供的攻击脚本整理成了三个独立的模块:AES攻击模块 、流密码攻击模块 和综合实战模块。
模块一: AES 常见攻击 (ECB & CBC)
这个模块包含了 ECB Oracle 逐字节攻击 和 CBC 字节翻转攻击 的完整实现。
适用场景:服务端将用户输入拼接上未知字符串(如flag),然后用AES-ECB加密返回密文。
攻击步骤:
-
确定块大小: 逐渐增加输入长度,观察密文长度何时增加一个块
-
确认 ECB 模式: 发送两个相同的块,检查密文中是否有重复块
-
逐字节恢复: 构造输入使目标字节位于块边界,枚举256种可能并对比
import os
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpadclass AESAttacker:
def init(self, key=None):
self.key = key if key else os.urandom(16)
self.BLOCK_SIZE = 16# ========================== # 1. ECB Oracle 攻击 # ========================== def ecb_oracle_server(self, user_input: bytes, secret: bytes) -> bytes: """模拟服务端:加密 (用户输入 + 未知秘密)""" plaintext = user_input + secret # PKCS7 填充 padded_pt = pad(plaintext, self.BLOCK_SIZE) cipher = AES.new(self.key, AES.MODE_ECB) return cipher.encrypt(padded_pt) def attack_ecb_oracle(self, oracle_func, secret_length: int): """ 执行 ECB Oracle 逐字节攻击 oracle_func: 接收 bytes,返回密文 bytes 的函数 secret_length: 未知秘密的长度 """ known = b"" print(f"[*] 开始 ECB Oracle 攻击...") for i in range(secret_length): # 1. 构造填充,使目标字节位于当前块的最后一个位置 # 比如:我们要猜第1个未知字节,需要填充 15 个 'A' pad_length = self.BLOCK_SIZE - 1 - (len(known) % self.BLOCK_SIZE) padding = b"A" * pad_length # 2. 获取目标密文块(包含我们要猜的那个字节) # 注意:这里我们发送 padding,让服务器加密 padding + secret # 此时密文的第 (len(known)//16) 个块包含了 secret 的第 i 个字节 target_cipher = oracle_func(padding) block_index = len(known) // self.BLOCK_SIZE target_block = target_cipher[block_index * self.BLOCK_SIZE : (block_index + 1) * self.BLOCK_SIZE] # 3. 暴力枚举最后一个字节 found = False for byte in range(256): # 构造测试输入:padding + 已知部分 + 猜测字节 test_input = padding + known + bytes([byte]) test_cipher = oracle_func(test_input) test_block = test_cipher[block_index * self.BLOCK_SIZE : (block_index + 1) * self.BLOCK_SIZE] if test_block == target_block: known += bytes([byte]) print(f"[+] 发现第 {i+1} 字节: {chr(byte) if 32 <= byte < 127 else '?'} (ASCII: {byte})") found = True break if not found: print("[-] 攻击失败,无法匹配字节") break return known # ========================== # 2. CBC 字节翻转攻击 # ========================== def cbc_bitflip_attack(self, iv: bytes, ct: bytes, target_changes: list): """ 执行 CBC 字节翻转攻击 target_changes: 列表,包含字典 {block_idx: int, offset: int, original: int, target: int} block_idx: 要修改的密文块索引(修改 C_i 会影响 P_{i+1}) offset: 块内偏移 original: 原始明文字节 target: 目标明文字节 """ ct_array = bytearray(ct) print(f"[*] 开始 CBC 字节翻转攻击...") for change in target_changes: block_idx = change['block_idx'] offset = change['offset'] original_byte = change['original'] target_byte = change['target'] # 计算位置:修改 C_i 的对应字节 pos = block_idx * self.BLOCK_SIZE + offset # 翻转公式:C'_i[j] = C_i[j] ^ P_{i+1}[j] ^ P'_{i+1}[j] # 在代码中:new_byte = old_byte ^ original ^ target ct_array[pos] ^= original_byte ^ target_byte print(f" -> 修改块 {block_idx} 偏移 {offset}: {chr(original_byte)} -> {chr(target_byte)}") return bytes(ct_array)--- 使用示例 ---
if name == "main":
attacker = AESAttacker()# 场景 1: ECB Oracle SECRET_FLAG = b"flag{ecb_is_not_secure}" # 模拟服务端函数 def server_encrypt(user_input): return attacker.ecb_oracle_server(user_input, SECRET_FLAG) recovered = attacker.attack_ecb_oracle(server_encrypt, len(SECRET_FLAG)) print(f"🚩 恢复的 Flag: {recovered.decode()}") print("-" * 30) # 场景 2: CBC Bitflip # 假设我们要把 "role=user" 变成 "role=admin" (需长度一致,这里演示 user->admi) # 实际上通常需要填充,这里简化演示原理 KEY = os.urandom(16) # 构造明文:Block0 | Block1("role=user;xxxx") # 假设 Block1 解密后是 "role=user;1234" # 我们修改 Block0 来改变 Block1 的明文 # 这里仅作逻辑演示,实际运行需要真实的解密环境验证 # 假设 Block0 的密文是 ct[0:16], Block1 是 ct[16:32] # 修改 ct[0] 会影响 Block1 解密后的第1个字节
CBC字节翻转攻击(Bit-Flipping Attack)
原理: 在CBC解密过程中,明文块P_i = D_K(C_i) ⊕ C_{i-1}。如果攻击者修改C_{i-1}的某个字节,会直接影响P_i的对应字节,同时P_{i-1}会变为乱码。
攻击公式:
设原始C_{i-1}[j]为c,原始P_i[j]为p,目标P_i[j]为t:
修改后的 C_{i-1}[j] = c ⊕ p ⊕ t
这样解密时:D_K(C_i)[j] ⊕ (c ⊕ p ⊕ t) = p ⊕ c ⊕ c ⊕ p ⊕ t = t
攻击场景示例: 假设服务端加密用户数据 "role=user",攻击者想修改为 "role=admin"。
Padding Oracle攻击
原理: 利用服务器对PKCS#7填充有效性的不同响应(如返回不同的错误码),逐字节恢复明文。
攻击条件:
- 使用CBC模式 + PKCS#7填充
- 服务器对"填充正确"和"填充错误"有可区分的响应
攻击步骤概要:
- 修改密文块的最后一个字节,枚举0-255
- 当服务器不报告填充错误时,意味着解密后的最后一个字节是 0x01
- 由此推导出中间值(intermediate value),进而恢复明文字节
- 对每个字节重复此过程,从后向前逐字节恢复整个块
模块二:流密码与 XOR 攻击工具
这个模块封装了 XOR 运算 和 Crib Dragging (已知明文拖拽) 攻击。
XOR 运算的核心性质:
- A ⊕ A = 0(自反性)
- A ⊕ 0 = A(恒等性)
- A ⊕ B = B ⊕ A(交换律)
- (A ⊕ B) ⊕ C = A ⊕ (B ⊕ C)(结合律)
一次性密码本( OTP, One-Time Pad ):
密钥与明文等长、完全随机、仅使用一次时,OTP是理论上不可破解的。但实际中密钥重用会导致严重安全问题。
密钥重用攻击( Two-Time Pad / Crib Dragging ):
若两段明文P1、P2使用同一密钥K加密:
C1 = P1 ⊕ K, C2 = P2 ⊕ K
C1 ⊕ C2 = P1 ⊕ K ⊕ P2 ⊕ K = P1 ⊕ P2
密钥被消除!攻击者得到两段明文的异或结果,然后通过"Crib Dragging"(已知词拖拽)逐步恢复明文。
def xor_bytes(a: bytes, b: bytes) -> bytes:
"""对两个字节串进行异或"""
return bytes(x ^ y for x, y in zip(a, b))
class StreamCipherAttacker:
@staticmethod
def crib_drag(c1_xor_c2: bytes, crib: bytes, min_printable_ratio=0.8):
"""
Crib Dragging 攻击
c1_xor_c2: 两个密文的异或结果 (P1 ^ P2)
crib: 猜测的明文片段 (如 b"the ", b"flag")
"""
results = []
print(f"[*] 正在使用 Crib '{crib.decode()}' 进行拖拽攻击...")
for i in range(len(c1_xor_c2) - len(crib) + 1):
segment = c1_xor_c2[i : i + len(crib)]
# P2_segment = (P1 ^ P2) ^ P1_guess
possible_p2 = xor_bytes(segment, crib)
# 检查可读性:统计可打印字符比例
printable_count = sum(1 for b in possible_p2 if 32 <= b < 127)
ratio = printable_count / len(possible_p2)
if ratio >= min_printable_ratio:
results.append({
"pos": i,
"p1_guess": crib.decode(),
"p2_recovered": possible_p2.decode(errors='ignore'),
"raw": possible_p2
})
return results
# --- 使用示例 ---
if __name__ == "__main__":
# 模拟 Two-Time Pad
KEY = b"supersecretkey" # 密钥重用
P1 = b"attack at dawn"
P2 = b"meeting at noon"
# 补齐长度
P1 = P1.ljust(len(KEY), b'\x00')
P2 = P2.ljust(len(KEY), b'\x00')
C1 = xor_bytes(P1, KEY)
C2 = xor_bytes(P2, KEY)
# 攻击者拿到 C1 ^ C2
C1_xor_C2 = xor_bytes(C1, C2)
attacker = StreamCipherAttacker()
# 假设攻击者猜测 P2 中包含 " at "
hits = attacker.crib_drag(C1_xor_C2, b" at ")
for hit in hits:
print(f"位置 {hit['pos']}: 如果 P2 是 '{hit['p1_guess']}', 则 P1 可能是 '{hit['p2_recovered']}'")
模块三:综合实战案例 (Web Cookie 篡改)
这是结合了前面知识的完整解题脚本。
|-----------------------------------------------------------------------------------------------------------------------------------|
| 📋 题目场景 一个Web应用使用AES-CBC加密cookie。cookie格式为:username=guest;role=user;。你能获取到自己的加密cookie(密文 + IV),需要通过修改密文,使解密后的role变为admin。 |
# 题目场景:修改 Cookie 中的 role=user 为 role=admin
# 已知:
# 1. 加密模式:AES-CBC
# 2. 明文结构:username=guest;role=user;
# 3. 目标:让解密后的 role 变为 admin
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import os
class CookieExploit:
def __init__(self):
self.KEY = os.urandom(16)
self.BLOCK_SIZE = 16
def get_encrypted_cookie(self, username):
"""服务端:生成加密 Cookie"""
data = f"username={username};role=user;".encode()
iv = os.urandom(16)
cipher = AES.new(self.KEY, AES.MODE_CBC, iv)
ct = cipher.encrypt(pad(data, self.BLOCK_SIZE))
return iv, ct
def check_admin(self, iv, ct):
"""服务端:解密并检查权限"""
cipher = AES.new(self.KEY, AES.MODE_CBC, iv)
try:
pt = unpad(cipher.decrypt(ct), self.BLOCK_SIZE)
if b"role=admin" in pt:
return True, pt
return False, pt
except:
return False, b"Decryption Error"
def exploit(self):
# 1. 获取原始 Cookie
# 注意:为了让 "role=user" 单独成块或对齐,我们需要控制 username 的长度
# 明文: "username=" (9) + "guest" (5) + ";role=user;" (11) = 25 字节
# 块1: "username=guest;r" (16)
# 块2: "ole=user;" + padding (16)
# 目标:修改 块2 中的 "user" -> "admin"
# 注意:user (4字节) vs admin (5字节),长度不一致,不能直接翻转!
# 技巧:如果题目允许,我们可以尝试修改为 "admi" 或者寻找其他注入点
# 这里为了演示,假设我们想把 "user" 改成 "adm1" (假设服务端接受非标准)
# 或者,更常见的 CTF 技巧是:利用填充或特定字符串构造
# 让我们换一个思路:
# 构造 username 使得 "role=user;" 刚好在第二个块
# username = "A" * 7
# 明文: "username=AAAAAAA;role=user;"
# 块1: "username=AAAAAAA" (16)
# 块2: ";role=user;..." (16) -> 这里 user 在偏移 6-9
iv, ct = self.get_encrypted_cookie("AAAAAAA")
print(f"[*] 原始密文块2: {ct[16:32].hex()}")
# 目标:将块2的 "user" (偏移6-9) 修改为 "adm1" (为了保持长度)
# 修改 块1 的对应字节 (偏移6-9)
ct_list = bytearray(ct)
target_block_idx = 1 # 目标明文在块2
modify_block_idx = 0 # 修改块1
original_text = b"user"
target_text = b"adm1" # 演示用,实际题目可能需要 "admin" 且长度对齐
for i in range(len(original_text)):
offset = 6 + i
# 翻转公式
ct_list[modify_block_idx * 16 + offset] ^= original_text[i] ^ target_text[i]
new_ct = bytes(ct_list)
# 2. 提交攻击
success, decrypted = self.check_admin(iv, new_ct)
print(f"[*] 解密结果: {decrypted}")
if success:
print("[+] 攻击成功!获得 Admin 权限")
else:
print("[-] 攻击失败,可能是长度不对或校验未通过")
if __name__ == "__main__":
exploit = CookieExploit()
exploit.exploit()
💡 总结与提示
- ECB 模式 :绝对不要在现实世界中使用,CTF 中见到 ECB 优先考虑 Byte-at-a-time 攻击。
- CBC 模式 :
- Bit-flipping:利用 Pi=DK(Ci)⊕Ci−1Pi=DK(Ci)⊕Ci−1 ,修改 Ci−1Ci−1 可以控制 PiPi 。
- Padding Oracle:利用服务器对填充错误的反馈,逆向推导明文。
- 流密码 :核心是 密钥流重用。只要发现 C1⊕C2=P1⊕P2C1⊕C2=P1⊕P2 ,就可以利用常见单词(Crib)进行拖拽攻击。