零、我是侧信道高手
仅需家访出题人

不难发现

注意到

很容易可以获得TLU1.5的源代码,摘录如下
import tkinter as tk
from tkinter import scrolledtext, messagebox, filedialog
hex_table = list("0123456789ABCDEF")
class TLU01:
def _get_off(self, bit_index):
return 1 if (bit_index + 1) % 2 == 1 else 2
def encode_char(self, ch):
code = ord(ch)
hx = hex(code)[2:].upper()
res = ""
for i, c in enumerate(hx):
pos = hex_table.index(c)
off = self._get_off(i)
new_p = (pos + off) % 16
res += hex_table[new_p]
head = f"{len(hx):02d}"
return head + res
def decode_block(self, block):
hx_len = int(block[:2])
shift_part = block[2:2 + hx_len]
raw_hx = ""
for i, c in enumerate(shift_part):
pos = hex_table.index(c)
off = self._get_off(i)
new_p = (pos - off + 16) % 16
raw_hx += hex_table[new_p]
char_code = int(raw_hx, 16)
return chr(char_code)
def encrypt_text(self, text):
cipher = ""
for c in text:
cipher += self.encode_char(c)
return cipher
def decrypt_text(self, cipher_text):
plain = []
ptr = 0
while ptr < len(cipher_text):
blk_len = int(cipher_text[ptr:ptr + 2])
block = cipher_text[ptr:ptr + 2 + blk_len]
plain.append(self.decode_block(block))
ptr += 2 + blk_len
return "".join(plain)
def _byte_encode(self, byte_val, idx):
off = self._get_off(idx)
return (byte_val + off) % 256
def _byte_decode(self, byte_val, idx):
off = self._get_off(idx)
return (byte_val - off + 256) % 256
def encrypt_file(self, input_path, output_path):
try:
with open(input_path, "rb") as f:
raw_data = f.read()
enc_bytes = bytearray()
for idx, b in enumerate(raw_data):
enc_bytes.append(self._byte_encode(b, idx))
with open(output_path, "wb") as f:
f.write(enc_bytes)
return True, "文件加密成功"
except Exception as e:
return False, str(e)
def decrypt_file(self, input_path, output_path):
try:
with open(input_path, "rb") as f:
enc_data = f.read()
raw_bytes = bytearray()
for idx, b in enumerate(enc_data):
raw_bytes.append(self._byte_decode(b, idx))
with open(output_path, "wb") as f:
f.write(raw_bytes)
return True, "文件解密成功"
except Exception as e:
return False, str(e)
tool = TLU01()
def create_gui():
root = tk.Tk()
root.title("TLU-1.5 文本/文件加密工具")
root.geometry("580x480")
root.option_add("*Font", ("微软雅黑", 9))
tk.Label(root, text="文本输入(支持中文)").pack(anchor="w", padx=10)
txt_in = scrolledtext.ScrolledText(root, height=5)
txt_in.pack(fill="x", padx=10)
tk.Label(root, text="文本输出结果").pack(anchor="w", padx=10)
txt_out = scrolledtext.ScrolledText(root, height=5)
txt_out.pack(fill="x", padx=10)
btn_frame1 = tk.Frame(root)
btn_frame1.pack(pady=6)
def text_enc():
s = txt_in.get("1.0", tk.END).strip()
res = tool.encrypt_text(s)
txt_out.delete("1.0", tk.END)
txt_out.insert("1.0", res)
def text_dec():
s = txt_in.get("1.0", tk.END).strip()
valid = set("0123456789ABCDEF")
if any(c not in valid for c in s):
messagebox.showerror("错误", "密文仅允许大写0-9,A-F")
return
try:
res = tool.decrypt_text(s)
txt_out.delete("1.0", tk.END)
txt_out.insert("1.0", res)
except:
messagebox.showerror("解密失败", "密文损坏")
tk.Button(btn_frame1, text="文本加密", command=text_enc).grid(row=0, column=0, padx=6)
tk.Button(btn_frame1, text="文本解密", command=text_dec).grid(row=0, column=1, padx=6)
tk.Button(btn_frame1, text="清空文本", command=lambda: (txt_in.delete("1.0", tk.END), txt_out.delete("1.0", tk.END))).grid(row=0, column=2, padx=6)
tk.Label(root, text="文件加解密区域", font=("微软雅黑", 10, "bold")).pack(pady=8)
file_frame = tk.Frame(root)
file_frame.pack()
src_var = tk.StringVar()
dst_var = tk.StringVar()
def sel_src():
p = filedialog.askopenfilename()
src_var.set(p)
def sel_dst():
p = filedialog.asksaveasfilename()
dst_var.set(p)
tk.Entry(file_frame, textvariable=src_var, width=35).grid(row=0, column=0)
tk.Button(file_frame, text="选择源文件", command=sel_src).grid(row=0, column=1, padx=4)
tk.Entry(file_frame, textvariable=dst_var, width=35).grid(row=1, column=0, pady=4)
tk.Button(file_frame, text="保存路径", command=sel_dst).grid(row=1, column=1, padx=4)
def file_encrypt():
s = src_var.get()
d = dst_var.get()
if not s or not d:
messagebox.showwarning("提示", "请选择源文件和保存路径")
return
ok, msg = tool.encrypt_file(s, d)
if ok:
messagebox.showinfo("完成", msg)
else:
messagebox.showerror("失败", msg)
def file_decrypt():
s = src_var.get()
d = dst_var.get()
if not s or not d:
messagebox.showwarning("提示", "请选择源文件和保存路径")
return
ok, msg = tool.decrypt_file(s, d)
if ok:
messagebox.showinfo("完成", msg)
else:
messagebox.showerror("失败", msg)
btn_frame2 = tk.Frame(root)
btn_frame2.pack(pady=6)
tk.Button(btn_frame2, text="加密文件", command=file_encrypt).grid(row=0, column=0, padx=8)
tk.Button(btn_frame2, text="解密文件", command=file_decrypt).grid(row=0, column=1, padx=8)
root.mainloop()
if __name__ == "__main__":
create_gui()
题面说明算法只包含:
- 字节编码;
- 十六进制位运算;
- 模 16 循环偏移;
- 固定位置重排;
- 固定摘要扰动。
且摘要扰动过程只与输入长度有关,不存在随机数 / 随机 IV / 外部状态。
最终恢复出的 FLAG 为:
flag{T1u_2026_Kc7f_Crypt0_M4ster!}
一、注意到 byte-pattern
先不动任何密码学知识,只把四组已知明文/密文放在一起看,就能发现明显的字节模式。
1.1 密文总是按 12 个十六进制字符对齐
| 明文 |
明文字节数 |
密文十六进制字符数 |
TLU |
3 |
12 |
Hello |
5 |
24 |
2026 |
4 |
24 |
abcd! |
5 |
24 |
3 个明文字节 = 6 个 ASCII 十六进制半字节,而密文块是 12 个字符(12 个半字节)。也就是说:
3 个明文字节 -> 1 个密文块(12 hex 字符)
4/5 个明文 -> 2 个密文块(24 hex 字符)
密文块里有 6 个位置是“有效的”,剩下 6 个位置是“固定摘要”。先把密文每 12 字符分组:
Hello:
34BB405504B5 | 223594B94C53
2026:
A48844556485 | 223322356483
FLAG 共 12 组:
14CC46555475
94BC475584C5
848A43551495
448C445584C5
D4C9475564C5
34A84B55A4B5
74BA4355F495
A48844556485
648C495534A5
548C4F5584A5
B4BB405554B5
22332235A4B3
1.2 完整块里的固定位置是常量
把四个“完整块”(都是 3 个明文字节)按下标展开:
下标: 0 1 2 3 4 5 6 7 8 9 10 11
TLU: 9 4 A A 4 8 5 5 0 4 9 5
Hel: 3 4 B B 4 0 5 5 0 4 B 5
202: A 4 8 8 4 4 5 5 6 4 8 5
abc: 5 4 7 B 4 7 5 5 8 4 B 5
立刻能看到:无论明文怎么变,下面这些下标永远相同:
下标 1 = 4
下标 4 = 4
下标 6 = 5
下标 7 = 5
下标 9 = 4
下标 11 = 5
这 6 个位置就是“固定摘要扰动”,不携带任何数据;真正携带数据的是:
0, 2, 3, 5, 8, 10
再结合“固定位置重排”,用四组样本交叉对齐,可以得到完整块的提取顺序:
[0, 2, 8, 10, 5, 3]
即对 12 字符密文块 c 依次取 c[0], c[2], c[8], c[10], c[5], c[3]。以 TLU 为例:
密文块 94AA48550495
-> c[0]=9 c[2]=A c[8]=0 c[10]=9 c[5]=8 c[3]=A
-> 9A098A
1.3 尾块模板只和长度有关(正好对应提示)
最后一块不足 3 个明文字节时,模板会变化,但只取决于剩下几个字节——这正是题面那句“摘要扰动过程只与输入长度有关”。
剩 2 个明文字节(Hello 的 lo、abcd! 的 d!):
2235 r0 4 r1 r3 4 r2 53
| 下标 |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
| 内容 |
2 |
2 |
3 |
5 |
r0 |
4 |
r1 |
r3 |
4 |
r2 |
5 |
3 |
提取顺序:
[4, 6, 9, 7]
例:223594B94C53 → 取下标 4,6,9,7 → 9BC9。
剩 1 个明文字节(2026 的 6):
22332235 r0 4 r1 3
提取顺序:
[8, 10]
例:223322356483 → 68。
FLAG 的最后一个密文块是 22332235A4B3,正好套用“剩 1 字节”模板,因此 FLAG 的明文长度为:
11 * 3 + 1 = 34 字节
二、验证剩下的部分和家访结果同源
第一步只拿到了“骨架”:哪些位置是摘要、哪些位置是数据、提取顺序是什么。但剩下的部分——有效半字节到底怎么变换(偏移多少、方向如何)、模板里的常数为什么是 4/5/55/2235/22332235/53——还不能凭空确定。
这一步靠“家访”:找 TLU1.5 的源代码(出题人工具),摘录关键算法如下:
hex_table = list("0123456789ABCDEF")
class TLU01:
def _get_off(self, bit_index):
return 1 if (bit_index + 1) % 2 == 1 else 2
def encode_char(self, ch):
code = ord(ch)
hx = hex(code)[2:].upper()
res = ""
for i, c in enumerate(hx):
pos = hex_table.index(c)
off = self._get_off(i)
new_p = (pos + off) % 16
res += hex_table[new_p]
head = f"{len(hx):02d}"
return head + res
def _byte_encode(self, byte_val, idx):
off = self._get_off(idx)
return (byte_val + off) % 256
把 TLU1.5 的算法设计与题面逐条对照:
| 题面要求的操作 |
TLU1.5 源码对应 |
| 字节编码 |
ord(ch) / _byte_encode |
| 十六进制位运算 |
hex() 转十六进制后逐位处理 |
| 模 16 循环偏移 |
(pos + off) % 16 |
| 固定位置重排 |
密文块固定槽位、固定提取顺序 |
| 固定摘要扰动(只与长度有关) |
head = f"{len(hx):02d}" 的按长度头部 |
可见 HexMaze 和 TLU1.5 同源:同一套“半字节级模 16 旋转 + 按长度加摘要”的算法骨架。因此“剩下的部分”可以直接用 TLU1.5 的骨架 + 密钥 121 复原:
- 密钥
121 拆成 1、2、1,基础偏移 1 + 2 + 1 = 4;
- 高半字节额外加首位
1(位置扰动),即高半字节 +5、低半字节 +4;
- 每 6 个有效半字节一块,整体做一次逆序(固定位置重排);
- 按剩余长度套 TLU1.5 式长度模板(
2235... / 22332235...)。
用 TLU 手工验证这个“同源”结论是否自洽:
TLU = 54 4C 55(ASCII hex:544C55)
高半字节 +5,低半字节 +4(模 16):
5+5=A 4+4=8
4+5=9 C+4=0
5+5=A 5+4=9
-> A890A9
整体逆序:
A890A9 -> 9A098A
套完整块模板 r0 4 r1 r5 4 r4 55 r2 4 r3 5:
r0=9 r1=A r2=0 r3=9 r4=8 r5=A
-> 94AA48550495
结果与题目给出的 TLU -> 94AA48550495 完全一致。说明从 byte-pattern 里“剩下的部分”和家访得到的 TLU1.5 是同一个来源,算法被完整锁定。
同样地,用 Hello 全流程回代(详见第五节验证),也完全一致。
三、得到答案:解密 FLAG
算法已经确定,直接对 FLAG 走解密流程。
第 1 步:按第一节的槽位,把每个密文块的有效半字节提取出来(最后一块按 [8,10]):
1C576C9B8C7C88193A488C4CDC6C793AABB87BF93AA86848683A9C588AFCBB5B0BAB
第 2 步:撤销全局逆序,整体反转:
BAB0B5BBCFA885C9A38684868AA39FB78BBAA397C6CDC4C884A39188C7C8B9C675C1
第 3 步:偶数下标减 5、奇数下标减 4(模 16),得到明文十六进制:
666C61677B5431755F323032365F4B6337665F4372797074305F4D3473746572217D
第 4 步:每两个十六进制字符转一个 ASCII 字节:
66 6C 61 67 7B 54 31 75 5F 32 30 32 36 5F 4B 63 37
66 5F 43 72 79 70 74 30 5F 4D 34 73 74 65 72 21 7D
得到:
flag{T1u_2026_Kc7f_Crypt0_M4ster!}
传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!