TsukuCTF 2025
TsukuCTFにbunkyowestenrsで出ていました。solvesチャンネルにURL爆撃失敗して悲しい人です。はい。
全体的に面白かったです。あと、opensslのコードはこりごりです()。
a8tsukuctf [241 Solves]
chall
1 | import string |
solve
pt[i]の鍵がenc[i-8]に依存しているので、先頭から8文字目以降が求まる。これで、"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"
なので、言うとおりにして、flagはTsukuCTF25{tsukuctf_is_fun}
1 | import string |
xortsukushift [34 solve]
chall
1 | import os |
solve
結構ぬまった()。「線形性なくないか???」といいつつ行列作って色々していた。z3も無理なのでどうしようか迷った挙句、order調べたら280だった。そんなことある????なので、適当に再利用してGG
TsukuCTF25{4_xor5h1ft_15_only_45_good_45_1t5_5h1ft_p4r4m3t3r5}
1 | from ptrlib import * |
PQC0 [149 Solves]
chall
1 | # REQUIRED: OpenSSL 3.5.0 |
solve
そういやopensslってML-KEM実装されてたなと思いつつopensslをビルドし、以下をたたいて復号
1 | openssl pkeyutl -decap -inkey priv-ml-kem-768.pem -in ciphertext.dat -secret recovered_shared.dat |
TsukuCTF25{W3lc0me_t0_PQC_w0r1d!!!}
PQC1 [21 solve]
chall
1 | # REQUIRED: OpenSSL 3.5.0 |
solve
priv keyの先頭128 bytesもらえるらしい。
デバッグ用の鍵を用意して以下のコマンド使って、与えられた鍵に含まれている内容を確認する。
構成的には、seed, dk, ek
なのでseed
の途中(所謂
1 | openssl pkey -in test/priv-ml-kem-768.pem -text |
で、ML-KEMって、
で、ここからが問題で、どのようにopensslのseedを固定するのかという。
色々方法あると思いますが、私はopensslのコード読んで書き換えました。
ml_kem_gen
関数のseedを上書きして、鍵を作り、PQC1と同様にコマンド打つことで解きました。
TsukuCTF25{seed_5eed_s33d}
追記)見返すとコマンドというよりc書いてました…すみません。
変更先は/providers/implementations/keymgmt/ml_kem_kmgmt.c
のml_kem_gen
で、cで出力してpythonで復号していました。
1 | static void *ml_kem_gen(void *vgctx, OSSL_CALLBACK *osslcb, void *cbarg) |
1 |
|
1 | from Crypto.Cipher import AES |
PQC2 [0 solve]
終わって1時間とか1時間半で解けました。もうopensslのコード読みとーない。
chall
1 | # REQUIRED: OpenSSL 3.5.0 |
solve
これは、privの後半が与えられた状態から復号しろということらしい。
結果から言うと、$ek$は全て既知、$dk$は先頭約100bytes未知
です。(ptr yudaiさん助かりました。)
1 | import hashlib |
で、opensslを触りたくないなぁとなっていた時に、isogeny つよつよCTF playerがkyberのpython実装やっていたことを思い出し、見てみると、
This repository contains a pure python implementation of both:
- ML-KEM: The NIST Module-Lattice-Based Key-Encapsulation Mechanism Standard following FIPS 203 from the NIST post-quantum cryptography project.
- CRYSTALS-Kyber: following (at the time of writing) the most recent specification (v3.02)
Note: This project accompanies
dilithium-py
which is a pure-python implementation of ML-DSA and CRYSTALS-Dilithium and shares a lot of the lower-level code of this implementation.This implementation currently passes all KAT tests for
kyber
andml_kem
え、いつの間にML実装してたの??しかもKAT通っているのでマジで使えるやつ。
早速パースして行きます。
1 | from kyber_py.ml_kem import ML_KEM_768 |
で、これら多項式やベクトルはNTTで書かれているのでこれを通常の多項式に戻します。
(NTTは説明すると長いのでここ見てくださいThe Number Theoretic Transform in Kyber and Dilithium)
1 | A = A_hat.from_ntt() |
ここに、keygenで使われている誤差項のDtribution D𝜂(𝑅𝑞)
からサンプルされた値で、大体[-2,2]の値を取ります。
結果として幾つかの関係式が手に入ったので、sのわからない部分をZZ[]
で変数化していきます。
1 | PR.<s00_0, s00_1, s00_2, s00_3, s00_4, s00_5, s00_6, s00_7, s00_8, s00_9, s00_10, s00_11, s00_12, s00_13, s00_14, s00_15, s00_16, s00_17, s00_18, s00_19, s00_20, s00_21, s00_22, s00_23, s00_24, s00_25, s00_26, s00_27, s00_28, s00_29, s00_30, s00_31, s00_32, s00_33, s00_34, s00_35, s00_36, s00_37, s00_38, s00_39, s00_40, s00_41, s00_42, s00_43, s00_44, s00_45, s00_46, s00_47, s00_48, s00_49, s00_50, s00_51, s00_52, s00_53, s00_54, s00_55, s00_56, s00_57, s00_58, s00_59, s00_60, s00_61, s00_62, s00_63, s00_64, s00_65, s00_66> = ZZ[] |
これで、
ここで、
1 | row = 150 |
最後に正しい鍵を構成して、decap関数に与えてやればflagが出ます。
TsukuCTF25{PQC_i5_fun_bu7_it_i5_4lso_difficu1t}
1 | s_hat = ML_KEM_768.M.decode_vector(dk[0: 384 * 3], ML_KEM_768.k, 12, is_ntt=True) |
最終的なコード
1 | from base64 import b64decode |