2011年9月10日土曜日

64bitプロセスから32bitプロセスにDLL Injection (C言語)

ググったら32bit→64bitはNGだけど、64bit→32bitは行けるという情報があったので試したら行けた。DLL Injectionって何?という人は「別のプロセスにコードを割り込ませる3つの方法」にとても詳しく書かれているのでそちらを参考に。(こういうマニアックな記事は好きだ)

面倒なので自作アプリのソースの一部をそのまま貼る。本筋じゃない処理も混ざってるけど自力で読み飛ばし推奨。GOTO_Eは最後のラベルにgotoするマクロ。
bool pAttachRemoteThread(DWORD pid, const char* dllName, bool autoFree, bool waitRemoteSemaphoreRelease){

bool result = false;
HANDLE hThread = NULL;
PWSTR vDllPath = NULL;
HANDLE hProcess = NULL;
HANDLE hSemaphore = NULL;
HANDLE hMutex = NULL;

hProcess = OpenProcess(
PROCESS_QUERY_INFORMATION |
PROCESS_CREATE_THREAD |
PROCESS_VM_OPERATION |
PROCESS_VM_WRITE,
FALSE, pid);

if(hProcess == NULL) GOTO_E;

BOOL isWow64;
IsWow64Process(hProcess, &isWow64);

// DLLのフルパスの編集
char dllPath[MAX_PATH+1];

GetModuleFileName(g_hInst, dllPath, sizeof(dllPath));
PathRemoveFileSpec(dllPath);
PathAppend(dllPath, dllName); //このDLLと同じディレクトリ

if(_stricmp(PathFindExtension(dllPath), ".dll") == 0){ // 末尾の.dllは除去(大小文字無視)
PathRemoveExtension(dllPath);
}

#ifdef _WIN64
if(!isWow64){ // 64bitのDLL名には64を追加
strcat(dllPath, "64");
}
#else
// 32bit版ビルドで64bitのプロセスにアタッチしようとしたらエラー
BOOL iamWow64;
IsWow64Process(GetCurrentProcess(), &iamWow64);
if(iamWow64 && !isWow64) GOTO_E;
#endif

strcat(dllPath, ".dll");

// ロードするDLLのパスをリモートプロセスのメモリに書きこむ。
size_t dllPathLen = strlen(dllPath) + 1;
vDllPath = (PWSTR)VirtualAllocEx(hProcess, NULL, dllPathLen, MEM_COMMIT, PAGE_READWRITE);
if(vDllPath == NULL) GOTO_E;
if(WriteProcessMemory(hProcess, vDllPath, (PVOID)dllPath, dllPathLen, NULL) == 0) GOTO_E;

// Loadlibraryのリモートプロセスでのアドレスを取得
PTHREAD_START_ROUTINE addr = pGetProcAddress(isWow64, "LoadLibraryA");
if (addr == NULL) GOTO_E;

// DLL名 + pidが同じ処理は同時実行を抑止。
char mutexName[FILENAME_MAX + 12 + 1];
sprintf(mutexName, ":%s%d", dllName, pid);
hMutex = CreateMutex(NULL, FALSE, mutexName);
if(WaitForSingleObject(hMutex, TIMEOUT) == WAIT_TIMEOUT) GOTO_E;

// リモートの処理終了を検知するセマフォ(名前:DLL名 + pid)を用意
if(waitRemoteSemaphoreRelease){
char* semaphoreName = pGetSemaphoreName(pid, dllName);
hSemaphore = CreateSemaphore(NULL, 0, 1, semaphoreName);
free(semaphoreName);
if(hSemaphore == NULL) GOTO_E;
}

// LoadLibraryを実行して終了コード判定
hThread= CreateRemoteThread(hProcess, NULL, 0, addr, vDllPath, 0, NULL);
if (hThread == NULL) GOTO_E;

// スレッドの終了待ち
if(WaitForSingleObject(hThread, TIMEOUT) == WAIT_TIMEOUT) GOTO_E;
DWORD exitCode;
GetExitCodeThread(hThread, &exitCode);
if(exitCode == NULL) GOTO_E;

// インジェクションしたDLLがセマフォをリリースするのを待つ。
if(waitRemoteSemaphoreRelease)
if(WaitForSingleObject(hSemaphore, TIMEOUT) == WAIT_TIMEOUT) GOTO_E;

// FreeLibraryを実行
if(autoFree){
addr = pGetProcAddress(isWow64, "FreeLibrary");
if (addr == NULL) GOTO_E;

CloseHandle(hThread);
hThread= CreateRemoteThread(hProcess, NULL, 0, addr, NULL, 0, NULL);
if (hThread == NULL) GOTO_E;

if(WaitForSingleObject(hThread, TIMEOUT) == WAIT_TIMEOUT) GOTO_E;
GetExitCodeThread(hThread, &exitCode);
if(exitCode == NULL) GOTO_E;
}

result = true;

ERROR_HANDLER:
if(vDllPath != NULL) VirtualFreeEx(hProcess, vDllPath, sizeof(dllPath), MEM_RELEASE);
if(hProcess != NULL) CloseHandle(hProcess);
if(hSemaphore != NULL) {
ReleaseSemaphore(hSemaphore, 1, NULL);
CloseHandle(hSemaphore);
}
if(hMutex != NULL) {
ReleaseMutex(hMutex);
CloseHandle(hMutex);
}
if(hThread != NULL) CloseHandle(hThread);

return result;
}

