LilCTF2025部分wp
crypto
[WARM UP] 对称!Just Decrypt
Title
如果放在 LilCTF Crypto,本题难度可定级为 签到
短短的也很可爱。
idea
"""Just Decrypt - A simple LilCTF warmup challenge."""
from random import Random
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
FLAG = b""
key = Random(2025).randbytes(16)
print(AES.new(key, AES.MODE_CBC, iv=FLAG[9:25]).encrypt(pad(FLAG, 16)).hex())
# ae39cfab1ba8d38fc3761216c393caf16e3c3f13fe57e2dedd52f1b13072fa93df405c7e731a193cabe5fd88ee3241f79aded62d139faba8c767a3b8efc5a855
- 密钥复用:通过固定种子2025生成的密钥可预测,直接复现即可。
- IV自指构造:IV取自FLAG[9:25],利用解密中间值和密文块异或关系逆向推导IV。
- 填充移除:解密后去除PKCS7填充即得原始FLAG。
exp
from random import Random
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
import binascii
key = Random(2025).randbytes(16)
ciphertext = binascii.unhexlify("ae39cfab1ba8d38fc3761216c393caf16e3c3f13fe57e2dedd52f1b13072fa93df405c7e731a193cabe5fd88ee3241f79aded62d139faba8c767a3b8efc5a855")
c0, c1, c2, c3 = [ciphertext[i*16:(i+1)*16] for i in range(4)]
cipher_ecb = AES.new(key, AES.MODE_ECB)
I0, I1 = cipher_ecb.decrypt(c0), cipher_ecb.decrypt(c1)
iv = bytearray(16)
iv_part2 = [I1[i] ^ c0[i] for i in range(9)] # iv[7:16]
for i in range(9):
iv[7 + i] = iv_part2[i]
for i in range(7):
iv[i] = I0[9 + i] ^ iv[9 + i]
IV = bytes(iv)
decrypted = AES.new(key, AES.MODE_CBC, IV).decrypt(ciphertext)
flag = unpad(decrypted, 16)
print(flag.decode())
'''
LILCTF{b1a6labl4@#$%_H4v3_fUn_W1tH_y0Ur_sYmM3try_3NcRyPt10n!}
'''
ez_math
Title
出题:sudopacman
难度:简单
什么, 你是说出题人随机凑了几个矩阵给大家求flag?
idea
from sage.all import *
from Crypto.Util.number import *
flag = b'LILCTF{test_flag}'[7:-1]
lambda1 = bytes_to_long(flag[:len(flag)//2])
lambda2 = bytes_to_long(flag[len(flag)//2:])
p = getPrime(512)
def mul(vector, c):
return [vector[0]*c, vector[1]*c]
v1 = [getPrime(128), getPrime(128)]
v2 = [getPrime(128), getPrime(128)]
A = matrix(GF(p), [v1, v2])
B = matrix(GF(p), [mul(v1,lambda1), mul(v2,lambda2)])
C = A.inverse() * B
print(f'p = {p}')
print(f'C = {str(C).replace(" ", ",").replace("\n", ",").replace("[,", "[")}')
# p = 9620154777088870694266521670168986508003314866222315790126552504304846236696183733266828489404860276326158191906907396234236947215466295418632056113826161
# C = [7062910478232783138765983170626687981202937184255408287607971780139482616525215270216675887321965798418829038273232695370210503086491228434856538620699645,7096268905956462643320137667780334763649635657732499491108171622164208662688609295607684620630301031789132814209784948222802930089030287484015336757787801],[7341430053606172329602911405905754386729224669425325419124733847060694853483825396200841609125574923525535532184467150746385826443392039086079562905059808,2557244298856087555500538499542298526800377681966907502518580724165363620170968463050152602083665991230143669519866828587671059318627542153367879596260872]
idea
- 代码其实构造了
- 其中
lambda1, lambda2是 flag 前后两半转成的整数。 - 相似矩阵有同样的特征值, D 的特征值就是
lambda1, lambda2。- ⇒ C 的特征值(模 p)就是
lambda1, lambda2。
- ⇒ C 的特征值(模 p)就是
- 对 2×2 矩阵 C,算出:
- 行列式 :
- 解二次方程:
- 用 Tonelli–Shanks 在模 p 下开根,得到两个根:这两个根就是
lambda1和lambda2。 - 因为 flag 很短,
lambda1, lambda2 < p,所以它们在模 p 下没绕圈,就是原值。 - 最后
lambda1, lambda2各自long_to_bytes,flag 就出来了。
exp
from Crypto.Util.number import long_to_bytes
p = 9620154777088870694266521670168986508003314866222315790126552504304846236696183733266828489404860276326158191906907396234236947215466295418632056113826161
C = [
[
7062910478232783138765983170626687981202937184255408287607971780139482616525215270216675887321965798418829038273232695370210503086491228434856538620699645,
7096268905956462643320137667780334763649635657732499491108171622164208662688609295607684620630301031789132814209784948222802930089030287484015336757787801,
],
[
7341430053606172329602911405905754386729224669425325419124733847060694853483825396200841609125574923525535532184467150746385826443392039086079562905059808,
2557244298856087555500538499542298526800377681966907502518580724165363620170968463050152602083665991230143669519866828587671059318627542153367879596260872,
],
]
def sqrt_mod(n, p):
if n == 0:
return 0
q, s = p - 1, 0
while q % 2 == 0:
q //= 2; s += 1
z = 2
while pow(z, (p - 1) // 2, p) != p - 1:
z += 1
m, c = s, pow(z, q, p)
t, r = pow(n, q, p), pow(n, (q + 1) // 2, p)
while t != 1:
i, tt = 1, t * t % p
while i < m and tt != 1:
tt = tt * tt % p; i += 1
b = pow(c, 1 << (m - i - 1), p)
r = r * b % p
c = b * b % p
t = t * c % p
m = i
return r
(a, b), (c, d) = C
T = (a + d) % p
D = (a * d - b * c) % p
Delta = (T * T - 4 * D) % p
s = sqrt_mod(Delta, p)
inv2 = pow(2, -1, p)
l1 = (T + s) * inv2 % p
l2 = (T - s) * inv2 % p
#print(long_to_bytes(l1), long_to_bytes(l2))
flag = b"LILCTF{" + long_to_bytes(l2) + long_to_bytes(l1) + b"}"
print(flag.decode())
'''
LILCTF{It_w4s_the_be5t_of_times_1t_wa5_the_w0rst_of_t1me5}
'''
mid_math
Title
出题:sudopacman
难度:简单
我再也不想学代数了
idea
from sage.all import *
from Crypto.Util.number import *
from tqdm import tqdm
from random import randint
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
flag = b'LILCTF{test_flag}'
p = getPrime(64)
P = GF(p)
key = randint(2**62, p)
def mul(vector, c):
return [vector[0]*c, vector[1]*c, vector[2]*c, vector[3]*c, vector[4]*c]
v1 = [getPrime(64), getPrime(64), getPrime(64), getPrime(64), getPrime(64)]
v2 = [getPrime(64), getPrime(64), getPrime(64), getPrime(64), getPrime(64)]
v3 = [getPrime(64), getPrime(64), getPrime(64), getPrime(64), getPrime(64)]
v4 = [getPrime(64), getPrime(64), getPrime(64), getPrime(64), getPrime(64)]
v5 = [getPrime(64), getPrime(64), getPrime(64), getPrime(64), getPrime(64)]
a, b, c, d, e = getPrime(64), getPrime(64), getPrime(64), getPrime(64), 0
A = matrix(P, [v1, v2, v3, v4, v5])
B = matrix(P, [mul(v1,a), mul(v2,b), mul(v3, c), mul(v4, d), mul(v5, e)])
C = A.inverse() * B
D = C**key
key = pad(long_to_bytes(key), 16)
aes = AES.new(key,AES.MODE_ECB)
msg = aes.encrypt(pad(flag, 64))
print(f"p = {p}")
print(f'C = {[i for i in C]}'.replace('(', '[').replace(')', ']'))
print(f'D = {[i for i in D]}'.replace('(', '[').replace(')', ']'))
print(f"msg = {msg}")
#p = 14668080038311483271
#C = [[11315841881544731102, 2283439871732792326, 6800685968958241983, 6426158106328779372, 9681186993951502212], [4729583429936371197, 9934441408437898498, 12454838789798706101, 1137624354220162514, 8961427323294527914], [12212265161975165517, 8264257544674837561, 10531819068765930248, 4088354401871232602, 14653951889442072670], [6045978019175462652, 11202714988272207073, 13562937263226951112, 6648446245634067896, 13902820281072641413], [1046075193917103481, 3617988773170202613, 3590111338369894405, 2646640112163975771, 5966864698750134707]]
#D = [[1785348659555163021, 3612773974290420260, 8587341808081935796, 4393730037042586815, 10490463205723658044], [10457678631610076741, 1645527195687648140, 13013316081830726847, 12925223531522879912, 5478687620744215372], [9878636900393157276, 13274969755872629366, 3231582918568068174, 7045188483430589163, 5126509884591016427], [4914941908205759200, 7480989013464904670, 5860406622199128154, 8016615177615097542, 13266674393818320551], [3005316032591310201, 6624508725257625760, 7972954954270186094, 5331046349070112118, 6127026494304272395]]
#msg = b"\xcc]B:\xe8\xbc\x91\xe2\x93\xaa\x88\x17\xc4\xe5\x97\x87@\x0fd\xb5p\x81\x1e\x98,Z\xe1n`\xaf\xe0%:\xb7\x8aD\x03\xd2Wu5\xcd\xc4#m'\xa7\xa4\x80\x0b\xf7\xda8\x1b\x82k#\xc1gP\xbd/\xb5j"
这道题主要在于矩阵相似性(Matrix Similarity)与特征值运算性质
-
题目中 \(B\) 的每一行是 \(A\) 对应行的倍数,这等价于 \(B = \Lambda A\),其中 \(\Lambda\) 是由 \((a,b,c,d,0)\) 组成的对角矩阵,因此 \(C = A^{-1}B = A^{-1}\Lambda A\),即 \(C\) 相似于对角矩阵 \(\Lambda\)。
-
根据相似矩阵的性质,\(D = C^{\text{key}} = A^{-1} \Lambda^{\text{key}} A\)。意味着 \(D\) 的特征值集合是 \(C\) 的特征值集合的
key次幂,即对于 \(C\) 的任意非零特征值 \(\lambda_C\) 和 \(D\) 的对应特征值 \(\lambda_D\),满足
- 利用上述性质,我们可以将其转化为 \(GF(p)\) 上的标量离散对数问题:已知 \(\lambda_D, \lambda_C, p\),求
key,由于 \(p\) 是 64 位素数,用SageMath 的discrete_log可以直接解出key拿到flag。
exp
from sage.all import *
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad, pad
from Crypto.Util.number import long_to_bytes
p = 14668080038311483271
C_data = [[11315841881544731102, 2283439871732792326, 6800685968958241983, 6426158106328779372, 9681186993951502212], [4729583429936371197, 9934441408437898498, 12454838789798706101, 1137624354220162514, 8961427323294527914], [12212265161975165517, 8264257544674837561, 10531819068765930248, 4088354401871232602, 14653951889442072670], [6045978019175462652, 11202714988272207073, 13562937263226951112, 6648446245634067896, 13902820281072641413], [1046075193917103481, 3617988773170202613, 3590111338369894405, 2646640112163975771, 5966864698750134707]]
D_data = [[1785348659555163021, 3612773974290420260, 8587341808081935796, 4393730037042586815, 10490463205723658044], [10457678631610076741, 1645527195687648140, 13013316081830726847, 12925223531522879912, 5478687620744215372], [9878636900393157276, 13274969755872629366, 3231582918568068174, 7045188483430589163, 5126509884591016427], [4914941908205759200, 7480989013464904670, 5860406622199128154, 8016615177615097542, 13266674393818320551], [3005316032591310201, 6624508725257625760, 7972954954270186094, 5331046349070112118, 6127026494304272395]]
msg = b"\xcc]B:\xe8\xbc\x91\xe2\x93\xaa\x88\x17\xc4\xe5\x97\x87@\x0fd\xb5p\x81\x1e\x98,Z\xe1n`\xaf\xe0%:\xb7\x8aD\x03\xd2Wu5\xcd\xc4#m'\xa7\xa4\x80\x0b\xf7\xda8\x1b\x82k#\xc1gP\xbd/\xb5j"
P = GF(p)
C = Matrix(P, C_data)
D = Matrix(P, D_data)
eig_C = [k for k in C.eigenvalues() if k != 0][0]
eigs_D = [k for k in D.eigenvalues() if k != 0]
for target in eigs_D:
key = discrete_log(target, eig_C)
if C**key == D:
print(f"key ={key}")
real_key = pad(long_to_bytes(int(key)), 16)
aes = AES.new(real_key, AES.MODE_ECB)
flag = unpad(aes.decrypt(msg), 64).decode()
print(f"{flag}")
'''
key =5273966641785501202
LILCTF{Are_y0u_5till_4wake_que5t1on_m4ker!}
'''
Linear
Title
出题:未央
难度:中等
Just solving a system of equations.
idea
import os
import random
import signal
signal.alarm(10)
flag = os.getenv("LILCTF_FLAG", "LILCTF{default}")
nrows = 16
ncols = 32
A = [[random.randint(1, 1919810) for _ in range(ncols)] for _ in range(nrows)]
x = [random.randint(1, 114514) for _ in range(ncols)]
b = [sum(A[i][j] * x[j] for j in range(ncols)) for i in range(nrows)]
print(A)
print(b)
xx = list(map(int, input("Enter your solution: ").strip().split()))
if xx != x:
print("Oh, your linear algebra needs to be practiced.")
else:
print("Bravo! Here is your flag:")
print(flag)
这道题主要是求解欠定线性方程组 :$$ A \cdot x = b $$
- \(A\) 是一个 \(16 \times 32\) 的随机矩阵,元素范围在 \([1, 1919810]\)。
- \(x\) 是一个 \(32\) 维的未知向量,元素范围在 \([1, 114514]\)。
- \(b\) 是计算得到的 \(16\) 维结果向量。
可以构造一个格基矩阵M(维度为 \(33 \times 48\)):
- \(I_{32}\) 是 \(32 \times 32\) 的单位矩阵。
- \(A^T\) 是矩阵 \(A\) 的转置(\(32 \times 16\))。
- \(b^T\) 是向量 \(b\) 的转置(\(1 \times 16\))。
- \(K\) 是一个较大的常数(权重因子)。
- \(0\) 是 \(1 \times 32\) 的零向量。
我们计算向量 \(v\) 与矩阵 \(M\) 的乘积(即对 \(M\) 的行进行线性组合):
展开来看,结果向量的前 32 位和后 16 位分别是:
- 前 32 位:$$ \sum_{i=1}^{32} x_i \cdot e_i = (x_1, x_2, \dots, x_{32}) = x $$
(其中 \(e_i\) 是单位向量) - 后 16 位:$$ \sum_{i=1}^{32} x_i \cdot (K \cdot A^T_i) + 1 \cdot (-K \cdot b^T) = K \cdot (\sum_{i=1}^{32} x_i A^T_i - b^T) = K \cdot (Ax - b) $$
如果 \(x\) 是方程组的真实解,那么 \(Ax - b = 0\)。
对于真实解 \(x\),对应的格向量为:
exp
from pwn import *
from sage.all import *
import ast
context.log_level = 'info'
def solve():
io = remote('challenge.imxbt.cn', 31214)
io.recvuntil(b"[[")
A_data = ast.literal_eval("[[" + io.recvuntil(b"]]").decode())
io.recvuntil(b"\n[")
b_data = ast.literal_eval("[" + io.recvuntil(b"]").decode())
A = Matrix(ZZ, A_data)
b = vector(ZZ, b_data)
nrows, ncols = A.nrows(), A.ncols()
K = 10**9
M = block_matrix([
[matrix.identity(ncols), K * A.transpose()],
[matrix(ZZ, 1, ncols), matrix(ZZ, 1, nrows, [-K * val for val in b])]
], subdivide=False)
print("Running LLL...")
for row in M.LLL():
if row[ncols:].is_zero():
sol = row[:ncols]
if all(x > 0 for x in sol):
print(f"Found solution: {sol}")
io.sendlineafter(b"solution: ", " ".join(map(str, sol)).encode())
io.recvline()
print(f"flag: {io.recvall().decode().strip()}")
return
io.close()
if __name__ == "__main__":
solve()
Space Travel
Title
出题:糖醋小鸡块
难度:中等
Jump up!![[crypto-space-travel.zip]]
[!感谢以下师傅的wp以供参考:]
LilCTF 2025 - SeanDictionary | 折腾日记
2025 LilCTF Writeup • Swizzer's Sound
LILCTF2025 POFP wp - 霍雅的博客
idea
#task.py
from Crypto.Cipher import AES
from hashlib import md5
from params import vecs
from os import urandom
key = int("".join([vecs[int.from_bytes(urandom(2)) & 0xfff] for _ in range(50)]), 2)
print("🎁 :", [[nonce := int(urandom(50*2).hex(), 16), (bin(nonce & key).count("1")) % 2] for _ in range(600)])
print("🚩 :", AES.new(key=md5(str(key).encode()).digest(), nonce=b"Tiffany", mode=AES.MODE_CTR).encrypt(open("flag.txt", "rb").read()))
- 该题像是一个 LPN (Learning with Parity Noise) 问题,且噪声为0。目标是根据 600 组
(nonce, bit)线性关系,求解 800 位的密钥key。但 600 个线性方程,有 800 个未知数(密钥的比特位)。这是一个欠定方程组,直接求解会产生巨大的解空间,尝试暴力破解无果。 - 密钥
key的每个 16 位块k_i都来自params.py中的vecs列表,vecs列表包含 4096 (=2^12) 个向量。通过高斯消元分析发现,vecs并非一个 16 维空间,而是一个 12 维的 仿射子空间。 - 仿射子空间可视为一个线性子空间
L经向量v0平移后的结果 (V = {v0 + l | l ∈ L})。
* 任取v0 = vecs[0]。
* 构造线性空间L = {v ^ v0 | v ∈ vecs}。
* 对L求基,得到一个 12x16 的基矩阵M。 - 每个 16 位的密钥块
k_i都可以用一个 12 位的向量k'_i来表示:
k_i = v0 + M^T * k'_i - 将此代入原始 LPN 方程
bit = Σ <nonce_i, k_i>,得到:
bit ^ Σ <nonce_i, v0> = Σ <nonce_i, M^T * k'_i> = Σ (nonce_i * M^T) * k'_i - 将原 800 未知数的系统
A*k=b转换为了一个 600 未知数 (K' = [k'_0, ..., k'_49]) 的新系统A'*K'=b'。A'是一个 600x600 的矩阵,现在就可以直接求解获取flag了。
exp
import ast
from Crypto.Cipher import AES
from hashlib import md5
from params import vecs
def mat_mult(A, B):
rows_A, cols_A = len(A), len(A[0])
rows_B, cols_B = len(B), len(B[0])
if cols_A != rows_B: raise ValueError("Matrix dimensions do not match")
C = [[0] * cols_B for _ in range(rows_A)]
for i in range(rows_A):
for j in range(cols_B):
s = 0
for k in range(cols_A): s ^= A[i][k] & B[k][j]
C[i][j] = s
return C
def mat_transpose(M):
if not M: return []
return [[M[j][i] for j in range(len(M))] for i in range(len(M[0]))]
def gaussian_elimination_basis(vectors):
basis = []
for v in vectors:
for b in basis: v = min(v, v ^ b)
if v > 0:
basis.append(v)
basis.sort(reverse=True)
return basis
def solve_linear_system(A, b):
n = len(A)
m = [row[:] for row in A]
sol = list(b)
pivot_cols, free_cols = [], []
row = 0
for col in range(n):
if row >= n:
free_cols.append(col)
continue
pivot = row
while pivot < n and m[pivot][col] == 0: pivot += 1
if pivot < n:
m[row], m[pivot] = m[pivot], m[row]
sol[row], sol[pivot] = sol[pivot], sol[row]
for j in range(n):
if j != row and m[j][col] == 1:
for k in range(col, n): m[j][k] ^= m[row][k]
sol[j] ^= sol[row]
pivot_cols.append((row, col))
row += 1
else:
free_cols.append(col)
xp = [0] * n
for i, j in reversed(pivot_cols):
val = sol[i]
for k in range(j + 1, n): val ^= m[i][k] & xp[k]
xp[j] = val
null_space_basis = []
for free_col in free_cols:
nv = [0] * n
nv[free_col] = 1
for i, j in reversed(pivot_cols):
val = 0
for k in range(j + 1, n): val ^= m[i][k] & nv[k]
nv[j] = val
null_space_basis.append(nv)
return xp, null_space_basis
if __name__ == "__main__":
vecs_int = [int(v, 2) for v in vecs]
v0_int = vecs_int[0]
linear_space = [v ^ v0_int for v in vecs_int]
basis = gaussian_elimination_basis(linear_space)
M = [[(b >> i) & 1 for i in range(15, -1, -1)] for b in basis]
v0_bits = [(v0_int >> i) & 1 for i in range(15, -1, -1)]
Mt = mat_transpose(M)
with open('output.txt', 'r') as f: content = f.read()
leaked_data = ast.literal_eval(content.split("🎁 : ")[1].split("🚩 : ")[0].strip())
encrypted_flag = ast.literal_eval(content.split("🚩 : ")[1].strip())
A_prime, b_prime = [], []
for nonce_int, bit_res in leaked_data:
new_row, new_b_val = [], bit_res
for i in range(50):
nonce_block_int = (nonce_int >> (i * 16)) & 0xFFFF
nonce_block_bits = [[(nonce_block_int >> j) & 1 for j in range(15, -1, -1)]]
b_offset = 0
for j in range(16): b_offset ^= nonce_block_bits[0][j] & v0_bits[j]
new_b_val ^= b_offset
row_part = mat_mult(nonce_block_bits, Mt)
new_row.extend(row_part[0])
A_prime.append(new_row)
b_prime.append(new_b_val)
K_prime_particular, null_basis = solve_linear_system(A_prime, b_prime)
possible_solutions = [K_prime_particular]
if null_basis:
alt_solution = [(K_prime_particular[i] ^ null_basis[0][i]) for i in range(len(K_prime_particular))]
possible_solutions.append(alt_solution)
for k_prime_sol in possible_solutions:
key = 0
for i in range(50):
k_prime_i = [[bit] for bit in k_prime_sol[i*12 : (i+1)*12]]
m_times_k_prime = mat_mult(Mt, k_prime_i)
k_i_int = 0
for j in range(16):
if (v0_bits[j] ^ m_times_k_prime[j][0]) == 1:
k_i_int |= (1 << (15 - j))
key |= (k_i_int << (i * 16))
aes_key = md5(str(key).encode()).digest()
cipher = AES.new(key=aes_key, nonce=b"Tiffany", mode=AES.MODE_CTR)
try:
decrypted_flag = cipher.decrypt(encrypted_flag).decode('utf-8')
print(f"[*] Flag: {decrypted_flag}")
break
except UnicodeDecodeError:
continue
'''
[*] Flag: LILCTF{Un1qUe_s0luti0n_1N_sUbSp4C3!}
'''
baaaaaag
Title
出题:pirater
难度:中等
背包难,背包坏;背包易,人好。
idea
from Crypto.Util.number import *
import random
from Crypto.Cipher import AES
import hashlib
from Crypto.Util.Padding import pad
from secret import flag
p = random.getrandbits(72)
assert len(bin(p)[2:]) == 72
a = [getPrime(90) for _ in range(72)]
b = 0
t = p
for i in a:
temp = t % 2
b += temp * i
t = t >> 1
key = hashlib.sha256(str(p).encode()).digest()
cipher = AES.new(key, AES.MODE_ECB)
flag = pad(flag,16)
ciphertext = cipher.encrypt(flag)
print(f'a = {a}')
print(f'b = {b}')
print(f"ciphertext = {ciphertext}")
'''
a = [965032030645819473226880279, 699680391768891665598556373, 1022177754214744901247677527, 680767714574395595448529297, 1051144590442830830160656147, 1168660688736302219798380151, 796387349856554292443995049, 740579849809188939723024937, 940772121362440582976978071, 787438752754751885229607747, 1057710371763143522769262019, 792170184324681833710987771, 912844392679297386754386581, 906787506373115208506221831, 1073356067972226734803331711, 1230248891920689478236428803, 713426848479513005774497331, 979527247256538239116435051, 979496765566798546828265437, 836939515442243300252499479, 1185281999050646451167583269, 673490198827213717568519179, 776378201435505605316348517, 809920773352200236442451667, 1032450692535471534282750757, 1116346000400545215913754039, 1147788846283552769049123803, 994439464049503065517009393, 825645323767262265006257537, 1076742721724413264636318241, 731782018659142904179016783, 656162889354758353371699131, 1045520414263498704019552571, 1213714972395170583781976983, 949950729999198576080781001, 1150032993579134750099465519, 975992662970919388672800773, 1129148699796142943831843099, 898871798141537568624106939, 997718314505250470787513281, 631543452089232890507925619, 831335899173370929279633943, 1186748765521175593031174791, 884252194903912680865071301, 1016020417916761281986717467, 896205582917201847609656147, 959440423632738884107086307, 993368100536690520995612807, 702602277993849887546504851, 1102807438605649402749034481, 629539427333081638691538089, 887663258680338594196147387, 1001965883259152684661493409, 1043811683483962480162133633, 938713759383186904819771339, 1023699641268310599371568653, 784025822858960757703945309, 986182634512707587971047731, 1064739425741411525721437119, 1209428051066908071290286953, 667510673843333963641751177, 642828919542760339851273551, 1086628537309368288204342599, 1084848944960506663668298859, 667827295200373631038775959, 752634137348312783761723507, 707994297795744761368888949, 747998982630688589828284363, 710184791175333909291593189, 651183930154725716807946709, 724836607223400074343868079, 1118993538091590299721647899]
b = 34962396275078207988771864327
ciphertext = b'Lo~G\xf46>\xd609\x8e\x8e\xf5\xf83\xb5\xf0\x8f\x9f6&\xea\x02\xfa\xb1_L\x85\x93\x93\xf7,`|\xc6\xbe\x05&\x85\x8bC\xcd\xe6?TV4q'
'''
题目利用 72 位随机整数 p 的二进制位作为系数,结合 90 位素数数组 a 计算子集和 b。这是一个典型的低密度子集和问题 (Low-Density Subset Sum Problem)。
利用 格规约 (Lattice Reduction) 算法(如 BKZ)求解。
格构造
构造格矩阵 \(M\)(闭包格):
其中 \(I\) 为单位矩阵,\(N\) 为大权重。目标是寻找形如 \((2x_i-1, \dots, 0)\) 的短向量,其中 \(x_i \in \{0, 1\}\) 为 p 的二进制位。
使用 BKZ 算法规约矩阵,提取短向量还原 p,计算出key,再进行AES-ECB解密得到flag。
exp
from sage.all import *
import hashlib
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
a = [965032030645819473226880279, 699680391768891665598556373, 1022177754214744901247677527, 680767714574395595448529297, 1051144590442830830160656147, 1168660688736302219798380151, 796387349856554292443995049, 740579849809188939723024937, 940772121362440582976978071, 787438752754751885229607747, 1057710371763143522769262019, 792170184324681833710987771, 912844392679297386754386581, 906787506373115208506221831, 1073356067972226734803331711, 1230248891920689478236428803, 713426848479513005774497331, 979527247256538239116435051, 979496765566798546828265437, 836939515442243300252499479, 1185281999050646451167583269, 673490198827213717568519179, 776378201435505605316348517, 809920773352200236442451667, 1032450692535471534282750757, 1116346000400545215913754039, 1147788846283552769049123803, 994439464049503065517009393, 825645323767262265006257537, 1076742721724413264636318241, 731782018659142904179016783, 656162889354758353371699131, 1045520414263498704019552571, 1213714972395170583781976983, 949950729999198576080781001, 1150032993579134750099465519, 975992662970919388672800773, 1129148699796142943831843099, 898871798141537568624106939, 997718314505250470787513281, 631543452089232890507925619, 831335899173370929279633943, 1186748765521175593031174791, 884252194903912680865071301, 1016020417916761281986717467, 896205582917201847609656147, 959440423632738884107086307, 993368100536690520995612807, 702602277993849887546504851, 1102807438605649402749034481, 629539427333081638691538089, 887663258680338594196147387, 1001965883259152684661493409, 1043811683483962480162133633, 938713759383186904819771339, 1023699641268310599371568653, 784025822858960757703945309, 986182634512707587971047731, 1064739425741411525721437119, 1209428051066908071290286953, 667510673843333963641751177, 642828919542760339851273551, 1086628537309368288204342599, 1084848944960506663668298859, 667827295200373631038775959, 752634137348312783761723507, 707994297795744761368888949, 747998982630688589828284363, 710184791175333909291593189, 651183930154725716807946709, 724836607223400074343868079, 1118993538091590299721647899]
b = 34962396275078207988771864327
ciphertext = b'Lo~G\xf46>\xd609\x8e\x8e\xf5\xf83\xb5\xf0\x8f\x9f6&\xea\x02\xfa\xb1_L\x85\x93\x93\xf7,`|\xc6\xbe\x05&\x85\x8bC\xcd\xe6?TV4q'
target_b = b - a[71]
target_a = a[:71]
n = 71
M = Matrix(ZZ, n + 1, n + 1)
N = int(2**90) # Large weight
for i in range(n):
M[i, i] = 2
M[i, n] = N * target_a[i]
for i in range(n):
M[n, i] = 1
M[n, n] = N * target_b
L = M.BKZ(block_size=24)
for row in L:
if row[n] == 0:
e = [row[i] for i in range(n)]
if all(abs(val) == 1 for val in e):
# Candidate 1
x1 = [(val + 1) // 2 for val in e]
if sum(x1[i] * target_a[i] for i in range(n)) == target_b:
p_bits = x1 + [1]
p_val = sum(bit * (2**i) for i, bit in enumerate(p_bits))
print(f"Recovered p: {p_val}")
key = hashlib.sha256(str(p_val).encode()).digest()
cipher = AES.new(key, AES.MODE_ECB)
print(f"Flag: {unpad(cipher.decrypt(ciphertext), 16).decode()}")
break
'''
Recovered p: 4208626653103825685156
Flag: LILCTF{M4ybe_7he_brut3_f0rce_1s_be5t}
'''
misc
[WARM UP] 签到!真实世界的过期问卷
Title
如果放在 LilCTF Misc,本题难度可定级为签到
Real World - LilCTF 正赛在多个方向也有由真实项目出成、不是为了出题而故意构造的题目
LilRan 希望得到尽可能多的答卷,所以他在提交后显示的提示语中放了一个 flag。但他竟然把问卷截止时间设置错了!你能在未与 LilRan 取得联系的情况下,通过巧妙的手段获取到 flag 吗?
https://wj.qq.com/s2/19564942/390b/
idea
问卷是腾讯问卷,腾讯问卷的设计/开发者将提示语作为问卷基本信息的一部分,在meta路由的响应中。按F12找到这个报文。
![[Pasted image 20251122113424.png]]
是谁没有阅读参赛须知?
Title
请看比赛描述找到一个匹配 LILCTF{.+?} 的字符串,将其提交
idea
正则表达式解释:
LILCTF{:字面量匹配。会精确匹配字符串 "LILCTF{"。.:匹配任意字符(除了换行符,除非使用了re.DOTALL标志)。+:量词,表示匹配前面的字符(这里是.,即任意字符)至少一次。?:量词,表示非贪婪匹配。会尽可能少地匹配前面的字符。如果没有?,+会尽可能多地匹配字符,直到字符串的末尾,然后再回溯到最后一个 "}"。}:字面量匹配。会精确匹配字符 "}"在比赛描述找到匹配 LILCTF{.+?} 的字符串
LILCTF{Me4n1ngFu1_w0rDs}
![[Pasted image 20251122113609.png]]
PNG Master
Title
出题:YanHuoLG
难度:简单
提到隐写,你能想到哪些常见的隐写方式呢?不过我相信misc手的脑洞一定能想到某个基于最低有效位实现的隐写方法吧?哦对了,我可不认为扩展名也是文件名的一部分。(比C3ngH简单)
idea
拿到附件,里面是一张png图片,先丢到随波逐流简单分析:
![[Pasted image 20251122124552.png]]
发现可疑编码:
6K6p5L2g6Zq+6L+H55qE5LqL5oOF77yM5pyJ5LiA5aSp77yM5L2g5LiA5a6a5Lya56yR552A6K+05Ye65p2lZmxhZzE6NGM0OTRjNDM1NDQ2N2I=
base64解码即可得到:
让你难过的事情,有一天,你一定会笑着说出来flag1:4c494c4354467b
对图片LSB分析时,同样发现可疑字符串在RGB中:
![[Pasted image 20251122133205.png]]
使用Stegsolve进行LSB分析发现:
5Zyo5oiR 5Lus5b+D6YeM77yM 5pyJ5LiA5Z2X5Zyw5pa55piv5peg5rOV6ZSB5L2P55qE77yM 6YKj5Z2X5Zyw5pa55Y+r5YGa5biM5pyb ZmxhZzI6NTkzMDc1NWYzNDcyMzM1ZjRk
![[Pasted image 20251122133333.png]]
base64解密:
在我们心里,有一块地方是无法锁住的,那块地方叫做希望flag2:5930755f3472335f4d
在对图片块分析中发现存在IDAT隐写,通过脚本在图片原本IDAT数据块后新添加了一块zilb压缩后的数据。IDAT块是png储存实际图像数据的数据块,每一块IDAT块的结构都是固定的且在填满上一个数据块前,不会填写新的数据块。由此我们可以知道,每一个正常的png文件中只有最后一块IDAT块数据长度与之前的块不符。回到题目,在使用随波逐流查看数据之后,我们发现在上一个块没填满的情况下,又新增了一个块。显而易见,这个块是人为后添加的,所以我们只需要将块分离出来,再使用zilb解压数据,就能得到我们想要的东西了。
![[Pasted image 20251122124836.png]]
这里我用到了官方给到的脚本:
import zlib
import struct
import sys
def extract_hidden_data(png_file, output_file):
try:
with open(png_file, 'rb') as f:
data = f.read()
if data[0:8] != b'\x89PNG\r\n\x1a\n':
raise ValueError("无效的PNG文件头")
idx = 8
idat_chunks = []
last_idat_position = None
while idx < len(data):
if idx + 12 > len(data):
break
chunk_len = struct.unpack(">I", data[idx:idx+4])[0]
chunk_type = data[idx+4:idx+8]
chunk_end = idx + 12 + chunk_len
if chunk_end > len(data):
break
if chunk_type == b'IDAT':
idat_chunks.append({
'position': idx,
'length': chunk_len,
'data': data[idx+8:idx+8+chunk_len],
'end': chunk_end
})
last_idat_position = idx
if chunk_type == b'IEND':
break
idx = chunk_end
if not idat_chunks:
raise ValueError("未找到IDAT块")
if len(idat_chunks) < 2:
raise ValueError("未找到数据块")
hidden_chunk = idat_chunks[-1]
calculated_crc = zlib.crc32(b'IDAT' + hidden_chunk['data'])
stored_crc = struct.unpack(">I", data[hidden_chunk['end']-4:hidden_chunk['end']])[0]
if calculated_crc != stored_crc:
raise ValueError(f"CRC校验失败")
try:
hidden_data = zlib.decompress(hidden_chunk['data'])
except zlib.error as e:
raise ValueError(f"解压失败: {str(e)}")
with open(output_file, 'wb') as out:
out.write(hidden_data)
return len(hidden_data)
except Exception as e:
print(f"提取错误: {str(e)}")
return 0
if __name__ == "__main__":
if len(sys.argv) != 3:
print("用法: python extract.py <PNG文件> <输出文件>")
sys.exit(1)
png_file = sys.argv[1]
output_file = sys.argv[2]
extracted_size = extract_hidden_data(png_file, output_file)
if extracted_size > 0:
print(f"成功提取")
else:
print("提取失败")
提取处理能到得一个zip压缩包
![[Pasted image 20251122132657.png]]
根据hint里的文字提示“救赎之道,就在其中”和记事本显示有65个字符,可以推测出使用了零宽字符隐写“
![[Pasted image 20251122132811.png]]
零宽字符解密:
![[Pasted image 20251122132912.png]]
将secret.bin与secret xor即可得到flag3:
flag3:61733765725f696e5f504e477d
![[Pasted image 20251122132945.png]]
最终flag:LILCTF{Y0u_4r3_Mas7er_in_PNG}
v我50(R)MB
Title
出题:ZianTT, LilRan
难度:简单
在预热阶段,LilRan 发现他的定时备份数据量增加得比预期快,原来是有人上传了一张 50MB 超清无码(指不含二维码)蓝光(指蓝色的灯光)队伍头像!
LilRan 下载到该头像后,立即在他的电脑上转成了更小的 webp 格式,并更新了服务器数据库中的相关数据。但慌乱之中,LilRan 忘了把 webp 文件放到服务器上原来的路径,导致原本的头像还在,却显示不出来了……
虽然正常用户看上去这个头像无法显示,但是 ZianTT 告诉 LilRan,就算不在服务端做更多操作,他也完完整整地获得了原本 50MB 的队伍头像。
以上是故事。本题是一个对当时情况的仿制环境,题目中未使用 A1CTF 代码。题目中的“头像”为 flag 图片,大小约为 1MB。
A1CTF 平台现已限制上传头像文件大小,并进行二次渲染。你无需检查 A1CTF 源码的处理方式,服务端的补丁与本题无关。你只需要在用户侧解决此题目。
给快速修好平台的 CarboFish 师傅加鸡腿🍗
idea
参考官方题解:v我50(R)MB - 飞书云文档
(博主太懒了.......
提前放出附件
Title
出题:C3ngH
难度:简单
还记得25年3月13日的那个晚上,Misc手们都在干嘛吗
idea
复现时下载的附件文件名变了(需要用到
参考官方题解:提前放出附件 - 飞书云文档

浙公网安备 33010602011771号