RTACTF 2023にUZQueenで参加しました。1

起きたらちょうどRTACTFのpwnをやっていたので、布団でしばらく観戦したあとcryptoだけ参戦しました。
というか走っていた方々、手元を見られながらも解けるの本当にすごい

XOR-CBC

xor-cbc

AESの部分がないので、XORでガチャガチャして復元します。平文はRTACTF{で始まることがわかっているので、先頭7バイト分の鍵はIV・暗号化されたフラグ・既知の平文のXORで復元できます。
鍵の残り1バイトは}で終わることを利用すれば導出できますが、めんどくさいので総当たりしました。

 1from toyotama import xor
 2
 3encrypted = bytes.fromhex("6528337d61658047295cef0310f933eb681e424b524bcc294261bd471ca25bcd6f3217494b1ca7290c158d7369c168b3")
 4KEY_SIZE = 8
 5
 6
 7def decrypt(ciphertext, key):
 8    iv, ciphertext = ciphertext[:KEY_SIZE], ciphertext[KEY_SIZE:]
 9
10    plaintext = b""
11    for i in range(0, len(ciphertext), KEY_SIZE):
12        c_block = ciphertext[i : i + KEY_SIZE]
13        p_block = p64(u64(iv) ^ u64(c_block) ^ u64(key))
14        plaintext += p_block
15        iv = c_block
16
17    return plaintext.rstrip(plaintext[-1:])
18
19
20for i in range(0x100):
21    iv = encrypted[:KEY_SIZE]
22    key = xor(encrypted[KEY_SIZE : KEY_SIZE + 7], b"RTACTF{", iv[:7]) + bytes([i])
23
24    flag = decrypt(encrypted, key)
25    if all(0x20 <= c < 0x7F for c in flag):
26        print(flag)

Collision-DES

collision-des

鍵key1が与えられます。 key1と同じ鍵長かつ、key1との積集合が空集合であるような鍵key2を見つけて下さいというやつです。 DESの鍵の各バイトの下位1ビットがパリティになっていて、実装では無視されるので、そのビットを反転させても暗号化結果は変わらずに制約を満たす鍵を作れます。 2

パリティビットの話は知ってはいたのですが、DESの問題最近全然解いておらず、調べてようやく思い出しました……

1from toyotama import *
2
3_r = Socket("nc 35.194.118.87 7002")
4
5key1 = _r.recvvalue(parser=lambda x: bytes.fromhex(x))
6key2 = bytes([k ^ 1 for k in key1])
7_r.sendlineafter(b"Key 2:", key2.hex())
8
9_r.interactive()

Reused-AES

reused-aes

鍵とIVを使いまわしたAESのCFBモードで、好きな平文を何回でも暗号化できます。 CFBモードはWaniCTFで作問したときに触ったと思うんですが、普通に忘れてました。

いろいろ試していると平文のバイトが一致していれば、対応する暗号文のバイトも一致することがわかるので、先頭から1バイトずつ総当たりして求めます。

 1from toyotama import *
 2
 3flag = b"RTACTF{"
 4for _ in range(100):
 5    for c in range(0x20, 0x7F):
 6        _r = Socket("nc 35.194.118.87 7001")
 7        enc_flag = bytes.fromhex(_r.recvline().decode())
 8
 9        payload = flag + bytes([c])
10        _r.sendlineafter(b"> ", payload)
11        enc_pt = bytes.fromhex(_r.recvline().decode())
12
13        if enc_flag[: len(flag) + 1] == enc_pt[: len(flag) + 1]:
14            flag += bytes([c])

1R-AES

1r-aes

時間内に解けなかった……

ラウンド数が1になっているのでいくつかの処理がスキップされ、encrypt_block内では入力に対して次の処理が実行されます。
add_round_key -> sub_bytes -> shift_rows -> add_round_key

ここで逆の操作をしようとすると、鍵がわからないのでadd_round_keyができません。
しかし、今回はencrypt_blockのオラクルがあるので、平文と暗号文の位置が対応づいてさえいれば1バイトずつ総当たりできます。

平文と暗号文の位置の対応が壊れる箇所はsub_bytesのみなので、そこだけ位置が対応づくように逆操作してあげれば、先述の通り復元できます。3

 1from toyotama import Socket
 2from aes import s_box, inv_s_box, bytes2matrix, matrix2bytes, inv_shift_rows, inv_sub_bytes
 3
 4_r = Socket("nc 35.194.118.87 7003")
 5enc_flag = _r.recvvalue(parser=lambda x: bytes.fromhex(x.strip()))
 6enc_flag = bytes2matrix(enc_flag)
 7inv_shift_rows(enc_flag)
 8inv_sub_bytes(enc_flag)
 9enc_flag = matrix2bytes(enc_flag)
10
11flag = b""
12i = 0
13for _ in range(100):
14    for c in range(0x20, 0x7F):
15
16        payload = flag + bytes([c])
17        payload = payload + b"\0" * (16 - len(payload))
18        payload = payload.hex()
19        _r.sendlineafter(b"msg > ", payload)
20
21        enc_msg = _r.recvvalue(parser=lambda x: bytes.fromhex(x.strip()))
22
23        enc_msg = bytes2matrix(enc_msg)
24        inv_shift_rows(enc_msg)
25        inv_sub_bytes(enc_msg)
26        enc_msg = matrix2bytes(enc_msg)
27
28        if enc_flag[i] == enc_msg[i]:
29            flag += bytes([c])
30            i += 1
31            if i == 16:
32                flag = f"RTACTF{{{flag.decode()}}}"
33                print(flag)
34                exit(0)
35            continue

  1. ゲーム開発部で唯一持ってないです😭 ↩︎

  2. https://github.com/Legrandin/pycryptodome/blob/8bba4a056fb6b5cb7cc9616da3d36893f759efe8/lib/Crypto/Cipher/DES.py#L90 ↩︎

  3. ソルバではshift_rowsの逆操作もしていますが、位置の対応は変わらないので別に要らないです ↩︎