Skip to main content
Xsec

EDR Neutralization

Published on 10 min read

Updated on

Seriesevasion
Part 3 of 3
In this series27 min read in total
  1. Windows Internals
  2. EDR Internals
  3. EDR Neutralization

From driver-signing trust to evidence of an unauthorized termination primitive.

DangerScope of the analysis

This article describes a defensive analysis and lab-validation method. A signed driver, an Input/Output Control (IOCTL) interface, or an import of a sensitive routine is never enough to establish a vulnerability. Evidence must cover the caller, target control, authorization, and capability gain on the exact versions under test.

Bring Your Own Vulnerable Driver (BYOVD) uses a legitimate but vulnerable driver to request a privileged kernel operation. Here, that operation is process termination. The analysis therefore does not start with a function name and jump directly to exploitation; it follows the entire device protocol, from opening the device to the final effect.

Three observations must remain separate:

  • a signature establishes loading trust under the active policy;
  • an IOCTL selects a command in the driver’s own protocol;
  • an import of ZwTerminateProcess shows that the binary can call the routine.

None of these observations proves on its own that a process can terminate any PID it chooses.

From loading trust to exposed capability

A code-integrity policy can trust a driver’s signature without knowing the quality of every authorization check it implements. BYOVD exploits precisely this gap: code is admitted into the kernel, then its device interface may offer an overly broad capability to a caller that can reach it.

That capability may remain narrow, such as terminating a process, or become broader, such as reading or writing kernel memory. A termination primitive is neither a kernel-write primitive nor arbitrary kernel code execution. This distinction determines what the flaw can actually neutralize.

In this scenario, the final effect may pass through ZwTerminateProcess. The routine executes from ntoskrnl.exe, is declared in ntddk.h, and is resolved by a driver through NtosKrnl.lib. It receives a process handle and an exit status, not a Process Identifier (PID). If the protocol accepts a PID, the driver must therefore convert it into an object or handle, often through ZwOpenProcess, before requesting termination.

The existence of that path raises a more precise question: who can open the device and have the command accepted?

Finding a candidate without confusing triage and proof

LOLDB centralizes metadata about known Windows drivers, including hashes, certificates, imports, and usage categories. It narrows the search space; it does not replace analysis of the exact binary or an authorization test.

LOLDB home page presenting the Windows driver database
LOLDB overview captured on July 23, 2026. Counters describe the database at capture time and may change.

The Process Killers filter groups entries associated with an observed termination capability. It does not mean that every listed driver exposes the same IOCTL, accepts the same buffer format, or is usable from the same privilege level.

LOLDB results filtered for drivers associated with process termination
LOLDB with the Process Killers filter. A record provides triage leads; the file version, device protocol, and checks still require lab validation.

Triage therefore identifies which binaries deserve analysis. It still does not show how an application reaches the sensitive branch. Answering that question requires reconstructing the driver’s dispatch routine.

Reconstructing the IOCTL contract

An IOCTL is a 32-bit value structured by CTL_CODE(DeviceType, Function, Method, Access). Method describes buffer transfer; Access describes the device-handle rights the I/O manager requires before creating the request. Even FILE_ANY_ACCESS does not automatically make the device public: its Access Control List (ACL) first determines which security principals can obtain a handle.

In a Windows Driver Model (WDM) driver, DriverEntry is the entry point compiled into the vendor’s .sys file. It initializes the DRIVER_OBJECT structure. The MajorFunction[IRP_MJ_DEVICE_CONTROL] entry can contain the address of a dispatch routine supplied by that same driver. IRP_MJ_DEVICE_CONTROL is a major request code, not a function exported by Windows.

NoteWhat a decompiler actually establishes

