Skip to content

Instantly share code, notes, and snippets.

@valinet
Created June 22, 2024 17:02
Show Gist options
  • Save valinet/0b61552b493079de1e3b4762378d352e to your computer and use it in GitHub Desktop.
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
// Example of a driver that helps Windhawk
// inject processes created by inaccessible processes early on
// ==========================================================================
// Valentin-Gabriel Radu, valentin.radu@valinet.ro
//
// 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;
}
// Example of a library that, when injected into a process, helps Windhawk
// inject processes created by inaccessible processes early on
// ==========================================================================
// Valentin-Gabriel Radu, valentin.radu@valinet.ro
//
// 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