Dragon CTF 2021 Writeup

この大会は2021/11/27 21:00(JST)~2021/11/28 21:00(JST)に開催されました。
今回もチームで参戦。結果は391点で247チーム中66位でした。
自分で解けた問題をWriteupとして書いておきます。

Sanity Check (Miscellaneous)

IRCLibera.Chatに接続し、#dragonsectorチャネルに入ると、メッセージにフラグが書いてあった。

DrgnS{Welcome_to_Dragon_CTF_2021!}

Baby MAC (Cryptography)

サーバの処理概要は以下の通り。

・key: ランダム16バイト
・以下繰り返し選択
 ・"sign"の場合
  ・data: 入力(hex)→hexデコード
   →'gimme flag'が含まれていたらNG
  ・(sign(data, key) + data)のhex表示
   ・dataを16バイトパディング
   ・blocks: 16バイトごとにブロック分け
   ・mac: '\x00' * 16
   ・順にブロックをmacとXORしてAES-ECB暗号化したものをmacにする。
   ・最後にmacをAES-ECB暗号化したものを返す。
 ・"verify"の場合
  ・data: 入力(hex)→hexデコード
   ・tag: dataの先頭16バイト
   ・data: dataからtagを除いた部分
   ・signした結果のmacとtagが一致していたら、Trueを返す。
  ・verify結果がTrueで、dataに'gimme flag'が含まれていたら、フラグを表示

macの計算イメージは以下の通り。

PT1       --(AES暗号化)--> CT1
PT2 ^ CT1 --(AES暗号化)--> CT2
PT3 ^ CT2 --(AES暗号化)--> CT3
CT3       --(AES暗号化)--> MAC

以下のような流れで'gimme flag'が含まれているMACを算出し、signのverifyする。

1回目sign
PT1 = pad('A')

PT1       --(AES暗号化)--> CT1
CT1       --(AES暗号化)--> MAC
→入力:'A'
→出力:MAC

2回目sign
G = pad('gimme flag')
PT1 = pad('A')
PT2 = '\x00' * 16
PT3 = MAC ^ G
PAD = '\x10' * 16

PT1       --(AES暗号化)--> CT1
CT1       --(AES暗号化)--> MAC
G         --(AES暗号化)--> CT3
PAD ^ CT3 --(AES暗号化)--> CT4
CT4       --(AES暗号化)--> new MAC
→入力:'A' + '\x0f' * 15 + '\x00' * 16 + (MAC ^ ('gimme flag' + '\x06' * 6))
→出力:new MAC

verify
・data: pad('gimme flag')
・tag: new MAC
#!/usr/bin/env python3
import socket

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

def pad(data, bsize):
    b = bsize - len(data) % bsize
    return data + bytes([b] * b)

def xor(a, b):
    return bytes(aa ^ bb for aa, bb in zip(a, b))

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('babymac.hackable.software', 1337))

#### get sign('A') ####
inp = b'A'
data = recvuntil(s, b'> ')
print(data + 'sign')
s.sendall(b'sign\n')
data = recvuntil(s, b'> ')
print(data + inp.hex())
s.sendall(inp.hex().encode() + b'\n')
data = recvuntil(s, b'\n').rstrip()
print(data)
mac = bytes.fromhex(data[:32])

#### get sign(pad('gimme flag')) ####
inp = pad(b'A', 16) + b'\x00' * 16 + xor(mac, pad(b'gimme flag', 16))
data = recvuntil(s, b'> ')
print(data + 'sign')
s.sendall(b'sign\n')
data = recvuntil(s, b'> ')
print(data + inp.hex())
s.sendall(inp.hex().encode() + b'\n')
data = recvuntil(s, b'\n').rstrip()
print(data)
new_mac = bytes.fromhex(data[:32])

#### verify pad('gimme flag') ####
inp = new_mac + pad(b'gimme flag', 16)
data = recvuntil(s, b'> ')
print(data + 'verify')
s.sendall(b'verify\n')
data = recvuntil(s, b'> ')
print(data + inp.hex())
s.sendall(inp.hex().encode() + b'\n')
data = recvuntil(s, b'\n').rstrip()
print(data)

実行結果は以下の通り。

What to do?
> sign
> 41
39cb16283075f894c2f6f3609e9b453941
What to do?
> sign
> 410f0f0f0f0f0f0f0f0f0f0f0f0f0f0f000000000000000000000000000000005ea27b4555559ef8a391f566989d433f
445b5163c84c47412daf9fc1eae3d1b1410f0f0f0f0f0f0f0f0f0f0f0f0f0f0f000000000000000000000000000000005ea27b4555559ef8a391f566989d433f
What to do?
> verify
> 445b5163c84c47412daf9fc1eae3d1b167696d6d6520666c6167060606060606
DrgnS{yuP_th4t_wa5nt_h4rD!1}
DrgnS{yuP_th4t_wa5nt_h4rD!1}

