remove infra.md.example, infra.md is the source of truth
This commit is contained in:
387
ayn-antivirus/ayn_antivirus/detectors/rootkit_detector.py
Normal file
387
ayn-antivirus/ayn_antivirus/detectors/rootkit_detector.py
Normal file
@@ -0,0 +1,387 @@
|
||||
"""Rootkit detector for AYN Antivirus.
|
||||
|
||||
Performs system-wide checks for indicators of rootkit compromise: known
|
||||
rootkit files, modified system binaries, hidden processes, hidden kernel
|
||||
modules, LD_PRELOAD hijacking, hidden network ports, and tampered logs.
|
||||
|
||||
Many checks require **root** privileges. On non-Linux systems, kernel-
|
||||
module and /proc-based checks are gracefully skipped.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Set
|
||||
|
||||
import psutil
|
||||
|
||||
from ayn_antivirus.constants import (
|
||||
KNOWN_ROOTKIT_FILES,
|
||||
MALICIOUS_ENV_VARS,
|
||||
)
|
||||
from ayn_antivirus.detectors.base import BaseDetector, DetectionResult
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RootkitDetector(BaseDetector):
|
||||
"""System-wide rootkit detection.
|
||||
|
||||
Unlike other detectors, the *file_path* argument is optional. When
|
||||
called without a path (or with ``file_path=None``) the detector runs
|
||||
every host-level check. When given a file it limits itself to checks
|
||||
relevant to that file.
|
||||
"""
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# BaseDetector interface
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "rootkit_detector"
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
return "Detects rootkits via file, process, module, and environment analysis"
|
||||
|
||||
def detect(
|
||||
self,
|
||||
file_path: str | Path | None = None,
|
||||
file_content: Optional[bytes] = None,
|
||||
file_hash: Optional[str] = None,
|
||||
) -> List[DetectionResult]:
|
||||
"""Run rootkit checks.
|
||||
|
||||
If *file_path* is ``None``, all system-wide checks are executed.
|
||||
Otherwise only file-specific checks run.
|
||||
"""
|
||||
results: List[DetectionResult] = []
|
||||
|
||||
if file_path is not None:
|
||||
fp = Path(file_path)
|
||||
# File-specific: is this a known rootkit artefact?
|
||||
results.extend(self._check_known_rootkit_file(fp))
|
||||
return results
|
||||
|
||||
# --- Full system-wide scan ---
|
||||
results.extend(self._check_known_rootkit_files())
|
||||
results.extend(self._check_ld_preload())
|
||||
results.extend(self._check_ld_so_preload())
|
||||
results.extend(self._check_hidden_processes())
|
||||
results.extend(self._check_hidden_kernel_modules())
|
||||
results.extend(self._check_hidden_network_ports())
|
||||
results.extend(self._check_malicious_env_vars())
|
||||
results.extend(self._check_tampered_logs())
|
||||
|
||||
return results
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Known rootkit files
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _check_known_rootkit_files(self) -> List[DetectionResult]:
|
||||
"""Check every path in :pydata:`KNOWN_ROOTKIT_FILES`."""
|
||||
results: List[DetectionResult] = []
|
||||
for path_str in KNOWN_ROOTKIT_FILES:
|
||||
p = Path(path_str)
|
||||
if p.exists():
|
||||
results.append(DetectionResult(
|
||||
threat_name="Rootkit.KnownFile",
|
||||
threat_type="ROOTKIT",
|
||||
severity="CRITICAL",
|
||||
confidence=90,
|
||||
details=f"Known rootkit artefact present: {path_str}",
|
||||
detector_name=self.name,
|
||||
))
|
||||
return results
|
||||
|
||||
def _check_known_rootkit_file(self, file_path: Path) -> List[DetectionResult]:
|
||||
"""Check whether *file_path* is a known rootkit file."""
|
||||
results: List[DetectionResult] = []
|
||||
path_str = str(file_path)
|
||||
if path_str in KNOWN_ROOTKIT_FILES:
|
||||
results.append(DetectionResult(
|
||||
threat_name="Rootkit.KnownFile",
|
||||
threat_type="ROOTKIT",
|
||||
severity="CRITICAL",
|
||||
confidence=90,
|
||||
details=f"Known rootkit artefact: {path_str}",
|
||||
detector_name=self.name,
|
||||
))
|
||||
return results
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# LD_PRELOAD / ld.so.preload
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _check_ld_preload(self) -> List[DetectionResult]:
|
||||
"""Flag the ``LD_PRELOAD`` environment variable if set globally."""
|
||||
results: List[DetectionResult] = []
|
||||
val = os.environ.get("LD_PRELOAD", "")
|
||||
if val:
|
||||
results.append(DetectionResult(
|
||||
threat_name="Rootkit.LDPreload.EnvVar",
|
||||
threat_type="ROOTKIT",
|
||||
severity="CRITICAL",
|
||||
confidence=85,
|
||||
details=f"LD_PRELOAD is set: {val}",
|
||||
detector_name=self.name,
|
||||
))
|
||||
return results
|
||||
|
||||
def _check_ld_so_preload(self) -> List[DetectionResult]:
|
||||
"""Check ``/etc/ld.so.preload`` for suspicious entries."""
|
||||
results: List[DetectionResult] = []
|
||||
ld_preload_file = Path("/etc/ld.so.preload")
|
||||
if not ld_preload_file.exists():
|
||||
return results
|
||||
|
||||
try:
|
||||
content = ld_preload_file.read_text().strip()
|
||||
except PermissionError:
|
||||
self._warn("Cannot read /etc/ld.so.preload")
|
||||
return results
|
||||
|
||||
if content:
|
||||
lines = [l.strip() for l in content.splitlines() if l.strip() and not l.startswith("#")]
|
||||
if lines:
|
||||
results.append(DetectionResult(
|
||||
threat_name="Rootkit.LDPreload.File",
|
||||
threat_type="ROOTKIT",
|
||||
severity="CRITICAL",
|
||||
confidence=85,
|
||||
details=f"/etc/ld.so.preload contains entries: {', '.join(lines[:5])}",
|
||||
detector_name=self.name,
|
||||
))
|
||||
return results
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Hidden processes
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _check_hidden_processes(self) -> List[DetectionResult]:
|
||||
"""Compare /proc PIDs with psutil to find hidden processes."""
|
||||
results: List[DetectionResult] = []
|
||||
proc_dir = Path("/proc")
|
||||
if not proc_dir.is_dir():
|
||||
return results # non-Linux
|
||||
|
||||
proc_pids: Set[int] = set()
|
||||
try:
|
||||
for entry in proc_dir.iterdir():
|
||||
if entry.name.isdigit():
|
||||
proc_pids.add(int(entry.name))
|
||||
except PermissionError:
|
||||
return results
|
||||
|
||||
psutil_pids = set(psutil.pids())
|
||||
hidden = proc_pids - psutil_pids
|
||||
|
||||
for pid in hidden:
|
||||
name = ""
|
||||
try:
|
||||
comm = proc_dir / str(pid) / "comm"
|
||||
if comm.exists():
|
||||
name = comm.read_text().strip()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
results.append(DetectionResult(
|
||||
threat_name="Rootkit.HiddenProcess",
|
||||
threat_type="ROOTKIT",
|
||||
severity="CRITICAL",
|
||||
confidence=85,
|
||||
details=f"PID {pid} ({name or 'unknown'}) visible in /proc but hidden from psutil",
|
||||
detector_name=self.name,
|
||||
))
|
||||
|
||||
return results
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Hidden kernel modules
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _check_hidden_kernel_modules(self) -> List[DetectionResult]:
|
||||
"""Compare ``lsmod`` output with ``/proc/modules`` to find discrepancies."""
|
||||
results: List[DetectionResult] = []
|
||||
proc_modules_path = Path("/proc/modules")
|
||||
if not proc_modules_path.exists():
|
||||
return results # non-Linux
|
||||
|
||||
# Modules from /proc/modules.
|
||||
try:
|
||||
proc_content = proc_modules_path.read_text()
|
||||
except PermissionError:
|
||||
return results
|
||||
|
||||
proc_mods: Set[str] = set()
|
||||
for line in proc_content.splitlines():
|
||||
parts = line.split()
|
||||
if parts:
|
||||
proc_mods.add(parts[0])
|
||||
|
||||
# Modules from lsmod.
|
||||
lsmod_mods: Set[str] = set()
|
||||
try:
|
||||
output = subprocess.check_output(["lsmod"], stderr=subprocess.DEVNULL, timeout=10)
|
||||
for line in output.decode(errors="replace").splitlines()[1:]:
|
||||
parts = line.split()
|
||||
if parts:
|
||||
lsmod_mods.add(parts[0])
|
||||
except (FileNotFoundError, subprocess.SubprocessError, OSError):
|
||||
return results # lsmod not available
|
||||
|
||||
# Modules in /proc but NOT in lsmod → hidden from userspace.
|
||||
hidden = proc_mods - lsmod_mods
|
||||
for mod in hidden:
|
||||
results.append(DetectionResult(
|
||||
threat_name="Rootkit.HiddenKernelModule",
|
||||
threat_type="ROOTKIT",
|
||||
severity="CRITICAL",
|
||||
confidence=80,
|
||||
details=f"Kernel module '{mod}' in /proc/modules but hidden from lsmod",
|
||||
detector_name=self.name,
|
||||
))
|
||||
|
||||
return results
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Hidden network ports
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _check_hidden_network_ports(self) -> List[DetectionResult]:
|
||||
"""Compare ``ss``/``netstat`` listening ports with psutil."""
|
||||
results: List[DetectionResult] = []
|
||||
|
||||
# Ports from psutil.
|
||||
psutil_ports: Set[int] = set()
|
||||
try:
|
||||
for conn in psutil.net_connections(kind="inet"):
|
||||
if conn.status == "LISTEN" and conn.laddr:
|
||||
psutil_ports.add(conn.laddr.port)
|
||||
except psutil.AccessDenied:
|
||||
return results
|
||||
|
||||
# Ports from ss.
|
||||
ss_ports: Set[int] = set()
|
||||
try:
|
||||
output = subprocess.check_output(
|
||||
["ss", "-tlnH"], stderr=subprocess.DEVNULL, timeout=10
|
||||
)
|
||||
for line in output.decode(errors="replace").splitlines():
|
||||
# Typical ss output: LISTEN 0 128 0.0.0.0:22 ...
|
||||
parts = line.split()
|
||||
for part in parts:
|
||||
if ":" in part:
|
||||
try:
|
||||
port = int(part.rsplit(":", 1)[1])
|
||||
ss_ports.add(port)
|
||||
except (ValueError, IndexError):
|
||||
continue
|
||||
except (FileNotFoundError, subprocess.SubprocessError, OSError):
|
||||
return results # ss not available
|
||||
|
||||
# Ports in ss but not in psutil → potentially hidden by a rootkit.
|
||||
hidden = ss_ports - psutil_ports
|
||||
for port in hidden:
|
||||
results.append(DetectionResult(
|
||||
threat_name="Rootkit.HiddenPort",
|
||||
threat_type="ROOTKIT",
|
||||
severity="HIGH",
|
||||
confidence=70,
|
||||
details=f"Listening port {port} visible to ss but hidden from psutil",
|
||||
detector_name=self.name,
|
||||
))
|
||||
|
||||
return results
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Malicious environment variables
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _check_malicious_env_vars(self) -> List[DetectionResult]:
|
||||
"""Check the current environment for known-risky variables."""
|
||||
results: List[DetectionResult] = []
|
||||
for entry in MALICIOUS_ENV_VARS:
|
||||
if "=" in entry:
|
||||
# Exact key=value match (e.g. "HISTFILE=/dev/null").
|
||||
key, val = entry.split("=", 1)
|
||||
if os.environ.get(key) == val:
|
||||
results.append(DetectionResult(
|
||||
threat_name="Rootkit.EnvVar.Suspicious",
|
||||
threat_type="ROOTKIT",
|
||||
severity="HIGH",
|
||||
confidence=75,
|
||||
details=f"Suspicious environment variable: {key}={val}",
|
||||
detector_name=self.name,
|
||||
))
|
||||
else:
|
||||
# Key presence check (e.g. "LD_PRELOAD").
|
||||
if entry in os.environ:
|
||||
results.append(DetectionResult(
|
||||
threat_name="Rootkit.EnvVar.Suspicious",
|
||||
threat_type="ROOTKIT",
|
||||
severity="HIGH",
|
||||
confidence=65,
|
||||
details=f"Suspicious environment variable set: {entry}={os.environ[entry][:100]}",
|
||||
detector_name=self.name,
|
||||
))
|
||||
|
||||
return results
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Tampered log files
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
_LOG_PATHS = [
|
||||
"/var/log/auth.log",
|
||||
"/var/log/syslog",
|
||||
"/var/log/messages",
|
||||
"/var/log/secure",
|
||||
"/var/log/wtmp",
|
||||
"/var/log/btmp",
|
||||
"/var/log/lastlog",
|
||||
]
|
||||
|
||||
def _check_tampered_logs(self) -> List[DetectionResult]:
|
||||
"""Look for signs of log tampering: zero-byte logs, missing logs,
|
||||
or logs whose mtime is suspiciously older than expected.
|
||||
"""
|
||||
results: List[DetectionResult] = []
|
||||
|
||||
for log_path_str in self._LOG_PATHS:
|
||||
log_path = Path(log_path_str)
|
||||
if not log_path.exists():
|
||||
# Missing critical log.
|
||||
if log_path_str in ("/var/log/auth.log", "/var/log/syslog", "/var/log/wtmp"):
|
||||
results.append(DetectionResult(
|
||||
threat_name="Rootkit.Log.Missing",
|
||||
threat_type="ROOTKIT",
|
||||
severity="HIGH",
|
||||
confidence=60,
|
||||
details=f"Critical log file missing: {log_path_str}",
|
||||
detector_name=self.name,
|
||||
))
|
||||
continue
|
||||
|
||||
try:
|
||||
st = log_path.stat()
|
||||
except OSError:
|
||||
continue
|
||||
|
||||
# Zero-byte log file (may have been truncated).
|
||||
if st.st_size == 0:
|
||||
results.append(DetectionResult(
|
||||
threat_name="Rootkit.Log.Truncated",
|
||||
threat_type="ROOTKIT",
|
||||
severity="HIGH",
|
||||
confidence=70,
|
||||
details=f"Log file is empty (possibly truncated): {log_path_str}",
|
||||
detector_name=self.name,
|
||||
))
|
||||
|
||||
return results
|
||||
Reference in New Issue
Block a user