当你在 CTF 赛场遇到 RSA 题型时,模数相关的攻击手法⚔️往往是破解的关键。无论是共模攻击、模重复攻击,还是模数分解的相关漏洞,这些都是 RSA 加解密中常见且高频出现的题型。掌握这些攻击手法,不仅能让你在 CTF 赛场上如鱼得水,还能加深你对 RSA 加解密原理的理解。

暴力分解模数 🥳

在 $N$ 的比特位数小于 512 的时候,可以采用大整数分解的策略获取 $p$ 和 $q$。一般来说,使用一些高效的分解工具(如 YAFU、msieve、CADO-NFS 等)可以在合理的时间内完成分解,获取 $p$ 和 $q$。

暴力分解模数例题

暴力分解模数例题下载

from Crypto.Util.number import *
from secret import M

p = getPrime(56)
q = getPrime(56)
e = 65537 
n = p * q
long_M = long_to_bytes(M.encode())
C = pow(long_M, e, n) 
print(C)
print(n)
#n: 3211733906383171840011432827424907
#C: 1251845031704156126184126788111750

在这个例题中,模数 $n$ 的比特位数为 112 位,使用暴力分解的方式可以在几秒钟内获取到 $p$ 和 $q$,从而计算出私钥 $d$,最终解密出原始消息 $M$。

解题代码示例:

from Crypto.Util.number import *
from sage.all import factor

n = 3211733906383171840011432827424907
C = 1251845031704156126184126788111750
result = factor(n)
p = result[0][0]
q = result[1][0]
phi = (p-1)*(q-1)
e = 65537
d = inverse(e, phi)
m = pow(C, d, n)
print(long_to_bytes(m))
# b'flag{factor}'

模数过大攻击 😇

由于 RSA 的安全性依赖于大整数分解的困难性,因此理论上如果模数 $N$ 的比特位数过大,攻击者可能无法在合理的时间内完成分解,从而无法获取到 $p$ 和 $q$,最终无法破解 RSA 加密。

但是,有时候模数过大,达到因子 $p$ 或者 $q$ 可以直接当加解密的模数时,我们拿到 $p$ 时,是可以直接解密的。

模数过大攻击例题

模数过大攻击例题下载

from Crypto.Util.number import *
from secret import flag

m=bytes_to_long(flag)
p=getPrime(512)
q=getPrime(512)
print('p=',p)
print('q=',q)
n=p*q
e=65537
c=pow(m,e,n)
print('c=',c)
# p= 12408795636519868275579286477747181009018504169827579387457997229774738126230652970860811085539129972962189443268046963335610845404214331426857155412988073
# q= 12190036856294802286447270376342375357864587534233715766210874702670724440751066267168907565322961270655972226761426182258587581206888580394726683112820379
# c= 21336045298142901291286840871587672865162787095337602832807363508232388776491914969338774738649382963457992989937263476335226789823301148587701912487928423012379208808206490825708386587184421011578572210993850461087767328588898076582321561621227540126149951300387265955685592427820272752547313291074696393753

这个题如果直接解密会发现无法解密,因为实际上构造时出了问题, $e$ 和 $q - 1$ 不是互素的,所以无法计算出私钥 $d$,但是我们拿到 $p$ 时,由于这个 $p$ 是远大于明文的,是可以直接解密的。

解题代码示例:

from sage.all import *
from Crypto.Util.number import *

e=65537
p= 12408795636519868275579286477747181009018504169827579387457997229774738126230652970860811085539129972962189443268046963335610845404214331426857155412988073
q= 12190036856294802286447270376342375357864587534233715766210874702670724440751066267168907565322961270655972226761426182258587581206888580394726683112820379
c= 21336045298142901291286840871587672865162787095337602832807363508232388776491914969338774738649382963457992989937263476335226789823301148587701912487928423012379208808206490825708386587184421011578572210993850461087767328588898076582321561621227540126149951300387265955685592427820272752547313291074696393753
g1 = gcd(p-1,e) 
g2 = gcd(q-1,e)
print(g1) # 1
print(g2) # 65537

dp = inverse(e,(p-1))
mp = pow(c,dp,p)

print(long_to_bytes(mp))
# b'flag{too_short_flag_is_not_secure}'

$p$ 和 $q$ 选取不当 😎

如果 $p$ 和 $q$ 选取不当,例如它们之间的差值过小,或者它们的比特位数相差过大,那么攻击者可以利用这些弱点进行攻击。

如果 $p$ 和 $q$ 的差值较小,可以使用 Fermat 分解法快速分解出 $n$,从而获取到 $p$ 和 $q$。

Fermat 分解法:
依据公式

$|p-q|$ 比较小,那么 $\frac{(p-q)^2}{4}$ 也小,所以 $\frac{(p+q)^2}{4}$ 接近于n,可以通过不断增加 $\frac{(p+q)}{2}$ 的值,直到 $\frac{(p-q)^2}{4}$ 为完全平方数。

