资讯动态

解密AES的CBC与CTR模式:从原理到Python实战

发布时间:2026/10/16 5:22:18 来源:尧图企业网站定制
拿到了这周的作业题目Week 2: AES CBCCTR解密作业。说实话第一次看到这个标题我以为是那种随便写两行代码调个库就能交差的练习结果真正动手才发现CBC和CTR这两种模式虽然都叫AES但背后的设计逻辑、代码写法、甚至踩坑姿势完全不一样。这篇就把我这一周从读题、写代码到反复调试的全过程记录下来给同样在啃这块内容的同学做个参考。先说结论如果你的作业也是解密老师提供的密文还原明文那么你真正要学的不是AES本身而是分组密码的工作模式。AES只是那个干活的黑盒CBC和CTR是怎么把数据块串起来、怎么引入随机性、怎么处理最后一块不满16字节的情况这些才是作业真正想让你掌握的。1. 拿到作业题以后先别急着写代码把CBC和CTR的脾气摸清楚我第一天的状态是打开编辑器import一个库然后对着密文发呆。因为我不知道题目里的密文到底是怎么来的IV是什么有没有Padding密文是Base64编码还是Hex编码。这些都是能直接让程序崩溃的问题。1.1 CBC和CTR到底差在哪CBC全称是Cipher Block Chaining密码分组链接模式。核心思想是每一个明文块在加密之前先和上一个密文块做一次XOR运算然后再进入AES加密器。第一个明文块没有上一个密文块所以用IV初始向量来顶替。CTR全称是Counter计数器模式。它做的事情看起来有点反直觉先用一个随机数Nonce加上计数器生成一个个数据块这些数据块依次经过AES加密得到一串密钥流然后把明文和密钥流做XOR就得到密文。注意明文本身不经过AES加密器。这两个模式的差别直接决定了解密代码的写法CBC解密对每个密文块做AES解密再与上一个密文块或IVXOR。解密过程依赖前一块密文。CTR解密用同一个Nonce和计数器序列生成密钥流把密钥流和密文XOR。解密完全不依赖密文本身的顺序。如果你想用一个词来记住两者的核心区别那就是CBC是块与块之间有关联CTR是块与块之间完全独立。CTR这种独立特性带来了一个很实用的好处——支持并行计算加密和解密可以同时处理多个块速度更快。1.2 作业里最常见的几种出题套路这周的作业题在不同的课程里出现过多个版本我总结了最常见的三种你可以对照自己的题目看看属于哪一种套路类型题目给出什么你需要做什么完整参数型密文、IV、密钥且标注了编码方式直接写解密函数处理Padding隐藏参数型只给密文和密钥故意不给IV在固定使用全零IV的假设下解密编码混合型密文是Base64IV是Hex格式混乱先统一编码再做解密大部分作业题都在第三种类型里翻车因为很多人根本没意识到密文是Base64字符串直接当成字节数组送进解密函数结果要么报错要么解出一堆乱码。我建议你拿到题目后先在纸上写下三行字密钥是什么格式IV是什么格式密文是什么格式这三件事确定之前不要碰代码。1.3 一个容易被忽略的知识点为什么CTR解密的代码和加密的代码长得一样这里必须单独说一下因为这是作业里最容易让你自我怀疑的地方。在CTR模式下加密和解密做的事情完全一样生成密钥流然后与输入数据做XOR。因为XOR操作满足偶对性——同一个数异或两次会回到原值。可以这么理解小明用密钥流K和明文P做XOR得到密文C也就是 C P XOR K。解密的时候小刚拿到C再次用同一个密钥流K做XOR得到 P C XOR K P XOR K XOR K P。所以CTR模式的解密函数其实就是加密函数代码可以复用。很多人在作业里看到解密函数调用了加密函数的内部逻辑以为写错了其实这是CTR模式的正常操作。2. 解密之前的准备工作环境、工具和最容易翻车的编码问题这一节全是我个人经验总结出来的必做项。我做这个作业的时候因为赶时间跳过了环境检查结果花了一个多小时在装依赖和调编码上非常亏。2.1 Python环境与常用库选型我选的是Python 3.11配合pycryptodome库。市面上还有cryptography这个库也可以用但pycryptodome的API更贴近教材里的CBC/CTR定义拿来写作业和做验证都比较顺手。安装命令pip install pycryptodome验证是否装好了python -c from Crypto.Cipher import AES; print(AES OK)有几个易混点必须提前说明。Crypto和pycryptodome的关系是老版本的pycrypto库已经多年不维护了很多人装错装到了这个僵尸库上然后各种报错。你先执行pip list | grep Crypto看看自己装的是哪个如果是pycrypto开头的建议先卸载。而且有些教程里会写from Crypto.Cipher import AES注意Crypto的大小写Linux系统上目录严格区分大小写你要是写成from crypto.Cipher import AES当场报错。2.2 Base64、Hex和原始字节编码问题能让你怀疑人生这是我认为整个作业里最不值得但最消耗时间的一关。如果是Hex编码每两个字符表示一个字节比如字符串a1b2c3表示3个字节0xA1 0xB2 0xC3。用Python转换cipher_hex a1b2c3... cipher_bytes bytes.fromhex(cipher_hex)如果是Base64编码通常末尾会有号填充。这种场景在我遇到的作业里出现频率最高因为Base64能直接嵌入文本文件线上传输也方便。转换方法import base64 cipher_b64 aBcD... cipher_bytes base64.b64decode(cipher_b64)这里有一个非常值得警惕的坑把密文从Base64变成bytes之后不要再调用decode()方法转成字符串。密文是任意的二进制数据很多字节不是合法的UTF-8字符强行转字符串会直接报UnicodeDecodeError。AES解密函数的输入输出都是bytes类型明文出来后如果需要看内容才考虑解码。2.3 密钥长度的选择16、24还是32字节AES支持三种密钥长度128位16字节、192位24字节、256位32字节。题目一般会直接说明没说明的话就看密钥字符串的长短。作业里最常见的是32字节也就是256位因为安全性更高写起来也更有专业感。如果题目给的是类似MySecretKey12345这种人类可读字符串你需要注意它到底有几个字节。字母和数字都是1字节但如果有中文情况就变了——中文在UTF-8编码下占用3个字节。我见过有人用含中文的密钥最后怎么加解密都对不上就是因为密钥实际字节数和AES要求的不匹配。判断方法很简单key_str your-key-here key_bytes key_str.encode(utf-8) print(len(key_bytes)) # 必须是 16、24 或 32否则会报错如果长度不对有些版本的库会直接抛出ValueError。但更隐蔽的情况是题目给的密钥是Hex字符串你直接把字符串本身当成密钥用了比如2b7e151628aed2a6abf7158809cf4f3c是16字节的Hex表示但你直接编码成了32字节的ASCII字符串。这种错很难发现解密出来的东西全是乱的而且不会报任何错。3. CBC模式解密实操从拿到密文到出明文的全过程CBC在本次作业中属于必须要会的基础操作。我先用一段可运行的完整代码讲清楚流程然后单独讲Padding。3.1 CBC解密代码的完整骨架假设题目给的参数如下密钥2b7e151628aed2a6abf7158809cf4f3cHex格式16字节IV000102030405060708090a0b0c0d0e0fHex格式16字节密文一段Base64字符串存放在ciphertext_base64.txt里完整代码from Crypto.Cipher import AES import base64 # 1. 把Hex字符串转成字节 key bytes.fromhex(2b7e151628aed2a6abf7158809cf4f3c) iv bytes.fromhex(000102030405060708090a0b0c0d0e0f) # 2. 把Base64密文转成字节 with open(ciphertext_base64.txt, r) as f: cipher_b64 f.read().strip() ciphertext base64.b64decode(cipher_b64) # 3. 创建CBC模式的AES解密器 cipher AES.new(key, AES.MODE_CBC, iviv) # 4. 解密 plaintext_padded cipher.decrypt(ciphertext) # 5. 去掉PKCS7 Padding后面细讲 pad_len plaintext_padded[-1] plaintext plaintext_padded[:-pad_len] print(plaintext.decode(utf-8))这段代码的流程很清晰转格式、建对象、解密、去填充。但实际作业里你可能遇到的问题集中在第5步。3.2 PKCS7 Padding机制以及为什么解密后要去掉它AES是分组密码每组16字节。如果明文长度不是16的倍数最后一块儿会缺斤少两。作业通常强制使用PKCS7填充来补齐。PKCS7的做法是缺多少个字节就补多少个值为缺少数目的字节。比如最后一块差3个字节那就在后面补3个0x03如果明文恰好也是16的倍数那就额外补整整16个0x10。这里有一个大多数新手不知道的细节即使明文长度恰好是16的倍数也必须补一个完整的填充块。这么做的原因是解密时没法区分最后一个字节恰好是0x01和根本没有填充为了消除歧义干脆永远加填充。很多人第一次解密时发现明文最后多了一堆\x10就是因为漏了这个知识点。反过来解密后要去掉填充判断依据就是看最后一个字节的值是多少然后删掉多少个字节。但这样做有一个隐藏前提最后一个字节的值必须合法也就是1到16之间。如果解密出的最后一个字节是0x00或者大于16要么是密钥或IV错了要么是密文被改动过。更稳健的去掉方式pad_len plaintext_padded[-1] if pad_len 1 or pad_len 16: raise ValueError(Padding长度不合法检查密钥/IV/密文) plaintext plaintext_padded[:-pad_len]3.3 实战中CBC解密最常见的报错与排查我这一周被几个报错反复折磨列出来帮你避雷报错1ValueError: AES key must be either 16, 24, or 32 bytes long密钥长度不对。先print(len(key))看看你的密钥字节数注意是字节数不是字符串长度。报错2ValueError: IV must be 16 bytes longIV长度必须是16字节和AES的块大小一致。很多人把IV当成字符串直接传入比如iv1234567890abcdef长度是16个字符但字符不是字节如果里面有中文或特殊符号长度就乱了。最稳妥的方法是先转成bytes再检查len()。报错3ValueError: Data must be padded to 16 byte boundary in CBC mode你的密文长度不是16的倍数。这种情况通常是Base64解码后的密文本身损坏了或者你输入的是Base64字符串而不是字节数据。记得加一行检查assert len(ciphertext) % 16 0。报错4不报错但明文全是乱码这个最烦。原因分三种密钥错误、IV错误、密文编码格式识别错。逐个排查方式是把IV全设成\x00试一次再把密钥从Hex转成字节重新试一次每换一个参数就看一下输出头几个字节是不是可读的ASCII。4. CTR模式解密实操看起来简单坑全在计数器上通过了CBC这一关之后CTR模式本以为能十分钟搞定结果我在这上面花的时间反而是CBC的两倍问题几乎全出在Counter的构建方式上。4.1 CTR模式到底是什么一个一次性密码本的工程化变体理解CTR之前先回忆一个概念一次一密。如果有一个和明文一样长的随机密钥流把两者XOR密文就完全不可破解。问题是密钥流怎么安全地发给接收方一次性密码本的致命缺陷是密钥流不能重复用否则XOR运算就可以被攻破。CTR的巧妙之处在于它不需要一个和明文一样长的随机字符串而是用一个固定长度的随机Nonce配合递增计数器经过AES加密后生成这个密钥流。因为AES是一个伪随机置换任何不同的输入块都会产生看起来完全不同的输出块所以只要Nonce不重复、计数器不溢出密钥流就和随机的一样。解密时只需要知道Nonce和计数器的初始值就能重新生成一模一样的密钥流然后与密文XOR还原明文。4.2 代码实现从库函数到手动控制Counter用pycryptodome实现CTR有两种方式一种是直接传一个counter回调函数另一种是构造Crypto.Util.Counter对象。作业里最常见的是第二种from Crypto.Cipher import AES from Crypto.Util import Counter # 假设密钥是32字节Nonce为8字节 key bytes.fromhex(2b7e151628aed2a6abf7158809cf4f3c) nonce bytes.fromhex(0000000000000001) # 8字节Nonce # 关键点计数器初始值 Nonce后面拼接一个初始计数器 ctr Counter.new(128, initial_valueint.from_bytes(nonce, big)) cipher AES.new(key, AES.MODE_CTR, counterctr) plaintext cipher.decrypt(ciphertext) print(plaintext.decode(utf-8))这里有几个细节你需要注意。Counter.new(128, ...)表示计数器本身是一个128位的大整数内部会按大端序转成16字节喂给AES。initial_value就是计数器的起点。如果题目单独给了Nonce通常是将Nonce左移若干位作为初始值或者把Nonce填充到128位。解密时用的计数器初始值必须和加密时完全一致差一个数字解出来的就是从第一个块开始的所有内容全部错乱。4.3 最常见的CTR坑计数器怎么都对齐不上我在作业里遇到了一种特别刁钻的情况加密方用的是16字节的Nonce直接当作counter初始值而我却按照8字节Nonce 8字节计数器拼接的方法去构造counter结果解出来只有前几个字节是对的后面的全错。这个问题等我自己把加密代码写了一遍才彻底搞明白。CTR的解密能不能解对完全取决于你是否能精确复现加密方的计数器序列。不同的库、不同的语言对此有不同的默认行为。比如OpenSSL里默认是128位计数器Nonce占前面后面从0递增。而Python的cryptography库则用32位计数器Nonce占96位。同一个密文用两种库去解解出来的结果完全不同因为计数器序列对不上。如果你在做跨语言的作业题比如加密文件是Java或C#生成的最好的办法是找到对方的加密源码确认它的counter构造方式找不到源码就多试几种常见的Nonce位置和计数器位宽组合。4.4 为什么CTR不需要处理Padding一个新手最容易搞错的地方很多人在解密CTR模式时会下意识地去找去掉填充的步骤因为CBC的经验让人形成了解密就必须去Padding的思维定式。CTR模式本质上是一个流密码模式它的加密单位不再是一个块而是一个字节。它先把计数器块加密生成密钥流密钥流的长度可以只取你需要的字节数不需要补齐16字节的倍数。因此如果明文是23个字节CTR生成的密钥流也只需要23个字节完全没有最后一块要补齐的烦恼。这意味着CTR模式的解密结果不需要任何Padding操作直接输出就是明文。如果你按CBC的习惯把解密结果的最后一个字节当成Padding长度去截断反而会把明文尾部正确的内容删掉。这是我这次作业里最典型的一个知识迁移错误——把上一个知识点的流程强行套到新知识点上。5. 从解出明文到获得满分验证、边界测试和作业之外的安全思考解出明文不代表可以交作业了。老师最爱在作业里埋的几个考察点恰恰是你以为已经完成之后才会暴露的。5.1 解密结果正确性的判断方式解密出的明文如果是一段英文直接print()看看能不能读懂就行。但作业题有时候会给你一段经过编码的明文比如URL编码、JSON转义甚至密文本身是某一轮加密后的中间结果需要二次解密。我建议你养成一个习惯解密后先把结果的前32个字节打印成repr形式看一眼确认没有诡异的控制字符。如果是Base64密文解出的结果又是Base64别急着以为解错了那可能是题目故意设计的加密链先用AES加密再对密文做Base64。你解密后得到的明文应该再Base64解码一次才能看到真正的原文。5.2 CBC和CTR的边界条件与安全性对比作业做完之后我把两种模式放到一起做了个对比这对面试和后续课程都很有用对比维度CBCCTR加密过程明文块与上一密文块XOR再进AES计数器值进AES生成密钥流再与明文XOR解密过程AES解密结果与上一密文块XOR密钥流与密文XOR是否依赖前序数据块是前一块错了影响当前块否每一块独立解是否支持并行解密可以加密不行加密和解密都可以并行Padding需要PKCS7补齐不需要主要安全风险IV不能重复且必须不可预测Nonce不能重复用否则密钥流相同容错性密文某位损坏影响本块和下一块密文某位损坏只影响对应位为什么CBC的IV必须不可预测如果IV是可预测的攻击者可以在加密前故意构造选定的明文诱导出泄露信息的密文这就是历史上著名的BEAST攻击的基本思路。在作业环境里你可能不需要考虑这个但如果将来做实际系统IV最好是密码学安全的随机数。为什么CTR的Nonce绝对不能重复因为CTR的密钥流只由Nonce和计数器决定。如果两条消息用了同一个Nonce它们生成的密钥流完全相同。攻击者拿到两个密文后把它们XOR密钥流就被抵消掉了剩下的就是两个明文的XOR。基于这个信息明文的统计特征可以被还原出来。所以CTR模式里唯一Nonce是一条铁律。5.3 复盘这一周我的踩坑时间线最后说一下实操时间线的复盘供参考。第一天我花了很多时间在编码转换上因为题目给的是Hex密文我没注意直接当成Base64去解结果全乱。第二天解决了CBC的Padding问题原因是没搞懂PKCS7规则把最后一个字节的值当成了真正的数据导致明文尾部多出16个填充字符。第三天的大部分时间花在CTR的计数器构造上前文已经详细说了。如果你也卡在某一步过不去千万不要死磕。把三个变量列出来——密钥、IV/Nonce、编码格式——然后每次只改一个变量做交叉测试基本上能在半小时内定位问题。我自己的排查顺序是先确认密钥长度和字节表示再确认IV/Nonce长度最后确认密文的Base64/Hex格式。这三者全部正确的前提下CBC模式一定能解出正常文本。5.4 一个额外的扩展思路把作业升级成验证器作业做完后如果你还有余力我强烈建议你写一个小的验证工具自己随机生成明文用随机密钥和IV分别做CBC和CTR加密然后用自己的解密函数去解看能否还原。这个操作看似简单但它能暴露加密方向和解密方向之间的不对称问题——尤其是Counter初值、IV生成方式、Padding填充规则这三处只要有一处不对称自测立刻失败。另外提醒一点和CBC不同的是CTR解密时即使你顺序颠倒了密文块也能解出所有对应的错位明文因为每块解密完全独立。这个特性不是为了让你玩乱序解密而是解释了为什么CTR可以直接在随机访问的存储系统比如加密磁盘上使用——想读第100块就能只解第100块无需解密前99块。这是CBC做不到的。作业如果问为什么磁盘加密偏好用CTR/XTS答案就在这个随机访问需求上。这次作业给我的最大收获并不是我会解密了而是认识到密码学里真正容易出错的地方永远是模式设计带来的边界效应而不是AES数学本身有多难。你不需要能默写轮函数但你必须清楚每一条XOR、每一次拼接、每一个Padding字节的来龙去脉才能在拿到任何一份陌生格式的密文时从容下手。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