38: RTTIとvftableの理解

概要

前回の記事では、TraceLine関数の処理内容を詳細に理解し、リバーシングに役立つポイントを割り出しました。
次に、TraceLine関数をリバーシングで見つけるという事をやりたいのですが、AssaultCube は PwnAdventure3 と違ってPDBが無く、このままだとまだリバーシングが結構しんどいため、もうワンステップ挟みます。
今回は、C++のリバーシングで強力な情報源となる、RTTIとvftableを理解する事を目標にやっていきます。
チート対象のゲームは、x86でオープンソースのAssault Cube v1.2.0.2を使います。

仮想関数と動的バインディング

RTTIを理解するためには、vftable/vtableを理解する必要があり、vftableを理解するにはそもそもC++の仮想関数がどういう物なのか、動的バインディングとは何かを知る必要があります。

一般的に、仮想関数とは、子クラスによって再定義される親クラスのメンバ関数の事で、virtual void func() のように定義されます。 例を見た方が早いです、以下のような感じです。
// weapon.h
struct weapon { virtual void attack(); };

struct grenade : weapon { void attack(); };
struct gun : weapon { void attack(); };

// weapon.cpp
void grenade::attack() {...}
void gun::attack() {...}

しかし、virtualはもっと本質的な意味として、virtualを付けた関数は動的バインディングされるという意味があります。 動的バインディングや、その反対の静的バインディングの意味は以下です。 この違いが分かるサンプルコードが以下です。
#include <iostream>

class weapon {
    public:
    // ここのvirtualを付けるか付けないかで、attackが動的バインドされるか静的バインドされるか変わる
    virtual void attack() {
        printf("weapon attack\n");
    }
};

struct grenade : weapon {   // structとclass混ざってますが、特に違いは無いのでお気になさらず...
    public:
    void attack() {
        printf("grenade attack\n");
    };
};

void func(weapon* w) {
    w->attack();
}


int main() {
    weapon  w;
    grenade g;

    func(&w);
    func(&g);

    return 0;
}
これの実行結果ですが、weapon の attack() に virtual を付けるか付けないかで、以下のように実行結果が異なります。
// virtual 有り
weapon attack
grenade attack

// virtual 無し
weapon attack
weapon attack
つまり、virtualを付け無いと、attack() は静的バインディングされる、すなわち w->attack(); をコンパイル時に weapon の attack() だと解釈するので、w が grenade のインスタンスだったとしても、呼ばれる関数は weapon の方になります。 一方で virtual を付けると、attack() は動的バインディングされる、すなわち w->attack(); の attack() の関数呼び出しは、 実行時に関数の定義とバインドされるため、w が grenade のインスタンスなら、grenade の attack() とバインドされる。 という事です。

実際にGhidraで両パターンのバイナリを見てみても、確かに virtual有りでは動的に関数ポインタにバインドされてるのに対し、 virtual無しだと既に静的にバインドされている事がわかります。 よって、関数の継承においては動的バインディングの方が理想な挙動である場合が多々なので、 親クラスであればあるほど多くの仮想関数を持っている傾向があります。 AssaultCubeのソースコード を実際に見てみても、親クラスのweaponとかは一杯virtual関数を含んでいる事がわかります。

vftable/vtable

仮想関数を一つでも持つクラス、あるいは仮想関数を持って無くとも親の仮想関数をオーバーライドしてる子クラスは、 自分のクラスの vftable (仮想関数テーブル) というデータ構造を持っています。
vftableというのは、仮想関数のポインタの配列です(vftableともvtableともいいます)。下の例を見た方が早いです。
#include <iostream>

class Base {
    public:
    virtual void func1() {
        printf("Base func1 called\n");
    }

    virtual void func2() {
        printf("Base func2 called\n");
    };

};

class Derived : public Base {
    public:
    void func1() {
        printf("Derived func1 called\n");
    }
};


int main() {
    Base b;
    Derived d;

    b.func1();  // => Base func1 called
    b.func2();  // => Base func2 called
    d.func1();  // => Derived func1 called
    d.func2();  // => Base func2 called

    return 0;
}
このようなBaseとDerivedクラスがあった時、それぞれ以下のようなvftableを持っています。 vftableへのポインタであるvfptrは、必ずインスタンスの構造体の一番最初の項目にあります。
実際にGhidraで見てみても、確かに以下のようにvftableが定義されています。 vftableはリバーシングの観点からすると、そのクラスの持ってるメソッドが一覧で見れるので、 vftableを解析する事は、そのクラスの持つ機能を洗い出していくという事になり、非常に有益な注目すべき情報となります。 しかし、注意としてvftableには仮想関数以外の関数はありません。 とはいえ、例えばゲームのコードなどでは、gun という一つの大きなクラスに virtual で shoot() 等の関数を宣言して置き、子クラスの subgun や rifle 等でオーバーロードするような実装が多々であるため、 大体vftableにそのクラスを特徴づける関数一覧が載ってると思って大丈夫だと思います。

