AlpacaHack Round 9 (Crypto) Writeup
1. はじめに
2025/1/26 12:00 JST ~18:00 に開催された「AlpacaHack Round9 (Crypto)」参加しました。数時間とはいえ、CTF にまとまった時間を投入できたのは久しぶりでした*1。2 完で 6 位。
最初の 1 問で色々迷走し、時間を使い過ぎてしまった点は大いなる反省点でした。
2. Writeup
AlpacaHackの過去問は常設コンテンツとなるため、フラグの記載は省略します。
2.1. RSAMPC (16 solves)
設問
I calculated the RSA public key using multi-party computation.
以下の 2 ファイルが提供されます。
「chall.py」
import os from Crypto.Util.number import getRandomRange, getPrime, bytes_to_long FLAG = os.environ.get("FLAG", "fakeflag").encode() def additive_share(a): t0, t1 = getRandomRange(-2**512, 2**512), getRandomRange(-2**512, 2**512) t2 = a-t0-t1 return t0, t1, t2 def replicated_share(a): t = additive_share(a) return [(t[i], t[(i+1)%3]) for i in range(3)] def multiply_shares(sa, sb): def mul(t, u): return t[0]*u[0]+t[0]*u[1]+t[1]*u[0] r = additive_share(0) z = [mul(sa[i], sb[i])+r[i] for i in range(3)] w = [(z[i], z[(i+1)%3]) for i in range(3)] return w def reconstruct(s): return s[0][0] + s[0][1] + s[1][1] p = getPrime(512) q = getPrime(512) sp = replicated_share(p) sq = replicated_share(q) print("your share of p:", sp[0]) print("your share of q:", sq[0]) spq = multiply_shares(sp, sq) print("your share of pq:", spq[0]) n = reconstruct(spq) assert n == p*q print("n:", n) e = 0x10001 c = pow(bytes_to_long(FLAG + os.urandom(127-len(FLAG))), e, n) print("e:", e) print("c:", c)
「output.txt」
your share of p: (-384164070196680113629973964276599320736606300184523772854135294036334447818682200607218877531386512793858125339877828582394197679795576991953411880314517, 178776721087372919385257940734429604253240493277094581482580652949038337321961407291832241379559936948198042043881180916670462219794291885959730598632423) your share of q: (-10504102453855211730773548202462643334445368588122773952797120588540073173181223269420294976331168878842123082669069593895980908615299058089156709125348617, 3324659724832936014805633502878093035237335054058544453532695059432217891926271390882999445452501190449380595220556388508799059755133895886341486877191502) your share of pq: (880194945859095512548778390949753106113259354062743403885130575509194611686622871911550689148439940097472063798899034574466553154127726867674397008987477001207806315461004286936941315001029394217039765579529660629019466179402060549350587729722354909331590051509695082365313846996923469825646557408789955494, 40388351148875096689764230410867470980240794826105168292967479483809364773078955483003274901375600951153408618729650715655666480989756454152565386666760509805904377793675351489295406907138019316039841793386393194481700178651652081449097569147179108704523020190287922457859082133424057955783092523665228634328) n: 122269467950798077326822634108968850809243750508493781647505745002863843379348700424238562022365315227978807541070854658246091147872559714237246479088170538196473585543281713624525798244748333546435600544573727499127916535316599284592352755786055339638261774730837681190375466416924715653324305527245715836447 e: 65537 c: 100976267335628681910815317357700490412039872278731196009735781349258998302355802361980783540754919888894607253589239383351290237447746132667260747986281172840910605287343986031579879857474734142154881821962810929745626899955618676413832332521656625264015203959361696843594006345498340544121922011105950850715
検討
リークされる sp[0] 、sq[0]、spq[0] が何なのかよくわからなかったので、SageMath の数式処理で把握することにしました。 「test.sage」
PR.<p,q,tp0,tp1,tq0,tq1,tr0,tr1> = PolynomialRing(ZZ) tp2 = p - tp0 -tp1 sp = [(tp0,tp1), (tp1,tp2), (tp2,tp0)] print(sp[0]) #(tp0,tp1) tq2 = q - tq0 -tq1 sq = [(tq0,tq1), (tq1,tq2), (tq2,tq0)] print(sq[0]) #(tq0,tq1) def mul(t, u): return t[0]*u[0]+t[0]*u[1]+t[1]*u[0] tr2 = -tr0-tr1 r = (tr0, tr1, tr2) z = [mul(sp[i], sq[i])+r[i] for i in range(3)] spq = [(z[i], z[(i+1)%3]) for i in range(3)] print(spq[0]) #(tp0*tq0 + tp1*tq0 + tp0*tq1 + tr0, q*tp1 - tp1*tq0 + p*tq1 - tp0*tq1 - tp1*tq1 + tr1)
spq[0][0]からは良い情報を得られそうにありませんので、spq[0][1]の方を見てみます。
tp0、tp1、tq0、tq1 は既知ですから、spq[0][1] の未知数は p, q, r[1] の 3つでいずれも 512bit、ということになります。
最初これを(愚かにも) LLL で処理しようとしたのですが、無理筋でした(未知数が大きすぎ!)。気を取り直して r[1] を "誤差" と見なし、 p (もしくは q)の近似値を求めて総当たりしてみたところ、うまくいきました。想定解法のようです。
解法(ソルバ)
「solve.sage」
from Crypto.Util.number import * import gmpy2 sp = (-384164070196680113629973964276599320736606300184523772854135294036334447818682200607218877531386512793858125339877828582394197679795576991953411880314517, 178776721087372919385257940734429604253240493277094581482580652949038337321961407291832241379559936948198042043881180916670462219794291885959730598632423) sq = (-10504102453855211730773548202462643334445368588122773952797120588540073173181223269420294976331168878842123082669069593895980908615299058089156709125348617, 3324659724832936014805633502878093035237335054058544453532695059432217891926271390882999445452501190449380595220556388508799059755133895886341486877191502) spq = (880194945859095512548778390949753106113259354062743403885130575509194611686622871911550689148439940097472063798899034574466553154127726867674397008987477001207806315461004286936941315001029394217039765579529660629019466179402060549350587729722354909331590051509695082365313846996923469825646557408789955494, 40388351148875096689764230410867470980240794826105168292967479483809364773078955483003274901375600951153408618729650715655666480989756454152565386666760509805904377793675351489295406907138019316039841793386393194481700178651652081449097569147179108704523020190287922457859082133424057955783092523665228634328) n = 122269467950798077326822634108968850809243750508493781647505745002863843379348700424238562022365315227978807541070854658246091147872559714237246479088170538196473585543281713624525798244748333546435600544573727499127916535316599284592352755786055339638261774730837681190375466416924715653324305527245715836447 e = 65537 c = 100976267335628681910815317357700490412039872278731196009735781349258998302355802361980783540754919888894607253589239383351290237447746132667260747986281172840910605287343986031579879857474734142154881821962810929745626899955618676413832332521656625264015203959361696843594006345498340544121922011105950850715 N = n * sp[1] * sq[1] b = spq[1] + sp[1]*sq[0] + sp[0]*sq[1] +sp[1]*sq[1] D = B**2 - 4*N sqD = int(gmpy2.isqrt(D)) approx_p = ((b + sqD)//2) // sq[1] for i in range(-100000,100000): p = approx_p+i if n % p == 0: print("found!") break q = n // p d = pow(e,-1,(p-1)*(q-1)) print(long_to_bytes(pow(c,d,n)))
2.2. addprimes (16solves)
設問
addprimes in PARI/GP is useful for implementing RSA decryption.
以下のファイルが提供されます。 「server.sage」
import os import signal from sage.misc.banner import require_version from Crypto.Util.number import getPrime, bytes_to_long assert require_version(10), "This challenge requires SageMath version 10 or above" signal.alarm(30) FLAG = os.environ.get("FLAG", "Alpaca{*** FAKEFLAG ***}").encode() assert FLAG.startswith(b"Alpaca{") p = getPrime(512) q = getPrime(512) n = p * q e = 37 print("n:", n) print("e:", e) c = int(input("ciphertext: ")) assert 1 < c < n-1 pari.addprimes(p) m = mod(c, n).nth_root(e) print("plaintext:", m) padded_flag = FLAG + os.urandom(127-len(FLAG)) print("encrypted flag:", pow(bytes_to_long(padded_flag), e, n))
検討
「e=37だから37個集めてCRTすれば終わりっしょ」…なわけありませんでした。ちゃんと padding されていますのでダメです。
そこで私はこう考えました。「Zmod(n) における 1 の e 乗根で 1 以外のもの(ここでは w と書くことにする)が判明して、たとえば w mod p ==1、w mod q !=1で1以外、なんていうことであれば、 GCD(w, n) == p となり幸せになれる!」*2
最近クジ運はそう悪くない*3ので、実装してみることにしました。
p, q が求められたら、mod.p、mod.q で e 乗根を計算して CRT すれば解候補を列挙できるので、そこから long_to_bytesして b"Alpaca{" を含むものを選べば OK です。
解法(ソルバ)
「solve.sage」
from Crypto.Util.number import * from Crypto.Cipher import AES from hashlib import sha256 from pwn import * import random import itertools HOST = <redacted> PORT = <redacted> while(True): r = remote(HOST, int(PORT)) try: n = int(r.readline().decode().replace("n: ","")) m0 = random.randint(2**1023,2**1024) % n c = pow(m0,e,n) r.readline() kw = b"ciphertext: " data = str(c).encode() r.sendlineafter(kw, data) m = int(r.readline().decode().replace("plaintext:","")) enc = int(r.readline().decode().replace("encrypted flag: ","")) r.close() if m != m0: print("hit") root = m * pow(m0,-1,n) % n if 1 < gcd(root - 1, n) < n: break except: try: r.close() except: pass p = gcd(root - 1, n) q = n // p d = pow(e,-1,(p-1)*(q-1)) mps = GF(p)(enc).nth_root(e, all=True) mqs = GF(q)(enc).nth_root(e, all=True) for mp in mps: for mq in mqs: pt = long_to_bytes(crt([int(mp),int(mq)],[p,q])) if b"Alpaca" in pt: print(pt) exit()
3. おわりに
久しぶりにまとまった時間を使って Crypto に取り組んで楽しめました。
解けなかった問題*4については「Upsolve queue」に投入し、腰を据えて取り込もうと思います。