こんにちは、8ayacです🐝

本記事では、2020/10/24(土)に開催したISCCTF 2020の準備と運営を振り返ります。

「ISCCTF 2020」の概要

  • 開催期間
    • 2020/10/24(土) 10:00 ~ 23:00 (JST)
  • 参加者数
    • 登録ユーザ数: 135
    • 獲得スコア1点以上のユーザ数: 84
  • 想定参加者レベル: CTF入門者~初心者
  • 運営: IPFactory
  • 問題アーカイブと公式Writeup: https://github.com/IPFactory/ISCCTF2020

開催の経緯

ISCCTF 2020は、情報科学専門学校のオンライン学園祭における催しの一つとして開催されました。

スケジュール

学園祭の日程に合わせ、CTFは2020/10/24(土)に開催しました。開催が決まってから当日までは6週間程度でした。ただし、正味の期間はもっと短く、不安な部分も残ったままの開催となりました。

CTFの対象者想定レベル

ターゲットは学内の入門者~初心者としました。ABCでいうB問題を解けるレベルのコーディングスキルがあり、Linuxコマンドが扱えれば少なくとも2〜3問は解けるレベルを目指しました。比較として、SECCON beginners CTF 2020よりも少し簡単なレベルを目指していました。

ターゲットは学内の入門者~初心者とした一方で、外部からの参加も認める方針が当初からありました。理由は概ね下記の2点です。

  • プライベート開催では参加者が少数になることが予想される
  • 外部の経験者からフィードバックが欲しい

参加者があまりに少ないと、イベントとして盛り上がりに欠けると考えました。また、ある程度の参加があった方が、発生するトラブルやサーバへの負荷などの知見が多く得られると考えました。

運営メンバーと役割分担

サークルIPFactoryの以下のメンバーで分担して運営しました。皆さん、ご協力いただきありがとうございました。

  • @8ayac: 作問(Web)・レビュー(Pwn/Rev/Misc)・問題サーバ以外のインフラ・文章作成(ルール、ツイート、etc)・プロジェクト管理・学校との調整等
  • @a_zara_n: レビュー(Web)・インフラ(問題サーバ)
  • @Drumato: レビュー(Rev)
  • @iria_piyo: 作問(Forensics)
  • @scgajge12: レビュー(Web)
  • @n01e0: 作問(Pwn全部/Rev全部/Misc)
  • @odgrso: レビュー(Web)
  • @ry0kvn: 作問・レビュー(Forensics)
  • @wa1m3im: 作問(Web)
  • @y0d3n: 作問(Web)
  • @yky4n4g1: 作問(Misc)

開催にあたり参考にしたCTF

下記のCTFを特に参考にしました。

  • TSGCTF
  • InterKosenCTF

これらのCTFを参考にした理由は、直近で開催されたCTFの中で作問・インフラともに評判がよかったためです。

また、開催記やアーカイブが残っていたことも理由の一つです。参加記やアーカイブは、2020年のものに限らず過去のものも遡って参考にさせていただきました。

入門者向けの問題の難易度を知るためにSECCON beginners CTF 2020も参考にしました。

作問について

作問・レビュー時に読んでおくとよい資料

作問・レビュー担当には、下記の資料の一読を勧めました。

レビューのポイント

今回ISCCTF 2020で出題した問題は、全て一回以上のレビューが行われています。

レビューでは次の点を最低限のレビュー項目にしていました。

  • エスパー問でないか
  • 想定外の解法が存在しないか
  • 問題を破壊することができないか
  • サーバーに過度な負荷がかからないか
  • 非本質的な妨害要素がないか
  • 「面白くない」要素が可能な限り排除されているか

これらの項目はicchy氏の『CTFの問題を作るときに気をつけること』とTSGCTFのhakatashi氏の『TSG CTF 開催記』を参考に決めました。項目は広くなりすぎないように、ある程度厳選しました。

PPC問題の扱い

PPC問題のmazeは、作問開始当初はMiscとして出題する予定でした。本番が近づくにつれ、CTFの趣旨に合うかを考え直しました。最終的に、作問者の@yky4n4g1に了承を得て、0点のおまけ問題として出題しました。

問題の評判

競技終盤で公開したSurveyでは、次のような評価でした。

問題の評判(数字が大きいほど高評価)

また、「問題に関する意見があれば教えてください。」という質問に対し、下記の回答をいただきました。

いわゆるエスパー問やクソ問はなく、どのカテゴリの問題もバラエティに富んでいて面白い問題ばかりで楽しむことができました。「CTF入門者向け」と銘打たれていましたが、初心者でも経験者でも楽しむことのできるCTFであったように思います。

インフラ

