这次 getting there 分类比赛的时候我看了两题,幸亏队友给力,帮我解决了其他的题目。此处要感谢队友,同时也要 继续批判临场背刺的叛徒
Nahan 题目描述 有一个未知 $l$ 位素数 $m$,满足 $8 \mid l$。题目给了一个互动选项:输入 $s, t$,要满足
$2 l < 6 (\log_{2}{s} + 1) < 3 l$
$2 l < 6 (\log_{2}{t} + 1) < 3 l$
记 $r$ 为不小于 $(st) \oplus (2^l)$ 的最小素数,并且 这个 $r$ 在每次互动的时候不能有重复的
将 $m \oplus r$ 的二进制位进行乱序,得到新的二进制数 $u$
返回结果 $n = r u$
上述互动选项至多可以进行 $l/2$ 次。
我的尝试 因为一开始我们连 $l$ 的数值都不知道,所以我们需要通过报错,先把 $l$ 的数值搞出来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 from pwn import *class Gao : def __init__ (self, n ): self.conn = process(['python3' , 'another.py' ]) self.n = n def gao_mybit (self ): self.conn.sendlineafter('[Q]uit\n' , 'G' ) s = t = 2 **self.n self.conn.sendlineafter('s, t: \n' , f'{s} ,{t} ' ) s = self.conn.recvline() return not (b'requirements' in s) def gao (self ): return self.gao_mybit() if __name__ == '__main__' : n = 1 while True : g = Gao(n) if g.gao(): print (f'OK {n = } ' ) break else : g.conn.close() n += 1
发现一个可行的 $n$ 后,一直重复输入 $(s, t) = (2^n, 2^n)$,求得 $l/2$ 的值,也就是求得 $l$ 的值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 from pwn import *class Gao : def __init__ (self ): self.conn = process(['python3' , 'another.py' ]) self.n = 82 def gao_l (self ): self.conn.sendlineafter('[Q]uit\n' , 'G' ) s = t = 2 **self.n self.conn.sendlineafter('s, t: \n' , f'{s} ,{t} ' ) self.conn.recvline() s = self.conn.recvline() if b'at most' in s: print (s) return False else : return True def gao (self ): while self.gao_l(): pass if __name__ == '__main__' : g = Gao() g.gao()
当然实际上也可以根据 $6 (n + 1) > 2 l$ 以及 $8 \mid l$ 算得这个 $l$
这里偷看了一下队友的协作文档,知道了正确的 $l$ 的数值,这个数值会影响到后面的解题。官方一个礼拜就关掉远程服务器了,真他妈的坑爹。
为了方便复现,后面我们就把 flag 设置为 CCTF{fuck_the_traitor_CAONIMA!}
来测试
然后在这里我们观察到上面我写的第三个条件,他那个代码写得太矬了,以至于后面那个限制都没有限制到:
1 2 3 4 5 6 7 8 9 while True : pr(f"{border} Options: \n{border} \t[G]et Nahan value! \n{border} \t[S]end secret! \n{border} \t[Q]uit" ) R, _b = [], False r = next_prime(s * t ^ 2 ** l) if r in R: die(border, 'You cannot use repeated integers! Bye!!' ) else : R.append(r)
疑似有点幽默了
然后重点看他那个返回结果,这里因为我们也可以算出 $r$,所以实际上 $u$ 也是已知的。因为我们输入的 $s, t$ 满足 $s$ 和 $t$ 的位数落在 $l/3$ 至 $l/2$ 位,所以实际上 $(st) \oplus (2^l)$ 相当于给 $r$ 添加了个高位。
问题的难点还是在第四步的乱序上:由于乱序,所以我们不能知道 $m \oplus r$ 的每一位是什么样的,但是我们能通过一些不变的统计信息,譬如 $m \oplus r$ 的二进制中 0 的个数和 1 的个数。以及,因为 $r$ 被限制成了素数,所以并不能精确地控制 $r$ 的取值,譬如没法让 $r$ 变成偶数
然后如果用线性代数的思维的话,如果我们把 $m \oplus r$ 的每个二进制位给异或起来的话,相当于 $GF(2)$ 下把 $m$ 和 $r$ 都看成一个向量,上述的统计信息为 汉明距离 。但是似乎还是没办法使得这个方程满秩 (秩为 $l$),就是利用差分,秩也是 $2/3 l$
不过这里如果说到求汉明距离,实际上我们已经脱离了 $GF(2)$ 的范畴了,因为我们要统计某种 加和 ,所以我们自然想到还是在 $\mathbb{Z}$ 中处理;同时注意到未知的 $m$ 被看成二进制向量后,每一个分量只有 0 和 1 两种取值,这就想到了格基规约中的短向量。现在问题就变成如何去用表达式等价地表示一个“异或”:定义 $m_i, r_i$ 为 $m, r$ 对应位置上的二进制分量,我们构造一个函数:
如果有学过数字电路、理论计算机等相关课程的就应该会清楚,上面这个形式实际上就是布尔逻辑中的 析取范式 ,可以通过 卡诺图 等工具辅助化简
上面这个函数是关于 $x$ 的一次函数,且等效于求 $x$ 和 $y$ 的“同或”,这点在真值表上可以进一步验证:
x
y
f(x, y)
x xor y
0
0
0
0
0
1
1
1
1
0
1
1
1
1
0
0
并且,根据已知的 $r_i$,我们可以将 $f(m_i, r_i)$ 进一步写成仅关于 $m_i$ 的形式:
然后关于 $u$ 的信息就转化成
其中 $z(u)$ 表示 $u$ 的二进制表示中 1 的个数。
上面的这个式子是关于 $m_0, m_1, \ldots, m_{l-1}$ 的线性式子,有 $l/2$ 组这样的式子,虽然不是满秩的不能直接解方程,但是可以通过格基规约的方式得到短向量。
然后还有一个问题,就是 $s, t$ 的取值限制了($l+1$ 位的) $r$ 的二进制高 3 位必为 100
,也就是实际上只有后面的会约束到 $m$,不过如果我们能求出个大概的话,剩下的实际上枚举两下就行了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 from pwn import *from sage.all import *from Crypto.Util.number import *from tqdm import trangefrom contextlib import contextmanagerfrom time import perf_counter@contextmanager def timeit (task_name ): print (f"[-] Start - {task_name} " ) start = perf_counter() try : yield finally : end = perf_counter() print (f"[-] End - {task_name} " ) print (f"[-] Elapsed time: {end - start:.2 f} seconds" ) class Gao : def __init__ (self ): self.conn = process(['python3' , 'another.py' ]) self.n = 248 self.m_list = [] self.u_list = [] def get_nums (self ): self.conn.sendlineafter('[Q]uit\n' , 'G' ) s = getRandomNBitInteger(self.n // 2 - 1 ) t = getRandomNBitInteger(self.n // 2 - 1 ) def next_prime (n ): while True : if isPrime(n): return n else : n += 1 r = next_prime(s * t ^ 2 ** self.n) self.conn.sendlineafter('s, t: \n' , f'{s} ,{t} ' ) s = self.conn.recvline() n = int (s.split(b' = ' )[1 ]) assert n % r == 0 u = n // r u1 = sum (map (int , f'{u:b} ' )) u1 -= 1 m = [] rbits = list (map (int , f'{r:b} ' ))[1 :] u1 -= (1 - rbits[0 ]) + (1 - rbits[-1 ]) for ri in rbits[1 :-1 ]: ri = int (ri) if ri == 0 : m.append(1 ) else : m.append(-1 ) u1 -= 1 self.m_list.append(m) self.u_list.append(u1) def gao_lll (self ): M = matrix(ZZ, self.m_list) U = matrix(ZZ, [self.u_list]) I = identity_matrix(self.n-2 ) O = matrix(ZZ, 1 , self.n-2 , lambda i, j: 1 ) L = block_matrix(ZZ, [[2 * I, 0 , M.T], [ O, 1 , U]]) weights = [1 ] * (self.n-1 ) + [2 **64 ] * (self.n // 2 ) L = L * diagonal_matrix(weights) p = x_ = vector(ZZ, list (map (int , f'{p:b} ' ))[1 :-1 ] + [-1 ]) y_ = x_ * L print (f'{y_ = } ' ) print () with timeit('ortho LLL' ): L = L.LLL() L2 = L[:self.n // 2 - 1 , :self.n-1 ] with timeit('BKZ' ): L2 = L2.BKZ(block_size=28 ) for v in L2: print (v) print (f'{v.norm()**2 = } ' ) y2_ = vector(ZZ, [2 * int (x) - 1 for x in f'{p:b} ' [1 :-1 ]] + [-1 ]) print () print (f'{y2_.norm()**2 = } ' ) print () x2_ = L2.solve_left(y2_) print (f'{x2_ = } ' ) def gao (self ): print ('Building equation' ) for i in trange(self.n // 2 ): self.get_nums() print ('LLL' ) self.gao_lll() if __name__ == '__main__' : g = Gao() g.gao()
然后就会发现,第二步 BKZ 怎样都出不来解。
拼尽全力,博至无憾,仍无成效,遂放弃之。
等风哥哥在比赛中也在用这个格,也是解不出
队友的援助 然后去翻合作文档,结果也是卡在这一步了,啥东西都没看着。
然后去 1997 群里面问未央是怎么搞的,结果他直接说是原。
原神,启动!
说是之前 SEKAI CTF 里面的原题,说是在变量比较小,譬如只有二元的时候可以用 ortools 来求解。真的牛批,啊,那真的牛批
示例用法大概长这样,更牛逼的用法详见
1 2 3 4 5 6 7 8 9 10 from ortools.sat.python import cp_modelmodel = cp_model.CpModel() X = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, 'x' ) y = model.NewIntVar(cp_model.INT32_MIN, cp_model.INT32_MAX, 'y' ) model.Add(x-y == 3 ) model.Add(3 *x-8 *y == 4 ) solver = cp_model.CpSolver() status = solver.Solve(model) if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE: print ('x =' , solver.Value(x), ',y =' ,solver.Value(y))
然后我一看,这他妈的不是跟 z3-solver 的用法是一样的吗?然后我就去试了 z3-solver 去解,不出意外地卡住了,why? tell me, why?
然后把 ortools 接到代码里面,太变态了,妈妈的秒出,这还玩个蛋蛋。说是 ortools 有对二元变量这种做特别的优化。
有点难蚌,要是不知道可以用这玩意儿秒的话,不就在这里牢住了。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 from pwn import *from Crypto.Util.number import *from ortools.sat.python import cp_modelfrom tqdm import trangeclass Gao : def __init__ (self ): self.conn = process(['python3' , 'another.py' ]) self.n = 248 self.m_list = [] self.u_list = [] def get_nums (self ): self.conn.sendlineafter('[Q]uit\n' , 'G' ) s = getRandomNBitInteger(self.n // 2 - 1 ) t = getRandomNBitInteger(self.n // 2 - 1 ) def next_prime (n ): while True : if isPrime(n): return n else : n += 1 r = next_prime(s * t ^ 2 ** self.n) self.conn.sendlineafter('s, t: \n' , f'{s} ,{t} ' ) s = self.conn.recvline() n = int (s.split(b' = ' )[1 ]) assert n % r == 0 u = n // r u1 = sum (map (int , f'{u:b} ' )) u1 -= 1 m = [] rbits = list (map (int , f'{r:b} ' ))[1 :] u1 -= (1 - rbits[0 ]) + (1 - rbits[-1 ]) for ri in rbits[1 :-1 ]: ri = int (ri) if ri == 0 : m.append(1 ) else : m.append(-1 ) u1 -= 1 self.m_list.append(m) self.u_list.append(u1) def gao_ortools (self ): model = cp_model.CpModel() my_vars = [model.NewIntVarFromDomain(cp_model.Domain.FromValues([0 , 1 ]), f'x_{i} ' ) for i in range (self.n-2 )] for i in trange(self.n // 2 ): model.Add(sum (x * y for x, y in zip (self.m_list[i], my_vars)) == self.u_list[i]) solver = cp_model.CpSolver() status = solver.Solve(model) if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE: solution = [solver.Value(x) for x in my_vars] else : raise Exception("GG" ) p = [1 ] + solution + [1 ] p = int ('' .join(map (str , p)), 2 ) self.p = p def submit_answer (self ): self.conn.sendlineafter('[Q]uit\n' , 'S' ) self.conn.sendlineafter('secret: \n' , str (self.p)) def gao (self ): print ('Building equation' ) for i in trange(self.n // 2 ): self.get_nums() print ('ORTOOLS' ) self.gao_ortools() self.submit_answer() self.conn.interactive() if __name__ == '__main__' : g = Gao() g.gao()
所以说出来混,还是得靠队友,幸好有队友相助,让我了解了 ortools 这个神奇的东西。不像叛徒,只会落井下石。
Sobata 题目描述 生成随机的 512 位素数 $p$ 满足 $p \equiv 1 \pmod 6$。 然后生成随机的椭圆曲线 $E_p: y^2 = x^3 + d$,并且生成模 $p$ 下的 3-torsion $a$ 和 2-torsion $b$:
$a \ne 1$ 且 $a^3 \equiv 1 \pmod p$
$b \ne 1$ 且 $b^2 \equiv 1 \pmod p$
并且生成一个随机的 $c$。假设 flag 为 $m$,则产生一个点 $P(m+\delta, y) := P(m_1, y)$,其中 $\delta$ 为一个不小于 0 的最小的数使得 $P \in E_p$
远程有以下操作:
加密 flag 对于点 $P(m_1, y)$,先算出点 $Q(am_1, by)$,然后返回 $c Q$
Walking 输入一个 $E_p$ 上的点 $R(x, y)$,先算出点 $Q(ax, by)$,然后返回 $c Q$
Jumping 输入一个 $E_p$ 上的点 $R(x, y)$,以及一个整数 $n$。先算得 $c’$ 为 $c$ 模椭圆曲线阶的 $n$ 次方 ,也就是 $c’ = c^{n} \bmod{|E_p|}$。服务器先算出点 $Q(ax, by)$,然后返回 $c’ Q$
就,jumping 这个选项名字挺让人难蚌的
试得到 $m$。
我的解答 注意到这里我们是没有参数信息的,包括素数 $p$,椭圆曲线 $E_p$,都没有。所以我们首要的任务是恢复出椭圆曲线来。
不过其实也比较简单,通过 Walking 操作我们可以拿到一系列的点:首先我们通过 加密 flag 拿到 $E_p$ 上的一个点 $T$,记为 $T_0$,然后把 $T_0$ 输入进 Walking 操作得到 $T_1$,把 $T_1$ 输入进 Walking 操作得到 $T_2$,然后就可以恢复出曲线参数了。这个套路在本次比赛的 Ikkyu San 题目中也有用到,在此不再赘述。
然后就是如何利用 Jumping 操作得到 $m_1$。首先因为模型对于整数 $n$ 没有其他限制,所以可以输入 $n=-1$,这样 Jumping 就变成了可以利用 $c^{-1}$ 去做倍点了。
然后是 $a, b$ 的取值,因为各自分别是 3-torsion 和 2-torsion,所以可以直接本地生成一个 $a, b$,有 $1/2$ 的概率蒙对
然后就注意输入输出要是啥就行了。先整理一下步骤:
生成随机的 $a, b$,有 $1/2$ 的成功率
通过 加密 flag ,拿到 $T = cQ$
利用 Jumping ,输入 $R(a^{-1} x_T, b^{-1} y_T)$,以及 $n=-1$,此时服务器返回的是 $c^{-1} T = Q$
算出 $m_1 = a^{-1} c \bmod p$
通过 long_to_bytes(m1)
看到 flag
照着做,然后多怼几次就出来了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 from pwn import *from sage.all import *from Crypto.Util.number import *class Gao : def __init__ (self ): self.conn = remote("91.107.161.140" , "11177" ) def gao_one (self ): self.conn.sendlineafter('[Q]uit\n' , 'c' ) c = self.conn.recvline().decode().strip() self.cnt += 1 if 'y = ' in c: y = self.R(c.split('y = ' )[1 ]) return True , y else : return False , 0 def recv_P (self ): self.conn.sendlineafter('[Q]uit\n' , 'e' ) self.conn.recvuntil('is: ' ) self.P = eval (self.conn.recvline().decode().strip()) def walk_P (self ): x1, y1 = self.P self.conn.sendlineafter('[Q]uit\n' , 'w' ) self.conn.sendlineafter('E:' , f'{x1} ,{y1} ' ) self.conn.recvuntil('is: ' ) x2, y2 = eval (self.conn.recvline().decode().strip()) self.conn.sendlineafter('[Q]uit\n' , 'w' ) self.conn.sendlineafter('E:' , f'{x2} ,{y2} ' ) self.conn.recvuntil('is: ' ) x3, y3 = eval (self.conn.recvline().decode().strip()) d1 = y1**2 - x1**3 d2 = y2**2 - x2**3 d3 = y3**2 - x3**3 p = gcd(d2-d1, d3-d2) self.E = EllipticCurve(GF(p), [0 , d1]) self.P = self.E((x1, y1)) self.p = p def get_flag (self ): self.conn.sendlineafter('[Q]uit\n' , 'j' ) x1, y1 = self.P.xy() a = pow (randrange(self.p), ((self.p - 1 ) // 3 ), self.p) b = pow (randrange(self.p), ((self.p - 1 ) // 2 ), self.p) ax1 = a * x1 % self.p by1 = b * y1 % self.p self.conn.sendlineafter('E:' , f'{ax1} ,{by1} ' ) self.conn.sendlineafter('point:' , '-1' ) self.conn.recvuntil('is: ' ) s = self.conn.recvline().decode().strip() x2, y2 = eval (s) x2 = x2 * a % self.p print (f'{long_to_bytes(int (x2)) = } ' ) def gao (self ): self.recv_P() self.walk_P() self.get_flag() self.conn.interactive() if __name__ == '__main__' : g = Gao() g.gao()
得到 m1 为:
1 CCTF{L1n3Ari7y_iN_w4lkIn9_ECC!\x7f
调整一下,flag 为:
1 CCTF{L1n3Ari7y_iN_w4lkIn9_ECC!}
赛后复盘 其实仔细观察的话,上面的解法有个地方没有说清:为什么当 $P(x, y) \in E_p$ 的时候,$Q(a x, b y) \in E_p$ 一定会成立?
在比赛的时候没想明白这一点,实际上根据我们的椭圆曲线形式 $E_p: y^2 = x^3 + d$,以及 $a^3 \equiv b^2 \equiv 1 \pmod{p}$,直接代入定义式验算即可:
然后还有一点是关于 $a$ 和 $b$ 的求法
赛后在 discord 上看到有人可以直接通过输入参数的方法求出 $a$ 和 $b$
在 Jumping 运算中,令 $R = Q, n = 0$,服务器返回的结果即为 $(a x_Q, b y_Q)$
看着比较简单直接,比赛的时候脑子有点过载了,这个都没想到,唉。
Vainrat 题目描述 题目的代码基于高精度的实数域
记 flag 为 $l$ 位十进制数 $m$,则令 $x_0$ 为 $10^{-l} m$。并且产生随机 $y_0$ 满足 $y_0 < x_0$。
然后远程提供一个迭代并看数的选项:
$x_i = {1/2} (x_{i-1} + y_{i-1})$
$y_i = (x_i y_{i-1})^{1/2}$
如果 $i \le 12$,则不能获得 $y_i$ 的值
如果 $12 < i < 19$,则有一定概率获得 $y_i$ 的值
如果 $i \ge 19$,则必定能获得 $y_i$ 的值
试求 $m$
我的解答 比较能直接想到的一个思路就是利用递推式的关系解除某个 $(x_i, y_i)$,然后往前推。
那么自然会想到去利用 两个连续的 $(y_i, y_{i+1})$ 去推。那么就假设已知 $y_i$ 和 $y_{i+1}$,利用第二个式子就有:
然后就是 $(x_i, y_i) \to (x_{i-1}, y_{i-1})$ 的前向递推:
重复做到 $(x_0, y_0)$ 即可。
然后就是恢复 $m$,这里我做得比较土味:枚举 $l$,不断试乘查看 $\lfloor10^{l} x_0 \rfloor$ 即可。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 from pwn import *from sage.all import *from Crypto.Util.number import *class Gao : def __init__ (self ): self.conn = remote("91.107.252.0" , "11117" ) nbit = 110 prec = 4 * nbit self.R = RealField(prec) self.cnt = 0 def gao_one (self ): self.conn.sendlineafter('[Q]uit\n' , 'c' ) c = self.conn.recvline().decode().strip() self.cnt += 1 if 'y = ' in c: y = self.R(c.split('y = ' )[1 ]) return True , y else : return False , 0 def recv_y0 (self ): self.conn.recvuntil('y0 = ' ) self.y0 = self.R(self.conn.recvline().decode().strip()) def gao (self ): self.recv_y0() ys = [] while True : ok, y = self.gao_one() if ok: ys.append(y) else : ys = [] if len (ys) == 2 : break y0, y1 = ys x1 = y1**2 / y0 x0 = 2 * x1 - y0 while (self.cnt > 1 ): x1, y1 = x0, y0 y0 = y1**2 / x0 x0 = 2 * x1 - y0 self.cnt -= 1 for i in range (300 ): msg = long_to_bytes(int (10 **i * x0)) if msg.startswith(b'CCTF{' ): print (msg) self.conn.interactive() if __name__ == '__main__' : g = Gao() g.gao()
得到 flag 为
1 CCTF{h3Ur1s7!c5_anD_iNv4rIanTs_iN_CryptoCTF_2025!}
运气好拿了个一血