Bootstrap

RSA加密与解密(一)

RSA加密与解密

简介

RSA算法的基础操作步骤

1.生成公钥和私钥

2.用公钥加密信息 

3.用私钥解密信息

攻击类型

(一)低加密指数攻击

(1)e=3时的小明文攻击:

(2)e=2时的小明文攻击:

(3)e=1时的小明文攻击:

(二)低加密指数广播攻击

(1)基础题型:

(2)进阶之爆破e:

(3)进阶之公因数求解:

(三)低解密指数攻击(Wiener Attack)

(四)共模攻击

(五)模不互素

(六)e与phi不互素

(1)e和phi不互素,但是e和p−1或者q−1互素,转化到模p或者模q下求解

(2)_gcd=gcd(e,phi)=2比较小,直接iroot开e//_gcd次根

(3)gcd(e,phi)=16也比较小,但尝试iroot开根跑不出来,这时考虑有限域内开方来求解

(4)gcd(e,φ(n))=e=1009算是很大了,且gcd(e,p−1) = e = 1009.直接用AMM

(5)n=p^r*q 时,e和phi不互素问题

简介

         在计算机中常用的加密算法分为两类:对称加密算法和非对称加密算法。

1.对称加密

        在对称加密技术中,对信息的加密和解密都使用了相同的密钥Key,也就是说使用同一个密钥Key对数据进行加密和解密。这种加密方法可简化加解密的处理过程,信息交换双方都不必彼此研究和交换专用的加解密算法。如果在交换阶段,密钥Key没有泄露,那么加密数据的机密性和报文的完整性就可以得到保证。

2.非对称加密

        在非对称加密中,不再只有一个密钥Key了。在非对称加密算法中,密钥被分解为一对,一个称为公开密钥,另一个称为私有密钥。对于公钥,可以通过非保密方式向他人公开,而私钥则由解密方保密,不对别人公开。

        最具有代表性的非对称加密方式就是RSA公钥密码体制。

RSA算法的基础操作步骤

1.生成公钥和私钥

        生成公钥PK和私钥SK的步骤如下:

        (1)随意选择两个大的素数P、Q,P不等于Q。

        (2)将P、Q两个素数相乘得到一个N,即N=P*Q

        (3)将P、Q分别减一,再相乘,得到一个数T,即T=(Q-1)*(P-1)

        (4)选择一个整数E,作为一个密钥,使E与T互质(即E与T的最大公约数为1),且E必须小于T。

        (5)根据公式D*E mod T = 1 ,计算出D的值,作为另一个密钥。

        (6)通过以上的步骤就可以求出N,E,D这三个数据,其中(N,E)作为公钥,(N,D)作为私钥。

2.用公钥加密信息 

        发送信息的一方收到公钥PK后,就可以通过公钥PK对数据进行加密,加密的操作步骤如下图所示,其中明文为:M,密文为:C

        明文:M

        加密:M^EmodN=C

        密文 :C

3.用私钥解密信息

        接收方持有私钥(N,D)在接受到密文C后,既可以通过私钥解密,得到明文M,解密过程如下:

        密文:C

        解密:C^DmodN=M

        明文:M

攻击类型

(一)低加密指数攻击

(1)e=3时的小明文攻击:

        特点:e=3,m很小,n很大

        1.当 e=3 时,如果明文过小,导致明文的三次方仍然小于n,那么通过直接对密文开三次方即可得到明文。

        即:C=m^e modn,如果e=3,且m^e<n,则:C=m^em=\sqrt[3]{C}

        2.如果明文的三次方比n大,但不是足够大,那么设k有:C=m^e+k*n

        爆破k,如果C−k∗n或者C+k∗n能开三次根式,那么就可以直接得到明文。

        关键代码:

from libnum import*   #python第三方库
from gmpy2 import*    #python第三方库
n= ......
c= ......
i=0
while 1:
    if(iroot(c+i*n,3)[1]==1):           #或者 iroot(c-i*n,3)
        print(iroot(c+i*n,3)[0])
        break
    i=i+1

