ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ECB Oracle攻击实战:不靠密钥,密文也能被逐字节啃成明文

ECB Oracle攻击实战:不靠密钥,密文也能被逐字节啃成明文 ECB Oracle 攻击实战不碰密钥怎么把密文一块块啃成明文先讲个场景。你拿到一个 Web 接口后端用 AES 加密了一个类似 session 的字段你把密文发回去服务器会解密、校验、返回不一样的结果。加密用的是最常见的分组密码模式之一ECB。表面上你没有密钥攻击似乎无从谈起。可实际上ECB 搭配一个会泄露“解密结果对不对”的 oracle密文在你眼里就是透明的。这篇文章要说的就是这种“ECB oracle attack”。我会从 ECB 为什么危险开始逐步拆几种真实可复现的攻击思路Byte-at-a-Time 解密密文、Cut-and-Paste 块重排以及围绕 Padding Oracle 的边界问题。适合三类人看打 CTF 的选手、做 Web 安全的工程师、还有后端开发里负责加解密逻辑的人。只要你看完能下意识地反问一句“我这边用 ECB 是不是也有这个问题”这篇文章就不白写。1. 先搞懂 ECB为什么“同一个明文块”会出卖你1.1 分组密码的工作方式回顾先花一分钟把基础对齐。AES、DES、SM4 这类算法本质上是“分组密码”一次处理固定长度的明文块。AES 是 16 字节一块。当明文长度超过一块就需要一种模式把多块串起来。ECBElectronic Codebook电子密码本是最简单的一种每个明文块独立加密输出对应的密文块块与块之间没有任何关联。伪代码就这么直白C1 E(K, P1) C2 E(K, P2) ... Cn E(K, Pn)也就是说加密函数E在同一个密钥K下是确定的。给定相同的 16 字节明文永远得到相同的 16 字节密文。这个性质放在 ECB 里就是一切灾难的根源。1.2 ECB 致命的确定性问题确定性的问题不只是“重复块会暴露规律”而是整个明文的结构、统计特征、分布规律都会原封不动地映射到密文里。最经典的演示是加密图片。你把一张熊猫图片按像素拆成块用 ECB 加密后重新拼图还能看出熊猫轮廓。为什么因为图片里大量相邻像素块是相同的ECB 让相同明文块变成了相同密文块视觉上自然保留了图案。CBC 等模式会引入随机性相同明文块被加密成不同密文块才能把统计特征打散。对文本数据也一样。一份英文文档里有大量空格、换行、常见单词前缀这些块的频率特征会出现在密文里。攻击者不需要解出密钥光靠频率分析和块匹配就能猜出大量内容。ECB 在密码学上泄露的“信息量”不是一点点。1.3 为什么现实系统里还能见到 ECB按理说 ECB 被所有密码学教材批判了几十年但在实际系统里它依然顽强存在。原因大概有三类赶进度开发图省事“反正 AES 加密了看不出来”。程序里直接AES.new(key, AES.MODE_ECB)没有任何模式安全意识。历史遗留老系统接手的加密模块就是 ECB要改模式还涉及密文兼容大家都不愿意动。认知偏差项目里数据量小、块少觉得重复块不明显自己骗自己“影响不大”。这三个原因我们见过太多次。实际上 ECB 只要用于两块以上相同内容的明文安全隐患就比你想的大得多。更别说它还会和 oracle 组合把密文直接变成明文。2. Oracle 到底是什么一个黑盒偷偷告诉你“对/不对”2.1 密码学里的 Oracle 概念“Oracle”这个词在密码学里不是某个软件而是一个抽象概念一个你可以向它发送请求、它会返回某种结果的黑盒系统。在攻击模型里Oracle 并不直接给你密钥但它会泄露“判断结果”。你利用这些判断结果反推加密内部的中间数据。最常见的 Oracle 泄露通道有填充校验 Oracle解密后发现 padding 不对返回错误码 500padding 对但业务失败返回 403。两种响应不同。登录 Oracle解密成功与否反馈在“能否登录成功”上。长度 Oracle返回响应时长的细微差异。时间 Oracle填充校验通过的路径和失败的路径耗时不同。对一个安全测试人员来说只要服务端某处返回了“解密是否成功”的差异就等于给了你一把撬棍。2.2 一个典型的可攻击接口长什么样假设后端是这样一段逻辑def decrypt_session(cookie): try: plaintext aes_ecb_decrypt(key, cookie) data parse(plaintext) unpad(data, 16) # 校验 PKCS#7 填充 return ok except ValueError: return padding error注意这里成功和失败返回的字符串不同。前端看不到也无所谓接口响应码不同、报错信息不同、甚至返回长度不同都是信号。攻击者马上可以判断这是一个 Padding Oracle。2.3 不是所有提示都叫可利用 Oracle需要强调Oracle 攻击的前提是“差异可观察且可稳定触发”。如果服务端把所有解密失败全部统一成同一个响应那 Padding Oracle 就失效了。但 Byte-at-a-Time 攻击还用到另一种 Oracle加密 Oracle。它不是你主动调用的漏洞而是“服务器会把你输入的内容拼到敏感数据后面再加密”这种功能设计。很多开发觉得这没毛病其实这就是一个定时炸弹。下面三个攻击案例都由 Oracle 拉开序幕。3. 攻击一Byte-at-a-Time ECB 解密——用选择明文把密钥之外的秘密逐字节挤出来3.1 攻击成立的前提这是 ECB oracle 攻击里最经典、思路也最优雅的一种。前提条件有两个存在一个加密 Oracle服务端会把攻击者可控的输入与某个未知 secret 拼接起来进行 AES-ECB 加密然后返回完整密文。攻击者能控制输入长度并能拿到完整密文。典型代码就是def encryption_oracle(user_input): plaintext user_input SECRET cipher AES.new(key, AES.MODE_ECB) return cipher.encrypt(pad(plaintext, BLOCK_SIZE))目标在不知道 key 的情况下恢复SECRET的全部内容。这个模型在 CTF 里极其常见。比如服务端加密username flag把结果放在 cookie 里回传比如加密接口支持“评论内容 隐藏消息”。只要拼接顺序是你可控内容在前、秘密在后理论上都是这种攻击的目标。3.2 核心思路把未知字节逼到块边界上ECB 的特点是每个块独立加密。假设块大小是 16。现在我们不知道 secret 的任何一个字节但我们可以通过控制输入长度让某个未知字节恰好落在某一块的最后一个字节位置。看这个过程以第 0 块为例正常加密input secret时第 0 块的内容是input[0:16] secret[0:...]的一部分。如果输入 15 个A第 0 块变成A * 15 secret[0]。我们不知道 secret[0] 是什么但可以暴力枚举假设它是字节 c0 到 255构造输入A * 15 c加密后看第一块是否与刚才的目标块相同。因为 ECB 在同密钥下对同一明文块的加密结果唯一一旦枚举到正确字节密文块就完全相等。这样 secret[0] 就拿到了。拿到第一个字节后继续。这次输入 14 个A第 0 块变成A * 14 secret[0] secret[1]。我们已知 secret[0]于是枚举 secret[1]。之后每恢复一个字节就减少一个A的填充量让新的未知字节被推到块末位。跨块时同理。恢复完前 16 个字节后进入第 1 块。此时要观察的密文块从第 0 块换成第 1 块填充模式一样只是把已知的 secret 内容补在A后面。3.3 带前缀时的处理办法不少现实场景并不是“输入直接拼 secret”而是“固定前缀 输入 secret”。比如用户名、UUID、时间戳都会破坏输入的对齐。攻击者第一步得先知道前缀长度否则没法精确控制块边界。一个常用的检测方法是寻找密文中的重复块。构造输入A * (i 32)让前缀加A恰好填满整数个块后面再出现两个连续相同的明文块密文里就会对应出现两个完全相同的密文块。遍历 i 从 0 到块大小 - 1首次出现相邻重复块的位置就能推算出前缀长度。具体推算逻辑不复杂但新手很容易绕晕。我自己通常写成工具函数每次复用def detect_prefix_len(oracle, block_size): for i in range(block_size): payload bA * (i block_size * 2) ct oracle(payload) blocks [ct[j:j block_size] for j in range(0, len(ct), block_size)] for k in range(len(blocks) - 1): if blocks[k] blocks[k 1]: # 第一个重复块的起点 prefix_len i k * block_size return block_size * (k 1) - (i block_size * 2) return 0如果检测出来前缀长度是 15那输入对齐时需要额外补 1 个A让“前缀 输入”变成整块。攻击代码里统一用align (-prefix_len) % block_size来处理后面恢复 secret 的循环就不会受前缀干扰。3.4 完整 Python 攻击脚本下面是我自己整理过的一个通用脚本适用于“输入 secret”模型的 ECB 解密。核心逻辑已经注释清楚from Crypto.Cipher import AES from Crypto.Util.Padding import pad import os # 模拟目标环境 key os.urandom(16) SECRET bflag{ecb-oracle-byt-at-a-time} BLOCK_SIZE 16 def oracle(data: bytes) - bytes: return AES.new(key, AES.MODE_ECB).encrypt(pad(data SECRET, BLOCK_SIZE)) def detect_block_size(oracle) - int: base len(oracle(b)) for i in range(1, 64): cur len(oracle(bA * i)) if cur ! base: return cur - base raise RuntimeError(block size too large) def recover_secret(oracle, block_sizeBLOCK_SIZE): # 前缀为 0如果有前缀先算 prefix_len再在每次输入里补 align 个字节 secret b total_len len(oracle(b)) secret_len total_len - block_size # 去掉末尾一个填充块严格按实际 padding 算也可 for pos in range(secret_len): offset pos % block_size block_idx pos // block_size # 把未知字节推到块末位 filler bA * (block_size - 1 - offset) # 观察目标块 target_ct oracle(filler) target_block target_ct[block_idx * block_size : (block_idx 1) * block_size] # 构造字典暴力枚举未知字节 known_in_block secret[pos - offset : pos] for c in range(256): guess_input filler known_in_block bytes([c]) guess_ct oracle(guess_input) guess_block guess_ct[block_idx * block_size : (block_idx 1) * block_size] if guess_block target_block: secret bytes([c]) break return secret print(recover_secret(oracle))这里有几个细节值得说secret_len的计算取决于尾部有没有标准 padding。如果整段输入不是块大小整数倍oracle(b)返回的长度会包含一个填充块直接用len(oracle(b)) - block_size粗略估算 secret 长度即可通常 CTF 里够用。暴力枚举 256 个字节每次调用 oracle 一次恢复一个 48 字节的 secret大概需要一万两千多次 oracle 调用。本地模拟很快但如果 oracle 是远程 HTTP 接口性能就要考虑。实际测试时可以用多线程或者先在本地起一个 mock 服务验证脚本再上真实目标。如果 oracle 返回结果里混入了非密文数据比如返回 JSON就先把密文部分取出来再按块切分。3.5 边界条件与踩过的坑这个攻击实现不算难但实操时坑不少。第一个坑是块大小检测。如果 oracle 底层用的是 DES 或 3DES块大小是 8不是 16。不能用固定值必须动态探测。我上面的detect_block_size是通用写法直接看“输入长度变化引起密文长度变化的差”。第二个坑是 prefix。很多人在攻击时忽略前缀直接套用“无前缀版”脚本结果永远匹配不上。先花时间确认拼接格式是inputsecret还是prefixinputsecret区别非常大。第三个坑是填充块。如果 secret 长度刚好是块大小的整数倍最后会多出一个纯填充块恢复时没关系但不要把填充块当成 secret 的一部分。第四个坑是“已知块内已恢复部分”的切片。我的脚本里用secret[pos - offset : pos]来取当前块内已经恢复的字节。新手容易写错成secret[-offset:]在跨块时直接切片越界或取错位置。4. 攻击二Cut-and-Paste 块重排——把密文块当乐高拼出任意明文4.1 攻击成立的条件Byte-at-a-Time 攻击把秘密从黑盒里“挤”出来但还有一种场景更简单粗暴服务端用 ECB 加密结构化数据攻击者虽然解密不了但能通过重新排列密文块构造出完全合法且语义不同的明文。这就是 Cut-and-Paste 攻击也叫块重排攻击。前提条件有几个服务端用 ECB 加密且不校验完整性没有 MAC、没有签名。明文结构是分段的字段边界恰好能被攻击者对齐到块边界。攻击者知道明文的字段结构至少能推测出某个字段在哪个块里。典型场景是加密 cookie。比如 cookie 明文是usernameadminroleuser打包成 16 字节一组后攻击者虽然不知道密钥但可以通过注册一个很长很长的用户名把敏感字段精确推到某个块位置然后把这个块剪下来替换到目标用户的 cookie 对应位置。4.2 边界对齐攻击的核心技巧块重排对边界对齐极其敏感。原理是 ECB 解密只关心每一块内部的密文完全不验证块之间的顺序。你可以把第 3 块搬到第 1 块的位置解密照样成功只是明文顺序变了。对齐的思路是控制用户名长度让roleuser或者roleadmin整个词单独落在某一个块内。举个例子块大小 16明文结构是usernamexxxxxxxx roleuser如果用户名长度是 13那么块0: usernamexxxxxxxx14字节?我们来实际算一下。假设前缀是username占 9 字节。用户名x长度设为 L明文后面跟着roleuser占 11 字节。我们希望roleuser整段恰好从一个块的起点开始。总前缀长度 len(username) 9。要让9 L是 16 的倍数L 需要满足(9 L) % 16 0取 L 7 时9 7 16块 0 就是完整的usernamexxxxxxx块 1 开头是roleuser正好从块边界开始。这样我们就能独立替换包含角色的块。但roleuser本身的长度是 11不到 16。要让整块被单独控制通常把字段写成xxxxxroleuser凑满 16或者把 admin 字段放在块内与可控制填充一起。更简单的模型来自 Cryptopals Set 2 的经典题目用 ECB 加密一段包含用户 email 和 role 的字符串攻击者通过构造 email 长度把roleadmin后面的数据对齐成独立块再拼接复制。4.3 一个实际的攻击流程假设服务端加密以下内容字段之间用分隔emailattackerexample.comuid100roleuser我们想把roleuser改成roleadmin。第一步注册一个 email 长度为精确值使密文块按我们想要的顺序排列。计算email是 6 字节example.com是 12 字节等等。目标是把role后面的 4 字节user对齐到块的边界。第二步找一段合法输入让admin后面被填充字节补齐使admin作为一个完整的块被加密。一般通过把 email 加长让明文里出现一个可以裁剪的roleadmin状态烘焙出一个单独块。第三步从自己的 cookie 密文中切出包含admin的块替换到目标 cookie 中user所在块的位置。由于 ECB 不检查块顺序也不做完整性校验服务端解密后拿到的就是roleadmin。4.4 为什么现实中这种攻击经常导致越权块重排攻击最常见的影响就是越权和身份伪造。加密 cookie 里只放了uid100攻击者把另一条包含uid1的密文块拼过来就可能变成管理员。不需要知道密钥不需要破解 AES只要服务端不做完整性校验。我遇到过一个真实业务场景服务端用 AES-ECB 加密用户的身份标识和过期时间接口返回密文当作 token。由于标识位设计得短攻击者只需要注册两个账号把两个 token 的前几块互换就完成了身份切换。当时排查时发现根因不是算法被破解而是“ECB 不防篡改”加上“没有任何认证机制”。所以在评估这类漏洞时补丁不只是换加密模式还要在密文上附加 MAC或者整体切换到认证加密模式。只把 ECB 换成 CBC 并不会解决块重排问题CBC 虽然块间有依赖但除了第一个字节翻转的差异同样不防重排。5. 攻击三Padding Oracle 与 ECB 的关系——别再踩“改算法就行”的坑5.1 PKCS#7 填充是怎么工作的分组密码要求明文长度是块大小的整数倍不够就补。PKCS#7 是最常见的填充方案缺几个字节就补几个字节缺 n 个字节就补 n 个值为 n 的字节。举例块大小 16如果明文最后一块只剩 12 个字节那就补 4 个0x04原始最后一块: XX XX XX XX XX XX XX XX XX XX XX XX 补齐之后: XX XX XX XX XX XX XX XX XX XX XX XX 04 04 04 04如果明文刚好是整块也要额外补一个完整的填充块也就是 16 个0x10。解密后必须校验末尾字节是否符合这个规则。5.2 经典 Padding Oracle 为什么主要打在 CBC 上大家都听说过 Padding Oracle Attack但经典攻击的主要对象其实是 CBC 模式不是 ECB。原因要从两种模式的结构差异说起。CBC 解密的公式是P_i D(C_i) XOR C_{i-1}攻击者可以修改C_{i-1}的任何一个字节精确改变解密后P_i对应位置的字节而不会影响C_i解出的中间结果。通过控制C_{i-1}的最后一个字节试出能让P_i最后一个字节等于0x01的值就可以反推出D(C_i)的最后一个字节。逐个字节推整个块的中间状态就全出来了。攻击者利用的就是 CBC 这种“修改前一块密文只影响当前块对应字节”的性质。这正是Padding Oracle 攻击的心脏。5.3 ECB 下直接套用会碰到什么限制如果试图在 ECB 上直接套经典 Padding Oracle 流程会发现一个麻烦ECB 解密的公式是P_i D(C_i)没有C_{i-1}这个独立控制入口。修改某一块的密文影响的是这一块内部的明文但每个字节之间也是独立解密的你无法在不影响其他字节的情况下精确翻转某一个明文字节。或者说你失去了 C 前面的“控制通道”。当然这不代表 ECB 没有 Padding Oracle 问题。如果服务端把“解密成功但业务失败”和“padding 失败”分开返回你至少能利用 oracle 做长度探测或填充有效性探测判断某个块是否位于末尾、明文长度是多少、某些字节是否匹配等。只是想要逐字节还原整个任意密文块不如 CBC 那么顺手实际攻击中也很少有人死磕这条路径。我建议遇到“ECB Padding Oracle”的真实目标时先试试 Byte-at-a-Time 和块重排这两个通常效率更高。如果条件不允许再考虑用 Padding Oracle 做辅助探测。5.4 真正的解法不要把 Oracle 这类信息泄露留给攻击者无论是 CBC 还是 ECBPadding Oracle 能成立的根本原因是服务端把“解密是否成功”的信息返回给了调用方。MySQL 的加密函数、Python 的 crypto 库、各种 Web 框架的错误处理都可能在无意间泄露这种状态。防御不是“把 ECB 换成 CBC”就结束。CBC 同样会中 Padding Oracle甚至经典的 Padding Oracle 主要就是打 CBC。正确的做法是使用认证加密模式如 AES-GCM、AES-CCM它们自带完整性校验密文被篡改后解密直接失败。如果必须用 CBC 或 ECB务必在密文上附加 HMAC采用 Encrypt-then-MAC 的顺序。解密失败时统一返回同一错误响应、同一响应时长不能区分“padding error”和“auth error”。在应用层再次校验数据合法性。这些不是理论建议每一条都是真实线上事故换来的。6. 实战前的自检清单与防御复盘6.1 怎么判断你的系统是否在用 ECB给读者一份可以照着检查的清单。查看加密代码看是否出现MODE_ECB、ECB、Cipher.getInstance(AES/ECB/...)之类的关键字。Java 的AES/ECB/PKCS5Padding是默认选项很多新手误以为这是标准写法。观察密文同一字段、相同值是否生成完全相同的密文块。你可以把 cookie 或 token 复制两份比较是否一模一样。如果相同几乎可以确定是 ECB。看加密库里是否显式设置了 IV。ECB 没有 IVCBC 一般有 16 字节的随机 IV。如果代码里没出现 IV大概率用了 ECB。6.2 安全加固的最小改动方案如果系统已经在线上跑短期内没法大规模改造那么按优先级做这几件事立即给密文加 HMAC。MAC HMAC(key2, ciphertext)发送ciphertext MAC。接收时先验证 MAC再解密。这一条可以同时挡住块重排和大部分混淆攻击。解密失败错误统一化。所有解密异常都返回同一个 HTTP 状态码和同一段提示文案收敛时间侧信道。排期把 ECB 替换成 AES-GCM。GCM 自带认证标签不需要额外维护 MAC是目前工程上最推荐的默认选择。对已有密文进行版本标记。在密文头部加一个版本字节线上旧 token 还能解析新 token 走新格式平滑迁移。6.3 容易踩的加密误区有些开发者以为“用的 AES 就是安全的”其实 AES 只是原语安全与否取决于模式和实现。ECB 是 AESGCM 也是 AES两者面对攻击者时的安全边界天差地别。还有人以为“密文里有随机 IV 就安全”。CBC 加上随机 IV 确实能防相同明文泄露但如果不做完整性校验Padding Oracle 和位翻转攻击依然有效。IV 只是解决了统计泄露不是万能药。更隐蔽的一个误区是“反正攻击者拿不到密钥他改不了密文”。ECB 的块重排攻击恰恰不需要密钥只需要把合法密文块换个顺序。密钥保护的是内容秘密不是完整性。完整性必须单独设计。7. 最后再聊一点经验从我的实际测试经验来说ECB oracle 攻击真正可怕的地方在于“成本极低、识别极容易”。byte-at-a-time 攻击脚本写下来不超过 60 行跑一轮可能只需要几十秒而很多系统的加密方案就是这么脆。打这类目标时最值得花时间的往往不是破解算法而是摸清拼接格式和 oracle 的响应差异。如果你在测自己的系统建议按这样的顺序排查先看加密模式确认不是 ECB再看失败响应确认没有泄露填充或认证状态最后看有没有对密文做完整性校验。三个点都过了才算在这条攻击链上站稳了脚跟。攻击是手段搞清楚原理才能在写代码的时候有这个意识。希望你下次新建一个加密接口时能多想一步如果攻击者知道模式能从我这里拿到什么想清楚这个问题比记住任何一行修复代码都重要。
返回列表