Daily AlpacaHackという毎日0時に1題ずつ公開される常設CTFの1月30日の問題、「Linear Coffee Generator」の作問を担当したので、公式writeupを書きます。当日に解けた方も解けなかった方もぜひ復習に利用していただけると嬉しいです。
問題ソースコードは以下の通り。
import os import hashlib from Crypto.Util.number import * from Crypto.Cipher import AES from Crypto.Util.Padding import pad, unpad flag = os.environ.get("FLAG", "Alpaca{dummy}").encode() # You don't need to focus on encrypt_flag/decrypt_flag :) def encrypt_flag(pt, params): key = hashlib.sha256(str(params).encode()).digest() cipher = AES.new(key, AES.MODE_CBC) return cipher.iv + cipher.encrypt(pad(pt, 16)) def decrypt_flag(ct, params): key = hashlib.sha256(str(params).encode()).digest() cipher = AES.new(key, AES.MODE_CBC, iv=ct[:16]) return unpad(cipher.decrypt(ct[16:]), 16) class LCG: def __init__(self): self.p = getPrime(64) self.a = getRandomRange(1, self.p) self.b = getRandomRange(1, self.p) self.s = getRandomRange(1, self.p) def serve_coffee(self): self.s = (self.a * self.s + self.b) % self.p return self.s lcg = LCG() enc_flag = encrypt_flag(flag, (lcg.p, lcg.a, lcg.b, lcg.s)).hex() print("#1 order:", lcg.serve_coffee()) print("#2 order:", lcg.serve_coffee()) print("#3 order:", lcg.serve_coffee()) print("#4 order:", lcg.serve_coffee()) # If you can recover p, a, b, and s, you can get the flag using decrypt_flag(bytes.fromhex(enc_flag), (p, a, b, s))! print("encrypted flag:", enc_flag)
64bitのランダムな素数、および
未満のランダムな整数
を用いて、以下のようにして
serve_coffeeの出力が計算されています。
与えられた4つの出力からをすべて復元することができたら
decrypt_flagでフラグが得られます。
これはLCG(Linear Congruential Generator)という疑似乱数生成器の一種です。
encrypt_flagとdecrypt_flagは(p, a, b, s)を使ってflagを暗号化/復号していますが、
特定の値を計算できたらフラグが得られる、という形式の問題ではよくあるパターンで、これらの関数には特に脆弱性はありません。
まずは、与えられた4つの出力から式を立てましょう。出力をそれぞれとすると、以下の4つの式が成り立ちます。
これらの式を変形して の形に持っていきます。
まず隣接する式同士を引き算して、を消去します。
さらにこれらの式を使って、も消去します。
2つ目と3つ目の式を使うと、
が成り立ちます。ここから、 が
の倍数であることが分かります。
具体的にこの値を計算するととなります。これは素数ではなく、124bit程度と大きいため、pと別の数の積となっていると考えられます。
この値を素因数分解してみると、となります。
ちなみに、ある程度高速な素因数分解アルゴリズムを使わないと時間がかかります。例えば、時間の試し割りアルゴリズムでは計算しているうちに地球が滅亡してしまいます。Daily AlpacaHackなのでせめて24時間以内には終わってほしいです。
Pythonであればsympy.ntheory.factorintや、SageMathのfactor関数を使うと一瞬で答えが求まります。それすら面倒であれば https://factordb.com などのオンラインサービスもあります。
さて、素因数のうち、64bitの素数は1つしかないので、が
であると分かります。
次はを求めましょう。
という式から、
と求まります。
はPythonであれば
pow(s2 - s1, -1, p)で計算できます。
も求まりました。次は
です。
という式を変形して、
と求まります。
最後にを求めましょう。
という式を変形して、
と求まります。
これでがすべて求まりました。あとは
decrypt_flagに与えてフラグを得ましょう。
これを実装したソルバは以下の通りです。
import hashlib from sympy.ntheory import factorint from Crypto.Cipher import AES from Crypto.Util.Padding import unpad s1 = 4052328936969804578 s2 = 8676271689691567645 s3 = 2647032430467963079 s4 = 6612596210231769351 flag_enc = "0c5355c5bb76b2a86aa7cf53279fb2350883865f2ca7423ff47512278a59a8db1ed85e82e0d84c2fec52e29d0b3aefd97d791f11edf18efdf1febc07ae860b8b" def decrypt_flag(ct, params): key = hashlib.sha256(str(params).encode()).digest() cipher = AES.new(key, AES.MODE_CBC, iv=ct[:16]) return unpad(cipher.decrypt(ct[16:]), 16) d1 = s2-s1 d2 = s3-s2 d3 = s4-s3 p = max(factorint(d2*d2 - d1*d3).keys()) a = d2*pow(d1,-1,p)%p b = (s2-s1*a)%p s = (s1-b)*pow(a,-1,p)%p print(decrypt_flag(bytes.fromhex(flag_enc), (p,a,b,s)))
ちなみに、SageMathを使うと面倒な式変形を一切せずに、グレブナー基底を計算する関数に与えられた式をそのまま投げるだけで解けます。
PR = PolynomialRing(ZZ, "a, b, s") a, b, s = PR.gens() I = PR.ideal([ s*a+b - s1, s1*a+b - s2, s2*a+b - s3, s3*a+b - s4, ]).groebner_basis() print(I) # [a + 1466936314606773311942879293366691031, b + 3280619628752045602450999089964835177, s + 2922826573297502520907432861726796897, 4503796536267106544318683573865873033] mul_p = int(I[3]) p = max(factorint(mul_p).keys()) a = int(I[0].univariate_polynomial().roots()[0][0] % p) b = int(I[1].univariate_polynomial().roots()[0][0] % p) s = int(I[2].univariate_polynomial().roots()[0][0] % p) print(p, a, b, s) # 13076220846716751461 6534081916410610007 12854780772490122855 12938961673315310206 print(decrypt_flag(bytes.fromhex(flag_enc), (p, a, b, s)))