x86コードとx64コードの混合

Windowsの64bit版にはWow64が用意されていて、64bit(x64)版にも関わらず32bit(x86)のイメージを実行できる。

しかしながら、32bitのコードと64bitのコードを混合して実行する方法は公式には用意されていないのである。

そこで、32bitプロセス内で64bitコードを実行する方法について少しばかり研究してみた。

ただ、Googleで検索する限り、既存の研究結果が存在するようである:

http://www.corsix.org/content/dll-injection-and-wow64

https://int0h.wordpress.com/2009/12/24/the-power-of-wow64/

https://int0h.wordpress.com/2011/02/22/anti-anti-debugging-via-wow64/

今回はこれらとは独立した視点から考察してみたいと思う。

この記事内にあるソースは https://github.com/rwfpl/rewolf-wow64ext の一部である。

サンプルソースはここにある。

x86 <-> x64 間の遷移

x86 <-> x64 間での遷移を簡単に確認する方法としては、x86バージョンとx64バージョンのNTDLL.DLL内のシステムコールを比較してみればよい。

x86 Win7上の32bit ntdllのシステムコール Win7 Wow64上の32bit ntdllのシステムコール
mov     eax, X

mov     edx, 7FFE0300h
call    dword ptr [edx]
        ;ntdll.KiFastSystemCall

retn    Z
mov     eax, X
mov     ecx, Y
lea     edx, [esp+4]
call    dword ptr fs:[0C0h]
        ;wow64cpu!X86SwitchTo64BitMode
add     esp, 4
ret     Z

見ての通り、64bitのシステムでは、通常のx86システムコールで使用されるntdll.KiFastSystemCallを経由せずに、fs:[0C0h] (wow64cpu!X86SwitchTo64BitMode)を経由している。

wow64cpu!X86SwitchTo64BitMode は単純に64bitセグメントへのFar Jumpとして実装されている:

	wow64cpu!X86SwitchTo64BitMode
	748c2320 jmp      0033:748C271E; wow64cpu!CpupReturnFromSimulatedCode

これは、64bitバージョンのWindowsでx64モードとx86モードを切り替えるいわば”魔法”である。さらに面白い点は、非Wow64プロセス(標準の64bitプロセス)でも動作可能であるので、32bitコードを64bitプロセス上で実行することができるという点である。要は、64bitプロセス上で動作するすべてのプロセス(x86 & x64)には、2つのコードセグメントが割り当てられているということである。

  • CS = 0x23 -> x86モード
  • CS = 0x33 -> x64モード

32bitプロセス内でのx64コードの実行

まずは、64bitコードの先頭と最後をマークするマクロをいくつか用意する。

#define EM(a) __asm __emit (a)

#define X64_Start_with_CS(_cs) \
{ \
	EM(0x6A) EM(_cs)                     /*  push   _cs                   */ \
	EM(0xE8) EM(0) EM(0) EM(0) EM(0)     /*  call   $+5                   */ \
	EM(0x83) EM(4) EM(0x24) EM(5)        /*  add    dword [esp], 5        */ \
	EM(0xCB)                             /*  retf                         */ \
}

#define X64_End_with_CS(_cs) \
{ \
	EM(0xE8) EM(0) EM(0) EM(0) EM(0)     /*  call   $+5                   */ \
	EM(0xC7) EM(0x44) EM(0x24) EM(4)     /*                               */ \
	EM(_cs) EM(0) EM(0) EM(0)            /*  mov    dword [rsp + 4], _cs  */ \
	EM(0x83) EM(4) EM(0x24) EM(0xD)      /*  add    dword [rsp], 0xD      */ \
	EM(0xCB)                             /*  retf                         */ \
}

#define X64_Start() X64_Start_with_CS(0x33)
#define X64_End() X64_End_with_CS(0x23)

X64_Start() マクロを実行後直ちにCPUx64モードに切り替えられ、X64_End() マクロでx86モードに戻る。これらのマクロはfar return命令のおかげで位置に依存しない。

x64バージョンのAPIを呼び出すことにも使える。私はx64バージョンのKERNEL32.DLLを読み込もうとしたが失敗した。どうやらNT Native APIだけを使う必要があるようだ。x64バージョンのKERNEL32.DLLを読み込み上での問題点としては、既に読み込まれているx86バージョンのKERNEL32.DLLと干渉することにある。x64バージョンのKERNEL32.DLLには読み込みを妨害するチェックがあるようである。kernel32!BaseDllInitializeをフックしてチェックを回避すれば使えるのだと思うが、非常に骨が折れる作業である。Windows Vista上では読み込みが可能であった。一方Windows 7上では不可能になっていた。

