Tsuku CTF 2025 writeup

  • ~24.25K 字
  1. 1. TsukuCTF 2025
  2. 2. a8tsukuctf [241 Solves]
    1. 2.0.1. chall
    2. 2.0.2. solve
  3. 2.1. xortsukushift [34 solve]
    1. 2.1.1. chall
    2. 2.1.2. solve
  4. 2.2. PQC0 [149 Solves]
    1. 2.2.1. chall
    2. 2.2.2. solve
  5. 2.3. PQC1 [21 solve]
    1. 2.3.1. chall
    2. 2.3.2. solve
  6. 2.4. PQC2 [0 solve]
    1. 2.4.1. chall
    2. 2.4.2. solve
    3. 2.4.3. 最終的なコード

TsukuCTF 2025

TsukuCTFにbunkyowestenrsで出ていました。solvesチャンネルにURL爆撃失敗して悲しい人です。はい。

全体的に面白かったです。あと、opensslのコードはこりごりです()。

a8tsukuctf [241 Solves]

chall

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import string

plaintext = '[REDACTED]'
key = '[REDACTED]'

# <plaintext> <ciphertext>
# ...?? tsukuctf, ??... -> ...aa tsukuctf, hj...
assert plaintext[30:38] == 'tsukuctf'


# https://ja.wikipedia.org/wiki/%E3%83%B4%E3%82%A3%E3%82%B8%E3%83%A5%E3%83%8D%E3%83%AB%E6%9A%97%E5%8F%B7#%E6%95%B0%E5%BC%8F%E3%81%A7%E3%81%BF%E3%82%8B%E6%9A%97%E5%8F%B7%E5%8C%96%E3%81%A8%E5%BE%A9%E5%8F%B7
def f(p, k):
p = ord(p) - ord('a')
k = ord(k) - ord('a')
ret = (p + k) % 26
return chr(ord('a') + ret)


def encrypt(plaintext, key):
assert len(key) <= len(plaintext)

idx = 0
ciphertext = []
cipher_without_symbols = []

for c in plaintext:
if c in string.ascii_lowercase:
if idx < len(key):
k = key[idx]
else:
k = cipher_without_symbols[idx-len(key)]
cipher_without_symbols.append(f(c, k))
ciphertext.append(f(c, k))
idx += 1
else:
ciphertext.append(c)

ciphertext = ''.join(c for c in ciphertext)

return ciphertext


ciphertext = encrypt(plaintext=plaintext, key=key)

with open('output.txt', 'w') as f:
f.write(f'{ciphertext=}\n')

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import string
ciphertext = "ayb wpg uujmz pwom jaaaaaa aa tsukuctf, hj vynj? mml ogyt re ozbiymvrosf bfq nvjwsum mbmm ef ntq gudwy fxdzyqyc, yeh sfypf usyv nl imy kcxbyl ecxvboap, epa 'avb' wxxw unyfnpzklrq."
ciphertext2 = "aybwpguujmzpwomjaaaaaaaatsukuctfhjvynjmmlogytreozbiymvrosfbfqnvjwsummbmmefntqgudwyfxdzyqycyehsfypfusyvnlimykcxbylecxvboapepaavbwxxwunyfnpzklrq"


def f_inv(c, k):
c = ord(c) - ord('a')
k = ord(k) - ord('a')
ret = (c - k) % 26
return chr(ord('a') + ret)


kid = 0
pt = ""
for pid in range(8 + 2, len(ciphertext)):
if ciphertext[pid] in string.ascii_lowercase:
p = f_inv(ciphertext[pid], ciphertext2[kid])
pt += p
kid += 1
else:
pt += ciphertext[pid]
pid += 1

print(pt)
# pt = "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"
# tsukuctf_is_fun

xortsukushift [34 solve]

chall

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import os
import secrets
import signal

FLAG = os.getenv("FLAG", "TsukuCTF25{dummy_flag}")

class xor_tsuku_shift:
def __init__(self, seed):
self.a = seed

def shift(self):
self.a ^= (self.a << 17) & 0xFFFFFFFFFFFFFFFF
self.a ^= (self.a >> 9) & 0xFFFFFFFFFFFFFFFF
self.a ^= (self.a << 18) & 0xFFFFFFFFFFFFFFFF
return self.a & 0xFFFFFFFFFFFFFFFF

