Expand rootkit monitoring and Claude docs
This commit is contained in:
parent
a7129e5666
commit
3e5f8fc3f7
16 changed files with 524 additions and 32 deletions
|
|
@ -28,9 +28,41 @@ SID_HIDDEN_PROC = 100022
|
|||
SID_HIDDEN_MODULE = 100023
|
||||
SID_HIDDEN_PORT = 100024
|
||||
SID_PROMISC = 100025
|
||||
SID_KNOWN_ROOTKIT_MODULE = 100028
|
||||
SID_TAINTED_MODULE = 100029
|
||||
SID_KERNEL_TAINTED = 100030
|
||||
SID_HIDDEN_UDP_PORT = 100031
|
||||
|
||||
IFF_PROMISC = 0x100
|
||||
|
||||
KNOWN_ROOTKIT_MODULES = frozenset({
|
||||
"adore", "adore-ng", "azazel", "diamorphine", "ipsecs_kbeast",
|
||||
"kbeast", "knark", "reptile", "suterusu", "syslogk",
|
||||
})
|
||||
|
||||
TAINT_FLAGS = {
|
||||
0: "proprietary-module",
|
||||
1: "forced-module-load",
|
||||
2: "unsafe-smp",
|
||||
3: "forced-module-unload",
|
||||
4: "machine-check",
|
||||
5: "bad-page",
|
||||
6: "user-tainted",
|
||||
7: "kernel-died-recently",
|
||||
8: "acpi-override",
|
||||
9: "kernel-warning",
|
||||
10: "staging-driver",
|
||||
11: "firmware-workaround",
|
||||
12: "out-of-tree-module",
|
||||
13: "unsigned-module",
|
||||
14: "soft-lockup",
|
||||
15: "live-patched",
|
||||
16: "auxiliary-taint",
|
||||
17: "randstruct-plugin",
|
||||
}
|
||||
|
||||
HIGH_RISK_TAINT_BITS = frozenset({1, 3, 7, 12, 13})
|
||||
|
||||
|
||||
# --- pure diff cores (unit-tested without a live system) ------------------
|
||||
|
||||
|
|
@ -49,6 +81,30 @@ def find_hidden_ports(procnet_ports: set[int], ss_ports: set[int]) -> set[int]:
|
|||
return procnet_ports - ss_ports
|
||||
|
||||
|
||||
def find_hidden_udp_ports(procnet_ports: set[int], ss_ports: set[int]) -> set[int]:
|
||||
"""UDP ports in /proc/net/udp that ss does not report."""
|
||||
return procnet_ports - ss_ports
|
||||
|
||||
|
||||
def _norm_module(name: str) -> str:
|
||||
return name.lower().replace("-", "_")
|
||||
|
||||
|
||||
def find_known_rootkit_modules(modules: set[str]) -> set[str]:
|
||||
known = {_norm_module(m) for m in KNOWN_ROOTKIT_MODULES}
|
||||
return {m for m in modules if _norm_module(m) in known}
|
||||
|
||||
|
||||
def decode_kernel_taint(value: int) -> list[str]:
|
||||
return [label for bit, label in sorted(TAINT_FLAGS.items())
|
||||
if value & (1 << bit)]
|
||||
|
||||
|
||||
def taint_severity(value: int) -> Severity:
|
||||
risky = any(value & (1 << bit) for bit in HIGH_RISK_TAINT_BITS)
|
||||
return Severity.HIGH if risky else Severity.MEDIUM
|
||||
|
||||
|
||||
# --- system views ---------------------------------------------------------
|
||||
|
||||
def proc_pids() -> set[int]:
|
||||
|
|
@ -110,6 +166,36 @@ def sys_live_modules() -> set[str]:
|
|||
return out
|
||||
|
||||
|
||||
def module_taints() -> dict[str, str]:
|
||||
"""Read per-module taint markers from /sys/module/*/taint.
|
||||
|
||||
Values are kernel-provided letters such as P/O/E for proprietary,
|
||||
out-of-tree, or unsigned modules. Built-ins often do not expose this file.
|
||||
"""
|
||||
out: dict[str, str] = {}
|
||||
base = "/sys/module"
|
||||
try:
|
||||
for m in os.listdir(base):
|
||||
try:
|
||||
with open(os.path.join(base, m, "taint")) as fh:
|
||||
taint = fh.read().strip()
|
||||
except OSError:
|
||||
continue
|
||||
if taint and taint != "0":
|
||||
out[m] = taint
|
||||
except OSError:
|
||||
pass
|
||||
return out
|
||||
|
||||
|
||||
def kernel_taint() -> int:
|
||||
try:
|
||||
with open("/proc/sys/kernel/tainted") as fh:
|
||||
return int(fh.read().strip())
|
||||
except (OSError, ValueError):
|
||||
return 0
|
||||
|
||||
|
||||
def _procnet_listen_ports(path: str) -> set[int]:
|
||||
ports: set[int] = set()
|
||||
try:
|
||||
|
|
@ -130,6 +216,28 @@ def procnet_listen_ports() -> set[int]:
|
|||
return _procnet_listen_ports("/proc/net/tcp") | _procnet_listen_ports("/proc/net/tcp6")
|
||||
|
||||
|
||||
def _procnet_udp_ports(path: str) -> set[int]:
|
||||
ports: set[int] = set()
|
||||
try:
|
||||
with open(path) as fh:
|
||||
next(fh, None) # header
|
||||
for line in fh:
|
||||
parts = line.split()
|
||||
if len(parts) < 2:
|
||||
continue
|
||||
local = parts[1] # HEX_ADDR:HEX_PORT
|
||||
port = int(local.rsplit(":", 1)[1], 16)
|
||||
if port:
|
||||
ports.add(port)
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
return ports
|
||||
|
||||
|
||||
def procnet_udp_ports() -> set[int]:
|
||||
return _procnet_udp_ports("/proc/net/udp") | _procnet_udp_ports("/proc/net/udp6")
|
||||
|
||||
|
||||
def ss_listen_ports(state: SystemState) -> set[int]:
|
||||
ports: set[int] = set()
|
||||
for s in state.listening_sockets():
|
||||
|
|
@ -139,6 +247,17 @@ def ss_listen_ports(state: SystemState) -> set[int]:
|
|||
return ports
|
||||
|
||||
|
||||
def ss_udp_ports(state: SystemState) -> set[int]:
|
||||
ports: set[int] = set()
|
||||
for s in state.sockets:
|
||||
if s.state not in {"UNCONN", "ESTAB"}:
|
||||
continue
|
||||
tail = s.local.rsplit(":", 1)
|
||||
if len(tail) == 2 and tail[1].isdigit():
|
||||
ports.add(int(tail[1]))
|
||||
return ports
|
||||
|
||||
|
||||
def promiscuous_interfaces() -> list[str]:
|
||||
out: list[str] = []
|
||||
base = "/sys/class/net"
|
||||
|
|
@ -184,7 +303,9 @@ def run(cfg: Config, state: SystemState | None = None) -> Iterator[Alert]:
|
|||
pids=tuple(sorted(confirmed)[:20]),
|
||||
sid=SID_HIDDEN_PROC, classtype="rootkit-hidden-process")
|
||||
|
||||
hidden_mods = find_hidden_modules(proc_modules(), sys_live_modules())
|
||||
proc_mods = proc_modules()
|
||||
sys_mods = sys_live_modules()
|
||||
hidden_mods = find_hidden_modules(proc_mods, sys_mods)
|
||||
for m in sorted(hidden_mods):
|
||||
yield Alert(
|
||||
severity=Severity.CRITICAL, signature="rootkit_hidden_module",
|
||||
|
|
@ -192,6 +313,36 @@ def run(cfg: Config, state: SystemState | None = None) -> Iterator[Alert]:
|
|||
detail=f"kernel module live in /sys/module but hidden from /proc/modules: {m}",
|
||||
sid=SID_HIDDEN_MODULE, classtype="rootkit-hidden-module")
|
||||
|
||||
all_modules = proc_mods | sys_mods
|
||||
known = find_known_rootkit_modules(all_modules)
|
||||
for m in sorted(known):
|
||||
yield Alert(
|
||||
severity=Severity.CRITICAL, signature="rootkit_known_module",
|
||||
key=f"rk:knownmod:{m}",
|
||||
detail=f"known LKM rootkit module name loaded or visible: {m}",
|
||||
sid=SID_KNOWN_ROOTKIT_MODULE, classtype="rootkit-known-module")
|
||||
|
||||
allowed = {_norm_module(m) for m in getattr(cfg, "rootcheck_module_allow", ())}
|
||||
for m, taint in sorted(module_taints().items()):
|
||||
if _norm_module(m) in allowed or m in known:
|
||||
continue
|
||||
yield Alert(
|
||||
severity=Severity.HIGH, signature="rootkit_tainted_module",
|
||||
key=f"rk:taintmod:{m}",
|
||||
detail=(f"kernel module has taint marker '{taint}' "
|
||||
f"(out-of-tree/proprietary/unsigned module): {m}"),
|
||||
sid=SID_TAINTED_MODULE, classtype="rootkit-tainted-module")
|
||||
|
||||
kt = kernel_taint()
|
||||
if kt:
|
||||
flags = decode_kernel_taint(kt)
|
||||
yield Alert(
|
||||
severity=taint_severity(kt), signature="kernel_tainted",
|
||||
key=f"rk:ktaint:{kt}",
|
||||
detail=(f"kernel taint value {kt} ({', '.join(flags) or 'unknown'}) — "
|
||||
"may indicate unsigned/out-of-tree modules, forced loads, or kernel faults"),
|
||||
sid=SID_KERNEL_TAINTED, classtype="kernel-integrity")
|
||||
|
||||
hidden_ports = find_hidden_ports(procnet_listen_ports(), ss_listen_ports(state))
|
||||
if hidden_ports:
|
||||
yield Alert(
|
||||
|
|
@ -201,6 +352,15 @@ def run(cfg: Config, state: SystemState | None = None) -> Iterator[Alert]:
|
|||
"(tool may be hooked): " + ", ".join(map(str, sorted(hidden_ports)))),
|
||||
sid=SID_HIDDEN_PORT, classtype="rootkit-hidden-port")
|
||||
|
||||
hidden_udp = find_hidden_udp_ports(procnet_udp_ports(), ss_udp_ports(state))
|
||||
if hidden_udp:
|
||||
yield Alert(
|
||||
severity=Severity.HIGH, signature="rootkit_hidden_udp_port",
|
||||
key=f"rk:hidudp:{min(hidden_udp)}",
|
||||
detail=("UDP port(s) in /proc/net/udp not reported by ss "
|
||||
"(tool may be hooked): " + ", ".join(map(str, sorted(hidden_udp)))),
|
||||
sid=SID_HIDDEN_UDP_PORT, classtype="rootkit-hidden-port")
|
||||
|
||||
for iface in promiscuous_interfaces():
|
||||
yield Alert(
|
||||
severity=Severity.MEDIUM, signature="promiscuous_interface",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue