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