ラベル C言語 の投稿を表示しています。 すべての投稿を表示
ラベル C言語 の投稿を表示しています。 すべての投稿を表示

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]);
}

デバッグ用マクロ (C言語)

デバッグ時のみ有効なエラー出力用マクロ。何となくWindows以外でも動きそうな感じに書いてみたけど、実際に使っているのはWindowsだけ。他のOSは試してないので動かないかもれない。(→追記:一応FreeBSD gcc4.2でそれっぽく動いたのでとりあえず良しとする。)

使い方は共通のヘッダファイルに下を書いておいて、それをincludeして
debug("ProcessId=%d",GetCurrentProcessId());
とか書けば
[D] test.cpp[55] WinMain[0]: ProcessId=6508
みたいにメッセージと一緒にソースコードのファイル名や行数、関数名、エラーコードをコンソールに出力してくれる。
GOTO_Eと書くと、メッセージを吐いてからERROR_HANDLERという名前のラベルにgotoする。
#ifndef _LOG_H
#define _LOG_H

#include <stdio.h>

#ifdef _WIN32
#include <windows.h>
#define ERRCD (int)GetLastError()
#define PATH_DELIM '\\'
#else
#include <stdarg.h>
#include <errno.h>
#include <string.h>
#define ERRCD errno
#define PATH_DELIM '/'
#endif

#ifdef _WIN32
// Ctrl-Cとかでコンソールウインドウを閉じなくする。
inline BOOL WINAPI HandlerRoutine(DWORD type)
{
switch(type)
{
case CTRL_C_EVENT: //Ctrl+C
case CTRL_BREAK_EVENT: //Ctrl+Break
case CTRL_CLOSE_EVENT: //CLOSE
return TRUE;
case CTRL_LOGOFF_EVENT: //LOGOFF
case CTRL_SHUTDOWN_EVENT: //SHUTDOWN
default:
return FALSE;
}
}
#endif

inline void debuglog(const int level, const char *file, const char* func, const int line, const int errcd, const char *fmt, ...)
{
#ifdef _WIN32
static bool hasConsole = false;

if(!hasConsole) {
HWND hWnd = GetConsoleWindow();
if(hWnd == NULL) {
FILE *fp;
AllocConsole();
SetConsoleTitle((LPCSTR)"Debug");
SetConsoleCtrlHandler(HandlerRoutine, TRUE);
freopen_s(&fp, "CONOUT$", "w", stderr);
hasConsole = true;

// 「閉じる」をボタン、右クリックメニューから削除。
// コンソールウインドウを閉じられるとプロセスが終了されるのを防げないため。
hWnd = GetConsoleWindow();
HMENU hMenu = GetSystemMenu (hWnd, FALSE);
HINSTANCE hinstance = (HINSTANCE) GetWindowLongPtr (hWnd, GWLP_HINSTANCE);
DeleteMenu(hMenu, SC_CLOSE, MF_BYCOMMAND);
DrawMenuBar(hWnd);
}
}
#endif
static char* table[] = {"[D]","[I]","[W]","[E]","[A]"};
#pragma warning(push)
#pragma warning(disable:4996)
static int limitlevel = getenv("loglevel")==NULL?0:atoi(getenv("loglevel"));
#pragma warning(pop)

if(level < limitlevel) return;

// フルパスだったらファイル名に
const char* fname = strrchr(file, PATH_DELIM);
if(fname == NULL)
fname = file;
else
fname++;

va_list argp;
fprintf(stderr, "%s %s[%d] %s[%d]: ", table[level], fname, line, func, errcd);
va_start(argp, fmt);
vfprintf(stderr, fmt, argp);
va_end(argp);
fprintf(stderr, "\n");
}

#ifdef _DEBUG
#define debug(...) debuglog(0, __FILE__, __FUNCTION__, __LINE__, ERRCD, __VA_ARGS__);
#define info(...) debuglog(1, __FILE__, __FUNCTION__, __LINE__, ERRCD, __VA_ARGS__);
#define warn(...) debuglog(2, __FILE__, __FUNCTION__, __LINE__, ERRCD, __VA_ARGS__);
#define error(...) debuglog(3, __FILE__, __FUNCTION__, __LINE__, ERRCD, __VA_ARGS__);
#define critical(...) debuglog(4, __FILE__, __FUNCTION__, __LINE__, ERRCD, __VA_ARGS__);

#define GOTO_E {error("[ERROR] goto FUNC_END"); goto FUNC_END;}
#else

#define debug(...)
#define info(...)
#define warn(...)
#define error(...)
#define critical(...)
#define GOTO_E goto FUNC_END
#endif
#endif

2011年8月22日月曜日

DLLの共有セクションを使う場合の注意点

グローバルフックを使用するときなど、複数のプロセスでデータを共有するために、DLLの共有セクションを使う場合がある。
この時、共有する変数は宣言と同時に明示的に初期化しなければ、共有されないので注意すること。

#pragma comment(linker, "/SECTION:.SHAREDMEM,RWS")
#pragma data_seg(".SHAREDMEM")
int shared_ng;                                //NG
int shared_ok = 0;                            //OK
struct shared_struct shared_struct_ng;        //NG
struct shared_struct shared_struct_ok ={0};   //OK
#pragma data_seg()

初期化していないグローバル変数は自動的に0に初期化されることになっているわけだけど、このためにBSSという専用のセクションが用意されていて、スタートアップルーチンでクリア処理が走る。
そのため、初期化をしていない変数はせっかく用意した共有用のデータセグメントではなく、BSSセクションに置かれてしまい共有できないということらしい。