最终排名第一,NKCTF 2024 所有 crypto 赛题的题解。
电脑打比赛最累的一集(确信)。这次 crypto 有两题随便写的 exp 要跑近 100 h,最后好歹优化到单核 2h 内能跑完。
eZ_Math
题解
有如下关系:
1 | n = p * q * r * s |
- Step 1 可以得到 q 。
- Step 2 得到 q 之后,由 (5) 我们 可以得到 ,从而得到 和 。
- 从 (2) (概率性),从而分解 。
解密:
- flag 第一部分的加密一个简单的 上的离散对数,由于指数不大于 , 直接二项展开求 log,或者用 sage 内置的 p-adic rings 求 log 即可。
- flag 第二部分在模 的有限域上求解,因为指数 与 不互素,需要开 次根,这里也直接 sage 求解就行。
EXP
1 | from Crypto.Util.number import * |
GGH
EXP
整个加密过程很简单:
问题出在 error 向量 的生成方式,熵太小了,穷举即可解出秘密向量 ,审计源码可知,Delta 的值只能是 1,3,7,对应 error 只有三种可能。于是直接爆破 error ,解 Z 上的线性方程组即可。
1 | import re |
EZ-random
题解
这题思路其实很容易,但是实现上需要考虑挺多优化上的细节(当然不排除我的做法是非预期 orz)。
题目给出的数组 T 和 R 实际上对应了 上的线性方程,即未知的 key 是 64 byte,即 512 比特。T , R 给出了二元域 上 个关于 key 的线性方程。 表示取第 个比特,则第 个方程如下,
那么我们还需要 64 个方程才能完全恢复出 64 字节的 Key。考虑到 key 的生成方式,如果不加入随机性的话, 它是线性的,相邻两个字节可以提供 8 个线性方程。可惜加入随机的 offset 之后,我们无法建立确定性的线性方程。
好好好,那么就考虑爆破吧!猜测某个 state 的状态(256 个),爆破后面 7 轮的 state,我们能够得到额外 64 格比特信息,配合前面的方程信息,刚好能够解出所有比特,此时检查 key 的合理性即可。理想的复杂度是 : ,拿 python plus 版本的 pypy 跑应该没问题。
实际上 exp 写出来的复杂度 ,因为题目给出数据在前面 个方程上进行部分高斯消元的形式不完美,为了高效计算,舍弃了 8 个消元后的方程,虽然这些方程也能用起来,但是比较麻烦,于是增加一点点暴力穷举的工作量。比赛时候的 exp 在 i7-13700 上用 pypy 3.10 单核预估时间 , 也就 6 个小时,开 8 核并行一下,运气不错很快出来了。写 wp 时优化了一下,每个子进程只需要 20 s,单核缩短到 1 h。
1 | $ pypy.exe exp.py 96 128 |
EXP
高斯消元 448 个方程,512 个变量,得到前 440 个比特的表达式(以后 64 + 9 = 72 个比特为未知元),比较完美的高斯消元形式应该可以把前 448 个比特用后面 64 个未知元表示。
1 | # sage |
最终 exp (8 核命令行手动并行)
1 | # -*- coding: utf-8 -*- |
drsn
题解
由于 ascii 字节高位固定为 0,在经过只有 轮密钥异或、shuffle (没有 Sbox)的加密算法时,这个比特位会加密成一个固定的值(对于相同 key),因此如果没有最后一轮与 key 的 rng 无关的纯随机的 shuffle,密文某个固定比特位会不变。
1 | def encrypt_block(self, block: bytes) -> bytes: |
题目对密文加入了一个随机 shuffle 后 (random.shuffle(block);
),ascii 字节高位 0 的密文比特位置不固定了,但是这个值固定是不变。这意味着给定一个 task(bit, n)
,存在某个不固定的位置的值恒为 0 (或者 1) ,其他位置为 0,1 均匀分布。
最后这个 shuffle 把固定位置不变的比特特征抹去了,但是并不能改变 0,1 比特数目不平衡(非随机分布)的特性,实际上密文的分布和明文的分布是一致的。即加密的密文(或者说明文分布)中的 0,1 分布比随机分布更不均匀,可以通过 0, 1 数量的差(or 方差)区分出这两个分布,从而通过题目中的 Oracle 挑战。
EXP
1 | from pwn import remote, context, log, process |
fly to the hyper
题解
定义一个 HyperellipticCurve :
记 ,给定一 80 个点集 ,和一个子集和 满足 :
我们需要恢复出秘密向量 $\vec b = (b1, \cdots, b_{80})$ ,其中 10 比特已知(password 生成过程中的字节高位为 1),实际上只需要恢复 70 比特。
这是一个基于 HyperellipticCurve 上 jocabian 群(divisor)的子集和(subset-sum-problem abbr ssp)问题,现学 HyperellipticCurve ,尝试寻找 jocabian 上一些特殊性质求解子集和,然后发现 ssp 的求解和群关系不大。
于是尝试转换到 上的 Modular SSP 问题,要完成这个转换,我们首先要完成下面几个步骤:
- 求 jocabian 群的阶。
- 寻找 jocabian 群的一个生成元(或者高阶元)
- 确认该 jocabian 群上的离散对数问题可解(或者部分可解)。
完成上述转换之后,求解 80 个点(因为 password 的每个字节的高位为 1 ,实际只需要 70 个)对 的离散对数,得到一个数集 , 离散对数对应子群的阶记为 , 同时求出点 对 的离散对数 ,从而得到 Modular SSP :
解这个 Modular SSP 即可恢复出 password 的所有比特。
一些细节
计算阶
计算阶:参考论文 An elementary introduction to hyperelliptic curves , 0CTF 2020 Simple Curve 中有具体实现。
得到的阶为:
1 | 2^5 * 3^12 * 13^2 * 19^2 * 101 * 157 * 271 * 461 * 523 * 701 * 829 * 1531 * 1747 * 20341 * 57901 * 71161 * 623881 * 93383701 * 1563346261 * 118050254940601 * 1559805923143337117401 |
这个阶相当光滑,于是可以应用 pohlig hellman 算法求解离散对数。
生成元
由于这个 jocabian 群上的 sage 内置点加并不快,随机生成寻找生成元还是很困难的,但是我们只要找到某个光滑子群的生成元即可,一类很好找的生成元,它的阶包含上述每个素因子都至少一次。
寻找到这个生成元:
1 | base = (x + z^179 + z^178 + z^175 + z^174 + z^172 + z^166 + z^164 + z^163 + z^162 + z^161 + z^159 + z^157 + z^155 + z^154 + z^153 + z^152 + z^149 + z^147 + z^145 + z^143 + z^142 + z^140 + z^139 + z^136 + z^134 + z^133 + z^131 + z^128 + z^127 + z^120 + z^116 + z^115 + z^109 + z^107 + z^103 + z^100 + z^96 + z^93 + z^92 + z^91 + z^90 + z^89 + z^87 + z^83 + z^78 + z^74 + z^73 + z^71 + z^70 + z^68 + z^66 + z^58 + z^57 + z^53 + z^52 + z^50 + z^49 + z^48 + z^44 + z^43 + z^42 + z^41 + z^36 + z^34 + z^32 + z^30 + z^29 + z^28 + z^25 + z^24 + z^23 + z^20 + z^19 + z^18 + z^16 + z^15 + z^14 + z^13 + z^12 + z^11 + z^10 + z^8 + z^7 + z^6 + z^4 + z^3 + z^2 + z + 1, z^177 + z^176 + z^175 + z^174 + z^173 + z^171 + z^169 + z^166 + z^165 + z^163 + z^162 + z^161 + z^159 + z^158 + z^155 + z^152 + z^151 + z^145 + z^141 + z^140 + z^139 + z^137 + z^135 + z^133 + z^131 + z^130 + z^128 + z^127 + z^126 + z^125 + z^119 + z^118 + z^115 + z^114 + z^113 + z^110 + z^108 + z^106 + z^104 + z^103 + z^102 + z^100 + z^97 + z^96 + z^95 + z^94 + z^81 + z^78 + z^77 + z^76 + z^75 + z^74 + z^73 + z^71 + z^69 + z^61 + z^60 + z^53 + z^51 + z^49 + z^46 + z^42 + z^41 + z^40 + z^38 + z^36 + z^35 + z^34 + z^32 + z^29 + z^28 + z^26 + z^24 + z^23 + z^22 + z^21 + z^14 + z^7 + z^6 + z^5 + z^4 + z^2) |
这个子群的阶为:
1 | base_order = 2^4 * 3^3 * 13 * 19 * 101 * 157 * 271 * 461 * 523 * 701 * 829 * 1531 * 1747 * 20341 * 57901 * 71161 * 623881 * 93383701 * 1563346261 * 118050254940601 * 1559805923143337117401 |
忽略掉大的素因子,最终选取的子群阶为:
1 | m_order = 2^4 * 3^3 * 13 * 19 * 101 * 157 * 271 * 461 * 523 * 701 * 829 * 1531 * 1747 * 20341 * 57901 * 71161 * 623881 |
离散对数
Sage 内置 discrete_log
有些 bug (9.2),需要 hook 改动的地方也比较多,不如自己写一个 pohlig hellman + bsgs 算法。还可以进行 batch log 优化。
Modular SSP 问题
属于是经常能碰到的问题了, 这里我们模数有 163 比特,集合大小 n = 70 ,背包密度小于 0.6 ,用一般的格即可规约出短向量,从而恢复出目标向量 。
EXP
SSP 转换 (i7-13700 上跑 1h40min,还可以进行优化,相同 base 的 bsgs 过程可以复用大部分计算)
1 | 100%|█████████████████████████████████████████████████████████████████████████████████| 70/70 [1:40:53<00:00, 86.48s/it] |
Sage:
1 | from Crypto.Util.number import * |
SSP Solver
1 | from random import shuffle |