最近打的CTF比赛有一点多,需要好好复盘,记录一下crypto方面的题解。这篇博客简要复盘一下2021 ByteCTF初赛,2021 KQCTF,2021 L3HCTF,2021 陇原杯, 2021 西湖论剑以及2021 N1CTF密码学方向的题解。原题存档于github仓库。
2021 Byte CTF crypto题解
字节的比赛赛题质量还是很高的。官方writeup。
JustDecrypt
第一次接触到CFB模式的一个oracle,一般来说CFB和移位寄存器的长度有关。详情可以参考wiki:分组加密的模式或者这篇博客。
本质上就是反复加密前一次的密文,生成用来异或的比特流,选取s位作为密钥流加密明文。那么b和s就是CFB最重要的两个参数。
这题中使用了python的Crypto库默认的CFB模式,所以我直接去找了Crypto库的C语言源码,确定了python中的CFB模式的加密移位寄存器的参数b和s是16和1byte,相当于字节流密钥。这意味着它的加密模式是这样的:(这是我用python经过ecb和cfb模式加密简单试出来的)
1 | iv=b"0"*16 |
每进行一个byte加密之后iv都会更新。所以这个oracle每进行一次解密的返回,我们只可以设置对应位置1byte的值。
利用如下:
我们先把寄存器里面的iv设置为已知的,直接通过填充16个字节“0”即可,为了防止padding解出来出错,所以将padding设置到320个byte,这样可以正确回显。
那么padding_res[-1]^ord(‘0’)结果就是iv加密后的密钥流,再异或一个b”H”就能使得我们解密出来的该位置的byte为“H”,其实更简便的一种方法是我们将padding改为“H”*320,直接将得到明文返回,第一个字节就会变成”H“。下面是第设置第一个字节的代码:
1 | target_msg=b"Hello, I'm a Bytedancer. Please give me the flag!"+b'0'*15 |
我在本地测试CFB模式的代码如下:
1 | from Crypto.Cipher import AES |
实际交互我们还有考虑最后的unpadding的时候正确,最终的exp:
1 | from pwn import * |
easyxor
经典的移位异或加掩码,这种加密我最先接触是在MT19937伪随机数生成器里面,我们每次可以恢复出固定的比特,这里有讲具体怎么逆。
我们观察到key的空间很小,只有2^20,我们可以直接通过爆破得到key,但是我们不知道明文和IV怎么确定哪个是正确的key呢?注意到CBC模式,每次异或的密钥流就是前面一个分组的密文,而且我们已知最后一个分组的padding的模式和flag以}结尾。这样我们就可以用这个特征进行爆破了。
另外一个IV的获取方式是通过已知明文攻击,在获取key之后,再通过提示flag以ByteCTF{开头,我们就可以得到iv,之后就只剩下解密了:
脚本如下:
1 | from random import randint, getrandbits |
overhead
典型的DH密钥交换,我们可以得到Alice,Bob的公钥,并且我们有一个oracle,可以用Bob的私钥加密消息,并且泄露加密后的高p.bitlength() - 64位(MSB)。不妨记为 k = p.bitlength() - 64位 (注意不是精确的位数)
注意到下面的等式,r是我们任意生成的随机数
1 | pow((pow(g, r, p) * Alice) % p, b, p) == (pow(Bob, r, p) * secret) % p |
这样我们可以提交pow(g, r, p) * Alice) % p
,oracle会计算上面的式子,并且返回给我们右边的高p.bitlength() - 64位,只有secret是未知的,这是一个典型的Hidden Number Problem (HNP)问题。关于HNP问题,一般可以构造格,进行格基规约,再使用Babai算法求解CVP问题。
关于HNP问题的详细解法,这里给几篇不错的参考和论文:
- https://www.iacr.org/archive/crypto2009/56770333/56770333.pdf
- https://kel.bz/post/hnp/
- https://www.anquanke.com/post/id/204846
HNP的解法简单来说就是构造下面(d+1)*(d+1)的矩阵:
1 | # HNP问题 alpha* t = u mod p, 给定t,oracle输出u的MSB,求alpha |
其中t就是每次随机生成的输入,那么在上述矩阵的列向量张成的格内,求解Closest Vector Problem (CVP)问题,对于oracle返回的d组MSB的值U=(u1,u2,… ,ud),我们求解U离上面的格的最短向量,有极大概率使得这个最短向量就是:
1 | closest_vector=(aplha*t1,alpha*t2,...,alpha*td,alpha/p) |
那么closest_vector[-1]*p就是我们需要求解的alpha。
参考上面第二个链接的解法,这里给出一个本地的跑的脚本,验证解法。
1 | # sage |
最后提一嘴,这是今年USENIX上面的一篇论文提到的攻击:
https://www.usenix.org/system/files/sec21summer_merget.pdf
另外一种解法是参考Nu1l战队的writeup,居然还可以用coppersmith来解,这是原writeup:
1 | from pwn import remote |
其中coppersmith.sage可以在这个repo找到,不直接使用sage自带的small_roots方法是因为sage目前还不支持多元多项式。
上面脚本的思想就是:
1 | 发送 Alice 公钥 ---> 返回 pow(Alice,b,p)=secret 的高k位MSB s1 |
有如下等式成立,其中x和y分别是secret和secret^2的低六十四位:
(s1+x)^2 - s2 - y == 0
那么我们可以考虑coppersmith算法:
对于一个有限域n上面 e 阶的多项式 f,:
- 在模 n 意义下,快速求出 $n^{1/e}$ 以内的根
- 给定 β,快速求出模某个 b 意义下较小的根,其中 b≥nβ,是 n 的因数。
这里我们的指数e=2,p有512比特,而根的范围在2**64以内,显然这题满足coppersmith的条件。
当然这题还有第三种解法,这里就不写了,恰好byte决赛也出现了,可以看看。
abusedkey
协议分析题,我ECC基础太差了,没写出来。(主要是对sage的库不太清楚)
这里放个MoMoMo的writeup:
- 首先按照协议2流程执行协议2,拿到hint。
- 接下来在协议2下爆破ds,爆破方式为先劫持msg23传输过程,把Qc | |Qs替换为Qc | | Rc,此时服务器端计算Yc= rt hcN(-1)^ hc Rc = rt Rc, Ys = rt hs(-1) Rc = hs(-1)Yc,在msg24中我们收到了Yc和Ys,因此可以穷举r(s)来计算ds,然后根据Ys和hsN(-1)*Yc是否相同来判断爆破出的ds是否正确。
- 最后回到协议1,可以直接进行流氓密钥攻击,我们劫持msg13传输过程,将sid1 || Tc替换为sid1 || Pc,此时服务器端计算Kcs =(-ds - ts) Pc + ts Pc =-ds * Pc,其中ds和Pc此时均已知,我们可以直接在本地计算出Kcs,按照协议1流程生成共享密钥,解密msg14过程中收到的FLAG的密文即可。
1 | #!/usr/bin/env sage |
2021 L3HCTF Crypto 题解
官方题解可以在这里找到:L3HCTF2021官方题解。题目存库于repo。
EzECDSA
这是一个HNP问题,提供了oracle,在github上面能直接找到对应的仓库:lattice-attack
可以直接利用这个代码解,只需要写交互的脚本就行。wsl2本地跑的时候发现ecdsa_lib.py报错,在118行注释即可:
1 | # ret = False |
1 | from hashlib import * |
p0o0w
难的在于逆向,rust逆向出来之后的符号实在太多了。
当时逆向的时候主要看了这个函数,应该是生成伪随机数的种子
真正关于prng的函数在Magic函数中
进入magic函数中:
我们发现只有异或和移位运算,从这里就可以看出大概的PRNG算法了,是Xorshift类的算法,因为产生的是16字节的随机数,即Xorshift128,并且移位的参数是23,17,26。官方wp说可以分析算法,也可以直接用z3解,因为涉及的运算只有移位异或。具体来说,先用爆破的方式得到前面6个随机数,得到初始的state,之后就可以预测随机数了,一个可行的z3解法如下:
1 | from pwn import * |
2021 西湖论剑 Crypto题解
题目存库于repo:西湖论剑Crypto
unknownDSA
一开始看到有很多的未知变量,其中ul和vl给了下面的关系
1 | ul[i]**2 - wl[i]* vl[i]**2==1 |
这个方程仔细研究发现是有名的佩尔方程 $x^2-n*y^2=1$
pell方程的sagemath解法
1 | def check_sat_pell(x, y, n): |
以及一个online的最小解网站,发现ul和vl就是给定的wl时的最小解。
解出来之后就是简单的DSA复用k的漏洞,可以直接通过同余方程解出。
如果在两次签名的过程中共享了 k,就可以进行攻击。假设签名的消息为 m1,m2,k相同,则两者的r值相同,此外
$s_1≡(H(m_1)+x_r)k^{−1}\ (mod\ q)$
$s_2≡(H(m_2)+x_r)k^{−1}\ (mod\ q)$
联立得到
$k(s_1−s_2)≡(H(m_1)−H(m_2))(modq)$
此时可解出 k,从而代入任意一个方程得到私钥x。
脚本如下
1 | from Crypto.Util.number import * |
FilterRandom
这题的writeup参考:SU战队
当时做这个题的时候概率算错了,现在重新估算一遍:
线性移位寄存器输出的比特流,有90%的概率来自lfsr1,10%的概率来自lfsr2,任意给定64比特,全部来自lfsr1的概率是:$0.9^{64}=0.00118$,但是我们有2048比特,有2048-64=1984个连续64比特串,加入它们都不是全部来自lfsr1,概率是:$(1-0.00118)^{1984}=0.096$,当然,这个算的也有问题,因为每个概率不是独立的,但是我们同时也注意到即使选中lfsr2,输出也很有可能与lfsr1相同因,此有我们姑且认为有很大概率可以找到连续的64比特串使它们全部来自lfsr1, 也有可能是出题人选择了一次符合条件的输出,降低了难度,通过爆破找到连续64个比特之后,只需要一直向前回溯,就能得到初始状态init1,这样我们就得到lfsr1的初始状态。
之后我们就考虑求解lfsr2的初始状态。可以用矩阵方程:$initA^{k-1}mask$ 得到第k个输出。已知100多比特的输出,完全可以求逆得到初始状态。那么就可列$xA=y$形式矩阵方程计算。
1 | N = 64 |
HardRSA
整体是一个离散对数求解以及RSA变种,叫做 Hensel lifting for Takagi’s scheme。具体可以参考Lazzzaro博客。这题RSA部分脚本也是取自上面的博客。先用sage自带的discrete_log求出p,之后用上面的方法解密即可。
1 | from Crypto.Util.number import * |
SpecialCurve2
这道题比赛的时候没有解出来,题目自己设计了一个乘法群,其实就是复数域上面的乘法,对于复数域上面的乘法,模p意义上的阶是。对于题目中的,它的阶就是,那么只要求出e,类似RSA加密,求解出e关于这个乘法群的逆d,就可以任意解密了。关键在于这个e怎么求。理论上面详细的证明可以参考这篇知乎(另外一种基于CRT来求e的解法也可以参考这篇)。
我们取模发现它的乘方保持模长上同态的关系,即:$|k*G|=|G|^k$,而给了我们一组已知明文,求e就可以直接转换为求离散对数。但是这个离散对数怎么求很头疼,sage上面的许多dlp实现都不太行,直到看到Nu1l战队的writeup,发现了一个高效求离散对数的实现,调用sage上面gp库的离散对数,这是一个专门的数论库,sagemath集成了gp/PARI的接口。GP接口参考。
1 | n = 92916331959725072239888159454032910975918656644816711315436128106147081837990823 |
求解脚本如下:
1 | from Crypto.Util.number import * |
2021 N1CTF Crypto题解
N1CTF的赛题质量还是很高,和西湖同时间打的比赛,我看自闭了,checkin都没写出来,想到了copperSmith算p,方程也列出来了,但是我没去跑,还以为差些条件。官方题解和题目存储都在Nu1l的Github上面有存档。以下writeup会参考韩国战队SupperGuesser的题解,也是本次比赛的第一名。
checkin
本题参考了这篇writeup:https://project-euphoria.dev/blog/26-n1ctf-2021/。
设。
这里我们为了减少运行时间,需要把q的leading bits也算出一部分,已知p的高22位和n,我们可以算出q的高21位,第22位不确定是因为可能有进位。简单的计算方法就是:$q1=n//(p0<<490),q0=q1>>491$ 。这样我们已知p的高22位和q的高21位。从而我们将x做如下分解:
前半部分记为x0,后半部分记为x1,其中x0已知,x1未知,x1的数量级大概在2**500左右,令$f(x)=x^2-hx+1$,最高次为2,根$x1<N^{1/2}$,那么我们可以用Coppersmith定理进行求解,但是需要注意epsilon值的设置,可以参考sage的文档。具体的联系我没有看论文,但是epsilon越小,解LLL的矩阵越大,耗时越长,得到解的概率越大。由X的bound的公式:
$X=ceil(\frac{1}{2}N^{β^2/δ−ϵ})$,由于N是1024比特,X的严格上界是2^505 (计算得的的解为2^502),得到的epsilon应该小于0,009。由于是估算,可以稍微设置大一些,也能在多项式时间内得到解。这里设置epsilon为1/80可以得到解,只需要10min。
求解x1的代码如下:
1 | def CopperSmith(): |
得到x1之后,我们就得到了的值,并且已知,直接联立解方程即可分解n,这里用z3解,脚本如下:
1 | from Crypto.Util.number import * |
n1token1
题目源码:
1 | from Crypto.Util.number import * |
首先注意到:p,q都是模4余3的,$p=4k_1+3,q=4k_2+3$,这是后面取sqrt的条件。token产生的过程是关键,弄清楚结果x的表达式即可。
token产生:选择920个素数,从这920个素数产生刚好大于920比特的模p和模q的二次剩余X,即也是模n的二次剩余。那么显然有
即$x_p和x_q是X模p的1/2次和X模q的1/2次$,再者,由:
这就是最后的token在数学上的表示形式。更严格地说上式是:$+\sqrt{X}或者-\sqrt{X}$,那么:,SupperGuesser战队的writeup提出一个巧妙解法:HNP。为什么可以当成HNP问题来解呢?注意到X的比特数是稍微比920大一点,而n是1024比特,这意味着我们知道在模n意义下X的前100比特都是0。把上式换写一下:。
其中$c^{-2}$就是HNP中需要求的秘密参数,而X我们已知其大概100位的MSB。这样就可以解HNP问题了,并且我们已知的MSB都是0,因此不再需要Babai算法求CVP了。直接LLL算法得到最短向量即可求解,当然需要除去一个特殊的最短格点(0,0…,1)。
1 | from Crypto.Util.number import * |
现在的问题是如何求解c的RSA加密,貌似没有显示地给出其他信息,所以我们再研究一下x的产生过程,它用到了额外的p,q信息,因为任意X的二次剩余有四个(在模n=p*q意义下),不妨记为:x,y,n-x,n-y。如果我们恰好已知的是两个非加法逆的x,y,那么:$x^2-y^2=0 \ mod \ n$,即(x+y)(x-y)|n,此时 x+y 和 x-y 都不是 n 的倍数,而 (x+y)(x-y) 却是 n 的倍数,说明 x+y 和 x-y 中分别包含一个 n 的真因数。此时计算 gcd(n, x+y) 即可得到 n 的一个真因数。这样就分解了n。
但是我们不能直接求出c或者,已知条件是、x、X和$c^2$。注意到X的产生,他们都是由sievebase中的920个素数产生的。
sievebase的长度是10000,所以我们可以直接遍历把这920个素因子求出来,同时我们知道每个X,可以求出对应素因子的次数。我们就可以考虑用这920个token组合使得L个X相乘恰好是一个完全平方数,即。如此:
,其中可以直接由完全平方数开方得到,我们可以直接求nthroot,由于在生成的过程中还乘了模n意义下1的二次剩余$\sqrt1$,因此我们还有可能得到另外一个$x^2=1 \ mod \ n $的解,如果我们得到该解,就可以进行n的分解。
再考虑L的奇偶性:
- L是偶数:那么我们可以由$C^2$计算出$C^L$,这样就可以得到$x^2=1 \ mod \ n $的一个解sqrt1,如果恰好这个解不等于1或则-1,我们就可以分解N。分解N之后其实就可以全部解c,解一次Rabin,再解一次RSA,找出有意义的解即可。
- L是奇数:我们可以得到:$C*\sqrt{1}$。如果恰好是1或者-1,我们就能得到C。
那么如何选出合适的token使得是一个完全平方数?构造矩阵M,920*920。其中代表第i个token的primes[j]的次数。这样我们发现矩阵M的右核空间(right_kernel),即满足。
取右核空间内的向量,取对应的token,就满足是完全平方数了,经过计算得到M的右核空间维数恰好是2,并且其中1的个数恰好一奇一偶,即L一奇一偶(实际经过上面的讨论只需要有偶数L就可以解)。
1 | from Crypto.Util.number import * |
n1token2
参考官方的writeup。
源码:
1 | import random |
已知的token值是,其中x取1到p-1的值。很自然地我们想到解线性方程组。但是问题在于e是随机的,有五个值。令 上面的方程可以改写为 。令$k[i] = (y(x)-e[i])/x$, 因此必定有一个i使得, $f(x)-k[i] \equiv 0 \pmod{p}$ 成立。
那么多项式恒成立。展开:
。 其中$(f(x))^i$, 是15次多项式, 这5个多项式的总未知系数是 76+61+46+31+16=230 , 我们有 250 个 tokens, 解线性方程即可。
1 | p = 251 |
n1login
一个侧信道的攻击,参考官方wp:n1login
2021 KQCTF crypto题解
赛题不是特别难,但是有两道比较有意思的题。一个是NTRU格密码,一个是多项式多点快速求值。题目存库于repo。
Polynomialtime
1 | import time |
解题流程很容易理清,先需要恢复一个MT19937的PRNG,这个可以直接用mt19937predictor
库进行。恢复后我们需要计算次数高达1048576次的多项式的100多万个点,这个计算量相当大。因此我们需要做优化。考虑用递归分治的算法进行求解,感觉这像是一个OI题,可以参考OI的wiki:多项式多点快速求值。sagemath脚本如下:
1 | # sage |
KQRUEncrypt
一个NTRU格密码,可以直接参考:https://latticehacks.cr.yp.to/ntru.html
主要记录一下源码和脚本:
1 | import random |
1 | #Sage |
2021 陇原杯 crypto题解
比较简单,也是记录一下脚本。
easytask
原来以为是LWE,其实是GGH,之后在详细了解一下。不过按照LWE的格构造来解其实也是差不多的。GGH加密可以参考这个。或者我们认为它是没有取模的 LWE 问题,格直接取输出的M矩阵即可,不需要再构造额外的对角q矩阵,LLL格基规约后用 Babai算法解CVP问题,可以得到 r-e ,我们再确认 error 的范围和源代码一致即可: [-3,3] ,之后解线 性方程组即可得到m。
1 | # Sage |