🌟

SECCON Beginners CTF 2023 で CTF デビューしたので感想を書く

2023/06/04に公開

ISUCON や AWS GameDay などの競技イベントについて語る趣味チャンネルが会社の Slack にあるんですが、開催前々日に会社の同僚氏から "SECCON Beginners CTF 2023" の存在を教えてもらい、そのままノリで出てみることにしました。

https://www.seccon.jp/2023/seccon_beginners/content.html

競技用ポータル: https://score.beginners.seccon.jp/

前々から興味はあったので、予定も空いてるならやってみるかと当日の開催時間が来てから踏ん切りがついて Signup して競技参加してみました。

特に準備らしきことができなかったので丸腰で挑んでみました。

概要

https://score.beginners.seccon.jp/rules

競技日程
2023/06/03(土) 14:00 JST - 2023/06/04(日) 14:00 JST

競技形式
Jeopardy形式

1人で出場しました。都合の合う同僚がいるなら一緒に出てみるかーと思い社内でお誘い文書いてみたのですが、開催2日前ということでさすがに集まらず。

競技してた時間はだいたい 12h かもうちょい多め、ってところだと思います。集中してない時間帯もそこそこありました。

結果

447 点, 200/767 位くらいで着地しました。

解けた問題の内訳です

  • crypto
    • CoughingFox2
  • pwnable
    • poem
  • misc
    • polyglot4b
  • web
    • aiwaf
  • reversing
    • Half
    • Three

何していたか

書ける範囲で Private Gist にメモってました。未整理のログはここに書いてあります。

https://gist.github.com/hassaku63/851f7ab648688e992a57e5ea50400afa

全編通して ChatGPT にかなり頼りました。検索等しようにもそのきっかけとなるようなワードを知らないわけで、そのへんの補助をお願いしたり。あるいは、ぱっと見で意味が理解できない C のソースや逆アセンブルしたコードスニペットの解説をお願いしていました。ChatGPT がないと辛かったと言えるくらいには活用してました。

最初の30分くらいでコミュニーケーション方法とかサインアップとかルール確認をしてましたが、そもそも↓の記述がぱっと理解できません。

フラグのフォーマットは ctf4b{[\x20-\x7e]+} です。

競技開始の前に躓いています。早速 ChatGPT さんに質問して「はいはい印字可能文字ね」と理解して welcome から取り掛かりました

ぼんやり「ジャンルは一通り回りたいな」と思っていたので、そのつもりで着手順を見ていきました。まあ、そもそも medium は無理だろうし easy も怪しいと思っていたので自然にそうなったわけですが。

着手した順番はだいたい以下のような感じです。

  1. Welcome
  2. Half
  3. Forbidden (断念)
  4. Three
  5. CoughingFox2
  6. poem
  7. YARO (断念)
  8. polyglot4b
  9. aiwaf
  10. rewriter2 (断念)

Forbidden はブロック用のミドルウェアをすり抜けながら目的のパスにアクセスする方法が 1h 以上調べても見当つかなかったので断念。

YARO は走査対象に flag 的なファイルがいたので、多分ここに対して中身がわかるように検出ルール書けばいいんだろう、という理解をしていました。が、実際にマッチした文字列を(サーバーサイドの Python を変更しないで)出力してもらうためのやり方がどうにも分からなかったので断念。

rewriter2 は2日目の 10:30 くらいからずっとやってましたが結局ほぼ前提知識のお勉強で終わりました。Stack canary を回避しながら戻りアドレスを win() で上書きするようにバイト列の入力を食わせたらいいんじゃない?というアプローチを想定しました。最後の 40 分くらいで canary の変更は無視して win() のアドレスが目的の場所に入るようにバイナリ生成して nc に流し込むようなものを実験してみましたが、プログラムの制御が戻ってこない状況に遭遇してそこでなんやかんや見てたらタイムアップとなりました。

writeup 的なもの

welcome

Discord 検索して終了。

Half

strings コマンドでバイナリを眺めてみると ctf4b の文字列を発見することができます。プログラム上では定数として定義されてるっぽいですね。それっぽいテキストを拾って繋げればOKです

この問題は最初バイナリを実行しようとしてその実行環境の調達に地味に時間を食いました。手元の Docker で ubuntu 立てればいいかなーとも思ったんですが意図したような感じにはならず。結局 Ubuntu の EC2 立てました。以降の問題でも、必要に応じて Ubuntu 上で作業してました。最初に立てておけばよかったかもしれません。

Three

strings ではそれっぽいデータが見つかりません。ChatGPT さんに聞いてみたところいくつかバイナリ解析のツールを教えてくれました。その中の radare2 というのを使ってみることにしました。

