この大会は2025/5/3 12:00(JST)~2025/5/4 12:00(JST)に開催されました。
今回もチームで参戦。結果は1200点で882チーム中84位でした。
自分で解けた問題をWriteupとして書いておきます。
len_len (web)
POSTでarrayにデータを渡せる。渡すデータはスペースを削除して、長さが10より短いとエラーになる。また、JSONとしてパースされた値をarrayとしたときにarray.lengthがマイナスになるときにフラグが表示される。
JSONで"length"の値をマイナスで指定すればよい。
$ curl http://challs.tsukuctf.org:28888 -d 'array={"length":-1}' TsukuCTF25{l4n_l1n_lun_l4n_l0n}
TsukuCTF25{l4n_l1n_lun_l4n_l0n}
flash (web)
各パスのサーバの処理概要は以下の通り。
・SEED: './static/seed.txt'を読み込みhexデコードしたもの ■"/" ・session.clear() ・session['session_id']: ランダム16バイトの16進数文字列 ・session['round'] = 0 ■/flash ・session_id = session['session_id'] ・r = session['round'] ・rが10以上の場合、/resultにリダイレクト ・digits = generate_round_digits(SEED, session_id, r) ・LCG_M, LCG_A, LCG_C = lcg_params(SEED, session_id) ※LCG_Mは固定値2147483693 ・h0 = hmac.new(SEED, session_id.encode(), hashlib.sha256).digest() ・state = int.from_bytes(h0, 'big') % LCG_M ・以下7*r回繰り返し ・state = (LCG_A * state + LCG_C) % LCG_M ・digits = [] ・以下7回繰り返し ・state = (LCG_A * state + LCG_C) % LCG_M ・digitsにstate % 10を追加 ・digitsを返却 ・session['round'] = r + 1 ・visible: session['round']が3以下または8以上の場合True、それ以外はFalse ・visibleがTrueの場合にdigitsの各値を表示 ■/result ※詳細は省略 ・/flashで見えていない数値も含め各ラウンドの値の合計値を正しく答えればフラグが表示される。
LCGの問題のようにも見えたが、これを解くのは至難の業。
スタートを押し、数字が表示されていき、最後の合計値を入力する画面で、クッキーにはsessionが設定される。
.eJwNx0kOgzAMAMC_-MzBZMPmMyjxIqFWiUTgVPXv7dzmA5fN530f93hZhx08ErmHKDFuCYmNozRpajkzZw-wwDWerrCvuMC0Oc_Rj_N_0FC5hNWaaqlWJW1JiQo2QSTPAt8faNshUw.aBaVhw.Cr1ElX2GhHlm7J88uSwoz4yf5MQ
合計値を入力する画面で、適当に数字を答えると、間違いになるが正解の値が表示される。クッキーが削除されるが、改めてスタートの画面で、このクッキーの値をsessionに設定し、スタートを押す。合計値を入力する画面に遷移するので、先ほどの正解の値を入力すると、フラグが表示された。
TsukuCTF25{Tr4d1on4l_P4th_Trav3rs4l}
a8tsukuctf (crypto)
暗号化処理の概要は以下の通り。
・plaintext: 未知文字列 ・key: 未知文字列 ・plaintext[30:38]は"tsukuctf"であることをチェック ・ciphertext = encrypt(plaintext=plaintext, key=key) ・keyの長さはplaintextの長さ以下であることをチェック ・idx = 0 ・ciphertext = [] ・cipher_without_symbols = [] ・plaintextの各文字cに対して以下を実行 ・cが英小文字の場合 ・idxがkeyの長さより小さい場合 ・k = key[idx] ・idxがkeyの長さ以上の場合 ・k = cipher_without_symbols[idx-len(key)] ・cipher_without_symbolsにf(c, k)を追加 ・ciphertextにf(c, k)を追加 ・idxを1プラス ・ciphertextにcを追加 ・ciphertextの各文字を結合して返却 ・ciphertextをoutput.txtに書き込み
暗号文の以下の部分に注目する。
jaaaaaa aa tsukuctf,
"a"が8文字続いた後の"tsukuctf"の部分は平文と変わらない。スペースを除くと、"j"が16文字目になっていることからkeyは8バイトであると推測できる。
keyは8バイトごとのブロックで、次のブロックの暗号に使用される。つまり、スペースを除く先頭8バイトは正確に復号することはできないが、他はその前提で復号することができる。
#!/usr/bin/env python3 import string def rev_f(c, k): c = ord(c) - ord('a') k = ord(k) - ord('a') ret = (c - k) % 26 return chr(ord('a') + ret) with open('output.txt', 'r') as f: ciphertext = eval(f.read().split('=')[1]) idx = 0 plaintext = '' cipher_without_symbols = [] for c in ciphertext: if c in string.ascii_lowercase: cipher_without_symbols.append(c) if idx < 8: plaintext += '*' else: plaintext += rev_f(c, cipher_without_symbols[idx - 8]) idx += 1 else: plaintext += c print(plaintext)
復号結果は以下の通り。
*** *** **joy this problem or tsukuctf, or both? the flag is concatenate the seventh word in the first sentence, the third word in the second sentence, and 'fun' with underscores.
フラグは、1番目の文の7番目の単語の"tsukuctf"、2番目の文の3番目の単語の"is"、あと"fun"をアンダースコアで結合したもので、以下の通りとなる。
tsukuctf_is_fun
TsukuCTF25{tsukuctf_is_fun}
PQC0 (crypto)
秘密鍵とカプセル化された鍵(ciphertext.dat)からshared.datを復元できる。あとはそれを使ってフラグを復号する。
#!/usr/bin/env python3 import os from Crypto.Cipher import AES from Crypto.Util.Padding import unpad with open("output.txt", "r") as f: lines = f.read().splitlines() priv = "\n".join(lines[1:57]) with open("priv-ml-kem-768.pem", "w") as f: f.write(priv) ciphertext = bytes.fromhex(lines[58]) with open("ciphertext.dat", "wb") as f: f.write(ciphertext) os.system("openssl pkeyutl -decap -inkey priv-ml-kem-768.pem -in ciphertext.dat -secret shared.dat") with open("shared.dat", "rb") as f: shared_secret = f.read() encrypted_flag = bytes.fromhex(lines[60]) cipher = AES.new(shared_secret, AES.MODE_ECB) flag = unpad(cipher.decrypt(encrypted_flag), 16).decode() print(flag)
TsukuCTF25{W3lc0me_t0_PQC_w0r1d!!!}