设 $x = \frac{(p+q)}{2}$, $y = \frac{(p-q)}{2}$
当满足 $x^2 - n = y^2$ 时即可求出方程

如果 $p$ 和 $q$ 的比特位数相差过大,那么攻击者可以尝试直接分解较小的素数,获取到其中一个素数后,再通过除法计算出另一个素数,从而获取到 $p$ 和 $q$。

$p$ 和 $q$ 选取不当例题一

选取不当例题一下载

from Crypto.Util.number import bytes_to_long, getPrime
from sercet import m

p = getPrime(1024)
q = getPrime(25)
n = p * q
e = 65537
c = pow(bytes_to_long(m.encode()), e, n)
print(f"n = {n}")
print(f"c = {c}")
# n = 1906011750017368771307796117673461157817901033778025473698320639347279300698085835647703350923834808686618760795173855605538695556283974551407384296775817454538392743419582983483423430619884117509611946945420798868634216459855241900807180705272425022537879022082812277121832800892726814372715231802506905483235703703   
# c = 549077691873070738038063799394443169955042904352211214070197952774248103687795272253319938425527517181518258210963581835901145261931143229164695429262477411771050634538709862613793004339395308836706979967103332910059842307878824556049956059889500094079522390808561349617771222185714804173669464216698326999828372795

在这个例题中,$p$ 和 $q$ 的比特位数相差过大,攻击者可以直接分解较小的素数 $q$,获取到 $q$ 后,再通过除法计算出 $p$,从而获取到 $p$ 和 $q$。

解题代码示例:

from Crypto.Util.number import long_to_bytes, inverse
for p in range (2**24,2**25):
    if n % p == 0:
        q = n // p
        print(f"Found factors:\np = {p}\nq = {q}")
        break
e = 65537
m = pow(c, inverse(e, (p-1)*(q-1)), n)
print(f"Decrypted message:\n{long_to_bytes(m)}")
# flag{u_had_to_chose_pq_very_carefully}

$p$ 和 $q$ 选取不当例题二

选取不当例题二下载

import gmpy2
from Crypto.Util.number import * 
from secret import m
p = getPrime(2048) 
q = gmpy2.next_prime(p)
for i in range(3600):
    if i%100 ==0:
        print(i)
    q = gmpy2.next_prime(q)

n = p * q
e = 0x10001

m = bytes_to_long(m)
c = pow(m,e,n)
print(c)
print(n)

# c = 62348462953990088934443336160656676256379196401827670581893451052313356600633156941411475073416038229710206387318746963512184736996831716413075320743814866642932860845362219164316326327314284158359209179674094486144680764515105492217458520575237366185272695957361214915406136484746226387883188256560994222709908095180408898696125963039783417587557949172960800557890764169451380255783986305118036512216986529314746084028860213669584595890037896692321861109269594521684968556512950683264437797627221443003439607555292132467628624371973634298128744397311157396100677819983319642517810760039637209759076927554454366134319010242392632132662642903994785600982132993886312621423872136988427063684972059758439594825762030726134443850618714647064586081678339263001700234301845400889943777913130192236281852096864533037602716295223417658798760710279949225248192831610982416617462182909627962556922338248721205926228772344215985646960602952014798520975237358594200048919941619698843434175749789671602254848991688128862206939872826391163844030010778464360088750386438734967569871044926861579066115140189872433762447450824103620809994330813092392551615553658883561091626338360944606525952944254241058687279915857632310080059282018449507613305332
# n = 359336501271521532887656688609295042672434709877081900753462802000755944104448697042366747461395534728915053035475459015263877654779607712559439958640046447246795492613180033737648218742508752449971055824413830268089448680617248179389657713305429663149548581330682010843775095659107771083498691437964118187909080096258126217389687367383301166809926191321584757387431941409060744328371424948988833446651828309230245313487641590365198484255142967088588384318797132203546546629955996480505149111166600546126192029751124771571918693276230154597894534213860659972166774036644473508159923466417979740342453286817394016723536509392328304731594460395586984216646043768390806512181233642534151692135644819066105646943194201485395325911256860832603278670900133367627500162513041761043011626948699652168654976313717938901597808754267996648935745100287531478172712432058146052903388546565632174028182472012197791653306648067962114207504888841853254752313822184342829344043028253285087763176816976839219602985900752633788798421802365156777163724743521780060375607506242692718315299078104710983637353515944430437712607451078303509530539260657291973611896675785100483218112781144038004807688281170137433780853291933873671533348523292439854954465561

在这个例题中,$p$ 和 $q$ 的差值过小,攻击者可以使用 Fermat 分解法快速分解出 $n$,从而获取到 $p$ 和 $q$。

