Unix C がちょっと書けるだけの俺による令和最新式 Ghidra による Windows C++ アプリケーション解析入門 - E-MU 1616M CardBus が刺さってる Panasonic Let's Note CF-S10E がスリープからの復帰に失敗する (その 10)
過去回で COM インタフェースなのに COM/OLE Viewer からタイプライブラリ情報すなわち MIDL をブッコ抜けない CtDPROXY.DLL の話をした。 レジストリ情報から読み取れるのはクラス名が EMU10Kx って事くらい。
このEMU10Kx はチップ名称なのは知ってたけど、分類として 3 種類あることを今回はじめて知った。
- EMU10K 1 (PCI\VEN_1102&DEV_0002&CC_040100) … Sound Blaster Live! の世代
- EMU10K 2 (PCI\VEN_1102&DEV_0004&CC_040100) … Sound Blaster Audigy の世代
- EMU10K 2.5 (PCI\VEN_1102&DEV_0008&CC_040100) … Sound Blaster Audigy 2 の世代
今はもう EMU10K 2と2.5のコストダウン版しか作ってないのかな、まぁ外付サウンドカードなんてもう死んだジャンルだから投資もしないよね。 つまりドライバさえあれば 20 年前のサウンドカードだって最新機種となんら変わらずに DTM できるということである。
アナログ写真趣味でもスキャナやフィルムスキャナの進歩はもう 20 年前に止まるどころか完全に死んでるからな。 俺の求めるものは中古ジャンクの世界にしか存在しないのだもはや。
今は公式ドライバのうちカーネルドライバ ctoss2k.sys を X-Fi Titunium の最終版ドライバのものに差し替えるだけで動いてるけど、将来的には kX Project のオープンソースなドライバのお世話になるしかないのかもね。 でもそうなると PatchMix DSP が使えなくなるので困る、たかがミキサーパッチベイに新しいソフトなんか覚えたくないねん。
おじさんはかつて普通じゃない軽音部に所属し音楽史研究にいそしむ傍ら PA 屋でバイトしてアナログコンソール弄ってた世代なのでそもそもソフトウェアベースの DTM 自体嫌いなのだ。 打ち込みだっていまだに KAWAI Q-80 が世界で最も優れたシーケンサーだと固く信じてるからな。 高名なミュージシャンだって Roland MC-4 を頑なに使い続けてる人だっているし道具ってのはそういうもんである。
ちゅーことで MIDL が無いので COM どうやって呼ぶのかバイナリ解析しないとならない、ということで CTDPROXY.DLL を Ghidra に解析させる編。
Ghidra のインストール
Ghidra は悪の枢軸トランプ帝政アメリカの国家安全保障局 (NSA) が公開してるリバースコンパイルツール。
NSA は高度な暗号技術輸出を禁止しようとしたり(合衆国憲法修正第 1 条を盾に RSA 暗号のソースを書籍にして海外に持ち出した) かつてはインターネッツのバックボーンに 641A 号室 なる盗聴部屋を作ってたことで悪名高いアレ、あまり近寄りたくない存在である。 俺の処刑もインターネットで公開するんでしょ!エロ同人みたいに!エロ同人みたいに!
インストールはアーカイブを適当な場所に展開するだけ、ただし Java で実装されてるので JRE のインストールが必要。 あと一部のプラグインに Python が必要になるけど使わないなら入れなくてよい。
この令和の世でも Java アプリを起動するのに *.bat をダブルクリックして黒窓がポップアップするいう地獄。 仮想マシンつまり jvm.exe に引数として渡さなければいけない宿命のせいだが、せめて *.jar をダブルクリックしたら実行できるくらいにならんのか。 なお当の故 Sun Microsystem は俺達にはブラウザからの Applet 実行そして *.jnlp すなわち Java Web Start があるなどという意味不明の供述をしており…
昔は Unix なんかでも execve システムコールで直接 .class を実行できるパッチなんてあったんだけどね(カーネルが magic が Java class なら JVM を探し実行する)。
みんな Java を嫌っていたのでそんなパッチが accept される未来は無かった結果がこれ。
プロジェクトを作成して解析対象の実行ファイルや共有ライブラリをインポートする手順は割愛、難しくないのでマニュアル読んでどうぞ。
まずは仮想関数テーブルを探す
COM の正体とはざっくりと言えば GUID と紐づけられた IUnknown インタフェースを実装するクラスの仮想関数テーブルでしかないので、まずはそれを探すことにする。 C++ ならコンパイル時に決定済だろうしどうせ書換禁止領域に配置されてんだろと考えて Ghidra で解析した疑似アセンブラコードから .text セクションを調べてみる。
そんで出てきたのがこれ。
PTR_FUN_02001384 XREF[2]: FUN_02002d93:02002d9c(*),
FUN_02002e16:02002e2c(*)
02001384 68 5f 00 02 addr FUN_02005f68
02001388 c6 ae 00 02 addr FUN_0200aec6
0200138c ca 2d 00 02 addr FUN_02002dca
02001390 60 2e 00 02 addr FUN_02002e60
…
addr つまりメモリアドレスの配列がずらずら続いてるのだけど、先頭と判定された位置に PTR_FUN_<先頭のアドレス> という名前でラベルを貼られている。先頭のアドレス>
ここから同じようなのが十数個ならんでるのでここが MIDL が出力したサーバースタブコードっぽいですな。
このメモリアドレスは何を指すアドレスかというと、実行コードすなわち関数のアドレスである。 つまり仮想関数テーブルとはただの関数ポインタの配列ってことがよくわかる。
これらの関数も書換禁止領域に置かれていて実行コードの先頭に FUN_<先頭のアドレス> というラベルが貼られているので、クリックすすればジャンプできる。先頭のアドレス>
void FUN_02005f68(int param_1,undefined4 *param_2,undefined4 *param_3)
{
FUN_020034cb(*(HANDLE *)(param_1 + 0x20),*(undefined4 *)(param_1 + 8),
*(undefined4 *)(param_1 + 0xc),(int *)(param_1 + 0x10),param_2,param_3);
return;
}
コンストラクタを探せ
そんでこの仮想関数テーブルにはコンストラクタは出現しないのでその探し方である。
まず C++ のコンストラクタについてについてのおさらい。
#include <iostream>
class Greeting {
const char* name;
Greeting(const char* name) {
this->name = name;
}
~Greeting() {
}
void Hello() {
std::cout << "Hello " << this->name << "." << std::endl;
}
};
int
main()
{
auto greeting = new Greeting("Mike");
if (greeting != nullptr) {
greeting->Hello();
delete greeting;
greeting = nullptr;
}
return EXIT_SUCCESS;
}
このコードは C で書いたら以下のようなコードになる。
#include <stdio.h>
#include <stdlib.h>
#define operator_new malloc
#define operator_delete free
/* クラス */
struct GreetingVFTBL;
struct Greeting {
/* メンバ関数 */
const struct GreetingVFTBL* vftbl;
/* メンバ変数 */
const char* name;
};
/* 仮想関数テーブル */
struct GreetingVFTBL {
void (*Hello)(struct Greeting*);
void (*Delete)(struct Greeting*);
};
static void Greeting_Hello(struct Greeting*);
static void Greeting_Delete(struct Greeting*);
static const struct GreetingVFTBL Greeting_vftbl = {
&Greeting_Hello,
&Greeting_Delete
};
/* コンストラクタ */
void
Greeting_Greeting(struct Greeting* greeting, const char *name)
{
greeting->vftbl = &Greeting_vftbl;
greeting->name = name;
}
/* メンバ関数の実装 */
static void
Greeting_Hello(struct Greeting* foo)
{
fputs("Hello ", stdout);
fputs(foo->name, stdout);
puts(".");
}
/* デストラクタ */
static void
Greeting_Delete(struct Greeting* foo)
{
operator_delete(foo);
}
int
main()
{
struct Greeting* greeting;
greeting = (struct Greeting*)operator_new(sizeof(*greeting));
if (greeting != NULL) {
Greeting_Greeting(greeting, "Mike");
(*greeting->vftbl->Hello)(greeting);
(*greeting->vftbl->Delete)(greeting);
greeting = NULL;
}
return EXIT_SUCCESS;
}
やってることは
- メモリアロケータによってヒープ上にインスタンス用のメモリ確保が成功するとインスタンスを引数にコンストラクタが呼ばれる
- コンストラクタ内でメモリの先頭に仮想関数テーブルのポインタが格納される
- メンバ関数とデストラクタはこの仮想関数テーブルのポインタ経由でこのインスタンスを第 1 引数に呼びだされる
- デストラクタでメモリが解放される
なのだがポイントは 2. で
- 仮想関数テーブルを参照して
- 第 1 引数にそれを代入してる
コードがあればそれはコンストラクタの可能性が高いってことなんですわ。
それじゃさっそくこのラベルを参照してる関数を検索してみようか、仮想関数テーブルのラベル上で 右クリック -> Reference -> Show Reference to ラベル名を選んでみよか。

