目录
一、模数分解总览
1.1直接分解法
1.2费马分解与Pollard_rho分解
1.3公约数分解
1.4其他模数分解
二、实战特训
2.1[黑盾杯 2020]Factor
2.2[GWCTF 2019]babyRSA
2.3[LitCTF 2023]yafu (中级)
2.4[RoarCTF 2019]RSA
2.5[CISCN 2022 西南]rsa
三、总结
一、模数分解总览
1.1直接分解法
在RSA的通信流程中,对外保密的是私钥,但是究其本质对外隐藏的是大素数P,Q。而正是大数分解难题的原因,使得即使暴露N,即PQ之积的值暴露,破解难度也依旧很大。所以,我们只需要掌握缺失的信息P,Q即可完成解密。
但是如果N值取的很小,我们通常可以进行暴力分解,从而获取P,Q。顺带一提,在工业中我们认为2048bit以上的N是安全的。但是,在CTF竞赛中,我们几乎不会遇到2048bit以上的素数分解。所以,这里我们可以放心使用。稍后,笔者会给处自己对与N很小的理解。
使用条件:在阅读部分书籍的密码学(crypto)部分的解析,以及一些题目经验,大部分适合直接数模分解的N一般小于512bit。该使用条件,是笔者的拙见,欢迎大家在评论区讨论。
1.2费马分解与Pollard_rho分解
在1.1中我们提及了,如果N值很小,那么我们可以直接分解。但是如果N值大,我们将寸步难行。可是,这种难度在于直接分解,而不是利用部分数学技巧与原理。
例如在费马分解中,当P,Q两个值过于接近,也就是P-Q的值很小。此时,我们就可以令A = (P + Q) / 2, B = (P - Q) / 2 则 N = A^2 - B^2。其中,在具体算法实现的过程中,因为P - Q很小,所以我们可以枚举爆破。而且根据 N = PQ,且N值一般已知,就有 A^2 = B^2 + N。
而在Pollard_rho分解中,则恰恰相反。该分解说明当P,Q相差过大时,可以被暴力分解。从费马分解的角度说,如果P - Q过大,那么P + Q就会过小。因此可以枚举P + Q暴力破解。然而该分解方式有一定的数学原理(本菜狗没学原理,赶比赛就停在应用层面先)。
Pollard算法的原理大体是通过某一种方式获取得到A,B值。计算p = gcd(a - b, n),直到p不为1,或者a,b出现循环。返回一个“因子”p。接着,我们可以递归的计算Pollard(p)与Pollard(n/p),值得一提得是我们p == n时,返回的就是n是一个质数退出。一般我们认为B = A^2 + 1。
这个方法很美好,但是显示很骨感。这种分解方式建立于我们知道P - Q的状态。然而,现实和比赛中,P、Q都是被隐藏的,所以我们很难判断是否可以使用这种方式破解。SO!我们只能抱着尝试的方法使用该方法。
1.3公约数分解
公约数分解一般是建立在多组信息(密文)在加密过程中,采取了相同的大素数P。因此,我们可以通过gcd来获取一个因此P,进而得知另外一个因子。
使用条件:出现多组密文。
1.4其他模数分解
一般这种分解方式也是最难的,因为会考察选手的数学能力。也就是题目会给出一些额外的数学表达式,选手根据表达式合理爆破。或者我们可以使用factordb.com进行分解。但是当数字过大时,网站会显示补全,hhh。
使用条件:当上述方式失效,或者题目给出额外的数学式子。
二、实战特训
2.1[黑盾杯 2020]Factor
点击这里,跳转至题目
在本道题中,我们获取得到了一下信息,如图所示。
这道题有许多的尝试方法,如下:
1.因为n比较小,可以直接分解n
2. 因为e比较小以及给出了其他的数学关系,我们可以使用小加密明文爆破+数学关系判断。
相对来讲,分解n更为简单。所以,我们优先尝试。注意:密码学更多的是尝试,而不是一眼定方法。使用yafu工具,我们获取一下因子。
因此,我们可以编写一下代码段。
from Crypto.Util.number import *
import sympy
import primefac
from libnum import n2s
import gmpy2
import wienerAttack
n = 3454083680130687060405946528826790951695785465926614724373
e = 3
c = 1347530713288996422676156069761604101177635382955634367208
# gcd(m, n) = 1
p = 17100682436035561357
q = 17172929050033177661
r = 11761833764528579549
phi = (q - 1) * (r - 1) * (p - 1)
d = primefac.modinv(e, phi)
m = pow(c, d, n)
print(long_to_bytes(m))
但是,我们在运行时,发现代码报错。别急不是因为你错了,可能是因为出现了“假素数”,即(某一大素数 - 1) 是 e 的倍数。因此经过排查,发现(p - 1) % e == 0。因此剔除p。修改代码得到。
from Crypto.Util.number import *
import sympy
import primefac
from libnum import n2s
import gmpy2
import wienerAttack
n = 3454083680130687060405946528826790951695785465926614724373
e = 3
c = 1347530713288996422676156069761604101177635382955634367208
# gcd(m, n) = 1
p = 17100682436035561357
q = 17172929050033177661
r = 11761833764528579549
phi = (q - 1) * (r - 1)
d = primefac.modinv(e, phi)
m = pow(c, d, q * r)
print(long_to_bytes(m))
最后获取旗帜FLAG{3_RSA}。
2.2[GWCTF 2019]babyRSA
点击这里,跳转至题目
根据题目附件,我们可以获取得到以下信息(未全部显示)。
以及以下数学式:c1 = F1 + F2, c2 = F1^3 + F2^3。以及p < q。
在这里我们获取到的信息是n值大概在2048bit。而且给出其他表达式,所以我们尝试其他数模分解法。
首先,我们明确思路。我们需要获取c1, c2来计算得到F1、F2。然后,将F1、F2转职为字符串即可。而获取c1、c2我们需要获取p,q。所以,现在最大的难题在于p,q。在这里说明两种方式。
方法1:关注到p,q是相邻的两个素数,如果你知道素数分布规律,即他们之间相差lnx。所以我们可以判断出 q - p 大约在1000。所以我们可以使用费马分解。
使用yafu分解得到以下结果。
方法2(推荐掌握): 根据p < q的关系,我们可以判断出 n > p^2,所以sqrt(n) > p。所以,我们可以猜想 nextprime(sqrt(n)) == nextprime(p) == q。证明如下:
n = n'^2 = pq = (n''^2 - d^2) ==> n'' = (p + q) / 2 > n' > p,也就是说 n' - p < q - p.建议画一个数轴理解。
import gmpy2
import sympy
N=636585149594574746909030160182690866222909256464847291783000651837227921337237899651287943597773270944384034858925295744880727101606841413640006527614873110651410155893776548737823152943797884729130149758279127430044739254000426610922834573094957082589539445610828279428814524313491262061930512829074466232633130599104490893572093943832740301809630847541592548921200288222432789208650949937638303429456468889100192613859073752923812454212239908948930178355331390933536771065791817643978763045030833712326162883810638120029378337092938662174119747687899484603628344079493556601422498405360731958162719296160584042671057160241284852522913676264596201906163
n = gmpy2.iroot(N, 2)[0]
q = sympy.nextprime(n)
p = n // q
运行结果如下:其中1038 == q - p --> 验证素数分布,方法1
两个方法没有优劣之分,知识方法二更贴合题意。方法一更快,但是需要我们判断p,q对应的数字。所以,按需对应选择。
接下来获取c1,c2就简单了。
imoprt primefac
phi = (p-1)*(q-1)
d = primefac.modinv(e, phi)
c1 = powmod(m1, d, N)
c2 = powmod(m2, d, N)
接下来获取F1,F2。构造二次方程x^2 - (F1 + F2)x + F1F2 = 0。使用c1,c2表示就为x^2 - c1x + (c1^2 - c2/c1)/3 = 0
A = gmpy2.mpz(1)
B = gmpy2.mpz(-c1)
C = gmpy2.mpz((c1*c1 - c2//c1)//3)
delta = gmpy2.mpz(gmpy2.iroot(B*B - 4*A*C,2)[0])
F1 = (-B - delta) // 2
F2 = (-B + delta) // 2
flag1 = long_to_bytes(F1)
flag2 = long_to_bytes(F2)
print(flag1 + flag2, flag2 + flag1)
得到旗帜FLAG{f709e0e2cfe7e530ca8972959a1033b2}
当然你也可以使用sympy.solve来获取根。
2.3[LitCTF 2023]yafu (中级)
点击这里,跳转至题目
这道题比较简单,题目就提示了使用yafu。一共会获取15个因子,这道题额外考察了欧拉函数的性质问题。解决代码如下。
p1=2151018733
p2=2201440207
p3=2315495107
p4=2585574697
p5=2719600579
p6=2758708999
p7=2767137487
p8=2906576131
p9=2923522073
p10=3354884521
p11=3355651511
p12=3989697563
p13=4021078331
p14=4044505687
p15=4171911923
phi = (p1 - 1) * (p2 - 1) * (p3 - 1) * (p4 - 1) * (p5 - 1) * (p6 - 1) * (p7 - 1) * (p8 - 1) * (p9 - 1) * (p10 - 1) * (p11 - 1) * (p12 - 1) * (p13 - 1) * (p14 - 1) * (p15 - 1)
d = gmpy2.invert(e, phi)
m = pow(c, d, n)
print(long_to_bytes(m))
2.4[RoarCTF 2019]RSA
点击这里,跳转至题目
我们可以获取信息如下。
在这里,我们知道P、Q可能相差过大,同时我们也获取了额外信息。所以我们可以尝试使用Pollard和其他数模分解法。
尝试Pollard,发现预估等待时间 >= 1h。果断停止尝试。
开始尝试其他数模分解。关注到有2019次方,所以枚举x,y的范围不会特别大,我们可以接受。
A = 2683349182678714524247469512793476009861014781004924905484127480308161377768192868061561886577048646432382128960881487463427414176114486885830693959404989743229103516924432512724195654425703453612710310587164417035878308390676612592848750287387318129424195208623440294647817367740878211949147526287091298307480502897462279102572556822231669438279317474828479089719046386411971105448723910594710418093977044179949800373224354729179833393219827789389078869290217569511230868967647963089430594258815146362187250855166897553056073744582946148472068334167445499314471518357535261186318756327890016183228412253724
for x in range(1, 1000):
for y in range(1, 1000):
D = x % y
if (D != 0) :
f=(((y%x)**5)%D)**2019+y**316+(y+1)//x
if (f == A) :
print(x, y)
break
获得x = 2, y = 83
然后我们关注到以下两个表达式:
因此n = p * q > x*y*z^2, 也就是 n / x / y > z^2,那我们可以像2.2那样,获取q
n = 117930806043507374325982291823027285148807239117987369609583515353889814856088099671454394340816761242974462268435911765045576377767711593100416932019831889059333166946263184861287975722954992219766493089630810876984781113645362450398009234556085330943125568377741065242183073882558834603430862598066786475299918395341014877416901185392905676043795425126968745185649565106322336954427505104906770493155723995382318346714944184577894150229037758434597242564815299174950147754426950251419204917376517360505024549691723683358170823416757973059354784142601436519500811159036795034676360028928301979780528294114933347127
x = 2
y = 83
n1 = n // 166 #可以存放
q = sympy.nextprime(gmpy2.iroot(n1, 2)[0])
p = n // q
assert isPrime(q) and isPrime(p)
print("OK") #确认步骤2正确
print(p, q)
获得p,q后,我们就可以开始正是解码
c = 41971850275428383625653350824107291609587853887037624239544762751558838294718672159979929266922528917912189124713273673948051464226519605803745171340724343705832198554680196798623263806617998072496026019940476324971696928551159371970207365741517064295956376809297272541800647747885170905737868568000101029143923792003486793278197051326716680212726111099439262589341050943913401067673851885114314709706016622157285023272496793595281054074260451116213815934843317894898883215362289599366101018081513215120728297131352439066930452281829446586562062242527329672575620261776042653626411730955819001674118193293313612128
e = 0x10001 # e = 65537
phi = (p - 1) * (q - 1)
d = primefac.modinv(e, phi)
m = pow(c, d, n)
print(long_to_bytes(m))
获取旗帜FLAG{wm-l1l1ll1l1l1l111ll}
2.5[CISCN 2022 西南]rsa
点击这里,跳转至题目
题目附件内容如下:
from Crypto.Util.number import *
import gmpy2
flag = b'XXXXXXXX'
p1 = getPrime(700)
r1 = getPrime(700)
for i in range(10):
q1 = 5*p1+i
n = p1*q1*r1
p3 = pow(p1,3,n)
q3 = pow(q1,3,n)
print(p3)
print(q3)
'''
p3 = 29914513810588158800677413177910972738704129106546850855032986405861482276089830788972187432277517348644647399654780884571794069905291936470934226328931651386658328163535027343107140438177837479649822914209171476632450951930287641742344330471734177295804718555774395704231261550376220154493373703096062950390869299905383682611063374747752091585836452902373843865043412096365874638466683035848817858586173172058756256354758712684819253211761289032789542371351760915771791997388241121078055468403109260493642435791152671979552597191217179672328555740595434990908530985477314228867209314472001848844089467987561661918366232980944933533
q3 = 66208618374366130551979192465001581263127328176551695213970812805980115496523825511250542987452691413485117902772315362811067501379171731387904074565035353566976164797769439898266222919741874340315356585585077141595328441423323822407738375537476582506440045835592730211502035261968878999959340204806442390319739977816872969200022096331677277225467021553564212725120939434924481787524609852608476848761521446441776154400518315701988027274150425936061679275540502720782853648148897480117033152064922234451671636288396704170234613549011854618414776342798740690128675106027908639984431432591397555541420243824539205614036979987830125678
'''
P = getPrime(1024)
Q = getPrime(1024)
N = P * Q
E = 65537
lcm = gmpy2.lcm(P-1, Q-1)
e1 = gmpy2.invert(p1, lcm)
e2 = gmpy2.invert(r1, lcm)
m = bytes_to_long(flag)
c = pow(m, E, N)
print(lcm)
print(c)
print(N)
'''
lcm = 4292158730589770192682795435047249488185453170529228019750042608688907718268448193363838203887391025871515871000364259326343790645215256385842265899206372365402431198699714374850409466996627163968391249416054093529090485677808301343590811445080871279796162536469847469761747058736980603093722710824453312207182881241846080117790728778291633761198069016865260030288832065807438020772711645648333908622890343009942617559434851450007195025869850769670769715654662127278293639938359741401336592219730356884542179574372134014927006215640945952229142436595334916765255426954857520777553915330597952622785359222832224632624
c = 4288727484183191191687364666620023549392656794153112764357730676861570386983002380982803054964588111708662498647767438881892355599604826306427809017097724346976778230464708540600157055782723189971534549543664668430013171469625043063261219462210251726207552819381767396148632877168530609902046293626355744288863460554297860696918890189350721960355460410677203131993419723440382095665713164422367291153108363066159712951217816814873413423853338021627653555202253351957999686659021298525147460016557904084617528199284448056532965033560516083489693334373695545423561715471204868795248569806148395196572046378679014697206
N = 17168634922359080770731181740188997952741812682116912079000170434755630873073792773455352815549564103486063484001457037305375162580861025543369063596825489461609724794798857499401637867986508655873564997664216374116361942711233205374363245780323485119184650145879389879046988234947922412374890843297813248828996855478005656041814919367820336728271583686844991928889831691815821365423570311291064846736832327637944358854661523107817781673029406341843040857813841671405147146887291204140157388049394514390098066284975682117038362207142272098796924412602725857521665773622056312191400612944442008222587867782281556388669
'''
这道题纯吓人,因为密文跟p3,q3没关系。所以我们不需要用到它。注意的是,这里phi用lcm来表示了。
d = primefac.modinv(E, lcm)
m = pow(c, d, n)
primt(long_to_bytes(m))
获得FLAG{h3ll0_wo21d!}
三、总结
在这里,涵盖了大部分的模数分解法的题目做法。我们分析了如何判断以及尝试的优先级。如何根据代码报错,进行实时的调整、排错。