0x00 几句废话
可能是由于刚开学的原因,CTF线上赛不仅数量多,赛题质量也比较高.而且智能合约越来越成为比赛的宠儿,有空需要专门学习记录一下相关题目的做法及常见漏洞.
下面记录一下两道比较有意思的Crypto类RSA相关题目.
0x01 数学知识补充
Legendre symbol
勒让德符号(下文写成a|p)有如下定义:
基本性质
- 如果$ (a|p)=1 $,a便称为modp下的二次剩余;如果$ (a|p)=-1 $,则a称为mod p下的二次非剩余.通常把0视为一种特殊情况.
其他性质
如果$ a \equiv b (mod p) $,则$ (a|p) = (b|p) $
一般二次互反律
如果p和q是奇素数,则$ (q|p)=(p|q)(-1)^{(p-1)/2(q-1)/2} $
二次互反律的第一补充
二次互反律的第二补充
Jacobi symbol
雅可比符号是勒让德符号的推广,整数a对整数m的雅可比符号表示为(a|m).设m是大于1的奇数,且m的素因数分解式为$ m=p1*p2*p3….pr $(式中因数可以相同),如果(a,m)=1,则雅可比符号定义为:
(a|m) = (a|p1)(a|p2)(a|p3)...(a|pr)
其中(a|pi)是a对pi的勒让德符号符号.
相关定理
- 定理1
- 定理2
- 定理3
0x02 Application of jacobi symbol
计算雅可比符号的python函数 gmpy2.jacobi()
题目来源
2019-9-6 N1ctf babyrsa
题目类型
Crypto,jacobi symbol,operator precedence
题目描述
该题目给出了加密脚本babyrsa.py
以及加密之后的文件flag.enc
.加密脚本如下:
1 | #!/usr/bin/env python2 |
题目分析
通过脚本我们可以看出大致的加密过程:首先将flag字符串转换成长整型数字.然后随机生成一定范围内的随机数并进行平方.由于运算符优先级问题,<<
的优先级高于+
,所以将处理后的随机数左移(1+flag[i])
位.将移动后的结果进行rsa加密,结果以hex的形式存储在输出文件中.
分析算法之后,我们有两点需要注意,第一是运算符优先级问题,即左移的位数只能是1或2.第二是由于m%2
的存在,加密的顺序是从flag的低位向高位进行,所以解密结果需要注意顺序.
下面来进行数学分析.经过上面的分析我们知道plain[i] = 2^(1+flag[i])*r^2
显然当flag[i] == 1
时,plain[i] = (2*r)^2(mod n)
是一个二次剩余.如果flag[i] == 0
时,plain[i]=2*r^2
,此时2^(-1)*plain[i]=r^2 (mod n)
是一个二次剩余.此类问题本质为模数域内的平方检测问题,于是联想到使用雅可比符号.
根据题目描述,我们可以得到cipher[i]=(2^(1+flag[i])*r^2)^e (mod n)
,根据性质计算雅可比符号:
由于$ N \equiv 5(mod 8) $,根据二次互反律可得$ (2|N) = -1 $,而且$ (1|N)=1 $.所以当雅可比符号为-1时, flag[i]=1
,当雅可比符号为1时,flag[i]=0
.根据这一分析我们可以最终恢复出flag.
Tips: If x is a quadratic residue and gcd(x,N)=1,then(x|N)=1.If the Jacobi symbol is 0, then x contains a factor of N.
解题脚本
1 | #!/usr/bin/env python3 |
0x03 RSAchain
题目来源
2019-9-22 Teaser-Dragon ctf RSAchained
题目类型
Crypto,rsa,sagemath,partial private key explosure attack
题目描述
题目给出了加密脚本task.py
以及加密之后的结果output.txt
.加密脚本如下:
1 | import os |
题目分析
拿到题目之后发现是一个链式rsa加密,明文经过四次加密得到密文.四次加密均采用rsa算法,加密指数e = 1667
,并且模数n满足:
1 | n1 = p1*q1 |
除大素数r之外的p,q每轮都单独生成.将n1-n4从小到大排列之后对flag进行多次加密.观察每次加密的输出,每次输出排序后对应的n以及d % (2**1050)
.
简单分析之后,我们可以得出一些思路:首先观察四个模数,显然n2,n3具有公因数r,通过math.gcd()
可以求出大素数r,并同时确定n2和n3的位置.然后我们会考虑到加密顺序的问题,由于上一步已经明确找出了n2和n3的位置,故确定n1,n4的顺序最多需要爆破两次.最后我们观察每次输出,给出了d % (2**1050)
,属于部分密钥暴露的情况,联想到使用partial private key explosure attack
,但由于无法确定暴露的位数故存在失败的可能性.
这里我们通过分析n1-n4的构成可以从另一个角度解决问题.
Pretreatment: e*d == 1 mod phi(N)
将上式变形为e\*d-k*phi(N)=1
.由于d<phi(N),故上式成立,则k<e,从而确定出k的爆破范围.将上式与d0建立转换联系,有:
1 | e * d == 1 mod phi(N) |
Case1: N = p*q*r
首先通过GCD函数求出大素数r的值以及n2,n3的位置.计算后发现r = 32619972550448885952992763634430245734911201344234854263395196070105784406551510361671421185736962007609664176708457560859146461127127352439294740476944600948487063407599124272043110271125538616418873138407229
,n2,n3分别位于加密的第一、二位.
观察上一步处理得到的关系式e * d0 - k*phi(N) == 1 mod 2**1050
,发现e,d0,k均为常数,于是联想到处理phi(N)
,由于phi(N) = (p-1)*(q-1)*(r-1)
,且N和r均为已知常数,故在方程两侧同乘变量进行变量转换,最终变成只包含p、N、r的方程:
1 | (p-1)*(q-1)*(r-1) = |
最终解上述方程(使用sagemath)可以找出p2,p3的值,成功分解n2,n3.
Case2: N = p*q**2
此种情况下,我们有如下信息:phi(N) = (p-1)*(q-1)*q
,N = p*q^2
.我们使用和前一种情况相同的方法进行方程转化.由于只有q的最高次数为2,所以将q作为未知参数进行转换:
1 | (p-1)*(q-1)*q = |
尽管我们无法确定最后两个密钥对应的顺序,我们只需要最多进行两次尝试就可以进行正确分解,从而得到q4,成功分解n4.
Case3: N = p*q
and q.nbits() == 1400
这种情况下,分解方法同上面的一样:
1 | (p-1)*(q-1) = |
成功分解n1之后依次解密可以得到最终的flag.
解题脚本
Case1: N = p*q*r
1 | # exp.sage |
Case2: N = p*q^2
将上述函数的方程改为results = solve_mod([e*d0*X - k*(n*X-n-X*X*X + X*X) == X], 2^1050)
即可.
Case3: N = p*q
and q.nbits() == 1400
将关键函数改为results = solve_mod([e*d0*X - k*(n*X-X*X-n+X) == X], 2^1050)
即可.
得到所有参数之后直接解密:
1 | import gmpy2 |
0x04 总结
这两道题目分别运用了雅可比符号和方程化简的知识点.第二道题目使用sagemath这一工具快速解决模域方程问题,但sagemath的效率较低(可以考虑并行编程).
折腾了好久终于成功安装上sagemath这一数学工具,但爆破效率也太低了吧….需要抽个时间好好研究一下sage编程.