文章总结: 本文详细解析了tinyCTF2014的一道密码学题目,展示了AES-ECB模式加密图像的安全隐患。作者通过识别OpenSSL文件头、统计分析密文块高重复率确认ECB模式,并结合文件大小与线索逆向推算出图像分辨率与色深。最终通过构造合法BMP文件头附加密文数据,成功还原出图像轮廓并获取Flag,生动证明了ECB模式无法隐藏明文结构特征。 综合评分: 98 文章分类: CTF,漏洞分析,逆向分析
第一步:识别文件格式——OpenSSL Salted 格式
先检查 ecb.bmp 的前 16 个字节:
$ xxd ecb.bmp | head -n 2
0000000: 5361 6c74 6564 5f5f ab31 b5e5 ca3d b94d Salted__.1...=.M
0000010: f409 1aa5 df88 b72c 0ebd 8a73 9815 ba69 .......,...s...i
开头 8 字节是 ASCII 字符串 Salted__,这是 OpenSSL 使用 enc 命令加密时产生的特征标识。
OpenSSL 加密文件的固定格式如下:
偏移 长度 内容
0 8 魔数 "Salted__"(ASCII)
8 8 盐值(Salt),用于密钥派生的随机数
16 N 实际的加密密文数据(AES 分组对齐)
因此,真正的加密数据从偏移 16 字节处开始。
文件尺寸分析:
ecb.bmp 总大小:4,147,344 字节
减去 16 字节 OpenSSL 头部:4,147,328 字节密文
这 4,147,328 字节就是 ECB 模式加密的原始图像数据(含 AES 末尾填充)。
第二步:发现 ECB 模式漏洞——相同密文块的统计分析
将密文数据切分为 16 字节的块,统计每个块出现的频率:
from collections import Counter
with open('ecb.bmp', 'rb') as f:
data = f.read()
ciphertext = data[16:] # 跳过 OpenSSL 头部
blocks = [ciphertext[i:i+16] for i in range(0, (len(ciphertext)//16)*16, 16)]
counter = Counter(blocks)
print(f"总块数: {len(blocks)}")
print(f"唯一块数: {len(counter)}")
for block, cnt in counter.most_common(5):
print(f" {block.hex()} : {cnt} 次 ({cnt/len(blocks)*100:.1f}%)")
输出结果:
总块数: 259,208
唯一块数: 211
最频繁的块:
323f8b1eb8209c99fcb546010bc94134 : 225,972 次 (87.2%)
b01707a0210e6a262b86dbb5b8649b91 : 14,333 次 (5.5%)
262870b320050ffdca0075999c1ae724 : 673 次 (0.3%)
fc551cd4ef31ba30373fd7a46499de2f : 657 次 (0.3%)
65539b0bd11d6b9079c11b8cd667a421 : 582 次 (0.2%)
结论极为清晰:
- 259,208 个密文块中,有 225,972 个(87.2%)完全相同
- 仅有 211 种不同的块(远小于理论上的 2^128 种可能)
- 前两种块合计占 92.7%
这完全印证了 ECB 模式的漏洞:图像中 87.2% 的 16 字节区域是相同的像素组合(白色背景),加密后也产生了完全相同的密文块 32 3f 8b 1e ...。加密并没有隐藏图像的结构信息。
第三步:确定图像参数——逆向推算分辨率和色深
要重建图像,需要知道原始图像的三个参数:宽度、高度、色深(每像素位数)。
题目给出了线索:4K 分辨率,黑白 BMP。
尝试推算:
BMP 格式要求每行像素数据按 4 字节边界对齐。设色深为 bpp(bits per pixel),宽度为 W,高度为 H,则:
每行字节数 = ceil(W * bpp / 8),向上取整到 4 的倍数
像素数据总大小 = 每行字节数 * H
密文大小为 4,147,328 字节(含最多 16 字节的 AES 填充),因此纯像素数据约为 4,147,200 字节。
验证 3840×2160、4bpp(16色调色板):
每行字节数 = ceil(3840 * 4 / 8) = 1920 字节(已是 4 的倍数)
像素数据总大小 = 1920 * 2160 = 4,147,200 字节
精确匹配。差值 4,147,328 – 4,147,200 = 128 字节,正好是 AES 填充(最多 16 字节)加上可能的额外块。
因此确定:
| 参数 | 值 | | — | — | | 宽度 | 3840 像素(4K) | | 高度 | 2160 像素(4K) | | 色深 | 4 bpp(16 色调色板) | | 每行字节数 | 1920 字节(已 4 字节对齐) | | 每行 16 字节块数 | 120 块 | | 每块覆盖像素 | 32 像素宽 |
第四步:构造有效的 BMP 文件头
BMP 文件由三部分组成:
1. BITMAPFILEHEADER(14 字节)
| 字节 | 字段 | 值 |
| — | — | — |
| 0-1 | 魔数 | BM |
| 2-5 | 文件总大小 | 4,147,318 |
| 6-9 | 保留字段 | 0 |
| 10-13 | 像素数据偏移 | 118(= 14 + 40 + 64) |
2. BITMAPINFOHEADER(40 字节)
| 字节 | 字段 | 值 | | — | — | — | | 0-3 | 头部大小 | 40 | | 4-7 | 宽度 | 3840 | | 8-11 | 高度 | 2160(正数 = 从下到上存储) | | 12-13 | 颜色平面数 | 1 | | 14-15 | 每像素位数 | 4 | | 16-19 | 压缩方式 | 0(BI_RGB,不压缩) | | 20-23 | 像素数据大小 | 4,147,200 | | 24-31 | 分辨率(像素/米) | 0 | | 32-35 | 调色板颜色数 | 16 | | 36-39 | 重要颜色数 | 0 |
3. 调色板(64 字节 = 16 色 x 4 字节/色)
使用 Windows 标准 16 色调色板,每个颜色以 BGRA 格式存储(蓝、绿、红、保留):
palette_rgb = [
(0,0,0), (128,0,0), (0,128,0), (128,128,0),
(0,0,128), (128,0,128), (0,128,128), (192,192,192),
(128,128,128), (255,0,0), (0,255,0), (255,255,0),
(0,0,255), (255,0,255), (0,255,255), (255,255,255)
]
为什么是 4bpp 而不是 1bpp(纯黑白)?
题目描述说”黑白格式”,但从密文块的统计来看,只有 211 种唯一块,超过了 1bpp 只有黑白两色的情况。实际上 4bpp 的 16 色 BMP 在题目所述的”黑白”场景下包含了渐变灰度,且 3840x2160x4/8 = 4,147,200 与密文大小精确匹配,1bpp 不满足。
第五步:完整解题脚本
import struct, os
from collections import Counter
with open('ecb.bmp', 'rb') as f:
data = f.read()
# 步骤1:去掉 OpenSSL Salted__ 头部(前 16 字节)
# 格式:"Salted__" (8字节) + Salt (8字节)
ciphertext = data[16:]
print(f"Magic: {data[:8]}")
print(f"Salt: {data[8:16].hex()}")
print(f"密文长度: {len(ciphertext)} 字节 = {len(ciphertext)//16} 个16字节块")
# 步骤2:ECB 统计分析
blocks = [ciphertext[i:i+16] for i in range(0, (len(ciphertext)//16)*16, 16)]
counter = Counter(blocks)
print(f"唯一块数: {len(counter)} / {len(blocks)}")
# 步骤3:图像参数
width, height, bpp = 3840, 2160, 4
row_bytes = (width * bpp + 7) // 8 # = 1920
img_data_size = row_bytes * height # = 4,147,200
pixel_offset = 14 + 40 + 64 # = 118(文件头 + DIB头 + 调色板)
file_size = pixel_offset + img_data_size # = 4,147,318
# 步骤4:构造 BMP 文件头
bmp_file_header = struct.pack('<2sIHHI',
b'BM', file_size, 0, 0, pixel_offset)
dib_header = struct.pack('<IiiHHIIiiII',
40, # 头部大小
width, # 宽度
height, # 高度(正数 = 从下往上存储)
1, # 颜色平面数
bpp, # 每像素位数
0, # 压缩方式(BI_RGB)
img_data_size, # 像素数据大小
0, 0, # 水平/垂直分辨率
16, # 调色板颜色数
0 # 重要颜色数
)
# 标准 Windows 16 色调色板(BGRA 格式)
palette_rgb = [
(0,0,0), (128,0,0), (0,128,0), (128,128,0),
(0,0,128), (128,0,128), (0,128,128), (192,192,192),
(128,128,128), (255,0,0), (0,255,0), (255,255,0),
(0,0,255), (255,0,255), (0,255,255), (255,255,255)
]
color_table = b''.join(
struct.pack('BBBB', b, g, r, 0) for r, g, b in palette_rgb
)
# 步骤5:组合头部 + 密文像素数据(取精确的图像大小)
header = bmp_file_header + dib_header + color_table
pixel_data = ciphertext[:img_data_size]
with open('result.bmp', 'wb') as f:
f.write(header + pixel_data)
print(f"成功写入 result.bmp,大小: {len(header) + len(pixel_data)} 字节")
第六步:理解为什么这个方法有效
关键原理:ECB 模式下密文保留了明文的结构。
原始图像的像素数据存储方式(BMP,16色,4bpp):
明文数据(像素字节流):
[像素0-31][像素32-63]...[像素3808-3839] <- 第 2160 行(BMP底部,文件最前)
[像素0-31][像素32-63]...[像素3808-3839] <- 第 2159 行
...
[像素0-31][像素32-63]...[像素3808-3839] <- 第 1 行(BMP顶部,文件最后)
每 16 字节 = 32 个像素(4bpp,每字节 2 像素)。
ECB 加密后:
密文数据(对应的密文块):
[加密(像素0-31)][加密(像素32-63)]...[加密(像素3808-3839)]
...
由于白色背景区域的 32 个像素完全相同(全部是白色的 4 位颜色值),ECB 加密后这些块的密文也完全相同(32 3f 8b 1e ...)。
当我们把有效的 BMP 文件头注入到密文前面,图像查看器就会把每个密文块直接当作像素值来渲染。虽然颜色会因为密钥不同而显示为随机颜色,但图像的结构(哪里是背景、哪里是内容)完整保留。这就是为什么图像”可读”——即使没有解密,结构性信息(flag 文字、企鹅图案的轮廓)依然清晰可见。
第七步:验证结果
运行脚本后,用图像查看器打开 result.bmp:
$ file result.bmp
result.bmp: PC bitmap, Windows 3.x format, 3840 x 2160 x 4,
image size 4147200, cbSize 4147318, bits offset 118
文件被正确识别为有效的 BMP 文件。打开图像后,可以清晰看到图像中显示的 flag 文字:
flag{no_penguin_here}
技术总结
ECB 模式对图像加密的危害
| 对比项目 | ECB 模式 | 安全模式(CBC/CTR/GCM) | | — | — | — | | 相同明文块 | 产生相同密文块 | 每次产生不同密文 | | 图像结构 | 完全暴露 | 完全隐藏 | | 统计特征 | 保留原始分布 | 密文均匀分布 | | 是否安全 | 不安全 | 安全 |
攻击方法的本质
本次攻击属于”已知密文攻击”的一种特殊形式——利用 ECB 的确定性特点(相同明文 -> 相同密文),通过统计分析和结构重建,在不知道密钥的情况下恢复了加密图像的可视内容。
解题的三个关键发现
- 文件识别:
Salted__魔数表明文件被 OpenSSL 加密,前 16 字节是 OpenSSL 头部,需要剥离才能得到纯密文数据。 - ECB 统计分析:259,208 个密文块中,仅有 211 种不同的值,且最常见的块占 87.2%。这直接暴露了 ECB 模式的使用,也说明原始图像颜色非常单一(大量白色背景)。
- 参数推算:通过密文长度(4,147,328 字节 ≈ 4,147,200 字节像素数据)与 4K 分辨率提示,精确推算出图像为 3840x2160x4bpp,并据此构造了合法的 BMP 文件头。
Flag
flag{no_penguin_here}
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:破镜安全 破镜安全 破镜安全《tinyCTF 2014 – ECB, it’s easy as 123 | 详细技术解析》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论