以下の内容はhttps://yocchin.hatenablog.com/entry/2024/08/05/225945より取得しました。


TFC CTF 2024 Writeup

この大会は2024/8/2 20:00(JST)~2024/8/4 20:00(JST)に開催されました。
今回もチームで参戦。結果は600点で924チーム中134位でした。
自分で解けた問題をWriteupとして書いておきます。

RULES (MISC, WARMUP)

Rulesのページにサンプルフラグが書いてあった。

TFCCTF{M4ny_ch4ng3s...m0r3_3ff0rt}

CCCCC (CRYPTO, WARMUP)

2バイト目にある"c"を除き、hexデコードする。

#!/usr/bin/env python3
with open('ccccc.txt', 'r') as f:
    enc = f.read()

hex_enc = ''
for i in range(0, len(enc), 2):
    hex_enc += enc[i]

flag = bytes.fromhex(hex_enc).decode()
print(flag)
TFCCTF{cshout_cout_ct0_cmy_cb0y_c4nd_cmy_cdog_cand_cmy_cc47}

GENETICS (CRYPTO, WARMUP)

フラグは"TFCCTF{"から始まることを前提にすると、以下のように2進数の変換をし、デコードすればよいことがわかる。

A: 00
C: 01
G: 10
T: 11
#!/usr/bin/env python3
enc = 'CCCA CACG CAAT CAAT CCCA CACG CTGT ATAC CCTT CTCT ATAC CGTA CGTA CCTT CGCT ATAT CTCA CCTT CTCA CGGA ATAC CTAT CCTT ATCA CTAT CCTT ATCA CCTT CTCA ATCA CTCA CTCA ATAA ATAA CCTT CCCG ATAT CTAG CTGC CCTT CTAT ATAA ATAA CGTG CTTC'
enc = enc.replace('A', '00').replace('C', '01')
enc = enc.replace('G', '10').replace('T', '11')
enc = enc.split(' ')

flag = ''
for c in enc:
    flag += chr(int(c, 2))
print(flag)
TFCCTF{1_w1ll_g3t_th1s_4s_4_t4tt00_V3ry_s00n}

CONWAY (CRYPTO, EASY)

暗号化処理の概要は以下の通り。

・initial = 11131221131211131231121113112221121321132132211331222113112211
・initial = generate_next_key(initial)
 ※generate_next_key関数は未知
・initialを出力
・initial = generate_next_key(initial)
・key: initialの文字列のsha256ダイジェスト
・keyを使って、flagをAES ECBモード暗号化したものを16進数表記で出力

11131221131211131231121113112221121321132132211331222113112211の次は以下の値になっている。

311311222113111231131112132112311321322112111312211312111322212311322113212221

どのように値が作られているかを推測する。

11131221131211131231121113112221121321132132211331222113112211

左から何個連続して値が構成されているかを見に行けば良さそうなので、確認してみる。

3個の1 → 31
1個の3 → 13
1個の1 → 11
2個の2 → 22
2個の1 → 21
  :

途中まで見ていて正しそう。関数を実装して、keyを割り出し、復号する。

#!/usr/bin/env python3
import hashlib
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad

def generate_next_key(n):
    s = str(n) + 'X'

    key = ''
    count = 1
    pre = s[0]
    for i in range(1, len(s)):
        if pre == s[i]:
            count += 1
        else:
            key += str(count) + pre
            count = 1
            pre = s[i]

    return int(key)

with open('output.txt', 'r') as f:
    params = f.read().splitlines()

initial2 = int(params[0])
enc_flag = bytes.fromhex(params[1])

initial = 11131221131211131231121113112221121321132132211331222113112211
initial = generate_next_key(initial)
assert initial == initial2

initial = generate_next_key(initial)
h = hashlib.sha256()
h.update(str(initial).encode())
key = h.digest()

