RSA总结
RSA总结
基本工具
-
大整数分解
sage (
divisors(n)
)(小素数)Pollard’s p−1 (
python -m primefac -vs -m=p-1 xxxxxxx
)(光滑数)Williams’s p+1(
python -m primefac -vs -m=p+1 xxxxxxx
)(光滑数)yafu
可用命令行也可从文件导入
bash>>>./yafu-x64.exe "factor(77)"
bash>>>./yafu-x64.exe "factor(@)" -batchfile n.txt
查看文件与解密
具体参考:https://jwt1399.top/posts/49954.html
openssl rsa -pubin -in pubkey.pem -text -modulus
openssl rsautl -decrypt -inkey private.pem -in flag.enc -out flag
-
生成解密密钥:
python rsatool.py -f PEM -o key.key -p 1 -q 1 -e 1 openssl rsautl -decrypt -inkey key.pem -in flag.enc -out flag openssl rsautl -decrypt -oaep -inkey key.pem -in flag.enc -out flag (OAEP方式)
脚本生成解密密钥:
# coding=utf-8 import math import sys from Crypto.PublicKey import RSA from Crypto.Cipher import PKCS1_OAEP rsa_components = (n1, e, int(d1), p, q1) myrsa = RSA.construct(rsa_components) private = open('private.pem', 'w') private.write(myrsa.exportKey()) private.close() rsakey = RSA.importKey(myrsa.exportKey()) rsakey = PKCS1_OAEP.new(rsakey) decrypted = rsakey.decrypt(c_bytes)
-
脚本集
-
https://github.com/Ganapati/RsaCtfTool
#用法一:已知公钥(自动求私钥) $ python3 RsaCtfTool.py --publickey 公钥文件 --uncipherfile 加密文件 #用法二:已知公钥求私钥 $ python3 RsaCtfTool.py --publickey 公钥文件 --private #用法三:密钥格式转换 #把PEM格式的公钥转换为n,e $ python3 RsaCtfTool.py --dumpkey --key 公钥文件 #把n,e转换为PEM格式 $ python3 RsaCtfTool.py --createpub -n 782837482376192871287312987398172312837182 -e 65537
-
-
python库
gmpy2
pycryptodome
pip install pycryptodome
factordb
pip install factordb-pycli
$ factordb 16 \\可以命令行
In [1]: from factordb.factordb import FactorDB \\ 也可以导入
In [2]: f = FactorDB(16)
In [3]: f.get_factor_list()
Out[3]: []
In [4]: f.connect()
Out[4]: <Response [200]>
In [5]: f.get_factor_list()
Out[5]: [2, 2, 2, 2]
In [6]: f.get_factor_from_api()
Out[6]: [['2', 4]]
primefac
整数分解库,包含了很多整数分解的算法。
Some conclusions
- 若p,q为质数,有
- 若a,b互素,有
- 若p为质数,有
- 若p为质数,且 \(p\equiv 3\ (mod\ 4)\),有
- 二次剩余
def powp(a, b, e, mod, base):
ansa = 1
ansb = 0
while e:
if e & 1:
ansa, ansb = (ansa * a + ansb * b * base) % mod, (ansa * b + ansb * a) % mod
e >>= 1
a, b = (a * a + b * b * base) % mod, (a * b + b * a) % mod
return ansa
def Cipolla(a, q):
# assert isPrime(q) and q&1
# get m s.t. m**2 % q == a
t = random.randint(2, q - 1)
while pow(t**2 - a, q - 1 >> 1, q) == 1:
t = random.randint(2, q - 1)
base = t**2 - a
m = powp(t, 1, q + 1 >> 1, q, base)
return m
证书格式
“ PKCS1_OAEP”是一种基于RSA的密码,使用OAEP(最佳非对称加密填充)填充来为加密带来不确定性和更高的安全性。
PEM
PEM 以 -----BEGIN
开头,以 -----END
结尾,中间包含 ASN.1 格式的数据。ASN.1 是经过 base64 转码的二进制数据。Wikipedia 上有完整 PEM 文件的例子。
用 Python 3 和 PyCryptodome 库可以与 PEM 文件交互并提取相关数据。例如我们想提取出模数 n
:
#!/usr/bin/env python3
from Crypto.PublicKey import RSA
with open("certificate.pem","r") as f:
key = RSA.import_key(f.read())
print(key.n)
DER
DER 是 ASN.1 类型的二进制编码。后缀 .cer
或 .crt
的证书通常包含 DER 格式的数据,但 Windows 也可能会接受 PEM 格式的数据。
我们可以用 openssl
将 PEM 文件转化为 DER 文件:
openssl x509 -inform DER -in certificate.der > certificate.pem
现在问题被简化成了如何读取 PEM 文件,所以我们可以重复使用上一小节中的 Python 代码。
其他格式转换
openssl x509 -outform der -in certificate.pem -out certificate.der
openssl x509 -inform der -in certificate.cer -out certificate.pem
模数Attacks
模数攻击也是最直接的攻击方式,如果能够分解rsa中的n那么可以直接获得私钥从而破解。
直接分解模数
模数比较小,那么我们可以直接分解
通过yafu(直接计算),或者factordb(并不是直接计算,而是保存了很多分解的结果)
http://www.factordb.com/index.php
from Cryoto.Util.number import *
import gmpy2 as gp
p =
q =
e =
c =
n = p*q
phi = (p-1)*(q-1)
d = gp.invert(e,phi)
m = pow(c,d,n)
print(long_to_bytes(m))
#print(bytes.fromhex(hex(m)[2:]))
关于更多的一些分解模数 N 的方法可以参考 https://en.wikipedia.org/wiki/Integer_factorization。
p & q 不当分解 N
|p-q| 很大
当 p-q 很大时,一定存在某一个参数较小,这里我们假设为 p,那么我们可以通过穷举的方法对模数进行试除,从而分解模数。
|p-q| 较小
首先
既然 |p-q| 较小,那么 \(\frac{(p-q)^2}{4}\) 自然也比较小,进而 \(\frac{(p+q)^2}{4}\) 只是比 N 稍微大一点,所以 \(\frac{p+q}{2}\) 与 \(\sqrt{n}\) 相近。那么我们可以按照如下方法来分解
- 顺序检查 \(\sqrt{n}\) 的每一个整数 x,在根号n的附近直到找到一个 x 使得 \(x^2-n\) 是平方数,记为 \(y^2\)
- 那么 \(x^2-n=y^2\), $x2-y2=n=(x+y)\times(x-y) $进而根据平方差公式即可分解 N
此方法为费马分解法
复杂度$ O({\Delta^2\over 4n^{1\over 2} }),\Delta=\arrowvert p- q \arrowvert,\(也就是说对于\) \Delta < n^{1\over 4} $都能很快分解 n
可能可以使用 Pollard's p − 1,Williams's p + 1 算法来分解 N,但是也不是完全可以成功的。
pq的值相近
#题目
from Crypto.Util.number import getPrime
import gmpy2
p = getPrime(512)
q = gmpy2.next_prime(p)
n=p*q
print('n =',n)
#exp
import gmpy2
n =
tmp=gmpy2.iroot(n,2)[0]
p=gmpy2.next_prime(tmp)
q=n//p
print("p=",p)
print("q=",q)
平方差遍历法
核心总结就是:令a是n的"中间值"(\(\sqrt{n}\)),然后让a以步长为1自增遍历,直到pow(a,2)-n的结果可以正好开方为止。那个结果开方就是b。
'''
p = getPrime(512)
q = gmpy2.next_prime(p)
n=p*q
'''
def factor(n):
a = gmpy2.iroot(n, 2)[0]
while 1:
B2 = pow(a, 2) - n
if gmpy2.is_square(B2):
b = gmpy2.iroot(B2, 2)[0]
p = a + b
q = a - b
return p, q
a += 1 # 千万别忘了a的自增步长为1
p,q=factor(n)
fermat
from Crypto.Util.number import *
from gmpy2 import next_prime
import random
# p = getPrime(512)
# q = next_prime(next_prime(p) + random.randint(2 ** 10, 2 ** 15))
def fermat_factors(n):
"""
Factor given number using Fermat approach, starting from sqrt(n)
:param n: modulus to factor
:return: p, q
"""
assert n % 2 != 0
import gmpy2
a = gmpy2.isqrt(n)
b2 = gmpy2.square(a) - n
while not gmpy2.is_square(b2):
a += 1
b2 = gmpy2.square(a) - n
factor1 = a + gmpy2.isqrt(b2)
factor2 = a - gmpy2.isqrt(b2)
return int(factor1), int(factor2)
n =
e =
c =
q, p = fermat_factors(n)
d = inverse(e, (p - 1) * (q - 1))
print(long_to_bytes(pow(c, d, n)))
2021年“绿城杯”网络安全大赛-Crypto-RSA2-PLUS
题目:
from Crypto.Util.number import *
import gmpy2
from flag import flag
assert flag[:5]==b'flag{'
m1 = bytes_to_long(flag[:20])
p = getPrime(512)
p1 = gmpy2.next_prime(p)
q = getPrime(512)
q1 = gmpy2.next_prime(q)
n1 = p*q*p1*q1
print('n1 =',n1)
e = 0x10001
c1 = pow(m1,e,n1)
print('c1 =',c1)
#n1 =
#c1 =
由题可知,这几个素数不会相差很大,因此\(p1q1,p1q2,p2q1,p2q2\)都很接近,根据费马因子分解,可求得因子\(p1q1,p1q2,p2q1,p2q2,gcd(p1q1,p1q2) = p1\)
from Crypto.Util.number import *
from gmpy2 import *
n =
c =
e = 0x10001
def fermat_factorization(n):
factor_list = []
get_context().precision = 2048
sqrt_n = int(sqrt(n))
c = sqrt_n
while True:
c += 1
d_square = c**2 - n
if is_square(d_square):
d_square = mpz(d_square)
get_context().precision = 2048
d = int(sqrt(d_square))
factor_list.append([c+d,c-d])
if len(factor_list)==2:
break
return factor_list
factor_list = fermat_factorization(n)
[X1,Y1] = factor_list[0]
[X2,Y2] = factor_list[1]
assert X1*Y1 == n
assert X2*Y2 == n
p1 = gcd(X1,X2)
q1 = X1 // p1
p2 = gcd(Y1,Y2)
q2 = Y1 // p2
Fai = (p1-1)*(q1-1)*(p2-1)*(q2-1)
d = invert(e,Fai)
print long_to_bytes(pow(c,d,n))
p - 1 光滑
-
光滑数 (Smooth number):指可以分解为小素数乘积的正整数
-
当\(p\)是\(N\)的因数,并且\(p−1\)是光滑数,可以考虑使用
Pollard's p-1
算法来分解NN -
根据费马小定理有
则有
即
- 根据
Pollard's p-1
算法如果
如果\(p\)是一个\(B-smooth\ number\),那么则存在
使得
成立,则有
如果结果不为\(1\)或\(N\),那么就已成功分解\(N\)。
因为我们只关心最后的$ gcd$ 结果,同时N
只包含两个素因子,则我们不需要计算\(M\),考虑\(n=2,3,...,\)令\(M=n!\) 即可覆盖正确的\(M\) 同时方便计算。
- 在具体计算中,可以代入降幂进行计算
- Python 代码实现
from gmpy2 import *
a = 2
n = 2
while True:
a = powmod(a, n, N)
res = gcd(a-1, N)
if res != 1 and res != N:
q = N // res
d = invert(e, (res-1)*(q-1))
m = powmod(c, d, N)
print(m)
break
n += 1
参考链接:
p + 1 光滑
-
当\(p\) 是\(N\) 的因数,并且\(p+1\) 是光滑数,可以考虑使用
Williams's p+1
算法来分解N -
已知\(N\) 的因数\(p\),且\(p+1\) 是一个光滑数
\[p = \left(\prod_{i=1}^k{q_i^{\alpha_i}}\right)+1 \]\(q_i\) 即第$i \(个素因数且有\)q_i^{\alpha_i}\le B_1, \(找到\)\beta_i$ 使得让\(q_i^{\beta_i}\le B_1\) 且\(q_i^{\beta_i+1}> B_1,\)然后令
\[R = \prod_{i=1}^k{q_i^{\beta_i}} \]显然有$p-1\mid R \(且当\)(N, a) = 1 \(时有\)a^{p-1}\equiv 1 \pmod{p}\(,所以有\)a^R\equiv 1\pmod{p}$,即
\[p\mid(N, a^R-1) \] -
\(令P,Q 为整数,\alpha,\beta 为方程x^2-Px+Q=0 的根,定义如下类卢卡斯序列\)
\[\begin{aligned} U_n(P, Q) &= (\alpha^n -\beta^n)/(\alpha - \beta)\\ V_n(P, Q) &= \alpha^n + \beta^n \end{aligned} \]\(同样有\Delta = (\alpha - \beta)^2 = P^2-4Q,则有\)
\[\begin{cases} U_{n+1} &= PU_n - QU_{n-1}\\ V_{n+1} &= PV_n - QV_{n-1} \end{cases}\tag{2.2} \]\[\begin{cases} U_{2n} &= V_nU_n\\ V_{2n} &= V_n^2 - 2Q^n \end{cases}\tag{2.3} \]\[\begin{cases} U_{2n-1} &= U_n^2 - QU_{n-1}^2\\ V_{2n-1} &= V_nV_{n-1} - PQ^{n-1} \end{cases}\tag{2.4} \]\[\begin{cases} \Delta U_{n} &= PV_n - 2QV_{n-1}\\ V_{n} &= PU_n - 2QU_{n-1} \end{cases}\tag{2.5} \]\[\begin{cases} U_{m+n} &= U_mU_{n+1} - QU_{m-1}U_n\\ \Delta U_{m+n} &= V_mV_{n+1} - QV_{m-1}V_n \end{cases}\tag{2.6} \]\[\begin{cases} U_{n}(V_k(P, Q), Q^k) &= U_{nk}(P, Q)/U_k(P, Q)\\ V_{n}(V_k(P, Q), Q^k) &= V_n(P, Q) \end{cases}\tag{2.7} \]同时我们有如果\((N, Q) = 1 且P^{'}Q\equiv P^2-2Q\pmod{N},\)则有\(P^{'}\equiv \alpha/\beta + \beta/\alpha\) 以及\(Q^{'}\equiv \alpha/\beta + \beta/\alpha = 1\),即
\[U_{2m}(P, Q)\equiv PQ^{m-1}U_m(P^{'}, 1)\pmod{N}\tag{2.8} \]根据扩展卢卡斯定理
\(如果 p 是奇素数,p\nmid Q 且勒让德符号(\Delta/p) = \epsilon,则\)
\[\begin{aligned} U_{(p-\epsilon)m}(P, Q) &\equiv 0\pmod{p}\\ V_{(p-\epsilon)m}(P, Q) &\equiv 2Q^{m(1-\epsilon)/2}\pmod{p} \end{aligned} \] -
第一种情况
:已知 N 的因数 p,且 p+1 是一个光滑数\[p = \left(\prod_{i=1}^k{q_i^{\alpha_i}}\right)-1 \]\(有p+1\mid R,当(Q, N)=1 且(\Delta/p) = -1 时有p\mid U_R(P, Q),即p\mid (U_R(P, Q), N)\)
为了找到\(U_R(P, Q)\),
Guy
和Conway
提出可以使用如下公式\[\begin{aligned} U_{2n-1} &= U_n^2 - QU_n^2 - 1\\ U_{2n} &= U_n(PU_n - 2QU_{n-1})\\ U_{2n+1} &= PU_{2n} - QU_{2n-1} \end{aligned} \]但是上述公式值太大了,不便运算,我们可以考虑如下方法
如果\(p \mid U_R(P, 1)\),根据
公式2.3
有\(p\mid U_{2R}(P, Q)\),所以根据公式2.8
有\(p \mid U_R(P^{'}, 1)\),设\(Q=1,\)则有\[V_{(p-\epsilon)m}(P, 1) \equiv 2\pmod{p} \]即,如果\(p\mid U_R(P, 1),则p\mid(V_R(P, 1) -2).\)
第一种情况可以归纳为:
让\(R = r_1r_2r_3\cdots r_m\),同时找到\(P_0\) 使得\((P_0^2-4, N) = 1,\)定义\(V_n(P) = V_n(P, 1), U_n(P) = U_n(P, 1)\) 且
\[P_j \equiv V_{r_j}(P_{j-1})\pmod{N}(j = 1,2,3,\dots,m) \]根据
公式2.7
,有\[P_m \equiv V_R(P_0)\pmod{N}\tag{3.1} \]要计算\(V_r = V_r(P)\) 可以用如下公式
根据
公式2.2
,公式2.3
,公式2.4
有\[\begin{cases} V_{2f-1}&\equiv V_fV_{f-1}-P\\ V_{2f}&\equiv V_f^2 - 2\\ V_{2f+1}&\equiv PV_f^2-V_fV_{f-1}-P\pmod(N) \end{cases} \]令
\[r = \sum_{i=0}^t{b_t2^{t-i}}\ \ \ \ (b_i=0,1) \]\(f_0=1, f_{k+1}=2f_k+b_{k+1},\)则\(f_t=r,同样V_0(P) = 2, V_1(P) = P,\)则最终公式为
\[(V_{f_{k+1}}, V_{f_{k+1}-1}) = \begin{cases} (V_{2f_k}, V_{2f_k-1})\ \ \ \ if\ b_{k+1}=0\\ (V_{2f_k+1}, V_{2f_k})\ \ \ \ if\ b_{k+1}=1 \end{cases} - \]第二种情况
:已知$ p+1$ 是一个光滑数\[p = s\left(\prod_{i=1}^k{q_i^{\alpha_i}}\right)-1 \]\(当s 是素数,且B_1<s\le B_2,有p\mid(a_m^s-1, N),定义s_j 和2d_j\)
\[2d_j = s_j+1-s_j \]如果\((\Delta/p) = -1 且p\nmid P_m-2,\)则根据
公式2.7
和公式3.1
有\(p\mid(U_s(P_m), N)。\)令\(U[n] \equiv U_n(P_m), V[n]\equiv V_n(P_m)\pmod{N},\)计算$U[2d_j-1], U[2d_j], U[2d_j+1] $通过
\(U[0] = 0, U[1] = 1, U[n+1] = P_mU[n] - U[n-1]\)
计算
\[T[s_i] \equiv \Delta U_{s_i}(P_m) = \Delta U_{s_iR}(P_0)/U_R(P_0)\pmod{N} \]通过
公式2.6
,公式2.7
和公式3.1
有\[\begin{cases} T[s_1]&\equiv P_mV[s_1]-2V[s_1-1]\\ T[s_1-1]&\equiv 2V[s_1]-P_mV[s_1-1]\pmod{N} \end{cases} \]即
\[\begin{cases} T[s_{i+1}]&\equiv T[s_i]U[2d_i+1]-T[s_i-1]U[2d_i]\\ T[s_{i+1}-1]&\equiv T[s_i]U[2d_i]-T[s_i-1]U[2d_i-1]\pmod{N} \end{cases} \]计算\(T[s_i], i=1,2,3\dots,\)然后计算
\[H_t = (\prod_{i=0}^c{T[s_{i+t}], N}) \]\(其中t = 1, c+1, 2c+1, \dots, c[B_2/c]+1,我们有p\mid H_i 当(\Delta/p)=-1\)
-
python 代码实现
def mlucas(v, a, n):
""" Helper function for williams_pp1(). Multiplies along a Lucas sequence modulo n. """
v1, v2 = v, (v**2 - 2) % n
for bit in bin(a)[3:]: v1, v2 = ((v1**2 - 2) % n, (v1*v2 - v) % n) if bit == "0" else ((v1*v2 - v) % n, (v2**2 - 2) % n)
return v1
for v in count(1):
for p in primegen():
e = ilog(isqrt(n), p)
if e == 0: break
for _ in xrange(e): v = mlucas(v, p, n)
g = gcd(v-2, n)
if 1 < g < n: return g # g|n
if g == n: break
参考链接
模不互素
适用情况:存在两个或更多模数 ,且gcd(N1,N2)!=1 。
当存在两个公钥的 N 不互素时,我们显然可以直接对这两个数求最大公因数,然后直接获得 p,q,进而获得相应的私钥。
多个模数n共用质数,则可以很容易利用欧几里得算法求得他们的质因数之一gcd(N1,N2) ,然后这个最大公约数可用于分解模数分别得到对应的p和q,即可进行解密。
SCTF RSA2
#脚本1
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5, PKCS1_OAEP
import gmpy2
from base64 import b64decode
n1 =
n2 =
p1 = gmpy2.gcd(n1, n2)
q1 = n1 / p1
e = 65537
phin = (p1 - 1) * (q1 - 1)
d = gmpy2.invert(e, phin)
cipher =
plain = gmpy2.powmod(cipher, d, n1)
plain = hex(plain)[2:]
if len(plain) % 2 != 0:
plain = '0' + plain
print plain.decode('hex')
#sH1R3_PRlME_1N_rsA_iS_4ulnEra5le
#脚本2
import gmpy2 as gp
n=[]
for i in n:
for j in n:
if (i<>j):
pub_p=gp.gcdext(i,j)
if (pub_p[0]<>1)&(i>j):
print(i)
print(j)
print(pub_p[0])
a=i,p=pub_p[0]
q=a//p
p =
q =
e =
c =
n = p*q
phi = (p-1) * (q-1)
d = gp.invert(e, phi)
m = pow(c, d, n)
print(hex(m)[2:])
print(bytes.fromhex(hex(m)[2:]))
共模攻击
当两个用户使用相同的模数 N、不同的私钥时,加密同一明文消息时即存在共模攻击。
设两个用户的公钥分别为 \(e_1\) 和 \(e_2\),且两者互质。明文消息为 \(m\),密文分别为:
当攻击者截获 \(c_1\) 和 \(c_2\) 后,就可以恢复出明文。用扩展欧几里得算法求出 \(re_1+se_2=1\bmod n\) 的两个整数 \(r\) 和 \(s\),由此可得:
#脚本1
import gmpy2
from Crypto.Util.number import *
def Commodulus(e1, e2, n, c1, c2):
g, s, t = gmpy2.gcdext(e1, e2)
m = gmpy2.powmod(c1, s, n) * gmpy2.powmod(c2, t, n) % n
print(long_to_bytes(m))
n =
e1 =
e2 =
c1 =
c2 =
Commodulus(e1, e2, n, c1, c2)
#脚本2
import gmpy2 as gp
def egcd(a, b):
if a == 0:
return (b, 0, 1)
else:
g, y, x = egcd(b % a, a)
return (g, x - (b // a) * y, y)
n =
c1 =
c2 =
e1 =
e2 =
s = egcd(e1, e2)
s1 = s[1]
s2 = s[2]
if s1<0:
s1 = - s1
c1 = gp.invert(c1, n)
elif s2<0:
s2 = - s2
c2 = gp.invert(c2, n)
m = pow(c1,s1,n)*pow(c2,s2,n) % n
print(hex(m)[2:])
print(bytes.fromhex(hex(m)[2:]))
公钥 Attacks
低公钥指数
如果公钥e,和消息m都很小时
由加密公式得
所以当\(e\)很小的时候,我们可以直接把\(m\)开\(e\)次方,直到开出整数即可,能不能开出来取决于\(m\)的规模,如果明文长度太大,运算时间不能接受
k = 0
while True:
# c + k * n 如果能被开e次方根的话
if iroot(c + k * n, e)[1]:
print(long_to_bytes(iroot(c + k * n, e)[0]))
break
k += 1
Basic Broadcast Attack
攻击条件
如果一个用户使用同一个加密指数 e 加密了同一个密文,并发送给了其他 e 个用户。那么就会产生广播攻击。这一攻击由 Håstad 提出。
攻击原理
这里我们假设 e 为 3,并且加密者使用了三个不同的模数 \(n_1,n_2,n_3\) 给三个不同的用户发送了加密后的消息 m,如下
这里我们假设 \(n_1,n_2,n_3\) 互素,不然,我们就可以直接进行分解,然后得到 d,进而然后直接解密。
既然他们互素,那么我们可以根据中国剩余定理,可得\(m^3 \equiv C \bmod n_1n_2n_3\)。
此外,既然 \(m<n_i, 1\leq i \leq 3\),那么我们知道 \(m^3 < n_1n_2n_3\) 并且 \(C<m^3 < n_1n_2n_3\),那么 \(m^3 = C\),我们对 C 开三次根即可得到 m 的值。
对于较大的 e 来说,我们只是需要更多的明密文对。
#sage
def chinese_remainder(modulus, remainders):
Sum = 0
prod = reduce(lambda a, b: a*b, modulus)
for m_i, r_i in zip(modulus, remainders):
p = prod // m_i
Sum += r_i * (inverse_mod(p,m_i)*p)
return Sum % prod
chinese_remainder([3,5,7],[2,3,2]) #23
#sage
crt([2,3,2],[3,5,7])
#BroadcastAttack
from Crypto.Util.number import *
from libnum import solve_crt
import gmpy2
def BroadcastAttack(N, C, e):
m = solve_crt(C, N)
ans = gmpy2.iroot(m, e)
if ans[1] == True:
return ans[0]
else:
return False
if __name__ == '__main__':
n1 =
c1 =
n2 =
c2 =
n3 =
c3 =
N = [n1, n2, n3]
C = [c1, c2, c3]
for i in range(3, 100):
if BroadcastAttack(N, C, i):
print(long_to_bytes(BroadcastAttack(N, C, i)), i)
Rabin 算法
密文:
解密:
- 计算出 \(m_p\) 和 \(m_q\):
- 用扩展欧几里得计算出 \(y_p\) 和 \(y_q\): [LRX.py](..........\Downloads\WeChat Files\wxid_f7zahwf7mk5q22\FileStorage\File\2020-10\LRX.py)
- 解出四个明文:
注意:如果 \(p \equiv q \equiv 3 \pmod 4\),则
而一般情况下,\(p \equiv q \equiv 3 \pmod 4\) 是满足的,对于不满足的情况下,请参考相应的算法解决。
\(tonelli\_shanks\) + \(crt\) 求解
#脚本1
from Crypto.Util.number import *
from Algorithm import tonelli_shanks
from libnum import solve_crt
def Rabin(p, q, c):
t = tonelli_shanks(c, p)
qlist = [-t, t]
t = tonelli_shanks(c, q)
plist = [-t, t]
for i in qlist:
for j in plist:
print(long_to_bytes(solve_crt([i, j], [p, q])))
#脚本2
import gmpy2
def rabin_decrypt(c, p, q, e=2):
n = p * q
mp = pow(c, (p + 1) // 4, p)
mq = pow(c, (q + 1) // 4, q)
yp = gmpy2.invert(p, q)
yq = gmpy2.invert(q, p)
r = (yp * p * mq + yq * q * mp) % n
rr = n - r
s = (yp * p * mq - yq * q * mp) % n
ss = n - s
return (r, rr, s, ss)
c =
p =
q =
m = rabin_decrypt(c,p,q)
for i in range(4):
try:
print(bytes.fromhex(hex(m[i])[2:]))
except:
pass
这里我们以 XMan 一期夏令营课堂练习(Jarvis OJ 有复现)为例,读一下公钥。
➜ Jarvis OJ-hard RSA git:(master) ✗ openssl rsa -pubin -in pubkey.pem -text -modulus
Public-Key: (256 bit)
Modulus:
00:c2:63:6a:e5:c3:d8:e4:3f:fb:97:ab:09:02:8f:
1a:ac:6c:0b:f6:cd:3d:70:eb:ca:28:1b:ff:e9:7f:
be:30:dd
Exponent: 2 (0x2)
Modulus=C2636AE5C3D8E43FFB97AB09028F1AAC6C0BF6CD3D70EBCA281BFFE97FBE30DD
writing RSA key
-----BEGIN PUBLIC KEY-----
MDowDQYJKoZIhvcNAQEBBQADKQAwJgIhAMJjauXD2OQ/+5erCQKPGqxsC/bNPXDr
yigb/+l/vjDdAgEC
-----END PUBLIC KEY-----
\(e=2\),考虑 Rabin 算法。首先我们先分解一下 p 和 q,得到
p=275127860351348928173285174381581152299
q=319576316814478949870590164193048041239
编写代码
#!/usr/bin/python
# coding=utf-8
import gmpy2
import string
from Crypto.PublicKey import RSA
# 读取公钥参数
with open('pubkey.pem', 'r') as f:
key = RSA.importKey(f)
N = key.n
e = key.e
with open('flag.enc', 'r') as f:
cipher = f.read().encode('hex')
cipher = string.atoi(cipher, base=16)
# print cipher
print "please input p"
p = int(raw_input(), 10)
print 'please input q'
q = int(raw_input(), 10)
# 计算yp和yq
inv_p = gmpy2.invert(p, q)
inv_q = gmpy2.invert(q, p)
# 计算mp和mq
mp = pow(cipher, (p + 1) / 4, p)
mq = pow(cipher, (q + 1) / 4, q)
# 计算a,b,c,d
a = (inv_p * p * mq + inv_q * q * mp) % N
b = N - int(a)
c = (inv_p * p * mq - inv_q * q * mp) % N
d = N - int(c)
for i in (a, b, c, d):
s = '%x' % i
if len(s) % 2 != 0:
s = '0' + s
print s.decode('hex')
拿到 flag,PCTF{sp3ci4l_rsa}
。
Boneh and Durfee attack
$e \(非常大接近于\)N\(,即\) d \(较小时。与低解密指数攻击类似,比低解密指数攻击\)(Wiener Attack)\(更强,可以解决\)\cfrac{1}{3}N^{\frac{1}{4}} \leq d \leq N^{0.292}$的问题。
设$ A=\frac{N+1}{2},y=\frac{-p-1}{2},x=2k,\(有\) f(k,y)=1+x\cdot(A+y)$
如果在模 $e $下解得该方程的根 \(x,y\),由 $ed=1+x\cdot(A+y) \(可以得到\) d$。
#2k [(N + 1)/2 + (-p -q)/2] + 1 = 0 mod e
#Boneh and Durfee attack:
f(x,y) = 1 + x * (A + y)
ed = x [(N + 1)/2 + y] + 1
x = 2k
y = (-p-q)/2
-
变种1:\(e\) 很大,\(dp\)很小,且$ d>2N^{\beta}$。
May’s Attack
假设$ e<\varphi(N),q \le N^{\beta},\beta \le \frac{1}{2}$,因 \(ed_p \equiv 1 \pmod {p-1}\),有$ ed_p=1+k(p-1)$,
对于$ k \in \mathbb{N}$,有 \(ed_p=(k-1)(p-1)+p\),即$ ed_pq=(k-1)(N-q)+N$。
设 x,y 为参数,则多项式$ f(x,y)=x(N-y)+N $在模 \(e\) 下存在根$ (x_0,y_0)=(k-1,q)\(,用\)coppersmith attack$可解。
Wiener's Attack
在 d 比较小(\(d<\frac{1}{3}N^{\frac{1}{4}}\))时,攻击者可以使用 Wiener's Attack 来获得私钥。
适用情况:已知 N,e,且 e 过大或过小。
\(\varphi(n) = (p-1)(q-1)=pq - (p + q) + 1=N - (p + q) + 1\)
$\because p, q \(非常大,\)\therefore,pq\gg p+q, \therefore\varphi(n)\approx N$
\(\because ed\equiv1\,mod\,\varphi(n),\therefore ed-1=k\varphi(n)\),这个式子两边同除$ d\varphi(n) $可得:
\(\cfrac{e}{\varphi(n)}-\cfrac{k}{d}=\cfrac{1}{d\varphi(n)}\)
\(\because \varphi(n)\approx N\),\(\therefore \cfrac{e}{N}-\cfrac{k}{d}=\cfrac{1}{d\varphi(n)}\),同样 \(d\varphi(n)\) 是一个很大的数,所以$ \cfrac{e}{N}$ 略大于$ \cfrac{k}{d}$
因为 \(e\) 和$ N$ 是知道的,所以计算出$ \cfrac{e}{N} \(后,比它略小的\) \cfrac{k}{d}$ ,可以通过计算$ \cfrac{e}{N} \(的连分数展开,依次算出这个分数每一个渐进分数,由于\) \cfrac{e}{N} \(略大于\) \cfrac{k}{d}\(,\)Wiener $证明了,该攻击能精确的覆盖 \(\cfrac{k}{d}\)。
在$ e$ 过大或过小的情况下,可使用算法从$ e$ 中快速推断出 $d \(的值。可以解决\) q<p<2q,d<\cfrac{1}{3}N^{\frac{1}{4}} $的问题。
攻击原理
- https://en.wikipedia.org/wiki/Wiener's_attack
- https://sagi.io/2016/04/crypto-classics-wieners-rsa-attack/
工具:
Continued Fractions(连分数)
连分数就是一个数的连续分式展开,它的式子长这样:
(其中\(a_0\) 是整数,\(a_1,a_2,…,a_n\)都是正整数)
通常我们用更简单的数组方式来描述,对任何有理数$p / q $来说:
计算连分数我们可以使用欧几里得算法:
参考:https://zh.wikipedia.org/w/index.php?title=连分数&oldid=65444935
Convergent(收敛)
我们定义\(c_i\)为连分数每一次分式展开的收敛,即:
对于每一个 $ \mathrm{c}{\mathrm{i}} $ 的结果$ \frac{\mathrm{p}{\mathrm{i}}}{\mathrm{qi}} $来说,都是对 $\frac{\mathrm{q}}{\mathrm{p}} $不断进行逼近收敛的近似值。
Legendre' s theorem
这是\(Wiener's attack\) 的一个比较重要的定理,定义如下:
\(Theorem 2.2 (Continued Fractions). Let \alpha \in \mathbb{Q} and c, d \in \mathbb{Z} satisfy\)
\(Then \mathrm{c} / d , in lowest terms, is one of the convergents in the continued fraction expansion of \alpha .\)
也就是说,当满足$ \left|\mathrm{a}-\frac{\mathrm{c}}{\mathrm{d}}\right|<\frac{1}{2 \mathrm{~d}^{2}} $ 时,$ \frac{\mathrm{c}}{\mathrm{d}} $ 就是 $\mathrm{a} $ 的连分数收敛。
根据这个定理,我们可以通过计算一个数$ (\mathrm{e} / \mathrm{N}) $ 的连分数来找到与这个数近似的两个数的比值$ (k/d)\(,这就是\)Wiener's attack$的攻击方法。
#脚本1
import gmpy2
def transform(x,y): #使用辗转相处将分数 x/y 转为连分数的形式
res=[]
while y:
res.append(x//y)
x,y=y,x%y
return res
def continued_fraction(sub_res):
numerator,denominator=1,0
for i in sub_res[::-1]: #从sublist的后面往前循环
denominator,numerator=numerator,i*numerator+denominator
return denominator,numerator #得到渐进分数的分母和分子,并返回
#求解每个渐进分数
def sub_fraction(x,y):
res=transform(x,y)
res=list(map(continued_fraction,(res[0:i] for i in range(1,len(res))))) #将连分数的结果逐一截取以求渐进分数
return res
def get_pq(a,b,c): #由p+q和pq的值通过维达定理来求解p和q
par=gmpy2.isqrt(b*b-4*a*c) #由上述可得,开根号一定是整数,因为有解
x1,x2=(-b+par)//(2*a),(-b-par)//(2*a)
return x1,x2
def wienerAttack(e,n):
for (d,k) in sub_fraction(e,n): #用一个for循环来注意试探e/n的连续函数的渐进分数,直到找到一个满足条件的渐进分数
if k==0: #可能会出现连分数的第一个为0的情况,排除
continue
if (e*d-1)%k!=0: #ed=1 (mod φ(n)) 因此如果找到了d的话,(ed-1)会整除φ(n),也就是存在k使得(e*d-1)//k=φ(n)
continue
phi=(e*d-1)//k #这个结果就是 φ(n)
px,qy=get_pq(1,n-phi+1,n)
if px*qy==n:
p,q=abs(int(px)),abs(int(qy)) #可能会得到两个负数,负负得正未尝不会出现
d=gmpy2.invert(e,(p-1)*(q-1)) #求ed=1 (mod φ(n))的结果,也就是e关于 φ(n)的乘法逆元d
return d
print("该方法不适用")
e =
n =
d=wienerAttack(e,n)
print("d=",d)`
#脚本2
#Sage
def rational_to_contfrac(x,y):
# Converts a rational x/y fraction into a list of partial quotients [a0, ..., an]
a = x // y
pquotients = [a]
while a * y != x:
x, y = y, x - a * y
a = x // y
pquotients.append(a)
return pquotients
def convergents_from_contfrac(frac):
# computes the list of convergents using the list of partial quotients
convs = [];
for i in range(len(frac)): convs.append(contfrac_to_rational(frac[0 : i]))
return convs
def contfrac_to_rational (frac):
# Converts a finite continued fraction [a0, ..., an] to an x/y rational.
if len(frac) == 0: return (0,1)
num = frac[-1]
denom = 1
for _ in range(-2, -len(frac) - 1, -1): num, denom = frac[_] * num + denom, num
return (num, denom)
n =
e =
c =
def egcd(a, b):
if a == 0: return (b, 0, 1)
g, x, y = egcd(b % a, a)
return (g, y - (b // a) * x, x)
def mod_inv(a, m):
g, x, _ = egcd(a, m)
return (x + m) % m
def isqrt(n):
x = n
y = (x + 1) // 2
while y < x:
x = y
y = (x + n // x) // 2
return x
def crack_rsa(e, n):
frac = rational_to_contfrac(e, n)
convergents = convergents_from_contfrac(frac)
for (k, d) in convergents:
if k != 0 and (e * d - 1) % k == 0:
phi = (e * d - 1) // k
s = n - phi + 1
# check if x*x - s*x + n = 0 has integer roots
D = s * s - 4 * n
if D >= 0:
sq = isqrt(D)
if sq * sq == D and (s + sq) % 2 == 0: return d
d = crack_rsa(e, n)
m = hex(pow(c, d, n))[2:]
print(bytes.fromhex(m))
#脚本3
from Crypto.Util.number import long_to_bytes
e =
n =
c =
#将分数x/y展开为连分数的形式
def transform(x,y):
arr=[]
while y:
arr+=[x//y]
x,y=y,x%y
return arr
#求解渐进分数
def sub_fraction(k):
x=0
y=1
for i in k[::-1]:
x,y=y,x+i*y
return (y,x)
data=transform(e,n)
for x in range(1,len(data)+1):
data1=data[:x]
d = sub_fraction(data1)[1]
m = pow(c,d,n)
flag = long_to_bytes(m)
if b'flag{' in flag:
print(flag)
break
#脚本4
import sympy
def fractions(x,y):
ans=[y//x]
if y%x==0: return ans
else:
ans.extend(fractions(y%x,x))
return ans
def continued_fractions(e,n):
ans=[]
x= fractions(e,n)
for i in range(1,len(x)):
k, d= 1, x[i-1]
for j in x[:i-1][::-1]:
k, d = d, d*j+k
ans.apped((k, d))
return ans
def Wiener(e,n):
for k, d in continued_fractions(e,n):
phi=(e*d-1)//k
#x**2 -(n-phi+1)x+n=0
if d == int(sympy.invert(e,phi)):
return d
break
#脚本5
def wiener(e, n):
m = 12345
c = pow(m, e, n)
q0 = 1
list1 = continued_fraction(Integer(e) / Integer(n))
conv = list1.convergents()
for i in conv:
k = i.numerator()
q1 = i.denominator()
for r in range(20):
for s in range(20):
d = r * q1 + s * q0
m1 = pow(c, d, n)
if m1 == m:
return d
q0 = q1
n =
e =
print (wiener(e, n))
- 变种1:\(\cfrac{N_1}{N_2}<\cfrac{q_1}{q_2}<1\)
Paper: https://eprint.iacr.org/2015/399.pdf
2020年羊城杯 - RRRRRRRSA
P1 = getPrime(1038)
P2 = sympy.nextprime(P1)
assert(P2 - P1 < 1000)
Q1 = getPrime(512)
Q2 = sympy.nextprime(Q1
N1 = P1 * P1 * Q1
N2 = P2 * P2 * Q2
尝试对$ \cfrac{N_1}{N_2} \(进行连分数展开并求其各项渐进分数,记为\) \cfrac{t_i}{s_i}$ 并验证$ N_1% {t_k}==0$ 是否成立,如果成立,那么$ q_1=t_k,q_2=s_k$。
from gmpy2 import *
from Crypto.Util.number import *
N1=
c1=
E1=
N2=
c2=
E2=
def continuedFra(x, y): #不断生成连分数的项
cF = []
while y:
cF += [x // y]
x, y = y, x % y
return cF
def Simplify(ctnf): #化简
numerator = 0
denominator = 1
for x in ctnf[::-1]: #倒叙遍历
numerator, denominator = denominator, x * denominator + numerator
return (numerator, denominator) #把连分数分成分子和算出来的分母
def getit(c):
cf=[]
for i in range(1,len(c)):
cf.append(Simplify(c[:i])) #各个阶段的连分数的分子和分母
return cf #得到一串连分数
def wienerAttack(e, n):
cf=continuedFra(e,n)
for (Q2,Q1) in getit(cf):#遍历得到的连分数,令分子分母分别是Q2,Q1
if Q1 == 0:
continue
if N1%Q1==0 and Q1!=1:#满足这个条件就找到了
return Q1
print('not find!')
Q1=wienerAttack(N1,N2)
print(N1%Q1)
Q2=next_prime(Q1)
P1=iroot(N1//Q1,2)[0]
P2=next_prime(P1)
phi1=(P1)*(P1-1)*(Q1-1)
phi2=(P2)*(P2-1)*(Q2-1)
d1=invert(E1,phi1)
d2=invert(E2,phi2)
flag1=long_to_bytes(pow(c1,d1,N1))
flag2=long_to_bytes(pow(c2,d2,N2))
print(flag1+flag2)
-
变种2:\(Ax \equiv y \pmod P,即 Ax-kP=y 或 Ax+kP=y\)
Wiener’s:
\(\Big\vert \cfrac{A}{P}-\cfrac{k}{x}\Big\vert \le \cfrac{1}{2x^2}\),连分数方法,用$ \cfrac{A}{P}$ 逼近求出$ \cfrac{k}{x}\(,进而解出\) y$。
Lattice:
\((x,k)\begin{bmatrix}1&A\\0&P\ \end{bmatrix} = (x,y)\),记为$ vM=w$,w 有很大概率为 \(M\) 里的最短向量,使用LLL算法求出最短向量,即解出$ w’=(x,y)$。
Extending Wiener 's attack
扩展维纳攻击
来自《Extending Wiener's Attack in the Presence of Many Decrypting Exponents》
,相关题目在 CTF 中已经出现了,例如 2020 羊城杯的 Simple。论文下载链接
维纳Wiener
提出了一种关于私钥过小时对\(N\) 进行分解的一种方式。并给出了证明当
满足时 (还应满足\(q<p<2q\),因这里及后文主要是对私钥进行探讨,故忽略这类条件) 一定能够分解\(N\)。
Wiener 's Approach
已知
这里$ \lambda(N)=l c m(p-1, q-1)=\varphi(N) / g $ , 令$ s=1-p-q$ 则有
将两边同时除以$ d g N $ 则有
我们知道这里有$ e \approx N, s \approx N^{1 / 2}$ ,所以有$ k /(d g) \approx 1$ 。则我们可以知道等式右边约等于 $N^{-1 / 2} $ 。我们都知道当
时则$ a / b$ 是一个$ x $ 连分数近似 (连分数定理\(Continued Fractions\))
所以当
时有$ k / d g$ 是 $ e / N $的连分数近似,即能通过连分数展开覆盖。
注意这里前面所说的范围和后面的范围并不矛盾
这里对一些参数的值的近似并不严格,所以和维纳攻击的严格范围有出入,具体细节可参考维纳攻击的证明。
Guo 's Approach With Two Exponents
假设对于一个$ N \(来说有两个\) e_{i}$ ,而且都有相对应比较小的$ d_{i} $ ,那么可以得到:
化简后可得:
两边同时除以\(k_2d_1e_2\),可推出
设 $ d_{i}<N^{\alpha} $ ,则等式右边约等于$ N^{-(1+\alpha)} $
则当
时$ k_{1} d_{2} /\left(k_{2} d_{1}\right) $ 是 $ e_{1} / e_{2} $ 的连分数近似。当 $ k_{2} 和 d_{1} $ 最多为$ N^{\alpha} $ 而且$ g $很小时,得到
所以,如果 $ 2\left(\mathrm{k}{2}-\mathrm{k}\right) \mathrm{d}{1} \mathrm{k}<\mathrm{e}{2}$ ,
那么就可以通过求$ \frac{\mathrm{e}{1}}{\mathrm{e}{2}} $ 的连分数找到$ \frac{\mathrm{d} \mathrm{k}{1}}{\mathrm{~d} \mathrm{k}{2}} $ 。
但是即使我们得到了$ \frac{\mathrm{d} \mathrm{k}{1}}{\mathrm{~d} \mathrm{k}_{2}} \(,还是无法分解\)N\(,原文后面还讨论了郭的提议,尝试对\)k_1d_2$ 进行分解。
Extending Wiener 's attack
Howgrave-Graham 和 Jean-Pierre Seifert 结合了Wiener 和Guo的想法,通过多个等式来构造格利用LLL化简来解决这个问题。
-
为了将分析扩展到$n \(个加密指数\)e_i\((解密指数\)d_i $很小),我们同时使用维纳和郭的方法,我们将关系
\[d_ige_i - k_iN = g + k_is \]记为维纳等式\(W_i\),同样我们可以得到关系
\[k_id_je_j - k_jd_ie_i = k_i - k_j \]记为郭等式\(G_{i,j}\)。
我们假设\(d_i\) 和$k_i \(都小于\)N^{\alpha_n}\(,且\)g$ 很小,\(s \approx N^{1/2}\)。可以注意到$W_i \(和\)G_i \(的右侧非常小,实际上分别最多为\)N^{1/2 + \alpha} \(和\)N^\alpha$。
最后,我们考虑复合关系式比如\(W_uG_{v,w}\),显然大小为\(N^{1/2 + 2\alpha}\)。
-
原文中这里是定义了两个关系式以及指出了他们的大小范围,这个范围很重要也容容易分析处理,之后我们所做的其实就是使用这两个式子的不同复合关系去构造一个格,然后通过求其基向量得到\(d_1g/k_1\),从而可以算得\(\varphi(N)\) 并可以进一步的对N 进行分解。
-
其实到这里原理分析已经结束,关于格的构造其实也并不复杂,但是核心是这里的复合关系的选取,以及对于最后$\alpha $大小的分析。
两个小解密指数的情况
-
我们选取关系\(W_1, G_{1,2},W_1W_2\), 这样便有
\[\begin{aligned} d_1ge_1 - k_1N &= g+k_1s\\ k_1d_2e_2 - k_2d_1e_1 &= k_1-k_2\\ d_1d_2g^2e_1e_2 - d_1gk_2e_1N - d_2gk_1e_2N + k_1k_2N^2 &= (g+k_1s)(g+k_2s) \end{aligned} \]我们对第一个关系式乘上\(k_2\),这样左边便全是由\(d_1d_2g^2, d_1gk_2, d_2gk_1 和k_1k_2\) 构成,这样我们便可以用已知内容构造格将上述式子转化为矩阵运算
\[\begin{pmatrix} k_1k_2&d_1gk_2&d_2gk_1&d_1d_2g^2 \end{pmatrix} \begin{pmatrix} 1&-N&0&N^2\\ &e_1&-e_1&-e_1N\\ &&e_2&-e_2N\\ &&&e_1e_2 \end{pmatrix} = \begin{pmatrix} k_1k_2&k_2(g+k_1s)&g(k_1 - k_2)&(g+k_1s)(g+k_2s) \end{pmatrix} \]等式右边向量的大小为\(N^{2\alpha_2}, N^{1/2+2\alpha_2}, N^{\alpha_2}, N^{1+2\alpha_2}\), 为了让大小相等,我们可以考虑构造一个 \(D\) 矩阵。
\[D = \begin{pmatrix} N&&&\\ &N^{1/2}&&\\ &&N^{1+\alpha_2}&\\ &&&1 \end{pmatrix} \]最终我们构造的矩阵为
\[L_2 = \begin{pmatrix} 1&-N&0&N^2\\ &e_1&-e_1&-e_1N\\ &&e_2&-e_2N\\ &&&e_1e_2 \end{pmatrix} * D \]这样向量\(b = \begin{pmatrix} k_1k_2&d_1gk_2&d_2gk_1&d_1d_2g^2 \end{pmatrix}\) 便有
\[\Vert bL_2 \Vert < 2N^{1+2\alpha_2} \]这也就是为什么前面需要构造\(D\) 矩阵的原因,给定\(D\) 矩阵后,我们可以得到一个上界,这样问题可以转化为类 SVP 问题。
那么这里的$ b \(向量其实我们使用格基规约算法例如`LLL`便可以得到基向量\)b\(,然后我们求解\)b_2/b_1 \(即得到\)d_1g/k_1$
之后我们就可以得到
\[\varphi(N) = \frac{edg}{k} - \frac{g}{k} = \lfloor edg/k\rceil \]我们假设这些格中最短向量长度为\(\Delta^{1/4-\epsilon}\),其中\(\Delta = det(L_2) = N^{13/2 + \alpha_2}\)。如果这些格是随机的,我们甚至几乎可以肯定没有格点比闵可夫斯基界\((Minkowski's bound)2\Delta^{1/4}\),所以$bL_2 $是最短向量当
\[N^{1+2\alpha_2} < (1/c_2)\left(N^{13/2+\alpha_2}\right)^{1/4} \]对于一些小的\(c_2\),如果有
\[\alpha_2 < 5/14 - \epsilon^{'} \]则我们可以通过格基规约找到向量b。
-
上述内容是原文中给出的当两个小解密指数是进行的攻击细节,并且分析了$\alpha $的大小关系。
分析
-
扩展维纳攻击结合上述三个例子已经详细的阐明了方法细节,但是其中没有讲解如何选取复合关系。其实在原文的附录中给出了复合关系的选取,以及给出了$\alpha_n $的表达式。
-
在原文附录部分,考虑\(n\) 个指数\(e_i\),这样则有\(2^n\) 个不同的量\(h_j\)(一个表达式$e_i \(的个数),这样我们的\)L_n$ 在乘上\(D\) 之前,矩阵$L_n \(的行列式为\)N{n2{n-1}}$
这样最后一个关系\(W_1W_2\dots W_n\) 最大为\(N^{n/2 + n\alpha_n}\),这样我们便知道了任意情况的最大界值,我们只需要让其他值增加到这么多即可(即构造\(D\) 矩阵)
引入了新的关系式
\[R_{u,v} = W_{i_1}\dots W_{i_u}G_{j_1, l_1}\dots G_{j_v, l_v} \]其中$i_1,\dots,i_u,j_1,\dots,j_u,l_1,\dots,l_v \(都不同,那么这里最多会有\)u + 2v \(个指数\)e_i\(,则我们的关系\)R_{u,v}$ 最多为\(N^{u/2 + (u+v)\alpha_n}\),同时注意我要需要所有系数的大小大致相同,所以我们在某些等式乘上\(k_i\),使得关系\(R_{u, v} = N^{u/2 + (n-v)\alpha_n}\)。
最后我们再计算所有的大小与最大大小$N^{n/2 + n\alpha_n} \(的差值,构造矩阵\)D$。
这样我们便完成了矩阵$D \(的构造,同时设矩阵\)D \(里面指数的乘积为\)\beta_n = x+y\alpha_n$,这样有
\[det(L_n) \approx N^{n2^{n-1} + x + y\alpha_n} \]则有
\[N^{n/2 + n\alpha_n} < (1/c_n)\left(N^{n2^{n-1} + x + y\alpha_n}\right)^{1/2^n} \]对于小\(c_n\),有
\[\alpha_n < \frac{x}{n2^n - y} - \epsilon^{'} \]所以我们要想让$\alpha_n \(更大就需要让\)x \(和\)y \(更大,这意味着我们要选取更多的\)v$ 和更小的\(u\)。比如在$n=2 \(的情况我们选取\)W_1, G_{1, 2}, W_1W_2 \(而不是\)W_1, W_2, W_1W_2$ 因为前者$\beta_2 = 5/2 + \alpha \(而后者\)\beta_2 = 2$。
-
到这里,其实已经讲清楚了扩展维纳攻击的整个流程,如何选择复合关系,如何构造格,如何构造矩阵\(D\) 以及如何求解。
这里给出n=2时候的EXP,2020羊城杯 Simple
from Crypto.Util.number import *
from gmpy2 import invert
c =
e1 =
e2 =
N =
a = 5/14
D = diagonal_matrix(ZZ, [N, int(N^(1/2)), int(N^(1+a)), 1])
M = matrix(ZZ, [[1, -N, 0, N^2], [0, e1, -e1, -e1*N], [0, 0, e2, -e2*N], [0, 0, 0, e1*e2]])*D
L = M.LLL()
t = vector(ZZ, L[0])
x = t * M^(-1)
phi = int(x[1]/x[0]*e1)
d = invert(0x10001,phi)
m=pow(c,d,N)
print(long_to_bytes(m))
参考文献
- Extending Wiener's Attack in the Presence of Many Decrypting Exponents
- 并行 LLL 算法研究综述
- Factoring Polynomials with Rational Coefficients
- A PARALLEL JACOBI-TYPE LATTICE BASIS REDUCTION ALGORITHM
e与phi(n)不互质
一般来说,RSA的公钥选取时,都会选择一个与\(\phi(n)\)互质的加密指数\(e\),这样才能计算私钥中的揭秘指数\(d\),满足\(e*d\equiv1\pmod{\phi(n)}\),并用\(d\)来恢复明文。 $$ c^d\equiv m^{ed}\equiv m \pmod{n} $$ 当\(e\)和\(\phi(n)\)不互质的时候,就不能直接计算\(d = inv(e, \phi(n))\)。然后不互质又可以分为两种情况,一是\(e\mid\phi(n)\),二是\(e\nmid\phi(n)\)
有一道CTF题是比较出名的,NCTF2019
\(1.e\nmid\phi(n)\)
这种情况时比较好处理的,我们令\(t=\gcd(e, \phi(n))\),那么\(e//t\)和\(\phi(n)\)时互质的,即\(\gcd(e//t,\phi(n))=1\),这样就可以计算\(d\),满足
然后将\(m^t\)看成一个整体
这个就是一个正常的RSA解密了,不过解出来的不是明文\(m\),而是\(m^t\),如何得到\(m\)呢?这就又要分两种情况考虑了
\(1)m^t<n\)
这就是最简单的了,直接开\(t\)次根,就直接得到\(m\)
from gmpy2 import iroot
from Crypto.Util.number import *
p =
q =
c =
n = p*q
e =
phi = (p - 1)*(q - 1)
t = GCD(e, phi)
d = inverse(e // t, phi)
_m = pow(c, d, n)
m = int(iroot(_m, t)[0])
\(2)m^t >n\)
如果\(m^t\)没有比\(n\)大多少,那就可以爆破
from gmpy2 import iroot
p =
q =
c =
n = p*q
e =
k = 0
while not iroot(_m + k*n, t)[1]:
k += 1
m = iroot(_m + k*n, t)[0]
其实就相当于转换成了一个低加密指数的题
- 也可以选择结合中国剩余定理
在不同的域上开根,然后把得到的结果进行CRT组合
#sage
from Crypto.Util.number import *
import gmpy2
p =
q =
c =
n =
e =
phi = (p - 1) * (q - 1)
R.<x> = Zmod(p)[]
f = x ^ e - c
f = f.monic()
res1 = f.roots()
R.<x> = Zmod(q)[]
f = x ^e - c
f = f.monic()
res2 = f.roots()
for i in res1:
for j in res2:
ai = [i[0],j[0]]
mi = [p,q]
flag = CRT_list(ai,mi)
flag = long_to_bytes(flag)
if b'flag' in flag:
print(flag)
\(2.e \mid\phi(n)\)
这个时候就不能用上面的类似RSA的方法解决了,我们就只能在有限域上开根了。
谈论有限域开根问题,AMM算法是绕不过的。AMM算法在RSA中适用于指数e整除phi的情况,也就是说phi % e == 0。其中有详细的论文,AMM算法具体的作用就是在有限域中开出一个根。具体实现论文也明确地给出了。
代码实现:
# sage
def AMM(o, r, q):
start = time.time()
print('\n----------------------------------------------------------------------------------')
print('Start to run Adleman-Manders-Miller Root Extraction Method')
print('Try to find one {:#x}th root of {} modulo {}'.format(r, o, q))
g = GF(q)
o = g(o)
p = g(random.randint(1, q))
while p ^ ((q-1) // r) == 1:
p = g(random.randint(1, q))
print('[+] Find p:{}'.format(p))
t = 0
s = q - 1
while s % r == 0:
t += 1
s = s // r
print('[+] Find s:{}, t:{}'.format(s, t))
k = 1
while (k * s + 1) % r != 0:
k += 1
alp = (k * s + 1) // r
print('[+] Find alp:{}'.format(alp))
a = p ^ (r**(t-1) * s)
b = o ^ (r*alp - 1)
c = p ^ s
h = 1
for i in range(1, t):
d = b ^ (r^(t-1-i))
if d == 1:
j = 0
else:
print('[+] Calculating DLP...')
j = - discrete_log(d, a)
print('[+] Finish DLP...')
b = b * (c^r)^j
h = h * c^j
c = c^r
result = o^alp * h
end = time.time()
print("Finished in {} seconds.".format(end - start))
print('Find one solution: {}'.format(result))
return result
其中$ \operatorname{AMM}(o, r, q) $ 的返回值满足$result \equiv o^{r} \quad(\bmod q) $
就是在有限域\(q\)中对\(o\)开\(r\)次根。还有一个比较方便的函数,但是没有具 体研究过,作用也是在有限域内得到一个根。得到一个x满足 $x^{n} \equiv a \quad(\bmod p) $
from sympy.ntheory.residue_ntheory import nthroot_mod
nthroot_mod(a,n,p)
![img](http://clingm.top/2022/07/11/对于RSA中e与phi-n-不互质情况的一点点总结/Untitled 1.png)
私钥 Attack
D Leaking Attack
首先当 \(d\) 泄露之后,我们自然可以解密所有加密的消息。我们甚至还可以对模数 N 进行分解。其基本原理如下
我们知道 \(ed \equiv 1 \bmod \varphi(n)\),那么存在一个 \(k\) 使得
又 \(\forall a\in {Z}_n^*\),满足\(a^{ed-1}\equiv1(\bmod n)\)。令
其中,\(t\) 是一个奇数。然后可以证明对于至少一半的 \(a\in {Z}_n^*\),存在一个 \(i\in[1,s]\),使得
成立。如果 \(a,i\) 满足上述条件,\(gcd(a^{2^{i-1}t}-1,n)\)是 \(n\) 的一个非平凡因子,所以可以对 \(n\) 进行暴力分解。
参考文献:
给e,d,n
计算$ k=ed-1\(,选择一个随机数\) g\(,\)g \in (1,N)$。
$k \(为偶数,故\) k=2^tr\(,其中\) r \(为奇数且\) t \ge 1$,然后计算 \(x = g^{\frac{k}{2}},g^{\frac{k}{4}},\cdots,g^{\frac{k}{2^t}} \pmod N\) 直到$ x \gt 1$ 且 \(y=\gcd(x-1,N) \gt 1\)。
如果这样的$ y$ 存在,则其中的因子$ p=y\(,\)q=\cfrac{N}{y}$;如这样的 y 不存在,则重新生成随机数 \(g\)。
def divide_pq(e, d, n):
k = e*d - 1
while True:
g = random.randint(2, n-1)
t = k
while True:
if t % 2 != 0:
break
t //= 2
x = pow(g, t, n)
if x > 1 and gmpy2.gcd(x-1, n) > 1:
p = gmpy2.gcd(x-1, n)
return (p, n//p)
DP Leaking Attack
这类问题是给出了\(dp, n, e,c\),我们的目的是求出 \(d\) 解密
由\(dp\) 定义我们知道
根据已知条件得到方程组
尝试联立
因为
我们知道\(\varphi(p) = p - 1\),然后\(dp = d \mod \varphi(p)\), \(dp\) 肯定小于模数\(\varphi(p)\) ,\(dp < \varphi(p)\),所以有
所以我们在小于\(e\) 的范围内循环检查分解\(e * \varphi(p) - 1\)即可,代码如下
from Crypto.Util.number import *
import gmpy2
def RSA_dp(e, n, c, dp):
for i in range(1, e):
if (e * dp - 1) % i == 0:
p = (e * dp - 1) // i + 1
if n % p == 0:
q = n // p
phin = (p - 1) * (q - 1)
d = inverse(e, phin)
print(long_to_bytes(pow(c, d, n)))
break
更直观的推导思路:
两边同乘e:
则有:
把\(dp \cdot e\)放在一边
因为\(dp\)比\(p-1\)小
变种1
-
\(变种 1 : 给 p, e, d_{p}, c, b , 其中 n=p^{b} q_{\text {。 }}\)
Hensel lifting for Takagi's scheme (p.189) :
from Crypto.Util.number import *
import gmpy2
p =
dp =
c =
b =
e =
mp1 = pow(c, dp, p)
mp = pow(c, dp - 1, p)
for i in range(1, b - 2):
x = pow(c - pow(mp1, e), 1, p**(i + 1))
y = pow(x * mp * (gmpy2.invert(e, p)), 1, p**(i + 1))
mp1 = mp1 + y
print(long_to_bytes(mp1))
变种2
- $变种 2 : 给 n, e, d p_{0}, c, k , 其中 d p_{0} 为 d p 高 (n bits -k) 位, 即 d p_{0}=d p>>k_{\text {。 }} $
(Coppersmith攻击, 已知dp高位攻击)
#Sage
dp0 =
e =
n =
F.<x> = PolynomialRing(Zmod(n))
d = inverse_mod(e, n)
for k in range(1, e):
f = (secret << 200) + x + (k - 1) * d
x0 = f.small_roots(X=2 ** (200 + 1), beta=0.44, epsilon=1/32)
if len(x0) != 0:
dp = x0[0] + (secret << 200)
for i in range(2, e):
p = (e * Integer(dp) - 1 + i) // i
if n % p == 0:
break
if p < 0:
continue
else:
print('k = ',k)
print('p = ',p)
print('dp = ',dp)
break
变种3
-
变种 3 : 给 n, e, d p, c , 其中 d p 很小, e 很大。
\(枚举 d p , 因 e \cdot d p \equiv 1(\bmod (p-1)) , 又由费马小定理, 对任意 r , 有 \\ m^{e \cdot d p} \equiv m(\bmod p) , 即 p \mid\left(m^{e \cdot d p}-m\right) ;\\ 又 p \mid n , 很大概率 p=\operatorname{gcd}\left(m^{e \cdot d p}-m, n\right) 。\)
变种4
- 变种4 : 给 N, e, c , 其中 d p 过小。
情形1: $q<N^{0.382} $
\(参数 \beta=\frac{q_{\text {bit }}}{N_{\text {bit }}}, \delta=\frac{d p_{\text {bit }}}{N_{\text {bit }}} ,满足 3 \beta<1+\beta^{2}+2 \delta , 可确定 \beta 和 \delta 的值。\\ 构造格子维度为 n , 格子中模数 N 的最大次幂为 m , 应满足关系\)
\(\frac{m(m+1)}{2}+\frac{n(n-1)(2 \delta+\beta)}{2}-(1-\beta) n m<0\)
确定 $ \beta $和 $\delta $之后,可枚举确定 n 和 m 的取值 (最小值) , $$ m=(1-\beta) n $$ 是 一个较优的取值。
beta =
delta =
n = round((1-2*beta-2*delta)/((1-beta)^2-2*delta-beta),6)
m = (1-beta)*n
print(m,n)
构造多项式,分解多项式为\((ax+by)\)的项,其中\(a=k\),\(b=dp\)。
# 脚本1
# Sage
def getC(Scale):
C = [[0 for __ in range(Scale)] for _ in range(Scale)]
for i in range(Scale):
for j in range(Scale):
if i == j or j == 0:
C[i][j] = 1
else:
C[i][j] = C[i-1][j-1] + C[i-1][j]
return C
def getMatrix(Scale, Mvalue, N, E, Del, Bet):
M = [[0 for __ in range(Scale)] for _ in range(Scale)]
C = getC(Scale)
X, Y = int(pow(N,Del)*(Scale+1)//2), int(pow(N,(Del+Bet))*(Scale+1)//2)
for i in range(Scale):
for j in range(Scale):
M[i][j] = N**max(Mvalue-i,0)*E**(max(i-j,0))*X**(Scale-1-j)*Y**j*C[i][j]*(-1)**j
return M
N =
E =
delta = 0.01
beta = 0.37
Scale = 35
Mvalue = 22
M = getMatrix(Scale,Mvalue,N,E,delta,beta)
M = matrix(ZZ,M)
A = M.LLL()[0]
p = []
X = int(pow(N,delta)*(Scale+1)//2)
Y = int(pow(N,(delta+beta))*(Scale+1)//2)
for i in range(Scale):
p.append(A[i]//(X**(Scale-1-i)*Y**i))
PR.<x,y> = PolynomialRing(ZZ)
f = 0
for i in range(Scale):
f += p[i]*x^(Scale-1-i)*y^i
print(f.factor())
# 脚本2
# Sage
N =
e =
n = 12
beta = 0.36
delta = 0.02
X = int(N ** delta*(n+1)/2)
Y = int(N ** (delta + beta)*(n+1)/2)
def C(a,b):
ret=1
for i in range(b):
ret *= (a-i)
ret /= (b-i)
return ret
def get_Matrix(n,m):
MM=[[0 for __ in range(n)] for _ in range(n)]
for j in range(n):
pN = max(0,m-j)
for i in range(j+1):
MM[j][i] = pow(N,pN)*pow(X,n-i-1)*pow(Y,i)*pow(e,j-i)*C(j,i)*pow(-1,i)
MM = Matrix(ZZ,MM)
return MM
M = get_Matrix(n,n//2+1)
L = M.LLL()[0]
x,y = var('x'),var('y')
f = 0
for i in range(n):
f += x**(n-i-1) * y**i * (L[i] // pow(X,n-i-1) // pow(Y,i))
print(f.factor())
参考:
Cryptanalysis of Unbalanced RSA with Small CRT-Exponent
https://hash-hash.github.io/2022/05/14/Unbalanced-RSA-with-Small-CRT-Exponent/#An-Approach-Modulo-e
NSSCTF Round#3 - Secure_in_N
情形2 : $q<N^{0.468} $
\(参数 \beta=\frac{q_{\text {bit }}}{N_{\text {bit }}}, \delta=\frac{d p_{\text {bit }}}{N_{\text {bit }}}, \alpha=\frac{e_{\text {bit }}}{N_{\text {bit }}} , \\变量上界 X=2 N^{\alpha+\beta+\delta-1}, Y=N^{\beta}, Z=2 N^{1-\beta} , 对于变量 m 需充分 大。\)
\(\tau=\frac{(1-\beta)^{2}-\delta}{2 \beta(1-\beta)}, \sigma=\frac{1-\beta-\delta}{2(1-\beta)}, t=\tau m, s=\sigma m\)
\(整数域上有根 (x, y, z)=\left(x_{0}, p, q\right) 。\)
from copy import deepcopy
# https://www.iacr.org/archive/pkc2006/39580001/39580001.pdf
# Author: ZM__________J, To1in
N =
e =
alpha = log(e, N)
beta =
delta =
P.<x,y,z>=PolynomialRing(ZZ)
X = ceil(2 * N^(alpha + beta + delta - 1))
Y = ceil(2 * N^beta)
Z = ceil(2 * N^(1 - beta))
def f(x,y):
return x*(N-y)+N
def trans(f):
my_tuples = f.exponents(as_ETuples=False)
g = 0
for my_tuple in my_tuples:
exponent = list(my_tuple)
mon = x ^ exponent[0] * y ^ exponent[1] * z ^ exponent[2]
tmp = f.monomial_coefficient(mon)
my_minus = min(exponent[1], exponent[2])
exponent[1] -= my_minus
exponent[2] -= my_minus
tmp *= N^my_minus
tmp *= x ^ exponent[0] * y ^ exponent[1] * z ^ exponent[2]
g += tmp
return g
m = 5 # need to be adjusted according to different situations
tau = ((1 - beta)^2 - delta) / (2 * beta * (1 - beta))
sigma = (1 - beta - delta) / (2 * (1 - beta))
print(sigma * m)
print(tau * m)
s = ceil(sigma * m)
t = ceil(tau * m)
my_polynomials = []
for i in range(m+1):
for j in range(m-i+1):
g_ij = trans(e^(m-i) * x^j * z^s * f(x, y)^i)
my_polynomials.append(g_ij)
for i in range(m+1):
for j in range(1, t+1):
h_ij = trans(e^(m-i) * y^j * z^s * f(x, y)^i)
my_polynomials.append(h_ij)
known_set = set()
new_polynomials = []
my_monomials = []
# construct partial order
while len(my_polynomials) > 0:
for i in range(len(my_polynomials)):
f = my_polynomials[i]
current_monomial_set = set(x^tx * y^ty * z^tz for tx, ty, tz in f.exponents(as_ETuples=False))
delta_set = current_monomial_set - known_set
if len(delta_set) == 1:
new_monomial = list(delta_set)[0]
my_monomials.append(new_monomial)
known_set |= current_monomial_set
new_polynomials.append(f)
my_polynomials.pop(i)
break
else:
raise Exception('GG')
my_polynomials = deepcopy(new_polynomials)
nrows = len(my_polynomials)
ncols = len(my_monomials)
L = [[0 for j in range(ncols)] for i in range(nrows)]
for i in range(nrows):
g_scale = my_polynomials[i](X * x, Y * y, Z * z)
for j in range(ncols):
L[i][j] = g_scale.monomial_coefficient(my_monomials[j])
# remove N^j
for i in range(nrows):
Lii = L[i][i]
N_Power = 1
while (Lii % N == 0):
N_Power *= N
Lii //= N
L[i][i] = Lii
for j in range(ncols):
if (j != i):
L[i][j] = (L[i][j] * inverse_mod(N_Power, e^m))
L = Matrix(ZZ, L)
nrows = L.nrows()
L = L.LLL()
# Recover poly
reduced_polynomials = []
for i in range(nrows):
g_l = 0
for j in range(ncols):
g_l += L[i][j] // my_monomials[j](X, Y, Z) * my_monomials[j]
reduced_polynomials.append(g_l)
# eliminate z
my_ideal_list = [y * z - N] + reduced_polynomials
# Variety
my_ideal_list = [Hi.change_ring(QQ) for Hi in my_ideal_list]
for i in range(len(my_ideal_list),3,-1):
print(i)
V = Ideal(my_ideal_list[:i]).variety(ring=ZZ)
print(V)
参考:
New Attacks on RSA with Small Secret CRT-Exponents
DP DQ Leaking Attack
首先需要知道,\(dp\)与\(dq\)是用来加速解密的,其中\(dp = d\:mod\:(p-1),dq = d\:mod\:(q-1)\).
他们对加密过程没有影响.下面给出解释
对于解密过程我们有\(m = c^{d}\: mod \:N\),其中 \(N=p q\),并且\(p,q\)都为素数.那么我们有
我们设 \(d = k\varphi(p) + d\:mod\varphi(p)\) ,那么我们得到
由欧拉定理 \(c^{\varphi (p)} \equiv 1 \text { mod } p\) 可得
所以我们设 \(d_{p} = d\bmod (p - 1), d_{q} = d\bmod (q- 1)\),得到
再由加纳公式(\(Garner's formula\)) 得到
即可得到明文\(m\)
python代码:
def RSA_dpdq(p, q, c, dp, dq):
# 原来计算的是pow(c,d,n )
# 相比于pow(c, d >> dp, n >> p)
m1 = pow(c, dp, p)
m2 = pow(c, dq, q)
qinv = inverse(q, p)
h = (qinv * (m1 - m2)) % p
m = m2 + h * q
print(long_to_bytes(m))
Coppersmith 相关攻击
基本原理
Coppersmith 相关攻击与Don Coppersmith 紧密相关,他提出了一种针对于模多项式(单变量,二元变量,甚至多元变量)找所有小整数根的多项式时间的方法。
这里我们以单变量为主进行介绍,假设
- 模数为 N ,N 具有一个因子 \(b\geq N^{\beta},0< \beta \leq 1\)
- 多项式 F 的次数为 \(\delta\)
那么该方法可以在\(O(c\delta^5log^9(N))\) 的复杂度内找到该多项式所有的根\(x_0\),这里我们要求 \(|x_0|<cN^{\frac{\beta^2}{\delta}}\) 。
在这个问题中,我们的目标是找到在模 N 意义下多项式所有的根,这一问题被认为是复杂的。Coppersmith method 主要是通过 Lenstra–Lenstra–Lovász lattice basis reduction algorithm(LLL)方法找到
- 与该多项式具有相同根 \(x_0\)
- 更小系数
- 定义域为整数域
的多项式 g,由于在整数域上找多项式的根是简单的(Berlekamp–Zassenhaus),从而我们就得到了原多项式在模意义下的整数根。
那么问题的关键就是如何将 f 转换到 g 呢?Howgrave-Graham 给出了一种思路
也就是说我们需要找到一个具有“更小系数”的多项式 g,也就是下面的转换方式
在 LLL 算法中,有两点是非常有用的
- 只对原来的基向量进行整数线性变换,这可以使得我们在得到 g 时,仍然以原来的 \(x_0\) 为根。
- 生成的新的基向量的模长是有界的,这可以使得我们利用 Howgrave-Graham 定理。
在这样的基础之上,我们再构造出多项式族 g 就可以了。
需要注意的是,由于 Coppersmith 根的约束,在 RSA 中的应用时,往往只适用于 e 较小的情况。
Coppersmith攻击(已知p的高位攻击)
知道$ p$ 的高位为$ p$ 的位数的约\(\frac12\)时即可。
#Sage
from sage.all import *
n =
p4 =
#p去0的剩余位
e =
pbits = 1024
kbits = pbits - p4.nbits()
print(p4.nbits())
p4 = p4 << kbits
PR.<x> = PolynomialRing(Zmod(n))
f = x + p4
roots = f.small_roots(X=2^kbits, beta=0.4)
#经过以上一些函数处理后,n和p已经被转化为10进制
if roots:
p = p4+int(roots[0])
print("n: "+str(n))
print("p: "+str(p))
print("q: "+str(n//p))
参考 http://ohroot.com/2016/07/11/rsa-in-ctf。
题目
- 2017 WHCTF OldDriver
- 2018 N1CTF easy_fs
Coppersmith攻击(已知m的高位攻击)
这里我们假设我们首先加密了消息 m,如下
并且我们假设我们知道消息$ m$ 的很大的一部分 \(m_0\),即 \(m=m_0+x\),但是我们不知道 \(x\)。那么我们就有可能通过该方法进行恢复消息。这里我们不知道的 x 其实就是多项式的根,需要满足 $Coppersmith $的约束。
可以参考 https://github.com/mimoo/RSA-and-LLL-attacks 。
$e \(足够小,且部分明文泄露时,可以采用\)Coppersmith$单变量模等式的攻击,如下:
\(c=m^{e}\bmod n=(mbar+x_{0})^{e}\bmod n\),其中$ mbar = (m >> k\text{bits}) << k\text{bits}$
当$ \vert x_{0}\vert\leq N^{\frac{1}{e}}$ 时,可以在$ \log N \(和\) e \(的多项式时间内求出\) x_0$。
#Sage
n =
e =
c =
mbar =
kbits =
beta = 1
nbits = n.nbits()
print("upper {} bits of {} bits is given".format(nbits - kbits, nbits))
PR.<x> = PolynomialRing(Zmod(n))
f = (mbar + x)^e - c
x0 = f.small_roots(X=2^kbits, beta=1)[0] # find root < 2^kbits with factor = n
print("m:", mbar + x0)
Broadcast Attack with Linear Padding
对于具有线性填充的情况下,仍然可以攻击,这时候就会使用 Coppersmith method 的方法了,这里暂不介绍。可以参考
相关消息
攻击条件
当 Alice 使用同一公钥对两个具有某种线性关系的消息 M1 与 M2 进行加密,并将加密后的消息 C1,C2 发送给了 Bob 时,我们就可能可以获得对应的消息 M1 与 M2。这里我们假设模数为 N,两者之间的线性关系如下
其中 f 为一个线性函数,比如说 \(f=ax+b\)。
在具有较小错误概率下的情况下,其复杂度为 \(O(elog^2N)\)。
这一攻击由 Franklin,Reiter 提出。
攻击原理
首先,我们知道 \(C_1 \equiv M_1 ^e \bmod N\),并且 \(M_1 \equiv f(M_2) \bmod N\),那么我们可以知道 \(M_2\) 是 \(f(x)^e \equiv C_1 \bmod N\) 的一个解,即它是方程 \(f(x)^e-C_1\) 在模 N 意义下的一个根。同样的,\(M_2\) 是 \(x^e - C_2\) 在模 N 意义下的一个根。所以说 \(x-M_2\) 同时整除以上两个多项式。因此,我们可以求得两个多项式的最大公因子,如果最大公因子恰好是线性的话,那么我们就求得了 \(M_2\)。需要注意的是,在 \(e=3\) 的情况下,最大公因子一定是线性的。
这里我们关注一下 \(e=3\),且 \(f(x)=ax+b\) 的情况。首先我们有
那么我们有
我们需要明确一下我们想要得到的是消息 m,所以需要将其单独构造出来。
首先,我们有式 1
再者我们构造如下式 2
根据式 1 我们有
继而我们有式 3
那么我们根据式 2 与式 3 可得
进而我们有
进而
进而
上面的式子中右边所有的内容都是已知的内容,所以我们可以直接获取对应的消息。
有兴趣的可以进一步阅读 A New Related Message Attack on RSA 以及 paper 这里暂不做过多的讲解。
SCTF RSA3 中的 level3
#脚本1
import gmpy2
id1 = 1002
id2 = 2614
c1 =
c2 =
n =
a = 1
b = id1 - id2
def getmessage(a, b, c1, c2, n):
b3 = gmpy2.powmod(b, 3, n)
part1 = b * (c1 + 2 * c2 - b3) % n
part2 = a * (c1 - c2 + 2 * b3) % n
part2 = gmpy2.invert(part2, n)
return part1 * part2 % n
message = getmessage(a, b, c1, c2, n) - id2
message = hex(message)[2:]
if len(message) % 2 != 0:
message = '0' + message
print (message.decode('hex'))
#脚本2
#sage
import binascii
def attack(c1, c2, b, e, n):
PR.<x>=PolynomialRing(Zmod(n))
g1 = x^e - c1
g2 = (x+b)^e - c2
def gcd(g1, g2):
while g2:
g1, g2 = g2, g1 % g2
return g1.monic()
return -gcd(g1, g2)[0]
c1 =
c2 =
n =
e=3
a = 1
id1 = 1002
id2 = 2614
b = id2 - id1
m1 = attack(c1,c2, b,e,n)
print (binascii.unhexlify("%x" % int(m1 - id1)))
Coppersmith’s short-pad attack
攻击条件
目前在大部分消息加密之前都会进行 padding,但是如果 padding 的长度过短,也有可能被很容易地攻击。\(m \in (0,\lfloor\frac{n.n\text{bits}()}{e^2}\rfloor]\)
这里所谓 padding 过短,其实就是对应的多项式的根会过小。
攻击原理
我们假设爱丽丝要给鲍勃发送消息,首先爱丽丝对要加密的消息 M 进行随机 padding,然后加密得到密文 C1,发送给鲍勃。这时,中间人皮特截获了密文。一段时间后,爱丽丝没有收到鲍勃的回复,再次对要加密的消息 M 进行随机 padding,然后加密得到密文 C2,发送给 Bob。皮特再一次截获。这时,皮特就可能可以利用如下原理解密。
这里我们假设模数 N 的长度为 k,并且 padding 的长度为 \(m=\lfloor \frac{k}{e^2} \rfloor\)。此外,假设要加密的消息的长度最多为 k-m 比特,padding 的方式如下
消息 M2 的 padding 方式类似。
那么我们可以利用如下的方式来解密。
首先定义
其中 \(y=r_2-r_1\)。显然这两个方程具有相同的根 M1。然后还有一系列的推导。
#脚本1
#Sage
import binascii
def attack(c1, c2, n, e):
PR.<x>=PolynomialRing(Zmod(n))
# replace a,b,c,d
g1 = (a*x+b)^e - c1
g2 = (c*x+d)^e - c2
def gcd(g1, g2):
while g2:
g1, g2 = g2, g1 % g2
return g1.monic()
return -gcd(g1, g2)[0]
c1 =
c2 =
n =
e =
m1 = attack(c1, c2, n, e)
print(binascii.unhexlify("%x" % int(m1)))
#脚本2
#Sage
def short_pad_attack(c1, c2, e, n):
PRxy.<x,y> = PolynomialRing(Zmod(n))
PRx.<xn> = PolynomialRing(Zmod(n))
PRZZ.<xz,yz> = PolynomialRing(Zmod(n))
g1 = x^e - c1
g2 = (x+y)^e - c2
q1 = g1.change_ring(PRZZ)
q2 = g2.change_ring(PRZZ)
h = q2.resultant(q1)
h = h.univariate_polynomial()
h = h.change_ring(PRx).subs(y=xn)
h = h.monic()
kbits = n.nbits()//(2*e*e)
diff = h.small_roots(X=2^kbits, beta=0.4)[0] # find root < 2^kbits with factor >= n^0.4
return diff
def related_message_attack(c1, c2, diff, e, n):
PRx.<x> = PolynomialRing(Zmod(n))
g1 = x^e - c1
g2 = (x+diff)^e - c2
def gcd(g1, g2):
while g2:
g1, g2 = g2, g1 % g2
return g1.monic()
return -gcd(g1, g2)[0]
if __name__ == '__main__':
n =
e =
c1 =
c2 =
diff = short_pad_attack(c1, c2, e, n)
print("difference of two messages is %d" % diff)
m1 = related_message_attack(c1, c2, diff, e, n)
print("m1:", m1)
print("m2:", m1 + diff)
- 变种1: \(\quad c_i=\left(a_i m+b_i\right)^e\left(\bmod n_i\right)\)
用CRT计算系数 \(T_i\) 使得 \(T_i \equiv 1\left(\bmod n_i\right), T_i \equiv 0\left(\bmod n_{j \neq i}\right)\) ,
则可建立多项式为 \(f(x)=\sum_i T_i\left[\left(a_i x+b_i\right)^e-c_i\right]\) ,符合 \(f(m) \equiv 0\left(\bmod n_i\right)\),
故 \(m\) 是 \(f(m) \equiv 0(\bmod N)\) 的一个根,其中 \(N=\prod_i n_i\) 。
如果 \(m<\left(\prod_i n_i\right)^{1 / e}=M^{1 / \operatorname{deg} f(x)}\) ,可以使用coppersmith找出 \(m\) 。
参考:
Security Fest 2022 - really_sick_æsthetic
PlaidCTF 2017 - Multicast
CakeCTF 2021 - Party Ticket
已知m高位
攻击条件
这里我们假设我们首先加密了消息 m,如下
并且我们假设我们知道消息 m 的很大的一部分 \(m_0\),即 \(m=m_0+x\),但是我们不知道 \(x\)。那么我们就有可能通过该方法进行恢复消息。这里我们不知道的 x 其实就是多项式的根,需要满足 Coppersmith 的约束。
可以参考 https://github.com/mimoo/RSA-and-LLL-attacks 。
\(e\) 足够小,且部分明文泄露时,可以采用\(Coppersmith\)单变量模等式的攻击,如下:
当$ \vert x_{0}\vert\leq N^{\frac{1}{e}}$ 时,可以在$ \log N \(和\) e \(的多项式时间内求出\) x_0$。
这里给出了\(m\)的高400位,我们只需要推断剩余的72位。记真实的 \(m\) 为\(highM +a\),则
这个方程的根很小,可以直接求解。
#脚本1
#Sage
n =
e =
c =
mbar =
kbits =
beta = 1
nbits = n.nbits()
print("upper {} bits of {} bits is given".format(nbits - kbits, nbits))
PR.<x> = PolynomialRing(Zmod(n))
f = (mbar + x)^e - c
x0 = f.small_roots(X=2^kbits, beta=1)[0] # find root < 2^kbits with factor = n
print("m:", mbar + x0)
#脚本2
#sage
def phase2(high m,n, c):
R.<x> = PolynomialRing(Zmod(n),implementation='NTL')
m = high m + x
M = m((m^3 - c).small roots()[])
print(hex(int(M))T2:1)
n =
c =
high_m =
phase2(high_m, n, c)
已知p高位
攻击条件
当我们知道一个公钥中模数 N 的一个因子的较高位时,我们就有一定几率来分解 N。
题目给出p的高位,该后门算法依赖于Coppersmith partial information attack算法, sage实现该算法
在模\(n\)的某个因数下的根。我们设\(p=pHigh+x\), 然后拿去求解方程
得到\(p\)之后即可推出私钥
#脚本1
def phase3(high_p, n, c):
R.<x> = PolynomialRing(Zmod(n), implementation='NTL')
p = high_p + x
x0 = p.small_roots(X = 2^128, beta = 0.1)[0]
P = int(p(x0))
Q = n // P
assert n == P*Q
d = inverse_mod(65537, (P-1)*(Q-1))
print(hex(power_mod(c, d, n)))
n =
c =
high_p =
phase3(high_p, n, c)
#脚本2
#Sage
from sage.all import *
n =
p4 =
#p去0的剩余位
e =
pbits = 1024
kbits = pbits - p4.nbits()
print(p4.nbits())
p4 = p4 << kbits
PR.<x> = PolynomialRing(Zmod(n))
f = x + p4
roots = f.small_roots(X=2^kbits, beta=0.4)
#经过以上一些函数处理后,n和p已经被转化为10进制
if roots:
p = p4+int(roots[0])
print("n: "+str(n))
print("p: "+str(p))
print("q: "+str(n//p))
已知d低位
既然已知 \(d\) 的低位, 也就是已知 \(d\) 在模 \(2^{512}\) 意义下的值, 又有 \(e=3\), 我们考虑等式
两边对 \(2^{512}\) 取模,有
以 \(\frac{n}{p}\) 代替 \(q\), 使上面的方程成为单变量的:
这个方程是模意义下的一元二次方程,是可解的。解出来之后得到了 \(p\) 的低位,通过与 phase 3 类似的方式可以得到 \(p, q\).
#脚本1
def getFullP(low_p, n):
R.<x> = PolynomialRing(Zmod(n), implementation='NTL')
p = x*2^512 + low_p
root = (p-n).monic().small_roots(X = 2^128, beta = 0.4)
if root:
return p(root[0])
return None
def phase4(low_d, n, c):
maybe_p = []
for k in range(1, 4):
p = var('p')
p0 = solve_mod([3*p*low_d == p + k*(n*p - p^2 - n + p)], 2^512)
maybe_p += [int(x[0]) for x in p0]
print(maybe_p)
for x in maybe_p:
P = getFullP(x, n)
if P: break
P = int(P)
Q = n // P
assert P*Q == n
d = inverse_mod(3, (P-1)*(Q-1))
print(hex(power_mod(c, d, n))[2:])
n =
c =
low_d =
phase4(low_d, n, c)
#脚本2
#Sage
def partial_p(p0, kbits, n):
PR.<x> = PolynomialRing(Zmod(n))
nbits = n.nbits()
f = 2^kbits*x + p0
f = f.monic()
roots = f.small_roots(X=2^(nbits//2-kbits), beta=0.4) # find root < 2^(nbits//2-kbits) with factor >= n^0.4
if roots:
x0 = roots[0]
p = gcd(2^kbits*x0 + p0, n)
return ZZ(p)
def find_p(d0, kbits, e, n):
X = var('X')
for k in range(1, e+1):
results = solve_mod([e*d0*X - k*X*(n-X+1) + k*n == X], 2^kbits)
for x in results:
p0 = ZZ(x[0])
p = partial_p(p0, kbits, n)
if p and p != 1:
return p
if __name__ == '__main__':
n =
e =
c =
d0 =
beta = 0.5
nbits = n.nbits()
kbits = d0.nbits()
print("lower %d bits (of %d bits) is given" % (kbits, nbits))
p = int(find_p(d0, kbits, e, n))
print("found p: %d" % p)
q = n//int(p)
print("d:", inverse_mod(e, (p-1)*(q-1)))
- 变种1: \(n=p \cdot q \cdot r\) ,已知 \(n, p, d=\operatorname{inv}(e, \varphi(n)), e, c\)
联立可得, \(\left(e d_0-1-k^{\prime} n^{\prime}-k^{\prime}\right) q+k^{\prime} q^2+k^{\prime} n^{\prime} \equiv 0\left(\bmod 2^{d_0 . n \mathrm{bits}()}\right)\)
即求解同余方程可得 \(q\) 的低 size ( \(d 0)\) 位,本来是个partial d的coppersmith 问题,但因为step1求解同余方程后得到的 \(q\) 已是完整的 \(q\) ,所以无需后续的 coppersmith 。
参考: Dragon CTF 2019 - RSA Chained
#Sage
def find_p(d0, kbits, e, n, p):
X = var('X')
for k in range(1, e + 1):
k_dot = k * (p - 1)
results = solve_mod([e * d0 * X - k_dot * X * (n - X + 1) + k_dot * n == X], 2^kbits)
for x in results:
q = ZZ(x[0])
if n % q == 0:
return q
return None
n = # q * r
p =
c =
d0 =
e =
kbits = d0.nbits()
q = find_p(d0, kbits, e, n, p)
phi = (p - 1) * (q - 1) * (n // q - 1)
d = inverse_mod(e, phi)
print(bytes.fromhex(hex(pow(c, d, p * n))[2:]))
已知N一个因子的高位,部分p
当我们知道一个公钥中模数 $N \(的一个因子的较高位时,我们就有一定几率来分解\) N$。
参考 https://github.com/mimoo/RSA-and-LLL-attacks 。
beta = 0.5
dd = f.degree()
epsilon = beta / 7
mm = ceil(beta**2 / (dd * epsilon))
tt = floor(dd * mm * ((1/beta) - 1))
XX = ceil(N**((beta**2/dd) - epsilon)) + 1000000000000000000000000000000000
roots = coppersmith_howgrave_univariate(f, N, beta, mm, tt, XX)
其中,
- 必须满足 \(q \geq N^{b e t a}\) ,所以这里给出了 beta \(=0.5\) ,显然两个因数中必然有一 个是大于的。
- \(\mathrm{XX}\) 是 \(f(x)=q^{\prime}+x\) 在模 \(q\) 意义下的根的上界,自然我们可以选择调整它, 这里其实也表明了我们已知的 \(q^{\prime}\) 与因数 \(q\) 之间可能的差距
#Sage
n =
e =
c =
pbar =
kbits =
print("upper %d bits (of %d bits) is given" % (pbar.nbits()-kbits, pbar.nbits()))
PR.<x> = PolynomialRing(Zmod(n))
f = x + pbar
x0 = f.small_roots(X=2^kbits, beta=0.4)[0] # find root < 2^kbits with factor >= n^0.4
p = x0 + pbar
print("p:", p)
q = n // int(p)
d = inverse_mod(e, (p-1)*(q-1))
print("m:", pow(c, d, n))
Boneh and Durfee attack
攻击条件
\(e\)非常大接近于\(N\),当 $d $较小时,满足 \(d < N^{0.292}\) 时,我们可以利用该攻击,比 Wiener's Attack 要强一些。
攻击原理
首先
进而有
即
又
所以
假设 \(A=\frac{N+1}{2}\),\(y=\frac{-p-q}{2}\) ,原式可化为
其中
\(|k|<\frac{2ed}{\varphi(N)}<\frac{3ed}{N}=3*\frac{e}{N}*d<3*\frac{e}{N}*N^{delta}\)
\(|y|<2*N^{0.5}\)
\(y\) 的估计用到了$ p、q$ 比较均匀的假设。这里$ delta$ 为预估的小于 0.292 的值。
如果我们求得了该二元方程的根,那么我们自然也就可以解一元二次方程 \(N=pq,p+q=-2y\) 来得到$ p$ 与$ q$。
更加具体的推导,参考 New Results on the Cryptanalysis of Low Exponent RSA.
攻击工具
请参考 https://github.com/mimoo/RSA-and-LLL-attacks 。上面有使用教程。
2015 PlaidCTF Curious
首先题目给了N,e,c。简单看一下可以发现 e 比较大。考虑使用 Wiener's Attack,使用更强的目前介绍的攻击。
代码如下:
nlist = list()
elist = list()
clist = list()
with open('captured') as f:
# read the line {N : e : c} and do nothing with it
f.readline()
for i in f.readlines():
(N, e, c) = i[1:-2].split(" : ")
nlist.append(long(N,16))
elist.append(long(e,16))
clist.append(long(c,16))
for i in range(len(nlist)):
print 'index i'
n = nlist[i]
e = elist[i]
c = clist[i]
d = solve(n,e)
if d==0:
continue
else:
m = power_mod(c, d, n)
hex_string = "%x" % m
import binascii
print "the plaintext:", binascii.unhexlify(hex_string)
return
结果如下
=== solution found ===
private key found: 23974584842546960047080386914966001070087596246662608796022581200084145416583
the plaintext: flag_S0Y0UKN0WW13N3R$4TT4CK!
-
变种1:\(e\) 很大,\(dp\) 很小,且$ d>2N^{\beta}$。
May’s Attack
假设$ e<\varphi(N),q \le N^{\beta}\(,\)\beta \le \frac{1}{2}$,因 \(ed_p \equiv 1 \pmod {p-1}\),有 \(ed_p=1+k(p-1)\),
对于$ k \in \mathbb{N}\(,有\) ed_p=(k-1)(p-1)+p\(,即\) ed_pq=(k-1)(N-q)+N$。
设$ x,y \(为参数,则多项式\) f(x,y)=x(N-y)+N \(在模 e 下存在根\) (x_0,y_0)=(k-1,q)$,用coppersmith attack可解。
RSA Hastad Attack with non-linear padding and different public keys(带非线性padding和不同公钥的广播攻击)
适用情况:m 经 k 次非线性padding处理后,分别用 k 组 (N_i,e_i) 加密后得 k 组 c_i。
参考:2020年羊城杯 - Invitation
#Sage
#e=3, padding: m²+(3^431)k
def linearPaddingHastads(cArray,nArray,aArray,bArray,eArray,eps):
if(len(cArray) == len(nArray) == len(aArray) == len(bArray) == len(eArray)):
for i in range(4):
cArray[i] = Integer(cArray[i])
nArray[i] = Integer(nArray[i])
aArray[i] = Integer(aArray[i])
bArray[i] = Integer(bArray[i])
eArray[i] = Integer(eArray[i])
TArray = [-1]*4
for i in range(4):
arrayToCRT = [0]*4
arrayToCRT[i] = 1
TArray[i] = crt(arrayToCRT,nArray)
P.<x> = PolynomialRing(Zmod(prod(nArray)))
gArray = [-1]*4
for i in range(4):
gArray[i] = TArray[i]*(pow(aArray[i]*x**2 + bArray[i],eArray[i]) - cArray[i])
g = sum(gArray)
g = g.monic()
roots = g.small_roots(epsilon=eps)
if(len(roots)== 0):
print("No Solutions found!")
return -1
return roots
else:
print("Input error!")
def nonLinearPadding():
eArr = [3 for i in range(4)]
nArr = []
cArr = []
aArr = [1 for i in range(4)]
bArr = [i * 3 ** 431 for i in [3,8,10,11]]
msg = linearPaddingHastads(cArr,nArr,aArr,bArr,eArr,eps=1/20)
for i in msg:
print(bytes.fromhex(hex(i)[2:]))
if __name__ == '__main__':
nonLinearPadding()
RSA 选择明密文攻击
选择明文攻击
这里给出一个例子,假如我们有一个加密 oracle,但是我们不知道 \(\mathrm{n}\) 和 \(\mathrm{e}\) ,那
- 我们可以通过加密 oracle 获取 \(n\) 。
- 在 \(\mathrm{e}\) 比较小 \(\left(e<2^{64}\right)\) 时,我们可以利用 Pollard's kangaroo algorithm 算法获取 \(\mathrm{e}\) 。这一点比较显然。
我们可以加密 2,4,8,16。那么我们可以知道
那么
故而
我们可以求出 \(k n\) 和 \(n\) 的最大公因数,很大概率就是 \(n\) 了。我们还可以构造更多的例子从来更加确定性地找 \(n\) 。
import gmpy2
def get_n():
nset = []
c2 = server_encode(2)
c4 = server_encode(4)
c8 = server_encode(8)
nset.append(c2 * c2 - c4)
nset.append(c2 * c2 * c2 - c8)
c3 = server_encode(3)
c9 = server_encode(9)
c27 = server_encode(27)
nset.append(c3 * c3 - c9)
nset.append(c3 * c3 * c3 - c27)
c5 = server_encode(5)
c25 = server_encode(25)
c125 = server_encode(125)
nset.append(c5 * c5 - c25)
nset.append(c5 * c5 * c5 - c125)
n = nset[0]
for x in nset:
n = gmpy2.gcd(x, n)
while n % 2 == 0:
n //= 2
while n % 3 == 0:
n //= 3
while n % 5 == 0:
n //= 5
print('n =', n)
return n
任意密文解密
假设爱丽丝创建了密文 \(C=P^e \bmod n\) 并且把 C 发送给鲍勃,同时假设我们要对爰丽伾加密后的任意密文解密,而不是只解 密 \(C\) ,那么我们可以拦截 \(C\) ,并运用下列步㡜求出 \(P\) :
- 选择任意的 \(X \in Z_n^*\) ,即 \(\mathrm{X}\) 与 \(N\) 互素
- 计算 \(Y=C \times X^e \bmod n\)
- 由于我们可以进行选择密文攻击,那么我们求得 \(Y\) 对应的解密结果 \(Z=Y^d\)
- 那么,由于 \(Z=Y^d=\left(C \times X^e\right)^d=C^d X=P^{e d} X=P X \bmod n\) ,由于 \(\mathrm{X}\) 与 \(\mathrm{N}\) 互素,我们很容易求得相应的逆 元,进而可以得到 \(P\)
from Crypto.Util.number import *
def get_M():
X = getPrime(5)
Y = (c * (X ** e)) % n
Z = server_decode(Y)
i = 0
while True:
M = (n * i + Z) // X
if 'flag' in long_to_bytes(M):
print(long_to_bytes(M))
break
RSA parity oracle
LSB Oracle Attack(Least Significant Bit Oracle Attack )
假设目前存在一个 Oracle,它会对一个给定的密文进行解密,并且会检查解密的明文的奇偶性, 并根据奇偶性返回相应的值,比如 1 表示奇数, 0 表示偶数。那么给定一个加密后的密文,我们只 需要 \(\log (\mathrm{N})\) 次就可以知道这个密文对应的明文消息。
原理
假设
\(C=P^e \bmod N\)
第一次时,我们可以给服务器发送
\(C * 2^e=(2 P)^e \bmod N\)
服务器会计算得到
\(2 P \bmod N\)
这里
-
\(2 \mathrm{P}\) 是偶数,它的幂次也是偶数。
-
\(N\) 是奇数,因为它是由两个大素数相乘得到。
那么 -
服务器返回奇数,即 \(2 P \bmod N\) 为奇数,则说明 \(2 P\) 大于 \(N\) ,且减去了奇数个 \(N\) ,又因为 \(2 P<2 N\) ,因此减去了一个 \(N\) ,即 \(\frac{N}{2} \leq P<N\) ,我们还可以考虑向下取整。
-
服务器返回偶数,则说明 \(2 \mathrm{P}\) 小于 \(N\) 。即 \(0 \leq P<\frac{N}{2}\) ,我们还可以向下取整。
这里我们使用数学归纳法,即假设在第 \(\mathrm{i}\) 次时, \(\frac{x N}{2^i} \leq P<\frac{x N+N}{2^i}\)
进一步,在第 \(i+1\) 次时,我们可以发送
\(C * 2^{(i+1) e}\)
服务器会计算得到
\(2^{i+1} P \bmod N=2^{i+1} P-k N\)\(0 \leq 2^{i+1} P-k N<N\)
\(\frac{k N}{2^{i+1}} \leq P<\frac{k N+N}{2^{i+1}}\)
根据第 \(\mathrm{i}\) 次的结果
那么
- 服务㗊返回奇数,则 \(k\) 必然是一个奇数, \(k=2 y+1\) ,那么 \(\frac{2 y N+N}{2^{i+1}} \leq P<\frac{2 y N+2 N}{2^{i+1}}\) 。与此同时,由于 \(\mathrm{P}\) 必然存 在,所以第 \(i+1\) 得到的这个范围和第 \(i\) 次得到的范围必然存在交集。所以 \(\mathrm{y}\) 必然与 \(x\) 相等。
- 服务器返回偶数,则 \(\mathrm{k}\) 必然是一个偶数, \(k=2 \mathrm{y}\) ,此时 \(\mathrm{y}\) 必然也与 \(\mathrm{x}\) 相等,那么 \(\angle \frac{2 x N}{2^{i+1}} \leq P<\frac{2 x N+N}{2^{i+1}}\)
进一步我们可以这么归纳
lb = 0
ub = N
if server returns 1
lb = (lb+ub)/2
else:
ub = (lb+ub)/2
这里虽然是整除, 即下取整,但是无所谓我们在最初时已经分析了这个问题。
由于此处有大量整除运算,所以最好用 decimal
库进行精确计算,否则最后结果很可能会出错。decimal.getcontext().prec
用来设定精度。
from Crypto.Util.number import *
import decimal
def get_flag():
k = n.bit_length()
decimal.getcontext().prec = k
L = decimal.Decimal(0)
R = decimal.Decimal(int(n))
for i in range(k):
c = (c * pow(2, e, n)) % n
recv = server_decode(c)
if recv == 1:
L = (L + R) // 2
else:
R = (L + R) // 2
print(long_to_bytes(int((R))))
更多信息可参考:RSA Least-Significant-Bit Oracle Attack 和 RSA least significant bit oracle attack 。
import decimal
def oracle():
return lsb == 'odd'
def partial(c, e, n):
k = n.bit_length()
decimal.getcontext().prec = k # for 'precise enough' floats
lo = decimal.Decimal(0)
hi = decimal.Decimal(n)
for i in range(k):
if not oracle(c):
hi = (lo + hi) // 2
else:
lo = (lo + hi) // 2
c = (c * pow(2, e, n)) % n
# print i, int(hi - lo)
return int(hi)
MSB Oracle Attack(Most Significant Bit Oracle Attack )
适用情况:可以选择密文并泄露明文的最高位 (奇偶性)。
假设远程提供一个解密服务,但是只返回明文的最高字节,并且明文的形式是64字 节,高位用 \(\backslash x 00\) 填充。
将加密的内容拿去解密会得到 \(m\) ,但是回显最高位是 \(\backslash x 00\) ,构造密文 \(c \cdot 2^e\) , 解密会得到 \(2 m \bmod n\) ,
不断构造密文 \(c \cdot 2^{i e}\) ,当最高位不是 \(\backslash \times 00\) 时,记录下值为 \(x\) ,则说明 \(m x>2^{\mathrm{kbit}}\)
然后再相应缩小 \(x\) ,可以利用二分法,比如当第一次拿到 \(m x>2^{\mathrm{kbit}}\) ,那么有 \(m\left(\frac{x}{2}\right)<2^{\mathrm{kbit}}\) ,因此新的 \(x^{\prime}\) 必定满足 \(\frac{x}{2}<x^{\prime}<x\) ,接着尝试 \(\frac{x+\frac{x}{2}}{2}\) 就好。
最終可以找到一个 \(X\) ,满足 \(m X<2^{\mathrm{kbit}}\) 且 \(m(X+1)>2^{\mathrm{kbit}}\) ,由于是整除, 所以会有误差,最后的 \(m\) 在 \(\frac{2^{\mathrm{kbit}}}{x}\) 附近。
参考: Pwnhub - pkcs4
RSA Byte Oracle
假设目前存在一个 Oracle,它会对一个给定的密文进行解密,并且会给出明文的最后一个字节。那么给定一个加密 后的密文,我们只需要 \(\log _{256} n\) 次就可以知道这个密文对应的明文消息。
原理
这个其实算作 RSA parity Oracle 的扩展,既然可以泄露出最后一个字节,那么按道理我们获取密文对应明文的次数 应该可以咸少。
假设
\(C=P^e \bmod N\)
第一次时,我们可以给服务器发送
\(C * 256^e=(256 P)^e \bmod N\)
服务器会计算得到
\(256 P \bmod N\)
这里
-
\(256 \mathrm{P}\) 是偶数。
-
\(N\) 是奇数,因为它是由两个大拜数相乘得到。
由于 \(\mathrm{P}\) 一般是小于 \(\mathrm{N}\) 的,那么 $ 256P ; mod ; N=256P -k n, k<256$ 。而且对于两个不同的 \(k_1, k_2\) ,我们有 \(256 P-k_1 n \not \equiv 256 P-k_2 n \bmod 256\)
我们可以利用反证法来证明上述不等式。同时 \(256 P-k n\) 的最后一个字节其实就是 \(-k n\) 在模 256 的情况下获取的。那么,其实我们可以首先枚举出 0~255 情况下的最后一个字节,构造一个 \(k\) 和最后一个字节的映射表 map
当服务器返回最后一个字节 \(b\) ,那么我们可以根据上述构造的映射表得知 \(k\) ,即减去了 \(k\) 个 \(N\) ,即 \(k N \leq 256 P \leq(k+1) N\) 。
此后,我们使用数学归纳法来获取 \(\mathrm{P}\) 的范围,即假设在第 \(\mathrm{i}\) 次时, \(\frac{x N}{256^i} \leq P<\frac{x N+N}{256^i}\)
进一步,在第 \(i+1\) 次时,我们可以发送
服务器会计算得到
根据第 \(\mathrm{i}\) 次的结果
我们这里可以假设 \(k=256 y+t\) ,而这里的 \(\mathrm{t}\) 就是我们可以通过映射表获取的。
与此同时,由于 \(P\) 必然存在,所以第 \(\mathrm{i}+1\) 得到的这个范围和第 \(\mathrm{i}\) 次得到的范围必然存在交集。
所以 \(\mathrm{y}\) 必然与 \(x\) 相等。
假设服务器返回了 b,那么可以归纳为:
L = 0
R = 1
for i in range(128):
k = mab[b]
L = L + k // 256**(i+1)
R = L + (k+1)// 256**(i+1)
M = L * n
或
k = mab[b]
interval = (ub-lb)/256
lb = lb + interval * k
ub = lb + interval
如果不知道 \(e\) 但服务器提供任意明文加密服务,可以让服务器加密 256 ,得到 \(256^e \bmod n_{\text {。 }}\)
由于有大量除法运算,为保证精度,将中间过程用 Fraction 库保存为分数。
最后一字节的数据不准确要减掉,从服务器返回精确的最后一字节数据。
其实只要求出 \(L\) 下限即可,无需求出 \(R\) 上限。
from Crypto.Util.number import *
from fractions import Fraction
def get_flag():
map = {}
for i in range(0, 256):
map[-n * i % 256] = i
cipher256 = server_encode(256)
backup = c
L = Fraction(0, 1)
R = Fraction(1, 1)
for i in range(128):
c = c * cipher256 % n
b = server_decode(c)
k = map[b]
L, R = L + Fraction(k, 256**(i+1)), L + Fraction(k+1, 256**(i+1))
m = int(L * n)
print(long_to_bytes(m - m % 256 + server_decode(backup)))
2018 HITCON lost key
from pwn import *
import gmpy2
from fractions import Fraction
p = process('./rsa.py')
#p = remote('18.179.251.168', 21700)
#context.log_level = 'debug'
p.recvuntil('Here is the flag!\n')
flagcipher = int(p.recvuntil('\n', drop=True), 16)
def long_to_hex(n):
s = hex(n)[2:].rstrip('L')
if len(s) % 2: s = '0' + s
return s
def send(ch, num):
p.sendlineafter('cmd: ', ch)
p.sendlineafter('input: ', long_to_hex(num))
data = p.recvuntil('\n')
return int(data, 16)
if __name__ == "__main__":
# get n
cipher2 = send('A', 2)
cipher4 = send('A', 4)
nset = []
nset.append(cipher2 * cipher2 - cipher4)
cipher3 = send('A', 3)
cipher9 = send('A', 9)
nset.append(cipher3 * cipher3 - cipher9)
cipher5 = send('A', 5)
cipher25 = send('A', 25)
nset.append(cipher5 * cipher5 - cipher25)
n = nset[0]
for item in nset:
n = gmpy2.gcd(item, n)
# get map between k and return byte
submap = {}
for i in range(0, 256):
submap[-n * i % 256] = i
# get cipher256
cipher256 = send('A', 256)
back = flagcipher
L = Fraction(0, 1)
R = Fraction(1, 1)
for i in range(128):
print i
flagcipher = flagcipher * cipher256 % n
b = send('B', flagcipher)
k = submap[b]
L, R = L + (R - L) * Fraction(k, 256), L + (R - L) * Fraction(k + 1, 256)
low = int(L * n)
print (long_to_hex(low - low % 256 + send('B', back)).decode('hex'))
RSA parity oracle variant
原理
如果 oracle 的参数会在一定时间、运行周期后改变,或者网络不稳定导致会话断开、重置,二分 法就不再适用了,为了减少错误,应当考虑逐位恢复。要恢复明文的第 2 低位,考虑
类似的
我们就可以使用前 \(\mathrm{i}-1\) 位与 oracle 的结果来得到第 \(\mathrm{i}\) 位。注意这里的 \(2^{-1}\) 是 \(2^1\) 模 \(N_1\) 的逆元。所以 对剩下的位,有
其中 \(2^{-i}\) 是 \(2^i\) 模 \(N_i\) 的逆元。
就可以逐步恢复原文所有的位信息了。这样的时间复杂度为 \(O(\operatorname{logm})\) 。
exp:
from Crypto.Util.number import *
mm = bytes_to_long(b'12345678')
l = len(bin(mm)) - 2
def genkey():
while 1:
p = getPrime(128)
q = getPrime(128)
e = getPrime(32)
n = p * q
phi = (p - 1) * (q - 1)
if GCD(e, phi) > 1:
continue
d = inverse(e, phi)
return e, d, n
e, d, n = genkey()
cc = pow(mm, e, n)
f = str(pow(cc, d, n) % 2)
for i in range(1, l):
e, d, n = genkey()
cc = pow(mm, e, n)
ss = inverse(2**i, n)
cs = (cc * pow(ss, e, n)) % n
lb = pow(cs, d, n) % 2
bb = (lb - (int(f, 2) * ss % n)) % 2
f = str(bb) + f
assert(((mm >> i) % 2) == bb)
print(long_to_bytes(int(f, 2)))
参考
- https://crypto.stackexchange.com/questions/11053/rsa-least-significant-bit-oracle-attack
- https://pastebin.com/KnEUSMxp
- https://github.com/ashutosh1206/Crypton
Common Private Exponent(共私钥指数攻击,d相同)
加密用同样的私钥并且私钥比较短,从而导致了加密系统被破解。
假定:
其中, \(N_1<N_2<\cdots<N_r<2 N_1\) 。
构造格:
其中 \(M=\left\lfloor N_r^{\frac{1}{2}}\right\rfloor\) 。
再利用LLL算法进行规约得到 \(\left|b_1\right|=M d\) ,则 \(d=\frac{\left|b_1\right|}{M}\) ,从而解密密文得到明
文。
- 使用条件:
- 参考:
Lattice Based Attack on Common Private Exponent RSA
SCTF 2020 - RSA
#Sage
from gmpy2 import *
e0=
n0=
c0=
e1=
n1=
c1=
e2=
n2=
c2=
M=iroot(int(n2),int(2))[0]
a=[0]*4
a[0]=[M,e0,e1,e2]
a[1]=[0,-n0,0,0]
a[2]=[0,0,-n1,0]
a[3]=[0,0,0,-n2]
Mat = matrix(ZZ,a)
Mat_LLL=Mat.LLL()
d = abs(Mat_LLL[0][0])/M
print(bytes.fromhex(hex(pow(c1,int(d),int(n1)))[2:]))
多组低解密指数攻击
适用情况:2-4组 \(e\) ,且 \(d\) 较小
- 给定2组
且有 \(e d-k \lambda(n)=1\) ,得到 \(e d g-k n=g+k s\) (1)
设 \(e_1\) 对应 \(k_1 , e_2\) 对应 \(k_2\) ,则有 \(k_2 d_1 e 1-k_1 d_2 e_2=k_2-k_1\) 由(1)(2)有:
上述等式组也可表示为
对部分参数进行上界估计, \(\mathrm{k}\) 上界近似于 \(d \approx N^{\alpha_2} ,|s|\) 上界 \(\approx N^{1 / 2}, g\) 一般 相对极小因此上面的矩阵表示 \(B A=C\) 中, \(C\) 的每个元的size都近似 \(n^{1+2 \alpha_2}\) ,所以 \(|C| \approx 2 \cdot n^{1+2 \alpha_2}\)
\(B\) 作为格基的格中,最短向量由Minkowski Bounds知
\(\approx \sqrt{4} \operatorname{det}(B)^{1 / 4} \approx 2 \cdot n^{\left(13 / 2+\alpha_2\right) / 4}\)
因此只要满足 \(n^{1+2 \alpha_2}<n^{\left(13 / 2+\alpha_2\right) / 4}\) 即可将问题转化为 SVP \(\left(\alpha_2<\frac{5}{14}\right)\)
from sage.all import *
import gmpy2
N =
e1 =
e2 =
c =
for i in range(1000):
alpha2 = i/1000
M1 = int(gmpy2.mpz(N)**0.5)
M2 = int( gmpy2.mpz(N)**(1+alpha2) )
D = diagonal_matrix(ZZ, [N, M1, M2, 1])
B = Matrix(ZZ, [ [1, -N, 0, N**2],
[0, e1, -e1, -e1*N],
[0, 0, e2, -e2*N],
[0, 0, 0, e1*e2] ]) * D
L = B.LLL()
v = Matrix(ZZ, L[0])
x = v * B**(-1)
phi = (x[0,1]/x[0,0]*e1).floor()
try:
d = inverse_mod( 65537, phi)
m = hex(power_mod(c, d, N))[2:]
if m.startswith('44415343'):
print(i)
print(bytes.fromhex(m))
break
except:
pass
- 给定3组
类似2组情况,其中
其中
from sage.all import *
import gmpy2
N =
e1 =
e2 =
e3 =
c =
for i in range(1000):
alpha2 = i/1000
M1 = int(gmpy2.mpz(N)**(3./2))
M2 = int( gmpy2.mpz(N) )
M3 = int(gmpy2.mpz(N)**(3./2 + alpha2))
M4 = int( gmpy2.mpz(N)**(0.5) )
M5 = int( gmpy2.mpz(N)**(3./2 + alpha2) )
M6 = int( gmpy2.mpz(N)**(1.+alpha2) )
M7 = int( gmpy2.mpz(N)**(1.+alpha2) )
D = diagonal_matrix(ZZ, [M1, M2, M3, M4, M5, M6, M7, 1])
B = Matrix(ZZ, [ [1, -N, 0, N**2, 0, 0, 0, -N**3],
[0, e1, -e1, -e1*N, -e1, 0, e1*N, e1*N**2],
[0, 0, e2, -e2*N, 0, e2*N, 0, e2*N**2],
[0, 0, 0, e1*e2, 0, -e1*e2, -e1*e2, -e1*e2*N],
[0, 0, 0, 0, e3, -e3*N, -e3*N, e3*N**2],
[0, 0, 0, 0, 0, e1*e3, 0, -e1*e3*N],
[0, 0, 0, 0, 0, 0, e2*e3, -e2*e3*N],
[0, 0, 0, 0, 0, 0, 0, e1*e2*e3] ]) * D
L = B.LLL()
v = Matrix(ZZ, L[0])
x = v * B**(-1)
phi_ = (e1*x[0,1]/x[0,0]).floor()
try:
d = inverse_mod( 65537, phi_)
m = hex(power_mod(c, d, N))[2:]
if m.startswith('44415343'):
print(i)
print(bytes.fromhex(m))
break
except:
pass
-
给定更多组
-
参考Paper
Common Modulus Attacks on Small Private Exponent RSA and Some Fast Variants (in Practice)
Extending Wiener’s Attack in the Presence of Many Decrypting Exponents
多项式RSA
在整数RSA原理基础上将多项式代入分析:
在有限域上选取两个不可约多项式 \(g(p), g(q) , g(n)=g(p) \cdot g(q)\) ,计算出 \(g(n)\) 的欧拉函数 \(\varphi(g(n))=\varphi\) ,
选取一个整数 \(e\) 作为公钥, \(e\) 与 \(\varphi\) 是互緟的,那么对于明文 \(g(m)\) ,加密过程为 \(g(m)^e \equiv g(c)(\bmod g(n))\) ,
计算私钥 \(d\) 满足 \(e d \equiv 1(\bmod \varphi)\) ,则 \(g(c)^d \equiv\left(g(m)^e\right)^d \equiv g(m)^{e d} \equiv g(m)^{\varphi+1}(\bmod g(n))\) ,
同样考虑 \(g(n)\) 与 \(g(m)\) 互素,欧拉定理对于多项式亦成立,
得到 \(g(m)^{\varphi+1} \equiv g(m)(\bmod g(n))\) ,所以 \(g(c)^d \equiv g(m)(\bmod g(n))\) 。
显然RSA对于整数的体制可以适用于有限域上的多项式。
太注意:
对于素数 \(x, \varphi(x)=x-1\) ,但是对于不可约多项式 \(g(x), \varphi(g(x))=p^n-1\) 。 (此 \(p\) 为 \(G F(p)\) 的模,此 \(n\) 为多项式最高项次数)
原因:
由欧拉函数定义本身,欧拉函数是小于 \(n\) 的所有与 \(n\) 互质的数的个数。
多项式的欧拉函数则类似,表示不高于 \(g(x)\) 募级的环内所有多项式中,与 \(g(x)\) 无公因式 (非 1 ) 的其他多项式的个数,所以每一个不高于 \(g(x)\) 昌级的环内多项式 (除了 它自己) 均满足此条件。
#脚本1
#Sage
#已知p,n,m^e
p=
P = PolynomialRing(Zmod(p), name = 'x')
x = P.gen()
e =
n =
c =
#分解N
q1, q2 = n.factor()
q1, q2 = q1[0], q2[0]
#求φ,注意求法,
phi = (p**q1.degree() - 1) * (p**q2.degree() - 1)
assert gcd(e, phi) == 1
d = inverse_mod(e, phi)
m = pow(c,d,n)
#取多项式系数
flag = bytes(m.coefficients())
print("Flag: ", flag.decode())
#脚本2
#Sage
#已知p=2,n,e,c
p =
P = PolynomialRing(GF(p), name = 'x')
x = P.gen()
e =
n =
R.<a> = GF(2^2049)
c = []
q1, q2 = n.factor()
q1, q2 = q1[0], q2[0]
phi = (p**q1.degree() - 1) * (p**q2.degree() - 1)
assert gcd(e, phi) == 1
d = inverse_mod(e, phi)
ans = ''
for cc in c:
cc = P(R.fetch_int(cc))
m = pow(cc,d,n)
m = R(P(m)).integer_representation()
print(m)
ans += chr(m)
print(ans)
#Sage
#x.nbits()==2^32
poly = sum(e * x^i for i,e in enumerate(Integer(n).digits(2^32)))
(p, _), (q, _) = poly.factor_list()
p, q = p(x=2^32), q(x=2^32)
参考:
Crypto CTF2020 - Decent RSA
SecurityFest CTF 2022 - small rsa
Weak prime factors ( \(p\) 具线性特征)
适用情况: \(p\) 满足 \(a p=u_0+M_1 u_1+\cdots+M_k u_k\)
先根据 \(n\) 确定 \(M\) 的大小,再根据 \(M\) 选取符合要求的 \(k\) 和 \(c\) ,然后构造一个格如 下:
用LLL算法进行格基规约,将规约后的某个向量作为多项式系数,再对多项式进行 分解,即可完成对 \(n\) 的分解。
- 参考
Factoring RSA moduli with weak prime factors
N1CTF2020 - easyRSA
from tqdm import tqdm
import gmpy2
class success(Exception):
pass
def attack_weak_prime(basenum, exp, n):
m = basenum^exp
k = len(n.str(base=basenum))//(2*exp) + 1
c = gmpy2.iroot(2*k^3, int(2))
# assert c[1] == True
tmp = int(c[0])
try:
for c in tqdm(range(1, tmp)):
amount = 2*k+1
M = Matrix(RationalField(), amount, amount)
for i in range(amount):
M[i, i] = 1
M[i, amount-1] = c*m^(2*k-i)
M[amount-1, amount-1] = -c*n
new_basis = M.LLL(delta=0.75)
for j in range(amount):
last_row = list(new_basis[j])
last_row[-1] = last_row[-1]//(-c)
poly = sum(e * x^(k*2-i) for i,e in enumerate(last_row))
fac = poly.factor_list()
if len(fac) == 2:
p_poly, q_poly = fac
p_coefficient = p_poly[0].list()
q_coefficient = q_poly[0].list()
ap = sum(m^i * j for i,j in enumerate(p_coefficient))
bq = sum(m^i * j for i,j in enumerate(q_coefficient))
p = gcd(ap, n)
q = gcd(bq, n)
if (p*q == n) and (p != 1) and (q != 1):
raise success
except:
print ('n =', n)
print ('p =', p)
print ('q =', q)
print ('p*q == n ?', bool(p*q == n))
if __name__ == '__main__':
print ('[+] Weak Prime Factorization Start!')
print ('-------------------------------------------------------------------------------------------------------------------------------')
basenum, exp = (3, 66)
n =
p多次幂因子
适用情况: \(N=p^r q\)
- 情形1
条件: \((N, e)\) 满足 \(e x-\varphi(N) y=z\) ,其中 \(x\) 与 \(|z|\) 为小参数。 \(f(x)=e x-z \equiv 0\left(\bmod p^{r-1}\right)\)
计算 \(\operatorname{gcd}(e x-z, N)=g\), 则
P.<x> = PolynomialRing(Zmod(n))
f = e * x - b
root = f.monic().small_roots(X=2**672,beta=0.75)[0]
g = gcd(int(e * root - b),n3)
- 情形2
条件: 小 \(\left|d_1-d_2\right| ,\left|d_1-d_2\right|<N^{\frac{r(r-1)}{(r+1)^2}}\)
\(f(x)=e_1 e_2\left(d_1-d_2\right)-\left(e_2-e_1\right) \equiv 0\left(\bmod p^{r-1}\right)\)
等价于 \(g(x)=x-a \equiv 0\left(\bmod p^{r-1}\right)\) ,其中 \(a \equiv\left(e_2-e_1\right)\left(e_1 e_2\right)^{-1}(\bmod N)\) 。
计算 \(\operatorname{gcd}\left(e_1 e_2 x-\left(e_2-e_1\right), N\right)=g\) ,则
P.<x> = PolynomialRing(Zmod(n))
f = e1*e2*x - e1 + e2
root = f.monic().small_roots(X=2**672,beta=0.75)[0]
g = gcd(int(e1*e2*root - e1 + e2),n)
情形3
条件: \(N_1=p_1^r q_1, N_2=p_2^r q_2\) ,小 \(\left|p_1-p_2\right| ,\left|p_1-p_2\right|<\frac{p_1}{2 r q_1 q_2}\) 。
利用 \(\frac{N_2}{N_1}\) 的连分数展开对应的渐进分数逼近 \(\frac{q_2}{q_1}\) 。
cf = continued_fraction(n1/n2)
fracs = cf.convergents()
for xx in tqdm(fracs):
q1 = xx.numerator()
q2 = xx.denominator()
if q1.nbits() in range(511, 513) and q2.nbits() in range(511, 513):
if n1 % q1 == 0:
print(q1)
assert n1 % q1 == 0
p1 = int((n1 // q1)^(1/2))
p2 = int((n2 // q2)^(1/2))
assert p1^2 * q1 == n1
break
参考:
New attacks on RSA with Moduli \(N=p^rq\)
D^3CTF 2022 - d3factor
RSA-CRT
- 错误模攻击 \(\alpha=q \cdot\left(q^{-1} \bmod p\right), \beta=p \cdot\left(p^{-1} \bmod q\right)\) 。
利用错淏模注入技术得到错淏签名 \(\sigma^{\prime}\) ,即 \(\sigma^{\prime}=\left(\sigma_p \cdot \alpha+\sigma_q \cdot \beta\right) \bmod N^{\prime}\) ,
对生成的两种签名 \(\sigma\) 和 \(\sigma^{\prime}\) 使用CRT可计算出 \(v=\left(\sigma_p \cdot \alpha+\sigma_q \cdot \beta\right) \bmod \left(N \cdot N^{\prime}\right)\) 。
针对 \(l \geq 5\) 的编码后消息进行分析,通过计算签名对 \(\left(\sigma, \sigma^{\prime}\right)\) 分解 \(N\) 的攻击方法:
- 对所有的 \(i\) ,计算出对应的整数 \(v_i=\operatorname{CRT}_{N, N^{\prime}}\left(\sigma_i, \sigma_i^{\prime}\right)\) ,这些对应的 \(v_i\) 构成 \(\mathbb{Z}^l\) 上的向量 \(\mathbf{v}=\left(v_1, \cdots, v_i\right)\) ;
- 利用LLL定理计算出垂直于向量 \(\mathbf{v}\) 的正交格 \(\mathbf{v}^{\perp}\) 的规约基 \(\mathbf{b}_1, \cdots, \mathbf{b}_{l-1}\) ,其中所有的向量和格的分布都是在 \(\mathbb{Z}^l\) 内。通过对存在于 \(\mathbb{Z}^{1+l}\) 中的格应用LLL定理,即对如 下矩阵使用LLLL定理:
其中 \(k\) 为合适的大常量,并去除计算出来的向量的第 1 个元素;
3. 前 \(l-2\) 个向量 \(\mathbf{b}_1, \cdots, \mathbf{b}_{l-2}\) 将生成秩为 \(l-2\) 的格 \(L^{\prime}\) ,再次利用LLL定理来计算出正交格 \(\left(L^{\prime}\right)^{\perp}\) 的规约基 \(\mathbf{x}^{\prime}, \mathbf{y}^{\prime}\) 。同样可以通过对如下矩阵使用LLL定理得到对应 的规约基:
同步㵵2,保留计算出来的向量的最后 \(l\) 个元素;
4. 将所有长度不超过 \(\sqrt{l N}\) 的并在 \(\left(L^{\prime}\right)^{\perp}\) 内的向量 \(\mathbf{z}=a \mathbf{x}^{\prime}+b \mathbf{y}^{\prime}\) 列举出来,对符合条件的向量 \(\mathbf{z}\), 计算出 \(\operatorname{gcd}(\mathbf{v}-\mathbf{z}, N)\) ,可以得出 \(N\) 中任何可能的素因子。
from tqdm import tqdm
import gmpy2,sys
def orthogonal_lattice(B):
LB = B.transpose().left_kernel(basis="LLL").basis_matrix()
return LB
cs = []
s = []
l = 6
v = []
for i in range(len(cs_)):
v.append(int(crt([s_[i], cs_[i]], [n, N])))
v = vector(ZZ, v)
Lv = orthogonal_lattice(Matrix(v))
L1 = orthogonal_lattice(Lv.submatrix(0, 0, l-2, l))
x, y = L1
for a in tqdm(range(333)):
for b in tqdm(range(333)):
z = a*x+b*y
for each in (v-z):
tmp = gcd(each,n)
if tmp>1:
p = tmp
print(p)
sys.exit()
参考:
Modulus Fault Attacks Against RSA-CRT Signatures
2022巅峰极客 - Learning with Fault
其他特别情形
- 多素数因子 (Multi-prime RSA)
- next_prime0
根据素数定理,表数的平均间隔为: \(\frac{x}{\pi(x)} \approx \ln (x)\) ,因此常见的下一个素数比当前素数大一点,一般不会超过 1500 。 - 变种1: \(n=p \cdot q \cdot \operatorname{nextprime}(p) \cdot \operatorname{nextprime}(q)\)
费马因式分解。 - 给 e,p,c
-
给 \(\mathrm{e}, \mathrm{d}, \operatorname{modinv}(q, p), \mathrm{c}\)
已知: \(p, q\) 同比特位数。
令 \(c f=q^{-1} \bmod p\) ,有 \(q \cdot c f=1(\bmod p)\) 。-
\(e d=1+k(p-1)(q-1)\),
比较比特位数, \(k\) 与 \(e\) 同长,可爆破 \(k\) ,得 \(\varphi(n)=(p-1)(q-1)=\frac{e d-1}{k}\) ; -
上式 \(\varphi(n)=(p-1)(q-1)(\bmod p)=-(q-1)(\bmod p)\) ,
结合 \(q \cdot c f=1(\bmod p)\) ,即 \(q \cdot c f-1=0(\bmod p)\) ,
联立:\[\begin{aligned} & \varphi(n)=(p-1)(q-1) \\ & =p q-p-q+1 \\ & =n-p-q+1 \\ & c f \cdot \varphi(n)=c f \cdot(n-p-q+1) \\ & =c f \cdot n-c f \cdot p-c f \cdot q+c f \\ & c f \cdot \varphi(n) \bmod p=(c f \cdot n-c f \cdot p-c f \cdot q+c f) \bmod p \\ & =0-0-(c f \cdot q)+c f \bmod p \\ & =-1+c f \bmod p \\ & 有1+c f \cdot \varphi(n)-c f=0(\bmod p) , \\ &即 x=1+c f \cdot \varphi(n)-c f 能被p 整除; \end{aligned} \] -
由费马小定理,存在 \(r\) 满足 \(r^{p-1}=1(\bmod p)\) ,
\[\begin{aligned} r^{\varphi(n)} & =\left(r^{(p-1)}\right)^{(q-1)} \\ & =1^{(q-1)}(\bmod p) \\ & =1 \quad(\bmod p) \end{aligned} \] -
因对于任意 \(r, k_1, k_2\) ,当 \(k_2\) 为 \(k_1\) 因子时, \(r \bmod k_2=\left(r \bmod k_1\right) \bmod k_2\) , 故 \(r^{\varphi(n)} \bmod p=\left(r^{\varphi(n)} \bmod x\right) \bmod p=1 \bmod p=k p\) ,
已知 \(\varphi(n)\) ,由 \(\left(r^{\varphi(n)} \bmod x\right) \bmod p=k p\) 可得到多组 \(p\) 的乘积,计算 \(\operatorname{gcd}\) 可得到 \(p\); -
由 \(q⋅cf=1(mod \; p)\) 求模逆可得 \(q\),再用 \(c\) 计算出 \(m\)。
-
参考:TSG CTF 2020 - Modulus Amittendus
-
\(\operatorname{gcd}(e, \varphi(n)) \neq 1\)
\(\operatorname{gcd}(e, \varphi(n)) \neq 1\) 时, \(e\) 与 \(\varphi(n)\) 不互紘,
则 \(c^{d^{\prime}} \equiv m^{\operatorname{gcd}(e, \varphi(n))}(\bmod n)\) 。
当 \(\operatorname{gcd}(e, \varphi(n))\) 较小时,可以直接对 \(c\) 开根,有两种情况:-
\(m^e=c<n\) ,这种情况直接对 \(c\) 开 \(e\) 次方即可;
-
\(m^e=c>n\) ,这种情况需要在有限域下对 \(c\) 开方,一般先计算
\(c_p=c \bmod p, c_q=c \bmod q\) ,分别求出 \(c_p, c_q\) 在 \(c\) 下的 \(e\) 次根 (可能 有多个),然后使用CRT遍历所有组合,分别check得出明文。
当 \(\operatorname{gcd}(e, \varphi(n))\) 较大时,求 \(p, q\) 的 \(e\) 次根步癷需要替换为一些有限域开根的 高效算法 (如AMM算法等) 进行计算。
参考:
De1CTF2019 - Baby RSA
0ctf 2016 - RSA? -
-
e|(p-1), e|(q-1)
上面的 \(\operatorname{gcd}(e, \varphi(n)) \neq 1\) 情况不针对 \(\operatorname{gcd}(e, \varphi(n))=e\) ,这里对 \(e|(p-1), e|(q-1)\) 的特殊情况进行讨论。
解题思路即求解 \(m \bmod p\) 和 \(m \bmod q\) ,再通过CRT还原 \(m \bmod n\) 。主要 难点则是在 \(\mathrm{GF}(p)\) 上求 \(e\) 次根。
在有限域上求r-th root有两个常见算法 (Adleman-Manders-Miller algorithm 和Cipolla-Lehmer algorithm),Namhun Koo提出一种更具一般性的开根算 法,且在 \(s\) 足够小的时候更高效 \(\left(r^s \mid(p-1), r^s \nmid(p-1)\right)\) 。
参考: NCTF 2019 - easyRSA (Adleman-Manders-Miller rth Root Extraction Method)
本题则为 \(e\) 和 \(p-1\) (或 \(q-1\) ) 的最大公约数就是 \(e\) 本身,也就是说 \(e \mid(p-1)\) ,只有对 \(c\) 开 \(e\) 次方根才行。
可以将同余方程 \(m^e \equiv c(\bmod n)\) 化成
-
然后分别在 \(\mathrm{GF}(p)\) 和 \(\mathrm{GF}(q)\) 上对 \(c\) 开 \(e\) 次方根,再用CRT组合一下即可得到 在 \(\bmod n\) 下的解。
问题是,如何在有限域内円根?
这里 \(e\) 与 \(p-1\) 和 \(q-1\) 都不互嗉,不能简单地求个逆元就完事。
这种情况下,开平方根可以用Tonelli-Shanks algorithm
,Wiki说这个算法可 以扩展到开 \(n\) 次方根。
在这篇paper里给出了具体的算法:Adleman-Manders-Miller $x$ th Root Extraction Method
。
这个算法只能开出一个根,实际上开 \(e\) 次方,最多会有 \(e\) 个根(这题的情况下 有 \(0 \times 1337\) 个根)。
如何找到其他根?
StackOverflow - Cube root modulo P 给出了方法。
如何找到所有的 primitive \(0 \times 1337\) th root of 1 ?
StackExchange - Finding the n-th root of unity in a finite field 给出了方 法。Exploit (以 \(e=0 \times 1337\) 为例)
- 先用 Adleman-Manders-Miller \(x\) th Root Extraction Method 在 \(\mathrm{GF}(p)\) 和 \(\mathrm{GF}(q)\) 上对 \(c\) 开 \(e\) 次方根,分别得到一个解。大概不到10秒。
- 然后去找到所有的 \(0 \times 1336\) 个 primitive nth root of 1 ,乘以上面那个解,得到所有的 \(0 \times 1337\) 个解。大概 1 分钟。
- 再用CRT对 \(\mathrm{GF}(p)\) 和 \(\mathrm{GF}(q)\) 上的两组 \(0 \times 1337\) 个解组合成 \(\bmod n\) 下的解,可以得到 \(0 \times 1337 * * 2=24196561\) 个 \(\bmod n\) 的解。最后能通过 check 0 的即为flag。 大概十几分钟。
#脚本1
#Sage
import random
import time
# About 3 seconds to run
def AMM(o, r, q):
start = time.time()
print('\n----------------------------------------------------------------------------------')
print('Start to run Adleman-Manders-Miller Root Extraction Method')
print('Try to find one {:#x}th root of {} modulo {}'.format(r, o, q))
g = GF(q)
o = g(o)
p = g(random.randint(1, q))
while p ^ ((q-1) // r) == 1:
p = g(random.randint(1, q))
print('[+] Find p:{}'.format(p))
t = 0
s = q - 1
while s % r == 0:
t += 1
s = s // r
print('[+] Find s:{}, t:{}'.format(s, t))
k = 1
while (k * s + 1) % r != 0:
k += 1
alp = (k * s + 1) // r
print('[+] Find alp:{}'.format(alp))
a = p ^ (r**(t-1) * s)
b = o ^ (r*alp - 1)
c = p ^ s
h = 1
for i in range(1, t):
d = b ^ (r^(t-1-i))
if d == 1:
j = 0
else:
print('[+] Calculating DLP...')
j = - discrete_log(a, d)
print('[+] Finish DLP...')
b = b * (c^r)^j
h = h * c^j
c = c ^ r
result = o^alp * h
end = time.time()
print("Finished in {} seconds.".format(end - start))
print('Find one solution: {}'.format(result))
return result
def findAllPRoot(p, e):
print("Start to find all the Primitive {:#x}th root of 1 modulo {}.".format(e, p))
start = time.time()
proot = set()
while len(proot) < e:
proot.add(pow(random.randint(2, p-1), (p-1)//e, p))
end = time.time()
print("Finished in {} seconds.".format(end - start))
return proot
def findAllSolutions(mp, proot, cp, p):
print("Start to find all the {:#x}th root of {} modulo {}.".format(e, cp, p))
start = time.time()
all_mp = set()
for root in proot:
mp2 = mp * root % p
assert(pow(mp2, e, p) == cp)
all_mp.add(mp2)
end = time.time()
print("Finished in {} seconds.".format(end - start))
return all_mp
c =
p =
q =
e = 0x1337
cp = c % p
cq = c % q
mp = AMM(cp, e, p)
mq = AMM(cq, e, q)
p_proot = findAllPRoot(p, e)
q_proot = findAllPRoot(q, e)
mps = findAllSolutions(mp, p_proot, cp, p)
mqs = findAllSolutions(mq, q_proot, cq, q)
print(mps, mqs)
def check(m):
h = m.hex()
if len(h) & 1:
return False
if bytes.fromhex(h).startswith(b'NCTF'):
print(bytes.fromhex(h))
return True
else:
return False
# About 16 mins to run 0x1337^2 == 24196561 times CRT
start = time.time()
print('Start CRT...')
for mpp in mps:
for mqq in mqs:
solution = CRT_list([int(mpp), int(mqq)], [p, q])
if check(solution):
print(solution)
print(time.time() - start)
end = time.time()
print("Finished in {} seconds.".format(end - start))
#脚本2
#Sage
c = 346925245648012783854132941104554194717281878370806475831055718275298366664505658836564073456294047402009856656647760
p = 21122913513992623721920275602985463699928507831138027
q = 16471885912035642894544190467774867069446937372970845578732298073
e = 239
P.<a>=PolynomialRing(Zmod(p),implementation='NTL')
f=a^e-c
mps=f.monic().roots()
P.<a>=PolynomialRing(Zmod(q),implementation='NTL')
g=a^e-c
mqs=g.monic().roots()
for mpp in mps:
x=mpp[0]
for mqq in mqs:
y=mqq[0]
solution = hex(CRT_list([int(x), int(y)], [p, q]))[2:]
if solution.startswith('666c'):
print(solution)
-
SMUPE 问题 (不同N,e加密线性关系明文)
a system of univariate polynomial equations problem \(=\) 一元多项式方程组求解问题-
定义
\(k\) 是一个整数, \(N\) 为满足RSA算法的模数, \(\delta\) 是多项式的阶。\[N_i<N_{i+1}, \delta_i \in N \quad(i=1,2, \cdots, k) \] -
多项式方程组表示如下,目的是求解 \(x\) :
\[\left\{\begin{array}{l} f_1(x) \equiv 0\left(\bmod N_1\right) \\ f_2(x) \equiv 0\left(\bmod N_2\right) \\ \vdots \\ f_k(x) \equiv 0\left(\bmod N_k\right) \end{array}\right. \] -
求解条件
Alexander May, Maike Ritzenhofent提出一种求解方法,简单地说当多项式的阶 \(\delta\) 满足以下情况时可解( \(\delta\) 是多项式的阶):\[\sum_{i=1}^k \frac{1}{\delta_i} \geq 1 \] -
具体描述:
令 \(\left(f_i, \delta_i, N_i\right) \quad(i=1,2, \cdots, k)\) 作为SMUPE问题的首一多项式组,
定义 \(M=\prod_{i=1}^k N_i^{\frac{\delta}{\delta_i}}, \delta=\operatorname{lcm}\left(\delta_i\right) \quad(i=1,2, \cdots, k)\)
则SMUPE问题可以在 \(O\left(\delta^6 \cdot \log _2 M\right)\) 复杂度解决。
-
-
反溸数 (emirp数)
已知: \(q=\) reverse \(\_\mathrm{x}(p) , \mathrm{x}\) 为进制数。
爆破思路类似RSA parity oracle。 \(p, q\) 是bit翻转关系,已知 \(p\) 最低的 \(k\) 位,则已知 \(q\) 最高的 \(k\) 位。 假设已知 \(k\) 位的 \(p, q\) ,记为 \(p h, q h\) ,利用不等式
逐位向低地址懪破,不断收缩不等式的范围,最終可求得 \(n\) 值。
参考:
ASIS 2015 Finals: RSASR
Midnight Sun CTF 2020 Quals
RoarCTF 2020 - Reverse
#python2
#x=10
n = 6528060431134312098979986223024580864611046696815854430382374273411300418237131352745191078493977589108885811759425485490763751348287769344905469074809576433677010568815441304709680418296164156409562517530459274464091661561004894449297362571476259873657346997681362092440259333170797190642839587892066761627543
def t(a, b, k):
# sqrt(n) has 155 digits, so we need to figure out 77 digits on each side
if k == 77:
if a*b == n:
print a, b
return
for i in xrange(10):
for j in xrange(10):
# we try to guess the last not-already-guessed digits of both primes
a1 = a + i*(10**k) + j*(10**(154-k))
b1 = b + j*(10**k) + i*(10**(154-k))
if a1*b1 > n:
# a1 and b1 are too large
continue
if (a1+(10**(154-k)))*(b1+(10**(154-k))) < n:
# a1 and b1 are too small
continue
if ((a1*b1)%(10**(k+1))) != (n%(10**(k+1))):
# The last digits of a1*b1 (which won't change later) doesn't match n
continue
# this a1 and b1 seem to be a possible match, try to guess remaining digits
t(a1, b1, k+1)
# the primes have odd number of digits (155), so we try all possible middle digits (it simplifies the code)
for i in xrange(10):
t(i*(10**77), i*(10**77), 0)
- \(4 p-1\) method
对使用一类特定䋤数乘积的模数的分解。 会将其视为 RSA 的后门之一,称之为 RSA backdoor 。- QiCheng Prime
import sys
sys.setrecursionlimit(10^6)
def QiCheng(n):
R = Integers(n)
attempts = 20
js = [0, (-2^5)^3, (-2^5*3)^3, (-2^5*3*5)^3, (-2^5*3*5*11)^3, (-2^6*3*5*23*29)^3]
for _ in range(attempts):
for j in js:
if j == 0:
a = R.random_element()
E = EllipticCurve([0, a])
else:
a = R(j)/(R(1728)-R(j))
c = R.random_element()
E = EllipticCurve([3*a*c^2, 2*a*c^3])
x = R.random_element()
z = E.division_polynomial(n, x)
g = gcd(z, n)
if g > 1:
return g
n =
p = int(QiCheng(Integer(n)))
Masaaki Shirase & Vladimir Sedlacek Improvement
更多 Ds 值。
参考:
NCTF 2020 - RSA_revenge
CryptoHack Challenge - RSA Backdoor Viability
-
Common Prime RSA
情形:gcd(p−1,q−1)=ggcd(p−1,q−1)=g
分解的n方法有四种:
(1)修改Pollard’s rho方法分解n;
(2)知道a、b的值分解n;
(3)知道g的值分解n;
(4)分解N-1。
# Pollard’s rho
from Crypto.Util.number import *
import gmpy2
def f(x, n):
return (pow(x, n - 1, n) + 3) % n
def rho(n):
i = 1
print 'Factorizing'
while True:
x1 = getRandomRange(2, n)
x2 = f(x1, n)
j = 1
while True:
p = gmpy2.gcd(abs(x1 - x2), n)
if p == n:
break
elif p > 1 and isPrime(p):
print 'Found!'
return (p, n // p)
else:
x1 = f(x1, n)
x2 = f(f(x2, n), n)
j += 1
i += 1
- Return of Coppersmith' s attack (ROCA)
CVE-2017-15361
形如 \(p=k M+\left(65537^a \bmod M\right)\) 生成表数的RSA系统, \(M\) 是前 \(n\) 个连续表数的乘积, \(n\) 是仅取决于所需密钥大小的常数。
https://github.com/jvdsn/crypto-attacks/blob/master/attacks/factorization/roca.py
https://github.com/FlorianPicca/ROCA
PEM密钥
由-----BEGIN <TAG>-----
开头,-----END <TAG>-----
结尾,中间是Base64编码的一串二进制,每64个字母(即解码后的48bytes)有一个换行。中间的Base64解码后是一串遵循ASN.1协议的DER编码,简单来说可以看成一种序列化,把一个结构体中的整数、字串等编码成一个方便传输的二进制。
生成代码:
from Crypto.PublicKey import RSA
rsa = RSA.generate(1024)
pk = rsa.publickey().exportKey()
sk = rsa.exportKey()
with open ('./pub.pem', 'wb') as f:
f.write(pk)
with open ('./priv.pem', 'wb') as f:
f.write(sk)
RSA私钥
-----BEGIN RSA PRIVATE KEY-----
...Base64 encoded key...
-----END RSA PRIVATE KEY-----
RFC3447定义:
RSAPrivateKey ::= SEQUENCE {
version Version,
modulus INTEGER, -- n
publicExponent INTEGER, -- e
privateExponent INTEGER, -- d
prime1 INTEGER, -- p
prime2 INTEGER, -- q
exponent1 INTEGER, -- d mod (p-1)
exponent2 INTEGER, -- d mod (q-1)
coefficient INTEGER, -- (inverse of q) mod p
otherPrimeInfos OtherPrimeInfos OPTIONAL
}
Version ::= INTEGER { two-prime(0), multi(1) }
(CONSTRAINED BY
{-- version must be multi if otherPrimeInfos present --})
例:
3082025d02010002818100a0d154d5bf97c40f7797b44819d09c608fa4b5c38e70d83bc13267138c6eff4c1aacefe3ddb571e1b41d911c7ab6136cf90493189563450e1f4270cabbc4207c54c4da7b84a20311cfbbabe82b9fe60bdf48a08d57839d0cdf9464d84262bcc06bc308095a6987f60ad07d669a312b5a7e4133213788eecf25863248b91349ef02030100010281800f8270c496903bf3e3ec4912450f15edc81cb1fcf4b154615aee11fbd428e64d402b5a8d66d5f770358f3e6df935b324e8d5349c83d7c992a5982249a31734acb1db19c4c8d829267514bc1ef7bbfbe242d4350f67a002a56d33e56d1a94adc71c68f020dc39ab7d0064c111b164e26ba0698dc94a03cdfd516ffd966e877949024100ca97e49c058237f96e99118ce383f91912cba1163de9236181ff754ef3ef1a260fac8d2d9aee866d51a8b6836983b05cf850e786289b6859925bc8695fc67c47024100cb3630aafffcb29607f0833dc7f05c143ee92fadfe975da4cf6719e71226bee72562e8631328a25d7351507a8d43c1295ab6ea242b60a28b109233a983f4211902401b4a32a541a8b4d988a85dd0d8a4e25d1a470bbfef3f0461121dd3337b706dd94aab37a9390180622169d48c071e921733ebd204245c2ac6460ccf0642bc7de90241008d9f44a7c823eaaa58fa2bdd20bcc8cf6b50c463f4acb51ca956e75c7ceff7d7cbdc74aca7ab880cacd39cccec2aae320e00b0896899be6e40ac43c8fe2763f1024100c67ca6d988f53abea82159431a146512a8d942978d4a8f83f2d426f1095e3bf1b5b9b8b1ccbbad2a31c6401880447a45f5e0790269061ac13b5f68f1777d7f07
30
是Sequence的tag,82
是指接下来后两个bytes是这个Sequence的长度,即0x025d
个bytes,也就是剩下全部都是;接着的020100
就是整数0,其中02
是整数的tag,01
是这个整数占1byte,00
是value同样的方法也可以解02818100a0...
和后面其他整数,拆分:
3082025d # Begin Sequence: len=0x025d
0201 # Version: (len=0x01)
00
028181 # n: (len=0x81)
00a0d154d5bf97c40f7797b44819d09c608fa4b5c38e70d83bc13267138c6eff4c1aacefe3ddb571e1b41d911c7ab6136cf90493189563450e1f4270cabbc4207c54c4da7b84a20311cfbbabe82b9fe60bdf48a08d57839d0cdf9464d84262bcc06bc308095a6987f60ad07d669a312b5a7e4133213788eecf25863248b91349ef
0203 # e: (len=0x03)
010001
028180 # d: (len=0x80)
0f8270c496903bf3e3ec4912450f15edc81cb1fcf4b154615aee11fbd428e64d402b5a8d66d5f770358f3e6df935b324e8d5349c83d7c992a5982249a31734acb1db19c4c8d829267514bc1ef7bbfbe242d4350f67a002a56d33e56d1a94adc71c68f020dc39ab7d0064c111b164e26ba0698dc94a03cdfd516ffd966e877949
0241 # p: (len=0x41)
00ca97e49c058237f96e99118ce383f91912cba1163de9236181ff754ef3ef1a260fac8d2d9aee866d51a8b6836983b05cf850e786289b6859925bc8695fc67c47
0241 # q: (len=0x41)
00cb3630aafffcb29607f0833dc7f05c143ee92fadfe975da4cf6719e71226bee72562e8631328a25d7351507a8d43c1295ab6ea242b60a28b109233a983f42119
0240 # d mod (p-1): (len=0x40)
1b4a32a541a8b4d988a85dd0d8a4e25d1a470bbfef3f0461121dd3337b706dd94aab37a9390180622169d48c071e921733ebd204245c2ac6460ccf0642bc7de9
0241 # d mod (q-1): (len=0x41)
008d9f44a7c823eaaa58fa2bdd20bcc8cf6b50c463f4acb51ca956e75c7ceff7d7cbdc74aca7ab880cacd39cccec2aae320e00b0896899be6e40ac43c8fe2763f1
0241 # (inverse of q) mod p: (len=0x41)
00c67ca6d988f53abea82159431a146512a8d942978d4a8f83f2d426f1095e3bf1b5b9b8b1ccbbad2a31c6401880447a45f5e0790269061ac13b5f68f1777d7f07
# End Sequence
RSA公钥
-----BEGIN PUBLIC KEY-----
...Base64 encoded key...
-----END PUBLIC KEY-----
例:
30819f300d06092a864886f70d010101050003818d0030818902818100a0d154d5bf97c40f7797b44819d09c608fa4b5c38e70d83bc13267138c6eff4c1aacefe3ddb571e1b41d911c7ab6136cf90493189563450e1f4270cabbc4207c54c4da7b84a20311cfbbabe82b9fe60bdf48a08d57839d0cdf9464d84262bcc06bc308095a6987f60ad07d669a312b5a7e4133213788eecf25863248b91349ef0203010001
拆分:
30819f # Begin Main Sequence: len=0x9f
300d # Begin Sub1 Sequence: len=0x0d
0609 # algo_oid: (1.2.840.113549.1.1.1 - PKCSv1.2)
2a864886f70d010101
0500 # params: (null)
# End Sub1 Sequence
03818d # BitString: len=0x8d ([n, e])
00308189 # Begin Sub2 Sequence: len=0x89
028181 # n:
00a0d154d5bf97c40f7797b44819d09c608fa4b5c38e70d83bc13267138c6eff4c1aacefe3ddb571e1b41d911c7ab6136cf90493189563450e1f4270cabbc4207c54c4da7b84a20311cfbbabe82b9fe60bdf48a08d57839d0cdf9464d84262bcc06bc308095a6987f60ad07d669a312b5a7e4133213788eecf25863248b91349ef
0203 # e:
010001
# End Sub2 Sequence
# End Main Sequence
参考
详细原理
[Cryptanalysis of RSA and It’s Variants](http://index-of.es/Varios-2/Cryptanalysis of RSA and It's Variants.pdf)