mirror of
https://github.com/kernelstub/Ferrum
synced 2026-06-21 13:55:09 +00:00
248 lines
7.1 KiB
Go
248 lines
7.1 KiB
Go
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)
|
|
}
|