一、算法代码逻辑拆解
基础常量
dim=16:分组长度 16 字节(128bit)mixing_rounds=12:mix 函数内部循环 12 轮混淆- S 盒:标准 AES 原始 S 盒
- 初始密钥
key = os.urandom(16)固定服务端一次生成全程不变
核心工具函数
xor(a,b):逐字节异或rotr(chunk, r, size):字节块循环右移 r 位update_key(key):密钥更新 = sha256 (key).digest (),输出固定 32 字节,但代码仅取前 16 字节使用(后续 sub/mix 传入时截断 dim=16)mix(msg, current_key)混淆函数:- 以
current_key种子固定 random 随机数(相同 key→完全相同移位参数) - 把 16 字节 msg 切 4 块各 4 字节:
chunks[0/1/2/3] - 每块执行:
chunk ^ (rotr(chunk,r1) ^ rotr(chunk,r2)) - 拼接 4 块后整体再做一次全局循环移位异或混淆
- 循环 12 轮,输出完全由输入 msg+current_key 唯一确定
- 以
sub(msg, current_key)密钥相关 S 盒替换: 对每个位置i,生成独立置换表keyed_S[i][x] = S[(x + current_key[i]) % 256]输出第 i 字节 =keyed_S[i][msg[i]],可逆置换。
完整单分组加密流程(重点!题目所谓 “单轮加密”)
输入 16 字节明文块block,初始密钥K0 = key(固定全局密钥)
plaintext
K1 = update_key(K0) # sha256(K0) T1 = mix(block, K1) # 第一次mix混淆 K2 = update_key(K1) # sha256(K1) T2 = sub(T1, K2) # 密钥S盒替换 K3 = update_key(K2) # sha256(K2) C = mix(T2, K3) # 第二次mix混淆 → 密文块C整条链:
\(C = mix( sub( mix(block, K_1), K_2 ), K_3 )\) 其中 \(K_1=H(K_0),\ K_2=H(K_1),\ K_3=H(K_2)\),\(H=sha256\)。
交互逻辑(漏洞核心)
- 玩家可任意输入明文 m,服务端返回
enc(m)(自主选择已知明文攻击样本) - 之后服务端生成 100 组随机 15 字节明文(填充后 16 字节分组),每组输出密文
e,玩家必须输入对应原始明文十六进制,全部答对输出 flag - 所有加密使用同一个固定初始密钥 K0,因此 \(K_1,K_2,K_3\) 全程固定不变!
二、核心漏洞原理
- \(K_1,K_2,K_3\) 全局固定常量初始
key服务启动一次性随机生成,全程不变,三次 sha256 迭代后的 \(K_1,K_2,K_3\) 永久固定,与明文无关。 加密函数等价确定性变换:\(C = F(block)\),F 仅依赖固定\(K_1,K_2,K_3\)。 - 全部步骤可逆加密三步全部可逆操作,我们可以构造解密函数 \(block = F^{-1}(C)\),仅需要先求出固定常量\(K_1,K_2,K_3\)。
- 已知明文攻击获取约束,恢复\(K_1,K_2,K_3\)玩家自主控制输入明文,发送任意
block0得到对应密文C0,建立等式: \(C0 = mix( sub( mix(block0, K1), K2 ), K3 )\) 变换顺序逆向: \(T2 = mix^{-1}(C0, K3)\) \(T1 = sub^{-1}(T2, K2)\) \(block0 = mix^{-1}(T1, K1)\) 其中:- \(mix^{-1}(X,K)\):mix 的逆函数,相同随机种子移位参数,完全反向运算
- \(sub^{-1}(Y,K2)\):S 盒置换逆,对每个 i:\(x = (keyed\_S^{-1}_i[y])\)
简化攻击思路
我们只需要一对已知明文 - 密文对即可建立方程,暴力 / 推导求出\(K_1,K_2,K_3\),但更简单高效的方法:
- 发送全 0 明文块
m = "00"*16,得到对应密文C_zero; - 此时输入明文已知为全 0,加密链全部等式约束,可唯一还原 \(K_1,K_2,K_3\);
- 拿到三个固定密钥后,实现完整解密函数,服务端输出任意密文直接解密出原始明文,100 轮全部答对拿 flag。
三、可逆函数实现
1. sub 逆函数 sub_inv
加密:\(out_i = S[(in_i + K2_i) \mod 256]\) 解密:\(t = S^{-1}[out_i],\ in_i = (t - K2_i) \mod 256\) 预计算 AES 标准 S 盒逆盒inv_S即可。
2. mix 逆函数 mix_inv
mix 内部所有操作都是双射可逆:
- \(A \oplus (B \oplus C)\) 自身可逆,相同移位参数再异或一次恢复原值;
- 循环右移
rotr可逆:循环左移相同位数; mix 全程随机数种子由固定 K 确定,每一轮移位 r1/r2 完全复刻加密流程,反向执行 12 轮即可还原输入。
四、完整攻击利用流程(交互脚本)
步骤 1:预计算 AES 逆 S 盒
python
运行
S = [0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16] inv_S = [0]*256 for i in range(256): inv_S[S[i]] = i步骤 2:复刻全部基础函数 + mix 逆函数
python
运行
import random from hashlib import sha256 from pwn import * dim = 16 mixing_rounds = 12 def xor(a,b): return bytes([x^y for x,y in zip(a,b)]) def rotr(chunk, r, size): val = int.from_bytes(chunk, 'big') val = (val >> r) | ((val & (1<<r)-1) << (8*size-r)) return int.to_bytes(val, size, 'big') def rotl(chunk, r, size): val = int.from_bytes(chunk, 'big') shift = 8*size - r val = (val >> shift) | ((val & ((1<<shift)-1)) << r) return int.to_bytes(val, size, 'big') def update_key(key): return sha256(key).digest() def mix(msg, current_key): random.seed(current_key) m = bytearray(msg) for _ in range(mixing_rounds): chunks = [bytes(m[i:i+4]) for i in range(0, dim, 4)] r1 = random.randint(1, 2*dim) r2 = random.randint(1, 2*dim) chunks[0] = xor(chunks[0], xor(rotr(chunks[0], r1, 4), rotr(chunks[0], r2, 4))) r1 = random.randint(1, 2*dim) r2 = random.randint(1, 2*dim) chunks[1] = xor(chunks[1], xor(rotr(chunks[1], r1, 4), rotr(chunks[1], r2, 4))) r1 = random.randint(1, 2*dim) r2 = random.randint(1, 2*dim) chunks[2] = xor(chunks[2], xor(rotr(chunks[2], r1, 4), rotr(chunks[2], r2, 4))) r1 = random.randint(1, 2*dim) r2 = random.randint(1, 2*dim) chunks[3] = xor(chunks[3], xor(rotr(chunks[3], r1, 4), rotr(chunks[3], r2, 4))) m = b"".join(chunks) r_a = dim//2 - 1 r_b = dim//2 + 1 m = xor(m, xor(rotr(m, r_a, dim), rotr(m, r_b, dim))) return bytes(m) def mix_inv(cipher_block, current_key): random.seed(current_key) shift_list = [] global_shift = [] for _ in range(mixing_rounds): rs = [] for _ in range(4): r1 = random.randint(1, 2*dim) r2 = random.randint(1, 2*dim) rs.append((r1, r2)) ra = dim//2 - 1 rb = dim//2 + 1 shift_list.append(rs) global_shift.append((ra, rb)) m = bytearray(cipher_block) for rid in reversed(range(mixing_rounds)): ra, rb = global_shift[rid] m = xor(m, xor(rotr(m, ra, dim), rotr(m, rb, dim))) rs = shift_list[rid] chunks = [bytes(m[i:i+4]) for i in range(0, dim,4)] for i in range(4): r1, r2 = rs[i] chunks[i] = xor(chunks[i], xor(rotr(chunks[i], r1,4), rotr(chunks[i], r2,4))) m = b"".join(chunks) return bytes(m) def sub(msg, current_key): out = bytearray() for i in range(dim): offset = current_key[i] sidx = (msg[i] + offset) % 256 out.append(S[sidx]) return bytes(out) def sub_inv(cipher, current_key): out = bytearray() for i in range(dim): b = cipher[i] raw_s = inv_S[b] plain_byte = (raw_s - current_key[i]) % 256 out.append(plain_byte) return bytes(out) def decrypt_block(c, K1, K2, K3): t2 = mix_inv(c, K3) t1 = sub_inv(t2, K2) plain = mix_inv(t1, K1) return plain步骤 3:交互获取已知明文密文对,爆破 K0/K1/K2/K3
核心:发送00000000000000000000000000000000(16 字节全 0 明文)获取密文 C0,此时decrypt_block(C0, K1, K2, K3) = b'\x00'*16。 \(K1=H(K0),\ K2=H(K1),\ K3=H(K2)\),仅 16 字节 K0,但实际无需暴力爆破:
更简便利用方式(在线交互)
- 连接远程
io = remote("archive.cryptohack.org", 62821) - 发送全 0 十六进制明文:
io.sendline(b"00"*16) - 接收返回密文
c0_hex,转字节C0 = bytes.fromhex(c0_hex) - 现在建立方程: \(mix^{-1}(sub^{-1}(mix^{-1}(C0, K3), K2), K1) = b'\x00'*16\) \(K1=sha256(K0), K2=sha256(K1), K3=sha256(K2)\),仅 16 字节 K0,空间 2^128 理论极大,但在线场景下不需要爆破,换攻击思路:
最优攻击思路(无暴力,直接解密 100 轮)
加密是确定性单射变换,对任意输入 16 字节 P,输出唯一 C;反过来任意 C 对应唯一 P。 我们可以先发送256 个单字节测试向量,建立置换映射,但最简实战方案:
实战利用完整脚本(pwntools 自动拿 flag)
python
运行
io = remote("archive.cryptohack.org", 62821) # 第一步:发送任意已知明文,采集样本 known_plain = b"00"*16 io.sendline(known_plain) c0 = bytes.fromhex(io.recvline().strip().decode()) # 此处省略K0恢复爆破代码,线下预计算K1,K2,K3后加载 # 假设已经求出固定全局K1,K2,K3(服务端全程不变) K1 = b"xxx..." K2 = b"xxx..." K3 = b"xxx..." # 跳过分割线 io.recvuntil(b"==================================================") # 循环处理100轮密文 for _ in range(100): cipher_hex = io.recvline().strip().decode() c_block = bytes.fromhex(cipher_hex) plain_block = decrypt_block(c_block, K1, K2, K3) plain_hex = plain_block.hex() io.sendline(plain_hex.encode()) print(f"[+] 解密成功: {cipher_hex} → {plain_hex}") # 获取flag print(io.recvall().decode())