Files

195 lines
5.3 KiB
Go

package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"ringer/extract"
"ringer/pefile"
"ringer/runtime"
)
// sysIoctl is the JSON form of an extracted IOCTL code.
type sysIoctl struct {
Code uint32 `json:"code"`
DeviceType uint16 `json:"device_type"`
DeviceName string `json:"device_type_name"`
Function uint16 `json:"function"`
Method uint8 `json:"method"`
MethodName string `json:"method_name"`
Access uint8 `json:"access"`
AccessName string `json:"access_name"`
Confidence string `json:"confidence"`
Source string `json:"source"`
RVA uint32 `json:"rva"`
}
// sysString is the JSON form of an extracted string.
type sysString struct {
Value string `json:"value"`
Encoding string `json:"encoding"`
Offset uint32 `json:"offset"`
Category string `json:"category"`
}
// systemDriver is the per-driver result of a system check.
type systemDriver struct {
Name string `json:"name"`
Path string `json:"path"`
ImageBase string `json:"image_base"`
ImageSize uint32 `json:"image_size"`
Ioctls []sysIoctl `json:"ioctls"`
RingBuffers []sysString `json:"ring_buffers"`
Devices []sysString `json:"devices"`
}
// systemCheckReport is the full system-check output.
type systemCheckReport struct {
TotalDrivers int `json:"total_drivers"`
Drivers []systemDriver `json:"drivers"`
}
// runSystemCheck audits every loaded kernel module, writes a JSON report to
// outPath, and prints a summary to stdout.
func runSystemCheck(outPath string, minString int) error {
mods, err := runtime.Modules()
if err != nil {
return fmt.Errorf("enumerate loaded modules: %w", err)
}
report := systemCheckReport{TotalDrivers: len(mods), Drivers: make([]systemDriver, 0, len(mods))}
for _, m := range mods {
d := systemDriver{
Name: filepath.Base(m.Path),
Path: m.Path,
ImageBase: fmt.Sprintf("0x%x", m.ImageBase),
ImageSize: m.ImageSize,
}
if f, err := pefile.Open(resolveModulePath(m.Path)); err == nil {
for _, c := range extract.ExtractIoctls(f) {
d.Ioctls = append(d.Ioctls, toSysIoctl(c))
}
for _, s := range extract.ExtractStrings(f, minString) {
ss := toSysString(s)
switch s.Category {
case "section", "ringbuffer":
d.RingBuffers = append(d.RingBuffers, ss)
case "device", "symlink":
d.Devices = append(d.Devices, ss)
}
}
}
report.Drivers = append(report.Drivers, d)
}
data, err := json.MarshalIndent(report, "", " ")
if err != nil {
return fmt.Errorf("marshal report: %w", err)
}
if err := os.WriteFile(outPath, data, 0o644); err != nil {
return fmt.Errorf("write report %q: %w", outPath, err)
}
printSystemSummary(report)
return nil
}
// printSystemSummary prints a concise summary of the system check to stdout.
func printSystemSummary(r systemCheckReport) {
totalIoctls, totalRB, totalDev, interesting := 0, 0, 0, 0
for _, d := range r.Drivers {
totalIoctls += len(d.Ioctls)
totalRB += len(d.RingBuffers)
totalDev += len(d.Devices)
if len(d.Ioctls) > 0 || len(d.RingBuffers) > 0 || len(d.Devices) > 0 {
interesting++
}
}
fmt.Printf("System check: %d drivers loaded\n", r.TotalDrivers)
fmt.Printf("================================\n")
fmt.Printf("Total IOCTLs: %d\n", totalIoctls)
fmt.Printf("Total ring buffers: %d\n", totalRB)
fmt.Printf("Total device names: %d\n", totalDev)
fmt.Printf("Drivers with findings: %d\n", interesting)
type row struct {
name string
io int
rb int
dev int
}
var rows []row
for _, d := range r.Drivers {
if len(d.Ioctls) == 0 && len(d.RingBuffers) == 0 && len(d.Devices) == 0 {
continue
}
rows = append(rows, row{d.Name, len(d.Ioctls), len(d.RingBuffers), len(d.Devices)})
}
sort.Slice(rows, func(i, j int) bool {
if rows[i].io != rows[j].io {
return rows[i].io > rows[j].io
}
return rows[i].name < rows[j].name
})
if len(rows) > 0 {
fmt.Printf("\n%-40s %8s %8s %8s\n", "Driver", "IOCTLs", "RingBuf", "Devices")
fmt.Printf("%-40s %8s %8s %8s\n", strings.Repeat("-", 40), "------", "------", "------")
for _, r := range rows {
fmt.Printf("%-40s %8d %8d %8d\n", r.name, r.io, r.rb, r.dev)
}
}
}
// toSysIoctl converts an extracted IOCTL to its JSON form.
func toSysIoctl(c extract.IoctlCode) sysIoctl {
dt := extract.DeviceTypeName(c.DeviceType)
if dt == "" {
dt = fmt.Sprintf("0x%04X", c.DeviceType)
}
return sysIoctl{
Code: c.Code,
DeviceType: c.DeviceType,
DeviceName: dt,
Function: c.Function,
Method: c.Method,
MethodName: extract.MethodName(c.Method),
Access: c.Access,
AccessName: extract.AccessName(c.Access),
Confidence: c.Confidence,
Source: c.Source,
RVA: c.RVA,
}
}
// toSysString converts an extracted string to its JSON form.
func toSysString(s extract.StringInfo) sysString {
return sysString{
Value: s.Value,
Encoding: s.Encoding,
Offset: s.Offset,
Category: s.Category,
}
}
// resolveModulePath converts a kernel module path (e.g. \SystemRoot\...) to a
// filesystem path.
func resolveModulePath(p string) string {
root := os.Getenv("SystemRoot")
if root == "" {
root = `C:\Windows`
}
if strings.HasPrefix(p, `\SystemRoot\`) {
return root + strings.TrimPrefix(p, `\SystemRoot`)
}
if strings.HasPrefix(p, `SystemRoot\`) {
return root + `\` + strings.TrimPrefix(p, `SystemRoot\`)
}
return p
}