その結果関数 FUN_02002e16 内の実行コードアドレス 02002e2c がヒットした。

undefined4 * __thiscall FUN_02002e16(void *this,int *param_1,undefined4 param_2,undefined4 param_3)
{
*(undefined ***)this = &PTR_FUN_02001384;
*(undefined4 *)((int)this + 4) = 1;
*(undefined4 *)((int)this + 8) = param_2;
*(undefined4 *)((int)this + 0xc) = param_3;
*(int *)((int)this + 0x10) = *param_1;
*(int *)((int)this + 0x14) = param_1[1];
*(int *)((int)this + 0x18) = param_1[2];
*(int *)((int)this + 0x1c) = param_1[3];
*(undefined4 *)((int)this + 0x20) = 0xffffffff;
FUN_02002ca6(param_2,param_3,this,(int *)((int)this + 0x10));
return this;
}
クラスのインスタンスである this が void * としか解析できないので、すべて先頭アドレスからのオフセットになってしまっているので可読性が死んでおられる。
これ情報を人力で足していかねえとならんのだ、え、そんなの人力でなく AI にやらせろって?
義手や義足ってのは運悪く手足を失った人がやむをえず使うものなんだけど、お前は義手義足があるからってもう健康な手足なんて要らないって切り落とすんか? 義脳があるからと大喜びで自分の首から上を切り捨ててしまうのはかつて家畜であった食肉のセリフなんですわ、認知的降伏というやつやで。 加速主義者からしたら俺がノロノロとコード解析してるのは知的ハンディキャップ持ちの作業所内労働としか見えないんだろうな。
クラスに名前を付ける
まぁいいや、まずこのクラスに仮で名前を付けておく。とりあえず “Unknown Object No.1” 略して Unko1 とでもしておくか。
Symbol Tree 画面で Class フォルダを選択して右クリック -> Create Class で空っぽの Unko1 クラスを作成する。


