メリット
継承関係を持つ実行ファイルをリバースエンジニアリングしたとき、
その継承関係がどのように表示されるのかを知ることができる。
前提
ソフトウェアリバースエンジニアリングツールのGhidraを使います。
インストール・操作方法は省かせていただきます。
Ghidraというツールは、アセンブリコードと、
そこから元のコードに近い状態に復元されたコードの両方を表示してくれるので、
本記事でも後者で比較したいと思います。
開発環境はVSCodeで、コンパイラはMinGWを使っています。
インストール・操作方法は省かせていただきます。
用意したソース・実行ファイル
以下のクラスを使った簡単なソースコードをビルドし、
実行ファイル(main.exe)を用意します。
実行ファイルは、コンソール画面に文字列を表示するのみです。
- Aクラス: Bクラスを継承
- Bクラス: 非純粋仮想関数を持つクラス
- Cクラス: Bクラスのオブジェクトをメンバとして持つ。仮想関数・継承は無し。
#ifndef A_H
#define A_H
#include "B.h"
class A : public B {
public:
A();
virtual ~A();
void show();
};
#endif // A_H#include "A.h"
#include <iostream>
A::A() {}
A::~A() {}
void A::show() {
std::cout << "A::show() called" << std::endl;
}#ifndef B_H
#define B_H
class B {
public:
B();
virtual ~B();
virtual void foo(); // 非純粋仮想関数
};
#endif // B_H#include "B.h"
#include <iostream>
B::B() {}
B::~B() {}
void B::foo() {
std::cout << "B::foo() called" << std::endl;
}#ifndef C_H
#define C_H
#include "B.h"
class C {
public:
C(B* b);
~C();
void callB();
private:
B* m_b;
};
#endif // C_H#include "C.h"
#include <iostream>
C::C(B* b) : m_b(b) {}
C::~C() {}
void C::callB() {
if (m_b) m_b->foo();
else std::cout << "B is null" << std::endl;
}#include "A.h"
#include "C.h"
#include <iostream>
int main() {
A a;
C c(&a);
a.show();
c.callB();
system("pause"); // ← これで「何かキーを押すまで」止まる
return 0;
}Ghidraで実行ファイルを読み込む
Main.exeを読み込むと以下のように表示されます。
以降の画像は、左がアセンブリコード、右がデコンパイル(=復元)コードです。
・Aクラスのコンストラクタ

B::B((B *)this);
上記は、A のコンストラクタ内で B のコンストラクタを呼んでいます。
AのオブジェクトをB*型にキャストして引数として渡しています。
このことよりA が B を継承していることが分かります。
*(undefined ***)this = &PTR_~A_140004610;
上記は、「Aのvtable(仮想関数テーブル)を this の先頭に書き込んでいる」箇所になります。仮想関数を持つクラスは、仮想関数テーブルというものを持ちます。
ということは、このクラスは仮想関数のメンバを持つことがわかります。
なぜそう言えるのかというと、そういう仕様だからです。
少し具体的に言うと、C++ の仮想関数を持つクラスの ABI(Application Binary Interface)で
“vtableのポインタはオブジェクトの先頭に置く”と決まっているからです。
・Bクラスのコンストラクタ

*(undefined ***)this = &PTR_~B_140004640;
上記は、「Bのvtable(仮想関数テーブル)を this の先頭に書き込んでいる」箇所になります。
・Cのコンストラクタ

*(undefined8 *)this = param_1;
上記は、C オブジェクトの先頭アドレスから 8 バイト後ろにあるメンバ変数へ、
param_1(8バイト値)をそのまま書き込んでいます。
undefined8 は Ghidra が「8バイトの未定義型」として扱う型です。
実質 uint64_t や void と同じサイズ* です。
メンバ変数名(m_b)まではわかりませんね。
Cクラスは継承しているクラス・仮想関数は持たないクラスのため、
A, Bのようにvtableは持ちません。
まとめ
細かい点は端折りましたが、デコンパイルしたときに、前述のようなコードがあれば、
継承関係があるということをまず覚えておくとよいと思います。
リバースエンジニアリングする際は、ぜひお試しください。
ただし、リバースエンジニアリングは法律(著作権法など)で制限されているので、
(デコンパイル自体は問題ないが、それを改修して公開するなどは禁止etc..)
バイナリファイルの取り扱いにはご注意ください。


コメント