osthing's blog
GitHub RSS
5 min read

web手从0学习密码的故事-2 加密模式

关于常见对称分组加密的加密方式以及对应的常见攻击手法

ctf crypto math

加密模式

ECB 模式

ECB(Electronic Codebook,电子密码本)模式是最基础、最简单的块密码加密模式。

加密流程:

  • 将明文分成固定长度的块 P1,P2,,PqP_1, P_2, \dots, P_q
  • 每个明文块使用相同的密钥 KK 独立加密
  • 加密公式:Ci=EK(Pi)C_i = E_K(P_i)
  • 各分组之间完全独立、互不影响
  • 最终密文为:C=C1C2CqC = C_1 \| C_2 \| \dots \| C_q

image-20260624093519409

解密流程:

  • 将密文分成等长的块 C1,C2,,CqC_1, C_2, \dots, C_q
  • 每个密文块使用相同的密钥 KK 独立解密
  • 解密公式:Pi=DK(Ci)P_i = D_K(C_i)
  • 各分组之间完全独立,可并行处理

image-20260624093527003

ECB 模式特点:

  • 简单直接:每个明文块独立加密,像查字典一样
  • 确定性:相同密钥下,相同明文块总是产生相同密文块(最大缺陷)
  • 可并行化:各块独立,可并行加密/解密
  • 不适合加密长消息或含重复模式的数据(如图片)

ECB 块重排攻击

在块加密中ECB模式中每个块都是独立加密的。因此攻击者可以在未知密钥的情况下,对密文中的块进行重新排列,组合成合法的可解密的新密文。 例如,现在有一个cookie:

admin=0;username=test123

假设块字节大小是8字节,那么明文就能拆分成三块

admin=0;
username
=test123

那么现在我们需要把admin变成1使其越权 那么我们就可以通过构造用户名来得到admin=1的块,然后替换掉第一块即可,比如

admin=0;username=test123admin=1;

那么拆分成块得到

admin=0;
username
=test123
admin=1;

我们就得到了admin=1的密文,替换掉第一个密文即可越权成为admin

ECB 字节频率统计 / 已知明文攻击

ECB 模式最大的缺陷是确定性:相同的明文块在同一密钥下必定产生相同的密文块。攻击者可以利用这一特性进行统计或已知明文分析。

示例 —— 加密 Tux 图像:

image-20260624094014339

假设原始企鹅图像包含大片相同颜色的背景。加密后:

原始位图(明文块):    加密后(密文块):
[黑][黑][黑][黑][轮廓]  →  [C1][C1][C1][C1][C5]
[黑][黑][黑][轮廓][轮廓] →  [C1][C1][C5][C5][C5]

观察密文就能还原出企鹅轮廓,完全不需要密钥。

已知明文攻击示例 —— 网站 cookie 模板:

假设服务器对固定格式 user=XXX;role=user 进行 ECB 加密(块大小 8 字节):

明文:    user=Alc;role=user
分块:    user=Alc | ;role=us | er????
密文:    abcdef01 | 12345678 | 9abcdef0

攻击者已知前两块的明文内容后,可以构造任意相同前缀的明文,再观察对应密文即可推断第三块内容。例如注册 user=Alc;role=admin 时第三块密文如果变为 fedcba98,即可推断该块对应 admin?? 的密文。

CBC 模式

CBC(Cipher Block Chaining,密码分组链接)模式通过引入”链接”机制解决了ECB的安全性问题。

加密流程:

  • 使用初始化向量 IVIV 启动加密过程
  • 每个明文块先与前一个密文块异或,再进行加密
  • 第一块:C1=EK(P1IV)C_1 = E_K(P_1 \oplus IV)
  • 后续块:Ci=EK(PiCi1)C_i = E_K(P_i \oplus C_{i-1})
  • 加密过程串行(必须等待前一块结果)

image-20260624093536297

