10: CheatEngineとGhidraでゲームを解析する

概要

一般に実行ファイルを解析する際、デバッガに加えて、機械語 => アセンブリ (ディスアセンブル)機械語 => Cみたいなコード (デコンパイル) といった事をしてくれるツール も使って解析する (こういうビルド済み実行ファイルを逆アセンブル等して解析する事をReverseEngineering(RE)と言う)。
前回、ダメージを受ける処理を無効化したが、自分が敵にダメージを与える命令も同じ命令であったため、 敵にダメージを与えられなくなってしまった。そこで、プレイヤーと敵を区別したい訳だが、その命令周辺のアセンブリだけを見てもどこに何の情報が入っているのかよくわからない(もっと言えばアセンブリで読みたくない)。
よって、今回はGhidraというツールを使い、前回特定したダメージを受ける命令の周辺のコードの意味を理解することを目的にやっていく。
より具体的には、プレイヤーや敵の構造体 と その構造体のメンバ を解析していく。
この記事は前回の続きです。

ダメージを受けるコードのデコンパイルの整理

前回、 ダメージを受ける処理は GameLogic.Actor::Damage+E5 またの表現で GameLogic+0x20C5 にある事が分かったので、まずはGameLogic.dllをGhidraで読み込む。

GameLogic.dllは、PwnAdventure3/Binaries/Win32/GameLogic.dllにある。
また、Ghidraはここからダウンロードできる。 Ghidraを起動した後、新規プロジェクトを作成し(自分はfor-demoという名前で作成)、下図のようにGameLogic.dllと、それと依存関係にあるDLLをインポートする。 OKを押すと、依存関係のあるDLLと共にインポートされ、サマリー情報が表示されるので、OK押した後、実際にGameLogic.dllをダブルクリックする。 すると、GameLogic.dll has not been analyzed. Would you like to analyze it now?と出てくるので、Yes > Analyzeで解析させましょう。 このAnalyzeは実行ファイルによっては結構時間がかかりますが、これに関しては数分で終わるはずです。

それでは早速解析していきます。
GameLogic.dllを読み込んで、Analyzeが無事終了したら、以下のような画面になっているかと思いますが、下図の赤く囲った0x10000000が、 このGhidra上でのGameLogic.dllのベースアドレスとなっています。 Ghidraの設定によってこの値が違ったりするかもしれないですが、ベースアドレスはとりあえず一番上にスクロールした時の一番上のアドレスと思っておけば良いです。
ダメージを受ける処理へのオフセットは、0x20C5だったので、ベースアドレスと足して、0x100020C5まで行ってみましょう (上のツールバーから、Navigation > Go To ...を選んでアドレスを入力すれば簡単に飛べます)。
すると、まさに前回見たダメージを受ける処理がある事がわかります。 右側のウィンドウのデコンパイルウィンドウの方が分かりやすいので右側を見ていきます。上図でハイライトされてる部分がダメージを受けるコードなので、 ざっくりと体力 = 体力 - ダメージというコードだと言う風にわかります。それなので、param_3はダメージの値だとわかるので、 この変数名を右クリック > Rename Variabledamage とかの名前に変えておきましょう。

続いて、this->m_statesの方は体力のはずですが、ここは名前的に体力という感じがしません。
このような場合、デコンパイル結果が間違っている可能性があります。これは別にGhidra自体にバグがあるとかではなく、そもそもデコンパイルというものが 間違いありきでコードを復元しているというのが理由です。なぜかと言うと、プログラムをコンパイルする時、実行ファイルには変数名とかの情報は残らないですし、構造体のサイズとかも どこかに書いてあるわけではないので、自動でGhidraが推測しないといけないからです。Reverse Engineeringでは、こういう小さなデコンパイルの間違いを 修正していくというのも大事な作業の一つになります。

