mirror of
https://github.com/kernelstub/Ferrum
synced 2026-06-21 13:55:09 +00:00
commit: add source
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 ""
|
||||
}
|
||||
}
|
||||
Vendored
+52
@@ -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 ""
|
||||
}
|
||||
}
|
||||
@@ -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, ", ")
|
||||
}
|
||||
@@ -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 ""
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 ""
|
||||
}
|
||||
}
|
||||
@@ -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, `"`)
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user