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
にも、オフセット8
のweapon
クラスのメンバが入ってます。なので、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クラスのオフセット4
をVector3
型に以下のようにしましょう。
最後に、座標の格納先の変数の型をVector3
型に変更し、名前も変え、以下のように最終的になってればOKです。
後は、
param_1
とかも突き詰めてみていきたい所ですが、流れは同じなので答えを行ってしまうと、これも Vector3
です。なので、以下のように型変更と名前変更をします。
その他色々適当に綺麗にしましょう。
EntityListの特定
TraceLine関数の特徴として、内部でEntityListをループで回しているという点があるので、EntityListのアドレスを先に出しておきます。EntityListのような重要なアドレスは、グローバル変数として宣言されている事が多いです。 AssaultCubeでは、プレイヤーのアドレスが
0x50F4F4
に固定されていたのと同じように、EntityListもグローバル変数になっています。
デコンパイル中にある DAT_***
は固定のアドレスで、そのままReClass.NETに入れたり、そのアドレスが指してるアドレスをReClass.NETに入れれば
どんな物かは容易に解析できます。EntityListのアドレスは、35回
で特定したので、ここでは詳細は省略します。以下のようにグローバル変数名を変えておきましょう。
- DAT_0050F4F4 => player
- DAT_0050F4F8 => entitylist
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関数のラッパー関数:
0x463810
- TraceLine関数:
0x48a310
TraceLine関数を綺麗にする
TraceLine関数は、自分のデコンパイル結果は最初こうなっていました。 これを綺麗にしていきます。まず、この関数の引数が多すぎるので整理したいです。 よく見ると、
param_1
と param_2
はどこでも使われておらず、
かつこの関数は __fastcall
と定義されています。
__fastcall
とは、最初の二つの引数をレジスタとして受け取る呼び出し規約ですが、
param_3
以降しか使われてないとなると、呼び出し規約が間違ってる可能性が高いです。レジスタを使わず、第一引数からスタックに入れるのは
__stdcall
か __cdecl
で、この二つの違いは以下です。
__stdcall
: 引数を呼び出した関数内で消してから(スタックポインタをずらしてから)リターンしてくる__cdecl
: リターンしてきた後に呼び出し側が引数分のスタックを戻す
add esp, 0x24
があるので、このTraceLine関数は __cdecl
です。
よって、以下のように直しておきましょう。
次に、param_1, param_2, param_3
と param_4, param_5, param_6
が連続して使われていたり、
¶m_1
や ¶m_4
へのアクセスが多い事から、これらは座標from/toだと考えられるので、
引数の方を直しましょう。
構造体と違って、型のサイズが変わってもその分引数の数を自動で減らすという事はないので、例えば param_1
を Vector3
型にしたら、param_2, param_3
を消すのを忘れないようにしましょう。次に、以下の
in_EAX
を直します。
この in_レジスタ
は、レジスタがこの関数内で書き込みよりも前に読み込みが行われてた時にこうなります。
つまり、引数として使われているという事です。これを綺麗にするには、以下のように引数を追加する事で対処します。
これで、以下のように変わってるはずです。
ここで、このparam_6
の型ですが、Vector3が入ってると思いきや、所々 param_6 + 3
に 0 か 1 の bool
が入ってる事がわかるので、これが衝突したかどうかの結果と、衝突位置の座標をまとめた、
traceresult構造体だとわかるので、構造体を定義しましょう。param_6
で右クリックし、Auto Create Structure
を押しましょう。
そうすると、astruct
という構造体が自動で定義されるので、それを以下のように変更します。
ついでに、param_6
も traceresult
にリネームしてこの対処は終了です。次に、
param_3
と param_4
を見てみます。
また、param_5
を見てみます。
以上より、以下がわかります。
param_3
: ownerで、アドレスなので4Byteparam_4
: このフラグにより、敵も障害物としてカウントするか切り替わるので、0で良さげなbool
param_5
: とりあえず0で良さげなbool
これで、以下のようになってれば終了です! 引数と結果がどう返ってくるかがわかれば、後は呼び出すだけです!