次に Listing/Decompile/Symbol Tree どこからでもいいので FUN_02002e16 を右クリック -> Rename Function を選択して関数名を変更する。
Unko1 クラスのコンストラクタなので Unko1::Unko1 になる。




この時 Ghidra は呼出規約が __thiscall だから第 1 引数の型を Unko1 * に自動的に変更してくれるのだけど、コンストラクタであることまでは理解できないもよう。
なので右クリック -> Edit Function Signature で戻り値の型も Unko1 * に変更しておく必要がある。




この画面からでも関数名は変更できるのだけど、Unko1::Unko1 を「クラス名+メンバ関数名」でなく「名前空間名+フリー関数名」と解釈してしまうのでお勧めしない。
クラスに情報をひたすら登録していく
この時点で Unko1 はサイズ 0 の構造体としてしか認識されてないから void * だった時からコードはほとんど変化していない。
なので可能な限り情報を足していくことになる。
まずぱっと思いつくのは Unko1 の sizeof である、これはメモリアロケーターの引数からわかるよねってのはさっきの C++ を C で書き直したコードから理解いただけるはず。
ということで Unko1::Unko1 を実行するコードを探せば、その直前にメモリアロケーターがいるはずである。



ということで発見した。
pUVar4 = operator_new(0x28);
…
pUVar4 = Unko1::Unko1(pUVar4,param_5,param_3,param_4);
…
引数に指定された sizeof(Unko1) は 0x28 ということがわかったので Data Type から Unko1 のサイズを変更する。



