Misay's blog

Back

爆零了,赛后看其他战队师傅的WP,发现有很多题还是在能解决的范围内的,关于调教AI的手法还有些许不足,也有些知识点不太全面,来进行一个复现

SuanP01y#

代数结构基于多项式环R=F2[x]R=F_2[x]上的商环

S=Rxr−1    r=16381S=\frac{R}{x^r-1}\;\;r=16381

给出两个稀疏随机多项式t0    t1t_0\;\;t_1 满足

  • 指数采样空间为0∼r30\sim \frac{r}{3}
  • 多项式不同指数数固定为d=41d=41
  • 多项式系数只为0或者1 即为二元稀疏多项式

随后进行随机的平移得到我们的

h0=t0Xk0    h1=t1Xk1h_0=t_0X^{k_0}\;\;h_1=t_1X^{k_1}

告诉你

hint=h0h1−1=t0t1−1Xk0−k1hint=h_0h_1^{-1}=t_0t_1^{-1}X^{k_0-k_1}

需要从中恢复h0h_0

给出的hinthint形如一系列包含商环元XX的代数式 我们可以把它会恢复到我们商环上对应到多项式P(x)P(x),也就是进行一个Lifting

P(x)≡hintmod  xr−1P(x)\equiv hint\mod x^r-1

此时我们的问题就转化为

P(x)≡h0(x)h1(x)mod  xr−1(1)P(x)\equiv\frac{h_0(x)}{h_1(x)}\mod x^r-1\tag{1}

要你去恢复我们的h0(x)h_0(x)

参考星盟师傅的思路 对于1式 我们可以进行如下的一个调整

P(x)≡Xk0−k1t0t1mod  xr−1P(x)\equiv X^{k_0-k_1}\frac{t_0}{t_1}\mod x^r-1

我们可以得到

P(x)Xk1−k0≡t0t1mod  xr−1P(x)X^{k_1-k_0}\equiv \frac{t_0}{t_1}\mod x^r-1

δ=k1−k0\delta=k_1-k_0完全可以爆破 那我们转化为

Q(x)≡t0t1Q(x)\equiv \frac{t_0}{t_1}

因为t0  t1t_0\;t_1的限制很足 可以放心进行多项式重构 我们补充点知识点

已知模多项式m(x)=xr−1m(x)=x^r-1 多项式w(x)w(x),当次数上界给定deg⁡numden≤B\deg num den \le B下,对于问题

w(x)≡num(x)den(x)mod  m(x)w(x)\equiv \frac{num(x)}{den(x)}\mod m(x)

如果

deg(num)+deg(den)<deg(m)deg(num)+deg(den)<deg(m)

那么我们的多项式重构比例是唯一的 这里我们的t0  t1t_0\;t_1的阶为r3\frac{r}{3} 恰好加起来小于m(x)m(x)的rr

那么关于有理重构的手法 山海关的师傅们给出了一个很优雅的EEA重构

用GPT-5.1-Codex试了下 能够直接看出我们的背景 BIKE/QC‑MDPC 下的私钥恢复 ↗

exp by Codex

Repair#

main.rs

lib.rs

题目构造了一个基于Rust的配对加密系统 逻辑如下

  • 启动时:用 setup() 生成 (msk, mpk),固定 id,派生 partial_sk→sk/“pk。随机取一对 GT 密钥 pairing_key,用 enc(&pk, id, pairing_key)生成挑战密文 ct。对 FLAG 做kdf(pairing_key)后异或得到encrypted_flag`。
  • 输出一行 banner,8 段 | 分隔:id_hex | DST_hex | pk_gt | h1(id)_g2 | c1 | c2 | c3 | encrypted_flag_hex。
  • 交互:读取用户回传的 c1'|c2'|c3';校验格式、非零、子群;仅拒绝与原始 (c1,c2,c3) 完全相同的输入。其它都解密:shared = dec(sk, CT{c1',c2',c3'}),再打印 hex(kdf(shared))。

因为这里的检测逻辑是

  • 格式检查:读取一行,按 | 切成三段,逐段用 parse_gt/parse_g1/parse_g2 解码,失败即 “bad”。
  • 零元检查:a2.is_zero() || a3.is_zero() 拒绝零点。
  • 子群检查:is_in_correct_subgroup_assuming_on_curve() 确保在正确的子群。
  • 原始密文检查:如果 c1'==c1 && c2'==c2 && c3'==c3 则 “no”,拒绝解密原始挑战。

所以说我们可以对我们的c1 c2 c3进行一个合法的重随机处理 让我们的靶机无意间对自己加密的flag进行解密 因为我们的计算过程是在EG上

c1=M×pktc_1=M\times pk^t c2=g×tc_2=g\times t c3=p×tc_3=p\times t

我们进行一个新的随机化 即

c1‘=c1×pkr=M×pkt+rc_1^{`}=c_1\times pk^r=M\times pk^{t+r} c2‘=c2+g×r=g×(t+r)c_2^{`}= c_2+g\times r=g\times (t+r) c3‘=c3+p×r=p×(t+r)c_3^{`}=c_3+p\times r=p\times (t+r)

