CTF实战:AES-ECB模式原理剖析与Python自动化破解
1. 项目概述当CTF遇上AES-ECB在CTFCapture The Flag竞赛的密码学赛道上AES加密算法是绝对的“常驻嘉宾”。而其中ECBElectronic Codebook电子密码本模式因其特性鲜明既是出题人的“心头好”也是新手解题时最容易“踩坑”的地方。你可能遇到过这样的场景题目给了一段密文提示是AES-ECB加密密钥未知但Flag就藏在解密后的信息里。看着那一串毫无规律的十六进制或Base64字符感觉无从下手。这个项目我们就来彻底解决这个问题。我不会只给你一个冷冰冰的Python脚本然后说“运行它就能出Flag”。相反我会带你从CTF实战的角度出发手把手拆解AES-ECB模式的原理、它在CTF题目中的常见“出题套路”以及我们如何利用Python一步步从密文分析到最终破解。你会学到如何识别ECB模式的特征、如何在没有密钥的情况下进行“选择明文攻击”或利用其“无初始化向量”的特性以及如何编写健壮的、能处理各种边界情况的解密脚本。更重要的是我会分享我在无数次实战和教学中总结出的“避坑指南”——那些官方文档不会告诉你但能让你解题效率翻倍的细节。无论你是刚接触CTF密码学的新手还是想系统梳理AES-ECB破解思路的爱好者这篇指南都将提供一条清晰的路径。我们将使用Python的cryptography库它比古老的pycrypto更现代、更安全并结合pwntools等CTF常用工具链的思维构建我们的破解工具箱。2. AES-ECB模式的核心原理与CTF考点剖析要破解一个东西必须先彻底理解它。AESAdvanced Encryption Standard是一种分组密码算法它规定一次只能处理固定长度的数据块AES-128是16字节。对于超过一个块的数据就需要用到“模式”ECB就是其中最基础的一种。2.1 ECB模式的工作原理像拼图一样加密你可以把ECB模式想象成用同一个模具去压印一堆独立的橡皮泥块。加密过程很简单将明文按16字节AES-128分块最后一块若不足则进行填充Padding。对每一个明文块使用同一个密钥进行AES加密生成对应的密文块。将所有密文块按顺序拼接就是最终的密文。解密则是逆过程用同一个密钥独立解密每一个密文块然后移除填充得到明文。它的核心特点决定了它的优缺点优点简单易于并行计算因为每个块独立。致命缺点相同的明文块必然产生相同的密文块。这是ECB模式在CTF中能被攻击的理论基石。注意ECB模式不需要初始化向量IV。如果你在题目描述或代码中看到IV那基本可以排除是纯ECB模式可能是CBC或其他模式。这是一个重要的快速判断题。2.2 CTF中ECB模式的常见“出题套路”出题人正是利用了ECB的“确定性”和“块独立性”来设计题目。以下是几种典型场景Flag被直接加密这是最基础的形式。题目给你一个AES-ECB加密后的密文可能是flag字符串本身也可能是flag{xxx}格式密钥未知但长度可猜如128位。这种题往往需要暴力破解密钥或者结合其他漏洞如侧信道、错误提示来解题纯ECB特性利用较少。Oracle攻击选择明文攻击这是ECB模式在CTF中的“高光时刻”。题目会提供一个“加密Oracle”即一个你可以访问的黑盒函数你输入任意明文它返回用固定密钥的AES-ECB加密结果。你的目标是利用这个Oracle逐步推算出未知的、被拼接在你输入后面的Flag。经典场景encrypt(your_input flag)。你可以控制your_input通过精心构造输入让Flag的字符逐个“挤”到新的明文块中并通过观察输出密文块的重复模式来逐字节破解Flag。这就是著名的“ECB字节翻转攻击”或“字典攻击”的变种。基于模式的识别与破解题目给出一段较长的、有重复部分的明文加密后的结果比如一张BMP图片的ECB加密。由于ECB下相同块输出相同密文中也会出现重复的块序列。通过分析密文的重复模式甚至可以反推出明文的某些结构信息或者识别出这是ECB模式与CBC等模式区分。填充Oracle攻击的预备知识虽然填充Oracle攻击主要针对CBC模式但理解ECB的填充如PKCS#7是基础。在某些ECB题目中如果服务器会对解密后的填充有效性进行反馈有效或无效也可能衍生出攻击方式。理解这些套路就像拿到了地图。接下来我们开始搭建破解环境并准备应对这些场景的武器。3. 环境搭建与核心工具库详解工欲善其事必先利其器。我们选择cryptography库作为核心因为它积极维护API清晰且默认使用更安全的设置。3.1 安装与基础加密解密首先安装必要的库pip install cryptography让我们先实现最基础的AES-ECB加密和解密函数这是所有操作的基石。from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.primitives import padding from cryptography.hazmat.backends import default_backend import os def aes_ecb_encrypt(key: bytes, plaintext: bytes) - bytes: 使用AES-ECB模式加密明文。 注意ECB模式不需要IV。 # 1. 创建Cipher对象使用ECB模式 cipher Cipher(algorithms.AES(key), modes.ECB(), backenddefault_backend()) encryptor cipher.encryptor() # 2. 对明文进行PKCS7填充AES块大小为16字节 padder padding.PKCS7(algorithms.AES.block_size).padder() padded_plaintext padder.update(plaintext) padder.finalize() # 3. 加密 ciphertext encryptor.update(padded_plaintext) encryptor.finalize() return ciphertext def aes_ecb_decrypt(key: bytes, ciphertext: bytes) - bytes: 使用AES-ECB模式解密密文。 cipher Cipher(algorithms.AES(key), modes.ECB(), backenddefault_backend()) decryptor cipher.decryptor() # 解密 padded_plaintext decryptor.update(ciphertext) decryptor.finalize() # 移除PKCS7填充 unpadder padding.PKCS7(algorithms.AES.block_size).unpadder() plaintext unpadder.update(padded_plaintext) unpadder.finalize() return plaintext # 示例用法 if __name__ __main__: # 生成一个随机密钥AES-128 key os.urandom(16) plaintext bThis is a secret message containing flag{dummy_flag} ciphertext aes_ecb_encrypt(key, plaintext) print(fCiphertext (hex): {ciphertext.hex()}) decrypted aes_ecb_decrypt(key, ciphertext) print(fDecrypted: {decrypted.decode()})实操心得cryptography库要求密钥长度必须是16AES-128、24AES-192或32AES-256字节。在CTF中密钥长度通常是给定的或是常见的16字节。如果遇到密钥不是标准长度可能需要考虑是否是经过编码如Hex、Base64或是需要破解的。3.2 识别ECB模式密文重复性检测在CTF中你拿到一段密文第一步就是判断它是不是ECB模式加密的。我们可以编写一个检测函数def detect_ecb(ciphertext: bytes, block_size: int 16) - bool: 通过检测密文中是否有重复的块来判断是否可能使用ECB模式。 返回True表示很可能为ECB模式。 # 将密文按块大小分块 blocks [ciphertext[i:iblock_size] for i in range(0, len(ciphertext), block_size)] # 计算唯一块的数量 unique_blocks len(set(blocks)) # 如果唯一块的数量小于总块数说明有重复块ECB可能性高 return len(blocks) unique_blocks # 示例ECB加密的密文会有重复块如果明文有重复 key os.urandom(16) # 构造一个包含重复内容的明文16字节块重复 plaintext_ecb bAAAABBBBCCCCDDDD * 4 # 重复4次相同的块 ciphertext_ecb aes_ecb_encrypt(key, plaintext_ecb) print(fECB ciphertext has repeating blocks: {detect_ecb(ciphertext_ecb)}) # 对比一下如果是随机的明文即使ECB加密密文也可能不重复但CTF题常构造重复明文 plaintext_random os.urandom(64) ciphertext_random_ecb aes_ecb_encrypt(key, plaintext_random) print(fRandom ECB ciphertext has repeating blocks: {detect_ecb(ciphertext_random_ecb)})这个函数是CTF中的“瑞士军刀”之一。在Misc类题目中给你一个加密文件先用这个函数跑一下如果返回True解题思路就瞬间清晰了一大半。4. 实战破解针对加密Oracle的字节提取攻击这是CTF中AES-ECB模式最经典、最高频的攻击场景。我们模拟一个真实的题目环境。4.1 攻击场景构建假设有一个服务器或本地模拟的函数它提供以下加密功能encrypt(user_data flag)其中user_data是你可控的flag是服务器端拼接的未知字符串密钥固定且未知。我们的目标是在不知道密钥的情况下仅通过观察加密结果逐字节还原出Flag。我们先模拟这个Oracleclass ECB_Oracle: 模拟一个ECB加密Oracle def __init__(self): self._key os.urandom(16) # 未知密钥 self._flag bflag{real_secret_flag_here} # 未知Flag def encrypt(self, user_data: bytes) - bytes: 加密 user_data flag plaintext user_data self._flag # 注意需要填充因为总长度可能不是16的倍数 return aes_ecb_encrypt(self._key, plaintext) # 实例化Oracle oracle ECB_Oracle()4.2 攻击原理与步骤拆解攻击的核心在于利用ECB模式的块独立性和确定性。确定块大小通常AES是16字节但我们可以通过输入长度变化来验证。发送长度递增的明文如A*1,A*2, ...观察密文长度何时发生跳变。跳变点之间的长度差就是块大小。因为填充会使总长度对齐到块大小的整数倍。确认ECB模式使用我们之前写的detect_ecb函数发送一段至少两个块且内容相同的明文如A*32查看密文前两个块是否相同。破解Flag的第一个字符我们知道加密结构是[我们的输入][Flag]。如果我们构造输入使得我们的输入 Flag的第一个字符恰好填满一个完整的块那么加密这个块的结果只取决于这个块的内容。具体操作我们发送15个已知字符比如A*15。那么第一个加密块的内容是AAA...A(15个) Flag[0](1个)。我们不知道Flag[0]是什么但我们可以暴力枚举所有可能字符例如可打印ASCII码。对于每一个枚举字符c我们本地计算加密AAA...A(15个) c的结果我们需要模拟加密但密钥未知这里有个技巧我们让Oracle加密AAA...A(15个) c但此时c是我们输入的最后一个字符Flag被拼在后面处于下一个块。不对我们需要让c和我们的15个A在同一个块。所以我们需要让我们的输入长度为块大小 - 1这样Flag的第一个字符就会补全这个块。更正思路我们发送prefix bA * (block_size - 1)。此时Oracle加密的第一个块是prefix flag[0]。我们无法直接让Oracle加密这个组合因为flag[0]不可控。正确攻击方法字典攻击 a. 我们先发送bA * (block_size - 1)获取密文记下第一个密文块C1。C1是(A*15 flag[0])的加密结果。 b. 然后我们进行离线爆破遍历所有可能的字符char让Oracle加密bA * (block_size - 1) char。注意此时我们的输入是A*15 charOracle会加密(A*15 char) flag。这个加密结果的第一个块就是(A*15 char)的加密结果。 c. 我们将这个第一个块与之前记录的C1比较。如果相等说明char flag[0]因为ECB模式下相同的明文块产生相同的密文块。破解后续字符在破解了第一个字符后我们将前缀减少一个A改为发送bA * (block_size - 2) flag[0]。此时第一个加密块是(A*14 flag[0] flag[1])。然后我们继续遍历char加密bA * (block_size - 2) flag[0] char比较第一个块找到flag[1]。以此类推像“滑动窗口”一样逐个字符推出整个Flag。4.3 完整攻击代码实现下面是实现上述攻击的Python代码def ecb_chosen_plaintext_attack(oracle, block_size16, flag_length_guess50, charsetNone): 对ECB Oracle实施选择明文攻击逐字节恢复Flag。 :param oracle: 一个可调用的对象oracle.encrypt(data) 返回密文 :param block_size: AES块大小默认为16 :param flag_length_guess: 预估的Flag最大长度 :param charset: 要暴力枚举的字符集默认为可打印ASCII :return: 恢复出的Flag字节串 if charset is None: import string charset string.printable.encode() # 可打印字符 known_flag b # 我们不知道Flag具体多长可以一直破解直到无法找到匹配字符或达到预估长度 for offset in range(flag_length_guess): # 计算本次需要的前缀长度让未知字节位于块的末尾 prefix_len block_size - 1 - (offset % block_size) # 构造前缀已知部分用A填充 已经破解出的Flag部分 # 注意当offset小于block_size时known_flag长度可能为0 # 我们需要确保前缀总长度为 (block_size - 1) # 具体是 (block_size - 1 - len(known_flag)) 个 A 然后加上 known_flag # 但更通用的方法是固定前缀为 A * prefix_len然后目标字节是Flag的下一个字符 # 我们需要调整参考密文的获取方式 # 1. 获取目标密文块包含我们想破解的那个字节 # 输入A * prefix_len。Oracle加密 (A*prefix_len) known_flag flag[next] ... # 我们关心的块索引是 (prefix_len len(known_flag)) // block_size # 因为prefix_len被设计为使目标字节位于某个块的最后一个字节。 # 更清晰的方法使用两个输入 # 输入1探测输入长度为 (block_size - 1) - len(known_flag) 的 A # 这样Oracle加密时明文结构为 [探测输入][known_flag][未知字节][...] # 我们想获取 [探测输入 known_flag 未知字节] 这个块的密文。 # 让我们采用更直观的“滑动窗口”实现 # 对于要破解的第 i 个字节从0开始 # 构造填充 bA * (block_size - 1 - (i % block_size)) # 这样当Oracle拼接后我们想破解的字节正好是某个块的最后一个字节。 fill_length block_size - 1 - (offset % block_size) probe_prefix bA * fill_length # 获取目标密文并提取目标块 target_ciphertext oracle.encrypt(probe_prefix) # 计算目标块是第几个块 # 我们发送了 probe_prefixOracle加上了 known_flag 和 未知flag部分。 # 我们想获取的块是 probe_prefix known_flag flag[offset] # 这个块在密文中的索引是 (len(probe_prefix) len(known_flag)) // block_size # 但注意known_flag是逐渐增长的我们需要动态计算。 # 一个更稳健的方法是我们取固定位置的一个块通过调整probe_prefix的长度让目标块总是出现在同一个索引位置比如索引1。 # 标准攻击流程通常是这样先发送一个长输入确定Flag开始的块索引。 # 让我们简化并采用一种更通用的方法适用于Flag不在第一个块的情况 # 假设我们不知道Flag在哪个块开始。我们可以先发送一个空输入得到密文块。 # 然后发送一个单字节输入再得到密文块。比较两个密文第一个不同的块就是Flag开始的块。 # 但这里我们假设Flag直接拼接且我们知道块大小。我们采用另一种经典实现 print(f[*] Cracking byte at offset {offset}...) found False # 我们为每个偏移量计算一个“参考密文块” # 参考输入 bA * (block_size - 1 - (offset % block_size)) known_flag # 但known_flag可能很长超过一个块。我们需要确保参考输入的长度使得目标字节位于块边界。 # 经典实现来自ECB字节翻转攻击 # 为了获取第offset个字节我们构造输入 bA * (block_size - 1 - offset % block_size) # 然后目标块索引是 (block_size - 1 - offset % block_size offset) // block_size # 这有点复杂。我们换用更清晰的“字典攻击”代码 # 重新组织攻击逻辑 # 1. 确定Flag开始的块索引。 # 发送两个不同长度的输入比较密文变化。 zero_len_ct oracle.encrypt(b) one_len_ct oracle.encrypt(bA) # 找到第一个不同的块索引 start_block_idx None for i in range(0, len(zero_len_ct), block_size): if zero_len_ct[i:iblock_size] ! one_len_ct[i:iblock_size]: start_block_idx i // block_size break if start_block_idx is None: print([-] Cannot determine starting block of flag. Attack may fail.) start_block_idx 1 # 假设从第1块开始0索引 # 2. 对于每个要破解的字节位置 offset # 我们构造输入使得我们想猜的字节位于一个固定的“目标块”中。 # 我们让输入长度 (block_size - offset % block_size - 1) # 这样输入 known_flag的前 (offset % block_size) 字节会填满目标块的前面部分 # 然后我们猜的字节就是目标块的最后一个字节。 # 但我们需要已知 known_flag 的前 offset 个字节。 # 让我们采用一种更直白的实现可能牺牲一些效率但更易懂 # 我们逐个字节破解。假设我们已经知道了前 offset 个字节。 # 我们想猜第 offset 个字节。 # 我们构造一个输入 P长度为 L block_size - (offset % block_size) - 1。 # 这样Oracle加密 P known_flag flag[offset] ...。 # 我们关注密文中从 Pknown_flag 开始的那个块即索引为 (L len(known_flag)) // block_size 的块。 # 这个块的内容是 P known_flag flag[offset] 的前缀因为块大小固定。 # 为了得到这个块的密文我们发送输入 P并提取目标块 C_target。 # 然后我们枚举所有字符 c发送输入 P known_flag c并提取**第一个块**因为我们控制了整个块内容。 # 比较这个块的密文是否等于 C_target。 # 注意P known_flag c 的长度可能超过一个块。我们只取第一个块进行比较。 # 但 known_flag 可能很长。我们需要确保 P known_flag c 的第一个块正好是 P known_flag的前缀 c。 # 这要求 len(P) len(known_flag) block_size。但 known_flag 会增长。 # 因此我们需要分块处理。标准做法是一次破解一个块内的字节。 # 鉴于篇幅和清晰度我给出一个针对 **Flag在一个独立块内且我们可控输入在其前面** 的简化场景的完整代码。 # 这是CTF中最常见的“附加式Flag”场景。 # 由于完整实现代码较长且需要处理分块、边界等细节我将核心攻击循环的简化版逻辑展示如下 # 假设Flag被附加在我们输入的后面且我们能够通过控制输入长度让Flag的每个字节依次成为某个块的最后一个字节。 print([] Simplified attack simulation for educational purpose:) # 模拟一个已知密钥的Oracle但我们假装不知道密钥只调用encrypt方法 # 我们假设Flag是相对较短的字符串并且我们通过填充使要破解的字节对齐到块末尾。 # 重新定义Oracle使其行为更典型 encrypt(我们的输入 || flag) # 我们使用之前定义的ECB_Oracle # 首先确定块大小和Flag起始块实战步骤 def find_block_size_and_flag_start(oracle): # 探测块大小 length_map {} for i in range(1, 65): # 尝试输入长度1到64 ct oracle.encrypt(bA * i) length_map[i] len(ct) # 找到长度跳变点 prev_len length_map[1] for i in range(2, 65): if length_map[i] prev_len: block_size length_map[i] - prev_len print(f[] Detected block size: {block_size}) break prev_len length_map[i] else: block_size 16 # 默认 print(f[-] Could not detect block size, using default {block_size}) # 探测Flag起始块索引通过发送空输入和单字节输入比较 ct_empty oracle.encrypt(b) ct_one oracle.encrypt(bA) for i in range(0, len(ct_empty), block_size): if ct_empty[i:iblock_size] ! ct_one[i:iblock_size]: flag_start_block i // block_size print(f[] Flag appears to start at block index: {flag_start_block}) break else: flag_start_block 0 print(f[-] Could not detect flag start block, assuming {flag_start_block}) return block_size, flag_start_block block_size, flag_start_block find_block_size_and_flag_start(oracle) # 开始逐字节破解Flag recovered_flag b # 我们不知道Flag长度可以一直破解直到连续多个字符无法找到或遇到特定结束符如} max_attempts 50 for offset in range(max_attempts): # 构造填充使得我们想破解的字节位于一个块的末尾 # 我们希望 (填充长度 len(recovered_flag)) % block_size block_size - 1 # 这样当我们发送填充时Oracle加密的明文中 recovered_flag 目标字节 将形成一个完整的块。 # 填充长度 (block_size - 1 - (len(recovered_flag) % block_size)) padding_length (block_size - 1 - (len(recovered_flag) % block_size)) % block_size padding bA * padding_length # 获取目标密文块 # 我们发送paddingOracle加密 padding recovered_flag flag[offset] ... # 我们关心的块索引是 (padding_length len(recovered_flag)) // block_size target_block_index (padding_length len(recovered_flag)) // block_size ciphertext oracle.encrypt(padding) target_block ciphertext[target_block_index * block_size: (target_block_index1) * block_size] found_char None # 暴力枚举所有可能字符 for c in charset: # 构造测试输入 padding recovered_flag c test_input padding recovered_flag bytes([c]) test_ciphertext oracle.encrypt(test_input) test_block test_ciphertext[target_block_index * block_size: (target_block_index1) * block_size] if test_block target_block: found_char bytes([c]) break if found_char: recovered_flag found_char print(f[] Offset {offset}: found char {found_char}, current flag: {recovered_flag}) # 检查是否可能结束例如遇到} if found_char b}: print([] Possible end of flag reached.) break else: print(f[-] Could not find character at offset {offset}. Stopping.) break return recovered_flag # 运行攻击 print(\n--- Launching ECB Chosen Plaintext Attack ---) recovered ecb_chosen_plaintext_attack(oracle.encrypt) # 传入加密函数 print(f\n[!] Recovered flag: {recovered.decode()})这段代码是一个教学示例演示了攻击的核心循环。在真实CTF中你需要根据题目具体的Oracle行为比如输入如何拼接、是否有前缀等调整填充长度的计算和目标块的索引。5. 避坑指南与实战经验总结在实战中理论完美代码未必一次成功。下面是我总结的几个常见“坑点”和解决技巧。5.1 编码与解码的陷阱密文和明文在传输、展示时经常被编码。常见编码Hex十六进制、Base64、Base32等。坑点题目给的密文可能是Base64字符串你需要先base64.b64decode()才能得到字节串进行解密。反之解密出的明文字节串可能需要解码decode(utf-8)才能看到Flag。有时Flag里包含非UTF-8字符可能需要latin-1或ignore错误处理。技巧写一个通用的decode_possible_encodings(data)函数尝试常见的解码方式。import base64 import binascii def smart_decode(data): 尝试自动解码Hex或Base64编码的字节串。 if isinstance(data, str): data data.encode() # 尝试Base64 try: decoded base64.b64decode(data, validateTrue) if len(decoded) 0: return decoded, base64 except (binascii.Error, ValueError): pass # 尝试Hex try: # 移除可能存在的分隔符如空格、冒号 hex_data data.replace(b , b).replace(b:, b) if len(hex_data) % 2 0 and all(c in b0123456789abcdefABCDEF for c in hex_data): decoded bytes.fromhex(hex_data.decode()) return decoded, hex except (ValueError, AttributeError): pass # 都不是返回原数据 return data, raw5.2 填充错误Padding ErrorAES分组加密需要填充。cryptography库默认使用PKCS#7填充。坑点在解密时如果密文长度不是块大小的整数倍或者填充字节不正确unpadder.finalize()会抛出InvalidPadding异常。在CTF中的利用这本身可能是一种攻击向量如Padding Oracle Attack。但在我们编写解密脚本时这意味着我们可能拿到的密文被篡改或不完整。处理确保密文长度正确。如果是题目故意给的错误填充你可能需要尝试其他填充方式如无填充NoPadding但需手动处理或者考虑这本身就是解题线索。5.3 密钥与块大小的不确定性密钥长度题目可能使用AES-128、192或256。通常从上下文或常见性猜128位。如果解密结果乱码可以尝试其他长度。块大小AES固定16字节。但其他算法如DES是8字节。用探测法确认。技巧写一个brute_force_key_length(ciphertext, plaintext_guess)函数如果你有明文的一部分比如知道Flag格式是flag{可以尝试用不同长度的随机密钥解密看解密结果是否包含该明文段。5.4 Oracle攻击中的边界条件在实现4.3节的攻击时最容易出错的是计算目标块的索引和填充长度。坑点当known_flag长度超过一个块时逻辑会变得复杂。你需要清楚当前破解的字节位于第几个块的哪个位置。调试方法在攻击代码中加入详细的打印语句输出每一步的padding_length、target_block_index、known_flag长度和内容。画图辅助理解画出明文块的结构。一个检查点在破解每个字节前可以先用一个已知字符如X测试一下你的逻辑是否正确。临时修改Oracle让Flag变为已知字符串验证你的攻击脚本是否能完整恢复它。5.5 性能优化当字符集很大比如所有字节0-255或Flag很长时暴力枚举可能较慢。优化优先尝试可打印字符string.printable。如果Flag是英文、数字、下划线格式可以缩小到string.ascii_letters string.digits {}_。并行可以使用concurrent.futures库进行多线程并发枚举显著提升速度。6. 完整实战案例破解一个模拟的CTF题目让我们用一个完整的例子串联所有步骤。假设题目描述如下我们有一个服务它会用AES-ECB加密你输入的数据并在后面附上Flag。你能拿到Flag吗 连接信息nc target.com 12345或本地运行一个脚本我们编写一个本地模拟的服务端和客户端攻击脚本。服务端模拟server_sim.py:# server_sim.py - 模拟题目服务器 from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.primitives import padding from cryptography.hazmat.backends import default_backend import os KEY os.urandom(16) FLAG bflag{ecb_is_not_secure_for_encryption} def encrypt(data: bytes) - bytes: plaintext data FLAG padder padding.PKCS7(128).padder() padded padder.update(plaintext) padder.finalize() cipher Cipher(algorithms.AES(KEY), modes.ECB(), backenddefault_backend()) encryptor cipher.encryptor() return encryptor.update(padded) encryptor.finalize() def handle_client(data: bytes) - bytes: 模拟服务器处理返回加密结果hex编码 ciphertext encrypt(data) return ciphertext.hex().encode() b\n # 简单测试 if __name__ __main__: # 模拟客户端发送“A”*15 test_input bA * 15 response handle_client(test_input) print(fServer response (hex): {response.decode().strip()})客户端攻击脚本attack.py:# attack.py - 攻击客户端 import socket import string import time def get_encryption(plaintext: bytes) - bytes: 连接服务器或本地函数获取加密结果 # 这里我们直接调用本地模拟函数实际CTF中需替换为socket通信 from server_sim import encrypt return encrypt(plaintext) # 实际网络攻击示例注释掉 # with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: # s.connect((target.com, 12345)) # s.send(plaintext b\n) # data s.recv(1024) # return bytes.fromhex(data.decode().strip()) def ecb_attack(): block_size 16 charset string.printable.encode() # 可打印字符 known_flag b print([*] Starting ECB chosen-plaintext attack...) # 首先自动检测块大小和Flag起始块简化版假设Flag在输入之后 # 通过发送空输入和逐渐增加输入观察密文长度变化 base_len len(get_encryption(b)) for i in range(1, 33): ct get_encryption(bA * i) if len(ct) ! base_len: block_size len(ct) - base_len print(f[] Detected block size: {block_size}) # 确定增加了多少块 blocks_added (len(ct) - base_len) // block_size # 通常当i使得我们的输入刚好对齐块边界时会新增一个块来容纳Flag的一部分 # 更精确的检测需要比较密文块内容 break # 确定Flag开始的块索引通过比较空输入和单字节输入的密文 ct_empty get_encryption(b) ct_one get_encryption(bA) flag_start_idx None for i in range(0, len(ct_empty), block_size): if ct_empty[i:iblock_size] ! ct_one[i:iblock_size]: flag_start_idx i // block_size print(f[] Flag starts at block index: {flag_start_idx}) break if flag_start_idx is None: flag_start_idx 0 print([-] Could not detect flag start, assuming block 0) # 逐字节破解 for offset in range(50): # 假设Flag不超过50字节 # 计算填充长度使目标字节位于块末尾 # 目标让 (padding_len len(known_flag)) % block_size block_size - 1 padding_len (block_size - 1 - (len(known_flag) % block_size)) % block_size padding bA * padding_len # 获取目标密文块 target_ct get_encryption(padding) # 目标块索引 (padding_len len(known_flag)) // block_size flag_start_idx target_block_idx (padding_len len(known_flag)) // block_size flag_start_idx target_block target_ct[target_block_idx*block_size:(target_block_idx1)*block_size] found False for c in charset: test_input padding known_flag bytes([c]) test_ct get_encryption(test_input) test_block test_ct[target_block_idx*block_size:(target_block_idx1)*block_size] if test_block target_block: known_flag bytes([c]) print(f[] Offset {offset}: found {chr(c)} - flag so far: {known_flag}) found True if c ord(}): # 假设Flag以}结尾 print([] Found closing brace, stopping.) return known_flag break if not found: print(f[-] Failed at offset {offset}. Recovered: {known_flag}) break time.sleep(0.01) # 避免请求过快如果是真实网络请求 return known_flag if __name__ __main__: flag ecb_attack() print(f\n[!] *** RECOVERED FLAG: {flag.decode()} ***)运行这个攻击脚本你应该能成功恢复出模拟服务器中的Flag。这个案例涵盖了从环境搭建、原理理解、工具编写到最终攻击的完整闭环。7. 总结与扩展思考通过这个从原理到实战的项目我们系统地走完了AES-ECB模式破解的整个流程。关键点在于理解ECB“相同明文块产生相同密文块”这一核心弱点并利用可控输入和加密Oracle通过精心构造的输入和比较操作像“挤牙膏”一样将Flag逐个字符挤出。避坑的核心在于细心处理编码、填充、块索引计算和边界条件。在真实CTF中题目可能会增加更多变形例如在Flag前也有不可控的前缀需要先确定前缀的长度和内容可能通过重复输入使前缀对齐块边界。使用非标准编码或自定义填充需要逆向分析服务器代码或通过错误信息推断。限制输入长度或字符集需要更精细地构造输入或者利用Hex/Base64编码绕过过滤。掌握AES-ECB的破解不仅是掌握一种攻击技术更是理解分组密码工作模式安全性的绝佳起点。它清晰地展示了为什么在现代加密中ECB模式不应被用于直接加密数据而必须使用带有随机化元素如IV的模式如CBC、CTR或认证模式如GCM。最后我个人的体会是密码学攻防就像一场智力游戏。工具和脚本是武器但对原理的深刻理解才是真正的内功。每次遇到新的题目先别急着写代码花时间画一画数据块的结构图理清加密的流程往往能更快地找到那条隐藏的攻击路径。试着用我们在这篇指南里构建的思维框架和代码工具去挑战一些在线的CTF平台上的真实ECB题目那才是技能巩固和提升的最佳方式。