🔐 2023年熵密杯 Flag1 精讲:自定义对称加密算法逆向
阅读指引:本文是"熵密杯历年真题精析"系列的第二篇。Flag1 是2023年熵密杯决赛阶段的第一道关卡,考察的是对称加密算法的逆向分析能力——给定加密源码和密文,要求还原出原始明文。这道题的算法设计虽然算是入门级,但完整覆盖了对称加密逆向的完整方法论,也非常考验同学们的思考能力,如何通过最少量的代码修改完成我们的解密是本题的一大乐趣。
📖 目录
- 3.1 算法总览
- 3.2 操作一:比特逆序
- 3.3 操作二:位置置换
- 3.4 操作三:字节循环左移
- 3.5 操作四:轮密钥异或
- 4.1 逆向方法论
- 4.2 每个操作的逆操作
- 4.3 逆位置置换详解
- 5.1 正向加密函数
- 5.2 解密函数(核心)
- 5.3 完整可运行代码
1. 开场白
想了很多,但是还是不知道晚上吃啥???QAQ,评论区推荐一下吧,3Q!!!
2. 题目场景还原
在2023年熵密杯的赛制中,选手需要先通过初始题(准入关) 才能进入决赛闯关阶段。Flag1 就是决赛阶段的第一道题。具体我们能拿到的材料如下所示(加密后的文件,密钥密文,以及加密密钥的代码)
题目描述:已知系统对一个文件进行了加密压缩,提供了加密后的压缩包和加密算法的源代码。你需要解开加密压缩包,得到原始文件中的 Flag。
与初始题侧重于"识别已知算法的使用缺陷"不同,决赛题更强调综合能力——你需要分析一个自定义的加密方案,理解其结构,然后逆向出解密算法。
💡 核心能力考察:阅读源码 → 理解加密流程 → 推导逆操作 → 编写解密代码。这是安全工程师在日常工作中反复使用的技能。
3. 加密算法完整剖析
完整加密代码(python版)
def reverse_bits(password):
for i in range(16):
temp = 0
for j in range(8):
temp |= ((password[i] >> j) & 1) << (7 – j)
password[i] = temp
def swap_positions(password):
positions = [
13, 4, 0, 5,
2, 12, 11, 8,
10, 6, 1, 9,
3, 15, 7, 14
]
temp = bytearray(16)
for i in range(16):
temp[positions[i]] = password[i]
for i in range(16):
password[i] = temp[i]
def left_shift_bytes(password):
for i in range(16):
password[i] = ((password[i] << 3) | (password[i] >> 5)) & 0xFF
def xor_with_keys(password, round_num):
key = (0x78 * round_num) & 0xFF
for i in range(16):
password[i] ^= key
def process_password(password: bytes):
password = bytearray(password)
if len(password) < 16:
password.extend(b'\\x00' * (16 – len(password)))
if len(password) > 16:
password = password[:16]
return password
def encrypt_password(password: bytes):
password = process_password(password)
for round_num in range(16):
reverse_bits(password)
swap_positions(password)
left_shift_bytes(password)
xor_with_keys(password, round_num)
return password
def print_hex(data):
return ' '.join(f'{b:02X}' for b in data)
if __name__ == "__main__":
password = input("请输入口令: ").encode()
encrypted = encrypt_password(password)
print("\\n原始口令(16字节):")
print(print_hex(process_password(password)))
print("\\n加密结果:")
print(print_hex(encrypted))
3.1 算法总览
这是一个经典的16轮迭代型对称加密算法,工作在固定长度的 16字节(128位)数据块上。每轮依次执行 4 个操作:
┌──────────────────────────────────────────────────────────┐
│ 加密流程 (16 轮,Round 0 → 15) │
│ │
│ 输入: 16 字节明文块 │
│ │
│ 每轮 (Round r = 0, 1, 2, …, 15): │
│ ┌─────────────┐ │
│ │ 1. 比特逆序 │ reverse_bits ← 打乱每个字节内部 │
│ ├─────────────┤ │
│ │ 2. 位置置换 │ swap_positions ← 打乱字节排列顺序 │
│ ├─────────────┤ │
│ │ 3. 字节左移 │ left_shift_bytes ← 每个字节循环左移3位│
│ ├─────────────┤ │
│ │ 4. 轮密钥异或 │ xor_with_keys ← 与轮密钥混淆 │
│ └─────────────┘ │
│ │
│ 输出: 16 字节密文块 │
└──────────────────────────────────────────────────────────┘
这个结构是不是很像你在教科书上见过的 SPN(替换-置换网络)?它有三个核心操作(比特逆序 + 位置置换 + 循环移位)构成了"混淆"层,轮密钥异或提供了"扩散"层。
接下来我们逐个操作深入剖析。
3.2 操作一:比特逆序
功能:将每个字节内部的 8 个比特位进行首尾颠倒。
图解比特逆序(以一个字节为例):
原始字节: [b₇][b₆][b₅][b₄][b₃][b₂][b₁][b₀]
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
逆序后: [b₀][b₁][b₂][b₃][b₄][b₅][b₆][b₇]
具体例子:
原值: 10110001 (0xB1)
逆序: 10001101 (0x8D)
代码实现:
def reverse_bits(password):
"""
对16字节数组中的每一个字节进行比特逆序。
"""
for i in range(16):
temp = 0
for j in range(8):
# (password[i] >> j) & 1 → 提取第 j 位的值 (0 或 1)
# << (7 – j) → 移到镜像位置
# temp |= … → 合并到结果中
temp |= ((password[i] >> j) & 1) << (7 – j)
password[i] = temp
⚡ 关键性质:比特逆序是自逆操作——执行两次就回到原值。因为 reverse(reverse(x)) = x。这对解密非常友好,我们不需要单独写"逆比特逆序"函数!
3.3 操作二:位置置换
功能:按照一个固定的映射表,重新排列 16 个字节的位置。
映射表(P盒):
原始位置 i: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
新位置 P[i]: 13 4 0 5 2 12 11 8 10 6 1 9 3 15 7 14
解读方式:
P[0] = 13 → 原来在位置 0 的字节,加密后跑到位置 13
P[1] = 4 → 原来在位置 1 的字节,加密后跑到位置 4
P[2] = 0 → 原来在位置 2 的字节,加密后跑到位置 0
…
可视化理解:
加密前 (按原位置排列):
┌────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┐
│ A │ B │ C │ D │ E │ F │ G │ H │ I │ J │ K │ L │ M │ N │ O │ P │
└────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┘
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
加密后 (按 P 表重新排列):
┌────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┐
│ C │ K │ E │ M │ B │ D │ J │ O │ H │ L │ I │ G │ F │ A │ P │ N │
└────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┘
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
代码实现:
def swap_positions(password):
positions = [13, 4, 0, 5, 2, 12, 11, 8, 10, 6, 1, 9, 3, 15, 7, 14]
temp = bytearray(16)
# 正向: temp[新位置] = password[原位置]
for i in range(16):
temp[positions[i]] = password[i]
# 复制回去
for i in range(16):
password[i] = temp[i]
⚡ 关键性质:位置置换不是自逆操作!解密时需要计算逆映射表。后面我们会详细讲解。
3.4 操作三:字节循环左移
功能:将每个字节循环左移 3 位。
图解循环左移3位(以一个字节 0x93 = 0b10010011 为例):
原始: ┌───┬───┬───┬───┬───┬───┬───┬───┐
│ 1 │ 0 │ 0 │ 1 │ 0 │ 0 │ 1 │ 1 │
└───┴───┴───┴───┴───┴───┴───┴───┘
b₇ b₆ b₅ b₄ b₃ b₂ b₁ b₀
左移3位后,高3位"溢出"被丢弃,低位补0:
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ 1 │ 0 │ 0 │ 1 │ 1 │ 0 │ 0 │ 0 │ ← (b << 3) = 0x98
└───┴───┴───┴───┴───┴───┴───┴───┘
b₄ b₃ b₂ b₁ b₀ 0 0 0
再将"溢出"的高3位补到低位:
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ │ │ │ │ │ 1 │ 0 │ 0 │ ← (b >> 5) = 0x04
└───┴───┴───┴───┴───┴───┴───┴───┘
0 0 0 0 0 b₇ b₆ b₅
两部分做"或"运算,得到最终结果:
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ 1 │ 0 │ 0 │ 1 │ 1 │ 1 │ 0 │ 0 │ = 0x9C
└───┴───┴───┴───┴───┴───┴───┴───┘
代码实现:
def left_shift_bytes(password):
"""每个字节循环左移3位"""
for i in range(16):
# (b << 3): 整体左移3位,高位溢出,低位补0
# (b >> 5): 整体右移5位,相当于把溢出的高3位移到低位
# | : 将两部分用"或"拼接
# & 0xFF: 只保留低8位
password[i] = ((password[i] << 3) | (password[i] >> 5)) & 0xFF
⚡ 关键性质:对于 8 位字节,循环左移 3 位的逆操作是循环右移 3 位(也等于循环左移 5 位)。
3.5 操作四:轮密钥异或
功能:将 16 个字节逐一与当前轮的轮密钥进行异或(XOR)。
轮密钥生成规则:
轮密钥 = (0x78 × 轮号) & 0xFF
即保留乘积的低8位(等价于 mod 256)
| Round 0 | 120 × 0 = 0 | 0x00 |
| Round 1 | 120 × 1 = 120 | 0x78 |
| Round 2 | 120 × 2 = 240 | 0xF0 |
| Round 3 | 120 × 3 = 360 mod 256 = 104 | 0x68 |
| Round 4 | 120 × 4 = 480 mod 256 = 224 | 0xE0 |
| … | … | … |
| Round 15 | 120 × 15 = 1800 mod 256 = 8 | 0x08 |
代码实现:
def xor_with_keys(password, round_num):
"""16字节逐一与轮密钥异或"""
key = (0x78 * round_num) & 0xFF
for i in range(16):
password[i] ^= key
⚡ 关键性质:XOR 是自逆操作!因为 A ⊕ K ⊕ K = A,所以加密和解密用完全相同的函数。
4. 解密逆向推导
4.1 逆向方法论
解密 = 加密的逆过程。对称加密逆向的核心思路就是:
加密 = OP₁ → OP₂ → OP₃ → OP₄(执行16轮,Round 0→15)
解密 = OP₄⁻¹ → OP₃⁻¹ → OP₂⁻¹ → OP₁⁻¹(执行16轮,Round 15→0)
两条原则:
| 操作逆序 | 加密时操作顺序是 1→2→3→4,解密就是 4→3→2→1 |
| 轮序逆序 | 加密是 Round 0 到 15,解密就是 Round 15 到 0 |
加密: Round 0: [比特逆序 → 置换 → 左移 → 异或]
Round 1: [比特逆序 → 置换 → 左移 → 异或]
…
Round 15:[比特逆序 → 置换 → 左移 → 异或]
解密: Round 15: [异或 → 右移 → 逆置换 → 比特逆序]
Round 14: [异或 → 右移 → 逆置换 → 比特逆序]
…
Round 0: [异或 → 右移 → 逆置换 → 比特逆序]