(2)e=2时的小明文攻击:

        e=2时,直接将密文C开平方获得解

        由于e只有2,相当于把明文m平方而已,得到的C也比n小很多。尝试直接将C开根号看能否得到明文。

from libnum import*   #python第三方库
from gmpy2 import*    #python第三方库
c=......              #C的值
m=isqrt(c)            #开平方根
#m=iroot(c,2)[0]      #开C的二次方根

print(n2s(m))     #字符串转数字

(3)e=1时的小明文攻击:

        加密过程:C≡m mod n ,明文与密文同模

        所以有:m=C+n*k,爆破k

from libnum import*
n=....
c=....
max_num = 7   #设置遍历上限
for k in range(max_num):
    m = c + n*k
    print(n2s(m))

(二)低加密指数广播攻击

        如果选取的加密指数较低,并且使用相同的加密指数给一个接受者的群发送相同的信息,那么可以进行广播攻击得到明文

        特点:n非常大,e一般很小。

(1)基础题型:

        加密指数e非常小。一般拿到的是多组n和c,且只有一个e,e还很小,且模数n不同,但使用相同的加密指数e进行多次加密。

import gmpy2
import os
from functools import reduce 
from Crypto.Util.number import long_to_bytes 
def CRT(items):
    N = reduce(lambda x, y: x * y, (i[1] for i in items))
    result = 0
    for a, n in items:
        m = N // n
        d, r, s = gmpy2.gcdext(n, m)
        if d != 1:
            raise Exception("Input not pairwise co-prime")
        result += a * s * m
    return result % N, N
# e, n, c
e = 
n=[]
c=[]
data = list(zip(c, n))     # zip()将对象打包成元组
x, n = CRT(data)
m = gmpy2.iroot(gmpy2.mpz(x), e)[0].digits()
print('m is: ' + long_to_bytes(m))

(2)进阶之爆破e:

        题目中不直接给出e。一份明文使用不同的模数n,相同的加密指数e进行多次加密,可以拿到每一份加密后的密文和对应的模数n、加密指数e。

c_1=m^e (mod n_1 )

c_2=m^e (mod n_2 )

c_3=m^e (mod n_3 )

对上述等式运用中国剩余定理求解出m的e次方,然后爆破e即可求出明文。

中国剩余定理:设n1,n2,n3是两两互素的正整数,M=n_1*n_2*n_3M_i=M/n_i (i=1,2,3)

则同余式组:m^e=C_i modn_i (i=1,2,3)

有唯一解:m^e=X=C_1 M_1 y_1+C_2 M_2 y_2+C_3 M_3 y_3 (modM)

其中M_i y_i=1 mod n_i(i=1,2,3)

import gmpy2
import os
from functools import reduce
from Crypto.Util.number import long_to_bytes
def CRT(items):
    N = reduce(lambda x, y: x * y, (i[1] for i in items)) # n_i 的乘积,N=n1*n2*n3
    result = 0
    for a, n in items:
        m = N // n    #M_i=M/n_i
        d, r, s = gmpy2.gcdext(n, m)
        if d != 1:
            raise Exception("Input not pairwise co-prime")
        result += a * s * m
    return result % N, N
# e, n, c
e = 1
n=[n1,n2,n3……]
c=[c1,c2,c3……]
data = list(zip(c, n))
x, n = CRT(data)
for i in range(1,30):
    e = i
    m = gmpy2.iroot(gmpy2.mpz(x), e)[0].digits()
    print('m is: ' + long_to_bytes(m))

(3)进阶之公因数求解:

        某些题目会给出多组n和c,但是e却不小,比如e=65537,这种情况下不建议使用中国剩余定理求解,可以尝试在n中寻找最大公约数gcd。

        由于模数n只能分解为p和q,所以当n很多时,p或q有相同的风险,因此不同的模数n中可能存在相同的p或者q。求出不同n之间的最大公约数 gcd()从而得到p或q,进而可得d,有私钥d就能得到明文。

