Misay's blog

Back

Above#

2026的第一场CTF 也是第二场XCTF 和W&M的师傅们一起看了题 感谢Lilac师傅们5道精彩的赛题! 同时在此处也给出题人WP ↗ 进行一个引流

学到很多 被虐的很爽 感谢招待😋

NoisyForest#

chall.py

一道结合随机数的混淆

系统简要可以概括为对于随机的bit 来决定对于中文字符是否进行9997的偏移 大部分情况下 中文字符通过9997的偏移后不会再是中文 而我们的input_str通过推断可以大概划分为 自然中文 + 夹杂中文乱码 + 自然中文三个部分 我们可以对首和尾两个Part 把所有的乱码减去9997 恢复初步的自然中文

但是此时我们注意到 部分中文加上9997偏移还是得到中文 这个时候就需要LLM审计这两段的文本 给出偏移前后 但是同为中文的部分 这部分的Perplexity会比较高

示例如下

chunk14
idx 14096
curr 真 U+771F
alt  倒 U+5012
context …几棵小树突然[真]了下去,发出很大的响声…
altctx  …几棵小树突然[倒]了下去,发出很大的响声…
bash

我们显然把”真”改回”倒”即可

赛中使用的是Gemini3-Preview 效果不错

我们恢复完首尾 大概可以得到

那些符号弯弯曲曲,就像爬动的小虫,又像风吹过沙子的痕迹,人类朋友也凑过来看,他皱起眉头,好像也不认识,老虎念出几个符号的发音,但连在一起完全不知道是什么意思,那些符号看起来是这样的,秢楠얜傻唆矍搆~~~~潳帝怖뢍癎鏖눰滛걧沍勩歾寗鮣犬栩놽뮀銴。人类朋友看着那些奇怪的字,脸色变得很严肃,他把手放在书页上,感觉到书里有一种古老而沉重的力量
bash

这样一块乱码是夹杂着中文和非中文的 我们必须要知道这里用了哪些bits才能还原回去

后半段也是和前半段类似的思路 使用偏移调整和人工审计的方法恢复 我们在这里可以得到后半段代码使用的bits流

此时我们可以通过还原MT 得到总共的bits流 前半段和后半段夹在中间的就是乱码处使用的bits流 我们逐步往前还原 就顺利恢复了所有的中文 从而完成题目

我们使用的文件

cipher.txt 密文

fixed.txt 审计后的前半段 后半段后的字符串

exp

nestDLP#

chall.py

两个部分组合而成的一道问题

Part1#

需要我们从题目给出的商环S\mathbf{S}上做离散对数 还原

这里问题在多变元商环上计算模幂 模数为p3p^3 考虑先将复杂的代数结构映射回整数环 再通过整环的p−adicp-adic来求解DLP

DexterJie师傅和Swizzer师傅分别考虑了两种映射手法 在这里记录

行列式映射#

首先先看这两段代码

def projected_basis(I, p):  # noqa: E741
  R = I.ring()
  names = R.variable_names()
  Rfp = PolynomialRing(GF(p), names=names)
  Ifp = Rfp.ideal([Rfp(f) for f in I.gens()])
  basis_fp = Ifp.normal_basis() # 在mod p上寻找一组基
  xs = R.gens()
  basis = []
  for m in basis_fp:
    exps = m.exponents()[0]
    mon = prod(x**e for x, e in zip(xs, exps))
    basis.append(mon)
  return basis
py

它的目的是在商环结构上找到一组基 便于往后表示商环运算中的元素 值得注意的是即使商环定义在模p3p^3上 一组模pp上的有效基也可以通过Lifting而得到模p3p^3的一组有效基

def get_matrix(element, quotient_ring, p):
  I = quotient_ring.defining_ideal()  # noqa: E741
  base_ring = quotient_ring.base_ring()
  basis = projected_basis(I, p)
  g_poly = element.lift()
  matrix_cols = []

  for b in basis:
    prod_poly = g_poly * b
    reduced_poly = quotient_ring(prod_poly).lift()
    coeffs = [reduced_poly.monomial_coefficient(mon) for mon in basis]
    matrix_cols.append(coeffs)

  M = matrix(base_ring, matrix_cols).transpose()
  return M
py

这里是把商环上的点向矩阵映射的关键 我们来分析 我们商环上的元素毕竟还是系数点 也就是A=(a1,a2,⋯ ,an)\mathbf{A}=(a_1,a_2,\cdots,a_n) 而我们选定的一组基就可以看成这样的一个基点S=(s1,s2,⋯ ,sn)\mathbf{S}=(s_1,s_2,\cdots,s_n) 对于我们商环上的运算 在系数的GF(p3)\mathbf{GF(p^3)}下 仍然可以看成一个线性系统 也就是加减乘和对I\mathbf{I}取模 而I\mathbf{I}的处理也具备严格线性 所以说我们商环上的任何点B\mathbf{B} 都可以看成基点S\mathbf{S}做了一个线性变换得到 而且线性变换的本质就可以看成矩阵乘法,我们把商环上的元素看成行向量 那么有

