你加密的图片其实没穿衣服ECB模式为什么等于没加密"

admin 2026-10-14 15:55:11 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

你加密的图片其实没穿衣服 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\",让他跑一遍再说。

觉得有用?点个「在看」让更多人看到

紫禁玄科

专注网络安全 · 深度分析


装了个看片软件,电脑却成了别人的遥控设备" 网络安全文章

装了个看片软件,电脑却成了别人的遥控设备"

国家互联网应急中心通报荐片播放器内置后门,八天内监测到70万台设备受控。该恶意软件具备自启动、服务注册、自恢复三层防清理机制,可静默安装程序、劫持剪贴板、篡改默认程序并远程执行代码。新版本更换域名但恶意行为不变。建议用户从正规渠道下载软件,排查自启动项、服务及外连通道,受控主机建议重装系统。
鱼缸智能插排整夜断电,一缸鱼全死了" 网络安全文章

鱼缸智能插排整夜断电,一缸鱼全死了"

沃尔达智能插排因服务器遭黑客攻击致用户鱼缸设备整夜断电,多地鱼友损失惨重。厂商称系网络攻击,但客服口径与用户感知存在差异。文章建议用户进行断云演练验证设备断网行为,识别关键负载并准备物理兜底,避免单点依赖。
评论:0   参与:  0