import gmpy2
import libnum
 
e = 65537
n0 = 
c0 = 
n1 = 
c1 = 
 
n2 = 
c2 = 
 
n3 = 
c3 = 
 
n4 = 
c4 = 
 
n5 = 
c5 = 
 
n6 = 
c6 = 
 
n7 = 
c7 = 
 
n8 = 
c8 = 
 
n9 = 
c9 = 
 
n10 = 
c10 = 
 
n=[n0,n1,n2,n3,n4,n5,n6,n7,n8,n9,n10……]
c=[c0,c1,c2,c3,c4,c5,c6,c7,c8,c9,c10……]
 
for i in range(len(n)):
    for j in range(len(n)):
        if(i!=j):
            if(gmpy2.gcd(n[i],n[j])!=1):   #对不同的n进行 欧几里得算法,以求出最大公约数(p)
                print(i,j)                 #输出对应的n的序号
                p = gmpy2.gcd(n[i],n[j])
                print("p = ",p)
                q = n[i] // p
                print("q = ",q)
                d = gmpy2.invert(e , (p-1)*(q-1))
                print("d = ",d)
                m = pow(c[i],d,n[i])
                print("m = ",m)
print(libnum.n2s(int(m)))

(三)低解密指数攻击(Wiener Attack)

        与低加密指数相同,低解密指数可以加快解密的过程,但同时也带来了安全问题。Wiener表示如果满足:d<\frac{1}{3}n^\frac{1}{4}

        那么一种基于连分数(数论中的问题)的特殊攻击类型,就可以危害RSA的安全,此时需要满足:q<p<2*q

        如果满足上述条件,通过Wiener Attack 可以在多项式时间中分解n。

        攻击原理:理论基础(勒让德定理):当e过大或过小时,\frac{e}{n}的连分数展开会逐渐趋向于\frac{k}{d}

phi=(p-1)×(q-1)=p×q-p-q+1=n-(p+q)+1 

\because p×q\gg p+q

\therefore phi\approx n

e*d=1(mod phi)

由(4)两边同时除d*phi

可得:

\frac{e}{phi}-\frac{k}{d}=\frac{1}{d*phi}

\because\frac{1}{d*phi}\approx 0

\therefore \frac{e}{phi}\approx \frac{k}{d}

(p+q)=n-phi+1\approx n-\frac{e*d}{k}+1

        虽然在此式子中无法得知d和k的具体值,但是由于连分数逼近原理可以得到两者之间的比值,所以(p+q)是可以得到相对接近的值的

        再通过构造方程x^2-(p+q)x+p*q=0

        韦达定理:x_1+x_2=(p+q),x_1*x_2=n

        求解方程即可得到p,q的值

        特点:e过大或过小,且n分解无望

        下载工具:rsa-wiener-attack

        github上有公开的攻击代码。

        将解密的代码放入wiener-attack的目录下即可。

        下载网址: GitHub - pablocelayes/rsa-wiener-attack: A Python implementation of the Wiener attack on RSA public-key encryption scheme.

        使用:wiener攻击脚本用于求出d的值(注意,这里要将攻击脚本和rsa-wiener-attack的py文件放在同一个目录下)

import  RSAwienerHacker
n = 
e = 
d =  RSAwienerHacker.hack_RSA(e,n)
if d:
    print(d)

        代码实现:

(SageMath)

