Anti-Cheat: Stopping Memory Acquisition via Crash Dumps


It’s no secret that cheaters and security researchers want to get their hands on anti-cheat software. Many anti-cheat developers virtualize and pack their binaries, making static analysis harder. This forces adversaries to analyze the anti-cheat software dynamically, which could entail stepping through the unpacking process manually then dumping the anti-cheat driver from memory. Attaching a kernel debugger can also be a problem due to anti-debugging checks, so how are people dumping anti-cheat drivers? And how could they prevent this in the future? That leads us to our topic today.

Memory Acquisition via Crash Dumps

During a quick chat with a seasoned game hacker, dumping anti-cheat drivers from crash dumps has been around for a while. If the adversary isn’t skilled enough to conduct a BYOVD attack and dump the driver, they can resort to this easier method. Here’s some external research to help you understand what this article will be about. On april 7th, 2023 a clever person on the guided hacking website named MrMoo showed us How To Dump EA’s Driver by intentionally crashing the system. Once the system rebooted he analyzed the kernel crash dump with volatility to dump the anti-cheat driver. June 7th, 2024 a user on unknowncheats LabGuy94 posted about the same technique. Two years later on march 5th, 2026 a researcher named s4dbrd published his analysis on BattlEye’s anti-cheat driver utilizing this technique and NotMyFault to dump their driver from memory. Finally on june 16th, 2026 I published a tutorial on the guided hacking showcasing how you can crash the system with and without NotMyFault. After the kernel memory dump was generated, I transfered it to my host machine and dumped the anti-cheat driver from the crash dump using windbg. One thing these all have in common is that they generate a bug check (BSOD). It may seem like this is a full proof method of dumping the anti-cheat because once the system is crashing the anti-cheat has no control. I like to think that this is far from the truth, the rest of this article aims to back that up and show you how this can be stopped.

Stopping Memory Acquisition via Crash Dumps

Our journey starts at the manually initiated crash screen, we know that users are crashing their systems on purpose and a common bug check code seen is MANUALLY_INITIATED_CRASH.

alt text

If we search for this bug check code and snoop around MSDN, we’ll that find Microsoft allows drivers to register callback functions that run when the system bug checks and starts to generate the kernel crash dump. A driver can register a bug check callback routine by calling KeRegisterBugCheckCallback or KeRegisterBugCheckReasonCallback. KeRegisterBugCheckReasonCallback is what we’ll be using, this function is useful because in the KBUGCHECK_CALLBACK_REASON Reason argument we can specify KbCallbackRemovePages. This enum value tells the windows kernel that our callback will be removing pages from the crash dump. I’m sure you can do something with the KbCallbackDumpIo type as well but I feel that my method is more straight forward. When you’re writing bug check callbacks you’re pretty restricted from what you can do so it’s best to keep it simple. A bug check callback with type KbCallbackDumpIo can be called multiple times during the crash, so I just played it safe.

About Bug Check Callback Routines

To understand more about what we’re doing let’s look at the implementation of KeRegisterBugCheckReasonCallback, and other related data structures.

// https://doxygen.reactos.org/d3/d13/bug_8c.html#a73fca11ec74d2871e8981bbcd00f0f24
BOOLEAN KeRegisterBugCheckReasonCallback(PKBUGCHECK_REASON_CALLBACK_RECORD CallbackRecord, PKBUGCHECK_REASON_CALLBACK_ROUTINE CallbackRoutine, KBUGCHECK_CALLBACK_REASON Reason, PUCHAR Component) {
    
    KIRQL OldIrql;
    BOOLEAN Status = FALSE;
    KeRaiseIrql(HIGH_LEVEL, &OldIrql);

    if ( CallbackRecord->State == BufferEmpty ) {
        CallbackRecord->Component = Component;
        CallbackRecord->CallbackRoutine = CallbackRoutine;
        CallbackRecord->State = BufferInserted;
        CallbackRecord->Reason = Reason;
        InsertTailList(&KeBugcheckReasonCallbackListHead,&CallbackRecord->Entry);
        Status = TRUE;
    }

    KeLowerIrql(OldIrql);
    return Status;
}