那么我们传回我们的(c1', c2', c3')就相当于用新的随机数r+tr+t对相同的明文MM进行加密 那么解密得到的kdf(M')就是一致的 直接XOR即可还原flag

exp

SuanHash#

Task.py

suanHash.py

Task要求在4轮内找到一组碰撞的明文msg,500轮后即可获得flag

先把SuanHash喂给LLM 审计后得到以下特点

SuanHash 结构

  • 海绵式:状态 128 位,rate 64、capacity 64(在实现中拆成 hi 64 / lo 64)。
  • 初始化:随机 perm_seed 生成 AES-ECB 置换 _permute;随机 cfg_hi/cfg_lo 作为 IV。
  • _pad_to_blocks:标准 0x80 + 0 填充到 16 字节倍数。
  • _core
    1. 初态 s0 = perm((cfg_hi<<64) | cfg_lo);拆成 upper, lower。
    2. 吸收:对每块 blk 做 mixed = ((upper<<64)|lower) ^ blk;s = perm(mixed);更新 upper = s>>64, lower = s&((1<<64)-1);同时记录 last_low = mixed & cap_mask。
    3. 榨出:输出比特来自迭代 perm,每轮取 cur_hi & rate_mask 进入高部,取 (cur_w ^ cur_lo) & cap_mask 进入低部,cur_w 初始为 last_low,下一轮设为输入的低 64 位。
    4. digest/hexdigest 缓存,copy 复制 cfg 和 perm seed。

关键点:输出的低 64 位是 cur_w ^ cur_lo,其中 cur_w 是上一轮输入低 64 位;在单块消息时,cur_w 就是第一块的低 64 位(被 XOR 进输出)。

Task 逻辑

  • 每轮重新构造 SuanHash(随机 perm 和 cfg),允许 4 次查询;如无碰撞则直接失败;共 500 轮,全部通过给 flag。
  • suanhash_fn 固定了 base 实例,但在每轮对 base.copy(),保证同一轮内配置一致,不同轮重新随机。

碰撞原因与构造 对单块消息 B(含填充):

  • 吸收后 t = ((upper0<<64) | lower0) ^ B,s = perm(t)。
  • 输出块时,低部为 low_out = t_low ^ s_low(因为 cur_w = t_low,cur_low = s_low);高部为 s_high。
  • 将该哈希 H(B) 作为第二块再喂入:
    • 新输入的低 64 位等于 low_out,而这一轮的 cur_w(上一输入低 64)正好也等于 low_out,因此榨出时低部变成 low_out ^ new_s_low;高部来自 new_s_high。
    • 关键:第二块吸收前的输入为 mix2 = ((s_high<<64)|s_low) ^ H(B)。如果两条消息的第一块低 64 位相同,则它们的 t_low 相同,low_out 相同。由于第二块是各自的 H(B),mix2 的低 64 位一致,高 64 位也一致(因为 s_high 仅由同样的低输入决定 perm 输出)。于是两条路径在进入第二块时状态完全一致,后续 padding 块一致,最终输出一致。

LLM没怎么说人话()

核心思路就是输入给AES置换的mixed的差分等价关系人工可控 我们假设喂进去的块是B 我们的输出是

  • high = U1 也就是吸收B后的高64位
  • low = (L0 xor B_low) xor L1 L0/1是吸收前/吸收后的State低64位, B_low是喂进去Pad后的低64位

首先high是没有任何干扰的 low的话 我们很明显知道B_low 而L0 也就是吸收前的state 在我们4轮轮询中是不变的

因为已知填充规则 我们有四次 先喂数据X Y

那么我们能得到 (high1,low1) (high2,low2)

吸收后State高位的差分就直接是 high_delta = high1 xor high2

那低位呢? 我们注意到X_low Y_low已知 那么直接去xor把我们喂的数据块给剥离 也就是

(low1 xor X_low) xor (low2 xor Y_low) 可以得到 L0 xor L0 xor Lx1 xor Ly1 = Lx1 xor Ly1

转换回我们想看到的 就是 Low1 xor Low2 = Lx1 xor Ly1 = (low1 xor X_low) xor (low2 xor Y_low) = low_delta

所以我们能构造出任意两条数据喂进去吸收之后的状态差分Delta

接下来我们利用这个Delta去构造对齐完成碰撞

我们先输入一个块M 它被吸完后对应状态S1 我们知道AES吃的是 mixed1 = M xor S1

我们构造N = M xor Delta 那么吸收完后对应的状态S2 想要AES吃的 mixed2 = N xor S2 = mixed1

此时因为N xor S2 = M xor S2 xor Delta

而我们知道所有的状态前后差分都是Delta 那么对应M N S1 S2 我们进行XOR 能将S2的状态纠正回S1

就得到了mixed1 = mixed2 此时AES置换输出相同 后续混淆也不用管了

思路理清楚了 让LLM给出了一个构造 第一个块选取低64位一致的X Y 第二块选取对应的哈希H(X) H(Y)

exp

RCTF2025
https://www.zt2misay2.cn/blog/rctf2025
Author Misay
Published at 2025年11月18日