Misay's blog

Back

Above#

周六打完系统安全初赛周日看了看题 Crypto质量都很不错 成功ak了 写本文来就其中的构造来进一步学习一下

话说这比赛有这么多人打吗 除了MISC其他方向基本都被打完了 Crypto的题出解量也比上次夸张了好多x)

出题人师傅blog引流 SUCTF 2026 Crypto方向部分赛题Write UP – 三极管域 | Triode Field ↗ 非常感谢师傅的题🥰


SU_AES#

AES.py

chal.py

很漂亮的一道对称 要求在300rounds内 通过与修改后的AES系统进行交互 从密文恢复flag

先从代码方面进行分析

AES实现的差异#

AES.py给出了一个已有的S盒 并且随机进行洗牌 Random(seed).shuffle(self.Sbox) 保留了S盒的双射完整性 随后调用生成master_key 开始修改密钥,这里的change_key接口问题不大

然后是关键的

    def change(self, s=None, k=None):
        if s:
            self.Sbox = Random(s).choices(self.Sbox, k=len(self.Sbox))
        if k:
            self.change_key(k)
py

这里使用的是choice意味着S盒重新放回的信息能产生重合 比如可能会有两个位置都映射到137这个置换出口 那么产生碰撞后 原本对应的映射就会缺失 从而 会破坏S盒的双射完整性

可以计算一下 进行一次有放回的抽样 假设某个元素xx 它对应到了137137这个映射 那么对于另一个元素yy 它在重排后不碰撞上137137的概率是255256\frac{255}{256} 而137137这个映射出口 在 256 次抽样中一次都没被抽中的概率 PmissP_{\text{miss}} 为:Pmiss=(1−1256)256≈36.8%P_{\text{miss}} = \left( 1 - \frac{1}{256} \right)^{256}\approx36.8\%

简而言之 抽样放回后的S盒 每个元素都有36.8%36.8\%的概率不被碰撞上 计算一下期望 有256×0.368≈94256\times0.368\approx94个元素 不会出现在抽样后的S盒输出中 😶‍🌫️

而我们的AES中 是支持只改变S盒 或者只改变key来进行加密Oracle的

最后 这里的ECB加密方式 彼此块之间都是独立的


然后是交互方面 给了300rounds

每一轮 有三种选择

发送自己构造的seed和key 修改AES状态

发送明文给AES 加密获取密文

重置AES为初始状态

至此梳理完毕

PFA与差分思路#

注意到这个AES加密实例允许修改S盒的值并且持久性的拿这个代数性质不好的S盒去加密 相关搜索后我找到了这篇文章

(PDF) Persistent Fault Analysis on Block Ciphers (researchgate.net) ↗

针对分组加密 如果能够持久性的给置换部分注入破坏随机性的因子 就可以产生相关的利用

文章中针对的是单块的加密 但本题恰好是ECB模式 也能够化用

在这个题这借助文章思路描述一下这个过程

在种子ss下 输入xx通过choice替换到了y=idxs[x]y=\text{idx}_s[x] 修改后的S盒可以看成

Qs(x)=P[idxs[x]]=P[y]Q_s(x) = P[\text{idx}_s[x]]=P[y]

如果是shuffle 那么yy是可以均匀地取到256个值的 但是在choice下 yy有94种情况都取不到了 但是这有什么用呢?我们看向

    def encrypt(self, plaintext):
        state = text2matrix(plaintext)
        self.add_round_key(state, self.round_keys[:4])
        for i in range(1, 10):
            self.sub_bytes(state)
            self.shift_rows(state)
            self.mix_columns(state)
            self.add_round_key(state, self.round_keys[4*i:4*(i+1)])
        self.sub_bytes(state)
        self.shift_rows(state)
        self.add_round_key(state, self.round_keys[40:])
        return matrix2text(state)
py

在最后一轮的加密 没有进行mix_columns 这意味着如果我们截取了密文的某一个字节 加密就是简单的

Cj=Qs(State9,j)⊕RK10[j]C_j = Q_s(State_{9, j}) \oplus RK_{10}[j]

而我们知道每次改变一次S盒,QsQ_s都有大约94个元素无法被输出 我们设 v∈{0,⋯ ,255}v\in\{0,\cdots,255\}就是一个无法被输出的元素 那么

Cj≠v⊕RK10[j]C_j \neq v \oplus RK_{10}[j]

顺着这个思路 我们注意到加密是不限制Block的个数的 那么可以发送很多组Block 都随机分布 因为vv的值域是被限制了 那么即便被第10轮密钥混淆 异或平移后还是明显会有相当于一部分密文没有出现 我们提取这个不出现的密文 也就是

M(s,j)={P[y]⊕RK10[j]∣y∈Ymissing(s)}M(s, j) = \{ P[y] \oplus RK_{10}[j] \mid y \in Y_{missing}(s) \}

这个有什么用呢 举个例子

假设底层有一个特定的索引 y=5y=5 消失

  • 在第 1 轮 seed 测试中,密文缺失集合 M(s1,j)M(s_1, j) 里必然包含数值 P[5]⊕RK10[j]P[5] \oplus RK_{10}[j]
  • 在第 2 轮 seed 测试中,如果 y=5y=5 又消失了,密文缺失集合 M(s2,j)M(s_2, j) 里也必然包含数值 P[5]⊕RK10[j]P[5] \oplus RK_{10}[j]

