Quantcast
Channel: プログラミング
Viewing all articles
Browse latest Browse all 8051

SECCON 13 予選 Writeup 兼参加記 FCCPC - kyuridenamidaのブログ

$
0
0

感想

FCCPC 国内13位(全体40位)

途中までいい感じだったんですけど僕がdual_summonでドハマりしたので失速しました(数弱)

赤ちゃんの世話しながらだったんですが、割と本戦に行きたかったので本格的にベビーシッター雇ったり、少し妻に多めに赤ちゃんの世話をさせてしまったり、いい年して24時間連続で起きていましたが普通に敗退したのでなんか俺何してんだろ...感すごいです。来年はちゃんと寝て本戦いきます。

実はSECCONに出るメンバーがそんなに集まっていなくて、ブーブーの本戦でお会いしたkusanoさんにダメ元で声をかけさせていただいたところいいよ!とのことだったので一緒に参加させてもらいました。

以下 Writeup

packed

UPX適用するやつね〜と適用したらカスのバイナリが現れ、わかんないからkusanoさんにお願いしたらUPXは罠だったらしい。ええ〜

reiwa_rot13

eが小さいから普通に累乗根求めるやつだと思って、チームメイトにsagemathのコード(ただし法が素数のときだけ動く)を投げ、迷惑をかける。 法が素数じゃなかったら普通にeはそんな小さくないね..となる。 rot13だけど、文字毎に折り返しが起きてるか、つまり桁ごとに+13するか-13を決める。みたいなことをすると、「xe%nと(x+a)e%nからxを求める」みたいな問題になるねという話をチームメイトとする。

RSAの一般的なテクはインターネットによくまとめられているので、一つ一つ使えそうなやつはないか精査していく。

Franklin-Reiter Related Message Attack とかいうやつがまさにそれじゃんとなり、実行するとdnjqygbmorがキーであることがわかったので、あとはチームメイトにフラグ復元してもらう。

https://project-euphoria.dev/blog/27-rsa-attacks/#franklin-reiter-related-message-attack

flag: SECCON{Vim_has_a_command_to_do_rot13.g?is_possible_to_do_so!!}

Jump

ARMいやすぎ〜。心を無にしてClaudeに逆アセンブリ結果をぶんなげ、すべてを解かせる。一回もgdbを実行することなくフラグをゲット。こんなんでいいのか?

flag: SECCON{5h4k3_1t_up_5h-5h-5h5hk3}

drive.google.com

dual_summon

簡単枠なのに全然わからないとチームで話題に。皆さんが少し考察を進めるもののあまり進捗はなさそうなので、自分も取り組んで見ることに(コンテスト開始から7時間後くらいのこと)。結果としてそこから12時間以上ハマることになりました。

そもそもガロア体ってなんだよ。

まず全然わからないのだけれどチームメイトがこれ関連だろうな〜ということでほん怖AES-GCMという記事を共有してくれた。

jovi0608.hatenablog.com

Wikipediaを読んでも細かいところの処理が全然わからないのでpycryptodomeの実装を読みまくることに。

コメントに出てくる色々もよくわからないので

https://nvlpubs.nist.gov/nistpubs/legacy/sp/nistspecialpublication800-38d.pdf

も読むことに。上記PDFは死ぬほど読みやすく、細かいところもしっかり把握できるようになっていて仕様書って最高となる。

とりあえずタグの計算を平文Mを含む数式に落とし込むことができたので、あとは色々頑張って内部のキーとかを特定してごにょごにょすることに。

紆余曲折、徹夜でふやけた頭にはきついバグで数時間溶かすなど、いろいろあり、コンテスト開始から22時間経ったくらいに

SECCON{Congratulation!_you are_master_of_summonor!_you_can_summon_2_monsters_in_one_turn}

コード(Sage)

import socket
import secrets
from Crypto.Cipher import AES
import time


