Akasec CTF 2024 Writeup

この大会は2024/6/7 22:37(JST)~2024/6/9 22:37(JST)に開催されました。
今回もチームで参戦。結果は872点で696チーム中105位でした。
自分で解けた問題をWriteupとして書いておきます。

Sanity Check (MISC)

Discordに入り、#rulesチャネルのトピックを見ると、フラグが書いてあった。

AKASEC{Typ1c4l_s4n1ty_ch3ck_1n_d1sc0rd}

Sperm Rev (REV)

$ strings sperm_rev| grep AKASEC                       
AKASEC{strings_b35t_t00l_1n_r3v3r5e_eng1n33r1ng}
AKASEC{strings_b35t_t00l_1n_r3v3r5e_eng1n33r1ng}

Lost (CRYPTO)

平文の上位ビットがわかっているので、Coppersmithの定理を使って復号する。

#!/usr/bin/env sage
from Crypto.Util.number import *

with open('out.txt', 'r') as f:
    params = f.read().splitlines()

e = 2
n = int(params[0].split(' ')[-1])
c = int(params[1].split(' ')[-1])
cor_m = int(params[2].split(' ')[-1])

PR.<x> = PolynomialRing(Zmod(n))
f = (cor_m + x)^e - c
x0 = int(f.small_roots(X=2^160, beta=1)[0])
m = cor_m + x0
flag = long_to_bytes(m).decode()
print(flag)
AKASEC{c0pp3r5m17h_4774ck_1n_1ov3_w17h_5m4ll_3xp0n3nts}

Power Over All (CRYPTO)

暗号処理の概要は以下の通り。

・ps = key_gen()
 ・ps = []
 ・n: ランダム2以上2**6以下の整数
 ・n回以下繰り返し
  ・p: 256ビット素数
  ・psにpを追加
 ・psを返却
・c = encrypt([FLAGの数値化したもの], ps)
 ・m: FLAGの数値化したもの
 ・psをソート
 ・psの各pについて以下を実行
  ・e = 2
  ・m = pow(m, e, p)
 ・mを返却
・psとcを出力

legendreの定理を使って、平方を取っていくことを繰り返す。複数の数値に復号されるので、文字列にしてフラグの形式になるものを探す。

#!/usr/bin/env python3
from Crypto.Util.number import *

