こんにちは、KoHの弟です。 SECCON CTFのすべてを、お話します。
revとpwnをすべて作りました。つらい。 writeupはこっち
難易度調整
KoHとの点数バランスを保ち、Jeopardyの問題が支配的になりすぎないように、静的配点を採用しました。 国内・国際の参加チームを確認し、どのチームが解けそうかを作問者に真剣に考えてもらい、次のようにsolve数を予測して点数をつけました。
結果として結構予想は外れてしまい、国内にとっては難しすぎて、国際にとっては簡単すぎる*1ような結果となってしまいました。特にcryptoは国際に解かれすぎていて可哀想でした。
rev
whiskyはもともとeasy pwnくらいで作る予定でしたが、uwsgiのドキュメントがカスすぎて何もできなかったので雑魚revに落ちました。 uwsgi iniを書けばプラグインを動かせることに気づけば、動的解析ですぐ解けると思います。
Paper Houseはハードウェアの問題です。 本当はハードウェアを設計して実機を配布したかったのですが、パーツが届きませんでした。 Amazon.co.jpの表示より大幅に遅れて昨日届きました。いらん。
Check in Abyssは予選に出題する予定でしたが、レビューで難しすぎるのではという声があり本戦に流しました。 ネタとなったSMMは会社のLT会で知ったもので、LT会の直後から問題を設計し始めていました。 事前知識があればそこまで難しくないかと思います。 ユーザー空間にナナチがいるのはすぐ分かりますが、SMMで動かしている暗号処理にも隠れナナチがいるので探してみてください。
pwn
とりあえずwarmupにUnicornのpwnでも置くかぁという適当な気持ちでdiagemuを作りました。 xbegin/xendを使う問題を考えていたところ、xendがUnicornに認識されず、かつ命令サイズが意味不明になったことに気づいて問題にしました。 適当にアセンブリを入れて未定義命令のサイズが異常なことに気づけばすぐ解けるかと思います。
babyescapeはもともとopen_by_handle_at
でescapeする問題でしたが、調べたらShockerという古いdockerの有名なexploitがあり、簡単すぎると判断してkexec_load
を代わりに許可しました。
カーネルドライバをビルドするのが一番面倒で、あとは作業ゲーだからあまり出したくないと思っていましたが、ふるつきに「出しちゃえ」と言われたので出題しました。
結果、多くのチームを「解けそうだけど解けない」状態に陥れて時間を吸収するお邪魔問題になっていたようです。
Dusty Storageはもともとtcacheのmp_.tcache_bin
に関する問題を作りたくて1年くらい経ってしまったので、そろそろ既出になる前に出そう、という焦りで問題化しました。
割と自然で綺麗な問題設計になったと思います。
Conversation Starterは予選に出す予定でしたが、十分な量の問題がすでにあったため本戦に回しました。 しふくろ先生に作問チェックしてもらい、無事非想定解が出たので修正しましたが、それでも非想定解が出ました。 libcのpartial overwriteは出してはいけない。
ブース問
いつか出すかもしれないのでネタは言えませんが、他にもたくさん実機問のアイデアがありました。 その中で一番実装と準備が簡単そうなLAN盗聴問題を出しました。
問題案自体はかなり前から必要な機材とともに提示していましたが、インフラチームとの兼ね合いでLANケーブルが微妙に足りませんでした。 初日必要分だけ前日に作り、1日目にインフラチームの方に買い出しに行っていただきました。
また、CTF for Beginnersのメンバー各位(@hi120ki, @n01e0, @satoki00, @Sz4rny, @task4233, @ushigai_sub)には特にお世話になりました。 両日ともにLANケーブルの作成を手伝ったもらい、無事必要なだけのLANケーブルを完成させられました。 また、当日はブースに拘束される監督役をお願いしました。
本当にありがとうございました。
SECCONは例年King of the HillかAttack & Defense形式でした。 今年はKing of the Hillにすると決まっていたので、pwnのSecLangにあたる問題アイデアだけを思いついていました。 しかし、cryptoやwebのKing of the Hillがいつまで経っても提案されないので、このままではKoHが1問になってしまうという危惧からrevのHeptarchyも作りました。
SecLang
このKoHでは、未知のプログラミング言語仕様と、そのインタプリタ(リモートで試せる)、コンパイラが渡されます。 コンパイラはC言語のように、配列の範囲外参照や型の取り違えなど健全でないコードを許してしまうため、Rustのように健全な機械語を生成できるように直しましょう、という問題です。
この問題は5分ごとに動く5つのランダムなテストケースの合格率と、他のチームのコンパイラを使って任意コード実行できるかかの2点で点数が付きます。 自分のコンパイラが攻撃を受けると、そのラウンドのテストケース結果はすべて無効になります。 したがって、他人のチームを攻撃しつつ、攻撃を防ぐために自分のチームのコンパイラを修正し、アップロードしなくてはなりません。 もはやA&Dですね。
発案から実装まで
実装が死ぬほど大変でした。体感3ヶ月以上使ったと思います。 と思ってログを見たら最初にアイデアを出していたのは8ヶ月前の6月でした。
ここから数ヶ月かけで地道に言語仕様の制定とインタプリタ作成を進めていきます。 途中でつらくなってやめた時期もあったと思います。
つらそうなメッセージを投げるとふるつきが手伝ってくれます*2。神。
当初は下の図のような感じで、SecLangよりも高級な文法が提供されていました。
が、コーナーケースでいろいろバグったり、コンパイルを作る段階になって死にそうになったりで、文法に制約をかけまくりました。
この問題はとにかくゲームバランスの設計が難しかったです。 7時間で攻撃と防御を両立できるよう、適度な量の脆弱性を埋め込んだり、攻撃を易しくするためNXを無効にして機械語をrwxにしたり、難易度を調整しました。 また、防御が難しくなりすぎないように、変数の型情報や配列のサイズ情報は残しつつ、それらを使ってないようなコンパイラを作りました。
さらに、他人がどういう修正を入れたかわからないと攻撃できない一方、パッチを公開すると他人の解法をコピーできてしまいます。 そこで、コンパイル時に生成されるプログラムのアセンブリのみを開示することで、脳死解法コピー解法を潰しました。
非想定解を潰す
参加者視点で考えると、テストケースは通しつつ攻撃は通さないようなハックを思いつこうとします。 次のような問題点を事前に潰しました。
- コンパイルされるプログラムをサンドボックス化する。 →テストケースにseccompをかけることで解決
- seccompの有無を判判別してテストケースのときだけ通るコードを作る。 →テストケースをseccompあり・なしで同時に動かして解決
- 配列や関数ポインタなどの危ない機能、pwnやshellcodeなどの文字列を弾く。 →テストケースでたくさんの機能を使い、exploitっぽい見た目のテストケースをたくさん作って解決
seccompあり・なしで同時に動かすという方針が良く、これによりフラグの削除やネットワーク経由で手元でテストケースとexploitを分類する方法、pythonやrubyなどに変換して安全に実行する方法などが防げました。
また、最初はインタプリタを配布する予定でしたが、レビューのときにふるつきが「コンパイル前にインタプリタで実行し、エラーが起きたらコンパイルしない」という外から見るとかなり攻撃が難しい手法を実装していたため、インタプリタはリモートで動作確認できるのみにとどめました。
サーバーを書く
インタプリタ・コンパイラ作成と理論詰めが終わったら、参加者がスッとコンパイラを修正して、スッと攻撃を通せるプラットフォームを作る必要があります。 問題設計上Dockerがドカドカ乱立するシステムなので、キューが詰まらないようにタイムアウト時間などを調整しました。 とくにテストケースがタイムアウトすると一部のチームだけ点数を落とす大問題が起きてしまうので、テストケースは最優先実行するなど細かい点にも配慮が必要でした。
ワーカーはコンパイル要求・実行要求・テストケース要求・フラグ更新などを担当し、リクエストが来る度に別プロセスを立ち上げて処理するようになっています。
設計上怖かった点は2つあります。
まず、データベースにはすべてsqliteを使っていました。 MySQLなどを使うと環境構築やバックアップが面倒なのと、参加者の少ない本戦ならsqliteで耐えられると判断したからです。
次に、投稿されたソースコードとアセンブル結果はすべてRedisに、コンパイルされたバイナリはすべて /tmp
直下に蓄積されるということです。
Redisは信頼しているので良いですが、1つのディレクトリに理論上数千のファイルが錬成される可能性があるため、そこが非常に不安でした。
ベンチマークする
初KoH作問なので、そんな不安を解消しないまま当日を迎えると不安で体重が255kg減少してしまうので、この問題はベンチマークでテストすることにしました。
具体的には、最大12チームで1チームずつ行動を予測したシミュレータを書き、7時間以上走らせ続けるというテストを合計5回ほど実施しました。
このシミュレーションにより、データベースのロックエラーが発生してテストケースが失敗する場合などが見つかりました。
このような問題は無事当日までに修正でき、SecLangは大きな問題はなく無事終了しました。*3
想定バグ一覧
バグというかexploit可能な仕様上の未定義動作はたくさんあります。
配列の範囲外参照
SecLangは配列をサポートしていますが、コンパイル後バイナリは配列の範囲外参照をチェックしません。
気づくのも直すのも簡単です。 強いチームはほぼほぼ修正していました。
異なる型どうしの演算
SecLangは動的型付けでありながらRustのように演算時の型に厳格で、ドキュメントに書いてあるように基本的に暗黙の型変換は発生しません。 しかし、コンパイラは型チェックをほとんどしないため、関数と整数の足し算や配列と整数の足し算などが可能です。
コンパイラは演算時に左側の型をそのまま計算結果の型に使うので、関数+整数をすると関数ポインタに値を加算して新しい関数ポインタを得られます。 同様に、配列に値を足すと配列のポインタをずらして認識させられます。 このバグは発生すると強くて、配列の範囲外チェックをつけていても、スタック上に作った偽配列構造にポインタを向けることでAAR/AAWが作れます。
気づくのは簡単ですが、すべて正確に直すにはちゃんと整理してから型チェックを書く必要があります。 強いチームはほぼほぼ修正していました。
任意の型の関数としての利用
関数ポインタへcastする演算子はありませんが、call構文はすべて関数として認識してしまいます。
call_expr
に型チェックをつけないと任意の関数が呼べてしまいます。
func main() { shellcode = [9796655895089, 7526411553527181056, 1080863911570857728, 14757395258967641093]; (shellcode + 0x10)(); }
任意の型の配列としての利用
同様に配列のインデックス参照では、すべて配列として認識してしまいます。 今回assemblerがomagicを使っており機械語領域が書き換え可能なので、関数を配列として使って書き込むと、機械語領域を書き換えられます。
func f() { return 1 + 2 + 3; } func main() { a = f; a[2] = 0x622fbf4856f63148; a[3] = 0x545768732f2f6e69; a[4] = 0x050f99583b6a5f; f(); }
配列のUse-after-Free
配列はスタックに確保されるので、配列をreturnするとUse-after-Freeが発生します。
func g() { print("nya\n"); } func f() { x = [g, g, g, g]; return x; } func overwriter(sc) { a = sc + 0x10; b = sc + 0x10; } func main() { shellcode = [9796655895089, 7526411553527181056, 1080863911570857728, 14757395258967641093]; v = f(); overwriter(shellcode); (v[3])(); }
if/whileスコープ中での配列の確保
このバグを使ったチームは今のところ観測していません。
SecLangのドキュメントをよく読むと、ifやwhileの中で変数を定義してはいけない、と書いてあります。 実はこのコンパイラは、配列が登場すると配列用の領域をスタックに動的に確保します。 したがって、ループ中に配列生成が登場すると、ループ回数だけスタックが上に伸びていきます。
一方でスタックフレームのサイズは「関数中で定義された変数のサイズ」で決まるので、ループ中で配列を定義するとリターンアドレスがズレます。
func f(sc) { v = (sc as uint) + 0x10; i = 0; while i < 2 { b = [1, 0xdeadbeef, 3, v]; i = i + 1; } } func main() { sc = [9796655895089, 7526411553527181056, 1080863911570857728, 14757395258967641093]; f(sc); }
関数引数での配列の確保
前述と同じ理由で、関数引数の途中で配列を確保してはいけません。 引数は後ろから順番にpushしますが、その過程で配列が生成されると引数スタックが配列で上書きされます。
こちらも気づいたチームは見つけていませんが、任意の型で任意の値を作れるため非常に強力です。
反省点
とくになし。 テストケースを落としてしまったときの失点が大きいのが微妙でしたが、他にやりようがなかったので仕方ないと思います。
シミュレーション結果から平均獲得点数は1500〜2000点と予測していましたが、大幅にずれることはなかったです。 他のKoHもですが、これくらい取れると〜ということを参加者に教えておくべきだったと思います。
Heptarchy
HeptarchyはrevのKoHです。 7つの言語で作られたバイナリが配られ、それと同じようにコンパイルする元のコードをなるべく高い精度で速く書く必要があります。
アルゴリズム
この問題を作るにあたってもっとも重要なのがバイナリ比較アルゴリズムです。 一般にバイナリは巨大なので、高速かつ良い精度で差分が求められるアルゴリズムが必要になりました。
競プロer各位に聞いたらいろいろ名前が挙がる中、ネコチャンさんの提示してくれたMyers Diffが計算量・制約ともに完璧だったので採用しました。
また、この問題の設計にあたってもう1つ考えたのが、inline assemblerによって簡単に解けないかです。 結局、インラインアセンブリに変換するコードを書く時間がないことや、これが使える言語がC, C++, Dくらいしかないこと、また書いても定数やオフセットなどがずれて良い結果が出ないことなどから、非想定解にはなり得ないと判断しました。
C
まずはwarmup、といってもC言語はIDAのデコンパイル結果をそのまま使えるので何でもいいです。 そこで、C言語の解析対象には、サーバー側でバイナリ比較に使っているMyers Diffをそのまま提示しました。
答え: github.com
これから始まる闘いで重要になるアルゴリズムを知ってもらう回でしたが、みなさん内容を理解せずIDA職人になっていたようで、あまり気づいた人はいなかったようです。
C++
C++特有の機能といえばクラスなので、クラスを使うプログラムを作りました。 最初は継承やvtableが登場する○☓ゲームを作っていましたが、1時間で解析するのは困難と判断してRC4に変更しました。
答え: github.com
注意すべき点は、最後のdeleteを入れ忘れないことや、stringが参照渡しになっていることくらいで、特に難しくなかったかと思います。
Rust
RustではいつもCakeCTF用のゲームを作っていたので、今回もゲームにしました。 1時間で解析できる必要があるので、じゃんけんにしました。
答え: github.com
ポイントとしては、最後にpanic!
ではなくunreachable!
を呼んでいること、そして勝利判定用の減算がwrapping_sub
であることがあります。
wrapping_sub
を使って提出できるチームはいないと予想していましたが、国際だとorganizerとDiceGang、国内だとrokataが正確にreversingできていました。素晴らしい :clap:
Go
この問題は動的解析が重要になっています。 channelやgoroutineを使っていることはデバッグ出力からわかるので、何をしているかを把握する必要があります。
適当に動かして出力される数列を調べると、コラッツ数列(が1になるまでの回数)であると分かります。
shrinker
やexpander
という名前とそのデコンパイル結果から、だいたいやっていることのイメージが付きます。
答え: github.com
差分が大きいとdiffがタイムアウトしてしまいますが、内容がわかればコードは定まるので解析速度ゲーです。
Python
多倍長整数が使える特性を利用したプログラムということで、multivariate-RSAを実装しました。
isPrime
やgetPrime
が独自実装なのが特徴です。
答え: github.com
pycが比較されるため、空ファイルを渡してもdiffは1000オーダーで小さく、どれだけ似せられるかの戦いになります。
D
問題のD言語です。 はじめはDartの出題を考えていましたが、そうとうreversingが大変だったのでやめました。
Dはかなり解析しやすい部類の言語ですが、今回の問題では一番難しかったかもしれません。 というのも、D言語はimportの書き方によってどの関数がバイナリに取り込まれるかが変わります。
また、今回のプログラムにはもう1つトラップ(?)を仕込んでおり、それがptraceです。
プログラム序盤にptraceシステムコールによるデバッガ検知があるのですが、意味を理解して実装しようとすると詰まります。
実は最初のデバッガチェックはinline assemblerで実装されているので、asm
でアセンブリ命令を愚直に再現するのが正解です。
答え: github.com
インラインアセンブリに気づいていたチームは意外と多く、国際ではorganizers, DiceGang, AAA, Cha Shu、国内ではDouble Lariat, chocorusk, rokataがasmを使っていました。
WebAssembly
最後にEmscriptenでCから生成されたwasmを解析するゲームになります。 wasmっぽいプログラムが思いつかなかったのですが、再帰関数を使うプログラムが今までなかったので深さ優先総当りプログラムにしました。
答え: github.com
愚直にwasm→c→elfにしてIDAで解析できますが、関数ポインタを使っている箇所がわかりにくかったかと思います。
反省点
1ヶ月前から作り始めたのでUIの詰めが甘かったです。
タイムアウトと未提出のdiffは変えるべきでしたし、アップロードミスを防ぐためアップロード済みコードをトップページに表示すべきでした。 投稿ミスで数ラウンド落としているチームがあったため、次回からKoHを作る時はfool-proofを徹底しようと思います。 *5
今回も予選と同じ(?)でチームLMTが頑張ってくれました。
前日までスコア送信などのテストができなかったため、KoHの作問者側もインフラ側もスコアの送受信にいくらかトラブルがありました。 結果として最後までスコアボードが正しい結果を表示できず、参加者に不便を強いてしまったのが心残りです。
インフラチームは2019年までの本戦にも関わっており、ある程度の十分な知見があったものと思います。 しかし、私は初KoH開催で不安だったため、心配性が発動して直前にインフラチームにpingしまくりました。 スコアリングシステムについても、不慣れな作問者側が仕組みを理解できておらず、前日に修正するようお願いしました。
当日もたくさん修正を急かしてしまったので、「こいつ面倒だなぁ」と思われたことかと思います。 ごめんなさい。
特にポン酢先生は優しいので頼りまくってしまい、一番迷惑をかけたと思います。 別のでかいCTF運営案件が来そうなので来年は運営に参加できるか分かりませんが、ふるつきがインフラに入ったら全部ふるつきに投げます。(は?)
まず初日、PCの電源を家に忘れたのでデプロイをすべてふるつきに任せました。ありがとう。
初日はひたすらサーバーが生きることと、問題コンセプトが崩壊しないことを祈っていました。 私の問題に関してはシミュレータで難度もテストしていたので、そこまで不安ではありませんでした。 実際大きな問題はほとんど発生せず終了しました。
それからブース問の様子をちょくちょく見に行きました。 ブース問は当日まで作問テストをしていないので(は?)解かれるか不安でしたが、最初に挑戦されたチームが両方ともフラグをとっており安心しました。ありがとう。
2日目の問題はSecLangよりもサーバーの設計が簡単なので、1日目を乗り切った時点でスター状態でした。 実際大きな問題は発生しませんでした。
しかし、kurenaif先生の暗号KoHでややインフラトラブルがありました。 問題が起きていたチームとのやりとりをkurenaif先生に伝え、ひたすら修正してもらう仕事を投げまくりました。 かなり焦っていたので修正や監視を他の人に投げるだけ投げて、私自身は修正を手伝えなかったのが申し訳ないです。
実はCTFだけでなく、配布物の製作にも関わっています。 初期からいろいろ作って配布したいと強く主張しており、SECCON側にはかなり予算を割いていただきました。
優勝盾とメダル
前回私が優勝したときはプレートだけ貰いましたが、記念品としても持って帰るのが大変だった*6ことから、持ち帰りやすい&飾りやすいアイテムを考えました。
トロフィー、優勝旗などの案もありましたが、どちらも鞄に入らなさそうということで、盾とメダルにしました。 間近で見るとこんな感じになっています。
がんばってつくったので、たいせつにしてね。
ステッカー
ステッカーは例年あるので関与しなくて良いと思っていましたが、ステッカーも私が作ることになっていたので作りました。 *7
今回私のステッカーは、CakeCTFステッカーと同じく、屋外でも利用できる強粘着で傷がつきにくい素材で発注しました。 SECCONは例年ステッカーを試行錯誤していたらしく、和紙みたいな素材を使っていた年も印象的です。 パソコンに貼って持ち運ぶ身としては、だんだん傷が付いて消えていくのが悲しいので、車に貼るような丈夫なステッカーを作るようにしています。
デザインはpwn, rev, web, cryotoの計4種類あります。 「ワレモノ注意」のステッカーをテーマにしており、例えばpwnのデザインはこんな感じになっています。
いつも通り、気合でLibreOffice標準図形を組み合わせてデザインしています。 ステッカーは製作経験があったので半日ですべてを終わらせました。
チーム旗やプレートなど
テーブルに掛かっていた旗などは「絶対作りたい」とだけ言って、Adobe Illustratorを持っていないので*8rexさんに丸投げしました。 かっこいい旗を作ってくれました。*9
クリアファイル
クリアファイルはCakeCTFで作った経験があるで「とりあえず作るぞ」とだけ宣言していましたが、無事丸投げしました。 こちらはデザイン経験のあるkurenaifさんに担当してもらい、SECCON x 魔女のオリジナルクリアファイルが大量に錬成されました。 いくつか貰いました。
お弁当の配布
CODEGATEでCTF中に自動的に飲み物とご飯が届くシステムが最高だったので*10、SECCONでも食料を配給するように強く主張しました。 予算会計関連の方に苦しい思いをさせてしまったかもしれませんが、参加者にはめちゃくちゃ好評だったので、配布できて良かったと思います。
私は主張だけして*11実際の注文や配給はすべて任せてしまったので、その点が申し訳ありません&ありがとうございました。
お菓子の配布
実はお菓子の配布は優先度も低かったため断念していました。 しかし、JNSA(?)からの差し入れのような形で、会場には有り余るほど大量のお菓子が設置されました。 また、スポンサーのCISCOからも煎餅が配られ、結果として競技者の糖分補給に活躍しました。
お菓子を差し入れ・配給してくれたみなさん、ありがとうございます。
作れなかったもの
本来作りたかったけど時間や予算の都合で作れなかったものもあります。
- First Blood賞品:KoHにすべての時間が吸収されました。
- Tシャツ:参加者チームのメールのレスポンス速度を見ていると、サイズを聞き出せる時間はないと判断して諦めました。
- オリジナルお菓子:優先度が低かったので時間の都合で消滅しました。
他にもこういうのがあったらいいな〜、というアイデアがあれば教えてください。
当初はドリンクのみ予定されていたようですが、ご厚意でお寿司などが発注されグレードアップされました。 基本的に作問者は質問対応の時間だったため片付けに参加できずごめんなさい。 *12
「Your challenges are f*cking awesome」という有り難いお言葉をrevプロ氏にもらったり、Conversation Starterのsbrkが面白かったと言ってもらったり、いろんな問題のいろんな解き方について共有できて楽しかったです。
アフターアフターパーティとして、秋葉原でお肉を焼きました。おいしかったです。
初めて本戦の運営に関わりましたが、楽しい点も苦しい点もオンラインの数〜数十倍あると感じました。
もう、とにかく疲れました。 KoHを2つ作ることになるとは思わなかったし、Jeopardyに7問+1ブース問作らなければならないとも思わなかったです。 だいたいSecLangが悪い。やつがいなければ仕事量は1/10くらいだったと思います。
まだ疲労を感じるので今はただ、犬と猫をもふもふしておきたいです。
CTF参加者には問題がメインに見えるので作問陣の努力が評価されがちですが、インフラや会場設営、併設イベントなども同じくらい努力していることを知っていただければと思います。 また、スポンサーのご支援がないと、賞金はもちろん、チームバナーや美味しいお弁当*13もなくなってしまいます。 スポンサーに圧倒的感謝しましょう。
次に私の問題に挑戦できるのはACSC 2023です。 本戦(ICC)の参加には国籍と年齢制限がありますが、予選(ACSC)の参加自体は誰でも可能なので是非お楽しみください。