ここまでは最初に紹介したリンクに書いてあることそのままなので、これで終わるならあちらを読んだ方が面白い。

ここからが問題の64bitプロセスから32bitプロセスのLoadLibraryのアドレスをどうやって知るかなのだが…
はじめはCreateToolhelp32Snapshotでkernel32のベースアドレス取ってきて、PEファイルの構造を読んで関数のRVAを足せば…と試みたのだけれど、ベースアドレスの取得が上手く行かなかった。64bitプロセスから見るとkernel32.dllはロードしておらず、WOW64関連のDLLだけをロードしているように見える。

結局、32bitプロセスを起動して取得するというしょぼい力技で対応することに。もっとスマートな方法あったら教えて欲しい。
PTHREAD_START_ROUTINE pGetProcAddress(BOOL isWow64, const char* funcName, const char* libName = "kernel32"){

if(_stricmp(libName, "user32") != 0 &&
_stricmp(libName, "kernel32") != 0 ) return NULL;

// kernel32, user32.dllの関数はどの全プロセスで同じアドレスにロードされるので
// 自プロセスで取得したアドレスを別プロセスでもそのまま使用できる。
// ただし、プロセスのbit数が異なる場合は別になるため、
// 64bitから32bitのプロセスにアタッチする場合は別の方法でアドレスを取得する。
if(!isWow64)
return (PTHREAD_START_ROUTINE) GetProcAddress(GetModuleHandle(libName), funcName);

// 二回目以降の呼び出しはキャッシュを返す。
static map<string, PTHREAD_START_ROUTINE> cache;
string key = string(libName);
key.append(funcName);
if(cache.count(key) ==1) return cache[key];

// 戻り値に32bitでの関数アドレスを返すコマンドを呼び出す。
PROCESS_INFORMATION pi;
STARTUPINFO si;
ZeroMemory(&si, sizeof(si));
si.cb=sizeof(si);

// コマンドプロンプトは表示させない。
si.dwFlags = STARTF_USESHOWWINDOW;
si.wShowWindow = SW_HIDE;

// このDLLと同じディレクトリ\GetProcAddress32.exe ライブラリ名 関数名
char cmdLine[MAX_PATH + 64 +1];
GetModuleFileName(g_hInst, cmdLine, sizeof(cmdLine));
PathRemoveFileSpec(cmdLine);
PathAppend(cmdLine, "GetProcAddress32.exe");
strcat(cmdLine, " ");
strcat(cmdLine, libName);
strcat(cmdLine, " ");
strcat(cmdLine, funcName);

if(CreateProcess(NULL, cmdLine, NULL,NULL, FALSE,NORMAL_PRIORITY_CLASS, NULL,NULL,&si,&pi) == NULL){
error("CreateProcess()", cmdLine);
return NULL;
}

CloseHandle(pi.hThread);
WaitForSingleObject(pi.hProcess,INFINITE);
DWORD exitCode;
GetExitCodeProcess(pi.hProcess, &exitCode);
CloseHandle(pi.hProcess);

// 実行結果をキャッシュ
PTHREAD_START_ROUTINE addr = (PTHREAD_START_ROUTINE) exitCode;
cache[key] = addr;
return addr;
}

上で呼び出しているコマンド。
#include <windows.h>

int main(int argc, char** argv)
{
if(argc != 3)
return NULL;
else
return (int) GetProcAddress(GetModuleHandle(argv[1]), argv[2]);
}

1 件のコメント:

  1. 64bitプロセスから32bitプロセスの kernel32 のベースアドレスを取る方法ですが、CreateToolhelp32Snapshot の第一引数を TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32 にすれば OK です。

    英語版の MSDN を見てください。(日本語版には載ってない)

    返信削除