对于某一个索引的情况 我们可以计算多个seed 让它在本地 都满足 这个y=5y=5索引缺失 那么取了足够多组缺失的情况 那个每次都在缺失集合内的值 就是

C=P[5]⊕RK10[j]C=P[5]\oplus RK_{10}[j]

对于多个索引 多个seed 缺失的常数情况 可以建立二维表 Tj[y]T_j[y]

  • 它有 256 行:对应未知的原始 S 盒的输入索引 y∈{0…255}y \in \{0 \dots 255\}。

  • 它有 16 列:对应 16 个密文字节的位置 j∈{0…15}j \in \{0 \dots 15\}。

例如如果有T0[5]=0x3FT_0[5] = \texttt{0x3F} 那么

P[5]⊕RK10[0]=0x3FP[5] \oplus RK_{10}[0] = \texttt{0x3F}

我们从此建立了初始S盒和末态密钥的关系 也就是PFA的攻击流程


不难看到 RK10RK_{10}还是一个未知量 我们再看回

    def encrypt(self, plaintext):
        state = text2matrix(plaintext)
        self.add_round_key(state, self.round_keys[:4])
        for i in range(1, 10):
            self.sub_bytes(state)
            self.shift_rows(state)
            self.mix_columns(state)
            self.add_round_key(state, self.round_keys[4*i:4*(i+1)])
        self.sub_bytes(state)
        self.shift_rows(state)
        self.add_round_key(state, self.round_keys[40:])
        return matrix2text(state)
py

它在一开始就进行了 add_round_key 调用了初始密钥进行了异或 这意味着什么呢

如果我们能让两个明文在 self.add_round_key(state, self.round_keys[:4])就成功碰撞 由分组加密结构 终态密文也一定成功碰撞

我们构造两块明文M    NM\;\;N 由前面的分析 对于已知的seed 可以轻松找到

a≠b,idxs[a]=idxs[b]a \neq b, \quad \text{idx}_s[a] = \text{idx}_s[b]

此时把这两个一样的索引传入修改S盒 会吐出一样的值

如果我们给碰撞后再去进行一个差分偏移

Mg[t]=a⊕gM_g[t] = a \oplus g

Ng[t]=b⊕gN_g[t] = b \oplus g

那么add_round_key后得到的状态是

Mg′[t]=Mg[t]⊕kt=a⊕g⊕ktM'_g[t] = M_g[t] \oplus k_t = a \oplus g \oplus k_t

Ng′[t]=Ng[t]⊕kt=b⊕g⊕ktN'_g[t] = N_g[t] \oplus k_t = b \oplus g \oplus k_t

