RSA 中 e, phi 不互素的解决方法

导言


在一般情况的RSA中,求出 ϕ(N)\phi(N) 就已经接近解出明文了。这个时候我们往往只需要通过 ed1(modϕ(N))ed\equiv1 \pmod {\phi (N)} 即可求出私钥 dd,从而求出明文。但是要直接求逆元 dd,需要满足 gcd(e,ϕ(N))=1gcd(e,\phi(N))=1,也就是 eeϕ(N)\phi(N) 互质的情况。如果不满足,则会大大提高难度。

简单情况( eeϕ(N)\phi(N) 的公约数 tt 比较小,且 mt<Nm^t<N


tt 有多小才算比较小呢,比较显然的是: 如果 mtNm^t \geq N,那么 mt(modN)m^t \pmod N 后再直接在有理数域下开根的结果就不是 mm 了,因为被模掉了,不过这步也能结合后面提到的有限域开根来计算 mm,来降低后者一定计算量

假设 gcd(e,ϕ(N))=tgcd(e,\phi(N))=t ,令 e=e/te'=e/t

memtec(modN)){m}^{e}\equiv{m^{t}}^{e'}\equiv c \pmod N)

d=invert(e,ϕ(N))d=\text{invert}(e',\phi(N)) ,换个写法 (e)1d(modϕ(N))(e')^{-1} \equiv d \pmod{\phi(N)}

所以 mtcd(modN){m}^{t}\equiv {c}^{d} \pmod N

此时 mtm^t 可求且 mt<Nm^t<N 所以可据此直接开方求得 mm

import gmpy2
from Crypto.Util.number import long_to_bytes

phi = (p - 1) * (q - 1)
t = gmpy2.gcd(phi, e)
d = gmpy2.invert(e // t, phi)
M = gmpy2.powmod(c, d, n)
m = gmpy2.iroot(M, e)[0]
print(long_to_bytes(m))

ee(p1)(p-1)(q1)(q-1) 互质


这种情况下,ee(p1)(p-1)(q1)(q-1) 互质,可以将 mec(modN)m^e \equiv c \pmod N 拆解成

{mec(modp)mec(modq)\left\{\begin{array}{rcl}m^{e}&\equiv c \pmod p\\m^{e} &\equiv c \pmod q \\\end{array} \right.

中国剩余定理 - OI Wiki

Using the CRT with RSA

复杂的情况: p1p-1q1q-1ee 的倍数


开始还是一样的操作,拆解为

{mec(modp)mec(modq)\left\{\begin{array}{rcl}m^{e}\equiv c\pmod p\\m^{e}\equiv c \pmod q\\\end{array} \right.

但此时 p1p-1q1q-1ee 的倍数,是倍数的话,就无法直接通过求逆元的办法求出 dd,这个时候就需要一种新的算法 —— AMM 算法

AMM 算法

先从原算法开始,

原算法中只是涉及了开平方根的方法,此时的 e=2e=2pp 为奇素数

对于 x2r(modp)x^{2}\equiv r\pmod p ,则说明 rrpp 的一个二次剩余,令 p1=2stp-1=2^{s}t,则有

r(p1)2r2s1t1(modp)r^{(p-1) \over 2}\equiv r^{2^{s-1}t}\equiv 1 \pmod p

s=1s=1

如果此时的 s=1s=1 则有

rt1(modp)r^t\equiv1 \pmod p

两边同时乘上 rr,再开方即可得到

rt+12±r±x(modp)r^{t+1 \over 2}\equiv \pm\sqrt{r} \equiv\pm x\pmod p

此时套回我们的原式,得到

±mct+12(modp)\pm m\equiv c^{t+1 \over 2}\pmod p

此时我们只需要根据 pp 算出 tt 即可

s>1s>1

回到原式

r2s1t1(modp)r^{2^{s-1}t}\equiv 1 \pmod p

由于此时的 s>1s>1,直接开根我们可以得到两个式子

r2s2t1(modp)r^{2^{s-2}t}\equiv 1 \pmod p

r2s2t1(modp)r^{2^{s-2}t}\equiv -1\pmod p

因为存在两种不同的开方结果,所以我们需要同时乘上一个二次非剩余项 n2s1tkn^{2^{s-1}tk} ,这样便消除了 1-1 根的影响

此式 nnpp 的一个非二次剩余,由 n(p1)21(modp)n^{(p-1) \over 2}\equiv -1\pmod p 到 同上变换为n2s1t1(modp)n^{2^{s-1}t} \equiv-1\pmod pkk 用来控制是否相乘,k=0k=0 时,此二次非剩余式则为 n0=1n^0=1,不做改变;k=1k=1 时,此二次剩余式为 n2s1t1(modp)n^{2^{s-1}t} \equiv-1 \pmod p,为 1-1

得到下式

r2s2tn2s1tk1(modp)r^{2^{s-2}t}n^{2^{s-1}tk}\equiv 1 \pmod p

这样我们便可以继续开方,以此往复,直到迭代成 s=1s=1 的情况,即

rtnt(2k1+22k2+...+2s1ks1)1(modp)r^{t}{n}^{t*(2k_1+2^{2}k_2+...+2^{s-1}k_{s-1})}\equiv 1 \pmod p

然后就和 s=1s=1 的情况一样,两边同时乘上 rr,再开方,得

rt+12nt(k1+2k2+...+2s2ks1)±r±x(modp)r^{t+1 \over 2}n^{t*(k_1+2k_2+...+2^{s-2}k_{s-1})} \equiv \pm \sqrt{r} \equiv \pm x \pmod p

ct+12nt(k1+2k2+...+2s2ks1)±m(modp)c^{t+1 \over 2}n^{t*(k_1+2k_2+...+2^{s-2}k_{s-1})}\equiv\pm m \pmod p

这里的 k1,k2k_1,k_2 等,只能够在一步步运算中得出

AMM 算法 ex

在 AMM 算法基础上,有人将开平方根的方法拓展到了开 nn 次方根

对于 xer(modp)x^{e}\equiv r \pmod p,则说明 rrpp 的一个 ee 次剩余,令 p1=estp-1=e^{s}t,则有

很明显,我们前提条件是能整除 p1p- 1

r(p1)eres1t1(modp)r^{(p-1) \over e}\equiv r^{e^{s-1}t}\equiv 1\pmod p

此时,我们可以找到一个数 δ\delta ,满足 t(eδ1)t|(e\delta -1),所以上式可以写为

res1(eδ1)1(modp)r^{e^{s-1}(e\delta -1)}\equiv 1\pmod p

s=1s=1

同样的,这里的 s=1s=1 时,直接有

reδ11(modp)r^{e \delta -1} \equiv 1 \pmod p

两边同时乘上 rr,再开 ee 次方

rδrex(modp)r^{\delta} \equiv \sqrt[e]{r} \equiv x \pmod p

s>1s>1

此时我们可以和 e=2e=2 的情况类似,构造一个 ee 次非剩余的集合,

Ki=ρip1e=ρies1t (i的范围为[0,e1])K_i=\rho^{{i}*{p-1\over{e}}}=\rho^{i*e^{s-1}t}\ (i \text{的范围为}[0,e-1])

Kie=ρiest=ρi(p1){K_{i}}^{e}=\rho^{i*e^{s}t}=\rho^{i*(p-1)}

根据欧拉定理 ρp11(modp)\rho^{p-1}\equiv1\pmod p

kie1(modp){k_{i}}^{e}\equiv 1 \pmod p

那么 KiKei=ρep1e=ρp1K_i*K_{e-i}=\rho^{e*{p-1 \over e}}=\rho^{p-1}

根据欧拉定理也得

KiKei1(modp)K_i*K_{e-i} \equiv 1\pmod p

KiK_iKeiK_{e-i} 互为逆元

回到原式

res1(eδ1)1(modp)r^{e^{s-1}(e\delta -1)}\equiv 1\pmod p

两边同时开 ee 次方得到一个集合 KK 中的数设为 KejK_{e-j} (根据群论性质得)

res2(eδ1)Kej(modp)r^{e^{s-2}(e\delta -1)}\equiv K_{e-j} \pmod p

后续也和 e=2e=2 的情况类似,只需要将上式不断乘上 KjK_j,开 ee 次方即可

res2(eδ1)KjKejKj1(modp)r^{e^{s-2}(e\delta -1)}K_j\equiv K_{e-j}K_j \equiv 1\pmod p

res2(eδ1)ρjes1t1(modp)r^{e^{s-2}(e\delta -1)}\rho^{j*e^{s-1}t}\equiv 1\pmod p

以此往复,最终可以迭代成

r(eδ1)ρt(ej1+e2j2+...+es1js1)1(modp)r^{(e\delta -1)}\rho^{t*(ej_1+e^{2}j_2+...+e^{s-1}j_{s-1})}\equiv 1\pmod p

再两边同时乘 rr,开 ee 次方

rδρt(j1+ej2+...+es2js1)x(modp)r^{\delta}\rho^{t*(j_1+ej_2+...+e^{s-2}j_{s-1})}\equiv x\pmod p

相当于

cδρt(j1+ej2+...+es2js1)m(modp)c^{\delta}\rho^{t*(j_1+ej_2+...+e^{s-2}j_{s-1})}\equiv m \pmod p

此时我们便得到了其中一个根

剩余的解可以通过不断乘上集合 KK 即可

Kie1 (mod p){K_i}^{e} \equiv 1\ (mod\ p)
所以 (mKi)ec(modp){(mK_i)}^{e}\equiv c \pmod p
所以 mKimK_i 也为一组解

复杂度分析

TR;DL

O(log4q+rlog3q)\mathcal{O(log^4 q + rlog^3 q)}

逐步分析

为了找到一个 rr 次非剩余 ρ\rho,需要检查 ρq1r1\rho^{q-1 \over r} \neq 1。这个计算需要 O(log3q)\mathcal{O}(\log^3 q) 的时间。如果我们对超过 O(1)logq\mathcal{O}(1) \log q 个不同的随机选择的 ρ\rho 做这个操作,那么至少有一个 ρ\rho 将给出一个 rr 次非剩余的概率大于 1(1q)O(1)1 - \left(\frac{1}{q}\right)^{\mathcal{O}(1)}。因此,找到一个 rr 次非剩余的期望时间是 O(log4q)\mathcal{O}(\log^4 q)

循环外的工作仅仅是少量的指数运算。因此,它需要 O(log3q)\mathcal{O}(\log^3 q) 的时间。要计算 brti1b^{r^{t-i-1}},需要 O((ti1)logrlog2q)\mathcal{O}((t-i-1)\log r \log^2 q) 的时间。因为总共有 1+2++(t1)1 + 2 + \cdots + (t-1) 步,所以需要 O(t2logrlog2q)\mathcal{O}(t^2 \log r \log^2 q) 的时间。

要计算离散对数 logad\log_a d,使用蛮力搜索需要 O(rlog2q)\mathcal{O}(r \log^2 q) 的时间。因为最坏情况下有 t1t-1 个离散对数,所以需要 O(trlog2q)\mathcal{O}(tr \log^2 q) 的时间。因此,最终的估计是 O(log4q+rlog3q)\mathcal{O}(\log^4 q + r \log^3 q)注意,如果 rr 足够大,算法无法在多项式时间内运行。

AMM 之后的操作

我们解出了两方程的多组解

{mec(modp)mec(modq)\left\{\begin{array}{rcl}m^{e} &\equiv c \pmod p\\m^{e} &\equiv c \pmod q \\\end{array} \right.

将1式的解和2式的解两两做一次 CRT,就有一组可以得到正确的 flag

Exploit

import random 
import time 
from Crypto.Util.number import * # pip install pycryptodome
import gmpy2 # pip install gmpy2
from tqdm import tqdm # pip install tqdm

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

def AMM_rth(x, e, N):
    assert ((N - 1) % e == 0)
    p = onemod(e, N)
    
    t = 0
    s = N - 1
    while (s % e == 0):
        s = s // e
        t += 1
    k = 1
    
    while ((s * k + 1) % e != 0):
        k += 1
    delta = (s * k + 1) // e

    a = gmpy2.powmod(p, e ** (t - 1) * s, N)
    b = gmpy2.powmod(x, e * delta - 1, N)
    c = gmpy2.powmod(p, s ,N)
    h = 1
    for i in tqdm(range(1, t)):
        d = gmpy2.powmod(b, e ** (t - 1 -i), N)
        if d == 1:
            j = 0
        else:
            j = (- math.Log(d, a)) % e
        b = b * (c ** (e ** j)) % N
        h = h * (c ** j) % N
        c = c ** e % N
    result = gmpy2.powmod(x, delta, N) * h
    return result

def ALL_ROOT2(r, q):  
    list1 = set()
    while(len(list1) < r):
        p = gmpy2.powmod(random.randint(1, q - 1), (q - 1) // r, q)
        list1.add(p)
    return list1

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

def calc(mp, mq, e, p, q):
    i = 1
    j = 1
    t1 = gmpy2.invert(q, p)
    t2 = gmpy2.invert(p, q)
    for mp1 in mp:
        for mq1 in mq:
            j += 1
            if j % 100000 == 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 flag_info in a:
            print(a)
            return True
        else:
            return False
    except:
        return False

n = 
c = 
p = 
q = 
e = 
flag_info = b'flag{'

cp = c % p
cq = c % q

print("start")

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)
print("run over")

一点点魔法

回到一个月前画的饼

强网杯 2023 线上赛 Crypto -- not only rsa

ee, ϕ(n)\phi(n) 不互素,后面这块专门出一期来讲(当时画的饼,现在实现了),对于本题又发现 nn 使用 factordb / factor(),可分为: n=p5n=p^5 ,结合 RSA 加密逻辑所以我们有 C=me(modp5)C = m ^ e \pmod{p^5} ,选择在有限域下开根

Sagemath

sage: import libnum
....: Zmn = Zmod(p ** 5)
....: m_list = Zmn(c).nth_root(e, all = True) # 必要
....: for i in m_list:
....:     m = libnum.n2s(int(i))
....:     if b'flag{' in m:
....:         print(m)

得到 flag,sage 的 nth_root() 不只有有限域开根,还附带了类似 Hensel's lemma 中的 lifting_roots,不过我还并未读过 nth_root() 的源码,链接放在了文章的 Reference 部分,感兴趣的朋友可以读读看,欢迎交流

特别感谢:

N1gh7ma|2e 师傅提供文档帮助

END.