refactor cid detection, remove old kpcrb detection

This commit is contained in:
lhodges1 2023-12-30 18:52:44 +11:00
parent 0a1e01e8e7
commit 47adcab90f
6 changed files with 117 additions and 132 deletions

View file

@ -8,19 +8,17 @@ open source anti cheat (lol) which I made for fun.
- Process module .text section integrity checks
- NMI stackwalking via isr iretq
- APC stackwalking via RtlCaptureStackBackTrace
- DPC stackwalking via RtlCaptureStackBackTrace (harder to disable)
- DPC stackwalking via RtlCaptureStackBackTrace
- Handle stripping via obj callbacks
- Process handle table enumeration
- System module verification
- System module .text integrity checks (see known issues)
- Unlinked process detection
- Hidden thread detection via KPRCB
- Hidden thread detection via PspCid table
- Removed thread PspCidTable entry detection
- Dispatch routine validation
- Extraction of hardware identifiers
- EPT hook detection (currently detects hyperdbg and DdiMon)
- Driver integrity checks both locally and over server
- Test signing detection
- Hypervisor detection
# planned features

View file

@ -1424,4 +1424,12 @@ _IRQL_requires_same_
VOID
KeSignalCallDpcDone(_In_ PVOID SystemArgument1);
PEPROCESS
NTAPI
PsGetNextProcess(IN PEPROCESS OldProcess OPTIONAL);
PETHREAD
NTAPI
PsGetNextProcessThread(IN PEPROCESS Process, IN PETHREAD Thread OPTIONAL);
#endif

View file

@ -358,7 +358,7 @@ DeviceControl(_In_ PDRIVER_OBJECT DriverObject, _Inout_ PIRP Irp)
DEBUG_INFO("IOCTL_VALIDATE_KPRCB_CURRENT_THREAD Received");
ValidateKPCRBThreads();
//ValidateKPCRBThreads();
break;
@ -518,7 +518,8 @@ _Dispatch_type_(IRP_MJ_CREATE) NTSTATUS
PAGED_CODE();
DEBUG_INFO("Handle to driver opened.");
DEBUG_VERBOSE("HELOO??");
HandleNmiIOCTL(Irp);
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return Irp->IoStatus.Status;
}

View file

