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
|
|
@ -6,7 +6,9 @@ from dataclasses import dataclass, field
|
|||
from enodia_sentinel.alert import Severity
|
||||
from enodia_sentinel.config import Config
|
||||
from enodia_sentinel.detectors import (
|
||||
deleted_exe, egress, ld_preload, new_listener, new_suid, reverse_shell,
|
||||
credential_access, deleted_exe, egress, input_snooper, ld_preload,
|
||||
new_listener, new_suid, reverse_shell,
|
||||
stealth_network,
|
||||
)
|
||||
from enodia_sentinel.system import Socket, SystemState
|
||||
|
||||
|
|
@ -23,6 +25,7 @@ class FakeProc:
|
|||
ppid: int = 1
|
||||
uid: int = 0
|
||||
cwd: str = "/"
|
||||
fd_targets: dict = field(default_factory=dict)
|
||||
|
||||
def stdio_socket_inode(self):
|
||||
return self._stdio_inode
|
||||
|
|
@ -90,6 +93,71 @@ class TestDeletedExe(unittest.TestCase):
|
|||
self.assertEqual(list(deleted_exe.detect(state, cfg())), [])
|
||||
|
||||
|
||||
class TestInputSnooper(unittest.TestCase):
|
||||
def test_input_device_alerts(self):
|
||||
proc = FakeProc(pid=320, comm="hoxha",
|
||||
fd_targets={"7": "/dev/input/event3"})
|
||||
alerts = list(input_snooper.detect(SystemState(processes=[proc]), cfg()))
|
||||
self.assertEqual(len(alerts), 1)
|
||||
self.assertEqual(alerts[0].signature, "input_snooper")
|
||||
self.assertEqual(alerts[0].severity, Severity.HIGH)
|
||||
|
||||
def test_allowlisted_input_process_ignored(self):
|
||||
proc = FakeProc(pid=321, comm="Xorg",
|
||||
fd_targets={"7": "/dev/input/event3"})
|
||||
self.assertEqual(list(input_snooper.detect(SystemState(processes=[proc]), cfg())), [])
|
||||
|
||||
|
||||
class TestCredentialAccess(unittest.TestCase):
|
||||
def test_shadow_access_alerts(self):
|
||||
proc = FakeProc(pid=330, comm="hoxha",
|
||||
fd_targets={"4": "/etc/shadow"})
|
||||
alerts = list(credential_access.detect(SystemState(processes=[proc]), cfg()))
|
||||
self.assertEqual(len(alerts), 1)
|
||||
self.assertEqual(alerts[0].signature, "credential_access")
|
||||
self.assertEqual(alerts[0].severity, Severity.CRITICAL)
|
||||
|
||||
def test_private_key_access_alerts(self):
|
||||
proc = FakeProc(pid=331, comm="collector",
|
||||
fd_targets={"5": "/home/luna/.ssh/id_ed25519"})
|
||||
alerts = list(credential_access.detect(SystemState(processes=[proc]), cfg()))
|
||||
self.assertEqual(len(alerts), 1)
|
||||
self.assertIn("private SSH key", alerts[0].detail)
|
||||
|
||||
def test_browser_access_by_browser_ignored(self):
|
||||
proc = FakeProc(
|
||||
pid=332, comm="firefox",
|
||||
fd_targets={"8": "/home/luna/.mozilla/firefox/a/key4.db"},
|
||||
)
|
||||
self.assertEqual(list(credential_access.detect(SystemState(processes=[proc]), cfg())), [])
|
||||
|
||||
|
||||
class TestStealthNetwork(unittest.TestCase):
|
||||
def test_sctp_public_peer_alerts(self):
|
||||
sock = Socket("ESTAB", "10.0.0.2:5000", "8.8.8.8:4443",
|
||||
1, "hoxha", 340, "sctp")
|
||||
alerts = list(stealth_network.detect(SystemState(sockets=[sock]), cfg()))
|
||||
self.assertEqual(len(alerts), 1)
|
||||
self.assertEqual(alerts[0].signature, "stealth_network")
|
||||
self.assertEqual(alerts[0].severity, Severity.HIGH)
|
||||
|
||||
def test_packet_socket_allowlisted_comm_ignored(self):
|
||||
sock = Socket("UNCONN", "*:eth0", "*", 1, "tcpdump", 341, "packet")
|
||||
self.assertEqual(list(stealth_network.detect(SystemState(sockets=[sock]), cfg())), [])
|
||||
|
||||
def test_allowlisted_kind_ignored(self):
|
||||
c = Config()
|
||||
c.stealth_network_allow_kinds = frozenset({"mptcp"})
|
||||
sock = Socket("ESTAB", "10.0.0.2:5000", "8.8.8.8:443",
|
||||
1, "curl", 342, "mptcp")
|
||||
self.assertEqual(list(stealth_network.detect(SystemState(sockets=[sock]), c)), [])
|
||||
|
||||
def test_tcp_ignored(self):
|
||||
sock = Socket("ESTAB", "10.0.0.2:5000", "8.8.8.8:443",
|
||||
1, "python3", 343, "tcp")
|
||||
self.assertEqual(list(stealth_network.detect(SystemState(sockets=[sock]), cfg())), [])
|
||||
|
||||
|
||||
class TestNewSuid(unittest.TestCase):
|
||||
def test_new_in_writable_is_critical(self):
|
||||
state = SystemState(
|
||||
|
|
|
|||
|
|
@ -32,6 +32,15 @@ class TestDiffCores(unittest.TestCase):
|
|||
self.assertEqual(
|
||||
rootcheck.find_hidden_udp_ports({53, 5353, 4444}, {53, 5353}), {4444})
|
||||
|
||||
def test_hidden_raw_protocols_in_procnet_not_in_ss(self):
|
||||
self.assertEqual(
|
||||
rootcheck.find_hidden_raw_protocols({1, 58}, {58}), {1})
|
||||
|
||||
def test_hidden_protocol_kinds_in_procnet_not_in_ss(self):
|
||||
self.assertEqual(
|
||||
rootcheck.find_hidden_protocol_kinds({"sctp", "packet"}, {"packet"}),
|
||||
{"sctp"})
|
||||
|
||||
def test_known_rootkit_module_names_normalized(self):
|
||||
self.assertEqual(
|
||||
rootcheck.find_known_rootkit_modules({"ext4", "diamorphine", "adore-ng"}),
|
||||
|
|
@ -62,6 +71,33 @@ class TestSsPortsFromState(unittest.TestCase):
|
|||
state = SystemState(sockets=socks)
|
||||
self.assertEqual(rootcheck.ss_udp_ports(state), {53, 5353})
|
||||
|
||||
def test_extracts_raw_protocols_from_injected_state(self):
|
||||
socks = [
|
||||
Socket("UNCONN", "0.0.0.0:icmp", "0.0.0.0:*", 10, "hoxha", 1, "raw"),
|
||||
Socket("UNCONN", "[::]:ipv6-icmp", "[::]:*", 11, "monitor", 2, "raw"),
|
||||
Socket("LISTEN", "0.0.0.0:22", "0.0.0.0:0", 12, "sshd", 3, "tcp"),
|
||||
]
|
||||
state = SystemState(sockets=socks)
|
||||
self.assertEqual(rootcheck.ss_raw_protocols(state), {1, 58})
|
||||
|
||||
def test_extracts_socket_kinds_from_injected_state(self):
|
||||
state = SystemState(sockets=[
|
||||
Socket("ESTAB", "10.0.0.2:5000", "8.8.8.8:4443", 10, "h", 1, "sctp"),
|
||||
Socket("UNCONN", "*:eth0", "*", 11, "tcpdump", 2, "packet"),
|
||||
])
|
||||
self.assertEqual(rootcheck.ss_protocol_kinds(state), {"sctp", "packet"})
|
||||
|
||||
def test_ss_parser_handles_netid_column(self):
|
||||
text = 'p_raw UNCONN 0 0 *:eth0 * users:(("dhcpcd",pid=12,fd=7)) ino:123'
|
||||
sock = SystemState._parse_ss(text, kind="packet")[0]
|
||||
self.assertEqual(sock.state, "UNCONN")
|
||||
self.assertEqual(sock.local, "*:eth0")
|
||||
self.assertEqual(sock.peer, "*")
|
||||
self.assertEqual(sock.comm, "dhcpcd")
|
||||
self.assertEqual(sock.pid, 12)
|
||||
self.assertEqual(sock.inode, 123)
|
||||
self.assertEqual(sock.kind, "packet")
|
||||
|
||||
|
||||
class TestRunIntegration(unittest.TestCase):
|
||||
"""Drive run() with every system view monkeypatched to a known state."""
|
||||
|
|
@ -71,8 +107,9 @@ class TestRunIntegration(unittest.TestCase):
|
|||
self._saved = {}
|
||||
for name in ("proc_pids", "alive_pids", "proc_modules",
|
||||
"sys_live_modules", "procnet_listen_ports",
|
||||
"procnet_udp_ports", "promiscuous_interfaces", "module_taints",
|
||||
"kernel_taint"):
|
||||
"procnet_udp_ports", "procnet_raw_protocols",
|
||||
"procnet_protocol_kinds",
|
||||
"promiscuous_interfaces", "module_taints", "kernel_taint"):
|
||||
self._saved[name] = getattr(rootcheck, name)
|
||||
|
||||
def tearDown(self):
|
||||
|
|
@ -86,6 +123,8 @@ class TestRunIntegration(unittest.TestCase):
|
|||
rootcheck.sys_live_modules = lambda: views.get("sys_live_modules", set())
|
||||
rootcheck.procnet_listen_ports = lambda: views.get("procnet_ports", set())
|
||||
rootcheck.procnet_udp_ports = lambda: views.get("procnet_udp_ports", set())
|
||||
rootcheck.procnet_raw_protocols = lambda: views.get("procnet_raw_protocols", set())
|
||||
rootcheck.procnet_protocol_kinds = lambda: views.get("procnet_protocol_kinds", set())
|
||||
rootcheck.promiscuous_interfaces = lambda: views.get("promisc", [])
|
||||
rootcheck.module_taints = lambda: views.get("module_taints", {})
|
||||
rootcheck.kernel_taint = lambda: views.get("kernel_taint", 0)
|
||||
|
|
@ -101,10 +140,14 @@ class TestRunIntegration(unittest.TestCase):
|
|||
proc_pids={1}, alive_pids={1},
|
||||
proc_modules={"ext4"}, sys_live_modules={"ext4", "diamorphine"},
|
||||
procnet_ports={22, 31337}, procnet_udp_ports={53, 4444},
|
||||
procnet_raw_protocols={1, 58},
|
||||
procnet_protocol_kinds={"sctp", "packet"},
|
||||
promisc=["eth0"])
|
||||
state = SystemState(sockets=[
|
||||
Socket("LISTEN", "0.0.0.0:22", "0.0.0.0:0", 10, "sshd", 1),
|
||||
Socket("UNCONN", "0.0.0.0:53", "0.0.0.0:0", 11, "dnsmasq", 2),
|
||||
Socket("UNCONN", "0.0.0.0:ipv6-icmp", "0.0.0.0:*", 12, "monitor", 3, "raw"),
|
||||
Socket("UNCONN", "*:eth0", "*", 13, "tcpdump", 4, "packet"),
|
||||
])
|
||||
alerts = {a.signature: a for a in rootcheck.run(self.cfg, state)}
|
||||
self.assertIn("rootkit_hidden_module", alerts)
|
||||
|
|
@ -115,6 +158,12 @@ class TestRunIntegration(unittest.TestCase):
|
|||
self.assertIn("31337", alerts["rootkit_hidden_port"].detail)
|
||||
self.assertIn("rootkit_hidden_udp_port", alerts)
|
||||
self.assertIn("4444", alerts["rootkit_hidden_udp_port"].detail)
|
||||
self.assertIn("rootkit_hidden_raw_socket", alerts)
|
||||
self.assertIn("1", alerts["rootkit_hidden_raw_socket"].detail)
|
||||
self.assertIn("raw_icmp_socket", alerts)
|
||||
self.assertEqual(alerts["raw_icmp_socket"].sid, rootcheck.SID_RAW_ICMP_SOCKET)
|
||||
self.assertIn("rootkit_hidden_protocol_socket", alerts)
|
||||
self.assertIn("sctp", alerts["rootkit_hidden_protocol_socket"].detail)
|
||||
self.assertIn("promiscuous_interface", alerts)
|
||||
self.assertEqual(alerts["promiscuous_interface"].sid, rootcheck.SID_PROMISC)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue