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 1337
gacha-gate.tar.gz 5 crypto Catch 134 146 cat-themed crypto, nc catch.chal.idek.team 1337
catch.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 1337
sad_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 1337
FITM.tar.gz *
的为赛后做出的sanity
check
survey
rev
constructor
dd
导出 42 byte|
Exploit
"""
Applies the decryption algorithm found in the binary's constructor
function to the extracted data.
"""
# The 42 encrypted bytes from address 0x403040
=
=
= 0xFFFFFFFF # Used to simulate 32-bit register behavior
= 0 # Simulates the %ecx register
# The encrypted byte is loaded into the low part of a 32-bit register
=
# Key 1: The full 32-bit value of the %ecx register
=
# Key 2: The loop counter right-shifted by 1
= >> 1
# constant
= 0x5a
# The decryption emulates the 32-bit 'xorl' operations
= ^ ^ ^
# The final character is the lowest byte of the result
= & 0xFF
+=
# The %ecx register is incremented for the next loop
= &
return
=
ski
txt
file from here: (((S ((S I) (K (K I)))) (K K)) _F0)) _F1)) _F2))...
Exploit
=
=
=
=
=
misc
gacha-gate
#!/usr/bin/env python3
= 32
=
= 10
=
=
=
return ,
=
return ,
=
, return f , ~
=
, =
, return f ,
return
=
return
return
=
=
: =
: =
=
=
=
=
return
=
return == ,
=
,
=
return
=
return
=
,
return
中缀表达式与逆波兰表达式
|
(或), ^
(异或), &
(与), ~
(非)。它们的优先级关系为:~
> &
> ^
> |
。核心算法:调度场算法 (Shunting-yard Algorithm)
shunting_yard_fix
函数,它实现了调度场算法。这个算法就像一个火车站的调度场:算法流程
((A & B) | C)
被分解为 (
, (
, A
, &
, B
, )
, |
, C
, )
。output
列表)。(
: 将左括号压入运算符栈(op_stack
)。)
: 从运算符栈中不断弹出运算符并发送到输出队列,直到遇到一个左括号为止。然后弹出这个左括号,但不会发送到输出队列。&
)在低优先级的运算符(例如 |
)之前被处理。示例:
((A & B) | C)
的转换过程Token op_stack
output
解释 (
[
[]
压入左括号 (
[(]
[]
再次压入左括号 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
# remote connection
=
= 1337
# Operator precedence and associativity
# Precedence: `~` (highest) > `&` > `^` > `|` (lowest)
# All are left-associative except for `~`, which is unary
=
# Associativity: L for left-associative, R for right-associative (not needed here)
=
"""
Correctly converts an infix expression to RPN using the Shunting-yard algorithm.
This version handles operator precedence and parentheses correctly.
"""
=
=
# Regex to tokenize the expression: numbers, variables, operators, and parentheses
# Note: the `|` operator is a bitwise OR, not a regex OR
=
# Simple state machine to distinguish unary `~`
# The server always formats it as `(~...)`, so `~` is always followed by `(`
# We can rely on this predictable format.
# Pop the left parenthesis
# 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.
=
# 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
pass # Ignore opening parentheses
# Closing a subexpression.
# Check for a binary op first. `(a op b)` -> `a b op`
=
=
=
# Check for a unary op. `(~a)` -> `a ~`
=
=
# 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.
=
=
# Tokenize the expression, including all symbols
=
# pop '('
# Handle unary `~`
# 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
# Binary operators
return
=
=
=
=
return
return
=
Crypto
Catch
题目
# 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.
= 0xe5db6a6d765b1ba6e727aa7a87a792c49bb9ddeb2bad999f5ea04f047255d5a72e193a7d58aa8ef619b0262de6d25651085842fd9c385fa4f1032c305f44b8a4f92b16c8115d0595cebfccc1c655ca20db597ff1f01e0db70b9073fbaa1ae5e489484c7a45c215ea02db3c77f1865e1e8597cb0b0af3241cd8214bd5b5c1491f
# Through cryptic patterns, our cat deciphers its next move.
# Each step is guided by a fragment of the cat's own secret mind.
=
= * + *
= * + *
return ,
# Enter the Cat: curious wanderer and keeper of hidden paths.
# The cat's starting position is born of pure randomness.
=
=
# Deep within, its mind holds a thousand mysterious fragments.
=
=
break
# The epic chase begins: the cat ponders and strides toward the horizon.
# A moment of reflection: choose a thought from the cat's endless mind.
=
# With each heartbeat, the cat takes a cryptic step.
=
, = ,
, # When the wild spirit reaches the edge, it respects the boundary and pauses.
%=
%=
break
# When the cosmos beckons, the cat reveals its secret coordinates.
return
# Adventurer, your quest: find and connect with 20 elusive cats.
=
# At the start, you and the cat share the same starlit square.
=
# But the cat, ever playful, dashes into the unknown...
# Your turn: recall the cat's secret path fragments to catch up.
=
# Step by step, follow the trail the cat has laid.
=
=
# At last, if destiny aligns...
# Triumph at last: the final cat yields the secret prize.
题目核心
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
"""Parses an 8-byte part into a tuple of 4 integers (matrix elements)."""
=
=
=
=
return
"""
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)
=
=
, ,
# The path was built in reverse, so we flip it.
return b
continue
=
,
=
, , , = * - *
continue
# Calculate the numerators for the inverse transformation
= * - *
= - * + *
# The key insight: intermediate coordinates must be integers.
=
return None # Should not be reached
=
# Parse initial position
=
=
,
# Parse the cat's mind
=
=
=
=
# Parse final position
=
=
,
# Solve the round
=
return
=
=
return
=
Sadness ECC
# chall.py
=
= True
assert ,
= ,
, = False
return
return
return
return
return
# ——— Distinct‑points case ———
= -
= -
=
= *
= %
=
=
= *
= -
= - 1337
= -3
= *
= *
= +
= +
= -
= %
= -
= *
= -
= %
return
= - 1337
= *
= 3 *
=
= 2 *
= *
= %
=
=
= *
= -
= - 1337
= -3 *
= *
= +
= -
= %
= -
= *
= -
= %
return
=
=
= +
= +
>>= 1
return
return f
return == and ==
=
= False
=
; continue
= True
=
= *
=
=
=
; continue
break
break
解题思路:奇异曲线上的坐标恢复
第一步:恢复曲线方程
isOnCurve
被隐藏,因此必须 从点的加法运算 (__add__
) 中反向推导出曲线方程。 点加法分为两种情况:两点相加 和 点倍加。通常点倍加的公式更简洁,是推导的突破口。= - 1337
= *
# ...
= *
s
表示点 $P(x,y)$ 处切线的斜率。 可写成数学形式:第二步:分析挑战与漏洞
第三步:求解策略 —— 多项式结式 (Polynomial Resultant)
1. 构造约束多项式
2. 第一次消元
3. 取公共根
4. 回代求解
__add__
(或使用上面的公式)计算小结
Sadness ECC - Revenge
Happy 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
题目
#Some magic from Willy Wonka
= 170829625398370252501980763763988409583
= 164164878498114882034745803752027154293
= 125172356708896457197207880391835698381
return %
#The diamond ticket is hiding inside chocolate
=
assert == 26
assert == b
assert == b
=
=
=
#Willy Wonka are making chocolates
#And he put the golden ticket at the end
#Augustus ate lots of chocolates, but he can't eat all cuz he is full now :D
=
#Compress all remain chocolates into one
= b
#The last chocolate is too important, so Willy Wonka did magic again
=
=
= *
=
=
=
= # A small gift
#How can you get it ?
"""
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
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,
)
func main()
idek{tks_f0r_ur_t1ck3t_xD}
Sadness ECC - Revenge
# bad ecc revenge exp.py
# sage
# PoW
=
=
= True
assert ,
= ,
, = False
return
return
return
return
return
# ——— Distinct‑points case ———
= -
= -
= 1 /
= *
=
= 1 /
= ** 3
= *
= -
= - 1337
= -3
= *
= *
= +
= +
= -
=
= -
= *
= -
=
return
= - 1337
= *
= 3 *
= 1 /
= 2 *
= *
=
= 1 /
= **3
= *
= -
= - 1337
= -3 *
= *
= +
= -
=
= -
= *
= -
=
return
=
=
= +
= +
>>= 1
return
return f
return == and ==
return
return
return % == 0
# Write the solver code to a file
# ECC
return
return
# Establish connection
=
# --- Handle PoW using the subprocess method ---
=
# Run the external solver script
=
=
# io.interactive()
= 18462925487718580334270042594143977219610425117899940337155124026128371741308753433204240210795227010717937541232846792104611962766611611163876559160422428966906186397821598025933872438955725823904587695009410689230415635161754603680035967278877313283697377952334244199935763429714549639256865992874516173501812823285781745993930473682283430062179323232132574582638414763651749680222672408397689569117233599147511410313171491361805303193358817974658401842269098694647226354547005971868845012340264871645065372049483020435661973539128701921925288361298815876347017295555593466546029673585316558973730767171452962355953
=
, , , =
, , # print(f"{s1}")
# print(f"{s2}")
# print(f"{s3}")
= - - + 3**/ + **3/**3 -*/ +
= -
=
=
= **2 - **3
= **2 - **3
=
=
=
=
=
=
=
=
= -
= -
=
=
=
=
=
=
= -
= -
=
=
= +
= ,
, =
# idek{the_idea_came_from_a_Vietnamese_high_school_Mathematical_Olympiad_competition_xD_sorry_for_unintended_:sob:_75f492115a34ff4324212e09e24aa5bd}
Happy ECC - Revenge
# chall.py
# Edited a bit from https://github.com/aszepieniec/hyperelliptic/blob/master/hyperelliptic.sage
=
=
=
# 1.
= # a*U1 + b*U2 == g
, , = # c*g + h3*(V1+V2) = d
, , = *
= *
# h1 * U1 + h2 * U2 + h3 * (V1+V2) = d = gcd(U1, U2, V1-V2)
# 2.
=
=
=
# 3.
=
=
= %
# 4.
=
=
=
# 5.
= ,
, # (6.)
# 7.
return ,
return
=
, return
return
=
=
return
return
=
=
= +
= +
>>= 1
return
return True
return False
=
=
=
=
=
return
=
=
=
continue
=
=
=
= *
=
= ** *
=
continue
= * /
+= *
return
=
=
,
=
=
= 0
=
; continue
=
=
= *
+= 1
continue
=
=
break
break
# crypto/Happy ECC - Revenge
=
# ===================================================================
# ## Part 1: Official PoW Solver Code
# ## This code will be written to a file named 'kctf_solver.py'.
# ===================================================================
=
# Write the solver code to a file to be called by the subprocess
"""Parses the server's polynomial string into a Sage polynomial object."""
return
# Connect to the challenge server
# conn = remote('happy-ecc.chal.idek.team', 1337)
=
# --- Part 1: Solve Proof-of-Work ---
# 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...")
=
=
# Run the external solver script and capture its output
=
=
pass
# --- Part 2: Recover f(x) via CRT ---
=
=
=
,
=
=
=
= ,
,
=
, =
# --- Part 3: Find True Order of G and Solve ---
=
# CORRECTED LINE: Use the Frobenius polynomial as you suggested.
=
# We still need the Jacobian object to work with its elements.
=
# Parse the point G from server output
=
=
, = ,
,
# Create the point G as a Jacobian element in Sage
=
= # Identity element
# Find the true order by factoring the group order
=
=
= //
=
break
=
=
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}$。|| ||
#!/usr/bin/env sage
# Filename: solve5.sage
# Precise imports
# --- Configuration ---
=
= 1337
= 11
=
"""
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
=
= 12 * + 1
break
# 2. In GF(p), find a primitive 12th root of unity (w)
=
<> =
. = ^12 - 1
=
assert == 12,
= None
# *** FIX: Iterate directly over the roots, not tuples ***
=
break
assert is not None,
# 3. Query the server
=
=
=
=
=
# 4. Use IDFT to calculate candidate coefficients
=
= /12
= 0
+= * ^
*=
# 5. Feed the data to the Go solver via its stdin
=
= f
# --- Start the Go Solver Subprocess ---
=
# --- Interact with Server and Feed Solver ---
=
# --- Get Result from Go and Submit ---
=
return
=
// Filename: solver4_final.go
package main
import (
"bufio"
"fmt"
"math/big"
"os"
"runtime"
"strings"
"sync"
"time"
"github.com/schollz/progressbar/v3"
)
type CrtTask struct
type CrtContext struct
func NewCrtContext() *CrtContext
// A robust, symmetric implementation of CRT
func (a, m, b, n *big.Int) (*big.Int, *big.Int)
func findSecretDFS(level int, currentS, currentMod *big.Int, tasks []CrtTask, resultChan chan *big.Int, bar *progressbar.ProgressBar, ctx *CrtContext) *big.Int
func main()
Web
*midi visualizer
上传目录
的文件,但我们并没有文件名,于是构造暴露出目标目录下所有信息> curl
> curl |
<a href="./flag-41589d62bca4bcc031e55ca2.mid">flag-41589d62bca4bcc031e55ca2.mid</a>