ISCCTF 2020のインフラにはGCPを使いました。選定基準は特にありませんが、TSGCTFさんの開催記を参考にできるという利点はあり、他のクラウドサービスよりも慣れていたというのは大きかったです。結果的には大きなトラブルもなく運営できました。

スコアサーバ

スコアサーバは、GCE(Google Compute Engine)でUbuntuのVMインスタンスを作成し、その上でCTFdを動かしていました。CTFdの起動には、公式のdocker-compose.ymlをほぼそのまま使いました。ただし、CTFdを稼働させる前にスコアサーバのHTTPS化の作業を行うため、Nginxだけはdockerコンテナとして起動せずUbuntuに予めインストールしたものを使っていました。

複数台構成のロードバランシング(はできなかった)

スコアサーバは終始1台で動いていました。これは単に私の勉強不足ですが、負荷分散は作問作業が落ち着いてからでもサクッと終わらせられるだろうと思っていました。

作問作業が落ち着いてきた頃になり、構成方法を調べたところ、構成にはインスタンステンプレートの作成やCTFdが利用するDBの集約などが必要だとわかりました。

気づいた時点で本番まで残り2日だったため、登録済みのデータを失うリスクを避け、複数台構成への移行は断念しました。

それでも落とさないために

複数台構成での負荷分散は断念しましたが、スコアサーバを落とさない対策は下記のようにしました。

  • インスタンスのマシンタイプをe2-standard-16に変更
  • Nginx/gunicornが起動するworkerプロセスの数を8に変更

Webサービス運用経験もCTFの運営経験も乏しいなかで、うまくやろうという気持ちを早めに捨て、スコアサーバを落とさない気合の策をとった次第ですが、あまり美談ではありません。

負荷試験

スコアサーバを落とさないように施す対策が上記のように落ち着くまでには、何度か負荷試験を行いました。試験の実施には、Apache JMeterを利用しました。例に漏れずこちらも見様見真似でやりました。

次の図は、スコアサーバに対する最終的な負荷試験の結果です。単純なトップページへのGETリクエストであれば、400rps程度の処理能力があることを示しています。

赤枠でrps(requests per second)の部分が囲われた負荷試験の実行結果画面

監視について

サーバへのアクセス・負荷の監視

サーバへのアクセスや負荷の監視は全て、GCPのMonitoring機能を用いて行いました。次のように、ダッシュボードでグラフを追加して、このダッシュボードを監視していました。

GCPのMonitoring機能を利用して作成したダッシュボード

DoSまがいの異常なアクセスの検知方法は、TSGCTFさんの開催記を参考にしました。競技中には、下図のような異常なアクセスも確認できました。

DoSまがいの異常リクエストが来ている箇所で明らかにグラフの線が飛び出ている様子

この異常なアクセスは、ほぼ1時間続いていました。BANの体制は整えていたものの、監視を担当していた私が他の作業をしていて見逃していました。サーバリソースにはかなり余裕があったので、レスポンスが悪くなることはなかったはずですが、反省が残るところです。

問題の死活監視

問題の死活監視も一部自動化していました。本番3日前の突貫クオリティではありますが、ソースコードを公開します。

https://github.com/IPFactory/ISCCTF2020_SolvabilityInspector

SolvabilityInspectorのおおまかな構造は、下記の図の通りです。

SolvabilityInspectorのクラス図

各問題のsolverを追加すれば、定期実行される死活監視に組み込めるようになっています。ログ出力やDiscordへのメッセージ送信を、ベースのInspectorで共通化しています。

指定したDiscordチャンネルに死活監視のログを送る機能も付けました。この機能は、本番前日にInterKosenCTFさんの開催記を読み返している際に思い出し実装しました。

以下の画像は、実際の競技中にDiscordで公開されていたログの様子です。

問題のSolvabilityをテストした際のログ

問題が正常に解けない状態だったときは、Discord上で所定のロールへメンションを飛ばします。さらに、問題が正常に解けない場合の通知には、環境へ接続できないのか、接続できるものの解けない状態なのかを切り分けやすい情報が付属します。競技中には問題が解けない状態になることは一度もなく、この通知機能が活躍することはありませんでした。下図は、試験的に動かしていた際の様子です。

問題が落ちた場合のログの例

一問だけ、疎通確認のみで監視していた問題があります。この問題は、解く過程で問題サーバにファイルを作成する必要があり、作成したファイルを削除できない制約がありました。死活監視のSolverが作成したファイルがヒントにならないよう、疎通確認のみに留めたという事情です。問題の死活監視を行う場合は、Solverによって参加者にヒントを与えてしまわないかは注意した方が良いでしょう。

