0%

RSA--e与phi不互素

前言

我们知道,在RSA系统中,解密依赖于欧拉定理

以及同余等式

,那么在已知

下,容易得到

但是当不互素的情况下,私钥就无法正常计算,在此之下想要顺利求解明文,衍生了一系列问题,本文基于笔者日常中遇到的相关题型,给出对应的处理方法

从简单情况出发

我们不妨令

那么考虑转化为公钥指数为的RSA,有

显然此时我们能够计算

如果不是很大(个位数),当的时候,我们直接能得到

,直接开次方就能得到

如果不那么大的话,我们姑且还能尝试爆破

,但是如果数据不支持,大到爆破不了,我们就得进一步观察已有数据中的代数结构了

如果同时与互质(CRT加速RSA问题)

这里也就是在RSA中使用CRT算法加速运算的例子,我们都来讲讲如何操作

为什么要用CRT?

在RSA的计算中,我们有

,我们知道往往非常大,那么计算上面两个式子就非常的消耗时间,而且也需要,我们注意到也是两个大质数,很有可能 ; 也互素,那么我能就能把问题转化到模 下来讨论,下面是推导过程

这是由于也就是模的传递性,那么我们转过来计算在模下的私钥,即

那么在解密的时候就有

这里是因为可能比要大,所以我们无法直接解出来,然后我们根据中国剩余定理可以构造出一个对的解,如下

由第二个同余式不妨设

我们进而只需要解出来,把这个式子带入另一个同余式

我们只需计算

那么有

得到之后回代即可成功求出

可见这里我们的计算都是在模的式子中进行的,乘法运算的次数少了不少,实现了加速

说回正题,从上面不难看出,我们自始至终都没有尝试计算,成功回避了这个问题

来个例子—黑盾杯 2020-Factor

1
2
3
4
n = 3454083680130687060405946528826790951695785465926614724373
e = 3
c = 1347530713288996422676156069761604101177635382955634367208
gcd(m, n) = 1

这里n不是很大,简单分解后能得到三个素因子,我们记为pqr,如下

1
2
3
4
5
6
7
8
9
10
p=11761833764528579549
q=17100682436035561357
r=17172929050033177661
print(n-p*q*r) # 0
print(gcd(e,phi)) # 3
print(gcd(e,p-1)) # 1
print(gcd(e,q-1)) # 3
print(gcd(e,r-1)) # 1
print(gcd(e,(p-1)*(r-1))) # 1

简单检查了关系,如上,这题方法很多,在上面的解法下即利用来使用CRT求解,代码如下

1
2
3
4
5
6
7
8
9
10
#sage
dp=inverse(e,p-1)
dr=inverse(e,r-1)
m1=pow(c,dp,p)
m2=pow(c,dr,r)
Ir=pow(r,-1,p)
h=pow(Ir*(m1-m2),1,p)
m=m2+h*r
print(long_to_bytes(m))
# CMISCCTF{3_RSA}

这里也能转化到在模下尝试求解,但是那对明文的大小有一定要求,我们还是使用通法,这里只要能找到两个满足要求的因子就行,但是这不是什么时候都能生效,请看下题

强网杯2022—[ASR]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from Crypto.Util.number import getPrime
from secret import falg
pad = lambda s:s + bytes([(len(s)-1)%16+1]*((len(s)-1)%16+1))

n = getPrime(128)**2 * getPrime(128)**2 * getPrime(128)**2 * getPrime(128)**2
e = 3
flag = pad(flag)
print(flag)
assert(len(flag) >= 48)
m = int.from_bytes(flag,'big')
c = pow(m,e,n)
print(f'n = {n}')
print(f'e = {e}')
print(f'c = {c}')
n = 8250871280281573979365095715711359115372504458973444367083195431861307534563246537364248104106494598081988216584432003199198805753721448450911308558041115465900179230798939615583517756265557814710419157462721793864532239042758808298575522666358352726060578194045804198551989679722201244547561044646931280001
e = 3
c = 945272793717722090962030960824180726576357481511799904903841312265308706852971155205003971821843069272938250385935597609059700446530436381124650731751982419593070224310399320617914955227288662661442416421725698368791013785074809691867988444306279231013360024747585261790352627234450209996422862329513284149