有且仅有 g==ktg==k_t的时候 才会得到 Qs(Mg′[t])=Qs(a)Q_s(M'_g[t]) = Q_s(a)

Qs(Ng′[t])=Qs(b)Q_s(N'_g[t]) = Q_s(b) 观察输出 这两个部分的字节完全相同 说明我们构造的 gg恰巧碰上了这一字节位置的初始密钥

而且我们的查询也是不限制大小的 如果想恢复第tt位置的初始密钥 完全可以发送类似M0,N0,M1,N1,…,M255,N255M_0,N_0,M_1,N_1,\dots,M_{255},N_{255}

总共 512 个块,只需 1 次 encrypt 即可爆破出是哪个字节 验证逻辑C2g=C2g+1⟹g 是候选C_{2g} = C_{2g+1} \Longrightarrow g \text{ 是候选}


好了 我们现在恢复了 K0K_0 那么怎么和 RK10[j]RK_{10}[j]扯上关系呢

先单独分析特定的 j=0j=0字节 也就是T0[y]=P[y]⊕RK10[0]T_0[y] = P[y] \oplus RK_{10}[0] 我们其实可以爆破Δ=RK10[0]∈{0,1,⋯ ,255}\Delta = RK_{10}[0]\in\{0,1,\cdots,255\} 都可以恢复出一个初始S盒候选PΔ[y]=T0[y]⊕ΔP_\Delta[y] = T_0[y] \oplus \Delta 可以检查它的性质

  • 它应该是一个256→256256\rightarrow256的置换

  • 用恢复的S盒PΔP_\Delta和初始密钥K0K_0重新进行AES过程 得到RK10(Δ)RK_{10}^{(\Delta)}

  • 检查是否有∀y,j,PΔ[y]⊕RK10(Δ)[j]=Tj[y]\forall y,j,\quad P_\Delta[y] \oplus RK_{10}^{(\Delta)}[j] = T_j[y]完全成立

这样子 我们可以逐字节恢复最终的密钥 那么再异或回去 就得到了初始SS盒 后续构造逆SS盒 就可以解密了


计算一下交互总数

  • 恢复K0K_0

    • 1 次 change
    • 32 次 encrypt
  • 恢复映射表

    • 先 1 次 reset
    • 再对于每一个seed
      • 1 次 change
      • 1 次 encrypt
      • 1 次 reset

    总共142次 给的很富裕了

exp.sage

SU_PRNG#

chal.py

也是非常简洁的一道流密码

思路梳理#

逻辑很简单 先定义了一个右移函数 ror 然后对于state长度为256的LCG实例 每轮状态转换为si+1≡a×si+bmod  2128s_{i+1}\equiv a\times s_{i}+b\mod 2^{128} 为了方便分析 给相关参数上下对半拆开为si=Hi×2128+Lis_i=H_i\times2^{128}+L_i a=a0+2128⋅a1a = a_0 + 2^{128} \cdot a_1 b=b0+2128⋅b1b = b_0 + 2^{128} \cdot b_1关于函数

ror((self.seed >> bits // 2) ^ (self.seed % 2**(bits // 2)), self.seed >> bits - 250, bits)
py

它右旋的部分是 Zi=Hi⊕LiZ_i=H_i\oplus L_i 右旋的步长为 ri≡S>>6mod  256r_i\equiv S>>6\mod 256

不难注意到 ZiZ_i的高128位都为0 而根据右旋函数的构造 右旋后的结果outiout_i一定也有128组0 那么就可以考虑左旋回去 直到高128位都是0 这样就还原了可能的步长rir_i集合 也就是r 是合法的步长   ⟺  ROL⁡256(ot,r)<2128r \text{ 是合法的步长 } \iff \operatorname{ROL}_{256}(o_t, r) < 2^{128}

我们再看向 rir_i的定义 它实际上也只取决于 当前state ss的低6∼146\sim14位 也就是只和低128位的LiL_i相关 即rt=Lt[6:14]r_t = L_t[6:14]


左旋恢复步长 逆向#

做到这一步 可以根据这里的State转移方式找到一篇参考的paper

https://tosc.iacr.org/index.php/ToSC/article/view/8700/8292 ↗

这里的PCG-64的右旋步长由高6位决定 而我们这里是低6~14位 可以参考这里的UnknownIncrement的求解思路 在低14位的计算中 截断会得到干净的代数结构 那么令xi=Li mod 214x_i = L_i \bmod 2^{14} 有 xi=(ri≪6)  ∣  t,0≤t<64x_i = (r_i \ll 6) \;|\; t,\qquad 0\le t<64

由前面的定义 可以推出Li+1=a0Li+b0(mod2128)L_{i+1} = a_0 L_i + b_0 \pmod{2^{128}} 模低位可以得到xi+1=a0xi+u(mod214)x_{i+1} = a_0 x_i + u \pmod{2^{14}} 也就是说一旦固定了u    x0u\;\;x_0 56组状态都会被直接确定 约束非常强 可以考虑进行差分消去uu 然后逐步爆破t  rit\;r_i 对于不同的xi  xi+1  xi+2x_i\;x_{i+1}\;x_{i+2}都带入验证 可以非常快的收束到唯一序列


我们已经确认了u  x0∼x55u\;x_0\sim x_{55} 那么可以精确的恢复rir_i 从而也得到了 精确的56组 Zi=Li⊕HiZ_i = L_i \oplus H_i


分析截断 构造线性式#

接下来审视手上已有的数据 xix_i都是低14位的 考虑围绕低14位和高114位开始分析结构 重新令Li=xi+214yi,L_i = x_i + 2^{14} y_i, 同时b0=u+214v.b_0 = u + 2^{14} v. 再代入 有a0xi+u=xi+1+214qi,a_0 x_i + u = x_{i+1} + 2^{14} q_i, yi+1=a0yi+v+qi(mod2114).y_{i+1} = a_0 y_i + v + q_i \pmod{2^{114}}. qi=a0xi+u−xi+1214.q_i = \frac{a_0 x_i + u - x_{i+1}}{2^{14}}. 这一步最核心的思路就是 面对未知的高114位 我们把低14位带来的影响 全部被吸纳入了微小的整形噪音qiq_i 我们审视这个方程yi+1=a0yi+v+qi(mod2114).y_{i+1} = a_0 y_i + v + q_i \pmod{2^{114}}. 有个很小的偏移 还有个114bits大小的未知常数vv 参考paper中的差分思路 先令Ti≡a0yi+v+qi(mod2128)T_i \equiv a_0 y_i + v + q_i \pmod{2^{128}} 由 yi+1≡Timod  2114y_{i+1}\equiv T_i\mod 2^{114} 可以得到 Ti=yi+1+2114Ci,T_i = y_{i+1} + 2^{114} C_i,

先在128bits的数量级下对TT进行一次截断 方便分析低位到高位之间的进位 代数量化等式

再在114bits下把进位显式地拆出来 把代数式和已知量扯上关系

我们分析出来了一个14bits的量 CiC_i 再结合对于 LiL_i的低14位信息 xix_i 放在异或的信息中去处理 对于式子hi=(Zi mod 214)⊕xih_i = (Z_i \bmod 2^{14}) \oplus x_i 可以写出hi+1≡a0hi+a1xi+b1,low+CARRY(mod214)h_{i+1} \equiv a_0 h_i + a_1 x_i + b_{1,\text{low}} + \text{CARRY} \pmod{2^{14}} 而由于截断 实际上也就能写成 hi+1≡a0hi+a1xi+b1,low+Ci(mod214)h_{i+1} \equiv a_0 h_i + a_1 x_i + b_{1,\text{low}} + C_i \pmod{2^{14}} 做到这一步 终于看到了可以处理的线性等式

我们观察这个递推 a0  a1  Ci  xia_0\;a_1\;C_i\;x_i都是已知/可计算的 由前面的异或关系 也可以完整的恢复 hih_i 那么考虑用做差消去未知的bb的低位 我们可以计算恢复ΔCi=Ci+1−Ci(mod214)\Delta C_i = C_{i+1} - C_i \pmod{2^{14}} 再看向下半图 由 Ti≡a0yi+v+qi(mod2128)T_i \equiv a_0 y_i + v + q_i \pmod{2^{128}} 依旧做差Di=Ti+1−TiD_i = T_{i+1} - T_i 得到Di+1=a0Di+δi(mod2128)D_{i+1} = a_0 D_i + \delta_i \pmod{2^{128}} 详细写一下yi+1=Ti mod 2114=Ti−2114Oiy_{i+1} = T_i \bmod 2^{114} = T_i - 2^{114} O_i 那么Ti+2−Ti+1=a0(Ti+1−Ti)−a02114(Oi+1−Oi)+(qi+2−qi+1)(mod2128)T_{i+2} - T_{i+1} = a_0 (T_{i+1} - T_i) - a_0 2^{114} (O_{i+1} - O_i) + (q_{i+2} - q_{i+1}) \pmod{2^{128}} 即Di+1=a0Di+[qi+2−qi+1−a02114(Oi+1−Oi)](mod2128)D_{i+1} = a_0 D_i + \left[ q_{i+2} - q_{i+1} - a_0 2^{114} (O_{i+1} - O_i) \right] \pmod{2^{128}} δi=qi+2−qi+1−a02114(Oi+1−Oi)(mod2128)\delta_i = q_{i+2} - q_{i+1} - a_0 2^{114} (O_{i+1} - O_i) \pmod{2^{128}} 而 2114⋅(Oi+1−Oi)≡2114⋅[(Oi+1−Oi) mod 214](mod2128)2^{114} \cdot (O_{i+1} - O_i) \equiv 2^{114} \cdot \left[ (O_{i+1} - O_i) \bmod 2^{14} \right] \pmod{2^{128}} 不论Oi+1−OiO_{i+1}-O_i多大 左移114位后 高14位以上的取值都被截断丢弃 所以在21142^{114}这个系数下 可以用先前计算的 ΔCi=Ci+1−Ci=approxi(mod214)\Delta C_i = C_{i+1} - C_i = \text{approx}_i\pmod{2^{14}} 放进去代替它得到 δi=qi+2−qi+1−a0⋅2114⋅approxi(mod2128)\delta_i = q_{i+2} - q_{i+1} - a_0 \cdot 2^{114} \cdot \text{approx}_i \pmod{2^{128}} 对于δ\delta 它完全可算 误差噪声也是非常小的 综上分析 列个表

变量符号 定义 状态 备注 / 作用
完全已知
a0,a1a_0, a_1 乘数 aa 的低 128 位和高 128 位 已知 题目给定的公开全局常数。
xix_i 内部状态 LiL_i 的最低 14 位 已知 通过第一步的动态规划(DP)完全枚举并锁定。
ZiZ_i 上下半部的异或真实值 Li⊕HiL_i \oplus H_i 已知 知道步长 rtr_t 后,对输出左旋解旋得到。
hih_i 上半部的最低 14 位 已知 由 hi=(Zi mod 214)⊕xih_i = (Z_i \bmod 2^{14}) \oplus x_i 直接算出。
qiq_i 底部 14 位算式产生的进位 已知 由 qi=(a0xi+u−xi+1)/214q_i = (a_0 x_i + u - x_{i+1}) / 2^{14} 精确算出。
approxi\text{approx}_i 进位 CiC_i 相邻做差在模 2142^{14} 下的近似 已知 利用上半部异或特征提取出来的进位变化差值。
δi\boldsymbol{\delta_i} 阻碍 DiD_i 成为等比数列的巨大干扰项 已知 由 qi+2−qi+1−a0⋅2114⋅approxiq_{i+2} - q_{i+1} - a_0 \cdot 2^{114} \cdot \text{approx}_i 算出的精确常数。
KiK_i 用于“平移”的特解序列 已知 人为构造:K0=0,Ki+1=a0Ki+δi(mod2128)K_0=0, K_{i+1} = a_0 K_i + \delta_i \pmod{2^{128}}。
用于格规约
obsi\mathrm{obs}_i 给到 Lattice 的目标向量的高 14 位快照 已知 obsi=approxi−top⁡14(Ki)(mod214)\mathrm{obs}_i = \text{approx}_i - \operatorname{top}_{14}(K_i) \pmod{2^{14}}。
ηi\boldsymbol{\eta_i} 真实的极小物理噪声 未知但极小 仅仅来源于高低位截断相减时的借位误差,ηi∈{0,1,2}\eta_i \in \{0, 1, 2\}。
期望在格中被恢复
yiy_i 下半部 LiL_i 的高 114 位商序列 未知 我们的最终目标状态。
TiT_i 包含 yiy_i 的 128 位核心多项式 未知 Ti≡a0yi+v+qi(mod2128)T_i \equiv a_0 y_i + v + q_i \pmod{2^{128}}。
DiD_i TiT_i 相邻做差的序列 未知 Di+1=a0Di+δi(mod2128)D_{i+1} = a_0 D_i + \delta_i \pmod{2^{128}}。
Gi\boldsymbol{G_i} 绝对纯净的几何序列(等比数列) 格规约的目标 Gi+1≡a0Gi(mod2128)G_{i+1} \equiv a_0 G_i \pmod{2^{128}}。

格规约#

现在很明显 手动构造偏移序列 Ki+1=a0Ki+δi(mod2128)K_{i+1} = a_0 K_i + \delta_i \pmod{2^{128}} 做差 Gi=Di−Ki(mod2128)G_i = D_i - K_i \pmod{2^{128}} 就只用恢复等比列 Gi+1=a0Gi(mod2128)G_{i+1} = a_0 G_i \pmod{2^{128}} 同时计算 obsi=approxi−top⁡14(Ki)\mathrm{obs}_i = \text{approx}_i - \operatorname{top}_{14}(K_i) obsi\text{obs}_i只会由于进位产生很小的整数误差 参考paper的格思路

做格B=(1a0a02⋯a0n−1021280⋯0002128⋯0⋮⋮⋮⋱⋮000⋯2128).B = \begin{pmatrix} 1 & a_0 & a_0^2 & \cdots & a_0^{n-1} \\ 0 & 2^{128} & 0 & \cdots & 0 \\ 0 & 0 & 2^{128} & \cdots & 0 \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ 0 & 0 & 0 & \cdots & 2^{128} \end{pmatrix}. 预期解CVP找 t=(obs02114,  obs12114,  …,  obsn−12114)t = (\mathrm{obs}_0 2^{114}, \; \mathrm{obs}_1 2^{114}, \; \dots, \; \mathrm{obs}_{n-1} 2^{114}) 考虑KannanEmbedding B′=(B0Ttμ)=(1a0a02⋯a0n−10021280⋯00⋮⋮⋮⋱⋮⋮000⋯21280t0t1t2⋯tn−12114)B' = \begin{pmatrix} B & \mathbf{0}^T \\ t & \mu \end{pmatrix} = \begin{pmatrix} 1 & a_0 & a_0^2 & \cdots & a_0^{n-1} & 0 \\ 0 & 2^{128} & 0 & \cdots & 0 & 0 \\ \vdots & \vdots & \vdots & \ddots & \vdots & \vdots \\ 0 & 0 & 0 & \cdots & 2^{128} & 0 \\ t_0 & t_1 & t_2 & \cdots & t_{n-1} & 2^{114} \end{pmatrix} 预期

(x0,  k1,  …,  kn−1,  −1)=(X,  −1)(x_0, \; k_1, \; \dots, \; k_{n-1}, \; -1) = (X, \; -1)

S=(X,  −1)⋅(B0Ttμ)=(X⋅B+(−1)⋅t,X⋅0T+(−1)⋅μ)=(e0,  e1,  …,  en−1,  −2114)S = (X, \; -1) \cdot \begin{pmatrix} B & \mathbf{0}^T \\ t & \mu \end{pmatrix}=\Big( X \cdot B + (-1) \cdot t, \quad X \cdot \mathbf{0}^T + (-1) \cdot \mu \Big) = \Big( e_0, \; e_1, \; \dots, \; e_{n-1}, \; -2^{114} \Big) ei=Gi−obsi2114=low⁡114(Gi)−ηi2114e_i = G_i - \mathrm{obs}_i 2^{114} = \operatorname{low}_{114}(G_i) - \eta_i 2^{114}在128bits系统中数量级为115bits 用20size的BKZ可以打通 并且能直接得到GiG_i 可以用∥obsi−(Gi≫114)∥∈{0,1,2}\|\mathrm{obs}_i - (G_i \gg 114)\|\in\{0,1,2\}验证正确性


利用约束恢复完整状态#

好了 费了那么多劲终于恢复了GG 先算Di=Gi+Ki.D_i = G_i + K_i. 而yi+2−yi+1=Di mod 2114.y_{i+2} - y_{i+1} = D_i \bmod 2^{114}. 那么yi+1≡y1+∑k=0i−1Dk(mod2114)y_{i+1} \equiv y_1 + \sum_{k=0}^{i-1} D_k \pmod{2^{114}} 也写成 yi=si+c(mod2114),y_i = s_i + c \pmod{2^{114}}, 既然cc是可以手动算的 那每轮算一下 判断sis_i超过21142^{114}多少 手动给它减去 得到 yi=si+c−wrapi⋅2114y_i = s_i + c - \text{wrap}_i \cdot 2^{114} 相当于化简了这个同于方程 当然也可以根据对cc的范围估计 进一步拆分化简 不过最后都会用约束送入z3/进行剪枝爆搜 就偷个懒了)

回代最后我们有的56组方程Hi+1=a0Hi+a1(xi+214yi)+B+carryi=Li⊕Zi(mod2128)H_{i+1} = a_0 H_i + a_1 (x_i + 2^{14} y_i) + B + \text{carry}_i=L_i\oplus Z_i \pmod{2^{128}} 没有乱七八糟的模了 就是一个XOR和加法乘法 约束足够厉害 解个高位问题不大了


ok 还原了yy 就可以还原偏移了 然后逆向LCG 用md5校验一下

gpt大人硬扛了这么大的码量…

exp.sage

SU_RSA#

找到了几乎原本的paper https://eprint.iacr.org/2025/1281.pdf ↗ 应该是对着 Theorem 6出的 但是用Theo5的板子 也就是经典Herrmann-May算法也可以出 赛后问了别的师傅 基本只要二元Copper的思路正确 LLM都能手写HM算法解出了

先贴下exp 再讲讲算法大体的思路链

exp.sage

首先从私钥的生成过程 可以得到ed−k(p−1)(q−1)=1ed - k(p-1)(q-1) = 1 然后令 x=kx = k,y=p+q−Sy = p+q-S 模ee下可以得到一个简单的方程 f(x,y)=x(S−N−1+y)−1≡0(mode)f(x,y) = x(S - N - 1 + y) - 1 \equiv 0 \pmod e 按照paper的定义 相关的bound为 X=Nα+δ0−1X = N^{\alpha+\delta_0-1} Y=NγY = N^\gamma

但是多元copper面对不齐次多项式效果不好 或许是shift多项式次数不相对齐 导致造出来的系数格不太均匀 这里就体现在 xyxy变元 那么可以引入一个新的变元 u=xy−1u=xy-1 我们不去那么关注x  y  ux\;y\;u自己本身的线性关系 而是利用小根的代数特性 让格帮我们在归约中自动用上潜在的代数关系 找出来小根 此时三元多项式方程长这样g(x,y,u)=x(S−N−1)+ug(x,y,u)=x(S-N-1)+u 虽然还是看起来2元 但是在构造shift多项式的时候 就会出现 y⋅f(x,y)=y(Ax+xy−1)=Axy+xy2−yy \cdot f(x,y) = y(Ax + xy - 1) = Axy + xy^2 - y 再替换 A(u+1)+y(u+1)−y=Au+A+yu+y−y=Au+yu+AA(u+1) + y(u+1) - y = Au + A + yu + y - y = Au + yu + A 实现了降次 优化了格的分布

后续看别的师傅的对话记录 也发现了另一种构造shift的思路 Jochemsz-May 方法

核心在于对于 f(x,y)=Ax+xy−1≡0(mode)f(x,y) = Ax + xy - 1 \equiv 0 \pmod e 线性项xx正常延展shift即可 而非线性的yy项 在延展过程中应该有选取的放入系数 对于 h(x,y)=yjf(x,y)kem−kh(x,y) = y^j f(x,y)^k e^{m-k} jj 从11取到 tt;kk 不能从 00 开始,而是从 ⌊mt⌋j\lfloor \frac{m}{t} \rfloor j 取到 mm 从而减少冗余次项

如果预见次数大于1 或者不太齐次的系统都最好不要直接上 copper/Lattice了 一般来说shift多项式的要求都会很高🫠

SU_Lattice#

逆向部分见 SUCTF-Lattice ↗

经过简单的测试 总结靶机流程如下

  • 动态生成1组64位整数mm

  • 动态生成 24组64位整数 ai  xia_i\;x_i

  • 能够无限获取hint

    • 先计算 y=∑i=023aixi(modm)y = \sum_{i=0}^{23} a_i x_i \pmod m
    • 再返回 h=y≫20h = y \gg 20
    • 再更新状态 (x0,x1,…,x22,x23)⟶(x1,x2,…,x23,y)(x_0,x_1,\dots,x_{22},x_{23}) \longrightarrow(x_1,x_2,\dots,x_{23},y)
  • 要求恢复 ans≡∑ximod  m\text{ans}\equiv\sum x_i\mod m

搜到是2323.pdf (iacr.org) ↗

相关的格构造 以及结式思路都来自文章 下面用比较通俗的思路讲讲如何用上文章的构造


对模数的恢复#

先看hint 能够得到 xi=220hi+eix_{i} = 2^{20}h_i + e_i 并且知道对于任何一个特别的xx 都能找到递推关系 xt+24−a23xt+23−⋯−a0xt≡0(modm)x_{t+24} - a_{23}x_{t+23} - \dots - a_0x_t \equiv 0 \pmod m 这个关系只取决于mm与aia_i 但是如果直接把 xi=220hi+eix_{i} = 2^{20}h_i + e_i代入 会导致eie_i与aia_i是大小不齐的未知量 而且mm也未知 完全无法建立有效约束

但是我们转换一下思路 反正要求的都是xix_i 能否直接找到小系数did_i 使得 ∑i=0r−1dixi≡0(modm)\sum_{i=0}^{r-1} d_i x_i \equiv 0 \pmod m 此时再带入等式 有220∑dihi+∑diei≡0(modm)2^{20}\sum d_i h_i + \sum d_i e_i \equiv 0 \pmod m

那么 ∑dihi≡−∑diei220(modm)\sum d_i h_i \equiv -\frac{\sum d_i e_i}{2^{20}} \pmod m 注意到eie_i和2202^{20}是一个数量级的 而∑d\sum d也是比较小的量 左边的hih_i却都已知 那就转化为了一个熟悉的SIS问题 解did_i 使得∑dihi≈0mod  m\sum d_ih_i\approx0\mod m 而在常见的格构造后 是可以解出did_i的


那么解出did_i有什么用呢?

我们知道 因为递推是存在的 那么满足 ∑i=0r−1dixi≡0(modm)\sum_{i=0}^{r-1} d_i x_i \equiv 0 \pmod m 的序列did_i是一定存在的 而能够满足 上述要求的序列 就肯定满足 ∑dihi≡−∑diei220(modm)\sum d_i h_i \equiv -\frac{\sum d_i e_i}{2^{20}} \pmod m 也就是 ∑dihi≈0mod  m\sum d_ih_i\approx0\mod m 我们可以取很多组的hh 通过递推关系的约束进一步限定dd 只要信息足够 那么满足条件的did_i序列就是可以被直接确定的 不会存在别的可能 做到这一步 我们终于触及到了第一个名词 化零多项式 F(x)=d0+d1x+d2x2+⋯+dr−1xr−1F(x) = d_0 + d_1 x + d_2 x^2 + \dots + d_{r-1} x^{r-1}

不难理解 did_i序列不是唯一的 那么通过格方法就可以恢复至少两组化零多项式 也就是对于任何化零多项式 都可以被分解 F(x)≡Q(x)⋅f(x)(modm)F(x) \equiv Q(x) \cdot f(x) \pmod m

ff就是这里特征多项式

构造结式的手法是给多项式灌入一个巨大的取值 然后对输出的结果做GCD 这样子除了基本底层线性保留的性质 带来的因子 其他的性质都会被剥离 也就R1,2=Res(f1,f2)=K1⋅m24R_{1,2} = \mathrm{Res}(f_1, f_2) = K_1 \cdot m^{24}

R1,3=Res(f1,f3)=K2⋅m24R_{1,3} = \mathrm{Res}(f_1, f_3) = K_2 \cdot m^{24}

做GCD 如果结果是24×64=153624\times64=1536大小的数 那尝试开2424方根 再简单搜一下可恢复mm

恢复mm后就做多项式GCD就可以恢复特征多项式ff 从而恢复系数aia_i


恢复LFSR状态#

由x24+k≡∑j=023Pk,jx24+j(modm)x_{24+k} \equiv \sum_{j=0}^{23} P_{k, j} x_{24+j} \pmod m 代入xi=220hi+eix_i = 2^{20}h_i + e_i 可以得到 e24+k≡∑j=023Pk,jej−Ck(modm)e_{24+k} \equiv \sum_{j=0}^{23} P_{k, j} e_j - C_k \pmod m 其中 Ck≡220(h24+k−∑j=023Pk,jhj)(modm)C_k \equiv 2^{20} \left( h_{24+k} - \sum_{j=0}^{23} P_{k, j} h_j \right) \pmod m

其中Pk,jP_{k,j}可以通过对递推系数的伴随矩阵 Q=(00…0a010…0a101…0a2⋮⋮⋱⋮⋮00…1a23)Q = \begin{pmatrix} 0 & 0 & \dots & 0 & a_0 \\ 1 & 0 & \dots & 0 & a_1 \\ 0 & 1 & \dots & 0 & a_2 \\ \vdots & \vdots & \ddots & \vdots & \vdots \\ 0 & 0 & \dots & 1 & a_{23} \end{pmatrix}的不断幂次算出 构造49维预期格 L=(219219…219(C24+219)…(C47+219)01…0P24,0…P47,0⋮⋱⋮⋮⋱⋮00…1P24,23…P47,2300…0m…0⋮⋮⋱⋮⋮⋱⋮00…00…m)L = \begin{pmatrix} 2^{19} & 2^{19} \dots 2^{19} & (C_{24} + 2^{19}) \dots (C_{47} + 2^{19}) \\ 0 & 1 \dots 0 & P_{24, 0} \dots P_{47, 0} \\ \vdots & \ddots \vdots & \vdots \ddots \vdots \\ 0 & 0 \dots 1 & P_{24, 23} \dots P_{47, 23} \\ 0 & 0 \dots 0 & m \dots 0 \\ \vdots & \vdots \ddots \vdots & \vdots \ddots \vdots \\ 0 & 0 \dots 0 & 0 \dots m \end{pmatrix} 会有u=(1−e0−e1…−e23−q0−q1…−q23)\mathbf{u} = \begin{pmatrix} 1 & -e_0 & -e_1 & \dots & -e_{23} & -q_0 & -q_1 & \dots & -q_{23} \end{pmatrix} 满足(1−e0−e1…−e23−q0−q1…−q23)×(219219…219(C24+219)…(C47+219)01…0P24,0…P47,0⋮⋱⋮⋮⋱⋮00…1P24,23…P47,2300…0m…0⋮⋮⋱⋮⋮⋱⋮00…00…m)=(219,219−e0,…,219−e47)\begin{pmatrix} 1 & -e_0 & -e_1 & \dots & -e_{23} & -q_0 & -q_1 & \dots & -q_{23} \end{pmatrix} \times\begin{pmatrix} 2^{19} & 2^{19} \dots 2^{19} & (C_{24} + 2^{19}) \dots (C_{47} + 2^{19}) \\ 0 & 1 \dots 0 & P_{24, 0} \dots P_{47, 0} \\ \vdots & \ddots \vdots & \vdots \ddots \vdots \\ 0 & 0 \dots 1 & P_{24, 23} \dots P_{47, 23} \\ 0 & 0 \dots 0 & m \dots 0 \\ \vdots & \vdots \ddots \vdots & \vdots \ddots \vdots \\ 0 & 0 \dots 0 & 0 \dots m \end{pmatrix}=\begin{pmatrix} 2^{19}, & 2^{19}-e_0, & \dots, & 2^{19}-e_{47} \end{pmatrix}

右侧都是2192^{19}量级的小量 可以被格出 恢复了e0,…,e23e_0,\dots,e_{23} 可以恢复(x24,x25,…,x47)(x_{24},x_{25},\dots,x_{47}) 逆推 xt≡c0−1(xt+24−∑i=123cixt+i)(modm)x_t \equiv c_0^{-1}\left(x_{t+24}-\sum_{i=1}^{23} c_i x_{t+i}\right)\pmod m 即可

exp.sage

SU_Isogeny#

是一道CI-HNP问题 可以参考 https://eprint.iacr.org/2023/1409.pdf ↗ 基本上是一样的情景复现了 这里简单学习一下相关同源的结构

def cal(A, sk):
    E = EllipticCurve(F, [0, A, 0, 1, 0])
    for sgn in [1, -1]:
        for e, ell in zip(sk, pl):
            for i in range(sgn * e):
                while not (P := (p + 1) // ell * E.random_element()) or ell * P != 0:
                    pass
                E = E.isogeny_codomain(P)
        E = E.quadratic_twist()
    return E.montgomery_model().a2()
py

相关的代数几何名词看的头痛😵 总结一下

对于一条蒙哥马利曲线 EA:y2=x3+Ax2+xE_A: y^2 = x^3 + Ax^2 + x 可以计算它的同源 B=2A−12A+2B = \frac{2A - 12}{A + 2} C=2A+122−AC = \frac{2A + 12}{2 - A} 对应的曲线 可以得到

AB−2A+2B+12=0AB - 2A + 2B + 12 = 0 BC−2B+2C+12=0BC - 2B + 2C + 12 = 0 AC+2A−2C+12=0AC + 2A - 2C + 12 = 0

落到题目里面使用 就是对于题目给我们的公钥Ma  MbM_a\;M_b 可以计算同源公钥

Mac=2Ma−12Ma+2M_{ac} = \frac{2M_a - 12}{M_a + 2} Mab=2Ma+122−MaM_{ab} = \frac{2M_a + 12}{2 - M_a}

提交给Gift接口 可以获得剩下的两组额外信息高位

A=Ahi⋅2200+xA = A_{hi} \cdot 2^{200} + x B=Bhi⋅2200+yB = B_{hi} \cdot 2^{200} + y C=Chi⋅2200+zC = C_{hi} \cdot 2^{200} + z

带回上式 可得

f(x,y,z)=(A0+x)(B0+y)−2(A0+x)+2(B0+y)+12f(x,y,z) = (A_0 + x)(B_0 + y) - 2(A_0 + x) + 2(B_0 + y) + 12 g(x,y,z)=(B0+y)(C0+z)−2(B0+y)+2(C0+z)+12g(x,y,z) = (B_0 + y)(C_0 + z) - 2(B_0 + y) + 2(C_0 + z) + 12 h(x,y,z)=(A0+x)(C0+z)+2(A0+x)−2(C0+z)+12h(x,y,z) = (A_0 + x)(C_0 + z) + 2(A_0 + x) - 2(C_0 + z) + 12

此时可以用作者在https://github.com/juliannowakowski/automated-coppersmith ↗ 提供的板子来打出来低位

exp.sage

SU_Restaurant#

https://eprint.iacr.org/2026/387.pdf ↗

基于热带半环的签名系统 可以复用Theo6 使用SMT的方法来解出 之前UniCTF也出过一道相关的热带半环的题 总之在逻辑结构层面的逻辑电路对于Z3相当友好 转化出 (c=a1)∨(c=a2)∨⋯∨(c=at)(c = a_1) \lor (c = a_2) \lor \dots \lor (c = a_t)利于求解的式子 然后针对可计算的公钥量 W(r)=A(r)⊗B(r)W^{(r)} = A^{(r)} \otimes B^{(r)} 从签名层面建立约束即可 Z3可能会找到伪解 多重连打几次就行 犯懒原谅这次先放exp了 后续遇到相关半环题一定详细写分析

exp.py

SUCTF2026
https://www.zt2misay2.cn/blog/suctf2026
Author Misay
Published at 2026年3月16日