package services import ( "fmt" "rmm-hunter/internal/pkg/hunt/detect/common" . "rmm-hunter/internal/suspicious" "strings" "github.com/Kraken-OffSec/Scurvy/core/service" ) // Whitelist for our own tool and legitimate system components var whitelist = []string{ "rmm-hunter", } func isWhitelisted(config service.ServiceConfig) bool { allText := strings.ToLower(strings.Join([]string{ config.DisplayName, config.ServiceStartName, config.BinaryPathName, config.Description, }, "|")) for _, w := range whitelist { if strings.Contains(allText, w) { return true } } return false } func Detect() []*Service { fmt.Printf("[*] Enumerating Services \n") scm, err := service.Connect() if err != nil { fmt.Printf("[-] Error getting Service Manager: %s\n", err.Error()) return []*Service{} } // Note: The service manager handle is managed by the Scurvy library // and should not be manually closed here to avoid invalid handle errors services, err := scm.ListServices() if err != nil { fmt.Printf("[-] Error enumerating services: %s\n", err.Error()) return []*Service{} } fmt.Printf(" [>] Dispositioning %d Services\n", len(services)) return compareServices(services, scm) } func compareServices(serviceStrings []string, scm *service.Mgr) []*Service { var suspiciousServices []*Service for _, serviceString := range serviceStrings { svc, err := scm.OpenService(serviceString) if err != nil { fmt.Printf(" [>-] Error opening service %s: %s\n", serviceString, err.Error()) continue } // Note: Individual service handles are also managed by Scurvy library config, err := svc.Config() if err != nil { fmt.Printf(" [>-] Error getting service config %s: %s\n", serviceString, err.Error()) continue } // Skip whitelisted services (our own tool) if isWhitelisted(config) { continue } if isSuspiciousService(config) { fmt.Printf(" [?] Found %s\n", config.DisplayName) suspiciousServices = append(suspiciousServices, &Service{ Name: serviceString, DisplayName: config.DisplayName, ServiceTypeRaw: config.ServiceType, ServiceType: getServiceType(config.ServiceType), StartTypeRaw: config.StartType, StartType: getStartType(config.StartType), ErrorControlRaw: config.ErrorControl, ErrorControl: getErrorControl(config.ErrorControl), BinaryPathName: config.BinaryPathName, LoadOrderGroup: config.LoadOrderGroup, TagId: config.TagId, Dependencies: config.Dependencies, ServiceStartName: config.ServiceStartName, Password: config.Password, Description: config.Description, SidType: config.SidType, DelayedAutoStart: config.DelayedAutoStart, }) } } fmt.Printf("[+] Found %d Suspicious Services\n", len(suspiciousServices)) return suspiciousServices } // isSuspiciousService uses multi-indicator scoring to detect RMM services // Requires at least 2 independent indicators to flag as suspicious func isSuspiciousService(config service.ServiceConfig) bool { score := 0 // Build searchable text from all service fields allText := strings.ToLower(strings.Join([]string{ config.DisplayName, config.ServiceStartName, config.BinaryPathName, config.Description, }, "|")) // Indicator 1: Known RMM vendor name match (CommonRMMs) rmmNameHit := false for _, rmm := range common.CommonRMMs { if strings.Contains(allText, strings.ToLower(rmm)) { rmmNameHit = true break } } if rmmNameHit { score++ } // Indicator 2: Known RMM executable/binary pattern in service binary path (CommonImageSuffixes) binaryPatternHit := false binaryPathLower := strings.ToLower(config.BinaryPathName) for _, pattern := range common.CommonImageSuffixes { patternLower := strings.ToLower(pattern) if strings.Contains(binaryPathLower, patternLower) { binaryPatternHit = true break } } if binaryPatternHit { score++ } // Indicator 3: Known RMM DNS/domain in binary path or description (CommonDNS) dnsHit := false for _, dns := range common.CommonDNS { dnsLower := strings.ToLower(dns) // Handle wildcard patterns: *.example.com should match anything.example.com if strings.HasPrefix(dnsLower, "*.") { // Match the domain suffix (e.g., ".example.com") domainSuffix := dnsLower[1:] // Remove the * but keep the dot if strings.Contains(allText, domainSuffix) { dnsHit = true break } } else if strings.HasSuffix(dnsLower, ".*") { // Handle patterns like example.* - match the prefix domainPrefix := dnsLower[:len(dnsLower)-2] // Remove the .* if strings.Contains(allText, domainPrefix) { dnsHit = true break } } else { // Exact domain match (no wildcard) if strings.Contains(allText, dnsLower) { dnsHit = true break } } } if dnsHit { score++ } // Indicator 4: Suspicious installation path (temp, public, programdata) pathSuspicious, _ := common.AnalyzeExecutablePath(config.BinaryPathName) if pathSuspicious { score++ } // Require at least 2 independent Indicators to reduce false positives return score >= 2 } func getServiceType(raw uint32) string { switch raw { case 1: return "KernelDriver" case 2: return "FileSystemDriver" case 4: return "Adapter" case 8: return "RecognizerDriver" case 16: return "Win32OwnProcess" case 32: return "Win32ShareProcess" case 256: return "InteractiveProcess" default: return "Unknown" } } func getStartType(raw uint32) string { switch raw { case 0: return "Boot" case 1: return "System" case 2: return "Automatic" case 3: return "Manual" case 4: return "Disabled" default: return "Unknown" } } func getErrorControl(raw uint32) string { switch raw { case 0: return "Ignore" case 1: return "Normal" case 2: return "Severe" case 3: return "Critical" default: return fmt.Sprintf("Unknown %d", raw) } }