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
+109
View File
@@ -0,0 +1,109 @@
# FERRUM
Ferrum is a Windows-first vulnerability research and security auditing framework written in Go. It is designed as a single binary, `ferrum.exe`, with modules registered through a small core interface.
## Build
```sh
GOOS=windows GOARCH=amd64 go build -o ferrum.exe ./cmd
```
Or use the included script:
```powershell
.\scripts\build-windows.ps1
```
From Linux/macOS:
```sh
./scripts/build-windows.sh
```
## Usage
```cmd
ferrum.exe --HELP
ferrum.exe --ALL --VERBOSE
ferrum.exe --ALL --OUTPUT ferrum-reports
ferrum.exe --CLSID
ferrum.exe --CLSID --OUTPUT clsid.txt
ferrum.exe --TOKENS
ferrum.exe --REGISTRY
ferrum.exe --POLICY
ferrum.exe --DLLSEARCH
ferrum.exe --SERVICES
ferrum.exe --DRIVERS
ferrum.exe --PIPES
ferrum.exe --STARTUP
ferrum.exe --SCHEDULED
ferrum.exe --ENV
ferrum.exe --MITIGATIONS
ferrum.exe --AUTORUNS
ferrum.exe --IFEO
ferrum.exe --SILENTEXIT
ferrum.exe --WINLOGON
ferrum.exe --LSA
ferrum.exe --APPINIT
ferrum.exe --APPCERT
ferrum.exe --UAC
ferrum.exe --INSTALLER
ferrum.exe --POWERSHELL
ferrum.exe --APPLOCKER
ferrum.exe --WDAC
ferrum.exe --DEFENDER
ferrum.exe --FIREWALL
ferrum.exe --RDP
ferrum.exe --WMI
ferrum.exe --HOSTS
ferrum.exe --SHARES
ferrum.exe --SHELL
ferrum.exe --BROWSER
ferrum.exe --PROTOCOLS
ferrum.exe --COMLOCAL
ferrum.exe --KNOWNDLLS
ferrum.exe --SVCPATHS
ferrum.exe --DRIVERPATHS
ferrum.exe --CERTIFICATES
ferrum.exe --NETWORKPROVIDERS
ferrum.exe --PRINT
ferrum.exe --WINSOCK
ferrum.exe --ACCESSIBILITY
ferrum.exe --CLSID --VERBOSE
ferrum.exe --CLSID --QUIET
```
## Architecture
- `cmd/` contains the CLI entry point.
- `core/` contains module registration, context, and banner code.
- `modules/` contains research modules. New modules implement `core.Module` and call `core.Register`.
- `windows/` contains build-tagged Windows API wrappers and non-Windows stubs.
- `output/` contains console logging.
## Output
Write a single module report:
```cmd
ferrum.exe --CLSID --OUTPUT clsid.txt
```
Run every module and write one file per module:
```cmd
ferrum.exe --ALL
ferrum.exe --ALL --OUTPUT ferrum-reports
```
Without `--OUTPUT`, `--ALL` creates a timestamped folder such as `ferrum-output-20260613-153000`.
## CLSID ProcMon Filter Model
`--CLSID` models this ProcMon workflow for COM hijack/LPE triage:
- `User is NT AUTHORITY\SYSTEM`
- `Path contains HKCU\Software\Classes`
- `Path contains InprocServer32`
- `Path contains LocalServer32`
- `Result is NAME NOT FOUND`
+335
View File
@@ -0,0 +1,335 @@
package main
import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"time"
"ferrum/core"
_ "ferrum/modules/advanced"
_ "ferrum/modules/clsid"
_ "ferrum/modules/dllsearch"
_ "ferrum/modules/drivers"
_ "ferrum/modules/env"
_ "ferrum/modules/mitigations"
_ "ferrum/modules/pipes"
_ "ferrum/modules/policy"
_ "ferrum/modules/registry"
_ "ferrum/modules/scheduled"
_ "ferrum/modules/services"
_ "ferrum/modules/startup"
_ "ferrum/modules/tokens"
"ferrum/output"
)
const build = "Development"
// Module packages register these command flags through core.Register during init.
// Flags are parsed case-insensitively and displayed in uppercase by --HELP.
var registeredModuleFlags = []string{
"--ACCESSIBILITY",
"--ACCESSTOKENS",
"--ACL",
"--ACTIVATIONCTX",
"--ALPC",
"--APPCERT",
"--APPINIT",
"--APPLOCKER",
"--APPCONTAINERBROKERS",
"--AUTORUNS",
"--AUTHPACKAGES",
"--BITS",
"--BROKERS",
"--BROWSER",
"--CERTIFICATES",
"--CLSID",
"--CLIPBOARD",
"--CLOUDAP",
"--COMDCOM",
"--COMELEVATION",
"--COMHIJACK",
"--COMLOCAL",
"--CONFUSEDDEPUTY",
"--CREDPROVIDERS",
"--CSRSS",
"--CUSTOMMARSHAL",
"--DCOMACTIVATION",
"--DDE",
"--DEFENDER",
"--DEVICEOBJECTS",
"--DLLHIJACKING",
"--DLLSEARCH",
"--DRAGDROP",
"--DRIVERPATHS",
"--DRIVERS",
"--EFSRPC",
"--ENDPOINTMAPPER",
"--ENV",
"--ENVINJECTION",
"--ETW",
"--EXPLOREREXT",
"--FIREWALL",
"--HANDLES",
"--HARDLINKS",
"--HOSTS",
"--HYPERV",
"--IFEO",
"--INSTALLER",
"--INSTALLERREPAIR",
"--IOCTLS",
"--JOBOBJECTS",
"--JUNCTIONS",
"--KNOWNDLLS",
"--LPC",
"--LSA",
"--LSAPLUGINS",
"--LSASSINTERFACES",
"--MITIGATIONS",
"--MINIFILTERS",
"--MMAP",
"--MOUNTPOINTS",
"--MSI",
"--NETWORKPROVIDERS",
"--NTOBJMGR",
"--OBJDIRS",
"--OLE",
"--OPLOCKS",
"--PATHCANON",
"--PIPES",
"--POLICY",
"--POWERSHELL",
"--PPL",
"--PREVIEWHANDLERS",
"--PRINT",
"--PROPERTYHANDLERS",
"--PROTOCOLS",
"--RDP",
"--RECOVERY",
"--REGSYMLINKS",
"--REGISTRY",
"--REPARSEPOINTS",
"--RPC",
"--SCHEDULED",
"--SCM",
"--SEARCHPOISON",
"--SECTIONOBJECTS",
"--SERVICES",
"--SESSIONISOLATION",
"--SHARES",
"--SHAREDMEMORY",
"--SHELL",
"--SILENTEXIT",
"--SMBIPC",
"--STARTUP",
"--SVCPATHS",
"--SXS",
"--SYMLINKS",
"--TASKRPC",
"--TEMPFILES",
"--THUMBNAILPROVIDERS",
"--TOKENS",
"--TOKENIMPERSONATION",
"--TOCTOU",
"--UAC",
"--UACAUTOELEVATION",
"--UPDATES",
"--URLMONIKERS",
"--USERPROFILESVC",
"--WDAC",
"--WDACPOLICY",
"--WINLOGON",
"--WIN32K",
"--WINRM",
"--WINSOCK",
"--WFP",
"--WINDOWSTATIONS",
"--WMI",
"--WSL",
}
func main() {
fmt.Print(core.Banner(build))
modules := core.Modules()
flags, err := parseArgs(os.Args[1:], modules)
logger := output.NewConsoleLogger(os.Stdout, flags.Verbose, flags.Quiet)
if err != nil {
logger.Error(err.Error())
printHelp(modules)
os.Exit(2)
}
if flags.Help || len(flags.Selected) == 0 {
printHelp(modules)
return
}
if flags.RunAll {
if err := runAllToDirectory(flags, modules); err != nil {
logger.Error(err.Error())
os.Exit(1)
}
return
}
closeOutput, logger, err := outputLogger(flags)
if err != nil {
logger.Error(err.Error())
os.Exit(1)
}
defer closeOutput()
ctx := core.NewContext(logger, build)
for _, module := range flags.Selected {
if err := module.Run(ctx); err != nil {
logger.Error(fmt.Sprintf("%s: %v", module.Name(), err))
}
}
}
type cliFlags struct {
Help bool
Quiet bool
Verbose bool
RunAll bool
OutputPath string
Selected []core.Module
}
func parseArgs(args []string, modules []core.Module) (cliFlags, error) {
var flags cliFlags
byFlag := make(map[string]core.Module, len(modules))
for _, module := range modules {
byFlag["--"+module.Name()] = module
}
for i := 0; i < len(args); i++ {
arg := args[i]
normalized := strings.ToLower(arg)
switch {
case normalized == "--all":
flags.RunAll = true
flags.Selected = append(flags.Selected, modules...)
case normalized == "--output":
if i+1 >= len(args) {
return flags, fmt.Errorf("--OUTPUT requires a file path")
}
i++
flags.OutputPath = args[i]
case strings.HasPrefix(normalized, "--output="):
flags.OutputPath = arg[len("--output="):]
if flags.OutputPath == "" {
return flags, fmt.Errorf("--OUTPUT requires a file path")
}
case normalized == "--help" || normalized == "-h" || normalized == "/?":
flags.Help = true
case normalized == "--verbose" || normalized == "-v":
flags.Verbose = true
case normalized == "--quiet" || normalized == "-q":
flags.Quiet = true
default:
module, ok := byFlag[normalized]
if !ok {
return flags, fmt.Errorf("unknown option: %s", arg)
}
flags.Selected = append(flags.Selected, module)
}
}
if flags.Quiet {
flags.Verbose = false
}
return flags, nil
}
func outputLogger(flags cliFlags) (func(), *output.ConsoleLogger, error) {
if flags.OutputPath == "" {
return func() {}, output.NewConsoleLogger(os.Stdout, flags.Verbose, flags.Quiet), nil
}
if err := ensureParent(flags.OutputPath); err != nil {
return nil, nil, err
}
file, err := os.Create(flags.OutputPath)
if err != nil {
return nil, nil, err
}
return func() { _ = file.Close() }, output.NewDualLogger(os.Stdout, file, flags.Verbose, flags.Quiet), nil
}
func runAllToDirectory(flags cliFlags, modules []core.Module) error {
dir := flags.OutputPath
if dir == "" {
dir = fmt.Sprintf("ferrum-output-%s", time.Now().Format("20060102-150405"))
}
if err := os.MkdirAll(dir, 0755); err != nil {
return err
}
console := output.NewConsoleLogger(os.Stdout, flags.Verbose, flags.Quiet)
console.Info("Writing per-module reports to " + dir)
for _, module := range modules {
path := filepath.Join(dir, strings.ToUpper(module.Name())+".txt")
file, err := os.Create(path)
if err != nil {
console.Error(fmt.Sprintf("%s: %v", module.Name(), err))
continue
}
logger := output.NewDualLogger(os.Stdout, file, flags.Verbose, flags.Quiet)
ctx := core.NewContext(logger, build)
logger.Info("Module: " + strings.ToUpper(module.Name()))
if err := module.Run(ctx); err != nil {
logger.Error(fmt.Sprintf("%s: %v", module.Name(), err))
}
_ = file.Close()
}
console.Info("Completed --ALL report directory: " + dir)
return nil
}
func ensureParent(path string) error {
parent := filepath.Dir(path)
if parent == "." || parent == "" {
return nil
}
return os.MkdirAll(parent, 0755)
}
func printHelp(modules []core.Module) {
fmt.Println("Usage:")
fmt.Println(" ferrum.exe [module] [options]")
fmt.Println(" ferrum.exe --ALL --VERBOSE")
fmt.Println(" ferrum.exe --CLSID --OUTPUT clsid.txt")
fmt.Println(" ferrum.exe --ALL --OUTPUT ferrum-reports")
fmt.Println()
fmt.Println("Modules:")
sort.Slice(modules, func(i, j int) bool {
return modules[i].Name() < modules[j].Name()
})
width := 0
for _, module := range modules {
flag := displayFlag(module.Name())
if len(flag) > width {
width = len(flag)
}
}
for _, module := range modules {
fmt.Printf(" %-*s %s\n", width+2, displayFlag(module.Name()), module.Description())
}
fmt.Println()
fmt.Println("Options:")
fmt.Println(" --VERBOSE Include additional context")
fmt.Println(" --QUIET Suppress banner and informational output")
fmt.Println(" --ALL Run every registered module")
fmt.Println(" --OUTPUT FILE Write a report file; with --ALL, use/create a report folder")
fmt.Println(" --HELP Show this help")
}
func displayFlag(name string) string {
return "--" + strings.ToUpper(name)
}
+23
View File
@@ -0,0 +1,23 @@
package core
import "fmt"
const banner = `
_____ _____ _____ _____ _____ _____
| __| __| __ | __ | | | |
| __| __| -| -| | | | | |
|__| |_____|__|__|__|__|_____|_|_|_|
Ferrum Windows Vulnerability Research Framework
Build : %s
Author: Kernelstub
#########################################
`
func Banner(build string) string {
return fmt.Sprintf(banner, build)
}
+17
View File
@@ -0,0 +1,17 @@
package core
type Logger interface {
Info(message string)
Success(message string)
Error(message string)
Verbose(message string)
}
type Context struct {
Logger Logger
Build string
}
func NewContext(logger Logger, build string) *Context {
return &Context{Logger: logger, Build: build}
}
+47
View File
@@ -0,0 +1,47 @@
package core
import (
"fmt"
"sort"
"strings"
"sync"
)
type Module interface {
Name() string
Description() string
Run(ctx *Context) error
}
var (
mu sync.RWMutex
registry = make(map[string]Module)
)
func Register(module Module) {
name := strings.ToLower(strings.TrimSpace(module.Name()))
if name == "" {
panic("module name cannot be empty")
}
mu.Lock()
defer mu.Unlock()
if _, exists := registry[name]; exists {
panic(fmt.Sprintf("module already registered: %s", name))
}
registry[name] = module
}
func Modules() []Module {
mu.RLock()
defer mu.RUnlock()
modules := make([]Module, 0, len(registry))
for _, module := range registry {
modules = append(modules, module)
}
sort.Slice(modules, func(i, j int) bool {
return modules[i].Name() < modules[j].Name()
})
return modules
}
+3
View File
@@ -0,0 +1,3 @@
module ferrum
go 1.26
+8
View File
@@ -0,0 +1,8 @@
package internal
func Limit[T any](items []T, max int) []T {
if max <= 0 || len(items) <= max {
return items
}
return items[:max]
}
+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
}
+53
View File
@@ -0,0 +1,53 @@
package output
import (
"fmt"
"io"
"sync"
)
type ConsoleLogger struct {
out io.Writer
artifact io.Writer
verbose bool
quiet bool
mu sync.Mutex
}
func NewConsoleLogger(out io.Writer, verbose, quiet bool) *ConsoleLogger {
return &ConsoleLogger{out: out, verbose: verbose, quiet: quiet}
}
func NewDualLogger(out, artifact io.Writer, verbose, quiet bool) *ConsoleLogger {
return &ConsoleLogger{out: out, artifact: artifact, verbose: verbose, quiet: quiet}
}
func (l *ConsoleLogger) Info(message string) {
l.printf(!l.quiet, "[*] %s\n", message)
}
func (l *ConsoleLogger) Success(message string) {
l.printf(!l.quiet, "[+] %s\n", message)
}
func (l *ConsoleLogger) Error(message string) {
l.printf(true, "[-] %s\n", message)
}
func (l *ConsoleLogger) Verbose(message string) {
if !l.verbose {
return
}
l.printf(!l.quiet, "[v] %s\n", message)
}
func (l *ConsoleLogger) printf(writeConsole bool, format string, args ...any) {
l.mu.Lock()
defer l.mu.Unlock()
if writeConsole {
fmt.Fprintf(l.out, format, args...)
}
if l.artifact != nil {
fmt.Fprintf(l.artifact, format, args...)
}
}
+13
View File
@@ -0,0 +1,13 @@
param(
[string]$Out = "ferrum.exe"
)
$ErrorActionPreference = "Stop"
$env:GOOS = "windows"
$env:GOARCH = "amd64"
$env:CGO_ENABLED = "0"
Write-Host "[*] Building Ferrum for Windows x64..."
go build -trimpath -ldflags "-s -w" -o $Out ./cmd
Write-Host "[+] Built $Out"
+8
View File
@@ -0,0 +1,8 @@
#!/usr/bin/env sh
set -eu
OUT="${1:-ferrum.exe}"
echo "[*] Building Ferrum for Windows x64..."
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -o "$OUT" ./cmd
echo "[+] Built $OUT"
+20
View File
@@ -0,0 +1,20 @@
# Windows Package Layout
The `ferrum/windows/facade` package is the compatibility layer used by modules. The
implementation lives in real subpackages under this directory.
Modules import `ferrum/windows/facade`, while internal code is organized by Windows
research area. New low-level collectors should usually be added to the relevant
subpackage and re-exported from `facade/` only when modules need it.
The package is organized by Windows research area:
- `advanced/`: advanced scanner dispatcher, built-ins, profiles, and helpers.
- `audit/`: registry/policy/DLL-search audit collectors.
- `env/`, `pipes/`, `scheduled/`, `startup/`: focused collectors.
- `facade/`: module-facing compatibility API.
- `process/`: process enumeration and non-Windows stubs.
- `registry/`: registry helpers plus CLSID/COM registry enumeration.
- `services/`: Service Control Manager, service, and driver inventory.
- `token/`: access token, integrity, elevation, and privilege helpers.
- `types/`: grouped data models shared by modules.
+88
View File
@@ -0,0 +1,88 @@
//go:build windows
package advanced
import (
"os"
"path/filepath"
"ferrum/windows/registry"
wintypes "ferrum/windows/types"
)
const advancedLimit = 120
type AdvancedFinding = wintypes.AdvancedFinding
func EnumerateAdvancedFindings(check string) ([]AdvancedFinding, error) {
var findings []AdvancedFinding
switch check {
case "autoruns":
findings = autorunFindings()
case "ifeo":
findings = ifeoFindings()
case "silentexit":
findings = silentExitFindings()
case "winlogon":
findings = registryNamedValues("Winlogon", registry.HkeyLocalMachine, "HKLM", `SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon`, []string{"Shell", "Userinit", "Notify", "GinaDLL"}, "High", "logon execution surface")
case "lsa":
findings = registryNamedValues("LSA", registry.HkeyLocalMachine, "HKLM", `SYSTEM\CurrentControlSet\Control\Lsa`, []string{"Authentication Packages", "Notification Packages", "Security Packages", "RunAsPPL", "LsaCfgFlags"}, "High", "LSA package or protection setting")
case "appinit":
findings = registryNamedValues("AppInit", registry.HkeyLocalMachine, "HKLM", `SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows`, []string{"LoadAppInit_DLLs", "AppInit_DLLs", "RequireSignedAppInit_DLLs"}, "High", "AppInit DLL load surface")
case "appcert":
findings = registryAllValues("AppCert", registry.HkeyLocalMachine, "HKLM", `SYSTEM\CurrentControlSet\Control\Session Manager\AppCertDLLs`, "High", "AppCert DLL process creation hook")
case "uac":
findings = registryNamedValues("UAC", registry.HkeyLocalMachine, "HKLM", `SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System`, []string{"EnableLUA", "ConsentPromptBehaviorAdmin", "ConsentPromptBehaviorUser", "LocalAccountTokenFilterPolicy", "FilterAdministratorToken"}, "Medium", "UAC policy value")
case "installer":
findings = append(findings, registryNamedValues("Installer", registry.HkeyCurrentUser, "HKCU", `Software\Policies\Microsoft\Windows\Installer`, []string{"AlwaysInstallElevated", "DisableMSI"}, "High", "per-user installer elevation policy")...)
findings = append(findings, registryNamedValues("Installer", registry.HkeyLocalMachine, "HKLM", `Software\Policies\Microsoft\Windows\Installer`, []string{"AlwaysInstallElevated", "DisableMSI"}, "High", "machine installer elevation policy")...)
case "powershell":
findings = powershellFindings()
case "applocker":
findings = registrySubkeys("AppLocker", registry.HkeyLocalMachine, "HKLM", `SOFTWARE\Policies\Microsoft\Windows\SrpV2`, "Info", "AppLocker rule collection")
findings = append(findings, registrySubkeys("SRP", registry.HkeyLocalMachine, "HKLM", `SOFTWARE\Policies\Microsoft\Windows\Safer\CodeIdentifiers`, "Info", "Software Restriction Policy surface")...)
case "wdac":
findings = registrySubkeys("WDAC", registry.HkeyLocalMachine, "HKLM", `SYSTEM\CurrentControlSet\Control\CI\Policy`, "Info", "code integrity policy key")
findings = append(findings, fileGlobFindings("WDAC", filepath.Join(os.Getenv("WINDIR"), `System32\CodeIntegrity`, "*.cip"), "Info", "WDAC policy file")...)
case "defender":
findings = defenderFindings()
case "firewall":
findings = firewallFindings()
case "rdp":
findings = rdpFindings()
case "wmi":
findings = wmiFindings()
case "hosts":
findings = hostsFindings()
case "shares":
findings = registryAllValues("Shares", registry.HkeyLocalMachine, "HKLM", `SYSTEM\CurrentControlSet\Services\LanmanServer\Shares`, "Medium", "configured SMB share")
case "shell":
findings = shellFindings()
case "browser":
findings = browserFindings()
case "protocols":
findings = protocolFindings()
case "comlocal":
findings = comLocalFindings()
case "knowndlls":
findings = registryAllValues("KnownDLLs", registry.HkeyLocalMachine, "HKLM", `SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs`, "Info", "KnownDLL protected load name")
case "svcpaths":
findings = servicePathFindings()
case "driverpaths":
findings = driverPathFindings()
case "certificates":
findings = certificateFindings()
case "networkproviders":
findings = networkProviderFindings()
case "print":
findings = printFindings()
case "winsock":
findings = winsockFindings()
case "accessibility":
findings = accessibilityFindings()
default:
findings = genericSurfaceFindings(check)
}
sortAdvanced(findings)
return limitAdvanced(findings), nil
}
+255
View File
@@ -0,0 +1,255 @@
//go:build windows
package advanced
import (
"fmt"
"os"
"path/filepath"
"strings"
"ferrum/windows/registry"
"ferrum/windows/services"
)
func autorunFindings() []AdvancedFinding {
checks := []struct {
scope uintptr
root string
path string
}{
{registry.HkeyCurrentUser, "HKCU", `Software\Microsoft\Windows\CurrentVersion\Run`},
{registry.HkeyCurrentUser, "HKCU", `Software\Microsoft\Windows\CurrentVersion\RunOnce`},
{registry.HkeyCurrentUser, "HKCU", `Software\Microsoft\Windows\CurrentVersion\RunServices`},
{registry.HkeyLocalMachine, "HKLM", `Software\Microsoft\Windows\CurrentVersion\Run`},
{registry.HkeyLocalMachine, "HKLM", `Software\Microsoft\Windows\CurrentVersion\RunOnce`},
{registry.HkeyLocalMachine, "HKLM", `Software\Microsoft\Windows\CurrentVersion\RunServices`},
{registry.HkeyLocalMachine, "HKLM", `Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Run`},
}
findings := []AdvancedFinding{}
for _, check := range checks {
findings = append(findings, registryAllValues("Autoruns", check.scope, check.root, check.path, "Medium", "autorun execution entry")...)
}
return findings
}
func ifeoFindings() []AdvancedFinding {
findings := []AdvancedFinding{}
for _, root := range []struct {
scope uintptr
name string
}{{registry.HkeyLocalMachine, "HKLM"}, {registry.HkeyCurrentUser, "HKCU"}} {
base := `SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options`
for _, key := range subkeys(root.scope, base) {
findings = append(findings, registryNamedValues("IFEO", root.scope, root.name, base+`\`+key, []string{"Debugger", "VerifierDlls", "GlobalFlag", "MitigationOptions"}, "High", "process interception or verifier setting")...)
}
}
return findings
}
func silentExitFindings() []AdvancedFinding {
findings := []AdvancedFinding{}
base := `SOFTWARE\Microsoft\Windows NT\CurrentVersion\SilentProcessExit`
for _, key := range subkeys(registry.HkeyLocalMachine, base) {
findings = append(findings, registryNamedValues("SilentProcessExit", registry.HkeyLocalMachine, "HKLM", base+`\`+key, []string{"MonitorProcess", "ReportingMode", "LocalDumpFolder"}, "High", "process-exit monitor or dump surface")...)
}
return findings
}
func powershellFindings() []AdvancedFinding {
findings := []AdvancedFinding{}
for _, root := range []struct {
scope uintptr
name string
}{{registry.HkeyLocalMachine, "HKLM"}, {registry.HkeyCurrentUser, "HKCU"}} {
findings = append(findings, registryAllValues("PowerShell", root.scope, root.name, `SOFTWARE\Policies\Microsoft\Windows\PowerShell`, "Medium", "PowerShell policy value")...)
findings = append(findings, registryAllValues("PowerShell", root.scope, root.name, `SOFTWARE\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell`, "Medium", "PowerShell shell policy value")...)
}
profiles := []string{
filepath.Join(os.Getenv("USERPROFILE"), `Documents\WindowsPowerShell\profile.ps1`),
filepath.Join(os.Getenv("USERPROFILE"), `Documents\PowerShell\profile.ps1`),
filepath.Join(os.Getenv("WINDIR"), `System32\WindowsPowerShell\v1.0\profile.ps1`),
}
for _, profile := range profiles {
if exists(profile) {
findings = append(findings, AdvancedFinding{Area: "PowerShell", Target: profile, Name: "profile", Severity: "Medium", Reason: "PowerShell profile script present"})
}
}
return findings
}
func defenderFindings() []AdvancedFinding {
findings := registryAllValues("Defender", registry.HkeyLocalMachine, "HKLM", `SOFTWARE\Policies\Microsoft\Windows Defender`, "High", "Defender policy value")
findings = append(findings, registryAllValues("DefenderExclusions", registry.HkeyLocalMachine, "HKLM", `SOFTWARE\Microsoft\Windows Defender\Exclusions\Paths`, "Medium", "Defender path exclusion")...)
findings = append(findings, registryAllValues("DefenderExclusions", registry.HkeyLocalMachine, "HKLM", `SOFTWARE\Microsoft\Windows Defender\Exclusions\Processes`, "Medium", "Defender process exclusion")...)
findings = append(findings, registryAllValues("DefenderFeatures", registry.HkeyLocalMachine, "HKLM", `SOFTWARE\Microsoft\Windows Defender\Features`, "Info", "Defender feature value")...)
return findings
}
func firewallFindings() []AdvancedFinding {
base := `SYSTEM\CurrentControlSet\Services\SharedAccess\Parameters\FirewallPolicy`
profiles := []string{"DomainProfile", "PublicProfile", "StandardProfile"}
findings := []AdvancedFinding{}
for _, profile := range profiles {
findings = append(findings, registryNamedValues("Firewall", registry.HkeyLocalMachine, "HKLM", base+`\`+profile, []string{"EnableFirewall", "DefaultInboundAction", "DefaultOutboundAction", "DisableNotifications"}, "Medium", profile+" firewall policy")...)
}
return findings
}
func rdpFindings() []AdvancedFinding {
findings := registryNamedValues("RDP", registry.HkeyLocalMachine, "HKLM", `SYSTEM\CurrentControlSet\Control\Terminal Server`, []string{"fDenyTSConnections", "fSingleSessionPerUser"}, "Medium", "Remote Desktop terminal server policy")
findings = append(findings, registryNamedValues("RDP", registry.HkeyLocalMachine, "HKLM", `SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp`, []string{"UserAuthentication", "SecurityLayer", "PortNumber"}, "Medium", "RDP listener policy")...)
return findings
}
func wmiFindings() []AdvancedFinding {
findings := fileGlobFindings("WMI", filepath.Join(os.Getenv("WINDIR"), `System32\wbem\AutoRecover`, "*.mof"), "Medium", "WMI AutoRecover MOF")
findings = append(findings, registryAllValues("WMI", registry.HkeyLocalMachine, "HKLM", `SOFTWARE\Microsoft\WBEM\CIMOM`, "Info", "WMI CIMOM configuration")...)
return findings
}
func hostsFindings() []AdvancedFinding {
path := filepath.Join(os.Getenv("WINDIR"), `System32\drivers\etc\hosts`)
data, err := os.ReadFile(path)
if err != nil {
return nil
}
findings := []AdvancedFinding{}
for _, line := range strings.Split(string(data), "\n") {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
findings = append(findings, AdvancedFinding{Area: "Hosts", Target: path, Name: "entry", Value: line, Severity: "Medium", Reason: "hosts file override"})
}
return findings
}
func shellFindings() []AdvancedFinding {
findings := registryAllValues("Shell", registry.HkeyLocalMachine, "HKLM", `SOFTWARE\Microsoft\Windows\CurrentVersion\ShellServiceObjectDelayLoad`, "Medium", "shell service object delay-load entry")
findings = append(findings, registryAllValues("Shell", registry.HkeyCurrentUser, "HKCU", `SOFTWARE\Microsoft\Windows\CurrentVersion\ShellServiceObjectDelayLoad`, "Medium", "per-user shell service object delay-load entry")...)
findings = append(findings, registrySubkeys("ShellExtensions", registry.HkeyLocalMachine, "HKLM", `SOFTWARE\Microsoft\Windows\CurrentVersion\Shell Extensions\Approved`, "Info", "approved shell extension")...)
findings = append(findings, registryAllValues("ExplorerPolicies", registry.HkeyCurrentUser, "HKCU", `SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer`, "Medium", "Explorer policy value")...)
return findings
}
func browserFindings() []AdvancedFinding {
findings := registrySubkeys("Browser", registry.HkeyLocalMachine, "HKLM", `SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Browser Helper Objects`, "Medium", "Browser Helper Object")
findings = append(findings, registrySubkeys("Browser", registry.HkeyCurrentUser, "HKCU", `SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Browser Helper Objects`, "Medium", "per-user Browser Helper Object")...)
findings = append(findings, registrySubkeys("Browser", registry.HkeyCurrentUser, "HKCU", `SOFTWARE\Google\Chrome\NativeMessagingHosts`, "Medium", "Chrome native messaging host")...)
findings = append(findings, registrySubkeys("Browser", registry.HkeyLocalMachine, "HKLM", `SOFTWARE\Google\Chrome\NativeMessagingHosts`, "Medium", "machine Chrome native messaging host")...)
findings = append(findings, registrySubkeys("Browser", registry.HkeyCurrentUser, "HKCU", `SOFTWARE\Microsoft\Edge\NativeMessagingHosts`, "Medium", "Edge native messaging host")...)
return findings
}
func protocolFindings() []AdvancedFinding {
findings := []AdvancedFinding{}
for _, root := range []struct {
scope uintptr
name string
}{{registry.HkeyCurrentUser, "HKCU"}, {registry.HkeyLocalMachine, "HKLM"}} {
base := `Software\Classes`
for _, key := range subkeys(root.scope, base) {
values, err := registry.Values(root.scope, base+`\`+key)
if err != nil {
continue
}
for _, value := range values {
if strings.EqualFold(value.Name, "URL Protocol") {
findings = append(findings, AdvancedFinding{Area: "Protocols", Target: root.name + `\` + base + `\` + key, Name: value.Name, Value: value.Value, Severity: "Medium", Reason: "custom URL protocol handler"})
}
}
if len(findings) >= advancedLimit {
return findings
}
}
}
return findings
}
func comLocalFindings() []AdvancedFinding {
findings := []AdvancedFinding{}
for _, entry := range mustCLSID() {
if entry.Kind == "InprocServer32" || entry.Kind == "LocalServer32" {
findings = append(findings, AdvancedFinding{Area: "COM", Target: `HKCU\Software\Classes\CLSID\` + entry.CLSID, Name: entry.Kind, Value: entry.Value, Severity: "Medium", Reason: "per-user COM server registration"})
}
}
return findings
}
func servicePathFindings() []AdvancedFinding {
services, err := services.EnumerateServices()
if err != nil {
return nil
}
findings := []AdvancedFinding{}
for _, service := range services {
if reason := pathRisk(service.BinaryPath); reason != "" {
findings = append(findings, AdvancedFinding{Area: "Services", Target: service.Name, Name: service.StartType, Value: service.BinaryPath, Severity: "High", Reason: reason})
}
}
return findings
}
func driverPathFindings() []AdvancedFinding {
drivers, err := services.EnumerateDrivers()
if err != nil {
return nil
}
findings := []AdvancedFinding{}
for _, driver := range drivers {
if reason := pathRisk(driver.BinaryPath); reason != "" {
findings = append(findings, AdvancedFinding{Area: "Drivers", Target: driver.Name, Name: driver.StartType, Value: driver.BinaryPath, Severity: "High", Reason: reason})
}
}
return findings
}
func certificateFindings() []AdvancedFinding {
checks := []struct {
scope uintptr
root string
path string
}{
{registry.HkeyLocalMachine, "HKLM", `SOFTWARE\Microsoft\SystemCertificates\Root\Certificates`},
{registry.HkeyLocalMachine, "HKLM", `SOFTWARE\Microsoft\SystemCertificates\AuthRoot\Certificates`},
{registry.HkeyCurrentUser, "HKCU", `SOFTWARE\Microsoft\SystemCertificates\Root\Certificates`},
{registry.HkeyCurrentUser, "HKCU", `SOFTWARE\Microsoft\SystemCertificates\My\Certificates`},
}
findings := []AdvancedFinding{}
for _, check := range checks {
keys := subkeys(check.scope, check.path)
findings = append(findings, AdvancedFinding{Area: "Certificates", Target: check.root + `\` + check.path, Name: "count", Value: fmt.Sprintf("%d", len(keys)), Severity: "Info", Reason: "certificate store inventory"})
}
return findings
}
func networkProviderFindings() []AdvancedFinding {
findings := registryAllValues("NetworkProviders", registry.HkeyLocalMachine, "HKLM", `SYSTEM\CurrentControlSet\Control\NetworkProvider\Order`, "Medium", "network provider order")
findings = append(findings, registrySubkeys("CredentialProviders", registry.HkeyLocalMachine, "HKLM", `SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers`, "Medium", "credential provider")...)
findings = append(findings, registrySubkeys("CredentialProviderFilters", registry.HkeyLocalMachine, "HKLM", `SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Provider Filters`, "Medium", "credential provider filter")...)
return findings
}
func printFindings() []AdvancedFinding {
findings := registrySubkeys("Print", registry.HkeyLocalMachine, "HKLM", `SYSTEM\CurrentControlSet\Control\Print\Monitors`, "Medium", "print monitor load surface")
findings = append(findings, registrySubkeys("Print", registry.HkeyLocalMachine, "HKLM", `SYSTEM\CurrentControlSet\Control\Print\Providers`, "Medium", "print provider load surface")...)
findings = append(findings, registrySubkeys("Print", registry.HkeyLocalMachine, "HKLM", `SYSTEM\CurrentControlSet\Control\Print\Environments\Windows x64\Print Processors`, "Medium", "print processor load surface")...)
return findings
}
func winsockFindings() []AdvancedFinding {
findings := registryAllValues("Winsock", registry.HkeyLocalMachine, "HKLM", `SYSTEM\CurrentControlSet\Services\WinSock2\Parameters\Protocol_Catalog9`, "Info", "Winsock protocol catalog")
findings = append(findings, registryAllValues("Winsock", registry.HkeyLocalMachine, "HKLM", `SYSTEM\CurrentControlSet\Services\WinSock2\Parameters\NameSpace_Catalog5`, "Info", "Winsock namespace catalog")...)
return findings
}
func accessibilityFindings() []AdvancedFinding {
binaries := []string{"sethc.exe", "utilman.exe", "osk.exe", "magnify.exe", "narrator.exe", "displayswitch.exe", "atbroker.exe"}
base := `SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options`
findings := []AdvancedFinding{}
for _, binary := range binaries {
findings = append(findings, registryNamedValues("Accessibility", registry.HkeyLocalMachine, "HKLM", base+`\`+binary, []string{"Debugger", "VerifierDlls", "GlobalFlag"}, "High", "accessibility binary interception")...)
}
return findings
}
+155
View File
@@ -0,0 +1,155 @@
//go:build windows
package advanced
import (
"os"
"path/filepath"
"sort"
"strings"
"ferrum/windows/registry"
)
func registryNamedValues(area string, scope uintptr, root, path string, names []string, severity, reason string) []AdvancedFinding {
values, err := registry.Values(scope, path)
if err != nil {
return nil
}
findings := []AdvancedFinding{}
for _, value := range values {
if containsName(names, value.Name) {
findings = append(findings, AdvancedFinding{Area: area, Target: root + `\` + path, Name: displayName(value.Name), Value: value.Value, Severity: severity, Reason: reason})
}
}
return findings
}
func registryAllValues(area string, scope uintptr, root, path, severity, reason string) []AdvancedFinding {
values, err := registry.Values(scope, path)
if err != nil {
return nil
}
findings := []AdvancedFinding{}
for _, value := range values {
findings = append(findings, AdvancedFinding{Area: area, Target: root + `\` + path, Name: displayName(value.Name), Value: value.Value, Severity: severity, Reason: reason})
}
return findings
}
func registrySubkeys(area string, scope uintptr, root, path, severity, reason string) []AdvancedFinding {
keys := subkeys(scope, path)
findings := make([]AdvancedFinding, 0, len(keys))
for _, key := range keys {
findings = append(findings, AdvancedFinding{Area: area, Target: root + `\` + path + `\` + key, Name: key, Severity: severity, Reason: reason})
}
return findings
}
func subkeys(scope uintptr, path string) []string {
key, err := registry.OpenKey(scope, path)
if err != nil {
return nil
}
defer registry.CloseKey(key)
keys, err := registry.EnumSubkeys(key)
if err != nil {
return nil
}
return keys
}
func fileGlobFindings(area, pattern, severity, reason string) []AdvancedFinding {
matches, err := filepath.Glob(pattern)
if err != nil {
return nil
}
findings := []AdvancedFinding{}
for _, match := range matches {
findings = append(findings, AdvancedFinding{Area: area, Target: match, Name: filepath.Base(match), Severity: severity, Reason: reason})
}
return findings
}
func pathRisk(path string) string {
trimmed := strings.TrimSpace(path)
lower := strings.ToLower(trimmed)
switch {
case trimmed == "":
return ""
case strings.Contains(trimmed, " ") && !strings.HasPrefix(trimmed, `"`) && strings.Contains(lower, ".exe"):
return "unquoted executable path with spaces"
case strings.Contains(lower, `\users\`) || strings.Contains(lower, `\temp\`) || strings.Contains(lower, `\downloads\`):
return "user-writable-looking image path"
case strings.Contains(lower, `\programdata\`):
return "commonly writable image path"
default:
return ""
}
}
func exists(path string) bool {
if path == "" {
return false
}
_, err := os.Stat(path)
return err == nil
}
func mustCLSID() []registry.CLSIDEntry {
entries, err := registry.EnumerateHKCUCLSID()
if err != nil {
return nil
}
return entries
}
func displayName(name string) string {
if name == "" {
return "(Default)"
}
return name
}
func containsName(names []string, name string) bool {
for _, item := range names {
if strings.EqualFold(item, name) {
return true
}
}
return false
}
func severityRank(severity string) int {
switch severity {
case "High":
return 4
case "Medium":
return 3
case "Low":
return 2
case "Info":
return 1
default:
return 0
}
}
func sortAdvanced(findings []AdvancedFinding) {
sort.Slice(findings, func(i, j int) bool {
if findings[i].Severity != findings[j].Severity {
return severityRank(findings[i].Severity) > severityRank(findings[j].Severity)
}
if findings[i].Area != findings[j].Area {
return findings[i].Area < findings[j].Area
}
return findings[i].Target < findings[j].Target
})
}
func limitAdvanced(findings []AdvancedFinding) []AdvancedFinding {
if len(findings) <= advancedLimit {
return findings
}
return findings[:advancedLimit]
}
+234
View File
@@ -0,0 +1,234 @@
//go:build windows
package advanced
import (
"fmt"
"os"
"strings"
winprocess "ferrum/windows/process"
"ferrum/windows/registry"
"ferrum/windows/services"
)
type surfaceProfile struct {
area string
severity string
reason string
registry []registryProbe
globs []string
services []string
processes []string
}
type registryProbe struct {
scope uintptr
root string
path string
}
func genericSurfaceFindings(check string) []AdvancedFinding {
profile, ok := surfaceProfiles()[check]
if !ok {
return []AdvancedFinding{{
Area: strings.ToUpper(check),
Target: check,
Severity: "Info",
Reason: "registered research surface; add a specialized collector for deeper enumeration",
}}
}
findings := []AdvancedFinding{}
for _, probe := range profile.registry {
findings = append(findings, registryAllValues(profile.area, probe.scope, probe.root, probe.path, profile.severity, profile.reason)...)
findings = append(findings, registrySubkeys(profile.area, probe.scope, probe.root, probe.path, profile.severity, profile.reason)...)
}
for _, pattern := range profile.globs {
findings = append(findings, fileGlobFindings(profile.area, expandEnvPath(pattern), profile.severity, profile.reason)...)
}
if len(profile.services) > 0 {
findings = append(findings, servicesMatching(profile.area, profile.services, profile.severity, profile.reason)...)
}
if len(profile.processes) > 0 {
findings = append(findings, processesMatching(profile.area, profile.processes, profile.severity, profile.reason)...)
}
if len(findings) == 0 {
findings = append(findings, AdvancedFinding{
Area: profile.area,
Target: check,
Severity: "Info",
Reason: "no concrete artifacts found; surface remains registered for targeted research",
})
}
return findings
}
func surfaceProfiles() map[string]surfaceProfile {
return map[string]surfaceProfile{
"comdcom": registryProfile("COM/DCOM", "Medium", "COM/DCOM registration or security surface", []string{`Software\Classes\AppID`, `Software\Classes\CLSID`, `SOFTWARE\Classes\AppID`, `SOFTWARE\Classes\CLSID`}),
"comelevation": registryProfile("COM Elevation", "High", "COM elevation or auto-approval surface", []string{`SOFTWARE\Microsoft\Windows NT\CurrentVersion\UAC\COMAutoApprovalList`, `Software\Classes\CLSID`}),
"dcomactivation": registryProfile("DCOM Activation", "Medium", "DCOM machine activation/security policy", []string{`SOFTWARE\Microsoft\Ole`, `Software\Classes\AppID`}),
"rpc": serviceProfile("RPC", "Medium", "RPC-capable service or endpoint surface", []string{"rpc", "RpcSs", "RpcEptMapper", "DcomLaunch"}),
"alpc": mixedProfile("ALPC", "Info", "ALPC broker or object namespace surface", []string{`SOFTWARE\Microsoft\WindowsRuntime\ActivatableClassId`}, nil, []string{"runtimebroker", "dllhost", "svchost"}),
"lpc": mixedProfile("LPC", "Info", "LPC/NT object boundary surface", []string{`SYSTEM\CurrentControlSet\Control\Session Manager`}, nil, []string{"csrss", "lsass", "winlogon"}),
"tokenimpersonation": serviceProfile("Token Impersonation", "High", "service process commonly relevant to impersonation research", []string{"RpcSs", "Spooler", "Schedule", "BITS", "WinRM", "WebClient"}),
"uacautoelevation": registryProfile("UAC Auto-Elevation", "High", "UAC auto-elevation approval or consent surface", []string{`SOFTWARE\Microsoft\Windows NT\CurrentVersion\UAC\COMAutoApprovalList`, `SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System`}),
"msi": registryProfile("MSI", "High", "Windows Installer policy or product registration surface", []string{`Software\Policies\Microsoft\Windows\Installer`, `SOFTWARE\Policies\Microsoft\Windows\Installer`, `SOFTWARE\Microsoft\Windows\CurrentVersion\Installer`}),
"installerrepair": registryProfile("Installer Repair", "Medium", "installer repair and advertised product surface", []string{`SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData`, `SOFTWARE\Classes\Installer\Products`}),
"wdacpolicy": registryProfile("WDAC/AppLocker Interfaces", "Info", "application control policy interface", []string{`SYSTEM\CurrentControlSet\Control\CI\Policy`, `SOFTWARE\Policies\Microsoft\Windows\SrpV2`}),
"brokers": mixedProfile("Broker Processes", "Medium", "privileged broker process or COM broker surface", []string{`SOFTWARE\Microsoft\WindowsRuntime\ActivatableClassId`}, nil, []string{"runtimebroker", "dllhost", "consent", "appinfo"}),
"appcontainerbrokers": mixedProfile("AppContainer Brokers", "Medium", "AppContainer capability or broker surface", []string{`SOFTWARE\Microsoft\SecurityManager\CapabilityClasses`, `SOFTWARE\Classes\Local Settings\Software\Microsoft\Windows\CurrentVersion\AppContainer`}, nil, []string{"runtimebroker", "applicationframehost"}),
"urlmonikers": registryProfile("URL Monikers", "Medium", "URL protocol or moniker activation surface", []string{`Software\Classes`, `SOFTWARE\Classes`}),
"comhijack": registryProfile("COM Hijacking", "High", "per-user or machine COM registration hijack surface", []string{`Software\Classes\CLSID`, `Software\Classes\AppID`, `SOFTWARE\Classes\CLSID`}),
"custommarshal": registryProfile("Custom Marshaling", "High", "COM custom marshaling registration surface", []string{`Software\Classes\Interface`, `SOFTWARE\Classes\Interface`}),
"dllhijacking": mixedProfile("DLL Hijacking", "High", "DLL search or writable path surface", []string{`SYSTEM\CurrentControlSet\Control\Session Manager`, `SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs`}, []string{`%WINDIR%\System32\*.local`, `%ProgramData%\*.dll`}, nil),
"sxs": mixedProfile("SxS", "Medium", "side-by-side assembly or manifest surface", []string{`SOFTWARE\Microsoft\Windows\CurrentVersion\SideBySide`}, []string{`%WINDIR%\WinSxS\Manifests\*.manifest`, `%ProgramFiles%\*\*.manifest`}, nil),
"activationctx": mixedProfile("Activation Context", "Medium", "manifest activation context surface", []string{`SOFTWARE\Microsoft\Windows\CurrentVersion\SideBySide`}, []string{`%ProgramFiles%\*\*.manifest`, `%ProgramFiles(x86)%\*\*.manifest`}, nil),
"ntobjmgr": registryProfile("NT Object Manager", "Info", "object namespace-related session manager surface", []string{`SYSTEM\CurrentControlSet\Control\Session Manager`, `SYSTEM\CurrentControlSet\Control\Session Manager\DOS Devices`}),
"objdirs": registryProfile("Object Directories", "Info", "object directory namespace indicator", []string{`SYSTEM\CurrentControlSet\Control\Session Manager\DOS Devices`}),
"symlinks": registryProfile("Symbolic Links", "Medium", "DOS device symbolic link surface", []string{`SYSTEM\CurrentControlSet\Control\Session Manager\DOS Devices`}),
"hardlinks": fileProfile("Hard Links", "Info", "hard-link-prone writable area for manual race research", []string{`%TEMP%\*`, `%ProgramData%\*`}),
"junctions": fileProfile("Junctions", "Medium", "junction-prone directory surface", []string{`%TEMP%\*`, `%ProgramData%\*`}),
"mountpoints": registryProfile("Mount Points", "Medium", "mounted device and mount manager surface", []string{`SYSTEM\MountedDevices`, `SYSTEM\CurrentControlSet\Services\mountmgr`}),
"reparsepoints": fileProfile("Reparse Points", "Medium", "reparse-point-prone writable area", []string{`%TEMP%\*`, `%ProgramData%\*`}),
"oplocks": fileProfile("OpLocks", "Info", "oplock/race-prone writable area", []string{`%TEMP%\*`, `%ProgramData%\*`}),
"regsymlinks": registryProfile("Registry Symlinks", "Medium", "registry link or virtualization-related surface", []string{`Software\Classes\VirtualStore`, `SOFTWARE\Classes\VirtualStore`}),
"scm": serviceProfile("SCM", "Medium", "service control manager service configuration surface", []string{""}),
"minifilters": serviceProfile("Minifilters", "Medium", "file system minifilter driver surface", []string{"FltMgr", "WdFilter", "FileInfo", "luafv"}),
"ioctls": serviceProfile("IOCTLs", "High", "kernel driver device control surface", []string{"driver", "filter", "kbd", "mou", "disk", "ndis"}),
"deviceobjects": registryProfile("Device Objects", "Medium", "device class and driver object surface", []string{`SYSTEM\CurrentControlSet\Enum`, `SYSTEM\CurrentControlSet\Control\Class`}),
"etw": registryProfile("ETW", "Info", "ETW provider registration surface", []string{`SOFTWARE\Microsoft\Windows\CurrentVersion\WINEVT\Publishers`, `SOFTWARE\Microsoft\Windows\CurrentVersion\ETW`}),
"win32k": mixedProfile("Win32k", "Medium", "GUI subsystem boundary surface", []string{`SYSTEM\CurrentControlSet\Control\Session Manager\SubSystems`}, nil, []string{"dwm", "winlogon", "csrss"}),
"csrss": mixedProfile("CSRSS", "High", "client/server runtime subsystem boundary", []string{`SYSTEM\CurrentControlSet\Control\Session Manager\SubSystems`}, nil, []string{"csrss"}),
"lsassinterfaces": registryProfile("LSASS Interfaces", "High", "LSASS authentication and security package surface", []string{`SYSTEM\CurrentControlSet\Control\Lsa`, `SYSTEM\CurrentControlSet\Control\SecurityProviders`}),
"accesstokens": serviceProfile("Access Tokens", "High", "privileged service token surface", []string{"RpcSs", "Schedule", "Spooler", "WinRM", "BITS"}),
"handles": mixedProfile("Handles", "Info", "handle duplication/leak research target", nil, nil, []string{"lsass", "csrss", "services", "winlogon", "spoolsv"}),
"jobobjects": mixedProfile("Job Objects", "Info", "process containment and job object research target", nil, nil, []string{"svchost", "runtimebroker", "dllhost"}),
"sectionobjects": mixedProfile("Section Objects", "Info", "section object and image mapping research target", nil, []string{`%TEMP%\*.tmp`, `%ProgramData%\*.tmp`}, []string{"csrss", "lsass", "services"}),
"sharedmemory": mixedProfile("Shared Memory", "Info", "shared memory IPC research target", nil, []string{`%TEMP%\*`}, []string{"explorer", "runtimebroker", "dwm"}),
"mmap": fileProfile("Memory-Mapped Files", "Info", "memory-mapped file candidate", []string{`%TEMP%\*`, `%ProgramData%\*`}),
"wfp": registryProfile("WFP", "Medium", "Windows Filtering Platform provider surface", []string{`SYSTEM\CurrentControlSet\Services\BFE`, `SYSTEM\CurrentControlSet\Services\SharedAccess`}),
"hyperv": serviceProfile("Hyper-V", "Medium", "Hyper-V component service surface", []string{"vmms", "vmcompute", "vmicheartbeat", "HvHost", "hns"}),
"wsl": serviceProfile("WSL", "Medium", "Windows Subsystem for Linux component surface", []string{"LxssManager", "vmcompute", "hns"}),
"efsrpc": serviceProfile("EFSRPC", "High", "Encrypting File System RPC surface", []string{"EFS", "EFSRPC"}),
"taskrpc": serviceProfile("Task Scheduler RPC", "Medium", "Task Scheduler service RPC surface", []string{"Schedule"}),
"bits": serviceProfile("BITS", "Medium", "Background Intelligent Transfer Service surface", []string{"BITS"}),
"endpointmapper": serviceProfile("Endpoint Mapper", "Medium", "RPC Endpoint Mapper and DCOM launch surface", []string{"RpcEptMapper", "RpcSs", "DcomLaunch"}),
"winrm": serviceProfile("WinRM", "Medium", "Windows Remote Management service surface", []string{"WinRM"}),
"smbipc": serviceProfile("SMB Local IPC", "Medium", "SMB and IPC service surface", []string{"LanmanServer", "LanmanWorkstation", "srv2"}),
"credproviders": registryProfile("Credential Providers", "High", "credential provider registration surface", []string{`SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers`, `SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Provider Filters`}),
"authpackages": registryProfile("Authentication Packages", "High", "authentication package registration surface", []string{`SYSTEM\CurrentControlSet\Control\Lsa`}),
"lsaplugins": registryProfile("LSA Plugins", "High", "LSA plugin and notification package surface", []string{`SYSTEM\CurrentControlSet\Control\Lsa`, `SYSTEM\CurrentControlSet\Control\SecurityProviders`}),
"cloudap": registryProfile("CloudAP", "High", "cloud authentication package surface", []string{`SYSTEM\CurrentControlSet\Control\Lsa`, `SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication`}),
"ppl": registryProfile("PPL", "High", "Protected Process Light policy indicator", []string{`SYSTEM\CurrentControlSet\Control\Lsa`, `SYSTEM\CurrentControlSet\Control\CI`}),
"userprofilesvc": serviceProfile("User Profile Service", "Medium", "profile service and profile path surface", []string{"ProfSvc"}),
"updates": serviceProfile("Update Mechanisms", "Medium", "update service or repair/update mechanism surface", []string{"wuauserv", "UsoSvc", "BITS", "TrustedInstaller", "WaaSMedicSvc"}),
"recovery": registryProfile("Recovery", "Medium", "repair and recovery mechanism surface", []string{`SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce`, `SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options`, `SYSTEM\Setup`}),
"tempfiles": fileProfile("Temporary Files", "Medium", "temporary file handling surface", []string{`%TEMP%\*`, `%TMP%\*`, `%WINDIR%\Temp\*`}),
"toctou": fileProfile("TOCTOU", "Medium", "race-prone writable filesystem surface", []string{`%TEMP%\*`, `%ProgramData%\*`, `%WINDIR%\Temp\*`}),
"pathcanon": fileProfile("Path Canonicalization", "Medium", "path parsing and canonicalization research surface", []string{`%TEMP%\*`, `%ProgramData%\*`}),
"confuseddeputy": serviceProfile("Confused Deputy", "Medium", "privileged service that may act on caller-controlled paths", []string{"Spooler", "Schedule", "BITS", "msiserver", "TrustedInstaller", "WinRM"}),
"acl": mixedProfile("ACL Misconfigurations", "High", "path commonly worth ACL review", nil, []string{`%ProgramData%\*`, `%WINDIR%\Temp\*`}, nil),
"envinjection": registryProfile("Environment Injection", "Medium", "environment variable injection surface", []string{`SYSTEM\CurrentControlSet\Control\Session Manager\Environment`, `Environment`}),
"searchpoison": registryProfile("Search Path Poisoning", "High", "search path or App Paths poisoning surface", []string{`SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths`, `Software\Microsoft\Windows\CurrentVersion\App Paths`, `SYSTEM\CurrentControlSet\Control\Session Manager\Environment`}),
"propertyhandlers": registryProfile("Property Handlers", "Medium", "shell property handler registration surface", []string{`SOFTWARE\Microsoft\Windows\CurrentVersion\PropertySystem\PropertyHandlers`, `Software\Microsoft\Windows\CurrentVersion\PropertySystem\PropertyHandlers`}),
"explorerext": registryProfile("Explorer Extensions", "Medium", "Explorer shell extension registration surface", []string{`SOFTWARE\Microsoft\Windows\CurrentVersion\Shell Extensions`, `Software\Microsoft\Windows\CurrentVersion\Shell Extensions`}),
"thumbnailproviders": registryProfile("Thumbnail Providers", "Medium", "thumbnail provider registration surface", []string{`SOFTWARE\Classes\CLSID`, `Software\Classes\CLSID`}),
"previewhandlers": registryProfile("Preview Handlers", "Medium", "preview handler registration surface", []string{`SOFTWARE\Microsoft\Windows\CurrentVersion\PreviewHandlers`, `Software\Microsoft\Windows\CurrentVersion\PreviewHandlers`}),
"sessionisolation": mixedProfile("Session Isolation", "Info", "session boundary process surface", []string{`SYSTEM\CurrentControlSet\Control\Terminal Server`}, nil, []string{"winlogon", "csrss", "dwm", "rdpclip"}),
"windowstations": registryProfile("Window Stations", "Info", "desktop/window station policy surface", []string{`SYSTEM\CurrentControlSet\Control\Windows`, `SYSTEM\CurrentControlSet\Control\Session Manager\SubSystems`}),
"clipboard": mixedProfile("Clipboard IPC", "Info", "clipboard IPC process surface", nil, nil, []string{"rdpclip", "explorer", "applicationframehost"}),
"dragdrop": mixedProfile("Drag-and-Drop IPC", "Info", "drag-and-drop broker process surface", nil, nil, []string{"explorer", "runtimebroker", "applicationframehost"}),
"dde": registryProfile("DDE", "Medium", "DDE command registration surface", []string{`Software\Classes`, `SOFTWARE\Classes`}),
"ole": registryProfile("OLE", "Medium", "OLE/COM embedding registration surface", []string{`Software\Classes`, `SOFTWARE\Classes`, `SOFTWARE\Microsoft\Ole`}),
}
}
func registryProfile(area, severity, reason string, paths []string) surfaceProfile {
probes := make([]registryProbe, 0, len(paths))
for _, path := range paths {
scope := uintptr(registry.HkeyLocalMachine)
root := "HKLM"
if strings.HasPrefix(path, "Software\\") || path == "Environment" {
scope = uintptr(registry.HkeyCurrentUser)
root = "HKCU"
}
if strings.HasPrefix(path, "SOFTWARE\\") || strings.HasPrefix(path, "SYSTEM\\") {
scope = uintptr(registry.HkeyLocalMachine)
root = "HKLM"
}
probes = append(probes, registryProbe{scope: scope, root: root, path: path})
}
return surfaceProfile{area: area, severity: severity, reason: reason, registry: probes}
}
func serviceProfile(area, severity, reason string, services []string) surfaceProfile {
return surfaceProfile{area: area, severity: severity, reason: reason, services: services}
}
func fileProfile(area, severity, reason string, globs []string) surfaceProfile {
return surfaceProfile{area: area, severity: severity, reason: reason, globs: globs}
}
func mixedProfile(area, severity, reason string, paths []string, globs []string, processes []string) surfaceProfile {
profile := registryProfile(area, severity, reason, paths)
profile.globs = globs
profile.processes = processes
return profile
}
func servicesMatching(area string, keywords []string, severity, reason string) []AdvancedFinding {
services, err := services.EnumerateServices()
if err != nil {
return nil
}
findings := []AdvancedFinding{}
for _, service := range services {
if len(keywords) == 1 && keywords[0] == "" || containsKeyword(service.Name, keywords) || containsKeyword(service.DisplayName, keywords) || containsKeyword(service.BinaryPath, keywords) {
findings = append(findings, AdvancedFinding{Area: area, Target: service.Name, Name: service.StartType, Value: service.BinaryPath, Severity: severity, Reason: reason})
}
}
return findings
}
func processesMatching(area string, keywords []string, severity, reason string) []AdvancedFinding {
processes, err := winprocess.EnumerateProcesses()
if err != nil {
return nil
}
findings := []AdvancedFinding{}
for _, process := range processes {
if containsKeyword(process.Name, keywords) {
findings = append(findings, AdvancedFinding{Area: area, Target: fmt.Sprintf("%s[%d]", process.Name, process.PID), Name: "process", Severity: severity, Reason: reason})
}
}
return findings
}
func containsKeyword(value string, keywords []string) bool {
value = strings.ToLower(value)
for _, keyword := range keywords {
if keyword == "" || strings.Contains(value, strings.ToLower(keyword)) {
return true
}
}
return false
}
func expandEnvPath(path string) string {
replacements := map[string]string{
"%TEMP%": os.Getenv("TEMP"),
"%TMP%": os.Getenv("TMP"),
"%WINDIR%": os.Getenv("WINDIR"),
"%ProgramData%": os.Getenv("ProgramData"),
"%ProgramFiles%": os.Getenv("ProgramFiles"),
"%ProgramFiles(x86)%": os.Getenv("ProgramFiles(x86)"),
}
for token, value := range replacements {
if value != "" {
path = strings.ReplaceAll(path, token, value)
}
}
return path
}
+288
View File
@@ -0,0 +1,288 @@
//go:build windows
package audit
import (
"os"
"path/filepath"
"sort"
"strings"
"ferrum/windows/registry"
wintypes "ferrum/windows/types"
)
type RegistryAuditFinding = registry.RegistryAuditFinding
type PolicyFinding = wintypes.PolicyFinding
type DLLSearchPathFinding = wintypes.DLLSearchPathFinding
func EnumerateRegistryAuditFindings() ([]RegistryAuditFinding, error) {
findings := []RegistryAuditFinding{}
checkValueSet := []struct {
scope uintptr
scopeName string
path string
names []string
severity string
reason string
}{
{registry.HkeyLocalMachine, "HKLM", `SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows`, []string{"AppInit_DLLs", "LoadAppInit_DLLs"}, "High", "AppInit DLL injection surface"},
{registry.HkeyLocalMachine, "HKLM", `SYSTEM\CurrentControlSet\Control\Session Manager`, []string{"AppCertDLLs"}, "High", "process creation DLL injection surface"},
{registry.HkeyLocalMachine, "HKLM", `SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon`, []string{"Shell", "Userinit", "Notify"}, "High", "Winlogon execution surface"},
{registry.HkeyLocalMachine, "HKLM", `SYSTEM\CurrentControlSet\Control\Lsa`, []string{"Authentication Packages", "Notification Packages", "Security Packages"}, "High", "LSA package load surface"},
{registry.HkeyLocalMachine, "HKLM", `SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug`, []string{"Debugger", "Auto"}, "Medium", "post-crash debugger execution surface"},
{registry.HkeyLocalMachine, "HKLM", `SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System`, []string{"EnableLUA", "ConsentPromptBehaviorAdmin", "LocalAccountTokenFilterPolicy"}, "Medium", "UAC and remote token policy"},
{registry.HkeyLocalMachine, "HKLM", `SYSTEM\CurrentControlSet\Control\Session Manager`, []string{"SafeDllSearchMode", "CWDIllegalInDllSearch"}, "Medium", "DLL search-order policy"},
{registry.HkeyLocalMachine, "HKLM", `SOFTWARE\Policies\Microsoft\Windows\PowerShell`, []string{"EnableScripts", "ExecutionPolicy"}, "Medium", "PowerShell execution policy"},
{registry.HkeyCurrentUser, "HKCU", `SOFTWARE\Policies\Microsoft\Windows\PowerShell`, []string{"EnableScripts", "ExecutionPolicy"}, "Medium", "per-user PowerShell policy"},
}
for _, check := range checkValueSet {
values, err := registry.Values(check.scope, check.path)
if err != nil {
continue
}
for _, value := range values {
if !containsName(check.names, value.Name) {
continue
}
findings = append(findings, RegistryAuditFinding{
Scope: check.scopeName,
Path: check.path,
Name: value.Name,
Value: value.Value,
Severity: check.severity,
Reason: check.reason,
})
}
}
findings = append(findings, enumerateIFEO(registry.HkeyLocalMachine, "HKLM")...)
findings = append(findings, enumerateIFEO(registry.HkeyCurrentUser, "HKCU")...)
findings = append(findings, enumerateSilentProcessExit()...)
findings = append(findings, enumerateCOMTreatAs()...)
sort.Slice(findings, func(i, j int) bool {
if findings[i].Severity != findings[j].Severity {
return severityRank(findings[i].Severity) > severityRank(findings[j].Severity)
}
return findings[i].Scope+findings[i].Path+findings[i].Name < findings[j].Scope+findings[j].Path+findings[j].Name
})
return findings, nil
}
func EnumeratePolicyFindings() ([]PolicyFinding, error) {
findings := []PolicyFinding{}
if enabled(registry.HkeyCurrentUser, `Software\Policies\Microsoft\Windows\Installer`, "AlwaysInstallElevated") &&
enabled(registry.HkeyLocalMachine, `Software\Policies\Microsoft\Windows\Installer`, "AlwaysInstallElevated") {
findings = append(findings, PolicyFinding{Name: "AlwaysInstallElevated", Value: "HKCU=1 HKLM=1", Severity: "High", Reason: "MSI packages can install elevated for standard users"})
}
if value := registryValue(registry.HkeyLocalMachine, `SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System`, "EnableLUA"); value == "00000000" || value == "0" {
findings = append(findings, PolicyFinding{Name: "EnableLUA", Value: value, Severity: "High", Reason: "UAC disabled"})
}
if value := registryValue(registry.HkeyLocalMachine, `SYSTEM\CurrentControlSet\Control\Session Manager`, "SafeDllSearchMode"); value == "00000000" || value == "0" {
findings = append(findings, PolicyFinding{Name: "SafeDllSearchMode", Value: value, Severity: "Medium", Reason: "legacy DLL search behavior"})
}
if hasSubkeys(registry.HkeyLocalMachine, `SOFTWARE\Policies\Microsoft\Windows\SrpV2`) {
findings = append(findings, PolicyFinding{Name: "AppLocker", Value: "Configured", Severity: "Info", Reason: "application control policy present"})
} else {
findings = append(findings, PolicyFinding{Name: "AppLocker", Value: "Not detected", Severity: "Low", Reason: "no AppLocker policy keys found"})
}
if hasSubkeys(registry.HkeyLocalMachine, `SYSTEM\CurrentControlSet\Control\CI\Policy`) {
findings = append(findings, PolicyFinding{Name: "WDAC", Value: "Policy keys present", Severity: "Info", Reason: "code integrity policy surface present"})
} else {
findings = append(findings, PolicyFinding{Name: "WDAC", Value: "Not detected", Severity: "Low", Reason: "no WDAC policy keys found"})
}
return findings, nil
}
func EnumerateDLLSearchPathFindings() ([]DLLSearchPathFinding, error) {
findings := []DLLSearchPathFinding{}
pathValue := os.Getenv("PATH")
seen := map[string]bool{}
for _, path := range filepath.SplitList(pathValue) {
path = strings.TrimSpace(path)
if path == "" || seen[strings.ToLower(path)] {
continue
}
seen[strings.ToLower(path)] = true
reason, severity := classifySearchPath(path)
if reason == "" {
continue
}
findings = append(findings, DLLSearchPathFinding{Path: path, Source: "PATH", Severity: severity, Reason: reason})
}
if cwd, err := os.Getwd(); err == nil {
reason, severity := classifySearchPath(cwd)
if reason != "" {
findings = append(findings, DLLSearchPathFinding{Path: cwd, Source: "CurrentDirectory", Severity: severity, Reason: reason})
}
}
for _, known := range knownDLLs() {
findings = append(findings, DLLSearchPathFinding{Path: known, Source: "KnownDLLs", Severity: "Info", Reason: "KnownDLL protected load name"})
}
sort.Slice(findings, func(i, j int) bool {
if findings[i].Severity != findings[j].Severity {
return severityRank(findings[i].Severity) > severityRank(findings[j].Severity)
}
return findings[i].Path < findings[j].Path
})
return findings, nil
}
func enumerateIFEO(scope uintptr, scopeName string) []RegistryAuditFinding {
base := `SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options`
root, err := registry.OpenKey(scope, base)
if err != nil {
return nil
}
defer registry.CloseKey(root)
keys, err := registry.EnumSubkeys(root)
if err != nil {
return nil
}
findings := []RegistryAuditFinding{}
for _, key := range keys {
values, err := registry.Values(scope, base+`\`+key)
if err != nil {
continue
}
for _, value := range values {
if containsName([]string{"Debugger", "VerifierDlls", "GlobalFlag"}, value.Name) {
findings = append(findings, RegistryAuditFinding{Scope: scopeName, Path: base + `\` + key, Name: value.Name, Value: value.Value, Severity: "High", Reason: "IFEO process interception or verifier surface"})
}
}
}
return findings
}
func enumerateSilentProcessExit() []RegistryAuditFinding {
base := `SOFTWARE\Microsoft\Windows NT\CurrentVersion\SilentProcessExit`
root, err := registry.OpenKey(registry.HkeyLocalMachine, base)
if err != nil {
return nil
}
defer registry.CloseKey(root)
keys, err := registry.EnumSubkeys(root)
if err != nil {
return nil
}
findings := []RegistryAuditFinding{}
for _, key := range keys {
values, err := registry.Values(registry.HkeyLocalMachine, base+`\`+key)
if err != nil {
continue
}
for _, value := range values {
if containsName([]string{"MonitorProcess", "ReportingMode"}, value.Name) {
findings = append(findings, RegistryAuditFinding{Scope: "HKLM", Path: base + `\` + key, Name: value.Name, Value: value.Value, Severity: "High", Reason: "SilentProcessExit monitor execution surface"})
}
}
}
return findings
}
func enumerateCOMTreatAs() []RegistryAuditFinding {
base := `Software\Classes\CLSID`
root, err := registry.OpenKey(registry.HkeyCurrentUser, base)
if err != nil {
return nil
}
defer registry.CloseKey(root)
keys, err := registry.EnumSubkeys(root)
if err != nil {
return nil
}
findings := []RegistryAuditFinding{}
for _, key := range keys {
treatAs, err := registry.OpenKey(root, key+`\TreatAs`)
if err != nil {
continue
}
value, _ := registry.QueryDefaultValue(treatAs)
registry.CloseKey(treatAs)
findings = append(findings, RegistryAuditFinding{Scope: "HKCU", Path: base + `\` + key + `\TreatAs`, Name: "(Default)", Value: value, Severity: "Medium", Reason: "per-user COM TreatAs redirection"})
}
return findings
}
func classifySearchPath(path string) (string, string) {
lower := strings.ToLower(path)
switch {
case path == "." || !filepath.IsAbs(path):
return "relative DLL search path element", "High"
case strings.Contains(lower, `\users\`) || strings.Contains(lower, `\temp\`) || strings.Contains(lower, `\downloads\`):
return "user-writable-looking DLL search path element", "High"
case strings.Contains(lower, `\programdata\`):
return "commonly writable DLL search path element", "Medium"
}
if _, err := os.Stat(path); err != nil {
return "missing DLL search path element", "Low"
}
return "", ""
}
func knownDLLs() []string {
values, err := registry.Values(registry.HkeyLocalMachine, `SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs`)
if err != nil {
return nil
}
names := []string{}
for _, value := range values {
if value.Name == "" || strings.HasPrefix(value.Name, "DllDirectory") {
continue
}
names = append(names, value.Value)
}
return names
}
func registryValue(scope uintptr, path, name string) string {
values, err := registry.Values(scope, path)
if err != nil {
return ""
}
for _, value := range values {
if strings.EqualFold(value.Name, name) {
return value.Value
}
}
return ""
}
func enabled(scope uintptr, path, name string) bool {
value := registryValue(scope, path, name)
return value == "1" || value == "00000001"
}
func hasSubkeys(scope uintptr, path string) bool {
key, err := registry.OpenKey(scope, path)
if err != nil {
return false
}
defer registry.CloseKey(key)
keys, err := registry.EnumSubkeys(key)
return err == nil && len(keys) > 0
}
func containsName(names []string, name string) bool {
for _, item := range names {
if strings.EqualFold(item, name) {
return true
}
}
return false
}
func severityRank(severity string) int {
switch severity {
case "High":
return 4
case "Medium":
return 3
case "Low":
return 2
case "Info":
return 1
default:
return 0
}
}
+27
View File
@@ -0,0 +1,27 @@
//go:build windows
package env
import (
"os"
"sort"
"strings"
wintypes "ferrum/windows/types"
)
type EnvVar = wintypes.EnvVar
func EnumerateEnvironment() ([]EnvVar, error) {
raw := os.Environ()
vars := make([]EnvVar, 0, len(raw))
for _, item := range raw {
name, value, ok := strings.Cut(item, "=")
if !ok {
continue
}
vars = append(vars, EnvVar{Name: name, Value: value})
}
sort.Slice(vars, func(i, j int) bool { return strings.ToUpper(vars[i].Name) < strings.ToUpper(vars[j].Name) })
return vars, nil
}
+9
View File
@@ -0,0 +1,9 @@
package errors
import "fmt"
type ErrUnsupported string
func (e ErrUnsupported) Error() string {
return fmt.Sprintf("Windows API enumeration is not supported on %s", string(e))
}
+46
View File
@@ -0,0 +1,46 @@
//go:build !windows
package facade
import (
"runtime"
winerrors "ferrum/windows/errors"
winprocess "ferrum/windows/process"
"ferrum/windows/registry"
wintypes "ferrum/windows/types"
)
type Process = winprocess.Process
type ProcessMitigation = winprocess.ProcessMitigation
type TokenInfo = wintypes.TokenInfo
type PolicyFinding = wintypes.PolicyFinding
type CLSIDEntry = registry.CLSIDEntry
type CLSIDProcMonCandidate = registry.CLSIDProcMonCandidate
type RegistryAuditFinding = registry.RegistryAuditFinding
type DLLSearchPathFinding = wintypes.DLLSearchPathFinding
type AdvancedFinding = wintypes.AdvancedFinding
type ServiceInfo = wintypes.ServiceInfo
type DriverInfo = wintypes.DriverInfo
type PipeInfo = wintypes.PipeInfo
type StartupEntry = wintypes.StartupEntry
type ScheduledTask = wintypes.ScheduledTask
type EnvVar = wintypes.EnvVar
func unsupported() error { return winerrors.ErrUnsupported(runtime.GOOS) }
func EnumerateProcesses() ([]Process, error) { return nil, unsupported() }
func InspectProcessToken(pid uint32) (TokenInfo, error) { return TokenInfo{}, unsupported() }
func EnumerateHKCUCLSID() ([]CLSIDEntry, error) { return nil, unsupported() }
func EnumerateCLSIDProcMonCandidates() ([]CLSIDProcMonCandidate, error) { return nil, unsupported() }
func EnumerateServices() ([]ServiceInfo, error) { return nil, unsupported() }
func EnumerateDrivers() ([]DriverInfo, error) { return nil, unsupported() }
func EnumerateNamedPipes() ([]PipeInfo, error) { return nil, unsupported() }
func EnumerateStartupEntries() ([]StartupEntry, error) { return nil, unsupported() }
func EnumerateScheduledTasks() ([]ScheduledTask, error) { return nil, unsupported() }
func EnumerateEnvironment() ([]EnvVar, error) { return nil, unsupported() }
func EnumerateProcessMitigations() ([]ProcessMitigation, error) { return nil, unsupported() }
func EnumerateRegistryAuditFindings() ([]RegistryAuditFinding, error) { return nil, unsupported() }
func EnumerateDLLSearchPathFindings() ([]DLLSearchPathFinding, error) { return nil, unsupported() }
func EnumeratePolicyFindings() ([]PolicyFinding, error) { return nil, unsupported() }
func EnumerateAdvancedFindings(check string) ([]AdvancedFinding, error) { return nil, unsupported() }
+60
View File
@@ -0,0 +1,60 @@
//go:build windows
package facade
import (
"ferrum/windows/advanced"
"ferrum/windows/audit"
winenv "ferrum/windows/env"
"ferrum/windows/migrations"
"ferrum/windows/pipes"
winprocess "ferrum/windows/process"
"ferrum/windows/registry"
"ferrum/windows/scheduled"
"ferrum/windows/services"
"ferrum/windows/startup"
"ferrum/windows/token"
wintypes "ferrum/windows/types"
)
type Process = winprocess.Process
type ProcessMitigation = winprocess.ProcessMitigation
type TokenInfo = wintypes.TokenInfo
type PolicyFinding = wintypes.PolicyFinding
type CLSIDEntry = registry.CLSIDEntry
type CLSIDProcMonCandidate = registry.CLSIDProcMonCandidate
type RegistryAuditFinding = registry.RegistryAuditFinding
type DLLSearchPathFinding = wintypes.DLLSearchPathFinding
type AdvancedFinding = wintypes.AdvancedFinding
type ServiceInfo = wintypes.ServiceInfo
type DriverInfo = wintypes.DriverInfo
type PipeInfo = wintypes.PipeInfo
type StartupEntry = wintypes.StartupEntry
type ScheduledTask = wintypes.ScheduledTask
type EnvVar = wintypes.EnvVar
func EnumerateProcesses() ([]Process, error) { return winprocess.EnumerateProcesses() }
func InspectProcessToken(pid uint32) (TokenInfo, error) { return token.InspectProcessToken(pid) }
func EnumerateHKCUCLSID() ([]CLSIDEntry, error) { return registry.EnumerateHKCUCLSID() }
func EnumerateCLSIDProcMonCandidates() ([]CLSIDProcMonCandidate, error) {
return registry.EnumerateCLSIDProcMonCandidates()
}
func EnumerateServices() ([]ServiceInfo, error) { return services.EnumerateServices() }
func EnumerateDrivers() ([]DriverInfo, error) { return services.EnumerateDrivers() }
func EnumerateNamedPipes() ([]PipeInfo, error) { return pipes.EnumerateNamedPipes() }
func EnumerateStartupEntries() ([]StartupEntry, error) { return startup.EnumerateStartupEntries() }
func EnumerateScheduledTasks() ([]ScheduledTask, error) { return scheduled.EnumerateScheduledTasks() }
func EnumerateEnvironment() ([]EnvVar, error) { return winenv.EnumerateEnvironment() }
func EnumerateProcessMitigations() ([]ProcessMitigation, error) {
return migrations.EnumerateProcessMitigations()
}
func EnumerateRegistryAuditFindings() ([]RegistryAuditFinding, error) {
return audit.EnumerateRegistryAuditFindings()
}
func EnumerateDLLSearchPathFindings() ([]DLLSearchPathFinding, error) {
return audit.EnumerateDLLSearchPathFindings()
}
func EnumeratePolicyFindings() ([]PolicyFinding, error) { return audit.EnumeratePolicyFindings() }
func EnumerateAdvancedFindings(check string) ([]AdvancedFinding, error) {
return advanced.EnumerateAdvancedFindings(check)
}
+71
View File
@@ -0,0 +1,71 @@
//go:build windows
package migrations
import (
"fmt"
"sort"
"syscall"
"unsafe"
winprocess "ferrum/windows/process"
)
const (
processDEPPolicy = 0
processASLRPolicy = 1
processStrictHandle = 2
processControlFlowGuard = 7
)
type mitigationFlags struct {
Flags uint32
}
var procGetProcessMitigationPolicy = kernel32.NewProc("GetProcessMitigationPolicy")
var (
kernel32 = syscall.NewLazyDLL("kernel32.dll")
procOpenProcess = kernel32.NewProc("OpenProcess")
procCloseHandle = kernel32.NewProc("CloseHandle")
)
type ProcessMitigation = winprocess.ProcessMitigation
const processQueryLimitedInformation = 0x1000
func EnumerateProcessMitigations() ([]ProcessMitigation, error) {
processes, err := winprocess.EnumerateProcesses()
if err != nil {
return nil, err
}
results := make([]ProcessMitigation, 0, len(processes))
for _, process := range processes {
handle, _, err := procOpenProcess.Call(processQueryLimitedInformation, 0, uintptr(process.PID))
if handle == 0 {
results = append(results, ProcessMitigation{Process: process, DEP: fmt.Sprintf("open: %v", err)})
continue
}
item := ProcessMitigation{Process: process}
item.DEP = mitigationValue(handle, processDEPPolicy)
item.ASLR = mitigationValue(handle, processASLRPolicy)
item.Strict = mitigationValue(handle, processStrictHandle)
item.CFG = mitigationValue(handle, processControlFlowGuard)
procCloseHandle.Call(handle)
results = append(results, item)
}
sort.Slice(results, func(i, j int) bool { return results[i].Name < results[j].Name })
return results, nil
}
func mitigationValue(handle uintptr, policy uintptr) string {
var flags mitigationFlags
ret, _, err := procGetProcessMitigationPolicy.Call(handle, policy, uintptr(unsafe.Pointer(&flags)), unsafe.Sizeof(flags))
if ret == 0 {
return fmt.Sprintf("error:%v", err)
}
if flags.Flags == 0 {
return "off"
}
return fmt.Sprintf("0x%08x", flags.Flags)
}
+30
View File
@@ -0,0 +1,30 @@
//go:build windows
package pipes
import (
"os"
"sort"
"strings"
wintypes "ferrum/windows/types"
)
type PipeInfo = wintypes.PipeInfo
func EnumerateNamedPipes() ([]PipeInfo, error) {
entries, err := os.ReadDir(`\\.\pipe\`)
if err != nil {
return nil, err
}
pipes := make([]PipeInfo, 0, len(entries))
for _, entry := range entries {
name := entry.Name()
if strings.TrimSpace(name) == "" {
continue
}
pipes = append(pipes, PipeInfo{Name: name})
}
sort.Slice(pipes, func(i, j int) bool { return strings.ToLower(pipes[i].Name) < strings.ToLower(pipes[j].Name) })
return pipes, nil
}
+13
View File
@@ -0,0 +1,13 @@
//go:build !windows
package process
import (
"runtime"
winerrors "ferrum/windows/errors"
)
func EnumerateProcesses() ([]Process, error) {
return nil, winerrors.ErrUnsupported(runtime.GOOS)
}
+67
View File
@@ -0,0 +1,67 @@
//go:build windows
package process
import (
"fmt"
"syscall"
"unsafe"
)
const (
th32csSnapProcess = 0x00000002
maxPath = 260
)
type processEntry32 struct {
Size uint32
Usage uint32
ProcessID uint32
DefaultHeapID uintptr
ModuleID uint32
Threads uint32
ParentProcessID uint32
PriClassBase int32
Flags uint32
ExeFile [maxPath]uint16
}
var (
kernel32 = syscall.NewLazyDLL("kernel32.dll")
procCreateToolhelp32Snap = kernel32.NewProc("CreateToolhelp32Snapshot")
procProcess32FirstW = kernel32.NewProc("Process32FirstW")
procProcess32NextW = kernel32.NewProc("Process32NextW")
procCloseHandle = kernel32.NewProc("CloseHandle")
)
func EnumerateProcesses() ([]Process, error) {
snapshot, _, err := procCreateToolhelp32Snap.Call(th32csSnapProcess, 0)
if snapshot == uintptr(syscall.InvalidHandle) {
return nil, fmt.Errorf("CreateToolhelp32Snapshot: %w", err)
}
defer procCloseHandle.Call(snapshot)
var entry processEntry32
entry.Size = uint32(unsafe.Sizeof(entry))
ret, _, err := procProcess32FirstW.Call(snapshot, uintptr(unsafe.Pointer(&entry)))
if ret == 0 {
return nil, fmt.Errorf("Process32FirstW: %w", err)
}
processes := make([]Process, 0, 256)
for {
processes = append(processes, Process{
PID: entry.ProcessID,
ParentPID: entry.ParentProcessID,
Name: syscall.UTF16ToString(entry.ExeFile[:]),
})
ret, _, err = procProcess32NextW.Call(snapshot, uintptr(unsafe.Pointer(&entry)))
if ret == 0 {
if errno, ok := err.(syscall.Errno); ok && errno == syscall.ERROR_NO_MORE_FILES {
break
}
break
}
}
return processes, nil
}
+31
View File
@@ -0,0 +1,31 @@
package process
type Process struct {
PID uint32
ParentPID uint32
Name string
Exe string
User string
Integrity string
Elevated bool
Privileges []string
}
func (p Process) Label() string {
user := p.User
if user == "" {
user = "Token"
}
if p.Integrity != "" {
return user + " / " + p.Integrity
}
return user
}
type ProcessMitigation struct {
Process
DEP string
ASLR string
Strict string
CFG string
}
+228
View File
@@ -0,0 +1,228 @@
//go:build windows
package registry
import (
"fmt"
"syscall"
"unsafe"
wintypes "ferrum/windows/types"
)
const (
hkeyCurrentUser = 0x80000001
hkeyLocalMachine = 0x80000002
keyRead = 0x20019
errorNoMoreItems = 259
)
const HkeyCurrentUser = hkeyCurrentUser
const HkeyLocalMachine = hkeyLocalMachine
var (
advapi32 = syscall.NewLazyDLL("advapi32.dll")
procRegOpenKeyEx = advapi32.NewProc("RegOpenKeyExW")
procRegCloseKey = advapi32.NewProc("RegCloseKey")
procRegEnumKeyEx = advapi32.NewProc("RegEnumKeyExW")
procRegEnumValue = advapi32.NewProc("RegEnumValueW")
procRegQueryValueEx = advapi32.NewProc("RegQueryValueExW")
)
type RegistryValue struct {
Name string
Type uint32
Value string
}
func EnumerateHKCUCLSID() ([]CLSIDEntry, error) {
root, err := openKey(hkeyCurrentUser, `Software\Classes\CLSID`)
if err != nil {
return nil, err
}
defer procRegCloseKey.Call(root)
clsids, err := enumSubkeys(root)
if err != nil {
return nil, err
}
entries := make([]CLSIDEntry, 0)
for _, clsid := range clsids {
for _, kind := range []string{"InprocServer32", "LocalServer32", "TreatAs", "ProgID"} {
subkey, err := openKey(root, clsid+`\`+kind)
if err != nil {
continue
}
value, _ := queryDefaultValue(subkey)
procRegCloseKey.Call(subkey)
entries = append(entries, CLSIDEntry{CLSID: clsid, Kind: kind, Value: value})
}
}
return entries, nil
}
func EnumerateCLSIDProcMonCandidates() ([]CLSIDProcMonCandidate, error) {
root, err := openKey(hkeyLocalMachine, `Software\Classes\CLSID`)
if err != nil {
return nil, err
}
defer procRegCloseKey.Call(root)
clsids, err := enumSubkeys(root)
if err != nil {
return nil, err
}
candidates := make([]CLSIDProcMonCandidate, 0)
for _, clsid := range clsids {
for _, kind := range []string{"InprocServer32", "LocalServer32"} {
machineKey, err := openKey(root, clsid+`\`+kind)
if err != nil {
continue
}
value, _ := queryDefaultValue(machineKey)
procRegCloseKey.Call(machineKey)
userPath := `Software\Classes\CLSID\` + clsid + `\` + kind
if keyExists(hkeyCurrentUser, userPath) {
continue
}
candidates = append(candidates, CLSIDProcMonCandidate{
CLSID: clsid,
Kind: kind,
Path: `HKCU\` + userPath,
Result: "NAME NOT FOUND",
MachineValue: value,
})
}
}
return candidates, nil
}
func openKey(parent uintptr, path string) (uintptr, error) {
pathPtr, err := syscall.UTF16PtrFromString(path)
if err != nil {
return 0, err
}
var handle uintptr
ret, _, callErr := procRegOpenKeyEx.Call(parent, uintptr(unsafe.Pointer(pathPtr)), 0, keyRead, uintptr(unsafe.Pointer(&handle)))
if ret != 0 {
return 0, syscall.Errno(ret)
}
_ = callErr
return handle, nil
}
func OpenKey(parent uintptr, path string) (uintptr, error) {
return openKey(parent, path)
}
func CloseKey(key uintptr) {
procRegCloseKey.Call(key)
}
func enumSubkeys(key uintptr) ([]string, error) {
keys := []string{}
for index := uint32(0); ; index++ {
name := make([]uint16, 256)
length := uint32(len(name))
ret, _, _ := procRegEnumKeyEx.Call(key, uintptr(index), uintptr(unsafe.Pointer(&name[0])), uintptr(unsafe.Pointer(&length)), 0, 0, 0, 0)
if ret == errorNoMoreItems {
break
}
if ret != 0 {
return keys, fmt.Errorf("RegEnumKeyEx: %w", syscall.Errno(ret))
}
keys = append(keys, syscall.UTF16ToString(name[:length]))
}
return keys, nil
}
func EnumSubkeys(key uintptr) ([]string, error) {
return enumSubkeys(key)
}
func queryDefaultValue(key uintptr) (string, error) {
var typ uint32
var needed uint32
ret, _, _ := procRegQueryValueEx.Call(key, 0, 0, uintptr(unsafe.Pointer(&typ)), 0, uintptr(unsafe.Pointer(&needed)))
if ret != 0 || needed == 0 {
return "", syscall.Errno(ret)
}
buffer := make([]uint16, needed/2+1)
ret, _, _ = procRegQueryValueEx.Call(key, 0, 0, uintptr(unsafe.Pointer(&typ)), uintptr(unsafe.Pointer(&buffer[0])), uintptr(unsafe.Pointer(&needed)))
if ret != 0 {
return "", syscall.Errno(ret)
}
return wintypes.CleanRegistryString(syscall.UTF16ToString(buffer)), nil
}
func QueryDefaultValue(key uintptr) (string, error) {
return queryDefaultValue(key)
}
func keyExists(parent uintptr, path string) bool {
key, err := openKey(parent, path)
if err != nil {
return false
}
procRegCloseKey.Call(key)
return true
}
func KeyExists(parent uintptr, path string) bool {
return keyExists(parent, path)
}
func registryValues(parent uintptr, path string) ([]RegistryValue, error) {
key, err := openKey(parent, path)
if err != nil {
return nil, err
}
defer procRegCloseKey.Call(key)
values := []RegistryValue{}
for index := uint32(0); ; index++ {
name := make([]uint16, 512)
nameLen := uint32(len(name))
data := make([]byte, 8192)
dataLen := uint32(len(data))
var typ uint32
ret, _, _ := procRegEnumValue.Call(key, uintptr(index), uintptr(unsafe.Pointer(&name[0])), uintptr(unsafe.Pointer(&nameLen)), 0, uintptr(unsafe.Pointer(&typ)), uintptr(unsafe.Pointer(&data[0])), uintptr(unsafe.Pointer(&dataLen)))
if ret == errorNoMoreItems {
break
}
if ret != 0 {
continue
}
values = append(values, RegistryValue{
Name: syscall.UTF16ToString(name[:nameLen]),
Type: typ,
Value: registryDataString(data[:dataLen], typ),
})
}
return values, nil
}
func Values(parent uintptr, path string) ([]RegistryValue, error) {
return registryValues(parent, path)
}
func registryDataString(data []byte, typ uint32) string {
if len(data) == 0 {
return ""
}
if typ == 1 || typ == 2 {
if len(data)%2 != 0 {
data = data[:len(data)-1]
}
chars := make([]uint16, len(data)/2)
for i := range chars {
chars[i] = uint16(data[i*2]) | uint16(data[i*2+1])<<8
}
return wintypes.CleanRegistryString(syscall.UTF16ToString(chars))
}
return fmt.Sprintf("%x", data)
}
+24
View File
@@ -0,0 +1,24 @@
package registry
type CLSIDEntry struct {
CLSID string
Kind string
Value string
}
type CLSIDProcMonCandidate struct {
CLSID string
Kind string
Path string
Result string
MachineValue string
}
type RegistryAuditFinding struct {
Scope string
Path string
Name string
Value string
Severity string
Reason string
}
+72
View File
@@ -0,0 +1,72 @@
//go:build windows
package scheduled
import (
"encoding/xml"
"os"
"path/filepath"
"sort"
"strings"
wintypes "ferrum/windows/types"
)
type ScheduledTask = wintypes.ScheduledTask
type taskXML struct {
RegistrationInfo struct {
Author string `xml:"Author"`
} `xml:"RegistrationInfo"`
Settings struct {
Enabled string `xml:"Enabled"`
} `xml:"Settings"`
Actions struct {
Exec []struct {
Command string `xml:"Command"`
Arguments string `xml:"Arguments"`
} `xml:"Exec"`
} `xml:"Actions"`
}
func EnumerateScheduledTasks() ([]ScheduledTask, error) {
root := filepath.Join(os.Getenv("WINDIR"), "System32", "Tasks")
if os.Getenv("WINDIR") == "" {
root = `C:\Windows\System32\Tasks`
}
tasks := []ScheduledTask{}
err := filepath.WalkDir(root, func(path string, entry os.DirEntry, err error) error {
if err != nil || entry.IsDir() {
return nil
}
data, err := os.ReadFile(path)
if err != nil {
return nil
}
var parsed taskXML
if err := xml.Unmarshal(data, &parsed); err != nil {
return nil
}
commands := []string{}
for _, exec := range parsed.Actions.Exec {
command := strings.TrimSpace(exec.Command + " " + exec.Arguments)
if command != "" {
commands = append(commands, command)
}
}
rel, _ := filepath.Rel(root, path)
tasks = append(tasks, ScheduledTask{
Path: `\` + strings.ReplaceAll(rel, string(filepath.Separator), `\`),
Command: strings.Join(commands, " | "),
Author: parsed.RegistrationInfo.Author,
Enabled: parsed.Settings.Enabled,
})
return nil
})
if err != nil {
return nil, err
}
sort.Slice(tasks, func(i, j int) bool { return strings.ToLower(tasks[i].Path) < strings.ToLower(tasks[j].Path) })
return tasks, nil
}
+219
View File
@@ -0,0 +1,219 @@
//go:build windows
package services
import (
"fmt"
"sort"
"syscall"
"unsafe"
wintypes "ferrum/windows/types"
)
const (
scManagerEnumerateService = 0x0004
serviceQueryConfig = 0x0001
serviceWin32 = 0x00000030
serviceDriver = 0x0000000b
serviceStateAll = 0x00000003
scEnumProcessInfo = 0
serviceBootStart = 0
serviceSystemStart = 1
serviceAutoStart = 2
serviceDemandStart = 3
serviceDisabled = 4
)
type serviceStatusProcess struct {
ServiceType uint32
CurrentState uint32
ControlsAccepted uint32
Win32ExitCode uint32
ServiceSpecificExitCode uint32
CheckPoint uint32
WaitHint uint32
ProcessID uint32
ServiceFlags uint32
}
type enumServiceStatusProcess struct {
ServiceName *uint16
DisplayName *uint16
Status serviceStatusProcess
}
type queryServiceConfig struct {
ServiceType uint32
StartType uint32
ErrorControl uint32
BinaryPathName *uint16
LoadOrderGroup *uint16
TagID uint32
Dependencies *uint16
ServiceStartName *uint16
DisplayName *uint16
}
type ServiceInfo = wintypes.ServiceInfo
type DriverInfo = wintypes.DriverInfo
var (
advapi32 = syscall.NewLazyDLL("advapi32.dll")
procOpenSCManager = advapi32.NewProc("OpenSCManagerW")
procEnumServicesStatusEx = advapi32.NewProc("EnumServicesStatusExW")
procOpenService = advapi32.NewProc("OpenServiceW")
procQueryServiceConfig = advapi32.NewProc("QueryServiceConfigW")
procCloseServiceHandle = advapi32.NewProc("CloseServiceHandle")
)
func EnumerateServices() ([]ServiceInfo, error) {
services, err := enumerateSCM(serviceWin32)
if err != nil {
return nil, err
}
sort.Slice(services, func(i, j int) bool { return services[i].Name < services[j].Name })
return services, nil
}
func EnumerateDrivers() ([]DriverInfo, error) {
services, err := enumerateSCM(serviceDriver)
if err != nil {
return nil, err
}
drivers := make([]DriverInfo, 0, len(services))
for _, service := range services {
drivers = append(drivers, DriverInfo{
Name: service.Name,
State: service.State,
StartType: service.StartType,
BinaryPath: service.BinaryPath,
})
}
sort.Slice(drivers, func(i, j int) bool { return drivers[i].Name < drivers[j].Name })
return drivers, nil
}
func enumerateSCM(serviceType uint32) ([]ServiceInfo, error) {
manager, _, err := procOpenSCManager.Call(0, 0, scManagerEnumerateService)
if manager == 0 {
return nil, fmt.Errorf("OpenSCManager: %w", err)
}
defer procCloseServiceHandle.Call(manager)
var needed uint32
var count uint32
var resume uint32
procEnumServicesStatusEx.Call(manager, scEnumProcessInfo, uintptr(serviceType), serviceStateAll, 0, 0, uintptr(unsafe.Pointer(&needed)), uintptr(unsafe.Pointer(&count)), uintptr(unsafe.Pointer(&resume)), 0)
if needed == 0 {
return nil, nil
}
buffer := make([]byte, needed)
ret, _, err := procEnumServicesStatusEx.Call(manager, scEnumProcessInfo, uintptr(serviceType), serviceStateAll, uintptr(unsafe.Pointer(&buffer[0])), uintptr(needed), uintptr(unsafe.Pointer(&needed)), uintptr(unsafe.Pointer(&count)), uintptr(unsafe.Pointer(&resume)), 0)
if ret == 0 {
return nil, fmt.Errorf("EnumServicesStatusEx: %w", err)
}
items := make([]ServiceInfo, 0, count)
base := uintptr(unsafe.Pointer(&buffer[0]))
itemSize := unsafe.Sizeof(enumServiceStatusProcess{})
for i := uint32(0); i < count; i++ {
entry := (*enumServiceStatusProcess)(unsafe.Pointer(base + uintptr(i)*itemSize))
info := ServiceInfo{
Name: utf16PtrToString(entry.ServiceName),
DisplayName: utf16PtrToString(entry.DisplayName),
State: serviceStateName(entry.Status.CurrentState),
ProcessID: entry.Status.ProcessID,
ServiceType: entry.Status.ServiceType,
}
if cfg, err := queryService(manager, info.Name); err == nil {
info.StartType = serviceStartName(cfg.StartType)
info.Account = utf16PtrToString(cfg.ServiceStartName)
info.BinaryPath = utf16PtrToString(cfg.BinaryPathName)
if info.DisplayName == "" {
info.DisplayName = utf16PtrToString(cfg.DisplayName)
}
}
items = append(items, info)
}
return items, nil
}
func queryService(manager uintptr, name string) (queryServiceConfig, error) {
namePtr, err := syscall.UTF16PtrFromString(name)
if err != nil {
return queryServiceConfig{}, err
}
service, _, err := procOpenService.Call(manager, uintptr(unsafe.Pointer(namePtr)), serviceQueryConfig)
if service == 0 {
return queryServiceConfig{}, err
}
defer procCloseServiceHandle.Call(service)
var needed uint32
procQueryServiceConfig.Call(service, 0, 0, uintptr(unsafe.Pointer(&needed)))
if needed == 0 {
return queryServiceConfig{}, fmt.Errorf("QueryServiceConfig: no buffer size")
}
buffer := make([]byte, needed)
ret, _, err := procQueryServiceConfig.Call(service, uintptr(unsafe.Pointer(&buffer[0])), uintptr(needed), uintptr(unsafe.Pointer(&needed)))
if ret == 0 {
return queryServiceConfig{}, err
}
return *(*queryServiceConfig)(unsafe.Pointer(&buffer[0])), nil
}
func serviceStateName(state uint32) string {
switch state {
case 1:
return "Stopped"
case 2:
return "StartPending"
case 3:
return "StopPending"
case 4:
return "Running"
case 5:
return "ContinuePending"
case 6:
return "PausePending"
case 7:
return "Paused"
default:
return fmt.Sprintf("State:%d", state)
}
}
func serviceStartName(start uint32) string {
switch start {
case serviceBootStart:
return "Boot"
case serviceSystemStart:
return "System"
case serviceAutoStart:
return "Auto"
case serviceDemandStart:
return "Manual"
case serviceDisabled:
return "Disabled"
default:
return fmt.Sprintf("Start:%d", start)
}
}
func utf16PtrToString(ptr *uint16) string {
if ptr == nil {
return ""
}
var values []uint16
for p := uintptr(unsafe.Pointer(ptr)); ; p += unsafe.Sizeof(*ptr) {
value := *(*uint16)(unsafe.Pointer(p))
if value == 0 {
break
}
values = append(values, value)
}
return syscall.UTF16ToString(values)
}
+80
View File
@@ -0,0 +1,80 @@
//go:build windows
package startup
import (
"os"
"path/filepath"
"sort"
"ferrum/windows/registry"
wintypes "ferrum/windows/types"
)
type StartupEntry = wintypes.StartupEntry
func EnumerateStartupEntries() ([]StartupEntry, error) {
entries := []StartupEntry{}
registryLocations := []struct {
scope uintptr
name string
path string
}{
{registry.HkeyCurrentUser, "User", `Software\Microsoft\Windows\CurrentVersion\Run`},
{registry.HkeyCurrentUser, "User", `Software\Microsoft\Windows\CurrentVersion\RunOnce`},
{registry.HkeyLocalMachine, "Machine", `Software\Microsoft\Windows\CurrentVersion\Run`},
{registry.HkeyLocalMachine, "Machine", `Software\Microsoft\Windows\CurrentVersion\RunOnce`},
{registry.HkeyLocalMachine, "Machine32", `Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Run`},
}
for _, location := range registryLocations {
values, err := registry.Values(location.scope, location.path)
if err != nil {
continue
}
for _, value := range values {
entries = append(entries, StartupEntry{
Scope: location.name,
Location: location.path,
Name: value.Name,
Command: value.Value,
})
}
}
startupFolders := []struct {
scope string
path string
}{
{"User", filepath.Join(os.Getenv("APPDATA"), `Microsoft\Windows\Start Menu\Programs\Startup`)},
{"Machine", filepath.Join(os.Getenv("ProgramData"), `Microsoft\Windows\Start Menu\Programs\Startup`)},
}
for _, folder := range startupFolders {
if folder.path == "" {
continue
}
files, err := os.ReadDir(folder.path)
if err != nil {
continue
}
for _, file := range files {
if file.IsDir() {
continue
}
entries = append(entries, StartupEntry{
Scope: folder.scope,
Location: folder.path,
Name: file.Name(),
Command: filepath.Join(folder.path, file.Name()),
})
}
}
sort.Slice(entries, func(i, j int) bool {
if entries[i].Scope != entries[j].Scope {
return entries[i].Scope < entries[j].Scope
}
return entries[i].Name < entries[j].Name
})
return entries, nil
}
+268
View File
@@ -0,0 +1,268 @@
//go:build windows
package token
import (
"fmt"
"sort"
"syscall"
"unsafe"
wintypes "ferrum/windows/types"
)
const (
processQueryLimitedInformation = 0x1000
tokenQuery = 0x0008
tokenUser = 1
tokenPrivilegesClass = 3
tokenElevation = 20
tokenIntegrity = 25
securityMandatoryUntrustedRID = 0x00000000
securityMandatoryLowRID = 0x00001000
securityMandatoryMediumRID = 0x00002000
securityMandatoryHighRID = 0x00003000
securityMandatorySystemRID = 0x00004000
sePrivilegeEnabled = 0x00000002
)
type sidAndAttributes struct {
Sid uintptr
Attributes uint32
}
type tokenUserStruct struct {
User sidAndAttributes
}
type tokenMandatoryLabel struct {
Label sidAndAttributes
}
type luid struct {
LowPart uint32
HighPart int32
}
type luidAndAttributes struct {
Luid luid
Attributes uint32
}
type tokenPrivilegesHeader struct {
PrivilegeCount uint32
Privileges [1]luidAndAttributes
}
type tokenElevationStruct struct {
TokenIsElevated uint32
}
type TokenInfo = wintypes.TokenInfo
var (
kernel32 = syscall.NewLazyDLL("kernel32.dll")
advapi32 = syscall.NewLazyDLL("advapi32.dll")
procOpenProcess = kernel32.NewProc("OpenProcess")
procOpenProcessToken = advapi32.NewProc("OpenProcessToken")
procGetTokenInformation = advapi32.NewProc("GetTokenInformation")
procConvertSidToStringSid = advapi32.NewProc("ConvertSidToStringSidW")
procLookupAccountSid = advapi32.NewProc("LookupAccountSidW")
procLookupPrivilegeName = advapi32.NewProc("LookupPrivilegeNameW")
procLocalFree = kernel32.NewProc("LocalFree")
procGetLengthSid = advapi32.NewProc("GetLengthSid")
procGetSidSubAuthority = advapi32.NewProc("GetSidSubAuthority")
procGetSidSubAuthorityCnt = advapi32.NewProc("GetSidSubAuthorityCount")
procCloseHandle = kernel32.NewProc("CloseHandle")
)
func InspectProcessToken(pid uint32) (TokenInfo, error) {
process, _, err := procOpenProcess.Call(processQueryLimitedInformation, 0, uintptr(pid))
if process == 0 {
return TokenInfo{}, fmt.Errorf("open process: %w", err)
}
defer procCloseHandle.Call(process)
var token uintptr
ret, _, err := procOpenProcessToken.Call(process, tokenQuery, uintptr(unsafe.Pointer(&token)))
if ret == 0 {
return TokenInfo{}, fmt.Errorf("open token: %w", err)
}
defer procCloseHandle.Call(token)
info := TokenInfo{}
if user, err := tokenUserName(token); err == nil {
info.User = user
}
if elevated, err := tokenElevated(token); err == nil {
info.Elevated = elevated
}
if integrity, err := tokenIntegrityLevel(token); err == nil {
info.Integrity = integrity
}
if privileges, err := tokenPrivileges(token); err == nil {
info.Privileges = privileges
}
return info, nil
}
func tokenUserName(token uintptr) (string, error) {
buffer, err := tokenInfoBuffer(token, tokenUser)
if err != nil {
return "", err
}
user := (*tokenUserStruct)(unsafe.Pointer(&buffer[0]))
if name := lookupAccount(user.User.Sid); name != "" {
return name, nil
}
return sidString(user.User.Sid)
}
func tokenElevated(token uintptr) (bool, error) {
buffer, err := tokenInfoBuffer(token, tokenElevation)
if err != nil {
return false, err
}
elevation := (*tokenElevationStruct)(unsafe.Pointer(&buffer[0]))
return elevation.TokenIsElevated != 0, nil
}
func tokenIntegrityLevel(token uintptr) (string, error) {
buffer, err := tokenInfoBuffer(token, tokenIntegrity)
if err != nil {
return "", err
}
label := (*tokenMandatoryLabel)(unsafe.Pointer(&buffer[0]))
rid := sidLastSubAuthority(label.Label.Sid)
switch {
case rid >= securityMandatorySystemRID:
return "System", nil
case rid >= securityMandatoryHighRID:
return "High", nil
case rid >= securityMandatoryMediumRID:
return "Medium", nil
case rid >= securityMandatoryLowRID:
return "Low", nil
case rid >= securityMandatoryUntrustedRID:
return "Untrusted", nil
default:
return "Unknown", nil
}
}
func tokenPrivileges(token uintptr) ([]string, error) {
buffer, err := tokenInfoBuffer(token, tokenPrivilegesClass)
if err != nil {
return nil, err
}
header := (*tokenPrivilegesHeader)(unsafe.Pointer(&buffer[0]))
count := int(header.PrivilegeCount)
base := uintptr(unsafe.Pointer(&header.Privileges[0]))
size := unsafe.Sizeof(luidAndAttributes{})
names := make([]string, 0, count)
for i := 0; i < count; i++ {
item := (*luidAndAttributes)(unsafe.Pointer(base + uintptr(i)*size))
if item.Attributes&sePrivilegeEnabled == 0 {
continue
}
if name := lookupPrivilege(item.Luid); name != "" {
names = append(names, name)
}
}
sort.Strings(names)
return names, nil
}
func tokenInfoBuffer(token uintptr, class uint32) ([]byte, error) {
var needed uint32
procGetTokenInformation.Call(token, uintptr(class), 0, 0, uintptr(unsafe.Pointer(&needed)))
if needed == 0 {
return nil, fmt.Errorf("GetTokenInformation(%d): no buffer size", class)
}
buffer := make([]byte, needed)
ret, _, err := procGetTokenInformation.Call(token, uintptr(class), uintptr(unsafe.Pointer(&buffer[0])), uintptr(needed), uintptr(unsafe.Pointer(&needed)))
if ret == 0 {
return nil, fmt.Errorf("GetTokenInformation(%d): %w", class, err)
}
return buffer, nil
}
func lookupAccount(sid uintptr) string {
var nameLen uint32
var domainLen uint32
var sidType uint32
procLookupAccountSid.Call(0, sid, 0, uintptr(unsafe.Pointer(&nameLen)), 0, uintptr(unsafe.Pointer(&domainLen)), uintptr(unsafe.Pointer(&sidType)))
if nameLen == 0 {
return ""
}
name := make([]uint16, nameLen)
domain := make([]uint16, domainLen)
ret, _, _ := procLookupAccountSid.Call(0, sid, uintptr(unsafe.Pointer(&name[0])), uintptr(unsafe.Pointer(&nameLen)), uintptr(unsafe.Pointer(&domain[0])), uintptr(unsafe.Pointer(&domainLen)), uintptr(unsafe.Pointer(&sidType)))
if ret == 0 {
return ""
}
n := syscall.UTF16ToString(name)
d := syscall.UTF16ToString(domain)
if d != "" {
return d + `\` + n
}
return n
}
func sidString(sid uintptr) (string, error) {
var out uintptr
ret, _, err := procConvertSidToStringSid.Call(sid, uintptr(unsafe.Pointer(&out)))
if ret == 0 {
return "", err
}
defer procLocalFree.Call(out)
return utf16PtrToString((*uint16)(unsafe.Pointer(out))), nil
}
func lookupPrivilege(id luid) string {
var nameLen uint32
procLookupPrivilegeName.Call(0, uintptr(unsafe.Pointer(&id)), 0, uintptr(unsafe.Pointer(&nameLen)))
if nameLen == 0 {
return ""
}
name := make([]uint16, nameLen+1)
ret, _, _ := procLookupPrivilegeName.Call(0, uintptr(unsafe.Pointer(&id)), uintptr(unsafe.Pointer(&name[0])), uintptr(unsafe.Pointer(&nameLen)))
if ret == 0 {
return ""
}
return syscall.UTF16ToString(name)
}
func sidLastSubAuthority(sid uintptr) uint32 {
countPtr, _, _ := procGetSidSubAuthorityCnt.Call(sid)
if countPtr == 0 {
return 0
}
count := *(*byte)(unsafe.Pointer(countPtr))
if count == 0 {
return 0
}
ridPtr, _, _ := procGetSidSubAuthority.Call(sid, uintptr(count-1))
if ridPtr == 0 {
return 0
}
return *(*uint32)(unsafe.Pointer(ridPtr))
}
func utf16PtrToString(ptr *uint16) string {
if ptr == nil {
return ""
}
var values []uint16
for p := uintptr(unsafe.Pointer(ptr)); ; p += unsafe.Sizeof(*ptr) {
value := *(*uint16)(unsafe.Pointer(p))
if value == 0 {
break
}
values = append(values, value)
}
return syscall.UTF16ToString(values)
}
+7
View File
@@ -0,0 +1,7 @@
package types
import "strings"
func CleanRegistryString(value string) string {
return strings.TrimRight(value, "\x00")
}
+17
View File
@@ -0,0 +1,17 @@
package types
type DLLSearchPathFinding struct {
Path string
Source string
Severity string
Reason string
}
type AdvancedFinding struct {
Area string
Target string
Name string
Value string
Severity string
Reason string
}
+42
View File
@@ -0,0 +1,42 @@
package types
type ServiceInfo struct {
Name string
DisplayName string
State string
StartType string
Account string
BinaryPath string
ProcessID uint32
ServiceType uint32
}
type DriverInfo struct {
Name string
State string
StartType string
BinaryPath string
}
type PipeInfo struct {
Name string
}
type StartupEntry struct {
Scope string
Location string
Name string
Command string
}
type ScheduledTask struct {
Path string
Command string
Author string
Enabled string
}
type EnvVar struct {
Name string
Value string
}
+15
View File
@@ -0,0 +1,15 @@
package types
type TokenInfo struct {
User string
Integrity string
Elevated bool
Privileges []string
}
type PolicyFinding struct {
Name string
Value string
Severity string
Reason string
}