classSummoningAPI:
    def__init__(self, host=None, port=None):
        if host and port:
            self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.sock.connect((host, port))
            self.sock_file = self.sock.makefile('rwb', buffering=0)
        else:
            # ローカルテスト用
            self.keys = [secrets.token_bytes(16) for _ inrange(2)]
            self.nonce = secrets.token_bytes(16)

    defrecv_until(self, delim=b'>'):
        """        delimまでの文字列を受信する"""ifnothasattr(self, 'sock'):
            return b''

        data = b''whilenot data.endswith(delim):
            data += self.sock_file.read(1)
        return data

    defsummon(self, number, plaintext):
        """        Args:            number (int): 1 or 2 (key number)            plaintext (bytes): 16 bytes data        Returns:            tuple: (ciphertext, tag)"""ifhasattr(self, 'sock'):
            # リモート接続時
            self.recv_until(b'>')
            self.sock_file.write(b'1\n')

            self.recv_until(b'>')
            self.sock_file.write(f"{number}\n".encode())

            self.recv_until(b'>')
            self.sock_file.write(plaintext.hex().encode() + b'\n')

            # monster nameの行をスキップwhile b'tag(hex)'notin (line := self.sock_file.readline()):
                continue# tag値を取得
            tag_hex = line.decode().split('=')[1].strip()
            tag = bytes.fromhex(tag_hex)

            returnNone, tag
        else:
            # ローカルテスト時
            aes = AES.new(key=self.keys[number - 1], mode=AES.MODE_GCM, nonce=self.nonce)
            x, y = aes.encrypt_and_digest(plaintext)
            print(x, y)
            return x, y

    defdual_summon(self, plaintext):
        """        Args:            plaintext (bytes): 16 bytes data that should produce same tag with both keys"""ifhasattr(self, 'sock'):
            self.recv_until(b'>')
            self.sock_file.write(b'2\n')

            self.recv_until(b'>')
            self.sock_file.write(plaintext.hex().encode() + b'\n')

            # フラグを含む応答を読む
            response = self.sock_file.readline().decode()
            if"master of summoner"in response:
                flag = self.sock_file.readline().decode().strip()
                print("Flag:", flag)
                return flag
        else:
            aes1 = AES.new(key=self.keys[0], mode=AES.MODE_GCM, nonce=self.nonce)
            aes2 = AES.new(key=self.keys[1], mode=AES.MODE_GCM, nonce=self.nonce)
            ct1, tag1 = aes1.encrypt_and_digest(plaintext)
            ct2, tag2 = aes2.encrypt_and_digest(plaintext)
            assert tag1 == tag2
            print("Congrats")

    defclose(self):
        ifhasattr(self, 'sock'):
            self.sock.close()


defexploit(api):
    """    GCMのtagが衝突する入力を総当たりで探す"""try:
        # ランダムな16バイトの入力を生成して試行
        start_time = time.time()
        attempts = 0for _ inrange(2 ** 16):  # 適当な試行回数
            attempts += 1if attempts % 100 == 0:
                elapsed = time.time() - start_time
                print(f"Attempts: {attempts}, Time elapsed: {elapsed:.2f}s")

            test_input = secrets.token_bytes(16)
            _, tag1 = api.summon(1, test_input)
            _, tag2 = api.summon(2, test_input)

            if tag1 == tag2:
                print(f"Found collision after {attempts} attempts!")
                print(f"Input (hex): {test_input.hex()}")
                print(f"Tag (hex): {tag1.hex()}")
                return api.dual_summon(test_input)

    exceptExceptionas e:
        print(f"Error occurred: {e}")
    finally:
        api.close()

    returnFalsedefexploit(api):
    """    GCMのtagが衝突する入力を総当たりで探す"""# ランダムな16バイトの入力を生成して試行for _ inrange(2 ** 16):  # 適当な試行回数
        test_input = secrets.token_bytes(16)
        _, tag1 = api.summon(1, test_input)
        _, tag2 = api.summon(2, test_input)

        if tag1 == tag2:
            print(f"Found collision! Input: {test_input.hex()}")
            api.dual_summon(test_input)
            returnTruereturnFalse# SageMath# Define GF(2^128) with the GCM reduction polynomialfrom Crypto.Util.strxor import strxor

X = GF(2).polynomial_ring().gen()
poly = X ** 128 + X ** 7 + X ** 2 + X ** 1 + 1
F.<a> = GF(2 ** 128, name='a', modulus=poly)
R.<H, E, Q, E_2, H_2, W_2, Q_2, M> = PolynomialRing(F)


defto_F(n):
    """Convert integer to field element"""return F([int(b) for b informat(n, '0128b')])


deffrom_F(fe):
    """Convert field element to integer"""returnint(''.join(['1'if b else'0'for b in (list(fe.polynomial()) + [0] * 128)[:128]]), 2)


