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