← PC Reverse Engineering

Reverse Engineering Windows 10's ObRegisterCallbacks and ObUnRegisterCallbacks

About the author. I’m Colin Xiao — an independent reverse engineer, mostly working solo on contract jobs. I dig into compiled code of every kind, including but not limited to: Android and iOS apps, x86/x64/ARM native binaries, packers and obfuscators, DRM and license checks, network protocols and APIs, crypto and signature algorithms, malware, game binaries, and embedded/IoT firmware. I also handle large-scale data collection and vulnerability research. If you have any project in mind, I’d be glad to take it on — whatever it is, as long as it’s within my wheelhouse. Just reach out. You can reach me on X at @colinxiao_lab or on Telegram at https://t.me/ColinXiao666.

This research is published for educational and defensive purposes. The target sample is anonymized throughout.

Introduction

I wrote this post mainly to record my analysis of the ObRegisterCallbacks function. Corrections are welcome. What’s shown is C pseudocode — it only illustrates the logic and makes no claim of safety.

Analysis

The ObRegisterCallbacks function

The C pseudocode:

NTSTATUS ObRegisterCallbacks(POB_CALLBACK_REGISTRATION CallbackRegistration, PVOID *RegistrationHandle) {
    POB_CALLBACK_ENTRY CallbackEntry;
    POB_CALLBACK_HEADER Header;
    ULONG TotalSize;
    NTSTATUS Status = STATUS_SUCCESS;
    ULONG i;

    // validate the basic structure version and flags
    if ((CallbackRegistration->Version & 0xFF00) != OB_FLT_REGISTRATION_VERSION ||
        CallbackRegistration->OperationCount == 0)
    {
        return STATUS_INVALID_PARAMETER;
    }

    // compute the memory size to allocate (Header + callback entry array)
    TotalSize = sizeof(OB_CALLBACK_HEADER) +
                CallbackRegistration->OperationCount * sizeof(OB_CALLBACK_ENTRY);

    // allocate a tagged pool
    Header = (POB_CALLBACK_HEADER)ExAllocatePoolWithTag(NonPagedPool, TotalSize, 'ObCf');
    if (!Header) {
        return STATUS_INSUFFICIENT_RESOURCES;
    }
    RtlZeroMemory(Header, TotalSize);

    // initialize the Header structure
    Header->Version = CallbackRegistration->Version;
    Header->OperationCount = CallbackRegistration->OperationCount;
    Header->RegistrationContext = CallbackRegistration->RegistrationContext;

    // process each callback entry
    for (i = 0; i < CallbackRegistration->OperationCount; i++) {
        POB_OPERATION_REGISTRATION Operation = &CallbackRegistration->OperationRegistration[i];
        CallbackEntry = &Header->Entries[i];

        // validate Altitude and the callback functions
        if (Operation->Altitude == 0 ||
            !MmVerifyCallbackFunctionCheckFlags(Operation->PreOperation, 0x20) ||
            (Operation->PostOperation &&
             !MmVerifyCallbackFunctionCheckFlags(Operation->PostOperation, 0x20)))
        {
            Status = STATUS_INVALID_PARAMETER;
            break;
        }

        // initialize the callback entry
        CallbackEntry->Altitude = Operation->Altitude;
        CallbackEntry->PreOperation = Operation->PreOperation;
        CallbackEntry->PostOperation = Operation->PostOperation;
        InitializeListHead(&CallbackEntry->Link);

        // insert into the global list sorted by Altitude
        Status = ObpInsertCallbackByAltitude(CallbackEntry);
        if (!NT_SUCCESS(Status)) {
            break;
        }
    }

    // error handling: roll back the registered entries
    if (!NT_SUCCESS(Status)) {
        for (ULONG j = 0; j < i; j++) {
            ObpRemoveCallbackEntry(&Header->Entries[j]);
        }
        ExFreePoolWithTag(Header, 'ObCf');
        return Status;
    }

    // set the output handle
    *RegistrationHandle = Header;
    return STATUS_SUCCESS;
}

Here is the original assembly:

nt!ObRegisterCallbacks:
fffff800`243ca320 48895c2408      mov     qword ptr [rsp+8],rbx
fffff800`243ca325 48896c2410      mov     qword ptr [rsp+10h],rbp
fffff800`243ca32a 4889742418      mov     qword ptr [rsp+18h],rsi
fffff800`243ca32f 57              push    rdi
fffff800`243ca330 4154            push    r12
fffff800`243ca332 4155            push    r13
fffff800`243ca334 4156            push    r14
fffff800`243ca336 4157            push    r15
fffff800`243ca338 4883ec20        sub     rsp,20h
fffff800`243ca33c 0fb701          movzx   eax,word ptr [rcx]
fffff800`243ca33f 4c8bf9          mov     r15,rcx
fffff800`243ca342 b900ff0000      mov     ecx,0FF00h
fffff800`243ca347 33ff            xor     edi,edi
fffff800`243ca349 6623c1          and     ax,cx
fffff800`243ca34c 4c8be2          mov     r12,rdx
fffff800`243ca34f b900010000      mov     ecx,100h
fffff800`243ca354 8bdf            mov     ebx,edi
fffff800`243ca356 663bc1          cmp     ax,cx
fffff800`243ca359 0f8532740900    jne     nt!ObRegisterCallbacks+0x97471 (fffff800`24461791)  Branch

nt!ObRegisterCallbacks+0x3f:
fffff800`243ca35f 410fb74702      movzx   eax,word ptr [r15+2]
fffff800`243ca364 6685c0          test    ax,ax
fffff800`243ca367 0f8424740900    je      nt!ObRegisterCallbacks+0x97471 (fffff800`24461791)  Branch

