2026蓝桥杯 Reverse WriteUp
题目信息
附件包含两个文件:
$ file veil_gate panel.dat
veil_gate: ELF 64-bit LSB pie executable, x86-64, dynamically linked, stripped
panel.dat: data运行程序会读取当前目录下的 panel.dat,然后从标准输入读取 flag:
$ ./veil_gate
xxx
no正确输入时输出:
ok最终 flag:
flag{9d7a228ca5825bc15cd60bee0bb6d585}1. 初步分析
strings 可以看到几个关键信息:
TPK2
panel.dat
load failed
read failed
no
ok说明程序核心逻辑是:
- 打开
panel.dat - 检查文件头
TPK2 - 从
panel.dat中读取若干参数和查表数据 - 读取用户输入
- 对输入做变换并与
panel.dat内的目标值比较
panel.dat 大小为 420 字节,开头如下:
54 50 4b 32 7c fa 7c 1b ...前 4 字节是魔数:
TPK22. panel.dat 格式解析
逆向 veil_gate 后可以看出,panel.dat 结构大致如下:
| Offset | Size | 含义 |
|---|---|---|
0x00 | 4 | magic,固定为 TPK2 |
0x04 | 4 | 加密后的 header |
0x08 | 24 | key material |
0x20 | 128 | table A |
0xa0 | 128 | table B |
0x120 | 64 | table C |
0x160 | 64 | target |
0x1a0 | 4 | seed |
header 会与常量 0x3d6ca55a 异或:
header = enc_header ^ 0x3d6ca55a解析得到:
header = 26 5f 10 26即:
header[0] = 0x26
header[1] = 0x5f
key_len = 0x10 = 16
flag_len = 0x26 = 38
seed = 0x7a6b5c4d所以 flag 长度应为 38。
3. 关键逻辑分析
程序会先根据 panel.dat 生成一个 16 字节 key:
cur = header[0]
for i in range(24):
key[i] = key_material[i] ^ cur ^ ((i & 3) * 7 + 0x33)
cur = (cur + 0x0b) & 0xff其中真正使用的是前 16 字节:
3c a9 0c 1e 1a 93 68 f8 74 18 bd 95 4c 6c bc d7随后程序构造一个 256 字节的 S-Box:
for i in range(128):
sbox[2 * i] = table_a[i] ^ key[i % key_len]
sbox[2 * i + 1] = table_b[i] ^ key[(i + 5) % key_len]还会从 table C 派生一个 64 字节数组:
idx_table[i] = table_c[i] ^ (i + 0x5d) ^ key[(3 * i + 1) % key_len]前 38 个值刚好是 0 ~ 37 的一个置换,用来决定每一轮处理 flag 的哪个字符:
[2, 27, 34, 17, 8, 26, 21, 7, 35, 13, 16, 12, 24, 14, 32, 3,
33, 15, 20, 22, 5, 0, 1, 4, 18, 31, 19, 28, 36, 30, 29, 10,
9, 37, 25, 11, 23, 6]程序里有两条校验路径。第一条会尝试用 seed == 0x7a6b5c4d 的路径比较,但不满足后会进入第二条 fallback 路径。真正的 flag 是通过第二条路径还原出来的。
4. 逆向求解脚本
下面脚本直接解析 panel.dat 并反推 flag:
from pathlib import Path
import struct
data = Path("panel.dat").read_bytes()
assert data[:4] == b"TPK2"
def rol8(x, r):
r &= 7
return ((x << r) | (x >> (8 - r))) & 0xff
def rol16(x, r):
r &= 15
return ((x << r) | (x >> (16 - r))) & 0xffff
# parse header
enc_header = struct.unpack("<I", data[4:8])[0]
header = (enc_header ^ 0x3d6ca55a).to_bytes(4, "little")
h0 = header[0]
h1 = header[1]
key_len = header[2]
flag_len = header[3]
key_material = data[8:32]
table_a = data[32:160]
table_b = data[160:288]
table_c = data[288:352]
target = data[352:416]
seed = struct.unpack("<I", data[416:420])[0]
# derive key
cur = h0
key = []
for i, b in enumerate(key_material):
key.append(b ^ cur ^ ((i & 3) * 7 + 0x33))
cur = (cur + 0x0b) & 0xff
key = key[:key_len]
# derive sbox
sbox = []
for i in range(128):
sbox.append(table_a[i] ^ key[i % key_len])
sbox.append(table_b[i] ^ key[(i + 5) % key_len])
# derive index table
idx_table = []
for i in range(64):
v = table_c[i] ^ ((i + 0x5d) & 0xff) ^ key[(3 * i + 1) % key_len]
idx_table.append(v)
# fallback checker inversion
edi = rol16(int.from_bytes(header[:2], "little"), 8)
r8 = 3
r9 = 1
flag = [None] * flag_len
for i in range(flag_len):
rem = (edi + i + idx_table[i]) % key_len
possible_tmp = []
for tmp in range(256):
a = key[rem] ^ tmp
cl = (rol8(edi & 0xff, i) + i) & 0xff
cl = (cl + sbox[a]) & 0xff
if cl == target[i]:
possible_tmp.append(tmp)
pos = idx_table[i] % flag_len
possible_char = []
for tmp in possible_tmp:
for ch in range(32, 127):
c = rol8(ch, 4) if (i & 1) == 0 else ch
calc = ((c ^ key[(pos + i) % key_len]) + rol8(h0, i)) & 0xff
if calc == tmp:
possible_char.append(ch)
assert len(set(possible_char)) == 1
flag[pos] = possible_char[0]
# update state with known target byte
out = target[i]
edi = (edi * 0x83) & 0xffffffff
edi ^= idx_table[r8 % flag_len]
r8 += 7
edi = (key[r9 % key_len] + ((out ^ edi) & 0xffffffff)) & 0xffffffff
r9 += 5
flag = bytes(flag).decode()
print(flag)运行结果:
$ python3 solve.py
flag{9d7a228ca5825bc15cd60bee0bb6d585}