過去回で CtDCIFCE.DLL の呼び出しに COM/OLE Viewer からぶっこ抜いた MIDL を使ってたんだが、ポインタを long で扱ってるから 64bit で死ぬわアレ。

	[
	  odl,
	  uuid(C3ED4631-2768-43E0-BEBC-1C399989E666),
	  helpstring("IDevConDevice2 Interface")
	]
	interface IDevConDevice2 : IUnknown {
		[helpstring("method GetNumDevices")]
		HRESULT _stdcall GetNumDevices(long* pdwNumDevices);
		[helpstring("method GetFirstDevice")]
		HRESULT _stdcall GetFirstDevice(long* pdwDevice);
		[helpstring("method GetNextDevice")]
		HRESULT _stdcall GetNextDevice(
						long dwCurrentDevice,
						long* pdwNextDevice);
		[helpstring("method GetDeviceInfo")]
		HRESULT _stdcall GetDeviceInfo(
						long dwDevice,
						long* pDeviceInfo);
		[helpstring("method GetDeviceInfoEx")]
		HRESULT _stdcall GetDeviceInfoEx(
						long dwDevice,
						long* pDeviceInfoEx);
…

LP64 な Unix で長年生きてきたせいでうっかり long だから 32bit と 64bit どちらでもヨシ!ってスルーしてしまってたわガハハ。

Windows は LLP64 とかいうクソの煮凝りなので

  • Win32 API であれば DWORD は 64bit 環境でも DWORD32 のエイリアスなので 64bit で DWORD64 になるエイリアスが必要なら DWORD_PTR を使う
  • C 標準なら long は 64bit 環境でも 32bit integer なので 64bit integer が必要なら long long か intptr_t を使う

必要があるということ。

Ghidra は型推論がよわよわで ILP32 ならほとんど int と判定されてしまうのも併せて全部コード読まんとならんから気が遠くなってきた。

ひたすらインタフェースの精査

そんじゃ Unko1から順に読んでいくか。

HRESULT Unko1::FUN_0200370a(Unko1 *this,int param_2,int *param_3)

{
  HRESULT HVar1;
  HRESULT HVar2;
  undefined4 extraout_ECX;
  undefined8 uVar1;
  int local_c;
  int local_8;
  
  local_c = 0;
  local_8 = 0;
  HVar1 = E_FAIL;
  if (this->handle != (HANDLE)INVALID_HANDLE) {
    uVar1 = FUN_02003566(this->handle,this->unknown2,this->unknown3,param_2,param_2 >> 0x1f,&local_c
                        );
    HVar1 = (HRESULT)uVar1;
    if (-1 < HVar1) {
      HVar2 = SimpleFactory(extraout_ECX,(int)((ulonglong)uVar1 >> 0x20),local_c,local_8,&GUID_Unko2,
                           &this);
      *param_3 = (int)this;
      return HVar2;
    }
  }
  *param_3 = 0;
  return HVar1;
}

そんでまたしても謎のローカル変数 extraout_ECX が発生していて SimpleFactory の第 1 引数として渡していて頭を抱える。 こいつは前回 COM インタフェースと GUID の対応を調べるために読んだ GUID からオブジェクトを作る Simple Factory パターンのコードである。

Ghidra の呼出規約の復元は信用するな

まず最初に疑ったのは前回もでてきた呼出規約である。

  • __stdcall であれば this はスタック渡し
  • __thiscall であれば this はレジスタ渡し

よって __stdcall の場合スタック渡しされた this を ECX レジスタにコピーするのだけど、そのコードが正しく解釈できてない可能性がある。

じゃあいアセンブラ読もうか、Ghidra の逆アセンブルコードって Intel 記法ベースの上に婉曲表現で理解しづらいなこれ。

                             **************************************************************
                             *                          FUNCTION                          *
                             **************************************************************
                               HRESULT __stdcall FUN_0200370a(Unko1 * this, long param_
                               assume FS_OFFSET = 0xffdff000
             HRESULT           EAX:4          <RETURN>
             Unko1 *           Stack[0x4]:4   this                                    XREF[3]:     02003711(R), 
...
                             Unko1::FUN_0200370a                             XREF[1]:     020013a0(*)  
…
        0200370c 55              PUSH       EBP
        0200370d 8b ec           MOV        EBP,ESP
…
        02003711 8b 4d 08        MOV        ECX,dword ptr [EBP + this]
…
  • this は Stack[0x4]:4 つまり スタックフレーム + 4 の位置にある 4byte データ
  • EAX レジスタのリターンアドレス 4byte が退避されてるからスタックフレームは [EBP + 4] の位置
  • よって [EBP + this] は [EBP + 8] の意味、通常ここには第 1 引数が積まれる
  • MOVE ECX, [EBP + 8] つまりスタックに積まれた第 1 引数 this を ECX レジスタにコピーしている

ということで推測は正しかった。

そんで SimpleFactory は IUnknown のメンバ関数で __thiscall ちゅうことかと思ったのだが、開けてびっくり __fastcall になっとるんよ。

void __fastcall
FUN_02002e86(undefined4 param_1,int param_2,int param_3,int param_4,GUID *param_5,
            undefined4 *param_6)

__thiscall が誤判定されてるだけか思ったが、よくよく調べたらフリー関数で __stdcall な関数みたいなんだわ。

整理すると

  • Unko1::FUN_0200370a はクラスのメンバ変数だけど COM インタフェースなので __stdcall で宣言されてる
  • よってコンパイラがスタックに置かれた第 1 引数の this を 改めて ECX レジスタに置きなおした
  • Ghidra はそのコードをみて SimpleFactory の呼出のために置いたと勘違いし __fastcallと誤判定

ってこと。

それに呼び元で戻り値がをチェックしてるのに戻り値が void になっとるのも整合性がついとらん。

結局のところ SimpleFactory のプロトタイプは __stdcall であり

HRESULT SimpleFactory(undefine4, undefined4, IID*, void**);

が正しいのかなと推測した。

これが x86_64 なら最初の引数 4 個は必ずレジスタ渡しになるから呼出し規約なんて有名無実化してるので誤判定なぞ起きようが無い。 幸運なことに今回は 64bit 版の CtDPROXY.DLL もあるのだから、そっち解析すれば正しい答えが出るよね。

x86_64 でも基本 x86 と同じ手順だからそこの解説は省略させていただく、ただしコンパイラの進化によって以下の違いがあることを知らないと大変だと思われる。

  • 最適化ががっつり効いてて GUID 定数が全部インライン展開され .text セクションにそもそも存在しないから、GUID の先頭 64bit でメモリ検索しないと探せない
  • コンストラクタもデストラクタもすべてインライン展開されている
  • もはや inline キーワードは有名無実化しすきあらば性能の為にインライン展開される
  • ありとあらゆる最適化により鼻から悪魔が飛び出す
  • AddRef/Release での参照カウントの増減は InterlockedIncrement/InterlockedDecrement を使わず LOCK/UNLOCK によるクリティカルセクションで実装されている

今の子は大変だね(憐憫)、そんで見つかったプロトタイプがこちら。

HRESULT SimpleFactory(undefined8, IID*, void**);

あー引数 3 つで第 1 引数は 64bit でそれが 32bit + 32bit に解釈されて引数 4 つに化けてんだわ、これについてはなぜそうなるかは後で説明する。

32bit 非 thunking 版の方も見てみるとこっちも __fastcall に誤判定されてたけどそこ修正してやると

HRESULT SimpleFactory(undefined4, IID *,void **);

とやはり引数 3 つだったわ、正解出たね。

Thunking すなわち帳尻合わせの実例

前回軽く触れた thinking のサンプルやねこれ、コードを脳内で書き直すと以下のようになる。

#if defined(THUNKING)
typedef DWORD64 THUNK_DWORD_PTR;
#else
typedef DWORD_PTR THUNK_DWORD_PTR;
#endif

extern HRESULT FUN_02003566(HANDLE, THUNK_DWORD_PTR, THUNK_DWORD_PTR, THUNK_DWORD_PTR*);
extern HRESULT SimpleFactory(THUNK_DWORD_PTR, IID*, void**);

class IUnknown {
protected:
	DWORD_PTR refcount;
	THUNK_DWORD_PTR unknown;
	IID iid;
	HANDLE handle;
};

class Unko1 : IUnknown {
public:
	HRESULT FUN_0200370a(Unko1 *this, DWORD_PTR param_2, DWORD_PTR* param_3) {
		HRESULT ret = E_FAIL;
		THUNK_DWORD_PTR value = 0;
		Unko2 *unko2 = nullptr;

		if (this->handle != INVALID_HANDLE) {
			ret = FUN_02003566(this->handle, this->unknown, (THUNK_DWORD_PTR)param_2, &value);
			if (SUCCEEDED(ret))
				ret = SimpleFactory(value, &GUID_Unko2, (void **)&unko2);
		}
		*param_3 = (DWORD_PTR)unko2;
		return ret;
	}
	...
};

前回 IUnknown の末尾 4byte が未使用だったのが謎だったんだけど、IUnknown::handle も Thunking して 64bit で宣言してるからっぽい。 ただしこいつは DeviceIoControl の第 1 引数に渡すものだからする必要もないし、仮に 64bit 化する必要があるなら INVALID_HANDLE は使えないはず。 このマクロって 32bit では 0xffffffff 64bit なら 0xffffffffffffffff になるから互換性無いんだわ、this->handle != (THUNK_DWORD_PTR)-1 とする必要がある。

しかし LLP64 ってのは 32bit でも 64bit でも long は 32bit だからコードの移行が楽なんて言われてるけど、Thunking が必要なケースになると非常にめんどくせえなこれ。

ところでなぜ Ghidra の疑似 C ソースを修正せずにず脳内で処理したか、それは Ghidra がバグっとるから。

Ghidra は古いバイナリの解析が苦手でバグを踏みまくる

Unko1::FUN_0200370a から呼出されてるもうひとつの関数 FUN_02003566 は DeviceIoControl でドライバを叩くだけのコード、x86_64 だとインライン化され消えてしまう程度のもの。

ただこいつも Ghidra はまともに解釈できないのだ。

/* WARNING: Function: __alloca_probe replaced with injection: alloca_probe */

void FUN_02003566(HANDLE param_1,undefined4 param_2,undefined4 param_3,undefined4 param_4,
                undefined4 param_5,undefined4 *param_6)

{
  undefined1 local_58 [8];
  undefined4 local_50;
  undefined4 local_4c;
  undefined4 uStack_44;
  undefined4 local_40;
  undefined4 local_3c;
  undefined4 local_38;
  undefined4 local_34;
  undefined4 local_30;
  undefined4 local_2c;
  undefined4 local_28;
  undefined4 local_24;
  undefined4 uStack_1c;
  DWORD local_c;
  uint canary;
  
  canary = __security_cookie ^ (uint)&stack0xfffffffc;
  uStack_1c = 0x2003582;
  uStack_44 = 0x2003594;
  local_3c = 0;
  local_40 = 2;
  local_38 = param_2;
  local_34 = param_3;
  local_30 = param_4;
  local_2c = param_5;
  local_28 = *param_6;
  local_24 = param_6[1];
  DeviceIoControl(param_1,0xf7770000,&local_40,0x20,local_58,0x10,&local_c,(LPOVERLAPPED)NULL);
  *param_6 = local_50;
  param_6[1] = local_4c;
  __security_check_cookie(canary ^ (uint)&stack0xfffffffc);
  return;
}

こいつも HRESULT 型のステータスコードを返すはずなんだが戻り値の型が void になってしまっているね。

そして SimpleFactory の話にも出てきたけど、正しいプロトタイプは

HRESULT FUN_02003566(HANDLE, THUNK_DWORD_PTR, THUNK_DWORD_PTR, THUNK_DWORD_PTR*);

になるはずなのだが 64bit な引数が 32bit+32bit に化けてしまっている。これは命令が 32bit に分割されてしまってるからである。

たとえば以下のコードが

void foo(undefined8 param_1)
{
	undefined8 local_8 = param_1;

が以下のように化けて解析されてしまうケースがある。

void foo(undefined4 param_1, undefined4 param_2)
{
	undefined4  local_c = param_2;
	undefined4  local_8 = param_1;

movq よりも mov を 2 度呼ぶ方が命令サイズが小さい(ニチャァ)というサイズ最適化がまかり通ってた時代の産物ですな。 今のコンパイラなら SIMD の movlpd 使うからこの手のトラブルは無縁のはずである。

Unko1::FUN_0200370a でも疑似アセンブラ画面では undefined8 local_c なのに

                             **************************************************************
                             *                          FUNCTION                          *
                             **************************************************************
                               HRESULT __stdcall FUN_0200370a(Unko1 * this, long param_
…
             undefined8        Stack[-0xc]:8  local_c                                 XREF[3,2]:   02003714(RW), 
…
                             Unko1::FUN_0200370a                             XREF[1]:     020013a0(*)  
…
        02003714 83 65 f8 00     AND        dword ptr [EBP + local_c],0x0
        02003718 83 65 fc 00     AND        dword ptr [EBP + local_c+0x4],0x0
…

疑似 C ソース画面では undefined4 local_c と undefined4 uStack_8 に分かれてしまってる。

  undefined4 local_c;
  undefined4 uStack_8;
  
  local_c = 0;
  uStack_8 = 0;

さらに関数呼び出しの為にこの 64bit な変数を引数としてスタックに積むコードも疑似アセンブラでは正しく解釈されてるけど

        02003755 ff 75 fc        PUSH       dword ptr [EBP + local_c+0x4]
        02003758 ff 75 f8        PUSH       dword ptr [EBP + local_c]
        0200375b e8 26 f7        CALL       FUN_02002e86                                     HRESULT FUN_02002e86(longlong pa
                 ff ff

疑似 C ソース画面ではやっぱり CONCAT44(uStack_8,local_c) つまり 32bit+32bit に化けているのだ。

    HVar2 = FUN_02002e86(CONCAT44(uStack_8,local_c),(IID *)&DAT_02001264,&this);

Ghidra が CONCAT44(hi,lo) などというオレオレマクロを何の解説も無くお出ししてきたけど 4byte 同士を結合するという意味だろう、それなら ((undefined8)hi << 32 | lo) でいいだろ…

ローカル変数が化けるのはまだマシな部類で ECX レジスタに置かれた Unko1* this の 64bit なメンバ変数 this->unknown をスタックに積むという以下のコードはもっとヤバい。

        02003734 ff 71 0c        PUSH       dword ptr [ECX + 0xc]
        02003737 ff 71 08        PUSH       dword ptr [ECX + 0x8]
…
        0200373b e8 26 fe        CALL       FUN_02003566                                     undefined4 FUN_02003566(HANDLE p
                 ff ff

クラス Unko1 の継承元である IUnknown は C 構造体で書くと

struct Unko1 {
	void*	vftbl;		/* 0x0 */
	DWORD32	refcount;	/* 0x4 */
	DWORD64 unknown;	/* 0x8 */
	HANDLE	handle;		/* 0x10 */
	/* unknown 4byte */
}

という定義なので PUSH [ECX + 0xc] PUSH [ECX + 0x8] というコードは this->unknown を 32bit づつに分けてスタックに積んでることがお判りいただけるであろう。

しかし困ったことに疑似 C ソース画面からみると以下のように完全にぶっ壊れて表示されてしまうのだ。

     (iVar1 = FUN_02003566(this->handle,CONCAT44(param_2,*(undefined4 *)((int)&this->unknown + 4)),
                           CONCAT44(&local_c,param_2 >> 0x1f),unaff_ESI), -1 < iVar1)) {

this->unknown4 が CONCAT44(param_2,*(undefined4 *)((int)&this->unknown + 4)) に化けてしまっている、無関係の param_2 どっから出てきたんだ。 これおそらく [ECX + 0xc] が [EBX + 0xc] つまり第 2 引数に化けたんだろうな。

とうことで Ghidra で 32bit+32bit に化けてしまった処理を 64bit に戻そうと試みるのはこのバグが直るまで不可能である。 俺は困ってないのでバグ修正はしないし NSA と関わりたくないのでバグ報告もしないよ…

Ghidra はコンパイラがインジェクションしたコードの解釈があまり得意でない

そして謎のマジックナンバー 0x2003582 0x2003594 なんてのまで現れた。

  uStack_1c = 0x2003582;
  uStack_44 = 0x2003594;

これは大きなサイズのスタック要求に対して Visual C/C++ は alloca_probe を呼び出しスタックオーバーフローを検知しようとする。

        0200357d e8 ae a7        CALL       __alloca_probe                                   undefined __alloca_probe(void)
                 00 00
        02003582 8b c4           MOV        EAX,ESP
…
        0200358f e8 9c a7        CALL       __alloca_probe                                   undefined __alloca_probe(void)
                 00 00
        02003594 83 66 04 00     AND        dword ptr [ESI + Stack[-0x3c]],0x0

マジックナンバーは __alloca_probe 呼出の次の命令のアドレスですな、つまりこれも Ghidra のバグである。

大きなスタックサイズ要求というのは DeviceIoControl の引数となるローカル変数宣言 local_40 の 32byte(0x20) と local_58 16byte(0x10) である、全然小さいやんけ。

Ghidraは構造体までは復元できないので

  • 入力データ
  undefined4 local_40;
  undefined4 local_3c;
  undefined4 local_38;
  undefined4 local_34;
  undefined4 local_30;
  undefined4 local_2c;
  undefined4 local_28;
  undefined4 local_24;
  • 出力データ
  undefined1 local_58 [8];
  undefined4 local_50;
  undefined4 local_4c;

のようにバラバラのローカル変数となってしまうので、以下の構造体を Ghidra に登録する。

/* 入力データ */
typedef struct {
	DWORD32	abi_version_lo;
	DWORD32	abi_version_hi;
	DWORD32	unknown1_lo;
	DWORD32	unknown1_hi;
	DWORD32	unknown2_lo;
	DWORD32	unknown2_hi;
	DWORD32	unknown3_lo;
	DWORD32	unknown3_hi;
} FUN_02003566_In;

/* 出力データ */
typedef struct struct {
	DWORD32	unknown1_lo;
	DWORD32	unknown1_hi;
	DWORD32	unknown2_lo;
	DWORD32	unknown4_hi;
} FUN_02003566_Out;

本当は全部 DWORD64 で定義したいのだが前述の Ghidra の 32bit+32bit バグを踏むから hi/lo で分けて定義している、そんで

  • local_40 の型を FUN_02003566_In 名前を inData に
  • local_58 の型を FUN_02003566_Out 名前を outData に

変更した結果がこちら。

void FUN_02003566(HANDLE param_1,DWORD32 param_2_lo,DWORD32 param_2_hi,DWORD32 param_3_lo,
                 DWORD32 param_3_hi,DWORD32 *param_6)

{
  FUN_02003566_Out outData;
  FUN_02003566_In inData;
  DWORD local_c;
  uint canary;
  
  canary = __security_cookie ^ (uint)&stack0xfffffffc;
  inData.abi_version_hi = 0;
  inData.abi_version_lo = 2;
  inData.unknown1_lo = param_2_lo;
  inData.unknown1_hi = param_2_hi;
  inData.unknown2_lo = param_3_lo;
  inData.unknown2_hi = param_3_hi;
  inData.unknown3_lo = *param_6;
  inData.unknown3_hi = param_6[1];
  DeviceIoControl(param_1,0xf7770000,&inData,0x20,&outData,0x10,&local_c,(LPOVERLAPPED)NULL);
  *param_6 = outData.unknown3;
  param_6[1] = outData.unknown4;
  __security_check_cookie(canary ^ (uint)&stack0xfffffffc);
  return;
}

よっしゃ謎のマジックナンバー入れてる変数が消えた。

ちなみに DeviceIoControl は Unix でいうところの ioctl で In/Out はドライバ固有なので構造体のメンバがそれぞれ何を意味するのかはデータシートでも手元に無いと判らん。 inData の先頭 8byte はおそらく ABI バージョンだとは想像つくが他はさっぱり、俺はドライバ屋でもハード屋でもないし。 kX Project 2 の Emu10k オープンソースドライバを読めばわかるかもしれないが、あれどうやって調べたんだろうな。

そして戻り値を返すはずなのに返さない問題、こいつは過去回でも解説したのだが Visual C/C++ の /GS オプションすなわち Guard Stack によるスタック破壊からのリターンアドレス書換攻撃を防ぐためのインジェクションコードが悪さをしてるせい。

  • リターンアドレスの手前、スタックの底に置かれるカナリア、ローカル変数 canary
  • 乱数の入ってるグローバル変数 __security_cookie
  • カナリア変数が破壊されてないかをチェックする CRT の関数 __security_check_cookie
  • スタックベースポインタを表す stack0xfffffffc つまり EBP

こいつらは全部インジェクションコードなので Ghidra が消し込まないとならないのだがそこが盛大にバグっていることで発生する。

戻り値は 8byte(=64bit) のはずなのだが void に誤判定されてるからとりあえず DWORD64 に変更する。

DWORD64 FUN_02003566(HANDLE param_1,DWORD32 param_2_lo,DWORD32 param_2_hi,DWORD32 param_3_lo,
                    DWORD32 param_3_hi,DWORD32 *param_6)

{
  undefined4 extraout_EAX;
  undefined4 extraout_EDX;
…
  uint canary;
  
  canary = __security_cookie ^ (uint)&stack0xfffffffc;
…
  __security_check_cookie(canary ^ (uint)&stack0xfffffffc);
  return CONCAT44(extraout_EDX,extraout_EAX);
}

うーん謎の変数 extraout_EDX extraout_EAX が生えてしまった、そりゃ戻り値が 64bit なら EDX と EAX レジスタ使うけどさぁ。

過去回でもやった通り __security_check_cookie をインライン指定すると消えてしまった戻り値が見えるようになる。

  return CONCAT44(extraout_EDX,outData.unknown1_lo);

なるほど、outDataの値を返してるようだ、しかし extraout_EDX は消えてくれんな。

俺の第六感が

  return CONCAT44(outData.unknown2_hi,outData.unknown1_lo);

なんじゃね?と囁いているのでアセンブラ読むしかないっすね。

        020035e7 ff 15 54        CALL       dword ptr [->KERNEL32.DLL::DeviceIoControl]      = 0000e350
                 10 00 02
        020035ed 8b 47 08        MOV        EAX,dword ptr [EDI + outData.unknown3]
        020035f0 89 03           MOV        dword ptr [EBX],EAX
        020035f2 8b 47 0c        MOV        EAX,dword ptr [EDI + outData.unknown4]
        020035f5 89 43 04        MOV        dword ptr [EBX + 0x4],EAX
        020035f8 8b 07           MOV        EAX,dword ptr [EDI]=>outData
        020035fa 8d 65 ec        LEA        ESP=>local_18,[EBP + -0x14]
        020035fd 5f              POP        EDI
        020035fe 5e              POP        ESI
        020035ff 5b              POP        EBX
        02003600 8b 4d fc        MOV        ECX,dword ptr [EBP + canary]
        02003603 33 cd           XOR        ECX,EBP
        02003605 e8 ec e9        CALL       __security_check_cookie                          void __security_check_cookie(int
                 ff ff
             assume FS_OFFSET = <UNKNOWN>
        0200360a c9              LEAVE
        0200360b c2 18 00        RET        0x18
             assume FS_OFFSET = 0xffdff000

うーん EDX 触ってねえや、俺の第六感はカス…

そもそも戻り値が 8byte ってのが変なんだよね。

        02003732 52              PUSH       EDX
        02003733 50              PUSH       EAX
        02003734 ff 71 0c        PUSH       dword ptr [ECX + 0xc]
        02003737 ff 71 08        PUSH       dword ptr [ECX + 0x8]
        0200373a 56              PUSH       ESI
        0200373b e8 26 fe        CALL       FUN_02003566                                     undefined8 FUN_02003566(HANDLE p
                 ff ff
        02003740 85 c0           TEST       EAX,EAX
        02003742 7d 08           JGE        LAB_0200374c

呼出し前に PUSH EDX して退避してるとはいえ、呼出し後は TEST EAX,EAX だけで EDX レジスタ見てないし HRESULT か BOOL だよなこれ。

HRESULT FUN_02003566(HANDLE param_1,DWORD32 param_2_lo,DWORD32 param_2_hi,DWORD32 param_3_lo,
                    DWORD32 param_3_hi,DWORD32 *param_6)

{
…
  return outData.unknown1_lo;
}

##

そんで上で 64bit バイナリの解析を元に SimpleFactory は __fastcall ではなく __stdcall と断定したのだけど、実は EDX の値参照する謎の条件分岐があるのだ。

  IID local_18;
...
  local_18.Data1 = param_3->Data1;
  local_18.Data2 = param_3->Data2;
  local_18.Data3 = param_3->Data3;
  local_18.Data4._0_4_ = *(undefined4 *)param_3->Data4;
  local_18.Data4._4_4_ = *(undefined4 *)(param_3->Data4 + 4);
  iVar2 = FUN_0200b894(&local_18);
  piVar2 = FUN_02002821(param_3,param_4,iVar1,(int *)&DAT_0200f8f0);
  if ((piVar2 != (int *)0x0) && (iVar1 = FUN_02002d76((int)piVar2), iVar1 == 0)) {
    switch(*(undefined4 *)(extraout_EDX + 0xc)) {
    case 0:
    case 1:
    case 2:
    case 3:
    case 4:
    case 5:
    case 7:
    case 8:
    case 9:
    case 10:
    case 0xb:
    case 0xc:
    case 0xd:
    case 0xe:
    case 0xf:
      piVar2 = (int *)(*(int *)(extraout_EDX + 8) + 4);
      *piVar2 = *piVar2 + 1;
      break;
    case 6:
      piVar2 = (int *)(*(int *)(extraout_EDX + 8) + 8);
      *piVar2 = *piVar2 + 1;
    }
    *param_6 = *(undefined4 *)(extraout_EDX + 8);
    param_2 = extraout_EDX;
    goto LAB_02003474;
  }

こいつもアセンブラ読むしかねえな。

        02002ec9 50              PUSH       EAX
        02002eca a5              MOVSD      ES:EDI,ESI
        02002ecb e8 c4 89        CALL       FUN_0200b894                                     int FUN_0200b894(int * param_1)
                 00 00
        02002ed0 68 f0 f8        PUSH       DAT_0200f8f0
                 00 02
        02002ed5 50              PUSH       EAX
        02002ed6 ff 75 0c        PUSH       dword ptr [EBP + param_2]
        02002ed9 ff 75 08        PUSH       dword ptr [EBP + param_1]
        02002edc e8 40 f9        CALL       FUN_02002821                                     int * FUN_02002821(int param_1, 
                 ff ff
        02002ee1 8b d0           MOV        EDX,EAX
        02002ee3 85 d2           TEST       EDX,EDX
        02002ee5 74 35           JZ         LAB_02002f1c
        02002ee7 52              PUSH       EDX
        02002ee8 e8 89 fe        CALL       FUN_02002d76                                     undefined4 FUN_02002d76(int para
                 ff ff
        02002eed 85 c0           TEST       EAX,EAX
        02002eef 75 2b           JNZ        LAB_02002f1c
        02002ef1 8b 4a 0c        MOV        ECX,dword ptr [EDX + 0xc]
        02002ef4 83 f9 0f        CMP        ECX,0xf
        02002ef7 8b 42 08        MOV        EAX,dword ptr [EDX + 0x8]
        02002efa 77 0f           JA         switchD_02002efc::default

引数を右からスタックに積んで FUN_02002821 を呼び、戻り値 EAX を EDX においてゼロかどうかのテストで条件分岐やってる。 この MOV EDX,EAX を Ghidra は無視して新たな変数 extraout_EDX 生やしちゃってるけど piVar2 だよなぁこれ。

この部分のコードは GUID から対応する IUnknown 実装オブジェクトが生成済かをグローバル変数 DAT_0200f8f0 のリストから探している。 つまりこの SimpleFactory は Object Pooling パターンだけでなく Flyweight パターンないし Singleton でもあるみたい。

そして switch/case 文は GUID を元に Unko15 とそれ以外で処理を分岐してる。

switch (unko->unknown3) {
case 0x0: /* GUID_IUnknown */
case 0x1: /* GUID_Unko1 */
case 0x2: /* GUID_Unko2 */
case 0x3: /* GUID_Unko3 */
case 0x4: /* GUID_Unko4 */
case 0x5: /* GUID_Unko5 */
case 0x7: /* GUID_Unko6 */
case 0x8: /* GUID_Unko7 */
case 0x9: /* GUID_Unko8 */
case 0xa: /* GUID_Unko9 */
case 0xb: /* GUID_Unko10 */
case 0xc: /* GUID_Unko11 */
case 0xd: /* GUID_Unko12 */
case 0xe: /* GUID_Unko13 */
case 0xf: /* GUID_Unko14 */
…
	piVar1 = (int *)(unko->unknown2 + 4);
	*piVar1 = *piVar1 + 1;
	break;
case 6:
	piVar1 = (int *)(unko->unknown2 + 8);
	*piVar1 = *piVar1 + 1;
	break;
case 0x6: /* GUID_Unko15 */
…
	break;
}
…

前回 Unko15 には仮想関数テーブルが 2 つ実装されているという話をしたけど、この部分は GUID に応じてメンバのオフセットを変える Thunking 処理ということですな。 要するに多重継承のキャストと同じである。

次回

このままクッソ地味な作業が続くので、先に書換禁止の .text でなく書換可能な .data からインタフェース名がらしきものがボロボロ出てきたからそこ調べるかな。

0200f344		ds "IDrvAdapterMgr"	s_IDrvAdapterMgr_0200f344
0200f368		ds "IDrvDevice"	s_IDrvDevice_0200f368
0200f38c		ds "IDrvProfile"	s_IDrvProfile_0200f38c
0200f3b0		ds "IDrvBMixer"	s_IDrvBMixer_0200f3b0
0200f3d4		ds "IDrvBMixerLine"	s_IDrvBMixerLine_0200f3d4
0200f3f8		ds "ICallbackManager"	s_ICallbackManager_0200f3f8
0200f41c		ds "IDrvWaveOut"	s_IDrvWaveOut_0200f41c
0200f440		ds "IDrvWaveOutStream"	s_IDrvWaveOutStream_0200f440
0200f464		ds "IDrvSBLive"	s_IDrvSBLive_0200f464
0200f488		ds "IDrvRemote"	s_IDrvRemote_0200f488
0200f4ac		ds "IDrvFeatureSet"	s_IDrvFeatureSet_0200f4ac
0200f4d0		ds "IUnknown"	s_IUnknown_0200f4d0
0200f4f4		ds "IDrvALDevice"	s_IDrvALDevice_0200f4f4
0200f518		ds "IDrvALContext"	s_IDrvALContext_0200f518
0200f560		ds "IDrvBMixerLine2"	s_IDrvBMixerLine2_0200f560
02001168	s_IDriver_02001168	ds "IDriver"	s_IDriver_02001168
0200161c	s_IDrvALContext_0200161c	ds "IDrvALContext"	s_IDrvALContext_0200161c
02001500	s_IDrvALDevice_02001500	ds "IDrvALDevice"	s_IDrvALDevice_02001500
020013b8	s_IDrvDevice_020013b8	ds "IDrvDevice"	s_IDrvDevice_020013b8