ひょんなことから異常に強いチームに入ってしまい、11月あたりからガチで1問も寄与できておらず、震えています。これは、そんな焦りからか睡眠や育児を時々放置しながら、24時間の競技時間のCTFに挑戦した人の物語。(なお時間内に解けてない。この記事は競技時間を終えた後、一連のおうちタスクをやり終えた後に書いたものです。)

問題概要

ゲームボーイのバイナリと、telnetのアドレスが与えられます。

チームメンバーからは、エミュレータディスアセンブラを紹介されました。ゲームボーイ問は初見でしたが、面白そうだったのでやってみました。

ゲームの内容は、弾を打ってくる敵と、自キャラが存在します。自キャラはBボタンで弾を撃てて、敵を倒すとGOLDが入ります。(1GOLD)SCOREはアイテムを使って増やします。SCOREが上がるほど敵の弾の速度が増えます。倒すとSCOREの分だけGOLDが手に入ります。

アイテムメニューはStartボタンで開けて、GOLDを消費してPOINTに変えられます。変えたPOINTはUSEかTOSS(捨てる?)かを選べて、USEにするとこれがSCOREに変換されます。その他、HINTやLUCKなどありますが、全部必要GOLDが3桁くらいで高く、まじめにやるならかなりの数の敵を倒し続ける必要があります。

こちらは買ったアイテムをUSEするかTOSSするかを決められる画面。上の1行が微妙に変わります。右矢印キーでTOSSの数を選択できます。これは分かりにくかった。

しばらくディスアセンブラやデバッガをにらめっこしてかなりの時間プレイしましたが、これと言ってバグは見つけられません。

一方で、フラグはバイナリの中に埋まっていてどこからも参照されていないこと。printfのような関数が2434にあるのも確認したので、この問題がどうにかしてフラグの文字列のアドレスをprintfに入れる必要があるということは掴みました。

数字に絡んだ処理はこの個数の部分にしかなかったので、ひたすらメニューにこもって試します。するとTOSSの機能で遊んでいるときに増殖バグを見つけました。TOSSに今所持している1つのPOINTをスタックした後、USEしてからTOSSすると、USEで一つ減っているにもかかわらずTOSSでさらに減らして255になります。これを利用して、SCOREを適当に増やす→100POINTをアンロックして個数を増やす、と続けることで、所持金の問題を解決しました。

気になるHINTやLUCKたち。結局Try harderとか言われて煽られるだけであまり役に立ちません。ここも詰まりましたが、少しガチャガチャやっていると、USEとTOSSの画面でカーソルが消えることに気づきます。個数を255で止めると、境界チェックが壊れてアイテムメニューの外側を参照できていました。TOSSを使うことで、ストックの配列の先にあるいくつかの値を好きに減らすことができました。ストック配列の先を確認します。

ストック配列は以下のc3b0から14バイト分がそれぞれうえからのストック数になっています。以下の例だと、1POINTが1ストックあることが分かります。続く 87 57 02 58 ...は、スコアの表示をする関数のアドレスです。ディスアセンブラで出てこなかったのは、起動時に動的にテーブルがここに作られるためでした。調べると、5787は1POINTのUSEをしたときに使われる関数でした。ストック配列のOOBは結果的に関数テーブルのアドレスを好きに減算できるという状態になっています。便利ですね。

ただし制限があって、ストック配列は符号付きで判定されているようで、MSBが立っているとスキップされてしまいます。上記だと87 57 ...のうち87が飛んで、57から始まります。ちょっと使いにくいです。

飛び先を探しますが、都合よくスタックにフラグのアドレスを入れてくれるようなところが見つかりません。幸いメモリ保護はないそうなので、コードを自分で書けば良さそうです。減算バグは面倒な制限がありますが、c3b0からの14バイトは、増殖バグのおかげで好きに値になります。これを使って以下のようにして、フラグを書くコードを埋めます。

