CTF密码学综合教学指南--第四章

📚 第一部分:核心理论梳理

4.1 对称加密基础

核心思想:加密与解密使用同一把密钥( Kenc=KdecKenc​=Kdec​ )。

特性 分组密码 流密码
处理方式 将明文切分为固定长度的块(如128位),逐块加密 生成密钥流,与明文逐字节/逐位异或
典型算法 AES, DES, 3DES RC4, ChaCha20, Salsa20
填充 需要(若明文长度不是块大小倍数) 不需要
速度 较慢(涉及复杂的置换和代换) 通常更快(硬件实现效率极高)

4.2 AES 算法详解

AES (Advanced Encryption Standard)

  • 标准 :分组长度固定为 128位(16字节)。
  • 密钥长度:128/192/256位。
  • 轮数:AES-128 (10轮), AES-192 (12轮), AES-256 (14轮)。

四大操作步骤(每轮)

  1. SubBytes (字节替换):非线性变换,通过S-Box替换字节。
  2. ShiftRows (行移位):状态矩阵的行进行循环左移。
  3. MixColumns (列混淆):列的线性变换(最后一轮省略)。
  4. AddRoundKey (轮密钥加):与轮密钥进行异或。

常见工作模式对比

模式 全称 特点 安全性
ECB 电子密码本 相同明文块 →→ 相同密文块 极差 (暴露数据模式)
CBC 密码分组链接 引入IV,块间异或,串行加密 良好 (需防填充攻击)
CTR 计数器模式 变分组为流密码,支持并行 良好 (无需填充)
GCM 伽罗瓦/计数器 CTR + 认证 (GMAC) 最佳 (推荐用于生产)

🛠️ 第二部分:实战攻击代码与工具库

我将你提供的攻击脚本整理成了三个独立的模块:AES攻击模块流密码攻击模块综合实战模块

模块一: AES 常见攻击 (ECB & CBC)

这个模块包含了 ECB Oracle 逐字节攻击CBC 字节翻转攻击 的完整实现。

适用场景:服务端将用户输入拼接上未知字符串(如flag),然后用AES-ECB加密返回密文。

