Files

193 lines
7.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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] <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):
```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