// SPDX-License-Identifier: GPL-3.0-or-later package detectors import ( "fmt" "strings" "codeberg.org/anassaeneroi/enodia-sentinal/go-agent/internal/config" "codeberg.org/anassaeneroi/enodia-sentinal/go-agent/internal/model" ) const ( SIDMemoryObfuscation = 100039 SIDProcessHidingLibrary = 100048 SIDProcessInjectionLibrary = 100049 ) var memoryWritablePrefixes = []string{"/tmp/", "/dev/shm/", "/var/tmp/", "/run/user/"} var memoryLibraryExtensions = []string{".so", ".so.", ".dylib"} var memoryHideHints = []string{ "libhide", "libprocesshide", "process_hide", "proc_hide", "rootkit_hide", } // MemoryObfuscation ports the /proc//maps detector. It reports one alert // per signature and process, preserving Python's useful signal compression // when a process has many suspicious mappings of the same kind. func MemoryObfuscation(state model.State, cfg config.Config) []model.Alert { alerts := make([]model.Alert, 0) for _, process := range state.Processes { comm := process.Comm if comm == "" { comm = "?" } seen := map[string]bool{} for _, memory := range process.MemoryMaps { signature, severity, detail := memoryIndicator(memory, cfg) if signature == "" || seen[signature] { continue } // JIT allowlisting applies only to generic executable memory. A // library whose name suggests hiding or injection stays reviewable. if signature == "memory_obfuscation" && cfg.MemoryObfuscationAllowComms[comm] { continue } seen[signature] = true sid, classtype := SIDMemoryObfuscation, "memory-obfuscation" if signature == "process_hiding_library" { sid, classtype = SIDProcessHidingLibrary, "process-hiding" } else if signature == "process_injection_library" { sid, classtype = SIDProcessInjectionLibrary, "process-injection" } alerts = append(alerts, model.Alert{ SID: sid, Severity: severity, Signature: signature, Classtype: classtype, Key: fmt.Sprintf("mem:%s:%d:%x-%x", signature, process.PID, memory.Start, memory.End), Detail: fmt.Sprintf("pid=%d comm=%s %s range=%x-%x", process.PID, comm, detail, memory.Start, memory.End), PIDs: []int{process.PID}, }) } } return alerts } func memoryIndicator(memory model.MemoryMap, cfg config.Config) (string, string, string) { path := memory.Path if pathAllowed(path, cfg.MemoryObfuscationAllowPaths) { return "", "MEDIUM", "" } if path != "" && hasMemoryHint(path) { return "process_hiding_library", "CRITICAL", fmt.Sprintf("mapped library name suggests process hiding: %s", path) } executable := strings.Contains(memory.Perms, "x") writable := strings.Contains(memory.Perms, "w") if executable && path != "" && writableLibrary(path) { return "process_injection_library", "HIGH", fmt.Sprintf("executable library mapped from writable/runtime path: %s", path) } if executable && (strings.Contains(path, "memfd:") || strings.Contains(path, "(deleted)")) { return "memory_obfuscation", "CRITICAL", fmt.Sprintf("executable transient mapping perms=%s path=%s", memory.Perms, displayPath(path)) } if executable && writable { return "memory_obfuscation", "HIGH", fmt.Sprintf("writable+executable memory mapping perms=%s path=%s", memory.Perms, displayPath(path)) } if executable && anonymousPath(path) { return "memory_obfuscation", "HIGH", fmt.Sprintf("executable anonymous memory mapping perms=%s path=%s", memory.Perms, displayPath(path)) } return "", "MEDIUM", "" } func pathAllowed(path string, prefixes []string) bool { for _, prefix := range prefixes { if prefix != "" && strings.HasPrefix(path, prefix) { return true } } return false } func hasMemoryHint(path string) bool { name := path if index := strings.LastIndex(name, "/"); index >= 0 { name = name[index+1:] } name = strings.ToLower(name) for _, hint := range memoryHideHints { if strings.Contains(name, hint) { return true } } return false } func writableLibrary(path string) bool { lower := strings.ToLower(path) allowedPrefix := false for _, prefix := range memoryWritablePrefixes { if strings.HasPrefix(lower, prefix) { allowedPrefix = true break } } if !allowedPrefix { return false } name := lower if index := strings.LastIndex(name, "/"); index >= 0 { name = name[index+1:] } for _, extension := range memoryLibraryExtensions { if strings.Contains(name, extension) { return true } } return false } func anonymousPath(path string) bool { return path == "" || path == "[heap]" || path == "[stack]" || strings.HasPrefix(path, "[anon") || strings.HasPrefix(path, "[stack:") } func displayPath(path string) string { if path != "" { return path } return "" }