はじめに
2回目のzer0pts CTF 2021を開催しました。 今年も去年に引き続き、中級者向け〜上級者向け難易度を目標にしました。 正直去年は全完チームが4チームいて簡単だった説が出ていたので、去年より少しwarmup以外の難易度を引き上げたと思います。 なので去年よりも今回全然解けなかったよ〜という人はがっかりせず、鍛錬して来年にご期待ください。 逆に何問か解けたよ〜という人は今後初心者を語らないでください。
1位はSuper Guesser、2位はK-Students、3位はr00timentaryでした。 上位は強いチームが勢揃いですね。 こんな強いチームにいっぱい参加して貰えて本当に嬉しいです。 ところで中国チームが少なかったのでインフラに問題があったかな?と思ったのですが、並行して中国謎プラットフォームCTFが開催されていたっぽいです。 安全客とかでwriteup読むの好きなのでちょっと残念。
開催前
作問
去年のzer0pts CTF終了後からちょくちょく問題を作っては追加していました。 rev担当が少ないので苦手なrev問を作ることになり、「面白いrevって何?」と永遠に悩んでいました。 なんか「どうせptr-yudaiが作るから大丈夫か」って思われてる気がするんですが、気のせい? 暗黒の魔法使いの力でCTF能力が奪われてしまったら手伝えないので、他のメンバーにもrevを作ってほしいです。
あれ、そういえば今年のyoshikingの問題はどこだ......???
作問チェック
他の人が作った問題をいくつかチェックして、非想定解の存在などを探しました。 せっかくなのでチェックした時の記録を載せておきます。
OT or NOT OTは非想定解だったようですが、こっちが想定解になるように変更されました。 3-AESとpure divisionは解けませんでしたが、極端に簡単になるような非想定解はなさそうということは確認しました。 Simple Blogはまったく分からなかったので何も分からなかったです。
今回開催までに誰にも(解法を求めるまでの)作問チェックされなかった問題は、3-AES, pure division, Simple Blog, Tokyo Networkとstopwatch以外のpwnです。 実はpwnってそんなやばい非想定解は生まれにくいからチェックしなくても大丈夫説が有力になってきましたね。 あれ、そういえば今年のyoshikingの問題はどこだ......???
CTFtimeへの登録
CTFtimeでの大会の登録を担当しました。 前回は登録に予想以上に時間がかかってギリギリになったので、今回は少し早めに動きました。 だいたい登録から反映まで2週間弱といったところです。 1度開催したらイベントの登録が無い分早いかなーと思ったらそんなことはなかったです。 遅くてキレてたんですが、話を聞けばCTFtimeって基本1人で運営してるそうです。 頑張って。
日程は去年と同じCTFにかぶせに行くか迷いました*1が、空いている場所があったのでそっちにしました。
開催中
問題追加や質問への対応、アナウンスなどを担当しました。 開催中はtheoremoonが一番大変そうだったので可能なことはしたかったですが、サーバー周りよぐわがらんのであまり手伝えなかったです。 theoremoonはCTF開催ごとにスコアサーバーを新しく作ったり改造したりしていて大変そうです。渋谷駅かな? だんだん機能が増えているのですが、そんなに頑張らんでも良いのよ?
開催中、非想定解が不安な問題はパケットから解法を見ていました。 GuestFS:RCE, nasm kit, goryptとかは見ましたが、だいたい想定解だったので安心しました*2。 OneShotは直前に入れた問題なので覚悟はしていましたが、やはり想定しない解き方が多数ありました*3。 あれ、そういえば今年のyoshikingの問題はどこだ......???
トラブル対応
小さいトラブルがちょっとあったくらいで、終始安定していたと思います。
まず、最初に大量の参加者がinfectedにncしたためmisc用サーバーが死にかけ、急遽PoWを追加しました。 SECCON運営時に作った依存の無いPoWが役に立ちました。 私は普通問題を手元で解いてから初めてncするので、あそこまで多くの人が先にncしてきたのは意外でした。
中盤でDiscordのbotに@everyoneを付けさせる人が1人現れました*4。 1回くらいなら放置でも良かったのですが、残念ながら真似する人が出てしまったのでtheoremoonに修正してもらいました。 無駄な仕事増やしてごめんね。
開催後
スコアボードのアップロード
kosenctfxにはなんとCTFtime compatibleな形式でスコアボードをダウンロードする機能が付いているので、さくっとアップロードしました。 今回の発見ですが、CTFtimeのスコアボードアップローダは間違った形式の時はちゃんと教えてくれます。 前回同様アップロードすると受け付けたメッセージだけ表示してくれます。 未解決問題は、再アップロードが可能かどうかです。
問題の追加
CTFtimeの問題一覧に問題を追加するのですが、ここはyoshikingにやってもらいました。 あれ、そういえば今年のyoshikingの問題はどこだ......???
prizeを送る
今やってます。 8位まで出すのでちゃんとやりとりできるか不安です。 というかCTFヤクザみたいなチームにメール出すのそれだけで怖いんですが。
強いチームのwriteupを見たいので、今回はwriteupを要求することにしました。 といっても上位チームはdicrodとかCTFtimeにペタペタwriteupを貼ってくれているし、webもなんか運営中にログから解法見てたっぽいので必要だったかはよく分かりません。 上位3チームにはこちらが指定した10問、それ以外には自由な3問を要求しています。 あれ、そういえば今年のyoshikingの問題はどこだ......???
作った問題たち
どんな経緯で作ったかとかの記録を書いておきます。
web
GuestFS:AFR (warmup)
ボタンぽちぽちしてたら解けるのでwarmupにしたんですが、解かれなさすぎて泣きました。 自分のワークスペースでファイルを読み書きしたりシンボリックリンクを張ったりできます。 シンボリックリンクを貼るとき、まずリンク先のパスが存在しているかの確認だけします。 リンクを作ったらreadlinkでリンク先を見てワークスペース内かを調べます。
ここで、シンボリックリンクが多重に繋がっている場合を考えます
evil --> middle --> victim
victimを削除するとmiddleの指すリンク先が消滅します。
evil --> middle --> ?
evilに対してシンボリックリンクを作成すると、readlinkはmiddleを指しているのでワークスペース内と判断され、任意のファイルにリンクが貼れます。
evil --> middle --> /var/www/root/???/../../../../../flag
あとはevilを読めば任意のファイルが読めてAFRの出来上がりです。
いやこれ冷静に考えたらweb問じゃねぇ。ごめん。
Baby SQLi (easy)
なんかWani CTFのSQLiで記号を全部バックスラッシュでエスケープするというのがあったのですが、それを解くときに思いついた問題です。 本当はMySQLのバックスラッシュコマンドを問題にしたがったですが、流石にmysql clientに入力を垂れ流すシチュエーションが分からなかったのでSQLiteにしました。 SQLiteではダブルクォートはバックスラッシュでエスケープできないのでSQLi的なものが存在します。 改行文字も入れられるので、sqlite3のクライアント上でだいたい任意の文章が打てる状態になります。 sqlite3 clientではドットから始まるコマンドがあり、「.shell」とかで任意コマンド実行できます。 あとはリバースシェルを張ってindex.htmlを読めばフラグが分かります。
問題文は適当に「Just login as admin」にしたのですが、これかなりミスリーディングでしたね。ごめん。
pwn
Not Beginner's Stack (warmup)
去年はstatic linkのROPで、今年は何にしようかなーと悩んでました。 結局saved rbpを書き換える問題にしました。 人間return address書き換えてるだけでは生きていけんのやで、という事を学んで欲しかったために作った問題です。
なんか最近のカーネルはバイナリのNXが無効でもNXを付ける機構が働くらしく、最近のカーネルを使ってる人たちから多数のご意見が来ました。 ご愁傷様です。
stopwatch (easy)
自明BOFがあるけどcanaryが付いてるのでリークする問題です。 scanfの戻り値を確認していないのと、allocaで最初にrspを調整できるので、タイマーのdouble値と同じアドレスにcanaryの残飯が来るようにすればリークできます。 この問題なんでこんなに解かれてないの?ってなりました。 BOFは自明だし、scanfの挙動についても私も複数のCTFで既に出題している周知の事実なので、組み合わせれば自然とallocまわりの使い方が導き出せるはずです。 ところで問題文のyoshiking scoreは本当にyoshikingが手動で出したスコアです。 みなさんも挑戦してみてください。
safe vector (medium-easy)
ヒープ大好き人間のために作った問題です。 符号付き整数で剰余を取っているので負の値になる事があり、それにより負の方向のインデックスが指定できてしまいます。 負の方向には読み書きできるので、ヒープがちゃがちゃしてunsorted binとかtcacheとか虐めればそのうち解けます。 結局こういう問題の方がいっぱい解かれるの納得いかない。 私がexploit書くときにコメントに「# heap feng shui」と書くことがあれば、それは悪です。
OneShot (medium)
warmup問を作るつもりがwarmupじゃなくなった問です。
本当はshow_flag関数とか用意してGOT overwriteするだけのwarmupにしようと思ったのですが、なんかshow_flagなくても解けるかなーと思って試したら面白いことが発覚してそのまま出した問題です。
すげー綺麗に解けるんですが、みんな想定解で解いてくれたかな?
ほとんどの人がsetbuf使ってました。悲しいね。
gorypt (medium-hard)
なんか昔中国謎CTFでgo言語のpwnが出てそれが非常に面白かったので、その時にノリで作った問題です。 そっちのはstack overflowでしたが、こっちはuse after freeにしました。 あのCTFでapacheのデバッグ法を学んだからGuestFS:RCEも作れたし、かなりお気に入り*5。
deferで設定した関数はpanic時にも呼び出されるため、同じポインタに2回freeが呼ばれるパスが存在します。 あとは偽のEVP_CIPHER構造体を作ってRIPを取ります。 そこからROPするのですが、想定解ではgo言語のランタイムスタックがASLRの影響を受けないことを利用します。 goだとスタックで引数を渡すのでpop gadget探さなくて良くて楽ですね。 AESの関数ポインタといいgo言語の仕組みといい、割とmiscよりな問題になったと思います。
nasm kit (hard)
unicornで任意のアセンブリコードをエミュレートできます。
結論から言うとコード的な脆弱性は(たぶん)存在しません。
システムコールをエミュレートするときにmmapをホスト側で叩いてしまうのですが、ここでMAP_FIXED
を付けると確保済みのアドレスを強制的にunmapしてからmapできます。
したがって、エミュレータの機械語領域だろうがスタックだろうがお構いなしに書き換えられます。
問題はPIEが有効で機械語領域の場所が分からないことですが、こちらもmmapを使います。
mmapでは、第一引数に指定したアドレスから第二引数で指定したサイズの範囲に既にマップ済みのメモリ領域がある場合、第一引数とは別のアドレスを返します。
この「指定アドレスにマップできたかオラクル」を利用して、proc baseを特定できます。
想定解ではこのproc base特定を二分探索でやることでかなり短時間でproc baseが特定できます。
いくらかのチームはO(n)な手法で何回も試していました。
この解き方も想定していましたが、あくまでMAP_FIXED
の部分がメインなのでPoWとかは付けませんでした。
GuestFS:RCE (lunatic)
今回のボス問で、一番最初に作った問題です。
GuestFS:AFRで「存在する」任意のファイルに対する読み書きが得られています。
普通に考えれば/proc/self/maps
からアドレスを読んで/proc/self/mem
をいじれば勝てそうですが、よく考えてみます。
まず、/proc/self/mem
ですが、apacheのように権限を落としているバイナリでは通常読めません。
しかし、今回はヒントに書かれていように/proc/sys/fs/suid_dumpable
が1なので読めます。
次に/proc/self/maps
を読むことを考えるのですが、このファイルはstatの結果が0になるので中身が空と判断されて読めません。
しかし、今回の攻撃対象はPHPでマルチプロセスなので、TOCTOU攻撃ができます。
シンボリックリンクを中身があるファイルに指しておき、statが終わってからfreadまでの間にリンクを/proc/self/maps
に書き換えればファイルの中身が読めます。
ということで、この問題はrace condition問でした。
あとは/proc/self/mem
に普通に書き込めるのですが、テキストモードでファイルを開いているので0x7fより大きい値は書き込めません。
想定解ではASCII ROPなるものを発明しているのですが、libphpの中身を書き換えてセッションIDをコマンドと認識させるという賢い解法も見られました。
rev
infected (warmup)
なんかCUSEをいじめるpwnを考えていたのですがpwnが大量発生していたのでrevになりました。 でもCUSEって誰が何のために使うのかさっぱり分からんかったのでバックドアとして使いました。 初めて使った感想としては、ユーザーランドで動くので変にカーネルを破壊する心配が無かったり、デバッグが楽だったり、馴染み深いシステムコールやlibc関数が使えたりで便利だなぁという感じでした。 不便なのはドキュメントが絶滅危惧種並に少ないのとcdevのseek的な操作が見当たらなかった点です。
それはさておき問題は簡単で、「b4ckd00r:<filepath>:<mode>
」みたいな文字列を/dev/backdoor
に書き込むと、filepathの権限をmodeで設定した値に変更してくれます。
ルートディレクトリの権限を書き換えたいところですが、regular fileのみchmod可能になっています。
想定解は、/etc/passwd
を書き換えてsuでrootになるとか、/init
を書き換えてpoweroff前にやりたい操作をやるとかです。
syscall 777 (easy)
seccompのフィルターがフラグチェッカーになってる感じのやつです。 フィルターは一見複雑ですが、普通に目で読んで手で再現できる程度の簡単なアルゴリズムです。 いい感じにZ3に投げれば解けるようになっています。 遥か昔に作った問題だからよく覚えてないですが、確か連立方程式+論理演算だった気がする。
Not Beginner's Rev (medium-hard)
問題文にも書いてある通り、Not Beginner's Stackと同じ感じの構造になっているので、一般的なデコンパイラは役に立ちません。 MUGIという日立が開発したPRNGを利用しています。 特に思い入れはありません。 なんとなくwikipediaの乱数生成器一覧から名前が面白そうなのを選びました。 麦チョコは好きです。
入力したフラグがPRNGの出力とxorされて出てきます。 鍵は固定ですが、IVが入力に依存します。 しかしMUGIにおけるa配列の生成過程を一部出力しているので、そこからIVを特定できます。 あとは手元の出力を同じIV, KEYでxorすればフラグになります。 たぶんMUGIって分かって解いた人は少ないんじゃないかな。
spotlight (hard)
Emotetか何かで見た難読化と同じノリで作った問題です。 関数に入るときにその関数の機械語が復号され、関数からreturnするときに再び暗号化されます。 今いる場所だけ照らされてるイメージからspotlightという問題名にしました。 復号自体は単純なので難読化も割と簡単に解除できます。
中身の処理ですが、KASUMIという三菱が開発した暗号化方式を利用しています。 なんか大学の暗号の授業でやったのを思い出してなんとなく使いました。 特に思い入れはありません。 あの授業のテスト、「セキュリティホール」が正解で「脆弱性」が不正解だったの許してないからな。
去年のrevは単純なxorとかRC4ばっかり使って一瞬で解かれたのが悲しかったので今年は割と複雑めなアルゴリズムを多用しています。 こっちはKASUMIって分かって解いた人多そう。
crypto
signme (hard)
今年の激やば非想定解問です。 大学の講義でBellcore attackのお勉強をしたので作ってみた問題です。 いやまぁ院の講義だから取ってないんですが、先輩に教えてるときに資料貰って知りました。 なんで後輩が先輩に大学の課題教えてるんだ?
今回はハードウェアのfaultではなく、pwnによりfaultを引き起こすという手法を考えてみました。 pwn担当がrealloc周りのバグを見つけてメモリがどうなるかを調べ、crypto担当が壊れたRSA計算式から素因数分解する方法を考える、という強力プレイ問を目指して作りました。
が、乱数の生成が甘く、なんやかんやでサーバーとの時間合わせを頑張るとシードが予測できるようです :sob:
misc
Tokyo Network (hard)
量子コンピュータ問です。 本当はShorとかGroverとかの典型を出す予定だったのですが、直前のDiceCTFでShorのアルゴリズムが出題されたのでQKDにしました。 さらに言うとQKDで伝達の順番を間違えて盗聴ができる問題にしようと思ったのですが、ただでさえ暗号問が多かったので単にQKDのクライアントを作れば終わる問題にしました。 流石にそれでは自明すぎるので、Shor Codeで符号化して雑音を載せることで多少の難易度を担保しました。 本当はCSS符号にしたかったけど自重しました。
数カ月前に研究室の本を漁ってるときに量子コンピュータの本を見つけて、それで勉強しただけなのでいろいろ不安でしたが、そんなに間違ったことはしてなかったみたいで安心です。 あの黄色い薄い本のシリーズ、すごい分かりやすかった。
ところで最初のビット送るところでShor Code使うのってセキュリティ的にどうなんですかね。 直感的には一部の状態のみ盗聴で破壊して、相手も正しく元の状態を観測できる確率が上がりそうなんですが、よく分からなかったです。 詳しい人がいたら教えてください。
アンケートの結果
毎度アンケートに書かれた意見は参考にしています。 「簡単にしてくれ」とかは応えづらいですが、丁寧に書いてくれてるやつほど対応する可能性が高いです。 「こんな脆弱性の問題をやってみたい」とかを書くと次以降その問題が出る可能性が上がるので、アンケートには是非ちゃんと答えてください。
ところで今回参加者数が予想を遥かに超えており、アンケートの量も異常なので読むのが大変です*6。 結果はだいたいこんな感じ。適当に数字選んでフラグだけ取りに来てる人もおり、やめてくれ、という感じ。
問題の質はこのままでも良さそうです。
スコアサーバーもこのままでも良さそうです。
難易度は、もう少し簡単なものも出した方が良いかな、という感じです。
開催期間は36hで良さそうです。
みんなが好きな問題:
goryptとかGuestFS:RCEみたいに頑張って設計した問題を評価してもらえるのは嬉しいです。
みんなが嫌いな問題:
Not Beginner's Stackは最新のカーネルの挙動を知らなかったために初心者に易しいとは言えなかったかもしれません。次回以降は注意します。 他は目立って嫌われている問題は無いように見えます。 pwnが他のジャンルに比べて嫌われていますね*7。反省します。
その他知見
- PoWはhashcatとか使うと、急遽PoWを追加したときにpow solverを再配布する必要がないので便利
- 「weight votingしてね」とアナウンスすると「too hard -10」おじさんがやってくるので、アナウンスしないのが吉
- forensicsを一切出さなくても意外と怒られない
- Kernel Exploitを出さなくても意外と怒られない
終わりに
今回のCTFを支援してくださったGitHub Security Lab、Tezos Community、Defenitにこの場を借りて感謝いたします。
そして何より貴重な時間を使って参加してくださった皆様もありがとうございます。(日本からも多くのご参加ありがとうございました!) もしかすると去年よりも難しくて解けず、がっかりした方もいるかもしれませんが、アンケートの結果を踏まえて難易度調整もしていくので、来年もよろしくお願いします。
*1:嫌がらせではなく、初心者向けと並列開催なら分散して良さそうという思い。あと面白くないCTFなので参加したくないという思い。
*2:ところでwebは全然分からないのですが、他のCTFでもXSS問って必ずと言っていいほど非想定解が出てませんか?魔のジャンル。
*3:callocにNULLを返させるのが本題なので非想定とまでは言えませんが、1人だけcallocを成功させたままshellまで到達する方法を考えた人がいて驚きました。結局途中で気付いて方針を変えたようですが、アイデアはとても面白かったです。
*4:テスト時に確認したはずなんですが、どっかで仕様が変わったのかも
*5:pwn以外はゴミだったけど
*6:とはいえoptionalの場所も埋めてくれている人は10%くらいかな
*7:理由の欄は全部i hate pwnとかtoo hardでした。