ついでに先頭 4byte は仮想関数テーブルなのでとりあえず void *vftbl としておくかね。

ウインドウを×ボタンで閉じる時に保存するかと聞かれるので Yes と答える。

これで Decompile による疑似 C ソース上に追加した情報が反映される、そんじゃさっきの Unko1::Unko1 を確認してみよう。
Unko1 * __thiscall Unko1::Unko1(Unko1 *this,int *param_1,undefined4 param_2,undefined4 param_3)
{
this->vftbl = &PTR_FUN_02001384;
*(undefined4 *)&this->field_0x4 = 1;
*(undefined4 *)&this->field_0x8 = param_2;
*(undefined4 *)&this->field_0xc = param_3;
*(int *)&this->field_0x10 = *param_1;
*(int *)&this->field_0x14 = param_1[1];
*(int *)&this->field_0x18 = param_1[2];
*(int *)&this->field_0x1c = param_1[3];
*(undefined4 *)&this->field_0x20 = 0xffffffff;
FUN_02002ca6(param_2,param_3,this,(int *)&this->field_0x10);
return this;
}
this->vftbl に仮想関数テーブルのポインタが代入されてるのがはっきりわかるようになったね。
とりあえず他のメンバ変数についても
struct Unko1 {
void *vftbl; /* 0x0 */
undefined4 unknown1; /* 0x4 */
undefined4 unknown2; /* 0x8 */
undefined4 unknown3; /* 0xc */
undefined4 unknown4[4]; /* 0x10 */
undefined4 unknown5; /* 0x20 */
/* unknown 4byte here */
};
みたいなメンバ変数がおぼろげながら見えてきたので Data Type に入力しておく。