本題に戻って、メモリにNTDLL.DLLx64バージョンを配置するために必要なNT Native APIを使用してみよう。これを実現するためには_PEB_LDR_DATA構造体からInLoadOrderModuleListをパースしなければならない。64bitの_PEBは64bitの_TEBから取得することができる。64bitの_TEBはx86上での取得方法と類似している(x64上ではfsセグメントの代わりにgsセグメントを利用する):

	mov   eax, gs:[0x30]

しかし、もっと簡単な方法がある。なぜなら、wow64cpu!CpuSimulate (CPUx86モードに切り替える担当の関数)がgs:[0x30]の値をr12レジスタに保存してくれているので、これを利用させていただくことにする。そして、私が実装したgetTEB64()は以下のようになった:

union reg64
{
	DWORD dw[2];
	DWORD64 v;
};

//x64レジスタのpushを簡単にするマクロ
#define X64_Push(r) EM(0x48 | ((r) >> 3)) EM(0x50 | ((r) & 7))

WOW64::TEB64* getTEB64()
{
	reg64 reg;
	reg.v = 0;

	X64_Start();
	
	X64_Push(_R12);
	//x64モードになったからpopでQWORDが出てくる
	__asm pop reg.dw[0]
	X64_End();

	//Wow64プロセスならレジスタ上位は0であったほうがいい
	if (reg.dw[1] != 0)
		return 0;

	return (WOW64::TEB64*)reg.dw[0];
}

WOW64名前空間は“os_structs.h”で定義されている。このファイルはサンプルソースの中にある。

64bitのNTDLL.DLLの位置を取得する関数は次のように定義される:

DWORD getNTDLL64()
{
	static DWORD ntdll64 = 0;
	if (ntdll64 != 0)
		return ntdll64;

	WOW64::TEB64* teb64 = getTEB64();
	WOW64::PEB64* peb64 = teb64->ProcessEnvironmentBlock;
	WOW64::PEB_LDR_DATA64* ldr = peb64->Ldr;

	printf("TEB: %08X\n", (DWORD)teb64);
	printf("PEB: %08X\n", (DWORD)peb64);
	printf("LDR: %08X\n", (DWORD)ldr);

	printf("Loaded modules:\n");
	WOW64::LDR_DATA_TABLE_ENTRY64* head = \
		(WOW64::LDR_DATA_TABLE_ENTRY64*)ldr->InLoadOrderModuleList.Flink;
	do
	{
		printf("  %ws\n", head->BaseDllName.Buffer);
		if (memcmp(head->BaseDllName.Buffer, L"ntdll.dll",
			   head->BaseDllName.Length) == 0)
		{
			ntdll64 = (DWORD)head->DllBase;
		}
		head = (WOW64::LDR_DATA_TABLE_ENTRY64*)head->InLoadOrderLinks.Flink;
	}
	while (head != (WOW64::LDR_DATA_TABLE_ENTRY64*)&ldr->InLoadOrderModuleList);
	printf("NTDLL x64: %08X\n", ntdll64);
	return ntdll64;
}

NT Native APIの呼び出しを完璧なものにするためにはGetProcAddressと同等なものが必要になる。これはntdll!LdrGetProcedureAddressを代用することで解決できる。次のコードはLdrGetProcedureAddressのアドレスを取得する:

DWORD getLdrGetProcedureAddress()
{
	BYTE* modBase = (BYTE*)getNTDLL64();
	IMAGE_NT_HEADERS64* inh = \
		(IMAGE_NT_HEADERS64*)(modBase + ((IMAGE_DOS_HEADER*)modBase)->e_lfanew);
	IMAGE_DATA_DIRECTORY& idd = \
		inh->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
	if (idd.VirtualAddress == 0)
		return 0;

	IMAGE_EXPORT_DIRECTORY* ied = \
		(IMAGE_EXPORT_DIRECTORY*)(modBase + idd.VirtualAddress);

	DWORD* rvaTable = (DWORD*)(modBase + ied->AddressOfFunctions);
	WORD* ordTable = (WORD*)(modBase + ied->AddressOfNameOrdinals);
	DWORD* nameTable = (DWORD*)(modBase + ied->AddressOfNames);

	for (DWORD i = 0; i < ied->NumberOfFunctions; i++)
	{
		if (strcmp((char*)modBase + nameTable[i], "LdrGetProcedureAddress"))
			continue;
		else
			return (DWORD)(modBase + rvaTable[ordTable[i]]);
	}
	return 0;
}