def janken(a, b):
return (a-b+3) % 3

rng = xor_tsuku_shift(seed=secrets.randbits(64))

signal.alarm(600)

print("Tsukushi: Let's play janken!")
print("Tsukushi: Win 294 times in a row and you'll get the flag.")

for challenge in range(300):
print(f"Tsukushi: You have {300-challenge:03} tries.")
for round in range(294):
print(f"--- Round {round:03} ---")
tsukushi = rng.shift()
you = int(input("Rock, Paper, Scissors... Go! (Rock: 0, Paper: 1, Scissors: 2): "))

if you != 0 and you != 1 and you != 2:
print("Tsukushi: Hey, you cheated!")
break

result = janken(you, tsukushi)

if result == 1:
print("Tsukushi: You win!")
if round != 293:
print("Tsukushi: Let's go to the next round!")
elif result == 0:
print("Tsukushi: Draw! ...But If you wanna get the flag, you have to win 294 rounds in a row.")
break
else:
print("Tsukushi: You lose!")
break

else:
print("Tsukushi: You won 294 times in a row?! That's incredible!")
print(f"Tsukushi: So, here is the flag. {FLAG}")
quit()

else:
print("Tsukushi: GGEZ, Bye!")

solve

結構ぬまった()。「線形性なくないか???」といいつつ行列作って色々していた。z3も無理なのでどうしようか迷った挙句、order調べたら280だった。そんなことある????なので、適当に再利用してGG

TsukuCTF25{4_xor5h1ft_15_only_45_good_45_1t5_5h1ft_p4r4m3t3r5}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from ptrlib import *

# io = process(["python3", "server.py"])
io = remote("nc challs.tsukuctf.org 30057")

def oracle(i):
io.sendlineafter(b"): ", int(1))
ret = io.recvline().decode()
print(i, ret)

if "lose" in ret:
return 2
if "Draw" in ret:
return 1
if "win" in ret:
return 0


def oracle_send(other):
io.sendlineafter(b"): ", int((other + 1) % 3))
ret = io.recvline().decode()
print(ret)
assert "win" in ret


io.debug = True


ret = [oracle(index) for index in range(280)]
ret = [oracle_send(ret[index%280]) for index in range(294)]
io.sh()

PQC0 [149 Solves]

chall

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# REQUIRED: OpenSSL 3.5.0

import os
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
# from flag import flag

# generate private key
os.system("openssl genpkey -algorithm ML-KEM-768 -out priv-ml-kem-768.pem")
# generate public key
os.system("openssl pkey -in priv-ml-kem-768.pem -pubout -out pub-ml-kem-768.pem")
# generate shared secret
os.system("openssl pkeyutl -encap -inkey pub-ml-kem-768.pem -secret shared.dat -out ciphertext.dat")

with open("priv-ml-kem-768.pem_2", "rb") as f:
private_key = f.read()

print("==== private_key ====")
print(private_key.decode())

with open("ciphertext.dat_2", "rb") as f:
ciphertext = f.read()

print("==== ciphertext(hex) ====")
print(ciphertext.hex())

with open("shared.dat_2", "rb") as f:
shared_secret = f.read()

encrypted_flag = AES.new(shared_secret, AES.MODE_ECB).encrypt(pad(flag, 16))

print("==== encrypted_flag(hex) ====")
print(encrypted_flag.hex())


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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# REQUIRED: OpenSSL 3.5.0

import os
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from flag import flag

# generate private key
os.system("openssl genpkey -algorithm ML-KEM-768 -out priv-ml-kem-768.pem")
# generate public key
os.system("openssl pkey -in priv-ml-kem-768.pem -pubout -out pub-ml-kem-768.pem")
# generate shared secret
os.system("openssl pkeyutl -encap -inkey pub-ml-kem-768.pem -secret shared.dat -out ciphertext.dat")

with open("priv-ml-kem-768.pem", "rb") as f:
private_key = f.read()

print("==== private_key[:128] ====")
print(private_key[:128].decode())

with open("ciphertext.dat", "rb") as f:
ciphertext = f.read()