その結果コードがこのように変化した。
Unko1 * __thiscall Unko1::Unko1(Unko1 *this,int *param_1,undefined4 param_2,undefined4 param_3)
{
this->vftbl = &PTR_FUN_02001384;
this->unknown1 = 1;
this->unknown2 = param_2;
this->unknown3 = param_3;
this->unknown4[0] = *param_1;
this->unknown4[1] = param_1[1];
this->unknown4[2] = param_1[2];
this->unknown4[3] = param_1[3];
this->unknown5 = 0xffffffff;
FUN_02002ca6(param_2,param_3,this,this->unknown4);
return this;
}
すっきりして読みやすくなった、まぁ何やってんのかはまださっぱりだが。
Ghidra でも復元できないコードがある
ただなんとなく第 2 引数の param_1 とメンバ変数の unknown4 は GUID 構造体くせえなぁと俺の第六感が囁くのである。
Unko1 * __thiscall Unko1::Unko1(Unko1 *this, const GUID *guid, undefined4 param_2, undefined4 param_3)
{
...
this->guid = *guid;
...
}
みたいに構造体の代入やってんじゃねえのという、だがしかし Ghidra は構造体の代入式を正しく復元できないので
(this->guid).Data1 = param_1->Data1;
uVar1 = param_1->Data3;
(this->guid).Data2 = param_1->Data2;
(this->guid).Data3 = uVar1;
*(undefined4 *)(this->guid).Data4 = *(undefined4 *)param_1->Data4;
*(undefined4 *)((this->guid).Data4 + 4) = *(undefined4 *)(param_1->Data4 + 4);
と可読性が逆に悪化してしまうのだ、まぁコンパイラは代入をメモリブロック転送命令で片付けるからしかたないね。
02002e3f 8b f8 MOV EDI,EAX
02002e41 a5 MOVSD ES:EDI,ESI
02002e42 a5 MOVSD ES:EDI,ESI
02002e43 50 PUSH EAX
02002e44 a5 MOVSD ES:EDI,ESI
02002e45 53 PUSH EBX
02002e46 52 PUSH EDX
02002e47 a5 MOVSD ES:EDI,ESI
memcpy なんかもコンパイラが最適化にインライン展開するので、構造体なんかの型を指定すると余計に可読性が落ちるケースが多い、なのでコメントでここは代入だよって書き残すしかないね。
それに GUID 構造体という確証もまだ無いのでそのままにしておくか。
最後に仮想関数テーブルの名前も PTR_FUN_02001384 から VFTBL_Unko1 とでも変えておこう。
デストラクタも見つかった
このテーブルにある関数を見ていくと一番最後はデストラクタみたいね。
undefined4 * __thiscall FUN_02002e60(void *this,byte param_1)
{
FUN_02002d93(this);
if ((param_1 & 1) != 0) {
operator_delete(this);
}
return this;
}
なのでコンストラクタと同じ手順で名前を Unko1::~Unko1 そして戻り値なしに変更しておこう。
void __thiscall Unko1::~Unko1(Unko1 *this,byte param_1)
{
FUN_02002d93(&this->vftbl);
if ((param_1 & 1) != 0) {
operator_delete(this);
}
return;
}
そんでデストラクタから呼ばれてる関数 FUN_02002d93 の引数の型も Unko1 * に直してやると
void __fastcall FUN_02002d93(Unko1 *param_1)
{
param_1->vftbl = &VFTBL_Unko1;
FUN_02002cf6(param_1->unknown2,param_1->unknown3,(int)param_1);
param_1->unknown1 = 0;
param_1->unknown2 = 0;
param_1->unknown3 = 0;
if ((HANDLE)param_1->unknown5 != (HANDLE)0xffffffff) {
FUN_02001969((HANDLE)param_1->unknown5);
}
return;
}
メモリを開放する前にメンバ変数をゼロクリアしてるくらいしかわかんねえな。 軽量化のために Object Pooling パターンでインスタンス使い回してそのための初期化なのかな、なんかデストラクタの第 2 引数に 0x1 ビット立ってたらメモリ開放しないみたいだし。 それかセキュリティ考えて念には念を?
閑話 メモリ開放前に機微情報はゼロクリアしろ
開放されたメモリからパスワードなんかの重要情報を読み取れたりするからね。
最適化によってメモリ開放される前に memsetでゼロ埋めしてもコンパイラはそれを消すこともあるから消し方も重要。
C11 で導入された memset_s で消そう、つーか C23 では memset_explict が追加されて今はこっちが推奨なんだっけ。
これは C11 が memset_s の実装を任意とするなんてやってしまったせい。
まだ界隈のジジイどもが元気だったのでバイク小屋論争で揉めたんですわ、俺の覚えてるだけで似たようなものがこんだけある。
SecureZeroMemory… Windowsmemzero_explicit… Linuxexplicit_bzero… Oexplicit_memset… N
うん volatile 指定した関数ポインタ経由で memset 呼ぶのが一番移植性が高いわクソが。
閑話休題
他は COM 関連だと思われるので俺の現時点での知識じゃ何やってるのかさっぱり、ただ unknown5 の型は HANDLE らしいというのは判った。
0xffffffff は無効なハンドルを指す INVALID_HANDLE マクロの値だもんね。
コンストラクタとデストラクタ以外のメンバ関数も第 1 引数を Unko1 * にして可読性を高めていくのだけど、この際にちょっと注意が必要。
C と C++ では呼び出し規約が異なる
通常 Visual C/C++ においてメンバ関数の呼び出し規約はコンストラクタの話でもちょっと触れたけど __thiscall である。
これは第 1 引数の this をスタックに積まずレジスタ (x86 の場合は ECX) で経由で渡す、これによってスタックのプッシュポップのコストを削減する。
C++ のメンバ関数は C のフリー関数より常に 1 個引数が多いわけで、このプッシュポップのコストも塵も積もれば山となり性能面でのデメリットも無視できなかったんだろう。
しかし COM インタフェースは C からも呼び出す必要があるので C ABI である必要がある、よって __thiscall は使えないのだ。
C 側はレジスタに this がレジスタ上にあるなんて知らないから仕方ないですな。
あとまんぐり返しこと C++ name mangling も C からは理解できないので禁止である。
そんで伝統的な C ABI では通常は引数は右から左にスタックに積んで(評価順は未定義)スタックの掃除は呼び出し元が行うことになっている、これを Microsoft は __cdecl と呼んでいる。
これは printf ライクな可変長引数の実装に必要なのだが、アセンブラにした時の可読性とバイナリサイズの最適化と性能面で不利なのだ。
それに可変長引数は型安全が死ぬからな、va_list とか悪夢としか言いようがない。
閑話 モダン()な言語は __cdecl の悪夢は繰り返さない
Java や Pascal のように可変長引数が無い言語は可読性のためにスタックは左から右に積み(評価順も左から右)、コードサイズの最適化と性能の為にスタックの掃除は呼ばれた関数が実行する。
まぁ理想と現実は違うので Java も Delphi も可変長引数もどきを実装する羽目になるのだが、これらは配列あるいはオブジェクトを使って実装してるので ABI はそのままである。
Java5 で java.io.PrintStream#printf が生えた時はマジ腹抱えて笑った、みんな C 好きなんすね~ゴスリングの負け。
それにしてもスタックに積む順番ごときで可読性が~って凄い時代なのだが、プログラマってのは 8086 アセンブラの Intel 表記と AT&T 表記どちらが優れているかで延々レスバできる悲しい生き物である。
閑話休題 __stdcall の誕生
当時を知らない世代には信じられないだろうが、当時は「本物のプログラマはPascalを使わない」どころか Visual C++ の前身の Microsoft C/C++ は Delphi の前身 Turbo Pascal に遅れをとってたのだ。
なので Windows NT 3.1 で 32bit 化するタイミングで伝統的な C ABI を捨て、新たに __stdcall を採用することにした。
右から左にスタックに積むのは従来通りだけど、スタックの掃除は Pascal 同様に呼ばれた関数が行うようにして Pascal と同等のバイナリサイズと性能を実現するものだった。
printf のような可変長引数がある関数についてのみ __cdecl が使われる。
ちなみに __stdcall と一緒に __fastcall という引数のうち右ふたつをレジスタ渡しにする呼び出し規約も追加されている、それに対して Boland はそれならうちはみっつにしてデフォルトにする!だとか Watcom はうちなんかよっつだぜ!子供のような争いが繰り広げられるのだがいい加減にしろ。
ブレードランナーの屋台のおっちゃんの「ふたつで十分ですよ、わかって下さいよ」はこのレジスタ数競争を諫めていた…?
それでもなお Windows プログラミングするなら Visual Studio より Delphi という風潮はヘルスバーグが Borland をリストラ(正しい用法)され Microsoft に拾われた後もしばらく続いた。 これは単純に RAD としての出来の良さがダンチだったから。ポトペタで猿でも画面作れるしコンパイルも速いし VCL は静的リンクだからランタイムも不要で DLL 地獄とは無縁だったし。 でも Borland が突然自ら命を絶ってしてしまったのでそんな時代もおしまい、国を失ったディアスポラは無謀にも Java に流れるのだけど Java の開発環境も Delphi で書かれてた Visual Cafe と初期の JBuilder 以外はすべてゴミでやんした。 その中でも Interstage Apworks だけは絶対に許さない。
というわけで COM インタフェースの ABI は __thiscall でも __cdecl でもなく __stdcall になる。
よってクラスメンバ関数だからと __thiscall に脳死で変更すると解析結果がぶっ壊れてしまう。
void FUN_02005f68(Unko1 *this,undefined4 *param_2,undefined4 *param_3)
{
FUN_020034cb((HANDLE)this->unknown5,this->unknown2,this->unknown3,this->unknown4,param_2,param_3);
return;
}
これを __thiscall にすると
void __thiscall FUN_02005f68(void *this,undefined4 *param_1,undefined4 *param_2)
{
undefined4 *in_stack_0000000c;
FUN_020034cb((HANDLE)param_1[8],param_1[2],param_1[3],param_1 + 4,param_2,in_stack_0000000c);
return;
}
レジスタに this は無いので this が Unko1 * でなく void * に化け、スタックもいっこズレるから苦し紛れのつじつま合わせで in_stack_0000000c なんてローカル変数が生えてしまう。
残りもいっきに片付ける
そんじゃ残り 3 つも一気に行こうか。
void FUN_02005f68(Unko1 *this,undefined4 *param_2,undefined4 *param_3)
{
FUN_020034cb((HANDLE)this->unknown5,this->unknown2,this->unknown3,this->unknown4,param_2,param_3);
return;
}
void FUN_0200aec6(Unko1 *this)
{
FUN_02002bfe((HANDLE)this->unknown5,this->unknown2,this->unknown3,this->unknown4);
InterlockedIncrement(&this->unknown1);
return;
}
LONG FUN_02002dca(Unko1 *this)
{
int iVar1;
LONG LVar2;
iVar1 = FUN_02002c2c((HANDLE)this->unknown5,this->unknown2,this->unknown3,this->guid);
LVar2 = InterlockedDecrement(&this->unknown1);
if ((iVar1 == 0) || (LVar2 == 0)) {
(**(code **)((int)this->vftbl + 0xc))(1);
LVar2 = 0;
}
else {
LVar2 = this->unknown1;
}
return LVar2;
}
どうやら 2 つめと 3 つめは対になってる、InterlockedIncrement/InterlockedDecrement はアトミックに引数で指定した値を 1 足し引きするもの。
これでこのインスタンスが複数のスレッドで共有されていることが判る。this->unknown1 はその参照カウントっぽいですね。
ここで COM の仕様を読み返すと IUnknown インタフェース(抽象クラス)には 3 つのメンバ関数があることを思い出す。
- QueryInterface … オブジェクトでサポートされているインターフェイスへのポインターを取得する
- AddRef … COM オブジェクトへのインターフェイス ポインターの参照カウントをインクリメントする
- Release … COM オブジェクトのインターフェイスの参照カウントをデクリメントする
おぉ、当たってんじゃーん、ということで Unko1 は IUnknown だったということ、Unko 1 は I Unko で大体合ってた。
ということで Symbol Tree と Data Type から
- Symbol Tree で クラス
Unko1をIUnknownに - Data Type で
Unko1構造体をIUnknownに undefined4 unknown1をLONG refcountundefined4 unknown4をCLSID *clsidundefined4 unknown5をHANDLE handle
と変更し、メンバ関数のプロトタイプも調整すると
VFTBL_IUnknown1 XREF[2]: FUN_02002d93:02002d9c(*),
IUnknown:02002e2c(*)
02001384 68 5f 00 02 addr IUnknown::QueryInterface
02001388 c6 ae 00 02 addr IUnknown::AddRef
0200138c ca 2d 00 02 addr IUnknown::Release
02001390 60 2e 00 02 addr IUnknown::~IUnknown
IUnknown * __thiscall
IUnknown::IUnknown(IUnknown *this,CLSID *clsid,undefined4 param_2,undefined4 param_3)
{
ushort uVar1;
this->vftbl = &VFTBL_IUnknown1;
this->refcount = 1;
this->unknown2 = param_2;
this->unknown3 = param_3;
/* this->clsid = *clsid; */
(this->clsid).Data1 = clsid->Data1;
uVar1 = clsid->Data3;
(this->clsid).Data2 = clsid->Data2;
(this->clsid).Data3 = uVar1;
*(undefined4 *)(this->clsid).Data4 = *(undefined4 *)clsid->Data4;
*(undefined4 *)((this->clsid).Data4 + 4) = *(undefined4 *)(clsid->Data4 + 4);
this->handle = (HANDLE)INVALID_HANDLE;
FUN_02002ca6(param_2,param_3,this,(int *)&this->clsid);
return this;
}
HRESULT IUnknown::QueryInterface(IUnknown *this,IID *riid,void **ppvObject)
{
int iVar1;
iVar1 = FUN_020034cb(this->handle,this->unknown2,this->unknown3,&this->clsid,riid,ppvObject);
return iVar1;
}
ULONG IUnknown::AddRef(IUnknown *this)
{
ULONG UVar1;
FUN_02002bfe(this->handle,this->unknown2,this->unknown3,(int *)&this->clsid);
UVar1 = InterlockedIncrement(&this->refcount);
return UVar1;
}
ULONG IUnknown::Release(IUnknown *this)
{
int iVar1;
LONG LVar2;
ULONG UVar3;
iVar1 = FUN_02002c2c(this->handle,this->unknown2,this->unknown3,&this->clsid);
LVar2 = InterlockedDecrement(&this->refcount);
if ((iVar1 == 0) || (LVar2 == 0)) {
(**(code **)((int)this->vftbl + 0xc))(1);
UVar3 = 0;
}
else {
UVar3 = this->refcount;
}
return UVar3;
}
void __thiscall IUnknown::~IUnknown(IUnknown *this,byte param_1)
{
FUN_02002d93(this);
if ((param_1 & 1) != 0) {
operator_delete(this);
}
return;
}
仮想関数テーブル自体も構造体として登録し直す
だいぶスッキリしたね、でも一点気になるのがここ。
(**(code **)((int)this->vftbl + 0xc))(1);
参照カウンタがゼロなら仮想関数テーブル経由でデストラクタを呼んでるのだけど void * としてしか認識してないからこうなる。
それに this がレジスタ渡しなのを知らないから第 1 引数が消えちゃってるよね。
ということで仮想関数テーブルをメモリアドレスの配列ではなく構造体として登録してやる。
struct IUnknownVFTBL {
void *QueryInterface;
void *AddRef;
void *Release;
void *~IUnknown;
};
VFTBL_IUnknown1 XREF[2]: FUN_02002d93:02002d9c(*),
IUnknown:02002e2c(*)
02001384 68 5f 00 IUnknown
02 c6 ae
00 02 ca
02001384 68 5f 00 02 void * IUnknown::QueryInterface QueryInterface XREF[2]: FUN_02002d93:02002d9c(*),
IUnknown:02002e2c(*)
02001388 c6 ae 00 02 void * IUnknown::AddRef AddRef
0200138c ca 2d 00 02 void * IUnknown::Release Release
02001390 60 2e 00 02 void * IUnknown::~IUnknown ~IUnknown
ULONG IUnknown::Release(IUnknown *this)
{
int iVar1;
LONG LVar2;
ULONG UVar3;
iVar1 = FUN_02002c2c(this->handle,this->unknown2,this->unknown3,&this->clsid);
LVar2 = InterlockedDecrement(&this->refcount);
if ((iVar1 == 0) || (LVar2 == 0)) {
(*this->vftbl->~IUnknown)(1);
UVar3 = 0;
}
else {
UVar3 = this->refcount;
}
return UVar3;
}
アドレスのオフセットでなくなり読み易くなった、ただし this->vftbl->~IUnknown をクリックしても IUnknown::~IUnknown に飛べないので不便といっちゃ不便。
メンバ関数のシグネチャも登録する
さらに
// C++ の場合
using IUnknown_delete = void (*IUnknown::*)(byte);
/* C の場合 */
typedef void /*__thiscall*/ (*IUnknown_delete)(IUnknown *, byte);
の定義も追加する。