cipher = AES.new(key, AES.MODE_ECB)
flag = unpad(cipher.decrypt(enc_flag), 16).decode()
print(flag)
TFCCTF{c0nway's_g4me_0f_sequences?}

PADGROUNDS (CRYPTO, MEDIUM)

Padding Oracle Attackが使えそうだが、復号したデータに以下のような処理があり、確率的に判断するしかなさそう。

padding = True
try:
    unpad(pt, 16)
except:
    padding = False
padding = (padding | (random.randint(1,10) > 7) ) & (random.randint(1,10) <= 7)

unpadに成功した場合、padding = 7/10の確率でTrue
unpadに失敗した場合、padding = 3/10 * 7/10 = 21/100の確率でTrue
フラグの形式はTFCCTF{[bdefgmnprsu012345_]+}で、{}の中身は18種の文字。

$ nc challs.tfcctf.com 31470
Lets see you decode this: 0UniKJi+ZZ66z8W3EiVZFLENEyKV5MigeiVArz1VrtKAXhEnpWWWVMHZcH+b6gVdvmSeC/3DIqPndg9Vu7NaVg==
I made my unpad truly random, there is nothing you can do, just give up already
>>> from base64 import *
>>> s = '0UniKJi+ZZ66z8W3EiVZFLENEyKV5MigeiVArz1VrtKAXhEnpWWWVMHZcH+b6gVdvmSeC/3DIqPndg9Vu7NaVg=='
>>> len(b64decode(s))
64

ivを含めて暗号ブロックは4ブロックのため、フラグは3ブロック。以下のような平文構成で、不明文字は40バイト。

0123456789abcdef
TFCCTF{xxxxxxxxx
xxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxx}

同じ暗号文を20回試し、最もTrueが多いものがunpad成功したものとして判断し、Padding Oracle Attackを行う。なお、試行回数の制限があるため、ブロックごとに接続し、フラグの形式から制限できる文字のみを対象として、復号する。

#!/usr/bin/env python3
import socket
from base64 import *
from Crypto.Util.strxor import strxor

def recvuntil(s, tail):
    data = b''
    while True:
        if tail in data:
            return data.decode()
        data += s.recv(1)

chars = b'bdefgmnprsu012345_}'

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('challs.tfcctf.com', 32038))

data = recvuntil(s, b'\n').rstrip()
print(data)
enc_flag = b64decode(data.split(' ')[-1])
data = recvuntil(s, b'\n').rstrip()
print(data)

block_index = 0
size = 9

enc_blocks = [enc_flag[i:i+16] for i in range(0, len(enc_flag), 16)]
curr_enc_block = enc_blocks[block_index]
next_enc_block = enc_blocks[block_index + 1]

xor_block = b''
for i in range(size):
    max_count = -1
    ch = 0
    for c in chars:
        try_c = bytes([c ^ curr_enc_block[-i-1] ^ (i + 1)])
        to_unpad = b'\x00' * (16 - i - 1) + try_c + strxor(xor_block, bytes([i + 1]) * i)
        to_unpad += next_enc_block
        to_unpad = b64encode(to_unpad).decode()

        count = 0
        for j in range(20):
            print(to_unpad)
            s.sendall(to_unpad.encode() + b'\n')
            data = recvuntil(s, b'\n').rstrip()
            print(data)
            if data == 'True':
                count += 1
        if count > max_count:
            max_count = count
            ch = c

    xor_block = bytes([ch ^ curr_enc_block[-i-1]]) + xor_block

flag1 = 'TFCCTF{' + strxor(xor_block, curr_enc_block[-size:]).decode()
print(flag1)
#!/usr/bin/env python3
import socket
from base64 import *
from Crypto.Util.strxor import strxor

def recvuntil(s, tail):
    data = b''
    while True:
        if tail in data:
            return data.decode()
        data += s.recv(1)

chars = b'bdefgmnprsu012345_}'

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('challs.tfcctf.com', 32038))

data = recvuntil(s, b'\n').rstrip()
print(data)
enc_flag = b64decode(data.split(' ')[-1])
data = recvuntil(s, b'\n').rstrip()
print(data)

block_index = 1
size = 16

enc_blocks = [enc_flag[i:i+16] for i in range(0, len(enc_flag), 16)]
curr_enc_block = enc_blocks[block_index]
next_enc_block = enc_blocks[block_index + 1]

xor_block = b''
for i in range(size):
    max_count = -1
    ch = 0
    for c in chars:
        try_c = bytes([c ^ curr_enc_block[-i-1] ^ (i + 1)])
        to_unpad = b'\x00' * (16 - i - 1) + try_c + strxor(xor_block, bytes([i + 1]) * i)
        to_unpad += next_enc_block
        to_unpad = b64encode(to_unpad).decode()

        count = 0
        for j in range(20):
            print(to_unpad)
            s.sendall(to_unpad.encode() + b'\n')
            data = recvuntil(s, b'\n').rstrip()
            print(data)
            if data == 'True':
                count += 1
        if count > max_count:
            max_count = count
            ch = c

    xor_block = bytes([ch ^ curr_enc_block[-i-1]]) + xor_block

flag2 = strxor(xor_block, curr_enc_block[-size:]).decode()
print(flag2)
#!/usr/bin/env python3
import socket
from base64 import *
from Crypto.Util.strxor import strxor

def recvuntil(s, tail):
    data = b''
    while True:
        if tail in data:
            return data.decode()
        data += s.recv(1)