c = continued_fraction(e/n) #直接输出e/n的连分数展开的数组
alist = c.convergents() #求e/n的连分数逼近
(Python)
from Crypto.Util.number import long_to_bytes
from gmpy2 import invert,isqrt
from libnum import n2s,s2n
#低解密指数攻击
#条件:d<pow(n,0.25)/3
def RSA_wiener (n,e,c):
    #连分数逼近,并列出逼近过程中的分子与分母
    def lian_fen(x,y):
        res = []
        while y:
            res.append(x//y)
            x,y = y,x%y
        resu = []
        for j in range(len(res)):
            a,b = 1,0
            for i in res[j::-1]:
                b,a = a,a*i+b
            resu.append((a,b))
        if resu[0] == (0,1):
            resu.remove((0,1))
        return resu[:-1]
    lianfen = lian_fen(e,n)
    def get_pq(a,b,c):
        par = isqrt((n-phi+1)**2-4*n)
        x1,x2 = (-b + par) // (2 * a), (-b - par) // (2 * a)
        return x1,x2
    for (k,d) in lianfen:
        phi = (e*d-1)//k
        p,q = get_pq(1,n-phi+1,n)
        if p*q == n:
            p,q = abs(int(p)),abs(int(q))
            d = invert(e,(p-1)*(q-1))
            break
    return m,long_to_bytes(pow(c,d,n))

(四)共模攻击

        共模攻击,Common Modulus Attack,也称为同模攻击。同模攻击利用的大前提就是,RSA体系在生成密钥的过程中使用了相同的模数n。

        对于同一条明文m, A和B对其进行加密:

A:C_1=M^{e_1} mod n

B:C_2=M^{e_2} mod n

        如果,此时有一个攻击者,同时监听了A和B接收到的密文C_1,C_2。因为模数不变,以及所有的公钥都是公开的,那么利用同模攻击,就可以在不知道d_1,d_2的条件下破解明文M。

from libnum import*   #python第三方库
from gmpy2 import*    #python第三方库
n = 
c1 = 
c2 = 
e1 = 
e2 = 
s = gcdext(e1,e2)    #gmpy2.gcdext(),扩展欧几里得算法,返回tuple元组,满足s[1]*e1+s[2]*e2=1
m = pow(c1,s[1],n)*pow(c2,s[2],n)%n   #获取明文m

print(n2s(m))

(五)模不互素

        存在多个模数,且 gcd(n1,n2)!=1

import gmpy2
import libnum
def gcd(a, b):
    if a < b:
        a, b = b, a
    while b != 0:
        temp = a % b
        a = b
        b = temp
    print a
    return a
def n2s(num):
    t = hex(num)[2:]
    if len(t) % 2 == 1:
        t = '0' + t
    return ''.join([chr(int(b, 16)) for b in [t[i:i + 2] for i in range(0, len(t), 2)]])
n1=
n2=
e=
c1=
c2=
q=gcd(n1,n2)
p1=n1//q
p2=n2//q
phi=(p1-1)*(q-1)
d=gmpy2.invert(e,phi)
m=pow(c1,d,n1)
print(n2s(m))

(六)e与phi不互素

        gcd(e,phi)比较小时可以考虑iroot直接开根,当直接开根跑不出来时,考虑有限域内开方

        gcd(e,phi)很大时,考虑AMM算法

(1)e和phi不互素,但是e和p1或者q1互素,转化到模p或者模q下求解

from Crypto.Util.number import *
p=
q=
e=
c = 
n = p*q
# _gcd = gcd(e,(p-1)*(q-1)) # 65537
gcd_q = gcd(e,q-1)   # 1

d = inverse(e,q-1)
m = pow(c,d,q)
print(long_to_bytes(int(m)))

        因为gcd(e,phi)=e=65537很大,可以用AMM算法求解

(SageMath)
import random
import math
import time
from Crypto.Util.number import bytes_to_long,long_to_bytes
p = 0
#设置模数
def GF(a):
    global p
    p = a
#乘法取模
def g(a,b):
    global p
    return pow(a,b,p)

def AMM(x,e,p):
    GF(p)
    y = random.randint(1, p-1)
    while g(y, (p-1)//e) == 1:
        y = random.randint(1, p-1)
        print(y)
    print("find")
    #p-1 = e^t*s
    t = 1
    s = 0
    while p % e == 0:
        t += 1
        print(t)
    s = p // (e**t)
    print('e =',e)
    print('p =',p)
    print('s =',s)
    print('t =',t)
    # s|ralpha-1
    k = 1    
    while((s * k + 1) % e != 0):
        k += 1
    alpha = (s * k + 1) // e
    #计算a = y^s b = x^s h =1
    #h为e次非剩余部分的积
    a = g(y, (e ** (t - 1) ) * s)
    b = g(x, e * alpha - 1)
    c = g(y, s)
    h = 1
    
    for i in range(1, t-1):
        d = g(b,e**(t-1-i))
        if d == 1:
            j = 0
        else:
            j = -math.log(d,a)
        b = b * (g(g(c, e), j))
        h = h * g(c, j)
        c = g(c, e)
    #return (g(x, alpha * h)) % p
    root = (g(x, alpha * h)) % p
    roots = set()
    for i in range(e):
        mp2 = root * g(a,i) %p
        assert(g(mp2, e) == x)
        roots.add(mp2)
    return roots
# def check(m):
#     if 'flag' in m:
#         print(m)
#         return True
#     else:
#         return False

p=
q=
e=
c = 
n = p*q
mps = AMM(c,e,p)
for mpp in mps:
        solution = long_to_bytes(int(mpp))
        if b'flag' in solution:
        #solution = int(mpp)
            print(solution)

(2)_gcd=gcd(e,phi)=2比较小,直接iroot开e//_gcd次根

import gmpy2
import libnum
p = 
q = 
e = 
c = 
n = p*q
phi = (p-1)*(q-1)
_gcd = gmpy2.gcd(e, phi)
d = gmpy2.invert(e//_gcd, phi)
m_gcd = gmpy2.powmod(c, d, n)
m = gmpy2.iroot(m_gcd1, _gcd)    
flag = libnum.n2s(int(m[0]))
print(flag)

(3)gcd(e,phi)=16也比较小,但尝试iroot开根跑不出来,这时考虑有限域内开方来求解

(SageMath)
from Crypto.Util.number import *
p = 
q = 
c = 
e = 
n = p*q

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()

#定义了一个名为P的多项式环,a是这个环里面的变量,Zmod(p)是在模p下定义了一个整数环。多项式的每一个#系数都是p的倍数,这样才能确保在模p下进行运算时不会出现浮点数。
#参数implementation='NTL'指示SageMath使用NTL(Number Theory Library)作为该环的实现。
#f=a^e-c定义了一个多项式,monic()是将多项式首一化
#f.roots()得到方程的一个根的列表,该列表包含f(x)=a^e-c在模p意义下的所有根。
#列表中的每个元素的格式为 (a,1),在此二元组表示中,1表示多重根,即该解在方程中的出现次数。

flag=[]
for mpp in mps:
    x=mpp[0]
    for mqq in mqs:
        y=mqq[0]
        solution = CRT_list([int(x), int(y)], [p, q])
        flag.append(solution)
for i in flag:
    m=long_to_bytes(i)
    if b'flag'in m:
        print(m)

(4)gcd(e,φ(n))=e=1009算是很大了,且gcd(e,p1) = e = 1009.直接用AMM

(SageMath)
from Crypto.Util.number import *
import random
import math

def onemod(e, q):
    p = random.randint(1, q-1)
    while(powmod(p, (q-1)//e, q) == 1):  # (r,s)=1
        p = random.randint(1, q)
    return p


def AMM_rth(o, r, q):  # r|(q-1)
    """
    x^r % q = o
    :param o:
    :param r:
    :param q:
    :return:
    """
    assert((q-1) % r == 0)
    p = onemod(r, q)

    t = 0
    s = q-1
    while(s % r == 0):
        s = s//r
        t += 1
    k = 1
    while((s*k+1) % r != 0):
        k += 1
    alp = (s*k+1)//r

    a = powmod(p, r**(t-1)*s, q)
    b = powmod(o, r*a-1, q)
    c = powmod(p, s, q)
    h = 1

    for i in range(1, t-1):
        d = powmod(int(b), r**(t-1-i), q)
        if d == 1:
            j = 0
        else:
            j = (-int(math.log(d, a))) % r
        b = (b*(c**(r*j))) % q
        h = (h*c**j) % q
        c = (c*r) % q
    result = (powmod(o, alp, q)*h)
    return result


def ALL_Solution(m, q, rt, cq, e):
    mp = []
    for pr in rt:
        r = (pr*m) % q
        # assert(pow(r, e, q) == cq)
        mp.append(r)
    return mp


def ALL_ROOT2(r, q):  # use function set() and .add() ensure that the generated elements are not repeated
    li = set()
    while(len(li) < r):
        p = powmod(random.randint(1, q-1), (q-1)//r, q)
        li.add(p)
    return li


def attack(p, q, e, check=None):
    cp = c % p
    cq = c % q

    mp = AMM_rth(cp, e, p)
    mq = AMM_rth(cq, e, q)

    rt1 = ALL_ROOT2(e, p)
    rt2 = ALL_ROOT2(e, q)

    amp = ALL_Solution(mp, p, rt1, cp, e)
    amq = ALL_Solution(mq, q, rt2, cq, e)

    if check is not None:
        j = 1
        t1 = invert(q, p)
        t2 = invert(p, q)
        for mp1 in amp:
            for mq1 in amq:
                j += 1
                if j % 1000000 == 0:
                    print(j)
                ans = (mp1 * t1 * q + mq1 * t2 * p) % (p * q)
                if check(ans):
                    return ans
    return amp, amq

def calc(mp, mq, e, p, q):
    i = 1
    j = 1
    t1 = invert(q, p)
    t2 = invert(p, q)
    for mp1 in mp:
        for mq1 in mq:
            j += 1
            if j % 1000000 == 0:
                print(j)
            ans = (mp1*t1*q+mq1*t2*p) % (p*q)
            if check(ans):
                return
    return

def check(m):
    try:
        a = long_to_bytes(m)
        if b'NSSCTF' in a:
            print(a)
            return True
        else:
            return False
    except:
        return False

if __name__ == '__main__':
    e = 1009
    n = 
    c = 
    p = 
    q = 
    cp = c % p
    cq = c % q

    mp = AMM_rth(cp, e, p)
    mq = AMM_rth(cq, e, q)

    rt1 = ALL_ROOT2(e, p)
    rt2 = ALL_ROOT2(e, q)

    amp = ALL_Solution(mp, p, rt1, cp, e)
    amq = ALL_Solution(mq, q, rt2, cq, e)

calc(amp, amq, e, p, q)

(5)n=p^r*q 时,e和phi不互素问题

        先恢复p、q

e*d=1 mod phi,gcd(e,phi)=1

e*x-1=k*phi

        在多项式时间内求解上面方程(copper)求出d.

因为n=p^r*q,所以gcd(e*x-1,n)=gcd(p^{r-1}*(p-1)*(q-1),p^r*q)=p^{r-1}

        用 small_roots() 开r-1次方就能恢复 p

(SageMath)
from gmpy2 import *
hint = 
n = 
e = 
c = 

P.<d> = PolynomialRing(Zmod(n))
f = e*d - 251
res = f.monic().small_roots(X = 2^256,beta = 0.4)

p_4 = gcd(int(f(res[0])),n)
p = iroot(p_4,4)[0]
q = n//p**4
print(f"p,q = {p},{q}")

        接下来就是有限域开方的问题了

(SageMath)
# 开251次方
from Crypto.Util.number import *
import itertools

hint = 
n = 
e = 
c = 
p,q= , 
p_list = [p,q]
n_list = [p**5,q]
print(n_list)

res=[]

for pi in n_list:
    d = inverse(int(e//251),euler_phi(pi))     # 对n_listt 每一个 pi 求欧拉函数
    m = pow(c,d,pi)
    temp = (Zmod(pi)(m).nth_root(251, all=True))
    #print('temp =',temp) # 列表 251
    if temp is not None:
            res.append(temp)
    else:
            print("None")
   
for vc in itertools.product(*res):
    _c = [int(x) for x in vc]
    m = long_to_bytes(int(crt(_c, n_list)))
    if b"flag" in m:
        print(m)

;