Implement initial detection and data structures for suspicious artifacts

This commit is contained in:
Evan Hosinski
2025-10-10 15:35:17 -04:00
commit 10b1bb7ed6
26 changed files with 2382 additions and 0 deletions
+109
View File
@@ -0,0 +1,109 @@
package cmd
import (
"fmt"
"os"
"rmm-hunter/internal/pkg"
"rmm-hunter/internal/pkg/hunter"
"github.com/spf13/cobra"
)
var (
excludeRMMs []string
inputFile string
outputFile string
)
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "rmm-hunter",
Short: "RMM-Hunter - Detect and eliminate Remote Monitoring and Management software",
Long: `RMM-Hunter is a tool designed to detect and eliminate Remote Monitoring
and Management (RMM) software on Windows systems. It can hunt for suspicious
processes, services, binaries, and network connections associated with RMM tools.`,
Version: "1.0.0",
}
// huntCmd represents the hunt command
var huntCmd = &cobra.Command{
Use: "hunt",
Short: "Hunt for RMM software on the system",
Long: `Hunt mode scans the system for signs of RMM software including:
- Suspicious processes
- Services
- Binaries and executables
- Network connections
- Scheduled tasks
- Registry entries`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Starting RMM Hunt...")
runHunt()
},
}
// eliminateCmd represents the eliminate command
var eliminateCmd = &cobra.Command{
Use: "eliminate",
Short: "Eliminate Sus software based on hunt results",
Long: `Eliminate mode removes detected Sus software from the system.
Requires a JSON input file containing hunt results to determine what to remove.`,
Run: func(cmd *cobra.Command, args []string) {
if inputFile == "" {
fmt.Println("Error: --input flag is required for eliminate command")
os.Exit(1)
}
fmt.Printf("Starting Sus Elimination using input file: %s\n", inputFile)
// TODO: Call eliminate.Eliminate() function
runEliminate()
},
}
// Execute adds all child commands to the root command and sets flags appropriately.
func Execute() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}
func init() {
// Add subcommands
rootCmd.AddCommand(huntCmd)
rootCmd.AddCommand(eliminateCmd)
// Global flags
rootCmd.PersistentFlags().StringSliceVar(&excludeRMMs, "exclude", []string{},
"Comma-separated list of Sus names to exclude from detection (optional)")
// Hunt command flags
huntCmd.Flags().StringSliceVar(&excludeRMMs, "exclude", []string{},
"Comma-separated list of Sus names to exclude from hunt")
huntCmd.Flags().StringVarP(&outputFile, "output", "o", "suspicious-hunter.json",
"Output file to write hunt results (optional) Default: suspicious-hunter.json")
// Eliminate command flags
eliminateCmd.Flags().StringVarP(&inputFile, "input", "i", "",
"JSON input file containing hunt results (required)")
eliminateCmd.MarkFlagRequired("input")
}
func runHunt() {
fmt.Println("Starting Sus Hunt...")
if len(excludeRMMs) > 0 {
fmt.Printf("Excluding RMMs: %v\n", excludeRMMs)
}
hunter.Start(pkg.RunOptions{
ExcludeRMMs: excludeRMMs,
})
}
func runEliminate() {
// TODO: Implement eliminate functionality
fmt.Println("Eliminate functionality not yet implemented")
fmt.Printf("Input file: %s\n", inputFile)
fmt.Printf("Excluded RMMs: %v\n", excludeRMMs)
}
+30
View File
@@ -0,0 +1,30 @@
module rmm-hunter
go 1.24.7
require (
github.com/Kraken-OffSec/Scurvy v0.0.0-20251010192328-967933276439
github.com/spf13/cobra v1.10.1
golang.org/x/sys v0.29.0
)
require (
github.com/Binject/debug v0.0.0-20230508195519-26db73212a7a // indirect
github.com/alwindoss/morse v1.0.1 // indirect
github.com/awgh/rawreader v0.0.0-20200626064944-56820a9c6da4 // indirect
github.com/cakturk/go-netstat v0.0.0-20200220111822-e5b49efee7a5 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
github.com/ecies/go/v2 v2.0.10 // indirect
github.com/elastic/go-sysinfo v1.15.1 // indirect
github.com/elastic/go-windows v1.0.2 // indirect
github.com/ethereum/go-ethereum v1.14.12 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/ryanuber/columnize v2.1.2+incompatible // indirect
github.com/spf13/pflag v1.0.9 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
howett.net/plist v1.0.1 // indirect
)
+63
View File
@@ -0,0 +1,63 @@
github.com/Binject/debug v0.0.0-20230508195519-26db73212a7a h1:4c0nc0krv8eh7gD809n+swLaCuFyHpxdrxwx0ZmHvBw=
github.com/Binject/debug v0.0.0-20230508195519-26db73212a7a/go.mod h1:QzgxDLY/qdKlvnbnb65eqTedhvQPbaSP2NqIbcuKvsQ=
github.com/Kraken-OffSec/Scurvy v0.0.0-20251010174510-8091571ab864 h1:zYVI4GRNB7wjLtorhpnPLP8v8w5T3axCpCtNDKI2LOs=
github.com/Kraken-OffSec/Scurvy v0.0.0-20251010174510-8091571ab864/go.mod h1:hljxQLLV5S60GVVG51+u3r1agCjZ45x8jd2WiJxy0wQ=
github.com/Kraken-OffSec/Scurvy v0.0.0-20251010183749-8ab59cb85591 h1:APveZhhJVm6tFcpldhMLxln4JR1V3Aw1xegt0SKGybg=
github.com/Kraken-OffSec/Scurvy v0.0.0-20251010183749-8ab59cb85591/go.mod h1:hljxQLLV5S60GVVG51+u3r1agCjZ45x8jd2WiJxy0wQ=
github.com/Kraken-OffSec/Scurvy v0.0.0-20251010184333-fb8a7710cf48 h1:nyCMY/8w7IsmduLZspdBuCmWutMUY6lzn5DCKVmQGt0=
github.com/Kraken-OffSec/Scurvy v0.0.0-20251010184333-fb8a7710cf48/go.mod h1:hljxQLLV5S60GVVG51+u3r1agCjZ45x8jd2WiJxy0wQ=
github.com/Kraken-OffSec/Scurvy v0.0.0-20251010184534-de2a2a349253 h1:ZfFDU6Kp9mFlEb0OZniWQR1E3w3Okr9gK2HlRb9lN6E=
github.com/Kraken-OffSec/Scurvy v0.0.0-20251010184534-de2a2a349253/go.mod h1:hljxQLLV5S60GVVG51+u3r1agCjZ45x8jd2WiJxy0wQ=
github.com/Kraken-OffSec/Scurvy v0.0.0-20251010185212-88db5cc88bc0 h1:g01ZBGUyvJXSWvxs7SVPTtqv3ruhbFsgsRGxCM2yYoY=
github.com/Kraken-OffSec/Scurvy v0.0.0-20251010185212-88db5cc88bc0/go.mod h1:hljxQLLV5S60GVVG51+u3r1agCjZ45x8jd2WiJxy0wQ=
github.com/Kraken-OffSec/Scurvy v0.0.0-20251010190109-3a4c7586120a h1:Z4cjdwk5DupnEg/F2dv4DPutwSEmDq7WWe565FjZrtQ=
github.com/Kraken-OffSec/Scurvy v0.0.0-20251010190109-3a4c7586120a/go.mod h1:hljxQLLV5S60GVVG51+u3r1agCjZ45x8jd2WiJxy0wQ=
github.com/Kraken-OffSec/Scurvy v0.0.0-20251010190312-0ad88c5f3f2f h1:XJ9IudxrEjAhodOLCTaWCIxWdj0fIa+JOdzfd1nST9k=
github.com/Kraken-OffSec/Scurvy v0.0.0-20251010190312-0ad88c5f3f2f/go.mod h1:hljxQLLV5S60GVVG51+u3r1agCjZ45x8jd2WiJxy0wQ=
github.com/Kraken-OffSec/Scurvy v0.0.0-20251010192328-967933276439 h1:n/B4+1K6vpKX34iISUKHzEKEND53PmxePHrtsy693Jo=
github.com/Kraken-OffSec/Scurvy v0.0.0-20251010192328-967933276439/go.mod h1:hljxQLLV5S60GVVG51+u3r1agCjZ45x8jd2WiJxy0wQ=
github.com/alwindoss/morse v1.0.1 h1:PkUh5m1UHMcZ1Upvl7CmSIBMxdEBejWoQ4rQQtgJsCQ=
github.com/alwindoss/morse v1.0.1/go.mod h1:qAqJOep3jEpIpiLgqSGgLk5Zh4BZKsyzMQHuAwVPMXc=
github.com/awgh/rawreader v0.0.0-20200626064944-56820a9c6da4 h1:cIAK2NNf2yafdgpFRNJrgZMwvy61BEVpGoHc2n4/yWs=
github.com/awgh/rawreader v0.0.0-20200626064944-56820a9c6da4/go.mod h1:SalMPBCab3yuID8nIhLfzwoBV+lBRyaC7NhuN8qL8xE=
github.com/cakturk/go-netstat v0.0.0-20200220111822-e5b49efee7a5 h1:BjkPE3785EwPhhyuFkbINB+2a1xATwk8SNDWnJiD41g=
github.com/cakturk/go-netstat v0.0.0-20200220111822-e5b49efee7a5/go.mod h1:jtAfVaU/2cu1+wdSRPWE2c1N2qeAA3K4RH9pYgqwets=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/ecies/go/v2 v2.0.10 h1:AaLxGio0MLLbvWur4rKnLzw+K9zI+wMScIDAtqCqOtU=
github.com/ecies/go/v2 v2.0.10/go.mod h1:N73OyuR6tuKznit2LhXjrZ0XAQ234uKbzYz8pEPYzlI=
github.com/elastic/go-sysinfo v1.15.1 h1:zBmTnFEXxIQ3iwcQuk7MzaUotmKRp3OabbbWM8TdzIQ=
github.com/elastic/go-sysinfo v1.15.1/go.mod h1:jPSuTgXG+dhhh0GKIyI2Cso+w5lPJ5PvVqKlL8LV/Hk=
github.com/elastic/go-windows v1.0.2 h1:yoLLsAsV5cfg9FLhZ9EXZ2n2sQFKeDYrHenkcivY4vI=
github.com/elastic/go-windows v1.0.2/go.mod h1:bGcDpBzXgYSqM0Gx3DM4+UxFj300SZLixie9u9ixLM8=
github.com/ethereum/go-ethereum v1.14.12 h1:8hl57x77HSUo+cXExrURjU/w1VhL+ShCTJrTwcCQSe4=
github.com/ethereum/go-ethereum v1.14.12/go.mod h1:RAC2gVMWJ6FkxSPESfbshrcKpIokgQKsVKmAuqdekDY=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v2.1.2+incompatible h1:C89EOx/XBWwIXl8wm8OPJBd7kPF25UfsK2X7Ph/zCAk=
github.com/ryanuber/columnize v2.1.2+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=
howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
+170
View File
@@ -0,0 +1,170 @@
package autorun
import (
"fmt"
"rmm-hunter/internal/pkg/hunt/detect/common"
. "rmm-hunter/internal/suspicious"
"strings"
"golang.org/x/sys/windows/registry"
)
func Detect() []AutoRun {
var suspiciousAutoRuns []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`,
}
// Check both HKLM and HKCU
roots := []registry.Key{registry.LOCAL_MACHINE, registry.CURRENT_USER}
rootNames := []string{"HKLM", "HKCU"}
totalEntries := 0
for i, root := range roots {
for _, keyPath := range autorunKeys {
entries := checkAutoRunKey(root, keyPath, rootNames[i])
totalEntries += len(entries)
suspiciousAutoRuns = append(suspiciousAutoRuns, entries...)
}
}
fmt.Printf(" [>] Dispositioning %d AutoRun Entries\n", totalEntries)
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
}
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
for _, rmm := range common.CommonRMMs {
rmmLower := strings.ToLower(rmm)
if strings.Contains(nameLower, rmmLower) || strings.Contains(commandLower, rmmLower) {
return true
}
}
// Check against common Suspicious executable patterns
for _, imageEnd := range common.CommonImageEnds {
imageEndLower := strings.ToLower(imageEnd)
if strings.Contains(commandLower, imageEndLower) {
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) {
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, ""
}
@@ -0,0 +1,16 @@
package autorun
import "testing"
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("Location: %s", ar.Location)
t.Logf("Enabled: %t", ar.Enabled)
t.Logf("Description: %s", ar.Description)
t.Logf("-----")
}
}
@@ -0,0 +1,94 @@
package binaries
import (
"fmt"
"os"
"path/filepath"
"rmm-hunter/internal/pkg/hunt/detect/common"
"strings"
"sync"
)
func Detect() []string {
var foundBinaries []string
var mu sync.Mutex
var wg sync.WaitGroup
fmt.Printf("[*] Enumerating Suspicious Binaries\n")
// Define search directories
searchDirs := []string{
os.Getenv("APPDATA"),
`C:\ProgramData\`,
`C:\Program Files\`,
`C:\Program Files (x86)\`,
`C:\Downloads\`,
}
fmt.Printf(" [>] Dispositioning %d Directories\n", len(searchDirs))
// Channel to collect results
resultChan := make(chan string, 100)
// Start goroutines for each directory
for _, dir := range searchDirs {
if dir == "" {
continue // Skip if environment variable is empty
}
wg.Add(1)
go func(searchDir string) {
defer wg.Done()
searchDirectory(searchDir, resultChan)
}(dir)
}
// Goroutine to close channel when all searches complete
go func() {
wg.Wait()
close(resultChan)
}()
// Collect results
for result := range resultChan {
mu.Lock()
foundBinaries = append(foundBinaries, result)
mu.Unlock()
fmt.Printf(" [?] Found %s\n", result)
}
fmt.Printf("[+] Found %d Suspicious Binaries\n", len(foundBinaries))
return foundBinaries
}
func searchDirectory(dir string, resultChan chan<- string) {
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
// Skip directories we can't access
return nil
}
// Skip directories, only check files
if info.IsDir() {
return nil
}
// Check if the file path ends with any CommonImageEnds
for _, imageEnd := range common.CommonImageEnds {
// Normalize path separators and make case-insensitive comparison
normalizedPath := strings.ToLower(filepath.ToSlash(path))
normalizedImageEnd := strings.ToLower(filepath.ToSlash(imageEnd))
if strings.HasSuffix(normalizedPath, normalizedImageEnd) {
resultChan <- path
break
}
}
return nil
})
if err != nil {
fmt.Printf(" [-] Error searching %s: %v\n", dir, err)
}
}
@@ -0,0 +1,12 @@
package binaries
import "testing"
func TestDetect(t *testing.T) {
binaries := Detect()
for _, binary := range binaries {
t.Logf("-----")
t.Logf("Binary: %s", binary)
t.Logf("-----")
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,71 @@
package common
import (
"fmt"
"strings"
)
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)
// Check for suspicious installation paths
suspiciousPaths = []string{
"\\temp\\", "\\tmp\\", "\\appdata\\local\\temp\\",
"\\users\\public\\", "\\programdata\\",
"\\windows\\temp\\", "\\%temp%\\",
}
for _, suspPath := range suspiciousPaths {
if strings.Contains(execPathLower, suspPath) {
// Check for trusted publishers/companies
trustedPublishers := []string{
"\\microsoft\\",
"\\adobe\\",
"\\google\\",
"\\intel\\",
"\\nvidia\\",
"\\oracle\\",
"\\citrix\\",
"\\vmware\\",
// Add more trusted publishers as needed
}
isTrusted := false
for _, publisher := range trustedPublishers {
if strings.Contains(execPathLower, publisher) {
isTrusted = true
break
}
}
if !isTrusted {
return true, fmt.Sprintf("Suspicious installation path: %s", suspPath)
}
}
}
return false, ""
}
@@ -0,0 +1,192 @@
package connections
import (
"bufio"
"fmt"
"net"
"os/exec"
"regexp"
"rmm-hunter/internal/pkg/hunt/detect/common"
. "rmm-hunter/internal/suspicious"
"strings"
)
func DetectOutboundConnections() []NetworkConnection {
var connections []NetworkConnection
fmt.Printf("[*] Enumerating Outbound Connections...\n")
// Get active connections via netstat
netstatConnections := getNetstatConnections()
connections = append(connections, netstatConnections...)
// Get DNS cache entries for hostname resolution
dnsCache := getDNSCache()
// Resolve hostnames for IP addresses
for i := range connections {
if hostname, exists := dnsCache[connections[i].RemoteAddr]; exists {
connections[i].RemoteHost = hostname
} else {
connections[i].RemoteHost = resolveHostname(connections[i].RemoteAddr)
}
}
fmt.Printf(" [>] Dispositioning %d Outbound Connections\n", len(connections))
return compareConnections(connections)
}
func compareConnections(connections []NetworkConnection) []NetworkConnection {
var suspiciousConnections []NetworkConnection
for _, conn := range connections {
remote := conn.RemoteHost
for _, dns := range common.CommonDNS {
if matchesDNSPattern(remote, dns) {
fmt.Printf(" [?] Found %s\n", conn.RemoteHost)
suspiciousConnections = append(suspiciousConnections, conn)
break
}
}
}
fmt.Printf("[+] Found %d Suspicious Outbound Connections\n", len(suspiciousConnections))
return suspiciousConnections
}
// matchesDNSPattern converts DNS pattern to regex and matches hostname
func matchesDNSPattern(hostname, pattern string) bool {
// Convert to lowercase for case-insensitive matching
pattern = strings.ToLower(pattern)
// Remove leading dot if present
if strings.HasPrefix(pattern, ".") {
pattern = pattern[1:]
}
// Escape special regex characters except * and .
pattern = regexp.QuoteMeta(pattern)
// Convert wildcards back to regex
pattern = strings.ReplaceAll(pattern, `\*`, `[^.]*`)
pattern = strings.ReplaceAll(pattern, `\.`, `\.`)
// Anchor the pattern to match end of hostname
pattern = `(^|\.)` + pattern + `$`
regex, err := regexp.Compile(pattern)
if err != nil {
return false
}
return regex.MatchString(hostname)
}
func getNetstatConnections() []NetworkConnection {
var connections []NetworkConnection
cmd := exec.Command("netstat", "-ano")
output, err := cmd.Output()
if err != nil {
return connections
}
scanner := bufio.NewScanner(strings.NewReader(string(output)))
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if strings.Contains(line, "TCP") && strings.Contains(line, "ESTABLISHED") {
fields := strings.Fields(line)
if len(fields) >= 5 {
localAddr := fields[1]
remoteAddr := fields[2]
state := fields[3]
pid := fields[4]
// Filter for outbound connections (exclude localhost)
if !strings.HasPrefix(remoteAddr, "127.0.0.1") &&
!strings.HasPrefix(remoteAddr, "::1") {
connections = append(connections, NetworkConnection{
LocalAddr: localAddr,
RemoteAddr: extractIP(remoteAddr),
State: state,
PID: pid,
})
}
}
}
}
return connections
}
func getDNSCache() map[string]string {
cache := make(map[string]string)
cmd := exec.Command("ipconfig", "/displaydns")
output, err := cmd.Output()
if err != nil {
return cache
}
lines := strings.Split(string(output), "\n")
var currentHost string
for _, line := range lines {
line = strings.TrimSpace(line)
if strings.Contains(line, "Record Name") {
parts := strings.Split(line, ":")
if len(parts) > 1 {
currentHost = strings.TrimSpace(parts[1])
}
}
if strings.Contains(line, "A (Host) Record") && currentHost != "" {
// Look for IP in next few lines
continue
}
if currentHost != "" && net.ParseIP(strings.TrimSpace(line)) != nil {
cache[strings.TrimSpace(line)] = currentHost
currentHost = ""
}
}
return cache
}
func extractIP(addr string) string {
if idx := strings.LastIndex(addr, ":"); idx != -1 {
return addr[:idx]
}
return addr
}
func resolveHostname(ip string) string {
names, err := net.LookupAddr(ip)
if err != nil || len(names) == 0 {
return ip
}
return strings.TrimSuffix(names[0], ".")
}
// GetHTTPHostnames extracts unique hostnames from outbound connections
func GetHTTPHostnames() []string {
connections := DetectOutboundConnections()
hostnameMap := make(map[string]bool)
var hostnames []string
for _, conn := range connections {
if conn.RemoteHost != "" && conn.RemoteHost != conn.RemoteAddr {
if !hostnameMap[conn.RemoteHost] {
hostnameMap[conn.RemoteHost] = true
hostnames = append(hostnames, conn.RemoteHost)
}
}
}
return hostnames
}
@@ -0,0 +1,17 @@
package connections
import "testing"
func TestDetectOutboundConnections(t *testing.T) {
conns := DetectOutboundConnections()
for _, conn := range conns {
t.Logf("-----")
t.Logf("PID: %s", conn.PID)
t.Logf("LocalAddr: %s", conn.LocalAddr)
t.Logf("RemoteAddr: %s", conn.RemoteAddr)
t.Logf("RemoteHost: %s", conn.RemoteHost)
t.Logf("State: %s", conn.State)
t.Logf("Process: %s", conn.Process)
t.Logf("-----")
}
}
@@ -0,0 +1,36 @@
package directory
import (
"fmt"
"os"
"path/filepath"
"rmm-hunter/internal/pkg/hunt/detect/common"
"strings"
)
var appData = os.Getenv("APPDATA")
func Detect() []string {
var suspiciousDirectories []string
fmt.Printf("[*] Enumerating Suspicious Directories \n")
// Check for common directories
for _, dir := range common.CommonDirectories {
dir = replaceAppData(dir)
if _, err := os.Stat(dir); err == nil {
fmt.Printf(" [?] Found %s\n", dir)
suspiciousDirectories = append(suspiciousDirectories, dir)
}
}
fmt.Printf("[+] Found %d Suspicious Directories\n", len(suspiciousDirectories))
return suspiciousDirectories
}
func replaceAppData(path string) string {
if strings.Contains(path, "{{APPDATA}}") {
p := strings.Replace(path, "{{APPDATA}}", "", -1)
return filepath.Join(appData, p)
}
return path
}
@@ -0,0 +1,12 @@
package directory
import "testing"
func TestDetect(t *testing.T) {
directories := Detect()
for _, dir := range directories {
t.Logf("-----")
t.Logf("Directory: %s", dir)
t.Logf("-----")
}
}
@@ -0,0 +1,75 @@
package processes
import (
"fmt"
"rmm-hunter/internal/pkg/hunt/detect/common"
. "rmm-hunter/internal/suspicious"
"strings"
"github.com/Kraken-OffSec/Scurvy/core/process"
)
func Detect() []Process {
fmt.Printf("[*] Enumerating Processes \n")
processes, err := process.Processes()
if err != nil {
fmt.Printf("[-] Error enumerating processes: %s\n", err.Error())
return []Process{}
}
fmt.Printf(" [>] Dispositioning %d Processes\n", len(processes))
return compareProcesses(processes)
}
func compareProcesses(processes []process.Process) []Process {
var suspiciousProcesses []Process
for _, proc := range processes {
procName := proc.Executable()
procNameLower := strings.ToLower(procName)
// Get full executable path if available
var fullPath string
if proc.Path() != "" {
fullPath = proc.Path()
}
// Check against known RMMs
isRMMMatch := false
for _, rmm := range common.CommonRMMs {
rmmLower := strings.ToLower(rmm)
if strings.Contains(procNameLower, rmmLower) {
isRMMMatch = true
break
}
}
// Check for suspicious path
isPathSuspicious := false
pathReason := ""
if fullPath != "" {
isPathSuspicious, pathReason = common.AnalyzeExecutablePath(fullPath)
}
if isRMMMatch || isPathSuspicious {
args := ""
if isPathSuspicious {
args = fmt.Sprintf("[%s]", pathReason)
}
fmt.Printf(" [?] Found %s\n", procName)
suspiciousProcesses = append(suspiciousProcesses, Process{
Name: procName,
PID: proc.Pid(),
PPID: proc.PPid(),
Path: fullPath,
Args: args,
})
}
}
fmt.Printf("[+] Found %d Suspicious Processes\n", len(suspiciousProcesses))
return suspiciousProcesses
}
@@ -0,0 +1,15 @@
package processes
import "testing"
func TestDetect(t *testing.T) {
processes := Detect()
for _, proc := range processes {
t.Logf("-----")
t.Logf("Name: %s", proc.Name)
t.Logf("PID: %d", proc.PID)
t.Logf("PPID: %d", proc.PPID)
t.Logf("Path: %s", proc.Path)
t.Logf("-----")
}
}
@@ -0,0 +1,54 @@
package scheduledTasks
import (
"fmt"
"rmm-hunter/internal/pkg/hunt/detect/common"
. "rmm-hunter/internal/suspicious"
"strings"
"time"
schTasks "github.com/Kraken-OffSec/Scurvy/core/scheduledTasks"
)
func Detect() []*ScheduledTask {
fmt.Printf("[*] Enumerating Scheduled Tasks \n")
tasks, err := schTasks.ListTasks()
if err != nil {
fmt.Printf("[-] Error enumerating scheduled tasks: %s\n", err.Error())
return []*ScheduledTask{}
}
fmt.Printf(" [>] Dispositioning %d Scheduled Tasks\n", len(tasks))
return compareTasks(tasks)
}
func compareTasks(tasks []schTasks.TaskInfo) []*ScheduledTask {
var suspiciousTasks []*ScheduledTask
for _, task := range tasks {
for _, rmm := range common.CommonRMMs {
rmmLower := strings.ToLower(rmm)
taskNameLower := strings.ToLower(task.Name)
if strings.Contains(taskNameLower, rmmLower) {
fmt.Printf(" [?] Found %s\n", task.Name)
suspiciousTasks = append(suspiciousTasks, &ScheduledTask{
Name: task.Name,
Author: task.Author,
LastRun: task.LastRun.Format(time.RFC3339),
NextRun: task.NextRun.Format(time.RFC3339),
LastResult: task.LastResult,
CreatedDate: task.CreationDate.Format(time.RFC3339),
State: task.State,
Path: task.Path,
Description: task.Description,
ModifiedDate: "",
Enabled: task.Enabled,
})
break
}
}
}
fmt.Printf("[+] Found %d Suspicious Scheduled Tasks\n", len(suspiciousTasks))
return suspiciousTasks
}
@@ -0,0 +1,22 @@
package scheduledTasks
import "testing"
func TestDetect(t *testing.T) {
tasks := Detect()
for _, task := range tasks {
t.Logf("-----")
t.Logf("Name: %s", task.Name)
t.Logf("Author: %s", task.Author)
t.Logf("LastRun: %s", task.LastRun)
t.Logf("NextRun: %s", task.NextRun)
t.Logf("LastResult: %s", task.LastResult)
t.Logf("CreatedDate: %s", task.CreatedDate)
t.Logf("State: %s", task.State)
t.Logf("Path: %s", task.Path)
t.Logf("Description: %s", task.Description)
t.Logf("ModifiedDate: %s", task.ModifiedDate)
t.Logf("Enabled: %t", task.Enabled)
t.Logf("-----")
}
}
@@ -0,0 +1,149 @@
package services
import (
"fmt"
"rmm-hunter/internal/pkg/hunt/detect/common"
. "rmm-hunter/internal/suspicious"
"strings"
"github.com/Kraken-OffSec/Scurvy/core/service"
"golang.org/x/sys/windows"
)
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{}
}
defer windows.Close(scm.Handle)
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
}
config, err := svc.Config()
if err != nil {
fmt.Printf(" [>-] Error getting service config %s: %s\n", serviceString, err.Error())
continue
}
svcStartName := strings.ToLower(config.ServiceStartName)
svcDisplayName := strings.ToLower(config.DisplayName)
svcBinaryPath := strings.ToLower(config.BinaryPathName)
// Check against known RMMs
isRMMMatch := false
for _, rmm := range common.CommonRMMs {
rmmLower := strings.ToLower(rmm)
if strings.Contains(svcDisplayName, rmmLower) || strings.Contains(svcStartName, rmmLower) || strings.Contains(svcBinaryPath, rmmLower) {
isRMMMatch = true
break
}
}
// 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)
}
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: description,
SidType: config.SidType,
DelayedAutoStart: config.DelayedAutoStart,
})
}
}
fmt.Printf("[+] Found %d Suspicious Services\n", len(suspiciousServices))
return suspiciousServices
}
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)
}
}
@@ -0,0 +1,28 @@
package services
import "testing"
func TestDetect(t *testing.T) {
services := Detect()
for _, svc := range services {
t.Logf("-----")
t.Logf("Name: %s", svc.Name)
t.Logf("DisplayName: %s", svc.DisplayName)
t.Logf("ServiceTypeRaw: %d", svc.ServiceTypeRaw)
t.Logf("ServiceType: %s", svc.ServiceType)
t.Logf("StartTypeRaw: %d", svc.StartTypeRaw)
t.Logf("StartType: %s", svc.StartType)
t.Logf("ErrorControlRaw: %d", svc.ErrorControlRaw)
t.Logf("ErrorControl: %s", svc.ErrorControl)
t.Logf("BinaryPathName: %s", svc.BinaryPathName)
t.Logf("LoadOrderGroup: %s", svc.LoadOrderGroup)
t.Logf("TagId: %d", svc.TagId)
t.Logf("Dependencies: %v", svc.Dependencies)
t.Logf("ServiceStartName: %s", svc.ServiceStartName)
t.Logf("Password: %s", svc.Password)
t.Logf("Description: %s", svc.Description)
t.Logf("SidType: %d", svc.SidType)
t.Logf("DelayedAutoStart: %t", svc.DelayedAutoStart)
t.Logf("-----")
}
}
+1
View File
@@ -0,0 +1 @@
package eliminate
+55
View File
@@ -0,0 +1,55 @@
package hunter
import (
"rmm-hunter/internal/pkg"
"rmm-hunter/internal/pkg/hunt/detect/autorun"
"rmm-hunter/internal/pkg/hunt/detect/binaries"
"rmm-hunter/internal/pkg/hunt/detect/connections"
"rmm-hunter/internal/pkg/hunt/detect/directory"
"rmm-hunter/internal/pkg/hunt/detect/processes"
"rmm-hunter/internal/pkg/hunt/detect/scheduledTasks"
"rmm-hunter/internal/pkg/hunt/detect/services"
. "rmm-hunter/internal/suspicious"
)
type Hunter struct {
Options pkg.RunOptions
Sus Suspicious
}
func Start(options pkg.RunOptions) {
hunter := Hunter{
Options: options,
}
hunter.run()
}
func (h *Hunter) run() {
// Find suspicious processes
processes := processes.Detect()
h.Sus.Processes = processes
// Find suspicious services
services := services.Detect()
h.Sus.Services = services
// Find suspicious autoruns
autoruns := autorun.Detect()
h.Sus.AutoRuns = autoruns
// Find suspicious outbound connections
connections := connections.DetectOutboundConnections()
h.Sus.OutboundConnections = connections
// Find suspicious scheduled tasks
tasks := scheduledTasks.Detect()
h.Sus.ScheduledTasks = tasks
// Find suspicious binaries
binaries := binaries.Detect()
h.Sus.Binaries = binaries
// Find suspicious directories
directories := directory.Detect()
h.Sus.Directories = directories
}
+5
View File
@@ -0,0 +1,5 @@
package pkg
type RunOptions struct {
ExcludeRMMs []string
}
+1
View File
@@ -0,0 +1 @@
package writer
+1
View File
@@ -0,0 +1 @@
package writer
+114
View File
@@ -0,0 +1,114 @@
package suspicious
/*
Suspicious
The object used to resemble the Suspicious artifacts and activities.
*/
type Suspicious struct {
Artifacts []Artifact `json:"artifacts"`
Persistence Persistence `json:"persistence"`
RootFolder string `json:"rootFolder"`
Binaries []string `json:"binaries"`
Directories []string `json:"directories"`
Services []*Service `json:"services"`
Processes []Process `json:"processes"`
OutboundConnections []NetworkConnection `json:"outboundConnections"`
AutoRuns []AutoRun `json:"autoRuns"`
ScheduledTasks []*ScheduledTask `json:"scheduledTasks"`
}
type NetworkConnection struct {
LocalAddr string
RemoteAddr string
RemoteHost string
State string
PID string
Process string
}
/*
Artifact
The object used to resemble the artifacts found by the Suspicious software.
*/
type Artifact struct {
Location string `json:"location"`
Content string `json:"content"`
SHA256 string `json:"sha256"`
}
/*
Persistence
The object used to resemble the persistence methods used by the Suspicious software.
*/
type Persistence struct {
AutoRuns []AutoRun `json:"autoRuns"`
ScheduledTasks []ScheduledTask `json:"scheduledTasks"`
}
/*
AutoRun
The object used to resemble the auto run methods used by the Suspicious software.
*/
type AutoRun struct {
Name string `json:"name"`
Command string `json:"command"`
Location string `json:"location"`
Enabled bool `json:"enabled"`
Description string `json:"description"`
}
/*
ScheduledTask
The object used to resemble the scheduled tasks used by the Suspicious software.
*/
type ScheduledTask struct {
Name string `json:"name"`
Author string `json:"author"`
CreatedDate string `json:"createdDate"`
ModifiedDate string `json:"modifiedDate"`
Description string `json:"description"`
State string `json:"state"`
Enabled bool `json:"enabled"`
LastResult string `json:"lastResult"`
NextRun string `json:"nextRun"`
LastRun string `json:"lastRun"`
Path string `json:"path"`
}
/*
Process
The object used to resemble the processes used by the Suspicious software.
*/
type Process struct {
Name string `json:"name"`
PID int `json:"pid"`
PPID int `json:"ppid"`
Parent string `json:"parent"`
Args string `json:"args"`
Created string `json:"created"`
Path string `json:"path"`
}
/*
Service
The object used to resemble the services used by the Suspicious software.
*/
type Service struct {
Name string `json:"name"`
DisplayName string `json:"displayName"`
ServiceTypeRaw uint32 `json:"serviceTypeRaw"`
ServiceType string `json:"serviceType"`
StartTypeRaw uint32 `json:"startTypeRaw"`
StartType string `json:"startType"`
ErrorControlRaw uint32 `json:"errorControlRaw"`
ErrorControl string `json:"errorControl"`
BinaryPathName string `json:"binaryPathName"`
LoadOrderGroup string `json:"loadOrderGroup"`
TagId uint32 `json:"tagId"`
Dependencies []string `json:"dependencies"`
ServiceStartName string `json:"serviceStartName"`
Password string `json:"password"`
Description string `json:"description"`
SidType uint32 `json:"sidType"`
DelayedAutoStart bool `json:"delayedAutoStart"`
}
+9
View File
@@ -0,0 +1,9 @@
package main
import (
"rmm-hunter/cmd"
)
func main() {
cmd.Execute()
}