print("==== ciphertext(hex) ====")
print(ciphertext.hex())

with open("shared.dat", "rb") as f:
shared_secret = f.read()

encrypted_flag = AES.new(shared_secret, AES.MODE_ECB).encrypt(pad(flag, 16))

print("==== encrypted_flag(hex) ====")
print(encrypted_flag.hex())

solve

priv keyの先頭128 bytesもらえるらしい。

デバッグ用の鍵を用意して以下のコマンド使って、与えられた鍵に含まれている内容を確認する。

構成的には、seed, dk, ekなのでseedの途中(所謂 )までもらえてそう。

1
openssl pkey -in test/priv-ml-kem-768.pem -text

で、ML-KEMって、しか鍵に使わないので、言っちゃ悪いがすべてもらえているものと同じ。

image-20250504154239166

で、ここからが問題で、どのようにopensslのseedを固定するのかという。

色々方法あると思いますが、私はopensslのコード読んで書き換えました。

ml_kem_gen関数のseedを上書きして、鍵を作り、PQC1と同様にコマンド打つことで解きました。

TsukuCTF25{seed_5eed_s33d}

追記)見返すとコマンドというよりc書いてました…すみません。

変更先は/providers/implementations/keymgmt/ml_kem_kmgmt.cml_kem_genで、cで出力してpythonで復号していました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
static void *ml_kem_gen(void *vgctx, OSSL_CALLBACK *osslcb, void *cbarg)
{
PROV_ML_KEM_GEN_CTX *gctx = vgctx;
ML_KEM_KEY *key;
uint8_t *nopub = NULL;
uint8_t *seed;
int genok = 0;
printf("called ml_kem_gen !!!!!!^^^^^^^^^^^^^^^^^^^^\n" );

if (gctx == NULL
|| (gctx->selection & OSSL_KEYMGMT_SELECT_KEYPAIR) ==
OSSL_KEYMGMT_SELECT_PUBLIC_KEY)
return NULL;


uint8_t new_seed[64] = {
0x69, 0xad, 0x87, 0x4f, 0x24, 0x26, 0xda, 0xf9,
0xa6, 0x0e, 0x6c, 0x39, 0x7a, 0x17, 0x8c, 0x26,
0x9e, 0xa0, 0x26, 0x6d, 0xec, 0x72, 0xb2, 0x25,
0xa0, 0xd6, 0x59, 0x17, 0x67, 0x8a, 0x9d, 0xc4,
0x19, 0x7c, 0xfc, 0xa9, 0xa4, 0xd3, 0x4d, 0x3b,
0x00, 0x15, 0xdd, 0x66, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};

// コピーする
// memcpy(seed, new_seed, 64);
seed = new_seed;
printf("passed modifyed ml_kem_gen !!!!!!^^^^^^^^^^^^^^^^^^^^\n" );

key = ossl_prov_ml_kem_new(gctx->provctx, gctx->propq, gctx->evp_type);
if (key == NULL)
return NULL;

if ((gctx->selection & OSSL_KEYMGMT_SELECT_KEYPAIR) == 0)
return key;

if (seed != NULL && !ossl_ml_kem_set_seed(seed, ML_KEM_SEED_BYTES, key))
return NULL;
genok = ossl_ml_kem_genkey(nopub, 0, key);

/* Erase the single-use seed */
if (seed != NULL)
OPENSSL_cleanse(seed, ML_KEM_SEED_BYTES);
gctx->seed = NULL;

if (genok) {
#ifdef FIPS_MODULE
if (!ml_kem_pairwise_test(key, ML_KEM_KEY_FIXED_PCT)) {
ossl_set_error_state(OSSL_SELF_TEST_TYPE_PCT);
ossl_ml_kem_key_free(key);
return NULL;
}
#endif /* FIPS_MODULE */
return key;
}

ossl_ml_kem_key_free(key);
return NULL;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
#include <openssl/evp.h>
#include <openssl/err.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>


void handleErrors(void) {
ERR_print_errors_fp(stderr);
abort();
}

void print_hex(const char *label, const unsigned char *data, size_t len) {
printf("%s (%zu bytes):\n", label, len);
for (size_t i = 0; i < len; i++) {
printf("%02X", data[i]);
if ((i + 1) % 32 == 0) printf("\n");
}
if (len % 32 != 0) printf("\n");
}



int main() {
EVP_PKEY_CTX *keygen_ctx = NULL;
EVP_PKEY_CTX *enc_ctx = NULL;
EVP_PKEY_CTX *dec_ctx = NULL;
EVP_PKEY *keypair = NULL;

unsigned char *ct = NULL;
unsigned char _ct[] = {---OMITTED---};
unsigned char *ss_enc = NULL;
unsigned char *ss_dec = NULL;
size_t ctlen = 0, ss_enc_len = 0, ss_dec_len = 0;

// 1. Generate keypair for ML-KEM-768
keygen_ctx = EVP_PKEY_CTX_new_from_name(NULL, "ML-KEM-768", NULL);
if (!keygen_ctx)
handleErrors();

if (EVP_PKEY_keygen_init(keygen_ctx) <= 0)
handleErrors();

if (EVP_PKEY_keygen(keygen_ctx, &keypair) <= 0)
handleErrors();

BIO *bio_out = BIO_new_fp(stdout, BIO_NOCLOSE);
EVP_PKEY_print_private(bio_out, keypair, 0, NULL);
BIO_free(bio_out);

// 2. Encapsulation
enc_ctx = EVP_PKEY_CTX_new(keypair, NULL);
if (!enc_ctx)
handleErrors();

if (EVP_PKEY_encapsulate_init(enc_ctx, NULL) <= 0)
handleErrors();

// First, determine sizes
if (EVP_PKEY_encapsulate(enc_ctx, NULL, &ctlen, NULL, &ss_enc_len) <= 0)
handleErrors();

ct = malloc(ctlen);
ss_enc = malloc(ss_enc_len);
if (!ct || !ss_enc)
handleErrors();

if (EVP_PKEY_encapsulate(enc_ctx, ct, &ctlen, ss_enc, &ss_enc_len) <= 0)
handleErrors();

printf("✅ Encapsulation complete.\nShared Secret (Enc): ");
for (size_t i = 0; i < ss_enc_len; i++)
printf("%02X", ss_enc[i]);
printf("\n");
printf("✅ Decapsulation start.\n");



size_t _ctlen = sizeof(_ct);
if (ct != NULL)
memcpy(ct, _ct, _ctlen);






// 3. Decapsulation
dec_ctx = EVP_PKEY_CTX_new(keypair, NULL);
if (!dec_ctx)
handleErrors();

// print_mlkem_raw_keys(keypair);

if (EVP_PKEY_decapsulate_init(dec_ctx, NULL) <= 0)
handleErrors();

// First, get decapsulated shared secret length
if (EVP_PKEY_decapsulate(dec_ctx, NULL, &ss_dec_len, ct, ctlen) <= 0)
handleErrors();

ss_dec = malloc(ss_dec_len);
if (!ss_dec)
handleErrors();

if (EVP_PKEY_decapsulate(dec_ctx, ss_dec, &ss_dec_len, ct, ctlen) <= 0)
handleErrors();

printf("✅ Decapsulation complete.\nShared Secret (Dec): ");
for (size_t i = 0; i < ss_dec_len; i++)
printf("%02X", ss_dec[i]);
printf("\n");

// Cleanup
EVP_PKEY_free(keypair);
EVP_PKEY_CTX_free(keygen_ctx);
EVP_PKEY_CTX_free(enc_ctx);
EVP_PKEY_CTX_free(dec_ctx);
free(ct);
free(ss_enc);
free(ss_dec);

return 0;
}
1
2
3
4
5
6
7
8
9
from Crypto.Cipher import AES

ss = bytes.fromhex("F57800AA4C7525B9215A83AE36392ECD4479540EEDD16ED5E3CE7B1947D8B75F")
enc = bytes.fromhex("fd302c76946654e6e469a4656b90a8d60fb3492ed8c2238350e8e833a35b3587")

flag = AES.new(ss, AES.MODE_ECB).decrypt(enc)

print(flag)
# b'TsukuCTF25{seed_5eed_s33d}\x06\x06\x06\x06\x06\x06'

PQC2 [0 solve]

終わって1時間とか1時間半で解けました。もうopensslのコード読みとーない

chall

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# REQUIRED: OpenSSL 3.5.0

import os
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from flag import flag

# generate private key
os.system("openssl genpkey -algorithm ML-KEM-768 -out priv-ml-kem-768.pem")
# generate public key
os.system("openssl pkey -in priv-ml-kem-768.pem -pubout -out pub-ml-kem-768.pem")
# generate shared secret
os.system("openssl pkeyutl -encap -inkey pub-ml-kem-768.pem -secret shared.dat -out ciphertext.dat")

with open("priv-ml-kem-768.pem", "rb") as f:
private_key = f.read()

print("==== private_key[294:] ====")
print(private_key[294:].decode())

with open("ciphertext.dat", "rb") as f:
ciphertext = f.read()

print("==== ciphertext(hex) ====")
print(ciphertext.hex())

with open("shared.dat", "rb") as f:
shared_secret = f.read()

encrypted_flag = AES.new(shared_secret, AES.MODE_ECB).encrypt(pad(flag, 16))

print("==== encrypted_flag(hex) ====")
print(encrypted_flag.hex())

solve

これは、privの後半が与えられた状態から復号しろということらしい。

結果から言うと、$ek$は全て既知、$dk$は先頭約100bytes未知です。(ptr yudaiさん助かりました。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import hashlib
from base64 import b64decode

def dump(name: str, data: bytes):
print(f"{name}:")
for i, c in enumerate(data):
if i % 15 == 0:
print(" ", end="")
print(f"{c:02x}:", end="")
if i % 15 == 14:
print()
print()
# with open("totyu.bin", "rb") as f:
with open("honban.bin", "rb") as f:
buf = f.read()

z = buf[-0x20:]
seed = b"?"*32 + z
dump("seed", seed)

dk = b"?" * 99 # 正確には99バイト目の下位4ビットはbuf[1] & 0xfで取れそう
dk += buf[2:] # dk = dk_pke || ek || keccak512(seed||rank) || keccak256(ek) || z

dump("dk", dk)

ek = buf[0x41f:0x8bf]
dump("ek", ek)

a = hashlib.sha3_256(ek).digest()
b = dk[-0x40:-0x20]
assert a == b

ek_pke = ek
dk_pke = dk[:dk.index(ek)]

で、opensslを触りたくないなぁとなっていた時に、isogeny つよつよCTF playerがkyberのpython実装やっていたことを思い出し、見てみると、

This repository contains a pure python implementation of both:

  1. ML-KEM: The NIST Module-Lattice-Based Key-Encapsulation Mechanism Standard following FIPS 203 from the NIST post-quantum cryptography project.
  2. 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 and ml_kem

え、いつの間にML実装してたの??しかもKAT通っているのでマジで使えるやつ。

早速パースして行きます。

1
2
3
4
5
6
7
8
9
from kyber_py.ml_kem import ML_KEM_768
s_hat = ML_KEM_768.M.decode_vector(dk[0: 384 * 3], ML_KEM_768.k, 12, is_ntt=True)
t_hat_bytes, rho = ek[:-32], ek[-32:]
t_hat = ML_KEM_768.M.decode_vector(t_hat_bytes, 3, 12, is_ntt=True)
A_hat = ML_KEM_768._generate_matrix_from_seed(rho, transpose=True)
ct = bytes.fromhex(---OMITTED---)
key = "key"
aesenc = bytes.fromhex(---OMITTED---)
A_hat = A_hat.T

で、これら多項式やベクトルはNTTで書かれているのでこれを通常の多項式に戻します。

(NTTは説明すると長いのでここ見てくださいThe Number Theoretic Transform in Kyber and Dilithium)

1
2
3
A = A_hat.from_ntt()
t = t_hat.from_ntt()
s = s_hat.from_ntt()

ここに、keygenで使われている誤差項のを追加すると以下の等式が成り立ちます。この時 Dtribution D𝜂(𝑅𝑞)からサンプルされた値で、大体[-2,2]の値を取ります。

結果として幾つかの関係式が手に入ったので、sのわからない部分をZZ[]で変数化していきます。

1
2
3
4
5
6
7
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[]
for i in range(67):
s_hat[0,0].coeffs[i] = PR.gens()[i]
# 67にしているのはご愛敬
# 再度sに代入
s = s_hat.from_ntt()
e = t - (A @ s)

これで、は係数行列として表現でき、具体的に行列で表現すると以下の形です。

ここで、は小さい値なことを思い出すと、LLLができそうです。ただ、幅がは大きすぎるので、実装ではにしています。

1
2
3
4
5
6
7
8
9
10
row = 150
es = Sequence([e[i,0].coeffs[k] for i in range(3) for k in range(len(e[i,0].coeffs))]).coefficient_matrix()
A = es[0].T[:,:row]
I = diagonal_matrix([1/(3329//2)]*67 + [3329*2])
pI = Matrix.identity(QQ, row)*3329
Zero = zero_matrix(row,68)
MAT = block_matrix([[pI,Zero],[A,I] ], subdivide=False )


_s_vec = [int(i%3329) for i in MAT.LLL()[-1][row:-1]*(3329//2)]

最後に正しい鍵を構成して、decap関数に与えてやればflagが出ます。

TsukuCTF25{PQC_i5_fun_bu7_it_i5_4lso_difficu1t}

1
2
3
4
5
6
7
8
9
10
s_hat = ML_KEM_768.M.decode_vector(dk[0: 384 * 3], ML_KEM_768.k, 12, is_ntt=True)

for i in range(66):
s_hat[0,0].coeffs[i] = int(_s_vec[i])

dk_pke = s_hat.encode(12)
dk = dk_pke + dk[384 * 3:]

ss = ML_KEM_768.decaps(dk, ct )
print(AES.new(ss, AES.MODE_ECB).decrypt(aesenc))

最終的なコード

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
from base64 import b64decode
from kyber_py.ml_kem import ML_KEM_768
from Crypto.Cipher import AES

import hashlib

def dump(name: str, data: bytes):
print(f"{name}:")
for i, c in enumerate(data):
if i % 15 == 0:
print(" ", end="")
print(f"{c:02x}:", end="")
if i % 15 == 14:
print()
print()

# with open("totyu.bin", "rb") as f:
with open("honban.bin", "rb") as f:
buf = f.read()

z = buf[-0x20:]
seed = b"?"*32 + z
# dump("seed", seed)

dk = b"?" * 99 # 正確には99バイト目の下位4ビットはbuf[1] & 0xfで取れそう
dk += buf[2:] # dk = dk_pke || ek || keccak512(seed||rank) || keccak256(ek) || z

# dump("dk", dk)

ek = buf[0x41f:0x8bf]
# dump("ek", ek)

a = hashlib.sha3_256(ek).digest()
b = dk[-0x40:-0x20]
assert a == b

ek_pke = ek
dk_pke = dk[:dk.index(ek)]


s_hat = ML_KEM_768.M.decode_vector(dk[0: 384 * 3], ML_KEM_768.k, 12, is_ntt=True)
t_hat_bytes, rho = ek[:-32], ek[-32:]
t_hat = ML_KEM_768.M.decode_vector(t_hat_bytes, 3, 12, is_ntt=True)
A_hat = ML_KEM_768._generate_matrix_from_seed(rho, transpose=True)
ct = bytes.fromhex("9956e487373793da71f9e70ea79a13a471bf7d512cb8b438c61532984a5309ed6ab6e663b615ff05b0ce792584db86dd82ca63092db11bd86b231daeb6fe5bd9e81c9dc27fcf84e71b843c2f7ed9048c9f2abd44e1244b8f9abf52b04651d4e4bbb40cd075b66b7ecf5bbd67082d9451c66cc5ffb9416b79db1eeece91173d0d11232d3e2ae3d59a50018b29553d6d2393ac4224a1fd94fa2a5e3d7d03b426ab7280385532724e19be44fc8bcdd4ea75853fd738163826e9a5359c6d5760c0e5de5907fa2b32256363114b3b4a785ea13e7273fbd8ffec00633523983e1bf9e3eab1b4cc86e9c22d104e3bc747a8179e70161ed21bdff6372324d0f726cea443b0f268e0df7f233efb2f51969115f00ba4af5ca69f0c1c65ca85cbad582d3ceb3c829615c1396808eb0da192560343f7c8bb5b71fca15b6c3bdcc5ea416148f569bb4d46f170f267356a91d4b6c1aa53fab54a788a549eedb7e349b332c417ea0000766bcb00150e02a0eb18b0f997be1badbbb62980ba4ae434c44560e01c75459e99799afaa07fcb880d619ccd19b98b1d1ac1b748ca89db0b019ddaccd21007ffc6965fe33434c91d91d64df237affd68133de514870159a8ef2a044d97ee1bc3b124bd3533aee83fc335b926b290e4d34c834a19ef80732f920783342e4f81721bde62e92334aaa67300ca301e1ccda61177e984d29629d2abc110bb90129697cceaedd268d121e34122952db4fce7af54dd0cffbcdd8ba63f4fe7c9d6d2244fcdbebe29a8b4e55384cd9b561a563a5f45a4f71cbbee5ec25b9fbcb47b112da7571cc3d021af31049b69f182b4ba7f230259a045c2b08bad89419ae37b1590ff405194f4b987d33e61435a40ffc1a9f9d9f0f5e9915ddbc5b7f0e4cc72d188c6c12593b38d96f98e7d4dfbcada0202a2b32226f9e111cc22c73a7d55e154d05115beab3b700fe62dbda9f86b5d8a4f5a758e4b913c3f96f11bc8768df25a851b0c817140d76cf75e5b045677b74208202c1827e66f4b81d5cd3fb93cc71dc7704f744dd278fd765959206fa0bb0b844db07ee040bc8d5563b797c8ecffb545e8c22dec89973f482ac6a29757cf5d51d2abe8f5abf074df8dac19a8d3b5804fe82a90694ba44a730ae12be00ff3dbfb1a55c3bda2bd93421dfd9f72025cf79bd8d6c5f8ff6d93895fd9743621eb141c00550a63b48f483ba5a67dd2b2ceeb4c126979a73433812a328405ee8402cc40b0b57cb8fd2c01496be77206a460cec5bca75a4d447665e0f150776b6966c3965bab258df823dba65fd9def5501623e88bc2e7cf4036792b4904a4932595a813f7c8e2b6e108d64e49d7ba5ea2949a40b2373596aec716375c2b5387e670cfe944db49b8e2eea00237216634c57a17fc1eb968158ebc502a599399a59a8eed3e11b9e02a12c3616b818d6c3d9d081d730d2ef62ee9b7337b995308c8b58baac95b38db76b9ea653f624b4d3e9ee1db51bc0204cc553763589d5a861510489141c71424c538592e8f7c75a55103a353")
key = "key"
aesenc = bytes.fromhex("bed3b7d98a1058fe7059c15bffac13205a39bc22263ef9110b5bde66f10c847fbe2eae728a1a427d99bbee0b48c9fd76")


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[]
for i in range(67):
s_hat[0,0].coeffs[i] = PR.gens()[i]

A_hat = A_hat.T

A = A_hat.from_ntt()
t = t_hat.from_ntt()
s = s_hat.from_ntt()

e = t - (A @ s)

row = 150
es = Sequence([e[i,0].coeffs[k] for i in range(3) for k in range(len(e[i,0].coeffs))]).coefficient_matrix()
A = es[0].T[:,:row]
I = diagonal_matrix([1/(3329//2)]*67 + [3329*2])
pI = Matrix.identity(QQ, row)*3329
Zero = zero_matrix(row,68)
MAT = block_matrix([[pI,Zero],[A,I] ], subdivide=False )



_s_vec = [int(i%3329) for i in MAT.LLL()[-1][row:-1]*(3329//2)]
s_hat = ML_KEM_768.M.decode_vector(dk[0: 384 * 3], ML_KEM_768.k, 12, is_ntt=True)

for i in range(66):
s_hat[0,0].coeffs[i] = int(_s_vec[i])

dk_pke = s_hat.encode(12)
dk = dk_pke + dk[384 * 3:]

ss = ML_KEM_768.decaps(dk, ct )
print(AES.new(ss, AES.MODE_ECB).decrypt(aesenc))

print(key)
print(ss)