#include "modules.h" #include "callbacks.h" #include "driver.h" #include "ioctl.h" #define WHITELISTED_MODULE_TAG 'whte' #define NMI_DELAY 200 * 10000 #define WHITELISTED_MODULE_COUNT 11 #define MODULE_MAX_STRING_SIZE 256 #define NTOSKRNL 0 #define CLASSPNP 1 #define WDF01000 2 /* * The modules seen in the array below have been seen to commonly hook other drivers' * IOCTL dispatch routines. Its possible to see this by using WinObjEx64 and checking which * module each individual dispatch routine lies in. These modules are then addded to the list * (in addition to either the driver itself or ntoskrnl) which is seen as a valid region * for a drivers dispatch routine to lie within. */ CHAR WHITELISTED_MODULES[WHITELISTED_MODULE_COUNT][MODULE_MAX_STRING_SIZE] = {"ntoskrnl.exe", "CLASSPNP.SYS", "Wdf01000.sys", "HIDCLASS.SYS", "storport.sys", "dxgkrnl.sys", "ndis.sys", "ks.sys", "portcls.sys", "rdbss.sys", "LXCORE.SYS"}; #define MODULE_REPORT_DRIVER_NAME_BUFFER_SIZE 128 #define REASON_NO_BACKING_MODULE 1 #define REASON_INVALID_IOCTL_DISPATCH 2 #define SYSTEM_IDLE_PROCESS_ID 0 #define SYSTEM_PROCESS_ID 4 #define SVCHOST_PROCESS_ID 8 typedef struct _WHITELISTED_REGIONS { UINT64 base; UINT64 end; } WHITELISTED_REGIONS, *PWHITELISTED_REGIONS; typedef struct _NMI_POOLS { PVOID thread_data_pool; PVOID stack_frames; PVOID nmi_context; } NMI_POOLS, *PNMI_POOLS; typedef struct _NMI_CORE_CONTEXT { INT nmi_callbacks_run; } NMI_CORE_CONTEXT, *PNMI_CORE_CONTEXT; typedef struct _MODULE_VALIDATION_FAILURE_HEADER { INT module_count; } MODULE_VALIDATION_FAILURE_HEADER, *PMODULE_VALIDATION_FAILURE_HEADER; typedef struct _NMI_CONTEXT { PVOID thread_data_pool; PVOID stack_frames; PVOID nmi_core_context; INT core_count; } NMI_CONTEXT, *PNMI_CONTEXT; typedef struct _NMI_CALLBACK_DATA { UINT64 kthread_address; UINT64 kprocess_address; UINT64 start_address; UINT64 stack_limit; UINT64 stack_base; uintptr_t stack_frames_offset; INT num_frames_captured; UINT64 cr3; } NMI_CALLBACK_DATA, *PNMI_CALLBACK_DATA; typedef struct _INVALID_DRIVER { struct _INVALID_DRIVER* next; INT reason; PDRIVER_OBJECT driver; } INVALID_DRIVER, *PINVALID_DRIVER; typedef struct _INVALID_DRIVERS_HEAD { PINVALID_DRIVER first_entry; INT count; } INVALID_DRIVERS_HEAD, *PINVALID_DRIVERS_HEAD; STATIC NTSTATUS PopulateWhitelistedModuleBuffer(_Inout_ PVOID Buffer, _In_ PSYSTEM_MODULES SystemModules); STATIC NTSTATUS ValidateDriverIOCTLDispatchRegion(_In_ PDRIVER_OBJECT Driver, _In_ PSYSTEM_MODULES Modules, _In_ PWHITELISTED_REGIONS WhitelistedRegions, _Out_ PBOOLEAN Flag); STATIC VOID InitDriverList(_Inout_ PINVALID_DRIVERS_HEAD ListHead); STATIC NTSTATUS AddDriverToList(_Inout_ PINVALID_DRIVERS_HEAD InvalidDriversHead, _In_ PDRIVER_OBJECT Driver, _In_ INT Reason); STATIC VOID RemoveInvalidDriverFromList(_Inout_ PINVALID_DRIVERS_HEAD InvalidDriversHead); STATIC VOID EnumerateInvalidDrivers(_In_ PINVALID_DRIVERS_HEAD InvalidDriversHead); STATIC NTSTATUS ValidateDriverObjectHasBackingModule(_In_ PSYSTEM_MODULES ModuleInformation, _In_ PDRIVER_OBJECT DriverObject, _Out_ PBOOLEAN Result); _IRQL_requires_max_(APC_LEVEL) _Acquires_lock_(_Lock_kind_critical_section_) _Releases_lock_(_Lock_kind_critical_section_) STATIC NTSTATUS ValidateDriverObjects(_In_ PSYSTEM_MODULES SystemModules, _Inout_ PINVALID_DRIVERS_HEAD InvalidDriverListHead); STATIC NTSTATUS AnalyseNmiData(_In_ PNMI_CONTEXT NmiContext, _In_ PSYSTEM_MODULES SystemModules, _Inout_ PIRP Irp); STATIC NTSTATUS LaunchNonMaskableInterrupt(_Inout_ PNMI_CONTEXT NmiContext); _IRQL_requires_max_(APC_LEVEL) STATIC VOID ApcRundownRoutine(_In_ PRKAPC Apc); _IRQL_requires_max_(APC_LEVEL) STATIC VOID ApcKernelRoutine(_In_ PRKAPC Apc, _Inout_ _Deref_pre_maybenull_ PKNORMAL_ROUTINE* NormalRoutine, _Inout_ _Deref_pre_maybenull_ PVOID* NormalContext, _Inout_ _Deref_pre_maybenull_ PVOID* SystemArgument1, _Inout_ _Deref_pre_maybenull_ PVOID* SystemArgument2); _IRQL_requires_max_(APC_LEVEL) STATIC VOID ApcNormalRoutine(_In_opt_ PVOID NormalContext, _In_opt_ PVOID SystemArgument1, _In_opt_ PVOID SystemArgument2); _IRQL_requires_max_(APC_LEVEL) STATIC VOID ValidateThreadViaKernelApcCallback(_In_ PTHREAD_LIST_ENTRY ThreadListEntry, _Inout_opt_ PVOID Context); #ifdef ALLOC_PRAGMA # pragma alloc_text(PAGE, FindSystemModuleByName) # pragma alloc_text(PAGE, PopulateWhitelistedModuleBuffer) # pragma alloc_text(PAGE, ValidateDriverIOCTLDispatchRegion) # pragma alloc_text(PAGE, InitDriverList) # pragma alloc_text(PAGE, AddDriverToList) # pragma alloc_text(PAGE, RemoveInvalidDriverFromList) # pragma alloc_text(PAGE, EnumerateInvalidDrivers) # pragma alloc_text(PAGE, ValidateDriverObjectHasBackingModule) # pragma alloc_text(PAGE, GetSystemModuleInformation) # pragma alloc_text(PAGE, ValidateDriverObjects) # pragma alloc_text(PAGE, HandleValidateDriversIOCTL) # pragma alloc_text(PAGE, IsInstructionPointerInInvalidRegion) # pragma alloc_text(PAGE, AnalyseNmiData) # pragma alloc_text(PAGE, LaunchNonMaskableInterrupt) # pragma alloc_text(PAGE, HandleNmiIOCTL) # pragma alloc_text(PAGE, ApcRundownRoutine) # pragma alloc_text(PAGE, ApcKernelRoutine) # pragma alloc_text(PAGE, ApcNormalRoutine) # pragma alloc_text(PAGE, FlipKThreadMiscFlagsFlag) # pragma alloc_text(PAGE, ValidateThreadsViaKernelApc) # pragma alloc_text(PAGE, ValidateThreadViaKernelApcCallback) #endif /* * TODO: this needs to be refactored to just return the entry not the whole fukin thing * TODO: return ntstatus and pass result in an out parameter */ PRTL_MODULE_EXTENDED_INFO FindSystemModuleByName(_In_ LPCSTR ModuleName, _In_ PSYSTEM_MODULES SystemModules) { PAGED_CODE(); if (!ModuleName || !SystemModules) return NULL; for (INT index = 0; index < SystemModules->module_count; index++) { PRTL_MODULE_EXTENDED_INFO system_module = (PRTL_MODULE_EXTENDED_INFO)((uintptr_t)SystemModules->address + index * sizeof(RTL_MODULE_EXTENDED_INFO)); if (strstr(system_module->FullPathName, ModuleName)) { return system_module; } } return NULL; } STATIC NTSTATUS PopulateWhitelistedModuleBuffer(_Inout_ PVOID Buffer, _In_ PSYSTEM_MODULES SystemModules) { PAGED_CODE(); if (!Buffer || !SystemModules) return STATUS_INVALID_PARAMETER; for (INT index = 0; index < WHITELISTED_MODULE_COUNT; index++) { LPCSTR name = WHITELISTED_MODULES[index]; PRTL_MODULE_EXTENDED_INFO module = FindSystemModuleByName(name, SystemModules); /* not everyone will contain all whitelisted modules */ if (!module) continue; WHITELISTED_REGIONS region = {0}; region.base = (UINT64)module->ImageBase; region.end = region.base + module->ImageSize; RtlCopyMemory((UINT64)Buffer + index * sizeof(WHITELISTED_REGIONS), ®ion, sizeof(WHITELISTED_REGIONS)); } return STATUS_SUCCESS; } STATIC NTSTATUS ValidateDriverIOCTLDispatchRegion(_In_ PDRIVER_OBJECT Driver, _In_ PSYSTEM_MODULES Modules, _In_ PWHITELISTED_REGIONS WhitelistedRegions, _Out_ PBOOLEAN Flag) { PAGED_CODE(); if (!Modules || !Driver || !Flag || !WhitelistedRegions) return STATUS_INVALID_PARAMETER; UINT64 dispatch_function = 0; UINT64 module_base = 0; UINT64 module_end = 0; *Flag = TRUE; dispatch_function = Driver->MajorFunction[IRP_MJ_DEVICE_CONTROL]; if (dispatch_function == NULL) return STATUS_SUCCESS; for (INT index = 0; index < Modules->module_count; index++) { PRTL_MODULE_EXTENDED_INFO system_module = (PRTL_MODULE_EXTENDED_INFO)((uintptr_t)Modules->address + index * sizeof(RTL_MODULE_EXTENDED_INFO)); if (system_module->ImageBase != Driver->DriverStart) continue; /* make sure our driver has a device object which is required for IOCTL */ if (Driver->DeviceObject == NULL) return STATUS_SUCCESS; module_base = (UINT64)system_module->ImageBase; module_end = module_base + system_module->ImageSize; /* firstly, check if its inside its own module */ if (dispatch_function >= module_base && dispatch_function <= module_end) return STATUS_SUCCESS; /* * The WDF framework and other low level drivers often hook the dispatch routines * when initiating the respective config of their framework or system. With a bit of * digging you can view the drivers reponsible for the hooks. What this means is * that there will be legit drivers with dispatch routines that point outside of * ntoskrnl and their own memory region. So, I have formed a list which contains the * drivers that perform these hooks and we iteratively check if the dispatch routine * is contained within one of these whitelisted regions. A note on how to imrpove * this is the fact that a code cave can be used inside a whitelisted region which * then jumps to an invalid region such as a manually mapped driver. So in the * future we should implement a function which checks for standard hook * implementations like mov rax jmp rax etc. */ for (INT index = 0; index < WHITELISTED_MODULE_COUNT; index++) { if (dispatch_function >= WhitelistedRegions[index].base && dispatch_function <= WhitelistedRegions[index].end) return STATUS_SUCCESS; } DEBUG_WARNING("Driver with invalid dispatch routine found: %s", system_module->FullPathName); *Flag = FALSE; return STATUS_SUCCESS; } return STATUS_SUCCESS; } STATIC VOID InitDriverList(_Inout_ PINVALID_DRIVERS_HEAD ListHead) { PAGED_CODE(); ListHead->count = 0; ListHead->first_entry = NULL; } STATIC NTSTATUS AddDriverToList(_Inout_ PINVALID_DRIVERS_HEAD InvalidDriversHead, _In_ PDRIVER_OBJECT Driver, _In_ INT Reason) { PAGED_CODE(); PINVALID_DRIVER new_entry = ExAllocatePool2( POOL_FLAG_NON_PAGED, sizeof(INVALID_DRIVER), INVALID_DRIVER_LIST_ENTRY_POOL); if (!new_entry) return STATUS_MEMORY_NOT_ALLOCATED; new_entry->driver = Driver; new_entry->reason = Reason; new_entry->next = InvalidDriversHead->first_entry; InvalidDriversHead->first_entry = new_entry; return STATUS_SUCCESS; } STATIC VOID RemoveInvalidDriverFromList(_Inout_ PINVALID_DRIVERS_HEAD InvalidDriversHead) { PAGED_CODE(); if (InvalidDriversHead->first_entry) { PINVALID_DRIVER entry = InvalidDriversHead->first_entry; InvalidDriversHead->first_entry = InvalidDriversHead->first_entry->next; ExFreePoolWithTag(entry, INVALID_DRIVER_LIST_ENTRY_POOL); } } STATIC VOID EnumerateInvalidDrivers(_In_ PINVALID_DRIVERS_HEAD InvalidDriversHead) { PAGED_CODE(); PINVALID_DRIVER entry = InvalidDriversHead->first_entry; while (entry != NULL) { DEBUG_VERBOSE("Invalid Driver: %wZ", entry->driver->DriverName); entry = entry->next; } } STATIC NTSTATUS ValidateDriverObjectHasBackingModule(_In_ PSYSTEM_MODULES ModuleInformation, _In_ PDRIVER_OBJECT DriverObject, _Out_ PBOOLEAN Result) { PAGED_CODE(); if (!ModuleInformation || !DriverObject || !Result) return STATUS_INVALID_PARAMETER; for (INT i = 0; i < ModuleInformation->module_count; i++) { PRTL_MODULE_EXTENDED_INFO system_module = (PRTL_MODULE_EXTENDED_INFO)((uintptr_t)ModuleInformation->address + i * sizeof(RTL_MODULE_EXTENDED_INFO)); if (system_module->ImageSize == 0 || system_module->ImageBase == 0) return STATUS_INVALID_MEMBER; if (system_module->ImageBase == DriverObject->DriverStart) { *Result = TRUE; return STATUS_SUCCESS; } } DEBUG_WARNING("Driver found with no backing system image at address: %llx", (UINT64)DriverObject->DriverStart); *Result = FALSE; return STATUS_SUCCESS; } // https://imphash.medium.com/windows-process-internals-a-few-concepts-to-know-before-jumping-on-memory-forensics-part-3-4a0e195d947b NTSTATUS GetSystemModuleInformation(_Out_ PSYSTEM_MODULES ModuleInformation) { PAGED_CODE(); if (!ModuleInformation) return STATUS_INVALID_PARAMETER; ULONG size = 0; NTSTATUS status = STATUS_UNSUCCESSFUL; /* * query system module information without an output buffer to get * number of bytes required to store all module info structures */ status = RtlQueryModuleInformation(&size, sizeof(RTL_MODULE_EXTENDED_INFO), NULL); if (!NT_SUCCESS(status)) { DEBUG_ERROR("RtlQueryModuleInformation failed with status %x", status); return status; } /* Allocate a pool equal to the output size of RtlQueryModuleInformation */ PRTL_MODULE_EXTENDED_INFO driver_information = ExAllocatePool2(POOL_FLAG_NON_PAGED, size, SYSTEM_MODULES_POOL); if (!driver_information) { DEBUG_ERROR("Failed to allocate pool LOL"); return STATUS_MEMORY_NOT_ALLOCATED; } /* Query the modules again this time passing a pointer to the allocated buffer */ status = RtlQueryModuleInformation(&size, sizeof(RTL_MODULE_EXTENDED_INFO), driver_information); if (!NT_SUCCESS(status)) { DEBUG_ERROR("RtlQueryModuleInformation 2 failed with status %x", status); ExFreePoolWithTag(driver_information, SYSTEM_MODULES_POOL); return STATUS_ABANDONED; } ModuleInformation->address = driver_information; ModuleInformation->module_count = size / sizeof(RTL_MODULE_EXTENDED_INFO); return status; } _IRQL_requires_max_(APC_LEVEL) _Acquires_lock_(_Lock_kind_critical_section_) _Releases_lock_(_Lock_kind_critical_section_) STATIC NTSTATUS ValidateDriverObjects(_In_ PSYSTEM_MODULES SystemModules, _Inout_ PINVALID_DRIVERS_HEAD InvalidDriverListHead) { PAGED_CODE(); if (!SystemModules || !InvalidDriverListHead) return STATUS_INVALID_PARAMETER; HANDLE handle = NULL; OBJECT_ATTRIBUTES attributes = {0}; PVOID directory = {0}; UNICODE_STRING directory_name = {0}; PVOID whitelisted_regions_buffer = NULL; NTSTATUS status = STATUS_UNSUCCESSFUL; POBJECT_DIRECTORY directory_object = NULL; RtlInitUnicodeString(&directory_name, L"\\Driver"); InitializeObjectAttributes(&attributes, &directory_name, OBJ_CASE_INSENSITIVE, NULL, NULL); status = ZwOpenDirectoryObject(&handle, DIRECTORY_ALL_ACCESS, &attributes); if (!NT_SUCCESS(status)) { DEBUG_ERROR("ZwOpenDirectoryObject failed with status %x", status); return status; } status = ObReferenceObjectByHandle( handle, DIRECTORY_ALL_ACCESS, NULL, KernelMode, &directory, NULL); if (!NT_SUCCESS(status)) { DEBUG_ERROR("ObReferenceObjectByHandle failed with status %x", status); ZwClose(handle); return status; } /* * Windows organises its drivers in object directories (not the same as * files directories). For the driver directory, there are 37 entries, * each driver is hashed and indexed. If there is a driver with a duplicate * index, it is inserted into same index in a linked list using the * _OBJECT_DIRECTORY_ENTRY struct. So to enumerate all drivers we visit * each entry in the hashmap, enumerate all objects in the linked list * at entry j then we increment the hashmap index i. The motivation behind * this is that when a driver is accessed, it is brought to the first index * in the linked list, so drivers that are accessed the most can be * accessed quickly */ directory_object = (POBJECT_DIRECTORY)directory; ExAcquirePushLockExclusiveEx(&directory_object->Lock, NULL); whitelisted_regions_buffer = ExAllocatePool2(POOL_FLAG_NON_PAGED, WHITELISTED_MODULE_COUNT * MODULE_MAX_STRING_SIZE, WHITELISTED_MODULE_TAG); if (!whitelisted_regions_buffer) goto end; status = PopulateWhitelistedModuleBuffer(whitelisted_regions_buffer, SystemModules); if (!NT_SUCCESS(status)) { DEBUG_ERROR("PopulateWhitelistedModuleBuffer failed with status %x", status); goto end; } for (INT index = 0; index < NUMBER_HASH_BUCKETS; index++) { POBJECT_DIRECTORY_ENTRY entry = directory_object->HashBuckets[index]; if (!entry) continue; POBJECT_DIRECTORY_ENTRY sub_entry = entry; while (sub_entry) { BOOLEAN flag = FALSE; PDRIVER_OBJECT current_driver = sub_entry->Object; /* validate driver has backing module */ status = ValidateDriverObjectHasBackingModule( SystemModules, current_driver, &flag); if (!NT_SUCCESS(status)) { DEBUG_ERROR( "ValidateDriverObjectHasBackingModule failed with status %x", status); goto end; } if (!flag) { status = AddDriverToList(InvalidDriverListHead, current_driver, REASON_NO_BACKING_MODULE); if (!NT_SUCCESS(status)) DEBUG_ERROR("AddDriverToList failed with status %x", status); else InvalidDriverListHead->count += 1; } /* validate drivers IOCTL dispatch routines */ status = ValidateDriverIOCTLDispatchRegion( current_driver, SystemModules, whitelisted_regions_buffer, &flag); if (!NT_SUCCESS(status)) { DEBUG_ERROR("ValidateDriverIOCTLDispatchRegion failed with status %x", status); goto end; } if (!flag) { status = AddDriverToList(InvalidDriverListHead, current_driver, REASON_INVALID_IOCTL_DISPATCH); if (!NT_SUCCESS(status)) DEBUG_ERROR("AddDriverToList failed with status %x", status); else InvalidDriverListHead->count += 1; } sub_entry = sub_entry->ChainLink; } } end: if (whitelisted_regions_buffer) ExFreePoolWithTag(whitelisted_regions_buffer, WHITELISTED_MODULE_TAG); ExReleasePushLockExclusiveEx(&directory_object->Lock, 0); ObDereferenceObject(directory); ZwClose(handle); return STATUS_SUCCESS; } NTSTATUS HandleValidateDriversIOCTL(_Inout_ PIRP Irp) { PAGED_CODE(); NTSTATUS status = STATUS_UNSUCCESSFUL; PVOID buffer = NULL; ULONG buffer_size = 0; SYSTEM_MODULES system_modules = {0}; MODULE_VALIDATION_FAILURE_HEADER header = {0}; PINVALID_DRIVERS_HEAD head = NULL; /* Fix annoying visual studio linting error */ RtlZeroMemory(&system_modules, sizeof(SYSTEM_MODULES)); status = GetSystemModuleInformation(&system_modules); if (!NT_SUCCESS(status)) { DEBUG_ERROR("GetSystemModuleInformation failed with status %x", status); return status; } head = ExAllocatePool2( POOL_FLAG_NON_PAGED, sizeof(INVALID_DRIVERS_HEAD), INVALID_DRIVER_LIST_HEAD_POOL); if (!head) { ExFreePoolWithTag(system_modules.address, SYSTEM_MODULES_POOL); return STATUS_MEMORY_NOT_ALLOCATED; } /* * Use a linked list here so that so we have easy access to the invalid drivers * which we can then use to copy the drivers logic for further analysis in * identifying drivers specifically used for the purpose of cheating */ InitDriverList(head); status = ValidateDriverObjects(&system_modules, head); if (!NT_SUCCESS(status)) { DEBUG_ERROR("ValidateDriverObjects failed with status %x", status); goto end; } header.module_count = head->count >= MODULE_VALIDATION_FAILURE_MAX_REPORT_COUNT ? MODULE_VALIDATION_FAILURE_MAX_REPORT_COUNT : head->count; if (head->count > 0) { DEBUG_VERBOSE("System has an invalid driver count of: %i", head->count); buffer_size = sizeof(MODULE_VALIDATION_FAILURE_HEADER) + MODULE_VALIDATION_FAILURE_MAX_REPORT_COUNT * sizeof(MODULE_VALIDATION_FAILURE); status = ValidateIrpOutputBuffer(Irp, buffer_size); if (!NT_SUCCESS(status)) { DEBUG_ERROR("Failed to validate output buffer."); goto end; } buffer = ExAllocatePool2(POOL_FLAG_NON_PAGED, buffer_size, MODULES_REPORT_POOL_TAG); if (!buffer) { ExFreePoolWithTag(head, INVALID_DRIVER_LIST_HEAD_POOL); ExFreePoolWithTag(system_modules.address, SYSTEM_MODULES_POOL); return STATUS_MEMORY_NOT_ALLOCATED; } Irp->IoStatus.Information = sizeof(MODULE_VALIDATION_FAILURE_HEADER) + MODULE_VALIDATION_FAILURE_MAX_REPORT_COUNT * sizeof(MODULE_VALIDATION_FAILURE); RtlCopyMemory(buffer, &header, sizeof(MODULE_VALIDATION_FAILURE_HEADER)); for (INT index = 0; index < head->count; index++) { /* make sure we free any non reported modules */ if (index >= MODULE_VALIDATION_FAILURE_MAX_REPORT_COUNT) { RemoveInvalidDriverFromList(head); continue; } MODULE_VALIDATION_FAILURE report = {0}; report.report_code = REPORT_MODULE_VALIDATION_FAILURE; report.report_type = head->first_entry->reason; report.driver_base_address = head->first_entry->driver->DriverStart; report.driver_size = head->first_entry->driver->DriverSize; ANSI_STRING string = {0}; string.Length = 0; string.MaximumLength = MODULE_REPORT_DRIVER_NAME_BUFFER_SIZE; string.Buffer = &report.driver_name; status = RtlUnicodeStringToAnsiString( &string, &head->first_entry->driver->DriverName, FALSE); /* still continue if we fail to get the driver name */ if (!NT_SUCCESS(status)) DEBUG_ERROR("RtlUnicodeStringToAnsiString failed with status %x", status); RtlCopyMemory((UINT64)buffer + sizeof(MODULE_VALIDATION_FAILURE_HEADER) + index * sizeof(MODULE_VALIDATION_FAILURE), &report, sizeof(MODULE_VALIDATION_FAILURE)); RemoveInvalidDriverFromList(head); } RtlCopyMemory(Irp->AssociatedIrp.SystemBuffer, buffer, sizeof(MODULE_VALIDATION_FAILURE_HEADER) + MODULE_VALIDATION_FAILURE_MAX_REPORT_COUNT * sizeof(MODULE_VALIDATION_FAILURE)); ExFreePoolWithTag(buffer, MODULES_REPORT_POOL_TAG); } else { DEBUG_INFO("Found no invalid drivers on the system."); } end: ExFreePoolWithTag(head, INVALID_DRIVER_LIST_HEAD_POOL); ExFreePoolWithTag(system_modules.address, SYSTEM_MODULES_POOL); return status; } NTSTATUS IsInstructionPointerInInvalidRegion(_In_ UINT64 RIP, _In_ PSYSTEM_MODULES SystemModules, _Out_ PBOOLEAN Result) { PAGED_CODE(); if (!RIP || !SystemModules || !Result) return STATUS_INVALID_PARAMETER; /* Note that this does not check for HAL or PatchGuard Execution */ for (INT i = 0; i < SystemModules->module_count; i++) { PRTL_MODULE_EXTENDED_INFO system_module = (PRTL_MODULE_EXTENDED_INFO)((uintptr_t)SystemModules->address + i * sizeof(RTL_MODULE_EXTENDED_INFO)); UINT64 base = (UINT64)system_module->ImageBase; UINT64 end = base + system_module->ImageSize; if (RIP >= base && RIP <= end) { *Result = TRUE; return STATUS_SUCCESS; } } *Result = FALSE; return STATUS_SUCCESS; } /* * todo: rename this to analyse stackwalk or something */ STATIC NTSTATUS AnalyseNmiData(_In_ PNMI_CONTEXT NmiContext, _In_ PSYSTEM_MODULES SystemModules, _Inout_ PIRP Irp) { PAGED_CODE(); if (!NmiContext || !SystemModules) return STATUS_INVALID_PARAMETER; NTSTATUS status = STATUS_UNSUCCESSFUL; for (INT core = 0; core < NmiContext->core_count; core++) { PNMI_CORE_CONTEXT context = (PNMI_CORE_CONTEXT)((uintptr_t)NmiContext->nmi_core_context + core * sizeof(NMI_CORE_CONTEXT)); /* Make sure our NMIs were run */ if (!context->nmi_callbacks_run) { NTSTATUS status = ValidateIrpOutputBuffer(Irp, sizeof(NMI_CALLBACK_FAILURE)); if (!NT_SUCCESS(status)) { DEBUG_ERROR("ValidateIrpOutputBuffer failed with status %x", status); return status; } NMI_CALLBACK_FAILURE report = {0}; report.report_code = REPORT_NMI_CALLBACK_FAILURE; report.kthread_address = NULL; report.invalid_rip = NULL; report.were_nmis_disabled = TRUE; Irp->IoStatus.Information = sizeof(NMI_CALLBACK_FAILURE); RtlCopyMemory( Irp->AssociatedIrp.SystemBuffer, &report, sizeof(NMI_CALLBACK_FAILURE)); return STATUS_SUCCESS; } PNMI_CALLBACK_DATA thread_data = (PNMI_CALLBACK_DATA)((uintptr_t)NmiContext->thread_data_pool + core * sizeof(NMI_CALLBACK_DATA)); DEBUG_VERBOSE("Analysing Nmi Data for: cpu number: %i callback count: %i", core, context->nmi_callbacks_run); /* Walk the stack */ for (INT frame = 0; frame < thread_data->num_frames_captured; frame++) { BOOLEAN flag = TRUE; DWORD64 stack_frame = *(DWORD64*)(((uintptr_t)NmiContext->stack_frames + thread_data->stack_frames_offset + frame * sizeof(PVOID))); status = IsInstructionPointerInInvalidRegion(stack_frame, SystemModules, &flag); if (!NT_SUCCESS(status)) { DEBUG_ERROR( "IsInstructionPointerInInvalidRegion failed with status %x", status); continue; } if (flag == FALSE) { status = ValidateIrpOutputBuffer(Irp, sizeof(NMI_CALLBACK_FAILURE)); if (!NT_SUCCESS(status)) { DEBUG_ERROR("ValidateIrpOutputBuffer failed with status %x", status); return status; } /* * Note: for now, we only handle 1 report at a time so we stop the * analysis once we receive a report since we only send a buffer * large enough for 1 report. In the future this should be changed * to a buffer that can hold atleast 4 reports (since the chance we * get 4 reports with a single NMI would be impossible) so we can * continue parsing the rest of the stack frames after receiving a * single report. */ NMI_CALLBACK_FAILURE report = {0}; report.report_code = REPORT_NMI_CALLBACK_FAILURE; report.kthread_address = thread_data->kthread_address; report.invalid_rip = stack_frame; report.were_nmis_disabled = FALSE; Irp->IoStatus.Information = sizeof(NMI_CALLBACK_FAILURE); RtlCopyMemory(Irp->AssociatedIrp.SystemBuffer, &report, sizeof(NMI_CALLBACK_FAILURE)); return STATUS_SUCCESS; } } } return STATUS_SUCCESS; } _IRQL_requires_max_(HIGH_LEVEL) STATIC BOOLEAN NmiCallback(_Inout_opt_ PVOID Context, _In_ BOOLEAN Handled) { UNREFERENCED_PARAMETER(Handled); PKTHREAD current_thread = KeGetCurrentThread(); NMI_CALLBACK_DATA thread_data = {0}; PNMI_CONTEXT nmi_context = (PNMI_CONTEXT)Context; ULONG proc_num = KeGetCurrentProcessorNumber(); if (!nmi_context) return TRUE; /* * Cannot allocate pool in this function as it runs at IRQL >= dispatch level * so ive just allocated a global pool with size equal to 0x200 * num_procs */ INT num_frames_captured = RtlCaptureStackBackTrace(NULL, STACK_FRAME_POOL_SIZE / sizeof(UINT64), (uintptr_t)nmi_context->stack_frames + proc_num * STACK_FRAME_POOL_SIZE, NULL); /* * This function is run in the context of the interrupted thread hence we can * gather any and all information regarding the thread that may be useful for analysis */ thread_data.kthread_address = (UINT64)current_thread; thread_data.kprocess_address = (UINT64)PsGetCurrentProcess(); thread_data.stack_base = *((UINT64*)((uintptr_t)current_thread + KTHREAD_STACK_BASE_OFFSET)); thread_data.stack_limit = *((UINT64*)((uintptr_t)current_thread + KTHREAD_STACK_LIMIT_OFFSET)); thread_data.start_address = *((UINT64*)((uintptr_t)current_thread + KTHREAD_START_ADDRESS_OFFSET)); thread_data.cr3 = __readcr3(); thread_data.stack_frames_offset = proc_num * STACK_FRAME_POOL_SIZE; thread_data.num_frames_captured = num_frames_captured; RtlCopyMemory(((uintptr_t)nmi_context->thread_data_pool) + proc_num * sizeof(thread_data), &thread_data, sizeof(thread_data)); PNMI_CORE_CONTEXT core_context = (PNMI_CORE_CONTEXT)((uintptr_t)nmi_context->nmi_core_context + proc_num * sizeof(NMI_CORE_CONTEXT)); core_context->nmi_callbacks_run += 1; return TRUE; } #define NMI_DELAY_TIME 100 * 10000 STATIC NTSTATUS LaunchNonMaskableInterrupt(_Inout_ PNMI_CONTEXT NmiContext) { PAGED_CODE(); if (!NmiContext) return STATUS_INVALID_PARAMETER; PKAFFINITY_EX ProcAffinityPool = ExAllocatePool2(POOL_FLAG_NON_PAGED, sizeof(KAFFINITY_EX), PROC_AFFINITY_POOL); if (!ProcAffinityPool) return STATUS_MEMORY_NOT_ALLOCATED; NmiContext->stack_frames = ExAllocatePool2( POOL_FLAG_NON_PAGED, NmiContext->core_count * STACK_FRAME_POOL_SIZE, STACK_FRAMES_POOL); if (!NmiContext->stack_frames) { ExFreePoolWithTag(ProcAffinityPool, PROC_AFFINITY_POOL); return STATUS_MEMORY_NOT_ALLOCATED; } NmiContext->thread_data_pool = ExAllocatePool2(POOL_FLAG_NON_PAGED, NmiContext->core_count * sizeof(NMI_CALLBACK_DATA), THREAD_DATA_POOL); if (!NmiContext->thread_data_pool) { ExFreePoolWithTag(NmiContext->stack_frames, STACK_FRAMES_POOL); ExFreePoolWithTag(ProcAffinityPool, PROC_AFFINITY_POOL); return STATUS_MEMORY_NOT_ALLOCATED; } LARGE_INTEGER delay = {0}; delay.QuadPart -= NMI_DELAY_TIME; for (ULONG core = 0; core < NmiContext->core_count; core++) { KeInitializeAffinityEx(ProcAffinityPool); KeAddProcessorAffinityEx(ProcAffinityPool, core); HalSendNMI(ProcAffinityPool); /* * Only a single NMI can be active at any given time, so arbitrarily * delay execution to allow time for the NMI to be processed */ KeDelayExecutionThread(KernelMode, FALSE, &delay); } ExFreePoolWithTag(ProcAffinityPool, PROC_AFFINITY_POOL); return STATUS_SUCCESS; } NTSTATUS HandleNmiIOCTL(_Inout_ PIRP Irp) { PAGED_CODE(); NTSTATUS status = STATUS_UNSUCCESSFUL; SYSTEM_MODULES system_modules = {0}; NMI_CONTEXT nmi_context = {0}; PVOID callback_handle = NULL; nmi_context.core_count = KeQueryActiveProcessorCountEx(0); nmi_context.nmi_core_context = ExAllocatePool2(POOL_FLAG_NON_PAGED, nmi_context.core_count * sizeof(NMI_CORE_CONTEXT), NMI_CONTEXT_POOL); if (!nmi_context.nmi_core_context) return STATUS_MEMORY_NOT_ALLOCATED; /* * We want to register and unregister our callback each time so it becomes harder * for people to hook our callback and get up to some funny business */ callback_handle = KeRegisterNmiCallback(NmiCallback, &nmi_context); if (!callback_handle) { DEBUG_ERROR("KeRegisterNmiCallback failed with no status."); ExFreePoolWithTag(nmi_context.nmi_core_context, NMI_CONTEXT_POOL); return STATUS_UNSUCCESSFUL; } /* * We query the system modules each time since they can potentially * change at any time */ status = GetSystemModuleInformation(&system_modules); if (!NT_SUCCESS(status)) { DEBUG_ERROR("GetSystemModuleInformation failed with status %x", status); return status; } status = LaunchNonMaskableInterrupt(&nmi_context); if (!NT_SUCCESS(status)) { DEBUG_ERROR("LaunchNonMaskableInterrupt failed with status %x", status); if (system_modules.address) ExFreePoolWithTag(system_modules.address, SYSTEM_MODULES_POOL); return status; } status = AnalyseNmiData(&nmi_context, &system_modules, Irp); if (!NT_SUCCESS(status)) DEBUG_ERROR("AnalyseNmiData failed with status %x", status); if (system_modules.address) ExFreePoolWithTag(system_modules.address, SYSTEM_MODULES_POOL); if (nmi_context.nmi_core_context) ExFreePoolWithTag(nmi_context.nmi_core_context, NMI_CONTEXT_POOL); if (nmi_context.stack_frames) ExFreePoolWithTag(nmi_context.stack_frames, STACK_FRAMES_POOL); if (nmi_context.thread_data_pool) ExFreePoolWithTag(nmi_context.thread_data_pool, THREAD_DATA_POOL); KeDeregisterNmiCallback(callback_handle); return status; } /* * The RundownRoutine is executed if the thread terminates before the APC was delivered to * user mode. */ _IRQL_requires_max_(APC_LEVEL) STATIC VOID ApcRundownRoutine(_In_ PRKAPC Apc) { PAGED_CODE(); FreeApcAndDecrementApcCount(Apc, APC_CONTEXT_ID_STACKWALK); } /* * The KernelRoutine is executed in kernel mode at APC_LEVEL before the APC is delivered. This * is also where we want to free our APC object. */ _IRQL_requires_max_(APC_LEVEL) STATIC VOID ApcKernelRoutine(_In_ PRKAPC Apc, _Inout_ _Deref_pre_maybenull_ PKNORMAL_ROUTINE* NormalRoutine, _Inout_ _Deref_pre_maybenull_ PVOID* NormalContext, _Inout_ _Deref_pre_maybenull_ PVOID* SystemArgument1, _Inout_ _Deref_pre_maybenull_ PVOID* SystemArgument2) { PAGED_CODE(); NTSTATUS status = STATUS_UNSUCCESSFUL; PVOID buffer = NULL; INT frames_captured = 0; UINT64 stack_frame = 0; BOOLEAN flag = FALSE; PAPC_STACKWALK_CONTEXT context = NULL; PTHREAD_LIST_ENTRY thread_list_entry = NULL; context = (PAPC_STACKWALK_CONTEXT)Apc->NormalContext; FindThreadListEntryByThreadAddress(KeGetCurrentThread(), &thread_list_entry); if (!thread_list_entry) return; buffer = ExAllocatePool2(POOL_FLAG_NON_PAGED, STACK_FRAME_POOL_SIZE, POOL_TAG_APC); if (!buffer) goto free; frames_captured = RtlCaptureStackBackTrace(NULL, STACK_FRAME_POOL_SIZE / sizeof(UINT64), buffer, NULL); if (frames_captured == NULL) goto free; for (INT index = 0; index < frames_captured; index++) { stack_frame = *(UINT64*)((UINT64)buffer + index * sizeof(UINT64)); /* * Apc->NormalContext holds the address of our context data structure that we passed * into KeInitializeApc as the last argument. */ status = IsInstructionPointerInInvalidRegion(stack_frame, context->modules, &flag); if (!NT_SUCCESS(status)) { DEBUG_ERROR("IsInstructionPointerInInvalidRegion failed with status %x", status); goto free; } if (flag == FALSE) { PAPC_STACKWALK_REPORT report = ExAllocatePool2( POOL_FLAG_NON_PAGED, sizeof(APC_STACKWALK_REPORT), POOL_TAG_APC); if (!report) goto free; report->report_code = REPORT_APC_STACKWALK; report->kthread_address = (UINT64)KeGetCurrentThread(); report->invalid_rip = stack_frame; RtlCopyMemory(&report->driver, (UINT64)stack_frame - 0x500, APC_STACKWALK_BUFFER_SIZE); InsertReportToQueue(report); } } free: if (buffer) ExFreePoolWithTag(buffer, POOL_TAG_APC); FreeApcAndDecrementApcCount(Apc, APC_CONTEXT_ID_STACKWALK); thread_list_entry->apc = NULL; thread_list_entry->apc_queued = FALSE; } /* * The NormalRoutine is executed in user mode when the APC is delivered. */ _IRQL_requires_max_(APC_LEVEL) STATIC VOID ApcNormalRoutine(_In_opt_ PVOID NormalContext, _In_opt_ PVOID SystemArgument1, _In_opt_ PVOID SystemArgument2) { PAGED_CODE(); } VOID FlipKThreadMiscFlagsFlag(_In_ PKTHREAD Thread, _In_ ULONG FlagIndex, _In_ BOOLEAN NewValue) { PAGED_CODE(); PLONG misc_flags = (PLONG)((UINT64)Thread + KTHREAD_MISC_FLAGS_OFFSET); ULONG mask = 1ul << FlagIndex; if (NewValue) *misc_flags |= mask; else *misc_flags &= ~mask; } #define THREAD_STATE_TERMINATED 4 #define THREAD_STATE_WAIT 5 #define THREAD_STATE_INIT 0 _IRQL_requires_max_(APC_LEVEL) STATIC VOID ValidateThreadViaKernelApcCallback(_In_ PTHREAD_LIST_ENTRY ThreadListEntry, _Inout_opt_ PVOID Context) { PAGED_CODE(); PKAPC apc = NULL; BOOLEAN apc_status = FALSE; PLONG misc_flags = NULL; PCHAR previous_mode = NULL; PUCHAR state = NULL; BOOLEAN apc_queueable = FALSE; PAPC_STACKWALK_CONTEXT context = (PAPC_STACKWALK_CONTEXT)Context; LPCSTR process_name = PsGetProcessImageFileName(ThreadListEntry->owning_process); /* * we dont want to schedule an apc to threads owned by the kernel * * Actually we do... todo: fix this. */ if (ThreadListEntry->owning_process == PsInitialSystemProcess || !Context) return; /* We are not interested in these processess.. for now lol */ if (!strcmp(process_name, "svchost.exe") || !strcmp(process_name, "Registry") || !strcmp(process_name, "smss.exe") || !strcmp(process_name, "csrss.exe") || !strcmp(process_name, "explorer.exe") || !strcmp(process_name, "svchost.exe") || !strcmp(process_name, "lsass.exe") || !strcmp(process_name, "MemCompression") || !strcmp(process_name, "WerFault.exe")) return; DEBUG_VERBOSE("Validating thread: %llx, process name: %s via kernel APC stackwalk.", ThreadListEntry->thread, process_name); if (ThreadListEntry->thread == KeGetCurrentThread() || !ThreadListEntry->thread) return; /* * Its possible to set the KThread->ApcQueueable flag to false ensuring that no APCs can be * queued to the thread, as KeInsertQueueApc will check this flag before queueing an APC so * lets make sure we flip this before before queueing ours. Since we filter out any system * threads this should be fine... c: */ misc_flags = (PLONG)((UINT64)ThreadListEntry->thread + KTHREAD_MISC_FLAGS_OFFSET); previous_mode = (PCHAR)((UINT64)ThreadListEntry->thread + KTHREAD_PREVIOUS_MODE_OFFSET); state = (PUCHAR)((UINT64)ThreadListEntry->thread + KTHREAD_STATE_OFFSET); /* we dont care about user mode threads */ // if (*previous_mode == UserMode) // return; /* todo: We should also flag all threads that have the flag set to false */ if (*misc_flags >> KTHREAD_MISC_FLAGS_APC_QUEUEABLE == FALSE) FlipKThreadMiscFlagsFlag( ThreadListEntry->thread, KTHREAD_MISC_FLAGS_APC_QUEUEABLE, TRUE); /* * force thread into an alertable state, noting that this does not guarantee that our APC * will be run. */ if (*misc_flags >> KTHREAD_MISC_FLAGS_ALERTABLE == FALSE) FlipKThreadMiscFlagsFlag( ThreadListEntry->thread, KTHREAD_MISC_FLAGS_ALERTABLE, TRUE); apc = (PKAPC)ExAllocatePool2(POOL_FLAG_NON_PAGED, sizeof(KAPC), POOL_TAG_APC); if (!apc) return; KeInitializeApc(apc, ThreadListEntry->thread, OriginalApcEnvironment, ApcKernelRoutine, ApcRundownRoutine, ApcNormalRoutine, KernelMode, Context); apc_status = KeInsertQueueApc(apc, NULL, NULL, IO_NO_INCREMENT); if (!apc_status) { DEBUG_ERROR("KeInsertQueueApc failed with no status."); ExFreePoolWithTag(apc, POOL_TAG_APC); return; } ThreadListEntry->apc = apc; ThreadListEntry->apc_queued = TRUE; IncrementApcCount(APC_CONTEXT_ID_STACKWALK); } /* * Since NMIs are only executed on the thread that is running on each logical core, it makes * sense to make use of APCs that, while can be masked off, provide us to easily issue a callback * routine to threads we want a stack trace of. Hence by utilising both APCs and NMIs we get * excellent coverage of the entire system. */ NTSTATUS ValidateThreadsViaKernelApc() { PAGED_CODE(); NTSTATUS status = STATUS_UNSUCCESSFUL; PAPC_STACKWALK_CONTEXT context = NULL; /* First, ensure we dont already have an ongoing operation */ GetApcContext(&context, APC_CONTEXT_ID_STACKWALK); if (context) { DEBUG_WARNING("Existing APC_STACKWALK operation already in progress."); return STATUS_SUCCESS; } context = ExAllocatePool2(POOL_FLAG_NON_PAGED, sizeof(APC_STACKWALK_CONTEXT), POOL_TAG_APC); if (!context) return STATUS_MEMORY_NOT_ALLOCATED; context->header.context_id = APC_CONTEXT_ID_STACKWALK; context->modules = ExAllocatePool2(POOL_FLAG_NON_PAGED, sizeof(SYSTEM_MODULES), POOL_TAG_APC); if (!context->modules) { ExFreePoolWithTag(context, POOL_TAG_APC); return STATUS_MEMORY_NOT_ALLOCATED; } status = GetSystemModuleInformation(context->modules); if (!NT_SUCCESS(status)) { DEBUG_ERROR("GetSystemModuleInformation failed with status %x", status); ExFreePoolWithTag(context->modules, POOL_TAG_APC); ExFreePoolWithTag(context, POOL_TAG_APC); return STATUS_MEMORY_NOT_ALLOCATED; } status = InsertApcContext(context); if (!NT_SUCCESS(status)) { DEBUG_ERROR("InsertApcContext failed with status %x", status); ExFreePoolWithTag(context->modules, POOL_TAG_APC); ExFreePoolWithTag(context, POOL_TAG_APC); return status; } context->header.allocation_in_progress = TRUE; EnumerateThreadListWithCallbackRoutine(ValidateThreadViaKernelApcCallback, context); context->header.allocation_in_progress = FALSE; return status; } VOID FreeApcStackwalkApcContextInformation(_Inout_ PAPC_STACKWALK_CONTEXT Context) { if (Context->modules->address) ExFreePoolWithTag(Context->modules->address, SYSTEM_MODULES_POOL); if (Context->modules) ExFreePoolWithTag(Context->modules, POOL_TAG_APC); } /* * Since NMI evasion methods are becoming commonplace, we can use interprocess * interrupts. For now i am just using the same nmi methods. To accomplish this * we can use KeIpiGenericCall, which runs a specified routine on all processors * simultaneously. The callback routine runs at IRQL IPI_LEVEL which is > DIRQL. */ NTSTATUS LaunchInterProcessInterrupt(_In_ PIRP Irp) { PAGED_CODE(); NTSTATUS status = STATUS_UNSUCCESSFUL; SYSTEM_MODULES system_modules = {0}; NMI_CONTEXT ipi_context = {0}; PVOID callback_handle = NULL; DEBUG_VERBOSE("Launching Inter Process Interrupt"); ipi_context.core_count = KeQueryActiveProcessorCountEx(0); ipi_context.nmi_core_context = ExAllocatePool2(POOL_FLAG_NON_PAGED, ipi_context.core_count * sizeof(NMI_CORE_CONTEXT), NMI_CONTEXT_POOL); if (!ipi_context.nmi_core_context) return STATUS_MEMORY_NOT_ALLOCATED; ipi_context.stack_frames = ExAllocatePool2( POOL_FLAG_NON_PAGED, ipi_context.core_count * STACK_FRAME_POOL_SIZE, STACK_FRAMES_POOL); if (!ipi_context.stack_frames) { status = STATUS_MEMORY_NOT_ALLOCATED; goto end; } ipi_context.thread_data_pool = ExAllocatePool2(POOL_FLAG_NON_PAGED, ipi_context.core_count * sizeof(NMI_CALLBACK_DATA), THREAD_DATA_POOL); if (!ipi_context.thread_data_pool) { status = STATUS_MEMORY_NOT_ALLOCATED; goto end; } /* * We query the system modules each time since they can potentially * change at any time */ status = GetSystemModuleInformation(&system_modules); if (!NT_SUCCESS(status)) { DEBUG_ERROR("GetSystemModuleInformation failed with status %x", status); goto end; } KeIpiGenericCall(NmiCallback, &ipi_context); /* * since the routines are run simultaneously, once we've reached here we can be sure * all routines have run. */ status = AnalyseNmiData(&ipi_context, &system_modules, Irp); if (!NT_SUCCESS(status)) DEBUG_ERROR("AnalyseNmiData failed with status %x", status); end: if (system_modules.address) ExFreePoolWithTag(system_modules.address, SYSTEM_MODULES_POOL); if (ipi_context.nmi_core_context) ExFreePoolWithTag(ipi_context.nmi_core_context, NMI_CONTEXT_POOL); if (ipi_context.stack_frames) ExFreePoolWithTag(ipi_context.stack_frames, STACK_FRAMES_POOL); if (ipi_context.thread_data_pool) ExFreePoolWithTag(ipi_context.thread_data_pool, THREAD_DATA_POOL); return status; }