注意到这里特别对m进行了填充,就是保证m比n的因子大,无法转化为在小因子的域下求解,看看下面这段失败的exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from Crypto.Util.number import *
e = 3
n = 8250871280281573979365095715711359115372504458973444367083195431861307534563246537364248104106494598081988216584432003199198805753721448450911308558041115465900179230798939615583517756265557814710419157462721793864532239042758808298575522666358352726060578194045804198551989679722201244547561044646931280001
c = 945272793717722090962030960824180726576357481511799904903841312265308706852971155205003971821843069272938250385935597609059700446530436381124650731751982419593070224310399320617914955227288662661442416421725698368791013785074809691867988444306279231013360024747585261790352627234450209996422862329513284149

p = 225933944608558304529179430753170813347
q = 260594583349478633632570848336184053653
r = 218566259296037866647273372633238739089
t = 223213222467584072959434495118689164399

print(gcd(e,p-1)) # 3
print(gcd(e,q-1)) # 1
print(gcd(e,r-1)) # 3
print(gcd(e,t-1)) # 1
dt=inverse(e,t-1)
dq=inverse(e,q-1)
m1=pow(c,dt,t)
m2=pow(c,dq,q)
Iq=pow(q,-1,t)
h=pow(Iq*(m1-m2),1,t)
m=m2+h*q
print(long_to_bytes(m))

这里的确找到了满足的两个因子,但是我们注意到,若,所以我们在上述代码中解出来的满足,也就是说,如果的时候,这个方法就暂时失效了,这是在多因子RSA下特别的问题,注意到我们还有两个因子没有使用,为了也能使用上它们,我们给出更加一般的方法

有限域开根

想想我们的核心目标是什么?—求解 那么我们为什么要找到这个?重新捋一遍过程,我们就发现,是为了去计算来求解满足,换言之,我们还是在求模不同素因子下的,我们不妨想一下,真的重要吗?,我们看这个方程

这不就是在下的方程吗?这里还非常小,完全可以在这个有限域下来开根求解所有符合题意的的取值集合,显然可能有多解,但是我们可以依次枚举中的值,由上述分析,若,那么否则也有

那么我们枚举这四个集合,一定能找到满足我们需要的

这就是中国剩余定理了,我们直接打CRT就能解出来m,而且我们用到了所有的因子,几乎可以保证这里是唯一的,如果不是唯一的,我们确定一下中的,转化到下就行,下面是修改后的exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# sage
from Crypto.Util.number import *
from itertools import product

# Given parameters
e = 3
n = 8250871280281573979365095715711359115372504458973444367083195431861307534563246537364248104106494598081988216584432003199198805753721448450911308558041115465900179230798939615583517756265557814710419157462721793864532239042758808298575522666358352726060578194045804198551989679722201244547561044646931280001
c = 945272793717722090962030960824180726576357481511799904903841312265308706852971155205003971821843069272938250385935597609059700446530436381124650731751982419593070224310399320617914955227288662661442416421725698368791013785074809691867988444306279231013360024747585261790352627234450209996422862329513284149

# Prime factors
p = 225933944608558304529179430753170813347
q = 260594583349478633632570848336184053653
r = 218566259296037866647273372633238739089
t = 223213222467584072959434495118689164399

# Create polynomial rings and find roots in each field
R.<x> = Zmod(p)[]
f = x^e - c
res_p = f.monic().roots()

R.<x> = Zmod(q)[]
f = x^e - c
res_q = f.monic().roots()

R.<x> = Zmod(r)[]
f = x^e - c
res_r = f.monic().roots()

R.<x> = Zmod(t)[]
f = x^e - c
res_t = f.monic().roots()

