# Ringer Static and live analysis of Windows kernel drivers (`.sys`). Ringer extracts **IOCTL control codes** and **shared ring-buffer strings** from a driver image, then — by default — loads the driver as a kernel service and verifies each finding against the *running* system. It is a generic tool: it does not know about any specific driver's protocol. It finds the IOCTLs a driver dispatches, the device names and symbolic links it registers, and the named shared-memory sections it creates, then reads back whatever the live driver returns. ## Features - **IOCTL extraction** from the PE image: - x86/x64 disassembly of `.text` — `CMP`/`MOV` immediates and MSVC `switch`-jump-table dispatch (high confidence). - Data scan of `.rdata`/`.data`/`.pdata` for 32-bit values that decode to a plausible IOCTL and are referenced by code (medium confidence). - Plain IOCTL-array detection for vendor device types `0x8000`–`0xBFFF` (low confidence). - Each code is decoded into `DeviceType`, `Function`, `Method`, and `Access` via the `CTL_CODE` layout, deduplicated, and sorted. - **String extraction** — ASCII and UTF-16LE, classified into device names, symbolic links, section names, and ring-buffer/shared-memory keywords. Format-string templates (e.g. `\??\%ls`) are filtered out. - **Live verification**: - `DeviceIoControl` against each discovered device, with a per-IOCTL timeout and automatic retry on `ERROR_INSUFFICIENT_BUFFER`. - `OpenFileMapping` + `MapViewOfFile` against each shared section, dumping the mapped region (raw hex — no protocol decoding). - **Runtime discovery** — enumerates the live object manager (`\Device`, `\GLOBAL??`, `\BaseNamedObjects`) and the loaded-module list, so device names and shared sections are taken from the running system rather than trusted from static strings alone. - **Auto-load** — installs and starts the driver as a demand-start kernel service (no-op if it is already running). - **System check** — audit every loaded kernel driver at once, extracting IOCTLs, ring-buffer strings, and device names from each and saving a JSON report. ## How it works ### IOCTL extraction Three passes over the image, merged by confidence: 1. **Disassembly** — a linear sweep of `.text` with `x86asm.Decode` (64-bit for AMD64, 32-bit for I386). 32-bit immediates in `CMP`/`MOV` are IOCTL candidates. Compiler-generated `switch` dispatch (`sub`/`add` + range check + jump table) is recognized and every case value is recovered, including sparse switches via their byte-index table. 2. **Data scan** — 32-bit values in `.rdata`/`.data`/`.pdata` that decode to a plausible IOCTL *and* are referenced by code (the `cmp IoControlCode, [table+idx*4]` pattern). 3. **Table scan** — runs of consecutive DWORDs sharing a custom vendor device type (`0x8000`–`0xBFFF`), which catches plain IOCTL arrays iterated in a loop and therefore missed by the xref-gated data scan. Candidates are filtered against known NTSTATUS values, degenerate codes, and values whose device type falls in the NTSTATUS error range, then deduplicated keeping the strongest confidence. ### Live verification Device names and section names are derived from the extracted strings, then cross-referenced against the live object manager. For each device, every IOCTL is sent with a zeroed input buffer and the output is read back. For each section, the mapping is opened and its first bytes are dumped. ## Building Requires Go 1.26+ and a Windows build target (the tool uses `golang.org/x/sys/windows`). ```sh go build -o ringer.exe . ``` To cross-compile from another OS: ```sh GOOS=windows GOARCH=amd64 go build -o ringer.exe . ``` ## Usage ```sh ringer.exe [flags] ``` | Flag | Default | Description | | --- | --- | --- | | `--file` | | Path to the driver `.sys` file (also accepted as a positional argument) | | `--device` | | Device name override (e.g. `\\.\MyDevice`) | | `--section` | | Section name override (e.g. `Global\MySection`) | | `--static` | `false` | Skip live verification (static extraction only) | | `--json` | `false` | Emit JSON instead of text | | `--min-string` | `4` | Minimum string length to extract | | `--buffer-size` | `4096` | IOCTL input/output buffer size in bytes | | `--timeout` | `2s` | Per-IOCTL timeout (e.g. `2s`, `500ms`) | | `--method` | `all` | Filter IOCTLs: `buffered`, `in_direct`, `out_direct`, `neither`, `all` | | `--cleanup` | `false` | Stop and delete the driver service after verification (only if this run created it) | | `--system-check` | `false` | Audit every loaded kernel driver and save a JSON report (see below) | | `--output` | `system-check.json` | Output path for the `--system-check` JSON report | ### Examples Static extraction only (safe — no driver is loaded or contacted): ```sh ringer.exe --static C:\path\to\driver.sys ``` Full analysis — load the driver as a service, discover its live device names and sections, and verify every IOCTL and section: ```sh ringer.exe C:\path\to\driver.sys ``` Full analysis, then stop and delete the service it created: ```sh ringer.exe --cleanup C:\path\to\driver.sys ``` Machine-readable output: ```sh ringer.exe --json --static driver.sys > report.json ``` ### System check Audit every loaded kernel driver on the machine, extract IOCTLs, ring-buffer strings, and device names from each, and save a JSON report: ```sh ringer.exe --system-check --output system-check.json ``` This prints a summary to stdout (total drivers, IOCTLs, ring buffers, device names, and a per-driver table sorted by IOCTL count) and writes the full per-driver findings to the JSON file. It does not load or contact any driver — it only reads the loaded-module list and the on-disk images. ## Output Text output is organized into sections: - **IOCTL Codes** — code, device type, function, method, access, confidence, and source (`disasm`, `data`, `table`, `switch`). - **Shared Ring Buffer Strings** — section names and ring-buffer keywords. - **Device Names / Symbolic Links** — names extracted from the image. - **Runtime Discovery** — the device names and shared sections found in the live object manager. - **Live Verification: IOCTL** — per-device `DeviceIoControl` results with hex-dumped output. - **Live Verification: Shared Sections** — mapped size and hex-dumped contents. `--json` emits the same data as structured JSON (`ioctls`, `shared_ring_buffer_strings`, `device_strings`, `runtime_devices`, `runtime_sections`, `ioctl_verification`, `section_verification`). ## Safety > **Live verification sends real IOCTLs to a kernel driver and can bugcheck the > machine — especially `METHOD_NEITHER`, where the kernel dereferences user > pointers directly. Run it in a VM.** - Loading a driver as a service requires **administrator privileges** and, for unsigned drivers, **test signing mode** (`bcdedit /set testsigning on`). - By default Ringer leaves the service it created installed and running. Pass `--cleanup` to stop and delete it after verification. A service that already existed before the run is never touched. - Use `--static` for analysis that never touches the live system. ## Limitations - **IOCTL codes cannot be read from kernel memory from user mode.** They are extracted from the loaded image (the `.sys` file the kernel maps) and confirmed by live `DeviceIoControl`. Device names and shared sections *are* read from the live object manager. - Split-immediate IOCTLs (`mov hi16; or lo16`) are not reconstructed. - Packed or encrypted drivers are flagged by high entropy; their static results are unreliable. - ARM64 images are data-scanned only (no x86 disassembly). ## License MIT