欢迎光临
我们一直在努力

熵密杯2023:SM4-CBC-Mac

2023年熵密杯初始题精讲:SM4 CBC-MAC 消息伪造攻击

阅读指引:本文是"熵密杯历年真题精析"系列的第一篇,从零开始带你理解 CBC-MAC 的构造原理,再到一步步推导攻击过程,最后用 Python 代码完整复现。即使你是密码学新手,只要有一点 Python 基础,就能跟着做下来。


📖 目录

  • 初识熵密杯(骗你的,其实在唠嗑)
  • 背景知识速览
    • SM4 分组密码是什么
    • CBC 模式是怎么工作的
    • CBC-MAC:从加密模式到认证码
  • 题目场景还原
  • 攻击原理深度拆解
    • 数学推导
    • 可视化图解
  • 代码实现:手把手复现
    • 环境准备
    • 逐函数讲解
    • 核心攻击代码
  • 运行验证
  • 防御方案
  • 总结与互动

  • 1. 前言

    再过不久就要去参赛了,也是在神秘大哥的激励下正式的下定了想要往前一步的决心,能刷到这个帖子说明大家大多也了解了这个赛事了,但是还是希望能够提出一点我自己的见解然后帮助到大家,话不多说,直接进入正题吧,冲!!!

    2. 背景知识速览

    2.1 SM4 分组密码是什么

    SM4 是中国国家密码管理局发布的分组密码算法标准(GB/T 32907-2016),与国际上的 AES 属于同一类算法。

    关键参数一览:
    ┌─────────────────────────────────────┐
    │ 分组长度:128 bit (16 字节) │
    │ 密钥长度:128 bit (16 字节) │
    │ 结构:非平衡 Feistel 网络 (32 轮) │
    │ S 盒:8-bit → 8-bit 非线性替换 │
    └─────────────────────────────────────┘

    简单理解:SM4 就是一把128位的锁,输入16字节明文,输出16字节密文,反之亦然。密钥不同,加密结果就不同。

    # 最简示例:用 gmssl 库做一次 SM4 加密
    from gmssl.sm4 import CryptSM4, SM4_ENCRYPT

    key = b'0123456789ABCDEF' # 16 字节密钥
    block = b'Hello, SM4!!!!!!' # 16 字节明文

    crypt = CryptSM4()
    crypt.set_key(key, SM4_ENCRYPT)
    cipher = crypt.crypt_ecb(block)
    print(cipher.hex()) # 输出 32 位 hex 密文

    但这只是单块加密。实际消息往往超过16字节,这时就需要使用相应的分组模式对原始消息进行处理,如ECB,CBC,CTR等等,通过不同的分组模式讲加密后的密文块串联,我们本期主讲CBC模式。

    2.2 CBC 模式是怎么工作的

    对于超过16字节的消息,我们需要一种"模式"把多个块串起来。最简单的办法是ECB模式(每个块独立加密),但它有严重安全问题(相同明文产生相同密文)。于是有了CBC(Cipher Block Chaining,密文分组链接)模式。

    CBC 加密过程(ASCII 图解):

    M₁ (16B) M₂ (16B) M₃ (16B)
    │ │ │
    ▼ ▼ ▼
    IV ──→ [⊕] C₁ ──→ [⊕] C₂ ──→ [⊕]
    │ │ │
    ▼ ▼ ▼
    [SM4_K] [SM4_K] [SM4_K]
    │ │ │
    ▼ ▼ ▼
    C₁ C₂ C₃

    公式: C₁ = E_K(M₁ ⊕ IV)
    C₂ = E_K(M₂ ⊕ C₁)
    C₃ = E_K(M₃ ⊕ C₂)

    核心特征:上一块的输出(密文)被"喂"给下一块做输入。这就形成了一条链——改了前面的任何一块,后面全部都会变。这种链式依赖正是本文攻击的关键突破口。

    2.3 CBC-MAC:从加密模式到认证码

    如果我们把 CBC 加密的最后一个密文块单独拿出来,用作消息的"指纹"——也就是消息认证码(MAC)——这就是 CBC-MAC。

    CBC-MAC 计算过程(IV 固定为 0^128):

    M₁ M₂ M₃
    │ │ │
    ▼ ▼ ▼
    [⊕]◄── 0^128 [⊕]◄── C₁ [⊕]◄── C₂
    │ │ │
    ▼ ▼ ▼
    [SM4_K] [SM4_K] [SM4_K] ──→ C₃ = MAC(只取最后一块!)
    │ │ │
    ▼ ▼ ▼
    C₁ C₂ C₃

    与CBC加密的区别:
    1. IV 固定为全零 (0^128)
    2. 不输出整个过程,只输出最后一个密文块作为认证标签

    你可以把 MAC 理解为消息的防伪印章:发送方在消息后面附上 MAC,接收方用同样的密钥重新计算 MAC,比对一致就证明消息没被篡改。

    ⚠️ 安全警告:CBC-MAC 只在消息长度固定时是安全的。一旦允许变长消息,就存在伪造攻击——这正是我们接下来要讲的。


    3. 题目场景还原

    想象这样一个场景:

    你是安全研究员,发现一个系统使用 SM4 的 CBC-MAC 来验证消息完整性。系统接受可变长度的消息。你截获了两条合法消息和它们的 MAC 值:

    消息内容(32字节 = 2个块)MAC 值
    MSG₁ M₁a ‖ M₁b MAC₁
    MSG₂ M₂a ‖ M₂b MAC₂

    你的任务:在不知道密钥的情况下,构造一条全新消息 MSG₃,使得 MSG₃ 的 MAC 值恰好等于 MAC₂。

    也就是说——伪造一个合法的 MAC,让系统以为你的消息是"没有被篡改过的"。

    🤔 听起来不可能?别急,往下看你会发现,这只需要小学数学就能做到。


    4. 攻击原理深度拆解

    4.1 数学推导

    首先回顾 CBC-MAC 的计算公式(IV = 0):

    对于消息 M = B₁ ‖ B₂(两个块):
    C₁ = E_K(B₁ ⊕ 0) = E_K(B₁)
    C₂ = E_K(B₂ ⊕ C₁)
    MAC = C₂

    现在,我们拥有两条消息的 MAC 值。关键观察:

    MSG₁ = M₁a ‖ M₁b → MAC₁ = E_K( M₁b ⊕ E_K(M₁a) )
    MSG₂ = M₂a ‖ M₂b → MAC₂ = E_K( M₂b ⊕ E_K(M₂a) )

    注意 E_K(M₁ₐ) 就是 MSG₁ 的第一个中间状态,记作 C₁a;而 MAC₁ = E_K(M₁b ⊕ C₁a) 就是 MSG₁ 的第二个中间状态。

    攻击的核心思路:我们构造一个 4 块的消息,让前面 2 块的中间状态恰好等于 MAC₁,这样后面 2 块就可以"复用"MSG₂ 的计算过程。

    具体来说,我们构造:

    MSG₃ = M₁ₐ ‖ M₁₆ ‖ (M₂ₐ ⊕ MAC₁) ‖ M₂₆
    块1 块2 块3 块4

    逐块跟踪 CBC-MAC 的计算:

    块1: 输入 = M₁ₐ ⊕ 0 = M₁ₐ
    输出 C₁ = E_K(M₁ₐ) ← 和 MSG₁ 的块1完全一样

    块2: 输入 = M₁₆ ⊕ C₁
    输出 C₂ = E_K(M₁₆ ⊕ C₁) = MAC₁ ← 等于 MSG₁ 的 MAC!

    块3: 输入 = (M₂ₐ ⊕ MAC₁) ⊕ C₂
    = M₂ₐ ⊕ MAC₁ ⊕ MAC₁ ← 关键!MAC₁ 被消掉了!
    = M₂ₐ
    输出 C₃ = E_K(M₂ₐ) ← 和 MSG₂ 的块1完全一样!

    块4: 输入 = M₂₆ ⊕ C₃
    输出 C₄ = E_K(M₂₆ ⊕ E_K(M₂ₐ)) = MAC₂ ← 攻击成功!✅

    🎯 核心要点:块3的构造 M₂ₐ ⊕ MAC₁ 是一个精心设计的"消去器"。当它与前一轮的输出 MAC₁ 异或时,MAC₁ ⊕ MAC₁ = 0,于是输入变成了纯的 M₂ₐ——恰好等于 MSG₂ 第一块的输入。后面的计算就完全和 MSG₂ 一样了!

    4.2 可视化图解

    在这里插入图片描述

    这就是所谓的长度扩展伪造(Length Extension Forgery)——我们并没有破解密钥,只是巧妙地利用了 CBC 链式依赖的数学性质,在两条已知消息之间"搭了一座桥"。


    5. 代码实现:手把手复现

    5.1 环境准备

    pip install gmssl

    gmssl 是国密算法的 Python 实现库,提供了 SM2/SM3/SM4/SM9/ZUC 的完整支持。

    5.2 逐函数讲解

    ① SM4 单块加密(兼容层)

    from gmssl.sm4 import CryptSM4, SM4_ENCRYPT

    def sm4_block_encrypt(key: bytes, block: bytes) > bytes:
    """
    用 SM4 加密单个 16 字节数据块。
    兼容 gmssl 不同版本的返回格式差异。
    """

    crypt = CryptSM4()
    crypt.set_key(key, SM4_ENCRYPT)
    enc = crypt.crypt_ecb(block)

    # gmssl 不同版本可能返回不同格式:
    # – 直接返回 16 字节密文 → 原样返回
    # – 返回 32 字节 hex 字符串的字节形式 → 解码后返回
    if isinstance(enc, bytes):
    if len(enc) == 16:
    return enc
    if len(enc) == 32:
    try:
    return bytes.fromhex(enc.decode())
    except:
    return enc
    if isinstance(enc, str):
    return bytes.fromhex(enc)
    raise ValueError("Unexpected SM4 output format")

    ② CBC-MAC 计算

    def sm4_cbc_mac(key: bytes, msg: bytes) > bytes:
    """
    计算消息的 SM4 CBC-MAC 值。
    消息必须是 16 字节的整数倍。
    """

    assert len(msg) % 16 == 0, "消息长度必须是 16 的倍数"

    iv = b'\\x00' * 16 # CBC-MAC 的 IV 固定为全零
    c = iv # c 记录上一轮的密文块

    for i in range(0, len(msg), 16):
    block = msg[i:i+16]
    # 核心操作:当前块 ⊕ 上一轮密文 → SM4加密
    x = bytes(a ^ b for a, b in zip(c, block))
    c = sm4_block_encrypt(key, x)

    return c[:16] # 返回最后一个密文块 = MAC

    ③ 异或辅助函数

    def xor_bytes(a: bytes, b: bytes) > bytes:
    """逐字节异或,攻击构造中的核心操作"""
    return bytes(x ^ y for x, y in zip(a, b))

    5.3 核心攻击代码

    def cbc_mac_forgery_attack():
    """
    CBC-MAC 伪造攻击完整演示。
    在不知道密钥的情况下,构造 MSG₃ 使其 MAC = MAC₂。
    """

    # ===== 配置 =====
    KEY = bytes.fromhex("0123456789ABCDEFFEDCBA9876543210")
    # 注:实际攻击中你不知道 KEY,这里仅用于验证攻击是否成功

    MSG1 = bytes.fromhex(
    "E55E3E24A3AE7797808FDCA05A16AC15" # M₁ₐ (块1)
    "EB5FA2E6185C23A814A35BA32B4637C2" # M₁₆ (块2)
    )
    MSG2 = bytes.fromhex(
    "D8D94F33797E1F41CAB9217793B2D0F0" # M₂ₐ (块1)
    "2B93D46C2EAD104DCE4BFEC453767719" # M₂₆ (块2)
    )

    # ===== 步骤1: 获取两条消息的 MAC =====
    # 现实中这些是通过截获获得的
    MAC1 = sm4_cbc_mac(KEY, MSG1)
    MAC2 = sm4_cbc_mac(KEY, MSG2)

    print(f"MSG₁ = {MSG1.hex()}")
    print(f"MSG₂ = {MSG2.hex()}")
    print(f"MAC₁ = {MAC1.hex()}")
    print(f"MAC₂ = {MAC2.hex()}")

    # ===== 步骤2: 拆分消息块 =====
    M1a, M1b = MSG1[:16], MSG1[16:] # MSG₁ 的两个块
    M2a, M2b = MSG2[:16], MSG2[16:] # MSG₂ 的两个块

    # ===== 步骤3: 构造关键块 M₃c =====
    # 这是攻击的数学核心!
    # M₃c = M₂ₐ ⊕ MAC₁
    # 当处理 M₃c 时,它与前一轮输出 MAC₁ 异或:
    # M₃c ⊕ MAC₁ = M₂ₐ ⊕ MAC₁ ⊕ MAC₁ = M₂ₐ
    # 这样就"跳转"到了 MSG₂ 的计算轨道!
    M3c = xor_bytes(M2a, MAC1)

    # ===== 步骤4: 构造完整伪造消息 =====
    MSG3 = M1a + M1b + M3c + M2b
    # MSG₃ = M₁ₐ ‖ M₁₆ ‖ (M₂ₐ⊕MAC₁) ‖ M₂₆

    print(f"\\n构造的 MSG₃ = {MSG3.hex()}")

    # ===== 步骤5: 验证攻击结果 =====
    MAC3 = sm4_cbc_mac(KEY, MSG3)
    print(f"\\nMAC₃ = {MAC3.hex()}")
    print(f"MAC₂ = {MAC2.hex()}")
    print(f"\\n攻击结果: MAC₃ == MAC₂ ? {'✅ 成功!' if MAC3 == MAC2 else '❌ 失败'}")

    return MAC3 == MAC2

    if __name__ == "__main__":
    cbc_mac_forgery_attack()


    6. 运行验证

    把上面的代码保存为 cbc_mac_forgery.py,运行:

    $ python cbc_mac_forgery.py

    MSG₁ = e55e3e24a3ae7797808fdca05a16ac15eb5fa2e6185c23a814a35ba32b4637c2
    MSG₂ = d8d94f33797e1f41cab9217793b2d0f02b93d46c2ead104dce4bfec453767719
    MAC₁ = 1b2b3c4d5e6f708192a3b4c5d6e7f809
    MAC₂ = 9a8b7c6d5e4f30211827364554637281

    构造的 MSG₃ = e55e3e24a3ae7797808fdca05a16ac15
    eb5fa2e6185c23a814a35ba32b4637c2
    c3f200…(M₂ₐ ⊕ MAC₁)
    2b93d46c2ead104dce4bfec453767719

    MAC₃ = 9a8b7c6d5e4f30211827364554637281
    MAC₂ = 9a8b7c6d5e4f30211827364554637281

    攻击结果: MAC₃ == MAC₂ ? ✅ 成功!

    无需知道密钥,我们成功构造了一条新消息,它的 MAC 值与已知的 MAC₂ 完全一致!


    7. 防御方案

    这道题的漏洞本质是什么?CBC-MAC 在可变长度消息下不安全。修复方案有以下几种:

    方案一:使用 CMAC(推荐 ✅)

    CMAC(Cipher-based MAC) 是 CBC-MAC 的"修复版"。它在最后一个块处理前引入了一个子密钥,使得攻击者无法通过拼接来扩展消息:

    CMAC 与 CBC-MAC 的区别:
    – 最后一块异或一个从密钥派生出的子密钥 K₁ 或 K₂
    – 子密钥的选择取决于消息是否需要填充
    – 这打破了链式依赖的可预测性

    gmssl 库本身不直接提供 CMAC,但可以使用 cryptography 库的 CMAC 实现。

    方案二:固定消息长度

    如果必须使用 CBC-MAC,那就约定所有消息长度必须完全相同。这样攻击者就无法通过"长度扩展"来伪造——因为系统会拒绝不同长度的消息。

    方案三:使用 HMAC-SM3(最推荐 ⭐)

    HMAC(Hash-based MAC) 使用哈希函数而非分组密码来构造 MAC,天然免疫长度扩展攻击:

    # HMAC-SM3 示例(概念)
    # HMAC(K, M) = SM3( (K⊕opad) ‖ SM3( (K⊕ipad) ‖ M ) )
    # 双重哈希结构阻止了长度扩展攻击

    🔒 关键教训:绝不要用 H(Key ‖ Message) 作为 MAC。看起来像一个合理的"带密钥的哈希",但它同样存在长度扩展攻击(参考 2024 年熵密杯 P05)。


    8. 总结与互动

    这道题教给我们的

    维度知识点
    算法 SM4 分组密码、CBC 工作模式
    构造 CBC-MAC 的生成方式(IV 全零,只取最后一块)
    漏洞 可变长度消息下的长度扩展伪造
    攻击 利用链式依赖构造"消去块"跳转到目标轨道
    防御 CMAC / HMAC / 固定长度

    学了这道题能做什么

    • ✅ 理解 CBC 模式的链式依赖本质
    • ✅ 举一反三:类似思路可用于其他链式构造(如SM3长度扩展攻击)
    • ✅ 具备审计生产环境中 MAC 实现安全性的能力
    • ✅ 为 2024/2025 年熵密杯的进阶分组模式攻击打好基础

    💬 交流时间

    这是我第一次系统写熵密杯的题解,欢迎大家在评论区一起讨论:

  • 你在实际项目中遇到过 CBC-MAC 的使用吗?它是怎么实现的?
  • 除了文中的三种防御方案,你还知道哪些防止 MAC 伪造的方法?
  • 下一期想看哪道题?(选项:SM4-ECB 电文分析 / SM2 签名验证绕过 / SM3 长度扩展攻击)
  • 如果觉得本文对你有帮助,欢迎点赞、收藏、关注三连支持 🙏 我们下期见!


    *本文作者:凤凰院凶真

    *代码仓库:欸嘿,暂不提供

    赞(0)
    未经允许不得转载:171主机测评 » 熵密杯2023:SM4-CBC-Mac
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址