LilCTF2025wp
Warm Up
我们来逐行分析题目:
密钥是固定的:
1
key = Random(2025).randbytes(16)这里使用了 random 库(梅森旋转算法),并且指定了种子 2025。这意味着无论何时运行这段代码,生成的 key 都是完全一样的。我们可以直接在解密脚本中生成同样的 Key。
CBC模式的IV依赖于明文:
1
AES.new(key, AES.MODE_CBC, iv=FLAG[9:25])使用了 AES 的 CBC 模式。
- Key: 已知。
- IV (初始化向量): 取自 FLAG 的第 9 到 25 个字节 (FLAG[9:25])。
- Plaintext: 填充后的 FLAG。
而 AES-CBC 模式的解密过程如下: Pi = Dk(Ci) ⊕ Ci − 1 其中,Ci 是密文块,Pi 是明文块,Dk 是解密函数。特殊地,C−1 就是初始化向量。
假设 Flag 长度经过填充后被分为 P0, P1, P2... 几个块(每块16字节),密文分为 C0, C1, C2...。
对于除了第一块以外的所有块 (i > 0): Pi = Deckey(Ci) ⊕ Ci − 1 因为 Key 是已知的,且密文 C 是已知的,所以我们可以直接解密出除了第一块以外的所有明文。这意味着我们知道了 FLAG[16:] 的所有内容。
对于第一块 (P0): P0 = Deckey(C0) ⊕ IV 这里 P0 是 FLAG[0:16],IV 是 FLAG[9:25]。令中间状态 X0 = Deckey(C0)(这是我们能算出来的),那么则有: FLAG[0 : 16] = X0 ⊕ FLAG[9 : 25] 如果我们按字节来看这个异或关系(设 fi 为 Flag 的第 i 个字节): fi = X0[i] ⊕ fi + 9 (对于 0 ≤ i ≤ 15) 这就是解题的关键:
- 我们通过解密 C1, C2... 已经知道了 f16 及其之后的所有字节。
- 利用公式 fi = X0[i] ⊕ fi + 9:
- 当 i = 7 时:f7 = X0[7] ⊕ f16。因为 f16 已知,我们可以算出 f7。
- 以此类推,我们可以算出 f7 到 f15(利用已知的 f16 到 f24)。
- 一旦我们知道了 f9 到 f15,我们再次利用公式:
- 当 i = 0 时:f0 = X0[0] ⊕ f9。因为 f9 刚才算出来了,所以 f0 可求。
- 以此类推,算出 f0 到 f6。
所以,解题代码如下:
1 | |
运行后得到结果:
ez_math
我们已知 A 向量,为了后续方便运算,我们写成列向量的形式: $$ A = \begin{bmatrix} v_1 \newline v_2 \end{bmatrix} $$ 我们根据 B 的生成方式,可以将其进行分解成两个矩阵相乘的形式: $$ B = \begin{bmatrix} v_1\lambda_1 \newline v_2\lambda_2 \end{bmatrix} = \begin{bmatrix} \lambda_1 & 0 \newline 0 & \lambda_2 \end{bmatrix} \begin{bmatrix} v_1 \newline v_2 \end{bmatrix} = \begin{bmatrix} \lambda_1 & 0 \newline 0 & \lambda_2 \end{bmatrix} A $$ 然后再观察生成的 C,可以发现貌似找到了思路: $$ C = A^{-1}B=A^{-1} \begin{bmatrix} \lambda_1 & 0 \newline 0 & \lambda_2 \end{bmatrix} A $$ 我们设 $$ D= \begin{bmatrix} \lambda_1 & 0 \newline 0 & \lambda_2 \end{bmatrix} $$ 上述的分析说明,C 和 D 矩阵相似,由相似矩阵的特性可知其特征值一样。那么直接求 C 的特征值即可,再转换为 ASCII 字符便能获取到 flag。
具体代码如下:
1 | |
运行后得到结果:
mid_math
本题和上边题目还是比较相似的,只是多了一步求解离散对数问题。 根据分析,可以发现矩阵 C 相似于一个对角矩阵: $$ \begin{bmatrix} a & 0 & 0 & 0 & 0 \newline 0 & b & 0 & 0 & 0 \newline 0 & 0 & c & 0 & 0 \newline 0 & 0 & 0 & d & 0 \newline 0 & 0 & 0 & 0 & 0 \end{bmatrix} $$ 其特征值为 a, b, c, d, 0 那么由 D = Ckey,则 D 对应的特征值为: akey, bkey, ckey, dkey, 0 mod p 在求出特征值后,我们便可以通过求解离散对数问题获取 key,之后便可以获取flag
具体代码如下:
1 | |
运行后得到结果:
Space Travel
代码的核心部分在于生成 key 和泄露信息(Gift):
Key 的生成:
key是由 50 个来自vecs列表的二进制字符串拼接而成。- 索引生成方式为
urandom(2) & 0xfff,这意味着索引范围是 0 到 4095 (212 − 1)。
泄露信息 (Linear Leakage):
题目输出了 600 组
[nonce, bit]。计算方式:
bit = (bin(nonce & key).count("1")) % 2。数学意义:这是
nonce和key两个向量在 GF(2) 有限域上的内积 (Inner Product)。设 K 为密钥的二进制向量,Ni 为第 i 个 nonce,bi 为结果位,则有: Ni ⋅ K ≡ bi (mod 2)
我们有 600 个方程。如果 Key 是完全随机的 800 位整数,则有 800
个变量。但这并不代表题目不可解。虽然每一个 bit 都在变,但因为
vecs 只有 4096 (212) 个元素,这些 16
位的向量在数学上必然构成一个 维度不超过 12
的线性子空间(或仿射子空间)。
降维原理: 虽然每个 Block 是 16 bits,但它们是由 12 bits 的索引生成的。如果
vecs是线性生成的,我们可以找到 12 个基向量 (Basis Vectors) 来表示所有的vecs元素。 Vec = C + k0 ⋅ B0 + k1 ⋅ B1 + ... + k11 ⋅ B11- C: 偏移向量 (Offset/Base
vector),通常取
vecs[0]。 - Bi: 基向量。
- ki: 系数 (0 或 1),这就是我们要解的新变量。
- C: 偏移向量 (Offset/Base
vector),通常取
变量统计:
- 新变量:50 个 Block × 12 个系数 = 600 个变量。
- 方程数:600 个。
- 600 = 600,此时满秩可解
具体解题代码如下:
1 | |
运行后得到结果:
Linear
构造格求解,具体的构造写在脚本注释中了。好吧,其实是我不会(),Gemini 王朝了。
求解代码如下:
1 | |
运行后得到结果:
baaaaag
可以发现是非常原始的背包加密系统,直接造格打 LLL 会发现出不了结果,但是使用更强的 BKZ,然后 block_size 设置的稍微大一点就能出。
具体代码如下:
1 | |
运行后得到结果:
详细的题目文件与解题脚本以及 WriteUp 的 pdf 可以通过关于页中我的 GitHub 仓库找到。