commit: add source

This commit is contained in:
kernelstub
2026-06-13 15:48:59 +02:00
parent aefef00f79
commit faa0ce7a67
48 changed files with 4037 additions and 0 deletions
+155
View File
@@ -0,0 +1,155 @@
package advanced
import (
"fmt"
"ferrum/core"
win "ferrum/windows/facade"
)
type definition struct {
name string
description string
}
var definitions = []definition{
{"comdcom", "Audit COM/DCOM registration and activation surface"},
{"comelevation", "Audit COM elevation moniker and auto-elevated COM surface"},
{"dcomactivation", "Audit DCOM activation and machine-wide COM security policy"},
{"rpc", "Audit RPC endpoint and service registration surface"},
{"alpc", "Audit ALPC-related object and broker surface"},
{"lpc", "Audit legacy LPC and object namespace surface"},
{"tokenimpersonation", "Audit token impersonation privilege and server process surface"},
{"autoruns", "Audit extended autorun and logon execution locations"},
{"ifeo", "Audit Image File Execution Options interception settings"},
{"silentexit", "Audit SilentProcessExit monitor process settings"},
{"winlogon", "Audit Winlogon shell, userinit, and notification surface"},
{"lsa", "Audit LSA authentication, notification, and security packages"},
{"appinit", "Audit AppInit DLL configuration"},
{"appcert", "Audit AppCert DLL process creation hooks"},
{"uac", "Audit UAC policy values"},
{"uacautoelevation", "Audit UAC auto-elevation and consent policy surface"},
{"installer", "Audit Windows Installer elevation policy"},
{"msi", "Audit Windows Installer and repair abuse surface"},
{"installerrepair", "Audit MSI repair and advertised shortcut abuse surface"},
{"powershell", "Audit PowerShell policy and profile surface"},
{"applocker", "Audit AppLocker and SRP policy presence"},
{"wdac", "Audit Windows Defender Application Control policy presence"},
{"wdacpolicy", "Audit WDAC and AppLocker policy interface surface"},
{"defender", "Audit Microsoft Defender policy and exclusions"},
{"firewall", "Audit Windows Firewall profile posture"},
{"rdp", "Audit Remote Desktop exposure policy"},
{"brokers", "Audit privileged broker process and COM broker surface"},
{"appcontainerbrokers", "Audit AppContainer broker and capability surface"},
{"wmi", "Audit WMI repository and AutoRecover MOF surface"},
{"hosts", "Inspect hosts file overrides"},
{"shares", "Audit configured LanmanServer shares"},
{"shell", "Audit Explorer shell extension and delay-load surface"},
{"browser", "Audit browser helper and native messaging extension surface"},
{"protocols", "Audit custom URL protocol handlers"},
{"urlmonikers", "Audit URL moniker and protocol activation surface"},
{"comlocal", "Audit per-user COM local/inproc server registrations"},
{"comhijack", "Audit COM registration hijacking surface"},
{"custommarshal", "Audit COM custom marshaling registration surface"},
{"knowndlls", "Inventory KnownDLL protected names"},
{"dllhijacking", "Audit DLL hijacking and search-order abuse surface"},
{"sxs", "Audit side-by-side assembly and activation context surface"},
{"activationctx", "Audit manifest and activation context abuse surface"},
{"ntobjmgr", "Audit NT Object Manager namespace surface"},
{"objdirs", "Audit object directory namespace surface"},
{"symlinks", "Audit symbolic link surface"},
{"hardlinks", "Audit hard link research surface"},
{"junctions", "Audit junction and directory link surface"},
{"mountpoints", "Audit mount point surface"},
{"reparsepoints", "Audit reparse point surface"},
{"oplocks", "Audit opportunistic lock race research surface"},
{"regsymlinks", "Audit registry symbolic link surface"},
{"svcpaths", "Audit service image path risk at scale"},
{"scm", "Audit Service Control Manager and service configuration surface"},
{"driverpaths", "Audit driver image path risk at scale"},
{"minifilters", "Audit file system minifilter driver surface"},
{"ioctls", "Audit kernel driver IOCTL and device exposure surface"},
{"deviceobjects", "Audit device object namespace exposure"},
{"etw", "Audit Event Tracing for Windows provider surface"},
{"win32k", "Audit Win32k and GUI subsystem boundary surface"},
{"csrss", "Audit CSRSS and console subsystem boundary surface"},
{"lsassinterfaces", "Audit LSASS interfaces and authentication package surface"},
{"accesstokens", "Audit access token privilege and integrity surface"},
{"handles", "Audit handle duplication and leak research surface"},
{"jobobjects", "Audit job object namespace and process containment surface"},
{"sectionobjects", "Audit section object and shared memory surface"},
{"sharedmemory", "Audit shared memory object namespace surface"},
{"mmap", "Audit memory-mapped file surface"},
{"wfp", "Audit Windows Filtering Platform provider surface"},
{"hyperv", "Audit Hyper-V component and service surface"},
{"wsl", "Audit Windows Subsystem for Linux component surface"},
{"certificates", "Inventory machine and user certificate store density"},
{"networkproviders", "Audit credential and network provider load order"},
{"print", "Audit print monitor, provider, and processor surface"},
{"efsrpc", "Audit EFSRPC service and RPC exposure surface"},
{"taskrpc", "Audit Task Scheduler RPC surface"},
{"bits", "Audit Background Intelligent Transfer Service surface"},
{"endpointmapper", "Audit DCOM/RPC Endpoint Mapper surface"},
{"winrm", "Audit Windows Remote Management surface"},
{"smbipc", "Audit SMB local IPC and LanmanServer surface"},
{"credproviders", "Audit credential provider and filter surface"},
{"authpackages", "Audit authentication package registration surface"},
{"lsaplugins", "Audit LSA plugin registration surface"},
{"cloudap", "Audit CloudAP and cloud authentication package surface"},
{"ppl", "Audit Protected Process Light boundary indicators"},
{"userprofilesvc", "Audit User Profile Service and profile path surface"},
{"updates", "Audit update mechanism service and policy surface"},
{"recovery", "Audit repair and recovery mechanism surface"},
{"tempfiles", "Audit temporary file handling risk surface"},
{"toctou", "Audit TOCTOU and race-prone filesystem surface"},
{"pathcanon", "Audit path canonicalization risk surface"},
{"confuseddeputy", "Audit confused deputy research surface"},
{"acl", "Audit file and registry ACL misconfiguration surface"},
{"envinjection", "Audit environment variable injection surface"},
{"searchpoison", "Audit search path poisoning surface"},
{"propertyhandlers", "Audit shell property handler surface"},
{"explorerext", "Audit Explorer extension surface"},
{"thumbnailproviders", "Audit thumbnail provider surface"},
{"previewhandlers", "Audit preview handler surface"},
{"winsock", "Audit Winsock catalog and namespace provider surface"},
{"accessibility", "Audit accessibility binary interception surface"},
{"sessionisolation", "Audit session isolation boundary surface"},
{"windowstations", "Audit desktop and window station object surface"},
{"clipboard", "Audit clipboard IPC surface"},
{"dragdrop", "Audit drag-and-drop IPC surface"},
{"dde", "Audit Dynamic Data Exchange surface"},
{"ole", "Audit OLE automation and embedding surface"},
}
func init() {
for _, def := range definitions {
core.Register(module{definition: def})
}
}
type module struct {
definition
}
func (m module) Name() string { return m.name }
func (m module) Description() string { return m.description }
func (m module) Run(ctx *core.Context) error {
ctx.Logger.Info("Running " + m.name + " audit...")
findings, err := win.EnumerateAdvancedFindings(m.name)
if err != nil {
return err
}
if len(findings) == 0 {
ctx.Logger.Info("No findings returned for " + m.name + ".")
return nil
}
for _, finding := range findings {
ctx.Logger.Success(fmt.Sprintf("%s %s > %s", finding.Severity, finding.Target, finding.Reason))
if finding.Name != "" || finding.Value != "" {
ctx.Logger.Verbose(fmt.Sprintf("%s : %s = %s", finding.Area, finding.Name, finding.Value))
}
}
return nil
}
+247
View File
@@ -0,0 +1,247 @@
package clsid
import (
"fmt"
"runtime"
"sort"
"strings"
"sync"
"ferrum/core"
"ferrum/internal"
win "ferrum/windows/facade"
)
const maxProcessWorkers = 16
const maxVerboseCLSID = 40
const maxProcMonCandidates = 120
func init() {
core.Register(Module{})
}
type Module struct{}
func (Module) Name() string {
return "clsid"
}
func (Module) Description() string {
return "Correlate privileged processes with HKCU COM registration surface"
}
func (Module) Run(ctx *core.Context) error {
ctx.Logger.Info("Applying ProcMon-style CLSID filters: User is NT AUTHORITY\\SYSTEM; Path contains HKCU\\Software\\Classes; Path contains InprocServer32 or LocalServer32; Result is NAME NOT FOUND.")
ctx.Logger.Info("Enumerating running processes...")
processes, err := win.EnumerateProcesses()
if err != nil {
return err
}
ctx.Logger.Info("Inspecting process security context...")
enriched := enrichProcesses(ctx, processes)
sortProcesses(enriched)
interesting := 0
for _, process := range enriched {
if !process.Interesting {
continue
}
interesting++
ctx.Logger.Success(fmt.Sprintf("%s[%d] > %s", process.Name, process.PID, process.Label()))
if process.Detail != "" {
ctx.Logger.Verbose(fmt.Sprintf("%s[%d] : %s", process.Name, process.PID, process.Detail))
}
}
if interesting == 0 {
ctx.Logger.Info("No privileged or elevated processes identified from accessible token data.")
}
ctx.Logger.Info("Scanning HKLM COM registrations for missing HKCU InprocServer32/LocalServer32 lookups...")
candidates, err := win.EnumerateCLSIDProcMonCandidates()
if err != nil {
ctx.Logger.Error(fmt.Sprintf("CLSID ProcMon candidate scan: %v", err))
} else {
reportProcMonCandidates(ctx, candidates, interestingProcesses(enriched))
}
ctx.Logger.Info("Inspecting HKCU\\Software\\Classes\\CLSID...")
entries, err := win.EnumerateHKCUCLSID()
if err != nil {
ctx.Logger.Error(fmt.Sprintf("HKCU CLSID enumeration: %v", err))
return nil
}
if len(entries) == 0 {
ctx.Logger.Info("No HKCU CLSID registrations found.")
return nil
}
for _, entry := range summarizeCLSID(entries) {
ctx.Logger.Success(fmt.Sprintf("HKCU\\Software\\Classes\\CLSID\\%s > %s", entry.CLSID, entry.Kind))
if entry.Value != "" {
ctx.Logger.Verbose(fmt.Sprintf("%s : %s", entry.CLSID, entry.Value))
}
}
if interesting > 0 {
ctx.Logger.Info("Privileged COM clients commonly search HKCU before HKLM for per-user COM classes; review user-controlled registrations above for unusual or hijack-prone behavior.")
}
return nil
}
func reportProcMonCandidates(ctx *core.Context, candidates []win.CLSIDProcMonCandidate, processes []processFinding) {
if len(candidates) == 0 {
ctx.Logger.Info("No HKCU COM NAME NOT FOUND candidates found for InprocServer32 or LocalServer32.")
return
}
sort.Slice(candidates, func(i, j int) bool {
if candidates[i].Kind != candidates[j].Kind {
return candidates[i].Kind < candidates[j].Kind
}
return candidates[i].CLSID < candidates[j].CLSID
})
processLabel := "NT AUTHORITY\\SYSTEM / privileged COM client"
if len(processes) > 0 {
names := make([]string, 0, len(processes))
for _, process := range internal.Limit(processes, 8) {
names = append(names, fmt.Sprintf("%s[%d]", process.Name, process.PID))
}
processLabel = strings.Join(names, ", ")
}
for _, candidate := range internal.Limit(candidates, maxProcMonCandidates) {
ctx.Logger.Success(fmt.Sprintf("%s > %s > %s", processLabel, candidate.Path, candidate.Result))
ctx.Logger.Verbose(fmt.Sprintf("%s : CLSID=%s machine=%s", candidate.Kind, candidate.CLSID, candidate.MachineValue))
}
if len(candidates) > maxProcMonCandidates {
ctx.Logger.Info(fmt.Sprintf("CLSID ProcMon candidate output limited to %d of %d rows.", maxProcMonCandidates, len(candidates)))
}
ctx.Logger.Info("These rows model the ProcMon filter Result=NAME NOT FOUND for HKCU COM override paths. Confirm live process access with ProcMon or ETW before treating a candidate as reachable.")
}
func interestingProcesses(processes []processFinding) []processFinding {
out := make([]processFinding, 0)
for _, process := range processes {
if process.Interesting {
out = append(out, process)
}
}
return out
}
type processFinding struct {
win.Process
Interesting bool
Priority int
Detail string
}
func enrichProcesses(ctx *core.Context, processes []win.Process) []processFinding {
workers := runtime.NumCPU()
if workers > maxProcessWorkers {
workers = maxProcessWorkers
}
if workers < 1 {
workers = 1
}
jobs := make(chan win.Process)
results := make(chan processFinding, len(processes))
var wg sync.WaitGroup
for i := 0; i < workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for process := range jobs {
finding := processFinding{Process: process}
token, err := win.InspectProcessToken(process.PID)
if err != nil {
finding.Detail = err.Error()
if strings.Contains(strings.ToLower(err.Error()), "access") {
ctx.Logger.Verbose(fmt.Sprintf("%s[%d] : %v", process.Name, process.PID, err))
}
results <- finding
continue
}
finding.User = token.User
finding.Integrity = token.Integrity
finding.Elevated = token.Elevated
finding.Privileges = token.Privileges
finding.Interesting, finding.Priority, finding.Detail = classify(token)
results <- finding
}
}()
}
go func() {
for _, process := range processes {
jobs <- process
}
close(jobs)
wg.Wait()
close(results)
}()
findings := make([]processFinding, 0, len(processes))
for finding := range results {
findings = append(findings, finding)
}
return findings
}
func classify(token win.TokenInfo) (bool, int, string) {
user := strings.ToUpper(token.User)
switch {
case strings.HasSuffix(user, `\SYSTEM`) || strings.EqualFold(token.User, "SYSTEM"):
return true, 100, detail(token)
case strings.HasSuffix(user, `\LOCAL SERVICE`) || strings.HasSuffix(user, `\LOCALSERVICE`):
return true, 90, detail(token)
case strings.HasSuffix(user, `\NETWORK SERVICE`) || strings.HasSuffix(user, `\NETWORKSERVICE`):
return true, 80, detail(token)
case token.Elevated:
return true, 70, detail(token)
default:
return false, 0, detail(token)
}
}
func detail(token win.TokenInfo) string {
parts := []string{}
if token.User != "" {
parts = append(parts, "user="+token.User)
}
if token.Integrity != "" {
parts = append(parts, "integrity="+token.Integrity)
}
if token.Elevated {
parts = append(parts, "elevated=true")
}
if len(token.Privileges) > 0 {
parts = append(parts, "privileges="+strings.Join(internal.Limit(token.Privileges, 8), ","))
}
return strings.Join(parts, " ")
}
func sortProcesses(processes []processFinding) {
sort.Slice(processes, func(i, j int) bool {
if processes[i].Priority != processes[j].Priority {
return processes[i].Priority > processes[j].Priority
}
return strings.ToLower(processes[i].Name) < strings.ToLower(processes[j].Name)
})
}
func summarizeCLSID(entries []win.CLSIDEntry) []win.CLSIDEntry {
sort.Slice(entries, func(i, j int) bool {
if entries[i].CLSID != entries[j].CLSID {
return entries[i].CLSID < entries[j].CLSID
}
return entries[i].Kind < entries[j].Kind
})
return internal.Limit(entries, maxVerboseCLSID)
}
+46
View File
@@ -0,0 +1,46 @@
package dllsearch
import (
"fmt"
"ferrum/core"
"ferrum/internal"
win "ferrum/windows/facade"
)
func init() { core.Register(Module{}) }
type Module struct{}
func (Module) Name() string { return "dllsearch" }
func (Module) Description() string {
return "Analyze DLL search path and KnownDLL context for hijack-prone surface"
}
func (Module) Run(ctx *core.Context) error {
ctx.Logger.Info("Analyzing DLL search path surface...")
findings, err := win.EnumerateDLLSearchPathFindings()
if err != nil {
return err
}
reported := 0
for _, finding := range findings {
if finding.Severity == "Info" {
ctx.Logger.Verbose(fmt.Sprintf("%s %s > %s", finding.Source, finding.Path, finding.Reason))
continue
}
reported++
ctx.Logger.Success(fmt.Sprintf("%s %s > %s", finding.Severity, finding.Source, finding.Reason))
ctx.Logger.Verbose(fmt.Sprintf("%s : %s", finding.Source, finding.Path))
}
if reported == 0 {
ctx.Logger.Info("No risky DLL search path entries matched the default heuristics.")
}
for _, finding := range internal.Limit(findings, 40) {
if finding.Severity == "Info" {
ctx.Logger.Verbose(fmt.Sprintf("KnownDLL : %s", finding.Path))
}
}
return nil
}
+56
View File
@@ -0,0 +1,56 @@
package drivers
import (
"fmt"
"strings"
"ferrum/core"
win "ferrum/windows/facade"
)
func init() { core.Register(Module{}) }
type Module struct{}
func (Module) Name() string { return "drivers" }
func (Module) Description() string {
return "Enumerate kernel drivers and suspicious driver load paths"
}
func (Module) Run(ctx *core.Context) error {
ctx.Logger.Info("Enumerating kernel driver services...")
drivers, err := win.EnumerateDrivers()
if err != nil {
return err
}
reported := 0
for _, driver := range drivers {
reason := driverReason(driver)
if reason == "" {
continue
}
reported++
ctx.Logger.Success(fmt.Sprintf("%s > %s", driver.Name, reason))
ctx.Logger.Verbose(fmt.Sprintf("%s : state=%s start=%s path=%s", driver.Name, driver.State, driver.StartType, driver.BinaryPath))
}
if reported == 0 {
ctx.Logger.Info("No driver load paths matched the suspicious-path heuristics.")
}
ctx.Logger.Verbose(fmt.Sprintf("Drivers enumerated: %d", len(drivers)))
return nil
}
func driverReason(driver win.DriverInfo) string {
path := strings.ToLower(driver.BinaryPath)
switch {
case strings.Contains(path, `\users\`) || strings.Contains(path, `\temp\`) || strings.Contains(path, `\programdata\`):
return "driver image in user-writable-looking location"
case driver.StartType == "Boot" || driver.StartType == "System":
return "early-load driver"
case driver.State == "Running":
return "running kernel driver"
default:
return ""
}
}
+52
View File
@@ -0,0 +1,52 @@
package env
import (
"fmt"
"strings"
"ferrum/core"
win "ferrum/windows/facade"
)
func init() { core.Register(Module{}) }
type Module struct{}
func (Module) Name() string { return "env" }
func (Module) Description() string {
return "Inspect process environment variables for audit-relevant values"
}
func (Module) Run(ctx *core.Context) error {
ctx.Logger.Info("Inspecting current process environment...")
vars, err := win.EnumerateEnvironment()
if err != nil {
return err
}
for _, env := range vars {
reason := envReason(env)
if reason == "" {
continue
}
ctx.Logger.Success(fmt.Sprintf("%s > %s", env.Name, reason))
ctx.Logger.Verbose(fmt.Sprintf("%s=%s", env.Name, env.Value))
}
ctx.Logger.Verbose(fmt.Sprintf("Environment variables inspected: %d", len(vars)))
return nil
}
func envReason(env win.EnvVar) string {
name := strings.ToUpper(env.Name)
value := strings.ToLower(env.Value)
switch {
case name == "PATH" && (strings.Contains(value, `\users\`) || strings.Contains(value, `\temp\`) || strings.Contains(value, `.`)):
return "PATH contains user-writable-looking or relative element"
case strings.Contains(name, "TOKEN") || strings.Contains(name, "SECRET") || strings.Contains(name, "PASSWORD") || strings.Contains(name, "KEY"):
return "sensitive-looking variable name"
case strings.Contains(value, `\users\`) || strings.Contains(value, `\temp\`):
return "user-writable-looking value"
default:
return ""
}
}
+65
View File
@@ -0,0 +1,65 @@
package mitigations
import (
"fmt"
"strings"
"ferrum/core"
win "ferrum/windows/facade"
)
func init() { core.Register(Module{}) }
type Module struct{}
func (Module) Name() string { return "mitigations" }
func (Module) Description() string {
return "Sample process mitigation policy posture for running processes"
}
func (Module) Run(ctx *core.Context) error {
ctx.Logger.Info("Sampling process mitigation policies...")
items, err := win.EnumerateProcessMitigations()
if err != nil {
return err
}
reported := 0
for _, item := range items {
reason := mitigationReason(item)
if reason == "" {
continue
}
reported++
ctx.Logger.Success(fmt.Sprintf("%s[%d] > %s", item.Name, item.PID, reason))
ctx.Logger.Verbose(fmt.Sprintf("%s[%d] : DEP=%s ASLR=%s StrictHandle=%s CFG=%s", item.Name, item.PID, item.DEP, item.ASLR, item.Strict, item.CFG))
}
if reported == 0 {
ctx.Logger.Info("No accessible process mitigation gaps matched the default heuristics.")
}
ctx.Logger.Verbose(fmt.Sprintf("Processes sampled: %d", len(items)))
return nil
}
func mitigationReason(item win.ProcessMitigation) string {
values := []string{item.DEP, item.ASLR, item.Strict, item.CFG}
for _, value := range values {
if strings.HasPrefix(value, "error:") || strings.HasPrefix(value, "open:") {
return ""
}
}
gaps := []string{}
if item.DEP == "off" {
gaps = append(gaps, "DEP off")
}
if item.ASLR == "off" {
gaps = append(gaps, "ASLR off")
}
if item.Strict == "off" {
gaps = append(gaps, "strict handle checks off")
}
if item.CFG == "off" {
gaps = append(gaps, "CFG off")
}
return strings.Join(gaps, ", ")
}
+56
View File
@@ -0,0 +1,56 @@
package pipes
import (
"fmt"
"strings"
"ferrum/core"
"ferrum/internal"
win "ferrum/windows/facade"
)
func init() { core.Register(Module{}) }
type Module struct{}
func (Module) Name() string { return "pipes" }
func (Module) Description() string {
return "Enumerate named pipes and flag security-relevant pipe names"
}
func (Module) Run(ctx *core.Context) error {
ctx.Logger.Info("Enumerating named pipes...")
pipes, err := win.EnumerateNamedPipes()
if err != nil {
return err
}
reported := 0
for _, pipe := range pipes {
reason := pipeReason(pipe.Name)
if reason == "" {
continue
}
reported++
ctx.Logger.Success(fmt.Sprintf("%s > %s", pipe.Name, reason))
}
ctx.Logger.Verbose(fmt.Sprintf("Pipes enumerated: %d", len(pipes)))
for _, pipe := range internal.Limit(pipes, 50) {
ctx.Logger.Verbose(fmt.Sprintf("pipe : %s", pipe.Name))
}
if reported == 0 {
ctx.Logger.Info("No named pipes matched the default high-signal name heuristics.")
}
return nil
}
func pipeReason(name string) string {
lower := strings.ToLower(name)
keywords := []string{"svc", "service", "rpc", "spool", "lsass", "samr", "netlogon", "winreg", "atsvc", "epmapper", "browser"}
for _, keyword := range keywords {
if strings.Contains(lower, keyword) {
return "security-relevant IPC surface"
}
}
return ""
}
+34
View File
@@ -0,0 +1,34 @@
package policy
import (
"fmt"
"ferrum/core"
win "ferrum/windows/facade"
)
func init() { core.Register(Module{}) }
type Module struct{}
func (Module) Name() string { return "policy" }
func (Module) Description() string {
return "Summarize hardening policy posture such as UAC, AppLocker, and WDAC"
}
func (Module) Run(ctx *core.Context) error {
ctx.Logger.Info("Checking hardening policy posture...")
findings, err := win.EnumeratePolicyFindings()
if err != nil {
return err
}
for _, finding := range findings {
ctx.Logger.Success(fmt.Sprintf("%s %s > %s", finding.Severity, finding.Name, finding.Reason))
ctx.Logger.Verbose(fmt.Sprintf("%s = %s", finding.Name, finding.Value))
}
if len(findings) == 0 {
ctx.Logger.Info("No policy findings returned from configured checks.")
}
return nil
}
+37
View File
@@ -0,0 +1,37 @@
package registry
import (
"fmt"
"ferrum/core"
win "ferrum/windows/facade"
)
func init() { core.Register(Module{}) }
type Module struct{}
func (Module) Name() string { return "registry" }
func (Module) Description() string {
return "Audit sensitive registry persistence, policy, and interception surfaces"
}
func (Module) Run(ctx *core.Context) error {
ctx.Logger.Info("Auditing sensitive registry surfaces...")
findings, err := win.EnumerateRegistryAuditFindings()
if err != nil {
return err
}
if len(findings) == 0 {
ctx.Logger.Info("No sensitive registry findings matched the configured checks.")
return nil
}
for _, finding := range findings {
ctx.Logger.Success(fmt.Sprintf("%s %s\\%s > %s: %s", finding.Severity, finding.Scope, finding.Path, finding.Name, finding.Reason))
if finding.Value != "" {
ctx.Logger.Verbose(fmt.Sprintf("%s\\%s\\%s = %s", finding.Scope, finding.Path, finding.Name, finding.Value))
}
}
return nil
}
+58
View File
@@ -0,0 +1,58 @@
package scheduled
import (
"fmt"
"strings"
"ferrum/core"
win "ferrum/windows/facade"
)
func init() { core.Register(Module{}) }
type Module struct{}
func (Module) Name() string { return "scheduled" }
func (Module) Description() string {
return "Inspect scheduled task XML for interesting execution paths"
}
func (Module) Run(ctx *core.Context) error {
ctx.Logger.Info("Enumerating scheduled tasks...")
tasks, err := win.EnumerateScheduledTasks()
if err != nil {
return err
}
reported := 0
for _, task := range tasks {
reason := taskReason(task)
if reason == "" {
continue
}
reported++
ctx.Logger.Success(fmt.Sprintf("%s > %s", task.Path, reason))
ctx.Logger.Verbose(fmt.Sprintf("%s : enabled=%s author=%s command=%s", task.Path, task.Enabled, task.Author, task.Command))
}
if reported == 0 {
ctx.Logger.Info("No scheduled tasks matched the default interesting-command heuristics.")
}
ctx.Logger.Verbose(fmt.Sprintf("Tasks enumerated: %d", len(tasks)))
return nil
}
func taskReason(task win.ScheduledTask) string {
lower := strings.ToLower(task.Command)
switch {
case task.Command == "":
return ""
case strings.Contains(lower, `\users\`) || strings.Contains(lower, `\temp\`) || strings.Contains(lower, `\programdata\`):
return "user-writable-looking task command"
case strings.Contains(lower, "powershell") || strings.Contains(lower, "wscript") || strings.Contains(lower, "cscript") || strings.Contains(lower, "rundll32") || strings.Contains(lower, "regsvr32"):
return "script or LOLBin task command"
case strings.EqualFold(task.Enabled, "false"):
return "disabled task with execution action"
default:
return ""
}
}
+67
View File
@@ -0,0 +1,67 @@
package services
import (
"fmt"
"strings"
"ferrum/core"
"ferrum/internal"
win "ferrum/windows/facade"
)
func init() { core.Register(Module{}) }
type Module struct{}
func (Module) Name() string { return "services" }
func (Module) Description() string {
return "Inventory Windows services and highlight audit-worthy configuration"
}
func (Module) Run(ctx *core.Context) error {
ctx.Logger.Info("Enumerating Windows services...")
services, err := win.EnumerateServices()
if err != nil {
return err
}
reported := 0
for _, service := range services {
reasons := serviceReasons(service)
if len(reasons) == 0 {
continue
}
reported++
ctx.Logger.Success(fmt.Sprintf("%s > %s", service.Name, strings.Join(reasons, ", ")))
ctx.Logger.Verbose(fmt.Sprintf("%s : state=%s start=%s account=%s pid=%d path=%s", service.Name, service.State, service.StartType, service.Account, service.ProcessID, service.BinaryPath))
}
if reported == 0 {
ctx.Logger.Info("No service configuration stood out from the default heuristics.")
}
ctx.Logger.Verbose(fmt.Sprintf("Services enumerated: %d", len(services)))
return nil
}
func serviceReasons(service win.ServiceInfo) []string {
reasons := []string{}
path := strings.TrimSpace(service.BinaryPath)
lowerPath := strings.ToLower(path)
if service.State == "Running" && service.Account != "" && !strings.Contains(strings.ToLower(service.Account), "localsystem") {
reasons = append(reasons, "non-System running account")
}
if isUnquotedPathWithSpaces(path) {
reasons = append(reasons, "unquoted path with spaces")
}
if strings.Contains(lowerPath, `\users\`) || strings.Contains(lowerPath, `\temp\`) || strings.Contains(lowerPath, `\programdata\`) {
reasons = append(reasons, "user-writable-looking path")
}
if service.StartType == "Auto" && service.State != "Running" {
reasons = append(reasons, "auto-start not running")
}
return internal.Limit(reasons, 4)
}
func isUnquotedPathWithSpaces(path string) bool {
path = strings.TrimSpace(path)
return strings.Contains(path, " ") && !strings.HasPrefix(path, `"`)
}
+48
View File
@@ -0,0 +1,48 @@
package startup
import (
"fmt"
"strings"
"ferrum/core"
win "ferrum/windows/facade"
)
func init() { core.Register(Module{}) }
type Module struct{}
func (Module) Name() string { return "startup" }
func (Module) Description() string {
return "Inspect Run keys and Startup folders for persistence surface"
}
func (Module) Run(ctx *core.Context) error {
ctx.Logger.Info("Enumerating startup persistence locations...")
entries, err := win.EnumerateStartupEntries()
if err != nil {
return err
}
if len(entries) == 0 {
ctx.Logger.Info("No startup entries found in common locations.")
return nil
}
for _, entry := range entries {
ctx.Logger.Success(fmt.Sprintf("%s\\%s > %s", entry.Scope, entry.Name, startupReason(entry.Command)))
ctx.Logger.Verbose(fmt.Sprintf("%s : %s", entry.Location, entry.Command))
}
return nil
}
func startupReason(command string) string {
lower := strings.ToLower(command)
switch {
case strings.Contains(lower, `\users\`) || strings.Contains(lower, `\temp\`) || strings.Contains(lower, `\programdata\`):
return "user-writable-looking command"
case strings.Contains(lower, "powershell") || strings.Contains(lower, "wscript") || strings.Contains(lower, "cscript") || strings.Contains(lower, "cmd.exe"):
return "script interpreter startup"
default:
return "startup entry"
}
}
+134
View File
@@ -0,0 +1,134 @@
package tokens
import (
"fmt"
"runtime"
"sort"
"strings"
"sync"
"ferrum/core"
"ferrum/internal"
win "ferrum/windows/facade"
)
func init() { core.Register(Module{}) }
type Module struct{}
func (Module) Name() string { return "tokens" }
func (Module) Description() string {
return "Hunt process tokens with sensitive privileges and elevated integrity"
}
func (Module) Run(ctx *core.Context) error {
ctx.Logger.Info("Enumerating process tokens and sensitive privileges...")
processes, err := win.EnumerateProcesses()
if err != nil {
return err
}
findings := inspect(processes)
sort.Slice(findings, func(i, j int) bool {
if findings[i].Score != findings[j].Score {
return findings[i].Score > findings[j].Score
}
return findings[i].Name < findings[j].Name
})
reported := 0
for _, finding := range findings {
if finding.Score == 0 {
continue
}
reported++
ctx.Logger.Success(fmt.Sprintf("%s[%d] > %s", finding.Name, finding.PID, strings.Join(finding.Reasons, ", ")))
ctx.Logger.Verbose(fmt.Sprintf("%s[%d] : %s privileges=%s", finding.Name, finding.PID, finding.Process.Label(), strings.Join(internal.Limit(finding.Privileges, 12), ",")))
}
if reported == 0 {
ctx.Logger.Info("No accessible process tokens matched the sensitive privilege heuristics.")
}
ctx.Logger.Verbose(fmt.Sprintf("Processes inspected: %d", len(processes)))
return nil
}
type tokenFinding struct {
win.Process
Reasons []string
Score int
}
func inspect(processes []win.Process) []tokenFinding {
workers := runtime.NumCPU()
if workers > 16 {
workers = 16
}
jobs := make(chan win.Process)
results := make(chan tokenFinding, len(processes))
var wg sync.WaitGroup
for i := 0; i < workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for process := range jobs {
token, err := win.InspectProcessToken(process.PID)
finding := tokenFinding{Process: process}
if err == nil {
finding.User = token.User
finding.Integrity = token.Integrity
finding.Elevated = token.Elevated
finding.Privileges = token.Privileges
finding.Reasons, finding.Score = tokenReasons(token)
}
results <- finding
}
}()
}
go func() {
for _, process := range processes {
jobs <- process
}
close(jobs)
wg.Wait()
close(results)
}()
findings := make([]tokenFinding, 0, len(processes))
for finding := range results {
findings = append(findings, finding)
}
return findings
}
func tokenReasons(token win.TokenInfo) ([]string, int) {
score := 0
reasons := []string{}
if token.Elevated {
reasons = append(reasons, "elevated token")
score += 20
}
if token.Integrity == "System" || token.Integrity == "High" {
reasons = append(reasons, token.Integrity+" integrity")
score += 15
}
interesting := map[string]int{
"SeDebugPrivilege": 40,
"SeImpersonatePrivilege": 35,
"SeAssignPrimaryTokenPrivilege": 35,
"SeTcbPrivilege": 45,
"SeBackupPrivilege": 25,
"SeRestorePrivilege": 25,
"SeLoadDriverPrivilege": 35,
"SeCreateTokenPrivilege": 45,
"SeTakeOwnershipPrivilege": 20,
"SeManageVolumePrivilege": 20,
"SeTrustedCredManAccessPrivilege": 40,
"SeRelabelPrivilege": 30,
"SeCreateGlobalPrivilege": 10,
}
for _, privilege := range token.Privileges {
if points, ok := interesting[privilege]; ok {
reasons = append(reasons, privilege)
score += points
}
}
return internal.Limit(reasons, 8), score
}