解密流程:

  • 每个密文块先解密,再与前一个密文块异或
  • 第一块:P1=DK(C1)IVP_1 = D_K(C_1) \oplus IV
  • 后续块:Pi=DK(Ci)Ci1P_i = D_K(C_i) \oplus C_{i-1}
  • 解密可并行(所有密文块已知)

image-20260624093541672

CBC 模式特点:

  • 安全性高:相同明文在不同位置生成不同密文
  • IV 必须不可预测,但不需保密
  • 加密串行,解密可并行
  • 错误传播有限:单块密文损坏只影响相邻两块

CBC字节翻转

CBC字节翻转攻击利用了解密过程中异或的特性。由于: Pi=DK(Ci)Ci1P_i = D_K(C_i) \oplus C_{i-1}

攻击者可以通过修改 Ci1C_{i-1} 的特定字节来控制 PiP_i 的对应字节。 image-20260624093554103

padding oracle

Padding Oracle 攻击能够在仅知道 padding 是否合法的情况下,完全解密任意密文。

攻击原理:

CBC 解密公式为: Pi=DK(Ci)Ci1P_i = D_K(C_i) \oplus C_{i-1}

假设我们想解密密文块 CiC_i,我们控制 Ci1C_{i-1}(即”伪造前一块”)。Padding Oracle 会告诉我们:解密后 PiP_i 的填充是否合法(PKCS#7 padding)。

Ii=DK(Ci)I_i = D_K(C_i)(即解密中间值,与密钥相关但固定)。

我们有: Pi=IiCi1P_i = I_i \oplus C_{i-1}

单字节恢复过程(恢复 PiP_i 的最后一个字节):

合法的 PKCS#7 padding 在最后字节为 \x01,或最后两字节为 \x02\x02,等等。

我们想让 PiP_i 的最后一个字节变成 \x01Ii[last]Ci1[last]=0x01I_i[\text{last}] \oplus C'_{i-1}[\text{last}] = 0x01 Ci1[last]=Ii[last]0x01\Rightarrow C'_{i-1}[\text{last}] = I_i[\text{last}] \oplus 0x01

通过暴力枚举 Ci1[last]C'_{i-1}[\text{last}](256 种可能),当 Padding Oracle 返回”合法”时,我们就获得了 Ii[last]I_i[\text{last}]

知道了 Ii[last]I_i[\text{last}] 后,将 Ci1C_{i-1} 的最后一个字节恢复为原值即可得到原明文最后一个字节。

恢复倒数第二个字节:

PiP_i 的最后两个字节为 \x02\x02

  • 倒数第二字节:Ci1[last-1]=Ii[last-1]0x02C'_{i-1}[\text{last-1}] = I_i[\text{last-1}] \oplus 0x02
  • 最后一字节:Ci1[last]=Ii[last]0x02C'_{i-1}[\text{last}] = I_i[\text{last}] \oplus 0x02(已知)

依此类推,可在 256×n256 \times n 次尝试内恢复整个块。

完整攻击示例(伪代码):

def padding_oracle_attack(oracle, ciphertext_block, orig_prev):
    intermediate = [0] * 16
    plaintext     = [0] * 16

    for byte_pos in range(15, -1, -1):
        padding = 16 - byte_pos

        # 构造伪造前一块,让已恢复的字节产生正确 padding
        forged_prev = [0] * 16
        for i in range(byte_pos + 1, 16):
            forged_prev[i] = intermediate[i] ^ padding

        # 暴力枚举当前字节
        for guess in range(256):
            forged_prev[byte_pos] = guess
            if oracle(bytes(forged_prev) + ciphertext_block):
                intermediate[byte_pos] = guess ^ padding
                plaintext[byte_pos]     = intermediate[byte_pos] ^ orig_prev[byte_pos]
                break
    return bytes(plaintext)

CFB 模式

CFB(Cipher Feedback,密文反馈)模式将分组密码转换为自同步的流密码。

加密流程:

  • 使用初始化向量 IVIV 填充反馈寄存器 FB1FB_1(长度 rr 位)
  • 从寄存器取最左 nn 位送入加密算法 EKE_K
  • 从加密输出取最左 jj 位与明文 PiP_i 异或,得到密文 CiC_i
  • CiC_i 反馈到寄存器右端(左移 jj 位后填充)
  • 生成新的反馈寄存器 FBi+1FB_{i+1}

image-20260624093604005

解密流程:

  • 同样使用加密算法 EKE_K(而非解密算法)
  • 用寄存器加密结果的前 jj 位与密文 CiC_i 异或得到 PiP_i
  • CiC_i 反馈更新寄存器

image-20260624093612227

CFB 模式特点:

  • 流密码特性:不需要填充,可处理任意长度数据
  • 自同步:丢失若干密文位后会自动恢复
  • 错误传播:错误位会停留在寄存器中影响后续块
  • 加密和解密都使用加密算法 EKE_K

CFB IV 重用攻击

CFB 模式将分组密码转换为流密码,密文为 Ci=PiEK(shift register)C_i = P_i \oplus E_K(\text{shift register})。如果 IV 重复使用,且密钥相同,攻击者就能恢复明文。

攻击原理:

CFB 的密钥流生成器为: O1=EK(IV)O_1 = E_K(IV) Oi+1=EK(shift(Oi,Ci))O_{i+1} = E_K(\text{shift}(O_i, C_i))

如果两次加密使用了相同的密钥 KK 和相同的 IVIV,那么前 jj 位(参数)的密钥流完全相同。即:

  • 第一次加密:C1=P1SC_1 = P_1 \oplus SSS 为密钥流)
  • 第二次加密:C1=P1SC'_1 = P'_1 \oplus S