defsolve_for_x(tag1_0, tag2_0, tag1_1, tag2_1):
    """    Solve for x given tags for PLAINTEXT=0 and PLAINTEXT=1    Arguments should be integers representing the 128-bit values"""# Convert all inputs to field elements
    tag1_0 = to_F(tag1_0)
    tag2_0 = to_F(tag2_0)
    tag1_1 = to_F(tag1_1)
    tag2_1 = to_F(tag2_1)

    # Using the fact that PLAINTEXT is either 0 or 1, we can try linear combinations# plaintext_x = α•0 + β•1 where α,β ∈ GF(2)# Due to AES-GCM's linearity in GF(2^128), if plaintext_x is a linear combination,# the corresponding tag will be the same linear combination of the tagsfor alpha_int inrange(2):
        for beta_int inrange(2):
            alpha = F(alpha_int)
            beta = F(beta_int)

            # Calculate expected tags for this combination
            tag1_x = alpha * tag1_0 + beta * tag1_1
            tag2_x = alpha * tag2_0 + beta * tag2_1
            if tag1_x == tag2_x:
                print(tag1_x)
                # Found a collision!# The plaintext will be the same linear combination of 0 and 1
                plaintext = beta_int  # since alpha•0 + beta•1 = beta•1 = betareturn plaintext, from_F(tag1_x)

    returnNone, None# Example usage:"""# Convert your tag values to integers firsttag1_0 = bytes_to_int(tag1_0_bytes)tag2_0 = bytes_to_int(tag2_0_bytes)tag1_1 = bytes_to_int(tag1_1_bytes)tag2_1 = bytes_to_int(tag2_1_bytes)plaintext, tag = solve_for_x(tag1_0, tag2_0, tag1_1, tag2_1)if plaintext is not None:    print(f"Found solution: plaintext = {plaintext}")    print(f"Tag = {hex(tag)}")"""from Crypto.Cipher import AES
import secrets
import signal

from Crypto.Util.number import bytes_to_long, long_to_bytes

signal.alarm(300)

api = SummoningAPI(host="dual-summon.seccon.games", port=2222)

_, tag0_2 = api.summon(2, long_to_bytes(0, 16))
_, tag1_2 = api.summon(2, long_to_bytes(1, 16))
_, tag0 = api.summon(1, long_to_bytes(0, 16))
_, tag1 = api.summon(1, long_to_bytes(1, 16))
W = to_F(bytes_to_long(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80'))
T_0 = to_F(bytes_to_long(tag0))
T_1 = to_F(bytes_to_long(tag1))
T_0_2 = to_F(bytes_to_long(tag0_2))
T_1_2 = to_F(bytes_to_long(tag1_2))
M_0 = to_F(0)
M_1 = to_F(1)
M_2 = to_F(2)

defsolve_H(f):
    # GF(2^128)上の多項式環を設定
    R.<a> = PolynomialRing(GF(2))
    # AES-GCMで使用される既約多項式
    modulus = a ^ 128 + a ^ 7 + a ^ 2 + a + 1# 有限体を構築
    F.<a> = GF(2 ^ 128, modulus=modulus)

    # 2次方程式の係数を設定# H^2の係数
    coeff_h2 = a ^ 127# Hの係数(この場合は0)
    coeff_h = 0# 定数項
    constant = f.constant_coefficient()

    # 2次方程式を解く# GF(2^n)では、x^2 + ax + b = 0 の解は# x = √(b/a^2) + √(b/a^2) * √(a^2/a) if a ≠ 0# この場合、H^2の係数が非ゼロで、Hの係数が0なので、# H = √(constant/coeff_h2)# 定数項を係数で割る
    ratio = constant / coeff_h2

    # 平方根を計算# GF(2^n)では、平方根は Frobenius 自己準同型を使用defsqrt_gf2n(x):
        # GF(2^n)では、平方根は x^(2^(n-1))
        n = F.degree()
        return x ^ (2 ^ (n - 1))

    solution = sqrt_gf2n(ratio)

    # GF(2)では、√x の解は ±√x だが、特性2の体では + と - が同じなので、解は1つ# 検証
    verification = solution ^ 2 * coeff_h2 + constant
    assert verification == 0return solution
F_0 = (M_0 + E) * H ^ 2 + W * H + Q - T_0
F_1 = (M_1 + E) * H ^ 2 + W * H + Q - T_1
F_0_2 = (M_0 + E) * H_2 ^ 2 + W * H_2 + Q_2 - T_0_2
F_1_2 = (M_1 + E) * H_2 ^ 2 + W * H_2 + Q_2 - T_1_2
H1 = solve_H(F_0 + F_1)
H2 = solve_H(F_0_2 + F_1_2)

COND1 = (M + E) * H ^ 2 + W * H + Q - ((M + E_2) * H_2 ^ 2 + W * H_2 + Q_2)
COND2 = (E) * H ^ 2 + W * H + Q - T_0 + ((E_2) * H_2 ^ 2 + W * H_2 + Q_2 - T_0_2)
M_formula = ((COND1 - COND2).subs({H: H1, H_2: H2}))

coef_M = M_formula.coefficient(M)  # Mの係数を取得
const_term = M_formula.constant_coefficient()  # 定数項を取得print(const_term)
M_sol = (const_term / coef_M).constant_coefficient()
M_bytes = long_to_bytes(from_F(M_sol), 16)
print(api.dual_summon(M_bytes))

JavaScrypto

ホントぽにょごめん (参考:

SECCON CTF 13 Quals writeup

) 。まさかこれが決め手で落ちるとは


Viewing all articles
Browse latest Browse all 8051