Daily AlpacaHack - Linear Coffee Generator
問題ソースコードは以下の通り。
問題ソースコード
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)))