周末的时候做了 NPUCTF Crypto 部分的题目,题目有些一言难尽,但还是有很多收获,写了点 Write up。
认清形势,建立信心
附件: task.py
1 | from Crypto.Util.number import * |
分析与解
考点:简单数论、DLP
分析
$$
c = m^e \pmod n
\\
c_1 = 2^e \pmod n
\\
c_2 = 4^e \pmod n
\\
c_3 = 8^e \pmod n
\\
\text{其中,} c, c_1, c_2, c_3 是已知的,其余的均未知。
$$
未知参数过少,正是麻烦所在。
基本思路是分别求出
n,e,之后就可以对c进行解密
解析
1. 参数 n
$2, 4, 8$, 可以看成 $2^1, 2^2, 2^3$,故 $c_1, c_2, c_3$ 存在如下关系:
$$
c_1^2 = c_2 \pmod n
\\
c_1^3 = c_3 \pmod n
\\
\therefore n , | , gcd(c_1^2 - c_2, c_1^3 - c_3)
$$
所以可以得到 $n$
1 | import gmpy2 |
得到:
1 | gcd = 2 * 18195301 * 28977097 |
2. 参数 e
$$
c=2^e \pmod {18195301 * 28977097}
$$
这是典型的 DLP(Discrete Logarithm Problem,离散对数问题)
3. Wolfram 爆破
可以用 Wolfram:(应该是用纯暴力枚举的方法,虽然底层是 C 语言,但之前二次剩余跑了十七个小时也没跑出来 🤧)
1 | Simplify[ |
得到结果:
1 | C[1] \[Element] Integers && C[1] >= 0 && k == 808723997 + 4393724623740 C[1] |
得到 e=808723997,并在终端进行简单验证:
1 | assert pow(2, 808723997, n) == c1 |
4. 常规解密
1 | # -*- coding: utf-8 -*- |
1 | npuctf{345y} |
3. 另解 BSGS 算法
在第三步爆破的时候,可以用 BSGS 算法。
BSGS(Baby Step Giant Step) 虽然不是 DLP 算法中 时间空间复杂度最低的算法,但相对于 Pollard’s $\rho$ 算法(当前认为最优的算法)实现起来容易很多,时间复杂度($O(\sqrt n)$)相同,所以无论是 ICPC 还是 CTF 都是最优的选择之一。
![]()
C 语言需要调大整数库,实现起来稍微比较麻烦。
Python 的 BSGS 板子(没有优化):
1 | def bsgs(g, a, p): |
先把原式子做如下推导
$$
c=2^e \pmod { 18195301 * 28977097 } \Rightarrow
\begin{cases}
c=2^{e_1} \pmod { 18195301 }
\\
c=2^{e_2} \pmod { 28977097 }
\end{cases}
\
c \in Z_{ 18195301 }^{\star} ; and ; c \in Z_{ 28977097 }^{\star}
\
\
\therefore \exist ,e’ \in Z_{min{ 28977097, 18195301 }} ;
\
s.t.
\begin{cases}
2^{e’} = 2^{e_1} \pmod { 18195301 }
\\
2^{e’} = 2^{e_2} \pmod { 28977097 }
\end{cases}
\
\Rightarrow
\begin{cases}
{e’} = e_1 \pmod {\phi{ (18195301) }}
\\
{e’} = e_2 \pmod {\phi{ (28977097) }}
\end{cases}
$$
分别用 BSGS 算法求出指数 $e1$, $e2$,然后用中国剩余定理求出 $e’$(下面是 0xDktb 师傅的代码+注释)
1 | from Crypto.Util.number import * |
这是什么觅🐎
附件: zip
解压之后得到图片

分析与解
第一印象
密文:
1 | F1 W1 S22 S21 T12 S11 W1 S13 |
容易想到字母对应星期x 的英文首字母:
Monday - M, Tuesday - T1, Wednesday - W, Thursday - T2,Friday - F, Saturday - S1, Sunday - S2
紧接着的数字是行号相关的,所以猜测:
1 | F1 W1 S22 S21 T12 S11 W1 S13 |
细节补充
还有两个细节:
1被差掉了- 数字 2 - 8 有圈
细节一
猜测:1 -> $。即 所有 1 都替换成 $
1 | flag{3$$25$44$$8} |
猜测:日历上的1跳过,即 W1 表示 8
1 | flag{38125144818} |
细节二
是否暗示 flag 所有的字符集都是在 2-8 之间呢?那 F 等之后的数字不意味着行,而意味着向后走 n 位。
1 | F1 W1 S22 S21 T12 S11 W1 S13 |
这个猜想最大的问题在于 S22 = S13 ,不是唯一标识。
结合细节一和细节二:
1 | 31658416 # 结合红圈,移位 |
均失败…… 😭😭😭
正解
实际上,在第一步得到数字之和,转成字母就是 flag 😲
1 | F1 W1 S22 S21 T12 S11 W1 S13 |
陷入沉思
果然是我想多了吗??
可能是这方面的题目没怎么研究过😭
Classical_Cipher
附件:flag.zip
key.txt
1 | 压缩包密码: gsv_pvb_rh_zgyzhs |
压缩包密码
容易发现,已知明文中 pvb 和 key 存在互补关系(ord(c) + ord(m) - 2*ord('a') % 25 == 0)(atbash 密码的典型特征)
写个简单脚本处理一下:
1 | cipher= "gsvpvbrhzgyzhs" |
得到压缩包密码:
1 | the_key_is_atbash |
图案密码
经过解压后得到图片:
解压之后得到图片 flag.png

可以看到猪圈密码和四个陌生的符号
1 | j ? ? p p l j ? ? j ? ? k |
那四个陌生的符号可能是图标含义英文的首字母。
后来看 WP 的时候才知道是古埃及象形文字……(为什么破解古典密码的人这么多 😭,我……)
1 | j l w p p l j w l j o d k |
Mersenne_twister
附件1:cipher.txt
1 | cef4876036ee8b55aa59bca043725bf350a5e491debdef7ef7d63e9609a288ca1e2c82a7fe566bd8709e73c8d495ea504a486ed11189faf8e6fb35617e47d2d1ad5e4783e96afeaae9f7104ec477fb39fe4ec619bf58289709e15c4449f03fc51cba918cd0ebfdc12376b41e7815406482733b3b200826b6c78d86563edaea94dccf459a4291517a4b8367d7b4a53aeecd7e0accf661bfc726f5ba62e1c0e04100108ad32e7d5711f780185cba5cf31d328bee84066be4ab9582cf9d4bfe3c6f96a7732e1c37d800c90fd46277147f0a26c149dcd5eeb0f2df0c075627bc220be5eefdd67186056ac28c21e155a7f247664aaecdb498134de274df10114d1f06f84dd21820f150d69c9439d909dec0f5ccfeab61b62db2ea91d31bc8163ff16c7f458006bd5ac4a5f5bfae2770b23ccfb7195b76aa0a9aa146831667a7b9fe08c19e691afadccb3ca5169ef3fabaa3dad47d536e89ed4cee6f788bc969c3ad3137850ebfc46a73af2b0c036c3da4b4a16506f499445c604dd73eeb846a52f881515a3ad0ab448b4f9ed3e0ab1fffac60b223dde6450ba6198e90e14de107aaf2 |
附件2:Mersenne_twister.py
1 | from hashlib import * |
分析
提供了它这里使用的梅森旋转算法
加密思路是:
每次加密明文:flag 里的每个字符
密钥:由 mt73991 对象生成的伪随机数(四个拼在一起)
加密函数:对明文一次 md5 加密,之后,加密后的明文和密钥逐位异或
解析
尝试写了一个对伪随机数预测的程序:
1 | from Mersenne_twister import * |
但是发现没有 seed 还是不行。
至少加深了对算法理解(自慰安慰 😢 )
估计算法的深处藏有爆破 seed 的方法(否则 $2^{32}$ 没办法得到 seed)。
共模攻击
附件1:hint.py
后来这个文件被下架了,直接给出其实 🔥😠🔥 .(好吧,其实没差,hhh)
1 | from gmpy2 import * |
1 | from gmpy2 import * |
hint.py
共模攻击
第一部分:
1 | p, q = getPrime(256), getPrime(256) |
典型的共模攻击,破解:
1 | from gmpy2 import * |
高次剩余
第二部分:
1 | m = bytes_to_long(hint) |
$$
c = m^{256} \pmod p
$$
这里要进行高次剩余方程求解。
不过网上的板子几乎都是用 C++ 写的(ICPCer 的作品…)我也尝试过用 N次剩余(详解+例题+代码) 的 CPP 板子,然而…… c 是在太大,不得行…… 🙂
关于算法,有篇博客写得很全面:数论算法 剩余系相关 学习笔记 (基础回顾,(ex)CRT,(ex)lucas,(ex)BSGS,原根与指标入门,高次剩余,Miller_Robin+Pollard_Rho) (仰望 ICPCer)
可以调用大整数库,写下算法……(濒危动物表示写不动 😭 )
Sympy
后来发现 Sympy 里有求 高次剩余的函数……
1 | from sympy.ntheory.residue_ntheory import nthroot_mod |
输出:
1 | m.bit_length() < 400 |
mma / Sage
用 mma:
1 | Simplify[ |
或者用 SageMath:
1 | p = 107316975771284342108362954945096489708900302633734520943905283655283318535709 |
+Python
1 | mlist = [623314401187286670257694436875298172611109072944,11812523831006378212663497485315375112114782670326938195125719701658999904789, 95504451940277963895699457459781114596785519963407582748779563953624318630920, 107316975771284342108362954944473175307713015963476826507029985482672209462765] |
输出:
1 | ['m.bit_length() < 400', |
所以,我们费了半天功夫得到的提示是:
1 | m.bit_length() < 400 |
😅😅😅😅😅😅😅😅
这不是废话吗???
🤕🤕🤕 稍后打脸
task.py
$$
n = q*p
\\
c_1 = m^p \pmod n
\\
c_2 = m^q \pmod n
$$
确实是共模攻击,和后来下架 hint.py 之后的新的问题提示一样,此“共模攻击”非彼“共模攻击”。
模数确实一样,但我们不知道密钥……
看了 WP,发现 Hint 是给 Coppersmith定理攻击 的可行性的提示……
Coppersmith 定理指出在一个 e 阶的 $\pmod n$ 多项式 $f(x)$ 中,如果有一个根小于$n^{1/e}$,就可以运用一个 $O(log n)$ 的算法求出这些根。
以下是分析:
hint 给出 m 比特长度上界,联想到 coppersmith,再可行性分析如下:
$$
c_1 = m^p = m \pmod p
\\
c_2 = m^q = m \pmod q
\\
\therefore n | (c_1-m)(c_2-m)
\\
\text{上界:} , \frac{1}{2}n^{1/2} \approx 2^{511}
$$
因为 m.bit_length() < 400,所以可行性分析通过。
Sage:
1 | n = 128205304743751985889679351195836799434324346996129753896234917982647254577214018524580290192396070591032007818847697193260130051396080104704981594190602854241936777324431673564677900773992273463534717009587530152480725448774018550562603894883079711995434332008363470321069097619786793617099517770260029108149 |
1 | 'verrrrrrry_345yyyyyyy_rsaaaaaaa_righttttttt?' |
ezRSA
1 | from gmpy2 import lcm, powmod, invert, gcd, mpz |
分析
λ(n) instead of φ(n)
在 RSA Theory 的 “Using λ(n) instead of φ(n)” 部分提到过:
$d = e^{-1} \pmod {\phi(n)}$ 可以替换成 $d’ = e^{-1} \pmod {\lambda(n)}$,其中 $ \lambda(n) = lcm(p-1, q-1) = \frac{(p-1)(q-1)}{gcd(p-1, q-1)}$.
这样一来,我们就可以用 λ(n)(题目中的 gift)来代替我们原本生成密钥的时候对 φ(n) 的需求。
需要注意的这道的中:$gcd(e, lcm) = 2$,不在 gift 的剩余系内(即没有逆元),所以需要进行适当地处理:
把 $c=m^e \pmod n$ 看成是 $c=(m^2 )^{(\frac{e}{2})} \pmod n$。
$$
e’ = \frac{e}{2}
\\
d’ = (e^{ ’ })^{ -1 } \pmod { \lambda(n) }
\\
m^2 = c^{ d’ } \pmod {n}
$$
之后就是一个二次剩余的问题了。
二次剩余
Wolfram 爆破
1 | Simplify[ |
然而并没有跑出来。
其他的方法也卡住了 😭
WP 告诉了我们新的思路:“非有限域下的二次根求解难度在m足够大的时候几乎不可行,于是思路转向分解n”。
⭐️ 这道题考察的点实际上是已知(e, n, d)来分解 n
TODO: https://0xdktb.top/2020/04/19/WriteUp-NPUCTF-Crypto/#EzRSA
总结
空闲时间比较有限,所以就碰了以上几道题目。
不得不说,主办方出的题有些魔性,参赛体验也比较糟糕,比赛开始没多久就被 DDoS 了,花了两天时间才恢复……
题目也很魔性,Misc 的抽象带师,flag 里面是中文+各种 emoji;一些题目信息放了又撤回等等……