虽然后续块的密钥流会因为 CiC_i 的不同而分叉,但只要反馈位 jj 小于分组大小 nn,就能构造 IV 碰撞场景。

示例 —— CFB-8 攻击(j=8,n=64j=8, n=64):

假设 CFB-8 用 64 位分组、8 位反馈:

加密消息1:Hello World!  → 密文 C1[0..10]
加密消息2:Goodbye!!!   → 密文 C2[0..10]
如果 IV1 == IV2,则:O1[0..7] 相等
因此:C1[0] ⊕ P1[0] == C2[0] ⊕ P2[0]
⇒ P1 ⊕ P2 == C1 ⊕ C2

只要知道其中一个明文(比如常见的 HTTP 响应头 HTTP/1.1),就能恢复另一个明文。

针对 CFB-1 的攻击(极端情况):

如果 j=1j=1,那么每次只加密 1 位,反馈 1 位。如果 IV 复用,每条消息的第一位都使用相同的密钥流位,多条消息的密文首位异或即可恢复明文首位异或。

OFB 模式

OFB(Output Feedback,输出反馈)模式将分组密码转换为同步流密码。

加密流程:

  • 使用初始化向量 IVIV 启动
  • EKE_KIVIV 加密,取输出前 jj 位作为密钥流 Y1Y_1
  • C1=P1Y1C_1 = P_1 \oplus Y_1
  • 关键:将 EKE_K 的完整 nn 位输出反馈到下一轮输入
  • 反馈链与明文/密文无关

image-20260624093623140

解密流程:

  • 与加密流程结构完全相同
  • 仅将底部的 PiP_iCiC_i 互换
  • Pi=CiYiP_i = C_i \oplus Y_i
  • 中间仍使用加密算法 EKE_K

image-20260624093627697

OFB 模式特点:

  • 同步流密码:密钥流可预先计算
  • 无错误传播:密文位错误只影响对应明文位
  • IV 必须唯一(nonce),否则严重的安全问题
  • 加解密结构相同

OFB nonce 重用 / 密钥流重用攻击

OFB 模式本质上是同步流密码,加密公式为 Ci=PiYiC_i = P_i \oplus Y_i,其中 YiY_i 是仅由密钥 KKIVIV 决定的密钥流。如果 IV(nonce)被重用,密钥流完全相同。