解题代码示例:

from Crypto.Util.number import *
import gmpy2 as gp

c = 62348462953990088934443336160656676256379196401827670581893451052313356600633156941411475073416038229710206387318746963512184736996831716413075320743814866642932860845362219164316326327314284158359209179674094486144680764515105492217458520575237366185272695957361214915406136484746226387883188256560994222709908095180408898696125963039783417587557949172960800557890764169451380255783986305118036512216986529314746084028860213669584595890037896692321861109269594521684968556512950683264437797627221443003439607555292132467628624371973634298128744397311157396100677819983319642517810760039637209759076927554454366134319010242392632132662642903994785600982132993886312621423872136988427063684972059758439594825762030726134443850618714647064586081678339263001700234301845400889943777913130192236281852096864533037602716295223417658798760710279949225248192831610982416617462182909627962556922338248721205926228772344215985646960602952014798520975237358594200048919941619698843434175749789671602254848991688128862206939872826391163844030010778464360088750386438734967569871044926861579066115140189872433762447450824103620809994330813092392551615553658883561091626338360944606525952944254241058687279915857632310080059282018449507613305332
n = 359336501271521532887656688609295042672434709877081900753462802000755944104448697042366747461395534728915053035475459015263877654779607712559439958640046447246795492613180033737648218742508752449971055824413830268089448680617248179389657713305429663149548581330682010843775095659107771083498691437964118187909080096258126217389687367383301166809926191321584757387431941409060744328371424948988833446651828309230245313487641590365198484255142967088588384318797132203546546629955996480505149111166600546126192029751124771571918693276230154597894534213860659972166774036644473508159923466417979740342453286817394016723536509392328304731594460395586984216646043768390806512181233642534151692135644819066105646943194201485395325911256860832603278670900133367627500162513041761043011626948699652168654976313717938901597808754267996648935745100287531478172712432058146052903388546565632174028182472012197791653306648067962114207504888841853254752313822184342829344043028253285087763176816976839219602985900752633788798421802365156777163724743521780060375607506242692718315299078104710983637353515944430437712607451078303509530539260657291973611896675785100483218112781144038004807688281170137433780853291933873671533348523292439854954465561
e = 65537

a = gp.iroot(4*n,2)[0] + 1
while 1:
    if gp.iroot(a*a - 4 * n,2)[1]:
        break
    a = a + 1
b = gp.iroot(a*a - 4 * n,2)[0]
p = (a+b)//2
q = (a-b)//2
print(p)
print(q)
phi = (p-1)*(q-1)
d = inverse(e,phi)
m = pow(c,d,n)
print(long_to_bytes(m))
# b'flag{u_know_the_fermat_factor!}'

模不互素

如果两个 RSA 公钥的模数 $N_1$ 和 $N_2$ 不是互素的,即它们有一个公共的素因子,那么攻击者可以通过计算它们的最大公约数(GCD)来获取到这个公共的素因子,从而分解出两个模数,获取到 $p$ 和 $q$。

当然,如果复杂一点,也可以是有多个模数之间存在公共的素因子,那么攻击者可以通过计算它们之间的 GCD 来获取到这个公共的素因子,从而分解出多个模数,获取到多个 $p$ 和 $q$。

模不互素例题

模不互素例题下载

from Crypto.Util.number import *
from secret import m1, m2

e = 65537
c1 = pow(bytes_to_long(m1.encode()), e, n1)
c2 = pow(bytes_to_long(m2.encode()), e, n2)
print(f"c1 = {c1}")
print(f"c2 = {c2}")
#n1 = 108895617879498519202012987242867653325279569011932848975855816698747912070665498376685393333717683240456930313786558144149400824750715586448894259226955671919120642624030060495951818893447378750288121119385599097111470518763491413669500398960687373958595273557114005654480025508336323302952322683369530980107
#n2 = 92629502961791811672543248549202232353258702786741743055932183827495468369794305903601634793096476966443748199284199451053350116924505444008940619603683242386320802539761036526333916507865522071840234750951837168343853321642508612600344263266261925281998163722124602125092198291300471453120330544931376631613
#c1 = 61235742166601682407600603003185668421611519337914948325202303574908037131452140001004081261277556291983868494754678213440816904181133566193839896694643634357526170240634104009464312693976654292785764598780495479072122249149103810491522930818342063044200307840151530874059848631341258068315918907694244629521
#c2 = 50837238937861806304599627769544061465095296860582855610664744497211980029955384757173623065990262445808162369513719809531991409013899575630438092515903564752263986071475906232663711236691132660232092717966742157839890484764660045346149600612477895717855763187735898010895553739962656299222384523235710729405

