アンチ・逆アセンブルを扱ったCTF問題のwriteup

先日、アンチ・逆アセンブルを扱ったCTF問題を解いたので、そのwriteup。
※このCTFチャレンジは個人的なツテで入手したもので、作問者からCTFのイベント名を公開しないことを条件にWrite Up記載の許可をもらっています。

サマリ

提供されたのは 32ビットのEXEファイル。ファイルはシンプルなキャッチ・ゲーム。方向キー(もしくはshift + K キーで左移動、shift + Mキーで右移動)でカニのキャラクターを操作して、落ちてくるアイテムをキャッチする。食べ物をキャッチする毎にスコアが加算され、爆弾をキャッチする毎にライフが減る。ライフの初期値は5。一度でも食べ物をキャッチし損なうと即ゲームオーバー。またライフが0になってもゲームオーバー。
ゲームをクリアするには食べ物を250回キャッチしなければならないが、アイテムがとてつもない速度で落ちてくるので普通にプレイしてクリアするのは不可能。クリアするにはゲームをデバッグ・モードでプレイするなり、レジスタの値を書き換えるなり、ファイルをパッチするなりしないといけない。クリア条件を満たすと暗号化されたフラグが復号されて、CONGRATULATIONSメッセージとともにフラグが出力される。フラグ復号のコードはアンチ・逆アセンブルが施されている。

解析

※逆アセンブル内の変数名や関数名は一部解析に当たり、わかりやすいようにデフォルトのものから変更している。また解析効率向上のため、あらかじめCFFでファイルのASLRを無効化した。

プログラム冒頭にアンチ・デバッグと思われるコードを発見。GetSystemInfo関数でプロセッサーの数を取得し、プロセッサー数が2以下だった場合、チート行為と見なされプログラムが終了する。


その後も何らかのチート行為のチェックが行われていたが、自分のデバッグ環境では全てのチェックに対して0を返しチェックをすり抜けたため、具体的に何をチェックしているコードだったのかは調べていない。


ランダムな数値を生成するコード。0x401DB2で生成された数値はドロップするアイテムの決定に (食べ物を落とすか爆弾を落とすか)、0x401DC4で生成された数値はアイテムをドロップする際の座標の決定に使われる。


0x401DB2で生成された数値が1から4だった場合、食べ物をドロップする。


0x401DB2で生成された数値が5から8だった場合、爆弾をドロップする。


再度アンチ・デバッグの確認が行われる。PEBのBeingDebuggedフラグを確認する定型文。


ドロップするアイテムと座標が決定すると、アイテムの投下を始める。kbhitとgetch関数でユーザーのキーボード入力を確認している。


スコアの加算処理。


ライフの減算処理。


ライフが0でなく、かつスコアが250に達していた場合、フラグ復号のための関数が呼び出される。フラグはアドレス0x405000にて暗号化された状態でハードコードされている。


フラグ復号処理の詳細を見るため、アドレス0x401A60に飛んだ。

アドレス0x401A94のジャンプ命令でアドレス0x41A96+1、すなわちアドレス0x41A97へ飛ぶのだが、その位置にあるコードをIDAが認識できていない。ファイルにアンチ・逆アセンブルが施されているためである。正しい逆アセンブルを表示させるには、まずアドレス0x41A96をハイライトしてDキーを押し、データ形式に変換させる。すると以下のような表示に切り替わる。


続いてアドレス0x41A97のデータを逆アセンブル形式に変換させる。アドレス0x41A97をハイライトしてCキーを押す。すると以下のような表示に切り替わる。

アドレス0x41A97にmov命令が出現した。直後のジャンプ命令でアドレス0x401AA0+9、すなわちアドレス0x401AA9へ飛ぶのだが、その位置にあるコードをIDAが認識できていない。先ほどと同じ要領でアドレス0x401AA0をハイライトしてDキーを押し、一旦データ形式に変換させる。


