CTFチームPlaid Parliament of Pwningによる"suggestions-for-running-a-ctf"という資料を和訳したものです。誤訳・変な表現・技術的な齟齬等ありましたらPRや@mt_caret等のscryptosのメンバーにリプを飛ばす等してください。
この資料はCTFの設計やCTFを支える技術ノウハウのいくつかを解説したものです。CTFクラスタの意見をまとめることを目指し、作問の上で避けるべき落とし穴を列挙しています。足りないところがあるようでしたらPRお願いします。
また、"The Many Maxims of Maximally Effective CTFs"という「面白いCTF」の何たるかを知る上で役に立つ非常によい資料もあるので参照してください。
参加者の立場を理解するためにもCTFに一杯参加してください。CTFの準備と運営には思うよりずっと時間と労力がかかり大変なので、余裕を持って取り組みましょう。:smile:
ctftime.org等を利用して他のCTFやセキュリティカンファレンスとバッティングしないように日程を調整しましょう。早めにCTF開催の告知を行い参加者がスケジュール調整をする余裕を作りましょう。過去に開催された規模の大きな大会の日程を予め調べておくのもいいかもしれません。多くの場合そういった大会は年次カンファレンスと同時に開催され、あなたが開催したいCTFのタイミングにあわせてリスケジュールすることは普通できません。
学生や社会人が参加しにくくなるため、CTFを平日に開催するのは控えましょう。
開催期間を24か48時間にすると、世界のどこに住んでいる参加者でも同程度の競技にあてられる「日照時間」が得られます。二日より長いと、最後まで持つだけ多くの質の高い問題を用意できないか、用意できたとしても参加者が疲弊してしまうためおすすめできません。
CTFにおいてフラグ形式は些細な要素に思えるかもしれませんが、フラグ形式がよくないとまともなCTFもあっという間に:poop:に早変わりします。
可能な限りフラグ形式は簡単なASCII文字列にし、
<md5(ターゲット名)>_<日時>_<関数のアドレス>
のような複雑怪奇なフォーマットは避けましょう。
こういった形式はわかりにくいだけではなく、一意に定まらないことがありえます。例えば、ターゲット名の最初の文字は大文字か小文字か、日時の区切りにどの記号を使うのか、アドレスはhexなのか、桁数はいくつなのか、"0x"で始まるのか、等々。
また、こういったいくつものパラメータを連結したフラグ形式の場合、参加者は正しいものを見つけたかどうか確信が持てなくなります。答えなのか分からないままフォーマットをいじり続けるのは苦痛でしかありません。(一方、複雑なフラグ形式が必須である良問もないとは言い切れません。そういう形式を採用する際は曖昧さに注意しましょう。)
同様に改行等のわずかな違いで結果が変わってしまうため、何かのハッシュをフラグとするのも避けましょう。ハッシュ結果を当てるのはハッシュ関数の入力を当てるのより難しいわけではないので、どうせならそのままフラグにしてしまいましょう。また、CTFを開催する上でログを見て参加者がどこで詰まってるかを把握しヒントが必要かどうか判断するCTF運営テクニックがありますが、提出されるのがハッシュだけだと何が何だかさっぱりです。
フラグはフラグであることが明らかなものであるべきです。フラグが"Congratulations you win!"や"1234"だと解けたと気付かずに無駄に時間をかけてしまう参加者も出てくるかもしれません。事前に"MyCTF{663d63e8c755f1b4}"や"funny_1337speaK_pHras3"等共通のフォーマットを決めましょう。(フラグの前に"The flag is:"等入れておくとなお分かりやすくなります。)
ブルートフォース可能なフラグは避けましょう。例えばフラグがある都市名の場合、参加者の中には何千個も可能性のある都市名を送信してくる者もいるかもしれません。ただし、ブルートフォースを避けるためにフラグ提出の時にCAPTCHAを要求するのは絶対にやめて下さい。参加者がストレスでハゲます。
フラグの正誤判定は可能な限り寛容にしましょう。大文字小文字を区别せず、常識の範囲内で通るようにしましょう。例えば、"CTF{663d63e8c755f1b4}"、"663D63E8C755F1B4"、 "flag is: 663D63E8C755F1B4"等を全て正解としたり、提出されるフラグ文字列の前後の空白部分を消して比較したりするとよいでしょう。
CTFはゲームですが、情報セキュリティのスキルを競う競技でもあります。CTFに色々とゲーム要素を付加すると面白みは増すかもしれませんが技術を試すことには必ずしも繋がりません。「よいCTF」とは、楽しさとCTFの本質を両立させたものです。参加者にゲーム要素を意識させるのは悪くありませんが、CTFの本質は情報セキュリティ技術の競い合いであることを忘れてはいけません。
したがって、どのような仕組みのCTFにしても情報セキュリティ技術の腕を最も発揮したチームに点数が行きやすいようにしなければいけません。CTFの仕組みは良く考えて設計し、仕組み自体に突くことができる欠陥があったり、問題から注意をそれるような仕組みになったりしないように気をつけましょう。(面白い仕組みが悪いというわけではなく、そういうお楽しみゲーム大会はこの資料のテーマから外れるので、そういったものを開催する上ではこれらの情報は役に立ちません。)
ジョパディ形式のCTFの仕組みの特徴:
- 技術を要する難易度の高い問題には高い得点がつき、トリビアやエスパー問は少ない得点がつく
これによって難しい問題に取り組み技術を学ぶインセンティブができます。細かい点数配分は気にする必要はありません。テスターの方が作者より難易度を正確に把握できるので、一般的にはテスターが点数を決定します。
- "breakthru point"制度
最初に問題を解いた3チームほどに(均衡を破る程度の)追加点を与えることによってフラグを貯めこみ競技の最後ギリギリに一気に提出する戦略をとりにくくなります。最後に突然順位を抜かれると拍子抜けするのでこういった溜め込みをしにくくする仕組みを導入すると便利です。
- 問題の公開状況は全チーム共通
公平性の確保と同時に相性の悪い問題を開いてしまったがため頭打ちになる事を防げます。
- 全てのチームが常に2~3問に取り組めるように調整された問題公開のタイミング
どんなチームでもある特定の問題で詰まってしまうことはあまりにもよくあります。全ての参加者が常に取り組める作業があるようにするのが充実したCTFへの近道です。
- 一度に公開する問題の数が限られている
チームの規模の大きさによるアドバンテージをある程度抑えることができます。いくつかの問題を公開せずにCTFが終わってしまったら、来年のCTFに回せば済む話ですしね。:wink:
人に解いてもらいフィードバックをもらう(テストする)かどうかで💩か良問かが決まるほどテストは重要です。問題が正しく動作しているかいつでも確認できるようにす想定解を試すスクリプト等を用意しておきましょう。また、作問者以外の人に解答を作ってもらい、エスパー以外にも解けることを確認しましょう。全ての問題をテストする時間がないときは、「ブラックボックス」系の問題を優先します。
開催者は可能な限りCTF中常に連絡がつくようにしましょう。IRCチャンネルを作り、解答の提出状況やサーバの動作を監視しましょう。開催者をIRCチャンネルの管理者にして、誰に質問や相談を持ちかければいいのかを明確にしておきましょう。
メールアドレスも公開し、対応しましょう。
公式ツイッターアカウントや告知ページ等上記の他に一つ以上の連絡手段を設け、参加者がCTFに関する重要な情報を逃さないようにしましょう。
問題に変更を加える度に、IRCに加え変更の旨を明記しましょう。問題説明も変更にあわせて更新し、ダウンロードする形式の問題ファイルは変更があったことが明らかなようにファイル名を変更しましょう。
可能な限り元の問題も動かし続けましょう。もしかしたら参加者の誰かが、アップデートで難易度を下げようとしていたpwn問をあと少しで解けるところまで来てるかもしれないですから。
あと少しで解けるところまできたpwn問がアップデートで難易度を下げられたら泣いちゃいます。
既に誰かに解かれた問題の変更は非常に慎重に行いましょう。(これは点数、問題ファイル、説明、ヒント等の変更も含みます)これらの対応はケース・バイ・ケースで対処する必要があるので、適切な判断の元行動して下さい。
一番簡単な方法はスコアサーバからHTTPで配布することです。Dropbox等のクラウドストレージサービスは帯域制限等に引っかかることがあるので避けましょう。
準備が入念な状況で非常に大きなファイルを配布する必要がある場合、gpgやopenssl等のオープンソースなツールで暗号化したファイルのtorrentをCTF前に配布するのが一番です。そして、CTFが始まった時(あるいは中盤)に暗号鍵を公開し参加者がファイルにアクセスできるようにします。
問題が公開される前に問題ファイルがアクセス可能にならないように注意して下さい。ファイルの中身のハッシュをファイル名の最後に入れたり、ウェブサーバのインデックスを無効にすると簡単に流出を防げます。
ファイルをダウンロードする上でログインが必要だとwgetやcurlでのファイルの取得が面倒になるので避けましょう。ファイル名が秘密であるかぎり問題はないはずです。
CTFの他と部分同様、インフラも全体をしっかりとテストしましょう。サイトや問題をクラウドでホストすると必要に応じて素早くインスタンスを作れるので便利です。
本番環境は隅々まで徹底的にテストしましょう!例えば最初のpCTFではpwn問がNXをサポートしないマシンで動いてました。CTF後までこれに気づくことができなかったのはプロダクションインフラがちゃんとテストされていなかったからです。
本番環境においてテストするべき項目の簡単なチェックリスト:
- チーム登録からフラグ提出までの一連の流れ
- フラグ提出にレースコンディションがないこと
- スコアボードとフラグ提出の負荷テスト
- pwn問が走るマシンが必要な防御機構をサポートしているか
- 全ての問題についてフラグまで行き着くか(解答スクリプトを走らせるとフラグ出力まで行き着くか)
- 問題更新の流れ(事故は起こるさ。備えあれば憂いなし)
- 動いているバイナリ/コードが与えられたものと一致しているかどうか
CTFの目的は参加者が学び、楽しむことであることを忘れないでください!問題は解かれなければ意味は無いわけですし、全ての問題が少なくとも1つのチームに解かれているぐらいがちょうどいいですね。問題を解くことによって何かを楽しく学べるように創意工夫を凝らしましょう。参加者はお客様です。(そしてお客様は神様、よって参加者 is GOD Q.E.D.)以下各ジャンルに応じた詳しい説明です。
pwnはLinuxバイナリに限定した話です。
ローカルで解くタイプのpwn問は、ある環境にSSHでログインし、そこにあるsetuid/setgidされたバイナリの脆弱性を突く形となるのが一般的です。チーム同士の作業が干渉しあったり、他のチームに情報が漏れたりすることのないよう、問題の環境上にチームごとのユーザを作るのが手っ取り早いでしょう。環境構築のための簡単なチェックリスト:
- 最新のパッチが当たっていることを確認する
- fork爆弾やその他のリソース枯渇を防ぐため、limits.confを設定する
sysctl -w kernel.dmesg_restrict=1 #これを/etc/sysctl.confに設定する (訳注:これをやると一般ユーザでdmesgコマンドが使えなくなる。つらい。)
mount -o remount,hidepid=2 /proc #procfsを封印
chmod 1733 /tmp /var/tmp /dev/shm #作業中のファイルを他のユーザに見られることを避けるため。ユーザごとにホームディレクトリがある場合はchmod 700だけでOK
- 問題バイナリ用のユーザを作り、問題を/home/problemuser/problemに配置する
chown -R root:root /home/problemuser
chmod 2755 /home/problemuser/problem
touch /home/problemuser/flag
chown root:problemuser /home/problemuser/flag
chmod 440 /home/problemuser/flag
他の全ての問題と同様、十分なテストをしてください。特に、CTFプレーヤーが使うユーザの権限で想定解法が成立すること、flagが作問者の意図しない方法で読めてしまわないこと、flagがroot権限以外で書き換えできないことは確認しておきましょう。
exploit問は、ある環境にSSHでログインし、そこで用意されているカーネルドライバの脆弱性を突く形となるのが一般的です。このタイプの問題は安定した環境をホストするのが難しく、またスケールアウトするのも簡単ではありません。攻撃が失敗した時にOSごと落ちることが多いため、各チームにそれぞれの独立したVMを提供するべきです。チーム数が少なく、十分なシステムリソースを用意できるといった点において、kernel exploit問はCTFの決勝で出題するのが適切でしょう。
環境構築の方法として各チームにそれぞれVMで1つ割り当て1つか複数のESXi上で走らせる、といったものが考えられます。各チームにSSHのcredentialを提供してください。
いくつかのヒントや注意事項:
- VMを20個作る代わりに、1個のVMを作った後に20個のリンクされたクローンを作る
- VMを作った後、各VMにログインして固有の静的IPをそれぞれ設定する
- OSが最新の状態にアップデートされ、公開されている脆弱性に対するパッチが当てられていることを確認する
- ユーザからsudo権限を剥奪する
- /rootにflagを置いた後に
chmod 400 -R /root; chown root:root -R /root
- トラブル対応ができるように、運営用のSSHキーを/root/.ssh/に置き、リモートからのrootログインを有効にする
- アドレスリークが問題の一部である場合を除き、
echo 0 > /proc/sys/kernel/kptr_restrict
で/proc/kallsymsを読めるようにする - oopsによるカーネルパニックを無効にする
echo 0 > /proc/sys/kernel/panic_on_oops
- リモートのVMをハイパーバイザにリブートさせるスクリプトをチームに配布しておく(ゲストOSはカーネルエクスプロイトによって既に応答不能になっている可能性があり、リブートができないかもしれないため)
- 想定解がVM内でも実現可能であり、Read-only memory・SMEP/SMAP等の想定しているセキュリティ機構がVM内でも動作していることを確認する
カーネルエクスプロイトはワクワクするものであるべきです。既にあるエクスプロイトを古いOSに使うだけで解けてしまうような問題は避けるべきでしょう。カーネルエクスプロイトの問題は、いくらでも工夫の施しようがあります。
またbuildrootを使った最小環境のVMを用意する手もあります。この場合は上記に加えてさらにいくつかの注意事項があります。
- VMはネットワークに接続しておく(shellcodeを直接ペーストするのは生き地獄だ)
- VM自体のデバッグを防ぐためにqemuモニターは
-monitor /dev/null
で切っておく - ログイン中のユーザーがCtrl+Cなどを入力してもセッションが切れないように、可能ならcursesモードでのログインシェルを用意しておく
- 大規模な大会などではqemuがspin-upする前に参加者の作業履歴などをキャプチャし保存しておく
- VMは接続の度に常にクリーンな状態の新しいVMに繋がるようにする(比較的小さなVMだったらオーバーヘッドは大したことない)
- KVMなどのハードウェアを使用したVMの高速化はEC2/Azureなどの環境では使用できない事に留意しておき、ハードウェア支援機構なしでも充分な速度が出るか事前にテストしておく
リモートのpwn問では脆弱なネットワークサービスを稼働させます。これを実現するために、xinetdを使うかバイナリにfork/acceptを実装するかの2つの方法がよく使われます。
コネクションごとにスレッドを作る実装は、その影響範囲をちゃんと理解している場合を除いて避けてください。ユーザ同士のexploitが意図的/非意図的を問わず干渉しあい、そびえ立つ:poop:になりかねません。
解く上でlibcリークが必要なとき、問題のバイナリと一緒にlibc.soも配布することを検討してください。libcを特定するスキルはCTFで試すようなものでもないです。
xinetdの代わりに独自のfork/acceptサーバを実装する場合、サービスが殺されたり乗っ取られたりされないように特別の注意を払いましょう。rootでサービスを起動し、forkした後に権限を落とすとよいでしょう。(socket fdが漏れないように注意してください)
この指針に沿ったfork/acceptサーバのサンプルをご覧ください。
xinetdサービス用のxinetd設定ファイルのサンプルはここにあります。
chrootされた環境や制限された環境で問題を動かす場合、その環境が/bin/sh, /bin/bash, /bin/catといった基本的なプログラムを持つことを確認してください。それができない場合は、問題文にその旨を明記してください。せっかくサービスをexploitしたのに、問題がchrootされた環境で動いていることに気付くのに1時間溶かすとかクソです。
リモートシェル奪取型のpwn問の環境を構築するための手順:
- 問題用のユーザを作り、/home/problemuser/problemに問題を配置する
chown -R root:problemuser /home/problemuser
chmod 750 /home/problemuser
touch /home/problemuser/flag
chown root:problemuser /home/problemuser/flag
chmod 440 /home/problemuser/flag
short readに依存することは避けましょう。リモートで攻撃する場合苦行になります。区切り文字がくるまで1バイトずつ読むか、長さを指定してもらった上で、その長さ分文字列を読み出す仕組みにできないか検討してください。同様に、read/recvの戻り値をチェックし、ユーザ入力の取りこぼしがないことを確認してください。
要点を明確にするために、4096バイト読み込むとき間違った例を示します。
char buf[4096];
recv(fd, buf, sizeof(buf)); // recvの戻り値は4096未満になり得るため、この書き方は誤り
正しい例として、fork_accept.cのrecvlen
関数を見てみてください。
フラグは/home/problemuser/flagといったわかりやすい場所に配置してください。サービスを攻略できたのにフラグ探しで時間が溶けると怒りがこみ上げてきます。
正しく動作するpwn問をつくる上で最も重要なことは(理想的には作者以外の1人以上による)テストです。pwnが正しく動作しないと誰かが言う度に競技環境において想定解でフラグを出せるか確認できるようにしましょう。
pwn問で腹が立つこと
- いやな出力形式
出力はシンプルで合理的にパースできる形であるべきです。
- バイナリがNX enabledになっていても、動かしているマシンがNXをサポートしていない
- 馬鹿げたコードや偽のバグ
もし90%のコードが解析者の時間を無駄にするランダムな定数の入力チェック群なら、それは多分びっくりするほどつまらない問題でしょう。そのバグが「ランダムな制約が満たされたときに特に意味もなくこのバッファに飛ぶ」というものなら、もう少し頭をつかうべきかもしれませんね。:smile:
しばしば、pwn問題は特定の防御機構が有効になっていることを必要とします。以下にgccでそれらの有効・無効を切り替える方法を示します。
-fstack-protector
/-fno-stack-protector
: スタックカナリア-D_FORTIFY_SOURCE=2
/-D_FORTIFY_SOURCE=0
(再定義警告を黙らせるためには-U_FORTIFY_SOURCE
を先頭に追加します): Use "*_check
" versions of libc functions likememcpy()
,sprintf()
,read()
, etc. バッファオーバーフローを検知した際に異常終了します。(この防御機構はとても完璧とは言えないため、多くのケースで動かないでしょう)。-fPIE -pie
/-fno-PIE
: 位置独立コード(ASLRをライブラリだけでなくmain binaryまで拡張したもの)。PIEは32bitだと効果がないことに注意してください(PIEを考慮しないエクスプロイトは百、千回に一度うまくいきます)。-fPIC
では-fPIE
と違って、最適化をいくつか無効にすることによって出力されるオブジェクトファイルの命令列を実行ファイルの外部に置けるようにします。-fpie
と-fpic
は使わないようにしましょう。-Wl,-z,relro,-z,now
/?
: Full RELRO(GOTとPLTに書き込まれた上でプログラムのロード中はread-onlyとしてマップされる)。
情報セキュリティが徐々に重視されるようになってきた昨今(「やったぜ。」)、デフォルトでどんどんコンパイル時やランタイム時の防御機構が有効化されつつあります。なので、混乱を避けるための本番の動作環境でのテストは非常に重要です。
問題が大量のリクエストや時間の計測を必要とする時は、リモートからでも問題なく解けることを確認しましょう。同じネットワーク内にある一般的なスクリプト言語やライブラリが入ったシステム上にpwn問を動かすとなおよいかもしれません。
以下の要素は避けるようにしましょう:
- どこからともなく出てきたURL parameterを推測させるもの(?debug=1など)
- ファイル名やディレクトリ名を推測させる(dirbusterでDoSしてくれと頼んでいるようなものだ)
- 認証情報を当てさせるもの
(いい加減どういったものを避けるべきかわかってきましたか?)
素晴らしいWeb問の一例として、全てのソースコードが与えられていてもなお解きがいのある難易度の高い問題が挙げられます。
入力が正しいかどうかをチェックする問題においては、解が一意に定まることがとても重要です。これをやらないのはReversing問で本当にありがちな作問ミスです。もしこれができない場合は、問題文で明示した上で条件を満たす全ての解を受け付けflagを出力するように作ってください。
実際のリバースエンジニアリングの主な目的はマルウェアの解析ですが、悪意のあるマルウェアをReversing問として配布するのはよくないと考えられています。これをする場合、悪意のある動作をするプログラムであることを明示するのを忘れないでください。
基本的に参加者に可能な限り情報を提供しましょう。動作するソースコードで秘密鍵が消されてると理想的。
暗号文のみの問題の場合、次のようにするといいでしょう:
- まともに解析できる程度の暗号文を提供する。(ASCII20文字程度だったらなんとでも解釈できる)
- 推測可能な暗号を使う。古典的な暗号と少量の暗号文では候補を絞るのは非常に難しい。どの暗号が使われているのか判別するのではなく、暗号系を破るのが問題の趣旨であるべきだ。
- 19世紀より後に作られた暗号系を使っている場合、どの暗号を使っているか明記すること。EnigmaとPurple Machineや3DESとGHOSTの判別なんて誰もしたくない。
計算を要するような問題の時、消費者向けの一般的なハードウェアで試してみましょう。競技終盤で解法が合っているが競技時間内に計算が終わらないと気付いた時は死にたくなります。一般論として、最近の消費者向けハードウェアで一時間以内程度が妥当でしょう。それより時間がかかる場合はそれなりの理由が必要です。
こういったものは避けましょう:
- 意味不明なエスパー問:angel:
- zipファイルやstegoプログラムのパスクラック
- stego問
- metasploit・nessus・dirbuster等を走らせるだけで解ける問題(よいCTF問は技術を要するべき)
- 時間がかかるrecon問:anger: