Implement initial detection and data structures for suspicious artifacts
This commit is contained in:
+109
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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=
|
||||
@@ -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("-----")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package eliminate
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package pkg
|
||||
|
||||
type RunOptions struct {
|
||||
ExcludeRMMs []string
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package writer
|
||||
@@ -0,0 +1 @@
|
||||
package writer
|
||||
@@ -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"`
|
||||
}
|
||||
Reference in New Issue
Block a user