攻击原理:

如果两个不同消息使用相同的 (K,IV)(K, IV) 加密:

  • 消息 A:CiA=PiAYiC^A_i = P^A_i \oplus Y_i
  • 消息 B:CiB=PiBYiC^B_i = P^B_i \oplus Y_i

则: CiACiB=PiAPiBC^A_i \oplus C^B_i = P^A_i \oplus P^B_i

攻击者只要知道其中一个明文,就能立刻恢复另一个明文。即使两者都不知道,密文异或也暴露了两段明文的异或关系,统计攻击可以恢复出明文内容。

经典案例 —— WEP 协议破解:

WEP(无线加密协议)使用 RC4 流密码,但 IV 只有 24 位。当流量较大时:

  • 24 位 IV 空间有限(约 1677 万)
  • 同一个 IV 重复使用会导致密钥流重用
  • 这正是 WEP 容易被破解的根本原因(虽然还有弱密钥等其他问题)

现代 WPA3 改用 SAE 协议从根本上解决了这一问题。

示例 —— 两段密文异或恢复明文:

假设 IV 重用,加密两段 HTTP 响应:

密文A: 1a2b3c4d5e6f7081...
密文B: 0f5e4a2c7d8b9012...

已知密文A对应: "HTTP/1.1 200"
密文B对应未知内容

CACB=PAPBC^A \oplus C^B = P^A \oplus P^B,前 12 字节为:

"HTTP/1.1 200" ⊕ 密文A ⊕ 密文B = 密文B对应的明文前12字节

CTR 模式

CTR(Counter,计数器)模式是现代加密中最常用的模式之一。

加密流程:

  • 使用一系列计数器值 T1,T2,,TqT_1, T_2, \dots, T_q(通常由 nonce + 计数序列构成)
  • 每个计数器值独立加密:Si=EK(Ti)S_i = E_K(T_i)
  • 密文:Ci=PiEK(Ti)C_i = P_i \oplus E_K(T_i)
  • 计数器值必须唯一

image-20260624093641900

解密流程:

  • 与加密流程完全对称
  • Pi=CiEK(Ti)P_i = C_i \oplus E_K(T_i)
  • 同样使用加密算法 EKE_K
  • 处理不满整块时:取 EK(Tq)E_K(T_q) 的前 dd 位与密文异或

image-20260624093712064

CTR 模式特点:

  • 完全并行:各分组独立计算,效率最高
  • 随机访问:可直接解密任意块
  • 无填充:可处理任意长度数据
  • 计数器唯一性是安全的关键
  • 加解密结构完全相同
  • 当前推荐使用的加密模式(如 AES-CTR)

CTR nonce 重用攻击

CTR 模式的加密公式为 Ci=PiEK(Ti)C_i = P_i \oplus E_K(T_i),其中 Ti=noncecounterT_i = \text{nonce} \| \text{counter}。如果 nonce 重用(即不同消息使用相同的计数器初始值),攻击方式与 OFB nonce 重用完全一致。

攻击原理:

如果两次加密使用了相同的 (K,nonce)(K, \text{nonce})

  • 消息 A:CiA=PiAEK(Ti)C^A_i = P^A_i \oplus E_K(T_i)
  • 消息 B:CiB=PiBEK(Ti)C^B_i = P^B_i \oplus E_K(T_i)

则每块对应的密钥流 Si=EK(Ti)S_i = E_K(T_i) 完全相同: CiACiB=PiAPiBC^A_i \oplus C^B_i = P^A_i \oplus P^B_i

示例 —— 已知明文恢复:

设 AES-256-CTR 加密两个 16 字节块的消息:

nonce = 0x00000000000000000000000000000001
T1    = 0x00000000000000000000000000000001
T2    = 0x00000000000000000000000000000002

密钥流: S1 = AES_K(T1), S2 = AES_K(T2)