chars = b'bdefgmnprsu012345_}'

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('challs.tfcctf.com', 32038))

data = recvuntil(s, b'\n').rstrip()
print(data)
enc_flag = b64decode(data.split(' ')[-1])
data = recvuntil(s, b'\n').rstrip()
print(data)

block_index = 2
size = 16

enc_blocks = [enc_flag[i:i+16] for i in range(0, len(enc_flag), 16)]
curr_enc_block = enc_blocks[block_index]
next_enc_block = enc_blocks[block_index + 1]

xor_block = b''
for i in range(size):
    max_count = -1
    ch = 0
    for c in chars:
        try_c = bytes([c ^ curr_enc_block[-i-1] ^ (i + 1)])
        to_unpad = b'\x00' * (16 - i - 1) + try_c + strxor(xor_block, bytes([i + 1]) * i)
        to_unpad += next_enc_block
        to_unpad = b64encode(to_unpad).decode()

        count = 0
        for j in range(20):
            print(to_unpad)
            s.sendall(to_unpad.encode() + b'\n')
            data = recvuntil(s, b'\n').rstrip()
            print(data)
            if data == 'True':
                count += 1
        if count > max_count:
            max_count = count
            ch = c

    xor_block = bytes([ch ^ curr_enc_block[-i-1]]) + xor_block

flag3 = strxor(xor_block, curr_enc_block[-size:]).decode()
print(flag3)

1ブロック目の実行結果は以下の通り。

Lets see you decode this: +55UZlXtwIGdO44nN4k6KkWxQoBbVaM6ZfYKK8DEtLZtvI5B+3pg0WTCC2n9QJKC3x8WNDS4Vtuis/hHfZJFEA==
I made my unpad truly random, there is nothing you can do, just give up already
AAAAAAAAAAAAAAAAAAAASUWxQoBbVaM6ZfYKK8DEtLY=
False
AAAAAAAAAAAAAAAAAAAASUWxQoBbVaM6ZfYKK8DEtLY=
False
                :
                :
AAAAAAAAAPWkAuNxTrRXR0WxQoBbVaM6ZfYKK8DEtLY=
False
AAAAAAAAAPWkAuNxTrRXR0WxQoBbVaM6ZfYKK8DEtLY=
False
AAAAAAAAAPWkAuNxTrRXR0WxQoBbVaM6ZfYKK8DEtLY=
False
TFCCTF{g00d_p4dd

2ブロック目の実行結果は以下の通り。

Lets see you decode this: fyeoNP2LtVhsDxLSnIWuy4jkNbLL+imXuTRHbzBu64Ko98wu0Dwd5D6+t20WwVSDcxlPw01Zce80cJ7GM02FlQ==
I made my unpad truly random, there is nothing you can do, just give up already
AAAAAAAAAAAAAAAAAAAA4aj3zC7QPB3kPr63bRbBVIM=
False
AAAAAAAAAAAAAAAAAAAA4aj3zC7QPB3kPr63bRbBVIM=
False
                :
                :
5ZpC0YSHDdjPVmYaThrOzaj3zC7QPB3kPr63bRbBVIM=
False
5ZpC0YSHDdjPVmYaThrOzaj3zC7QPB3kPr63bRbBVIM=
False
5ZpC0YSHDdjPVmYaThrOzaj3zC7QPB3kPr63bRbBVIM=
True
1ngs_m4_fr1end5_

3ブロック目の実行結果は以下の通り。

Lets see you decode this: BVBqoGm/O7krS807xFGrmZIlasp/7YQyvf+6r/U2FEntofI8gRuiT1FwcWlixDkBeJv4PGNEe3SakwcKwk7gCg==
I made my unpad truly random, there is nothing you can do, just give up already
AAAAAAAAAAAAAAAAAAAAYnib+DxjRHt0mpMHCsJO4Ao=
True
AAAAAAAAAAAAAAAAAAAAYnib+DxjRHt0mpMHCsJO4Ao=
False
                :
                :
gNSPSfxpgS0eUj4fB7oYbHib+DxjRHt0mpMHCsJO4Ao=
False
gNSPSfxpgS0eUj4fB7oYbHib+DxjRHt0mpMHCsJO4Ao=
False
gNSPSfxpgS0eUj4fB7oYbHib+DxjRHt0mpMHCsJO4Ao=
False
rememb3r_2_fun1}

結合すると、フラグになる。

TFCCTF{g00d_p4dd1ngs_m4_fr1end5_rememb3r_2_fun1}



以上の内容はhttps://yocchin.hatenablog.com/entry/2024/08/05/225945より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14