これのおまけとしてx86 C/C++コードからx64 NT Native APIを直接呼び出すことができるヘルパー関数を作ってみた:

DWORD64 X64Call(DWORD func, int argC, ...)
{
	va_list args;
	va_start(args, argC);
	DWORD64 _rcx = (argC > 0) ? argC--, va_arg(args, DWORD64) : 0;
	DWORD64 _rdx = (argC > 0) ? argC--, va_arg(args, DWORD64) : 0;
	DWORD64 _r8 = (argC > 0) ? argC--, va_arg(args, DWORD64) : 0;
	DWORD64 _r9 = (argC > 0) ? argC--, va_arg(args, DWORD64) : 0;
	reg64 _rax;
	_rax.v = 0;

	DWORD64 restArgs = (DWORD64)&va_arg(args, DWORD64);

	//インラインアセンブリで楽にするためにQWORDにしておく
	DWORD64 _argC = argC;
	DWORD64 _func = func;

	DWORD back_esp = 0;

	__asm
	{
		;//back_espに元のespを保持する
		mov    back_esp, esp

		;//espを8バイトアライメントする
		and    esp, 0xFFFFFFF8

		X64_Start();

		;//最初の4つの引数を埋める
		push   _rcx
		X64_Pop(_RCX);
		push   _rdx
		X64_Pop(_RDX);
		push   _r8
		X64_Pop(_R8);
		push   _r9
		X64_Pop(_R9);

		push   edi

		push   restArgs
		X64_Pop(_RDI);

		push   _argC
		X64_Pop(_RAX);

		;//残りの引数をスタックに入れる
		test   eax, eax
		jz     _ls_e
		lea    edi, dword ptr [edi + 8*eax - 8]

		_ls:
		test   eax, eax
		jz     _ls_e
		push   dword ptr [edi]
		sub    edi, 8
		sub    eax, 1
		jmp    _ls
		_ls_e:

		;//退避用スタックエリア作成
		sub    esp, 0x20

		call   _func

		;//スタックを掃除
		push   _argC
		X64_Pop(_RCX);
		lea    esp, dword ptr [esp + 8*ecx + 0x20]

		pop    edi

		;//戻り値をセット
		X64_Push(_RAX);
		pop    _rax.dw[0]

		X64_End();

		mov    esp, back_esp
	}
	return _rax.v;
}

少し長いができるだけシンプルに作成。最初の引数は呼び出したいx64関数のアドレス、2番目には引数の数である。残りの引数は呼び出される関数による。残りの引数はすべてDWORD64にキャストしておかないといけない。

X64Call()の簡単な使い方:

DWORD64 GetProcAddress64(DWORD module, char* funcName)
{
	static DWORD _LdrGetProcedureAddress = 0;
	if (_LdrGetProcedureAddress == 0)
	{
		_LdrGetProcedureAddress = getLdrGetProcedureAddress();
		printf("LdrGetProcedureAddress: %08X\n", _LdrGetProcedureAddress);
		if (_LdrGetProcedureAddress == 0)
			return 0;
	}

	WOW64::ANSI_STRING64 fName = { 0 };
	fName.Buffer = funcName;
	fName.Length = strlen(funcName);
	fName.MaximumLength = fName.Length + 1;
	DWORD64 funcRet = 0;
	X64Call(_LdrGetProcedureAddress, 4,
		(DWORD64)module, (DWORD64)&fName,
		(DWORD64)0, (DWORD64)&funcRet);

	printf("%s: %08X\n", funcName, (DWORD)funcRet);
	return funcRet;
}

32bitプロセス内でのx64コードの実行

これも問題が少々ある。64bitバージョンのMS C/C++コンパイラはインラインアセンブリをサポートしていない。だから、.asmファイルを別途作成しなければならない。以下がMASM64用のX86_StartマクロとX86_Endマクロの定義である:

X86_Start MACRO
	LOCAL  xx, rt
	call   $+5
	xx     equ $
	mov    dword ptr [rsp + 4], 23h
	add    dword ptr [rsp], rt - xx
	retf
	rt:
ENDM

X86_End MACRO
	db 6Ah, 33h			; push  33h
	db 0E8h, 0, 0, 0, 0		; call  $+5
	db 83h, 4, 24h, 5		; add   dword ptr [esp], 5
	db 0CBh				; retf
ENDM

原文: http://blog.rewolf.pl/blog/?p=102

コメントする