UMDCTF 2026 - Weave
The weavers of the Gabidulin code have woven a disguise. But the shuttle leaves a trace, and the frays can only hide so much. Unravel the warp to find the secret.
加比杜林码的织者们编织了一层伪装。但梭子留下了痕迹,毛边只能遮掩有限的信息。解开经线,找到秘密。
Challenge files: challenge.sage
providing the public parameters warp, bolt,
shuttle, knot, pegs,
N, K, and an AES-GCM encrypted flag.
前置知识
本题涉及秩度量码(Rank-Metric Codes),属于编码论(Coding Theory)在秩度量下的分支。以下概念有助于理解:
| 概念 | 说明 |
|---|---|
| 有限域 GF(2^43) | 扩体,每个元素可看作 43-bit 向量 |
| Frobenius 自同构 | σ(x) = x²,Gabidulin 生成矩阵的构建基础 |
| 秩距离 vs Hamming 距离 | 前者看矩阵的秩,后者数不同符号个数 |
| Reed-Solomon 码 | 消息→多项式求值→码字,Gabidulin 的类比基础 |
| Gabidulin 码 | RS 在秩度量下的对应:线性化多项式 + Moore 矩阵 |
| MRD 特性 | 最小秩距离 d = N - K + 1,解码半径 t = ⌊(N-K)/2⌋ |
| 积秩攻击 | rank(A·B) ≤ rank_F₂(A) × dim(span(B)) |
Gabidulin Codes in a Nutshell
A Gabidulin code is a linear rank-metric code over an extension field \(\mathbb{F}_{q^m}\). Its generator matrix is a Vandermonde-like matrix (called a Moore matrix) built from linearly independent elements \(g_0, \dots, g_{N-1} \in \mathbb{F}_{q^m}\):
\[ G = \begin{pmatrix} g_0 & g_1 & \cdots & g_{N-1} \\ g_0^{[1]} & g_1^{[1]} & \cdots & g_{N-1}^{[1]} \\ \vdots & \vdots & \ddots & \vdots \\ g_0^{[K-1]} & g_1^{[K-1]} & \cdots & g_{N-1}^{[K-1]} \end{pmatrix} \]
where \(x^{[i]} = x^{q^i}\) is the \(i\)-th Frobenius power. Gabidulin codes can uniquely decode up to \(\left\lfloor\frac{N-K}{2}\right\rfloor\) rank errors, making them the rank-metric analogue of Reed-Solomon codes.
Key difference from RS: RS uses distinct evaluation points and counts symbol errors (Hamming distance). Gabidulin uses \(\mathbb{F}_q\)-linearly independent evaluation points and measures error by rank — meaning an error vector like \((e, e, e, \dots, e)\) has rank 1 even if every symbol is wrong.
The Challenge Disguise
The challenge constructs a loom — the true Gabidulin generator matrix over \(\mathbb{F}_{2^{43}}\) with parameters \(N=40\), \(K=8\):
1 | loom = Matrix(Fqm, K, N, lambda j, i: qpow(pegs[i], j)) |
Here pegs are \(N\)
linearly independent elements of \(\mathbb{F}_{2^{43}}\). The codeword is
\(s \cdot \mathsf{loom}\) where \(s \in \mathbb{F}_{2^{43}}^K\) is the secret
message.
The disguise is a similarity transform:
\[ \mathsf{warp} = \mathsf{knot} \cdot \mathsf{loom} \cdot \mathsf{shuttle}^{-1} \]
\[ \mathsf{bolt} = s \cdot \mathsf{warp} + \mathsf{frays} \]
All matrices \(\mathsf{knot}\),
\(\mathsf{shuttle}\), and the list of
pegs are published to the player. At first
glance this seems to completely mask the underlying Gabidulin structure
— but the key insight is that when the attacker knows the masking
matrices, the disguise is fully reversible.
The Key Insight: Low-Rank Structure of the Masking
The vulnerability comes from how \(\mathsf{shuttle}\) is constructed. Its
entries are drawn from the span of only FIBER_D=3 linearly
independent elements over \(\mathbb{F}_{2^{43}}\). This means:
\[ \mathsf{shuttle} \in \mathsf{Span}\{\alpha_1, \alpha_2, \alpha_3\}^{N \times N} \]
Consequently, multiplying an error vector by \(\mathsf{shuttle}^{-1}\) is not well-defined over the integers — but multiplying \(\mathsf{bolt}\) by \(\mathsf{shuttle}\) is the right direction:
\[ \mathsf{bolt} \cdot \mathsf{shuttle} = s \cdot \mathsf{knot} \cdot \mathsf{loom} + \mathsf{frays} \cdot \mathsf{shuttle} \]
Let \(s' = s \cdot \mathsf{knot}\) and \(\mathsf{error} = \mathsf{frays} \cdot \mathsf{shuttle}\).
The error \(\mathsf{frays}\) has
rank at most FRAYS=5 over \(\mathbb{F}_2\) (not over \(\mathbb{F}_{2^{43}}\) — this is a much
stronger constraint, because \(\mathbb{F}_{2^{43}}\) elements unfold to
43-bit vectors over \(\mathbb{F}_2\)).
And because \(\mathsf{shuttle}\)
entries come from a 3-dimensional subspace, the product \(\mathsf{frays} \cdot \mathsf{shuttle}\) has
rank at most:
\[ \mathsf{rank}(\mathsf{frays} \cdot \mathsf{shuttle}) \le \mathsf{FRAYS} \cdot \mathsf{FIBER\_D} = 5 \times 3 = 15 \]
Why this inequality holds: Write \(\mathsf{shuttle} = \sum_{t=1}^{3} b_t \cdot S^{(t)}\) where \(b_t\) are the subspace basis and \(S^{(t)}\) have \(\mathbb{F}_2\) entries. Then \(\mathsf{frays} \cdot \mathsf{shuttle} = \sum b_t \cdot (\mathsf{frays} \cdot S^{(t)})\), a sum of 3 matrices each with \(\mathbb{F}_2\)-rank ≤ 5. The product rank is bounded by \(5 \times 3 = 15\).
The Gabidulin unique decoding radius for \(N=40, K=8\) is:
\[ \left\lfloor\frac{40 - 8}{2}\right\rfloor = 16 \]
Since \(15 < 16\), the error is within the unique decoding radius. We can recover \(s'\) uniquely.
Solution
The solve script is straightforward:
Remove the disguise: Compute \(\mathsf{received} = \mathsf{bolt} \cdot \mathsf{shuttle}\) to obtain a true Gabidulin codeword plus low-rank error.
Build the original loom: Reconstruct \(\mathsf{loom}\) from
pegsusing the same lambda.Decode: Use a Gabidulin decoder (e.g., the Gao-style decoder or the standard rank-metric syndrome decoder) to recover \(s'\) from \(\mathsf{received}\) given \(\mathsf{loom}\).
Recover the secret: Multiply by \(\mathsf{knot}^{-1}\): \[s = s' \cdot \mathsf{knot}^{-1}\]
Derive the AES key: Hash the secret's byte representation with SHA-256 and take the first 16 bytes:
1
key = sha256(secret_bytes).digest()[:16]
Decrypt the flag: Use AES-GCM with this key and the nonce/tag provided in the challenge output.
Solve Script
1 | from hashlib import sha256 |
A complete standalone implementation of the Gabidulin Gao decoder can also be written from scratch using the standard algorithm — the core operation is solving a linearized polynomial interpolation problem using the \(q\)-polynomial Euclidean algorithm.
Why This Works
The fundamental trick is the rank structure
mismatch. The error frays is low-rank over \(\mathbb{F}_2\), and shuttle is
low-rank over \(\mathbb{F}_{2^{43}}\)
(its entries live in a 3-dimensional subspace). The product amplifies
the rank, but only multiplicatively — and the original Gabidulin code
parameters were chosen so that this amplified rank still falls within
the unique decoding radius:
\[5 \times 3 = 15 < 16 = \left\lfloor\frac{40-8}{2}\right\rfloor\]
Moreover, because all masking matrices are
published, there is no secret hiding the code. The disguise is
a form of key-committing encryption without the key being
secret — anyone can peel it off. The true security would require keeping
at least one of \(\mathsf{knot}\),
\(\mathsf{shuttle}\), or the
pegs private.