从零实现AES核心算法:深入CTF密码学实战与Python代码剖析
1. 项目概述与核心价值最近在带新人入门CTFCapture The Flag时发现很多朋友对AES高级加密标准这个“老熟人”既熟悉又陌生。熟悉是因为在各种Web、Crypto密码学题目里它频繁登场陌生则是因为一旦题目稍微绕点弯子比如给个错误的IV初始化向量或者玩点CBC密码分组链接模式的比特翻转大家就容易卡壳。正好我在复盘一个经典的Educoder平台上的AES题目时觉得其设计非常精妙它没有停留在“调用库函数加解密”的层面而是引导你亲手用Python复现AES加解密流程中的关键步骤比如字节代换SubBytes、行移位ShiftRows和列混淆MixColumns。这个过程对于真正理解AES的“内脏”是如何工作的以及后续在CTF中灵活应对各种变形攻击有着不可替代的价值。所以这篇内容我就想围绕“从CTF实战看AES”这个核心带你一起用Python复现那道题目的精髓。这不是一个简单的库函数调用教程而是一次深入到算法内部的“外科手术”。我们会从AES的基本概念和CTF中的常见考察点聊起然后一步步用代码构建S盒、实现状态矩阵的变换最后将这些模块组合起来去解决题目中设置的那些小关卡。无论你是正在入门CTF密码学方向还是已经有一定基础但想更透彻地理解AES亦或是单纯对用Python实现经典算法感兴趣我相信跟着走完这一趟你都会有实实在在的收获。我们不止于“知其然”更要“知其所以然”明白每一个步骤背后的数学原理和设计意图这样在赛场上遇到“魔改AES”时你才能心中有底快速找到突破口。2. AES核心概念与CTF常见考点解析在深入代码之前我们必须先打好地基清晰理解AES是什么以及在CTF竞赛中出题人最喜欢在它的哪些环节上“做文章”。2.1 AES算法简介与工作模式AES是一种对称分组密码算法意味着加密和解密使用同一把密钥。它处理的数据块大小固定为128位16字节密钥长度则可以是128、192或256位。我们最常打交道的也是CTF题目中最常见的是AES-128。AES的加密过程可以看作是对一个4x4的字节矩阵称为“状态State”进行多轮10, 12或14轮取决于密钥长度的变换。每一轮都包含四个基本步骤最后一轮略有不同字节代换SubBytes 通过一个非线性的S盒Substitution-box替换状态中的每一个字节。这是AES提供混淆Confusion的主要来源能有效抵抗线性密码分析。行移位ShiftRows 将状态矩阵的每一行进行循环左移第0行不移第1行左移1字节第2行左移2字节第3行左移3字节。这一步提供了扩散Diffusion。列混淆MixColumns 将状态矩阵的每一列视为在有限域GF(2^8)上的一个多项式并与一个固定的多项式进行模乘运算。这是AES中计算最复杂的一步提供了极强的列间扩散。轮密钥加AddRoundKey 将当前的状态与当前轮的轮密钥由初始密钥通过密钥扩展算法得到进行简单的按位异或XOR操作。在CTF中单纯的AES算法实现很少直接考察更多的是考察其工作模式和使用方式。最常见的工作模式包括ECB电子密码本 最简单的模式每个数据块独立加密。致命弱点是相同的明文块会加密成相同的密文块如果数据有规律比如图像密文中会保留明文的模式。CTF中常利用这一点通过观察密文块重复来推断信息或构造攻击。CBC密码分组链接 当前明文块在加密前会先与前一个密文块或初始向量IV进行XOR。这是CTF中的“明星考点”。因为解密时当前密文块先解密再与前一个密文块XOR得到明文。这导致了一个重要特性篡改前一个密文块C[i-1]会影响下一个明文块P[i]的解密结果而当前密文块C[i]的篡改会影响当前明文块P[i]的解密结果。这就是“CBC字节翻转攻击”的基础。CTR计数器 将计数器加密后与明文XOR。它实际上将分组密码变成了流密码。在CTF中可能考察其并行性、不可重复的计数器等特性。注意 很多CTF题目不会直接告诉你模式需要你根据提供的代码、交互方式或已知信息去判断。例如如果题目给了iv参数那大概率是CBC模式。2.2 CTF中AES题目的典型套路与破局点理解了基础我们来看看实战中题目怎么出又该怎么想。弱密钥或密钥硬编码 这是最简单的类型。题目源码或附件中直接包含了加密密钥。你的任务就是从源码、内存dump或逆向工程中找到它。解题关键在于代码审计或静态分析。CBC字节翻转攻击Bit Flipping 这是必考题型。题目通常提供一个加密后的数据如Cookie、Token给你并且服务端会解密后检查其中某个字段如admin:0。你无法得知密钥但可以修改密文或IV并提交服务端会解密并返回错误信息如Padding Error或部分结果。攻击的目标是通过精心修改前一个密文块中的特定字节使得在解密下一个明文块时能够将目标字符如从0变成1成功翻转。攻击原理 回忆CBC解密公式P[i] Decrypt(C[i]) XOR C[i-1]。如果我们想改变P[i]的第j个字节我们可以修改C[i-1]的第j个字节。因为解密过程是P‘[i][j] Decrypt(C[i])[j] XOR C‘[i-1][j]。如果我们令C‘[i-1][j] C[i-1][j] XOR P[i][j] XOR Target那么解密后得到的P‘[i][j]就会是我们想要的Target值。实操难点 通常需要结合Padding Oracle填充提示攻击。服务端在解密后发现填充不正确时会返回错误利用这个“差异”我们可以逐个字节地推断出中间值Decrypt(C[i])从而精确构造出我们想要的C‘[i-1]。Padding Oracle攻击 如上所述这种攻击不直接恢复密钥而是利用服务器对填充有效性的不同反馈错误信息、响应时间差异等来逐步推导出明文或构造出任意明文的合法密文。它是CBC模式下的一个经典攻击。ECB模式攻击 由于ECB模式缺乏扩散攻击方法多样。字典攻击/重排攻击 如果加密的内容由已知的、可重复的短块组成比如adminuser攻击者可以在不知道密钥的情况下通过观察密文块识别、删除或重排这些块来伪造消息。字节替换攻击 在某些场景下如加密userxxxroleuser攻击者可以截取roleadmin对应的密文块去替换自己密文中roleuser的块。侧信道与实现漏洞 较难的题目可能考察算法实现本身的漏洞比如查表实现的时序攻击但Python层面较少或者题目故意提供一个有缺陷的AES实现比如S盒错误、MixColumns系数错误要求你利用这个缺陷进行攻击或恢复密钥。我们这次要复现的Educoder题目其高明之处在于它避开了上述这些“应用层”的攻击而是深入到算法内部考察你对AES核心变换步骤的理解和实现能力。它要求你补全SubBytes、ShiftRows、MixColumns等函数的代码。这相当于让你亲手搭建起AES的引擎对于后续理解更高级的攻击比如差分分析、线性分析的概念基础有莫大好处。3. 用Python构建AES核心模块现在我们开始动手。我们将使用纯Python仅依赖copy等基础库来实现AES-128加解密的核心步骤。我们会遵循自底向上的顺序先实现最基础的有限域运算和S盒再搭建状态矩阵变换函数。3.1 有限域GF(2^8)上的运算基础AES的MixColumns和S盒生成都依赖于在伽罗瓦域GF(2^8)上的运算。这个域可以理解为所有系数在0或1上的、次数小于8的多项式的集合。一个字节8位b7b6b5b4b3b2b1b0就对应一个多项式。例如字节0x57二进制01010111对应多项式x^6 x^4 x^2 x 1。我们需要实现两个核心操作加法 在GF(2^8)中加法就是按位异或XOR。因为系数模2110。乘法 乘法是模一个不可约多项式m(x) x^8 x^4 x^3 x 1对应十六进制0x11b的多项式乘法。def gf256_add(a, b): GF(2^8)加法即异或。 return a ^ b def gf256_mul(a, b): GF(2^8)乘法模不可约多项式0x11b。 p 0 for _ in range(8): if b 1: # 如果b的最低位是1 p ^ a # 则将a加到p上异或 high_bit_set a 0x80 # 判断a的最高位是否为1 a 1 # a左移一位 if high_bit_set: a ^ 0x11b # 如果溢出则模0x11b b 1 # b右移一位 return p # 测试一下 print(f0x57 0x83 {hex(gf256_add(0x57, 0x83))}) # 应为 0xd4 print(f0x57 * 0x83 {hex(gf256_mul(0x57, 0x83))}) # 应为 0xc1实操心得 有限域乘法的这个实现是经典的“移位-判断-异或”方法。理解它的关键在于多项式乘法a(x) * b(x)可以分解为a(x)乘以b(x)的每一位。循环中我们检查b的最低位如果是1就把当前的a累加到结果p上。然后a左移相当于乘以x如果最高位溢出即原a的x^7系数为1就需要减去在GF(2)上就是异或不可约多项式m(x)即0x11b。这个过程和整数乘法的竖式计算很像只不过加法换成了XOR进位规则变成了模0x11b。3.2 S盒Substitution-box的生成与使用AES的S盒是一个16x16的查找表用于将输入字节xy高4位为x低4位为y映射到一个输出字节。它由两个变换复合而成首先在GF(2^8)上求乘法逆元0映射到自身然后进行一个仿射变换。def generate_sbox(): 生成AES的S盒。 sbox [0] * 256 # 首先计算GF(2^8)上的乘法逆元 for i in range(256): if i 0: # 0的逆元定义为0 inv 0 else: # 在GF(2^8)中一个非零元素的逆元是满足 a * inv 1 的元素 # 由于域很小我们可以暴力查找。更高效的方法是用扩展欧几里得算法这里为清晰起见用暴力法。 inv 0 for j in range(256): if gf256_mul(i, j) 1: inv j break # 对逆元进行仿射变换 b inv # 仿射变换: b M * b c其中M是一个8x8的矩阵c是常数0x63 # 可以展开为位运算 b_transformed b for _ in range(4): # 这个循环是仿射变换矩阵乘法的展开形式 b_transformed ((b_transformed 1) 0xFF) ^ ((b_transformed 7) * 0x1B) b_transformed ^ 0x63 sbox[i] b_transformed 0xFF return sbox def generate_inv_sbox(sbox): 根据S盒生成逆S盒。 inv_sbox [0] * 256 for i in range(256): inv_sbox[sbox[i]] i return inv_sbox # 生成并验证 SBOX generate_sbox() INV_SBOX generate_inv_sbox(SBOX) print(fSBOX[0x00] {hex(SBOX[0])}) # 应为 0x63 print(fSBOX[0x53] {hex(SBOX[0x53])}) # 可以查标准表验证 print(fINV_SBOX[{hex(SBOX[0x53])}] {hex(INV_SBOX[SBOX[0x53]])}) # 应返回0x53注意事项 在实际解题或工程中我们几乎不会在运行时动态计算S盒而是直接使用预定义好的常量表因为这样速度极快。这里手动生成是为了理解其数学本质。Educoder的题目很可能就是给你一个不完整的S盒生成函数让你补全或者直接给你S盒数组让你应用。3.3 状态矩阵与基础变换实现AES处理的数据块是16字节在内部表示为4x4的状态矩阵按列优先顺序排列。即字节B0, B1, B2, B3, B4, ... B15对应矩阵[ B0, B4, B8, B12 ] [ B1, B5, B9, B13 ] [ B2, B6, B10, B14 ] [ B3, B7, B11, B15 ]我们先实现一些辅助函数来处理这个状态矩阵。def bytes_to_state(data): 将16字节的数组转换为4x4状态矩阵列优先。 if len(data) ! 16: raise ValueError(Input data must be exactly 16 bytes long.) state [[0 for _ in range(4)] for _ in range(4)] for i in range(4): for j in range(4): state[j][i] data[i * 4 j] # 注意列优先所以行索引j变化快 return state def state_to_bytes(state): 将4x4状态矩阵转换回16字节数组列优先。 data [0] * 16 for i in range(4): for j in range(4): data[i * 4 j] state[j][i] return bytes(data) def print_state(state, label): 以十六进制形式打印状态矩阵便于调试。 if label: print(label) for row in state: print( .join([f{x:02x} for x in row])) print()现在实现核心的变换步骤def sub_bytes(state, sbox): 字节代换使用提供的S盒替换状态矩阵中的每个字节。 for i in range(4): for j in range(4): state[i][j] sbox[state[i][j]] return state def inv_sub_bytes(state, inv_sbox): 逆字节代换。 return sub_bytes(state, inv_sbox) # 逻辑相同只是S盒不同 def shift_rows(state): 行移位第0行不移第1行左移1位第2行左移2位第3行左移3位。 # 第1行 state[1][0], state[1][1], state[1][2], state[1][3] state[1][1], state[1][2], state[1][3], state[1][0] # 第2行 state[2][0], state[2][1], state[2][2], state[2][3] state[2][2], state[2][3], state[2][0], state[2][1] # 第3行 state[3][0], state[3][1], state[3][2], state[3][3] state[3][3], state[3][0], state[3][1], state[3][2] return state def inv_shift_rows(state): 逆行移位即右移。 # 第1行右移1位 state[1][0], state[1][1], state[1][2], state[1][3] state[1][3], state[1][0], state[1][1], state[1][2] # 第2行右移2位 state[2][0], state[2][1], state[2][2], state[2][3] state[2][2], state[2][3], state[2][0], state[2][1] # 第3行右移3位 state[3][0], state[3][1], state[3][2], state[3][3] state[3][1], state[3][2], state[3][3], state[3][0] return stateMixColumns是最复杂的一步。它把状态的每一列看作GF(2^8)上的一个四次多项式并与固定多项式c(x) 0x03*x^3 0x01*x^2 0x01*x 0x02进行模x^4 1乘法。这可以表示为一个矩阵乘法[02 03 01 01] [s0,c] [01 02 03 01] * [s1,c] [01 01 02 03] [s2,c] [03 01 01 02] [s3,c]其中乘法是GF(2^8)乘法。我们可以为每一列单独计算。def mix_columns(state): 列混淆变换。 new_state [[0 for _ in range(4)] for _ in range(4)] # 固定矩阵 mix_matrix [ [0x02, 0x03, 0x01, 0x01], [0x01, 0x02, 0x03, 0x01], [0x01, 0x01, 0x02, 0x03], [0x03, 0x01, 0x01, 0x02] ] for col in range(4): # 取出当前列 col_vec [state[row][col] for row in range(4)] for row in range(4): # 计算点积 val 0 for i in range(4): val ^ gf256_mul(mix_matrix[row][i], col_vec[i]) new_state[row][col] val return new_state def inv_mix_columns(state): 逆列混淆变换。使用逆矩阵。 new_state [[0 for _ in range(4)] for _ in range(4)] # 逆矩阵 inv_mix_matrix [ [0x0e, 0x0b, 0x0d, 0x09], [0x09, 0x0e, 0x0b, 0x0d], [0x0d, 0x09, 0x0e, 0x0b], [0x0b, 0x0d, 0x09, 0x0e] ] for col in range(4): col_vec [state[row][col] for row in range(4)] for row in range(4): val 0 for i in range(4): val ^ gf256_mul(inv_mix_matrix[row][i], col_vec[i]) new_state[row][col] val return state核心原理解读 为什么MixColumns的逆矩阵系数这么复杂0x0e, 0x0b等这是因为在GF(2^8)上c(x)的逆多项式d(x)的系数就是这些值。c(x) * d(x) 1 mod (x^41)。这个逆矩阵确保了MixColumns和InvMixColumns互为逆操作是解密正确性的关键。在CTF中如果题目要求你实现解密而你只记得加密的MixColumns那么你必须能正确实现或查找这个逆矩阵。4. 密钥扩展与完整加解密流程组装有了核心变换我们还需要从初始密钥生成每一轮使用的轮密钥。对于AES-128我们需要11个轮密钥第0轮是初始密钥加然后10轮加密每轮一个。4.1 密钥扩展算法实现密钥扩展算法也涉及S盒和轮常数Rcon。def key_expansion(key): 将16字节的密钥扩展为44个字176字节的轮密钥列表。 # AES-128密钥为4个字16字节 key_size 16 # 轮数 n_rounds 10 # 总字数为 (轮数1) * 4 44 expanded_key [0] * (4 * (n_rounds 1)) # 轮常数表 Rcon[i] [rc(i), 0x00, 0x00, 0x00] rcon [0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36] # 1. 将初始密钥拷贝到扩展密钥的前4个字 for i in range(4): expanded_key[i] (key[4*i] 24) | (key[4*i1] 16) | (key[4*i2] 8) | key[4*i3] # 2. 生成后续的字 for i in range(4, 4 * (n_rounds 1)): temp expanded_key[i-1] if i % 4 0: # 对temp进行RotWord、SubWord、异或Rcon操作 # RotWord: 循环左移一个字节 temp ((temp 8) 0xFFFFFFFF) | (temp 24) # SubWord: 对每个字节应用S盒 byte0 (temp 24) 0xFF byte1 (temp 16) 0xFF byte2 (temp 8) 0xFF byte3 temp 0xFF temp (SBOX[byte0] 24) | (SBOX[byte1] 16) | (SBOX[byte2] 8) | SBOX[byte3] # 异或Rcon temp ^ (rcon[i//4 - 1] 24) # 生成新字 W[i] W[i-4] XOR temp expanded_key[i] expanded_key[i-4] ^ temp # 将字数组转换为轮密钥列表每轮一个状态矩阵16字节 round_keys [] for round in range(n_rounds 1): round_key [[0 for _ in range(4)] for _ in range(4)] base round * 4 for i in range(4): word expanded_key[base i] round_key[0][i] (word 24) 0xFF round_key[1][i] (word 16) 0xFF round_key[2][i] (word 8) 0xFF round_key[3][i] word 0xFF round_keys.append(round_key) return round_keys4.2 轮密钥加与完整加密/解密单轮实现def add_round_key(state, round_key): 轮密钥加状态矩阵与轮密钥矩阵按位异或。 for i in range(4): for j in range(4): state[i][j] ^ round_key[i][j] return state现在我们可以组装完整的AES-128加密和解密函数了。注意解密是加密的逆过程步骤顺序相反并且InvMixColumns需要使用逆矩阵。def aes_encrypt_block(plaintext, key): AES-128加密一个16字节的数据块。 # 1. 密钥扩展 round_keys key_expansion(key) # 2. 初始状态 state bytes_to_state(plaintext) # 3. 初始轮密钥加 state add_round_key(state, round_keys[0]) # 4. 进行9轮标准轮函数 for round_num in range(1, 10): state sub_bytes(state, SBOX) state shift_rows(state) state mix_columns(state) state add_round_key(state, round_keys[round_num]) # 5. 最后一轮无MixColumns state sub_bytes(state, SBOX) state shift_rows(state) state add_round_key(state, round_keys[10]) # 6. 返回密文 ciphertext state_to_bytes(state) return ciphertext def aes_decrypt_block(ciphertext, key): AES-128解密一个16字节的数据块。 round_keys key_expansion(key) state bytes_to_state(ciphertext) # 解密第一轮对应加密的最后一轮 state add_round_key(state, round_keys[10]) state inv_shift_rows(state) state inv_sub_bytes(state, INV_SBOX) # 中间9轮 for round_num in range(9, 0, -1): state add_round_key(state, round_keys[round_num]) state inv_mix_columns(state) state inv_shift_rows(state) state inv_sub_bytes(state, INV_SBOX) # 最后轮密钥加 state add_round_key(state, round_keys[0]) plaintext state_to_bytes(state) return plaintext4.3 测试我们的实现让我们用一个标准测试向量来验证我们的实现是否正确。# 来自NIST的测试向量 AES-128 # Key: 2b7e151628aed2a6abf7158809cf4f3c # Plaintext: 3243f6a8885a308d313198a2e0370734 # Ciphertext: 3925841d02dc09fbdc118597196a0b32 key bytes.fromhex(2b7e151628aed2a6abf7158809cf4f3c) plaintext bytes.fromhex(3243f6a8885a308d313198a2e0370734) expected_ciphertext bytes.fromhex(3925841d02dc09fbdc118597196a0b32) print(测试AES-128加密...) ciphertext aes_encrypt_block(plaintext, key) print(f计算密文: {ciphertext.hex()}) print(f期望密文: {expected_ciphertext.hex()}) print(f加密测试: {通过 if ciphertext expected_ciphertext else 失败}) print(\n测试AES-128解密...) decrypted aes_decrypt_block(ciphertext, key) print(f解密结果: {decrypted.hex()}) print(f原始明文: {plaintext.hex()}) print(f解密测试: {通过 if decrypted plaintext else 失败})如果一切正确你应该能看到“通过”的输出。这证明我们从头实现的AES核心逻辑是正确的。5. 模拟Educoder题目实战与扩展思考现在我们有了完整的AES实现可以回过头来模拟Educoder那类题目的解题过程。题目通常不会让你实现整个加密而是聚焦于某个或某几个关键函数。5.1 题目场景模拟与分步破解假设题目给出如下框架和提示“请补全以下AES加密函数中的sub_bytes和mix_columns函数使得程序能够正确加密给定的明文。”题目可能会提供一个不完整的Python文件以及一组测试用例输入、密钥、期望输出。你的任务就是像我们上面做的那样根据AES标准算法描述补全缺失的函数。解题步骤理解上下文 仔细阅读题目给出的代码框架确定它使用的是AES-128以及数据存储方式很可能是4x4状态矩阵列优先。定位缺失部分 找到需要补全的函数查看其输入输出格式。例如mix_columns函数接收一个状态矩阵返回处理后的状态矩阵。回忆算法细节对于sub_bytes 遍历状态矩阵的每个字节通过S盒题目可能直接给出S盒列表SBOX进行替换。对于mix_columns 这是难点。需要回忆或推导那个固定的矩阵[ [2,3,1,1], ... ]并实现GF(2^8)上的乘法题目可能提供gf_mul(a,b)函数也可能需要你自己写。实现与测试 在本地或题目环境中补全代码用题目给的简单测试用例验证。例如先测试sub_bytes对一个已知状态的处理结果是否正确。提交验证 将补全的代码提交到平台平台会用更全面的测试用例来验证你的实现是否正确。一个具体的例子 题目可能只给了shift_rows让你补全mix_columns。你补全后发现加密结果还是不对。这时你需要检查你的mix_columns是按列处理吗GF(2^8)乘法实现了吗矩阵系数对吗状态矩阵的索引[row][col]对吗通过打印中间状态与标准算法中间结果对比可以快速定位问题。5.2 从底层实现到高层攻击的思维跨越亲手实现一遍AES后你对CTF中那些AES相关题目的理解会深刻得多。面对CBC字节翻转 你现在清楚地知道密文块在解密时会先经过AES解密算法包括InvShiftRows,InvSubBytes,InvMixColumns,AddRoundKey变成一个中间状态再与前一密文块XOR。当你尝试翻转前一密文块的某个字节时你实际上是在影响这个XOR操作。因为AES解密算法是确定的所以Decrypt(C[i])是固定的。你的攻击就是在解这个方程C‘[i-1] C[i-1] XOR P[i] XOR P‘[i]。理解了AES内部结构你会更清楚这个“固定值”是怎么来的。面对Padding Oracle 你会明白为什么错误的填充会导致服务器返回错误。因为PKCS#7等填充规则在解密后会被验证。你也更能理解攻击如何利用服务器的错误响应逐字节地推算出Decrypt(C[i])进而推算出明文或伪造密文。面对魔改AES 这是最体现价值的场景。题目可能修改了S盒或者调整了MixColumns的矩阵甚至改变了行移位的规则。如果你对标准AES的每一步都了如指掌你就能快速识别出哪里被改了。然后你可以根据修改后的算法要么尝试寻找新的弱点比如S盒线性度变高要么就老老实实用修改后的算法去加密/解密来解题。你甚至可以直接把题目给的“魔改”函数替换掉我们上面实现的标准函数快速搭建一个解题工具。5.3 常见问题与调试技巧实录在实现和解题过程中你肯定会遇到各种bug。下面是我踩过的一些坑和解决方法状态矩阵索引搞反 这是最常见的问题。AES标准文档和很多代码使用state[row][col]且是列优先存储。但在循环时i和j哪个代表行哪个代表列很容易混淆。一个记忆方法是外循环i遍历列0到3内循环j遍历行0到3那么state[j][i]就是正确的列优先存储。在mix_columns中我们固定一列col然后计算该列所有行row的新值。GF(2^8)乘法错误 自己实现的gf256_mul函数一定要用标准测试向量验证。一个常见的错误是在判断最高位和模0x11b时出错。记住0x11b的二进制是100011011它对应多项式x^8 x^4 x^3 x 1。当左移后a的最高位第8位即0x100为1时才需要异或0x11b。S盒或逆S盒不匹配 确保加密用SBOX解密用INV_SBOX。并且要验证SBOX[INV_SBOX[x]] x对所有x成立。如果题目直接给S盒数组要确认它是16x16的一维列表索引是(row4)|col。最后一轮忘记省略MixColumns 加密的第1到第9轮有MixColumns第10轮没有。解密则相反第1轮对应加密第10轮没有InvMixColumns。这个顺序千万不能错。密钥扩展的轮常数索引错误Rcon[i]中的i是从1开始的对应第一轮扩展。在代码中我们使用rcon[i//4 - 1]因为i是字索引每4个字即每轮用一次Rcon。字节序问题 当密钥或明文以十六进制字符串给定时bytes.fromhex()是正确的。但如果题目给的是整数数组或别的格式要小心处理字节顺序。我们的实现假设输入是字节串bytes object内部按大端序处理字(b024)|(b116)...。调试建议单元测试 对每个小函数gf256_mul,sub_bytes等单独编写测试与已知值对比。打印中间状态 在加密函数中每轮结束后打印state与标准算法中间结果对比网上可以找到AES的逐步计算示例。使用已知库交叉验证 用pycryptodome或cryptography库加密同样的数据对比结果。但注意这些库通常处理的是完整的数据和模式如CBC要确保你比较的是单个分块的ECB模式结果。利用在线工具 有一些网站提供AES每一步的详细计算过程可以输入你的中间值进行比对。最后我想说的是密码学在CTF中之所以有趣就是因为它结合了严谨的数学和灵活的攻击思维。通过这次从零实现AES核心步骤的练习我希望你获得的不仅仅是一段可以运行的代码更是一种“透视”加密算法的能力。下次再遇到AES相关的黑盒或白盒题目你可以自信地打开调试器或者拿起笔从最基本的S盒和行移位开始分析。真正的安全高手都是从理解基础原理开始的。