# Iterate through all possible combinations of roots
for (m_p, _) in res_p:
for (m_q, _) in res_q:
for (m_r, _) in res_r:
for (m_t, _) in res_t:
# Combine using CRT in stages
# First combine p and q
m_pq = crt(int(m_p), int(m_q), p, q)
# Then combine r and t
m_rt = crt(int(m_r), int(m_t), r, t)
# Finally combine the two results
m = crt(m_pq, m_rt, p*q, r*t)

mes = long_to_bytes(m)
print(mes)

# b'flag{Fear_can_hold_you_prisoner_Hope_can_set_you_free}\x06\x06\x06\x06\x06\x06'

在这个脚本下我们能解决大部分问题,但是当太大的时候我们就得考虑其他的方法来处理了

根据题型,我们有几种应付的思路

UCSCCTF2025—EzCalculate

题面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
p = 9586253455468582613875015189854230646329578628731744411408644831684238720919107792959420247980417763684885397749546095133107188260274536708721056484419031
q = 8998523072192453101232205847855618180700579235012899613083663121402246420191771909612939404791268078655630846054784775118256720627970477420936836352759291
n = p * q
e = 65536
c = 74962027356320017542746842438347279031419999636985213695851878703229715143667648659071242394028952959096683055640906478244974899784491598741415530787571499313545501736858104610426804890565497123850685161829628373760791083545457573498600656412030353579510452843445377415943924958414311373173951242344875240776

phi = (p - 1) * (q - 1)
print(gcd(e, phi)) # 4
print(gcd(e, p - 1)) # 2
print(gcd(e, q - 1)) # 2

