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:

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