Files

7.6 KiB
Raw Permalink Blame History

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 .textCMP/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 0x80000xBFFF (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 (0x80000xBFFF), 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 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