TSG CTF 2020 開催記
※このエントリは TSG Advent Calendar 2020 の1日目の記事です。
博多市です。
今年の7月11日から12日にかけて、東京大学のサークルTSGの主催により、TSG CTF 2020 が開催されました。
- 特設サイト: https://ctf.tsg.ne.jp/
- スコアサーバー (アーカイブ): https://score.ctf.tsg.ne.jp
- 問題ファイルなど一式: https://github.com/tsg-ut/tsgctf2020
昨年5月に行われた TSG CTF に次いで、TSGとして2度目のCTF開催となります。わたくし博多市は今回も全体の進行を司るなど、主催に際して実質的にリーダー的役割を果たしました。
なんとなく開催記を書くのを後回しにしてたんですが、前回の開催記が異様に好評なのもあってゆっくり思い出しながら Advent Calendar のタイミングで出すことにしました。よろしくお願いします。
CTFを開催するという点での大まかな知見は前回の記事に詳しく書いてあるので、今回は新たに得られた知見についてなるべく詳しく書いていこうと思います。
TL;DR
- CTFを主催するのは、大変だけど、楽しい!
前回からのアップデート
開催時期について
前回の開催記にも書きましたが、CTFを計画する上で一番難しいのは日程選びです。我々のような弱小CTFにおいては他の大きなCTFと日程がかぶらないことはより多くの参加者を募るという点で非常に重要です。昨年の TSG CTF は5月上旬に開催しましたが、今年は作問メンバーが忙しくなる時期などを考慮して6月下旬をターゲットにしようと2月頃には決定していました。そこで当時CTFTime上で予定の空いていた6/27から6/28を開催予定日として、CTFTime上で予定登録し他のCTFとかぶらないように早めに予定登録をしていました。
ところが開催予定日1ヶ月前の5月下旬になって事件が発生します。世界的に有名なCTF大会である0CTFが TSG CTF とまるまるかぶる日程で予定を突っ込んできました。これはTSGとしてもかなり望ましくない事態なのでTSG一同大慌てしたのですが、最終的に0CTFやスポンサーともコンタクトを取った結果、TSG CTF の開催を2週間後の他のCTFがやや空いている時間帯に移動させることにしました。日程変更に伴う混乱は多少ありましたが、結果的に参加者数は前回よりも増えたので、移動させたこと自体は悪い判断ではなかったと思います。
ちなみにCTFTime上で開催時間を変更する操作ができないのが懸念点の一つでしたが、連絡したらすぐにデータを修正してくれました。神対応。(ちなみにCTFTimeはSECCONに対しては塩対応だったので結構気分によるんだと思います)
教訓として、どれだけ早めに予定を入れていても、スケジュールがかぶるときはかぶります。というか、近年は開催されるCTFの数自体が増えているのか、他とかぶらない日程を探すことがそもそも難しくなりつつあります。最近はそういうことをあまり気にしすぎるのも良くないのかなと思い始めています。
リリーススケジュールの事前公開
近年、「より良いCTFを設計するためにはどうすればいいのか」という議論がさまざまな場所で盛んです。このような取り組みの一つとして、GoogleCTFの Eduardo Vela さんが編集した CTF Design Guidelines (bit.ly/ctf-design) というドキュメントが昨年12月に公開され、大きな反響を呼びました。このドキュメントにはこれまでに議論されたCTFの設計に関するアドバイスが丁寧かつ網羅的にまとめられており、CTFを主催するすべての人に読んでほしい必読書となっています。
僕も2度目の TSG CTF を開催する上でこのドキュメントの内容には一通り目を通しました。このドキュメントには「問題のリリーススケジュール」というセクションがあり、様々なリリースパターンとそれぞれの長所短所について詳しく記述されています。特にCTFでよくあるパターンである「開始後、徐々に問題をリリースする」か「競技開始と同時にすべての問題を公開する」かの選択は非常に難しく、TSG CTF を開催する上でも非常に悩んだのですが、これに加えて、このドキュメントには以下のような提案がありました。
The ideal case is to be transparent with players and reveal, ahead of time, how many challenges are expected to be released and when. And create such a schedule taking into consideration timezone challenges.
個人的にもこのアイデアは非常に優れていると感じたので、第三の選択肢として、「事前にどの問題をいつリリースするかを公開しておき、そのスケジュール通りに少しずつ問題を公開する」というパターンを選びました。参加者間の公平性という面では「競技開始と同時にすべての問題を公開する」パターンが最も優れていますが、TSG CTF では1人あたりの問題作成数が多く、複数の問題のサポートを同時に行うのが難しいという点を考慮して行いませんでした (あと、個人的に少しずつ問題が増えていくのがCTFの楽しみの一つでもあると思っているのもあります)。
参加者の反応を見る限り、この試みは非常に好評だったので、次回以降もリリーススケジュールの公開は行うと思います。なお僕の知る限りこのパターンを採用したCTFは TSG CTF 以前では知りませんが、直後に開催された HacktivityCon CTF では TSG CTF 2020 と同様に問題のリリーススケジュールが事前に公開されていました。
Beginner問題の整備
前回の反省点として、開催後のフィードバックとして「全体的に問題が難しすぎて全く手がつけられなかった」という感想が多く寄せられたというものがあります。これを受けて今回の TSG CTF では、初心者向けの問題をすべてのジャンルに1問ずつ用意し、大会開始と同時にリリースすることにしました。
この「初心者向けの問題」を TSG CTF で出題するというのが非常に厄介な問題でした。前回の開催記でも述べたとおり、基本的には我々は「面白い問題は難しい」と思っているので、あまりに自明で解いていて面白くない問題は出題したくないという思いがありました。例えば、フラグ文字列をbase64で100回エンコードしたものをcryptoの初心者向け問題として出題するなどのことはしたくありませんでした。よって方針としては、初心者でも解けて、かつ「CTFのパズルとしての面白さ」を体験できる問題を作成しようと考えた結果、「セキュリティやCTFに特有の知識ベースや経験をほぼ必要とせず、プログラマーなら誰でも知っている知識のみで解けるが、解くにはある程度発想の転換やパズルを解く必要がある (=自明ではない) 問題」を「初心者向け問題」として定義し、出題することにしました。
例えば、Miscジャンルの初心者向け問題として用意した "Beginner's Misc" はこの意味で特によくできた問題だと思っています。この問題の配布ファイルは実質以下の3行だけで構成されています。
exploit = input('? ') if eval(b64encode(exploit.encode('UTF-8'))) == math.pi: print(open('flag.txt').read())
ここで登場する技術はUTF-8・Base64・Pythonと、どれもセキュリティ的な文脈でなくても広く使われる基礎的な技術ばかりです。一方でこの問題を解くにはUTF-8やBase64のエンコーディング形式について詳しく調べ、かつ指定された条件を満たすような入力をスクリプトを書いて生成する必要があり、必ずしも「自明でつまらない問題」ではないと思います (ちなみに、Pythonのstr.encodeのデフォルト値はUTF-8なので明示的に指定する必要はないのですが、UTF-8が使用されていることが分かりやすいようわざわざ引数で指定してあります)。
競技プログラミングに詳しい方には、同じ難易度でも「ABCのC問題」ではなく「AGCのA問題」を目指して作ったと言えばわかりやすいでしょうか。とにかく、知識勝負ではなく発想勝負になるような問題をBeginner問題として出題するようにしました。
加えて、TSG内部のCTF初心者からは「CTFの経験がないと、問題の内容から何をすればいいか・問題の目的が自明ではない」というフィードバックがあったため、全てのBeginner問題には、問題の目的がなにか、何を達成できればフラグが取得できるのかについて詳細に記述しました。これらの取り組みによって、CTF初心者であっても「何をすればいいか全くわからない」というようなことがなく、かつ「解くことによってCTFの面白さをわかってもらえる」問題を目指しました。
反響ですが、依然として「難しすぎる」「初心者向けと書いてあるのに全く解けなかった」という反応は多かったです。「初心者向け」と銘打って出題したことで問題に対する期待と実際の問題に乖離が生じてしまったのは反省するべき点かなと思います。特にCTFを「セキュリティ技術を競い合う大会」だと思って TSG CTF に参加しに来た人には「セキュリティに関する知識が要求されず、純粋なパズルのように見える問題」が出題されたことで戸惑わせてしまったのではないかと思われます。
次回どのような方針で作問するかについては未定ですが、できれば今回出題したような「特殊な知識はほぼ必要としないが、ひらめきを用いることで解ける問題」は (初心者向けというラベルを付けるかはともかく) 出題できたらいいなと思っています。
ちなみに、大会中にDiscordで問題の難易度について「beginners < easy < med < hard」という発言をしましたが、これはかなりミスディレクションな発言だったと反省しています。上に述べたとおりBeginners問題の本質は「解くのにCTFへの参加経験を必要としない」ことだと思っているので、難易度とは別軸の基準だと思っています。が、一方でCTF初心者から見たときの敷居の高さという面ではEasy問題よりもBeginners問題のほうが低いということも同時に言えるのではないかと思います。
動的スコアの計算式の見直し
前回の TSG CTF ではCTFdのデフォルトの動的スコアをそのまま使用して失敗してしまったため、今回は動的スコアの計算式を事前によく検討して「TSGが考える最高の動的スコア」になるように調整を行いました。
この過程で、他のいくつかのCTFの動的スコアの計算式を比較して参考にしました。プロットすると以下のようになります。
前回の TSG CTF ではCTFdのデフォルトの計算式を用いたので青色の線が該当します。改めて見ると本当にめちゃくちゃ頭の悪い動きをしていてびっくりしちゃいますね。
TSGでは、これまでのCTFへの参加経験から、以下のような特徴を持った動的スコアの計算式が優れていると考えました。
- 序盤は多くのCTFと似たexponentialな動きをし、10solves程度で約半分の得点になる
- 一方で関数の漸近が遅く、solve数が増えてもゆるやかに得点が減っていく
これらの条件を満たす計算式として、bit.ly/ctf-designで提案されている以下のlogモデルはかなり理想に近いものでした。
が、この計算式には一つ大きな問題があります。特定の収束値を持たないのでsolve数が増えるにつれて無限に得点が下がっていき、問題の得点がマイナスになる可能性があるということです。
そこで、TSGが誇る数学のプロフェッショナル@naan112358に依頼して、上の条件を満たすような計算式を新たに考案してもらいました。その結果が以下の式です。
ここで、は調整のためのパラメーターです。TSG CTF 2020 では となるよう以下のパラメーターを使用しました。
これをプロットしたのが上のグラフの緑の線です。なめらかに動きゆるやかに0に収束していくことがわかります。
この計算式は実際の運用でも問題なく機能し、またこのあと開催された SECCON 2020 でも同じ計算式が採用されました。実際かなりよくできた式だと思うのでぜひ他のCTFでも積極的に採用してもらえればと思います。
Discordの運用体制
今回も TSG CTF の参加者同士のコミュニケーションにはDiscordを採用しました。近年IRCからDiscordに移行するCTFが増えてきているのを感じるので、これはかなり時代にも則った妥当な判断だと思います。
前回のDiscordでは、random, random_ja, general の3チャンネルによる運用でしたが、前回のDiscordの活動状況から日本語でのコミュニケーションが少なかったことを受けて、今回はrandom_jaチャンネルを使用しませんでした。その代わり他のCTFでよく見かけるように web, pwn, crypto などジャンルごとのチャンネルを設けてそれぞれの話題についての会話ができるようにしました。
結果として、コンテスト中はそれほどコミュニケーションが活発化されなかったこと、また問題の内容に抵触するような内容について話される危険が増すことからあまりいい措置ではなかったと思います。特に TSG CTF のような「真面目な」CTFではコンテスト中のDiscordは完全に参加者が運営に質問をする専用の場所として割り切ってしまうのが良いのかなと思います。
一方で、コンテスト終了後に問題の解法について話し合う際には、複数のチャンネルがあったほうが話題が散逸せず話し合いやすいという利点もあります。この問題を解決するため、SECCON 2020 の運営ではあらかじめ問題のジャンルごとにpost-mortemチャンネルを作成しておき、コンテスト終了と同時にアクセスを解禁するという手法を取りました。これはかなりうまく行ったと思っているので、次回があればこの手法を試そうと思っています。
セキュリティへの配慮
前回の開催記にも詳しく書いていますが、CTFのプレイヤーは一般にあらゆる意味で行儀が悪い (褒め言葉です) ので、CTFの問題サーバーを構成するにあたってセキュリティ的な配慮を行うことは通常にも増して重要だと考えています。前回に引き続き、今回も問題全体のデプロイにDockerを用いており、GCPの container-optimized OS を採用することにより全体的なセキュリティの向上を図っています。
加えて、一部の問題ではセキュリティに関して特別な配慮を行う必要がありました。今回出題したstd::vectorという問題では、ユーザーから与えられたプログラムをサンドボックス環境で実行する必要があるため、DinD (Docker in Docker) を用いた構成を採用しています。この構成ではユーザーからの接続を受けるマスタープログラムがDockerを自由に駆動できる必要があるため、外側のDockerコンテナにpriviledgedフラグを付与する必要があり、コンテナが実質的にサンドボックスの役割を果たしません。つまりマスタープログラムに深刻な脆弱性があった場合、即座にサーバー全体を乗っ取られる危険性があるということを意味します。
なので当然マスタープログラムの実装に際しては脆弱性が存在しないか細心のチェックを行いましたが、やはり人間によるチェックにはどうしても限界があります。そこで万一マスタープログラムがハックされた場合のセーフネットとして、この問題をデプロイするサーバーには他の問題を絶対に同居させず、サーバーに置くファイルも必要最小限のものとし、ネットワーク的にも他のサーバーと隔離することによって、被害の範囲が最大でもその問題のフラグの流出に留まるようにしました。
CTFにおいて大会全体の問題の解法やフラグが流出するのは考えうる限り最悪のシチュエーションなので、常にこのような可能性に気を配って大会を運用したいところです。というかそもそもこのようなタイプの問題をより安全に駆動するためのアイデアがないので、もしあればぜひ教えて下さい (?)。
トラブル・反省点
開始直後のアクセス過剰とサーバーダウン
これは前回の TSG CTF ではちゃんと回っていたけど、今回はうまく行かなかったことの一つです。
前回の TSG CTF では、コンテスト開始直後には大量のアクセスが見込まれることから、開始時刻にだけ強いインスタンスのサーバーを用意しておき、その後アクセスが落ち着くと同時にインスタンスを減らしていくという手法を取りました。今回も同じようにコンテスト開始までにインスタンスを増やしておこうという算段を立てていたんですが、あまり重要なタスクだと意識していなかったためコンテスト開始直前までこのことを完全に忘却しており、結局サーバーのスケールアップが間に合っていない状態でコンテスト開始時刻を迎えてしまいました。結果、コンテスト開始と同時に大量のアクセスに耐えきれずサーバーがダウンし、その後サーバーのスケールアップが完了するまでの十数分間程度、参加者がスコアサーバーにアクセスできない状況が続きました。TSG CTF のインフラ担当としてこれらのアクセスに対応できなかったのは深く反省するべき点だと思います。
コンテスト開始時に問題の内容にアクセスできないのはかなり参加者側の体験として良くない (Discordがかなり荒れました) 上に、場合によってはチーム間の公平性に関わってくるため、かなり避けたいことの一つです。
コンテスト開始直後は、やはりというか、通常よりもかなり多いアクセスが一気に流れ込んでくるので、甘く見ずにちゃんとインフラの対応を行おう、というのが教訓だと思います。
問題チェックの甘さとレビューの難しさ
CTFの問題の作問を行うたびに思いますが、やはりCTFの問題のレビューというのは難しいです。特に今回の TSG CTF 2020 では問題に対する非想定解がコンテスト中に多く発見されました。これは、より洗練された問題を作ろうとした場合、想定解より洗練されていない解をすべて通らないように設計しないといけない、という関係性があるため、より優れた問題を作ろうとする上ではどうしても避けて通れないジレンマだと思います。
これを踏まえても、やはりよりよいCTFを設計しようとするならば、決してレビューを軽んじず、1つの問題に対するレビュー人数と時間を増やすことが肝心です。ましてレビュー無しで問題をリリースするなどというのは絶対にありえないことだと思います。
今回ももちろんリリースした問題に対しては一通り他のメンバーによるレビューを行っていますが、作問陣営の人数がそこまで多くなかったこと、また後述するように結局ほとんどの問題が完成したのが開催直前だったというのもあり、決して「徹底したレビュー」とは言えなかったと思います。また例えば今夏のWeb問題 "Notes" における、Cache Probing を用いた非想定解などに関しては、そもそもこれを用いた非想定解の可能性について作問陣で一度は考慮したものの現実的に可能なexploitを構築できなかったという経緯があり、レビューメンバーの技術的な限界が露呈したとも言えると思います。
これらは本当に難しい問題ですし、簡単な解決策は無いと思います。あるとすれば、日頃から多くのCTFに参加してCTFプレイヤーとしても強くなり、非想定解に対する感度を高めるくらいしかないでしょう。チームTSGとしてもやはり精進は続けていくべきだと改めて思わされます。
クローラー系問題のキュー詰まり
CTFのWebジャンルの問題の典型的な構成として、adminアカウントを持ったブラウザに特定のURLを踏ませることによってXSSなどの攻撃を発火させるというものがあります。これをCTFの問題として実現するために、URLを送信することでヘッドレスなブラウザから自動でそのURLにアクセスしてくれるBOTが実装されることが多いです。これを指してクローラーなどと呼ばれることが多いですが、今回の TSG CTF では Notes (と Notes Revenge) という問題でこのクローラーを含む問題の実装を行いました。
クローラーの動作は、実際のブラウザをまるごと動かすだけあって非常に重いので、Notesではキューによる順次実行を行うことにより並列実行が行われないようにしています。が、問題を公開してからしばらく経った時点で、クローラーの処理能力を超えた数のURLが送信されることによりタスク実行が詰まり始め、URLを送信したにもかかわらずかなり長い間クローラーにアクセスされないという問い合わせが多発しました。
いちおう、このようなことが起きたときにちゃんとスケールできるよう、クローラーの実行は複数インスタンスに分散可能なように設計されていますし、URLの送信に対して Proof of Work を設ける準備もできていたのですが、肝心のキューが詰まっているという事実をちゃんと運営側で把握できていなかったため、対応が後手に回る結果となりました。
CTFが限られた時間で問題を解く競技であることを考えると、クローラーのアクセスが遅いことは競技終了までに問題が解けるかどうかという部分に直接的に関わってくるため、このような事態はなるべく避けたいところです。クローラーなど重たい処理を含む部分は、ちゃんとキューの待ちタスク数や負荷状況を監視対象とすることを忘れないようにしたいです。また現在のクローラーの状態がわかるよう、キューに積んだときに現在の待ちタスク数をユーザーに表示する、などの対策も有効だと考えられます。
最近ではFaaSなどを活用したスケーラブルなWebクローラー実装も見られるようなので、次回があればぜひ実装してみたいなと思っています。
リモート開催の難しさ
昨今の情勢は TSG CTF の運営にも大きな影響を与えました。昨年の TSG CTF では運営が一箇所に集まって泊りがけで24時間の運営作業を行いましたが、今年はやはり物理的に集まるのは良くない (加えて、場所が見つからなかった) という事情により、各々の自宅を音声通話でつないでリモートで運営を行いました。
感想ですが、やはりCTFの運営をリモートで行うのは難しいなと改めて感じさせられました。CTFという大会の特性上、参加者からの質問に対して作問者による対応が必要なことが多々あるのですが、オンサイトで集まっていれば寝ていても気軽に (?) 起こしに行けるのですが、リモートだと作問者による対応を確実に乞うことはどうやっても難しいです。CTFだとこういう場合参加者に「作問者が寝てるからちょっと待ってくれ」と言っても許される風潮がありますが、そのせいで最後まで問題が解けなかったりするとやはり体験としては非常に悪いです。これは通常の開催でもそうなのですが、なるべく作問者の他にも問題に対する質問に対応できる人間を増やしておく、具体的には問題の内容と解法をより多くの人間が理解し、サーバーなどのオペレーションに必要な認証情報が適切に共有されているということが、リモート開催に際してはより一層重要になってくるかなと思います。
ちなみに今回の TSG CTF 2020 ではわたくし博多市が終了数時間前に仮眠をとったあと不慮の事故により寝過ごしてしまい、本来僕がやる予定だったCTFの終了に際する告知作業を他の部員が代行して行う羽目になるというトラブルがありました。運営を担当した僕以外のTSGerがめちゃくちゃスマートだったのでこれは概ね恙なく完了した (ありがとう) のですが、WebPushの通知を送信するのに必要なOneSignalのトークンだけ事前に共有するのを忘れていたため片手落ちとなってしまいました。慢心、ダメ、絶対。
質問への対応作業の集約
TSG CTF 2020 では、問題の作問者がわかるとその情報から問題の解法がメタ読みされる危険があるという思想から、なるべく問題の作問者を秘匿するようにしました。それに伴う問題として、Discord上での質問に対する対応に作問者直々に回答することができなくなってしまうため、TSG CTF 2020 ではDiscord上で@tsgctf-adminという質問集約のためのアカウントを設け、問題への質問などは全部そこに送ってもらい、運営者なら誰でもそのDM内容を見て回答できるようにしました。これには作問者を秘匿することができることに加えて以下のような利点があると考えました。
- 参加者は "who is admin for xxx?" → "please dm @xxx" という質問作業を行うことなく一発で問題に対する質問を送ることができる
- 未対応の会話が放置されず、運営全員が気づいて対応することができる
で、実際にやってみた結果ですが、こちらもうまく回ったとは言い難いです。そもそも論として、問題の作問者を隠すことに関してかなり賛否両論でした。問題の作問者は、製品の生産者表示のように参加者に対して問題のクオリティを保証する意味を同時に持っているため、むしろ積極的に表示したほうがいいという意見も多かったです。また未対応の会話が放置されないという点ですが、実際に運用してみると膨大な量のメッセージを捌く必要があったため、対応済み・未対応の管理が難しく、ちゃんと漏れなく対応できていたとは言い難かったです。加えてDiscordが複数アカウントを切り替えるという操作を想定していない、メッセージの既読未読状況がブラウザごとではなくアカウントで共有されてしまうなどのプラットフォームの問題もあり、結果的にかなり運営の負担が増えてしまいました。たぶん次回は廃止すると思います。
これはCTFの伝統とコンフリクトするため僕も導入するのを渋っているのですが、やはり上のような問題を解決するためには競プロのようなclarシステムを設けるほうがいいのかもしれません。が、CTFの場合、参加者の問題を解決するために会話を何往復かさせなければいけないことも多いため、単純な問題でないのも事実です。
通知ボタン
前回に引き続き、今回もスコアサーバーから参加者のブラウザにWebPushで問題追加などの通知を送るようにしています。前回の反省点として「通知の許可ダイアログが出ると反射的に拒否してしまう」というものがあったため、今回は右下に通知ONボタンを表示し、ページロード時ではなく通知を許可したいときにボタンを押して貰う形式にしました (OneSignalの機能を使っています) が、通知をONにした人の数は前回よりも少なかったです。悲しいね。
プロジェクトマネジメント
これはもう半ば諦めているんですが、最終的に出題する問題が出揃ったのは今回もかなり開催直前になってからでした。余裕を持った作問スケジュールというのは存在しうるんでしょうか?
本来の開催予定日だった6月末の時点で、作問状況はこんな感じでした。
hakatashi以外の問題が6問 (うち1問未完成) しかない!
で、今回も直前の追い上げで問題を一気に完成させたわけですが、そうやって完成した問題にはやはり穴が多かったというか、非想定解とかトラブルが多かった印象があります。あまりに当然ですが、問題はやはり早めに完成しておくに越したことはないようです。
いろいろなCTFのオーガナイザーに話を聞いてみると、年1回のCTF開催を目標に、年間を通して問題を考えておき、1年分のストックをCTFで放出するというスタイルを取っているCTFが多いようです。そんなわけで、TSGでも次回の TSG CTF 開催に向けて今から作問作業を行っています⋯⋯が、やっぱり作問状況は芳しくないですね。安定してCTFの問題を供給するのはすごく難しいことだと思います。
完璧なプロジェクトマネジメントなどといったものは存在しない。完璧な絶望が存在しないようにね。
次回について
2021年の TSG CTF についてですが、大きな問題がなければ十中八九開催します。時期などは全く決まっていませんが、今回の反省内容を生かして、より面白くてクオリティの高い大会にできたらいいな〜と思っています。あと、新しい取り組みもなるべく取り入れたいです。
今回の開催記はこんな感じでしょうか。最後まで読んでいただきありがとうございます。また、スポンサー協力を頂いたFlatt社のみなさん、および TSG CTF 2020 に参加していただいたみなさんにも感謝の気持ちが尽きません。TSGだけではCTFは成り立たないんだということを改めて感じさせます。今後ともよろしくお願いします。