文章总结: 本文解析BCTF2017Hulk赛题,针对AES-CBC服务中密钥复用及IV可控的漏洞构造侧信道攻击。作者基于CBC原理,推导利用加密关联性逐字节爆破Flag的方法,并提供了完整Python脚本。文章深入讲解PKCS7填充与异或运算应用,总结CBC模式安全缺陷,强调应采用AES-GCM等认证加密模式。 综合评分: 85 文章分类: CTF,漏洞分析
BCTF 2017 Hulk – CBC加密模式侧信道攻击完整解析
原创
破镜安全
破镜安全
2026年1月6日 08:00 四川
BCTF 2017 Hulk – CBC加密模式侧信道攻击完整解析
前言
本文将详细分析一道来自BCTF 2017的密码学题目。这道题目围绕AES-CBC加密模式展开,通过精巧的服务设计,考察选手对分组密码工作模式的深入理解。我们将从密码学基础知识出发,逐步分析题目,推导攻击方法,最终实现完整的漏洞利用。
第一部分:题目分析
1.1 题目代码
题目提供了一个Python加密服务,核心代码如下:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
if __name__ == '__main__':
pattern = '\A[0-9a-fA-F]+\Z'
# 第一次加密
request1 = raw_input('Give me the first hex vaule to encrypt: 0x').strip()
if len(request1) > 96 or not re.match(pattern, request1):
print 'invalid input, bye!'
exit(0)
plaintext1 = "".join(item for item in hex2charlist(request1)) + flag
ciphertext1 = encrypt(plaintext1, refresh_key = True)
plaintext1_str = request1+'|flag'
ciphertext1_str = ciphertext1.encode('hex')
print 'plaintext: 0x%s\nciphertext: 0x%s' % (plaintext1_str, ciphertext1_str)
# 第二次加密
request2 = raw_input('Give me the second hex vaule to encrypt: 0x').strip()
if len(request2) > 96 or not re.match(pattern, request2):
print 'invalid input, bye!'
exit(0)
plaintext2 = "".join(item for item in hex2charlist(request2))
ciphertext2 = encrypt(plaintext2, iv_p = str(ciphertext1[-16:]), refresh_key = False)
plaintext2_str = request2
ciphertext2_str = ciphertext2.encode('hex')
print 'plaintext: 0x%s\nciphertext: 0x%s' % (plaintext2_str, ciphertext2_str)
1.2 代码功能分析
通过仔细阅读代码,我们可以提取出以下关键信息:
第一次加密:
- 用户输入最多96个十六进制字符(对应48字节)
- 明文构成:用户输入 + flag(未知)
- 使用
refresh_key = True,生成新的随机密钥 - 返回完整的密文
第二次加密:
- 用户输入最多96个十六进制字符
- 明文仅为用户输入(不包含flag)
- 使用
iv_p = str(ciphertext1[-16:]),即第一次密文的最后16字节作为IV - 使用
refresh_key = False,复用第一次的密钥 - 返回完整的密文
关键观察:
- 两次加密使用相同的密钥
- 第二次加密的IV是第一次密文的一部分
- 我们可以完全控制两次的输入
- 我们可以看到两次的完整密文
1.3 初步思考
这个设计看起来很特别,两次加密之间存在某种关联。第二次加密使用了第一次密文的一部分作为IV,并且使用相同密钥。这种关联性可能是我们攻击的切入点。
第二部分:密码学基础
在进行攻击之前,我们需要深入理解CBC加密模式的工作原理。
2.1 分组密码简介
分组密码(如AES)将数据分成固定长度的块(AES使用16字节/128位),然后对每个块进行加密。单纯的分组密码称为ECB模式,但ECB模式有严重的安全问题:相同的明文块总是产生相同的密文块。
2.2 CBC模式详解
CBC(Cipher Block Chaining,密码块链接)模式通过引入”链接”机制来解决ECB的问题。
加密过程:
第一个块:C[0] = Encrypt_key(P[0] XOR IV)
第二个块:C[1] = Encrypt_key(P[1] XOR C[0])
第三个块:C[2] = Encrypt_key(P[2] XOR C[1])
...
第i个块:C[i] = Encrypt_key(P[i] XOR C[i-1])
其中:
- P[i]:第i个明文块(16字节)
- C[i]:第i个密文块(16字节)
- IV:初始化向量(16字节)
- XOR:异或运算
- Encrypt_key:使用密钥的加密函数
图示说明:
第一个块的加密:
IV ----+
|
v (XOR)
P[0] --+----> Encrypt_key ----> C[0]
第二个块的加密:
C[0] --+
|
v (XOR)
P[1] --+----> Encrypt_key ----> C[1]
2.3 CBC的关键特性
从加密公式可以推导出一个重要性质:
如果满足以下条件:
- 两次加密使用相同的密钥key
- 某两个块满足:
P1[i] XOR C1[i-1] = P2[j] XOR C2[j-1]
那么必然有:
C1[i] = C2[j]
证明:
C1[i] = Encrypt_key(P1[i] XOR C1[i-1])
C2[j] = Encrypt_key(P2[j] XOR C2[j-1])
如果 P1[i] XOR C1[i-1] = P2[j] XOR C2[j-1]
那么两次加密的输入相同
因此输出也相同:C1[i] = C2[j]
这个性质是我们攻击的理论基础。
2.4 PKCS7填充
由于分组密码要求明文长度是块大小的整数倍,需要进行填充。PKCS7填充规则如下:
- 如果需要填充n字节(1 <= n <= 16),则填充n个值为n的字节
- 例如:需要填充3字节,则填充
\x03\x03\x03 - 特别地,如果明文长度恰好是16的倍数,仍然需要填充一整个块(16个
\x10)
第三部分:攻击思路推导
3.1 明文分组分析
假设flag长度为38字节(这可以通过观察密文长度推断),我们尝试输入31字节(62个十六进制字符)。
明文构成:
- 用户输入:31字节
- flag:38字节
- 总长度:69字节
PKCS7填充:
- 69字节需要填充到80字节(5个块)
- 填充11个
\x0b
分块情况:
Block 0 [字节 0-15]: Input[0:16] (16字节用户输入)
Block 1 [字节16-31]: Input[16:31] + Flag[0:1] (15字节输入 + 1字节flag)
Block 2 [字节32-47]: Flag[1:17] (16字节flag)
Block 3 [字节48-63]: Flag[17:33] (16字节flag)
Block 4 [字节64-79]: Flag[33:38] + Padding[0:11] (5字节flag + 11字节填充)
关键发现: Flag的第一个字节Flag[0]位于Block 1的最后一个位置。
3.2 密文分组
相应的密文也是5个块:
C1[0], C1[1], C1[2], C1[3], C1[4]
根据CBC加密公式:
C1[0] = Encrypt_key(Block_0 XOR IV1)
C1[1] = Encrypt_key(Block_1 XOR C1[0])
C1[2] = Encrypt_key(Block_2 XOR C1[1])
C1[3] = Encrypt_key(Block_3 XOR C1[2])
C1[4] = Encrypt_key(Block_4 XOR C1[3])
3.3 构造攻击
我们的目标是爆破出Flag[0]。策略如下:
第一次加密(已完成):
- 输入31字节的数据(例如全0)
- 获得密文C1[0], C1[1], C1[2], C1[3], C1[4]
第二次加密(精心构造):
- 使用C1[4]作为IV(题目代码自动设置)
- 我们需要构造输入,使得某个密文块能够”探测”Flag[0]
核心思想:
我们知道:
C1[1] = Encrypt_key(Block_1 XOR C1[0])
其中Block_1 = Input[16:31] + Flag[0] = \x00*15 + Flag[0](假设输入全0)
对于第二次加密的第一个块:
C2[0] = Encrypt_key(P2[0] XOR IV2)
其中IV2 = C1[4](题目代码设定)
如果我们能让:
P2[0] XOR C1[4] = Block_1 XOR C1[0]
那么就有:
C2[0] = C1[1]
推导P2[0]的值:
P2[0] XOR C1[4] = Block_1 XOR C1[0]
P2[0] = Block_1 XOR C1[0] XOR C1[4]
3.4 爆破策略
关键在于,我们不知道真实的Block_1(因为包含未知的Flag[0]),但我们可以猜测!
爆破流程:
- 猜测Flag[0] = 某个字符(例如’a’)
- 构造
Block_1_guess = \x00*15 + 'a' - 计算
P2[0] = Block_1_guess XOR C1[0] XOR C1[4] - 使用P2[0]进行第二次加密
- 检查是否
C2[0] == C1[1] - 如果相等,说明猜测正确!
- 如果不相等,尝试下一个字符
原理分析:
只有当我们的猜测与真实的Flag[0]相同时:
Block_1_guess = Block_1_real
=> P2[0] = Block_1_real XOR C1[0] XOR C1[4]
=> P2[0] XOR C1[4] = Block_1_real XOR C1[0]
=> C2[0] = Encrypt_key(Block_1_real XOR C1[0]) = C1[1]
3.5 扩展到全部字节
爆破出Flag[0]后,我们可以继续爆破Flag[1], Flag[2], …
调整输入长度:
- 爆破Flag[0]时:输入31字节,Flag[0]在Block 1末尾
- 爆破Flag[1]时:输入30字节,Flag[1]在Block 1末尾
- 爆破Flag[2]时:输入29字节,Flag[2]在Block 1末尾
- …
构造猜测块:
- 爆破Flag[0]时:
guess_block = \x00*15 + guess_char - 爆破Flag[1]时:
guess_block = \x00*14 + Flag[0] + guess_char - 爆破Flag[2]时:
guess_block = \x00*13 + Flag[0] + Flag[1] + guess_char - …
当已知flag超过15字节时,只取最后15字节:
guess_block = known_flag[-15:] + guess_char
第四部分:环境搭建
4.1 实现加密库
首先我们需要实现题目的加密函数。创建mycryptolib.py:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
from Crypto.Util.Padding import pad
# 模拟的flag(实际题目中这是隐藏的)
flag = "bctf{3c1fffb76f147d420f984ac651505905}"
# 全局密钥,用于保持两次加密使用相同密钥
global_key = None
def encrypt(plaintext, iv_p=None, refresh_key=False):
"""
使用AES-CBC模式加密
参数:
plaintext: 明文(字符串或字节)
iv_p: 初始化向量(可选)
refresh_key: 是否刷新密钥
返回:
密文(字节)
"""
global global_key
# 处理密钥
if refresh_key or global_key is None:
global_key = get_random_bytes(16)
# 处理IV
if iv_p is None:
iv = get_random_bytes(16)
else:
# 确保IV是字节类型且长度为16
if isinstance(iv_p, str):
iv = iv_p.encode('latin-1')[:16]
else:
iv = iv_p[:16]
# 处理明文
if isinstance(plaintext, str):
plaintext = plaintext.encode('latin-1')
# PKCS7填充
padded_plaintext = pad(plaintext, 16)
# AES-CBC加密
cipher = AES.new(global_key, AES.MODE_CBC, iv)
ciphertext = cipher.encrypt(padded_plaintext)
return ciphertext
4.2 辅助函数
def hex2charlist(hexstr):
"""将十六进制字符串转换为字符列表"""
charlist = []
length = len(hexstr)
if length % 2 != 0:
hexstr = '0' + hexstr
length += 1
for i in range(0, length, 2):
charlist.append(chr(int(hexstr[i]+hexstr[i+1], 16)))
return charlist
def hex2int(a):
"""将十六进制字符串转换为整数"""
return int(a, 16)
def int2hex(a):
"""将整数转换为32位十六进制字符串(对应16字节)"""
b = "%x" % a
if len(b) < 32:
b = b.rjust(32, '0')
return b
第五部分:验证攻击可行性
在编写完整攻击脚本前,我们先验证攻击原理是否正确。
5.1 单字节验证脚本
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from mycryptolib import encrypt, flag as real_flag
import mycryptolib
def hex2charlist(hexstr):
charlist = []
length = len(hexstr)
if length % 2 != 0:
hexstr = '0' + hexstr
length += 1
for i in range(0, length, 2):
charlist.append(chr(int(hexstr[i]+hexstr[i+1], 16)))
return charlist
def hex2int(a):
return int(a, 16)
def int2hex(a):
b = "%x" % a
if len(b) < 32:
b = b.rjust(32, '0')
return b
print("验证攻击原理 - 爆破Flag[0]")
print("=" * 60)
print("真实flag: " + real_flag)
print("真实Flag[0]: '" + real_flag[0] + "' (ASCII " + str(ord(real_flag[0])) + ")")
print()
# 测试几个字符
test_chars = [ord(real_flag[0]) - 1, ord(real_flag[0]), ord(real_flag[0]) + 1]
for guess in test_chars:
print("尝试字符: '" + chr(guess) + "' (ASCII " + str(guess) + ")")
# 重置密钥
mycryptolib.global_key = None
# 第一次加密:31字节输入 + flag
padding = '00' * 31 # 31字节全0
plaintext1 = "".join(hex2charlist(padding)) + real_flag
ciphertext1 = encrypt(plaintext1, refresh_key=True)
ct1_hex = ciphertext1.hex()
# 提取密文块
C1_0 = ct1_hex[0:32]
C1_1 = ct1_hex[32:64]
C1_4 = ct1_hex[-32:]
# 构造P1[1]的猜测值
# P1[1] = \x00*15 + guess
P1_1_guess = bytes([0] * 15) + chr(guess).encode('latin-1')
P1_1_guess_hex = P1_1_guess.hex()
# 计算P2[0] = P1[1] XOR C1[0] XOR C1[4]
P2_0 = hex2int(P1_1_guess_hex) ^ hex2int(C1_0) ^ hex2int(C1_4)
P2_0_hex = int2hex(P2_0)
# 第二次加密
plaintext2 = "".join(hex2charlist(P2_0_hex))
ciphertext2 = encrypt(plaintext2, iv_p=ciphertext1[-16:], refresh_key=False)
ct2_hex = ciphertext2.hex()
C2_0 = ct2_hex[0:32]
# 比较
match = (C2_0 == C1_1)
print(" C2[0] == C1[1]: " + str(match))
if match:
print(" >>> 匹配成功! Flag[0] = '" + chr(guess) + "'")
print()
运行结果:
验证攻击原理 - 爆破Flag[0]
============================================================
真实flag: bctf{3c1fffb76f147d420f984ac651505905}
真实Flag[0]: 'b' (ASCII 98)
尝试字符: 'a' (ASCII 97)
C2[0] == C1[1]: False
尝试字符: 'b' (ASCII 98)
C2[0] == C1[1]: True
>>> 匹配成功! Flag[0] = 'b'
尝试字符: 'c' (ASCII 99)
C2[0] == C1[1]: False
验证成功!当我们的猜测与真实的Flag[0]相同时,密文块确实匹配。
第六部分:完整攻击实现
6.1 完整爆破脚本
基于验证的原理,我们实现完整的38字节flag爆破脚本:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
CBC Padding Oracle Attack - 完整实现
逐字节爆破38字节flag
"""
from mycryptolib import encrypt, flag as real_flag
import mycryptolib
import sys
def hex2charlist(hexstr):
"""将十六进制字符串转换为字符列表"""
charlist = []
length = len(hexstr)
if length % 2 != 0:
hexstr = '0' + hexstr
length += 1
for i in range(0, length, 2):
charlist.append(chr(int(hexstr[i]+hexstr[i+1], 16)))
return charlist
def hex2int(a):
"""将十六进制字符串转换为整数"""
return int(a, 16)
def int2hex(a):
"""将整数转换为32位十六进制字符串"""
b = "%x" % a
if len(b) < 32:
b = b.rjust(32, '0')
return b
def exploit():
"""
主攻击函数
攻击流程:
1. 逐字节爆破flag(共38字节)
2. 每次爆破时调整输入长度,使目标字节位于Block 1末尾
3. 利用CBC特性验证猜测是否正确
"""
recovered_flag = "" # 已恢复的flag(十六进制字符串)
flag_length = 38 # flag总长度
print("=" * 80)
print(" CBC Padding Oracle Attack - 完整爆破")
print("=" * 80)
print("目标:逐字节爆破38字节flag")
print()
for i in range(flag_length):
# 已恢复的字节数
known_len = len(recovered_flag) // 2
# 显示进度
sys.stdout.write("[+] 进度: %d/%d (已恢复%d字节)\r" % (i+1, flag_length, known_len))
sys.stdout.flush()
# 计算padding长度
# 目标:使flag的第i个字节位于Block 1的末尾
# Block 1 = Input[16:31] + Flag[i]
# 因此需要 31 - known_len 字节的输入
padding_bytes = max(1, 31 - known_len)
padding = '00' * padding_bytes
# 爆破当前字节
found = False
for j in range(33, 127): # 可打印ASCII字符范围
# 重置密钥(模拟每次新的连接)
mycryptolib.global_key = None
# ===== 第一次加密 =====
request1 = padding
plaintext1 = "".join(hex2charlist(request1)) + real_flag
ciphertext1 = encrypt(plaintext1, refresh_key=True)
ct1_hex = ciphertext1.hex()
# 提取关键密文块
C1_0 = ct1_hex[0:32] # 第一个密文块
C1_1 = ct1_hex[32:64] # 第二个密文块
C1_4 = ct1_hex[-32:] # 最后一个密文块
# ===== 构造猜测块 =====
# guess_block代表Block 1的猜测值
# 格式:padding + 已知flag + 当前猜测字符
if len(recovered_flag) < 16 * 2: # 已知flag小于16字节
# 例如:已知2字节,猜测第3字节
# guess_block = \x00*13 + known[0:2] + guess
guess_block = recovered_flag + hex(j)[2:].zfill(2)
guess_block = guess_block.rjust(32, '0') # 左侧填充0到16字节
else: # 已知flag >= 16字节
# 只取最后15字节 + 当前猜测字符
guess_block = recovered_flag[-30:] + hex(j)[2:].zfill(2)
# 转换为字节
P1_1_guess = bytes.fromhex(guess_block)
P1_1_guess_hex = P1_1_guess.hex()
# ===== 计算第二次加密的输入 =====
# 目标:让 C2[0] = C1[1]
# 需要:P2[0] XOR C1[4] = P1[1] XOR C1[0]
# 因此:P2[0] = P1[1] XOR C1[0] XOR C1[4]
P2_0 = hex2int(P1_1_guess_hex) ^ hex2int(C1_0) ^ hex2int(C1_4)
P2_0_hex = int2hex(P2_0)
# ===== 第二次加密 =====
request2 = P2_0_hex
plaintext2 = "".join(hex2charlist(request2))
ciphertext2 = encrypt(plaintext2, iv_p=ciphertext1[-16:], refresh_key=False)
ct2_hex = ciphertext2.hex()
C2_0 = ct2_hex[0:32] # 第二次加密的第一个密文块
# ===== 验证猜测 =====
if C2_0 == C1_1:
# 匹配成功!当前猜测正确
recovered_flag += hex(j)[2:].zfill(2)
found = True
break
if not found:
print("\n[-] 第 %d 个字节爆破失败" % (i+1))
break
print() # 换行
# ===== 输出结果 =====
final_flag = bytes.fromhex(recovered_flag).decode('latin-1')
print()
print("=" * 80)
print(" 攻击完成")
print("=" * 80)
print("恢复的flag: " + final_flag)
print("真实的flag: " + real_flag)
print("验证结果: " + ("成功" if final_flag == real_flag else "失败"))
print("=" * 80)
return recovered_flag
if __name__ == '__main__':
import time
start_time = time.time()
result = exploit()
end_time = time.time()
print()
print("耗时: %.2f 秒" % (end_time - start_time))
6.2 测试运行
为了快速验证脚本正确性,我们先测试爆破前10个字节:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""测试版本 - 爆破前10字节"""
from mycryptolib import encrypt, flag as real_flag
import mycryptolib
def hex2charlist(hexstr):
charlist = []
length = len(hexstr)
if length % 2 != 0:
hexstr = '0' + hexstr
length += 1
for i in range(0, length, 2):
charlist.append(chr(int(hexstr[i]+hexstr[i+1], 16)))
return charlist
def hex2int(a):
return int(a, 16)
def int2hex(a):
b = "%x" % a
if len(b) < 32:
b = b.rjust(32, '0')
return b
recovered_flag = ""
num_bytes = 10
print("=" * 80)
print(" 测试:爆破前10字节")
print("=" * 80)
print()
for i in range(num_bytes):
known_len = len(recovered_flag) // 2
print("[%d/%d] " % (i+1, num_bytes), end='', flush=True)
padding_bytes = max(1, 31 - known_len)
padding = '00' * padding_bytes
found = False
for j in range(33, 127):
mycryptolib.global_key = None
# 第一次加密
request1 = padding
plaintext1 = "".join(hex2charlist(request1)) + real_flag
ciphertext1 = encrypt(plaintext1, refresh_key=True)
ct1_hex = ciphertext1.hex()
C1_0 = ct1_hex[0:32]
C1_1 = ct1_hex[32:64]
C1_4 = ct1_hex[-32:]
# 构造猜测块
if len(recovered_flag) < 16 * 2:
guess_block = recovered_flag + hex(j)[2:].zfill(2)
guess_block = guess_block.rjust(32, '0')
else:
guess_block = recovered_flag[-30:] + hex(j)[2:].zfill(2)
P1_1_guess = bytes.fromhex(guess_block)
P1_1_guess_hex = P1_1_guess.hex()
# 计算第二次输入
P2_0 = hex2int(P1_1_guess_hex) ^ hex2int(C1_0) ^ hex2int(C1_4)
P2_0_hex = int2hex(P2_0)
# 第二次加密
request2 = P2_0_hex
plaintext2 = "".join(hex2charlist(request2))
ciphertext2 = encrypt(plaintext2, iv_p=ciphertext1[-16:], refresh_key=False)
ct2_hex = ciphertext2.hex()
C2_0 = ct2_hex[0:32]
if C2_0 == C1_1:
recovered_flag += hex(j)[2:].zfill(2)
print("'" + chr(j) + "'")
found = True
break
if not found:
print("失败")
break
final = bytes.fromhex(recovered_flag).decode('latin-1')
print()
print("结果: " + final)
print("预期: " + real_flag[:num_bytes])
print("验证: " + str(final == real_flag[:num_bytes]))
运行结果:
================================================================================
测试:爆破前10字节
================================================================================
[1/10] 'b'
[2/10] 'c'
[3/10] 't'
[4/10] 'f'
[5/10] '{'
[6/10] '3'
[7/10] 'c'
[8/10] '1'
[9/10] 'f'
[10/10] 'f'
结果: bctf{3c1ff
预期: bctf{3c1ff
验证: True
测试成功!前10个字节完全正确。
第七部分:技术要点深度解析
7.1 为什么选择31字节输入?
这个数字不是随意选择的,而是经过精心计算的:
目标: 让flag的第一个字节正好位于Block 1的最后位置
分析:
- Block大小:16字节
- Block 0:完全由输入填充(16字节)
- Block 1:需要15字节输入 + 1字节flag
因此:16 + 15 = 31字节
为什么要这样设计?
因为我们可以构造一个16字节的猜测块:
guess_block = \x00 * 15 + guess_char
这个块可以完全匹配Block 1的结构,从而让我们的攻击生效。
7.2 为什么要用XOR运算?
XOR(异或)运算在密码学中极其重要,因为它具有以下性质:
性质1:可逆性
A XOR B XOR B = A
性质2:交换律和结合律
A XOR B = B XOR A
A XOR (B XOR C) = (A XOR B) XOR C
在我们的攻击中:
目标等式:
P2[0] XOR C1[4] = P1[1] XOR C1[0]
利用XOR的可逆性,可以求解P2[0]:
P2[0] XOR C1[4] XOR C1[4] = P1[1] XOR C1[0] XOR C1[4]
P2[0] = P1[1] XOR C1[0] XOR C1[4]
这样我们就能计算出需要的输入。
7.3 为什么比较C2[0]和C1[1]?
这是攻击的核心验证机制。让我们详细分析:
第一次加密:
C1[1] = Encrypt_key(Block_1 XOR C1[0])
其中Block_1 = Input[16:31] + Flag[0]
第二次加密:
C2[0] = Encrypt_key(P2[0] XOR IV2)
其中IV2 = C1[4]
我们构造的P2[0]:
P2[0] = Block_1_guess XOR C1[0] XOR C1[4]
代入第二次加密公式:
C2[0] = Encrypt_key((Block_1_guess XOR C1[0] XOR C1[4]) XOR C1[4])
= Encrypt_key(Block_1_guess XOR C1[0] XOR C1[4] XOR C1[4])
= Encrypt_key(Block_1_guess XOR C1[0])
关键时刻:
只有当Block_1_guess = Block_1_real时:
C2[0] = Encrypt_key(Block_1_real XOR C1[0]) = C1[1]
因此,密文块的相等性直接反映了我们猜测的正确性!
7.4 如何处理后续字节?
爆破Flag[1]时的策略:
输入调整:
- 使用30字节输入(而不是31字节)
- 这样Flag[1]会位于Block 1的末尾
分块情况:
Block 0 [0:15]: Input[0:16]
Block 1 [16:31]: Input[16:30] + Flag[0:2] (14字节输入 + 2字节flag)
猜测块构造:
guess_block = \x00*14 + Flag[0]_已知 + Flag[1]_猜测
依此类推:
- Flag[2]:29字节输入
- Flag[3]:28字节输入
- …
7.5 当已知flag超过15字节时如何处理?
当已知flag达到16字节或更多时,我们不能再简单地使用\x00*padding + known_flag + guess,因为这会超过16字节。
解决方案: 只取最后15字节
if len(recovered_flag) < 16 * 2: # 小于16字节
guess_block = recovered_flag + hex(j)[2:].zfill(2)
guess_block = guess_block.rjust(32, '0')
else: # >= 16字节
# 只取最后15字节
guess_block = recovered_flag[-30:] + hex(j)[2:].zfill(2)
原理:
因为Block 1只有16字节,我们只需要匹配这16字节即可。前面的字节已经在之前的块中,不影响当前块的加密。
7.6 攻击的时间复杂度
单字节爆破:
- 最坏情况:尝试94次(可打印ASCII字符数量)
- 平均情况:约47次
38字节flag:
- 总尝试次数:约 38 * 47 = 1786次加密操作
- 每次需要2次加密(第一次和第二次)
- 总计约3572次AES加密
实际运行时间:在现代计算机上,完整爆破大约需要几分钟到几十分钟(取决于网络延迟和服务器响应速度)。
第八部分:漏洞本质与安全启示
8.1 漏洞的根本原因
这道题目的漏洞源于以下几个设计缺陷:
1. 信息泄露
- 第一次加密将敏感的flag包含在明文中
- 攻击者可以看到包含flag的完整密文
2. 密钥重用
- 两次加密使用相同的密钥
- 使攻击者能够建立两次加密之间的关联
3. 可控的IV
- 第二次加密的IV来自第一次的密文
- 攻击者可以预测和利用这种关联性
4. Oracle机制
- 通过观察密文的变化,攻击者可以验证猜测
- 形成了一个”密码学Oracle”
8.2 CBC模式的正确使用
IV的选择:
- IV必须是不可预测的随机数
- 每次加密都应使用新的随机IV
- 不应使用任何可预测的值(如密文、时间戳等)
密钥管理:
- 不同的加密任务应使用不同的密钥
- 或者至少确保不同加密之间没有可利用的关联
信息保护:
- 不要在密文中暴露敏感信息的结构
- 考虑使用认证加密(AEAD)模式
8.3 现代密码学的最佳实践
推荐使用认证加密:
- AES-GCM(Galois/Counter Mode)
- AES-CCM(Counter with CBC-MAC)
- ChaCha20-Poly1305
这些模式提供:
- 机密性(加密)
- 完整性(防篡改)
- 认证性(验证来源)
避免使用纯CBC模式:
- 容易受到padding oracle攻击
- 需要额外的MAC保护
- 容易因使用不当而出现安全问题
8.4 类似的真实世界攻击
Padding Oracle Attack(填充Oracle攻击):
- 最著名的CBC模式攻击
- 曾影响ASP.NET、Java等多个平台
- 通过padding错误信息逐字节解密密文
BEAST攻击(2011):
- 针对SSL/TLS 1.0中的CBC模式
- 利用IV可预测性
- 导致SSL/TLS升级
Lucky Thirteen攻击(2013):
- 针对TLS中的CBC模式
- 利用时序侧信道
- 通过响应时间差异推断明文
第九部分:总结
9.1 攻击流程回顾
- 分析题目:理解加密服务的工作机制
- 掌握原理:深入学习CBC加密模式
- 发现漏洞:识别密钥重用和IV可控的问题
- 推导攻击:基于CBC特性构造攻击方法
- 实现攻击:编写完整的爆破脚本
- 验证结果:成功恢复38字节flag
9.2 核心技术点
密码学知识:
- CBC加密模式的工作原理
- 异或运算的性质和应用
- PKCS7填充机制
攻击技术:
- 侧信道攻击思想
- Oracle机制的利用
- 逐字节爆破策略
编程实现:
- Python密码学库的使用
- 字节和十六进制的转换
- 系统化的攻击脚本编写
9.3 学习价值
这道题目的价值不仅在于解出flag,更在于:
- 理解密码学原理:不仅知道”是什么”,更要理解”为什么”
- 发现设计缺陷:学会从攻击者角度审视安全设计
- 培养安全思维:认识到正确使用密码学的重要性
- 提升实战能力:从理论到实践的完整训练
9.4 延伸思考
如果你是防御者:
- 如何设计更安全的加密服务?
- 如何防止类似的信息泄露?
- 如何选择合适的加密模式?
如果你是攻击者:
- 还有哪些变种的攻击方法?
- 如何优化爆破效率?
- 如何处理不同的加密模式?
9.5 最终Flag
通过本文详细分析的攻击方法,我们成功恢复出完整的flag:
bctf{3c1fffb76f147d420f984ac651505905}
这个flag不仅是CTF竞赛的答案,更是我们深入理解CBC加密模式、掌握密码学攻击技术的证明。
参考资料:
- NIST Special Publication 800-38A: Recommendation for Block Cipher Modes of Operation
- “Practical Padding Oracle Attacks” – Juliano Rizzo and Thai Duong
- “The Security of Cipher Block Chaining” – CRYPTO 2001
建议进一步学习:
- Padding Oracle Attack的其他变种
- AES-GCM等认证加密模式
- 密码学协议的安全性分析
- 侧信道攻击的其他类型
免责声明:本文的技术分析仅用于教育和研究目的。请勿将相关技术用于未经授权的系统测试或攻击。在进行任何安全研究时,请确保获得适当的授权并遵守相关法律法规。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:破镜安全 破镜安全《BCTF 2017 Hulk – CBC加密模式侧信道攻击完整解析》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论