この大会は2021/11/27 21:00(JST)~2021/11/28 21:00(JST)に開催されました。
今回もチームで参戦。結果は391点で247チーム中66位でした。
自分で解けた問題をWriteupとして書いておきます。
Sanity Check (Miscellaneous)
IRCでLibera.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}