IDA Pro or Ghidra can expose imports, cross-references, branches, and buffer accesses. Recovered pseudocode remains a reading aid, not the vendor’s source code. Types, variable names, and authorization conditions must be checked against disassembly and, where needed, an instrumented execution.

  1. Identify the binary and device

    Preserve the hash, version, and signature of the analyzed file. Locate device creation, its symbolic link, and the security descriptors governing who can open it.

  2. Recover the dispatch routine

    Follow initialization of DRIVER_OBJECT.MajorFunction[] to the IRP_MJ_DEVICE_CONTROL entry. The resulting address belongs to a function compiled into the .sys, even though the major code is defined by the Windows Driver Kit (WDK).

  3. Decode each command

    For every compared value, decode DeviceType, Function, Method, and Access, then record buffer sizes, offsets, and directions. A constant without its input format does not reconstruct the contract.

  4. Follow the privileged effect

    Trace caller-controlled data to ZwOpenProcess, ZwTerminateProcess, or another sensitive routine. Record every branch that validates caller identity, target, and execution context.

  5. Measure capability gain

    Compare the effect obtained through the device with the same caller’s ordinary rights. A vulnerability exists when the driver grants a privileged operation the caller could not perform directly.

Reading the reverse-engineering captures

The preceding method supplies the narrative thread. The following captures show how its individual proofs appear during static analysis. They document one sample and lab session; they do not describe every driver with a similar import.

The import table first establishes which kernel routines the binary can resolve. It does not yet show which user-controlled entry reaches them.

Kernel import table displayed in IDA
Lab observation: an excerpt from the analyzed binary's import table. An import describes a possible dependency, not a necessarily reachable branch.

Finding ZwTerminateProcess narrows the search to its references. The routine appears here as a symbol imported from the Windows kernel.

ZwTerminateProcess import and references in IDA
Lab observation: `ZwTerminateProcess` in the imports. The routine executes from `ntoskrnl.exe`; the capture proves only that this sample can reference it.

Cross-references then connect the import to functions in the driver that call it. This establishes internal reachability, but not yet reachability from an IOCTL.

Cross-references to ZwTerminateProcess in IDA
Lab observation: code references to the import. Each caller must be traced back to an entry in the device protocol.

The control-flow graph exposes the call and surrounding branches. Visible loops and tests must be interpreted with their buffers and bounds.

Control-flow graph around a ZwTerminateProcess call
Lab observation: a branch containing `ZwTerminateProcess`, followed by handle closure. The graph does not yet prove who supplies those handles.

Pseudocode makes the relation between the handle and the call easier to read. The value’s origin must still be recovered before concluding that the caller controls the target.

Pseudocode showing several ZwTerminateProcess calls
Lab observation: termination calls on handles stored in a local structure. Their origin and preceding checks determine whether the path is secure.

References to that function finally lead to the routine preparing its arguments. This backward chain links the privileged effect to its command.

Cross-references to the function calling ZwTerminateProcess
Lab observation: tracing callers of the termination function. The goal is to identify the dispatch routine and the data's origin.

The visible dispatch branch selects an IOCTL constant. That value identifies a command for this binary version, not a generic Windows code.

Dispatch branch comparing an IOCTL value in pseudocode
Lab observation: constant `0xB4A00404` reaches the analyzed function after a size check. Device ACLs, caller authorization, and target restrictions remain to be established.

These screens reconstruct a code path. Classifying it as a vulnerability still requires dynamic and comparative evidence.

When a termination IOCTL is actually a vulnerability

The documented path separates I/O-manager gates, vendor-driver logic, and the kernel-supplied effect:

Technical diagram
FROM IOCTL TO PROCESS TERMINATION: A visible control code and a kernel import do not establish unauthorized reachability.BYOVD ANALYSIS · DEVICE CONTROL PATHFROM IOCTL TO PROCESS TERMINATIONA visible control code and a kernel import do not establish unauthorized reachability.USER MODEKERNEL MODEDeviceIoControlIRPopendeliverauthorized PIDkernel handleCALLERApplicationCaller-controlled PIDFIRST GATEDevice ACLOpen handleI/O MANAGERDeviceIoControlBuild an IRPSECOND GATEIOCTL AccessHandle rightsVENDOR .SYSDispatch routineDecode + validateRUNTIME POLICYCaller + target checksAllow or rejectNTOSKRNL.EXEZwOpenProcessPROCESS_TERMINATENTOSKRNL.EXEZwTerminateProcessHandle, not PIDVULNERABILITY TEST · REACHABLE CALLER · CONTROLLED TARGET · MISSING AUTHORIZATION · PRIVILEGED EFFECT · REPRODUCIBLE IMPACT
Documented request path. The vendor driver owns the device protocol, target validation, authorization, and the decision to invoke the kernel routines.How to read the diagramRead the solid path from the caller to the final effect. The device ACL gates opening the device, the IOCTL Access field gates request delivery, and the vendor's runtime checks decide whether the target is authorized. Only a capability gain beyond the caller's ordinary rights establishes a vulnerability.

