Created
June 22, 2024 17:02
-
-
Save valinet/0b61552b493079de1e3b4762378d352e to your computer and use it in GitHub Desktop.
Exmple of DLL and driver that signal Windhawk to scan for new processes
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Example of a driver that helps Windhawk | |
// inject processes created by inaccessible processes early on | |
// ========================================================================== | |
// Valentin-Gabriel Radu, [email protected] | |
// | |
// Upstream issue: | |
// https://github.com/ramensoftware/windhawk/issues/197 | |
// | |
#include <ntifs.h> | |
#define INVALID_HANDLE_VALUE ((HANDLE)(LONG_PTR)-1) | |
//#define WITH_DBGPRINT | |
UINT64 bRegisteredRoutine = FALSE; | |
void CreateProcessNotifyRoutine(_Inout_ PEPROCESS Process, _In_ HANDLE ProcessId, _Inout_opt_ PPS_CREATE_NOTIFY_INFO CreateInfo) { | |
UNREFERENCED_PARAMETER(Process); | |
UNREFERENCED_PARAMETER(ProcessId); | |
UNREFERENCED_PARAMETER(CreateInfo); | |
NTSTATUS rv = STATUS_SUCCESS; | |
#ifdef WITH_DBGPRINT | |
DbgPrint("WhSignalDrv: CreateProcessNotifyRoutine: %p\n", CreateInfo); | |
#endif | |
if (CreateInfo) { | |
UNICODE_STRING wszScanEventName; | |
RtlInitUnicodeString(&wszScanEventName, L"\\BaseNamedObjects\\Global\\WindhawkScanForProcesses"); | |
OBJECT_ATTRIBUTES oaScan; | |
RtlZeroMemory(&oaScan, sizeof(oaScan)); | |
InitializeObjectAttributes(&oaScan, &wszScanEventName, 0, NULL, NULL); | |
HANDLE hScanEvent = NULL; | |
rv = ZwOpenEvent(&hScanEvent, EVENT_MODIFY_STATE, &oaScan); | |
#ifdef WITH_DBGPRINT | |
DbgPrint("WhSignalDrv: ZwOpenEvent -> %d\n", rv); | |
#endif | |
if (hScanEvent && hScanEvent != INVALID_HANDLE_VALUE) { | |
rv = ZwSetEvent(hScanEvent, NULL); | |
#ifdef WITH_DBGPRINT | |
DbgPrint("WhSignalDrv: ZwSetEvent -> %d\n", rv); | |
#endif | |
ZwClose(hScanEvent); | |
} | |
} | |
} | |
NTSTATUS DriverUnload(_In_ PDRIVER_OBJECT driverObject) { | |
UNREFERENCED_PARAMETER(driverObject); | |
if (bRegisteredRoutine) PsSetCreateProcessNotifyRoutineEx(&CreateProcessNotifyRoutine, TRUE); | |
#ifdef WITH_DBGPRINT | |
DbgPrint("WhSignalDrv: DriverUnload\n"); | |
#endif | |
return STATUS_SUCCESS; | |
} | |
NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT driverObject, _In_ PUNICODE_STRING registryPath) { | |
UNREFERENCED_PARAMETER(registryPath); | |
NTSTATUS rv = STATUS_SUCCESS; | |
#ifdef WITH_DBGPRINT | |
DbgPrint("WhSignalDrv: DriverEntry\n"); | |
#endif | |
driverObject->DriverUnload = DriverUnload; | |
rv = PsSetCreateProcessNotifyRoutineEx(&CreateProcessNotifyRoutine, FALSE); | |
if (NT_SUCCESS(rv)) bRegisteredRoutine = TRUE; | |
#ifdef WITH_DBGPRINT | |
DbgPrint("WhSignalDrv: PsSetCreateProcessNotifyRoutineEx -> %d\n", rv); | |
#endif | |
return rv; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Example of a library that, when injected into a process, helps Windhawk | |
// inject processes created by inaccessible processes early on | |
// ========================================================================== | |
// Valentin-Gabriel Radu, [email protected] | |
// | |
// Upstream issue: | |
// https://github.com/ramensoftware/windhawk/issues/197 | |
// | |
// Instead of <windows.h>, using <phnt.h> which gives access to | |
// native Nt* APIs, get a copy from: | |
// https://github.com/mrexodia/phnt-single-header/releases/latest/download/phnt.h | |
// | |
// Compilation tested to work only in Release mode | |
// | |
#define _CRT_SECURE_NO_WARNINGS | |
#define PHNT_VERSION PHNT_WIN11 | |
#include "phnt.h" | |
// | |
// Do not link in the CRT; instead, only link ntdll.lib | |
// This is required if the library is set up to be injected as a verifier engine | |
// DLL, since such DLLs are injected very early on in process lifetime, | |
// even before kernel32.dll; we aim to maintain the "natural" order of | |
// loading DLLs for that process, so we restrict this to only using ntdll APIs, | |
// which always is the first and mandatory loaded DLL in a process. | |
// | |
#pragma comment(linker,"/DEFAULTLIB:ntdll.lib") | |
// | |
// Specify out custom entry point | |
// | |
#pragma comment(linker,"/ENTRY:DllMain") | |
// | |
// Define constants and structs used when library is set up as verifier engine. | |
// | |
#define DLL_PROCESS_VERIFIER 4 | |
typedef struct _RTL_VERIFIER_THUNK_DESCRIPTOR { | |
PCHAR ThunkName; | |
PVOID ThunkOldAddress; | |
PVOID ThunkNewAddress; | |
} RTL_VERIFIER_THUNK_DESCRIPTOR, * PRTL_VERIFIER_THUNK_DESCRIPTOR; | |
typedef struct _RTL_VERIFIER_DLL_DESCRIPTOR { | |
PWCHAR DllName; | |
ULONG DllFlags; | |
PVOID DllAddress; | |
PRTL_VERIFIER_THUNK_DESCRIPTOR DllThunks; | |
} RTL_VERIFIER_DLL_DESCRIPTOR, * PRTL_VERIFIER_DLL_DESCRIPTOR; | |
typedef void (NTAPI* RTL_VERIFIER_DLL_LOAD_CALLBACK) ( | |
PWSTR DllName, | |
PVOID DllBase, | |
SIZE_T DllSize, | |
PVOID Reserved); | |
typedef void (NTAPI* RTL_VERIFIER_DLL_UNLOAD_CALLBACK) ( | |
PWSTR DllName, | |
PVOID DllBase, | |
SIZE_T DllSize, | |
PVOID Reserved); | |
typedef void (NTAPI* RTL_VERIFIER_NTDLLHEAPFREE_CALLBACK) ( | |
PVOID AllocationBase, | |
SIZE_T AllocationSize); | |
typedef struct _RTL_VERIFIER_PROVIDER_DESCRIPTOR { | |
ULONG Length; | |
PRTL_VERIFIER_DLL_DESCRIPTOR ProviderDlls; | |
RTL_VERIFIER_DLL_LOAD_CALLBACK ProviderDllLoadCallback; | |
RTL_VERIFIER_DLL_UNLOAD_CALLBACK ProviderDllUnloadCallback; | |
PWSTR VerifierImage; | |
ULONG VerifierFlags; | |
ULONG VerifierDebug; | |
PVOID RtlpGetStackTraceAddress; | |
PVOID RtlpDebugPageHeapCreate; | |
PVOID RtlpDebugPageHeapDestroy; | |
RTL_VERIFIER_NTDLLHEAPFREE_CALLBACK ProviderNtdllHeapFreeCallback; | |
} RTL_VERIFIER_PROVIDER_DESCRIPTOR; | |
RTL_VERIFIER_DLL_DESCRIPTOR noHooks{}; | |
RTL_VERIFIER_PROVIDER_DESCRIPTOR desc = { | |
sizeof(desc), | |
&noHooks, | |
[](auto, auto, auto, auto) {}, | |
[](auto, auto, auto, auto) {}, | |
nullptr, 0, 0, | |
nullptr, nullptr, nullptr, | |
[](auto, auto) {}, | |
}; | |
// | |
void NTAPI WaitForWh(LPVOID pParam1, LPVOID pParam2, LPVOID pParam3) { | |
PTEB teb = (PTEB)NtCurrentTeb(); | |
PPEB peb = teb->ProcessEnvironmentBlock; | |
PPEB_LDR_DATA ldr = peb->Ldr; | |
PLIST_ENTRY moduleList = &ldr->InLoadOrderModuleList; | |
PLIST_ENTRY currentEntry = moduleList->Flink; | |
// | |
// Since we do not link against kernel32, we have to wait for some | |
// other part of the application to call it in; here, we continously | |
// parse the PEB and check whether kernelbase is finally loaded. | |
// | |
HMODULE hKernelBase = nullptr; | |
while (currentEntry != moduleList) { | |
PLDR_DATA_TABLE_ENTRY currentModule = CONTAINING_RECORD(currentEntry, LDR_DATA_TABLE_ENTRY, InLoadOrderLinks); | |
wchar_t name[MAX_PATH]; | |
// | |
// All wcs* functions here are intrinsic (don't require the CRT). | |
// | |
if (wcslen(currentModule->BaseDllName.Buffer) < MAX_PATH - 1) { | |
wcscpy(name, currentModule->BaseDllName.Buffer); | |
for (wchar_t* p = name; *p != L'\0'; ++p) if (p[0] >= L'A' && p[0] <= L'Z') p[0] += (L'a' - L'A'); | |
if (!wcscmp(name, L"kernelbase.dll")) { | |
hKernelBase = reinterpret_cast<HMODULE>(currentModule->DllBase); | |
} | |
} | |
currentEntry = currentEntry->Flink; | |
} | |
// | |
// When unable to locate kernelbase, schedule a retry for later. | |
// | |
if (!hKernelBase) NtQueueApcThread(NtCurrentThread(), reinterpret_cast<PPS_APC_ROUTINE>(WaitForWh), 0, 0, 0); | |
else { | |
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)hKernelBase; | |
PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)((BYTE*)hKernelBase + dosHeader->e_lfanew); | |
DWORD exportDirRVA = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress; | |
PIMAGE_EXPORT_DIRECTORY exportDir = (PIMAGE_EXPORT_DIRECTORY)((BYTE*)hKernelBase + exportDirRVA); | |
DWORD* nameRVAs = (DWORD*)((BYTE*)hKernelBase + exportDir->AddressOfNames); | |
WORD* ordinals = (WORD*)((BYTE*)hKernelBase + exportDir->AddressOfNameOrdinals); | |
DWORD* funcRVAs = (DWORD*)((BYTE*)hKernelBase + exportDir->AddressOfFunctions); | |
// | |
// Now that we have kernelbase.dll, find CreateProcessInternalW in it, | |
// which Windhawk patches - this is how we know it successfully | |
// injected and patched. GetProcAddress could be used instead, but | |
// we can also continue parsing the PEB and locating the address | |
// manually | |
// | |
PVOID pCreateProcessInternalW = nullptr; | |
for (DWORD i = 0; i < exportDir->NumberOfNames; i++) { | |
const char* functionName = (const char*)((BYTE*)hKernelBase + nameRVAs[i]); | |
WORD ordinal = ordinals[i]; | |
DWORD funcRVA = funcRVAs[ordinal]; | |
PVOID funcAddress = (BYTE*)hKernelBase + funcRVA; | |
// | |
// memcmp is intrinsic (it will be inlined), but compiles so only | |
// when the string to check against is up to 18 characters long (using MSVC), | |
// so we need this "hack" (for some reason, strcmp doesn't work | |
// as intrinsic on my compiler, despite the documentation | |
// claiming so). | |
// | |
if (!memcmp(functionName, "CreateProcessInter", 18) && !memcmp(functionName + 18, "nalW", 5)) { | |
pCreateProcessInternalW = funcAddress; | |
break; | |
} | |
} | |
if (pCreateProcessInternalW) { | |
// | |
// Instead of using GetTickCount64(), we can obtain the tick count | |
// directly from the KUSER_SHARED_DATA structure that the kernel | |
// maps into every processes' address space at 0x7ffe0000. | |
// | |
auto GetTickCount64 = []() { return (ULONGLONG)((*(ULONGLONG*)0x7ffe0320 * (ULONGLONG)(*(DWORD*)0x7ffe0004)) >> 24); }; | |
// | |
// Busy wait here, waiting for Windhawk to install its patches | |
// | |
auto start = GetTickCount64(); | |
while (true) { | |
// | |
// This is key here: allow APCs scheduled to this thread to | |
// execute while we busy wait here | |
// | |
NtAlertThread(NtCurrentThread()); | |
if (*(BYTE*)pCreateProcessInternalW == 0xE9) { | |
// | |
// Windhawk has installed its patches, so we can exit | |
// from here and resume normal program execution (the | |
// real entry point finally gets a chance to execute) | |
// | |
break; | |
} | |
else if (GetTickCount64() - start > 1000) { | |
// | |
// If Windhawk hasn't patched in 1000ms after we signaled | |
// it, we presume it is dead/crashed/was unable to | |
// inject, so simply give up | |
// | |
break; | |
} | |
} | |
} | |
} | |
} | |
BOOL NTAPI DllMain(_In_ HINSTANCE Instance, _In_ DWORD Reason, _In_ PVOID lpReserved) { | |
if (Reason == DLL_PROCESS_ATTACH) { | |
// | |
// Native API equivalent to DisableThreadLibraryCalls() - disables | |
// DllMain notifications when a thread is created or destroyed. | |
// | |
LdrDisableThreadCalloutsForDll(Instance); | |
// | |
// Here, we obtain a handle to an event Windhawk is waiting on; | |
// when signaled, Windhawk will begin right away scanning for | |
// and injecting new processes, instead of waiting for the default | |
// up to 1000ms timeout. | |
// | |
UNICODE_STRING wszEventName; | |
RtlInitUnicodeString(&wszEventName, L"\\BaseNamedObjects\\Global\\WindhawkScanForProcesses"); | |
OBJECT_ATTRIBUTES oa{}; | |
InitializeObjectAttributes(&oa, &wszEventName, 0, nullptr, nullptr); | |
HANDLE hEvent = nullptr; | |
NtOpenEvent(&hEvent, EVENT_MODIFY_STATE, &oa); | |
if (hEvent && hEvent != INVALID_HANDLE_VALUE) { | |
NtSetEvent(hEvent, nullptr); | |
// | |
// Schedule an APC in which we wait for Windhawk to inject this | |
// process - a new thread should be created eventually which | |
// will load Windhawk's DLL (Windhawk won't queue an APC on this | |
// thread because the program is already executing). | |
// | |
// Using an APC here is mandatory since spinlocking here instead | |
// would keep holding the library load lock (that we currently own | |
// when being here), which would make others, including | |
// Windhawk, unable to inject their own libraries. Instead, we | |
// schedule this "readiness" check for later on, allowing others | |
// to load their libraries as well further on. | |
// | |
NtQueueApcThread(NtCurrentThread(), reinterpret_cast<PPS_APC_ROUTINE>(WaitForWh), 0, 0, 0); | |
NtClose(hEvent); | |
} | |
} | |
else if (Reason == DLL_PROCESS_VERIFIER) { | |
// | |
// This gets called when the library is injected as a verifier | |
// engine DLL; simply returning here is not enough, we have to feed | |
// the main verifier engine a structure that specifies what our | |
// library expects; we return stubs here, since we do not want to | |
// hook anything, we are fine just injected in the target executable. | |
// | |
*(PVOID*)lpReserved = &desc; | |
} | |
return true; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment