[BCTF 2016] Hyper RSA
The Plaid Parliament of Pwning participated in (and won) BCTF 2016, hosted by Tsinghua University. Here’s a writeup of one of the problems, which was to decrypt a ciphertext encrypted with a variant of RSA. It was categorized as a cryptography problem and worth 500 points (a hard problem).
We are provided a zipfile containing a censored picture of one run of the program showing the encryption of two messages and their ciphertexts, as well as the server program which implements the Hyper RSA algorithm.
The first message is a random message m1
, and the second message m2
is the flag (hidden). Both ciphertexts c1
and c2
are shown. The zipfile also contains the source code for the server.
A hint specifies that we can extract the digits from the picture without having to copy them down by hand. Opening the JPEG in a hex editor and scrolling to the bottom reveals that m1
, c1
, and c2
, in plain text, have been appended to the end.
The “Hyper RSA” algorithm consists simply of encrypting a message twice in succession with different RSA keys, i.e.
$latex c = (m^{e_1} mod n_1) ^ {e_2} mod n_2$
where n1
< n2
. The server outputs the d1
and d2
for a given e1
and e2
, and thus we can trivially obtain the prime factors p1
, q1
, p2
and q2
(PyCrypto’s Crypto.PublicKey.RSA
computes p
and q
automatically given n
, e
and d
):
from Crypto.PublicKey import RSA n1 = 15648839760729136857611980482709455780823249059433822421717774306890912422191494755459005142111607992828285551194094172211798962121953438089140493712726087426107589267481640653515380577953758668008397344082640238769913089651014044547964783609544160815580263066891992072813496322236255529332740322328153851312961936661859569054622011061087082284128807141569973069232563526976554397613026154984099426439128269723550458457280468287574830259658190319585969033124798862730603620119663542163404213185266003198150728644986436777489704208328734400642015199114476591920301342491956560907159473860286478358614493839228472359041 n2 = 27651436873059903761538209234805214287355683678982466405670882487591411436642516539341196342881585483549012954046212429788184908293604454771642818107648395738428697059696871430441866540519287523373879820608795756105878030307235272632107775558053732559632831099019813290110266640343749177716635424859416795786735047143696302657241912433915888805902148923106710885221002650355051396187383500174867165635719900881013089530132697822486081686748180965007087119379517849387088878883805428586240671351051269319387834355886582079118795891068466933583725489045111641949617725339393138570317367622108145858007000011779852522969 e1 = e2 = 65537L d1 = 801341322260814246824630460655399752818145838891922243118922907272623130275640575542371809160116520803999669038976151516590587254242271361630155437385122135618308277487043746787274626846099365088975410634318638956800407843947741481956296653701423679708979093527160617610847210849213017935527664094073337580381435393973290534456518582972757046608969344358144168412979473661735534751582089315363317465134585473746496679748814555064082863346987139238598778896812527500338856153967668724198040388824191290206624717357686156052566833286119146484827004344810295922696833773843457620396590761038521683261440122811630416689 d2 = 6112369592444250206652792120246931342309257205203457448753438128045787531968813603085828027210972868763821210389054724359391408920906476284797740298236146132148504421988015570026264866754702204115498069199988161171641286983855187064121722759807809689051998476150877140147205896190852409136531995665629661421145487633863099571087668485030471815418539707337877086184665963496813415503578237691985252628347956725542260965108071256348393247267672508371278456781458278900243152599830219742880994068918547883063177015037779478763064694436867660864340069789075521188306329779784328758407937664440952020715843097541060377469 m1 = 5 c1 = 8148957383904486872437828423394164937538328715010815908123576452594275320666784525739878146313196804749598164516555556022796130174158806815977650057288420682101057911994435698789551418753532099866792794103336174859039809215065280033792539448692428913979724682589906786272695510606035436810953248855087491552441188016588591196283340505465679930375746901953008354883148151847641317044080506488273851897144518225216105202686360980631572592060500127238603945606195064022141933818855331043259736453831468578091145713709685544140187848965375739364164364508893499612921555255013961546838234986344590731606667403558040625997 m2 = 6 c2 = 27329182398211347522899299621621113679724446962789840876562317801394564111071637212019506119701609492788475262089552181420526961381419064457579903801235959637592232728341456521117490176148105725533459268071829049691129946006080272549563173732461269479077427460450913074453578152281632632054473142217920030060804696732297435050576686329939098304245739417728714301710598689242692778518299820705103997578604937875292660978826056357830177747872982479026583072444666566525631068826210835394802088986175053586772425867255572780854762523224511193257888117843294675036009361255570186337202498474539261702804344134624782089612 k1 = RSA.construct((n1, e1, d1)) print 'p1 =', k1.p print 'q1 =', k1.q k2 = RSA.construct((n2, e2, d2)) print 'p2 =', k2.p print 'q2 =', k2.q
With this information we have the entire private key. However, the exponents e1
and e2
used to encrypt the flag are censored out, so we have to find them in order to decrypt the flag.
e1
and e2
are each 9 digits long, and we are given (in a second hint) the first digits of each. Thus, we’re looking at testing 108 possibilities for each. We can’t bruteforce both simultaneously – that would be 1016. However, we can apply a meet-in-the-middle attack – encrypt m1
with all possible e1
to get all possible intermediate values, then decrypt c1
with all possible e2
to find a matching intermediate value (thus “meeting in the middle” of the encryption algorithm). This will only require 2*108 operations. One snag is that RSA decryption is very slow – on the order of O(k3) where k
is the number of bits in the key. Thus, we have to optimize our attack carefully, and use a lot of CPU power to achieve bruteforce.
We ended up making three major optimizations:
- When building the table, we use an incremental approach – instead of computing
pow(m1, e1, n1)
for eache1
, we just multiply in one power ofm1
per iteration, making building thee1
lookup table extremely fast. - Because computing decryptions mod
n2
is slow (becausen2
is long), we do decryptions and checking modp2
instead, which is half as long (and thus saves us a factor of 8 on decryption). - We use
gmp
instead of Python’s built-inpow
operators, resulting in approximately a 3x speedup. Note that the bottleneck is in the math operations, so using Python incurs only a very small overhead.
Here’s the bruteforce script:
from gmpy2 import mpz, powmod, invert # meet in the middle attack n1 = mpz(15648839760729136857611980482709455780823249059433822421717774306890912422191494755459005142111607992828285551194094172211798962121953438089140493712726087426107589267481640653515380577953758668008397344082640238769913089651014044547964783609544160815580263066891992072813496322236255529332740322328153851312961936661859569054622011061087082284128807141569973069232563526976554397613026154984099426439128269723550458457280468287574830259658190319585969033124798862730603620119663542163404213185266003198150728644986436777489704208328734400642015199114476591920301342491956560907159473860286478358614493839228472359041) n2 = mpz(27651436873059903761538209234805214287355683678982466405670882487591411436642516539341196342881585483549012954046212429788184908293604454771642818107648395738428697059696871430441866540519287523373879820608795756105878030307235272632107775558053732559632831099019813290110266640343749177716635424859416795786735047143696302657241912433915888805902148923106710885221002650355051396187383500174867165635719900881013089530132697822486081686748180965007087119379517849387088878883805428586240671351051269319387834355886582079118795891068466933583725489045111641949617725339393138570317367622108145858007000011779852522969) p1 = mpz(115828295588509413765657818406867516713730517750406039982953961273698342446377470065543059874105959316059071676627111613027995982755516722160046640303691478999889733390879707640738371507340736543076237714407338833975162482946492306016288364033279369606285818202050898542042181475127562843844261502082049451227) q1 = mpz(135103773056655064969327506003939867649872833235629494898764581981413343302990941336623787091683416428670125487239155394679492615647758178987290636992620311385747374284630346686620855693905295085863655381869122493807428330545415365431415934084226911263424822550817854835936312436952242226359922048497488873683) p2 = mpz(163285832687189913045952152660621786604149906417402530050953092553171258537152762832184438186860672227164970561277480748279613378334451380709795952602873533444456320203007264412590461372740523801471983692035577494148110307472755151546109784012759812974741115311165842719401610463185434834345546986734727953667) q2 = mpz(169343760067858061351166362331954299819512318283672226414306253481409314715323098520315259960487582355659180136088759841701142965265979658078717533720357215488070848738430999730412016543181673065902912717103510024351166643904333588480201806104579175415269242403617262452711780440867442673992494021555894641907) m1 = mpz(1291575311227647404937745977674300102552159140130724372672273512124326901689475940631797723907388299601908770592275163938119318483657985194901345321320) c1 = mpz(3612783941217721669487918278209911521323696929552012175100130528144953435277957012327876297909487756767163399469731122537754642368918321895211682844060051902226307200945658526452184995263241224610748656158064831467711457662659609765597325107742484418555573571733455873166185394833773943367518472411975358177757721075900099526140241289604501293079211412322023186020382617546224962628798465635111775435684570876736680218992899063074934853328180993221927800114698375815660980849891750946707282694641977171200300901905249566807230865872745785916874656741470951342227797544099362706484433003094851882722308369118455538479) c2 = mpz(13554020299054897579091249483707073724753575878577890667835151695153327166821147325776948828180031343971239625502353717032241825522884776444516550436927491420615799010177208450427000928631877919824890289537268758084609024561404818087048530732153137144763446412469130412144867577343408806134871838870680552607734311806612058857668100766723847349927926306865890803886354120634869482202437175237946453446209830904959307383604085169656048730378082783123213285644191038076249938111121280355516863102198358773908064076460289440391523937387728878878040531689192081196613749284337216979844083031586740238692461049241310327972) # test: e1=e2=65537 # m1 = mpz(5) # c1 = mpz(8148957383904486872437828423394164937538328715010815908123576452594275320666784525739878146313196804749598164516555556022796130174158806815977650057288420682101057911994435698789551418753532099866792794103336174859039809215065280033792539448692428913979724682589906786272695510606035436810953248855087491552441188016588591196283340505465679930375746901953008354883148151847641317044080506488273851897144518225216105202686360980631572592060500127238603945606195064022141933818855331043259736453831468578091145713709685544140187848965375739364164364508893499612921555255013961546838234986344590731606667403558040625997) cache = {} cur = powmod(m1, 10000000, n1) for e1 in xrange(10000000, 20000000): if e1 % 100000 == 0: print "build progress:", e1 cache[cur % p2] = e1 cur = (cur * m1) % n1 p2a = p2-1 c1p = c1 % p2 def doit(e2): if e2 % 100000 == 0: print "check progress:", e2 try: dp = invert(e2, p2a) except Exception: return None mp = powmod(c1p, dp, p2) if mp in cache: print "FOUND IT!!!", cache[mp], e2 return cache[mp], e2 return None import multiprocessing p = multiprocessing.Pool() results = p.map(doit, xrange(20000000, 30000000)) for r in results: if r is not None: print r
We were able to obtain a solution in approximately 15 minutes after running it on a 48-core machine. The values were e1 = 134217757, e2 = 251658263
.
We plugged these in to the HyperRSA decryption algorithm:
e1 = 134217757 e2 = 251658263 n1 = 15648839760729136857611980482709455780823249059433822421717774306890912422191494755459005142111607992828285551194094172211798962121953438089140493712726087426107589267481640653515380577953758668008397344082640238769913089651014044547964783609544160815580263066891992072813496322236255529332740322328153851312961936661859569054622011061087082284128807141569973069232563526976554397613026154984099426439128269723550458457280468287574830259658190319585969033124798862730603620119663542163404213185266003198150728644986436777489704208328734400642015199114476591920301342491956560907159473860286478358614493839228472359041 n2 = 27651436873059903761538209234805214287355683678982466405670882487591411436642516539341196342881585483549012954046212429788184908293604454771642818107648395738428697059696871430441866540519287523373879820608795756105878030307235272632107775558053732559632831099019813290110266640343749177716635424859416795786735047143696302657241912433915888805902148923106710885221002650355051396187383500174867165635719900881013089530132697822486081686748180965007087119379517849387088878883805428586240671351051269319387834355886582079118795891068466933583725489045111641949617725339393138570317367622108145858007000011779852522969 p1 = 115828295588509413765657818406867516713730517750406039982953961273698342446377470065543059874105959316059071676627111613027995982755516722160046640303691478999889733390879707640738371507340736543076237714407338833975162482946492306016288364033279369606285818202050898542042181475127562843844261502082049451227 q1 = 135103773056655064969327506003939867649872833235629494898764581981413343302990941336623787091683416428670125487239155394679492615647758178987290636992620311385747374284630346686620855693905295085863655381869122493807428330545415365431415934084226911263424822550817854835936312436952242226359922048497488873683 p2 = 163285832687189913045952152660621786604149906417402530050953092553171258537152762832184438186860672227164970561277480748279613378334451380709795952602873533444456320203007264412590461372740523801471983692035577494148110307472755151546109784012759812974741115311165842719401610463185434834345546986734727953667 q2 = 169343760067858061351166362331954299819512318283672226414306253481409314715323098520315259960487582355659180136088759841701142965265979658078717533720357215488070848738430999730412016543181673065902912717103510024351166643904333588480201806104579175415269242403617262452711780440867442673992494021555894641907 m1 = 1291575311227647404937745977674300102552159140130724372672273512124326901689475940631797723907388299601908770592275163938119318483657985194901345321320 c1 = 3612783941217721669487918278209911521323696929552012175100130528144953435277957012327876297909487756767163399469731122537754642368918321895211682844060051902226307200945658526452184995263241224610748656158064831467711457662659609765597325107742484418555573571733455873166185394833773943367518472411975358177757721075900099526140241289604501293079211412322023186020382617546224962628798465635111775435684570876736680218992899063074934853328180993221927800114698375815660980849891750946707282694641977171200300901905249566807230865872745785916874656741470951342227797544099362706484433003094851882722308369118455538479 c2 = 13554020299054897579091249483707073724753575878577890667835151695153327166821147325776948828180031343971239625502353717032241825522884776444516550436927491420615799010177208450427000928631877919824890289537268758084609024561404818087048530732153137144763446412469130412144867577343408806134871838870680552607734311806612058857668100766723847349927926306865890803886354120634869482202437175237946453446209830904959307383604085169656048730378082783123213285644191038076249938111121280355516863102198358773908064076460289440391523937387728878878040531689192081196613749284337216979844083031586740238692461049241310327972 from Crypto.Util.number import inverse d1 = inverse(e1, (p1-1)*(q1-1)) d2 = inverse(e2, (p2-1)*(q2-1)) m = pow(pow(c2, d2, n2), d1, n1) print format(m, 'x').decode('hex')
to get the flag: BCTF{learn-y0u-A_haske11-f0r_9rea7-g00d}