そうしたらちゃんと __thiscall であることを理解してくれた。
(*this->vftbl->~IUnknown)(this,1);
いかがでしたか?
これ Unix C がちょっと書けるだけの俺によるプログラミング言語 C によるオブジェクト指向プログラミング講座の間違いでは?
何も言い返せなかったわ…
Ghidra を使って C++ で書かれたコードをリバースエンジニアリングするにはまず C でオブジェクト指向プログラミングができるようになる必要があるのだ。
C でオブジェクト指向は X Window System なんかで多くの先人たちにゲロを吐かせてきた汚物なんだけど、C++ も C と本質的にはなんら変わりないただの糖衣構文でしかないからね。 前回ちょっと触れたラムダ式もコンパイラがキャプチャ変数をメンバ変数に持つ無名クラスを作りそいつにラムダ式を関数として実装するだけだもの。
俺は C でも Perl でもオブジェクト指向プログラミングを苦も無くやってきたので C++ や他の LL 使うモチベーションが何一つ無いのだ、みんなもやろうよ C でオブジェクト指向プログラミング!
次回
こんな感じの作業を残りの仮想関数テーブルの数だけ続けていかねばならないのである、地獄なんやな。
次は COM インタフェースに紐づけられた CLSID/REFIID を探す作業をするはずである。 できれば MIDL を書くとこまで進めたいがもう飽きて投げ出す可能性が非常に高まっている。