攻击步骤:

  1. 确定块大小: 逐渐增加输入长度,观察密文长度何时增加一个块

  2. 确认 ECB 模式: 发送两个相同的块,检查密文中是否有重复块

  3. 逐字节恢复: 构造输入使目标字节位于块边界,枚举256种可能并对比

    import os
    from Crypto.Cipher import AES
    from Crypto.Util.Padding import pad, unpad

    class AESAttacker:
    def init(self, key=None):
    self.key = key if key else os.urandom(16)
    self.BLOCK_SIZE = 16

    复制代码
     # ==========================
     # 1. ECB Oracle 攻击
     # ==========================
     def ecb_oracle_server(self, user_input: bytes, secret: bytes) -> bytes:
         """模拟服务端:加密 (用户输入 + 未知秘密)"""
         plaintext = user_input + secret
         # PKCS7 填充
         padded_pt = pad(plaintext, self.BLOCK_SIZE)
         cipher = AES.new(self.key, AES.MODE_ECB)
         return cipher.encrypt(padded_pt)
    
     def attack_ecb_oracle(self, oracle_func, secret_length: int):
         """
         执行 ECB Oracle 逐字节攻击
         oracle_func: 接收 bytes,返回密文 bytes 的函数
         secret_length: 未知秘密的长度
         """
         known = b""
         print(f"[*] 开始 ECB Oracle 攻击...")
         
         for i in range(secret_length):
             # 1. 构造填充,使目标字节位于当前块的最后一个位置
             # 比如:我们要猜第1个未知字节,需要填充 15 个 'A'
             pad_length = self.BLOCK_SIZE - 1 - (len(known) % self.BLOCK_SIZE)
             padding = b"A" * pad_length
             
             # 2. 获取目标密文块(包含我们要猜的那个字节)
             # 注意:这里我们发送 padding,让服务器加密 padding + secret
             # 此时密文的第 (len(known)//16) 个块包含了 secret 的第 i 个字节
             target_cipher = oracle_func(padding)
             block_index = len(known) // self.BLOCK_SIZE
             target_block = target_cipher[block_index * self.BLOCK_SIZE : (block_index + 1) * self.BLOCK_SIZE]
             
             # 3. 暴力枚举最后一个字节
             found = False
             for byte in range(256):
                 # 构造测试输入:padding + 已知部分 + 猜测字节
                 test_input = padding + known + bytes([byte])
                 test_cipher = oracle_func(test_input)
                 test_block = test_cipher[block_index * self.BLOCK_SIZE : (block_index + 1) * self.BLOCK_SIZE]
                 
                 if test_block == target_block:
                     known += bytes([byte])
                     print(f"[+] 发现第 {i+1} 字节: {chr(byte) if 32 <= byte < 127 else '?'} (ASCII: {byte})")
                     found = True
                     break
             
             if not found:
                 print("[-] 攻击失败,无法匹配字节")
                 break
                 
         return known
    
     # ==========================
     # 2. CBC 字节翻转攻击
     # ==========================
     def cbc_bitflip_attack(self, iv: bytes, ct: bytes, target_changes: list):
         """
         执行 CBC 字节翻转攻击
         target_changes: 列表,包含字典 {block_idx: int, offset: int, original: int, target: int}
                         block_idx: 要修改的密文块索引(修改 C_i 会影响 P_{i+1})
                         offset: 块内偏移
                         original: 原始明文字节
                         target: 目标明文字节
         """
         ct_array = bytearray(ct)
         
         print(f"[*] 开始 CBC 字节翻转攻击...")
         for change in target_changes:
             block_idx = change['block_idx']
             offset = change['offset']
             original_byte = change['original']
             target_byte = change['target']
             
             # 计算位置:修改 C_i 的对应字节
             pos = block_idx * self.BLOCK_SIZE + offset
             
             # 翻转公式:C'_i[j] = C_i[j] ^ P_{i+1}[j] ^ P'_{i+1}[j]
             # 在代码中:new_byte = old_byte ^ original ^ target
             ct_array[pos] ^= original_byte ^ target_byte
             print(f"    -> 修改块 {block_idx} 偏移 {offset}: {chr(original_byte)} -> {chr(target_byte)}")
             
         return bytes(ct_array)

    --- 使用示例 ---

    if name == "main":
    attacker = AESAttacker()

    复制代码
     # 场景 1: ECB Oracle
     SECRET_FLAG = b"flag{ecb_is_not_secure}"
     # 模拟服务端函数
     def server_encrypt(user_input):
         return attacker.ecb_oracle_server(user_input, SECRET_FLAG)
     
     recovered = attacker.attack_ecb_oracle(server_encrypt, len(SECRET_FLAG))
     print(f"🚩 恢复的 Flag: {recovered.decode()}")
     print("-" * 30)
    
     # 场景 2: CBC Bitflip
     # 假设我们要把 "role=user" 变成 "role=admin" (需长度一致,这里演示 user->admi)
     # 实际上通常需要填充,这里简化演示原理
     KEY = os.urandom(16)
     # 构造明文:Block0 | Block1("role=user;xxxx")
     # 假设 Block1 解密后是 "role=user;1234"
     # 我们修改 Block0 来改变 Block1 的明文
     
     # 这里仅作逻辑演示,实际运行需要真实的解密环境验证
     # 假设 Block0 的密文是 ct[0:16], Block1 是 ct[16:32]
     # 修改 ct[0] 会影响 Block1 解密后的第1个字节

CBC字节翻转攻击(Bit-Flipping Attack)

原理: 在CBC解密过程中,明文块P_i = D_K(C_i) ⊕ C_{i-1}。如果攻击者修改C_{i-1}的某个字节,会直接影响P_i的对应字节,同时P_{i-1}会变为乱码。

攻击公式:

设原始C_{i-1}[j]为c,原始P_i[j]为p,目标P_i[j]为t:

修改后的 C_{i-1}[j] = c ⊕ p ⊕ t

这样解密时:D_K(C_i)[j] ⊕ (c ⊕ p ⊕ t) = p ⊕ c ⊕ c ⊕ p ⊕ t = t

攻击场景示例: 假设服务端加密用户数据 "role=user",攻击者想修改为 "role=admin"。

Padding Oracle攻击

原理: 利用服务器对PKCS#7填充有效性的不同响应(如返回不同的错误码),逐字节恢复明文。

攻击条件:

  • 使用CBC模式 + PKCS#7填充
  • 服务器对"填充正确"和"填充错误"有可区分的响应

攻击步骤概要:

  1. 修改密文块的最后一个字节,枚举0-255
  2. 当服务器不报告填充错误时,意味着解密后的最后一个字节是 0x01
  3. 由此推导出中间值(intermediate value),进而恢复明文字节
  4. 对每个字节重复此过程,从后向前逐字节恢复整个块

模块二:流密码与 XOR 攻击工具

这个模块封装了 XOR 运算Crib Dragging (已知明文拖拽) 攻击。

