AliCTF 2026 Crypto - Griffin
Griffin 这题的关键是 Phase1 恢复的 $x$ 存在仿射不确定性,导致 Phase2 求根得到的是 $x = a m + b$ 而不是 $m$,需要对 $(a,b)$ 做 affine 枚举。
结论
- flag:
alictf{**} - 关键坑点: Phase1 恢复的 $x$ 存在仿射不确定性,导致 Phase2 求根得到的是 $x = a m + b$ 而非 $m$,必须做 affine 枚举,最终命中 $(a,b)=(-1,256)$
Full solve code (gist)
复现参数
以下数值来自一次成功复现的检查点和附件内容:
output_sha256 = 008fa1904e2b86bada59bc94fccb8c9b65767b115a13225fd5160cdc08a78b8b- 曲线参数: $p = 119233298728349195870188085271656035445451492895027,\ a=1,\ b=0$
- 群阶:
- $n = 29808324682087298967547021317914008861362873223757$
- $n$ 的素因子:
66179, 67247, 70271, 76679, 78803, 86693, 107791, 116273, 119677, 121309
- Phase1 取 $q = 121309$
- 附件给出的
flagct = 9412139614776358237302032187121621394441766988270 - Phase2 根枚举得到
x_candidates数量 432
题目结构
附件 path/chal.sage 的核心逻辑如下。
- 生成 flag:
FLAG = "alictf{"+str(uuid4())+"}" - 参数: $m=80, d=20, k=250$
- 椭圆曲线:
- $E = \mathrm{EllipticCurve}(\mathbb{F}_p, [a,b])$
- 基点
G = E.lift_x(3137) - 群阶 $n = \mathrm{ord}(G)$
- 选择 $2d=40$ 个横坐标:
- $x_i \in \{1,\dots,256\}$,共 40 个且去重
- 生成 80 个随机次数 $
- 输出点阵:
- Hawk: 40 行,每行 80 列,元素为 $(f_j(x_i) G).xy()$
- Lion: 250 行随机点,每行 80 列
- Griffin = Hawk + Lion,然后 shuffle,共 290 行
- 输出:
Griffin = [[(x,y), ...], ...]维度 290 × 80flagct = f_0(m)其中 $m = \text{int}(\mathrm{hex}(\mathrm{ASCII}(FLAG)), 16)$
- 输出点阵:
目标: 从输出恢复 FLAG。
输出文件格式
附件 path/output.txt 有两行:
Griffin = [[(...), ...], ...]flagct = ...
其中 Griffin 每个元素是点坐标 $(x,y)$ 的整数对,但 $p,a,b$ 没给出,需要先恢复曲线参数。
Phase0 恢复曲线模数 p 和参数 a b
曲线满足 $y^2 \equiv x^3 + a x + b \pmod p$。
对任意点 $(x_i,y_i)$ 定义
$$ r_i = y_i^2 - x_i^3 $$则有
$$ r_i \equiv a x_i + b \pmod p $$这是关于 $x$ 的一次函数。用 3 个点可以消去 $a,b$,构造必为 0 的表达式:
$$ t = (r_1 - r_2)(x_2 - x_3) - (r_2 - r_3)(x_1 - x_2) \equiv 0 \pmod p $$所以对很多三元组计算 $t$,取 $\gcd$,就能得到 $p$ 或其倍数。实测很快收敛为 167-bit 素数。
得到 $p$ 后,用两个 $x$ 不同的点即可解出 $a,b$:
$$ a \equiv (r_1-r_2)(x_1-x_2)^{-1} \pmod p,\quad b \equiv r_1 - a x_1 \pmod p $$本题恢复到:
- $a=1, b=0$
随后用 Sage 构造曲线 $E$ 并取 G = E.lift_x(3137),求 $n=\mathrm{ord}(G)$ 得到 165-bit 群阶。
Phase1 分离 40 行 Hawk 并恢复 xs
结构观察
Hawk 部分的 40 行满足:
$$ H_{i,j} = f_j(x_i) \pmod n $$其中 $f_j$ 的次数 $<20$。将这些值视为模某个素数 $q \mid n$ 的元素,则 40 × 80 的矩阵 $H$ 的秩满足:
$$ \mathrm{rank}(H) \le 20 $$原因是每一列都是次数 $<20$ 多项式在 40 个点上的取值向量,列空间维度最多 20。
Lion 行随机,几乎不可能落入 Hawk 行张成的低维子空间。
选择小素数模 q
直接在 $\mathbb{Z}_n$ 上做线性代数不方便。因为 $n$ 可分解且最大素因子约 $10^5$,取
$$ q = \max\{p_i : p_i \mid n\} $$这样可以在 $\mathbb{F}_q$ 上做所有矩阵操作,同时离散对数也能用表暴力。
本题 $q=121309$。
离散对数到标量矩阵 A
对每个输出点 $P=(x,y)$:
- 将其还原成曲线点 $P \in E(\mathbb{F}_p)$
- 投影到阶为 $q$ 的子群: $$ P_q = (n/q)\, P $$
- 预先建表 $\{k G_q\}_{k=0}^{q-1}$,其中 $G_q = (n/q)\, G$
- 查表得到 $k = \log_{G_q}(P_q) \in \mathbb{F}_q$
从而得到 290 × 80 的矩阵 $A$,元素是离散对数值 mod $q$。
找到 Hawk 行的低维子空间
做法是“随机找一组 80 行当作基,找一个外部行的稀疏线性表示”,这类思路本质是在找 matroid 的小 circuit。
实现要点:
- 随机抽 80 行形成方阵 $B$,若 $B^T$ 在 mod $q$ 可逆,则对外部行 $v$ 有 $$ v = B^T c \Rightarrow c = (B^T)^{-1} v $$
- 如果 $v$ 属于某个低维结构子空间,系数向量 $c$ 更可能出现较小的汉明重量
- 一旦找到稀疏 $c$,其 support 对应的行集合通常包含足够多 Hawk 行信息
- 用这些行生成一个行空间基,然后遍历所有 290 行做 membership test,得到恰好 40 个 inliers
最终拿到 inlier_rows 大小为 40。
恢复 xs 的仿射代表
在 inliers 的 40 × 80 子矩阵里,列空间是次数 $<20$ 多项式的取值空间。这个空间在 $x$ 做仿射变换 $x \mapsto \alpha x + \beta$ 时保持不变,因此只能恢复 xs 的一个仿射代表。
脚本做法:
- 取列空间基 $B_c \in \mathbb{F}_q^{40 \times 20}$
- 构造整数格: $$ \Lambda = \{ q z + B_c y : z \in \mathbb{Z}^{40}, y \in \mathbb{Z}^{20} \} \subset \mathbb{Z}^{40} $$
- BKZ 找短向量 $v$,使其分量在 $[-q/2,q/2]$ 内尽量小
- 试图把 $v$ 归一化成 $v_i \equiv a X_i + b \pmod q$,并要求 $X_i \in [1,256]$ 且互异
得到一组 xs,并在日志里记录 xs_min 和 xs_max。
内存布局 Flag 字节布局
本题 $FLAG$ 是 44 字节 ASCII 字符串:
alictf{xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx}
其中:
- 总长度 $L=44$,索引为 $0..43$
- 固定位置:
- 0..6 是
alictf{ - 43 是
} - 15,20,25,30 是
- - 21 是
4代表 UUID v4 - 26 是 variant 位,只允许
8,9,a,b
- 0..6 是
- 其余 31 个位置是十六进制字符
0..9a..f
按位置范围展开更直观:
| 索引范围 | 内容 | 约束 |
|---|---|---|
| 0..6 | alictf{ |
固定 |
| 7..14 | hex | 0..9a..f |
| 15 | - |
固定 |
| 16..19 | hex | 0..9a..f |
| 20 | - |
固定 |
| 21 | 4 |
固定 |
| 22..24 | hex | 0..9a..f |
| 25 | - |
固定 |
| 26 | variant | 8,9,a,b |
| 27..29 | hex | 0..9a..f |
| 30 | - |
固定 |
| 31..42 | hex | 0..9a..f |
| 43 | } |
固定 |
对应脚本里的两个关键结构:
var_pos共 31 个位置:
[7, 8, 9, 10, 11, 12, 13, 14,
16, 17, 18, 19,
22, 23, 24,
26, 27, 28, 29,
31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42]
fixed只包含固定字节位置,常见形式:
0..6 = alictf{
15,20,25,30 = -
21 = 4
43 = }
将其按 big endian 解释为整数 $m$:
$$ m = \sum_{i=0}^{L-1} b_i \cdot 256^{L-1-i} $$附件代码 chal.sage 里 int(FLAG.encode().hex(), 16) 等价于 bytes_to_long(FLAG.encode()),即同一个 $m$。
我们最终在 Phase3 里使用模方程:
$$ m \equiv r \pmod n $$其中 $r$ 来自 Phase2 的根枚举并经过 affine 反变换得到。
Phase2 恢复 f0 并求根得到 432 个候选
完整离散对数 fs0(xs_i) mod n
Phase1 给出 40 行 Hawk 的索引。取它们的第 0 列点:
$$ P_i = f_0(x_i) G $$目标是求出 $y_i = f_0(x_i) \pmod n$。
因为 $n$ 分解为 10 个约 $10^5$ 的素数:
$$ n = \prod_{j=1}^{10} p_j $$可以对每个 $p_j$ 建一个大小 $p_j$ 的离散对数表,得到 $y_i \bmod p_j$,再用 CRT 合并回模 $n$ 的结果。
日志里实际素因子为:
66179, 67247, 70271, 76679, 78803, 86693, 107791, 116273, 119677, 121309
插值 f0
在环 $\mathbb{Z}_n[x]$ 上用拉格朗日插值恢复次数 $<20$ 的多项式 $f_0$。
使用前 20 对点 $(x_i,y_i)$ 做插值,并用全部 40 点验证:
$$ f_0(x_i) \equiv y_i \pmod n $$求根
题目给出:
$$ flagct \equiv f_0(m) \pmod n $$移项:
$$ g(x) = f_0(x) - flagct $$要解 $g(x)\equiv 0 \pmod n$。
做法:
- 分解 $n=\prod p_j$
- 在每个 $\mathbb{F}_{p_j}$ 上求 $g(x)=0$ 的根集合 $R_j$
- CRT 枚举所有组合
本题每个素数模上的根数为:
1, 1, 1, 3, 3, 3, 1, 2, 4, 2
所以总候选数:
$$ 1\cdot 1\cdot 1\cdot 3\cdot 3\cdot 3\cdot 1\cdot 2\cdot 4\cdot 2 = 432 $$得到 x_candidates 长度 432。
Phase3 仿射不确定性与 gap 嵌入
仿射不确定性来源
Phase1 中恢复 xs 只能确定到仿射变换,意味着我们恢复到的插值多项式实际上可能是:
$$ f_0^*(x) = f_0(a^{-1}(x-b)) $$因此 Phase2 解出的根对应的是:
$$ x = a m + b \pmod n $$而我们真正需要的是 $m$ 的 residue。
如何枚举 affine
xs 真值范围被限制在 $[1,256]$,我们拿到的 xs 也在这个范围内。由此可推断仿射参数只能是很小的集合。
脚本枚举:
- $a \in \{1,-1\}$
- $b$ 取值范围由“使得仿射变换后仍落在 $[1,256]$”约束推导
对每个 $(a,b)$:
设 Phase2 求得的根集合为 $X \subset \mathbb{Z}_n$。对每个 $(a,b)$ 定义预像集合:
$$ R = \{ (x-b)\,a^{-1} \bmod n : x \in X \} $$脚本里对应把 x_candidates 变换为 m_candidates,然后对每个 $r \in R$ 尝试恢复完整 flag 字符串。
本题最终命中:
- $a=-1, b=256$
gap 字符建模
对 31 个未知 hex 字符分两类:
- digit:
0..9,以'5'为中心,偏移 $s \in [-5,4]$ - alpha:
a..f,以'c'为中心,偏移 $s \in [-2,3]$
再用 $e \in \{\pm\delta\}$ 表示选择:
- $e=-\delta$ 表示 digit
- $e=+\delta$ 表示 alpha
这样每个未知字节都由 $(e_i,s_i)$ 表示,且都很小。
模方程展开
令未知字节集合索引为 $i=0..k-1$,本题 $k=31$。
设第 $i$ 个未知字节所在位置为 $p_i$。脚本中用 var_pos 保存所有 $p_i$。
对应的系数为
$$ a_i = 256^{L-1-p_i} \bmod n $$固定字节的整体贡献记为 $m_fixed$,脚本中称为 fixed_mod。
模方程为:
$$ \sum_i a_i \cdot \mathrm{byte}_i \equiv r - m_fixed \pmod n $$将所有未知字节先假设为 '5',把常数项移到右侧,得到一个形如:
其中 $\Delta\mathrm{byte}_i$ 可以由 $e_i,s_i$ 线性表示,且两者都很小。
格向量布局
gap 嵌入的格维度为:
$$ dim = 2k + 2 = 64 $$向量坐标布局:
- 第 0 维: 模方程残差坐标
- 第 1..k: $e_0..e_{k-1}$
- 第 1+k..1+2k: $s_0..s_{k-1}$
- 最后一维: embedding 的 $\pm\delta$ 标记
基矩阵布局
设 $K$ 为权重因子,实际用 $K=2^{40}$。
基矩阵由以下行组成:
- 对每个 $i$ 的 e 行:
- 第 0 维放 $a_i(p2-p1)K$
- 第 $1+i$ 维放 $2\delta$
- 对每个 $i$ 的 s 行:
- 第 0 维放 $a_i K$
- 第 $1+k+i$ 维放 1
- embedding 行:
- 第 0 维放 $rhs K$
- 所有 e 坐标放 $-\delta$
- 最后一维放 $+\delta$
- modulus 行:
- 第 0 维放 $nK$
然后对该格做 LLL,再做若干阶段 BKZ,比如 $(30,36,40)$,loops=4。
规约后从基向量或其小线性组合中寻找满足以下特征的短向量:
- 第 0 维为 0
- 最后一维为 $\pm\delta$
- $e_i$ 全部为 $\pm\delta$
- $s_i$ 落在 digit 或 alpha 的小范围
解码出字节后按 UUID v4 正则验证,并验证
bytes_to_long(flag.encode("ascii")) % n == r
最后再做一次严格校验:
$$ f_0(a m + b) \equiv flagct \pmod n $$复现与检查点
下述命令默认你已经在算力充足的环境中,并位于仓库根目录。
export OMP_NUM_THREADS=1 OPENBLAS_NUM_THREADS=1 MKL_NUM_THREADS=1 NUMEXPR_NUM_THREADS=1
python3 -u path/griffin_fullsolve_onefile.py \
--output path/output.txt \
--x-affine auto \
--gap-maxtasksperchild 1 \
--gap-bkz 30,36,40 --gap-loops 4 --gap-K 1099511627776 --gap-delta 4 \
--workdir path
成功后会生成 path/griffin_onefile_.../,其中包含:
run.logsolver_snapshot.pyfinal_flag.txtcheckpoint_phase0.json..checkpoint_phase3.json
其中 checkpoint_phase3.json 会记录最终 affine 参数:
{
"affine": {"a": -1, "b": 256},
"flag": "alictf{**}",
"output_sha256": "..."
}
如需复用某次运行目录(断点续跑或复查),使用 --run-dir path/griffin_onefile_...。
并行与内存控制
Phase3 的 BKZ 扫描是最重的部分,稳定性主要靠两点:
- 禁用 BLAS 和 OpenMP 的自动多线程,避免每个子进程再开线程导致调度和内存抖动
OMP_NUM_THREADS=1OPENBLAS_NUM_THREADS=1MKL_NUM_THREADS=1NUMEXPR_NUM_THREADS=1
- 多进程池设置
maxtasksperchild=1,避免 fpylll 长时间运行后的内存膨胀
性能与格规约次数
以一次成功运行日志为准(run.log):
- 端到端约 26 分钟
- Phase3 总计约 1555 秒
- affine 尝试 12 组,前 10 组每组扫完 432 个候选无解,第 11 组 $a=-1,b=256$ 在 247/432 处命中
格规约调用次数口径:
- gap 每个候选 1 次 LLL + 最多 3 次 BKZ stage
- 因为并行 pool 在命中时会 terminate,统计以日志中
tried为下界
本题至少完成的候选数为 $10\cdot 432 + 247 = 4567$,因此 Phase3 最少规约调用次数:
- LLL: $\ge 4567$
- BKZ: $\ge 4567\cdot 3 - 2$ 量级,实际约 1.37 万次
进展与尝试记录
这题后半部分可以用很多不同策略做约束求解,例如 MILP, CP-SAT, CVP 嵌入等。实际踩坑后确认本题最稳的是 gap 嵌入 + affine 枚举:
- 早期错误假设
- 认为 Phase1 恢复的
xs就是真值,导致 Phase2 的根集合x_candidates直接当作 $m \bmod n$ - 结果: Phase3 无论用什么求解器都找不到合法 UUID4,因为真正约束是 $x = a m + b$
- 认为 Phase1 恢复的
- 修正后关键改动
- 基于 $x \in [1,256]$ 的约束推导 affine 搜索空间很小
- 对每个 $(a,b)$ 先把根集合从 $x$ 空间映射回 $m$ 空间,再进入 Phase3
- 最终命中 $(a,b)=(-1,256)$
被尝试过但最终不如 gap 稳定的方向:
- MILP
- 优点: 约束表达直观
- 缺点: 每个候选 residue 需要单独求解,整体很慢,而且对 solver 依赖更强
- CP-SAT
- 优点: 能做离散域和逻辑约束
- 缺点: 同样需要处理大量 residue,且在模方程 + 字符域的组合约束下经常卡在搜索
- CVP 类嵌入
- 优点: 可以把“接近 ASCII 的小偏移”变成近似最近向量问题
- 缺点: 对 scaling 和中心化很敏感,成功率不如 gap
后续若要继续优化:
- BKZ stage 可以根据机器资源做调整
- 也可以在 gap 解码阶段增加更多基向量线性组合策略来减少 BKZ block size