Improve AutoRun and service detection with enhanced vendor/token matching, reduced false positives, and isolated changes
This commit is contained in:
@@ -9,12 +9,65 @@ import (
|
|||||||
"github.com/Kraken-OffSec/Scurvy/core/autoruns"
|
"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 {
|
func Detect() []AutoRun {
|
||||||
var suspiciousAutoRuns []AutoRun
|
var suspiciousAutoRuns []AutoRun
|
||||||
|
|
||||||
fmt.Printf("[*] Enumerating AutoRun Applications\n")
|
fmt.Printf("[*] Enumerating AutoRun Applications\n")
|
||||||
|
|
||||||
// Use Scurvy to enumerate autoruns from multiple sources
|
// Enumerate autoruns from Registry and COM Services
|
||||||
autoRuns := autoruns.GetAllAutoruns()
|
autoRuns := autoruns.GetAllAutoruns()
|
||||||
fmt.Printf(" [>] Dispositioning %d AutoRun Entries\n", len(autoRuns))
|
fmt.Printf(" [>] Dispositioning %d AutoRun Entries\n", len(autoRuns))
|
||||||
|
|
||||||
@@ -43,50 +96,51 @@ func Detect() []AutoRun {
|
|||||||
return suspiciousAutoRuns
|
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
|
// checking image path/name, location, entry and launch string against
|
||||||
// common RMM indicators and suspicious image suffixes. It also flags
|
// common RMM indicators and suspicious image suffixes. It also flags
|
||||||
// suspicious installation paths.
|
// suspicious installation paths.
|
||||||
func isSuspiciousAutoRunEntry(ar AutoRun) bool {
|
func isSuspiciousAutoRunEntry(ar AutoRun) bool {
|
||||||
// Prepare lowercase fields for matching
|
// Build a single string of fields we care about
|
||||||
fields := []string{
|
joined := strings.ToLower(strings.Join([]string{ar.ImageName, ar.ImagePath, ar.Entry, ar.LaunchString}, "|"))
|
||||||
strings.ToLower(ar.ImageName),
|
|
||||||
strings.ToLower(ar.ImagePath),
|
|
||||||
strings.ToLower(ar.Location),
|
|
||||||
strings.ToLower(ar.LaunchString),
|
|
||||||
strings.ToLower(ar.Entry),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Match against known RMM names/keywords
|
// 1) Vendor token hit (filter out generic words)
|
||||||
for _, rmm := range common.CommonRMMs {
|
vendorHit := false
|
||||||
r := strings.ToLower(rmm)
|
for _, tok := range common.CommonRMMs {
|
||||||
for _, f := range fields {
|
if !isNonGenericToken(tok) {
|
||||||
if strings.Contains(f, r) {
|
continue
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
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)
|
imgPathLower := strings.ToLower(ar.ImagePath)
|
||||||
imgNameLower := strings.ToLower(ar.ImageName)
|
imgNameLower := strings.ToLower(ar.ImageName)
|
||||||
for _, suf := range common.CommonImageSuffixes {
|
for _, suf := range common.CommonImageSuffixes {
|
||||||
s := strings.ToLower(suf)
|
s := strings.ToLower(suf)
|
||||||
if strings.Contains(imgPathLower, s) || strings.Contains(imgNameLower, s) {
|
if strings.Contains(imgPathLower, s) || strings.Contains(imgNameLower, s) {
|
||||||
return true
|
suffixHit = true
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Suspicious installation paths
|
// 3) Known vendor DNS in launch string/command
|
||||||
if suspicious, _ := common.AnalyzeExecutablePath(ar.ImagePath); suspicious {
|
dnsHit := false
|
||||||
return true
|
ls := strings.ToLower(ar.LaunchString)
|
||||||
}
|
for _, d := range common.CommonDNS {
|
||||||
// Consider launch string as a command line too
|
if strings.Contains(ls, strings.ToLower(d)) {
|
||||||
if ar.LaunchString != "" {
|
dnsHit = true
|
||||||
if suspicious, _ := common.AnalyzeExecutablePath(ar.LaunchString); suspicious {
|
break
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Require two independent signals to reduce false positives
|
||||||
|
if (vendorHit && (suffixHit || dnsHit)) || (suffixHit && dnsHit) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,25 @@ import (
|
|||||||
"golang.org/x/sys/windows"
|
"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 {
|
func Detect() []*Service {
|
||||||
fmt.Printf("[*] Enumerating Services \n")
|
fmt.Printf("[*] Enumerating Services \n")
|
||||||
|
|
||||||
@@ -52,6 +71,9 @@ func compareServices(serviceStrings []string, scm *service.Mgr) []*Service {
|
|||||||
// Check against known RMMs
|
// Check against known RMMs
|
||||||
isRMMMatch := false
|
isRMMMatch := false
|
||||||
for _, rmm := range common.CommonRMMs {
|
for _, rmm := range common.CommonRMMs {
|
||||||
|
if !isNonGenericToken(rmm) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
rmmLower := strings.ToLower(rmm)
|
rmmLower := strings.ToLower(rmm)
|
||||||
if strings.Contains(svcDisplayName, rmmLower) || strings.Contains(svcStartName, rmmLower) || strings.Contains(svcBinaryPath, rmmLower) {
|
if strings.Contains(svcDisplayName, rmmLower) || strings.Contains(svcStartName, rmmLower) || strings.Contains(svcBinaryPath, rmmLower) {
|
||||||
isRMMMatch = true
|
isRMMMatch = true
|
||||||
@@ -59,15 +81,8 @@ func compareServices(serviceStrings []string, scm *service.Mgr) []*Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for suspicious path regardless of RMM match
|
// Only flag when there is a positive RMM vendor token match
|
||||||
isPathSuspicious, pathReason := common.AnalyzeExecutablePath(config.BinaryPathName)
|
if isRMMMatch {
|
||||||
|
|
||||||
if isRMMMatch || isPathSuspicious {
|
|
||||||
description := config.Description
|
|
||||||
if isPathSuspicious {
|
|
||||||
description += fmt.Sprintf(" [%s]", pathReason)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf(" [?] Found %s\n", config.DisplayName)
|
fmt.Printf(" [?] Found %s\n", config.DisplayName)
|
||||||
suspiciousServices = append(suspiciousServices, &Service{
|
suspiciousServices = append(suspiciousServices, &Service{
|
||||||
Name: serviceString,
|
Name: serviceString,
|
||||||
@@ -84,7 +99,7 @@ func compareServices(serviceStrings []string, scm *service.Mgr) []*Service {
|
|||||||
Dependencies: config.Dependencies,
|
Dependencies: config.Dependencies,
|
||||||
ServiceStartName: config.ServiceStartName,
|
ServiceStartName: config.ServiceStartName,
|
||||||
Password: config.Password,
|
Password: config.Password,
|
||||||
Description: description,
|
Description: config.Description,
|
||||||
SidType: config.SidType,
|
SidType: config.SidType,
|
||||||
DelayedAutoStart: config.DelayedAutoStart,
|
DelayedAutoStart: config.DelayedAutoStart,
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user