First KeRegisterBugCheckReasonCallback raises the IRQL to HIGH_LEVEL, then it checks if the callback record was initialized by KeInitializeCallbackRecord (This is just a macro for PKBUGCHECK_REASON_CALLBACK_RECORD->State = BufferEmpty). If it was then the callback record is initialized, and it gets inserted into the KeBugcheckReasonCallbackList. In our case it should be in the KeBugCheckAddRemovePagesCallbackList.

But wait there’s two more bug check lists:

  • KeBugcheckCallbackListHead: Used by callbacks registered by KeRegisterBugCheckCallback
  • KeBugCheckTriageDumpDataArrayListHead: Used by callbacks of type KbCallbackTriageDumpData

Maybe you can remove a callback from these lists? I’m not sure if these are protected by patch guard (KPP) or not.

Hypervisor Detection Using Bug Check Callbacks

To give you more insight on how other people have used bug check callbacks in the anti-cheat space, let’s go back six years. On april 13, 2020 members from secret.club (daax, iPower, ajkhoury and drew) wrote about how anti-cheats detect system emulation. In the section named XSETBV, they talk about using the XSETBV instruction to intentionally generate a fault that will cause a VM-exit. Majority of public hypervisors used to analyze anti-cheat software will handle this in the host’s XSETBV handler. Because of this and the driver not being able to handle SEH (it’s manually mapped), the host will bug check (BSOD). This is why they registered a bug check callback routine using KbCallbackSecondaryDumpData, to add data to the crash dump and uniquely identify their crash data. Today we’ll be removing data from the crash dump.

Removing The Anti-Cheat Driver From The Crash Dump

Before we get started here’s a little recap/road map, we’ll call KeRegisterBugCheckReasonCallback to register our bug check reason callback so we can run code when the system bug checks. When calling this function we will supply KbCallbackRemovePages in the Reason argument, so the windows kernel knows that our callback will remove pages from the dump. Here’s how I registered my bug check callback.

callback_record = ExAllocatePoolWithTag(NonPagedPool, sizeof(KBUGCHECK_REASON_CALLBACK_RECORD), CARDINAL_MEM_TAG);
if (!callback_record)
    return;

cardinal_get_info();
UCHAR component[] = "Bugcheckin\n";
KeInitializeCallbackRecord(callback_record);
KeRegisterBugCheckReasonCallback(callback_record, cardinal_bugcheck_cb, KbCallbackRemovePages, component);

Each bug check reason callback routine has the following prototype.

typedef VOID (*bugcheck_cb_t)(KBUGCHECK_CALLBACK_REASON Reason, PKBUGCHECK_REASON_CALLBACK_RECORD Record, PVOID ReasonSpecificData, ULONG ReasonSpecificDataLength)

Reason will be one of the enum values for example.. KbCallbackDumpIo, KbCallbackRemovePages, KbCallbackAddPages, etc. Depending on the Reason, ReasonSpecificData can be cast to many other data structures. In our case we’ll be casting it to KBUGCHECK_REMOVE_PAGES. When this callback routine is called, the kernel will have already populated the BugCheckCode member of this struct. We’ll use that to determine what kind of crash it was.