@ -5,6 +5,8 @@
#include "ioctl.h"
#include "ia32.h"
#include "thread.h"
#define WHITELISTED_MODULE_TAG 'whte'
#define NMI_DELAY 200 * 10000
@ -801,7 +803,8 @@ IsInstructionPointerInInvalidRegion(_In_ UINT64 RIP,
}
/*
* todo: rename this to analyse stackwalk or something
* todo: i think we should split this function up into each analysis i.e one for the interrupted
* rip, one for the cid etc.
*/
STATIC
NTSTATUS
@ -848,6 +851,52 @@ AnalyseNmiData(_In_ PNMI_CONTEXT NmiContext, _In_ PSYSTEM_MODULES SystemModules,
core,
NmiContext[core].callback_count);
/*
* Our NMI callback allows us to interrupt every running thread on each core. Now it
* is common practice for malicious programs to either unlink their thread from the
* KTHREAD linked list or remove their threads entry from the PspCidTable or both.
* Now the reason an unlinked thread can still be scheduled is because the scheduler
* keeps a seperate list that it uses to schedule threads. It then places these
* threads in the KPRCB in either the CurrentThread, IdleThread or NextThread.
*
* Since you can't just set a threads affinity to enumerate over all cores and read
* the KPCRB->CurrentThread (since it will just show your thread) we have to
* interrupt the thread. So below we are validating that the thread is indeed in our
* own threads list using our callback routine and then using PsGetThreadId
*
* I also want to integrate a way to SAFELY determine whether a thread has been
* removed from the KTHREADs linked list, maybe PsGetNextProcess ?
*/
if (!ValidateThreadsPspCidTableEntry(NmiContext[core].kthread))
{
DEBUG_WARNING("Thread: %llx was not found in the pspcid table.",
NmiContext[core].kthread);
PHIDDEN_SYSTEM_THREAD_REPORT report =
ExAllocatePool2(POOL_FLAG_NON_PAGED,
sizeof(HIDDEN_SYSTEM_THREAD_REPORT),
REPORT_POOL_TAG);
if (!report)
continue;
report->report_code = REPORT_HIDDEN_SYSTEM_THREAD;
report->found_in_kthreadlist = FALSE; // wip
report->found_in_pspcidtable = FALSE;
report->thread_id = PsGetThreadId(NmiContext[core].kthread);
report->thread_address = NmiContext[core].kthread;
RtlCopyMemory(
report->thread, NmiContext[core].kthread, sizeof(report->thread));
InsertReportToQueue(report);
}
else
{
DEBUG_VERBOSE("Thread: %llx was found in PspCidTable", NmiContext[core].kthread);
}
if (NmiContext[core].user_thread)
continue;
@ -992,16 +1041,13 @@ HandleNmiIOCTL(_Inout_ PIRP Irp)
SYSTEM_MODULES system_modules = {0};
PNMI_CONTEXT nmi_context = NULL;
nmi_context = ExAllocatePool2(POOL_FLAG_NON_PAGED,
KeQueryActiveProcessorCount(0) * sizeof(NMI_CONTEXT),
NMI_CONTEXT_POOL);
if (!nmi_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
@ -1373,9 +1419,9 @@ _IRQL_requires_(DISPATCH_LEVEL)
_IRQL_requires_same_
VOID
DpcStackwalkCallbackRoutine(_In_ PKDPC Dpc,
_In_opt_ PVOID DeferredContext,
_In_opt_ PVOID SystemArgument1,
_In_opt_ PVOID SystemArgument2)
_In_opt_ PVOID DeferredContext,
_In_opt_ PVOID SystemArgument1,
_In_opt_ PVOID SystemArgument2)
{
PDPC_CONTEXT context = &((PDPC_CONTEXT)DeferredContext)[KeGetCurrentProcessorNumber()];

View file

@ -8,131 +8,53 @@
#include "queue.h"
#ifdef ALLOC_PRAGMA
# pragma alloc_text(PAGE, ValidateKPCRBThreads)
# pragma alloc_text(PAGE, DetectThreadsAttachedToProtectedProcess)
# pragma alloc_text(PAGE, ValidateThreadsPspCidTableEntry)
#endif
typedef struct _KPRCB_THREAD_VALIDATION_CTX
{
UINT64 current_kpcrb_thread;
UINT8 thread_found_in_pspcidtable;
UINT8 thread_found_in_kthreadlist;
BOOLEAN finished;
} KPRCB_THREAD_VALIDATION_CTX, *PKPRCB_THREAD_VALIDATION_CTX;
_IRQL_always_function_min_(DISPATCH_LEVEL) STATIC VOID
KPRCBThreadValidationProcessCallback(_In_ PTHREAD_LIST_ENTRY ThreadListEntry,
_Inout_opt_ PVOID Context)
{
UINT32 thread_id = 0;
PKPRCB_THREAD_VALIDATION_CTX context = (PKPRCB_THREAD_VALIDATION_CTX)Context;
if (!Context || context->finished == TRUE)
return;
if (ThreadListEntry->thread == context->current_kpcrb_thread)
{
context->thread_found_in_kthreadlist = TRUE;
thread_id = PsGetThreadId(ThreadListEntry->thread);
if (thread_id != NULL)
{
context->thread_found_in_pspcidtable = TRUE;
context->finished = TRUE;
}
}
}
/*
* How this will work:
*
* 1. The KPCRB (processor control block) contains 3 pointers to 3 threads:
*
* +0x008 CurrentThread : Ptr64 _KTHREAD
* +0x010 NextThread : Ptr64 _KTHREAD
* +0x018 IdleThread : Ptr64 _KTHREAD
*
* 2. These threads are stored in a list that is seperate to the KTHREADs linked list.
* We know this because if you unlink a process, the threads are still scheduled by
* the OS, meaning the OS has a seperate list that it uses to schedule these threads.
*
* 3. From here we can firstly check if the KTHREAD is within the KTHREAD linked list,
* if it is we can then use this to check if its in the PspCidTable by passing it
* to PsGetThreadId which returns the thread id by enumerating the PspCidTable and
* finding the corresponding object pointer. If the thread id is not found, we know
* that it's been removed from the PspCidTable, and if the thread is not in any
* process' thread list , we know it's been removed from the KTHREAD linked list.
*
*/
VOID
ValidateKPCRBThreads()
BOOLEAN
ValidateThreadsPspCidTableEntry(_In_ PETHREAD Thread)
{
PAGED_CODE();
UINT64 kpcr = 0;
UINT64 kprcb = 0;
KAFFINITY old_affinity = {0};
KPRCB_THREAD_VALIDATION_CTX context = {0};
NTSTATUS status = STATUS_UNSUCCESSFUL;
HANDLE thread_id = NULL;
PETHREAD thread = NULL;
for (LONG processor_index = 0; processor_index < KeQueryActiveProcessorCount(0);
processor_index++)
/*
* PsGetThreadId simply returns ETHREAD->Cid.UniqueThread
*/
thread_id = PsGetThreadId(Thread);
/*
* For each core on the processor, the first x threads equal to x cores will be assigned a
* cid equal to its equivalent core. These threads are generally executing the HLT
* instruction or some other boring stuff while the processor is not busy. The reason this
* is important is because passing in a handle value of 0 which, even though is a valid cid,
* returns a non success status meaning we mark it an invalid cid entry even though it is.
* To combat this we simply add a little check here. The problem is this can be easily
* bypassed by simply modifying the ETHREAD->Cid.UniqueThread identifier.. So while it isnt a
* perfect detection method for now it's good enough.
*/
if ((UINT64)thread_id < (UINT64)KeQueryActiveProcessorCount(NULL))
return TRUE;
/*
* PsLookupThreadByThreadId will use a threads id to find its cid entry, and return
* the pointer contained in the HANDLE_TABLE entry pointing to the thread object.
* Meaning if we pass a valid thread id which we retrieved above and dont receive a
* STATUS_SUCCESS the cid entry could potentially be removed or disrupted..
*/
status = PsLookupThreadByThreadId(thread_id, &thread);
if (!NT_SUCCESS(status))
{
old_affinity = KeSetSystemAffinityThreadEx((KAFFINITY)(1ull << processor_index));
while (KeGetCurrentProcessorNumber() != processor_index)
YieldProcessor();
kpcr = __readmsr(IA32_GS_BASE);
kprcb = kpcr + KPRCB_OFFSET_FROM_GS_BASE;
/* sanity check */
if (!MmIsAddressValid(kprcb + KPCRB_CURRENT_THREAD))
continue;
context.current_kpcrb_thread = *(UINT64*)(kprcb + KPCRB_CURRENT_THREAD);
DEBUG_VERBOSE("Proc number: %lx, Current thread: %llx",
processor_index,
context.current_kpcrb_thread);
if (!context.current_kpcrb_thread)
continue;
EnumerateThreadListWithCallbackRoutine(KPRCBThreadValidationProcessCallback,
&context);
DEBUG_VERBOSE("Was thread found in kthread: %lx, Was thread found in cid table: %lx",
(UINT32)context.thread_found_in_kthreadlist,
(UINT32)context.thread_found_in_pspcidtable);
if (context.current_kpcrb_thread == FALSE ||
context.thread_found_in_pspcidtable == FALSE)
{
PHIDDEN_SYSTEM_THREAD_REPORT report =
ExAllocatePool2(POOL_FLAG_NON_PAGED,
sizeof(HIDDEN_SYSTEM_THREAD_REPORT),
REPORT_POOL_TAG);
if (!report)
goto increment;
report->report_code = REPORT_HIDDEN_SYSTEM_THREAD;
report->found_in_kthreadlist = context.thread_found_in_kthreadlist;
report->found_in_pspcidtable = context.thread_found_in_pspcidtable;
report->thread_id = PsGetThreadId(context.current_kpcrb_thread);
report->thread_address = context.current_kpcrb_thread;
RtlCopyMemory(
report->thread, context.current_kpcrb_thread, sizeof(report->thread));
InsertReportToQueue(report);
}
increment:
KeRevertToUserAffinityThreadEx(old_affinity);
DEBUG_WARNING(
"Failed to lookup thread by id. PspCidTable entry potentially removed.");
return FALSE;
}
return TRUE;
}
_IRQL_always_function_min_(DISPATCH_LEVEL) STATIC VOID

View file

@ -4,6 +4,7 @@
#include <ntifs.h>
#include "common.h"
#include "callbacks.h"
typedef struct _HIDDEN_SYSTEM_THREAD_REPORT
{
@ -24,8 +25,17 @@ typedef struct _ATTACH_PROCESS_REPORT
} ATTACH_PROCESS_REPORT, *PATTACH_PROCESS_REPORT;
VOID
ValidateKPCRBThreads();
typedef struct _KPRCB_THREAD_VALIDATION_CTX
{
UINT64 thread;
BOOLEAN thread_found_in_pspcidtable;
//BOOLEAN thread_found_in_kthreadlist;
BOOLEAN finished;
} KPRCB_THREAD_VALIDATION_CTX, *PKPRCB_THREAD_VALIDATION_CTX;
BOOLEAN
ValidateThreadsPspCidTableEntry(_In_ PETHREAD Thread);
VOID
DetectThreadsAttachedToProtectedProcess();