mirror of
https://github.com/NomanNasirMinhas/Ringer
synced 2026-08-19 05:01:13 +00:00
238 lines
7.4 KiB
Go
238 lines
7.4 KiB
Go
// Command ringer extracts IOCTL codes and shared ring buffer strings from a
|
|
// Windows kernel driver (.sys) and, by default, verifies each by connecting to
|
|
// the live driver and reading its output.
|
|
package main
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"ringer/extract"
|
|
"ringer/load"
|
|
"ringer/pefile"
|
|
"ringer/report"
|
|
"ringer/runtime"
|
|
"ringer/verify"
|
|
)
|
|
|
|
func main() {
|
|
var (
|
|
file = flag.String("file", "", "path to the driver .sys file")
|
|
device = flag.String("device", "", "device name override (e.g. \\\\.\\MyDevice)")
|
|
section = flag.String("section", "", "section name override (e.g. Global\\MySection)")
|
|
static = flag.Bool("static", false, "skip live verification (static extraction only)")
|
|
jsonOut = flag.Bool("json", false, "emit JSON instead of text")
|
|
minString = flag.Int("min-string", 4, "minimum string length to extract")
|
|
bufferSize = flag.Int("buffer-size", 4096, "IOCTL input/output buffer size in bytes")
|
|
timeoutStr = flag.String("timeout", "2s", "per-IOCTL timeout (e.g. 2s, 500ms)")
|
|
method = flag.String("method", "all", "filter IOCTLs by method: buffered|in_direct|out_direct|neither|all")
|
|
cleanup = flag.Bool("cleanup", false, "stop and delete the driver service after verification (only if it was created by this run)")
|
|
systemCheck = flag.Bool("system-check", false, "audit all loaded drivers and save a JSON report")
|
|
output = flag.String("output", "system-check.json", "output path for the --system-check JSON report")
|
|
)
|
|
flag.Parse()
|
|
|
|
if *systemCheck {
|
|
if err := runSystemCheck(*output, *minString); err != nil {
|
|
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
return
|
|
}
|
|
|
|
path := *file
|
|
if path == "" && flag.NArg() > 0 {
|
|
path = flag.Arg(0)
|
|
}
|
|
if path == "" {
|
|
fmt.Fprintln(os.Stderr, "usage: ringer [flags] <driver.sys>")
|
|
flag.PrintDefaults()
|
|
os.Exit(2)
|
|
}
|
|
|
|
timeout, err := time.ParseDuration(*timeoutStr)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "invalid --timeout %q: %v\n", *timeoutStr, err)
|
|
os.Exit(2)
|
|
}
|
|
|
|
f, err := pefile.Open(path)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
ioctls := extract.ExtractIoctls(f)
|
|
ioctls = filterByMethod(ioctls, *method)
|
|
strs := extract.ExtractStrings(f, *minString)
|
|
|
|
r := &report.Report{
|
|
File: path,
|
|
Machine: f.Machine.String(),
|
|
Is64: f.Is64,
|
|
Entropy: pefile.Entropy(f.Raw),
|
|
Ioctls: ioctls,
|
|
Strings: strs,
|
|
}
|
|
|
|
if !*static {
|
|
// Load the driver as a kernel service (no-op if it is already running
|
|
// as a service), then discover its live device names and shared sections
|
|
// from the object manager rather than trusting the static strings alone.
|
|
abs, aerr := filepath.Abs(path)
|
|
if aerr != nil {
|
|
abs = path
|
|
}
|
|
svcName, created, loadErr := load.LoadDriver(abs)
|
|
if loadErr != nil {
|
|
fmt.Fprintf(os.Stderr, "warning: could not load driver as service: %v\n", loadErr)
|
|
} else {
|
|
fmt.Fprintf(os.Stderr, "driver service %q is running\n", svcName)
|
|
}
|
|
|
|
if mod, ok := runtime.FindModule(filepath.Base(path)); ok {
|
|
fmt.Fprintf(os.Stderr, "module in memory: base=0x%x size=0x%x\n", mod.ImageBase, mod.ImageSize)
|
|
} else {
|
|
fmt.Fprintf(os.Stderr, "warning: module %q not found in loaded module list\n", filepath.Base(path))
|
|
}
|
|
|
|
liveDevices := runtime.DeviceNames()
|
|
liveSections := runtime.SectionNames()
|
|
|
|
// The driver's live names, discovered from the object manager.
|
|
r.RuntimeDevices = matchLive(verify.DeviceCandidates(strs), liveDevices)
|
|
r.RuntimeSections = matchLive(verify.SectionCandidates(strs), liveSections)
|
|
|
|
// Verification targets: live names when found, static candidates otherwise.
|
|
devices := deviceList(*device, strs, liveDevices)
|
|
sections := sectionList(*section, strs, liveSections)
|
|
|
|
if len(devices) > 0 && len(ioctls) > 0 {
|
|
warnMethodNeither(ioctls)
|
|
r.IoctlResults = verify.VerifyIoctls(devices, ioctls, *bufferSize, timeout)
|
|
}
|
|
if len(sections) > 0 {
|
|
r.SectionResults = verify.VerifySections(sections, *bufferSize)
|
|
}
|
|
|
|
// Stop and delete the service we created, if requested. A pre-existing
|
|
// service is left untouched.
|
|
if *cleanup && created {
|
|
if err := load.UnloadDriver(svcName); err != nil {
|
|
fmt.Fprintf(os.Stderr, "warning: could not clean up service %q: %v\n", svcName, err)
|
|
} else {
|
|
fmt.Fprintf(os.Stderr, "service %q stopped and deleted\n", svcName)
|
|
}
|
|
}
|
|
}
|
|
|
|
if *jsonOut {
|
|
if err := r.WriteJSON(os.Stdout); err != nil {
|
|
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
return
|
|
}
|
|
r.WriteText(os.Stdout)
|
|
}
|
|
|
|
// filterByMethod keeps only IOCTLs matching the requested transfer method.
|
|
func filterByMethod(ioctls []extract.IoctlCode, method string) []extract.IoctlCode {
|
|
var want uint8
|
|
switch method {
|
|
case "all":
|
|
return ioctls
|
|
case "buffered":
|
|
want = 0
|
|
case "in_direct", "in-direct":
|
|
want = 1
|
|
case "out_direct", "out-direct":
|
|
want = 2
|
|
case "neither":
|
|
want = 3
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "warning: unknown --method %q, ignoring filter\n", method)
|
|
return ioctls
|
|
}
|
|
var out []extract.IoctlCode
|
|
for _, c := range ioctls {
|
|
if c.Method == want {
|
|
out = append(out, c)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// deviceList returns the device names to probe: the --device override if given,
|
|
// otherwise the live object-manager device names that match a statically
|
|
// extracted device/symlink string, falling back to the static candidates when
|
|
// nothing matches live.
|
|
func deviceList(override string, strs []extract.StringInfo, live []string) []string {
|
|
if override != "" {
|
|
return []string{override}
|
|
}
|
|
candidates := verify.DeviceCandidates(strs)
|
|
if matched := matchLive(candidates, live); len(matched) > 0 {
|
|
return matched
|
|
}
|
|
return candidates
|
|
}
|
|
|
|
// sectionList returns the section names to map: the --section override if
|
|
// given, otherwise the live object-manager section names that match a
|
|
// statically extracted section string, falling back to the static candidates.
|
|
func sectionList(override string, strs []extract.StringInfo, live []string) []string {
|
|
if override != "" {
|
|
return []string{override}
|
|
}
|
|
candidates := verify.SectionCandidates(strs)
|
|
if matched := matchLive(candidates, live); len(matched) > 0 {
|
|
return matched
|
|
}
|
|
return candidates
|
|
}
|
|
|
|
// matchLive returns the live names that correspond to a static candidate. It
|
|
// returns nil when the enumeration is empty or nothing matches, so callers can
|
|
// distinguish "found live" from "not found".
|
|
func matchLive(candidates, live []string) []string {
|
|
want := map[string]bool{}
|
|
for _, c := range candidates {
|
|
want[normalizeName(c)] = true
|
|
}
|
|
var out []string
|
|
for _, l := range live {
|
|
if want[normalizeName(l)] {
|
|
out = append(out, l)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// normalizeName strips object-manager namespace prefixes and lowercases a name
|
|
// so that \\.\X, \\.\Global\X, \Device\X, \DosDevices\X, and Global\X all
|
|
// compare equal.
|
|
func normalizeName(s string) string {
|
|
s = strings.ToLower(s)
|
|
for _, p := range []string{`\\.\global\`, `\\.\`, `\device\`, `\dosdevices\`, `\??\`, `\basenamedobjects\`, `global\`} {
|
|
s = strings.TrimPrefix(s, p)
|
|
}
|
|
return s
|
|
}
|
|
|
|
// warnMethodNeither prints a warning before METHOD_NEITHER IOCTLs are sent,
|
|
// since the kernel dereferences user pointers directly and a bad pointer can
|
|
// bugcheck the machine.
|
|
func warnMethodNeither(ioctls []extract.IoctlCode) {
|
|
for _, c := range ioctls {
|
|
if c.Method == 3 {
|
|
fmt.Fprintln(os.Stderr, "WARNING: METHOD_NEITHER IOCTLs will be sent. The kernel dereferences user pointers directly; a bad pointer can BSOD. Run in a VM.")
|
|
return
|
|
}
|
|
}
|
|
}
|