def legendre(a, p):
    return pow(a, (p - 1) // 2, p)

def tonelli_shanks(a, p):
    if legendre(a, p) != 1:
        return -1

    q = p - 1
    s = 0
    while q % 2 == 0:
        q >>= 1
        s += 1

    for z in range(2, p):
        if legendre(z, p) == p - 1:
            break

    m = s
    c = pow(z, q, p)
    t = pow(a, q, p)
    r = pow(a, (q + 1) // 2, p)

    t2 = 0
    while True:
        if t == 0: return 0
        if t == 1: return r
        t2 = (t * t) % p
        for i in range(1, m):
            if t2 % p == 1:
                break
            t2 = (t2 * t2) % p
        b = pow(c, 1 << (m - i - 1), p)
        m = i
        c = (b * b) % p
        t = (t * c) % p
        r = (r * b) % p

with open('out.txt', 'r') as f:
    params = f.read().splitlines()

ps = eval(params[0].split(' = ')[1])
c = int(params[1].split(' = ')[1])

ms = [c]
for p in ps[::-1]:
    tmp = []
    for m in ms:
        x = tonelli_shanks(m, p)
        if x != -1:
            tmp.append(x)
            tmp.append(p - x)
    ms = tmp

for m in ms:
    flag = long_to_bytes(m)
    if flag.startswith(b'AKASEC{'):
        flag = flag.decode()
        print(flag)
        break
AKASEC{akasec+palestine=<3}

GCL (CRYPTO)

暗号処理の概要は以下の通り。

・BIITS = 128
・m: 128ビット素数
・s: ランダム127ビット整数
・a: ランダム127ビット整数
・b: ランダム127ビット整数
・c = []
・r = s
・FLAGの各文字について以下を実行
 ・r = lcg(r, ord(i))
  ・ord(i) * (a * r + b) % mを返却
 ・cにrを追加
・m、cを出力

flagが"AKASEC{"から始まることを前提にa, b, sを求める。
n番目の文字を処理した後のrをrnと表すと、以下のようになる。

r0 = flag[0] * (a * s + b) % m
r1 = flag[1] * (a * r0 + b) % m
r2 = flag[2] * (a * r1 + b) % m
        :

そして以下のように変形できる。

r0 * inverse(flag[0], m) = (a * s + b) % m
r1 * inverse(flag[1], m) = (a * r0 + b) % m
r2 * inverse(flag[2], m) = (a * r1 + b) % m

さらに以下のように変形でき、a, b, sを算出できる。

(r2 * inverse(flag[2], m) - r1 * inverse(flag[1], m)) % m = a * (r1 - r0) % m
a = (r2 * inverse(flag[2], m) - r1 * inverse(flag[1], m)) * inverse(r1 - r0, m) % m
b = (r1 * inverse(flag[1], m) - a * r0) % m
s = (r0 * inverse(flag[0], m) - b) * inverse(a, m) % m

あとは順に逆関数inverseを使って、flagの各文字を求めていく。

#!/usr/bin/env python3
from Crypto.Util.number import *

with open('out.txt', 'r') as f:
    params = f.read().splitlines()

m = int(params[0].split(' = ')[1])
c = eval(params[1].split(' = ')[1])

head_flag = b'AKASEC{'

a = (c[2] * inverse(head_flag[2], m) - c[1] * inverse(head_flag[1], m)) * inverse(c[1] - c[0], m) % m
b = (c[1] * inverse(head_flag[1], m) - a * c[0]) % m
s = (c[0] * inverse(head_flag[0], m) - b) * inverse(a, m) % m
print('[+] a =', a)
print('[+] b =', b)
print('[+] s =', s)

assert head_flag[0] * (a * s + b) % m == c[0]
for i in range(6):
    assert head_flag[i+1] * (a * c[i] + b) % m == c[i+1]

c = [s] + c
flag = ''
for i in range(len(c) - 1):
    code = c[i + 1] * inverse(a * c[i] + b, m) % m
    flag += chr(code)
print('[*] flag:', flag)

実行結果は以下の通り。

[+] a = 57879387829375788513313321928485165943
[+] b = 123253563871232818483050010946596502567
[+] s = 96397753169964322958020651044633108293
[*] flag: AKASEC{++see_?!_just_some_math--}
AKASEC{++see_?!_just_some_math--}

Twin (CRYPTO)

暗号処理の概要は以下の通り。

・e = 5
・p, q: 256ビット素数
・n = p * q
・m1: FLAGを数値化したもの
・m2: m1を8ビットシフトしたもの
・c1 = pow(m1, e, n)
・c2 = pow(m2, e, n)
・n, c1, c2を出力

m1は以下の式で表せる。

m1 = m2 * 256 + A (A: 0以上255以下)

m2 * 256 と m1 の差は255以下で、それぞれのRSA暗号化したものはわかる。このため、Coppersmith's Short Pad Attackで復号できる。

#!/usr/bin/env sage
from Crypto.Util.number import *

def short_pad_attack(c1, c2, e, n):
    PRxy.<x,y> = PolynomialRing(Zmod(n))
    PRx.<xn> = PolynomialRing(Zmod(n))
    PRZZ.<xz,yz> = PolynomialRing(Zmod(n))

    g1 = x^e - c1
    g2 = (x+y)^e - c2

    q1 = g1.change_ring(PRZZ)
    q2 = g2.change_ring(PRZZ)

    h = q2.resultant(q1)
    h = h.univariate_polynomial()
    h = h.change_ring(PRx).subs(y=xn)
    h = h.monic()

    kbits = n.nbits()//(2*e*e)
    diff = h.small_roots(X=2^kbits, beta=0.5)[0]

    return diff

def related_message_attack(c1, c2, diff, e, n):
    PRx.<x> = PolynomialRing(Zmod(n))
    g1 = x^e - c1
    g2 = (x+diff)^e - c2

    def gcd(g1, g2):
        while g2:
            g1, g2 = g2, g1 % g2
        return g1.monic()

    return -gcd(g1, g2)[0]

with open('out.txt', 'r') as f:
    params = f.read().splitlines()

e = 5
n = int(params[0].split(' = ')[1])
c1 = int(params[1].split(' = ')[1])
c2 = int(params[2].split(' = ')[1])

c2_2 = c2 * pow(256, e, n) % n

diff = short_pad_attack(c2_2, c1, e, Integer(n))
m2 = related_message_attack(c2_2, c1, diff, e, n)
m1 = int(m2 + diff)
flag = long_to_bytes(m1).decode()
print(flag)
AKASEC{be_on_the_right_side_of_history_free_palestine}