VOID cardinal_bugcheck_cb(KBUGCHECK_CALLBACK_REASON Reason, PKBUGCHECK_REASON_CALLBACK_RECORD Record, PVOID ReasonSpecificData, ULONG ReasonSpecificDataLength) {
    PKBUGCHECK_REMOVE_PAGES bugcheck = (PKBUGCHECK_REMOVE_PAGES)ReasonSpecificData;
    if ( // Bug checks intentionally generated by the keyboard and NotMyFault
        bugcheck->BugCheckCode == MANUALLY_INITIATED_CRASH ||
        bugcheck->BugCheckCode == KERNEL_MODE_HEAP_CORRUPTION ||
        bugcheck->BugCheckCode == PAGE_FAULT_IN_NONPAGED_AREA ||
        bugcheck->BugCheckCode == DRIVER_OVERRAN_STACK_BUFFER ||
        bugcheck->BugCheckCode == DRIVER_IRQL_NOT_LESS_OR_EQUAL
    [..snip..]

If the bug check code was one of the following, then we can start to remove the anti-cheat driver from the crash dump. To do this we need to populate three more members of the KBUGCHECK_REMOVE_PAGES struct. First, KBUGCHECK_REMOVE_PAGES.Flags this member determines if the address that we supply next is going to be a virtual or physical address. I’ll be setting the KB_REMOVE_PAGES_FLAG_VIRTUAL_ADDRESS flag.

bugcheck->Flags |= KB_REMOVE_PAGES_FLAG_VIRTUAL_ADDRESS;

To finalize removing the anti-cheat from the crash dump, we need to populate two more members. First KBUGCHECK_REMOVE_PAGES.Address, this tells the windows kernel where in the crash dump to start removing pages. This will be set to the base address of our driver. Second KBUGCHECK_REMOVE_PAGES.Count, this determines how many pages need to be removed from the start address. To populate both of these members I wrote a function that locates the base address and size of the anti-cheat driver, then calculates how many pages need to be removed from the crash dump.

VOID cardinal_get_info() {

    [..snip..]

    PKLDR_DATA_TABLE_ENTRY entry = (PKLDR_DATA_TABLE_ENTRY)PsLoadedModuleList->Flink;
    while ( (PLIST_ENTRY)entry != PsLoadedModuleList ) {

        if ( !RtlCompareUnicodeString(&entry->BaseDllName, &drv_name, TRUE) ) {
            base = (ULONG_PTR)entry->DllBase;
            size = (ULONG_PTR)entry->SizeOfImage;
            break;
        }
        
        entry = (PKLDR_DATA_TABLE_ENTRY)entry->InLoadOrderLinks.Flink;
    }

    cardinal_pg_sz = (size + PAGE_SIZE - 1) / PAGE_SIZE;
    cardinal_base = base;

}

Once these global variables are populated we can populate the members inside of our bug check callback.

bugcheck->Address = cardinal_base;
bugcheck->Count = cardinal_pg_sz;

Now if we intentionally crash the system while the anti-cheat driver is loaded, we won’t be able to dump it to disk because the driver’s pages we’re removed from the crash dump! Rendering this method useless :). Let’s see this in action, I won’t be showing you how to crash the system you can find that information else where! What I did to test this was crash the system with the bug check callback registered. At first glance I didn’t think this worked because I was able to see it in the list of loaded modules (stupid of me), then I tried to view the base address of the driver in windbg’s Dissasembly tab and I was met with something interesting.

alt text

Trying to view the module in windbg also tells us that we can’t view the module’s info, because the pages aren’t present in the crash dump and that’s exactly what we want to hear :).

alt text

Bonus: Detecting Crash Dump Registry Keys

Instead of fighting during the crash, why not just check to see if the registy keys that enable these types of crashes are enabled? This way we don’t have to register a bug check callback routine if we don’t have to because we’ll know what to prepare for. The table below is a list of registry keys that can be used to bug check intentionally.

Path Key Value
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\i8042prt\crashdump CrashOnCtrlScroll 0x01
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\kbdhid\crashdump CrashOnCtrlScroll 0x01

Remarks

I left out two methods of intentionally crashing the system from the bug check routine. I think it would be good for both sides to do the research themselves and find out what can be done next. I really enjoyed learning about this and I think that it’s a very niche technique. I can’t really see this being used anywhere else other than in rootkits and anti-cheats that are packed, and want to stunt analysis when they’re unpacked. You can view the full code for this article here.

Bug Check References