From e8356296432ee0635a408bd7a0851fcb29be28d3 Mon Sep 17 00:00:00 2001 From: Evan Hosinski Date: Sat, 11 Oct 2025 15:26:42 -0400 Subject: [PATCH] Improve AutoRun and service detection with enhanced vendor/token matching, reduced false positives, and isolated changes --- internal/pkg/hunt/detect/autorun/autorun.go | 110 +++++++++++++----- internal/pkg/hunt/detect/services/services.go | 35 ++++-- 2 files changed, 107 insertions(+), 38 deletions(-) diff --git a/internal/pkg/hunt/detect/autorun/autorun.go b/internal/pkg/hunt/detect/autorun/autorun.go index 0c065b3..80d7889 100644 --- a/internal/pkg/hunt/detect/autorun/autorun.go +++ b/internal/pkg/hunt/detect/autorun/autorun.go @@ -9,12 +9,65 @@ import ( "github.com/Kraken-OffSec/Scurvy/core/autoruns" ) +// basic allow/deny helpers kept local to keep changes scoped +var systemDirs = []string{"\\windows\\system32\\", "\\windows\\syswow64\\", "\\windows\\", "\\program files\\windowsapps\\"} +var safeSystemBinaries = []string{ + "svchost.exe", "lsass.exe", "services.exe", "winlogon.exe", "explorer.exe", + "ctfmon.exe", "spoolsv.exe", "dwm.exe", "smss.exe", "csrss.exe", + "runtimebroker.exe", "shellexperiencehost.exe", "searchui.exe", "sihost.exe", + "taskhostw.exe", "wininit.exe", "rdpclip.exe", +} +var genericTokens = map[string]struct{}{ + "remote": {}, "control": {}, "support": {}, "assist": {}, "viewer": {}, + "server": {}, "service": {}, "manager": {}, "desktop": {}, "host": {}, + "client": {}, "agent": {}, "connect": {}, "access": {}, "admin": {}, + "vpn": {}, "ssh": {}, "vnc": {}, "rdp": {}, "microsoft": {}, "windows": {}, +} + +func inSlice(slice []string, v string) bool { + v = strings.ToLower(v) + for _, s := range slice { + if strings.ToLower(s) == v { + return true + } + } + return false +} +func containsAny(haystack string, needles []string) bool { + h := strings.ToLower(haystack) + for _, n := range needles { + if strings.Contains(h, strings.ToLower(n)) { + return true + } + } + return false +} +func isInSystemDir(p string) bool { + pl := strings.ToLower(p) + for _, d := range systemDirs { + if strings.Contains(pl, d) { + return true + } + } + return false +} +func isNonGenericToken(t string) bool { + t = strings.ToLower(strings.TrimSpace(t)) + if len(t) < 4 { + return false + } + if _, ok := genericTokens[t]; ok { + return false + } + return true +} + func Detect() []AutoRun { var suspiciousAutoRuns []AutoRun fmt.Printf("[*] Enumerating AutoRun Applications\n") - // Use Scurvy to enumerate autoruns from multiple sources + // Enumerate autoruns from Registry and COM Services autoRuns := autoruns.GetAllAutoruns() fmt.Printf(" [>] Dispositioning %d AutoRun Entries\n", len(autoRuns)) @@ -43,50 +96,51 @@ func Detect() []AutoRun { return suspiciousAutoRuns } -// isSuspiciousAutoRunEntry determines if an autorun looks like an RMM by +// isSuspiciousAutoRunEntry determines if an autorun appears to be an RMM by // checking image path/name, location, entry and launch string against // common RMM indicators and suspicious image suffixes. It also flags // suspicious installation paths. func isSuspiciousAutoRunEntry(ar AutoRun) bool { - // Prepare lowercase fields for matching - fields := []string{ - strings.ToLower(ar.ImageName), - strings.ToLower(ar.ImagePath), - strings.ToLower(ar.Location), - strings.ToLower(ar.LaunchString), - strings.ToLower(ar.Entry), - } + // Build a single string of fields we care about + joined := strings.ToLower(strings.Join([]string{ar.ImageName, ar.ImagePath, ar.Entry, ar.LaunchString}, "|")) - // Match against known RMM names/keywords - for _, rmm := range common.CommonRMMs { - r := strings.ToLower(rmm) - for _, f := range fields { - if strings.Contains(f, r) { - return true - } + // 1) Vendor token hit (filter out generic words) + vendorHit := false + for _, tok := range common.CommonRMMs { + if !isNonGenericToken(tok) { + continue + } + if strings.Contains(joined, strings.ToLower(tok)) { + vendorHit = true + break } } - // Match against common suspicious image suffix/patterns (path or name) + // 2) Known image suffix/file pattern hit (robust to registry naming) + suffixHit := false imgPathLower := strings.ToLower(ar.ImagePath) imgNameLower := strings.ToLower(ar.ImageName) for _, suf := range common.CommonImageSuffixes { s := strings.ToLower(suf) if strings.Contains(imgPathLower, s) || strings.Contains(imgNameLower, s) { - return true + suffixHit = true + break } } - // Suspicious installation paths - if suspicious, _ := common.AnalyzeExecutablePath(ar.ImagePath); suspicious { + // 3) Known vendor DNS in launch string/command + dnsHit := false + ls := strings.ToLower(ar.LaunchString) + for _, d := range common.CommonDNS { + if strings.Contains(ls, strings.ToLower(d)) { + dnsHit = true + break + } + } + + // Require two independent signals to reduce false positives + if (vendorHit && (suffixHit || dnsHit)) || (suffixHit && dnsHit) { return true } - // Consider launch string as a command line too - if ar.LaunchString != "" { - if suspicious, _ := common.AnalyzeExecutablePath(ar.LaunchString); suspicious { - return true - } - } - return false } diff --git a/internal/pkg/hunt/detect/services/services.go b/internal/pkg/hunt/detect/services/services.go index 7c37c5a..302d09d 100644 --- a/internal/pkg/hunt/detect/services/services.go +++ b/internal/pkg/hunt/detect/services/services.go @@ -10,6 +10,25 @@ import ( "golang.org/x/sys/windows" ) +// ignore overly-generic tokens when matching vendor names +var genericTokens = map[string]struct{}{ + "remote": {}, "control": {}, "support": {}, "assist": {}, "viewer": {}, + "server": {}, "service": {}, "manager": {}, "desktop": {}, "host": {}, + "client": {}, "agent": {}, "connect": {}, "access": {}, "admin": {}, + "vpn": {}, "ssh": {}, "vnc": {}, "rdp": {}, "microsoft": {}, "windows": {}, +} + +func isNonGenericToken(t string) bool { + t = strings.ToLower(strings.TrimSpace(t)) + if len(t) < 4 { + return false + } + if _, ok := genericTokens[t]; ok { + return false + } + return true +} + func Detect() []*Service { fmt.Printf("[*] Enumerating Services \n") @@ -52,6 +71,9 @@ func compareServices(serviceStrings []string, scm *service.Mgr) []*Service { // Check against known RMMs isRMMMatch := false for _, rmm := range common.CommonRMMs { + if !isNonGenericToken(rmm) { + continue + } rmmLower := strings.ToLower(rmm) if strings.Contains(svcDisplayName, rmmLower) || strings.Contains(svcStartName, rmmLower) || strings.Contains(svcBinaryPath, rmmLower) { isRMMMatch = true @@ -59,15 +81,8 @@ func compareServices(serviceStrings []string, scm *service.Mgr) []*Service { } } - // Check for suspicious path regardless of RMM match - isPathSuspicious, pathReason := common.AnalyzeExecutablePath(config.BinaryPathName) - - if isRMMMatch || isPathSuspicious { - description := config.Description - if isPathSuspicious { - description += fmt.Sprintf(" [%s]", pathReason) - } - + // Only flag when there is a positive RMM vendor token match + if isRMMMatch { fmt.Printf(" [?] Found %s\n", config.DisplayName) suspiciousServices = append(suspiciousServices, &Service{ Name: serviceString, @@ -84,7 +99,7 @@ func compareServices(serviceStrings []string, scm *service.Mgr) []*Service { Dependencies: config.Dependencies, ServiceStartName: config.ServiceStartName, Password: config.Password, - Description: description, + Description: config.Description, SidType: config.SidType, DelayedAutoStart: config.DelayedAutoStart, })