原文:CTF中常见的RSA相关问题总结
前言
理解基本概念后,代码就可以说明一切,所以本文将每种攻击方式的实现方法都提炼成了一个函数,在理解原理时会有帮助,在需要时也可以直接调用。
基础
RSA概要
在开始前可以通过 《RSA算法详解》 这篇文章了解关于RSA的基础知识,包括加解密方法,算法原理和可行性证明等。
应用流程
选取两个较大的互不相等的质数p和q,计算
n = p * q
。计算
phi = (p-1) * (q-1)
。选取任意e,使得e满足
1<e<phi
且gcd(e , phi) == 1
。计算e关于n的模逆元d, 即d满足
(e * d)% n ==1
。加解密:
c = (m ^ e) % n
,m = (c ^ d) % n
。其中m为明文,c为密文,(n,e)为公钥对,d为私钥,要求0 <= m < n
。
理解模逆运算
- 如果
(a*b)%c==1
,那么a和b互为对方模c的模逆元/数论倒数,也写作 。 - 关于最大公约数有一个基本事实:
给予两整数a、c,必存在整数x、y使得ax + cy = gcd(a,c)
,基于这个事实,当a,c互素即gcd(a,c)==1
时,有ax+cy=1
,那么就有(a*x)%c==1
,所以x就是a 对c的模逆元。因此,a对c存在模逆元b的充要条件是gcd(a,c)==1
。显然对于每一组a,c
,存在一族满足条件的x,在求模逆元时我们取得是最小正整数解x mod n
。 - 上述的基本事实很容易理解,因为a和c的最大公约数是gcd(a,b),所以a和c都可表示为gcd(a,b)的整数倍,那么a和b的任意整系数的线性组合ax+by也必定能表示成gcd(a,c)的整数倍,他们当中最小的正整数就应该是gcd(a,c)。实际上最大公约数有一个定义就是:
a和b的最大公约数g是a和b的线性和中的最小正整数
。
求模逆元主要基于扩展欧几里得算法,贴一个Python实现:
1
2
3
4
5
6
7
8
9def egcd ( a , b ):
if (b == 0):
return 1, 0, a
else:
x , y , q = egcd( b , a % b ) # q = GCD(a, b) = GCD(b, a%b)
x , y = y, ( x - (a // b) * y )
return x, y, q
def mod_inv(a,b):
return egcd(a,b)[0]%b #求a模b得逆元
1 | (a + b) % n ≡ (a % n + b % n) % n |
欧几里得算法
欧几里得算法是求最大公约数的算法, 也就是中学学的 辗转相除法 。记 gcd(a,b)
为a和b的最大公约数,欧几里得算法的基本原理是gcd(a,b)==gcd(b,a%b),(b!=0)
和 gcd(a,0)==a
。
Python实现如下:
1 | # 递归版 |
扩展欧几里得算法
扩展欧几里得算法基于欧几里得算法,能够求出使得 ax+by=gcd(a,b)
的一组x,y。
这篇文章 解释得很到位,对照下图和以下递归版实现容易理解。
Python实现如下:
1 | # 递归版 |
中国剩余定理
维基百科 给出了简洁生动的说明:
参考以上说明进行的Python实现:
1 | def CRT(mi, ai): |
以上程序将mi当作两两互质处理,实际上有时会遇到其他情况,这时就需要逐一两两合并方程组。我参照下图实现了一个互质与不互质两种情况下都能工作良好的中国剩余定理(解同余方程组)的Python程序。
1 | def GCRT(mi, ai): |
图片截自 中国剩余定理(互质与不互质的情况) 。
常见攻击方式实践
准备工具
- python
- gmpy2库
- Windows:可从https://pypi.org/project/gmpy2/#files 直接下载已编译的安装包。
- Linux:
sudo apt install python-gmpy2
- libnum库:
git clone https://github.com/hellman/libnum.git && cd libnum && python setup.py install
- gmpy2库
- yafu
- RSATool2v17.exe
RSA解密
若已知私钥d,则可以直接解密: m=pow(c,d,n)
。
若已知质数p和q,则通过依次计算欧拉函数值phi、私钥d可解密。简易实现如下:
1 | def rsa_decrypt(e, c, p, q): |
在选取加密指数e时要求phi,e互质,也就是gcd(phi,e)==1
,如果不满足是无法直接解密的。
为什么说这个呢?是因为有时会有乍一看有点奇怪的情况。比如SCTF2018的Crypto - a number problem
,题目是
1 | x**33=1926041757553905692219721422025224638913707 mod 3436415358139016629092568198745009225773259 |
其中n=3436415358139016629092568198745009225773259
可以直接分解得到p,q,出phi=(p-1)*(q-1)
,然后惊奇地发现gcd(phi,33)==3
。这时如果对加密过程比较熟悉的话,就可以想到实际上公钥e=11
,明文是m=x^3
,应该先求出m。然后再爆破x。
1 | for i in range(1000000): |
查询已知的n的可分解情况
api接口:
1 | curl http://factordb.com/api?query=12345 |
使用yafu分解N
适用情况:p,q相差较大或较小时可快速分解。
使用方法:yafu-x64.exe factor(233)
,yafu-x64.exe help
模不互素 (gcd(N1,N2)!=1
)
适用情况:存在两个或更多模数 ,且gcd(N1,N2)!=1
。
多个模数n共用质数,则可以很容易利用欧几里得算法求得他们的质因数之一gcd(N1,N2)
,然后这个最大公约数可用于分解模数分别得到对应的p和q,即可进行解密。实现参照本文欧几里得算法
部分和RSA解密
部分。
共模攻击
适用情况:明文m、模数n相同,公钥指数e、密文c不同,gcd(e1,e2)==1
对同一明文的多次加密使用相同的模数和不同的公钥指数可能导致共模攻击。简单证明见代码注释。
Python实现:
1 | def common_modulus(n, e1, e2, c1, c2): |
例子:QCTF2018-XMan选拔赛 / Xman-RSA 【共模攻击+模不互素】
这道题利用了共模攻击和模不互素。刚开始是一个字符替换,与本文无关。encryption.encrypted文件被做了字符替换,根据语法确定替换表,修复文件得到源文件如下。
题目附件见文末链接。
1 | from gmpy2 import is_prime |
n2,n3已知,利用共模攻击得到n1,由gcd(n1,n2)==p1
分解n1,n2,就可解密得到两部分msg,拼接即可。
解题脚本如下:
1 | # -*- coding: utf-8 -*- |
小明文攻击
适用情况:e较小,一般为3。
公钥e很小,明文m也不大的话,于是m^e=k*n+m
中的的k值很小甚至为0,爆破k或直接开三次方即可。
Python实现:
1 | def small_msg(e, n, c): |
例子:Extremely hard RSA
题目提供的n是4096位的,e=3。
1 | import gmpy2,binascii,libnum,time |
Rabin加密中的N可被分解
适用情况:e==2
Rabin加密是RSA的衍生算法,e==2是Rabin加密典型特征,可以百度或阅读 https://en.wikipedia.org/wiki/Rabin_cryptosystem 以了解到详细的说明,这里只关注解密方法。一般先通过其他方法分解得到p,q,然后解密。
Python实现:
1 | def rabin_decrypt(c, p, q, e=2): |
函数返回四个数,这其中只有一个是我们想要的明文,需要通过其他方式验证,当然CTF中显然就是flag字眼了。
解密方法是参照维基百科的,截图如下:
例子:Jarvis OJ hard RSA
解题脚本
1 | import gmpy2,libnum |
Wiener’s Attack
适用情况:e过大或过小。
工具:https://github.com/pablocelayes/rsa-wiener-attack
在e过大或过小的情况下,可使用算法从e中快速推断出d的值。详细的算法原理可以阅读:低解密指数攻击 。
1 | from Crypto.PublicKey import RSA |
例子:2018强网杯nextrsa-Level2
1 | n = 0x92411fa0c93c1b27f89e436d8c4698bcf554938396803a5b62bd10c9bfcbf85a483bd87bb2d6a8dc00c32d8a7caf30d8899d90cb8f5838cae95f7ff5358847db1244006c140edfcc36adbdcaa16cd27432b4d50d2348b5c15c209364d7914ef50425e4c3da07612cc34e9b93b98d394b43f3eb0a5a806c70f06697b6189606eb9707104a7b6ff059011bac957e2aae9ec406a4ff8f8062400d2312a207a9e018f4b4e961c943dfc410a26828d2e88b24e4100162228a5bbf0824cf2f1c8e7b915efa385efeb505a9746e5d19967766618007ddf0d99525e9a41997217484d64c6a879d762098b9807bee46a219be76941b9ff31465463981e230eecec69691d1L |
私钥文件修复
适用情况:提供破损的私钥文件。
例子:Jarvis OJ-God Like RSA
参考 https://www.40huo.cn/blog/rsa-private-key-recovery-and-oaep.html 修复存储私钥的文件,得到p和q。
1 | import gmpy2,binascii,libnum,time |
LSB Oracle Attack
适用情况:可以选择密文并泄露最低位。
在一次RSA加密中,明文为m,模数为n,加密指数为e,密文为c。我们可以构造出c'=((2^e)*c)%n=((2^e)*(m^e))%n=((2*m)^e)%n
, 因为m的两倍可能大于n,所以经过解密得到的明文是 m'=(2*m)%n
。我们还能够知道 m'
的最低位lsb
是1还是0。 因为n是奇数,而2*m
是偶数,所以如果lsb
是0,说明(2*m)%n
是偶数,没有超过n,即m<n/2.0
,反之则m>n/2.0
。举个例子就能明白2%3=2
是偶数,而4%3=1
是奇数。以此类推,构造密文c"=(4^e)*c)%n
使其解密后为m"=(4*m)%n
,判断m"
的奇偶性可以知道m
和 n/4
的大小关系。所以我们就有了一个二分算法,可以在对数时间内将m的范围逼近到一个足够狭窄的空间。
更多信息可参考:RSA Least-Significant-Bit Oracle Attack 和 RSA least significant bit oracle attack 。
Python实现:
1 | import decimal |
例子:QCTF2018-XMan选拔赛/Baby RSA
题目如下
1 | e = 0x10001 |
解题脚本:
1 | # -*- coding: utf-8 -*- |
选择密文攻击
适用情况:可以构造任意密文并获得对应明文。
这个好理解,在一个RSA加密过程中,明文为m,密文为c,模数为n,加密指数为e,选取x以满足gcd(x,n)==1
从而使x模n的逆存在,构造密文 c'=c*(x^e)
使解密后明文为 m'=(m*x)%n
,则m=m'*x^-1(mod n)
。可参看模意义下的运算法则部分
。
广播攻击
适用情况:模数n、密文c不同,明文m、加密指数e相同。一般会是e=k,然后给k组数据
使用不同的模数n,相同的公钥指数e加密相同的信息。就会得到多个(m^e) ==ci (mod ni),将(m^e)视为一个整体M,这就是典型的中国剩余定理适用情况。按照本文的中国剩余定理
小节容易求得m^e的值,当e较小时直接开e方即可,可使用gmpy2.iroot(M,e)
方法。
Python实现:参见本文 中国剩余定理
小节。
例子:2018强网杯nextrsa-Level9
1 | m = random.randint(0x100000000000, 0xffffffffffff) |
其他例题
【Jarvis OJ Medium RSA】解析公钥文件
使用命令从PEM文件(Privacy-Enhanced Mail 是用于存储和发送密钥、证书等数据的文件格式)中解析公钥对(n,e),n可在线查询 (http://factordb.com/) 到质因子,分解n得到p和q,便能够计算欧拉函数值及解密指数,从而解密。
1 | C:\Users\neo\Downloads\mediumRSA.rar |
carck.py
1 | import gmpy2,binascii |
后话
RSA可谓现代密码学的中流砥柱,关于它的可行攻击方法研究还有很多,诸如Timing Attack ,Padding oracle attack,Side-channel analysis attacks等类型的攻击,本文仅介绍了一些通俗易懂的方法,读者还可以阅读 CTF wiki中的非对称加密部分 ,以及以 RSA (cryptosystem) 为目录结合谷歌进行进一步学习。
本文的例题附件、代码段、工具和后续更新都会放在 RSA-ATTACK ,欢迎 star & watch 。