b=s⋅MB\mathbf{b}=\mathbf{s}\cdot\mathbf{M_B}

在DLP的代数系统中 我们选取的元素基本上不会包含有关pp的信息 故而可以认为B\mathbf{B}是可逆的 那么对于商环上的计算

A→B\mathbf{A}\rightarrow\mathbf{B}

可以视为

s⋅MA→s⋅MB\mathbf{s}\cdot\mathbf{M_A}\rightarrow\mathbf{s}\cdot\mathbf{M_B}

我们也就把商环上的复杂元素映射到了矩阵乘法 而我们也有

det(A×B)=det(A)×det(B)\mathbf{det(A\times B)} = \mathbf{det(A)}\times\mathbf{det(B)}

此时就有

E=ge→det(ME)=det(Mg)e\mathbf{E}=\mathbf{g}^e\rightarrow \mathbf{det(M_E)}=\mathbf{det(M_g)}^e

这样我们就把问题转化为了在Z/pk\mathbf{Z/p^k}上的整数DLP问题 可以考虑如下快速的求法

def padic_dlp(g: int, y: int, p: int, s: int):
  def theta(k, p, s):
    return (pow(k, (p - 1) * p ** (s - 1), p ** (2 * s - 1)) - 1) // (p**s)

  g, y, p, s = int(g), int(y), int(p), int(s)
  return pow(theta(g, p, s), -1, p ** (s - 1)) * theta(y, p, s) % (p ** (s - 1))
py

它针对的是

y≡gxmod  psy\equiv g^x\mod p^s

我们知道

ϕ(ps)=(p−1)×ps−1\phi(p^s)=(p-1)\times p^{s-1}

对于任意满足(k,p)=1(k,p)=1的kk 都有

kϕ(ps)≡1mod  psk^{\phi(p^s)}\equiv1\mod p^s

也就是

kϕ(ps)=1+m×psk^{\phi(p^s)}=1+m\times p^s

所以我们的theta函数干的就是

θ(k)=kϕ(ps)−1psmod  ps−1\boldsymbol{\theta}(k)=\frac{k^{\phi(p^s)}-1}{p^s}\mod p^{s-1}

令λ=ϕ(ps)\lambda=\phi(p^s)

y≡gxmod  ps→yλ≡(gλ)xmod  ps→yλ≡(gλ)xmod  p2sy\equiv g^x\mod p^s \rightarrow y^{\lambda}\equiv (g^{\lambda})^x \mod p^s\rightarrow y^{\lambda}\equiv (g^{\lambda})^x \mod p^{2s}

所以说

yλ≡(1+k⋅ps)xmod  p2sy^{\lambda}\equiv(1+k\cdot p^s)^x\mod p^{2s}

二项式展开 超过p2sp^{2s}直接约 得到

yλ≡1+x×k×psmod  p2sy^{\lambda}\equiv1+x\times k\times p^{s}\mod p^{2s}

那么

x×k≡yλ−1psmod  psx\times k\equiv \frac{y^{\lambda}-1}{p^s}\mod p^s

我们这里的kk不就是θ(g)\boldsymbol{\theta}(g)吗 那么

x≡θ(g)−1×θ(y)mod  psx\equiv\boldsymbol{\theta}(g)^{-1}\times\boldsymbol{\theta}(y)\mod p^s

求值同态#

为了理解这个方法 我们需要稍微理解商环的运算过程

商环S\mathbf{S}上不同的元素a  b\mathbf{a}\;\mathbf{b}实际上对应的是一个个不同的系数多项式 我们商环上的变元(X,Y)(X,Y)可以带入其中 计算得到对应的a(X,Y)  b(X,Y)\mathbf{a}(X,Y)\;\mathbf{b}(X,Y),那么我们在商环上对元素进行计算 也就是对系数之间进行线性处理;而我们的理想I就可以理解为这样两个特殊的多项式f1  f2\mathbf{f_1}\;\mathbf{f_2} 满足对于所有的变元 它们在商环的模运算下被归类到0\mathbf{0} 也就是在 S=R[x,y]/⟨f1,f2⟩\mathbf{S}=\mathbf{R}[x,y]/ \langle f_1, f_2 \rangle中

A(x,y)=B(x,y)+k1(x,y)f1(x,y)+k2(x,y)f2(x,y)\mathbf{A}(x,y) = \mathbf{B}(x,y) + \mathbf{k_1}(x,y)\mathbf{f_1}(x,y) + \mathbf{k_2}(x,y)\mathbf{f_2}(x,y)

此时元素A  BA\;B等价

我们希望找到一个映射 使得商环上的元素能够和整数一一对应 也就是

ϕ(g)=g(X,Y)\phi(\mathbf{g}) = \mathbf{g}(X, Y)

