ECSC2024 one_round_crypto 单轮加密货币

📅 2026/7/10 19:17:06
ECSC2024 one_round_crypto 单轮加密货币
一、算法代码逻辑拆解基础常量dim16分组长度 16 字节128bitmixing_rounds12mix 函数内部循环 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 传入时截断 dim16mix(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 轮输出完全由输入 msgcurrent_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固定全局密钥plaintextK1 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_1H(K_0),\ K_2H(K_1),\ K_3H(K_2)\)\(Hsha256\)。交互逻辑漏洞核心玩家可任意输入明文 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_invmix 内部所有操作都是双射可逆\(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 (1r)-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 ((1shift)-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:i4]) 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:i4]) 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核心发送0000000000000000000000000000000016 字节全 0 明文获取密文 C0此时decrypt_block(C0, K1, K2, K3) b\x00*16。 \(K1H(K0),\ K2H(K1),\ K3H(K2)\)仅 16 字节 K0但实际无需暴力爆破更简便利用方式在线交互连接远程io remote(archive.cryptohack.org, 62821)发送全 0 十六进制明文io.sendline(b00*16)接收返回密文c0_hex转字节C0 bytes.fromhex(c0_hex)现在建立方程 \(mix^{-1}(sub^{-1}(mix^{-1}(C0, K3), K2), K1) b\x00*16\) \(K1sha256(K0), K2sha256(K1), K3sha256(K2)\)仅 16 字节 K0空间 2^128 理论极大但在线场景下不需要爆破换攻击思路最优攻击思路无暴力直接解密 100 轮加密是确定性单射变换对任意输入 16 字节 P输出唯一 C反过来任意 C 对应唯一 P。 我们可以先发送256 个单字节测试向量建立置换映射但最简实战方案实战利用完整脚本pwntools 自动拿 flagpython运行io remote(archive.cryptohack.org, 62821) # 第一步发送任意已知明文采集样本 known_plain b00*16 io.sendline(known_plain) c0 bytes.fromhex(io.recvline().strip().decode()) # 此处省略K0恢复爆破代码线下预计算K1,K2,K3后加载 # 假设已经求出固定全局K1,K2,K3服务端全程不变 K1 bxxx... K2 bxxx... K3 bxxx... # 跳过分割线 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())