Implement initial detection and data structures for suspicious artifacts
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user