// 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] ") 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 } } }