Files

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
}
}
}