this->m_stateとなっているという事は、thisは構造体という事です。そして、この関数の第一引数のデコンパイル結果を見れば、 Actor *thisとなっているので、Actorという型の構造体である事がわかります。構造体のメンバとかを編集できる構造体エディタを開くために、 this->m_statem_stateを右クリックして、Edit Data Typeを押しましょう。
上図の構造体エディタによると、m_stateメンバの一個前のメンバにm_healthという明らかこっちだよなというメンバがあります。 this->m_stateとなっているという事は、オフセットを見るとthis+48を指しているという事ですが、this+48が4バイト手前のm_health を指すようにするためには、下図のように構造体の最初の方に余計に4バイト追加すれば良いことがわかります。 これで、+ボタンの左側にある 保存ボタン を押すると、以下のようにデコンパイルが切り替わります。 こうすることで、正しいデコンパイル結果になり、ちゃんとダメージを減らすコードなんだなというのがわかります。
また、ちょっと下を見れば、体力が0になると(this->m_health<1)、スポーンの処理みたいなのがある事がわかります。 this+0x78とあるので、どうやらActor構造体のオフセット120(0x78)にスポーン関連の処理を行うコードのアドレスが入っているそうですが、 ここは関係ないのでこのままにしておきます。

ディスアセンブルとデコンパイルとの比較

基本解析はデコンパイルを見ればいいのですが、少しだけ重要な点があるのでディスアセンブルの方も見てみます。
ダメージを受ける命令は、 のようになっていますが、0x30 (48)を見てわかる通り、EDIがデコンパイルで言うthis、 すなわち、Actor構造体のインスタンスのアドレスを持っていることがわかります。
Ghidraで解析中にこれを知る事に特に意味はありません。しかし、CheatEngineではアセンブラしか見れないわけですが、ダメージを受けるこの処理をCheatEngineで見た際、[EDI+0x30]のような表記を見たら、 EDIはおそらく体力とかをメンバに持つプレイヤーの構造体とかなんだろうなと予想が付くようにして置く事はとても大事になります。

プレイヤーと敵を識別する情報の解析

Actor構造体の全てのメンバは分かっていないですが、現状見えているメンバで、どの情報がプレイヤーと敵の識別に使えそうかを見てみます。
この中だと、m_blueprintNameに名前みたいなのが入ってそうなので、使えそうだとわかります。
しかし、実際にじゃあプレイヤーだとm_blueprintNameにどんな値が入り、敵だとm_blueprintNameにどんな値が入るのかは、 実際にゲームをやっている時にここを覗かないとわかりません(Ghidraはプログラムを実行しているわけではない)。よって、ここでCheatEngineの出番です。

どうやってこの値を覗くかは、以下のようにしていきます。
  1. ダメージを受ける命令にブレークポイントを打っておく
  2. ゲーム上で敵からダメージを受けると上記のブレークポイントで止まる
  3. ブレークポイントで止まっていると、その時点でのレジスタの値が見れるので、EDIレジスタの値をメモする
  4. m_blueprintNameのオフセットはGhidraの構造体エディタを見れば、24 (0x18)であることがわかるので、メモしたEDIのアドレスに0x18を足す
  5. 上記で求めたアドレスをCheatEngineで覗き、値を確認する
では実際に1からやってみると、まずダメージを受ける命令の所に飛び、以下のようにしてハードウェアブレークポイントを打つ。 そして、敵に当たるとゲームが止まるはずなので、以下のようにしてEDIレジスタの値を読み取る。 最後に、この0x62045300 + 0x18 = 0x62045318を、CheatEngineのアドレスリストに追加して、中の値を見る。 この時点で気づくのは、Ghidraの構造体エディタではm_blueprintNameはActor構造体のアドレスからオフセット24 (0x18) の位置にあるとなっていたが、実際にそこに入っていた値はer...と書いてある。
なんとなくPlayererっぽいので、4バイト引いたアドレスにしてみると、 となり、オフセットは0x18ではなく、0x14が正しい事がわかる!
ここで思い返すと、先程自分でActor構造体の一番上に4バイト追加したが、これをしなかったらm_blueprintNameの方のオフセットは正しかったわけなので、 4バイトを追加するべきだったのは、構造体の先頭ではなく、m_blueprintNamem_healthの間であった事がわかる(ここでは単純にm_blueprintNameのサイズを4バイト大きくする)。 ここまでで、Actor構造体+0x14 すなわち、EDI+0x14の位置に、プレイヤーならPlayerという文字列がある事がわかった。
また、今度は同じ手順で敵に攻撃を当ててみると、このようにちゃんと別の値が入っていることがわかる。
よって、Actor構造体のオフセット0x14にある、m_blueprintNameメンバの値が"Player"かどうかで敵と自分を区別でき、 そのActor構造体は、ダメージを受ける命令が実行されるタイミングにおいて、ediにアドレスが入っていることがわかった。