(ちなみに、先程のweaponとgrenadeのコードではvftableを参照するようなコードになっていなかったのを疑問に思った方もいるかもですが、 理由としては、あっちはgccでコンパイルしたのに対し、こっちの例は Microsoft Visual C++ のコンパイラでコンパイルしたためです。)

RTTI

RTTIは、プログラムの実行中に動的に型を判別するための情報で、Win32バイナリの生成で最もよく使用される Microsoft Visual C++ のコンパイラに備わってる機能(他のコンパイラもあるものはある)です。

具体的な使用例は、例えば親クラスBaseを継承してる子クラスDerivedがあった場合、 func(Base* b)の関数には、上記のweaponとgrenadeの方であった通り、 引数としてBaseのインスタンスとDerivedのインスタンスの両方を入れる事ができます。 この際、実際に引数にどっちのインスタンスが入れられたかを判別する時に、RTTIは使われます。
また、typeid(*b).name()というコードを実行すれば、実際にbの型の名前が表示されます。 しかし、型情報やクラス(構造体)の名前というのは、コンパイル時に失われるはずです。 失われなかったら、Ghidraでわざわざ構造体を再構築したり等の作業が必要ないはずです。 それでも、型の名前がこれで表示されるのは、RTTIの中にその情報がいれられていて、それを参照しているからです。

RTTIの具体的な説明に関しては、このopenrceのigorskさんの記事 ほど完璧な説明は無いと思うので、 これをそのまま見てほしいです。
また、その記事にある このRTTIの図 はかなり参考になります。
ざっと、RTTIに含まれてる主要な構造体だけ簡単に説明すると、以下になります。
コンパイラのオプション等によりRTTIを作らないようにする事もできるのですが、ゲームの実行ファイルにRTTI情報がある場合は、 絶対に活用すべきです。ゲームのリバーシングにおいてまず初めにやる事は、このRTTIをパースし、クラス名や階層構造を復元する事 といっても過言では無いと思います。

AssaultCubeでRTTIを見てみる

最後に、実際に ReClass.NET を用いて AssaultCube のPlayerクラスのRTTIを見てみます。 先程のRTTIの図を見ながらやると良いと思います。
AssaultCubeのPlayerインスタンスのアドレスは、いつも0x50f4f4のアドレスに格納されていますので、このアドレスを最初に読み取りましょう。 このPlayerインスタンスのアドレスをReClass.NETに入力して見てみましょう。 Playerインスタンスの最初の項目が vfptr (vftableへのポインタ) でしたので、vftableのアドレス 0x4e4a98 を実際にReClass.NETに入れてみてみましょう。 これがPlayerクラスのvftableです。
確かに関数のアドレスのようなのが一杯あります。
vftableの一つ上の項目に、RTTIのCOLへのポインタがありましたので、4Byte引いた 0x4e4a94 を見ていきます。 赤枠の所がCOLへのポインタなので、COLのアドレス 0x4f5708 を見てましょう。 これが COL です。
最初にTypeDescriptorの方を見てみましょう。0x4ffB70 を見てみましょう。 これが TypeDescriptor です。
TypeDescriptorにはこのインスタンスのクラス名が書いてあり、実際は playerent という事がわかります。 ちなみにその下にも、dynent や worldobjreference クラスのTypeDescriptorがあるのも見えますね。 では今度は一つ戻って、ClassHierarchyDescriptorを見てみます。0x4f571c をReClass.NETに入れて見てます。 これが ClassHierarchyDescriptor です。
BaseClassArrayのアドレスが 0x4f572c である事がわかるので、実際に入れてみてみましょう。 これが BaseClassArray です。
確かに5つ項目がある事がわかります。BaseClassArrayから、このクラスの親クラスがどんな名前なのかを見てみましょう。 とりあえず2番目のアドレス 0x4f57e4 を入れてみましょう。 これが このplayerentクラスの親クラスのBaseClassDescriptor です。
最初の項目が pTypeDescriptor で、このクラスの TypeDescriptor へのアドレスなので、0x4ffb88 を入れてみましょう。 これが playerentクラスの親クラスのTypeDescriptor です。
どうやら先程の dynent クラスだった見たいですね。

最初PlayerインスタンスのアドレスをReClass.NETに入れた時に気づいたと思いますが、ReClass.NETは優秀で、 すでに playerent : dynent : physent というRTTIのパース結果を表示してくれていました。 しかし、それは今やったようにRTTIを辿っていった結果の物です。
Ghidraにも、RTTIを自動でパースする拡張機能があるので、次回はそれを使ってTraceLine関数をリーバスしていきます。