TexSAW 2026 Writeup

TexSAW 2026 Writeup

本文整理 TexSAW 2026 中两道题的解题过程。图片和附件本身没有随文章保存,因此只保留可以复现的文字、命令和代码。

题目索引

Secret Word

解法

题目附件是一个 DOCX 文件。DOCX 本质上是一个 ZIP 容器,直接解压即可查看其中的文件:

1
2
unzip challenge.docx -d challenge
find challenge -type f -name 'secret.txt' -print

找到 secret.txt 后,对其内容进行 Base64 解码:

1
base64 -d challenge/secret.txt

解码后的内容就是题目需要提交的 secret word。

A Different Side Channel

题目分析

题目提供了:

  • 500 条功耗轨迹,traces.npy 的形状为 500 × 100;
  • 与轨迹对应的明文,plaintexts.npy 的形状为 500 × 16;
  • 一个 64 字节的加密 flag。

这是一个典型的相关功耗分析(Correlation Power Analysis,CPA)场景。攻击 AES 第一轮的中间值:

功耗模型采用汉明重量:

对每个字节枚举 k 的 256 种可能值,计算假设功耗和真实轨迹之间的皮尔逊相关系数。相关系数绝对值最大的候选就是该字节最可能的密钥值。

CPA 脚本

下面是整理并修正缩进后的脚本。AES S-Box 使用标准的 256 项查找表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
import numpy as np
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad


SBOX = np.array(
[
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,
],
dtype=np.uint8,
)

HW = np.array([bin(value).count("1") for value in range(256)])

plaintexts = np.load("plaintexts.npy")
traces = np.load("traces.npy")
with open("encrypted_flag.bin", "rb") as file:
encrypted_flag = file.read()

n_traces, n_samples = traces.shape
trace_centered = traces - traces.mean(axis=0)
trace_std = traces.std(axis=0)
recovered_key = []

for byte_index in range(16):
plaintext_byte = plaintexts[:, byte_index].astype(np.uint8)
max_correlations = np.zeros(256)

for candidate in range(256):
hypothesis = HW[SBOX[plaintext_byte ^ candidate]].astype(float)
hypothesis_centered = hypothesis - hypothesis.mean()
covariance = hypothesis_centered @ trace_centered / n_traces
correlation = np.abs(
covariance / (hypothesis.std() * trace_std + 1e-10)
)
max_correlations[candidate] = correlation.max()

best = int(np.argmax(max_correlations))
recovered_key.append(best)
print(
f"Byte {byte_index:2d}: "
f"0x{best:02x}, corr={max_correlations[best]:.4f}"
)

key = bytes(recovered_key)
print(f"\nKey: {key.hex()}")

# 文件的前 16 字节是 IV,剩余部分是 CBC 密文。
iv = encrypted_flag[:16]
ciphertext = encrypted_flag[16:]
flag = unpad(AES.new(key, AES.MODE_CBC, iv).decrypt(ciphertext), 16)
print(f"Flag: {flag.decode()}")

解密过程

加密文件长度为 64 字节,其中前 16 字节可以作为 IV,剩下 48 字节是 AES-CBC 密文。恢复 16 字节密钥后,使用 CBC 模式解密并去除 PKCS#7 padding 即可得到 flag。

题目材料给出了运行结果:

1
texsaw{d1ffer3nti&!*p0w3r*@n4!y51s}

Docker-ception

解题思路

后端会执行类似 ping admin 的操作,输入没有被正确限制,因此可以从主机名或参数位置尝试命令注入。利用注入读取 /home/ctfer 目录,找到其中保存的 flag。

原始材料没有记录具体的 Payload 和最终 flag,因此这里只保留攻击方向。

Lost my keys

题目提供了两张看起来完全相同的 PNG。将两张图片逐像素比较,把不同像素转换成二进制序列,再按 ASCII 解码即可得到 flag。

图片附件没有随文章保存,因此没有在文章中添加图片引用;原始材料也没有记录解码后的具体 flag。

Broken Quest

状态机分析

这是一个“任务”模拟器。玩家通过 0 到 8 号操作改变一个包含 8 个整数的状态数组,满足目标状态后执行 turn_in。

关键函数如下:

函数 作用
turn_in 用 memcmp 比较状态前 8 字节,要求 state[0] == 2、state[1] == 6,通过后调用 handle_flag。
handle_flag 使用完整状态作为密钥,通过置换矩阵和 8 种运算解密数据。
transform 对密文逐字节执行 key_byte XOR enc_byte。
calc_val 循环调用 op0 到 op7,生成每个字节的密钥。

从 main 中提取出的目标状态为:

1
[2, 6, -4, 6, 0, 4, -3, 1]

解密

将 8 个状态值按置换矩阵展开为 8×8 矩阵,然后分成 8 段解密,每段使用不同的行、偏移和长度。最终得到:

1
texsaw{1t_ju5t_work5_m0r3_l1k3_!t_d0e5nt_w0rk}