BuckeyeCTF 2023 Writeup

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

Triple D Columbus (web)

HTMLソースを見たら、フラグが書いてあった。

<p hidden>bctf{In$pecT_eLemEnt_Is_prEttY_c00L_ayE?}</p>
bctf{In$pecT_eLemEnt_Is_prEttY_c00L_ayE?}

Beginner Menu (pwn)

メニュー選択で正の数を入力すると、フラグを表示せずに終了する。負の数を入力すればよい。

$ nc chall.pwnoh.io 13371
Enter the number of the menu item you want:
1: Hear a joke
2: Tell you the weather
3: Play the number guessing game
4: Quit
-1
bctf{y0u_ARe_sNeaKy}
bctf{y0u_ARe_sNeaKy}

My First Hash (crypto)

CrackStationでクラックする。

orchestra
bctf{orchestra}

Rivest-Shamir-Adleman (crypto)

RSA暗号。p, qがわかっているので、通常通り復号する。

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

p = 3782335750369249076873452958462875461053
q = 9038904185905897571450655864282572131579
e = 65537
n = 34188170446514129546929337540073894418598952490293570690399076531159358605892687
c = 414434392594516328988574008345806048885100152020577370739169085961419826266692

et = (p - 1) * (q - 1)
d = pow(e, -1, et)
m = pow(c, d, n)
flag = long_to_bytes(m).decode()
print(flag)
bctf{1_u53d_y0ur_k3y_7h4nk5}

Secret Code (crypto)

先頭の":"の前の数値と末尾の":"の後の数値が1桁で、他は":"区切りにすると2桁になっている。":"区切りではなく、数値:数値で一つのセットとして、"snub_wrestle"を鍵としてXORの復号をする。

#!/usr/bin/env python3
enc = '1:10:d0:10:42:41:34:20:b5:40:03:30:91:c5:e1:e3:d2:a2:72:d1:61:d0:10:e3:a0:43:c1:01:10:b1:b1:b0:b1:40:9'

key = 'snub_wrestle'

flag = ''
for i in range(0, len(enc), 3):
    index = i // 3
    flag += chr(int(enc[i] + enc[i+2], 16) ^ ord(key[index % len(key)]))