続いてアドレス0x401AA9をハイライトしてCキーを押し、逆アセンブル形式に変換させる。するとアドレス0x401AA9にcmp命令が出現した。

アドレス0x401AB1のジャンプ命令でアドレス0x401AB3+1、すなわちアドレス0x401AB4へ飛ぶのだが、その位置にあるコードをIDAが認識できていない。同じ要領でアドレス0x401AB3をハイライトしてDキーを押し、一旦データ形式に変換させる。


続いてアドレス0x401AB4をハイライトしてCキーを押し、逆アセンブル形式に変換させる。するとアドレス0x401AB4にmov命令が出現した。

上記のコードを見ると、アドレス0x401A30にある関数をcallしている。アドレス0x401A30に飛んでみると、ここでもアンチ・逆アセンブルが施されていた。
アドレス0x401A38のジャンプ命令でアドレス0x401A3A+1、すなわちアドレス0x401A3Bへ飛ぶのだが、その位置にあるコードをIDAが認識できていない。


アドレス0x401A3AをハイライトしてDキーを押し、一旦データ形式に変換させる。


アドレス0x401A3BをハイライトしてCキーを押し、逆アセンブル形式に変換させる。すると、アドレス0x401A3BにXOR命令が出現した。


以上で全てのアンチ・逆アセンブルを解除できた。ざっとコードを解析したところ、引数として渡されたフラグと復号鍵を1バイトずつXORさせてフラグを復号する模様。ただし、引数に渡された復号鍵をそのまま使うのではなく、関数0x401A30にてANDしたり、左シフトしたり、右シフトしたり、ORしたりしてコネくり回してから、フラグとXORさせる。


フラグ復号の大まかな処理はわかったので、残るは復号鍵の取得である。デバッガ (x32dbg) で以下のコードを解析した。

食べ物をキャッチすると配列dec_keyにスコアが hex形式で0x00、0x01、0x02...という具合に格納されていく。スコアが250に達すると配列dec_keyには以下の値が格納されることになる。

00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f
10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 
20 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 
30 31 32 33 34 35 36 37 38 39 3a 3b 3c 3d 3e 3f 
40 41 42 43 44 45 46 47 48 49 4a 4b 4c 4d 4e 4f 
50 51 52 53 54 55 56 57 58 59 5a 5b 5c 5d 5e 5f 
60 61 62 63 64 65 66 67 68 69 6a 6b 6c 6d 6e 6f 
70 71 72 73 74 75 76 77 78 79 7a 7b 7c 7d 7e 7f 
80 81 82 83 84 85 86 87 88 89 8a 8b 8c 8d 8e 8f 
90 91 92 93 94 95 96 97 98 99 9a 9b 9c 9d 9e 9f 
a0 a1 a2 a3 a4 a5 a6 a7 a8 a9 aa ab ac ad ae af 
b0 b1 b2 b3 b4 b5 b6 b7 b8 b9 ba bb bc bd be bf 
c0 c1 c2 c3 c4 c5 c6 c7 c8 c9 ca cb cc cd ce cf 
d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df 
e0 e1 e2 e3 e4 e5 e6 e7 e8 e9 ea eb ec ed ee ef 
f0 f1 f2 f3 f4 f5 f6 f7 f8 f9 fa


フラグ復号のために自分が踏んだ手順は以下の通り。

レジスタの値を書き換えてライフ・ポイントのチェックを突破する。


レジスタの値を書き換えてスコアのチェック (スコアが250に達しているか否か)を突破する。


フラグ復号の関数の呼び出し直前にECXレジスタの値を書き換える。

ECXレジスタに上記の正しい鍵データを書き込めば、あとはプログラムがフラグを復号して出力してくれる。

参考
Practical Malware Analysis (by Michael Sikorski and Andrew Honig) P.328 - 349

Leave a Reply

Your email address will not be published. Required fields are marked *