Compress The Flag (Miscellaneous)

サーバの処理概要は以下の通り。

・data = ''
・以下繰り返し
 ・idx: '\n'のインデックス
  ・'\n'がない場合
   ・データが128バイトを超える場合は終了
   ・d: 入力→ない場合終了
   ・dataにdを連結
  ・line: 改行前まで
  ・data: 改行後から
  ・seed: lineの1つ目の":"の前(数値)
  ・string: lineの1つ目の":"の後
  ・random.seed(int(seed))
  ・random.shuffle(flag)
  ・test_string = string + bytes(flag)
$ nc compresstheflag.hackable.software 1337
Please send: seed:string\n
I'll then show you the compression benchmark results!
Note: Flag has format DrgnS{[A-Z]+}
123:test
    none   29
    zlib   37
   bzip2   67
    lzma   88

zlibの同じ文字列の繰り返しがある場合に圧縮率が高くなる特徴を使ったCRIMEの問題と推測できる。
4バイトの入力でnoneが29のため、フラグの長さは25。ただフラグはシャッフルされるので、seedの値を調整して、できるだけ先頭にフラグ文字列の先頭を持ってくる。さらにseedの値により、"{", "}"の位置はわかるので、その場合だけそのまま文字を入れる。あとは英大文字だけという条件のため、1文字ずつブルートフォースでzlibのサイズが最も小さくなる場合の文字を探していけばよい。

#!/usr/bin/env python3
import socket
import random
import string

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

def zlib_size(s):
    data = recvuntil(s, b'\n').rstrip()
    print(data)
    data = recvuntil(s, b'\n').rstrip()
    print(data)
    zlib_len = int(data.split(' ')[-1])
    for _ in range(3):
        data = recvuntil(s, b'\n').rstrip()
        print(data)
    return zlib_len

def reverse_shuffle(tbl1, tbl2, ct):
    pt = [''] * len(ct)
    for i in range(len(ct)):
        index = tbl1.index(tbl2[i])
        pt[index] = ct[i]
    return ''.join(pt)

flag1 = string.ascii_uppercase[:25].encode()

for seed in range(1000000):
    FLAG = bytearray(flag1)
    random.seed(seed)
    random.shuffle(FLAG)
    if string.ascii_uppercase[:4].encode() == bytes(FLAG[:4]):
        break

flag2 = bytes(FLAG)

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('compresstheflag.hackable.software', 1337))

data = recvuntil(s, b'}\n').rstrip()
print(data)

idx_bracket1 = flag2.index(b'F')
idx_bracket2 = flag2.index(b'Y')

flag = 'Drgn'
for i in range(4, 25):
    if i == idx_bracket1:
        flag += '{'
        continue
    elif i == idx_bracket2:
        flag += '}'
        continue
    min_size = 9999
    min_flag = ''
    for c in string.ascii_uppercase:
        try_flag = flag + c
        payload = str(seed) + ':' + try_flag
        print(payload)
        s.sendall(payload.encode() + b'\n')
        size = zlib_size(s)
        if size < min_size:
            min_size = size
            min_flag = try_flag
    flag = min_flag

print(flag)

flag = reverse_shuffle(flag1, flag2, flag)
print(flag)

実行結果は以下の通り。

Please send: seed:string\n
I'll then show you the compression benchmark results!
Note: Flag has format DrgnS{[A-Z]+}
198763:DrgnA
    none   30
    zlib   36
   bzip2   66
    lzma   88

198763:DrgnB
    none   30
    zlib   36
   bzip2   67
    lzma   88

198763:DrgnC
    none   30
    zlib   36
   bzip2   66
    lzma   88

198763:DrgnD
    none   30
    zlib   36
   bzip2   66
    lzma   88

    :

198763:DrgnSESSE{RAUIGTSIMISCH}X
    none   50
    zlib   37
   bzip2   73
    lzma   96

198763:DrgnSESSE{RAUIGTSIMISCH}Y
    none   50
    zlib   37
   bzip2   73
    lzma   96

198763:DrgnSESSE{RAUIGTSIMISCH}Z
    none   50
    zlib   37
   bzip2   73
    lzma   96

DrgnSESSE{RAUIGTSIMISCH}I
DrgnS{THISISACRIMEIGUESS}
DrgnS{THISISACRIMEIGUESS}