11 82 6d:   ld de, 0x6d82 <- フラグのアドレス
d5:         push de
cd 34 24:   call 2434 <- スタックのアドレスの文字列を表示する
18 f5:      jp $ - 11 <- 無限ループ(文字列を消す命令埋められなかったため、続けて何度も表示する必要があった。少し戻りすぎたが上は全部nopなので問題なし。)

実際に配置するとこのようになります。延々連打することになり少々しんどいです。

満足したところで一番の問題は、どのようにipを向けるかです。関数テーブルは全員5xxxで、0までの減算しかできないため、このc3xxというアドレスに向けることはできません。

c1xxあたりからnopでつながっているので、この辺に飛んでくれそうなガジェットを探しますが、良さそうなのが見つかりません。例えば03dbにはc3xxに直接飛ぶガジェットがいましたが、dbという数字を作ることができないため不採用でした。

疲れてデバッガを眺めると、スタックに答えがありました。このデバッガはご丁寧にアドレス高位を上側に配置していたため、使えないなとしばらく思っていて、時間が無駄になっていました。

関数テーブルが呼ばれるときはDEE9にスタックがあるので、+0aするガジェットがあれば終わりです。 grepで探してみると、かなり都合の良いガジェットが5587で見つかります。関数テーブルの先頭のアドレスは5787なので、2回引けばよいです。

これで1POINTのUSEからスタックピボットしてストック配列上のコードを走ってくれます。スクリーンショットだとフラグが平和に表示されますが、実際は無限ループしたので見えない速さで回り続けています。

こちらが本番バージョンで、表示がミスってないならこれがフラグでしょう(読めない)

割といろいろ見て、発見して試して上手くいったので楽しかったです。


(2/2 追記) 動的ロード部含め、ダンプも利用して静的解析をしてみて、バグの箇所を特定します。

やりたいことは、「どう解析すれば解けていたか」です。

できたことは、最初のダンプを見て、文字列処理の関数を特定して、これらの関数がどこからも参照されていないことを確認するところまででした。

やるべき次のステップとしては、デバッガからのメモリダンプの解析です。

ゲームを起動してすぐ、c3bfの関数テーブルが完成したあたりのダンプを使ってghidraで調べます。ghidraはSM83を解釈できるよう拡張を入れてあります。

関数テーブルやストックの配列がc3b0付近にあることは、サーチをかければわかります。関数テーブルはc3bfから始まっているので、これを参照する部分を特定すればよいでしょう。

call 2434を中心に文字列から周辺の処理を追うと、関数テーブルの関数は直接参照されていません。隠されているようです。

この関数テーブルは”new score xxxx”という文字列が現れる関数が並んでいて、これらはアイテムUSEの画面でAボタンを押すと進むはずです。このボタンの処理部分を探します。

少し解析を進めると、30b7jp hlという関数があり、これが複数個所から参照されていることが分かります。HLにはそれぞれ1バイトずつ飛び先のアドレスを格納するため、ディスアセンブラがこのアドレスをアドレスと認識できず、どこに飛ぶのかがわからなくなっていました。entryの0100から追った無限ループを見ると、66c8に主要な処理が固まっていて、先のジャンプ関数で飛んでくることが分かります。

66c8はかなり大きい関数ですが、文字列を中心に、操作と結果を紐づけながら見ていけばよいです。入力についてもここで確認できます。c85a, c859はビットがキーの配置になっていて、6ビットが下位桁から順に右、左、上、下、A、B、?、Startとなっています。

一番肝心なカーソルが消える時の動作については、c7feにアイテムのインデックスがあっておかしな値になることが確認できて、実際にコントロールできるところはTOSSを使って値を減らすことで確認していけます。非ゼロかつMSBが立っていないところをスキップする挙動は6a3eあたりのようです。これはなかなか静的には見つけられないかも。

その他目印になりそうなのは以下です。

  • c7fc: buyメニューかどうか
  • c7fd: useメニューかどうか
  • c7ff: tossの数

カーソルのバグは思ったより丁寧に作りこまれた印象を受けました。次のGB問が解けることを願いつつ。