XOR 运算的核心性质:

  • A ⊕ A = 0(自反性)
  • A ⊕ 0 = A(恒等性)
  • A ⊕ B = B ⊕ A(交换律)
  • (A ⊕ B) ⊕ C = A ⊕ (B ⊕ C)(结合律)

一次性密码本( OTP, One-Time Pad ):

密钥与明文等长、完全随机、仅使用一次时,OTP是理论上不可破解的。但实际中密钥重用会导致严重安全问题。

密钥重用攻击( Two-Time Pad / Crib Dragging ):

若两段明文P1、P2使用同一密钥K加密:

C1 = P1 ⊕ K, C2 = P2 ⊕ K

C1 ⊕ C2 = P1 ⊕ K ⊕ P2 ⊕ K = P1 ⊕ P2

密钥被消除!攻击者得到两段明文的异或结果,然后通过"Crib Dragging"(已知词拖拽)逐步恢复明文。

复制代码
def xor_bytes(a: bytes, b: bytes) -> bytes:
    """对两个字节串进行异或"""
    return bytes(x ^ y for x, y in zip(a, b))

class StreamCipherAttacker:
    @staticmethod
    def crib_drag(c1_xor_c2: bytes, crib: bytes, min_printable_ratio=0.8):
        """
        Crib Dragging 攻击
        c1_xor_c2: 两个密文的异或结果 (P1 ^ P2)
        crib: 猜测的明文片段 (如 b"the ", b"flag")
        """
        results = []
        print(f"[*] 正在使用 Crib '{crib.decode()}' 进行拖拽攻击...")
        
        for i in range(len(c1_xor_c2) - len(crib) + 1):
            segment = c1_xor_c2[i : i + len(crib)]
            # P2_segment = (P1 ^ P2) ^ P1_guess
            possible_p2 = xor_bytes(segment, crib)
            
            # 检查可读性:统计可打印字符比例
            printable_count = sum(1 for b in possible_p2 if 32 <= b < 127)
            ratio = printable_count / len(possible_p2)
            
            if ratio >= min_printable_ratio:
                results.append({
                    "pos": i,
                    "p1_guess": crib.decode(),
                    "p2_recovered": possible_p2.decode(errors='ignore'),
                    "raw": possible_p2
                })
                
        return results

# --- 使用示例 ---
if __name__ == "__main__":
    # 模拟 Two-Time Pad
    KEY = b"supersecretkey" # 密钥重用
    P1 = b"attack at dawn"
    P2 = b"meeting at noon"
    
    # 补齐长度
    P1 = P1.ljust(len(KEY), b'\x00')
    P2 = P2.ljust(len(KEY), b'\x00')
    
    C1 = xor_bytes(P1, KEY)
    C2 = xor_bytes(P2, KEY)
    
    # 攻击者拿到 C1 ^ C2
    C1_xor_C2 = xor_bytes(C1, C2)
    
    attacker = StreamCipherAttacker()
    # 假设攻击者猜测 P2 中包含 " at "
    hits = attacker.crib_drag(C1_xor_C2, b" at ")
    
    for hit in hits:
        print(f"位置 {hit['pos']}: 如果 P2 是 '{hit['p1_guess']}', 则 P1 可能是 '{hit['p2_recovered']}'")

模块三:综合实战案例 (Web Cookie 篡改)

这是结合了前面知识的完整解题脚本。

|-----------------------------------------------------------------------------------------------------------------------------------|
| 📋 题目场景 一个Web应用使用AES-CBC加密cookie。cookie格式为:username=guest;role=user;。你能获取到自己的加密cookie(密文 + IV),需要通过修改密文,使解密后的role变为admin。 |

复制代码
# 题目场景:修改 Cookie 中的 role=user 为 role=admin
# 已知:
# 1. 加密模式:AES-CBC
# 2. 明文结构:username=guest;role=user;
# 3. 目标:让解密后的 role 变为 admin

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import os