我们把商环的元素多项式g\mathbf{g}带入 它对应一个标量整数 g(X,Y)\mathbf{g}(X,Y) 我们来看看(X,Y)(X,Y)要满足什么性质 可知如果A=B\mathbf{A} = \mathbf{B}

一定有

ϕ(A)=ϕ(B)+k1(X,Y)⋅f1(X,Y)+k2(X,Y)⋅f2(X,Y)\phi(\mathbf{A}) = \phi(\mathbf{B}) + k_1(X,Y)\cdot \mathbf{f_1(X,Y)} + k_2(X,Y)\cdot \mathbf{f_2(X,Y)}

那么如果想要满足这个映射关系 就很容易发现 有

f1(X,Y)=0f_1(X,Y) = 0 且 f2(X,Y)=0f_2(X,Y) = 0

此时对于这个点 我们可以把商环元素和整数一一对应

我们先求出模pp下的一组(X,Y)(X,Y) 采用的是一种消元的思想 这里的代数式度并不高 可以借由理想的限制完成降次消元工作

R_fp = PolynomialRing(GF(p), names="x,y")
x_fp, y_fp = R_fp.gens()
f1_fp = x_fp**3 + y_fp**5 + 13 * x_fp * y_fp - 37
f2_fp = y_fp**3 + x_fp**5 + 37 * x_fp - 13
I_fp = R_fp.ideal([f1_fp, f2_fp])
Iy = I_fp.elimination_ideal([x_fp])
poly_y = Iy.gens()[0]
Ry = PolynomialRing(GF(p), 'y')
py = Ry(poly_y)
y_roots_pair = py.roots()
py

随后对于解一元方程得到的解进行遍历 来找到一组可以用的解

roots = []
for r_val_y, _ in y_roots_pair:
    Rx = PolynomialRing(GF(p), 'x')
    fx1 = Rx(f1_fp.subs(y=r_val_y))
    fx2 = Rx(f2_fp.subs(y=r_val_y))
    x_roots_pair = fx1.roots()
    for r_val_x, _ in x_roots_pair:
        if fx2(r_val_x) == 0:
            roots.append({'x': r_val_x, 'y': r_val_y})
py

然后就是Hensel-Lifting来提升解 这个基本上都很公式 核心思路就是牛顿迭代/泰勒展开式

这里给出Gemini3老师的公式分析

Hensel Lifting(亨泽尔引理提升) 在这种场景下确实是固定、公式化且极其自然的步骤。 这就像我们在解方程时使用的“牛顿迭代法”:一旦你有了一个粗糙的近似解(Mod pp),你就可以通过一套固定的公式,一步步把它打磨成高精度的解(Mod p3p^3)。

  1. 为什么说是“自然的”? 因为代数结构通常具有局部性质。 一个方程 f(x)≡0(modp)f(x) \equiv 0 \pmod p 的解,往往暗示了在 pp 的邻域内(即模 p2,p3…p^2, p^3 \dots)存在一个更精确的解。只要这个点不是“奇异点”(导数不为0),这种提升就是唯一且确定的。

  2. 提升的公式(多元牛顿迭代) 我们要解方程组:

    F(x)=(f1(x,y)f2(x,y))≡0(modpk)\mathbf{F}(\mathbf{x}) = \begin{pmatrix} f_1(x,y) \\ f_2(x,y) \end{pmatrix} \equiv \mathbf{0} \pmod{p^k}

公式如下:

假设 xk=(xkyk)\mathbf{x}_k = \begin{pmatrix} x_k \\ y_k \end{pmatrix} 是模 pkp^k 下的解。 我们想求 xk+1\mathbf{x}_{k+1} 使得它是模 pk+1p^{k+1}(或者 p2kp^{2k})下的解

也就是

xnext=xk−J−1(xk)⋅F(xk)(modp2k)\mathbf{x}_{next} = \mathbf{x}_k - J^{-1}(\mathbf{x}_k) \cdot \mathbf{F}(\mathbf{x}_k) \pmod{p^{2k}}

这里的核心组件是

  • xk\mathbf{x}_k 当前的解

  • F(xk)\mathbf{F}(\mathbf{x}_k): 当前解代入方程后的残差**(Residual) **

    • 因为 xk\mathbf{x}_k 是模 pkp^k 的解,所以 F(xk)\mathbf{F}(\mathbf{x}_k) 一定是 pkp^k 的倍数
  • J(xk)J(\mathbf{x}_k): 雅可比矩阵 (Jacobian Matrix)在点 xk\mathbf{x}_k 处的值。 J=(∂f1∂x∂f1∂y∂f2∂x∂f2∂y)J = \begin{pmatrix} \frac{\partial f_1}{\partial x} & \frac{\partial f_1}{\partial y} \\ \frac{\partial f_2}{\partial x} & \frac{\partial f_2}{\partial y} \end{pmatrix} 我们需要计算这个矩阵的逆矩阵。

代码实现

随后就是愉快的p-adic

part2#