消息A明文: "GET /admin  HTTP"    (16字节)
消息B明文: "GET /public HTTP"    (16字节)

密文A[0]: 0x9a4b3c2d1e5f7081...
密文B[0]: 0x7c8d9e0f1a2b3c4d...

由密文异或得到:

C^A[0] ⊕ C^B[0] = "GET /admin" ⊕ "GET /public" = 0xe6c6a222043f4c2c...

若已知密文A对应明文”GET /admin “,则立即恢复密文B的明文:

P^B[0] = C^B[0] ⊕ C^A[0] ⊕ P^A[0]

Bit-flipping 攻击(与 CBC 类似):

CTR 模式没有 IV,也没有链接,但密文 = 明文 ⊕ 密钥流。攻击者直接修改密文 CiC_i 的某个字节,新明文对应字节会变化: Pi=CiSi=(CiΔ)Si=PiΔP'_i = C'_i \oplus S_i = (C_i \oplus \Delta) \oplus S_i = P_i \oplus \Delta

这种”密文可控则明文可控”的特性使得 CTR 模式也需要 MAC 保护。

多消息密钥流恢复:

如果攻击者收集了大量使用相同 nonce 加密的消息,可以构造多组 (CiACiB)(C^A_i \oplus C^B_i) 等式。如果某个消息明文完全已知(例如硬编码的固定响应),就能恢复所有其他消息的明文。

CTR 计数器碰撞 / 比特翻转

计数器碰撞攻击:

CTR 模式的核心安全性在于计数器值的唯一性。如果两个不同的明文块使用了相同的计数器值 TiT_i,那么它们会被相同的密钥流加密,攻击方式与 nonce 重用一致。

碰撞场景示例:

假设 nonce 是 32 位,计数器是 32 位:

nonce  = 0x00000001, counter = 0x00000001  → T = 0x0000000100000001
nonce  = 0x00000000, counter = 0x00000001  → T = 0x0000000000000001
不同 nonce + counter 不应该碰撞,但如果实现错误(如拼接顺序错误、位数截断),就可能发生碰撞。

整数溢出碰撞:

32 位计数器空间只有约 42 亿。如果加密了超过 2322^{32} 个块(约 64 GB 数据),计数器必然回绕,密钥流就会重复。

WEP 中 24 位 IV 空间更小,几小时内就会碰撞。

比特翻转(Bit-flipping)攻击:

CTR 模式同样可以被比特翻转攻击:

Ci=CiΔPi=PiΔC'_i = C_i \oplus \Delta \Rightarrow P'_i = P_i \oplus \Delta

攻击者无需知道密钥,只需修改密文对应位即可控制明文对应位。

示例 —— 构造 admin 用户:

假设加密格式:role=guest;username=test

明文块[0]: role=guest  →  密文块[0]: 0xa1b2c3...
密钥流 S0:           =  明文 ⊕ 密文
S0[0..3] = "role" ⊕ 0xa1b2c3... = 密钥流前缀

如果想改成 role=admin:
新密文块[0] = 新明文 ⊕ S0 = "role=admin" ⊕ S0

但 CTR 模式下,攻击者只要知道原密文和想改成的明文,就可算出需要的密文修改值:

密文' = 密文 ⊕ 原明文 ⊕ 新明文 = 密文 ⊕ "guest" ⊕ "admin"

无需关心密钥流!

为什么 CTR 模式仍然安全:

  • 计数器唯一 → 密钥流唯一 → 流密码语义安全
  • 加密算法是 PRF(伪随机函数)
  • 攻击者不能获得密钥流明文对

CTR vs CBC 比对:

  • CBC:修改 Ci1C_{i-1} 影响 PiP_i,修改 CiC_i 影响 PiP_iPi+1P_{i+1}
  • CTR:修改 CiC_i 精确影响 PiP_i,错误传播最小

两者都需要 MAC 保护才能防止篡改。