Detecting BYOVD Attacks (T1068): KQL and SPL Queries for Vulnerable Driver Abuse
Bring Your Own Vulnerable Driver (BYOVD) is one of the few attack patterns that can turn your entire detection stack into a spectator. The adversary doesn't bypass your EDR with clever obfuscation — they load a legitimately signed but vulnerable kernel driver, use its arbitrary read/write primitive to walk kernel structures, and unlink your EDR's callbacks from kernel space. Every detection you built above the kernel goes silent, and it happens in seconds.
This technique maps primarily to T1068 (Exploitation for Privilege Escalation), with heavy overlap into T1562.001 (Impair Defenses: Disable or Modify Tools) and T1543.003 (Create or Modify System Process: Windows Service). The good news for detection engineers: BYOVD has a very loud prelude. The driver has to get on disk, a service has to be created, and the kernel has to load the image. All three are observable.
Why BYOVD Works
Windows requires kernel drivers to be signed. Attackers don't break that requirement — they satisfy it by reusing drivers that were legitimately signed by their vendor but contain exploitable flaws. A driver that exposes an IOCTL allowing arbitrary physical memory mapping, MSR writes, or process handle elevation is a kernel-mode exploit primitive with a valid Authenticode signature attached.
Microsoft maintains a Vulnerable Driver Blocklist (HVCI-enforced), and the community-maintained LOLDrivers project catalogs known-abused drivers. But blocklists are reactive: they cover drivers that have already been abused publicly. Your detections need to catch the behavior, not just the hash.
Detection 1: Kernel Driver Load From Non-Standard Paths
Legitimate drivers live in C:\Windows\System32\drivers\. Drivers loading from C:\Users\, C:\ProgramData\, or %TEMP% are anomalous almost without exception. This is your highest-signal, lowest-effort detection.
KQL (Microsoft Sentinel / Defender XDR)
DeviceEvents
| where ActionType == "DriverLoad"
| extend DriverPath = tostring(parse_json(AdditionalFields).FileName)
| where isnotempty(DriverPath)
| where DriverPath !startswith @"C:\Windows\System32\drivers\"
and DriverPath !startswith @"C:\Windows\System32\DriverStore\"
and DriverPath !startswith @"\SystemRoot\System32\drivers\"
| extend SignerName = tostring(parse_json(AdditionalFields).Signer)
| project TimeGenerated, DeviceName, DriverPath, SignerName, InitiatingProcessFileName,
InitiatingProcessAccountName, InitiatingProcessCommandLine, SHA256
| order by TimeGenerated descTune this against 7–14 days of your own telemetry first. Virtualization software, backup agents, and some GPU tooling stage drivers in odd locations. Build an allowlist keyed on SHA256 plus SignerName, never on path alone — path allowlists are trivially abused by an attacker who writes to the allowlisted directory.
SPL (Splunk, Sysmon Event ID 6)
index=windows sourcetype="XmlWinEventLog:Microsoft-Windows-Sysmon/Operational" EventCode=6
| eval driver_dir=replace(ImageLoaded, "\\\\[^\\\\]+$", "")
| search NOT driver_dir IN ("C:\\Windows\\System32\\drivers",
"C:\\Windows\\System32\\DriverStore\\FileRepository*")
| stats count min(_time) as first_seen max(_time) as last_seen
values(Signature) as signer values(Signed) as signed
dc(host) as host_count values(host) as hosts
by ImageLoaded, Hashes
| where host_count <= 3
| convert ctime(first_seen) ctime(last_seen)
| sort - last_seenThe host_count <= 3 filter is doing real work here. Rare-driver-on-few-hosts is the signal. A driver loading on 4,000 endpoints is your VPN client; a driver loading on one endpoint at 03:14 is worth a look. Sysmon Event ID 6 is essential for this — if your Sysmon config excludes DriverLoad, you have a blind spot you cannot query your way out of.
Detection 2: Kernel Service Creation via sc.exe and Registry
To load a driver, something must register a kernel service. Loading via sc.exe create with type= kernel, or a direct write to HKLM\SYSTEM\CurrentControlSet\Services\<name> with Type = 1, is the setup step — and it fires before the driver ever touches ring 0. Catching it here is catching it in time.
DeviceProcessEvents
| where FileName in~ ("sc.exe", "cmd.exe", "powershell.exe", "pwsh.exe")
| where ProcessCommandLine has_any ("type= kernel", "type=kernel", "New-Service", "binPath")
| where ProcessCommandLine has_any (".sys", "kernel")
| project TimeGenerated, DeviceName, AccountName, FileName, ProcessCommandLine,
InitiatingProcessFileName, InitiatingProcessCommandLine
| order by TimeGenerated descThe registry-write path bypasses sc.exe entirely — plenty of loaders call NtLoadDriver directly after writing the service key themselves. Cover it:
DeviceRegistryEvents
| where ActionType in ("RegistryValueSet", "RegistryKeyCreated")
| where RegistryKey startswith @"HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\"
| where RegistryValueName in~ ("ImagePath", "Type")
| where RegistryValueData has ".sys" or RegistryValueData == "1"
| where InitiatingProcessFileName !in~ ("services.exe", "msiexec.exe", "TiWorker.exe",
"TrustedInstaller.exe", "drvinst.exe")
| project TimeGenerated, DeviceName, RegistryKey, RegistryValueName, RegistryValueData,
InitiatingProcessFileName, InitiatingProcessAccountName
| order by TimeGenerated descThe SPL equivalent, using Sysmon Event ID 13 (registry value set):
index=windows sourcetype="XmlWinEventLog:Microsoft-Windows-Sysmon/Operational"
EventCode=13 TargetObject="HKLM\\System\\CurrentControlSet\\Services\\*\\ImagePath"
| search Details="*.sys*"
| search NOT Image IN ("C:\\Windows\\system32\\services.exe",
"C:\\Windows\\system32\\msiexec.exe",
"C:\\Windows\\servicing\\TrustedInstaller.exe")
| eval svc_name=mvindex(split(TargetObject,"\\"),5)
| table _time host User Image svc_name Details ProcessGuid
| sort - _timeNote the exclusion list. services.exe, msiexec.exe, and TrustedInstaller writing driver service keys is normal patching and installation activity. A user-writable binary out of %APPDATA% doing it is not. See T1543.003 for the broader service-creation detection set.
Detection 3: Driver Load Immediately Followed by Security Tooling Going Dark
This is the correlation that converts a noisy driver-load alert into a confirmed incident. BYOVD is a means, not an end — the payoff is almost always killing defensive processes. Chain a rare driver load to a security process terminating within a short window:
let SecurityProcesses = dynamic(["MsMpEng.exe", "MsSense.exe", "SenseIR.exe",
"CSFalconService.exe", "SentinelAgent.exe", "elastic-agent.exe", "xagt.exe"]);
let DriverLoads = DeviceEvents
| where ActionType == "DriverLoad"
| extend DriverPath = tostring(parse_json(AdditionalFields).FileName)
| where DriverPath !startswith @"C:\Windows\System32\drivers\"
| project DriverTime = TimeGenerated, DeviceName, DriverPath, DriverSHA = SHA256;
let ProcKills = DeviceProcessEvents
| where ActionType == "ProcessCreated" or isnotempty(ProcessCommandLine)
| where FileName in~ SecurityProcesses
| project KillTime = TimeGenerated, DeviceName, KilledProcess = FileName;
DriverLoads
| join kind=inner (ProcKills) on DeviceName
| where KillTime between (DriverTime .. (DriverTime + 10m))
| project DriverTime, KillTime, DeviceName, DriverPath, DriverSHA, KilledProcess
| order by DriverTime descTen minutes is a starting window — most tooling acts within seconds of the driver landing. Widen to 30 minutes for hunting, tighten to 2 minutes for alerting. If your EDR emits a heartbeat or health event, correlate against a gap in that heartbeat instead of a process-termination event; a kernel-mode callback unlink may kill the agent's visibility without producing any termination record at all. That absence-of-signal is itself the detection, and it's the reason this correlation belongs in a scheduled query rather than a real-time rule. Pair it with the queries in T1562.001.
Hunting: Rare Signers in Your Driver Population
Rather than chasing known-bad hashes, invert the problem and profile what's normal:
DeviceEvents
| where ActionType == "DriverLoad"
| where TimeGenerated > ago(30d)
| extend DriverPath = tostring(parse_json(AdditionalFields).FileName),
SignerName = tostring(parse_json(AdditionalFields).Signer)
| summarize DeviceCount = dcount(DeviceName), LoadCount = count(),
FirstSeen = min(TimeGenerated), Devices = make_set(DeviceName, 10)
by SignerName, DriverPath, SHA256
| where DeviceCount < 5 and FirstSeen > ago(7d)
| order by DeviceCount asc, FirstSeen descNew signer + new hash + small blast radius in the last week is the profile of a staged BYOVD driver. Run this weekly. Most of your enterprise driver population is stable and boring, which makes the outliers genuinely findable.
Controls That Make Detection Easier
Detection is the backstop; prevention narrows what you have to detect. Enable Microsoft's Vulnerable Driver Blocklist, which is on by default on new Windows 11 installs but frequently off on upgraded and older Enterprise fleets — verify rather than assume. HVCI (Memory Integrity) blocks a large class of these drivers outright. Where HVCI isn't feasible due to compatibility, a WDAC policy in audit mode still generates the CodeIntegrity events (Microsoft-Windows-CodeIntegrity/Operational, Event IDs 3076 and 3077) that give you a clean high-signal feed with no tuning burden at all.
Response Priorities
Treat a confirmed BYOVD load as a full host compromise with kernel-level access. Anything the host reports about itself after the load is untrusted — including your EDR's own telemetry. Isolate at the network layer rather than relying on agent-based containment, collect the driver binary and service key for analysis, and rebuild rather than remediate. Then pivot: pull the driver's hash and search your full fleet for other loads, because the same operator will reuse the same driver, and the second host is usually easier to catch than the first.