enodia-sentinal/enodia_sentinel/detectors/memory_obfuscation.py

120 lines
4.4 KiB
Python

# SPDX-License-Identifier: GPL-3.0-or-later
"""memory_obfuscation — memory-map indicators of hiding or injected payloads.
Encrypted/packed implants still need executable memory after decrypting code.
This detector does not read process memory; it only inspects ``/proc/<pid>/maps``
for high-signal shapes: executable anonymous mappings, executable memfd/deleted
mappings, RWX pages, and mapped libraries associated with process hiding or
process injection.
"""
from __future__ import annotations
from collections.abc import Iterator
from ..alert import Alert, Severity
from ..config import Config
from ..system import MemoryMap, SystemState
SID_MEMORY_OBFUSCATION = 100039
SID_PROCESS_HIDING_LIBRARY = 100048
SID_PROCESS_INJECTION_LIBRARY = 100049
_HIDE_LIBRARY_HINTS = frozenset({
"libhide", "libprocesshide", "process_hide", "proc_hide", "rootkit_hide",
})
_WRITABLE_MAP_PREFIXES = ("/tmp/", "/dev/shm/", "/var/tmp/", "/run/user/")
_LIB_EXTENSIONS = (".so", ".so.", ".dylib")
def _maps(proc) -> list[MemoryMap]:
maps = getattr(proc, "memory_maps", [])
return maps() if callable(maps) else maps
def _is_anon_exec_path(path: str) -> bool:
if not path:
return True
if path in {"[heap]", "[stack]"}:
return True
return path.startswith("[anon") or path.startswith("[stack:")
def _hide_library(path: str) -> bool:
name = path.rsplit("/", 1)[-1].lower()
return any(hint in name for hint in _HIDE_LIBRARY_HINTS)
def _writable_library(path: str) -> bool:
lower = path.lower()
if not lower.startswith(_WRITABLE_MAP_PREFIXES):
return False
name = lower.rsplit("/", 1)[-1]
return any(ext in name for ext in _LIB_EXTENSIONS)
def _allowed_path(path: str, cfg: Config) -> bool:
for prefix in cfg.memory_obfuscation_allow_paths:
if prefix and path.startswith(prefix):
return True
return False
def _indicator(mm: MemoryMap, cfg: Config) -> tuple[str, Severity, str]:
path = mm.path
if path and _allowed_path(path, cfg):
return "", Severity.MEDIUM, ""
if path and _hide_library(path):
return "process_hiding_library", Severity.CRITICAL, (
f"mapped library name suggests process hiding: {path}"
)
executable = "x" in mm.perms
writable = "w" in mm.perms
if executable and path and _writable_library(path):
return "process_injection_library", Severity.HIGH, (
f"executable library mapped from writable/runtime path: {path}"
)
if executable and ("memfd:" in path or "(deleted)" in path):
return "memory_obfuscation", Severity.CRITICAL, (
f"executable transient mapping perms={mm.perms} path={path or '<anonymous>'}"
)
if executable and writable:
return "memory_obfuscation", Severity.HIGH, (
f"writable+executable memory mapping perms={mm.perms} path={path or '<anonymous>'}"
)
if executable and _is_anon_exec_path(path):
return "memory_obfuscation", Severity.HIGH, (
f"executable anonymous memory mapping perms={mm.perms} path={path or '<anonymous>'}"
)
return "", Severity.MEDIUM, ""
def detect(state: SystemState, cfg: Config) -> Iterator[Alert]:
for proc in state.processes:
comm = proc.comm or "?"
seen: set[str] = set()
for mm in _maps(proc):
signature, severity, detail = _indicator(mm, cfg)
if not signature or signature in seen:
continue
if signature == "memory_obfuscation" and comm in cfg.memory_obfuscation_allow_comms:
continue
seen.add(signature)
sid = {
"process_hiding_library": SID_PROCESS_HIDING_LIBRARY,
"process_injection_library": SID_PROCESS_INJECTION_LIBRARY,
}.get(signature, SID_MEMORY_OBFUSCATION)
yield Alert(
severity=severity,
signature=signature,
key=f"mem:{signature}:{proc.pid}:{mm.start:x}-{mm.end:x}",
detail=(
f"pid={proc.pid} comm={comm} {detail} "
f"range={mm.start:x}-{mm.end:x}"
),
pids=(proc.pid,),
sid=sid,
classtype={
"process_hiding_library": "process-hiding",
"process_injection_library": "process-injection",
}.get(signature, "memory-obfuscation"),
)