你加密的图片其实没穿衣服 ECB模式为什么等于没加密
原创
紫禁
紫禁
紫禁玄科
2026年10月12日 10:49
四川
在小说阅读器读本章
去阅读
在公众号小说中沉浸阅读
SECURITY · INTELLIGENCE
你加密的图片其实没穿衣服 ECB模式为什么等于没加密
2026年10月11日 · 8 min
网络安全#安全编程#密码学
01
审计台上的那张加密照片
我之前在做一次代码审计项目时,遇到一个特别自信的客户。他指着架构图跟我说:用户上传的照片我们全部加密存储,算法是 AES,密钥独立保管,放心。我说好,把加密后的文件导一份给我看看。
拿到手我就愣住了——那是一堆密文没错,但我一眼就能看出照片里有座山、有一片天,山的轮廓清清楚楚。我没有密钥,也没有做任何破解。是密文自己把轮廓画了出来。
客户用的确实是 AES。AES 算法本身没毛病,毛病出在它的工作模式上:ECB。今天这篇把这事讲透:为什么\"用了 AES\"不等于\"加密了\",以及你花两分钟就能上手的两个自查脚本。
加密为何会漏出轮廓
02
Adobe那1.53亿个密码是怎么裸奔的
先看真事,这个案子十来年了,但教科书级。
2013 年 10 月 3 日,Adobe 发出安全公告:攻击者访问了客户的 ID 和加密后的密码。注意措辞——是 encrypted,不是 hashed。一个月后细节被媒体和研究人员挖出来:2013 年 11 月 4 日 CSO Online 报道,Adobe 确认那批密码是\"加密\"而非\"哈希\"存储;中文媒体随后跟进,那套所谓保护用的是 3DES。最终受影响记录约 1.53 亿条,而且每条还附带了用户自己填的\"密码提示词\"。
真正致命的一环是:那套加密用的是 ECB 模式。ECB 的特点是\"相同的输入永远得到相同的输出\",于是攻击者根本不用拿到密钥,就能把密文按出现频率分组——某个密文块出现了几百万次?那它对应的多半是 123456。再瞄一眼提示词\"我老婆生日\",大批密码就这么还原了。
所以这事的教训不是\"Adobe 没加密\",而是加密模式选错,等于给 1.53 亿个密码只穿了层纱。纱是透明的,但穿纱的人以为自己穿了铠甲。
03
企鹅图为什么骗不了人
机制说穿了就一句话:ECB 把明文按 16 字节一块独立加密,相同的明文块必然产出相同的密文块。
ECB vs CBC · 分组逻辑对比(伪代码)
ECB: C1 = E(P1), C2 = E(P2), C3 = E(P3) # 各块独立
CBC: C1 = E(P1^IV), C2 = E(P2^C1), C3 = E(P3^C2) # 链式咬合
图像是最惨的受害者。照片里大片是同色——天空、墙面、皮肤,这些区域堆满了重复的 16 字节明文块,ECB 一加密,密文里就堆满了重复的密文块,轮廓原样浮出来。安全圈那张著名的\"ECB 企鹅图\"就是这么来的:拿 Linux 的企鹅 Tux 加密,加密完还是一只企鹅。GitHub 上有可复现的复刻项目(robertdavidgraham/ecb-penguin),你自己就能跑一遍。
同色块原形毕露
这不是什么野路子发现。NIST 在 SP 800-38A 里把分组模式定义得明明白白,业界共识就一句:ECB 只适合加密单个随机块,成块的数据千万别用它。
04
代码里最常见的三个踩坑
我审计过的项目里,十有八九栽在这三处。你也可以拿这三条去对照自家代码:
踩坑一:默认加密器。Java 里写 Cipher.getInstance(\"AES\"),看着人畜无害,JDK 默认给你配的就是 ECB。必须写全 AES/GCM/NoPadding 才算数。
踩坑二:复制粘贴的第一段示例代码。Python 的 AES.new(key, AES.MODE\_ECB)、PHP 老项目 mcrypt 的默认模式,网上一搜一大把的\"快速上手示例\"就是这么写的。示例追求短,生产环境跟着短就出事。
踩坑三:密钥和 IV 写死。key 直接贴在代码里、iv 固定一串 0——ECB 压根没有 IV,不少人\"升级\"到 CBC 之后 IV 还是写死的,等于换了件透光的纱。
审查加密模式
这三处的共同点是:代码跑起来完全正常,功能测试全绿。不专门去看模式,谁也发现不了。所以才需要下面的自查。
05
自查一:不拿密钥也能看出ECB
判断一段密文是不是 ECB,不需要密钥、不需要算力——统计 16 字节块的重复率就够了。ECB 的指纹就写在密文表面。下面这个脚本纯标准库、零依赖,直接能跑:
ECB-DETECT · 密文模式指纹检测
\"\"\"ecb\_detect.py —— 判断一段密文是不是 ECB 模式加密的(纯标准库,零依赖)
原理:ECB 把明文按 16 字节分块独立加密,相同的明文块必然得到相同的密文块。
统计密文里 16 字节块的重复率,就能反推加密模式。
用法:python3 ecb\_detect.py # 内置演示(ECB vs CBC 对比)
\"\"\"
import sys
import hashlib
from collections import Counter
BLOCK = 16
def detect(ct):
\"\"\"统计密文的分块重复率,返回 (块数, 重复块数, 重复率)\"\"\"
blocks = [ct[i:i + BLOCK] for i in range(0, len(ct) - BLOCK + 1, BLOCK)]
if len(blocks) < 8:
return 0, 0, 0.0
cnt = Counter(blocks)
dup = sum(v - 1 for v in cnt.values() if v > 1)
return len(blocks), dup, dup / len(blocks)
def judge(rate):
if rate > 0.10:
return \"疑似 ECB 模式(重复密文块比例过高,相同明文在泄漏)\"
if rate > 0.01:
return \"可疑,建议人工复核\"
return \"不像 ECB(密文块基本互不重复,符合 CBC/CTR/GCM 特征)\"
以下仅为演示用的\"玩具块加密\":SHA256(key||块) 取前16字节。
真实场景把 toy 换成 AES 即可,ECB/CBC 的分组逻辑与这里完全一致。
def toy\_encrypt\_block(key, block):
return hashlib.sha256(key + block).digest()[:BLOCK]
def demo():
key = b\"demo-key-2026\"
# 模拟\"大片同色\"的图像数据:32 个块里有大量重复(照片里的天空、墙面)
sky = b\"\x87\xce\xeb\" \* 5 + b\"\x00\"
wall = b\"\xf5\xf5\xf5\" \* 5 + b\"\x00\"
plain = sky \ 10 + wall \ 12 + bytes(range(10)) \* 16
# ECB:每块独立加密
ecb = b\"\".join(toy\_encrypt\_block(key, plain[i:i + BLOCK])
for i in range(0, len(plain), BLOCK))
# CBC:每块先与前一段密文异或再加密(链式)
cbc, prev = b\"\", b\"\x00\" \* BLOCK
for i in range(0, len(plain), BLOCK):
xored = bytes(a ^ b for a, b in zip(plain[i:i + BLOCK], prev))
prev = toy\_encrypt\_block(key, xored)
cbc += prev
print(\"[演示] 模拟图像数据 %d 字节(含大片重复色块)\" % len(plain))
for name, ct in ((\"ECB\", ecb), (\"CBC\", cbc)):
n, dup, rate = detect(ct)
print(\" %s 密文: 分块 %d 个, 重复块 %d, 重复率 %.1f%% -> %s\"
% (name, n, dup, rate \* 100, judge(rate)))
if \\_name\\ == \"\\main\\_\":
if len(sys.argv) > 1:
data = open(sys.argv[1], \"rb\").read()
n, dup, rate = detect(data)
print(\"文件 %s: 分块 %d, 重复块 %d, 重复率 %.2f%%\" % (sys.argv[1], n, dup, rate \* 100))
print(\"判定:\", judge(rate))
else:
demo()
拿它跑一遍内置演示(模拟\"大片同色\"的图像数据):
实跑结果 · 2026-10-11
[演示] 模拟图像数据 512 字节(含大片重复色块)
ECB 密文: 分块 32 个, 重复块 25, 重复率 78.1% -> 疑似 ECB 模式(重复密文块比例过高,相同明文在泄漏)
CBC 密文: 分块 32 个, 重复块 0, 重复率 0.0% -> 不像 ECB(密文块基本互不重复,符合 CBC/CTR/GCM 特征)
同一份数据,ECB 密文重复率 78.1%,CBC 只有 0.0%。真实场景里,只要重复率明显大于零(经验阈值 10% 就该亮红灯),基本可以判定\"相同明文在泄漏\"。把脚本第 44 行的玩具块加密换成 AES,就是一次正经的密文体检。
06
自查二:两分钟扫掉一个项目的坑
第二个脚本管源码。把我前面说的三个踩坑全部写成正则规则,顺带把弱随机数、MD5 存密码这些一起扫了,带时间戳输出报告,可以直接进 CI:
CRYPTO-AUDIT · 危险加密写法扫描
\"\"\"crypto\_audit.py —— 扫描源码里的危险加密写法(安全审计用,纯标准库)
用法:python3 crypto\_audit.py [目录或文件,默认当前目录]
\"\"\"
import os, re, sys, datetime
RULES = [
(\"高危\", \"ECB 模式\", re.compile(r\"MODE\_ECB|AES/ECB|/ECB/|ECB\.new\"),
\"换 AES-GCM(带认证),IV/nonce 必须每次随机\"),
(\"高危\", \"弱默认加密器\", re.compile(r'getInstance\(\s\\"AES\"\s\\)'),
\"Java 的 getInstance(\\"AES\\") 默认就是 ECB,写全 AES/GCM/NoPadding\"),
(\"高危\", \"硬编码密钥\", re.compile(r'(?i)(secret|api\_key|aes\_key)\s\=\s\[\"\'][A-Za-z0-9+/=]{8,}[\"\']'),
\"密钥进 KMS/环境变量/密钥管理服务,禁止写死在代码里\"),
(\"中危\", \"固定 IV/nonce\", re.compile(r'(?i)\b(iv|nonce)\s\=\s\[\"\']'),
\"IV/nonce 每次加密用密码学随机数生成,绝不复用\"),
(\"中危\", \"弱随机数\", re.compile(r\"(?i)(random\.random|random\.randint|Math\.random)\"),
\"令牌/验证码用 secrets 模块或 CSPRNG\"),
(\"中危\", \"弱哈希存密码\", re.compile(r\"(?i)(md5|sha1)\s\\(.\(password|passwd|pwd)\"),
\"密码存储用 argon2id / bcrypt / scrypt\"),
(\"低危\", \"加密存密码\", re.compile(r\"(?i)encrypt.\*password\"),
\"密码只该哈希加盐,不该可逆加密(Adobe 2013 就是这么泄露的)\"),
]
def audit(path):
hits, files = [], []
if os.path.isfile(path):
files = [path]
else:
for root, \_, names in os.walk(path):
files += [os.path.join(root, n) for n in names
if n.endswith((\".py\", \".java\", \".js\", \".php\", \".go\", \".ts\"))]
for f in files:
for ln, line in enumerate(open(f, encoding=\"utf-8\", errors=\"ignore\"), 1):
if line.lstrip().startswith(\"#\"):
continue
for level, name, rx, fix in RULES:
if rx.search(line):
hits.append((level, name, f, ln, line.strip()[:60], fix))
return hits
if \\_name\\ == \"\\main\\_\":
target = sys.argv[1] if len(sys.argv) > 1 else \".\"
ts = datetime.datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\")
hits = audit(target)
print(\"加密安全审计报告 生成时间:\", ts)
print(\"扫描目标:\", os.path.abspath(target), \" 命中:\", len(hits), \"处\")
print(\"-\" \* 72)
for level, name, f, ln, line, fix in sorted(hits):
print(\"[%s] %s %s:%d\" % (level, name, os.path.relpath(f), ln))
print(\" 代码:\", line)
print(\" 修复:\", fix)
if not hits:
print(\"未发现危险加密写法。\")
拿一个满是坑的演示项目(含 ECB、硬编码密钥、固定 IV、MD5 存密码等)试跑:
实跑结果 · 2026-10-11
加密安全审计报告 生成时间: 2026-10-11 22:14:57
扫描目标: /root/wx\_articles/vuln\_20261011/demo\_project 命中: 7 处
[中危] 固定 IV/nonce demo\_project/app\_crypto.py:17
代码: iv = \"0000000000000000\"
修复: IV/nonce 每次加密用密码学随机数生成,绝不复用
[中危] 弱哈希存密码 demo\_project/app\_crypto.py:25
代码: return hashlib.md5(password.encode()).hexdigest()
修复: 密码存储用 argon2id / bcrypt / scrypt
[中危] 弱随机数 demo\_project/app\_crypto.py:21
代码: return random.randint(100000, 999999)
修复: 令牌/验证码用 secrets 模块或 CSPRNG
[低危] 加密存密码 demo\_project/app\_crypto.py:28
代码: def encrypt\_password(password, key):
修复: 密码只该哈希加盐,不该可逆加密(Adobe 2013 就是这么泄露的)
[低危] 加密存密码 demo\_project/app\_crypto.py:30
代码: return cipher.encrypt(password.encode())
修复: 密码只该哈希加盐,不该可逆加密(Adobe 2013 就是这么泄露的)
[高危] ECB 模式 demo\_project/app\_crypto.py:10
代码: cipher = AES.new(key, AES.MODE\_ECB)
修复: 换 AES-GCM(带认证),IV/nonce 必须每次随机
[高危] ECB 模式 demo\_project/app\_crypto.py:29
代码: cipher = AES.new(key, AES.MODE\_ECB)
修复: 换 AES-GCM(带认证),IV/nonce 必须每次随机
一次命中 7 处:2 处 ECB、1 处硬编码密钥、1 处固定 IV、1 处弱随机数验证码、1 处 MD5 存密码、1 处可逆加密存密码。每条都带修复建议,改起来不用翻文档。
我的习惯是把这脚本挂在提交钩子上,命中\"高危\"直接拦下——加密写法这种东西,靠人眼 review 是盯不住的。
07
正确姿势其实就三条
改法没有想象中复杂,记住三条就够:
1. 数据加密用 AES-GCM。它自带完整性校验(AEAD),密文被篡改能立刻发现。nonce 每次加密都用密码学随机数新生成,绝不复用;密钥进 KMS 或环境变量,别落代码。
2. 密码不是\"加密\",是\"哈希\"。argon2id / bcrypt / scrypt 加盐单向存储。可逆加密存密码等于给自己留了张万能钥匙——上周那篇《你的用户密码正在数据库里裸奔》写过改造实录,原理一脉相承。
3. 不会选模式就别自己选。用成熟库的高层封装(libsodium 的 secretbox、Google Tink 这类),默认参数就是安全的。密码学的坑,前人已经替你踩完了。
排个自查顺序的话,照这个流程走:
加密自查流程
08
说给不写代码的你
你可能一辈子不写代码,但一定会遇到同样的问题。网盘说\"加密备份\",相册说\"端到端加密\",某个 App 说\"数据全程加密\"——下次再听到这种宣传,你可以在心里问三个问题:
一,加密用的什么模式?(答不上来或者只说\"军用级 AES\"的,多半自己也没数)
二,密钥在谁手里?(在服务商手里,那服务商被拖库时你就是 Adobe 那 1.53 亿分之一)
三,密码是哈希还是加密?(能\"找回密码\"的,基本都是可逆加密)
说到底,\"加密\"这个词在宣传里是形容词,在工程里是名词。模式选错,AES 也只是昂贵的马赛克。
没穿衣服的加密比不加密更危险——因为它让人放心地把秘密交了出去。
今天这两个脚本,建议你转给团队里写代码的那位。如果他看完说\"我们肯定没用 ECB\",让他跑一遍再说。
觉得有用?点个「在看」让更多人看到
紫禁玄科
专注网络安全 · 深度分析
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论