39: TraceLine関数の解析

概要

37回38回 に渡って、TraceLine関数がどんなものかを理解し、リバーシングに役立つRTTIやvftableを理解しました。
今回は、TraceLine関数をリバーシングし、呼び出しに必要な引数等を把握する事を目標にやっていきます。
チート対象のゲームは、x86でオープンソースのAssault Cube v1.2.0.2を使います。

kaiju と OOAnalyzer のインストール

GhidraにてRTTIを反映させるには、kaijuというプラグインが必要です。 RTTIの解析にはOOAnalyzerというツールが必要で、 OOAnalyzerでRTTIを解析した結果を、kaijuプラグインでGhidraに反映させる流れになります。

OOAnalyzerのインストール

公式のインストール方法に色々種類が書いてありますが、 自分は Pre-built Docker Images (Easiest) をLinuxのマシンに立ててます。ちなみに docker run -v ホストのディレクトリ:VM側のディレクトリ で、カレントディレクトリをマウントしたい時は `pwd` を指定すればよいです。

Kaijuプラグインのインストール

Kaijuも公式のインストール方法を参考に入れていただければ。 GhidraのプラグインはZipを展開せずに入れるのでそこだけ注意です。

RTTI の Ghidra への適用

両方のインストールが完了したら、早速使って見ましょう。
まず、C:\Program Files (x86)\AssaultCube\bin_win32 とかにあると思われる ac_client.exe を、 適当のフォルダにコピーし、docker run でそのフォルダをマウントしましょう。 コンテナのシェルが立ち上がると思うので、マウント先のディレクトリへ行き、 そこで ooanalyzer --json=ac_ooanalyze.json ac_client.exe を実行しましょう(その他詳しいオプション等は ここ に書いてあります)。結構時間がかかりますという点と、色々とWARNやERRORが出てきますが気にしなくていいです。

終ったら、RTTIの解析結果である ac_ooanalyze.json を Kaiju でGhidraに読み込みます。
ooanalyzerのコンテナから抜けた後、Ghidraを起動し、ac_client.exe をいつも通り Analyze します。 その後、Kaiju > OOAnalyzer Importer を選択し、出てきたポップアップで、Open JSON File を押し、ac_ooanalyze.json を選択しましょう。OKを押してちょっとすると、Loaded 116 Class みたいな表示が出てくるはずです。

実際どれくらい変わったかbefore/afterを見てみましょう。
ファイル先頭からのオフセット 0x63600 に弾を打つ関数(以降Shoot関数)があります。 このShoot関数のOOAnalyzer適用前後はこんな感じです。 RTTIは クラス名 と クラスの階層構造 の情報ですので、その情報が確かに反映されている事がわかります。 また、OOAnalyzer/Kaijuが自動で各クラスの構造体やvftableを定義してくれているので、 後はそれを解析して、仮想関数に名前を付けていくとかをしていけばいい感じです。

TraceLine関数が使われてそうな関数の特定

TraceLine関数をまずは突き止めるために、TraceLine関数が使われていそうな関数を特定します。
例えば、Shoot関数で、TraceLineで障害物があったらダメージ入れない、障害物無かったらダメージ入る とか書いてありそうなので、 Shoot関数を突き止めます。球数とかでCheatEngineでメモリスキャンし、それに書き込みをしてる命令を探してください。 そうすれば、先ほどのオフセット 0x63600 のShoot関数にたどり着くはずです。

座標 from/to の特定

TraceLine関数を特定するための下準備である情報収集をしていきます。
37回 でまとめた通り、TraceLine関数の特徴の一つとして、座標fromとtoを引数として取るという性質がありました。 座標はVector3みたいな構造体としてあるはずなので、float (4Byte)が三つ並んでいるデータを見ていきます。
パッと見、以下が怪しいです。 怪しい理由は、dVar4からの4Byteずつの連続したオフセット4,8,cを入れてるのと、 param_1も型がfloatで、同じく連続したデータを入れてるからです。