print(flag)
bctf{d0n't_lo0k_uP_snub_wResTling}

Starter Buffer (pwn)

BOFで上書きし、flagが0x45454545になるようにする。

$ nc chall.pwnoh.io 13372
Enter your favorite number: EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE
bctf{wHy_WriTe_OveR_mY_V@lUeS}
bctf{wHy_WriTe_OveR_mY_V@lUeS}

Currency Converter (rev)

Bytecode Viewerでデコンパイルする。

public class CurrencyConverter {
   public static String convert_euro(double var0) {
      double var2 = var0 * 0.92D;
      return "Euro: " + var2;
   }

   public static String convert_canada(double var0) {
      double var2 = var0 * 1.36D;
      return "Canadian: " + var2;
   }

   public static String convert_yen(double var0) {
      double var2 = var0 * 145.14D;
      return "Japanese Yen: " + var2;
   }

   private static String flag() {
      return "bctf{o0ps_y0u_fOuNd_mE}";
   }
}
bctf{o0ps_y0u_fOuNd_mE}

8ball (rev)

Ghidraでデコンパイルする。

undefined8 main(int param_1,char **param_2)

{
  int iVar1;
  time_t tVar2;
  char *pcVar3;
  long lVar4;
  char **ppcVar5;
  char **ppcVar6;
  byte bVar7;
  char *local_168 [41];
  int local_1c;
  ulong local_18;
  int local_c;
  
  bVar7 = 0;
  setvbuf(stdout,(char *)0x0,2,0);
  tVar2 = time((time_t *)0x0);
  srand((uint)tVar2);
  if (param_1 != 2) {
    puts("Every question has answer... if you know how to ask");
    printf("Go ahead, ask me anything.\n",*param_2);
                    /* WARNING: Subroutine does not return */
    exit(0);
  }
  local_c = 0;
  iVar1 = strcmp(*param_2,"./magic8ball");
  if (iVar1 == 0) {
    puts("Why, I guess you\'re right... I am magic :D");
    local_c = 1;
  }
  ppcVar5 = &PTR_s_Outlook_not_so_good._004024a0;
  ppcVar6 = local_168;
  for (lVar4 = 0x28; lVar4 != 0; lVar4 = lVar4 + -1) {
    *ppcVar6 = *ppcVar5;
    ppcVar5 = ppcVar5 + (ulong)bVar7 * -2 + 1;
    ppcVar6 = ppcVar6 + (ulong)bVar7 * -2 + 1;
  }
  puts("You asked:");
  msleep(0,500);
  printf("\"%s\"\n",param_2[1]);
  msleep(1,0);
  printf("Hmmm");
  msleep(1,0);
  putchar(0x2e);
  msleep(1,0);
  putchar(0x2e);
  msleep(1,0);
  putchar(0x2e);
  msleep(1,0);
  puts(".");
  msleep(2,0);
  if ((local_c != 0) && (pcVar3 = strstr(param_2[1],"flag"), pcVar3 != (char *)0x0)) {
    puts("Why yes, here is your flag!");
    print_flag();
    return 0;
  }
  local_18 = 0x28;
  iVar1 = rand();
  local_1c = (int)((ulong)(long)iVar1 % local_18);
  puts(local_168[local_1c]);
  return 0;
}

実行ファイル名はmagic8ballである必要がある。また引数に"flag"を指定すれば、フラグが表示される。

$ mv 8ball magic8ball
$ ./magic8ball flag
Why, I guess you're right... I am magic :D
You asked:
"flag"
Hmmm....
Why yes, here is your flag!
bctf{Aw_$hucK$_Y0ur3_m@k1Ng_m3_bLu$h}
bctf{Aw_$hucK$_Y0ur3_m@k1Ng_m3_bLu$h}

Needle in the Wifi Stack (misc)

SSIDbase64文字列が設定された通信がたくさんある。base64デコードしてフラグの形式になるものを探す。

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

packets = rdpcap('frames.pcap')

for p in packets:
    ssid = p[Dot11Elt].info
    flag = b64decode(ssid).decode().rstrip()
    if flag.startswith('bctf{'):
        print(flag)
        break
bctf{tw0_po1nt_4_g33_c0ng3s7i0n}

Electronical (crypto)

https://electronical.chall.pwnoh.io/sourceにアクセスすると、ソースコードが見える。

from Crypto.Cipher import AES
from flask import Flask, request, abort, send_file
import math
import os

app = Flask(__name__)

key = os.urandom(32)
flag = os.environ.get('FLAG', 'bctf{fake_flag_fake_flag_fake_flag_fake_flag}')

cipher = AES.new(key, AES.MODE_ECB)

def encrypt(message: str) -> bytes:
    length = math.ceil(len(message) / 16) * 16
    padded = message.encode().ljust(length, b'\0')
    return cipher.encrypt(padded)

@app.get('/encrypt')
def handle_encrypt():
    param = request.args.get('message')

    if not param:
        return abort(400, "Bad")
    if not isinstance(param, str):
        return abort(400, "Bad")

    return encrypt(param + flag).hex()

@app.get('/source')
def handle_source():
    return send_file(__file__, "text/plain")

@app.get('/')
def handle_home():
    return """
        <style>
            form {
                display: flex;
                flex-direction: column;
                max-width: 20em;
                gap: .5em;
            }

            input {
                padding: .4em;
            }
        </style>
        <form action="/encrypt">
            <h2><i>ELECTRONICAL</i></h2>
            <label for="message">Message to encrypt:</label>
            <input id="message" name="message"></label>
            <input type="submit" value="Submit">
            <a href="/source">Source code</a>
        </form>
    """

if __name__ == "__main__":
    app.run()

入力した文字列にflagを結合して、暗号化している。フラグを1文字ずつはみ出して暗号化することを繰り返し、ブロック単位で一致するものを探す。
まず入力文字列の長さと暗号化文字列の長さからフラグの長さを割り出す。

a
adf7831e7d3f9f5bd937ece1d8e3cd22a709973530e457da18fe8d927853a62089a33d05de133e083a5459b2761be8fa

aaaa
3fd2f9b4088db54a0aa1e9c29ff3f293c7dec898fdc897f269b99d9eab79c263f3397dfd9f5a3e117fcfd5eac0aa9573

aaaaaaaa
d8ae2f37f580ba9cf8d36797cb76967be72ae3a0dcdf98ba892c79d2956ec406b185d30001c7283f5e9a5b48645c557f

aaaaaaaaa
5af8b166b9a98261961930cc1b8275f63ba9a48ad336ad6deffd805bb3cde75b78eef1a18a6b16241e55422018d0ce218e6bb581a88f1575d092acbef1011b8f

以下のイメージで暗号化されている。

0123456789abcdef
aaaaaaaaFFFFFFFF
FFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFF

フラグの長さは40バイト。あとは1文字ずつはみ出させ、ブルートフォースでブロック単位で暗号化データが同じになる平文を探し、フラグを求める。

0123456789abcdef
aaaaaaaaaaaaaaa?
aaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaF
FFFFFFFFFFFFFFFF
FFFFFFFFFFFFFFFF
FFFFFFF000000000
#!/usr/bin/env python3
import requests

base_url = 'https://electronical.chall.pwnoh.io/encrypt?message='

flag = ''
for i in range(40):
    for code in range(32, 127):
        if i < 16:
            pt = 'a' * (15 - i) + flag + chr(code)
        else:
            pt = flag[-15:] + chr(code)
        pt += 'a' * (47 - i)
        print('[+] pt:', pt)
        url = base_url + pt
        r = requests.get(url)
        ct = r.text
        if ct[:32] == ct[96:128]:
            flag += chr(code)
            break

print('[*] flag:', flag)

実行結果は以下の通り。

        :
[+] pt: und_my_c0d3b00kpaaaaaaaa
[+] pt: und_my_c0d3b00kqaaaaaaaa
[+] pt: und_my_c0d3b00kraaaaaaaa
[+] pt: und_my_c0d3b00ksaaaaaaaa
[+] pt: und_my_c0d3b00ktaaaaaaaa
[+] pt: und_my_c0d3b00kuaaaaaaaa
[+] pt: und_my_c0d3b00kvaaaaaaaa
[+] pt: und_my_c0d3b00kwaaaaaaaa
[+] pt: und_my_c0d3b00kxaaaaaaaa
[+] pt: und_my_c0d3b00kyaaaaaaaa
[+] pt: und_my_c0d3b00kzaaaaaaaa
[+] pt: und_my_c0d3b00k{aaaaaaaa
[+] pt: und_my_c0d3b00k|aaaaaaaa
[+] pt: und_my_c0d3b00k}aaaaaaaa
[*] flag: bctf{1_c4n7_b3l13v3_u_f0und_my_c0d3b00k}
bctf{1_c4n7_b3l13v3_u_f0und_my_c0d3b00k}

Real Smooth (crypto)

複数のパスワードがChaCha20で暗号化されている。ChaCha20はストリーム暗号で、鍵ストリームを生成して平文とXORする。
18バイトになるようスペースをパディングして暗号化しているので、右側で共通部分が多い部分はスペースの暗号化と推測できる。
右側からXOR鍵を推測しながら、復号する。
右側から"24f2dc11e8510fc249ad48"をスペースの暗号化と推測して復号すると、11159行目の復号結果は以下のようになる。

b'*******olvidare\n  '

rockyou.txtからこれを検索すると、以下の単語と推測できる。

nuncateolvidare

このことから鍵を割り出し、復号する。

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

with open('database.txt', 'r') as f:
    cts = f.read().splitlines()

#### guess key ####
space_ct = bytes.fromhex('24f2dc11e8510fc249ad48')
key = strxor(space_ct, b' ' * len(space_ct))

#### get key (add later) ####
pt = b'nuncateolvidare\n  '
ct = bytes.fromhex(cts[11158])
key = strxor(pt, ct)
print(key)
exit(0)

i = 0
for ct in cts:
    pt = b'*' * (18 - len(key))
    pt += strxor(key, bytes.fromhex(ct)[-len(key):])
    print(i, ':', pt)
    i += 1

実行結果は以下の通り。

0 : b'frozen\n           '
1 : b'floppy\n           '
2 : b'tequila\n          '
3 : b'jersey\n           '
        :
        :
7104 : b'btcf{w3_d0_4_l177l'
        :
        :
10880 : b'3_kn0wn_pl41n73x7}'
        :
        :
btcf{w3_d0_4_l177l3_kn0wn_pl41n73x7}

Survey (misc)

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

bctf{Th@nk$_F0r_pL@y1ng}