DeviceIoControl is the user-mode API that sends the control code and buffers. Its entry point is exported by Kernel32.dll, its prototype is declared in ioapiset.h, and the application links it through Kernel32.lib. The I/O manager turns the call into an I/O Request Packet (IRP) carrying the IRP_MJ_DEVICE_CONTROL major code, then invokes the dispatch routine registered by the driver.

ZwOpenProcess and ZwTerminateProcess execute from ntoskrnl.exe, are declared in ntddk.h, and are linked by the driver through NtosKrnl.lib. The former can request a handle with PROCESS_TERMINATE; the latter attempts to terminate the process represented by that handle. Either can fail if the identifier is stale, access is not granted, the handle is invalid, applicable target protections intervene, or the process is already terminating.

Classifying the path as vulnerable requires evidence for the complete chain:

PropertyRequired evidence
AccessibilityA caller within the threat model can open the device object
ReachabilityThe IOCTL reaches the termination branch through IRP_MJ_DEVICE_CONTROL
Target controlThe caller provides or influences the PID or handle being used
Insufficient authorizationThe driver does not correctly validate identity, rights, or target
Capability gainThe driver terminates a process the caller cannot open directly with PROCESS_TERMINATE
ImpactThe target can be a system process, security service, or another user’s process
ReproducibilityThe effect is confirmed on the exact driver, product, and Windows versions

The factual demonstration is comparative: a caller can open the device, send the command with a controlled target, and cause a termination it could not obtain directly under its own rights. If the device is restricted to SYSTEM or a designated service, the handler authenticates the caller, and targets are tightly constrained, the same ZwTerminateProcess call may be an intentional administration function.

ImportantPrimitive boundary

A termination IOCTL provides, at most, a termination primitive. It does not by itself modify the internal structures representing kernel callbacks. That operation requires a separate flaw providing a write of a chosen value to a chosen kernel address, followed by discovery specific to the build under examination.

This boundary connects directly to the EDR internals technical review, which separates sensor registration, event delivery, correlation, and response. Terminating a service or removing one signal does not erase observations already collected through other sources.

Reducing BYOVD risk

Analysis of one case shows where to repair its protocol. Fleet-wide defense must also act before driver loading and after the driver appears:

  • apply vendor updates and remove vulnerable versions;
  • enable and verify the Microsoft Vulnerable Driver Blocklist, then complement it with Windows Defender Application Control or App Control for Business policy as appropriate;
  • use Hypervisor-protected Code Integrity (HVCI) to strengthen kernel code integrity without treating it as a guarantee against every signed vulnerable driver;
  • restrict the device ACL, choose an appropriate IOCTL Access field, and validate sizes, caller identity, and every target in the handler;
  • monitor driver-service installation, kernel-image loading, access to sensitive devices, and security-product health;
  • confirm the expected policies are actually active on endpoints because coverage depends on Windows version, configuration, and list state.

Conclusion

EDR neutralization through BYOVD is not the automatic consequence of a signature, a readable IOCTL, or a ZwTerminateProcess import. It results from a complete chain: the driver is admitted, its device is accessible, a command reaches a privileged routine, the target is controllable, and authorization fails to contain the effect.

This narrative turns a search for function names into a security analysis: establish the contract, trace the data, compare rights, and bound the exact primitive obtained.

References

Use with an AI

Actions