4.2 每个操作的逆操作
| 比特逆序 | ✅ 是 | 比特逆序(同一个函数) | 颠倒两次回到原位 |
| 位置置换 | ❌ 否 | 逆位置置换 | 需要计算逆映射表 |
| 循环左移3位 | ❌ 否 | 循环右移3位 | 左3+右3=8=回到原位 |
| 轮密钥异或 | ✅ 是 | 轮密钥异或(同一个函数) | A⊕K⊕K=A |
4.3 逆位置置换详解(⭐ 核心难点)
这是整个解密过程中唯一需要"推导"的步骤。正向置换的逻辑是:
正向: temp[ P[i] ] = password[i]
即:原位置 i 的元素,放到新位置 P[i]
那么逆置换的逻辑就是反过来:
逆向: temp[i] = password[ P[i] ]
即:从密文的第 P[i] 个位置取数据,放到明文的第 i 个位置
推导逆映射表:
正向 P: [13, 4, 0, 5, 2, 12, 11, 8, 10, 6, 1, 9, 3, 15, 7, 14]
含义: P[0]=13 → 位置0的元素去了位置13
P[1]=4 → 位置1的元素去了位置4
…
那么反过来:
位置0的元素来自哪里? → 找 P[?] == 0 → P[2] = 0 → 来自位置2
位置1的元素来自哪里? → 找 P[?] == 1 → P[10] = 1 → 来自位置10
位置2的元素来自哪里? → 找 P[?] == 2 → P[4] = 2 → 来自位置4
…
由此得到逆映射表 P⁻¹:
[2, 10, 4, 12, 1, 3, 9, 14, 7, 11, 8, 6, 5, 0, 15, 13]
验证:
正向: 元素 A 从位置 0 → 位置 13
逆向: 从位置 13 取回 → 放回位置 0 ← 验证: P⁻¹[13] = 0 ✓
def inverse_swap_positions(password):
"""位置置换的逆操作"""
positions = [13, 4, 0, 5, 2, 12, 11, 8, 10, 6, 1, 9, 3, 15, 7, 14]
temp = bytearray(16)
# 逆向: temp[i] = password[ P[i] ]
for i in range(16):
temp[i] = password[positions[i]]
# 复制回去
for i in range(16):
password[i] = temp[i]
💡 技巧:你也可以显式构造逆映射表 [2, 10, 4, 12, 1, 3, 9, 14, 7, 11, 8, 6, 5, 0, 15, 13],然后把正向置换的代码反过来写。但上面这种"直接用原映射表+反过来读"的方式更简洁,也不容易写错。
5. 代码实现:手把手复现
5.1 正向加密函数
先把加密逻辑整理清楚:
def encrypt_password(password: bytes) –> bytearray:
"""
对 16 字节口令执行完整的 16 轮加密。
"""
# 预处理: 统一为 16 字节 (不足补0, 超出截断)
password = bytearray(password)
if len(password) < 16:
password.extend(b'\\x00' * (16 – len(password)))
if len(password) > 16:
password = password[:16]
# 16 轮加密, Round 0 → 15
for round_num in range(16):
reverse_bits(password) # ① 比特逆序
swap_positions(password) # ② 位置置换
left_shift_bytes(password) # ③ 循环左移 3 位
xor_with_keys(password, round_num) # ④ 与轮密钥异或
return password
5.2 解密函数(核心 ⭐)
解密就是加密的镜像——操作顺序相反,轮序也相反:
def decrypt_password(cipher: bytes) –> bytearray:
"""
对 16 字节密文执行完整的 16 轮解密。
与加密的对应关系:
加密: Round 0→15, 操作顺序 ①→②→③→④
解密: Round 15→0, 操作顺序 ④→③⁻¹→②⁻¹→① (③⁻¹=右移, ②⁻¹=逆置换, ④和①自逆)
"""
# 预处理: 统一为 16 字节
cipher = bytearray(cipher)
if len(cipher) < 16:
cipher.extend(b'\\x00' * (16 – len(cipher)))
if len(cipher) > 16:
cipher = cipher[:16]
# 16 轮解密, Round 15 → 0 (逆序!)
for round_num in range(15, –1, –1):
xor_with_keys(cipher, round_num) # ④ 轮密钥异或 (自逆)
right_shift_bytes(cipher) # ③⁻¹ 循环右移 3 位
inverse_swap_positions(cipher) # ②⁻¹ 逆位置置换
reverse_bits(cipher) # ① 比特逆序 (自逆)
return cipher
🎯 核心要点:解密函数的 for round_num in range(15, -1, -1) 是最容易被忽视的细节。加密用 Round 0 的密钥先加密,解密就必须用 Round 15 的密钥先解密——顺序完全相反!
5.3 完整可运行代码
下面是可以直接运行的完整解密脚本:
"""
====================================================
口令解密工具 — Password_Dec.py
2023年熵密杯 Flag1: 对称加密逆向
====================================================
加密算法: 16轮迭代, 每轮4个操作 (比特逆序→位置置换→左移→异或)
解密算法: 逆序执行逆操作 (异或→右移→逆置换→比特逆序)
====================================================
"""
# ==================== 操作函数 ====================
def reverse_bits(password):
"""比特逆序: 将每个字节的8个比特位首尾颠倒 (自逆操作)"""
for i in range(16):
temp = 0
for j in range(8):
temp |= ((password[i] >> j) & 1) << (7 – j)
password[i] = temp
def swap_positions(password):
"""位置置换: 按固定映射表重排16个字节的位置"""
positions = [13, 4, 0, 5, 2, 12, 11, 8, 10, 6, 1, 9, 3, 15, 7, 14]
temp = bytearray(16)
for i in range(16):
temp[positions[i]] = password[i]
for i in range(16):
password[i] = temp[i]
def inverse_swap_positions(password):
"""逆位置置换: 位置置换的逆操作"""
positions = [13, 4, 0, 5, 2, 12, 11, 8, 10, 6, 1, 9, 3, 15, 7, 14]
temp = bytearray(16)
for i in range(16):
temp[i] = password[positions[i]] # ← 注意和正向的区别!
for i in range(16):
password[i] = temp[i]
def left_shift_bytes(password):
"""循环左移: 每个字节循环左移3位"""
for i in range(16):
password[i] = ((password[i] << 3) | (password[i] >> 5)) & 0xFF
def right_shift_bytes(password):
"""循环右移: 每个字节循环右移3位 (左移的逆操作)"""
for i in range(16):
password[i] = ((password[i] >> 3) | (password[i] << 5)) & 0xFF
def xor_with_keys(password, round_num):
"""轮密钥异或: 每个字节与 (0x78 × 轮号) & 0xFF 异或 (自逆操作)"""
key = (0x78 * round_num) & 0xFF
for i in range(16):
password[i] ^= key
# ==================== 加密/解密主函数 ====================
def encrypt_password(password: bytes) –> bytearray:
"""加密: 16轮, Round 0→15, 操作①②③④"""
password = bytearray(password)
if len(password) < 16:
password.extend(b'\\x00' * (16 – len(password)))
if len(password) > 16:
password = password[:16]
for round_num in range(16): # 正向轮序
reverse_bits(password) # ① 比特逆序
swap_positions(password) # ② 位置置换
left_shift_bytes(password) # ③ 左移3位
xor_with_keys(password, round_num) # ④ 轮密钥异或
return password
def decrypt_password(cipher: bytes) –> bytearray:
"""解密: 16轮, Round 15→0, 操作④③⁻¹②⁻¹①"""
cipher = bytearray(cipher)
if len(cipher) < 16:
cipher.extend(b'\\x00' * (16 – len(cipher)))
if len(cipher) > 16:
cipher = cipher[:16]
for round_num in range(15, –1, –1): # ★ 逆向轮序!
xor_with_keys(cipher, round_num) # ④ 异或(自逆)
right_shift_bytes(cipher) # ③⁻¹ 右移3位
inverse_swap_positions(cipher) # ②⁻¹ 逆置换
reverse_bits(cipher) # ① 比特逆序(自逆)
return cipher
# ==================== 主程序 ====================
if __name__ == "__main__":
print("=" * 60)
print("2023年熵密杯 Flag1 — 对称加密逆向解密工具")
print("=" * 60)
# 输入密文 (32个十六进制字符 = 16字节)
cipher_hex = input("\\n请输入密文 (16字节, 32 Hex): ")
try:
cipher_bytes = bytes.fromhex(cipher_hex.strip())
decrypted = decrypt_password(cipher_bytes)
print(f"\\n解密结果 (Hex): {decrypted.hex()}")
# 尝试解码为可读字符串
try:
plaintext = decrypted.decode().rstrip('\\x00')
print(f"解密结果 (字符串): {plaintext}")
except:
print("(解密结果不是有效的 UTF-8 字符串)")
except Exception as e:
print(f"解密失败: {e}")
6. 运行验证
将题目给出的密文输入程序:
$ python Password_Dec.py
============================================================
2023年熵密杯 Flag1 — 对称加密逆向解密工具
============================================================
请输入密文 (16字节, 32 Hex): [题目提供的密文Hex值]
解密结果 (Hex): [解出的明文Hex值]
解密结果 (字符串): flag{…}
验证加密和解密的互逆性:
# 验证: encrypt → decrypt → 回到原文
original = b"Hello, CTF!!!!!!"
encrypted = encrypt_password(original)
decrypted = decrypt_password(encrypted)
print(f"原文: {original}")
print(f"密文: {encrypted.hex()}")
print(f"解密: {bytes(decrypted)}")
print(f"验证: {original == bytes(decrypted)}") # True ✅
7. 举一反三:对称加密逆向套路
这道题虽然算法结构简单(没有真实的 S 盒和复杂的密钥编排),但它精准地考察了对称加密逆向的通用方法论。把这个套路迁移到任何自定义加密算法都适用:
┌─────────────────────────────────────────────────────────┐
│ 对称加密逆向通用流程 │
│ │
│ Step 1: 阅读源码,识别加密操作 │
│ ├── 共有几轮? │
│ ├── 每轮有哪些操作? │
│ └── 每个操作的数学性质是什么? │
│ │
│ Step 2: 逐个分析操作的逆 │
│ ├── 自逆操作: 异或、比特逆序、字节交换 │
│ ├── 需推导逆操作: 置换、S盒替换、矩阵乘法 │
│ └── 对于非自逆操作: 推导逆公式/逆映射表 │
│ │
│ Step 3: 逆序组装解密函数 │
│ ├── 操作顺序反转: OP₁→OP₂→…→OPₙ 变为 OPₙ⁻¹→…→OP₁⁻¹│
│ ├── 轮序反转: Round 0→N-1 变为 Round N-1→0 │
│ └── 密钥编排也要反转(如果有) │
│ │
│ Step 4: 验证互逆性 │
│ └── encrypt(decrypt(x)) == x && decrypt(encrypt(x))==x│
│ │
│ Step 5: 解密真实密文,得到 Flag │
└─────────────────────────────────────────────────────────┘
常见对称加密操作及其逆操作对照表:
| XOR (异或) | XOR (自逆) | ⭐ |
| 加法 (mod 256) | 减法 (mod 256) | ⭐ |
| 比特逆序 | 比特逆序 (自逆) | ⭐ |
| 循环左移 n 位 | 循环右移 n 位 | ⭐ |
| 位置置换 | 逆位置置换 (查逆映射表) | ⭐⭐ |
| S 盒替换 | 逆 S 盒替换 (查逆表) | ⭐⭐ |
| 矩阵乘法 (mod p) | 矩阵求逆 × 密文 | ⭐⭐⭐ |
| 非线性函数 (如 Feistel) | 逆推公式 | ⭐⭐⭐ |
8. 总结与互动
这道题教给我们的
| 算法结构 | 16轮迭代型对称加密,每轮4个操作 |
| 核心操作 | 比特逆序、位置置换、循环移位、轮密钥异或 |
| 逆向方法 | 操作逆序 + 轮序逆序 + 逆操作推导 |
| 关键陷阱 | 位置置换不是自逆的!必须推导逆映射 |
| 轮密钥 | 简单线性派生 key = 0x78 × round(非安全实现) |
| 方法论 | 对称加密逆向的通用5步法 |
💬 交流时间
这是"熵密杯历年真题精析"系列的第二篇,欢迎大家在评论区一起讨论:
如果觉得本文对你有帮助,欢迎点赞、收藏、关注三连支持 🙏 我们下期见!
本文作者:凤凰院凶真 | | 2026/6/11
代码仓库:什么???你真的很想看我的仓库??
*上一篇:[2023年熵密杯初始题精讲:SM4 CBC-MAC 消息伪造攻击]