print(gcd(e,(p-1)//2)) # 1
print(gcd(e,(q-1)//2)) # 1

在这里,我们注意到同时与互素,于是我们能有一个取巧的方法,先转化到对因子的同余式,即

再不妨记

容易有

那么必然存在

写成等式,即

我们考虑同余式

这里就初见端倪了,我们做一个换元

那不就是二次剩余的判别式吗

注意到这里我们得到了重要的模式子,回看解密过程

也就是

也就是说我们有

回到了熟悉的地方,再构造CRT就可以解出

那么这里如果再改成

呢,思路还是考虑这个式子

可以考虑去计算次的本原单位根,但考虑到虚数的引用带来的一系列不确定性,不再过度深入。

那么如果这种大素数呢?往往会伴随另外一个条件,是,在这种情况下,我们可以使用AMM算法来找到一个满足条件的

AMM算法

我们先从上面的RSA题目中抽离出来,来看看整数域开根下的一般问题,我们面对的是同余式

我们往往已知是一个素数,我们想要找出一个满足方程的,从简单出发,我们先讨论的情况

e=2时

也就是处理方程

因为是素数,我们可以这样来设

其中

那么我们知道,对于所有的模的二次剩余数这里就是一个模的二次剩余)有

代入上面的式子,可以得到

再次从简单出发,如果

那么直接有

两边同乘一个 ,又,直接开方,有

前面强调了就是这样的一个二次剩余,也就是说,我们代入,这个方程是成立的,所以说

这个方程两边平方后,就是我们非常熟悉的

所以说,我们就找到了一个符合要求的

那么,如果呢?

我们知道对于所有的模的非二次剩余,有

我们不难得到

开方,能得到两种结果,即

为了避免负数的出现导致开根开出虚数,我们乘上一个非二次剩余,即

这里用来记录是否为负,是自己生成的

注意到我们这里实际上就是,而都是已知的,在代码中我们可以直接算这个,如果是1,就令,否则为1

同余式右边为1后继续开方,即

确定的方法和上面一样,我们一直开方,直到的指数幂中不含,会得到形似如下的式子

两边同乘一个,再开方就得到了我们需要的

这里的核心思路就是消去,构造

的代数式

这里AMM找到的只是一个满足要求的解,我们知道在的情况下,符合条件的有两个,为了找到另一个解,也就是描述在模下的,我们使用如下方法,任意找到一个,使得它不是模的二次剩余,那么有

也就是说,,都是满足要求的解,在

e更大的时候

我们现在要考虑的是

这里的问题是从二次剩余转到次剩余,这里我们需要条件,设

那么对我们要求的,有

还是从简单出发,如果

此时如果,那么可以参考RSA的解法

代入,

可见这里就是一个满足要求的解

如果可以考虑离散对数的方法

如果

我们站在高一点的视角下一个结论

先看要讨论的式子

其中

我们可以令

由欧拉定理,当

这里可以视为一个生成元为,阶为的模子群

那么稍微变形一下

那么我们就构造了一个模下,生成元为阶子群

特别注意不能是模次剩余,也就是

否则就是1开根开出来的一个结果,你拿生成的群就无法包含所有可能的根

接下来我们说明中元素进行次幂后模为1

那么也就是说,在模群下对1开次方根的结果都会落在群

知道这个结论,我们对上式做一个小变形

我们都知道,仿照上面的思路,先对

次方根,就能得到

我们再直接计算这个元素的值,然后在这个群中找到逆元

假设

那么逆元就是

所以说上面的同余式开根后就直接是

如此往下计算,直到同余式左边只剩下次幂,最后解出来之后还要记得根据这个群个单位根对应出组解,当的时候,我们来操作一下,就是

分解p-1

计算逆元d,找到初始解

逐步上升

先找到生成元

再定义一些初始变量

可知

再在每一次迭代的过程中,计算

如果

说明这次开方的结果开出来就是次剩余根,无需调整,否则要进行修改,并同时对进行修正

最后对得到的根乘上个单位根,得到解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# sage
def AM1M(residue, e, p):
"""
返回 x^e ≡ residue mod p 的所有解
"""
if (p-1) % e != 0:
raise ValueError(f"e={e} 不能整除 p-1={p-1},AMM 不适用")

# 分解 p-1 = e^t * s
t = valuation(p - 1, e)
s = (p - 1) // (e^t)

# 计算 d = e^{-1} mod s
d = inverse_mod(e, s)

# 简单情况:t=1
if t == 1:
x = power_mod(residue, d, p)
F = GF(p)
zeta = F.zeta(e)
return [ (x * zeta**i) % p for i in range(e) ]


# 寻找 e 次非剩余(生成元)
F = GF(p)
g = F.multiplicative_generator()
noresidue = g
while (noresidue^((p-1)//e)) == 1:
noresidue *= g

# 计算单位根和辅助变量
a = power_mod(noresidue, e^(t-1) * s, p)
b = power_mod(residue, e * d - 1, p)
c = power_mod(noresidue, s, p)
h = 1

# Hensel 提升
for i in range(1, t):
d_i = power_mod(b, e^(t-1-i), p)
if d_i == 1:
j = 0
else:
j = -discrete_log(d_i, a)
b = (power_mod(c, e, p)^j * b) % p
h = (power_mod(c, j, p) * h) % p
c = power_mod(c, e, p)

x0 = (power_mod(residue, d, p) * h) % p
ω = F.zeta(e) # e 阶单位根
return [ (x0 * ω^i) % p for i in range(e) ] # 返回所有解

这里能得到所有模p下的解,再整合起来打CRT就解决了

来个实战例子

NCTF2019—EzRSA

1
2
3
4
5
6
7
8
9
10
11
12
e = 0x1337
p = 199138677823743837339927520157607820029746574557746549094921488292877226509198315016018919385259781238148402833316033634968163276198999279327827901879426429664674358844084491830543271625147280950273934405879341438429171453002453838897458102128836690385604150324972907981960626767679153125735677417397078196059
q = 112213695905472142415221444515326532320352429478341683352811183503269676555434601229013679319423878238944956830244386653674413411658696751173844443394608246716053086226910581400528167848306119179879115809778793093611381764939789057524575349501163689452810148280625226541609383166347879832134495444706697124741
n = p * q
c = 10562302690541901187975815594605242014385201583329309191736952454310803387032252007244962585846519762051885640856082157060593829013572592812958261432327975138581784360302599265408134332094134880789013207382277849503344042487389850373487656200657856862096900860792273206447552132458430989534820256156021128891296387414689693952047302604774923411425863612316726417214819110981605912408620996068520823370069362751149060142640529571400977787330956486849449005402750224992048562898004309319577192693315658275912449198365737965570035264841782399978307388920681068646219895287752359564029778568376881425070363592696751183359

phi = (p - 1) * (q - 1)
print(gcd(e, phi)) #4919
print(gcd(e, p - 1)) #4919
print(gcd(e, q - 1)) # 4919
print(int(e)) #4919

exp 这里使用python多进程加快运算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
from Crypto.Util.number import *
import itertools
from multiprocessing import Pool, cpu_count
from tqdm import tqdm
from sage.all import *

e = 0x1337
p = 199138677823743837339927520157607820029746574557746549094921488292877226509198315016018919385259781238148402833316033634968163276198999279327827901879426429664674358844084491830543271625147280950273934405879341438429171453002453838897458102128836690385604150324972907981960626767679153125735677417397078196059
q = 112213695905472142415221444515326532320352429478341683352811183503269676555434601229013679319423878238944956830244386653674413411658696751173844443394608246716053086226910581400528167848306119179879115809778793093611381764939789057524575349501163689452810148280625226541609383166347879832134495444706697124741
n = p * q
c = 10562302690541901187975815594605242014385201583329309191736952454310803387032252007244962585846519762051885640856082157060593829013572592812958261432327975138581784360302599265408134332094134880789013207382277849503344042487389850373487656200657856862096900860792273206447552132458430989534820256156021128891296387414689693952047302604774923411425863612316726417214819110981605912408620996068520823370069362751149060142640529571400977787330956486849449005402750224992048562898004309319577192693315658275912449198365737965570035264841782399978307388920681068646219895287752359564029778568376881425070363592696751183359

cp = c % p
cq = c % q

inv_p = pow(p, -1, q)
inv_q = pow(q, -1, p)

def AMM(residue, e, p):
if (p-1) % e != 0:
raise ValueError(f"e={e} does not divide p-1={p-1}")

t = valuation(p - 1, e)
s = (p - 1) // (e**t)

d = inverse_mod(e, s)

if t == 1:
x = power_mod(residue, d, p)
F = GF(p)
zeta = F.zeta(e)
return [ (x * zeta**i) % p for i in range(e) ]

F = GF(p)
g = F.multiplicative_generator()
noresidue = g
while (noresidue**((p-1)//e)) == 1:
noresidue *= g

a = power_mod(noresidue, e**(t-1) * s, p)
b = power_mod(residue, e * d - 1, p)
c = power_mod(noresidue, s, p)
h = 1

for i in range(1, t):
d_i = power_mod(b, e**(t-1-i), p)
if d_i == 1:
j = 0
else:
j = -discrete_log(d_i, a)
b = (power_mod(c, e, p)**j * b) % p
h = (power_mod(c, j, p) * h) % p
c = power_mod(c, e, p)

x0 = (power_mod(residue, d, p) * h) % p
ω = F.zeta(e)
return [ (x0 * ω**i) % p for i in range(e) ]

def compute_crt(args):
m1, m2 = args
m1_int = int(m1) # Convert to Python int
m2_int = int(m2) # Convert to Python int
x = (m1_int * q * inv_q + m2_int * p * inv_p) % n
return x

def find_flag():
resp = AMM(cp, e, p)
resq = AMM(cq, e, q)

print(f"[+] Solutions in GF(p): {len(resp)}")
print(f"[+] Solutions in GF(q): {len(resq)}")
print(f"[+] Total combinations: {len(resp) * len(resq)}")

with Pool(cpu_count()) as pool:
combinations = itertools.product(resp, resq)
for x in tqdm(
pool.imap_unordered(compute_crt, combinations, chunksize=10000),
total=len(resp) * len(resq),
desc="Brute-forcing CRT"
):
flag = long_to_bytes(x)
if b'NCTF{' in flag:
print(f"\n[+] Flag: {flag.decode()}")
pool.terminate()
return

print("[-] Flag not found")

if __name__ == "__main__":
find_flag()

# 大约跑三分钟