Refactor AutoRun detection to use Scurvy library, enhance suspicious entry checks, and update UI rendering for detailed info

This commit is contained in:
Evan Hosinski
2025-10-11 15:15:35 -04:00
parent 02ed2ce046
commit 53f527feff
8 changed files with 149 additions and 150 deletions
+54 -133
View File
@@ -6,7 +6,7 @@ import (
. "rmm-hunter/internal/suspicious"
"strings"
"golang.org/x/sys/windows/registry"
"github.com/Kraken-OffSec/Scurvy/core/autoruns"
)
func Detect() []AutoRun {
@@ -14,158 +14,79 @@ func Detect() []AutoRun {
fmt.Printf("[*] Enumerating AutoRun Applications\n")
// Check common autorun registry locations
autorunKeys := []string{
`SOFTWARE\Microsoft\Windows\CurrentVersion\Run`,
`SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce`,
`SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Run`,
`SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\RunOnce`,
`SOFTWARE\Microsoft\Windows\CurrentVersion\RunServices`,
`SOFTWARE\Microsoft\Windows\CurrentVersion\RunServicesOnce`,
`SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer\Run`,
`SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\Userinit`,
`SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\Shell`,
`SOFTWARE\Microsoft\Active Setup\Installed Components`,
}
// Use Scurvy to enumerate autoruns from multiple sources
autoRuns := autoruns.GetAllAutoruns()
fmt.Printf(" [>] Dispositioning %d AutoRun Entries\n", len(autoRuns))
// Check both HKLM and HKCU
roots := []registry.Key{registry.LOCAL_MACHINE, registry.CURRENT_USER}
rootNames := []string{"HKLM", "HKCU"}
for _, ar := range autoRuns {
// Map Scurvy autorun to our Suspicious.AutoRun struct
sar := AutoRun{
Type: ar.Type,
Location: ar.Location,
ImagePath: ar.ImagePath,
ImageName: ar.ImageName,
Arguments: ar.Arguments,
MD5: ar.MD5,
SHA1: ar.SHA1,
SHA256: ar.SHA256,
Entry: ar.Entry,
LaunchString: ar.LaunchString,
}
fmt.Printf(" [>] Dispositioning %d AutoRun Keys\n", len(autorunKeys)*len(roots))
totalEntries := 0
for i, root := range roots {
for _, keyPath := range autorunKeys {
entries := checkAutoRunKey(root, keyPath, rootNames[i])
totalEntries += len(entries)
suspiciousAutoRuns = append(suspiciousAutoRuns, entries...)
if isSuspiciousAutoRunEntry(sar) {
fmt.Printf(" [?] Found %s | %s | %s\n", sar.Location, sar.Entry, sar.ImagePath)
suspiciousAutoRuns = append(suspiciousAutoRuns, sar)
}
}
fmt.Printf("[+] Found %d Suspicious AutoRun Applications\n", len(suspiciousAutoRuns))
return suspiciousAutoRuns
}
func checkAutoRunKey(root registry.Key, keyPath, rootName string) []AutoRun {
var autoRuns []AutoRun
key, err := registry.OpenKey(root, keyPath, registry.QUERY_VALUE)
if err != nil {
return autoRuns
}
defer key.Close()
valueNames, err := key.ReadValueNames(-1)
if err != nil {
return autoRuns
// isSuspiciousAutoRunEntry determines if an autorun looks like 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),
}
for _, valueName := range valueNames {
value, _, err := key.GetStringValue(valueName)
if err != nil {
continue
}
// Check if this autorun entry matches any known Suspicious patterns
if isSuspiciousAutoRun(valueName, value) {
// Analyze the executable path for additional suspicious indicators
isPathSuspicious, pathReason := analyzeExecutablePath(value)
description := extractDescription(value)
if isPathSuspicious {
description += fmt.Sprintf(" [%s]", pathReason)
}
fmt.Printf(" [?] Found %s\\%s: %s = %s\n", rootName, keyPath, valueName, value)
autoRuns = append(autoRuns, AutoRun{
Name: valueName,
Command: value,
Location: fmt.Sprintf("%s\\%s", rootName, keyPath),
Enabled: true,
Description: description,
})
}
}
return autoRuns
}
func isSuspiciousAutoRun(name, command string) bool {
// Convert to lowercase for case-insensitive comparison
nameLower := strings.ToLower(name)
commandLower := strings.ToLower(command)
// Check against known Suspicious names
// Match against known RMM names/keywords
for _, rmm := range common.CommonRMMs {
rmmLower := strings.ToLower(rmm)
if strings.Contains(nameLower, rmmLower) || strings.Contains(commandLower, rmmLower) {
r := strings.ToLower(rmm)
for _, f := range fields {
if strings.Contains(f, r) {
return true
}
}
}
// Match against common suspicious image suffix/patterns (path or name)
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
}
}
// Check against common Suspicious executable patterns
for _, imageEnd := range common.CommonImageSuffixes {
imageEndLower := strings.ToLower(imageEnd)
if strings.Contains(commandLower, imageEndLower) {
return true
}
// Suspicious installation paths
if suspicious, _ := common.AnalyzeExecutablePath(ar.ImagePath); suspicious {
return true
}
// Additional suspicious patterns
suspiciousPatterns := []string{
"remote", "control", "assist", "support", "vnc", "rdp", "teamview",
"anydesk", "logmein", "screenconnect", "splashtop", "ultravnc",
}
for _, pattern := range suspiciousPatterns {
if strings.Contains(nameLower, pattern) || strings.Contains(commandLower, pattern) {
// Consider launch string as a command line too
if ar.LaunchString != "" {
if suspicious, _ := common.AnalyzeExecutablePath(ar.LaunchString); suspicious {
return true
}
}
return false
}
func extractDescription(command string) string {
// Extract just the executable name from the command
parts := strings.Fields(command)
if len(parts) > 0 {
return parts[0]
}
return command
}
func analyzeExecutablePath(command string) (bool, string) {
// Extract executable path from command
var execPath string
if strings.HasPrefix(command, "\"") {
// Handle quoted paths
endQuote := strings.Index(command[1:], "\"")
if endQuote != -1 {
execPath = command[1 : endQuote+1]
}
} else {
// Handle unquoted paths
parts := strings.Fields(command)
if len(parts) > 0 {
execPath = parts[0]
}
}
// Check for suspicious installation paths
suspiciousPaths := []string{
"\\temp\\", "\\tmp\\", "\\appdata\\local\\temp\\",
"\\users\\public\\", "\\programdata\\",
"\\windows\\temp\\", "\\%temp%\\",
}
execPathLower := strings.ToLower(execPath)
for _, suspPath := range suspiciousPaths {
if strings.Contains(execPathLower, suspPath) {
return true, fmt.Sprintf("Suspicious installation path: %s", suspPath)
}
}
return false, ""
}
@@ -6,11 +6,14 @@ func TestAutoRun(t *testing.T) {
autoruns := Detect()
for _, ar := range autoruns {
t.Logf("-----")
t.Logf("Name: %s", ar.Name)
t.Logf("Command: %s", ar.Command)
t.Logf("Type: %s", ar.Type)
t.Logf("Entry: %s", ar.Entry)
t.Logf("Location: %s", ar.Location)
t.Logf("Enabled: %t", ar.Enabled)
t.Logf("Description: %s", ar.Description)
t.Logf("Image: %s", ar.ImagePath)
t.Logf("Args: %s", ar.Arguments)
t.Logf("MD5: %s", ar.MD5)
t.Logf("SHA1: %s", ar.SHA1)
t.Logf("SHA256: %s", ar.SHA256)
t.Logf("-----")
}
}
+7 -4
View File
@@ -434,11 +434,14 @@ const htmlTemplate = `<!DOCTYPE html>
{{if .Findings.AutoRuns}}
{{range .Findings.AutoRuns}}
<div class="item">
<div class="item-title">{{.Name}}</div>
<div class="item-detail"><strong>Command:</strong> {{.Command}}</div>
<div class="item-title">{{.ImageName}}</div>
<div class="item-detail"><strong>Entry:</strong> {{.Entry}}</div>
<div class="item-detail"><strong>Type:</strong> {{.Type}}</div>
<div class="item-detail"><strong>Location:</strong> {{.Location}}</div>
<div class="item-detail"><strong>Enabled:</strong> {{.Enabled}}</div>
{{if .Description}}<div class="item-detail"><strong>Description:</strong> {{.Description}}</div>{{end}}
<div class="item-detail"><strong>Image:</strong> {{.ImagePath}}</div>
{{if .Arguments}}<div class="item-detail"><strong>Arguments:</strong> {{.Arguments}}</div>{{end}}
{{if .LaunchString}}<div class="item-detail"><strong>Launch:</strong> {{.LaunchString}}</div>{{end}}
<div class="item-detail"><strong>Hashes:</strong> MD5={{.MD5}} SHA1={{.SHA1}} SHA256={{.SHA256}}</div>
</div>
{{end}}
{{else}}