Overview
This article will analyze a sample code that implements a stealthy process termination tool for Windows. It terminates processes by sending IOCTL requests to a kernel driver. We will dig into its internal mechanisms to help readers understand the relevant concepts.
https://github.com/b1-team/superman
The program provides a command-line interface to specify a target process ID for termination. The main features include:
- Loading a signed kernel driver
- Sending IOCTL codes to the driver
- The driver terminates processes by HOOKING ZwTerminateProcess
- Support for recursive mode to continuously terminate processes
Implementation Analysis
Let's look at the key implementations in detail.
Initialization
The program entry is in the main function. It first calls init for initialization:
fn init() -> Result<PathBuf, io::Error> { // Get cache directory let mut path = dirs::cache_dir().unwrap_or_default(); // Create temp folder under cache path.push("temp"); if !path.exists() { fs::create_dir(&path)?; } // Set driver file path path.push("superman.sys"); // Write from resource if driver does not exist if !path.exists() { fs::write(&path, INCLUDE_BYTES!("../driver/superman.sys"))?; } return Ok(path); }
This ensures we get the driver file path, creating directories and files if needed.
Loading Driver
With the driver file, next is loading it into the kernel.
This is done via the Windows API CreateService:
unsafe { // Open SC manager let scm = OpenSCManager(null_mut(), null_mut(), SC_MANAGER_ALL_ACCESS); // Register driver service let service = CreateService(scm, name, ...); // Start driver service StartService(service, ...); // Close handles CloseServiceHandle(scm); CloseServiceHandle(service); }
After creating and starting the service, the driver will be loaded into the kernel.
Sending IOCTL Requests
After loading, the program opens the device and sends IOCTL (Input Output Control) codes to interact.
First, it sends the init code to notify the driver to get ready:
let init_code = 0x8000C004; let pid = 1234; unsafe { DeviceIoControl(device, init_code, &pid, size_of::<u32>(), null_mut(), 0, &mut bytes, null_mut()); }
Then it sends the terminate process code:
let kill_code = 0x8000C008; unsafe { DeviceIoControl(device, kill_code, &pid, size_of::<u32>(), null_mut(), 0, &mut bytes, null_mut()); }
The driver handles these codes based on the logic.
Driver Internal
The main flow of the driver is:
- HOOK ZwTerminateProcess in DriverEntry
- Save original function pointer
- Implement new TerminateProcess function
- Intercept calls based on IOCTL
It can be illustrated as:
+---------------------------+ | | | Process --> Original ZwTerminate | | | +---------------------------+ | | HOOK V +---------------------------+ | | | Intercept Call | | Call Original or Intercept| | | +---------------------------+
By HOOKING the original, the driver can stealthily filter termination requests.
Specifically, the driver uses the INLINE HOOK method:
- Get original function address, save to global
- Modify original function header to JMP to new function
- New function checks request, calls original or intercepts
- Restore original on unload
This enables the driver to terminates processes without file artifacts.
Summary
Through the code analysis, we focused on:
- Implementation details like init, loading, IOCTLs
- Internal mechanism of the driver HOOK and stealthy termination
- Technical details such as DRIVERENTRY and INLINE HOOK
This aims to help understand how kernel drivers work and the security considerations involved.