恢复exps后 我们发现它是576bits 说明要求的flag也是576bits 但是我们只有384组的padding 未知数规模比较大 得考虑格规约

赛中第一时间想到的是2022SEECTF的Neutrality ↗

这里稍微有所改动

pads的汉明重量是2 而不是0

flag长为576Bits 而我们只有384组已知的Bits

首先映射的思路是可以通用的

ϕ({0,1},⊕)→ϕ′({1,−1},×)\phi(\{0,1\},\oplus)\rightarrow\phi^{'}(\{1,-1\},\times)

为什么这里优先考虑映射?

主要是pads汉明重量不为0 如果是一半0 一半1 利用Bits的分布规律 一半的异或结果eije_{ij}与flagfjf_{j}相同 另一半不同确实可以得到

∑j=1576(fj−eij)2=288\sum^{576}_{j=1}(f_j-e_{ij})^2=288

但是这里可惜不是 所以说右边的常量会有多种可能 但是我们映射到乘法后 就直接可以得到

∑jfj⋅eij=−2\sum_{j} f_j\cdot e_{ij}=-2

我们可以这样理解

eij=fj⊕cij→eij=fj×cije_{ij} = f_j\oplus c_{ij}\rightarrow e_{ij}=f_{j}\times c_{ij}

这个运算系统下 fj2=1f_{j}^2=1 也就是

cij=eij×fjc_{ij} = e_{ij}\times f_{j}

而cijc_{ij}中 1比0多两个 就得到了上述的方程

然后我们知道flag的72字节 肯定是可打印可读ASCII 那么最高位的比特一定是 0 映射后是1

所以说我们相当于知道了72比特的 也就是

384组线性约束 求解504个未知量 其中未知量都是1或者-1的小量 这个比规约0和1更好

也就是在线性系统

A384×504⋅f504×1=e384\mathbf{A_{384\times504}}\cdot\mathbf{f_{504\times1}}=\mathbf{e_{384}}

中 我们知道 A384×504\mathbf{A_{384\times504}} 它就是384组exp 也知道 e384\mathbf{e_{384}} 它是 −2I\mathbf{-2I}

我们该如何去求f504×1\mathbf{f_{504\times1}}?

我们直观的看这个线性系统

504个变量,384组约束 说明 自由变量只有120个 也就是这120个非常小的自由变量决定了整个线性系统 我们把这个些自由变量称为线性系统的核 列式子就是

我们虽然知道

A⋅fflag=e\mathbf{A}\cdot\mathbf{f_flag}=\mathbf{e}

也能找到一组特解f0\mathbf{f_0} 这个不难 满足

A⋅f0=e\mathbf{A}\cdot\mathbf{f_0}=\mathbf{e}

做差就有

A⋅(fflag−f0)=0\mathbf{A}\cdot(\mathbf{f_flag}-\mathbf{f_0})=\mathbf{0}

此时我们知道 既然我们的自由变量只有120个 那么线性系统中所有的504长度向量均可表示为120 个基向量 每个基向量是k504×1\mathbf{k}_{504\times1}的整数线性组合:

v=x1k1+x2k2+⋯+x120k120\mathbf{v} = x_1 \mathbf{k}_1 + x_2 \mathbf{k}_2 + \dots + x_{120} \mathbf{k}_{120}

其中 xix_i 是整数系数。

回代 那么有

fflag=f0+∑i=1120xiki\mathbf{f}_{flag} = \mathbf{f_0} + \sum_{i=1}^{120} x_i \mathbf{k}_i

这样子 我们就完成了降维 从找504维的fflag\mathbf{f_flag} 到去找120维的(x1,x2,…,x120)(x_1, x_2, \dots, x_{120})

我们不知道系数 xix_i 是多少,但我们知道 fflag\mathbf{f}_{flag} 很短(全为 ±1\pm 1)。 我们要构造一个格,使得格子里包含 fflag\mathbf{f}_{flag} 这个向量。 我们构造一个 (120+1)×(504+1)(120+1) \times (504+1) 的矩阵 L\mathbf{L}。 每一行代表一个格基向量:

L=(—k1—0—k2—0⋮⋮⋮⋮—k120—0—f0—1)\mathbf{L} = \begin{pmatrix} \text{---} & \mathbf{k}_1 & \text{---} & 0 \\ \text{---} & \mathbf{k}_2 & \text{---} & 0 \\ \vdots & \vdots & \vdots & \vdots \\ \text{---} & \mathbf{k}_{120} & \text{---} & 0 \\ \text{---} & \mathbf{f_0} & \text{---} & 1 \end{pmatrix}

当我们对矩阵 L\mathbf{L} 进行线性组合时,格空间里的向量 w\mathbf{w} 长这样:

w=x1(k1,0)+⋯+x120(k120,0)+1⋅(f0,1)\mathbf{w} = x_1(\mathbf{k}_1, 0) + \dots + x_{120}(\mathbf{k}_{120}, 0) + 1 \cdot (\mathbf{f_0}, 1) w=(f0+∑xiki⏟这是我们要找的 Flag,1) \mathbf{w} = (\underbrace{\mathbf{f_0} + \sum x_i \mathbf{k}_i}_{\text{这是我们要找的 Flag}}, \quad 1)

我们要求xx 但是我们把xx放入我们的目标向量 而是让它在规约过程中 给我们的实际小量ff带出来 这样子就顺利规约了!

我们的伪代码思路如下

exp

值得注意的是 这里LLM给出了提取kernel更优化的一种方法

D, U, V = A.smith_form()
r = A.rank()
K = V[:, r:].transpose()
py

这里也贴下Gemini老师的分析

这段代码是利用 Smith Normal Form (SNF, 史密斯标准型) 来手动提取矩阵 AA 的 整数核空间(Integer Kernel) 的基底。

这是比直接调用 .right_kernel() 更底层、更具几何直观性的做法,能确保我们得到的是**饱和(Saturated)**的整数基。

下面我一步步带你拆解这个数学过程:

  1. 核心公式:坐标变换

SageMath 的 A.smith_form() 返回三个矩阵 (D,U,V)(D, U, V),满足以下恒等式: D=U⋅A⋅VD = U \cdot A \cdot V

其中:

  1. AA:原始系数矩阵 (384×504384 \times 504)。

  2. DD:对角矩阵(Diagonal Matrix)。只有对角线上有非零元素,其他全是 0。

  3. UU:左变换矩阵,代表行变换(在这个问题里不重要)。

  4. VV:右变换矩阵,代表列变换。这是一个 单模矩阵(Unimodular Matrix),意味着它在整数域上可逆(行列式为 ±1\pm 1)。

  5. 推导:为什么 VV 的后几列就是核?

我们的目标是解齐次方程: Ax=0A \mathbf{x} = \mathbf{0}

利用 SNF 公式 D=UAVD = U A V,我们可以把 AA 反解出来(因为 U,VU, V 都可逆): A=U−1DV−1A = U^{-1} D V^{-1}

代入方程 Ax=0A \mathbf{x} = \mathbf{0}: (U−1DV−1)x=0(U^{-1} D V^{-1}) \mathbf{x} = \mathbf{0}

两边同时左乘 UU(UU 是可逆的,不会改变方程的解): D(V−1x)=0D (V^{-1} \mathbf{x}) = \mathbf{0}

这里我们做一个精妙的换元(坐标变换): 令 y=V−1x\mathbf{y} = V^{-1} \mathbf{x},也就是 x=Vy\mathbf{x} = V \mathbf{y}。 此时方程变成了极其简单的形式: Dy=0D \mathbf{y} = \mathbf{0}

  1. 分析 y\mathbf{y} 的解结构

矩阵 DD 是 Smith 标准型,它的长相是这样的:

D=(d10…0…00d2…0…0⋮⋱00…dr…000…000⋮⋮)D = \begin{pmatrix} d_1 & 0 & \dots & 0 & \dots & 0 \\ 0 & d_2 & \dots & 0 & \dots & 0 \\ \vdots & & \ddots & & & \\ 0 & 0 & \dots & d_r & \dots & 0 \\ 0 & 0 & \dots & 0 & 0 & 0 \\ \vdots & & & & & \vdots \end{pmatrix}

其中 rr 是矩阵的秩(Rank),前 rr 个对角元素非零,第 rr 行之后全为 0。

我们要解 Dy=0D \mathbf{y} = \mathbf{0},其中 y=[y1,y2,…,yn]T\mathbf{y} = [y_1, y_2, \dots, y_n]^T。 展开来看就是:

{d1y1=0d2y2=0⋮dryr=00⋅yr+1=0⋮0⋅yn=0\begin{cases} d_1 y_1 = 0 \\ d_2 y_2 = 0 \\ \vdots \\ d_r y_r = 0 \\ 0 \cdot y_{r+1} = 0 \\ \vdots \\ 0 \cdot y_{n} = 0 \end{cases}

因为 d1…drd_1 \dots d_r 都是非零整数,所以必须有 y1=y2=⋯=yr=0y_1 = y_2 = \dots = y_r = 0。 但是!yr+1y_{r+1} 到 yny_n 对应的方程是 0=00=0,所以它们可以是任意整数!

所以,y\mathbf{y} 的通解形式一定是: y=(0⋮0c1⋮cn−r)\mathbf{y} = \begin{pmatrix} 0 \\ \vdots \\ 0 \\ c_1 \\ \vdots \\ c_{n-r} \end{pmatrix} 只有后 n−rn-r 个分量是非零自由变量。

  1. 还原回 x\mathbf{x} (Key Step)

记得我们定义的 x=Vy\mathbf{x} = V \mathbf{y} 吗? 这就意味着 x\mathbf{x} 是矩阵 VV 的列向量的线性组合。

x=V⋅y=(v1v2…vn)⋅(0⋮0c1⋮cn−r)\mathbf{x} = V \cdot \mathbf{y} = \begin{pmatrix} \mathbf{v}_1 & \mathbf{v}_2 & \dots & \mathbf{v}_n \end{pmatrix} \cdot \begin{pmatrix} 0 \\ \vdots \\ 0 \\ c_1 \\ \vdots \\ c_{n-r} \end{pmatrix}

由矩阵乘法规则,前 rr 列 v1…vr\mathbf{v}_1 \dots \mathbf{v}_r 都被 0 乘掉了。 结果只剩下: x=c1vr+1+c2vr+2+⋯+cn−rvn\mathbf{x} = c_1 \mathbf{v}_{r+1} + c_2 \mathbf{v}_{r+2} + \dots + c_{n-r} \mathbf{v}_n

结论: 矩阵 VV 的第 rr 列之后的所有列,就是核空间的一组基底!

  1. 代码逐行解释
# 1. 计算 Smith 标准型
# D 是对角阵,V 是列变换矩阵
D, U, V = A.smith_form()

# 2. 获取矩阵的秩 r
# 这告诉我们要丢掉前 r 列,因为它们对应非零的主元,被方程锁死为 0 了
r = A.rank()

# 3. 提取核空间基底
# V[:, r:]  --> 切片操作:取 V 的所有行,但只取从第 r 列开始到最后的列
# .transpose() --> Sage 中通常习惯用行向量表示基底,所以转置一下
K = V[:, r:].transpose()
python

总结:为什么要这样做?

相比于直接用高斯消元求核,利用 Smith Form 的 VV 矩阵有极其重要的优势:

单模性(Unimodular): 矩阵 VV 的行列式是 ±1\pm 1。这意味着它张成的格(Lattice)是完备的整数格(Saturated Lattice)。 这保证了我们提取出的基向量 vr+1…vn\mathbf{v}_{r+1} \dots \mathbf{v}_n 能够通过整数线性组合生成核空间里的每一个整数向量。

如果用普通的行阶梯形(RREF)求核,有时会得到分数基,去分母后可能会导致格变得稀疏(丢失了解)。而 SNF 方法是求解此类整数方程最稳健的方法。

直接用鸡块的方法 做一个 (508+1)×(508+384)(508+1)\times(508+384)的SVP也可以 用flatter加速可以规约出来 具体参考 星盟在这里的做法LilacCTF 2026 Writeup - 星盟安全团队 (xmcve.com) ↗

预期解使用的是

from ortools.sat.python import cp_model
py

有点类似z3 是在已知约束的情况下去寻找可行解的工具 比z3快不少)

记几个工具 后面研究下怎么用

ludopulles/BLASter: Fast lattice reduction using segmentation, multithreading, Seysen reduction and BLAS (github.com) ↗

keeganryan/cuso (github.com) ↗

*myRSA#

未出解🤕 即使到最后也没能找到正确的方向ww

给出一个rsa系统 其模数具有特殊的代数结构

n=p×q×rn=p\times q\times r

其中

p=pp2+3pp+3p=pp^2+3pp+3 q=pp2+5pp+7q=pp^2+5pp+7

这里的oracle允许输入一个xx观察其对于p  q  rp\;q\;r是否同时满足二次剩余并且开根 经测试只有x=96x=96能够返回一个res

从我们之前熟知的Pollard p-1算法出发 我们本质是找到了群Z/pZ\mathbb{Z/pZ}的阶的倍数 也就是B!∣ϕ(p)B! | \phi(p) 此时用阶的倍数进行模幂运算 就能够返回一个单位元 从而构造一个非平凡的的结构因子 也就是

aB!−1≡(aϕ(p))k−1≡0mod  pa^{B!}-1\equiv (a^{\phi(p)})^k-1\equiv0\mod p

所以说我们分解的思路都是去找一个满足特别代数结构的运算群 它的阶的信息我们能够直接或者间接的得到 然后在利用阶相关的运算中 得到关于模数的非平凡因子

这里基于ECM的分解 或者是另一种基于分圆多项式的分解

n=p×Φk(p)×rn=p\times\mathbf{\Phi_k}(p)\times r

的分解都是寻找一个已知阶的运算群

话说回来 我们的p  qp\;q不难看成

f1(x)=x2+3x+3    p=f1(pp)f_1(x)=x^2+3x+3\;\;p = f_1(pp) f2(x)=x2+5x+7    q=f2(pp)f_2(x)=x^2+5x+7\;\;q=f_2(pp)

接下来我们计算ff的判别式

Δ1=b12−4a1c1=−3\Delta_1=b_1^2-4a_1c_1=\mathbf{-3} Δ2=b22−4a2c2=−3\Delta_2=b_2^2-4a_2c_2=\mathbf{-3}

由一元二次方程求根公式产生负数的情况 我们可以知道此时方程的根能够表示为

x=−b1±Δ12a1x=\frac{-b_1\pm\sqrt{\Delta_1}}{2a_1}

所以说由求根公式 我们可以得到

4p=(2pp+3)2+3×12(1)4p = (2pp+3)^2 + 3 \times 1^2\tag{1} 4q=(2pp+5)2+3×12(2)4q = (2pp+5)^2 + 3 \times 1^2\tag{2}

通过LLM 我们知道当我们的素数p  qp\;q满足这种特殊的代数结构 那么对于一条模p  qp\;q的椭圆曲线 它就具备了**复乘(CM)**性质

为什么要提到这个复乘?

因为具备复乘性质的椭圆曲线 它的阶我们可以直接得到

如何判断一条椭圆曲线是否具备复乘?

对于椭圆曲线E\mathbf{E} 如果它的模数pp满足类似性质

4p=t2+D×y24p=t^2+D\times y^2

yy是一个任意的整数

那么E\mathbf{E}满足DD的复乘 此时根据DD的取值可以构造一条特殊结构的椭圆曲线Ep\mathbf{E_p} 在这条曲线上

tt是曲线Ep\mathbf{E_p}的迹(trace)

复乘怎么用?

我们由复乘可以算出曲线Ep\mathbf{E_p}的迹 这在普通曲线中是做不到的 而对于任意曲线EE 它的阶可以表示为

#E=p+1±t\mathbf{\#E}=p+1\pm t

这里给出复乘不同的DD针对曲线的构造表格

DD 值 判别条件 (Δ\Delta) 曲线方程形式 / 标准方程 j-invariant (整数值) 备注 / jj 值特征
3 Δ=−3\Delta = -3 y2=x3+by^2 = x^3 + b 00 b≠0b \neq 0
4 Δ=−4\Delta = -4 y2=x3+axy^2 = x^3 + ax 17281728 a≠0a \neq 0
7 Δ=−7\Delta = -7 y2=x3−35x+98y^2 = x^3 - 35x + 98 −3375-3375 −153-15^3
8 Δ=−8\Delta = -8 y2=x3−30x+56y^2 = x^3 - 30x + 56 80008000 20320^3
11 Δ=−11\Delta = -11 由 j 生成 −32768-32768 −215-2^{15}
19 Δ=−19\Delta = -19 由 j 生成 −884736-884736 −215⋅33-2^{15} \cdot 3^3
43 Δ=−43\Delta = -43 由 j 生成 −884736000-884736000 −218⋅33⋅53-2^{18} \cdot 3^3 \cdot 5^3
67 Δ=−67\Delta = -67 由 j 生成 −147197952000-147197952000 −215⋅33⋅53⋅113-2^{15} \cdot 3^3 \cdot 5^3 \cdot 11^3
163 Δ=−163\Delta = -163 由 j 生成 −262537412640768000-262537412640768000 −6403203-640320^3

只要确定了 jj 变换,可以通过以下系数构造曲线 y2=x3+Ax+By^2 = x^3 + Ax + B:

A=3k(1728−j)A = 3k(1728 - j)

B=2k(1728−j)2B = 2k(1728 - j)^2

其中 kk 是缩放因子。通常在 SageMath 中,可以直接使用 EllipticCurve_from_j(j) 命令一键生成。

说回本题

所以说此时我们知道 对于模pp或者模qq的椭圆曲线 它具备D=3D=3的复乘性质 我们构造特殊的曲线 E:y2≡x3+b\mathbf{E}: y^2\equiv x^3+b

  • 对于Ep\mathbf{E_p} 由1式我们可以得到它的迹为tp=2pp+3t_p=2pp+3 可以计算阶 #E=p+1±(2pp+3)\mathbf{\#E}=p+1\pm (2pp+3) 对于曲线的扭曲镜像 我们有#Ep=p+1+(2pp+3)=q\mathbf{\#E_p}=p+1+ (2pp+3)=q
  • 对于Eq\mathbf{E_q} 由1式我们可以得到它的迹为tq=2pp+5t_q=2pp+5 可以计算阶 #E=q+1±(2pp+5)\mathbf{\#E}=q+1\pm (2pp+5) 对于曲线的原像 我们有#Ep=q+1−(2pp+5)=p\mathbf{\#E_p}=q+1- (2pp+5)=p

那么我们也就完全符合了前文提到的 构造一个已知阶关系的运算群的要求了

我们知道我们要构造的椭圆曲线形如E:y2≡x3+b\mathbf{E}: y^2\equiv x^3+b 这里bb可以随便取 但是我们注意oracle输入x=96x=96会返回一个res 那不就是

res2≡96mod  nres^2\equiv 96\mod n

所以说我们可以知道

{x=1      b=95x=2      b=88x=3      b=69x=4      b=32⋯\begin{cases}x=1\;\;\;b=95\\x=2\;\;\;b=88\\x=3\;\;\;b=69\\x=4\;\;\;b=32\\\cdots\end{cases}

对于不同的bb 我们可以构造曲线 然后得到一个曲线上的点(x,res)(x,res) 然后进行后续的操作 经测试 (1,95)(1,95)和(4,32)(4,32)都能很好的完成分解 我推测是这两条曲线的阶恰好满足上面的互为对方模数的关系

好的 我们接下来就已知曲线En\mathbf{E_n} 还有它上面的点 G(x,res)G(x,res) 我们知道 GG也一定在Ep\mathbf{E_p} Eq\mathbf{E_q}上 此时我们去计算 n×Gn\times G

因为我们知道nn一定是阶的倍数 那么在椭圆曲线中 对一个点进行阶的点乘 就会得到无穷远点 也就是

n×G=On\times G= \mathcal{O}

在sagemath中这个运算会失败

底层上来看 我们在模pp域下 某步计算后会得到0 但是别的域下不会得到 那么我们在点加求斜率的过程中 涉及除法就会失败 也就是模逆计算失败

此时我们手动追踪除法的因子 做gcdgcd 即可求出来 这也就是

Lenstra elliptic-curve factorization.

exp

在CTF-2025/crypto/madoka-rsa/README.md at main · infobahnctf/CTF-2025 (github.com) ↗

中 pp的Δ\Delta 是64×364\times3 我们 mul(G, 2 * n, n)就行😇

*myBlock#

chall.py

cipher.py

一个定义在8轮Feistel网络上的分组加密

处理过程为

R′=LR^{'}=L L′=R⊕(L⊕Ki)3L^{'}=R\oplus(L\oplus K_i)^3

我们可以选取nums组数据进行加密 要求从已有的数据中恢复16*8bits的subkeys

母题是corCTF 2023 - Superbox (crypto) (affine.group) ↗

我们知道输进去 和传出来的数据 可以考虑MITM攻击 从输入开始加密 从输出开始解密 如果在中间产生了碰撞 说明结果正确

corCTF2023这道题需再GF(3135)\mathbf{GF(3^{135})}下解6个子密钥 而且轮函数是L′=R⊕(L⊕Ki)2L^{'}=R\oplus(L\oplus K_i)^2 我们可以理解为加密不过是一个8次的多项式运算 解密也是类似 这个线性系统可以直接被GrobnerBasis找到一组碰撞

但是在这道Myblock中 轮函数是3次 而且是4轮的MITM Grobner足足有81次 基本不可解

而且LLM在看到GF(2)\mathbf{GF(2)}会尝试用z3 这更不可能考虑得到解了 启发式算法在这个问题规模下基本不可能得到答案

通过阅读https://rbtree.blog/posts/2022-11-07-interpolation-attack/ ↗ 我们来分析下什么是块密码上的插值攻击

块密码上的多项式插值攻击#

理论基础#

  1. 在我们的分组加密过程中 虽然密钥kk是未知的 但它是固定的 也就是我们可以把它看成在题目规定有限域GF\mathbf{GF}上的常数
  2. 在分组加密过程中 我们进行的不论是⊕\oplus还是×\times还是立方3^3本质上都可以看成有限域上的线性变换 所以说任何系统中的变换都可以看成一个对系数的多元多项式处理 也就是 C=fd(P)=adPd+ad−1Pd−1+⋯+a1P+a0C = f_d(P) = a_d P^d + a_{d-1} P^{d-1} + \dots + a_1 P + a_0 系数 aia_i 是由未知密钥 kk 决定的常数
  3. 直接求解kk会由于问题规模产生困难 我们把目光转向尝试求解aia_i 只要解出所有的多项式系数 也就相当于还原了加密 解密的过程

实现细节#

  1. 对于度数为dd的多项式 我们想恢复它肯定要尝试有至少d+1d+1组的Ci=fd(Pi)C_i=f_d(P_i) 数学上多项式是唯一的 但是我们在MITM中处理的实际上是方程 fcoeff(L,R)−gcoeff(C)=0f_{coeff}(L, R) - g_{coeff}(C) = 0 这里的未知量是 多项式的系数 ci,jc_{i,j} 也就是处理线性系统A⋅c=0\mathbf{A}\cdot\mathbf{c}=\mathbf{0} 我们可以多收集数据 让关于c\mathbf{c}的解收敛到唯一
  2. 本题中轮函数是3次 我们使用类似MITM的思路 分别做两边的4轮Feistel加密/解密 我们的多项式的度是81 相当于要解82个项 而我们可以获得最多674组的约束

求解#

本题中

  print(subkeys[-1])
py

直接给出了第7轮密钥 可以打一个前向加密4轮 后向解密3轮的MITM 用代数多项式化的方法 拿足够的约束去插值 尝试还原出多项式后对每轮key进行一个搜索

这里参考星盟的师傅 用Cpp写了运算组件 然后用python做交互 调用可执行程序求解 exp to be done 😶

BootsTrapping#

ckks

chall.cpp

CKKS的赛题 属于是wp都还看不太懂 🥲 To be Done

LilacCTF2026
https://www.zt2misay2.cn/blog/lilacctf2026
Author Misay
Published at 2026年1月28日