x64驅動基礎教程 34
一、 RING3 INLINE HOOK
在 WIN64 系統上進行 RING0 INLINE HOOK 比較麻煩(要破 PatchGuard),但進行 RING3 INLINE HOOK 還是比較輕鬆的。 在 Win64 下進行 Ring 3 Inline Hook的基本流程跟 Win32 下差不多,首先要編寫一個能掛鉤程序自身內部函式的 DLL,再把這個 DLL 注射進別的程序體內。一個標準的 DLL 注入程式如下:
BOOL WINAPI InjectProxyW(DWORD dwPID, PCWSTR pwszProxyFile) { BOOL ret = FALSE; HANDLE hToken = NULL; HANDLE hProcess = NULL; HANDLE hThread = NULL; FARPROC pfnThreadRtn = NULL; PWSTR pwszPara = NULL; hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID); pfnThreadRtn = GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryW"); size_t iProxyFileLen = wcslen(pwszProxyFile)*sizeof(WCHAR); pwszPara = (PWSTR)VirtualAllocEx(hProcess, NULL, iProxyFileLen, MEM_COMMIT, PAGE_READWRITE); WriteProcessMemory(hProcess, pwszPara, (PVOID)pwszProxyFile, iProxyFileLen, NULL); hThread = CreateRemoteThread(hProcess, NULL, 1024, (LPTHREAD_START_ROUTINE)pfnThreadRtn, pwszPara, 0, NULL); WaitForSingleObject(hThread, INFINITE); CloseHandle(hThread); VirtualFreeEx(hProcess, pwszPara, 0, MEM_RELEASE); CloseHandle(hProcess); return(TRUE); }
以上程式碼必須編譯成 64 位 EXE 才能。接下來要編寫用來 Hook API 的 DLL了。由於這是 Win64,跟 Win32 有了天壤之別,掛鉤絕對不再是“原函式頭 5位元組改 JMP 跳到偽函式,偽函式處理完畢後再跳到原函式地址+5 的地方”這麼簡單了。現在我們遇到兩個難題: 1.JMP 的最大跳轉範圍是 4GB,然而原函式所在的 DLL 通常不和偽函式所在的 DLL 在同一個 4GB 中; 那麼, E9 XX XX XX XX也要改成 FF 15 XX XX XX XX; 2.Win64 上函式的前 N 位元組通常都是和堆疊有關的指令,不是 mov edi,edi 等無意義的指令,如果亂改,堆疊會被搞亂的。堆疊一亂,程序就掛了。不過,這個麻煩的問題外國朋友早解決了。 國外比較成熟的 RING3 INLINEHOOK 庫有 mHOOK 和 MiniHookEngine, 個人比較傾向於用 MiniHookEngine。MiniHookEngine 的作者是 Daniel Pistelli, MiniHookEngine 同時支援 WIN32和 WIN64。 MiniHookEngine 的核心程式碼如下:
// 10000 hooks should be enough #define MAX_HOOKS 10000 typedefstruct_HOOK_INFO { ULONG_PTR Function; // Address of the original function ULONG_PTR Hook; // Address of the function to call // instead of the original ULONG_PTR Bridge; // Address of the instruction bridge // necessary because of the hook jmp // which overwrites instructions } HOOK_INFO, *PHOOK_INFO; HOOK_INFO HookInfo[MAX_HOOKS]; UINTNumberOfHooks = 0; BYTE*pBridgeBuffer = NULL; // Here are going to be stored all the bridges UINTCurrentBridgeBufferSize = 0; // This number is incremented as // the bridge buffer is growing HOOK_INFO *GetHookInfoFromFunction(ULONG_PTR OriginalFunction) { if (NumberOfHooks == 0) return NULL; for (UINT x = 0; x < NumberOfHooks; x++) { if (HookInfo[x].Function == OriginalFunction) return &HookInfo[x]; } return NULL; } // // This function retrieves the necessary size for the jump // UINTGetJumpSize(ULONG_PTR PosA, ULONG_PTR PosB) { ULONG_PTR res = max(PosA, PosB) - min(PosA, PosB); // if you want to handle relative jumps /*if (res <= (ULONG_PTR) 0x7FFF0000) { return 5; // jmp rel } else {*/ // jmp [xxx] + addr #ifdef _M_IX86 return 10; #else ifdef _M_AMD64 return 14; //} return 0; // error } // // This function writes unconditional jumps // both for x86 and x64 // VOIDWriteJump(VOID *pAddress, ULONG_PTR JumpTo) { DWORD dwOldProtect = 0; VirtualProtect(pAddress, JUMP_WORST, PAGE_EXECUTE_READWRITE, &dwOldProtect); BYTE *pCur = (BYTE *)pAddress; // if you want to handle relative jumps /*if (JumpTo - (ULONG_PTR) pAddress <= (ULONG_PTR) 0x7FFF0000) { *pCur = 0xE9; // jmp rel DWORD RelAddr = (DWORD) (JumpTo - (ULONG_PTR) pAddress) - 5; memcpy(++pCur, &RelAddr, sizeof (DWORD)); } else {*/ #ifdef _M_IX86 *pCur = 0xff; // jmp [addr] *(++pCur) = 0x25; pCur++; *((DWORD *)pCur) = (DWORD)(((ULONG_PTR)pCur) + sizeof(DWORD)); pCur += sizeof(DWORD); *((ULONG_PTR *)pCur) = JumpTo; #else ifdef _M_AMD64 *pCur = 0xff; // jmp [rip+addr] *(++pCur) = 0x25; *((DWORD *) ++pCur) = 0; // addr = 0 pCur += sizeof(DWORD); *((ULONG_PTR *)pCur) = JumpTo; #endif //} DWORD dwBuf = 0; // nessary othewrise the function fails VirtualProtect(pAddress, JUMP_WORST, dwOldProtect, &dwBuf); } // // This function creates a bridge of the original function // VOID*CreateBridge(ULONG_PTR Function, const UINT JumpSize) { if (pBridgeBuffer == NULL) return NULL; #define MAX_INSTRUCTIONS 100 _DecodeResult res; _DecodedInst decodedInstructions[MAX_INSTRUCTIONS]; unsigned int decodedInstructionsCount = 0; #ifdef _M_IX86 _DecodeType dt = Decode32Bits; #else ifdef _M_AMD64 _DecodeType dt = Decode64Bits; #endif _OffsetType offset = 0; res = distorm_decode(offset, // offset for buffer (const BYTE *)Function, // buffer to disassemble 50, // function size (code size to disasm) // 50 instr should be _quite_ enough dt, // x86 or x64? decodedInstructions, // decoded instr MAX_INSTRUCTIONS, // array size &decodedInstructionsCount // how many instr were disassembled? ); if (res == DECRES_INPUTERR) return NULL; DWORD InstrSize = 0; VOID *pBridge = (VOID *)&pBridgeBuffer[CurrentBridgeBufferSize]; for (UINT x = 0; x < decodedInstructionsCount; x++) { if (InstrSize >= JumpSize) break; BYTE *pCurInstr = (BYTE *)(InstrSize + (ULONG_PTR)Function); // // This is an sample attempt of handling a jump // It works, but it converts the jz to jmp // since I didn't write the code for writing // conditional jumps // /* if (*pCurInstr == 0x74) // jz near { ULONG_PTR Dest = (InstrSize + (ULONG_PTR) Function) + (char) pCurInstr[1]; WriteJump(&pBridgeBuffer[CurrentBridgeBufferSize], Dest); CurrentBridgeBufferSize += JumpSize; } else {*/ memcpy(&pBridgeBuffer[CurrentBridgeBufferSize], (VOID *)pCurInstr, decodedInstructions[x].size); CurrentBridgeBufferSize += decodedInstructions[x].size; //} InstrSize += decodedInstructions[x].size; } WriteJump(&pBridgeBuffer[CurrentBridgeBufferSize], Function + InstrSize); CurrentBridgeBufferSize += GetJumpSize((ULONG_PTR) &pBridgeBuffer[CurrentBridgeBufferSize], Function + InstrSize); return pBridge; } // // Hooks a function // extern"C"__declspec(dllexport) BOOL__cdecl HookFunction(ULONG_PTR OriginalFunction, ULONG_PTR NewFunction) { // // Check if the function has already been hooked // If so, no disassembling is necessary since we already // have our bridge // HOOK_INFO *hinfo = GetHookInfoFromFunction(OriginalFunction); if (hinfo) { WriteJump((VOID *)OriginalFunction, NewFunction); } else { if (NumberOfHooks == (MAX_HOOKS - 1)) return FALSE; VOID *pBridge = CreateBridge(OriginalFunction, GetJumpSize(OriginalFunction, NewFunction)); if (pBridge == NULL) return FALSE; HookInfo[NumberOfHooks].Function = OriginalFunction; HookInfo[NumberOfHooks].Bridge = (ULONG_PTR)pBridge; HookInfo[NumberOfHooks].Hook = NewFunction; NumberOfHooks++; WriteJump((VOID *)OriginalFunction, NewFunction); } return TRUE; } // // Unhooks a function // extern"C"__declspec(dllexport) VOID__cdecl UnhookFunction(ULONG_PTR Function) { // // Check if the function has already been hooked // If not, I can't unhook it // HOOK_INFO *hinfo = GetHookInfoFromFunction(Function); if (hinfo) { // // Replaces the hook jump with a jump to the bridge // I'm not completely unhooking since I'm not // restoring the original bytes // WriteJump((VOID *)hinfo->Function, hinfo->Bridge); } } // // Get the bridge to call instead of the original function from hook // extern"C"__declspec(dllexport) ULONG_PTR__cdecl GetOriginalFunction(ULONG_PTR Hook) { if (NumberOfHooks == 0) return NULL; for (UINT x = 0; x < NumberOfHooks; x++) { if (HookInfo[x].Hook == Hook) return HookInfo[x].Bridge; } return NULL; }
把上述程式碼編譯成 DLL 才能在別的程序中使用。不過我們用來 HOOK API 的DLL 還要另寫一個。在下個 DLL 中,我們要通過 Hook OpenProcess 來保護計算器不被非法關閉,並告訴大家一個在 Win64 上進行全域性 HOOK 的方法。
第一步:編寫偽函式 Fake_OpenProcess,用來保護計算器程序
HANDLE Fake_OpenProcess(DWORD da, BOOL ih, DWORD PId) { HWND myhwnd = 0; DWORD mypid = 0; HANDLE(WINAPI *pOpenProcess)(DWORD da, BOOL ih, DWORD PId); pOpenProcess = (HANDLE(WINAPI *)(DWORD, BOOL, DWORD))GetOriginalFunction((ULONG_PTR)Fake_OpenProcess); myhwnd = pfnFindWindowW(L"CalcFrame", L"Calculator"); if (myhwnd == 0) myhwnd = pfnFindWindowW(L"CalcFrame", L"計算器"); if (myhwnd == 0) { return pOpenProcess(da, ih, PId); } else { pfnGetWindowThreadProcessId(myhwnd, &mypid); if (PId == mypid) return pOpenProcess(da, ih, 0); //set pid as 0 else return pOpenProcess(da, ih, PId); } }
第二步:編寫偽函式 Fake_CreateProcess,來把現在寫的這個 DLL 注入進新建立的程序。這裡說一句,在 Win64 上 Hook NtResumeThread 是沒用的(假設現在寫的這個 DLL 放在 C 盤根目錄下,名為 HookDll.dll)。
BOOL WINAPI Fake_CreateProcessW ( __in_opt LPCWSTR lpApplicationName, __inout_opt LPWSTR lpCommandLine, __in_opt LPSECURITY_ATTRIBUTES lpProcessAttributes, __in_opt LPSECURITY_ATTRIBUTES lpThreadAttributes, __in BOOL bInheritHandles, __in DWORD dwCreationFlags, __in_opt LPVOID lpEnvironment, __in_opt LPCWSTR lpCurrentDirectory, __in LPSTARTUPINFO lpStartupInfo, __out LPPROCESS_INFORMATION lpProcessInformation ) { BOOL cpwret; BOOL(WINAPI *pCreateProcessW)(LPCWSTR lpApplicationName, LPWSTR lpCommandLine, LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes, BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, LPCWSTR lpCurrentDirectory, LPSTARTUPINFO lpStartupInfo, LPPROCESS_INFORMATION lpProcessInformation); pCreateProcessW = (BOOL(WINAPI *)(LPCWSTR, LPWSTR, LPSECURITY_ATTRIBUTES, LPSECURITY_ATTRIBUTES, BOOL, DWORD, LPVOID, LPCWSTR, LPST ARTUPINFO, LPPROCESS_INFORMATION))GetOriginalFunction((ULONG_PTR)Fake_CreateProcessW); cpwret = pCreateProcessW(lpApplicationName, lpCommandLine, lpProcessAttributes, lpThreadAttribute s, bInheritHandles, dwCreationFlags | CREATE_SUSPENDED, lpEnvironment, lpCurrentDirectory, lpStartu pInfo, lpProcessInformation); InjectProxyW(lpProcessInformation->dwProcessId, L"C:\\HOOKDLL.DLL"); HANDLE hp = OpenProcess(PROCESS_ALL_ACCESS, 0, lpProcessInformation->dwProcessId); NtResumeProcess(hp); CloseHandle(hp); return cpwret; }
第三步:獲得 user32.dll 中某些函式的地址並掛鉤 API(把MiniHookEngine 編譯的 DLL 放在 C 盤根目錄,更名為 NtHookEngine.dll。需要特別注意的是, Kernel32.dll 匯出的那個 OpenProcess 函式只是個 stub,真正實現“開啟程序”這個功能的 OpenProcess 函式在 KernelBase.dll 裡,所以我們這裡掛鉤的是位於 KernelBase.dll 中的 OpenProcess 函式。而CreateProcess 無此情況,直接掛鉤 Kernel32.dll 匯出的 CreateProcess 即可)。
Void InitHook() { HMODULE hHookEngineDll = LoadLibraryW(L"C:\\NtHookEngine.dll"); HookFunction = (BOOL(__cdecl *)(ULONG_PTR, ULONG_PTR))GetProcAddress(hHookEngineDll, "HookFunction"); UnhookFunction = (VOID(__cdecl *)(ULONG_PTR))GetProcAddress(hHookEngineDll, "UnhookFunction"); GetOriginalFunction = (ULONG_PTR(__cdecl *)(ULONG_PTR))GetProcAddress(hHookEngineDll, "GetOriginalFunction"); if (HookFunction == NULL || UnhookFunction == NULL || GetOriginalFunction == NULL) return; HookFunction((ULONG_PTR) GetProcAddress(GetModuleHandleW(L"kernelbase.dll"), "OpenProcess"), (ULONG_PTR) &Fake_OpenProcess); HookFunction((ULONG_PTR) GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "CreateProcessW"), (ULONG_PTR) &Fake_CreateProcessW); NtResumeProcess = (NTRESUMEPROCESS)GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "NtResumeProcess"); pfnFindWindowW = (FINDWINDOWW)GetProcAddress(LoadLibraryW(L"user32.dll"), "FindWindowW"); pfnGetWindowThreadProcessId = (GETWINDOWTHREADPROCESSID)GetProcAddress(LoadLibraryW(L"user32.dll"), "GetWindowThreadProcessId"); }
第四部:解除安裝 API HOOK(防止程序退出時出現錯誤):
Case DLL_PROCESS_DETACH : { UnhookFunction((ULONG_PTR)GetProcAddress(GetModuleHandleW(L"kernelbase.dll"), "OpenProcess") ); UnhookFunction((ULONG_PTR)GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "CreateProcessW" )); break; }
把上述兩段程式碼分別編譯成 64 位的 DLL,並更名為 NtHookEngine.dll 和HookDll.DLL。為了讓這些 DLL 能在沒有安裝.NET Framework 4.0 的電腦上使用,在編譯前請選擇“在靜態庫中使用 MFC”。編譯好後,用 stud_PE 檢視匯入表,會發現沒有 MSVCRT100.DLL 只有 Kernel32.DLL:
測試效果如下:
3.嘗試結束“計算器”程序:
4.利用工作管理員啟動控制檯程式 CMD.EXE,並在 CMD.EXE 裡啟動控制檯程式taskkill.exe 結束“計算器”程序:
5.關閉一切剛才啟動的程式,然後啟動 CMD.EXE->把 DLL 注入 CMD.EXE 中->利用 CMD.EXE 啟動“工作管理員”(taskmgr.exe) ->用“工作管理員”結束計算器程序:
請注意:無論是注入 DLL 還是被注入 DLL 的程序必須都是 64 位程序。以上
四張截圖不僅證明我的鉤子起了作用,而且在[被注入了 DLL 的程序 A]啟動[新
程序 B]時,程序 A 同時也把我們的 DLL 注入到了程序 B 中,無論程序 B 是 CUI
程式還是 GUI 程式。
二、 ANTI RING3 INLINE HOOK有了 RING3 INLINE HOOK, 自然有人需要反制它。 比如kernelbase!OpenProcess 被掛鉤了,怎麼繞過鉤子呢?我們可以把kernelbase.dll 複製到 C 盤,並改名為 basekernel.dll,再用 LoadLibraryW載入這個 DLL。當我要呼叫 OpenProcess 時,首先獲得 basekernel.dll 中OpenProcess 的地址,再用函式指標的方式呼叫即可:
typedef HANDLE(__stdcall *MYOPENPROCESS)(DWORD, BOOL, DWORD); int _tmain(int argc, _TCHAR* argv[]) { DWORD dwpid = 0, mypid = GetCurrentProcessId(); printf("My PID is %ld\n", mypid); printf("Inject [hookdll.dll] to this process, then input PID of [calc.exe]: "); scanf("%ld", &dwpid); HANDLE h = OpenProcess(1, 0, dwpid); printf("Process Handle=%ld\n", h); TerminateProcess(h, 0); getchar(); printf("Reload kernelbase.dll now!"); getchar(); CopyFileW(L"c:\\windows\\system32\\kernelbase.dll", L"c:\\basekernel.dll", 0); HMODULE hlib = LoadLibraryW(L"c:\\basekernel.dll"); if (hlib == 0) { printf("Cannot load [c:\\basekernel.dll]!"); getchar(); return 0; } MYOPENPROCESS MyOpenProcess = (MYOPENPROCESS)GetProcAddress(hlib, "OpenProcess"); h = MyOpenProcess(1, 0, dwpid); printf("Process Handle=%ld\n", h); TerminateProcess(h, 0); CloseHandle(h); FreeLibrary(hlib); DeleteFileW(L"c:\\basekernel.dll"); getchar(); return 0; }
把含上述程式碼的程序中的 kernelbase!OpenProcess 勾住無法阻止它殺程序,因為它在開啟程序的過程中壓根沒有用到 kernelbase.dll 中的函式,用的是 basekernel!OpenProcess。儘管 basekernel,dll 根本就是 kernelbase.dll的馬甲,但是系統竟然沒有認出來
三、 ANTI-ANTI RING3 INLINE HOOK
如果 RING3 INLINE HOOK 這麼容易被繞過,那就太沒意思了,針對這種 ANTI的方法, 我又想出了對抗的方法, 稱為“反反 HOOK”。“反反 HOOK”的方法也很簡單暴力, 掛鉤 ZwReadFile, 直接拒絕對被 HOOK DLL 磁碟檔案的讀取。 下面直接貼出對 ZwReadFile 的處理:BOOL __stdcall Fake_ZwReadFile( IN HANDLE FileHandle, IN HANDLE Event OPTIONAL, IN PVOID ApcRoutine OPTIONAL, IN PVOID ApcContext OPTIONAL, OUT PIO_STATUS_BLOCK IoStatusBlock, OUT PVOID Buffer, IN ULONG Length, IN PLARGE_INTEGER ByteOffset OPTIONAL, IN PULONG Key OPTIONAL) { long (WINAPI *pZwReadFile)( IN HANDLE FileHandle, IN HANDLE Event OPTIONAL, IN PVOID ApcRoutine OPTIONAL, IN PVOID ApcContext OPTIONAL, OUT PIO_STATUS_BLOCK IoStatusBlock, OUT PVOID Buffer, IN ULONG Length, IN PLARGE_INTEGER ByteOffset OPTIONAL, IN PULONG Key OPTIONAL); pZwReadFile = (long (WINAPI *)(HANDLE, HANDLE, PVOID, PVOID, PIO_STATUS_BLOCK, PVOID, ULONG, PLARGE_INTEGER, PULONG))GetOriginal Function((ULONG_PTR)Fake_ZwReadFile); char lszsrc[522]; char dllname[] = "kernelbase.dll"; char dllname2[] = "ntdll.dll"; char *ptrx, *ptry; WCHAR wszsrc[MAX_PATH + 1]; GetFileNameFromHandleW3(FileHandle, wszsrc); PWSTR2PCHAR(wszsrc, lszsrc); ptrx = strstr(strlwr(lszsrc), dllname); ptry = strstr(strlwr(lszsrc), dllname2); if (ptrx != NULL || ptry != NULL) return 0x80070000; else return pZwReadFile(FileHandle, Event, ApcRoutine, ApcContext, IoStatusBlock, Buffer, Length, ByteOffset, Ke y); }
簡單解釋以上程式碼:首先從檔案控制代碼中獲得檔名,如果檔名中包含kernelbase.dll 或者 ntdll.dll 的字樣,就返回 0x80070000,否則不做處理。從檔案控制代碼獲取檔名的程式碼如下:
LPWSTR GetFileNameFromHandleW3(HANDLE hFile, LPWSTR lpFilePath) { const int FileNameInformation = 9; // enum FILE_INFORMATION_CLASS; typedef struct _IO_STATUS_BLOCK { union { ULONG Status; // NTSTATUS PVOID Pointer; }; ULONG_PTR Information; } IO_STATUS_BLOCK, *PIO_STATUS_BLOCK; // Defined in Wdm.h typedef struct _FILE_NAME_INFORMATION { ULONG FileNameLength; WCHAR FileName[MAX_PATH]; } FILE_NAME_INFORMATION, *PFILE_NAME_INFORMATION; typedef LONG(CALLBACK* ZWQUERYINFORMATIONFILE)( HANDLE FileHandle, IO_STATUS_BLOCK *IoStatusBlock, PVOID FileInformation, ULONG Length, ULONG FileInformationClass ); lpFilePath[0] = 0x00; HMODULE hNtDLL = GetModuleHandleW(L"ntdll.dll"); if (hNtDLL == 0x00) { return 0x00; } ZWQUERYINFORMATIONFILE ZwQueryInformationFile = (ZWQUERYINFORMATIONFILE)GetProcAddress(hNtDLL, "ZwQueryInformationFile"); if (ZwQueryInformationFile == 0x00) { return 0x00; } FILE_NAME_INFORMATION fni; IO_STATUS_BLOCK isb; if (ZwQueryInformationFile(hFile, &isb, &fni, sizeof(fni), FileNameInformation) != 0) { return 0x00; } fni.FileName[fni.FileNameLength / sizeof(WCHAR)] = 0x00; BY_HANDLE_FILE_INFORMATION fi; if (!GetFileInformationByHandle(hFile, &fi) || (fi.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) { return 0x00; } WCHAR szDrive[MAX_PATH]; WCHAR *lpDrive = szDrive; int iPathLen; if (GetLogicalDriveStringsW(MAX_PATH - 1, szDrive) >= MAX_PATH) { return 0x00; } while ((iPathLen = lstrlenW(lpDrive)) != 0) { DWORD dwVolumeSerialNumber; if (!GetVolumeInformation(lpDrive, NULL, NULL, &dwVolumeSerialNumber, NULL, NULL, NULL, NULL)) { return 0x00; } if (dwVolumeSerialNumber == fi.dwVolumeSerialNumber) { lstrcpynW(lpFilePath, lpDrive, lstrlen(lpDrive)); lstrcatW(lpFilePath, fni.FileName); break; } lpDrive += iPathLen + 1; } return lpFilePath; }
經測試結果如下: