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