Add eliminate package with functions for removing suspicious artifacts (files, directories, processes, services, scheduled tasks) and enhance detection logic to include whitelist checks and multi-indicator scoring
This commit is contained in:
@@ -3,7 +3,7 @@ module rmm-hunter
|
|||||||
go 1.24.7
|
go 1.24.7
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Kraken-OffSec/Scurvy v0.0.0-20251011184544-e9265efd21c6
|
github.com/Kraken-OffSec/Scurvy v0.0.0-20251011211525-6bf6bee1b100
|
||||||
github.com/charmbracelet/bubbles v0.21.0
|
github.com/charmbracelet/bubbles v0.21.0
|
||||||
github.com/charmbracelet/bubbletea v1.3.10
|
github.com/charmbracelet/bubbletea v1.3.10
|
||||||
github.com/charmbracelet/lipgloss v1.1.0
|
github.com/charmbracelet/lipgloss v1.1.0
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ github.com/Kraken-OffSec/Scurvy v0.0.0-20251010192328-967933276439 h1:n/B4+1K6vp
|
|||||||
github.com/Kraken-OffSec/Scurvy v0.0.0-20251010192328-967933276439/go.mod h1:hljxQLLV5S60GVVG51+u3r1agCjZ45x8jd2WiJxy0wQ=
|
github.com/Kraken-OffSec/Scurvy v0.0.0-20251010192328-967933276439/go.mod h1:hljxQLLV5S60GVVG51+u3r1agCjZ45x8jd2WiJxy0wQ=
|
||||||
github.com/Kraken-OffSec/Scurvy v0.0.0-20251011184544-e9265efd21c6 h1:CRH0t964ocRHXspOo8cB0DPcSfEtsGh8FenjML252HI=
|
github.com/Kraken-OffSec/Scurvy v0.0.0-20251011184544-e9265efd21c6 h1:CRH0t964ocRHXspOo8cB0DPcSfEtsGh8FenjML252HI=
|
||||||
github.com/Kraken-OffSec/Scurvy v0.0.0-20251011184544-e9265efd21c6/go.mod h1:0pPwYHy+r8KGzXZ8vBgyYd6qy3vX+AMRo9XLiGc8WGE=
|
github.com/Kraken-OffSec/Scurvy v0.0.0-20251011184544-e9265efd21c6/go.mod h1:0pPwYHy+r8KGzXZ8vBgyYd6qy3vX+AMRo9XLiGc8WGE=
|
||||||
|
github.com/Kraken-OffSec/Scurvy v0.0.0-20251011204529-faafd6327395 h1:5VcLiLUs33Hvqp5Jiyft+ZzzhfjTVb6fOB3MWiIDp1M=
|
||||||
|
github.com/Kraken-OffSec/Scurvy v0.0.0-20251011204529-faafd6327395/go.mod h1:0pPwYHy+r8KGzXZ8vBgyYd6qy3vX+AMRo9XLiGc8WGE=
|
||||||
|
github.com/Kraken-OffSec/Scurvy v0.0.0-20251011211525-6bf6bee1b100 h1:Om4wnKb+fpfYi3uRfc27Pz8uG/3CNrM2G3sSBwerSXA=
|
||||||
|
github.com/Kraken-OffSec/Scurvy v0.0.0-20251011211525-6bf6bee1b100/go.mod h1:0pPwYHy+r8KGzXZ8vBgyYd6qy3vX+AMRo9XLiGc8WGE=
|
||||||
github.com/alwindoss/morse v1.0.1 h1:PkUh5m1UHMcZ1Upvl7CmSIBMxdEBejWoQ4rQQtgJsCQ=
|
github.com/alwindoss/morse v1.0.1 h1:PkUh5m1UHMcZ1Upvl7CmSIBMxdEBejWoQ4rQQtgJsCQ=
|
||||||
github.com/alwindoss/morse v1.0.1/go.mod h1:qAqJOep3jEpIpiLgqSGgLk5Zh4BZKsyzMQHuAwVPMXc=
|
github.com/alwindoss/morse v1.0.1/go.mod h1:qAqJOep3jEpIpiLgqSGgLk5Zh4BZKsyzMQHuAwVPMXc=
|
||||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||||
|
|||||||
@@ -9,58 +9,22 @@ import (
|
|||||||
"github.com/Kraken-OffSec/Scurvy/core/autoruns"
|
"github.com/Kraken-OffSec/Scurvy/core/autoruns"
|
||||||
)
|
)
|
||||||
|
|
||||||
// basic allow/deny helpers kept local to keep changes scoped
|
// Whitelist for our own tool and legitimate system components
|
||||||
var systemDirs = []string{"\\windows\\system32\\", "\\windows\\syswow64\\", "\\windows\\", "\\program files\\windowsapps\\"}
|
var whitelist = []string{
|
||||||
var safeSystemBinaries = []string{
|
"rmm-hunter",
|
||||||
"svchost.exe", "lsass.exe", "services.exe", "winlogon.exe", "explorer.exe",
|
|
||||||
"ctfmon.exe", "spoolsv.exe", "dwm.exe", "smss.exe", "csrss.exe",
|
|
||||||
"runtimebroker.exe", "shellexperiencehost.exe", "searchui.exe", "sihost.exe",
|
|
||||||
"taskhostw.exe", "wininit.exe", "rdpclip.exe",
|
|
||||||
}
|
|
||||||
var genericTokens = map[string]struct{}{
|
|
||||||
"remote": {}, "control": {}, "support": {}, "assist": {}, "viewer": {},
|
|
||||||
"server": {}, "service": {}, "manager": {}, "desktop": {}, "host": {},
|
|
||||||
"client": {}, "agent": {}, "connect": {}, "access": {}, "admin": {},
|
|
||||||
"vpn": {}, "ssh": {}, "vnc": {}, "rdp": {}, "microsoft": {}, "windows": {},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func inSlice(slice []string, v string) bool {
|
func isWhitelisted(ar AutoRun) bool {
|
||||||
v = strings.ToLower(v)
|
allText := strings.ToLower(strings.Join([]string{
|
||||||
for _, s := range slice {
|
ar.ImageName, ar.ImagePath, ar.Entry, ar.LaunchString,
|
||||||
if strings.ToLower(s) == v {
|
}, "|"))
|
||||||
|
for _, w := range whitelist {
|
||||||
|
if strings.Contains(allText, w) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
func containsAny(haystack string, needles []string) bool {
|
|
||||||
h := strings.ToLower(haystack)
|
|
||||||
for _, n := range needles {
|
|
||||||
if strings.Contains(h, strings.ToLower(n)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
func isInSystemDir(p string) bool {
|
|
||||||
pl := strings.ToLower(p)
|
|
||||||
for _, d := range systemDirs {
|
|
||||||
if strings.Contains(pl, d) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
func isNonGenericToken(t string) bool {
|
|
||||||
t = strings.ToLower(strings.TrimSpace(t))
|
|
||||||
if len(t) < 4 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if _, ok := genericTokens[t]; ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func Detect() []AutoRun {
|
func Detect() []AutoRun {
|
||||||
var suspiciousAutoRuns []AutoRun
|
var suspiciousAutoRuns []AutoRun
|
||||||
@@ -86,6 +50,11 @@ func Detect() []AutoRun {
|
|||||||
LaunchString: ar.LaunchString,
|
LaunchString: ar.LaunchString,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip whitelisted entries (our own tool)
|
||||||
|
if isWhitelisted(sar) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if isSuspiciousAutoRunEntry(sar) {
|
if isSuspiciousAutoRunEntry(sar) {
|
||||||
fmt.Printf(" [?] Found %s | %s | %s\n", sar.Location, sar.Entry, sar.ImagePath)
|
fmt.Printf(" [?] Found %s | %s | %s\n", sar.Location, sar.Entry, sar.ImagePath)
|
||||||
suspiciousAutoRuns = append(suspiciousAutoRuns, sar)
|
suspiciousAutoRuns = append(suspiciousAutoRuns, sar)
|
||||||
@@ -96,51 +65,107 @@ func Detect() []AutoRun {
|
|||||||
return suspiciousAutoRuns
|
return suspiciousAutoRuns
|
||||||
}
|
}
|
||||||
|
|
||||||
// isSuspiciousAutoRunEntry determines if an autorun appears to be an RMM by
|
// isSuspiciousAutoRunEntry uses multi-Indicator scoring to detect RMMs
|
||||||
// checking image path/name, location, entry and launch string against
|
// Requires at least 2 independent Indicators to flag as suspicious
|
||||||
// common RMM indicators and suspicious image suffixes. It also flags
|
// Hash match alone is sufficient (high confidence)
|
||||||
// suspicious installation paths.
|
|
||||||
func isSuspiciousAutoRunEntry(ar AutoRun) bool {
|
func isSuspiciousAutoRunEntry(ar AutoRun) bool {
|
||||||
// Build a single string of fields we care about
|
score := 0
|
||||||
joined := strings.ToLower(strings.Join([]string{ar.ImageName, ar.ImagePath, ar.Entry, ar.LaunchString}, "|"))
|
|
||||||
|
|
||||||
// 1) Vendor token hit (filter out generic words)
|
// Build searchable text from all fields
|
||||||
vendorHit := false
|
allText := strings.ToLower(strings.Join([]string{
|
||||||
for _, tok := range common.CommonRMMs {
|
ar.ImageName, ar.ImagePath, ar.Entry, ar.LaunchString, ar.Location, ar.Arguments,
|
||||||
if !isNonGenericToken(tok) {
|
}, "|"))
|
||||||
continue
|
|
||||||
|
// Indicator 0: Known RMM hash match (SHA256 or SHA1) - HIGHEST CONFIDENCE
|
||||||
|
// A hash match alone is sufficient to flag as suspicious
|
||||||
|
if ar.SHA256 != "" {
|
||||||
|
sha256Lower := strings.ToLower(ar.SHA256)
|
||||||
|
for _, hash := range common.CommonRMMHashes {
|
||||||
|
if strings.ToLower(hash) == sha256Lower {
|
||||||
|
return true // Hash match is definitive
|
||||||
}
|
}
|
||||||
if strings.Contains(joined, strings.ToLower(tok)) {
|
}
|
||||||
vendorHit = true
|
}
|
||||||
|
if ar.SHA1 != "" {
|
||||||
|
sha1Lower := strings.ToLower(ar.SHA1)
|
||||||
|
for _, hash := range common.CommonRMMHashesSHA1 {
|
||||||
|
if strings.ToLower(hash) == sha1Lower {
|
||||||
|
return true // Hash match is definitive
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Indicator 1: Known RMM vendor name match (CommonRMMs)
|
||||||
|
rmmNameHit := false
|
||||||
|
for _, rmm := range common.CommonRMMs {
|
||||||
|
if strings.Contains(allText, strings.ToLower(rmm)) {
|
||||||
|
rmmNameHit = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if rmmNameHit {
|
||||||
|
score++
|
||||||
|
}
|
||||||
|
|
||||||
// 2) Known image suffix/file pattern hit (robust to registry naming)
|
// Indicator 2: Known RMM executable/binary pattern (CommonImageSuffixes)
|
||||||
suffixHit := false
|
binaryPatternHit := false
|
||||||
imgPathLower := strings.ToLower(ar.ImagePath)
|
imgPathLower := strings.ToLower(ar.ImagePath)
|
||||||
imgNameLower := strings.ToLower(ar.ImageName)
|
imgNameLower := strings.ToLower(ar.ImageName)
|
||||||
for _, suf := range common.CommonImageSuffixes {
|
launchLower := strings.ToLower(ar.LaunchString)
|
||||||
s := strings.ToLower(suf)
|
for _, pattern := range common.CommonImageSuffixes {
|
||||||
if strings.Contains(imgPathLower, s) || strings.Contains(imgNameLower, s) {
|
patternLower := strings.ToLower(pattern)
|
||||||
suffixHit = true
|
if strings.Contains(imgPathLower, patternLower) ||
|
||||||
|
strings.Contains(imgNameLower, patternLower) ||
|
||||||
|
strings.Contains(launchLower, patternLower) {
|
||||||
|
binaryPatternHit = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if binaryPatternHit {
|
||||||
|
score++
|
||||||
|
}
|
||||||
|
|
||||||
// 3) Known vendor DNS in launch string/command
|
// Indicator 3: Known RMM DNS/domain in command line or launch string (CommonDNS)
|
||||||
dnsHit := false
|
dnsHit := false
|
||||||
ls := strings.ToLower(ar.LaunchString)
|
argsLower := strings.ToLower(ar.Arguments)
|
||||||
for _, d := range common.CommonDNS {
|
for _, dns := range common.CommonDNS {
|
||||||
if strings.Contains(ls, strings.ToLower(d)) {
|
dnsLower := strings.ToLower(dns)
|
||||||
|
// Handle wildcard patterns: *.example.com should match anything.example.com
|
||||||
|
if strings.HasPrefix(dnsLower, "*.") {
|
||||||
|
// Match the domain suffix (e.g., ".example.com")
|
||||||
|
domainSuffix := dnsLower[1:] // Remove the * but keep the dot
|
||||||
|
if strings.Contains(launchLower, domainSuffix) || strings.Contains(argsLower, domainSuffix) {
|
||||||
|
dnsHit = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else if strings.HasSuffix(dnsLower, ".*") {
|
||||||
|
// Handle patterns like example.* - match the prefix
|
||||||
|
domainPrefix := dnsLower[:len(dnsLower)-2] // Remove the .*
|
||||||
|
if strings.Contains(launchLower, domainPrefix) || strings.Contains(argsLower, domainPrefix) {
|
||||||
|
dnsHit = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Exact domain match (no wildcard)
|
||||||
|
if strings.Contains(launchLower, dnsLower) || strings.Contains(argsLower, dnsLower) {
|
||||||
dnsHit = true
|
dnsHit = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if dnsHit {
|
||||||
|
score++
|
||||||
|
}
|
||||||
|
|
||||||
// Require two independent signals to reduce false positives
|
// Indicator 4: Suspicious installation path (temp, public, programdata)
|
||||||
if (vendorHit && (suffixHit || dnsHit)) || (suffixHit && dnsHit) {
|
pathSuspicious, _ := common.AnalyzeExecutablePath(ar.ImagePath)
|
||||||
return true
|
if !pathSuspicious && ar.LaunchString != "" {
|
||||||
|
pathSuspicious, _ = common.AnalyzeExecutablePath(ar.LaunchString)
|
||||||
}
|
}
|
||||||
return false
|
if pathSuspicious {
|
||||||
|
score++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Require at least 2 independent Indicator to reduce false positives
|
||||||
|
return score >= 2
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
// CommonRMMHashes contains known SHA256 hashes of RMM executables
|
||||||
|
// These are high-confidence indicators - a hash match is a strong signal
|
||||||
|
// Sources: VirusTotal, LOLRMM, threat intelligence reports
|
||||||
|
var CommonRMMHashes = []string{
|
||||||
|
// TODO: Add hashes here
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommonRMMHashesSHA1 contains known SHA1 hashes of RMM executables
|
||||||
|
var CommonRMMHashesSHA1 = []string{
|
||||||
|
// TODO: Add hashes here
|
||||||
|
// SHA256 is preferred for collision resistance
|
||||||
|
}
|
||||||
@@ -9,6 +9,23 @@ import (
|
|||||||
"github.com/Kraken-OffSec/Scurvy/core/process"
|
"github.com/Kraken-OffSec/Scurvy/core/process"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Whitelist for our own tool and legitimate system components
|
||||||
|
var whitelist = []string{
|
||||||
|
"rmm-hunter",
|
||||||
|
}
|
||||||
|
|
||||||
|
func isWhitelisted(proc process.Process) bool {
|
||||||
|
allText := strings.ToLower(strings.Join([]string{
|
||||||
|
proc.Executable(), proc.Path(),
|
||||||
|
}, "|"))
|
||||||
|
for _, w := range whitelist {
|
||||||
|
if strings.Contains(allText, w) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func Detect() []Process {
|
func Detect() []Process {
|
||||||
fmt.Printf("[*] Enumerating Processes \n")
|
fmt.Printf("[*] Enumerating Processes \n")
|
||||||
|
|
||||||
@@ -27,6 +44,11 @@ func compareProcesses(processes []process.Process) []Process {
|
|||||||
var suspiciousProcesses []Process
|
var suspiciousProcesses []Process
|
||||||
|
|
||||||
for _, proc := range processes {
|
for _, proc := range processes {
|
||||||
|
// Skip whitelisted processes (our own tool)
|
||||||
|
if isWhitelisted(proc) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
procName := proc.Executable()
|
procName := proc.Executable()
|
||||||
procNameLower := strings.ToLower(procName)
|
procNameLower := strings.ToLower(procName)
|
||||||
|
|
||||||
|
|||||||
@@ -10,24 +10,22 @@ import (
|
|||||||
"golang.org/x/sys/windows"
|
"golang.org/x/sys/windows"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ignore overly-generic tokens when matching vendor names
|
// Whitelist for our own tool and legitimate system components
|
||||||
var genericTokens = map[string]struct{}{
|
var whitelist = []string{
|
||||||
"remote": {}, "control": {}, "support": {}, "assist": {}, "viewer": {},
|
"rmm-hunter",
|
||||||
"server": {}, "service": {}, "manager": {}, "desktop": {}, "host": {},
|
|
||||||
"client": {}, "agent": {}, "connect": {}, "access": {}, "admin": {},
|
|
||||||
"vpn": {}, "ssh": {}, "vnc": {}, "rdp": {}, "microsoft": {}, "windows": {},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func isNonGenericToken(t string) bool {
|
func isWhitelisted(config service.ServiceConfig) bool {
|
||||||
t = strings.ToLower(strings.TrimSpace(t))
|
allText := strings.ToLower(strings.Join([]string{
|
||||||
if len(t) < 4 {
|
config.DisplayName, config.ServiceStartName, config.BinaryPathName, config.Description,
|
||||||
return false
|
}, "|"))
|
||||||
}
|
for _, w := range whitelist {
|
||||||
if _, ok := genericTokens[t]; ok {
|
if strings.Contains(allText, w) {
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func Detect() []*Service {
|
func Detect() []*Service {
|
||||||
fmt.Printf("[*] Enumerating Services \n")
|
fmt.Printf("[*] Enumerating Services \n")
|
||||||
@@ -64,25 +62,13 @@ func compareServices(serviceStrings []string, scm *service.Mgr) []*Service {
|
|||||||
fmt.Printf(" [>-] Error getting service config %s: %s\n", serviceString, err.Error())
|
fmt.Printf(" [>-] Error getting service config %s: %s\n", serviceString, err.Error())
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
svcStartName := strings.ToLower(config.ServiceStartName)
|
|
||||||
svcDisplayName := strings.ToLower(config.DisplayName)
|
|
||||||
svcBinaryPath := strings.ToLower(config.BinaryPathName)
|
|
||||||
|
|
||||||
// Check against known RMMs
|
// Skip whitelisted services (our own tool)
|
||||||
isRMMMatch := false
|
if isWhitelisted(config) {
|
||||||
for _, rmm := range common.CommonRMMs {
|
|
||||||
if !isNonGenericToken(rmm) {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
rmmLower := strings.ToLower(rmm)
|
|
||||||
if strings.Contains(svcDisplayName, rmmLower) || strings.Contains(svcStartName, rmmLower) || strings.Contains(svcBinaryPath, rmmLower) {
|
|
||||||
isRMMMatch = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only flag when there is a positive RMM vendor token match
|
if isSuspiciousService(config) {
|
||||||
if isRMMMatch {
|
|
||||||
fmt.Printf(" [?] Found %s\n", config.DisplayName)
|
fmt.Printf(" [?] Found %s\n", config.DisplayName)
|
||||||
suspiciousServices = append(suspiciousServices, &Service{
|
suspiciousServices = append(suspiciousServices, &Service{
|
||||||
Name: serviceString,
|
Name: serviceString,
|
||||||
@@ -110,6 +96,83 @@ func compareServices(serviceStrings []string, scm *service.Mgr) []*Service {
|
|||||||
return suspiciousServices
|
return suspiciousServices
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isSuspiciousService uses multi-indicator scoring to detect RMM services
|
||||||
|
// Requires at least 2 independent indicators to flag as suspicious
|
||||||
|
func isSuspiciousService(config service.ServiceConfig) bool {
|
||||||
|
score := 0
|
||||||
|
|
||||||
|
// Build searchable text from all service fields
|
||||||
|
allText := strings.ToLower(strings.Join([]string{
|
||||||
|
config.DisplayName, config.ServiceStartName, config.BinaryPathName, config.Description,
|
||||||
|
}, "|"))
|
||||||
|
|
||||||
|
// Indicator 1: Known RMM vendor name match (CommonRMMs)
|
||||||
|
rmmNameHit := false
|
||||||
|
for _, rmm := range common.CommonRMMs {
|
||||||
|
if strings.Contains(allText, strings.ToLower(rmm)) {
|
||||||
|
rmmNameHit = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if rmmNameHit {
|
||||||
|
score++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Indicator 2: Known RMM executable/binary pattern in service binary path (CommonImageSuffixes)
|
||||||
|
binaryPatternHit := false
|
||||||
|
binaryPathLower := strings.ToLower(config.BinaryPathName)
|
||||||
|
for _, pattern := range common.CommonImageSuffixes {
|
||||||
|
patternLower := strings.ToLower(pattern)
|
||||||
|
if strings.Contains(binaryPathLower, patternLower) {
|
||||||
|
binaryPatternHit = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if binaryPatternHit {
|
||||||
|
score++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Indicator 3: Known RMM DNS/domain in binary path or description (CommonDNS)
|
||||||
|
dnsHit := false
|
||||||
|
for _, dns := range common.CommonDNS {
|
||||||
|
dnsLower := strings.ToLower(dns)
|
||||||
|
// Handle wildcard patterns: *.example.com should match anything.example.com
|
||||||
|
if strings.HasPrefix(dnsLower, "*.") {
|
||||||
|
// Match the domain suffix (e.g., ".example.com")
|
||||||
|
domainSuffix := dnsLower[1:] // Remove the * but keep the dot
|
||||||
|
if strings.Contains(allText, domainSuffix) {
|
||||||
|
dnsHit = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else if strings.HasSuffix(dnsLower, ".*") {
|
||||||
|
// Handle patterns like example.* - match the prefix
|
||||||
|
domainPrefix := dnsLower[:len(dnsLower)-2] // Remove the .*
|
||||||
|
if strings.Contains(allText, domainPrefix) {
|
||||||
|
dnsHit = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Exact domain match (no wildcard)
|
||||||
|
if strings.Contains(allText, dnsLower) {
|
||||||
|
dnsHit = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if dnsHit {
|
||||||
|
score++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Indicator 4: Suspicious installation path (temp, public, programdata)
|
||||||
|
pathSuspicious, _ := common.AnalyzeExecutablePath(config.BinaryPathName)
|
||||||
|
if pathSuspicious {
|
||||||
|
score++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Require at least 2 independent Indicators to reduce false positives
|
||||||
|
return score >= 2
|
||||||
|
}
|
||||||
|
|
||||||
func getServiceType(raw uint32) string {
|
func getServiceType(raw uint32) string {
|
||||||
switch raw {
|
switch raw {
|
||||||
case 1:
|
case 1:
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package eliminate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
. "rmm-hunter/internal/suspicious"
|
||||||
|
|
||||||
|
"github.com/Kraken-OffSec/Scurvy"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EliminateAutoRun removes an autorun entry from the system
|
||||||
|
func EliminateAutoRun(ar AutoRun) error {
|
||||||
|
all := scurvy.ListAutoruns()
|
||||||
|
for _, a := range all {
|
||||||
|
if a.MD5 == ar.MD5 {
|
||||||
|
// Found it, delete it
|
||||||
|
return scurvy.DeleteAutorun(a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("%s | %s not found", ar.Location, ar.Entry)
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package eliminate
|
||||||
|
|
||||||
|
import "os"
|
||||||
|
|
||||||
|
// EliminateBinary removes a binary from the system
|
||||||
|
func EliminateBinary(path string) error {
|
||||||
|
return os.Remove(path)
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package eliminate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/Kraken-OffSec/Scurvy/core/firewall"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EliminateConnection adds an outbound block for the connection to the Windows firewall
|
||||||
|
func EliminateConnection(dst string) error {
|
||||||
|
// Create a new WindowsFirewall instance
|
||||||
|
fw, err := firewall.NewWindowsFirewall()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if firewall is enabled
|
||||||
|
if !fw.Enabled() {
|
||||||
|
return fmt.Errorf("windows firewall is currently disabled. please enable it and try again")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a block rule for the destination
|
||||||
|
return fw.AddRule(firewall.FirewallRule{
|
||||||
|
Name: fmt.Sprintf("Block Outgoing %s", dst),
|
||||||
|
Direction: "outbound",
|
||||||
|
Protocol: "any",
|
||||||
|
LocalPort: "any",
|
||||||
|
RemotePort: "any",
|
||||||
|
LocalAddress: "",
|
||||||
|
RemoteAddress: "",
|
||||||
|
Action: "block",
|
||||||
|
Profile: "",
|
||||||
|
Destination: dst,
|
||||||
|
Source: "",
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package eliminate
|
||||||
|
|
||||||
|
import "os"
|
||||||
|
|
||||||
|
func EliminateDirectory(path string) error {
|
||||||
|
return os.RemoveAll(path)
|
||||||
|
}
|
||||||
@@ -1 +0,0 @@
|
|||||||
package eliminate
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package eliminate
|
||||||
|
|
||||||
|
import (
|
||||||
|
. "rmm-hunter/internal/suspicious"
|
||||||
|
|
||||||
|
scurvy "github.com/Kraken-OffSec/Scurvy"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EliminateProcess kills a process and removes its binary from the system
|
||||||
|
func EliminateProcess(p Process) error {
|
||||||
|
err, proc := scurvy.FindProcessByPID(p.PID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return proc.Kill()
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package eliminate
|
||||||
|
|
||||||
|
import (
|
||||||
|
. "rmm-hunter/internal/suspicious"
|
||||||
|
|
||||||
|
scurvy "github.com/Kraken-OffSec/Scurvy"
|
||||||
|
)
|
||||||
|
|
||||||
|
func EliminateScheduledTask(t ScheduledTask) error {
|
||||||
|
return scurvy.DeleteScheduledTask(t.Name)
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package eliminate
|
||||||
|
|
||||||
|
import (
|
||||||
|
. "rmm-hunter/internal/suspicious"
|
||||||
|
|
||||||
|
scurvy "github.com/Kraken-OffSec/Scurvy"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EliminateService stops and removes a service from the system
|
||||||
|
func EliminateService(s Service) error {
|
||||||
|
return scurvy.RemoveService(s.Name)
|
||||||
|
}
|
||||||
@@ -50,12 +50,6 @@ AutoRun
|
|||||||
The object used to resemble the auto run methods used by the Suspicious software.
|
The object used to resemble the auto run methods used by the Suspicious software.
|
||||||
*/
|
*/
|
||||||
type AutoRun struct {
|
type AutoRun struct {
|
||||||
//Name string `json:"name"`
|
|
||||||
//Command string `json:"command"`
|
|
||||||
//Location string `json:"location"`
|
|
||||||
//Enabled bool `json:"enabled"`
|
|
||||||
//Description string `json:"description"`
|
|
||||||
|
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Location string `json:"location"`
|
Location string `json:"location"`
|
||||||
ImagePath string `json:"image_path"`
|
ImagePath string `json:"image_path"`
|
||||||
|
|||||||
Reference in New Issue
Block a user