Overview
The challenge encodes the flag body as a vector of base-33 digits
and plants it as one row of a small random matrix A (57 rows, 88 columns). It
publishes only the product
h = a * A (mod p)
for a secret row vector a and a 1084-bit prime p, and withholds both a and
A. The goal is to recover the planted row.
The digit vector
1base = B + 1 # 33
2while x:
3 row.append(x % base)
4 x //= base
So the planted row f satisfies x = sum(f[j] * 33**j) with every coordinate in
[0, 32]. Recovering f and reading it as base-33 gives the flag body.
Vulnerability
Work in the lattice of integer vectors that are consistent with the single published modular relation:
L = { v in Z^88 : <h, v> = 0 (mod p) }
Every integer null-vector of A lies in L, because A v = 0 implies
<h, v> = a A v = 0. With 57 rows and 88 columns, A has expected nullity
88 - 57 = 31, and those 31 null-vectors are short since every entry
of A is small. Any vector that only satisfies the modular relation is huge by
comparison, on the order of p. on L therefore recovers exactly the 31
short null-relations.
Insight
Two rounds of LLL. The first recovers the null-space of the hidden matrix as
short vectors. Stack them into Y. Every original row of A is orthogonal to
Y, so the integer right kernel of Y contains the row lattice of A. A
second LLL on that kernel produces short vectors in the hidden row lattice. One
of them, up to sign and small combinations, has all 88 coordinates in [0, 32]
and decodes to printable text.
Solver
1# SageMath, run from the challenge directory
2inv = inverse_mod(h[0], p)
3rows = [[p] + [0] * (m - 1)]
4for j in range(1, m):
5 v = [0] * m
6 v[0] = ZZ((-h[j] * inv) % p)
7 if v[0] > p // 2:
8 v[0] -= p
9 v[j] = 1
10 rows.append(v)
11
12K = Matrix(ZZ, rows).LLL(delta=0.999)
13short = [v for v in K.rows() if v.norm() < 10000] # ~31 of these
14
15Y = Matrix(ZZ, short).row_space().basis_matrix()
16R = Y.right_kernel_matrix().LLL(delta=0.999) # hidden row lattice
17
18def decode(v):
19 if min(v) < 0 or max(v) > B:
20 return None
21 x = sum(ZZ(v[i]) * ZZ(B + 1) ** i for i in range(m))
22 return int(x).to_bytes((int(x).bit_length() + 7) // 8, 'big')
23
24# search the short basis, its signs, and modest pairwise combinations
25candidates = list(R.rows())
26for i in range(R.nrows()):
27 for j in range(i):
28 candidates += [R[i] + R[j], R[i] - R[j], -R[i] + R[j], -R[i] - R[j]]
29for v in candidates:
30 for w in (v, -v):
31 msg = decode(w)
32 if msg and (b'TFCCTF' in msg or all(32 <= c < 127 for c in msg)):
33 print('CANDIDATE', msg)
$sage solve_math_or_meth.sage short modular relations: 31 relation rank: 31 row lattice: 57 88 CANDIDATE b'this_is_a_very_very_long_flag_for_a_short_ctf_chall_ggs'
The challenge source wraps the recovered body in TFCCTF{}.
Flag
TFCCTF{this_is_a_very_very_long_flag_for_a_short_ctf_chall_ggs}