Expand monitoring for credential theft and covert protocols
This commit is contained in:
parent
3e5f8fc3f7
commit
cb334c0c94
17 changed files with 675 additions and 25 deletions
|
|
@ -32,6 +32,9 @@ SID_KNOWN_ROOTKIT_MODULE = 100028
|
|||
SID_TAINTED_MODULE = 100029
|
||||
SID_KERNEL_TAINTED = 100030
|
||||
SID_HIDDEN_UDP_PORT = 100031
|
||||
SID_HIDDEN_RAW_SOCKET = 100034
|
||||
SID_RAW_ICMP_SOCKET = 100035
|
||||
SID_HIDDEN_PROTOCOL_SOCKET = 100037
|
||||
|
||||
IFF_PROMISC = 0x100
|
||||
|
||||
|
|
@ -62,6 +65,13 @@ TAINT_FLAGS = {
|
|||
}
|
||||
|
||||
HIGH_RISK_TAINT_BITS = frozenset({1, 3, 7, 12, 13})
|
||||
ICMP_RAW_PROTOCOLS = frozenset({1, 58}) # ICMP, IPv6-ICMP
|
||||
RAW_PROTOCOL_NAMES = {
|
||||
"icmp": 1,
|
||||
"ipv6-icmp": 58,
|
||||
"icmp6": 58,
|
||||
"sctp": 132,
|
||||
}
|
||||
|
||||
|
||||
# --- pure diff cores (unit-tested without a live system) ------------------
|
||||
|
|
@ -86,6 +96,18 @@ def find_hidden_udp_ports(procnet_ports: set[int], ss_ports: set[int]) -> set[in
|
|||
return procnet_ports - ss_ports
|
||||
|
||||
|
||||
def find_hidden_raw_protocols(procnet_protocols: set[int],
|
||||
ss_protocols: set[int]) -> set[int]:
|
||||
"""Raw socket protocols in /proc/net/raw that ss does not report."""
|
||||
return procnet_protocols - ss_protocols
|
||||
|
||||
|
||||
def find_hidden_protocol_kinds(procnet_kinds: set[str],
|
||||
ss_kinds: set[str]) -> set[str]:
|
||||
"""Special socket families visible in /proc/net but absent from ss."""
|
||||
return procnet_kinds - ss_kinds
|
||||
|
||||
|
||||
def _norm_module(name: str) -> str:
|
||||
return name.lower().replace("-", "_")
|
||||
|
||||
|
|
@ -238,6 +260,60 @@ def procnet_udp_ports() -> set[int]:
|
|||
return _procnet_udp_ports("/proc/net/udp") | _procnet_udp_ports("/proc/net/udp6")
|
||||
|
||||
|
||||
def _procnet_raw_protocols(path: str) -> set[int]:
|
||||
protocols: 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_PROTO
|
||||
proto = int(local.rsplit(":", 1)[1], 16)
|
||||
if proto:
|
||||
protocols.add(proto)
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
return protocols
|
||||
|
||||
|
||||
def procnet_raw_protocols() -> set[int]:
|
||||
return (_procnet_raw_protocols("/proc/net/raw")
|
||||
| _procnet_raw_protocols("/proc/net/raw6"))
|
||||
|
||||
|
||||
def _procnet_has_rows(path: str) -> bool:
|
||||
try:
|
||||
with open(path) as fh:
|
||||
next(fh, None) # header
|
||||
for line in fh:
|
||||
if line.strip():
|
||||
return True
|
||||
except OSError:
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
def procnet_protocol_kinds() -> set[str]:
|
||||
kinds: set[str] = set()
|
||||
checks = {
|
||||
"sctp": (
|
||||
"/proc/net/sctp/eps",
|
||||
"/proc/net/sctp/assocs",
|
||||
"/proc/net/sctp/remaddr",
|
||||
),
|
||||
"dccp": ("/proc/net/dccp", "/proc/net/dccp6"),
|
||||
"packet": ("/proc/net/packet",),
|
||||
"tipc": ("/proc/net/tipc/socket",),
|
||||
"xdp": ("/proc/net/xdp",),
|
||||
}
|
||||
for kind, paths in checks.items():
|
||||
if any(_procnet_has_rows(path) for path in paths):
|
||||
kinds.add(kind)
|
||||
return kinds
|
||||
|
||||
|
||||
def ss_listen_ports(state: SystemState) -> set[int]:
|
||||
ports: set[int] = set()
|
||||
for s in state.listening_sockets():
|
||||
|
|
@ -258,6 +334,31 @@ def ss_udp_ports(state: SystemState) -> set[int]:
|
|||
return ports
|
||||
|
||||
|
||||
def _protocol_from_endpoint(endpoint: str) -> int | None:
|
||||
tail = endpoint.rsplit(":", 1)
|
||||
if len(tail) != 2:
|
||||
return None
|
||||
label = tail[1].strip("[]").lower()
|
||||
if label.isdigit():
|
||||
return int(label)
|
||||
return RAW_PROTOCOL_NAMES.get(label)
|
||||
|
||||
|
||||
def ss_raw_protocols(state: SystemState) -> set[int]:
|
||||
protocols: set[int] = set()
|
||||
for s in state.sockets:
|
||||
if s.kind != "raw":
|
||||
continue
|
||||
proto = _protocol_from_endpoint(s.local)
|
||||
if proto is not None:
|
||||
protocols.add(proto)
|
||||
return protocols
|
||||
|
||||
|
||||
def ss_protocol_kinds(state: SystemState) -> set[str]:
|
||||
return {s.kind for s in state.sockets if s.kind}
|
||||
|
||||
|
||||
def promiscuous_interfaces() -> list[str]:
|
||||
out: list[str] = []
|
||||
base = "/sys/class/net"
|
||||
|
|
@ -361,6 +462,37 @@ def run(cfg: Config, state: SystemState | None = None) -> Iterator[Alert]:
|
|||
"(tool may be hooked): " + ", ".join(map(str, sorted(hidden_udp)))),
|
||||
sid=SID_HIDDEN_UDP_PORT, classtype="rootkit-hidden-port")
|
||||
|
||||
raw_protocols = procnet_raw_protocols()
|
||||
hidden_raw = find_hidden_raw_protocols(raw_protocols, ss_raw_protocols(state))
|
||||
if hidden_raw:
|
||||
yield Alert(
|
||||
severity=Severity.HIGH, signature="rootkit_hidden_raw_socket",
|
||||
key=f"rk:hidraw:{min(hidden_raw)}",
|
||||
detail=("raw socket protocol(s) in /proc/net/raw not reported by ss "
|
||||
"(tool may be hooked): " + ", ".join(map(str, sorted(hidden_raw)))),
|
||||
sid=SID_HIDDEN_RAW_SOCKET, classtype="rootkit-hidden-socket")
|
||||
|
||||
raw_icmp = raw_protocols & ICMP_RAW_PROTOCOLS
|
||||
if raw_icmp:
|
||||
yield Alert(
|
||||
severity=Severity.HIGH, signature="raw_icmp_socket",
|
||||
key=f"rk:rawicmp:{min(raw_icmp)}",
|
||||
detail=("persistent raw ICMP socket protocol(s) visible in /proc/net/raw: "
|
||||
+ ", ".join(map(str, sorted(raw_icmp)))
|
||||
+ " — validate authorized ping/monitoring tools or ICMP knockers"),
|
||||
sid=SID_RAW_ICMP_SOCKET, classtype="raw-socket-backdoor")
|
||||
|
||||
hidden_protocols = find_hidden_protocol_kinds(
|
||||
procnet_protocol_kinds(), ss_protocol_kinds(state))
|
||||
if hidden_protocols:
|
||||
yield Alert(
|
||||
severity=Severity.HIGH, signature="rootkit_hidden_protocol_socket",
|
||||
key=f"rk:hidproto:{sorted(hidden_protocols)[0]}",
|
||||
detail=("special protocol socket family present in /proc/net but "
|
||||
"absent from ss output (tool may be hooked): "
|
||||
+ ", ".join(sorted(hidden_protocols))),
|
||||
sid=SID_HIDDEN_PROTOCOL_SOCKET, classtype="rootkit-hidden-socket")
|
||||
|
||||
for iface in promiscuous_interfaces():
|
||||
yield Alert(
|
||||
severity=Severity.MEDIUM, signature="promiscuous_interface",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue