RSA总结

RSA总结

基本工具#

  • 大整数分解

    factordb.com

    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)
    
  • 脚本集

  • 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为质数,有

pq p (mod pq)

  • 若a,b互素,有

aϕ(b)+bϕ(a)1 (mod ab)

  • 若p为质数,有

(p1)! 1 (mod p)

  • 若p为质数,且 p3 (mod 4),有

(p12)!±1 (mod p)

  • 二次剩余
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)24n=(p+q)24pq=(p+q)244pq4=(pq)24

既然 |p-q| 较小,那么 (pq)24 自然也比较小,进而 (p+q)24 只是比 N 稍微大一点,所以 p+q2n 相近。那么我们可以按照如下方法来分解

  • 顺序检查 n 的每一个整数 x,在根号n的附近直到找到一个 x 使得 x2n 是平方数,记为 y2
  • 那么 x2n=y2, $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的"中间值"(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,p2q2gcd(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):指可以分解为小素数乘积的正整数

  • pN的因数,并且p1是光滑数,可以考虑使用Pollard's p-1算法来分解NN

  • 根据费马小定理有

pa, ap11(modp)

则有

at(p1)1t1(modp)

at(p1)1=kp

  • 根据Pollard's p-1算法如果

如果p是一个Bsmooth number,那么则存在

M=qBqlogqB

使得

(p1)M

成立,则有

gcd(aM1,N)

如果结果不为1N,那么就已成功分解N

因为我们只关心最后的gcd 结果,同时N只包含两个素因子,则我们不需要计算M,考虑n=2,3,...,M=n! 即可覆盖正确的M 同时方便计算。

  • 在具体计算中,可以代入降幂进行计算

an!modN={(amodN)2modNn=2(a(n1)!modN)nmodNn3

  • 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 光滑#

  • pN 的因数,并且p+1 是光滑数,可以考虑使用Williams's p+1算法来分解N

  • 已知N 的因数p,且p+1 是一个光滑数

    p=(i=1kqiαi)+1

    qi 即第$i q_i^{\alpha_i}\le B_1, \beta_i$ 使得让qiβiB1qiβi+1>B1然后令

    R=i=1kqiβi

    显然有$p-1\mid R (N, a) = 1 a^{p-1}\equiv 1 \pmod{p}a^R\equiv 1\pmod{p}$,即

    p(N,aR1)

  • P,Qα,βx2Px+Q=0

    Un(P,Q)=(αnβn)/(αβ)Vn(P,Q)=αn+βn

    Δ=(αβ)2=P24Q

    (2.2){Un+1=PUnQUn1Vn+1=PVnQVn1

    (2.3){U2n=VnUnV2n=Vn22Qn

    (2.4){U2n1=Un2QUn12V2n1=VnVn1PQn1

    (2.5){ΔUn=PVn2QVn1Vn=PUn2QUn1

    (2.6){Um+n=UmUn+1QUm1UnΔUm+n=VmVn+1QVm1Vn

    (2.7){Un(Vk(P,Q),Qk)=Unk(P,Q)/Uk(P,Q)Vn(Vk(P,Q),Qk)=Vn(P,Q)

    同时我们有如果(N,Q)=1PQP22Q(modN)则有Pα/β+β/α 以及Qα/β+β/α=1,即

    (2.8)U2m(P,Q)PQm1Um(P,1)(modN)

    根据扩展卢卡斯定理

    ppQ(Δ/p)=ϵ

    U(pϵ)m(P,Q)0(modp)V(pϵ)m(P,Q)2Qm(1ϵ)/2(modp)

  • 第一种情况:已知 N 的因数 p,且 p+1 是一个光滑数

    p=(i=1kqiαi)1

    p+1R(Q,N)=1(Δ/p)=1pUR(P,Q)p(UR(P,Q),N)

    为了找到UR(P,Q)GuyConway提出可以使用如下公式

    U2n1=Un2QUn21U2n=Un(PUn2QUn1)U2n+1=PU2nQU2n1

    但是上述公式值太大了,不便运算,我们可以考虑如下方法

    如果pUR(P,1),根据公式2.3pU2R(P,Q),所以根据公式2.8pUR(P,1),设Q=1则有

    V(pϵ)m(P,1)2(modp)

    即,如果pUR(P,1)p(VR(P,1)2).

    第一种情况可以归纳为:

    R=r1r2r3rm,同时找到P0 使得(P024,N)=1定义Vn(P)=Vn(P,1),Un(P)=Un(P,1)

    PjVrj(Pj1)(modN)(j=1,2,3,,m)

    根据公式2.7,有

    (3.1)PmVR(P0)(modN)

    要计算Vr=Vr(P) 可以用如下公式

    根据公式2.2公式2.3公式2.4

    {V2f1VfVf1PV2fVf22V2f+1PVf2VfVf1P(mod()N)

    r=i=0tbt2ti    (bi=0,1)

    f0=1,fk+1=2fk+bk+1ft=rV0(P)=2,V1(P)=P则最终公式为

    (Vfk+1,Vfk+11)={(V2fk,V2fk1)    if bk+1=0(V2fk+1,V2fk)    if bk+1=1

    第二种情况:已知p+1 是一个光滑数

    p=s(i=1kqiαi)1

    sB1<sB2p(ams1,N)sj2dj

    2dj=sj+1sj

    如果(Δ/p)=1pPm2则根据公式2.7公式3.1p(Us(Pm),N)

    U[n]Un(Pm),V[n]Vn(Pm)(modN)计算U[2dj1],U[2dj],U[2dj+1]通过

    U[0]=0,U[1]=1,U[n+1]=PmU[n]U[n1]

    计算

    T[si]ΔUsi(Pm)=ΔUsiR(P0)/UR(P0)(modN)

    通过公式2.6公式2.7公式3.1

    {T[s1]PmV[s1]2V[s11]T[s11]2V[s1]PmV[s11](modN)

    {T[si+1]T[si]U[2di+1]T[si1]U[2di]T[si+11]T[si]U[2di]T[si1]U[2di1](modN)

    计算T[si],i=1,2,3然后计算

    Ht=(i=0cT[si+t],N)

    t=1,c+1,2c+1,,c[B2/c]+1pHi(Δ/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不同的私钥时,加密同一明文消息时即存在共模攻击。

设两个用户的公钥分别为 e1e2,且两者互质。明文消息为 m,密文分别为:

c1=me1modNc2=me2modN

当攻击者截获 c1c2 后,就可以恢复出明文。用扩展欧几里得算法求出 re1+se2=1modn 的两个整数 rs,由此可得:

c1rc2smre1mse2modnm(re1+se2)modnmmodn

#脚本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都很小时

由加密公式得

cmemodNme=c+kN

m=c+k×Ne

所以当e很小的时候,我们可以直接把me次方,直到开出整数即可,能不能开出来取决于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,并且加密者使用了三个不同的模数 n1,n2,n3 给三个不同的用户发送了加密后的消息 m,如下

c1=m3modn1c2=m3modn2c3=m3modn3

这里我们假设 n1,n2,n3 互素,不然,我们就可以直接进行分解,然后得到 d,进而然后直接解密。

既然他们互素,那么我们可以根据中国剩余定理,可得m3Cmodn1n2n3

此外,既然 m<ni,1i3,那么我们知道 m3<n1n2n3 并且 C<m3<n1n2n3,那么 m3=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 算法#

密文:

c=m2modn

解密:

  • 计算出 mpmq

mp=cmodpmq=cmodq

  • 用扩展欧几里得计算出 ypyq: [LRX.py](..........\Downloads\WeChat Files\wxid_f7zahwf7mk5q22\FileStorage\File\2020-10\LRX.py)

ypp+yqq=1

  • 解出四个明文:

a=(yppmq+yqqmp)modnb=nac=(yppmqyqqmp)modnd=nc

注意:如果 pq3(mod4),则

mp=c14(p+1)modpmq=c14(q+1)modq

而一般情况下,pq3(mod4) 是满足的,对于不满足的情况下,请参考相应的算法解决。

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}$的问题。

ed=kφ+1kφ+10(mode)k(N+1pq)+10(mode)2k(N+12+pq2)0(mode)

设$ 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$。

参考 RSA-and-LLL-attacks

#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
  • 变种1e 很大,dp很小,且d>2Nβ

    May’s Attack

    假设e<φ(N),qNβ,β12,因 edp1(modp1),有edp=1+k(p1)

    对于kN,有 edp=(k1)(p1)+p,即edpq=(k1)(Nq)+N

    设 x,y 为参数,则多项式f(x,y)=x(Ny)+N在模 e 下存在根$ (x_0,y_0)=(k-1,q)coppersmith attack$可解。

Wiener's Attack#

在 d 比较小(d<13N14)时,攻击者可以使用 Wiener's Attack 来获得私钥。

适用情况:已知 N,e,且 e 过大或过小。

φ(n)=(p1)(q1)=pq(p+q)+1=N(p+q)+1

$\because p, q \therefore,pq\gg p+q, \therefore\varphi(n)\approx N$

ed1modφ(n)ed1=kφ(n),这个式子两边同除dφ(n)可得:

eφ(n)kd=1dφ(n)

φ(n)NeNkd=1dφ(n),同样 dφ(n) 是一个很大的数,所以eN 略大于kd

因为 eN 是知道的,所以计算出$ \cfrac{e}{N} \cfrac{k}{d} \cfrac{e}{N} \cfrac{e}{N} \cfrac{k}{d}Wiener $证明了,该攻击能精确的覆盖 kd

e 过大或过小的情况下,可使用算法从e 中快速推断出 $d q<p<2q,d<\cfrac{1}{3}N^{\frac{1}{4}} $的问题。

攻击原理

工具:

Continued Fractions(连分数)#

连分数就是一个数的连续分式展开,它的式子长这样:

a0+1a1+1a2+1+1an

(其中a0 是整数,a1,a2,,an都是正整数)

通常我们用更简单的数组方式来描述,对任何有理数p/q来说:

pq=[a0;a1,a2,,an]

计算连分数我们可以使用欧几里得算法:

p=a0q+r0q=a1r0+r1r1=a2r1+r2rn2=anrn1+0.

参考:https://zh.wikipedia.org/w/index.php?title=连分数&oldid=65444935

Convergent(收敛)#

我们定义ci为连分数每一次分式展开的收敛,即:

i[0,n],ci=[a0;a1,,ai]

对于每一个 $ \mathrm{c}{\mathrm{i}} \frac{\mathrm{p}{\mathrm{i}}}{\mathrm{qi}} \frac{\mathrm{q}}{\mathrm{p}} $不断进行逼近收敛的近似值。

Legendre' s theorem#

这是Wienersattack 的一个比较重要的定理,定义如下:
Theorem2.2(ContinuedFractions).LetαQandc,dZsatisfy

|αcd|<12d2.

Thenc/d,inlowestterms,isoneoftheconvergentsinthecontinuedfractionexpansionofα.
也就是说,当满足|acd|<12 d2 时,cd 就是 a 的连分数收敛。
根据这个定理,我们可以通过计算一个数(e/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))
  • 变种1N1N2<q1q2<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

image-20230204155430197

尝试对$ \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:Axy(modP)AxkP=yAx+kP=y

    Wiener’s

    |APkx|12x2,连分数方法,用AP 逼近求出$ \cfrac{k}{x} y$。

    Lattice

    (x,k)[1A0P ]=(x,y),记为vM=w,w 有很大概率为 M 里的最短向量,使用LLL算法求出最短向量,即解出w=(x,y)

    参考:Wiener’s v.s Lattices —— Ax≡y(mod P)的方程解法笔记

Extending Wiener 's attack#

扩展维纳攻击来自《Extending Wiener's Attack in the Presence of Many Decrypting Exponents》,相关题目在 CTF 中已经出现了,例如 2020 羊城杯的 Simple。论文下载链接

维纳Wiener提出了一种关于私钥过小时对N 进行分解的一种方式。并给出了证明当

d<13N14

满足时 (还应满足q<p<2q,因这里及后文主要是对私钥进行探讨,故忽略这类条件) 一定能够分解N

Wiener 's Approach#

已知

edkλ(N)=1

这里λ(N)=lcm(p1,q1)=φ(N)/g , 令s=1pq 则有

edgkN=g+ks

将两边同时除以dgN 则有

eNkdg=g+ksdgN=(kdg)(sN)+1dN

我们知道这里有eN,sN1/2 ,所以有k/(dg)1 。则我们可以知道等式右边约等于 N1/2 。我们都知道当

|xa/b|<1/(2b2)

时则a/b 是一个x 连分数近似 (连分数定理ContinuedFractions)
所以当

d<22gN14

时有k/dge/N的连分数近似,即能通过连分数展开覆盖。
注意这里前面所说的范围和后面的范围并不矛盾
这里对一些参数的值的近似并不严格,所以和维纳攻击的严格范围有出入,具体细节可参考维纳攻击的证明。

Guo 's Approach With Two Exponents#

假设对于一个$ N e_{i} d_{i} $ ,那么可以得到:

{e1 d1gk1φ(N)=ge2 d2gk2φ(N)=g

化简后可得:

e1 d1k2e2 d2k1=k2k1(G(1,2))

两边同时除以k2d1e2,可推出

|e1e2d2k1 d1k2|=|k2k1|e2 d1k1

di<Nα ,则等式右边约等于N(1+α)
则当

2(k2d1)2<N1+α

k1d2/(k2d1)e1/e2 的连分数近似。当 k2d1 最多为Nα 而且g很小时,得到

α<1/3ϵ(ϵ>0)

所以,如果 $ 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}} Nk_1d_2$ 进行分解。

Extending Wiener 's attack#

Howgrave-Graham 和 Jean-Pierre Seifert 结合了Wiener 和Guo的想法,通过多个等式来构造格利用LLL化简来解决这个问题。

  • 为了将分析扩展到$n e_id_i $很小),我们同时使用维纳和郭的方法,我们将关系

    digeikiN=g+kis

    记为维纳等式Wi,同样我们可以得到关系

    kidjejkjdiei=kikj

    记为郭等式Gi,j

    我们假设di 和$k_i N^{\alpha_n}g$ 很小,sN1/2。可以注意到$W_i G_i N^{1/2 + \alpha} N^\alpha$。

    最后,我们考虑复合关系式比如WuGv,w,显然大小为N1/2+2α

  • 原文中这里是定义了两个关系式以及指出了他们的大小范围,这个范围很重要也容容易分析处理,之后我们所做的其实就是使用这两个式子的不同复合关系去构造一个格,然后通过求其基向量得到d1g/k1,从而可以算得φ(N) 并可以进一步的对N 进行分解。

  • 其实到这里原理分析已经结束,关于格的构造其实也并不复杂,但是核心是这里的复合关系的选取,以及对于最后α大小的分析。

两个小解密指数的情况#

  • 我们选取关系W1,G1,2,W1W2, 这样便有

    d1ge1k1N=g+k1sk1d2e2k2d1e1=k1k2d1d2g2e1e2d1gk2e1Nd2gk1e2N+k1k2N2=(g+k1s)(g+k2s)

    我们对第一个关系式乘上k2,这样左边便全是由d1d2g2,d1gk2,d2gk1k1k2 构成,这样我们便可以用已知内容构造格将上述式子转化为矩阵运算

    (k1k2d1gk2d2gk1d1d2g2)(1N0N2e1e1e1Ne2e2Ne1e2)=(k1k2k2(g+k1s)g(k1k2)(g+k1s)(g+k2s))

    等式右边向量的大小为N2α2,N1/2+2α2,Nα2,N1+2α2, 为了让大小相等,我们可以考虑构造一个 D 矩阵。

    D=(NN1/2N1+α21)

    最终我们构造的矩阵为

    L2=(1N0N2e1e1e1Ne2e2Ne1e2)D

    这样向量b=(k1k2d1gk2d2gk1d1d2g2) 便有

    bL2<2N1+2α2

    这也就是为什么前面需要构造D 矩阵的原因,给定D 矩阵后,我们可以得到一个上界,这样问题可以转化为类 SVP 问题。

    那么这里的$ b 使LLL便bb_2/b_1 d_1g/k_1$

    之后我们就可以得到

    φ(N)=edgkgk=edg/k

    我们假设这些格中最短向量长度为Δ1/4ϵ,其中Δ=det(L2)=N13/2+α2。如果这些格是随机的,我们甚至几乎可以肯定没有格点比闵可夫斯基界Minkowskisbound2Δ1/4,所以bL2是最短向量当

    N1+2α2<(1/c2)(N13/2+α2)1/4

    对于一些小的c2,如果有

    α2<5/14ϵ

    则我们可以通过格基规约找到向量b。

  • 上述内容是原文中给出的当两个小解密指数是进行的攻击细节,并且分析了α的大小关系。

分析#

  • 扩展维纳攻击结合上述三个例子已经详细的阐明了方法细节,但是其中没有讲解如何选取复合关系。其实在原文的附录中给出了复合关系的选取,以及给出了αn的表达式。

  • 在原文附录部分,考虑n 个指数ei,这样则有2n 个不同的量hj(一个表达式$e_i L_n$ 在乘上D 之前,矩阵$L_n N{n2{n-1}}$

    这样最后一个关系W1W2Wn 最大为Nn/2+nαn,这样我们便知道了任意情况的最大界值,我们只需要让其他值增加到这么多即可(即构造D 矩阵)

    引入了新的关系式

    Ru,v=Wi1WiuGj1,l1Gjv,lv

    其中$i_1,\dots,i_u,j_1,\dots,j_u,l_1,\dots,l_v u + 2v e_iR_{u,v}$ 最多为Nu/2+(u+v)αn,同时注意我要需要所有系数的大小大致相同,所以我们在某些等式乘上ki,使得关系Ru,v=Nu/2+(nv)αn

    最后我们再计算所有的大小与最大大小$N^{n/2 + n\alpha_n} D$。

    这样我们便完成了矩阵$D D \beta_n = x+y\alpha_n$,这样有

    det(Ln)Nn2n1+x+yαn

    则有

    Nn/2+nαn<(1/cn)(Nn2n1+x+yαn)1/2n

    对于小cn,有

    αn<xn2nyϵ

    所以我们要想让$\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))

参考文献#

e与phi(n)不互质#

一般来说,RSA的公钥选取时,都会选择一个与ϕ(n)互质的加密指数e,这样才能计算私钥中的揭秘指数d,满足ed1(modϕ(n)),并用d来恢复明文。 cdmedm(modn)eϕ(n)不互质的时候,就不能直接计算d=inv(e,ϕ(n))。然后不互质又可以分为两种情况,一是eϕ(n),二是eϕ(n)

有一道CTF题是比较出名的,NCTF2019

1.eϕ(n)#

这种情况时比较好处理的,我们令t=gcd(e,ϕ(n)),那么e//tϕ(n)时互质的,即gcd(e//t,ϕ(n))=1,这样就可以计算d,满足

(e/t)d1(modϕ(n))

然后将mt看成一个整体

cme(mt)et(modn)

cd(mt)etd(modn)

这个就是一个正常的RSA解密了,不过解出来的不是明文m,而是mt,如何得到m呢?这就又要分两种情况考虑了

1)mt<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)mt>n#

如果mt没有比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]

其实就相当于转换成了一个低加密指数的题

  • 也可以选择结合中国剩余定理

xec(modp)xec(modq)

在不同的域上开根,然后把得到的结果进行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ϕ(n)#

这个时候就不能用上面的类似RSA的方法解决了,我们就只能在有限域上开根了。

谈论有限域开根问题,AMM算法是绕不过的。AMM算法在RSA中适用于指数e整除phi的情况,也就是说phi % e == 0。其中有详细的论文,AMM算法具体的作用就是在有限域中开出一个根。具体实现论文也明确地给出了。

Untitled

代码实现:

# 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

其中AMM(o,r,q) 的返回值满足resultor(modq)

就是在有限域q中对or次根。还有一个比较方便的函数,但是没有具 体研究过,作用也是在有限域内得到一个根。得到一个x满足 xna(modp)

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 进行分解。其基本原理如下

我们知道 ed1modφ(n),那么存在一个 k 使得

ed1=kφ(n)

aZn,满足aed11(modn)。令

ed1=2st

其中,t 是一个奇数。然后可以证明对于至少一半的 aZn,存在一个 i[1,s],使得

a2i1t±1(modn),a2it1(modn)

成立。如果 a,i 满足上述条件,gcd(a2i1t1,n)n 的一个非平凡因子,所以可以对 n 进行暴力分解。

参考文献:

给e,d,n#

计算$ k=ed-1 g,g \in (1,N)$。

$k k=2^tr r t \ge 1$,然后计算 x=gk2,gk4,,gk2t(modN) 直到x>1y=gcd(x1,N)>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 定义我们知道

dpdmodφ(p)dp=d+kφ(p)

根据已知条件得到方程组

{dp=d+k1φ(p)n=pqed=1+k2φ(p)φ(q)ed1modφ(pq)

尝试联立

dp=1+k2φ(p)φ(q)e+k1φ(p)edp1=(k1φ(q)+k2e)×φ(p)

因为

edp1=(k1φ(q)+k2e)×φ(p)e×dp=(k1φ(q)+k2e)×φ(p)

我们知道φ(p)=p1,然后dp=dmodφ(p), dp 肯定小于模数φ(p) ,dp<φ(p),所以有

φ(p)>dpe>k1φ(q)+k2e

所以我们在小于e 的范围内循环检查分解eφ(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

更直观的推导思路:

dpdmod(p1)

两边同乘e:

dpedemod(p1)dedpemod(p1)de=dpe+k1(p1)

则有:

dpe+k1(p1)1modφ(n)dpe+k1(p1)=1+k2(p1)(q1)

dpe放在一边

dpe=1+k2(p1)(q1)k(p1)dpe=1+(k2(q1)k1)(p1)

因为dpp1

dp<p1(k2(q1)k1)(0,e) 惼历 (1,e), 当同时满足 (dpe1)modi==0 和 nmod((dpe1)//i+1)==0 时, N 成功分解。 

变种1#

image

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,dp0,c,k,dp0dp(nbitsk),dp0=dp>>k。 
    (Coppersmith攻击, 已知dp高位攻击)

edped1(mod(p1))edp=k(p1)+1=kpk+1edp+k10(modp)dp<p1,k<ee(dp0<<k+x)+k10(modp)

#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 很大。

    dp,edp1(mod(p1)),,r,medpm(modp),p(medpm)pn,p=gcd(medpm,n)

变种4#

  • 变种4 : 给 N, e, c , 其中 d p 过小。

情形1: q<N0.382#

β=qbit Nbit ,δ=dpbit Nbit 3β<1+β2+2δ,βδn,Nm,

m(m+1)2+n(n1)(2δ+β)2(1β)nm<0

确定 βδ之后,可枚举确定 n 和 m 的取值 (最小值) , m=(1β)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=kb=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<N0.468#

β=qbit Nbit ,δ=dpbit Nbit ,α=ebit Nbit X=2Nα+β+δ1,Y=Nβ,Z=2N1β,m

τ=(1β)2δ2β(1β),σ=1βδ2(1β),t=τm,s=σm

(x,y,z)=(x0,p,q)

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

NCTF 2022 - dp_promax

DP DQ Leaking Attack#

首先需要知道,dpdq是用来加速解密的,其中dp=dmod(p1),dq=dmod(q1).

他们对加密过程没有影响.下面给出解释

对于解密过程我们有m=cdmodN,其中 N=pq,并且p,q都为素数.那么我们有

{mp=cdmodpmq=cdmodq

我们设 d=kφ(p)+dmodφ(p) ,那么我们得到

cd=ckφ(p)+dmodφ(p)=(cφ(p))kcdmodφ(p)

由欧拉定理 cφ(p)1 mod p 可得

cd=1kcdmodφ(p)cdmodφ(p)modp


所以我们设 dp=dmod(p1),dq=dmod(q1),得到

{mp=cdpmodpmq=cdqmodq

再由加纳公式(Garnersformula) 得到

m=mq+q(q1(mpmq)modp)

即可得到明文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 相关攻击

基本原理#

image-20220811223126321

Coppersmith 相关攻击与Don Coppersmith 紧密相关,他提出了一种针对于模多项式(单变量,二元变量,甚至多元变量)找所有小整数根的多项式时间的方法。

这里我们以单变量为主进行介绍,假设

  • 模数为 N ,N 具有一个因子 bNβ,0<β1
  • 多项式 F 的次数为 δ

那么该方法可以在O(cδ5log9(N)) 的复杂度内找到该多项式所有的根x0,这里我们要求 |x0|<cNβ2δ

在这个问题中,我们的目标是找到在模 N 意义下多项式所有的根,这一问题被认为是复杂的。Coppersmith method 主要是通过 Lenstra–Lenstra–Lovász lattice basis reduction algorithm(LLL)方法找到

  • 与该多项式具有相同根 x0
  • 更小系数
  • 定义域为整数域

的多项式 g,由于在整数域上找多项式的根是简单的(Berlekamp–Zassenhaus),从而我们就得到了原多项式在模意义下的整数根。

那么问题的关键就是如何将 f 转换到 g 呢?Howgrave-Graham 给出了一种思路

image-20180717210921382

也就是说我们需要找到一个具有“更小系数”的多项式 g,也就是下面的转换方式

image-20180717211351350

在 LLL 算法中,有两点是非常有用的

  • 只对原来的基向量进行整数线性变换,这可以使得我们在得到 g 时,仍然以原来的 x0 为根。
  • 生成的新的基向量的模长是有界的,这可以使得我们利用 Howgrave-Graham 定理。

在这样的基础之上,我们再构造出多项式族 g 就可以了。

需要注意的是,由于 Coppersmith 根的约束,在 RSA 中的应用时,往往只适用于 e 较小的情况。

Coppersmith攻击(已知p的高位攻击)#

知道p 的高位为p 的位数的约12时即可。

#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,如下

CmemodN

并且我们假设我们知道消息m 的很大的一部分 m0,即 m=m0+x,但是我们不知道 x。那么我们就有可能通过该方法进行恢复消息。这里我们不知道的 x 其实就是多项式的根,需要满足 Coppersmith的约束。

可以参考 https://github.com/mimoo/RSA-and-LLL-attacks

$e Coppersmith$单变量模等式的攻击,如下:

c=memodn=(mbar+x0)emodn,其中mbar=(m>>kbits)<<kbits

|x0|N1e 时,可以在$ \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,两者之间的线性关系如下

M1f(M2)modN

其中 f 为一个线性函数,比如说 f=ax+b

在具有较小错误概率下的情况下,其复杂度为 O(elog2N)

这一攻击由 Franklin,Reiter 提出。

攻击原理

首先,我们知道 C1M1emodN,并且 M1f(M2)modN,那么我们可以知道 M2f(x)eC1modN 的一个解,即它是方程 f(x)eC1 在模 N 意义下的一个根。同样的,M2xeC2 在模 N 意义下的一个根。所以说 xM2 同时整除以上两个多项式。因此,我们可以求得两个多项式的最大公因子,如果最大公因子恰好是线性的话,那么我们就求得了 M2。需要注意的是,在 e=3 的情况下,最大公因子一定是线性的。

这里我们关注一下 e=3,且 f(x)=ax+b 的情况。首先我们有

C1M13modN,M1aM2+bmodN

那么我们有

C1(aM2+b)3modN,C2M23modN

我们需要明确一下我们想要得到的是消息 m,所以需要将其单独构造出来。

首先,我们有式 1

(aM2+b)3=a3M23+3a2M2b+3aM2b2+b3

再者我们构造如下式 2

(aM2)3b3(aM2b)(a2M22+aM2b+b2)modN

根据式 1 我们有

a3M232b3+3b(a2M22+aM2b+b2)C1modN

继而我们有式 3

3b(a2M22+aM2b+b2)C1a3C2+2b3modN

那么我们根据式 2 与式 3 可得

(a3C2b3)3b(aM2b)(C1a3C2+2b3)modN

进而我们有

aM2b=3a3bC23b4C1a3C2+2b3

进而

aM22a3bC2b4+C1bC1a3C2+2b3

进而

M22a3bC2b4+C1baC1a4C2+2ab3=baC1+2a3C2b3C1a3C2+2b3

上面的式子中右边所有的内容都是已知的内容,所以我们可以直接获取对应的消息。

有兴趣的可以进一步阅读 A New Related Message Attack on RSA 以及 paper 这里暂不做过多的讲解。

image-20220811230528251

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(0,n.nbits()e2]

这里所谓 padding 过短,其实就是对应的多项式的根会过小。

攻击原理

我们假设爱丽丝要给鲍勃发送消息,首先爱丽丝对要加密的消息 M 进行随机 padding,然后加密得到密文 C1,发送给鲍勃。这时,中间人皮特截获了密文。一段时间后,爱丽丝没有收到鲍勃的回复,再次对要加密的消息 M 进行随机 padding,然后加密得到密文 C2,发送给 Bob。皮特再一次截获。这时,皮特就可能可以利用如下原理解密。

这里我们假设模数 N 的长度为 k,并且 padding 的长度为 m=ke2。此外,假设要加密的消息的长度最多为 k-m 比特,padding 的方式如下

M1=2mM+r1,0r12m

消息 M2 的 padding 方式类似。

那么我们可以利用如下的方式来解密。

首先定义

g1(x,y)=xeC1g2(x,y)=(x+y)eC2

其中 y=r2r1。显然这两个方程具有相同的根 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)

已知m高位#

攻击条件

这里我们假设我们首先加密了消息 m,如下

CmdmodN

并且我们假设我们知道消息 m 的很大的一部分 m0,即 m=m0+x,但是我们不知道 x。那么我们就有可能通过该方法进行恢复消息。这里我们不知道的 x 其实就是多项式的根,需要满足 Coppersmith 的约束。

可以参考 https://github.com/mimoo/RSA-and-LLL-attacks

e 足够小,且部分明文泄露时,可以采用Coppersmith单变量模等式的攻击,如下:

c=memodn=(mbar+x0)emodnmbar=(m>>kbits)<<kbits

|x0|N1e 时,可以在$ \log N e x_0$。

image-20220811223356315

这里给出了m的高400位,我们只需要推断剩余的72位。记真实的 mhighM+a,则

m3c=(highM+x)3c=0

这个方程的根很小,可以直接求解。

#脚本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。

image-20220811224255692


题目给出p的高位,该后门算法依赖于Coppersmith partial information attack算法, sage实现该算法

在模n的某个因数下的根。我们设p=pHigh+x​, 然后拿去求解方程

p=0(modsthdividesn)

得到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 在模 2512 意义下的值, 又有 e=3, 我们考虑等式

ed1(mod(p1)(q1))3d=1+k(p1)(q1) where k<3

两边对 2512 取模,有

3dLow1+k(npq+1)(mod2512)

np 代替 q, 使上面的方程成为单变量的:

3dLowpp+k(npp2n+p)(mod2512)

这个方程是模意义下的一元二次方程,是可解的。解出来之后得到了 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=pqr ,已知 n,p,d=inv(e,φ(n)),e,c

k(p1)k,qrn,q+rsed01+k(ns+1)(mod2d0nbits())q2sq+n0(mod2d0nbits())

联立可得, (ed01knk)q+kq2+kn0(mod2d0.nbits())
即求解同余方程可得 q 的低 size ( d0) 位,本来是个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)

其中,

  • 必须满足 qNbeta ,所以这里给出了 beta =0.5 ,显然两个因数中必然有一 个是大于的。
  • XXf(x)=q+x 在模 q 意义下的根的上界,自然我们可以选择调整它, 这里其实也表明了我们已知的 q 与因数 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<N0.292 时,我们可以利用该攻击,比 Wiener's Attack 要强一些。

攻击原理

首先

ed1modφ(N)/2

进而有

ed+kφ(N)/2=1

kφ(N)/21mode

φ(N)=(p1)(q1)=qppq+1=Npq+1

所以

k(Npq+1)/21mode

假设 A=N+12y=pq2 ,原式可化为

f(k,y)=k(A+y)1mode

其中

|k|<2edφ(N)<3edN=3eNd<3eNNdelta

|y|<2N0.5

y 的估计用到了pq 比较均匀的假设。这里delta 为预估的小于 0.292 的值。

如果我们求得了该二元方程的根,那么我们自然也就可以解一元二次方程 N=pq,p+q=2y 来得到pq

更加具体的推导,参考 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!
  • 变种1e 很大,dp 很小,且d>2Nβ

    May’s Attack

    假设$ e<\varphi(N),q \le N^{\beta},\beta \le \frac{1}{2}$,因 edp1(modp1),有 edp=1+k(p1)

    对于$ 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,但是我们不知道 ne ,那

  1. 我们可以通过加密 oracle 获取 n
  2. e 比较小 (e<264) 时,我们可以利用 Pollard's kangaroo algorithm 算法获取 e 。这一点比较显然。
    我们可以加密 2,4,8,16。那么我们可以知道

c2=2emodnc4=4emodnc8=8emodn

那么

c22c4modnc23c8modn

故而

c22c4=knc23c8=tn

我们可以求出 knn 的最大公因数,很大概率就是 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=Pemodn 并且把 C 发送给鲍勃,同时假设我们要对爰丽伾加密后的任意密文解密,而不是只解 密 C ,那么我们可以拦截 C ,并运用下列步㡜求出 P :

  1. 选择任意的 XZn ,即 XN 互素
  2. 计算 Y=C×Xemodn
  3. 由于我们可以进行选择密文攻击,那么我们求得 Y 对应的解密结果 Z=Yd
  4. 那么,由于 Z=Yd=(C×Xe)d=CdX=PedX=PXmodn ,由于 XN 互素,我们很容易求得相应的逆 元,进而可以得到 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(N) 次就可以知道这个密文对应的明文消息。
原理
假设
C=PemodN
第一次时,我们可以给服务器发送
C2e=(2P)emodN
服务器会计算得到
2PmodN
这里

  • 2P 是偶数,它的幂次也是偶数。

  • N 是奇数,因为它是由两个大素数相乘得到。
    那么

  • 服务器返回奇数,即 2PmodN 为奇数,则说明 2P 大于 N ,且减去了奇数个 N ,又因为 2P<2N ,因此减去了一个 N ,即 N2P<N ,我们还可以考虑向下取整。

  • 服务器返回偶数,则说明 2P 小于 N 。即 0P<N2 ,我们还可以向下取整。
    这里我们使用数学归纳法,即假设在第 i 次时, xN2iP<xN+N2i
    进一步,在第 i+1 次时,我们可以发送
    C2(i+1)e
    服务器会计算得到
    2i+1PmodN=2i+1PkN

    02i+1PkN<N

    kN2i+1P<kN+N2i+1

根据第 i 次的结果

2xN2i+1P<2xN+2N2i+1

那么

  • 服务㗊返回奇数,则 k 必然是一个奇数, k=2y+1 ,那么 2yN+N2i+1P<2yN+2N2i+1 。与此同时,由于 P 必然存 在,所以第 i+1 得到的这个范围和第 i 次得到的范围必然存在交集。所以 y 必然与 x 相等。
  • 服务器返回偶数,则 k 必然是一个偶数, k=2y ,此时 y 必然也与 x 相等,那么 2xN2i+1P<2xN+N2i+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 AttackRSA 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字 节,高位用 x00 填充。
将加密的内容拿去解密会得到 m ,但是回显最高位是 x00 ,构造密文 c2e , 解密会得到 2mmodn
不断构造密文 c2ie ,当最高位不是 ×00 时,记录下值为 x ,则说明 mx>2kbit
然后再相应缩小 x ,可以利用二分法,比如当第一次拿到 mx>2kbit ,那么有 m(x2)<2kbit ,因此新的 x 必定满足 x2<x<x ,接着尝试 x+x22 就好。
最終可以找到一个 X ,满足 mX<2kbitm(X+1)>2kbit ,由于是整除, 所以会有误差,最后的 m2kbitx 附近。
参考: Pwnhub - pkcs4

RSA Byte Oracle#

假设目前存在一个 Oracle,它会对一个给定的密文进行解密,并且会给出明文的最后一个字节。那么给定一个加密 后的密文,我们只需要 log256n 次就可以知道这个密文对应的明文消息。
原理
这个其实算作 RSA parity Oracle 的扩展,既然可以泄露出最后一个字节,那么按道理我们获取密文对应明文的次数 应该可以咸少。
假设
C=PemodN
第一次时,我们可以给服务器发送
C256e=(256P)emodN
服务器会计算得到
256PmodN
这里

  • 256P 是偶数。

  • N 是奇数,因为它是由两个大拜数相乘得到。

由于 P 一般是小于 N 的,那么 256P;mod;N=256Pkn,k<256 。而且对于两个不同的 k1,k2 ,我们有 256Pk1n256Pk2nmod256
我们可以利用反证法来证明上述不等式。同时 256Pkn 的最后一个字节其实就是 kn 在模 256 的情况下获取的。那么,其实我们可以首先枚举出 0~255 情况下的最后一个字节,构造一个 k 和最后一个字节的映射表 map
当服务器返回最后一个字节 b ,那么我们可以根据上述构造的映射表得知 k ,即减去了 kN ,即 kN256P(k+1)N
此后,我们使用数学归纳法来获取 P 的范围,即假设在第 i 次时, xN256iP<xN+N256i
进一步,在第 i+1 次时,我们可以发送

C256(i+1)e

服务器会计算得到

256i+1PmodN=256i+1PkN0256i+1PkN<NkN256i+1P<kN+N256i+1

根据第 i 次的结果

256xN256i+1P<256xN+256N256i+1

我们这里可以假设 k=256y+t ,而这里的 t 就是我们可以通过映射表获取的。

256yN+tN256i+1P<256yN+(t+1)N256i+1

与此同时,由于 P 必然存在,所以第 i+1 得到的这个范围和第 i 次得到的范围必然存在交集。
所以 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 ,得到 256emodn。 
由于有大量除法运算,为保证精度,将中间过程用 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 低位,考虑

{(c(21e1modN1))d1modN1}(mod2)m21m(21modN1)mod2=(i=0logm1ai2i)21mod2=[2(i=1logm1ai2i1)+a020]21mod2=i=1logm1ai2i1+a02021mod2a1+a02021y(mod2)y(a020)21=(m21mod2)(a020)21a1(mod2)

类似的

{(c(22e2modN2))d2modN2}(mod2)m22m(22modN2)mod2=(i=0logm1ai2i)22mod2=[22(i=2logm1ai2i2)+a121+a020]22mod2=i=2logm1ai2i1+(a121+a020)22mod2a2+(a121+a020)22y(mod2)y(a121+a020)22=(m22mod2)(a121+a020)22a2(mod2)

我们就可以使用前 i1 位与 oracle 的结果来得到第 i 位。注意这里的 2121N1 的逆元。所以 对剩下的位,有

{(c(2ieimodNi))dimodNi}(mod2)m2iai(m2imod2)j=0i1aj2j(mod2),i=1,2,,logm1

其中 2i2iNi 的逆元。
就可以逐步恢复原文所有的位信息了。这样的时间复杂度为 O(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)))

参考#

Common Private Exponent(共私钥指数攻击,d相同)

加密用同样的私钥并且私钥比较短,从而导致了加密系统被破解。

假定:

{e1d=1+k1φ(N1)e2d=1+k2φ(N2)erd=1+krφ(Nr)

其中, N1<N2<<Nr<2N1
构造格:

Br=[Me1e2er0N10000N20000Nr]

其中 M=Nr12
再利用LLL算法进行规约得到 |b1|=Md ,则 d=|b1|M ,从而解密密文得到明
文。

  • 使用条件:

d<Nrδr,δr<1212(r+1)logNr(6)

#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组

g=gcd(p1,q1),λ(n)=φ(n)g,s=1pq

且有 edkλ(n)=1 ,得到 edgkn=g+ks (1)
e1 对应 k1e2 对应 k2 ,则有 k2d1e1k1d2e2=k2k1 由(1)(2)有:

{e1d1gk1n=g+k1sk2d1e1k1d2e2=k2k1e1e2d1d2g2e1d1gk2ne2d2gk1n+k1k2n2=(g+k1s)(g+k2s)

上述等式组也可表示为

bL2=[k1k2,k2d1g,k1d2g,d1d2g2][nM1n0n20M1e1M2e1e1n00M2e2e2n000e1e2]=[k1k2n,M1k2(g+k1s),M2g(k2k1)(g+k1s)(g+k2s)](M1=n1/2,M2=n1+α2,dnα2)

对部分参数进行上界估计, k 上界近似于 dNα2|s| 上界 N1/2,g 一般 相对极小因此上面的矩阵表示 BA=C 中, C 的每个元的size都近似 n1+2α2 ,所以 |C|2n1+2α2
B 作为格基的格中,最短向量由Minkowski Bounds知
4det(B)1/42n(13/2+α2)/4
因此只要满足 n1+2α2<n(13/2+α2)/4 即可将问题转化为 SVP (α2<514)

 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

参考:De1CTF 2020 - easyRSA

  • 给定3组

类似2组情况,其中

b=[k1k2k3,d1gk2k3,k1d2gk3,d1d2g2k3,k1k2d3g,k1d3g,k2d3g,d1d2d3g3]

L3=[1N0N2000N3e1e1e1Ne0e1Ne1N20e2e2N0e2N0e2N200e1e20e1e2e1e2e1e2N000e3e3Ne3Ne3N30000e1e30e1e3N00000e2e3e2e3N000000e1e2e3]×D

其中

D=diag(N3/2,N,N(3/2)+α3,N1/2,N(3/2)+α3,N1+α3,N1+α31)

参考:3kCTF - RSA Textbook

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

多项式RSA#

在整数RSA原理基础上将多项式代入分析:
在有限域上选取两个不可约多项式 g(p),g(q)g(n)=g(p)g(q) ,计算出 g(n) 的欧拉函数 φ(g(n))=φ
选取一个整数 e 作为公钥, eφ 是互緟的,那么对于明文 g(m) ,加密过程为 g(m)eg(c)(modg(n))
计算私钥 d 满足 ed1(modφ) ,则 g(c)d(g(m)e)dg(m)edg(m)φ+1(modg(n))
同样考虑 g(n)g(m) 互素,欧拉定理对于多项式亦成立,
得到 g(m)φ+1g(m)(modg(n)) ,所以 g(c)dg(m)(modg(n))
显然RSA对于整数的体制可以适用于有限域上的多项式。
太注意:
对于素数 x,φ(x)=x1 ,但是对于不可约多项式 g(x),φ(g(x))=pn1 。 (此 pGF(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)

参考:

0ctf - babyrsa

watevrCTF 2019 - Swedish RSA

InCTF 2020 - PolyRSA

Polynomial based RSA

Crypto CTF2020 - Decent RSA

SecurityFest CTF 2022 - small rsa

Weak prime factors ( p 具线性特征)#

适用情况: p 满足 ap=u0+M1u1++Mkuk
先根据 n 确定 M 的大小,再根据 M 选取符合要求的 kc ,然后构造一个格如 下:

M(L)=[1000CM2k0100CM2k10001CM0000CN]

用LLL算法进行格基规约,将规约后的某个向量作为多项式系数,再对多项式进行 分解,即可完成对 n 的分解。

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=prq

  • 情形1
    条件: (N,e) 满足 exφ(N)y=z ,其中 x|z| 为小参数。 f(x)=exz0(modpr1)
    计算 gcd(exz,N)=g, 则

p={g1r1, if g=pr1g1r, if g=prNg, if g=pr1q

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
    条件: 小 |d1d2||d1d2|<Nr(r1)(r+1)2
    f(x)=e1e2(d1d2)(e2e1)0(modpr1)
    等价于 g(x)=xa0(modpr1) ,其中 a(e2e1)(e1e2)1(modN)
    计算 gcd(e1e2x(e2e1),N)=g ,则

p={g1r1, if g=pr1g1r, if g=prNg, if g=pr1q

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
条件: N1=p1rq1,N2=p2rq2 ,小 |p1p2||p1p2|<p12rq1q2

|N2N1q2q1|=q1q2|p1rp2r|q12p1r<12q12

利用 N2N1 的连分数展开对应的渐进分数逼近 q2q1

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=prq

D^3CTF 2022 - d3factor

RSA-CRT#

  • 错误模攻击 α=q(q1modp),β=p(p1modq)
    利用错淏模注入技术得到错淏签名 σ ,即 σ=(σpα+σqβ)modN
    对生成的两种签名 σσ 使用CRT可计算出 v=(σpα+σqβ)mod(NN)
    针对 l5 的编码后消息进行分析,通过计算签名对 (σ,σ) 分解 N 的攻击方法:
  1. 对所有的 i ,计算出对应的整数 vi=CRTN,N(σi,σi) ,这些对应的 vi 构成 Zl 上的向量 v=(v1,,vi)
  2. 利用LLL定理计算出垂直于向量 v 的正交格 v 的规约基 b1,,bl1 ,其中所有的向量和格的分布都是在 Zl 内。通过对存在于 Z1+l 中的格应用LLL定理,即对如 下矩阵使用LLLL定理:

(kv110kvl01)

其中 k 为合适的大常量,并去除计算出来的向量的第 1 个元素;
3. 前 l2 个向量 b1,,bl2 将生成秩为 l2 的格 L ,再次利用LLL定理来计算出正交格 (L) 的规约基 x,y 。同样可以通过对如下矩阵使用LLL定理得到对应 的规约基:
同步㵵2,保留计算出来的向量的最后 l 个元素;
4. 将所有长度不超过 lN 的并在 (L) 内的向量 z=ax+by 列举出来,对符合条件的向量 z, 计算出 gcd(vz,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)

n=p1k1p2k2pmkmφ(n)=φ(p1k1)φ(p2k2)φ(pmkm)=(p1k11(p11))(p2k21(p21))(pmkm1(pm1))

  • next_prime0
    根据素数定理,表数的平均间隔为: xπ(x)ln(x) ,因此常见的下一个素数比当前素数大一点,一般不会超过 1500 。
  • 变种1: n=pqnextprime(p)nextprime(q)
    费马因式分解
  • 给 e,p,c

cme(modn)c1c(modp)me(modp) 亽 ed11(mod(p1)) ,有 mcd(modn)c1d1(modp)

  • e,d,modinv(q,p),c
    已知: p,q 同比特位数。
    cf=q1modp ,有 qcf=1(modp)

    • ed=1+k(p1)(q1),
      比较比特位数, ke 同长,可爆破 k ,得 φ(n)=(p1)(q1)=ed1k

    • 上式 φ(n)=(p1)(q1)(modp)=(q1)(modp)
      结合 qcf=1(modp) ,即 qcf1=0(modp)
      联立:

      φ(n)=(p1)(q1)=pqpq+1=npq+1cfφ(n)=cf(npq+1)=cfncfpcfq+cfcfφ(n)modp=(cfncfpcfq+cf)modp=00(cfq)+cfmodp=1+cfmodp1+cfφ(n)cf=0(modp)x=1+cfφ(n)cfp

    • 由费马小定理,存在 r 满足 rp1=1(modp)

      rφ(n)=(r(p1))(q1)=1(q1)(modp)=1(modp)

    • 因对于任意 r,k1,k2 ,当 k2k1 因子时, rmodk2=(rmodk1)modk2 , 故 rφ(n)modp=(rφ(n)modx)modp=1modp=kp
      已知 φ(n) ,由 (rφ(n)modx)modp=kp 可得到多组 p 的乘积,计算 gcd 可得到 p;

    • qcf=1(modp) 求模逆可得 q,再用 c 计算出 m

参考:TSG CTF 2020 - Modulus Amittendus

  • gcd(e,φ(n))1
    gcd(e,φ(n))1 时, eφ(n) 不互紘,
    cdmgcd(e,φ(n))(modn)
    gcd(e,φ(n)) 较小时,可以直接对 c 开根,有两种情况:

    • me=c<n ,这种情况直接对 ce 次方即可;

    • me=c>n ,这种情况需要在有限域下对 c 开方,一般先计算
      cp=cmodp,cq=cmodq ,分别求出 cp,cqc 下的 e 次根 (可能 有多个),然后使用CRT遍历所有组合,分别check得出明文。

    gcd(e,φ(n)) 较大时,求 p,qe 次根步癷需要替换为一些有限域开根的 高效算法 (如AMM算法等) 进行计算。
    参考:
    De1CTF2019 - Baby RSA
    0ctf 2016 - RSA?

  • e|(p-1), e|(q-1)
    上面的 gcd(e,φ(n))1 情况不针对 gcd(e,φ(n))=e ,这里对 e|(p1),e|(q1) 的特殊情况进行讨论。
    解题思路即求解 mmodpmmodq ,再通过CRT还原 mmodn 。主要 难点则是在 GF(p) 上求 e 次根。
    在有限域上求r-th root有两个常见算法 (Adleman-Manders-Miller algorithm 和Cipolla-Lehmer algorithm),Namhun Koo提出一种更具一般性的开根算 法,且在 s 足够小的时候更高效 (rs(p1),rs(p1))
    参考: NCTF 2019 - easyRSA (Adleman-Manders-Miller rth Root Extraction Method)
    本题则为 ep1 (或 q1 ) 的最大公约数就是 e 本身,也就是说 e(p1) ,只有对 ce 次方根才行。
    可以将同余方程 mec(modn) 化成

{mec(modp)mec(modq)

  • 然后分别在 GF(p)GF(q) 上对 ce 次方根,再用CRT组合一下即可得到 在 modn 下的解。
    问题是,如何在有限域内円根?
    这里 ep1q1 都不互嗉,不能简单地求个逆元就完事。
    这种情况下,开平方根可以用 Tonelli-Shanks algorithm,Wiki说这个算法可 以扩展到开 n 次方根。
    在这篇paper里给出了具体的算法: Adleman-Manders-Miller $x$ th Root Extraction Method
    这个算法只能开出一个根,实际上开 e 次方,最多会有 e 个根(这题的情况下 有 0×1337 个根)。
    如何找到其他根?
    StackOverflow - Cube root modulo P 给出了方法。
    如何找到所有的 primitive 0×1337 th root of 1 ?
    StackExchange - Finding the n-th root of unity in a finite field 给出了方 法。

    Exploit (以 e=0×1337 为例)

    • 先用 Adleman-Manders-Miller x th Root Extraction Method 在 GF(p)GF(q) 上对 ce 次方根,分别得到一个解。大概不到10秒。
    • 然后去找到所有的 0×1336 个 primitive nth root of 1 ,乘以上面那个解,得到所有的 0×1337 个解。大概 1 分钟。
    • 再用CRT对 GF(p)GF(q) 上的两组 0×1337 个解组合成 modn 下的解,可以得到 0×13372=24196561modn 的解。最后能通过 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算法的模数, δ 是多项式的阶。

      Ni<Ni+1,δiN(i=1,2,,k)

    • 多项式方程组表示如下,目的是求解 x :

      {f1(x)0(modN1)f2(x)0(modN2)fk(x)0(modNk)

    • 求解条件
      Alexander May, Maike Ritzenhofent提出一种求解方法,简单地说当多项式的阶 δ 满足以下情况时可解( δ 是多项式的阶):

      i=1k1δi1

    • 具体描述:
      (fi,δi,Ni)(i=1,2,,k) 作为SMUPE问题的首一多项式组,
      定义 M=i=1kNiδδi,δ=lcm(δi)(i=1,2,,k)
      则SMUPE问题可以在 O(δ6log2M) 复杂度解决。

      参考:2019红帽杯 - 精明的Alice

  • 反溸数 (emirp数)
    已知: q= reverse _x(p)x 为进制数。
    爆破思路类似RSA parity oracle。 p,q 是bit翻转关系,已知 p 最低的 k 位,则已知 q 最高的 k 位。 假设已知 k 位的 p,q ,记为 ph,qh ,利用不等式

phqh210242k<=n<(ph+1)(qh+1)210242k , 

逐位向低地址懪破,不断收缩不等式的范围,最終可求得 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)
  • 4p1 method
    对使用一类特定䋤数乘积的模数的分解。 会将其视为 RSA 的后门之一,称之为 RSA backdoor 。
    • QiCheng Prime

Ds={3,11,19,43,67,163}

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 值。

参考:

浅谈 QiCheng Prime

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

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

参考

手撕PEM密钥

详细原理

二十年以来对 RSA 密码系统攻击综述

CTF Wiki - RSA

0xDktb’s Blog

RSA常见攻击方法

[Cryptanalysis of RSA and It’s Variants](http://index-of.es/Varios-2/Cryptanalysis of RSA and It's Variants.pdf)

RSA总结

作者:chinjinyu

出处:https://www.cnblogs.com/vconlln/p/17092254.html

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   vconlln  阅读(915)  评论(2编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
more_horiz
keyboard_arrow_up dark_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示