7.6 KiB
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/MOVimmediates and MSVCswitch-jump-table dispatch (high confidence). - Data scan of
.rdata/.data/.pdatafor 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, andAccessvia theCTL_CODElayout, deduplicated, and sorted.
- x86/x64 disassembly of
- 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:
DeviceIoControlagainst each discovered device, with a per-IOCTL timeout and automatic retry onERROR_INSUFFICIENT_BUFFER.OpenFileMapping+MapViewOfFileagainst 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:
- Disassembly — a linear sweep of
.textwithx86asm.Decode(64-bit for AMD64, 32-bit for I386). 32-bit immediates inCMP/MOVare IOCTL candidates. Compiler-generatedswitchdispatch (sub/add+ range check + jump table) is recognized and every case value is recovered, including sparse switches via their byte-index table. - Data scan — 32-bit values in
.rdata/.data/.pdatathat decode to a plausible IOCTL and are referenced by code (thecmp IoControlCode, [table+idx*4]pattern). - 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).
go build -o ringer.exe .
To cross-compile from another OS:
GOOS=windows GOARCH=amd64 go build -o ringer.exe .
Usage
ringer.exe [flags] <driver.sys>
| 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):
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:
ringer.exe C:\path\to\driver.sys
Full analysis, then stop and delete the service it created:
ringer.exe --cleanup C:\path\to\driver.sys
Machine-readable output:
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:
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
DeviceIoControlresults 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
--cleanupto stop and delete it after verification. A service that already existed before the run is never touched. - Use
--staticfor 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
.sysfile the kernel maps) and confirmed by liveDeviceIoControl. 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