もう既にVector3だなと確信しても良いですが、このShoot関数全体を見てみると、weaponインスタンス のメンバへのアクセスが非常に多い事がわかります。上図のdVar4にも、オフセット8weaponクラスのメンバが入ってます。なので、ReClass.NET で weaponクラスのメンバを割り出していきましょう。 上図の dVar4 = (this.weapon).mbr_0x8 のアセンブラは、ac_client.exe+0x6373d にあるので、そこにBPを打ち、ESI読み取る事で、weaponインスタンスのアドレスが取れます。 Shoot関数内なので、一回打てばBPにヒットします。 このアドレスをReClass.NETに入れてみてみましょう。 最初の項目(オフセット0) がvptrで、ReClassが自動でRTTIを解釈してくれるので、 これがweaponクラスを継承してるgunクラスを継承してるassaultrifleのインスタンスである事がわかります。
オフセット8はヒープにある何かを見てるっぽいので、これをReClassに入れて見てみましょう。 最初のオフセットの項目がvptrらしく、RTTIのパース結果が書いてあり、playerentと書いてあるので、どうやらプレイヤークラスらしいです。 先程のShootで、ownerのオフセット4,8,cにアクセスしましたが、その部分を見てみても、確かにプレイヤーの座標っぽい数字になってます。
ちなみに、これは自分が弾を打ってBPにヒットさせましたが、相手が弾を打った時にもこのBPはヒットし、同じ事をやれば、 このインスタンスのオフセット8は以下のように、ボットのアドレスを指しています。 これはつまり、weaponインスタンスのオフセット8は、その武器の保持者(owner)だとわかります。
よって、Ghidraの方でリネームしましょう。 また、ownerのポインタの格納先の変数の型も playerent * にしておきましょう。 そして、playerentのオフセット4,8,cが座標だとわかったので、Vector3型に変えたいのですが、 Vector3型が定義されていないので、以下のようにして定義しましょう。 そして、playerentクラスのオフセット4Vector3型に以下のようにしましょう。 最後に、座標の格納先の変数の型をVector3型に変更し、名前も変え、以下のように最終的になってればOKです。
後は、param_1 とかも突き詰めてみていきたい所ですが、流れは同じなので答えを行ってしまうと、これも Vector3 です。なので、以下のように型変更と名前変更をします。 その他色々適当に綺麗にしましょう。

EntityListの特定

TraceLine関数の特徴として、内部でEntityListをループで回しているという点があるので、EntityListのアドレスを先に出しておきます。
EntityListのような重要なアドレスは、グローバル変数として宣言されている事が多いです。 AssaultCubeでは、プレイヤーのアドレスが 0x50F4F4 に固定されていたのと同じように、EntityListもグローバル変数になっています。 デコンパイル中にある DAT_*** は固定のアドレスで、そのままReClass.NETに入れたり、そのアドレスが指してるアドレスをReClass.NETに入れれば どんな物かは容易に解析できます。EntityListのアドレスは、35回 で特定したので、ここでは詳細は省略します。以下のようにグローバル変数名を変えておきましょう。

TraceLine関数の特定

ここまでで、TraceLine関数が使われていそうなShoot関数を特定し、TraceLine関数に引数として渡される座標from/toの特定と、 TraceLine関数の中を見た時にEntityListが使われているとすぐわかるようにグローバル変数の名前をentitylistに変える という下準備ができました。

では、実際にこれら情報を元にTraceLineを特定します。座標from/toが使われている関数を見ると、以下の赤枠の二つになります(緑枠は後述)。 TraceLineの引数には、もう一つ owner のアドレスも必要なので、二番目の赤枠の方が確率は高そうですが、 Ghidraのデコンパイルがここら辺は大分崩れているので、引数として認識されていない可能性もあるので、一応両方見ていきます。

0x462020 の関数

virt_meth_0x462020_20というのは、vftable上の関数と言う意味で、関数自体は 0x462020 にあります。この関数を見てみましょう。 一つ注意なのは、この関数がTraceLineなのではなく、IsVisibleのようなTraceLine関数のラッパー関数である可能性があるという点です。 むしろその可能性の方が高いです。なので、この関数の中で呼び出してる関数で、座標from/to や owner を引数として取っている関数もチェックする必要があります
それを踏まえた上で色々見てみると、この関数には、「entitylistをループで回し、各エンティティに対し衝突判定を行う」という処理が無い事が確認できると思います。 なので、これはTraceLineではありません

0x4613b0 の関数

この関数は、かなりTraceLineっぽいです。 特に、FUN_00460670の以下の部分で、entitylistへのループでのアクセスがあるのでかなりそれっぽいです。 パッと見赤枠の二つの関数しか座標from/toを引数として取っている関数は無く、かつこれがここまで似ていると正直これをTraceLineと断定してもおかしくないと思います。 しかし、「戻り値や引数でcollideしてるかどうか(1か0かのbool)を返す」という処理がどこにも無いです。 例えば IsVisible のようなラッパー関数でも、返すのはboolであるはずですが、この関数は 1 を入れたり 0 を入れたりと言った処理が無いです。
なので、これもTraceLineではありません