class CookieExploit:
    def __init__(self):
        self.KEY = os.urandom(16)
        self.BLOCK_SIZE = 16

    def get_encrypted_cookie(self, username):
        """服务端:生成加密 Cookie"""
        data = f"username={username};role=user;".encode()
        iv = os.urandom(16)
        cipher = AES.new(self.KEY, AES.MODE_CBC, iv)
        ct = cipher.encrypt(pad(data, self.BLOCK_SIZE))
        return iv, ct

    def check_admin(self, iv, ct):
        """服务端:解密并检查权限"""
        cipher = AES.new(self.KEY, AES.MODE_CBC, iv)
        try:
            pt = unpad(cipher.decrypt(ct), self.BLOCK_SIZE)
            if b"role=admin" in pt:
                return True, pt
            return False, pt
        except:
            return False, b"Decryption Error"

    def exploit(self):
        # 1. 获取原始 Cookie
        # 注意:为了让 "role=user" 单独成块或对齐,我们需要控制 username 的长度
        # 明文: "username=" (9) + "guest" (5) + ";role=user;" (11) = 25 字节
        # 块1: "username=guest;r" (16)
        # 块2: "ole=user;" + padding (16)
        # 目标:修改 块2 中的 "user" -> "admin"
        # 注意:user (4字节) vs admin (5字节),长度不一致,不能直接翻转!
        # 技巧:如果题目允许,我们可以尝试修改为 "admi" 或者寻找其他注入点
        # 这里为了演示,假设我们想把 "user" 改成 "adm1" (假设服务端接受非标准)
        # 或者,更常见的 CTF 技巧是:利用填充或特定字符串构造
        
        # 让我们换一个思路:
        # 构造 username 使得 "role=user;" 刚好在第二个块
        # username = "A" * 7
        # 明文: "username=AAAAAAA;role=user;"
        # 块1: "username=AAAAAAA" (16)
        # 块2: ";role=user;..." (16) -> 这里 user 在偏移 6-9
        
        iv, ct = self.get_encrypted_cookie("AAAAAAA")
        print(f"[*] 原始密文块2: {ct[16:32].hex()}")
        
        # 目标:将块2的 "user" (偏移6-9) 修改为 "adm1" (为了保持长度)
        # 修改 块1 的对应字节 (偏移6-9)
        
        ct_list = bytearray(ct)
        target_block_idx = 1 # 目标明文在块2
        modify_block_idx = 0 # 修改块1
        
        original_text = b"user"
        target_text = b"adm1" # 演示用,实际题目可能需要 "admin" 且长度对齐
        
        for i in range(len(original_text)):
            offset = 6 + i
            # 翻转公式
            ct_list[modify_block_idx * 16 + offset] ^= original_text[i] ^ target_text[i]
            
        new_ct = bytes(ct_list)
        
        # 2. 提交攻击
        success, decrypted = self.check_admin(iv, new_ct)
        print(f"[*] 解密结果: {decrypted}")
        if success:
            print("[+] 攻击成功!获得 Admin 权限")
        else:
            print("[-] 攻击失败,可能是长度不对或校验未通过")

if __name__ == "__main__":
    exploit = CookieExploit()
    exploit.exploit()

💡 总结与提示

  1. ECB 模式 :绝对不要在现实世界中使用,CTF 中见到 ECB 优先考虑 Byte-at-a-time 攻击。
  2. CBC 模式
    • Bit-flipping:利用 Pi=DK(Ci)⊕Ci−1Pi=DK(Ci)⊕Ci−1 ,修改 Ci−1Ci−1 可以控制 PiPi 。
    • Padding Oracle:利用服务器对填充错误的反馈,逆向推导明文。
  3. 流密码 :核心是 密钥流重用。只要发现 C1⊕C2=P1⊕P2C1⊕C2=P1⊕P2 ,就可以利用常见单词(Crib)进行拖拽攻击。
相关推荐
DevilSeagull2 小时前
电脑上安装的服务会自动消失? 推荐项目: localhostSCmanager. 更好管理你的服务!
测试工具·安全·react·vite·localhost·hono·trpc
草履虫君2 小时前
VMware 虚拟机网络性能优化指南:从 11 秒到 4 秒的完整调优实践
服务器·网络·经验分享·性能优化
@insist1233 小时前
信息安全-防火墙技术演进全景:从代理NAT 到下一代及专项防火墙
网络·安全·web安全·软考·信息安全工程师·软件水平考试
优化Henry3 小时前
TDD-LTE站点Rilink=3链路故障处理案例---BBU侧C口“有发光、无收光”的排查与恢复
运维·网络·信息与通信·tdd
浪客灿心3 小时前
Linux网络传输层协议
linux·运维·网络
05候补工程师3 小时前
【ROS 2 具身智能】Gazebo 仿真避坑指南:从“幽灵机器人”到传感器数据流打通
人工智能·经验分享·笔记·ubuntu·机器人
chushiyunen3 小时前
pandas使用笔记、数据清洗、json_normalize
笔记·pandas
HERR_QQ3 小时前
端到端课程自用 4 规划 基于自规划AR的端到端规划 AI 笔记
人工智能·笔记·自动驾驶·transformer
二哈赛车手4 小时前
新人笔记---实现简易版的rag的bm25检索(利用ES),以及RAG上传时的ES与向量数据库双写
java·数据库·笔记·spring·elasticsearch·ai