fix: CLSID data truncation & more (1.0.1)

This commit is contained in:
kernelstub
2026-06-14 00:10:43 +02:00
parent c2df7bf07f
commit 8b338dc95e
18 changed files with 175 additions and 59 deletions
+1
View File
@@ -145,6 +145,7 @@ func (m module) Run(ctx *core.Context) error {
ctx.Logger.Info("No findings returned for " + m.name + ".")
return nil
}
ctx.Logger.Info(fmt.Sprintf("%s findings returned: %d", m.name, len(findings)))
for _, finding := range findings {
ctx.Logger.Success(fmt.Sprintf("%s %s > %s", finding.Severity, finding.Target, finding.Reason))
if finding.Name != "" || finding.Value != "" {
+25 -19
View File
@@ -8,13 +8,10 @@ import (
"sync"
"ferrum/core"
"ferrum/internal"
win "ferrum/windows/facade"
)
const maxProcessWorkers = 16
const maxVerboseCLSID = 40
const maxProcMonCandidates = 120
func init() {
core.Register(Module{})
@@ -41,14 +38,16 @@ func (Module) Run(ctx *core.Context) error {
ctx.Logger.Info("Inspecting process security context...")
enriched := enrichProcesses(ctx, processes)
sortProcesses(enriched)
ctx.Logger.Info(fmt.Sprintf("Processes enumerated: %d", len(enriched)))
interesting := 0
for _, process := range enriched {
if !process.Interesting {
continue
}
status := "process"
if process.Interesting {
interesting++
ctx.Logger.Success(fmt.Sprintf("%s[%d] > %s", process.Name, process.PID, process.Label()))
status = "privileged/elevated process"
}
ctx.Logger.Success(fmt.Sprintf("%s[%d] > %s > %s", process.Name, process.PID, status, process.Label()))
if process.Detail != "" {
ctx.Logger.Verbose(fmt.Sprintf("%s[%d] : %s", process.Name, process.PID, process.Detail))
}
@@ -62,6 +61,7 @@ func (Module) Run(ctx *core.Context) error {
if err != nil {
ctx.Logger.Error(fmt.Sprintf("CLSID ProcMon candidate scan: %v", err))
} else {
ctx.Logger.Info(fmt.Sprintf("CLSID ProcMon candidates: %d", len(candidates)))
reportProcMonCandidates(ctx, candidates, interestingProcesses(enriched))
}
@@ -76,11 +76,16 @@ func (Module) Run(ctx *core.Context) error {
ctx.Logger.Info("No HKCU CLSID registrations found.")
return nil
}
ctx.Logger.Info(fmt.Sprintf("HKCU CLSID values enumerated: %d", len(entries)))
for _, entry := range summarizeCLSID(entries) {
ctx.Logger.Success(fmt.Sprintf("HKCU\\Software\\Classes\\CLSID\\%s > %s", entry.CLSID, entry.Kind))
name := entry.Name
if name == "" {
name = "(Default)"
}
ctx.Logger.Success(fmt.Sprintf("%s > %s > %s", entry.Path, name, entry.Kind))
if entry.Value != "" {
ctx.Logger.Verbose(fmt.Sprintf("%s : %s", entry.CLSID, entry.Value))
ctx.Logger.Verbose(fmt.Sprintf("%s : type=%d value=%s", entry.CLSID, entry.Type, entry.Value))
}
}
@@ -106,19 +111,17 @@ func reportProcMonCandidates(ctx *core.Context, candidates []win.CLSIDProcMonCan
processLabel := "NT AUTHORITY\\SYSTEM / privileged COM client"
if len(processes) > 0 {
names := make([]string, 0, len(processes))
for _, process := range internal.Limit(processes, 8) {
for _, process := range processes {
names = append(names, fmt.Sprintf("%s[%d]", process.Name, process.PID))
}
processLabel = strings.Join(names, ", ")
processLabel = fmt.Sprintf("%d privileged/elevated process(es)", len(processes))
ctx.Logger.Info("Privileged/elevated process set: " + strings.Join(names, ", "))
}
for _, candidate := range internal.Limit(candidates, maxProcMonCandidates) {
for _, candidate := range candidates {
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.")
}
@@ -222,7 +225,7 @@ func detail(token win.TokenInfo) string {
parts = append(parts, "elevated=true")
}
if len(token.Privileges) > 0 {
parts = append(parts, "privileges="+strings.Join(internal.Limit(token.Privileges, 8), ","))
parts = append(parts, "privileges="+strings.Join(token.Privileges, ","))
}
return strings.Join(parts, " ")
}
@@ -241,7 +244,10 @@ func summarizeCLSID(entries []win.CLSIDEntry) []win.CLSIDEntry {
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)
if entries[i].Path != entries[j].Path {
return entries[i].Path < entries[j].Path
}
return entries[i].Name < entries[j].Name
})
return entries
}
+2 -2
View File
@@ -4,7 +4,6 @@ import (
"fmt"
"ferrum/core"
"ferrum/internal"
win "ferrum/windows/facade"
)
@@ -24,6 +23,7 @@ func (Module) Run(ctx *core.Context) error {
if err != nil {
return err
}
ctx.Logger.Info(fmt.Sprintf("DLL search entries enumerated: %d", len(findings)))
reported := 0
for _, finding := range findings {
if finding.Severity == "Info" {
@@ -37,7 +37,7 @@ func (Module) Run(ctx *core.Context) error {
if reported == 0 {
ctx.Logger.Info("No risky DLL search path entries matched the default heuristics.")
}
for _, finding := range internal.Limit(findings, 40) {
for _, finding := range findings {
if finding.Severity == "Info" {
ctx.Logger.Verbose(fmt.Sprintf("KnownDLL : %s", finding.Path))
}
+2 -1
View File
@@ -24,8 +24,10 @@ func (Module) Run(ctx *core.Context) error {
if err != nil {
return err
}
ctx.Logger.Info(fmt.Sprintf("Drivers enumerated: %d", len(drivers)))
reported := 0
for _, driver := range drivers {
ctx.Logger.Verbose(fmt.Sprintf("driver inventory : name=%s state=%s start=%s path=%s", driver.Name, driver.State, driver.StartType, driver.BinaryPath))
reason := driverReason(driver)
if reason == "" {
continue
@@ -37,7 +39,6 @@ func (Module) Run(ctx *core.Context) error {
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
}
+7 -1
View File
@@ -24,15 +24,21 @@ func (Module) Run(ctx *core.Context) error {
if err != nil {
return err
}
ctx.Logger.Info(fmt.Sprintf("Environment variables inspected: %d", len(vars)))
reported := 0
for _, env := range vars {
ctx.Logger.Verbose(fmt.Sprintf("environment inventory : %s=%s", env.Name, env.Value))
reason := envReason(env)
if reason == "" {
continue
}
reported++
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)))
if reported == 0 {
ctx.Logger.Info("No environment variables matched the default audit heuristics.")
}
return nil
}
+2 -1
View File
@@ -24,8 +24,10 @@ func (Module) Run(ctx *core.Context) error {
if err != nil {
return err
}
ctx.Logger.Info(fmt.Sprintf("Process mitigations sampled: %d", len(items)))
reported := 0
for _, item := range items {
ctx.Logger.Verbose(fmt.Sprintf("mitigation inventory : name=%s pid=%d DEP=%s ASLR=%s StrictHandle=%s CFG=%s", item.Name, item.PID, item.DEP, item.ASLR, item.Strict, item.CFG))
reason := mitigationReason(item)
if reason == "" {
continue
@@ -37,7 +39,6 @@ func (Module) Run(ctx *core.Context) error {
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
}
+1 -2
View File
@@ -5,7 +5,6 @@ import (
"strings"
"ferrum/core"
"ferrum/internal"
win "ferrum/windows/facade"
)
@@ -35,7 +34,7 @@ func (Module) Run(ctx *core.Context) error {
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) {
for _, pipe := range pipes {
ctx.Logger.Verbose(fmt.Sprintf("pipe : %s", pipe.Name))
}
if reported == 0 {
+1
View File
@@ -23,6 +23,7 @@ func (Module) Run(ctx *core.Context) error {
if err != nil {
return err
}
ctx.Logger.Info(fmt.Sprintf("Policy checks returned: %d", len(findings)))
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))
+1
View File
@@ -27,6 +27,7 @@ func (Module) Run(ctx *core.Context) error {
ctx.Logger.Info("No sensitive registry findings matched the configured checks.")
return nil
}
ctx.Logger.Info(fmt.Sprintf("Registry findings returned: %d", len(findings)))
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 != "" {
+2 -1
View File
@@ -24,8 +24,10 @@ func (Module) Run(ctx *core.Context) error {
if err != nil {
return err
}
ctx.Logger.Info(fmt.Sprintf("Scheduled tasks enumerated: %d", len(tasks)))
reported := 0
for _, task := range tasks {
ctx.Logger.Verbose(fmt.Sprintf("task inventory : path=%s enabled=%s author=%s command=%s", task.Path, task.Enabled, task.Author, task.Command))
reason := taskReason(task)
if reason == "" {
continue
@@ -37,7 +39,6 @@ func (Module) Run(ctx *core.Context) error {
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
}
+3 -3
View File
@@ -5,7 +5,6 @@ import (
"strings"
"ferrum/core"
"ferrum/internal"
win "ferrum/windows/facade"
)
@@ -25,8 +24,10 @@ func (Module) Run(ctx *core.Context) error {
if err != nil {
return err
}
ctx.Logger.Info(fmt.Sprintf("Services enumerated: %d", len(services)))
reported := 0
for _, service := range services {
ctx.Logger.Verbose(fmt.Sprintf("service inventory : name=%s display=%s state=%s start=%s account=%s pid=%d type=%d path=%s", service.Name, service.DisplayName, service.State, service.StartType, service.Account, service.ProcessID, service.ServiceType, service.BinaryPath))
reasons := serviceReasons(service)
if len(reasons) == 0 {
continue
@@ -38,7 +39,6 @@ func (Module) Run(ctx *core.Context) error {
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
}
@@ -58,7 +58,7 @@ func serviceReasons(service win.ServiceInfo) []string {
if service.StartType == "Auto" && service.State != "Running" {
reasons = append(reasons, "auto-start not running")
}
return internal.Limit(reasons, 4)
return reasons
}
func isUnquotedPathWithSpaces(path string) bool {
+2
View File
@@ -24,11 +24,13 @@ func (Module) Run(ctx *core.Context) error {
if err != nil {
return err
}
ctx.Logger.Info(fmt.Sprintf("Startup entries enumerated: %d", len(entries)))
if len(entries) == 0 {
ctx.Logger.Info("No startup entries found in common locations.")
return nil
}
for _, entry := range entries {
ctx.Logger.Verbose(fmt.Sprintf("startup inventory : scope=%s location=%s name=%s command=%s", entry.Scope, entry.Location, entry.Name, entry.Command))
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))
}
+11 -4
View File
@@ -8,7 +8,6 @@ import (
"sync"
"ferrum/core"
"ferrum/internal"
win "ferrum/windows/facade"
)
@@ -35,19 +34,24 @@ func (Module) Run(ctx *core.Context) error {
}
return findings[i].Name < findings[j].Name
})
ctx.Logger.Info(fmt.Sprintf("Process tokens inspected: %d", len(findings)))
reported := 0
for _, finding := range findings {
if finding.Error != "" {
ctx.Logger.Verbose(fmt.Sprintf("%s[%d] : token inspection failed: %s", finding.Name, finding.PID, finding.Error))
continue
}
ctx.Logger.Verbose(fmt.Sprintf("token inventory : name=%s pid=%d %s privileges=%s", finding.Name, finding.PID, finding.Process.Label(), strings.Join(finding.Privileges, ",")))
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), ",")))
ctx.Logger.Verbose(fmt.Sprintf("%s[%d] : %s privileges=%s", finding.Name, finding.PID, finding.Process.Label(), strings.Join(finding.Privileges, ",")))
}
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
}
@@ -55,6 +59,7 @@ type tokenFinding struct {
win.Process
Reasons []string
Score int
Error string
}
func inspect(processes []win.Process) []tokenFinding {
@@ -78,6 +83,8 @@ func inspect(processes []win.Process) []tokenFinding {
finding.Elevated = token.Elevated
finding.Privileges = token.Privileges
finding.Reasons, finding.Score = tokenReasons(token)
} else {
finding.Error = err.Error()
}
results <- finding
}
@@ -130,5 +137,5 @@ func tokenReasons(token win.TokenInfo) ([]string, int) {
score += points
}
}
return internal.Limit(reasons, 8), score
return reasons, score
}
+1 -3
View File
@@ -10,8 +10,6 @@ import (
wintypes "ferrum/windows/types"
)
const advancedLimit = 120
type AdvancedFinding = wintypes.AdvancedFinding
func EnumerateAdvancedFindings(check string) ([]AdvancedFinding, error) {
@@ -84,5 +82,5 @@ func EnumerateAdvancedFindings(check string) ([]AdvancedFinding, error) {
findings = genericSurfaceFindings(check)
}
sortAdvanced(findings)
return limitAdvanced(findings), nil
return findings, nil
}
-3
View File
@@ -159,9 +159,6 @@ func protocolFindings() []AdvancedFinding {
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
-7
View File
@@ -146,10 +146,3 @@ func sortAdvanced(findings []AdvancedFinding) {
return findings[i].Target < findings[j].Target
})
}
func limitAdvanced(findings []AdvancedFinding) []AdvancedFinding {
if len(findings) <= advancedLimit {
return findings
}
return findings[:advancedLimit]
}
+112 -13
View File
@@ -4,6 +4,7 @@ package registry
import (
"fmt"
"strings"
"syscall"
"unsafe"
@@ -26,6 +27,7 @@ var (
procRegCloseKey = advapi32.NewProc("RegCloseKey")
procRegEnumKeyEx = advapi32.NewProc("RegEnumKeyExW")
procRegEnumValue = advapi32.NewProc("RegEnumValueW")
procRegQueryInfoKey = advapi32.NewProc("RegQueryInfoKeyW")
procRegQueryValueEx = advapi32.NewProc("RegQueryValueExW")
)
@@ -36,7 +38,8 @@ type RegistryValue struct {
}
func EnumerateHKCUCLSID() ([]CLSIDEntry, error) {
root, err := openKey(hkeyCurrentUser, `Software\Classes\CLSID`)
const base = `Software\Classes\CLSID`
root, err := openKey(hkeyCurrentUser, base)
if err != nil {
return nil, err
}
@@ -49,19 +52,58 @@ func EnumerateHKCUCLSID() ([]CLSIDEntry, error) {
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})
}
clsidPath := clsid
walkCLSIDKey(root, base, clsid, clsidPath, &entries)
}
return entries, nil
}
func walkCLSIDKey(parent uintptr, base, clsid, relative string, entries *[]CLSIDEntry) {
key, err := openKey(parent, relative)
if err != nil {
return
}
defer procRegCloseKey.Call(key)
path := base + `\` + relative
kind := clsidKind(relative)
values, err := enumValues(key)
if err == nil {
for _, value := range values {
*entries = append(*entries, CLSIDEntry{
CLSID: clsid,
Kind: kind,
Path: `HKCU\` + path,
Name: displayRegistryName(value.Name),
Type: value.Type,
Value: value.Value,
})
}
}
subkeyNames, err := enumSubkeys(key)
if err != nil {
return
}
for _, subkey := range subkeyNames {
walkCLSIDKey(parent, base, clsid, relative+`\`+subkey, entries)
}
}
func clsidKind(relative string) string {
parts := strings.Split(relative, `\`)
if len(parts) < 2 {
return "(CLSID)"
}
return parts[1]
}
func displayRegistryName(name string) string {
if name == "" {
return "(Default)"
}
return name
}
func EnumerateCLSIDProcMonCandidates() ([]CLSIDProcMonCandidate, error) {
root, err := openKey(hkeyLocalMachine, `Software\Classes\CLSID`)
if err != nil {
@@ -124,9 +166,17 @@ func CloseKey(key uintptr) {
}
func enumSubkeys(key uintptr) ([]string, error) {
info, err := queryKeyInfo(key)
if err != nil {
return nil, err
}
nameCapacity := info.MaxSubKeyLen + 1
if nameCapacity < 256 {
nameCapacity = 256
}
keys := []string{}
for index := uint32(0); ; index++ {
name := make([]uint16, 256)
name := make([]uint16, nameCapacity)
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 {
@@ -183,11 +233,28 @@ func registryValues(parent uintptr, path string) ([]RegistryValue, error) {
}
defer procRegCloseKey.Call(key)
return enumValues(key)
}
func enumValues(key uintptr) ([]RegistryValue, error) {
info, err := queryKeyInfo(key)
if err != nil {
return nil, err
}
nameCapacity := info.MaxValueNameLen + 1
if nameCapacity < 512 {
nameCapacity = 512
}
dataCapacity := info.MaxValueLen
if dataCapacity < 8192 {
dataCapacity = 8192
}
values := []RegistryValue{}
for index := uint32(0); ; index++ {
name := make([]uint16, 512)
name := make([]uint16, nameCapacity)
nameLen := uint32(len(name))
data := make([]byte, 8192)
data := make([]byte, dataCapacity)
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)))
@@ -226,3 +293,35 @@ func registryDataString(data []byte, typ uint32) string {
}
return fmt.Sprintf("%x", data)
}
type keyInfo struct {
MaxSubKeyLen uint32
MaxValueNameLen uint32
MaxValueLen uint32
}
func queryKeyInfo(key uintptr) (keyInfo, error) {
var subKeys uint32
var maxSubKeyLen uint32
var values uint32
var maxValueNameLen uint32
var maxValueLen uint32
ret, _, _ := procRegQueryInfoKey.Call(
key,
0,
0,
0,
uintptr(unsafe.Pointer(&subKeys)),
uintptr(unsafe.Pointer(&maxSubKeyLen)),
0,
uintptr(unsafe.Pointer(&values)),
uintptr(unsafe.Pointer(&maxValueNameLen)),
uintptr(unsafe.Pointer(&maxValueLen)),
0,
0,
)
if ret != 0 {
return keyInfo{}, syscall.Errno(ret)
}
return keyInfo{MaxSubKeyLen: maxSubKeyLen, MaxValueNameLen: maxValueNameLen, MaxValueLen: maxValueLen}, nil
}
+3
View File
@@ -3,6 +3,9 @@ package registry
type CLSIDEntry struct {
CLSID string
Kind string
Path string
Name string
Type uint32
Value string
}