remove infra.md.example, infra.md is the source of truth

This commit is contained in:
Azreen Jamal
2026-03-03 03:06:13 +08:00
parent 1ad3033cc1
commit a3c6d09350
86 changed files with 17093 additions and 39 deletions

View 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