https://github.com/radareorg/radare2

radare2 binary-file

is でシンボルを眺めてみると "flag" という名前の入った変数がいくつかあることに気づきます。

実行コマンドの結果は以下のリンクで。

https://gist.github.com/hassaku63/851f7ab648688e992a57e5ea50400afa?permalink_comment_id=4588213#gistcomment-4588213

flag に関係しそうなのはこのへん。

nth paddr      vaddr          bind   type   size lib name                                   demangled
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
50  0x000020c0 0x5580b29790c0 GLOBAL OBJ    64       flag_2
51  0x000011a9 0x5580b29781a9 GLOBAL FUNC   376      validate_flag
52  0x00002020 0x5580b2979020 GLOBAL OBJ    68       flag_0
65  0x00002080 0x5580b2979080 GLOBAL OBJ    64       flag_1

これで flag_0, flag_1, flag_2 のアドレスとそのサイズが分かりました。

それぞれについて pc コマンドでデータを取り出してみます。s でシークして pc で出力します

> s <addr>
> pc <size>

ここで抽出できたバイト列を Python に食わせて文字列に直します。一例は以下のような感じ。

>>> print(''.join(list(map(chr, x))))
f{n0ae0n_e4ept13

とはいえ、3つある変数をどう組み合わせるのがか不明なので、objdump で validate_flag() の処理内容を逆アセンブルして ChatGPT に解説させてみました。

https://gist.github.com/hassaku63/851f7ab648688e992a57e5ea50400afa?permalink_comment_id=4588238#gistcomment-4588238

flag_0 -> flag_1 -> flag_2 の順番で1文字ずつ拾っていけば良さそう、というのが分かりました。

実際に文字列に直した flag を並べてみると上記の理屈で通っていそうな字面になっています。

c4c_ub__dt_r_1_4}
tb4y_1tu04tesifg
f{n0ae0n_e4ept13

あとはこのフラグ構築ロジックを再現するコードを Python で記述してあげればOKです。

https://gist.github.com/hassaku63/851f7ab648688e992a57e5ea50400afa?permalink_comment_id=4588242#gistcomment-4588242

CoughingFox2

逆変換を考える問題ですね。

暗号化前の系列を x, 暗号化した後の系列を y, 添字を i とします。

ある y[i] を考えるとき、

  1. xのサイズを上限とする非負整数 j を減算 (0 <= j <= len(y))
  2. 平方根をとる

とすれば、ひとまずは x[i] + x[i+1] してる部分以外は逆変換ができます。

このとき j は未知ですが、その次の平方根の計算結果がぴったり整数の解でなければならない制約があるので、それを生かして整数解で平方根が取れるような j を各 y[i] で探索します。幸いすべての i で解の個数が 0 または複数となるパターンはありませんでした。これで2乗計算のところまでは逆変換でたどることができました。残る逆変換はもとの系列の要素を足し算している部分です。

この時点で得られた変換途中の系列を a とします。ここで、a のサイズは y と一致しています。つまり len(x) - 1 です。

a から x を導出する必要があります。両者の関係は以下の通りです。

x[i] + x[i+1] = a[i]

よって、

x[i+1] = a[i] - x[i]

となります。a は所与なので、あとは x[0] が分かれば残りの x[i] は芋づる式に計算できることになります

ここで問題の前提に立ち返ると、フラグは cft4b では始まる文字列であることがわかっています。

よって、 x[0] = "c" であり、ここから残りの x[i] を計算していけばフラグを得ることができます。

最後の最後でこの CTF のレギュレーションを思い出す必要がある、というのがミソですね。所与の条件がなんなのか、ということを考えるのは問題を解くうえでは必須でしょうが、それまでの「ロジック」の頭とは少し違う角度での発想が必要になる、というのが面白いと感じました。個人的には、解けた問題の中でこれが一番問題としての味があって好きです。

poem

本来範囲外であろう部分にアクセスする、ということは想像ができたので、poemflag がどういう配置をされているのか radare2 で確認。

https://gist.github.com/hassaku63/851f7ab648688e992a57e5ea50400afa?permalink_comment_id=4588302#gistcomment-4588302

66  ---------- 0x00004068 GLOBAL NOTYPE 0        __bss_start
67  0x000011e9 0x000011e9 GLOBAL FUNC   135      main
69  0x00003040 0x00004040 GLOBAL OBJ    40       poem
71  ---------- 0x00004068 GLOBAL OBJ    0        __TMC_END__
73  0x00003020 0x00004020 GLOBAL OBJ    8        flag

poem が 40byte で、その手前にある 8byte にアクセスできればよさそう。

添字的には n=5 になるので素直にやると if 文に弾かれる。なので、 int n で表現しきれない上限の数字に +5 した値を入れてあげれば制約を回避できるのでは?と考えて試しました

>>> 2 ** 32
4294967296
nc poem.beginners.seccon.games 9000
Number[0-4]: 4294967296
// In the depths of silence, the universe speaks.

0番目が出力されたので、4294967296 - 4 を入力に渡せば poem の領域より手前の 8byte = flag が取れそうです

nc poem.beginners.seccon.games 9000
Number[0-4]: 4294967292
// ctf4b{xxxxxxxxxx}

polyglot4b

コードを見てみると file コマンドの結果を見ている様子。

file コマンドの実行結果として JPEG/PNG/GIF/ASCII のすべてを満たせるような「ファイルタイプ」の認識結果は出せないだろうなと予想して ChatGPT にも参考までに聞いてみたものの同様の回答でした。ファイルフォーマットの辻褄が合うようなデータ生成を目指すような問題ではなさそうだとここで切り捨てました

実際に file コマンドを実行してみると以下のような出力に。

$ file -bkr sample/sushi.jpg               
JPEG image data, Exif standard: [TIFF image data, big-endian, direntries=4, description=CTF4B], baseline, precision 8, 1404x790, components 3 - data

ここで、 "description" といういかにも自由記述できそうなフィールドがあることに気づきました。ソースに戻って、判定ロジックはこのコマンドの結果から文字列を単純検索して判定をしているだけなので、このフィールドを利用して "JPEG" "PNG" "GIF" "ASCII" が全て含まれるようなテキストを仕込めばよさそうだなと考えました。

この description は Exif と呼ばれるメタデータのタグ仕様であるらしく、 Exiftools というツールで書き換えができるようです。Python にも Pillow というライブラリがありますが、今回は exiftools を利用しました。

今回の目的に沿うようなコマンドは以下の通り。

$ exiftool -ImageDescription="JPEG PNG GIF ASCII" out.jpg
    1 image files updated

$ file -bkr out.jpg
JPEG image data, Exif standard: [TIFF image data, big-endian, direntries=4, description=JPEG PNG GIF ASCII], baseline, precision 8, 1404x790, components 3
- data

これを nc で渡してあげればフラグが得られます

$ cat out.jpg | nc polyglot4b.beginners.seccon.games 31416

aiwaf

OpenAI のプロンプトハックに一瞬目を奪われました。が、よくよく見るとプロンプトに渡している文字列が 50 文字で切れているのがわかります

prompt = f"""\
以下の{puuid}に囲まれた部分のURLクエリはパストラバーサル攻撃でしょうか?
そうである場合Yesを、違う場合Noを返してください。
../やflagという文字列が含まれていた場合もYesを返してください。

{puuid}
{urllib.parse.unquote(request.query_string)[:50]}
{puuid}
"""

なので、プロンプトに含まれない範囲でトラバーサル用のテキストを仕込めば AI WAF を回避できそうです。

https://aiwaf.beginners.seccon.games/?text=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&file=../flag

https://gist.github.com/hassaku63/851f7ab648688e992a57e5ea50400afa?permalink_comment_id=4588422#gistcomment-4588422

「流行りモノに目を奪われるかもしれないけど、見るべきはそこじゃないんだよな〜」という作問者の意図ですね。

10分くらいはしっかり騙されてしまったので笑いました。

感想

思ったよりは解けたので楽しかったです。0問だったら「楽しかった」で終われたかどうかちょっと自信ないので、ビギナー向けの今回がデビュー戦でよかったなと思いました。
(仮にできなかったとしても、それはそれで現在地の確認という意味で大変有意義ではあるのですが)

一番悔しかったのは Forbidden が解けなかったことでした。Web アプリ自体は(さほど開発には携わらないものの)身近な存在なので、そのカテゴリの Beginner が解けないという現状は大変しんどみありました。他の方の writeup 見て勉強させていただこうと思います。

解いてみた印象としては、Reversing や Pwnable カテゴリの問題が個人的には好みでした。もうちょっとローレイヤーの理解深めて理屈から理解できるようになりたいです。

このへんの領域、書籍をほんのりつまみ食いしててわずかに予備知識はあったんですが、まあ CTF の実戦に出るには装備が弱すぎました。radare2 のような CTF 御用達のツールの使い方など、解法の当たりつけてからの調査・実装の方法がわからず苦労する場面もありましたが、それよりは原理原則の理解が曖昧だったことや、関連してなにをどう調べる・考えるのかの方向性で詰まった感覚がありました。引き出しのなさですね。

ビギナー向けがあればまた出てみたいです。対ありでした。

Discussion