作業効率化について

GitHub

作問・レビュー作業は、GitHub上で行いました。

GitHub上での問題レビューの様子1 GitHub上での問題レビューの様子2

GitHubを使うことで、下記のようにチーム開発的に作問作業を進められました。

  1. <カテゴリ名>-<問題名>という名前でブランチを切る
  2. 問題ができたらプルリクを作る
  3. プルリクのレビュワーに問題のレビュワーを指定する
  4. レビュワーが問題のレビューをする
    1. レビューで修正点がない場合: 作問者側でmasterブランチにマージして終了
    2. レビューに修正点があった場合: 修正をpushして再レビューを申請し、4に戻る

上記の手順で、masterブランチにあるものが本番で出題する問題という状態をほとんどの段階で保つことができ、わかりやすかったと思います。

また、GitHubでは、リポジトリの権限をユーザ単位で管理できるため、問題の流出対策としても良かったと思います。HackMDの共有機能やGistのSecret Gistなどを用いる話もありましたが、URLが露出した際に問題の内容が漏れるという状況を避けるため、今回はGitHubの方法を選びました。運営メンバーの中には、オンライン授業やオンライン勉強会で自分の作業画面を映す機会が多いメンバーもいたため、必要な対策だったと思います。

また、大会が終わったあとで、作問不参加のサークルメンバーに様子1を見せるのがスムーズという点も利点です。

Googleスプレッドシート

各問題の進捗状況や問題で使うポートは、Google スプレッドシートで管理しました。なお、今回は問題ごとにホストを分けたため、結果的にポートの管理までは不要でした。

作問状況を共有するスプレッドシート

ステータスの変更日時はGASで自動記録されるようにしました。

各問題のflagは「データの入力規則」で、ルールで定めたflagの形式に合うかを検証するようにしました。

入力規則機能で警告が出ている様子

スプレッドシートへの情報の登録は自動化の手が及ばず、スプレッドシートと実際の進捗状況が一致しないことがよくありました。

楽しいCTFになるように

Solveログの提供

InterKosenCTF 2020を真似させていただき、下図のようにDiscord上で誰がどの問題を解いたかを流していました。

問題の正答をDiscord上のbotが祝っている様子

この機能のため、CTFdのソースコードに次のように手を加えました。

diff --git a/CTFd/api/v1/challenges.py b/CTFd/api/v1/challenges.py
index f24e38d..adfa315 100644
--- a/CTFd/api/v1/challenges.py
+++ b/CTFd/api/v1/challenges.py
@@ -1,6 +1,8 @@
+import os
 import datetime
 from typing import List

+import requests
 from flask import abort, render_template, request, url_for
 from flask_restx import Namespace, Resource
 from sqlalchemy.sql import and_
@@ -579,6 +581,10 @@ class ChallengeAttempt(Resource):
                     )
                     clear_standings()

+                discord_webhook_url = os.getenv('DISCORD_WEBHOOK_URL')
+                discord_log_msg = f'Conguratulations! `{user.name}` solved `[{challenge.category}]{challenge.name}` :tada:'
+                requests.post(discord_webhook_url, data={'content': discord_log_msg})
+
                 log(
                     "submissions",
                     "[{date}] {name} submitted {submission} on {challenge_id} with kpm {kpm} [CORRECT]",

Twitterで参加者のツイートをいいね

Twitterでは、ISCCTF 2020に関連するツイートを探し、IPFactoryのアカウントからいいねしていました。公式からのリアクションで「イベントに参加している感」を持ってもらい、少しでも楽しんでもらえれば良いなという思いでした。

ここは手動です。Tweetdeckに検索結果のカラムを作り、そこに現れるツイートをいいねしていました。カラムに現れたユーザをリストにまとめておけば、クエリにかからないツイートも拾えるので、次回からはそうしたいところです。

その他反省点

CTFdの通知機能を途中まで忘れていた

CTFdのNotifications機能で、スコアサーバ上から参加者全体に連絡事項を通知できることを、途中まで忘れていました。そのため、しばらくはアナウンスがDiscord上でのみとなってしまっていました。ここは参加者が必ず使うスコアサーバで通知した上で、Discordでもアナウンスすべきでした。

もし複数のプラットフォームでアナウンスする場合は、自動化した方が良いかもしれません。

まとめ

以前、学内向けの講義をやった際のミニCTF2を除けば、今回が初めてのCTF運営でしたが、とても楽しかったです。

今回作った問題については、作問者Writeup3にまとめました。

注釈

1 競技終了までは、IPFactoryメンバーであっても、ISCCTF 2020運営メンバーでない限りは、CTF開発用リポジトリの内容は見られないようになっていました。