nt!ObRegisterCallbacks+0x4d:
fffff800`243ca36d 410fb76f08      movzx   ebp,word ptr [r15+8]
fffff800`243ca372 448d6f01        lea     r13d,[rdi+1]
fffff800`243ca376 8bc8            mov     ecx,eax
fffff800`243ca378 83c520          add     ebp,20h
fffff800`243ca37b c1e106          shl     ecx,6
fffff800`243ca37e 41b84f62466c    mov     r8d,6C46624Fh
fffff800`243ca384 03e9            add     ebp,ecx
fffff800`243ca386 418bcd          mov     ecx,r13d
fffff800`243ca389 8bd5            mov     edx,ebp
fffff800`243ca38b 448bf5          mov     r14d,ebp
fffff800`243ca38e e87dcc1e00      call    nt!ExAllocatePoolWithTag (fffff800`245b7010)
fffff800`243ca393 488bf0          mov     rsi,rax
fffff800`243ca396 4885c0          test    rax,rax
fffff800`243ca399 0f841f730900    je      nt!ObRegisterCallbacks+0x9739e (fffff800`244616be)  Branch

nt!ObRegisterCallbacks+0x7f:
fffff800`243ca39f 458bc6          mov     r8d,r14d
fffff800`243ca3a2 33d2            xor     edx,edx
fffff800`243ca3a4 488bc8          mov     rcx,rax
fffff800`243ca3a7 e8d48cc4ff      call    nt!memset (fffff800`24013080)
fffff800`243ca3ac b800010000      mov     eax,100h
fffff800`243ca3b1 668906          mov     word ptr [rsi],ax
fffff800`243ca3b4 498b4718        mov     rax,qword ptr [r15+18h]
fffff800`243ca3b8 48894608        mov     qword ptr [rsi+8],rax
fffff800`243ca3bc 450fb74708      movzx   r8d,word ptr [r15+8]
fffff800`243ca3c1 412be8          sub     ebp,r8d
fffff800`243ca3c4 6644894612      mov     word ptr [rsi+12h],r8w
fffff800`243ca3c9 6644894610      mov     word ptr [rsi+10h],r8w
fffff800`243ca3ce 8bcd            mov     ecx,ebp
fffff800`243ca3d0 4803ce          add     rcx,rsi
fffff800`243ca3d3 48894e18        mov     qword ptr [rsi+18h],rcx
fffff800`243ca3d7 498b5710        mov     rdx,qword ptr [r15+10h]
fffff800`243ca3db e8e089c4ff      call    nt!memcpy (fffff800`24012dc0)
fffff800`243ca3e0 8bef            mov     ebp,edi
fffff800`243ca3e2 66413b7f02      cmp     di,word ptr [r15+2]
fffff800`243ca3e7 0f83b6000000    jae     nt!ObRegisterCallbacks+0x183 (fffff800`243ca4a3)  Branch

nt!ObRegisterCallbacks+0xcd:
fffff800`243ca3ed 448bf5          mov     r14d,ebp
fffff800`243ca3f0 49c1e605        shl     r14,5
fffff800`243ca3f4 4d037720        add     r14,qword ptr [r15+20h]
fffff800`243ca3f8 8bdd            mov     ebx,ebp
fffff800`243ca3fa 41397e08        cmp     dword ptr [r14+8],edi
fffff800`243ca3fe 0f847c730900    je      nt!ObRegisterCallbacks+0x97460 (fffff800`24461780)  Branch

nt!ObRegisterCallbacks+0xe4:
fffff800`243ca404 498b06          mov     rax,qword ptr [r14]
fffff800`243ca407 488b08          mov     rcx,qword ptr [rax]
fffff800`243ca40a f6414240        test    byte ptr [rcx+42h],40h
fffff800`243ca40e 0f846c730900    je      nt!ObRegisterCallbacks+0x97460 (fffff800`24461780)  Branch

nt!ObRegisterCallbacks+0xf4:
fffff800`243ca414 498b4e10        mov     rcx,qword ptr [r14+10h]
fffff800`243ca418 4885c9          test    rcx,rcx
fffff800`243ca41b 0f84a7720900    je      nt!ObRegisterCallbacks+0x973a8 (fffff800`244616c8)  Branch

nt!ObRegisterCallbacks+0x101:
fffff800`243ca421 ba20000000      mov     edx,20h
fffff800`243ca426 e8a552beff      call    nt!MmVerifyCallbackFunctionCheckFlags (fffff800`23faf6d0)
fffff800`243ca42b 85c0            test    eax,eax
fffff800`243ca42d 0f84bf720900    je      nt!ObRegisterCallbacks+0x973d2 (fffff800`244616f2)  Branch

nt!ObRegisterCallbacks+0x113:
fffff800`243ca433 498b4e18        mov     rcx,qword ptr [r14+18h]
fffff800`243ca437 4885c9          test    rcx,rcx
fffff800`243ca43a 0f85a0720900    jne     nt!ObRegisterCallbacks+0x973c0 (fffff800`244616e0)  Branch

nt!ObRegisterCallbacks+0x120:
fffff800`243ca440 48c1e306        shl     rbx,6
fffff800`243ca444 4883c320        add     rbx,20h
fffff800`243ca448 4803de          add     rbx,rsi
fffff800`243ca44b 488d4b38        lea     rcx,[rbx+38h]
fffff800`243ca44f 48895b08        mov     qword ptr [rbx+8],rbx
fffff800`243ca453 48891b          mov     qword ptr [rbx],rbx
fffff800`243ca456 e8f5d4aeff      call    nt!ExInitializePushLock (fffff800`23eb7950)
fffff800`243ca45b 418b4608        mov     eax,dword ptr [r14+8]
fffff800`243ca45f 488bd3          mov     rdx,rbx
fffff800`243ca462 894310          mov     dword ptr [rbx+10h],eax
fffff800`243ca465 48897318        mov     qword ptr [rbx+18h],rsi
fffff800`243ca469 498b06          mov     rax,qword ptr [r14]
fffff800`243ca46c 488b08          mov     rcx,qword ptr [rax]
fffff800`243ca46f 48894b20        mov     qword ptr [rbx+20h],rcx
fffff800`243ca473 498b4610        mov     rax,qword ptr [r14+10h]
fffff800`243ca477 48894328        mov     qword ptr [rbx+28h],rax
fffff800`243ca47b 498b4618        mov     rax,qword ptr [r14+18h]
fffff800`243ca47f 48894330        mov     qword ptr [rbx+30h],rax
fffff800`243ca483 e86c000000      call    nt!ObpInsertCallbackByAltitude (fffff800`243ca4f4)
fffff800`243ca488 8bd8            mov     ebx,eax
fffff800`243ca48a 85c0            test    eax,eax
fffff800`243ca48c 7815            js      nt!ObRegisterCallbacks+0x183 (fffff800`243ca4a3)  Branch

nt!ObRegisterCallbacks+0x16e:
fffff800`243ca48e 6644016e02      add     word ptr [rsi+2],r13w
fffff800`243ca493 4103ed          add     ebp,r13d
fffff800`243ca496 410fb74f02      movzx   ecx,word ptr [r15+2]
fffff800`243ca49b 3be9            cmp     ebp,ecx
fffff800`243ca49d 0f824affffff    jb      nt!ObRegisterCallbacks+0xcd (fffff800`243ca3ed)  Branch

nt!ObRegisterCallbacks+0x183:
fffff800`243ca4a3 85db            test    ebx,ebx
fffff800`243ca4a5 0f884c720900    js      nt!ObRegisterCallbacks+0x973d7 (fffff800`244616f7)  Branch

nt!ObRegisterCallbacks+0x18b:
fffff800`243ca4ab 663b7e02        cmp     di,word ptr [rsi+2]
fffff800`243ca4af 7316            jae     nt!ObRegisterCallbacks+0x1a7 (fffff800`243ca4c7)  Branch

nt!ObRegisterCallbacks+0x191:
fffff800`243ca4b1 8bc7            mov     eax,edi
fffff800`243ca4b3 4103fd          add     edi,r13d
fffff800`243ca4b6 48c1e006        shl     rax,6
fffff800`243ca4ba 44096c3034      or      dword ptr [rax+rsi+34h],r13d
fffff800`243ca4bf 0fb74602        movzx   eax,word ptr [rsi+2]
fffff800`243ca4c3 3bf8            cmp     edi,eax
fffff800`243ca4c5 72ea            jb      nt!ObRegisterCallbacks+0x191 (fffff800`243ca4b1)  Branch

nt!ObRegisterCallbacks+0x1a7:
fffff800`243ca4c7 49893424        mov     qword ptr [r12],rsi

nt!ObRegisterCallbacks+0x1ab:
fffff800`243ca4cb 8bc3            mov     eax,ebx

nt!ObRegisterCallbacks+0x1ad:
fffff800`243ca4cd 488b5c2450      mov     rbx,qword ptr [rsp+50h]
fffff800`243ca4d2 488b6c2458      mov     rbp,qword ptr [rsp+58h]
fffff800`243ca4d7 488b742460      mov     rsi,qword ptr [rsp+60h]
fffff800`243ca4dc 4883c420        add     rsp,20h
fffff800`243ca4e0 415f            pop     r15
fffff800`243ca4e2 415e            pop     r14
fffff800`243ca4e4 415d            pop     r13
fffff800`243ca4e6 415c            pop     r12
fffff800`243ca4e8 5f              pop     rdi
fffff800`243ca4e9 c3              ret

nt!ObRegisterCallbacks+0x9739e:
fffff800`244616be b89a0000c0      mov     eax,0C000009Ah
fffff800`244616c3 e9058ef6ff      jmp     nt!ObRegisterCallbacks+0x1ad (fffff800`243ca4cd)  Branch

nt!ObRegisterCallbacks+0x973a8:
fffff800`244616c8 49397e18        cmp     qword ptr [r14+18h],rdi
fffff800`244616cc 0f84ae000000    je      nt!ObRegisterCallbacks+0x97460 (fffff800`24461780)  Branch

nt!ObRegisterCallbacks+0x973b2:
fffff800`244616d2 4885c9          test    rcx,rcx
fffff800`244616d5 0f84588df6ff    je      nt!ObRegisterCallbacks+0x113 (fffff800`243ca433)  Branch

nt!ObRegisterCallbacks+0x973bb:
fffff800`244616db e9418df6ff      jmp     nt!ObRegisterCallbacks+0x101 (fffff800`243ca421)  Branch

nt!ObRegisterCallbacks+0x973c0:
fffff800`244616e0 ba20000000      mov     edx,20h
fffff800`244616e5 e8e6dfb4ff      call    nt!MmVerifyCallbackFunctionCheckFlags (fffff800`23faf6d0)
fffff800`244616ea 85c0            test    eax,eax
fffff800`244616ec 0f854e8df6ff    jne     nt!ObRegisterCallbacks+0x120 (fffff800`243ca440)  Branch

nt!ObRegisterCallbacks+0x973d2:
fffff800`244616f2 bb220000c0      mov     ebx,0C0000022h

nt!ObRegisterCallbacks+0x973d7:
fffff800`244616f7 663b7e02        cmp     di,word ptr [rsi+2]
fffff800`244616fb 7370            jae     nt!ObRegisterCallbacks+0x9744d (fffff800`2446176d)  Branch

nt!ObRegisterCallbacks+0x973dd:
fffff800`244616fd bdb8000000      mov     ebp,0B8h

nt!ObRegisterCallbacks+0x973e2:
fffff800`24461702 8bc7            mov     eax,edi
fffff800`24461704 4c8d7620        lea     r14,[rsi+20h]
fffff800`24461708 48c1e006        shl     rax,6
fffff800`2446170c 4c03f0          add     r14,rax
fffff800`2446170f 65488b042588010000 mov   rax,qword ptr gs:[188h]
fffff800`24461718 66ff88e6010000  dec     word ptr [rax+1E6h]
fffff800`2446171f 498b4e20        mov     rcx,qword ptr [r14+20h]
fffff800`24461723 33d2            xor     edx,edx
fffff800`24461725 4803cd          add     rcx,rbp
fffff800`24461728 e8433c9eff      call    nt!ExAcquirePushLockExclusiveEx (fffff800`23e45370)
fffff800`2446172d 498b0e          mov     rcx,qword ptr [r14]
fffff800`24461730 4c397108        cmp     qword ptr [rcx+8],r14
fffff800`24461734 7554            jne     nt!ObRegisterCallbacks+0x9746a (fffff800`2446178a)  Branch

nt!ObRegisterCallbacks+0x97416:
fffff800`24461736 498b4608        mov     rax,qword ptr [r14+8]
fffff800`2446173a 4c3930          cmp     qword ptr [rax],r14
fffff800`2446173d 754b            jne     nt!ObRegisterCallbacks+0x9746a (fffff800`2446178a)  Branch

nt!ObRegisterCallbacks+0x9741f:
fffff800`2446173f 488908          mov     qword ptr [rax],rcx
fffff800`24461742 33d2            xor     edx,edx
fffff800`24461744 48894108        mov     qword ptr [rcx+8],rax
fffff800`24461748 498b4e20        mov     rcx,qword ptr [r14+20h]
fffff800`2446174c 4803cd          add     rcx,rbp
fffff800`2446174f e81c419eff      call    nt!ExReleasePushLockEx (fffff800`23e45870)
fffff800`24461754 65488b0c2588010000 mov   rcx,qword ptr gs:[188h]
fffff800`2446175d e80e409eff      call    nt!KiLeaveGuardedRegionUnsafe (fffff800`23e45770)
fffff800`24461762 0fb74602        movzx   eax,word ptr [rsi+2]
fffff800`24461766 4103fd          add     edi,r13d
fffff800`24461769 3bf8            cmp     edi,eax
fffff800`2446176b 7295            jb      nt!ObRegisterCallbacks+0x973e2 (fffff800`24461702)  Branch

nt!ObRegisterCallbacks+0x9744d:
fffff800`2446176d ba4f62466c      mov     edx,6C46624Fh
fffff800`24461772 488bce          mov     rcx,rsi
fffff800`24461775 e836591500      call    nt!ExFreePool (fffff800`245b70b0)
fffff800`2446177a 90              nop
fffff800`2446177b e94b8df6ff      jmp     nt!ObRegisterCallbacks+0x1ab (fffff800`243ca4cb)  Branch

nt!ObRegisterCallbacks+0x97460:
fffff800`24461780 bb0d0000c0      mov     ebx,0C000000Dh
fffff800`24461785 e9198df6ff      jmp     nt!ObRegisterCallbacks+0x183 (fffff800`243ca4a3)  Branch

nt!ObRegisterCallbacks+0x9746a:
fffff800`2446178a b903000000      mov     ecx,3
fffff800`2446178f cd29            int     29h

nt!ObRegisterCallbacks+0x97471:
fffff800`24461791 b80d0000c0      mov     eax,0C000000Dh
fffff800`24461796 e9328df6ff      jmp     nt!ObRegisterCallbacks+0x1ad (fffff800`243ca4cd)  Branch

Reasoning about ObRegisterCallbacks. The function prologue saves several registers — rbx, rbp, rsi, rdi, and r12–r15 — meaning it uses these registers and restores them at the end. Then it allocates 0x20 bytes of stack space, typically for locals.

Then it checks the input parameters — standard practice in kernel functions. movzx eax, word ptr [rcx]: here rcx is the first parameter, presumably a pointer to the OB_CALLBACK_REGISTRATION structure. It then ANDs with 0xFF00 to check whether a specific flag is set. If not, it jumps to error handling and returns 0xC000000D (STATUS_INVALID_PARAMETER).

Next it reads the word at r15+2 (likely OperationCount); if zero, it also jumps to the error path — checking that the operation count is valid.

It then computes the allocation size and calls ExAllocatePoolWithTag with tag 0x6C46624F — ASCII 'ObCf', presumably “Object Callback Function”. On failure it returns 0xC000009A (STATUS_INSUFFICIENT_RESOURCES).

After allocating, it zeroes the memory with memset (here RtlZeroMemory) and sets the structure’s fields. Then it loops over each callback operation, validating each callback’s Altitude and function pointers — specifically calling MmVerifyCallbackFunctionCheckFlags to validate the callback flags.

For each callback item it allocates a callback block and inserts it into the global callback list via ObpInsertCallbackByAltitude, which keeps the list sorted by Altitude. If insertion fails, the function rolls back allocated resources and returns the corresponding error. KeEnterCriticalRegion and friends are the APC-related functions.

The kernel uses ExAllocatePoolWithTag too — yet the WDK insists on ExAllocatePool2. :) The most interesting of these functions is probably MmVerifyCallbackFunctionCheckFlags, which validates callback legitimacy; I analyze it below.

The ObUnRegisterCallbacks function

NTSTATUS ObUnRegisterCallbacks(PVOID RegistrationHandle) {
    POB_CALLBACK_HEADER Header = (POB_CALLBACK_HEADER)RegistrationHandle;
    USHORT i = 0;

    // iterate over all callback entries
    while (i < Header->OperationCount) {
        POB_CALLBACK_ENTRY Entry = (POB_CALLBACK_ENTRY*)((PUCHAR)Header->Entries + i * 0x40);

        // wait for rundown protection to complete
        ExWaitForRundownProtectionRelease(&Entry->RundownProtect);

        // enter critical region and acquire the push lock
        KeEnterCriticalRegion();
        ExAcquirePushLockExclusiveEx(&Entry->Lock, 0);

        // safely remove from the global list
        if (ValidateLinkedList(Entry)) { // list-integrity check
            RemoveEntryList(&Entry->Link);
            InitializeListHead(&Entry->Link); // prevent a dangling pointer
        } else {
            KeBugCheckEx(LIST_ENTRY_CORRUPT, 3, 0, 0, 0);
        }

        // release the lock and leave the critical region
        ExReleasePushLockExclusiveEx(&Entry->Lock);
        KeLeaveCriticalRegion();

        i++;
    }

    // free the whole callback-structure memory
    ExFreePoolWithTag(Header, 'ObCf');
    return STATUS_SUCCESS;
}

The original assembly:

nt!ObUnRegisterCallbacks:
fffff800`244e0340 48895c2408      mov     qword ptr [rsp+8],rbx
fffff800`244e0345 4889742410      mov     qword ptr [rsp+10h],rsi
fffff800`244e034a 57              push    rdi
fffff800`244e034b 4883ec20        sub     rsp,20h
fffff800`244e034f 33f6            xor     esi,esi
fffff800`244e0351 488bd9          mov     rbx,rcx
fffff800`244e0354 663b7102        cmp     si,word ptr [rcx+2]
fffff800`244e0358 737b            jae     nt!ObUnRegisterCallbacks+0x95 (fffff800`244e03d5)  Branch

nt!ObUnRegisterCallbacks+0x1a:
fffff800`244e035a 8bc6            mov     eax,esi
fffff800`244e035c 488d7b20        lea     rdi,[rbx+20h]
fffff800`244e0360 48c1e006        shl     rax,6
fffff800`244e0364 4803f8          add     rdi,rax
fffff800`244e0367 488d4f38        lea     rcx,[rdi+38h]
fffff800`244e036b e8e01e9fff      call    nt!ExWaitForRundownProtectionRelease (fffff800`23ed2250)
fffff800`244e0370 65488b042588010000 mov   rax,qword ptr gs:[188h]
fffff800`244e0379 66ff88e6010000  dec     word ptr [rax+1E6h]
fffff800`244e0380 488b4f20        mov     rcx,qword ptr [rdi+20h]
fffff800`244e0384 33d2            xor     edx,edx
fffff800`244e0386 4881c1b8000000  add     rcx,0B8h
fffff800`244e038d e8de4f96ff      call    nt!ExAcquirePushLockExclusiveEx (fffff800`23e45370)
fffff800`244e0392 488b0f          mov     rcx,qword ptr [rdi]
fffff800`244e0395 48397908        cmp     qword ptr [rcx+8],rdi
fffff800`244e0399 7558            jne     nt!ObUnRegisterCallbacks+0xb3 (fffff800`244e03f3)  Branch

nt!ObUnRegisterCallbacks+0x5b:
fffff800`244e039b 488b4708        mov     rax,qword ptr [rdi+8]
fffff800`244e039f 483938          cmp     qword ptr [rax],rdi
fffff800`244e03a2 754f            jne     nt!ObUnRegisterCallbacks+0xb3 (fffff800`244e03f3)  Branch

nt!ObUnRegisterCallbacks+0x64:
fffff800`244e03a4 488908          mov     qword ptr [rax],rcx
fffff800`244e03a7 33d2            xor     edx,edx
fffff800`244e03a9 48894108        mov     qword ptr [rcx+8],rax
fffff800`244e03ad 488b4f20        mov     rcx,qword ptr [rdi+20h]
fffff800`244e03b1 4881c1b8000000  add     rcx,0B8h
fffff800`244e03b8 e8b35496ff      call    nt!ExReleasePushLockEx (fffff800`23e45870)
fffff800`244e03bd 65488b0c2588010000 mov   rcx,qword ptr gs:[188h]
fffff800`244e03c6 e8a55396ff      call    nt!KiLeaveGuardedRegionUnsafe (fffff800`23e45770)
fffff800`244e03cb 0fb74302        movzx   eax,word ptr [rbx+2]
fffff800`244e03cf ffc6            inc     esi
fffff800`244e03d1 3bf0            cmp     esi,eax
fffff800`244e03d3 7285            jb      nt!ObUnRegisterCallbacks+0x1a (fffff800`244e035a)  Branch

nt!ObUnRegisterCallbacks+0x95:
fffff800`244e03d5 ba4f62466c      mov     edx,6C46624Fh
fffff800`244e03da 488bcb          mov     rcx,rbx
fffff800`244e03dd e8ce6c0d00      call    nt!ExFreePool (fffff800`245b70b0)
fffff800`244e03e2 488b5c2430      mov     rbx,qword ptr [rsp+30h]
fffff800`244e03e7 488b742438      mov     rsi,qword ptr [rsp+38h]
fffff800`244e03ec 4883c420        add     rsp,20h
fffff800`244e03f0 5f              pop     rdi
fffff800`244e03f1 c3              ret

nt!ObUnRegisterCallbacks+0xb3:
fffff800`244e03f3 b903000000      mov     ecx,3
fffff800`244e03f8 cd29            int     29h
fffff800`244e03fa cc              int     3
fffff800`244e03fb cc              int     3
fffff800`244e03fc cc              int     3
fffff800`244e03fd cc              int     3
fffff800`244e03fe cc              int     3
fffff800`244e03ff cc              int     3
fffff800`244e0400 48895c2408      mov     qword ptr [rsp+8],rbx
fffff800`244e0405 48896c2410      mov     qword ptr [rsp+10h],rbp
fffff800`244e040a 4889742418      mov     qword ptr [rsp+18h],rsi
fffff800`244e040f 57              push    rdi
fffff800`244e0410 4883ec20        sub     rsp,20h
fffff800`244e0414 488bfa          mov     rdi,rdx
fffff800`244e0417 488be9          mov     rbp,rcx

nt!ObpCallPostOperationCallbacks+0x1a:
fffff800`244e041a 48393f          cmp     qword ptr [rdi],rdi
fffff800`244e041d 7455            je      nt!ObpCallPostOperationCallbacks+0x74 (fffff800`244e0474)  Branch

nt!ObpCallPostOperationCallbacks+0x1f:
fffff800`244e041f 488b7708        mov     rsi,qword ptr [rdi+8]
fffff800`244e0423 48393e          cmp     qword ptr [rsi],rdi
fffff800`244e0426 7545            jne     nt!ObpCallPostOperationCallbacks+0x6d (fffff800`244e046d)  Branch

nt!ObpCallPostOperationCallbacks+0x28:
fffff800`244e0428 488b4608        mov     rax,qword ptr [rsi+8]
fffff800`244e042c 483930          cmp     qword ptr [rax],rsi
fffff800`244e042f 753c            jne     nt!ObpCallPostOperationCallbacks+0x6d (fffff800`244e046d)  Branch

nt!ObpCallPostOperationCallbacks+0x31:
fffff800`244e0431 48894708        mov     qword ptr [rdi+8],rax
fffff800`244e0435 488bd5          mov     rdx,rbp
fffff800`244e0438 488938          mov     qword ptr [rax],rdi
fffff800`244e043b 488b5e10        mov     rbx,qword ptr [rsi+10h]
fffff800`244e043f 488b4618        mov     rax,qword ptr [rsi+18h]
fffff800`244e0443 48894518        mov     qword ptr [rbp+18h],rax
fffff800`244e0447 488b4b18        mov     rcx,qword ptr [rbx+18h]
fffff800`244e044b 488b4330        mov     rax,qword ptr [rbx+30h]
fffff800`244e044f 488b4908        mov     rcx,qword ptr [rcx+8]
fffff800`244e0453 e8e876b2ff      call    nt!guard_dispatch_icall (fffff800`24007b40)
fffff800`244e0458 488d4b38        lea     rcx,[rbx+38h]
fffff800`244e045c e81fbf96ff      call    nt!ExReleaseRundownProtection (fffff800`23e4c380)
fffff800`244e0461 33d2            xor     edx,edx
fffff800`244e0463 488bce          mov     rcx,rsi
fffff800`244e0466 e8456c0d00      call    nt!ExFreePool (fffff800`245b70b0)
fffff800`244e046b ebad            jmp     nt!ObpCallPostOperationCallbacks+0x1a (fffff800`244e041a)  Branch

nt!ObpCallPostOperationCallbacks+0x6d:
fffff800`244e046d b903000000      mov     ecx,3
fffff800`244e0472 cd29            int     29h

nt!ObpCallPostOperationCallbacks+0x74:
fffff800`244e0474 488b4d08        mov     rcx,qword ptr [rbp+8]
fffff800`244e0478 ba4f624362      mov     edx,6243624Fh
fffff800`244e047d e8be5696ff      call    nt!ObfDereferenceObjectWithTag (fffff800`23e45b40)
fffff800`244e0482 e8295996ff      call    nt!KeLeaveCriticalRegion (fffff800`23e45db0)
fffff800`244e0487 488b5c2430      mov     rbx,qword ptr [rsp+30h]
fffff800`244e048c 33c0            xor     eax,eax
fffff800`244e048e 488b6c2438      mov     rbp,qword ptr [rsp+38h]
fffff800`244e0493 488b742440      mov     rsi,qword ptr [rsp+40h]
fffff800`244e0498 4883c420        add     rsp,20h
fffff800`244e049c 5f              pop     rdi
fffff800`244e049d c3              ret

Reasoning. Start with the prologue: it saves registers and allocates stack space, then zeroes esi and moves rcx (the first parameter) into rbx. It compares si with [rbx+2]; if si is greater than or equal, it jumps to the memory-freeing part — likely checking whether the callback count has been fully processed.

The loop then processes each callback entry. It uses shl rax,6 and add rdi,rax to compute each entry’s location — each entry is apparently 0x40 (64) bytes. It calls ExWaitForRundownProtectionRelease (waiting for the reference count to hit zero), then gets the current thread, decrements the resource count, enters the critical region, acquires the PushLock, manipulates the list to remove the entry, releases the lock, and leaves the critical region. It loops until all entries are processed, then frees the memory.

Then ObpCallPostOperationCallbacks walks the list and invokes each callback’s post-operation function. It checks whether the list is empty to decide whether to continue, pulls each node out, validates it, calls the callback via guard_dispatch_icall, releases resources, and frees memory. Finally it dereferences the object and leaves the critical region. This reconstruction may be less accurate than the previous one.

Helper functions of ObRegisterCallbacks / ObUnRegisterCallbacks

1. MmVerifyCallbackFunctionCheckFlags

BOOLEAN MmVerifyCallbackFunctionCheckFlags(PVOID Address, ULONG Flags)
{
    KTHREAD* CurrentThread;
    BOOLEAN Result = FALSE;
    PMMPTE DataTableEntry;

    // if it's 1, return FALSE immediately
    if (MiGetSystemRegionType(Address) == 1) {
        return FALSE;
    }

    // enter critical region
    CurrentThread = KeGetCurrentThread();
    InterlockedDecrement16((SHORT*)&CurrentThread->ResourceCount);

    // acquire the loaded-module resource lock in shared mode
    ExAcquireResourceSharedLite(&PsLoadedModuleResource, TRUE);

    // look up the data table entry
    DataTableEntry = MiLookupDataTableEntry(Address, 0);
    if (DataTableEntry != NULL) {
        // check the result based on the flags
        if (Flags == 0) {
            // when Flags is 0, the mere existence of an entry passes
            Result = TRUE;
        } else {
            // check whether the entry flags match the input Flags
            ULONG EntryFlags = DataTableEntry->Flags;
            if ((EntryFlags & Flags) != 0) {
                Result = TRUE;
            }
        }
    }

    // release the resource lock and restore thread state
    ExReleaseResourceLite(&PsLoadedModuleResource);
    KeLeaveCriticalRegionThread(CurrentThread);

    return Result;
}

Reasoning. The prologue pushes rbx, rbp, and rsi, then rdi, then adjusts rsp to allocate stack space — the usual function-entry sequence. Then mov edi, edx and mov rsi, rcx save the parameters into edi and rsi; then it calls MiGetSystemRegionType and stores the result in eax. It compares eax with 1; if equal, it jumps to fffff800'23faf75e, clears eax, and returns — meaning if MiGetSystemRegionType returns 1, the function returns 0.

If the comparison isn’t equal (i.e. eax != 1), it continues. From fffff800'23faf6f3, it reads gs:[188h] — the current thread’s KTHREAD — into rbp, then xor ebx, ebx to zero ebx (return value / intermediate). It decrements the word at [rbp+1E4h] — likely some thread counter, like a resource or recursion count. Then mov dl,1 prepares to call ExAcquireResourceSharedLite with rcx = PsLoadedModuleResource and dl = 1 (wait for the resource).

Next it calls MiLookupDataTableEntry with rsi (the original rcx, the first parameter) and edx (now 0). If the returned rax is NULL it jumps to the resource-release part. Otherwise it tests edi (the original edx, the second parameter); if zero it sets ebx to 1. Otherwise it checks whether the value at rax+68h intersects edi; if so ebx stays 0, else it’s set to 1. Then it releases the resource, restores thread state, and returns ebx.

MiLookupDataTableEntry. Its prototype returns a pointer to a data-table-entry structure that has a Flags field at offset 0x68. How do I know it’s Flags? Dump LDR_DATA_TABLE_ENTRY in WinDbg:

kd> dt _LDR_DATA_TABLE_ENTRY
   +0x068 Flags

As long as Flags is given a nonzero value, the check happens — and ObRegisterCallbacks passes a hardcoded 0x20. That’s why so many posts say you must set the DriverSection Flag to 0x20.

So the bypass is simply setting the Flags value to 0x20, which is done by modifying the LDR_DATA_TABLE_ENTRY structure. The analysis and example code follow below.

2. ObpInsertCallbackByAltitude — Altitude-sorted list insertion

NTSTATUS ObpInsertCallbackByAltitude(POB_CALLBACK_ENTRY Entry)
{
    PLIST_ENTRY PrevEntry;
    KeEnterCriticalRegion();
    ExAcquirePushLockExclusiveEx(&ObCallbackLock, 0);

    // walk the list to find the right position
    LIST_FOR_EACH(PrevEntry, &ObCallbackList) {
        POB_CALLBACK_ENTRY Current = CONTAINING_RECORD(PrevEntry, OB_CALLBACK_ENTRY, Link);
        if (Entry->Altitude < Current->Altitude) {
            break;
        }
    }

    // insert after PrevEntry
    InsertTailList(PrevEntry, &Entry->Link);

    ExReleasePushLockExclusiveEx(&ObCallbackLock);
    KeLeaveCriticalRegion();
    return STATUS_SUCCESS;
}

The Altitude value is also ambiguous in Microsoft’s documentation; for testing/development, 321000 should probably work (I suspect) — a production value has to be requested from Microsoft. I didn’t dig into the specifics of Altitude.

3. ObpCallPostOperationCallbacks — post-operation callback processing, and ValidateLinkedList

VOID ObpCallPostOperationCallbacks(PVOID OperationParams, PLIST_ENTRY CallbackList) {
    while (!IsListEmpty(CallbackList)) {
        PLIST_ENTRY EntryLink = RemoveHeadList(CallbackList);
        POB_CALLBACK_ENTRY Entry = CONTAINING_RECORD(EntryLink, OB_CALLBACK_ENTRY, Link);

        // run the post-operation callback
        if (Entry->PostOperation) {
            ((POB_POST_OPERATION_CALLBACK)Entry->PostOperation)(
                Entry->RegistrationContext,
                OperationParams
            );
        }

        // release rundown protection
        ExReleaseRundownProtection(&Entry->RundownProtect);

        // free the callback-entry memory
        ExFreePoolWithTag(Entry, 'CbOp');
    }

    // free the operation-parameters object
    ObfDereferenceObjectWithTag(OperationParams, 'TagO');
    KeLeaveCriticalRegion();
}


BOOLEAN ValidateLinkedList(POB_CALLBACK_ENTRY Entry) {
    PLIST_ENTRY Prev = Entry->Link.Blink;
    PLIST_ENTRY Next = Entry->Link.Flink;

    // check that the forward and back pointers are consistent
    return (Prev->Flink == &Entry->Link) &&
           (Next->Blink == &Entry->Link);
}

The reasoning for these is the same as for ObRegisterCallbacks. The structures below are based entirely on WinDbg symbol analysis.

4. The LDR_DATA_TABLE_ENTRY structure

typedef struct _LDR_DATA_TABLE_ENTRY {
    // some lists
    LIST_ENTRY  InLoadOrderLinks;           // +0x000
    LIST_ENTRY  InMemoryOrderLinks;         // +0x010
    LIST_ENTRY  InInitializationOrderLinks; // +0x020

    // EntryPoint and such
    PVOID       DllBase;                    // +0x030
    PVOID       EntryPoint;                 // +0x038
    ULONG       SizeOfImage;                // +0x040

    // related info
    UNICODE_STRING FullDllName;             // +0x048
    UNICODE_STRING BaseDllName;             // +0x058

    // name this unnamed union FlagBitfield
    // this is where Flags lives
    union _FlagBitfield {
        UCHAR    FlagGroup[4];              // +0x068
        ULONG    Flags;
        struct _FlagStruct{
            ULONG PackagedBinary : 1;   // Bit 0
            ULONG MarkedForRemoval : 1;   // Bit 1
            ULONG ImageDll : 1;   // Bit 2
            ULONG LoadNotificationsSent : 1;   // Bit 3
            ULONG TelemetryEntryProcessed : 1;   // Bit 4
            ULONG ProcessStaticImport : 1;   // Bit 5
            ULONG InLegacyLists : 1;   // Bit 6
            ULONG InIndexes : 1;   // Bit 7
            ULONG ShimDll : 1;   // Bit 8
            ULONG InExceptionTable : 1;   // Bit 9
            ULONG ReservedFlags1 : 2;   // Bits 10-11
            ULONG LoadInProgress : 1;   // Bit 12
            ULONG LoadConfigProcessed : 1;   // Bit 13
            ULONG EntryProcessed : 1;   // Bit 14
            ULONG ProtectDelayLoad : 1;   // Bit 15
            ULONG ReservedFlags3 : 2;   // Bits 16-17
            ULONG DontCallForThreads : 1;   // Bit 18
            ULONG ProcessAttachCalled : 1;   // Bit 19
            ULONG ProcessAttachFailed : 1;   // Bit 20
            ULONG CorDeferredValidate : 1;   // Bit 21
            ULONG CorImage : 1;   // Bit 22
            ULONG DontRelocate : 1;   // Bit 23
            ULONG CorILOnly : 1;   // Bit 24
            ULONG ChpeImage : 1;   // Bit 25
            ULONG ReservedFlags5 : 2;   // Bits 26-27
            ULONG Redirected : 1;   // Bit 28
            ULONG ReservedFlags6 : 2;   // Bits 29-30
            ULONG CompatDatabaseProcessed : 1;   // Bit 31
        }FlagStruct;
    }FlagBitfield;

    // unknown purpose
    USHORT      ObsoleteLoadCount;          // +0x06c
    USHORT      TlsIndex;                   // +0x06e

    // hash table
    LIST_ENTRY  HashLinks;                  // +0x070
    ULONG       TimeDateStamp;              // +0x080

    // context
    PVOID       EntryPointActivationContext; // +0x088
    PVOID       Lock;                       // +0x090
    PVOID       DdagNode;                   // +0x098 (LDR_DDAG_NODE)
    LIST_ENTRY  NodeModuleLink;             // +0x0a0

    // the parent's base address
    PVOID       LoadContext;                // +0x0b0 (LDRP_LOAD_CONTEXT)
    PVOID       ParentDllBase;              // +0x0b8

    // address mapping
    PVOID       SwitchBackContext;          // +0x0c0
    RTL_BALANCED_NODE BaseAddressIndexNode; // +0x0c8
    RTL_BALANCED_NODE MappingInfoIndexNode; // +0x0e0

    // load time
    ULONGLONG   OriginalBase;              // +0x0f8
    LARGE_INTEGER LoadTime;                // +0x100

    // other
    ULONG       BaseNameHashValue;         // +0x108
    ULONG       LoadReason;                // +0x10c (LDR_DLL_LOAD_REASON)
    ULONG       ImplicitPathOptions;       // +0x110
    ULONG       ReferenceCount;            // +0x114
    ULONG       DependentLoadFlags;        // +0x118
    UCHAR       SigningLevel;              // +0x11c
} LDR_DATA_TABLE_ENTRY, * PLDR_DATA_TABLE_ENTRY;

This is the breakthrough for bypassing MmVerifyCallbackFunctionCheckFlags: set the Flags value to 0x20. Example:

BOOLEAN PassMmVerifyCallbackFunction(PDRIVER_OBJECT DriverObject)
{
    BOOLEAN bStatus = FALSE;
    PLDR_DATA_TABLE_ENTRY pLdrData = NULL;

    pLdrData = (PLDR_DATA_TABLE_ENTRY)DriverObject->DriverSection;
    if (pLdrData == NULL) {
        DbgPrint("Error in convert PLDR_DATA_TABLE_ENTRY,DriverObject:%p\n", DriverObject);
        return bStatus;
    }
    pLdrData->FlagBitfield.Flags = pLdrData->FlagBitfield.Flags | 0x20;
    bStatus = TRUE;
    return bStatus;
}

Result:

Passing the callback-function check

5. KLDR_DATA_TABLE_ENTRY

typedef struct _KLDR_DATA_TABLE_ENTRY {
    LIST_ENTRY InLoadOrderLinks;      // linked list 0x000
    PVOID      ExceptionTable;        // SEH exception-table pointer 0x010
    ULONG      ExceptionTableSize;    // exception-table size 0x018
    PVOID      GpValue;               // global pointer value (GP value) 0x020
    PVOID      NonPagedDebugInfo;     // debug-info pointer 0x028
    PVOID      DllBase;               // loaded-module base address 0x030
    PVOID      EntryPoint;            // module entry-point address 0x038
    ULONG      SizeOfImage;           // image size 0x040
    UNICODE_STRING FullDllName;       // full DLL path name 0x048
    UNICODE_STRING BaseDllName;       // base DLL name 0x058
    ULONG      Flags;                 // the magic Flags 0x068
    USHORT     LoadCount;             // reference count 0x06c
    union {                           // union 0x06e
        USHORT  SignatureLevel : 4;   // code-signature level (4 bits)
        USHORT  SpareFlags : 12;      // reserved flag bits (12 bits)
    } Unknown1;
    PVOID      SectionPointer;        // section-object pointer 0x070
    ULONG      CheckSum;              // image checksum 0x078
    ULONG      CoverageSectionSize;   // coverage-data size 0x07c
    PVOID      CoverageSection;       // coverage-data pointer 0x080
    PVOID      LoadedImports;         // descriptor pointer 0x088
    PVOID      Spare;                 // reserved offset 0x090
    ULONG      SizeOfImageNotRounded; // original image size (unaligned) 0x098
    ULONG      TimeDateStamp;         // PE-header timestamp 0x09c
} KLDR_DATA_TABLE_ENTRY, * PKLDR_DATA_TABLE_ENTRY;

This is referenced in some other functions, so I just analyze the structure.

6. OB_CALLBACK_ENTRY

typedef struct _OB_CALLBACK_ENTRY {
    LIST_ENTRY Link;               // linked list
    PVOID PreOperation;            // pre-operation callback
    PVOID PostOperation;           // post-operation callback
    PVOID RegistrationContext;     // context
    EX_PUSH_LOCK Lock;             // lock-protection structure
    EX_RUNDOWN_REF RundownProtect; // rundown protection
} OB_CALLBACK_ENTRY, *POB_CALLBACK_ENTRY;

This is used in ObUnRegisterCallbacks.

Conclusion

The analysis above is based on Windows 10 build 19041. Corrections are welcome.

Colin Xiao — I take on contract reverse-engineering work, and writeups like this are how I show what I can do. If you need help with anything in this space — including but not limited to the areas I cover above — I’m available. No project is too big or too small — if it’s reverse engineering and I can do it, I’ll take it. DM me on X (@colinxiao_lab) or Telegram (@ColinXiao666).

Get in touch