在这个例题中,两个模数 $N_1$ 和 $N_2$ 不是互素的,它们有一个公共的素因子,攻击者可以通过计算它们的 GCD 来获取到这个公共的素因子,从而分解出两个模数,获取到 $p$ 和 $q$。

解题代码示例:

from Crypto.Util.number import *
p = gcd(n1, n2)
q1 = n1 // p
q2 = n2 // p
phi1 = (p-1)*(q1-1)
phi2 = (p-1)*(q2-1)
d1 = inverse_mod(e, phi1)
d2 = inverse_mod(e, phi2)
m1 = pow(c1, d1, n1)
m2 = pow(c2, d2, n2)
flag = long_to_bytes(m1) + long_to_bytes(m2)
print(flag.decode())
# flag{u_know_that_the_same_prime_is_dangerous}

光滑模数 😝

由于 $N$ 是由两个大素数 $p$ 和 $q$ 组成的,如果 $p-1$ 或 $q-1$ 是光滑数(即它们的质因数都比较小),那么攻击者可以利用 Pollard 的 p-1 算法来分解出 $N$,从而获取到 $p$ 和 $q$。

如果 $p+1$ 或 $q+1$ 是光滑数,那么攻击者可以利用 Williams 的 p+1 算法来分解出 $N$,从而获取到 $p$ 和 $q$。

光滑模数例题

光滑模数例题下载

from Crypto.Util.number import *
from secret import m

n = 149767527975084886970446073530848114556615616489502613024958495602726912268566044330103850191720149622479290535294679429142532379851252608925587476670908668848275349192719279981470382501117310509432417895412013324758865071052169170753552224766744798369054498758364258656141800253652826603727552918575175830897
e = 65537
c = pow(bytes_to_long(m.encode()), e, n)
print("c =", c)
#c = 43990054306025482446722327591812617378270114979666211260906092677157557305353209885519658615131923119123915114818456042307622384696570346974045212401273302638527074498582589588420265753013768960836315458711255307430199646105955701939961071797607203329554440687624048258855706057753535085388735489880883291030

在这个例题中,模数 $n$ 的 $p-1$ 或 $q-1$ 是光滑数,攻击者可以利用 Pollard 的 p-1 算法来分解出 $N$,从而获取到 $p$ 和 $q$。当然,$p+1$ 或 $q+1$ 也是光滑数,那么攻击者也可以利用 Williams 的 p+1 算法来分解出 $N$,从而获取到 $p$ 和 $q$。

解题代码示例:

from sage.all import *
from Crypto.Util.number import *
from itertools import count
from sympy import primerange

# williams's p+1 factorization
def mlucas(v, a, n):
    v1, v2 = v, (v ** 2 - 2) % n
    for bit in bin(a)[3:]:
        if bit == "0":
            v1, v2 = (v1 ** 2 - 2) % n, (v1 * v2 - v) % n
        else:
            v1, v2 = (v1 * v2 - v) % n, (v2 ** 2 - 2) % n
    return v1

# 素数生成器
def primegen():
    yield from primerange(2, 10**6)  # 生成到 10^6 的素数,够用了

# 整数对数:ilog(x, b) = 最大整数 l,使得 b^l <= x
def ilog(x, b):
    l = 0
    while x >= b:
        x //= b
        l += 1
    return l

# Williams p+1 分解攻击
def Williams_p_plus_1_attack(n):
    for v in count(1):  # 不断尝试新的 v
        for p in primegen():
            e = ilog(isqrt(n), p)
            if e == 0:
                break
            for _ in range(e):
                v = mlucas(v, p, n)
            g = gcd(v - 2, n)
            if 1 < g < n:
                return int(g), int(n // g)
            if g == n:
                break

# 开始攻击
p1, q1 = Williams_p_plus_1_attack(n)
if p1 and q1:
    print("williams's p+1 factorization successful!")
    print(f"p = {p1}")
    print(f"q = {q1}")

# pollard's p-1 factorization
def pollard_p_minus_1(n, B=10**5): # B为素数表的上限,可以根据实际往上调
    a = 2  # 通常选2作为基
    for p in primerange(2, B):
        e = int(isqrt(n).bit_length() / p.bit_length())
        a = pow(a, pow(p, e), n)
    g = gcd(a - 1, n)
    if 1 < g < n:
        return g, n // g
    else:
        return None

p2, q2 = pollard_p_minus_1(n)
if p2 and q2:
    print("pollard's p-1 factorization successful!")
    print(f"p = {p2}")
    print(f"q = {q2}")
phi = (p1 - 1) * (q1 - 1)
d = inverse(e, phi)
m = pow(c, d, n)
print(long_to_bytes(m))
# flag{smoothness_is_important}

上一章:RSA加解密专题 - 前言 👈
下一章:RSA加解密专题 - 公钥指数相关攻击 👈
回到开始:关于我 👈

相关链接:
密码学数学基础 - 整数关系 👈