vsCTF 2024 Writeup

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

Sanity Check (web)

ブラウザ上で右クリックやデベロッパーツールが使えないので、curlでアクセスする。

$ curl https://sanity-check.vsc.tf/
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Sanity Check</title>
    <style>
        body {
            background-color: #000021;
            font-family: "JetBrains Mono", monospace;
        }

        h1 {
            background: linear-gradient(to right, #fe5d00, #fe8c00);
            background-clip: text;
            -webkit-background-clip: text;
            color: transparent;
            text-align: center;
        }

        .centered {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
        }
    </style>
    <script>
        setInterval("debugger", 10)
    </script>
</head>

<body oncontextmenu="return false">
    <script>
        document.addEventListener('contextmenu', (e) => {
            e.preventDefault();
        }
        );
        document.onkeydown = function (e) {
            if (event.keyCode == 123) {
                return false;
            }
            if (e.ctrlKey && e.shiftKey && e.keyCode == 'I'.charCodeAt(0)) {
                return false;
            }
            if (e.ctrlKey && e.shiftKey && e.keyCode == 'C'.charCodeAt(0)) {
                return false;
            }
            if (e.ctrlKey && e.shiftKey && e.keyCode == 'J'.charCodeAt(0)) {
                return false;
            }
            if (e.ctrlKey && e.keyCode == 'U'.charCodeAt(0)) {
                return false;
            }
        }
    </script>
    <div class="centered">
        <h1>you know what to do</h1>
        <!-- vsctf{c0ngratulati0ns_y0u_viewed_the_s0urc3!...welcome_to_vsctf_2024!} -->
    </div>
</body>

</html>

コメントにフラグが書いてあった。

vsctf{c0ngratulati0ns_y0u_viewed_the_s0urc3!...welcome_to_vsctf_2024!}

Discord (misc)

Discordに入り、#announcementsチャネルのメッセージを見たら、フラグが書いてあった。

vsctf{welcome_to_vsctf_2024!}

baby-game (algo)

1行目にセルの縦の長さ=横の長さが表示される。次の行にCaring Koalaの位置(x1, y1)、Red Pandaの位置(x2, y2)としてx1 y1 x2 y2が表示される。Caring Koalaから縦横に交互に動き、同じ位置に追いついたら、勝ちになるので、勝つ方を表示するプログラムをSubmitして正しければ、フラグが表示される。
お互いに近づいて行って、どうなるかを見てみる。
2x2で以下の左端の配置の場合、以下のようにC(Caring Koala)から進み、R(Red Panda)が相手のマスに入り、勝つ。

_ C   C _
R _   R _

3x3の以下の左端の配置の場合、以下のようにC(Caring Koala)から進み、C(Caring Koala)が相手のマスに入り、勝つ。

_ C _  C _ _  C _ _
_ _ _  _ _ _  R _ _
R _ _  R _ _  _ _ _

初期位置が(x1, y1), (x2, y2)で、|x1 - x2| + |y1 - y2|が偶数の場合Red Pandaが勝ち、奇数の場合Caring Koalaが勝つ。このことを前提にプログラムを作成する。
以下のコードをSubmitしたら、フラグが表示された。

#include <iostream>
using namespace std;
int main() {
    int n;
    int x1, x2, y1, y2, diff;

    scanf("%d", &n);
    scanf("%d %d %d %d", &x1, &y1, &x2, &y2);
    diff = x1 - x2 + y1 - y2;
    if (diff % 2 == 0) {
        cout << "Red Panda" << endl;
    } else {
        cout << "Caring Koala" << endl;
    }

    return 0;
}
vsctf{baby_game_pure_luck_uwu}

intro-reversing (rev)

Ghidraでデコンパイルする。

undefined8 main(void)

{
  int local_c;
  
  for (local_c = 0; local_c < 0x8ae; local_c = local_c + 0xca) {
    printf("%.*s\n",0xca,flag + local_c);
    sleep(0xb1aaf);
  }
  return 0;
}

sleepの該当箇所のアセンブリは以下のようになっている。

        001011a6 bf af 1a        MOV        EDI,0xb1aaf
                 0b 00
        001011ab e8 c0 fe        CALL       <EXTERNAL>::sleep                                uint sleep(uint __seconds)
                 ff ff

sleepの時間が長いので、バイナリの該当箇所を以下のように書き換えて、0にする。

bf af 1a 0b 00 -> bf 00 00 00 00

書き換えたバイナリを実行すると、ASCIIアートでフラグが現れた。

$ ./chall_mod
                                 /$$      /$$$$$$   /$$$   /$$          /$$$$$$$$         /$$$$$$                  /$$$$$$              /$$$$$$  /$$$$$$$  /$$$$$$$   /$$                       /$$ /$$$  
                                | $$     /$$__  $$ /$$_/ /$$$$         |__  $$__/        /$$$_  $$                /$$__  $$            /$$__  $$| $$__  $$| $$____/ /$$$$                      | $$|_  $$ 
 /$$    /$$ /$$$$$$$  /$$$$$$$ /$$$$$$  | $$  \__/| $$  |_  $$   /$$$$$$$ | $$  /$$$$$$ | $$$$\ $$        /$$$$$$|__/  \ $$ /$$    /$$|__/  \ $$| $$  \ $$| $$     |_  $$   /$$$$$$$   /$$$$$$ | $$  | $$ 
|  $$  /$$//$$_____/ /$$_____/|_  $$_/  | $$$$    /$$$    | $$  | $$__  $$| $$ /$$__  $$| $$ $$ $$       /$$__  $$  /$$$$$/|  $$  /$$/   /$$$$$/| $$$$$$$/| $$$$$$$  | $$  | $$__  $$ /$$__  $$| $$  | $$$
 \  $$/$$/|  $$$$$$ | $$        | $$    | $$_/   |  $$    | $$  | $$  \ $$| $$| $$  \__/| $$\ $$$$      | $$  \__/ |___  $$ \  $$/$$/   |___  $$| $$__  $$|_____  $$ | $$  | $$  \ $$| $$  \ $$|__/  | $$/
  \  $$$/  \____  $$| $$        | $$ /$$| $$      \ $$    | $$  | $$  | $$| $$| $$      | $$ \ $$$      | $$      /$$  \ $$  \  $$$/   /$$  \ $$| $$  \ $$ /$$  \ $$ | $$  | $$  | $$| $$  | $$      | $$ 
   \  $/   /$$$$$$$/|  $$$$$$$  |  $$$$/| $$      |  $$$ /$$$$$$| $$  | $$| $$| $$      |  $$$$$$/      | $$     |  $$$$$$/   \  $/   |  $$$$$$/| $$  | $$|  $$$$$$//$$$$$$| $$  | $$|  $$$$$$$ /$$ /$$$/ 
    \_/   |_______/  \_______/   \___/  |__/       \___/|______/|__/  |__/|__/|__/       \______//$$$$$$|__/      \______/     \_/     \______/ |__/  |__/ \______/|______/|__/  |__/ \____  $$|__/|___/  
                                                                                                |______/                                                                              /$$  \ $$           
                                                                                                                                                                                     |  $$$$$$/           
                                                                                                                                                                                      \______/            
vsctf{1nTr0_r3v3R51ng!}

not-quite-caesar (crypto)

flagの各文字のASCIIコードxに対して、x+3, x-3, x*3, x^3のいずれかをランダムに行っている。ただし、seedを使っているので、どの計算をしているのかがわかり、逆算してフラグがわかる。

#!/usr/bin/env python3
import random

random.seed(1337)

rev_ops = [
    lambda x: x - 3,
    lambda x: x + 3,
    lambda x: x // 3,
    lambda x: x ^ 3,
]

with open('out.txt', 'r') as f:
    out = eval(f.read())

flag = ''
for o in out:
    flag += chr(random.choice(rev_ops)(o))
print(flag)
vsctf{looks_like_ceasar_but_isnt_a655563a0a62ef74}

aes-but-twice (crypto)

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

・nonce: ランダム8バイト文字列
・iv: ランダム16バイト文字列
・key: ランダム16バイト文字列
・CTR_ENC: key, nonceを使ったAES CTRモード暗号化オブジェクト
・CBC_ENC: key, ivを使ったAES CBCモード暗号化オブジェクト
・flag: flagをパディング
・ctr_encrypt(flag)を表示
 ・flagをパディングし、CTR_ENCでAES CTRモード暗号化し、16進数表記にしたものを表示
・cbc_encrypt(flag)を表示
 ・flagをパディングし、CBC_ENCでAES CBCモード暗号化し、16進数表記にしたものを表示
・nonceを16進数表記で表示
・以下繰り返し
 ・inp: 入力
 ・inpが"exit"の場合、繰り返し終了
 ・data: inpをhexデコード
 ・ctr_encrypt(data)を表示
 ・cbc_encrypt(data)を表示
$ nc vsc.tf 5000
f1d134fdbf36b95cbd1fc416b39d8f05e5f6963f4421588742386cfbccb3e7bc3a2df0e071c3f5c70b92dae860c612dc4c2360eaf3b8724c9fc1ab2e7ccc89de
d85b2aebd762788b3673e2ea899c66a18f3db18e81d6650072268d095d71fd93474a496a9606abcdfc5265237a2e90301d935afe61538070fc37f43489c47a81
7a79e0630b0f0d06
11111111
a47d282d91e4ded1a5fbc982f37211e9
54b915c2c6ba7bd6f7fea68c73c02ee0
11111111
867e7ff320220a03e80502273f17760f
51c8d1869666406a2e58f9c9c43f373f

flagをパディングしたものをFFF...FFF(48バイト)とすると、FFF...FFF(48バイト)をパディングしたものは以下の通りとなる。

FLAG = FFF...FFF(48バイト) + "\x10" * 16

NNNNNNNNをnonce、FLAGnをFLAGのnブロック目とすると以下のようなイメージになる。

AES暗号化(CTRモード)
NNNNNNNN\x00....\x00\x00 --(AES暗号化)--> TMP_CT0 ^ FLAG0 = CT1_0
NNNNNNNN\x00....\x00\x01 --(AES暗号化)--> TMP_CT1 ^ FLAG1 = CT1_1
NNNNNNNN\x00....\x00\x02 --(AES暗号化)--> TMP_CT2 ^ FLAG2 = CT1_2
NNNNNNNN\x00....\x00\x03 --(AES暗号化)--> TMP_CT3 ^ FLAG3 = CT1_3

AES暗号化(CBCモード)
FLAG0 ^ IV    --(AES暗号化)--> CT2_0
FLAG1 ^ CT2_0 --(AES暗号化)--> CT2_1
FLAG2 ^ CT2_1 --(AES暗号化)--> CT2_2
FLAG3 ^ CT2_2 --(AES暗号化)--> CT2_3

NNNNNNNN\x00....\x00\x00のAES暗号化を求めれば、XORでFLAG0を求めることができる。
CBCモードは最後のブロックの暗号化がIVとなって、次のブロックを暗号化するので、以下の通りとなるよう平文を指定すれば、その暗号化データを取得できる。

PT0 ^ IV = NNNNNNNN\x00....\x00\x00

同様に他のブロックも計算すれば、FLAGを求めることができ、アンパディングすればflagになる。

#!/usr/bin/env python3
import socket
from Crypto.Util.strxor import strxor
from Crypto.Util.Padding import unpad

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(('vsc.tf', 5000))

data = recvuntil(s, b'\n').rstrip()
print(data)
ctr_enc_flag = bytes.fromhex(data)
data = recvuntil(s, b'\n').rstrip()
print(data)
cbc_enc_flag = bytes.fromhex(data)
data = recvuntil(s, b'\n').rstrip()
print(data)
nonce = bytes.fromhex(data)

iv = cbc_enc_flag[-16:]
flag = b''
for i in range(len(ctr_enc_flag) // 16 - 1):
    pt = strxor(nonce + i.to_bytes(8, 'big'), iv).hex()
    print(pt)
    s.sendall(pt.encode() + b'\n')
    data = recvuntil(s, b'\n').rstrip()
    print(data)
    data = recvuntil(s, b'\n').rstrip()
    print(data)
    tmp_ct = bytes.fromhex(data[:32])
    iv  = bytes.fromhex(data[32:])
    flag_part = strxor(tmp_ct, ctr_enc_flag[i*16:i*16+16])
    flag += flag_part

flag = unpad(flag, 16).decode()
print(flag)

実行結果は以下の通り。

d8105edd4cfe36caf245a2315f71269594c2fbaa9fa8c907e8e12309dd05014d973df1c66f8470adfbd8b6aa644abca6cddeba94208f8ba25403ea8c2bc6bc28
f7f5aa05293e5b001a9e2b33fda03f911d290764dc4743cb622114804db03d528bee586e44e145f7cca8406b1210fb1330d7571c3decb78ffe32a6f8026dcc04
f14df5a7c6efb154
c19aa2bbfb0306dbfe32a6f8026dcc04
ecc42332313815abd726875466c6a6f7613ca4032838cbdcaa96657f292b5663
ae633da92a855bafad32c75f001244f6de1f1d57a6cdb16edb136e55cd7e5bcf
2f52e8f06022003adb136e55cd7e5bce
3344b25be8b6483b977d6e342a72ff6ca986c6eb327babcf0c921542871f0d59
cbf49892aa9dfd32dbd01438eb3639290df05d396349386377c89d5dcc6b65e2
fcbda89ea5a6893777c89d5dcc6b65e0
aa3001cb9f76194d774a3b15d51d48344c191d8cc331eabc4c680711347fa6ac
a240ffc8618a7ea3f5d6b8a46a44b2a820108a480b351a0a1fb3cb6261a68cfd
vsctf{me_wen_cbc_6c855453171638d5}
vsctf{me_wen_cbc_6c855453171638d5}

dream (crypto)

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

・seed: seed.txtの内容
・seed: seedを数値化したもの
・seedをシードとしてランダム設定
・idxs: 配列入力
・idxsの長さが8より大きい場合、終了
・idxが0~623に対して、以下を実行
 ・rand_out: ランダム32ビット整数
 ・idx が idxs に含まれている場合、rand_outを表示
・key: ランダム256ビット整数
・nonce: ランダム256ビット整数
・aes_key: keyを文字列としたsha256ダイジェストの先頭16バイト
・aes_nonce: nonceを文字列としたsha256ダイジェストの先頭16バイト
・aes_key, aes_nonceを使ってパディングしたフラグをAES GCMモード暗号化して16進数表記で表示

78回アクセスすれば、必要なランダム値を入手できるので、Mersenne Twisterの特性からkey, nonceを取得できる。あとはそれを使って、AES GCMモードの復号をすれば、フラグがわかる。

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

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

def untemper(rand):
    rand ^= rand >> 18;
    rand ^= (rand << 15) & 0xefc60000;
 
    a = rand ^ ((rand << 7) & 0x9d2c5680);
    b = rand ^ ((a << 7) & 0x9d2c5680);
    c = rand ^ ((b << 7) & 0x9d2c5680);
    d = rand ^ ((c << 7) & 0x9d2c5680);
    rand = rand ^ ((d << 7) & 0x9d2c5680);
 
    rand ^= ((rand ^ (rand >> 11)) >> 11);
    return rand

N = 624
state = []
for i in range(624 // 8):
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect(('vsc.tf', 5001))

    idxs = [j for j in range(i * 8, i * 8 + 8)]

    data = recvuntil(s, b'>>> ')
    print(data + str(idxs))
    s.sendall(str(idxs).encode() + b'\n')

    for j in range(8):
        data = recvuntil(s, b'\n').rstrip()
        print(data)
        state.append(untemper(int(data)))

    if len(state) == N:
        data = recvuntil(s, b'\n').rstrip()
        print(data)
        enc_flag = bytes.fromhex(data)

    s.close()

state.append(N)
random.setstate([3, tuple(state), None])

key = random.getrandbits(256)
nonce = random.getrandbits(256)
aes_key = sha256(str(key).encode()).digest()[:16]
aes_nonce = sha256(str(nonce).encode()).digest()[:16]
cipher = AES.new(aes_key, AES.MODE_GCM, nonce=aes_nonce)
flag = unpad(cipher.decrypt(enc_flag), 16).decode()
print(flag)

実行結果は以下の通り。

>>> [0, 1, 2, 3, 4, 5, 6, 7]
475736381
2599178730
639420308
1400092731
2055728736
3311659743
3349287473
3292155355
>>> [8, 9, 10, 11, 12, 13, 14, 15]
3972270152
3453764163
774805574
1416637968
1686108321
2690133874
2498232095
247303664
>>> [16, 17, 18, 19, 20, 21, 22, 23]
4130893192
3985880470
1585398883
1755674673
3083186055
1418857434
736357969
3074716874
        :
        :
379443684
2558372521
705855492
292749589
3239662607
3073875086
3509591978
458422530
>>> [608, 609, 610, 611, 612, 613, 614, 615]
1938171477
2450633279
3509420931
1402183533
2579036895
182179989
799027111
2248812622
>>> [616, 617, 618, 619, 620, 621, 622, 623]
1171136879
634080450
3657292713
1917859602
2761658247
93487198
327330884
1608754247
cb3cc14d2f5eeac6b5645bb66fa268d88399d1654df20668110e1d04bf135db71930985b5eba307c0197b035f2e9203f
vsctf{dream_luck???_5e3ec2f2d338fc9f}
vsctf{dream_luck???_5e3ec2f2d338fc9f}