__purecall_16

これはGhidraのデコンパイルが相当汚くて、かなりハードコースになっていますが、 実はTraceLine関数は、引数とかが全く上手く解釈されていないだけで、緑枠で囲った __purecall_16 と表記されてる関数内にあります。
__purecall_** とGhidraでなっている時は、そこを上手くGhidraが解釈できていないという事なのですが、 ここが上手く解釈できていない理由は、OOAnalyzerの問題なのかなと思っています。

これはバグなのか/リバーシングの限界的な制約なのか/実装上そういうものなのかよく理由はわからないのですが、 まずOOAnalyzerはgunクラスをどうやら定義できていません。RTTI上で subgun : gun : weapon と出ていた通り、 本来は親クラスにgunクラスがあるはずです。
このShoot関数は、自分がassaultrifleを使ってるときにも、敵がsubgunを使ってる時にも呼ばれていたため、 OOAnalyzerではこの関数は subgun::Shoot と認識していますが、本来は gun::Shoot なはずです。

もう一つ奇妙な点は、Shoot関数は引数として subgun *this を取っているわけですが、 関数内部では、全部 (this->weapon)となっています。現に、subgun構造体の定義を見ると、 メンバにweaponしかありません… なので、結局 thisはweaponクラスとして扱われています。

よって、例えばweaponにて仮想関数を宣言し、それを継承したgunで実際に実装した関数を呼び出した場合、 このOOAnalyzerの仕様だと、その関数がweaponのvftableにあると解釈するので、invalidな関数 __purecall_** と表示されていると考えられます。

よって、gunクラスにある関数へのポインタは、子クラスに継承されているので、__purecall_16 の正体は、 subgunクラスのvftableを見ればわかります。 この 0x463810 の関数がそうっぽいので、中を見てみます。 また飽きれる程デコンパイルが汚いですが、最初の関数 0x48a310 を見てみます。 これが実はTraceLine関数です
entitylistの各エンティティをループで回しており、衝突判定らしき FUN_0048aa80 があります。さらに、(in_EAX + 3) に 1 という値を入れてるのがわかります。

まとめると、以下が答えです。

TraceLine関数を綺麗にする

TraceLine関数は、自分のデコンパイル結果は最初こうなっていました。 これを綺麗にしていきます。

まず、この関数の引数が多すぎるので整理したいです。 よく見ると、param_1param_2 はどこでも使われておらず、 かつこの関数は __fastcall と定義されています。 __fastcallとは、最初の二つの引数をレジスタとして受け取る呼び出し規約ですが、 param_3以降しか使われてないとなると、呼び出し規約が間違ってる可能性が高いです。
レジスタを使わず、第一引数からスタックに入れるのは __stdcall__cdecl で、この二つの違いは以下です。 よって、この二つはTraceLineの呼び出し側でESPを戻す処理があるかどうかで見分けられるので、見てみましょう。 間に余計な命令がありますが、add esp, 0x24 があるので、このTraceLine関数は __cdecl です。 よって、以下のように直しておきましょう。 次に、param_1, param_2, param_3param_4, param_5, param_6 が連続して使われていたり、 &param_1&param_4 へのアクセスが多い事から、これらは座標from/toだと考えられるので、 引数の方を直しましょう。 構造体と違って、型のサイズが変わってもその分引数の数を自動で減らすという事はないので、例えば param_1Vector3 型にしたら、param_2, param_3 を消すのを忘れないようにしましょう。

次に、以下の in_EAX を直します。 この in_レジスタ は、レジスタがこの関数内で書き込みよりも前に読み込みが行われてた時にこうなります。 つまり、引数として使われているという事です。これを綺麗にするには、以下のように引数を追加する事で対処します。 これで、以下のように変わってるはずです。 ここで、このparam_6 の型ですが、Vector3が入ってると思いきや、所々 param_6 + 3 に 0 か 1 の bool が入ってる事がわかるので、これが衝突したかどうかの結果と、衝突位置の座標をまとめた、 traceresult構造体だとわかるので、構造体を定義しましょう。
param_6 で右クリックし、Auto Create Structure を押しましょう。 そうすると、astruct という構造体が自動で定義されるので、それを以下のように変更します。 ついでに、param_6traceresult にリネームしてこの対処は終了です。

次に、param_3param_4 を見てみます。 また、param_5 を見てみます。 以上より、以下がわかります。
これで、以下のようになってれば終了です! 引数と結果がどう返ってくるかがわかれば、後は呼び出すだけです!