本文将深入分析一道经典的密码学CTF题目,该题考察了AES CBC加密模式的安全性缺陷。题目分为两个阶段:工作量证明(Proof of Work)和多轮AES CBC挑战。通过完成9轮挑战后,可以获取最终的flag。
题目服务器地址:47.107.168.16:40605
连接服务器后,首先遇到的是工作量证明(PoW)挑战。服务器要求找到一个特定的字节序列x,使其SHA256哈希值的前8位符合给定值。
x = chr(random.randint(0,0xff)) + chr(random.randint(0,0xff)) + chr(random.randint(0,0x1f))
hashlib.sha256(x).hexdigest()[0:8] == '指定的8位十六进制值'
这意味着x由3个字节组成:
第一个字节:范围0-255 (256种可能)
第二个字节:范围0-255 (256种可能)
第三个字节:范围0-31 (32种可能)
总搜索空间为:256 × 256 × 32 = 2,097,152
由于搜索空间相对较小,我们采用暴力枚举的方法:
def solve_pow(target_hash):
"""
暴力枚举求解工作量证明
"""
for a in range(256):
for b in range(256):
for c in range(32):
x = bytes([a, b, c])
hash_result = hashlib.sha256(x).hexdigest()
if hash_result[:8] == target_hash:
return x.hex()
return None
这个过程通常需要1-2分钟完成。虽然可以通过多线程优化,但单线程已经足够快。
通过PoW验证后,进入真正的密码学挑战环节。理解这个挑战需要深入了解AES CBC模式的工作原理。
CBC(Cipher Block Chaining)是一种分组密码工作模式。在CBC模式中,每个明文块在加密前都要与前一个密文块进行XOR运算。
加密流程:
C[0] = AES_Encrypt(P[0] XOR IV, key)
C[i] = AES_Encrypt(P[i] XOR C[i-1], key) (i > 0)
其中:
P[i] 是第i个明文块
C[i] 是第i个密文块
IV 是初始化向量
AES块大小为16字节
解密过程是加密的逆运算:
P[0] = AES_Decrypt(C[0], key) XOR IV
P[i] = AES_Decrypt(C[i], key) XOR C[i-1] (i > 0)
这个公式是理解后续攻击的关键。注意到每个明文块的解密结果依赖于前一个密文块。
每轮挑战,服务器提供:
c1:已知密文(base64编码,256字节)
m1:已知明文(base64编码,256字节)
8个目标要求,格式为:m2[pos] = char
要求:构造新的密文c2,使得解密后的明文m2在指定的8个位置上具有目标字符。
示例:
m2[23]=Z
m2[63]=R
m2[86]=l
m2[119]=x
m2[147]=Q
m2[182]=W
m2[213]=r
m2[254]=c
CBC模式的安全弱点在于:修改前一个密文块的某个字节,可以精确控制当前明文块对应位置的字节。这就是所谓的字节翻转攻击(Bit-Flipping Attack)。
假设我们想要修改位置pos的明文字符,从原始值m1[pos]变为目标值target[pos]。
首先,我们需要理解位置pos在哪个块中:
block_num = pos // 16 # 块编号
block_offset = pos % 16 # 块内偏移
根据CBC解密公式:
原始解密:m1[pos] = D[pos] XOR c1[pos-16]
目标解密:m2[pos] = D[pos] XOR c2[pos-16]
这里D[pos]代表AES解密函数的中间输出,它只依赖于密文块和密钥。由于我们不改变当前密文块,也不知道密钥,所以D[pos]保持不变。
从第一个等式可以得出:
D[pos] = m1[pos] XOR c1[pos-16]
代入第二个等式:
m2[pos] = (m1[pos] XOR c1[pos-16]) XOR c2[pos-16]
要使m2[pos]等于target[pos],需要:
target[pos] = m1[pos] XOR c1[pos-16] XOR c2[pos-16]
整理得到攻击公式:
c2[pos-16] = c1[pos-16] XOR m1[pos] XOR target[pos]
对于目标位置pos,我们需要修改的密文位置是:
modify_pos = (block_num - 1) * 16 + block_offset
这是因为在CBC模式中,要影响块i的解密结果,需要修改块i-1的密文。
需要注意的是,第一个块(block 0)的明文是由IV控制的:
P[0] = AES_Decrypt(C[0], key) XOR IV
要修改第一个块的明文,需要修改IV,但通常我们无法访问IV。因此,这个攻击只能影响第二个块(block 1)及之后的块。
基于上述原理,我们实现CBC字节翻转攻击函数:
def cbc_bitflip_attack(c1_b64, m1_b64, targets):
"""
执行CBC字节翻转攻击
参数:
c1_b64: Base64编码的原始密文
m1_b64: Base64编码的原始明文
targets: 字典,键为位置,值为目标字符的ASCII码
返回:
Base64编码的修改后密文c2
"""
# 解码base64
c1 = base64.b64decode(c1_b64)
m1 = base64.b64decode(m1_b64)
# 从原始密文开始构造c2
c2 = bytearray(c1)
# 对每个目标位置应用字节翻转
for pos, target_char in targets.items():
# 计算块信息
block_num = pos // 16
block_offset = pos % 16
if block_num == 0:
# 第一块需要修改IV,通常无法访问
continue
# 计算需要修改的密文位置
modify_pos = (block_num - 1) * 16 + block_offset
# 应用XOR翻转公式
c2[modify_pos] = c1[modify_pos] ^ m1[pos] ^ target_char
# 编码为base64返回
return base64.b64encode(bytes(c2)).decode()
从服务器响应中提取挑战数据需要使用正则表达式:
def parse_aes_challenge(text):
"""
解析AES挑战数据
"""
# 提取c1 (密文)
c1_match = re.search(
r"c1\.encode\('base64'\)=([A-Za-z0-9+/=\s]+?)(?=\n#|\n@|$)",
text,
re.DOTALL
)
c1_b64 = c1_match.group(1).replace('\n', '').replace(' ', '').strip()
# 提取m1 (明文)
m1_match = re.search(
r"m1\.encode\('base64'\)=([A-Za-z0-9+/=\s]+?)(?=\n#|\n@|$)",
text,
re.DOTALL
)
m1_b64 = m1_match.group(1).replace('\n', '').replace(' ', '').strip()
# 提取目标位置和字符
targets = {}
for match in re.finditer(r"m2\[(\d+)\]=(\w)", text):
pos = int(match.group(1))
char = match.group(2)
targets[pos] = ord(char)
return {
'c1_b64': c1_b64,
'm1_b64': m1_b64,
'targets': targets
}
我们采用面向对象的设计,创建一个AES挑战求解器类:
class AESChallengeSolver:
"""AES CBC字节翻转攻击求解器"""
def __init__(self, host, port):
self.host = host
self.port = port
self.sock = None
def connect(self):
"""连接到挑战服务器"""
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.connect((self.host, self.port))
def disconnect(self):
"""关闭连接"""
if self.sock:
self.sock.close()
def recv_data(self, timeout=2.0):
"""接收数据,设置超时"""
all_data = ""
self.sock.settimeout(timeout)
while True:
try:
data = self.sock.recv(4096).decode('utf-8', errors='ignore')
if data:
all_data += data
else:
break
except socket.timeout:
break
return all_data
def send_data(self, data):
"""发送数据"""
self.sock.send((data + "\n").encode())
def run(self):
"""主执行流程"""
# 连接服务器
self.connect()
# 第一步:解决PoW挑战
pow_data = self.recv_data(timeout=3)
target_hash = self.parse_pow_challenge(pow_data)
pow_answer = self.solve_pow(target_hash)
self.send_data(pow_answer)
# 第二步:多轮AES挑战
round_num = 0
while round_num < 20:
round_num += 1
# 接收挑战数据
challenge_data = self.recv_data(timeout=3)
# 检查是否获得flag
if 'flag{' in challenge_data:
flag_match = re.search(r'(flag\{[^}]+\})', challenge_data)
if flag_match:
print(f"成功获取FLAG: {flag_match.group(1)}")
break
# 解析AES挑战
aes_challenge = self.parse_aes_challenge(challenge_data)
if not aes_challenge:
break
# 执行CBC字节翻转攻击
c2_answer = self.cbc_bitflip_attack(
aes_challenge['c1_b64'],
aes_challenge['m1_b64'],
aes_challenge['targets']
)
# 提交答案
self.send_data(c2_answer)
self.disconnect()
让我们通过一个真实的例子来理解攻击过程。
假设在第一轮挑战中,我们需要将位置23的字符从'X'修改为'Z'。
已知信息:
位置23的原始明文:m1[23] = 'X' (ASCII码0x58)
目标字符:target = 'Z' (ASCII码0x5A)
原始密文字节:c1[7] = 0xCD (假设值)
位置计算:
pos = 23
block_num = 23 // 16 = 1 # 位置23在第1块
block_offset = 23 % 16 = 7 # 块内偏移为7
modify_pos = (1 - 1) * 16 + 7 = 7 # 需要修改密文的位置7
应用攻击公式:
c2[7] = c1[7] XOR m1[23] XOR target
= 0xCD XOR 0x58 XOR 0x5A
二进制验证:
0xCD = 11001101
0x58 = 01011000
-------- XOR
10010101
0x5A = 01011010
-------- XOR
0xCF = 11001111
因此,c2[7] = 0xCF
攻击效果:
当服务器使用修改后的密文c2进行解密时:
解密中间值 D = AES_Decrypt(C1, key) (保持不变)
m2[23] = D[7] XOR c2[7]
= D[7] XOR 0xCF
由于原始解密时:
m1[23] = D[7] XOR c1[7]
0x58 = D[7] XOR 0xCD
D[7] = 0x58 XOR 0xCD = 0x95
所以:
m2[23] = 0x95 XOR 0xCF = 0x5A = 'Z'
成功将字符从'X'修改为'Z',而其他位置的字符保持不变。
在实际的第一轮挑战中,我们需要同时修改8个位置:
位置 | 块号 | 偏移 | 修改位置 | 原字符 | 目标字符
-----|------|------|----------|--------|----------
23 | 1 | 7 | c2[7] | 'X' | 'Z'
63 | 3 | 15 | c2[47] | 'G' | 'R'
86 | 5 | 6 | c2[70] | 'M' | 'l'
119 | 7 | 7 | c2[103] | 'J' | 'x'
147 | 9 | 3 | c2[131] | 'P' | 'Q'
182 | 11 | 6 | c2[166] | 'Y' | 'W'
213 | 13 | 5 | c2[197] | 'O' | 'r'
254 | 15 | 14 | c2[238] | 'Z' | 'c'
我们的程序会自动对每个位置应用上述公式,一次性修改8个密文字节,从而精确控制8个明文字符。
运行完整的攻击脚本后,成功完成9轮挑战:
============================================================
AES CBC Bit-Flipping Attack - Complete Solution
============================================================
[+] Connected to 47.107.168.16:40605
[STEP 1] Proof of Work Challenge
[*] Solving PoW: sha256(x)[0:8] == '5767ab2e'
[+] PoW solved: x = a4781f
[STEP 2] Multi-Round AES CBC Challenges
============================================================
ROUND 1
============================================================
[+] Parsed AES challenge
Targets: 8 positions
[*] Performing CBC bit-flipping attack...
[+] Generated c2 (length: 344 chars)
[*] Sending answer to server...
[继续完成ROUND 2-9...]
============================================================
SUCCESS! FLAG CAPTURED!
============================================================
flag{2b6f73f87a8350b130577ced2c58c454}
最终FLAG:flag{2b6f73f87a8350b130577ced2c58c454}
这道题目揭示了CBC模式在没有完整性保护时的严重安全问题:
1. 保密性不等于完整性
CBC模式提供了保密性(攻击者无法读取明文),但不提供完整性保护(攻击者可以修改密文)。即使攻击者不知道加密密钥,也能通过修改密文来精确控制解密结果。
2. 可锻造性
在已知明文-密文对的情况下,攻击者可以构造新的密文,使其解密后产生任意想要的明文。这种攻击被称为"选择密文攻击"(Chosen Ciphertext Attack)的一种形式。
3. 实际危害
在真实系统中,这种攻击可能导致:
权限提升(修改用户角色信息)
数据篡改(修改交易金额)
认证绕过(修改登录凭证)
方案一:使用认证加密模式
现代密码学推荐使用AEAD(Authenticated Encryption with Associated Data)模式:
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
# 加密
aesgcm = AESGCM(key)
ciphertext = aesgcm.encrypt(nonce, plaintext, associated_data)
# 解密(自动验证完整性)
try:
plaintext = aesgcm.decrypt(nonce, ciphertext, associated_data)
except InvalidTag:
print("密文被篡改,拒绝解密")
推荐的AEAD模式:
AES-GCM(最流行)
ChaCha20-Poly1305(移动设备友好)
AES-CCM
方案二:Encrypt-then-MAC
如果必须使用CBC模式,应该配合HMAC使用:
import hmac
from hashlib import sha256
# 加密端
ciphertext = aes_cbc_encrypt(plaintext, key, iv)
mac = hmac.new(mac_key, ciphertext, sha256).digest()
message = ciphertext + mac
# 解密端
received_ciphertext = message[:-32]
received_mac = message[-32:]
# 先验证MAC
computed_mac = hmac.new(mac_key, received_ciphertext, sha256).digest()
if not hmac.compare_digest(computed_mac, received_mac):
raise ValueError("MAC验证失败,密文已被篡改")
# MAC验证通过后才解密
plaintext = aes_cbc_decrypt(received_ciphertext, key, iv)
关键点:
必须先验证MAC,再解密
使用constant-time比较防止时序攻击
加密密钥和MAC密钥必须独立
方案三:安全参数选择
如果使用上述方案,还需注意:
IV必须随机且不可预测
每条消息使用唯一的IV
密钥长度至少128位(推荐256位)
使用密码学安全的随机数生成器
import os
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
# 生成随机IV
iv = os.urandom(16) # 16字节 = 128位
# 正确使用
cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
encryptor = cipher.encryptor()
ciphertext = encryptor.update(plaintext) + encryptor.finalize()
# 错误使用(可预测的IV)
# iv = b'\x00' * 16 # 永远不要这样做!
1. 密码学原理
深入理解CBC模式的加密/解密流程
掌握XOR运算的可逆性
理解保密性与完整性的区别
2. 数学推导
从CBC解密公式推导攻击公式
理解块和字节位置的计算方法
掌握二进制运算验证技巧
3. 编程实现
Socket网络编程
Base64编解码
正则表达式数据提取
字节级数据操作
4. 自动化技术
多轮交互式挑战处理
异常处理和重试机制
日志记录和调试输出
这道题目不仅是一个CTF挑战,更是一个深入学习密码学的绝佳案例:
理论层面:
理解为什么现代密码学强调"认证加密"
认识到加密算法的正确使用比算法本身更重要
学习密码学中"攻击模型"的概念
实践层面:
从理论推导到代码实现的完整流程
实际攻击案例的调试和优化经验
安全防御措施的设计和实施
工程层面:
清晰的代码结构和文档
可维护的自动化脚本
完整的错误处理机制
时间复杂度:
PoW求解:O(256 × 256 × 32) ≈ O(2^21),约200万次哈希运算
CBC攻击:O(n),n为目标位置数量(每轮8个)
总体:受PoW阶段主导,约1-2分钟
空间复杂度:
O(密文长度),每轮约256字节
内存占用极小,适合任何环境运行
这种攻击在以下情况下有效:
使用CBC模式但没有完整性保护
攻击者能够截获和修改密文
攻击者知道部分或全部明文
系统不验证密文的完整性就解密
在以下情况下攻击失效:
使用认证加密模式(AES-GCM等)
正确实施了Encrypt-then-MAC
解密前验证了HMAC
使用了数字签名
本文通过对一道CTF密码学题目的深入分析,展示了AES CBC模式在缺乏完整性保护时的安全漏洞。我们从数学原理推导出攻击公式,实现了完整的自动化攻击脚本,并深入探讨了正确的防御措施。
密码学是一个"细节决定成败"的领域。即使使用了强大的加密算法,错误的使用方式仍然会导致严重的安全问题。这道题目提醒我们:
加密不等于安全,还需要认证
理解原理比记住API更重要
安全是一个系统工程,不能依赖单一措施
实践是检验理论的最好方式
希望本文能帮助读者深入理解CBC模式的工作原理和安全隐患,在实际开发中选择合适的加密方案,构建真正安全的系统。
NIST SP 800-38A: Recommendation for Block Cipher Modes of Operation
RFC 5116: An Interface and Algorithms for Authenticated Encryption
OWASP Cryptographic Failures (原Top 10 A3:2017-Sensitive Data Exposure)
"Cryptography Engineering" by Ferguson, Schneier, and Kohno
Dan Boneh的密码学课程(Coursera)
由于篇幅限制,完整的攻击代码已整合到独立的Python脚本中。主要包括:
complete_solution.py:完整的自动化攻击脚本
attack_visualization.py:攻击原理的可视化演示
solve_pow.py:PoW独立求解器
这些代码采用面向对象设计,具有良好的可读性和可维护性,可作为学习密码学编程的参考范例。
最终获取的FLAG:flag{2b6f73f87a8350b130577ced2c58c454}
本文仅用于安全研究和教育目的。请勿将相关技术用于未经授权的系统。