merge: testing → main (reconcile 2-week divergence)
This commit is contained in:
13
decnet/prober/__init__.py
Normal file
13
decnet/prober/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
"""
|
||||
DECNET-PROBER — standalone active network probing service.
|
||||
|
||||
Runs as a detached host-level process (no container). Sends crafted TLS
|
||||
probes to discover C2 frameworks and other attacker infrastructure via
|
||||
JARM fingerprinting. Results are written as RFC 5424 syslog + JSON to the
|
||||
same log file the collector uses, so the existing ingestion pipeline picks
|
||||
them up automatically.
|
||||
"""
|
||||
|
||||
from decnet.prober.worker import prober_worker
|
||||
|
||||
__all__ = ["prober_worker"]
|
||||
252
decnet/prober/hassh.py
Normal file
252
decnet/prober/hassh.py
Normal file
@@ -0,0 +1,252 @@
|
||||
"""
|
||||
HASSHServer — SSH server fingerprinting via KEX_INIT algorithm ordering.
|
||||
|
||||
Connects to an SSH server, completes the version exchange, captures the
|
||||
server's SSH_MSG_KEXINIT message, and hashes the server-to-client algorithm
|
||||
fields (kex, encryption, MAC, compression) into a 32-character MD5 digest.
|
||||
|
||||
This is the *server* variant of HASSH (HASSHServer). It fingerprints what
|
||||
the server *offers*, which identifies the SSH implementation (OpenSSH,
|
||||
Paramiko, libssh, Cobalt Strike SSH, etc.).
|
||||
|
||||
Stdlib only (socket, struct, hashlib) plus decnet.telemetry for tracing (zero-cost when disabled).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import socket
|
||||
import struct
|
||||
from typing import Any
|
||||
|
||||
from decnet.telemetry import traced as _traced
|
||||
|
||||
# SSH protocol constants
|
||||
_SSH_MSG_KEXINIT = 20
|
||||
_KEX_INIT_COOKIE_LEN = 16
|
||||
_KEX_INIT_NAME_LISTS = 10 # 10 name-list fields in KEX_INIT
|
||||
|
||||
# Blend in as a normal OpenSSH client
|
||||
_CLIENT_BANNER = b"SSH-2.0-OpenSSH_9.6\r\n"
|
||||
|
||||
# Max bytes to read for server banner
|
||||
_MAX_BANNER_LEN = 256
|
||||
|
||||
# Max bytes for a single SSH packet (KEX_INIT is typically < 2KB)
|
||||
_MAX_PACKET_LEN = 35000
|
||||
|
||||
|
||||
# ─── SSH connection + KEX_INIT capture ──────────────────────────────────────
|
||||
|
||||
@_traced("prober.hassh_ssh_connect")
|
||||
def _ssh_connect(
|
||||
host: str,
|
||||
port: int,
|
||||
timeout: float,
|
||||
) -> tuple[str, bytes] | None:
|
||||
"""
|
||||
TCP connect, exchange version strings, read server's KEX_INIT.
|
||||
|
||||
Returns (server_banner, kex_init_payload) or None on failure.
|
||||
The kex_init_payload starts at the SSH_MSG_KEXINIT type byte.
|
||||
"""
|
||||
sock = None
|
||||
try:
|
||||
sock = socket.create_connection((host, port), timeout=timeout)
|
||||
sock.settimeout(timeout)
|
||||
|
||||
# 1. Read server banner (line ending \r\n or \n)
|
||||
banner = _read_banner(sock)
|
||||
if banner is None or not banner.startswith("SSH-"):
|
||||
return None
|
||||
|
||||
# 2. Send our client version string
|
||||
sock.sendall(_CLIENT_BANNER)
|
||||
|
||||
# 3. Read the server's first binary packet (should be KEX_INIT)
|
||||
payload = _read_ssh_packet(sock)
|
||||
if payload is None or len(payload) < 1:
|
||||
return None
|
||||
|
||||
if payload[0] != _SSH_MSG_KEXINIT:
|
||||
return None
|
||||
|
||||
return (banner, payload)
|
||||
|
||||
except (OSError, socket.timeout, TimeoutError, ConnectionError):
|
||||
return None
|
||||
finally:
|
||||
if sock is not None:
|
||||
try:
|
||||
sock.close()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def _read_banner(sock: socket.socket) -> str | None:
|
||||
"""Read the SSH version banner line from the socket."""
|
||||
buf = b""
|
||||
while len(buf) < _MAX_BANNER_LEN:
|
||||
try:
|
||||
byte = sock.recv(1)
|
||||
except (OSError, socket.timeout, TimeoutError):
|
||||
return None
|
||||
if not byte:
|
||||
return None
|
||||
buf += byte
|
||||
if buf.endswith(b"\n"):
|
||||
break
|
||||
|
||||
try:
|
||||
return buf.decode("utf-8", errors="replace").rstrip("\r\n")
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _read_ssh_packet(sock: socket.socket) -> bytes | None:
|
||||
"""
|
||||
Read a single SSH binary packet and return its payload.
|
||||
|
||||
SSH binary packet format:
|
||||
uint32 packet_length (not including itself or MAC)
|
||||
byte padding_length
|
||||
byte[] payload (packet_length - padding_length - 1)
|
||||
byte[] padding
|
||||
"""
|
||||
header = _recv_exact(sock, 4)
|
||||
if header is None:
|
||||
return None
|
||||
|
||||
packet_length = struct.unpack("!I", header)[0]
|
||||
if packet_length < 2 or packet_length > _MAX_PACKET_LEN:
|
||||
return None
|
||||
|
||||
rest = _recv_exact(sock, packet_length)
|
||||
if rest is None:
|
||||
return None
|
||||
|
||||
padding_length = rest[0]
|
||||
payload_length = packet_length - padding_length - 1
|
||||
if payload_length < 1 or payload_length > len(rest) - 1:
|
||||
return None
|
||||
|
||||
return rest[1 : 1 + payload_length]
|
||||
|
||||
|
||||
def _recv_exact(sock: socket.socket, n: int) -> bytes | None:
|
||||
"""Read exactly n bytes from socket, or None on failure."""
|
||||
buf = b""
|
||||
while len(buf) < n:
|
||||
try:
|
||||
chunk = sock.recv(n - len(buf))
|
||||
except (OSError, socket.timeout, TimeoutError):
|
||||
return None
|
||||
if not chunk:
|
||||
return None
|
||||
buf += chunk
|
||||
return buf
|
||||
|
||||
|
||||
# ─── KEX_INIT parsing ──────────────────────────────────────────────────────
|
||||
|
||||
def _parse_kex_init(payload: bytes) -> dict[str, str] | None:
|
||||
"""
|
||||
Parse SSH_MSG_KEXINIT payload and extract the 10 name-list fields.
|
||||
|
||||
Payload layout:
|
||||
byte SSH_MSG_KEXINIT (20)
|
||||
byte[16] cookie
|
||||
10 × name-list:
|
||||
uint32 length
|
||||
byte[] utf-8 string (comma-separated algorithm names)
|
||||
bool first_kex_packet_follows
|
||||
uint32 reserved
|
||||
|
||||
Returns dict with keys: kex_algorithms, server_host_key_algorithms,
|
||||
encryption_client_to_server, encryption_server_to_client,
|
||||
mac_client_to_server, mac_server_to_client,
|
||||
compression_client_to_server, compression_server_to_client,
|
||||
languages_client_to_server, languages_server_to_client.
|
||||
"""
|
||||
if len(payload) < 1 + _KEX_INIT_COOKIE_LEN + 4:
|
||||
return None
|
||||
|
||||
offset = 1 + _KEX_INIT_COOKIE_LEN # skip type byte + cookie
|
||||
|
||||
field_names = [
|
||||
"kex_algorithms",
|
||||
"server_host_key_algorithms",
|
||||
"encryption_client_to_server",
|
||||
"encryption_server_to_client",
|
||||
"mac_client_to_server",
|
||||
"mac_server_to_client",
|
||||
"compression_client_to_server",
|
||||
"compression_server_to_client",
|
||||
"languages_client_to_server",
|
||||
"languages_server_to_client",
|
||||
]
|
||||
|
||||
fields: dict[str, str] = {}
|
||||
for name in field_names:
|
||||
if offset + 4 > len(payload):
|
||||
return None
|
||||
length = struct.unpack("!I", payload[offset : offset + 4])[0]
|
||||
offset += 4
|
||||
if offset + length > len(payload):
|
||||
return None
|
||||
fields[name] = payload[offset : offset + length].decode(
|
||||
"utf-8", errors="replace"
|
||||
)
|
||||
offset += length
|
||||
|
||||
return fields
|
||||
|
||||
|
||||
# ─── HASSH computation ──────────────────────────────────────────────────────
|
||||
|
||||
def _compute_hassh(kex: str, enc: str, mac: str, comp: str) -> str:
|
||||
"""
|
||||
Compute HASSHServer hash: MD5 of "kex;enc_s2c;mac_s2c;comp_s2c".
|
||||
|
||||
Returns 32-character lowercase hex digest.
|
||||
"""
|
||||
raw = f"{kex};{enc};{mac};{comp}"
|
||||
return hashlib.md5(raw.encode("utf-8"), usedforsecurity=False).hexdigest()
|
||||
|
||||
|
||||
# ─── Public API ─────────────────────────────────────────────────────────────
|
||||
|
||||
@_traced("prober.hassh_server")
|
||||
def hassh_server(
|
||||
host: str,
|
||||
port: int,
|
||||
timeout: float = 5.0,
|
||||
) -> dict[str, Any] | None:
|
||||
"""
|
||||
Connect to an SSH server and compute its HASSHServer fingerprint.
|
||||
|
||||
Returns a dict with the hash, banner, and raw algorithm fields,
|
||||
or None if the host is not running an SSH server on the given port.
|
||||
"""
|
||||
result = _ssh_connect(host, port, timeout)
|
||||
if result is None:
|
||||
return None
|
||||
|
||||
banner, payload = result
|
||||
fields = _parse_kex_init(payload)
|
||||
if fields is None:
|
||||
return None
|
||||
|
||||
kex = fields["kex_algorithms"]
|
||||
enc = fields["encryption_server_to_client"]
|
||||
mac = fields["mac_server_to_client"]
|
||||
comp = fields["compression_server_to_client"]
|
||||
|
||||
return {
|
||||
"hassh_server": _compute_hassh(kex, enc, mac, comp),
|
||||
"banner": banner,
|
||||
"kex_algorithms": kex,
|
||||
"encryption_s2c": enc,
|
||||
"mac_s2c": mac,
|
||||
"compression_s2c": comp,
|
||||
}
|
||||
506
decnet/prober/jarm.py
Normal file
506
decnet/prober/jarm.py
Normal file
@@ -0,0 +1,506 @@
|
||||
"""
|
||||
JARM TLS fingerprinting — pure stdlib implementation.
|
||||
|
||||
JARM sends 10 crafted TLS ClientHello packets to a target, each varying
|
||||
TLS version, cipher suite order, extensions, and ALPN values. The
|
||||
ServerHello responses are parsed and hashed to produce a 62-character
|
||||
fingerprint that identifies the TLS server implementation.
|
||||
|
||||
Reference: https://github.com/salesforce/jarm
|
||||
|
||||
Only DECNET import is decnet.telemetry for tracing (zero-cost when disabled).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import socket
|
||||
import struct
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from decnet.telemetry import traced as _traced
|
||||
|
||||
# ─── Constants ────────────────────────────────────────────────────────────────
|
||||
|
||||
JARM_EMPTY_HASH = "0" * 62
|
||||
|
||||
_INTER_PROBE_DELAY = 0.1 # seconds between probes to avoid IDS triggers
|
||||
|
||||
# TLS version bytes
|
||||
_TLS_1_0 = b"\x03\x01"
|
||||
_TLS_1_1 = b"\x03\x02"
|
||||
_TLS_1_2 = b"\x03\x03"
|
||||
_TLS_1_3 = b"\x03\x03" # TLS 1.3 uses 0x0303 in record layer
|
||||
|
||||
# TLS record types
|
||||
_CONTENT_HANDSHAKE = 0x16
|
||||
_HANDSHAKE_CLIENT_HELLO = 0x01
|
||||
_HANDSHAKE_SERVER_HELLO = 0x02
|
||||
|
||||
# Extension types
|
||||
_EXT_SERVER_NAME = 0x0000
|
||||
_EXT_EC_POINT_FORMATS = 0x000B
|
||||
_EXT_SUPPORTED_GROUPS = 0x000A
|
||||
_EXT_SESSION_TICKET = 0x0023
|
||||
_EXT_ENCRYPT_THEN_MAC = 0x0016
|
||||
_EXT_EXTENDED_MASTER_SECRET = 0x0017
|
||||
_EXT_SIGNATURE_ALGORITHMS = 0x000D
|
||||
_EXT_SUPPORTED_VERSIONS = 0x002B
|
||||
_EXT_PSK_KEY_EXCHANGE_MODES = 0x002D
|
||||
_EXT_KEY_SHARE = 0x0033
|
||||
_EXT_ALPN = 0x0010
|
||||
_EXT_PADDING = 0x0015
|
||||
|
||||
# ─── Cipher suite lists per JARM spec ────────────────────────────────────────
|
||||
|
||||
# Forward cipher order (standard)
|
||||
_CIPHERS_FORWARD = [
|
||||
0x0016, 0x0033, 0x0067, 0xC09E, 0xC0A2, 0x009E, 0x0039, 0x006B,
|
||||
0xC09F, 0xC0A3, 0x009F, 0x0045, 0x00BE, 0x0088, 0x00C4, 0x009A,
|
||||
0xC008, 0xC009, 0xC023, 0xC0AC, 0xC0AE, 0xC02B, 0xC00A, 0xC024,
|
||||
0xC0AD, 0xC0AF, 0xC02C, 0xC072, 0xC073, 0xCCA8, 0x1301, 0x1302,
|
||||
0x1303, 0xC013, 0xC014, 0xC02F, 0x009C, 0xC02E, 0x002F, 0x0035,
|
||||
0x000A, 0x0005, 0x0004,
|
||||
]
|
||||
|
||||
# Reverse cipher order
|
||||
_CIPHERS_REVERSE = list(reversed(_CIPHERS_FORWARD))
|
||||
|
||||
# TLS 1.3-only ciphers
|
||||
_CIPHERS_TLS13 = [0x1301, 0x1302, 0x1303]
|
||||
|
||||
# Middle-out cipher order (interleaved from center)
|
||||
def _middle_out(lst: list[int]) -> list[int]:
|
||||
result: list[int] = []
|
||||
mid = len(lst) // 2
|
||||
for i in range(mid + 1):
|
||||
if mid + i < len(lst):
|
||||
result.append(lst[mid + i])
|
||||
if mid - i >= 0 and mid - i != mid + i:
|
||||
result.append(lst[mid - i])
|
||||
return result
|
||||
|
||||
_CIPHERS_MIDDLE_OUT = _middle_out(_CIPHERS_FORWARD)
|
||||
|
||||
# Rare/uncommon extensions cipher list
|
||||
_CIPHERS_RARE = [
|
||||
0x0016, 0x0033, 0xC011, 0xC012, 0x0067, 0xC09E, 0xC0A2, 0x009E,
|
||||
0x0039, 0x006B, 0xC09F, 0xC0A3, 0x009F, 0x0045, 0x00BE, 0x0088,
|
||||
0x00C4, 0x009A, 0xC008, 0xC009, 0xC023, 0xC0AC, 0xC0AE, 0xC02B,
|
||||
0xC00A, 0xC024, 0xC0AD, 0xC0AF, 0xC02C, 0xC072, 0xC073, 0xCCA8,
|
||||
0x1301, 0x1302, 0x1303, 0xC013, 0xC014, 0xC02F, 0x009C, 0xC02E,
|
||||
0x002F, 0x0035, 0x000A, 0x0005, 0x0004,
|
||||
]
|
||||
|
||||
|
||||
# ─── Probe definitions ────────────────────────────────────────────────────────
|
||||
|
||||
# Each probe: (tls_version, cipher_list, tls13_support, alpn, extensions_style)
|
||||
# tls_version: record-layer version bytes
|
||||
# cipher_list: which cipher suite ordering to use
|
||||
# tls13_support: whether to include TLS 1.3 extensions (supported_versions, key_share, psk)
|
||||
# alpn: ALPN protocol string or None
|
||||
# extensions_style: "standard", "rare", or "no_extensions"
|
||||
|
||||
_PROBE_CONFIGS: list[dict[str, Any]] = [
|
||||
# 0: TLS 1.2 forward
|
||||
{"version": _TLS_1_2, "ciphers": _CIPHERS_FORWARD, "tls13": False, "alpn": None, "style": "standard"},
|
||||
# 1: TLS 1.2 reverse
|
||||
{"version": _TLS_1_2, "ciphers": _CIPHERS_REVERSE, "tls13": False, "alpn": None, "style": "standard"},
|
||||
# 2: TLS 1.1 forward
|
||||
{"version": _TLS_1_1, "ciphers": _CIPHERS_FORWARD, "tls13": False, "alpn": None, "style": "standard"},
|
||||
# 3: TLS 1.3 forward
|
||||
{"version": _TLS_1_2, "ciphers": _CIPHERS_FORWARD, "tls13": True, "alpn": "h2", "style": "standard"},
|
||||
# 4: TLS 1.3 reverse
|
||||
{"version": _TLS_1_2, "ciphers": _CIPHERS_REVERSE, "tls13": True, "alpn": "h2", "style": "standard"},
|
||||
# 5: TLS 1.3 invalid (advertise 1.3 support but no key_share)
|
||||
{"version": _TLS_1_2, "ciphers": _CIPHERS_FORWARD, "tls13": "no_key_share", "alpn": None, "style": "standard"},
|
||||
# 6: TLS 1.3 middle-out
|
||||
{"version": _TLS_1_2, "ciphers": _CIPHERS_MIDDLE_OUT, "tls13": True, "alpn": None, "style": "standard"},
|
||||
# 7: TLS 1.0 forward
|
||||
{"version": _TLS_1_0, "ciphers": _CIPHERS_FORWARD, "tls13": False, "alpn": None, "style": "standard"},
|
||||
# 8: TLS 1.2 middle-out
|
||||
{"version": _TLS_1_2, "ciphers": _CIPHERS_MIDDLE_OUT, "tls13": False, "alpn": None, "style": "standard"},
|
||||
# 9: TLS 1.2 with rare extensions
|
||||
{"version": _TLS_1_2, "ciphers": _CIPHERS_RARE, "tls13": False, "alpn": "http/1.1", "style": "rare"},
|
||||
]
|
||||
|
||||
|
||||
# ─── Extension builders ──────────────────────────────────────────────────────
|
||||
|
||||
def _ext(ext_type: int, data: bytes) -> bytes:
|
||||
return struct.pack("!HH", ext_type, len(data)) + data
|
||||
|
||||
|
||||
def _ext_sni(host: str) -> bytes:
|
||||
host_bytes = host.encode("ascii")
|
||||
# ServerNameList: length(2) + ServerName: type(1) + length(2) + name
|
||||
sni_data = struct.pack("!HBH", len(host_bytes) + 3, 0, len(host_bytes)) + host_bytes
|
||||
return _ext(_EXT_SERVER_NAME, sni_data)
|
||||
|
||||
|
||||
def _ext_supported_groups() -> bytes:
|
||||
groups = [0x0017, 0x0018, 0x0019, 0x001D, 0x0100, 0x0101] # secp256r1, secp384r1, secp521r1, x25519, ffdhe2048, ffdhe3072
|
||||
data = struct.pack("!H", len(groups) * 2) + b"".join(struct.pack("!H", g) for g in groups)
|
||||
return _ext(_EXT_SUPPORTED_GROUPS, data)
|
||||
|
||||
|
||||
def _ext_ec_point_formats() -> bytes:
|
||||
formats = b"\x00" # uncompressed only
|
||||
return _ext(_EXT_EC_POINT_FORMATS, struct.pack("B", len(formats)) + formats)
|
||||
|
||||
|
||||
def _ext_signature_algorithms() -> bytes:
|
||||
algos = [
|
||||
0x0401, 0x0501, 0x0601, # RSA PKCS1 SHA256/384/512
|
||||
0x0201, # RSA PKCS1 SHA1
|
||||
0x0403, 0x0503, 0x0603, # ECDSA SHA256/384/512
|
||||
0x0203, # ECDSA SHA1
|
||||
0x0804, 0x0805, 0x0806, # RSA-PSS SHA256/384/512
|
||||
]
|
||||
data = struct.pack("!H", len(algos) * 2) + b"".join(struct.pack("!H", a) for a in algos)
|
||||
return _ext(_EXT_SIGNATURE_ALGORITHMS, data)
|
||||
|
||||
|
||||
def _ext_supported_versions_13() -> bytes:
|
||||
versions = [0x0304, 0x0303] # TLS 1.3, 1.2
|
||||
data = struct.pack("B", len(versions) * 2) + b"".join(struct.pack("!H", v) for v in versions)
|
||||
return _ext(_EXT_SUPPORTED_VERSIONS, data)
|
||||
|
||||
|
||||
def _ext_psk_key_exchange_modes() -> bytes:
|
||||
return _ext(_EXT_PSK_KEY_EXCHANGE_MODES, b"\x01\x01") # psk_dhe_ke
|
||||
|
||||
|
||||
def _ext_key_share() -> bytes:
|
||||
# x25519 key share with 32 random-looking bytes
|
||||
key_data = b"\x00" * 32
|
||||
entry = struct.pack("!HH", 0x001D, 32) + key_data # x25519 group
|
||||
data = struct.pack("!H", len(entry)) + entry
|
||||
return _ext(_EXT_KEY_SHARE, data)
|
||||
|
||||
|
||||
def _ext_alpn(protocol: str) -> bytes:
|
||||
proto_bytes = protocol.encode("ascii")
|
||||
proto_entry = struct.pack("B", len(proto_bytes)) + proto_bytes
|
||||
data = struct.pack("!H", len(proto_entry)) + proto_entry
|
||||
return _ext(_EXT_ALPN, data)
|
||||
|
||||
|
||||
def _ext_session_ticket() -> bytes:
|
||||
return _ext(_EXT_SESSION_TICKET, b"")
|
||||
|
||||
|
||||
def _ext_encrypt_then_mac() -> bytes:
|
||||
return _ext(_EXT_ENCRYPT_THEN_MAC, b"")
|
||||
|
||||
|
||||
def _ext_extended_master_secret() -> bytes:
|
||||
return _ext(_EXT_EXTENDED_MASTER_SECRET, b"")
|
||||
|
||||
|
||||
def _ext_padding(target_length: int, current_length: int) -> bytes:
|
||||
pad_needed = target_length - current_length - 4 # 4 bytes for ext type + length
|
||||
if pad_needed < 0:
|
||||
return b""
|
||||
return _ext(_EXT_PADDING, b"\x00" * pad_needed)
|
||||
|
||||
|
||||
# ─── ClientHello builder ─────────────────────────────────────────────────────
|
||||
|
||||
def _build_client_hello(probe_index: int, host: str = "localhost") -> bytes:
|
||||
"""
|
||||
Construct one of 10 JARM-specified ClientHello packets.
|
||||
|
||||
Args:
|
||||
probe_index: 0-9, selects the probe configuration
|
||||
host: target hostname for SNI extension
|
||||
|
||||
Returns:
|
||||
Complete TLS record bytes ready to send on the wire.
|
||||
"""
|
||||
cfg = _PROBE_CONFIGS[probe_index]
|
||||
version: bytes = cfg["version"]
|
||||
ciphers: list[int] = cfg["ciphers"]
|
||||
tls13 = cfg["tls13"]
|
||||
alpn: str | None = cfg["alpn"]
|
||||
|
||||
# Random (32 bytes)
|
||||
random_bytes = b"\x00" * 32
|
||||
|
||||
# Session ID (32 bytes, all zeros)
|
||||
session_id = b"\x00" * 32
|
||||
|
||||
# Cipher suites
|
||||
cipher_bytes = b"".join(struct.pack("!H", c) for c in ciphers)
|
||||
cipher_data = struct.pack("!H", len(cipher_bytes)) + cipher_bytes
|
||||
|
||||
# Compression methods (null only)
|
||||
compression = b"\x01\x00"
|
||||
|
||||
# Extensions
|
||||
extensions = b""
|
||||
extensions += _ext_sni(host)
|
||||
extensions += _ext_supported_groups()
|
||||
extensions += _ext_ec_point_formats()
|
||||
extensions += _ext_session_ticket()
|
||||
extensions += _ext_encrypt_then_mac()
|
||||
extensions += _ext_extended_master_secret()
|
||||
extensions += _ext_signature_algorithms()
|
||||
|
||||
if tls13 == True: # noqa: E712
|
||||
extensions += _ext_supported_versions_13()
|
||||
extensions += _ext_psk_key_exchange_modes()
|
||||
extensions += _ext_key_share()
|
||||
elif tls13 == "no_key_share":
|
||||
extensions += _ext_supported_versions_13()
|
||||
extensions += _ext_psk_key_exchange_modes()
|
||||
# Intentionally omit key_share
|
||||
|
||||
if alpn:
|
||||
extensions += _ext_alpn(alpn)
|
||||
|
||||
ext_data = struct.pack("!H", len(extensions)) + extensions
|
||||
|
||||
# ClientHello body
|
||||
body = (
|
||||
version # client_version (2)
|
||||
+ random_bytes # random (32)
|
||||
+ struct.pack("B", len(session_id)) + session_id # session_id
|
||||
+ cipher_data # cipher_suites
|
||||
+ compression # compression_methods
|
||||
+ ext_data # extensions
|
||||
)
|
||||
|
||||
# Handshake header: type(1) + length(3)
|
||||
handshake = struct.pack("B", _HANDSHAKE_CLIENT_HELLO) + struct.pack("!I", len(body))[1:] + body
|
||||
|
||||
# TLS record header: type(1) + version(2) + length(2)
|
||||
record = struct.pack("B", _CONTENT_HANDSHAKE) + _TLS_1_0 + struct.pack("!H", len(handshake)) + handshake
|
||||
|
||||
return record
|
||||
|
||||
|
||||
# ─── ServerHello parser ──────────────────────────────────────────────────────
|
||||
|
||||
def _parse_server_hello(data: bytes) -> str:
|
||||
"""
|
||||
Extract cipher suite and TLS version from a ServerHello response.
|
||||
|
||||
Returns a pipe-delimited string "cipher|version|extensions" that forms
|
||||
one component of the JARM hash, or "|||" on parse failure.
|
||||
"""
|
||||
try:
|
||||
if len(data) < 6:
|
||||
return "|||"
|
||||
|
||||
# TLS record header
|
||||
if data[0] != _CONTENT_HANDSHAKE:
|
||||
return "|||"
|
||||
|
||||
struct.unpack_from("!H", data, 1)[0] # record_version (unused)
|
||||
record_len = struct.unpack_from("!H", data, 3)[0]
|
||||
hs = data[5: 5 + record_len]
|
||||
|
||||
if len(hs) < 4:
|
||||
return "|||"
|
||||
|
||||
# Handshake header
|
||||
if hs[0] != _HANDSHAKE_SERVER_HELLO:
|
||||
return "|||"
|
||||
|
||||
hs_len = struct.unpack_from("!I", b"\x00" + hs[1:4])[0]
|
||||
body = hs[4: 4 + hs_len]
|
||||
|
||||
if len(body) < 34:
|
||||
return "|||"
|
||||
|
||||
pos = 0
|
||||
# Server version
|
||||
server_version = struct.unpack_from("!H", body, pos)[0]
|
||||
pos += 2
|
||||
|
||||
# Random (32 bytes)
|
||||
pos += 32
|
||||
|
||||
# Session ID
|
||||
if pos >= len(body):
|
||||
return "|||"
|
||||
sid_len = body[pos]
|
||||
pos += 1 + sid_len
|
||||
|
||||
# Cipher suite
|
||||
if pos + 2 > len(body):
|
||||
return "|||"
|
||||
cipher = struct.unpack_from("!H", body, pos)[0]
|
||||
pos += 2
|
||||
|
||||
# Compression method
|
||||
if pos >= len(body):
|
||||
return "|||"
|
||||
pos += 1
|
||||
|
||||
# Parse extensions for supported_versions (to detect actual TLS 1.3)
|
||||
actual_version = server_version
|
||||
extensions_str = ""
|
||||
if pos + 2 <= len(body):
|
||||
ext_total = struct.unpack_from("!H", body, pos)[0]
|
||||
pos += 2
|
||||
ext_end = pos + ext_total
|
||||
ext_types: list[str] = []
|
||||
while pos + 4 <= ext_end and pos + 4 <= len(body):
|
||||
ext_type = struct.unpack_from("!H", body, pos)[0]
|
||||
ext_len = struct.unpack_from("!H", body, pos + 2)[0]
|
||||
ext_types.append(f"{ext_type:04x}")
|
||||
|
||||
if ext_type == _EXT_SUPPORTED_VERSIONS and ext_len >= 2:
|
||||
actual_version = struct.unpack_from("!H", body, pos + 4)[0]
|
||||
|
||||
pos += 4 + ext_len
|
||||
extensions_str = "-".join(ext_types)
|
||||
|
||||
version_str = _version_to_str(actual_version)
|
||||
cipher_str = f"{cipher:04x}"
|
||||
|
||||
return f"{cipher_str}|{version_str}|{extensions_str}"
|
||||
|
||||
except Exception:
|
||||
return "|||"
|
||||
|
||||
|
||||
def _version_to_str(version: int) -> str:
|
||||
return {
|
||||
0x0304: "tls13",
|
||||
0x0303: "tls12",
|
||||
0x0302: "tls11",
|
||||
0x0301: "tls10",
|
||||
0x0300: "ssl30",
|
||||
}.get(version, f"{version:04x}")
|
||||
|
||||
|
||||
# ─── Probe sender ────────────────────────────────────────────────────────────
|
||||
|
||||
@_traced("prober.jarm_send_probe")
|
||||
def _send_probe(host: str, port: int, hello: bytes, timeout: float = 5.0) -> bytes | None:
|
||||
"""
|
||||
Open a TCP connection, send the ClientHello, and read the ServerHello.
|
||||
|
||||
Returns raw response bytes or None on any failure.
|
||||
"""
|
||||
try:
|
||||
sock = socket.create_connection((host, port), timeout=timeout)
|
||||
try:
|
||||
sock.sendall(hello)
|
||||
sock.settimeout(timeout)
|
||||
response = b""
|
||||
while True:
|
||||
chunk = sock.recv(1484)
|
||||
if not chunk:
|
||||
break
|
||||
response += chunk
|
||||
# We only need the first TLS record (ServerHello)
|
||||
if len(response) >= 5:
|
||||
record_len = struct.unpack_from("!H", response, 3)[0]
|
||||
if len(response) >= 5 + record_len:
|
||||
break
|
||||
return response if response else None
|
||||
finally:
|
||||
sock.close()
|
||||
except (OSError, socket.error, socket.timeout):
|
||||
return None
|
||||
|
||||
|
||||
# ─── JARM hash computation ───────────────────────────────────────────────────
|
||||
|
||||
def _compute_jarm(responses: list[str]) -> str:
|
||||
"""
|
||||
Compute the final 62-character JARM hash from 10 probe response strings.
|
||||
|
||||
The first 30 characters are the raw cipher/version concatenation.
|
||||
The remaining 32 characters are a truncated SHA256 of the extensions.
|
||||
"""
|
||||
if all(r == "|||" for r in responses):
|
||||
return JARM_EMPTY_HASH
|
||||
|
||||
# Build the fuzzy hash
|
||||
raw_parts: list[str] = []
|
||||
ext_parts: list[str] = []
|
||||
|
||||
for r in responses:
|
||||
parts = r.split("|")
|
||||
if len(parts) >= 3 and parts[0] != "":
|
||||
cipher = parts[0]
|
||||
version = parts[1]
|
||||
extensions = parts[2] if len(parts) > 2 else ""
|
||||
|
||||
# Map version to single char
|
||||
ver_char = {
|
||||
"tls13": "d", "tls12": "c", "tls11": "b",
|
||||
"tls10": "a", "ssl30": "0",
|
||||
}.get(version, "0")
|
||||
|
||||
raw_parts.append(f"{cipher}{ver_char}")
|
||||
ext_parts.append(extensions)
|
||||
else:
|
||||
raw_parts.append("000")
|
||||
ext_parts.append("")
|
||||
|
||||
# First 30 chars: cipher(4) + version(1) = 5 chars * 10 probes = 50... no
|
||||
# JARM spec: first part is c|v per probe joined, then SHA256 of extensions
|
||||
# Actual format: each response contributes 3 chars (cipher_first2 + ver_char)
|
||||
# to the first 30, then all extensions hashed for the remaining 32.
|
||||
|
||||
fuzzy_raw = ""
|
||||
for r in responses:
|
||||
parts = r.split("|")
|
||||
if len(parts) >= 3 and parts[0] != "":
|
||||
cipher = parts[0] # 4-char hex
|
||||
version = parts[1]
|
||||
ver_char = {
|
||||
"tls13": "d", "tls12": "c", "tls11": "b",
|
||||
"tls10": "a", "ssl30": "0",
|
||||
}.get(version, "0")
|
||||
fuzzy_raw += f"{cipher[0:2]}{ver_char}"
|
||||
else:
|
||||
fuzzy_raw += "000"
|
||||
|
||||
# fuzzy_raw is 30 chars (3 * 10)
|
||||
ext_str = ",".join(ext_parts)
|
||||
ext_hash = hashlib.sha256(ext_str.encode()).hexdigest()[:32]
|
||||
|
||||
return fuzzy_raw + ext_hash
|
||||
|
||||
|
||||
# ─── Public API ──────────────────────────────────────────────────────────────
|
||||
|
||||
@_traced("prober.jarm_hash")
|
||||
def jarm_hash(host: str, port: int, timeout: float = 5.0) -> str:
|
||||
"""
|
||||
Compute the JARM fingerprint for a TLS server.
|
||||
|
||||
Sends 10 crafted ClientHello packets and hashes the responses.
|
||||
|
||||
Args:
|
||||
host: target IP or hostname
|
||||
port: target port
|
||||
timeout: per-probe TCP timeout in seconds
|
||||
|
||||
Returns:
|
||||
62-character JARM hash string, or all-zeros on total failure.
|
||||
"""
|
||||
responses: list[str] = []
|
||||
|
||||
for i in range(10):
|
||||
hello = _build_client_hello(i, host=host)
|
||||
raw = _send_probe(host, port, hello, timeout=timeout)
|
||||
if raw is not None:
|
||||
parsed = _parse_server_hello(raw)
|
||||
responses.append(parsed)
|
||||
else:
|
||||
responses.append("|||")
|
||||
|
||||
if i < 9:
|
||||
time.sleep(_INTER_PROBE_DELAY)
|
||||
|
||||
return _compute_jarm(responses)
|
||||
27
decnet/prober/osfp/__init__.py
Normal file
27
decnet/prober/osfp/__init__.py
Normal file
@@ -0,0 +1,27 @@
|
||||
"""Passive + active OS fingerprinting providers.
|
||||
|
||||
Consumed by the profiler's `sniffer_rollup` (and, longer-term, by a
|
||||
dedicated prober pass). Each provider implements `base.Provider`: given
|
||||
a dict of observed TCP/IP quirks (window, wscale, mss, options
|
||||
signature, TTL, etc.), return a best-match OS label with confidence.
|
||||
|
||||
Layout mirrors `decnet/geoip/` and `decnet/bus/`: `base.py` defines the
|
||||
protocol, `factory.py` is the only sanctioned accessor, and each
|
||||
concrete source (p0f today, nmap-osdb / DECNET-observed later) lives in
|
||||
its own subpackage. Don't import concrete provider classes directly —
|
||||
use :func:`factory.get_provider` or :func:`factory.get_all_providers`.
|
||||
"""
|
||||
from decnet.prober.osfp.base import OsMatch, Provider
|
||||
from decnet.prober.osfp.factory import (
|
||||
get_all_providers,
|
||||
get_provider,
|
||||
reset_cache,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"OsMatch",
|
||||
"Provider",
|
||||
"get_all_providers",
|
||||
"get_provider",
|
||||
"reset_cache",
|
||||
]
|
||||
59
decnet/prober/osfp/base.py
Normal file
59
decnet/prober/osfp/base.py
Normal file
@@ -0,0 +1,59 @@
|
||||
"""OS-fingerprint provider protocol + OsMatch result shape.
|
||||
|
||||
Each concrete provider (p0f v2 today; nmap-osdb / DECNET-observed DB
|
||||
later) implements `Provider`. Callers go through
|
||||
:func:`decnet.prober.osfp.factory.get_provider` or
|
||||
:func:`decnet.prober.osfp.factory.get_all_providers` — direct imports
|
||||
of a concrete class are forbidden, mirroring the convention in
|
||||
``decnet/geoip`` and ``decnet/bus``.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Optional
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class OsMatch:
|
||||
"""The result of matching an observation against a provider's DB.
|
||||
|
||||
Consumers should prefer higher ``confidence``. Providers compute
|
||||
confidence as the fraction of signature fields that matched exactly
|
||||
(vs. wildcard / modulo / "any" predicates) — a signature with every
|
||||
field constrained scoring 1.0, one with every field wildcarded
|
||||
approaching 0.0. This is explicit so the profiler can pick the
|
||||
most-specific match when multiple providers fire.
|
||||
"""
|
||||
|
||||
os: str
|
||||
flavor: str
|
||||
confidence: float
|
||||
provider: str
|
||||
is_userland: bool = False
|
||||
|
||||
def __str__(self) -> str:
|
||||
tag = "userland" if self.is_userland else self.os
|
||||
return f"{tag} {self.flavor} ({self.confidence:.2f} via {self.provider})"
|
||||
|
||||
|
||||
class Provider(ABC):
|
||||
"""Abstract OS-fingerprint source.
|
||||
|
||||
Providers consume a dict of observed TCP/IP quirks (``window``,
|
||||
``wscale``, ``mss``, ``options_sig``, ``ttl``, ``df``,
|
||||
``total_len``, ``quirks`` — not all fields required) and return a
|
||||
best-match :class:`OsMatch` or ``None`` when nothing matches.
|
||||
|
||||
Providers MUST NOT raise on malformed or partial input — the
|
||||
upstream caller (`profiler/fingerprint.py::sniffer_rollup`) runs
|
||||
on data that may be missing any or all fields depending on the
|
||||
event mix, and a raising provider would wedge every attacker
|
||||
profile rebuild. Return ``None`` instead.
|
||||
"""
|
||||
|
||||
name: str
|
||||
|
||||
@abstractmethod
|
||||
def match(self, obs: dict[str, Any]) -> Optional[OsMatch]:
|
||||
"""Return best-match OsMatch for *obs*, or None."""
|
||||
87
decnet/prober/osfp/factory.py
Normal file
87
decnet/prober/osfp/factory.py
Normal file
@@ -0,0 +1,87 @@
|
||||
"""OS-fingerprint provider factory.
|
||||
|
||||
Dispatch is env-driven (``DECNET_OSFP_PROVIDERS``, comma-separated),
|
||||
with ``p0f-v2`` as the current default. Structure mirrors
|
||||
:mod:`decnet.geoip.factory` exactly: lazy singletons, a ``reset_cache``
|
||||
for tests, no dialect-specific globals past this module.
|
||||
|
||||
Callers have two entry points:
|
||||
|
||||
- :func:`get_provider` — fetch one provider by name (or the default).
|
||||
Used by anything that wants a single authoritative answer.
|
||||
- :func:`get_all_providers` — fetch the full priority chain as a list.
|
||||
Used by the profiler's :func:`~decnet.profiler.fingerprint.sniffer_rollup`
|
||||
to try each provider in turn and take the highest-confidence match
|
||||
across all of them.
|
||||
|
||||
Reserved names ``dbip`` / ``maxmind`` don't apply here — we use
|
||||
``nmap-osdb`` (pending Fyodor's grant) and ``decnet-observed`` (our
|
||||
own DB of honeypot-captured signatures) as the reserved slots that
|
||||
raise :class:`NotImplementedError` until their subpackages ship.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
from decnet.prober.osfp.base import Provider
|
||||
|
||||
|
||||
_DEFAULT_PROVIDERS = "p0f-v2"
|
||||
|
||||
# Lazy singletons, one per name, keyed by the env-selected order so
|
||||
# resetting the env (via reset_cache in tests) rebuilds cleanly.
|
||||
_cached: dict[str, Provider] = {}
|
||||
|
||||
|
||||
def _configured_names() -> list[str]:
|
||||
raw = os.environ.get("DECNET_OSFP_PROVIDERS", _DEFAULT_PROVIDERS)
|
||||
return [n.strip() for n in raw.split(",") if n.strip()]
|
||||
|
||||
|
||||
def _build(name: str) -> Provider:
|
||||
if name == "p0f-v2":
|
||||
from decnet.prober.osfp.p0f.provider import P0fV2Provider
|
||||
return P0fV2Provider()
|
||||
if name in ("nmap-osdb", "decnet-observed"):
|
||||
raise NotImplementedError(
|
||||
f"OS-fingerprint provider {name!r} is reserved but not yet wired."
|
||||
)
|
||||
raise ValueError(f"Unsupported OS-fingerprint provider: {name!r}")
|
||||
|
||||
|
||||
def get_provider(name: Optional[str] = None) -> Provider:
|
||||
"""Return a single provider — *name* if given, otherwise the first
|
||||
entry of ``DECNET_OSFP_PROVIDERS`` (default ``p0f-v2``).
|
||||
|
||||
Lazily built, memoised. Callers MUST go through this or
|
||||
:func:`get_all_providers` — direct imports of the concrete
|
||||
provider class are forbidden per the provider-subpackage convention.
|
||||
"""
|
||||
if name is None:
|
||||
names = _configured_names()
|
||||
name = names[0] if names else _DEFAULT_PROVIDERS
|
||||
cached = _cached.get(name)
|
||||
if cached is not None:
|
||||
return cached
|
||||
provider = _build(name)
|
||||
_cached[name] = provider
|
||||
return provider
|
||||
|
||||
|
||||
def get_all_providers() -> list[Provider]:
|
||||
"""Return every configured provider, in priority order.
|
||||
|
||||
Declared order in ``DECNET_OSFP_PROVIDERS`` IS priority order. The
|
||||
consumer (``sniffer_rollup``) iterates and picks the best-scoring
|
||||
match across all of them; a later provider CAN beat an earlier one
|
||||
if its signature is more specific, so the "priority" is a tiebreaker,
|
||||
not a short-circuit.
|
||||
"""
|
||||
return [get_provider(n) for n in _configured_names()]
|
||||
|
||||
|
||||
def reset_cache() -> None:
|
||||
"""Forget memoised providers — tests use this when monkeypatching
|
||||
``DECNET_OSFP_PROVIDERS`` or ``decnet/prober/osfp/p0f/data/``."""
|
||||
_cached.clear()
|
||||
6
decnet/prober/osfp/p0f/__init__.py
Normal file
6
decnet/prober/osfp/p0f/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
"""p0f v2 fingerprint database provider.
|
||||
|
||||
Upstream: https://lcamtuf.coredump.cx/p0f.shtml (p0f v2.0.8, 2006)
|
||||
License: LGPL-2.1 (preserved in data/LICENSE.p0f-upstream)
|
||||
Used in DECNET under GPL-3.0 via LGPL-2.1 §3 — see data/README.md.
|
||||
"""
|
||||
498
decnet/prober/osfp/p0f/data/LICENSE.p0f-upstream
Normal file
498
decnet/prober/osfp/p0f/data/LICENSE.p0f-upstream
Normal file
@@ -0,0 +1,498 @@
|
||||
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 2.1, February 1999
|
||||
|
||||
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
|
||||
59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
Licenses are intended to guarantee your freedom to share and change
|
||||
free software--to make sure the software is free for all its users.
|
||||
|
||||
This license, the Lesser General Public License, applies to some
|
||||
specially designated software packages--typically libraries--of the
|
||||
Free Software Foundation and other authors who decide to use it. You
|
||||
can use it too, but we suggest you first think carefully about whether
|
||||
this license or the ordinary General Public License is the better
|
||||
strategy to use in any particular case, based on the explanations below.
|
||||
|
||||
When we speak of free software, we are referring to freedom of use,
|
||||
not price. Our General Public Licenses are designed to make sure that
|
||||
you have the freedom to distribute copies of free software (and charge
|
||||
for this service if you wish); that you receive source code or can get
|
||||
it if you want it; that you can change the software and use pieces of
|
||||
it in new free programs; and that you are informed that you can do
|
||||
these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
distributors to deny you these rights or to ask you to surrender these
|
||||
rights. These restrictions translate to certain responsibilities for
|
||||
you if you distribute copies of the library or if you modify it.
|
||||
|
||||
For example, if you distribute copies of the library, whether gratis
|
||||
or for a fee, you must give the recipients all the rights that we gave
|
||||
you. You must make sure that they, too, receive or can get the source
|
||||
code. If you link other code with the library, you must provide
|
||||
complete object files to the recipients, so that they can relink them
|
||||
with the library after making changes to the library and recompiling
|
||||
it. And you must show them these terms so they know their rights.
|
||||
|
||||
We protect your rights with a two-step method: (1) we copyright the
|
||||
library, and (2) we offer you this license, which gives you legal
|
||||
permission to copy, distribute and/or modify the library.
|
||||
|
||||
To protect each distributor, we want to make it very clear that
|
||||
there is no warranty for the free library. Also, if the library is
|
||||
modified by someone else and passed on, the recipients should know
|
||||
that what they have is not the original version, so that the original
|
||||
author's reputation will not be affected by problems that might be
|
||||
introduced by others.
|
||||
|
||||
Finally, software patents pose a constant threat to the existence of
|
||||
any free program. We wish to make sure that a company cannot
|
||||
effectively restrict the users of a free program by obtaining a
|
||||
restrictive license from a patent holder. Therefore, we insist that
|
||||
any patent license obtained for a version of the library must be
|
||||
consistent with the full freedom of use specified in this license.
|
||||
|
||||
Most GNU software, including some libraries, is covered by the
|
||||
ordinary GNU General Public License. This license, the GNU Lesser
|
||||
General Public License, applies to certain designated libraries, and
|
||||
is quite different from the ordinary General Public License. We use
|
||||
this license for certain libraries in order to permit linking those
|
||||
libraries into non-free programs.
|
||||
|
||||
When a program is linked with a library, whether statically or using
|
||||
a shared library, the combination of the two is legally speaking a
|
||||
combined work, a derivative of the original library. The ordinary
|
||||
General Public License therefore permits such linking only if the
|
||||
entire combination fits its criteria of freedom. The Lesser General
|
||||
Public License permits more lax criteria for linking other code with
|
||||
the library.
|
||||
|
||||
We call this license the "Lesser" General Public License because it
|
||||
does Less to protect the user's freedom than the ordinary General
|
||||
Public License. It also provides other free software developers Less
|
||||
of an advantage over competing non-free programs. These disadvantages
|
||||
are the reason we use the ordinary General Public License for many
|
||||
libraries. However, the Lesser license provides advantages in certain
|
||||
special circumstances.
|
||||
|
||||
For example, on rare occasions, there may be a special need to
|
||||
encourage the widest possible use of a certain library, so that it becomes
|
||||
a de-facto standard. To achieve this, non-free programs must be
|
||||
allowed to use the library. A more frequent case is that a free
|
||||
library does the same job as widely used non-free libraries. In this
|
||||
case, there is little to gain by limiting the free library to free
|
||||
software only, so we use the Lesser General Public License.
|
||||
|
||||
In other cases, permission to use a particular library in non-free
|
||||
programs enables a greater number of people to use a large body of
|
||||
free software. For example, permission to use the GNU C Library in
|
||||
non-free programs enables many more people to use the whole GNU
|
||||
operating system, as well as its variant, the GNU/Linux operating
|
||||
system.
|
||||
|
||||
Although the Lesser General Public License is Less protective of the
|
||||
users' freedom, it does ensure that the user of a program that is
|
||||
linked with the Library has the freedom and the wherewithal to run
|
||||
that program using a modified version of the Library.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow. Pay close attention to the difference between a
|
||||
"work based on the library" and a "work that uses the library". The
|
||||
former contains code derived from the library, whereas the latter must
|
||||
be combined with the library in order to run.
|
||||
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License Agreement applies to any software library or other
|
||||
program which contains a notice placed by the copyright holder or
|
||||
other authorized party saying it may be distributed under the terms of
|
||||
this Lesser General Public License (also called "this License").
|
||||
Each licensee is addressed as "you".
|
||||
|
||||
A "library" means a collection of software functions and/or data
|
||||
prepared so as to be conveniently linked with application programs
|
||||
(which use some of those functions and data) to form executables.
|
||||
|
||||
The "Library", below, refers to any such software library or work
|
||||
which has been distributed under these terms. A "work based on the
|
||||
Library" means either the Library or any derivative work under
|
||||
copyright law: that is to say, a work containing the Library or a
|
||||
portion of it, either verbatim or with modifications and/or translated
|
||||
straightforwardly into another language. (Hereinafter, translation is
|
||||
included without limitation in the term "modification".)
|
||||
|
||||
"Source code" for a work means the preferred form of the work for
|
||||
making modifications to it. For a library, complete source code means
|
||||
all the source code for all modules it contains, plus any associated
|
||||
interface definition files, plus the scripts used to control compilation
|
||||
and installation of the library.
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running a program using the Library is not restricted, and output from
|
||||
such a program is covered only if its contents constitute a work based
|
||||
on the Library (independent of the use of the Library in a tool for
|
||||
writing it). Whether that is true depends on what the Library does
|
||||
and what the program that uses the Library does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Library's
|
||||
complete source code as you receive it, in any medium, provided that
|
||||
you conspicuously and appropriately publish on each copy an
|
||||
appropriate copyright notice and disclaimer of warranty; keep intact
|
||||
all the notices that refer to this License and to the absence of any
|
||||
warranty; and distribute a copy of this License along with the
|
||||
Library.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy,
|
||||
and you may at your option offer warranty protection in exchange for a
|
||||
fee.
|
||||
|
||||
2. You may modify your copy or copies of the Library or any portion
|
||||
of it, thus forming a work based on the Library, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) The modified work must itself be a software library.
|
||||
|
||||
b) You must cause the files modified to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
c) You must cause the whole of the work to be licensed at no
|
||||
charge to all third parties under the terms of this License.
|
||||
|
||||
d) If a facility in the modified Library refers to a function or a
|
||||
table of data to be supplied by an application program that uses
|
||||
the facility, other than as an argument passed when the facility
|
||||
is invoked, then you must make a good faith effort to ensure that,
|
||||
in the event an application does not supply such function or
|
||||
table, the facility still operates, and performs whatever part of
|
||||
its purpose remains meaningful.
|
||||
|
||||
(For example, a function in a library to compute square roots has
|
||||
a purpose that is entirely well-defined independent of the
|
||||
application. Therefore, Subsection 2d requires that any
|
||||
application-supplied function or table used by this function must
|
||||
be optional: if the application does not supply it, the square
|
||||
root function must still compute square roots.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Library,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Library, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote
|
||||
it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Library.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Library
|
||||
with the Library (or with a work based on the Library) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may opt to apply the terms of the ordinary GNU General Public
|
||||
License instead of this License to a given copy of the Library. To do
|
||||
this, you must alter all the notices that refer to this License, so
|
||||
that they refer to the ordinary GNU General Public License, version 2,
|
||||
instead of to this License. (If a newer version than version 2 of the
|
||||
ordinary GNU General Public License has appeared, then you can specify
|
||||
that version instead if you wish.) Do not make any other change in
|
||||
these notices.
|
||||
|
||||
Once this change is made in a given copy, it is irreversible for
|
||||
that copy, so the ordinary GNU General Public License applies to all
|
||||
subsequent copies and derivative works made from that copy.
|
||||
|
||||
This option is useful when you wish to copy part of the code of
|
||||
the Library into a program that is not a library.
|
||||
|
||||
4. You may copy and distribute the Library (or a portion or
|
||||
derivative of it, under Section 2) in object code or executable form
|
||||
under the terms of Sections 1 and 2 above provided that you accompany
|
||||
it with the complete corresponding machine-readable source code, which
|
||||
must be distributed under the terms of Sections 1 and 2 above on a
|
||||
medium customarily used for software interchange.
|
||||
|
||||
If distribution of object code is made by offering access to copy
|
||||
from a designated place, then offering equivalent access to copy the
|
||||
source code from the same place satisfies the requirement to
|
||||
distribute the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
5. A program that contains no derivative of any portion of the
|
||||
Library, but is designed to work with the Library by being compiled or
|
||||
linked with it, is called a "work that uses the Library". Such a
|
||||
work, in isolation, is not a derivative work of the Library, and
|
||||
therefore falls outside the scope of this License.
|
||||
|
||||
However, linking a "work that uses the Library" with the Library
|
||||
creates an executable that is a derivative of the Library (because it
|
||||
contains portions of the Library), rather than a "work that uses the
|
||||
library". The executable is therefore covered by this License.
|
||||
Section 6 states terms for distribution of such executables.
|
||||
|
||||
When a "work that uses the Library" uses material from a header file
|
||||
that is part of the Library, the object code for the work may be a
|
||||
derivative work of the Library even though the source code is not.
|
||||
Whether this is true is especially significant if the work can be
|
||||
linked without the Library, or if the work is itself a library. The
|
||||
threshold for this to be true is not precisely defined by law.
|
||||
|
||||
If such an object file uses only numerical parameters, data
|
||||
structure layouts and accessors, and small macros and small inline
|
||||
functions (ten lines or less in length), then the use of the object
|
||||
file is unrestricted, regardless of whether it is legally a derivative
|
||||
work. (Executables containing this object code plus portions of the
|
||||
Library will still fall under Section 6.)
|
||||
|
||||
Otherwise, if the work is a derivative of the Library, you may
|
||||
distribute the object code for the work under the terms of Section 6.
|
||||
Any executables containing that work also fall under Section 6,
|
||||
whether or not they are linked directly with the Library itself.
|
||||
|
||||
6. As an exception to the Sections above, you may also combine or
|
||||
link a "work that uses the Library" with the Library to produce a
|
||||
work containing portions of the Library, and distribute that work
|
||||
under terms of your choice, provided that the terms permit
|
||||
modification of the work for the customer's own use and reverse
|
||||
engineering for debugging such modifications.
|
||||
|
||||
You must give prominent notice with each copy of the work that the
|
||||
Library is used in it and that the Library and its use are covered by
|
||||
this License. You must supply a copy of this License. If the work
|
||||
during execution displays copyright notices, you must include the
|
||||
copyright notice for the Library among them, as well as a reference
|
||||
directing the user to the copy of this License. Also, you must do one
|
||||
of these things:
|
||||
|
||||
a) Accompany the work with the complete corresponding
|
||||
machine-readable source code for the Library including whatever
|
||||
changes were used in the work (which must be distributed under
|
||||
Sections 1 and 2 above); and, if the work is an executable linked
|
||||
with the Library, with the complete machine-readable "work that
|
||||
uses the Library", as object code and/or source code, so that the
|
||||
user can modify the Library and then relink to produce a modified
|
||||
executable containing the modified Library. (It is understood
|
||||
that the user who changes the contents of definitions files in the
|
||||
Library will not necessarily be able to recompile the application
|
||||
to use the modified definitions.)
|
||||
|
||||
b) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (1) uses at run time a
|
||||
copy of the library already present on the user's computer system,
|
||||
rather than copying library functions into the executable, and (2)
|
||||
will operate properly with a modified version of the library, if
|
||||
the user installs one, as long as the modified version is
|
||||
interface-compatible with the version that the work was made with.
|
||||
|
||||
c) Accompany the work with a written offer, valid for at
|
||||
least three years, to give the same user the materials
|
||||
specified in Subsection 6a, above, for a charge no more
|
||||
than the cost of performing this distribution.
|
||||
|
||||
d) If distribution of the work is made by offering access to copy
|
||||
from a designated place, offer equivalent access to copy the above
|
||||
specified materials from the same place.
|
||||
|
||||
e) Verify that the user has already received a copy of these
|
||||
materials or that you have already sent this user a copy.
|
||||
|
||||
For an executable, the required form of the "work that uses the
|
||||
Library" must include any data and utility programs needed for
|
||||
reproducing the executable from it. However, as a special exception,
|
||||
the materials to be distributed need not include anything that is
|
||||
normally distributed (in either source or binary form) with the major
|
||||
components (compiler, kernel, and so on) of the operating system on
|
||||
which the executable runs, unless that component itself accompanies
|
||||
the executable.
|
||||
|
||||
It may happen that this requirement contradicts the license
|
||||
restrictions of other proprietary libraries that do not normally
|
||||
accompany the operating system. Such a contradiction means you cannot
|
||||
use both them and the Library together in an executable that you
|
||||
distribute.
|
||||
|
||||
7. You may place library facilities that are a work based on the
|
||||
Library side-by-side in a single library together with other library
|
||||
facilities not covered by this License, and distribute such a combined
|
||||
library, provided that the separate distribution of the work based on
|
||||
the Library and of the other library facilities is otherwise
|
||||
permitted, and provided that you do these two things:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work
|
||||
based on the Library, uncombined with any other library
|
||||
facilities. This must be distributed under the terms of the
|
||||
Sections above.
|
||||
|
||||
b) Give prominent notice with the combined library of the fact
|
||||
that part of it is a work based on the Library, and explaining
|
||||
where to find the accompanying uncombined form of the same work.
|
||||
|
||||
8. You may not copy, modify, sublicense, link with, or distribute
|
||||
the Library except as expressly provided under this License. Any
|
||||
attempt otherwise to copy, modify, sublicense, link with, or
|
||||
distribute the Library is void, and will automatically terminate your
|
||||
rights under this License. However, parties who have received copies,
|
||||
or rights, from you under this License will not have their licenses
|
||||
terminated so long as such parties remain in full compliance.
|
||||
|
||||
9. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Library or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Library (or any work based on the
|
||||
Library), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Library or works based on it.
|
||||
|
||||
10. Each time you redistribute the Library (or any work based on the
|
||||
Library), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute, link with or modify the Library
|
||||
subject to these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties with
|
||||
this License.
|
||||
|
||||
11. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Library at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Library by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Library.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under any
|
||||
particular circumstance, the balance of the section is intended to apply,
|
||||
and the section as a whole is intended to apply in other circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
12. If the distribution and/or use of the Library is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Library under this License may add
|
||||
an explicit geographical distribution limitation excluding those countries,
|
||||
so that distribution is permitted only in or among countries not thus
|
||||
excluded. In such case, this License incorporates the limitation as if
|
||||
written in the body of this License.
|
||||
|
||||
13. The Free Software Foundation may publish revised and/or new
|
||||
versions of the Lesser General Public License from time to time.
|
||||
Such new versions will be similar in spirit to the present version,
|
||||
but may differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Library
|
||||
specifies a version number of this License which applies to it and
|
||||
"any later version", you have the option of following the terms and
|
||||
conditions either of that version or of any later version published by
|
||||
the Free Software Foundation. If the Library does not specify a
|
||||
license version number, you may choose any version ever published by
|
||||
the Free Software Foundation.
|
||||
|
||||
14. If you wish to incorporate parts of the Library into other free
|
||||
programs whose distribution conditions are incompatible with these,
|
||||
write to the author to ask for permission. For software which is
|
||||
copyrighted by the Free Software Foundation, write to the Free
|
||||
Software Foundation; we sometimes make exceptions for this. Our
|
||||
decision will be guided by the two goals of preserving the free status
|
||||
of all derivatives of our free software and of promoting the sharing
|
||||
and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
|
||||
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
|
||||
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
|
||||
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
|
||||
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
|
||||
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
|
||||
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
|
||||
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
|
||||
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
|
||||
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
|
||||
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
|
||||
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
|
||||
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
|
||||
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
|
||||
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
||||
DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Libraries
|
||||
|
||||
If you develop a new library, and you want it to be of the greatest
|
||||
possible use to the public, we recommend making it free software that
|
||||
everyone can redistribute and change. You can do so by permitting
|
||||
redistribution under these terms (or, alternatively, under the terms of the
|
||||
ordinary General Public License).
|
||||
|
||||
To apply these terms, attach the following notices to the library. It is
|
||||
safest to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least the
|
||||
"copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the library's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the library, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the
|
||||
library `Frob' (a library for tweaking knobs) written by James Random Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1990
|
||||
Ty Coon, President of Vice
|
||||
|
||||
72
decnet/prober/osfp/p0f/data/README.md
Normal file
72
decnet/prober/osfp/p0f/data/README.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# p0f v2 fingerprint database (vendored)
|
||||
|
||||
This directory contains the p0f v2.0.8 TCP/IP fingerprint database as
|
||||
published by Michal Zalewski in 2006, vendored here so DECNET's prober
|
||||
and profiler can do passive / active OS fingerprinting without a runtime
|
||||
network fetch.
|
||||
|
||||
## What's in here
|
||||
|
||||
| File | Purpose | Sigs |
|
||||
|-----------------------|-----------------------------------------------|------|
|
||||
| `p0f.fp` | SYN fingerprints (passive, incoming) | 262 |
|
||||
| `p0fa.fp` | SYN-ACK fingerprints (active probe responses) | 61 |
|
||||
| `p0fr.fp` | RST+ fingerprints (reset-response quirks) | 46 |
|
||||
| `p0fo.fp` | "stray" fingerprints | 6 |
|
||||
| `LICENSE.p0f-upstream`| Verbatim LGPL-2.1 text from upstream | — |
|
||||
|
||||
## Provenance
|
||||
|
||||
**Authoritative source:** Debian snapshot archive, `p0f_2.0.8.orig.tar.gz`.
|
||||
|
||||
- Archive URL: `https://snapshot.debian.org/archive/debian-archive/20120328T092752Z/debian/pool/main/p/p0f/p0f_2.0.8.orig.tar.gz`
|
||||
- SHA-1 (upstream-recorded by Debian): `7b4d5b2f24af4b5a299979134bc7f6d7b1eaf875`
|
||||
|
||||
Files in this directory are byte-identical copies of the corresponding
|
||||
files inside `p0f_2.0.8.orig.tar.gz::p0f/{doc/COPYING, *.fp}`.
|
||||
|
||||
## License + DECNET-side licensing stance
|
||||
|
||||
Upstream files are licensed under the **GNU Lesser General Public
|
||||
License, version 2.1** (see `LICENSE.p0f-upstream` — verbatim copy of
|
||||
upstream's `doc/COPYING`). Attribution belongs to Michal Zalewski and
|
||||
the named contributors in the original upstream `CREDITS` file.
|
||||
|
||||
DECNET is licensed under **GPL-3.0-or-later**. LGPL-2.1 §3 explicitly
|
||||
permits converting an LGPL-2.1 work to any version of the GPL at the
|
||||
recipient's choice. DECNET exercises that conversion for the vendored
|
||||
files: when consumed as part of DECNET they are effectively under
|
||||
GPL-3.0. The upstream LGPL-2.1 notice is preserved so:
|
||||
|
||||
- Recipients of DECNET see the full chain (original LGPL-2.1 → §3
|
||||
conversion → GPL-3.0), and
|
||||
- Anyone who wants to use these signatures under LGPL-2.1 terms
|
||||
(e.g. in an unrelated library) can still do so by pulling the files
|
||||
directly from upstream.
|
||||
|
||||
## Modifications to upstream
|
||||
|
||||
**None.** The four `.fp` files in this directory are verbatim copies.
|
||||
Any DECNET-authored additions go into a sibling file (`p0f-decnet.fp`,
|
||||
currently absent) under GPL-3.0, loaded by the same parser. Keeping
|
||||
upstream untouched means:
|
||||
|
||||
1. Syncing future upstream changes is a one-step file replacement.
|
||||
2. Attribution is unambiguous: entries in `p0f*.fp` here are Michal's,
|
||||
entries in `p0f-decnet.fp` are DECNET's.
|
||||
3. If we ever want to contribute signatures back to upstream, it's a
|
||||
one-file diff.
|
||||
|
||||
## Refreshing upstream
|
||||
|
||||
```
|
||||
curl -O https://snapshot.debian.org/archive/debian-archive/20120328T092752Z/debian/pool/main/p/p0f/p0f_2.0.8.orig.tar.gz
|
||||
echo "7b4d5b2f24af4b5a299979134bc7f6d7b1eaf875 p0f_2.0.8.orig.tar.gz" | sha1sum -c
|
||||
tar xzf p0f_2.0.8.orig.tar.gz
|
||||
cp p0f/p0f.fp p0f/p0fa.fp p0f/p0fr.fp p0f/p0fo.fp decnet/prober/osfp/p0f/data/
|
||||
cp p0f/doc/COPYING decnet/prober/osfp/p0f/data/LICENSE.p0f-upstream
|
||||
```
|
||||
|
||||
p0f v2 is no longer actively maintained upstream (last release 2006),
|
||||
so refreshes are effectively N/A — but the procedure is recorded for
|
||||
the case where a mirror we trust publishes a signed rebuild.
|
||||
834
decnet/prober/osfp/p0f/data/p0f.fp
Normal file
834
decnet/prober/osfp/p0f/data/p0f.fp
Normal file
@@ -0,0 +1,834 @@
|
||||
#
|
||||
# p0f - SYN fingerprints
|
||||
# ----------------------
|
||||
#
|
||||
# .-------------------------------------------------------------------------.
|
||||
# | The purpose of this file is to cover signatures for incoming TCP/IP |
|
||||
# | connections (SYN packets). This is the default mode of operation for |
|
||||
# | p0f. This is also the biggest and most up-to-date set of signatures |
|
||||
# | shipped with this project. The file also contains a detailed discussion |
|
||||
# | of all metrics examined by p0f, and some practical notes on how to |
|
||||
# | add new signatures. |
|
||||
# `-------------------------------------------------------------------------'
|
||||
#
|
||||
# (C) Copyright 2000-2006 by Michal Zalewski <lcamtuf@coredump.cx>
|
||||
#
|
||||
# Each line in this file specifies a single fingerprint. Please read the
|
||||
# information below carefully before attempting to append any signatures
|
||||
# reported by p0f as UNKNOWN to this file to avoid mistakes. Note that
|
||||
# this file is compatible only with the default operation mode, and not
|
||||
# with -R or -A options (SYN+ACK and RST+ modes).
|
||||
#
|
||||
# We use the following set metrics for fingerprinting:
|
||||
#
|
||||
# - Window size (WSS) - a highly OS dependent setting used for TCP/IP
|
||||
# performance control (max. amount of data to be sent without ACK).
|
||||
# Some systems use a fixed value for initial packets. On other
|
||||
# systems, it is a multiple of MSS or MTU (MSS+40). In some rare
|
||||
# cases, the value is just arbitrary.
|
||||
#
|
||||
# NEW SIGNATURE: if p0f reported a special value of 'Snn', the number
|
||||
# appears to be a multiple of MSS (MSS*nn); a special value of 'Tnn'
|
||||
# means it is a multiple of MTU ((MSS+40)*nn). Unless you notice the
|
||||
# value of nn is not fixed (unlikely), just copy the Snn or Tnn token
|
||||
# literally. If you know this device has a simple stack and a fixed
|
||||
# MTU, you can however multiply S value by MSS, or T value by MSS+40,
|
||||
# and put it instead of Snn or Tnn. One system may exhibit several T
|
||||
# or S values. In some situations, this might be a source of some
|
||||
# additional information about the setup if you have some time to dig
|
||||
# thru the kernel sources; in some other cases, like Windows, there seem
|
||||
# to be a multitude of variants and WSS selection algorithms, but it's
|
||||
# rather difficult to find a pattern without having the source.
|
||||
#
|
||||
# If WSS looks like a regular fixed value (for example is a power of two),
|
||||
# or if you can confirm the value is fixed by looking at several
|
||||
# fingerprints, please quote it literaly. If there's no apparent pattern
|
||||
# in WSS chosen, you should consider wildcarding this value - but this
|
||||
# should be the last option.
|
||||
#
|
||||
# NOTE: Some NAT devices, such as Linux iptables with --set-mss, will
|
||||
# modify MSS, but not WSS. As a result, MSS is changed to reflect
|
||||
# the MTU of the NAT device, but WSS remains a multiple of the original
|
||||
# MSS. Fortunately for us, the source device would almost always be
|
||||
# hooked up to Ethernet. P0f handles it automatically for the original
|
||||
# MSS of 1460, by adding "NAT!" tag to the result.
|
||||
#
|
||||
# In certain configurations, Linux erratically (?) uses MTU from another
|
||||
# interface on the default gw interface. This only happens on systems with
|
||||
# two network interfaces. Thus, some Linux systems that do not go thru NAT,
|
||||
# but have multiple interfaces instead, will be also tagged this way.
|
||||
#
|
||||
# P0f recognizes and automatically wildcards WSS of 12345, as generated
|
||||
# by sendack and sendsyn utilities shipped with the program, when
|
||||
# reporting a new signature. See test/sendack.c and test/sendsyn.c for more
|
||||
# information about this.
|
||||
#
|
||||
# - Overall packet size - a function of all IP and TCP options and bugs.
|
||||
# While this is partly redundant in the real world, we record this value
|
||||
# to capture rare cases when there are IP options (which we do not currently
|
||||
# examine) or packet data past the headers. Both situations are rare.
|
||||
#
|
||||
# Packet size MAY be wildcarded, but the meaning of the wildcard is
|
||||
# very special, and means the packet must be larger than PACKET_BIG
|
||||
# (defined in config.h as 100). This is usually not necessary, except
|
||||
# for some really broken implementations in RST+ mode. For more information,
|
||||
# see p0fr.fp. P0f automatically wildcards big packets when reporting
|
||||
# new signatures.
|
||||
#
|
||||
# NEW SIGNATURE: Copy this value literally.
|
||||
#
|
||||
# - Initial TTL - We check the actual TTL of a received packet. It can't
|
||||
# be higher than the initial TTL, and also shouldn't be dramatically
|
||||
# lower (maximum distance is defined in config.h as 40 hops).
|
||||
#
|
||||
# NEW SIGNATURE: *Never* copy TTL from a p0f-reported signature literally.
|
||||
# You need to determine the initial TTL. The best way to do it is to
|
||||
# check the documentation for a remote system, or check its settings.
|
||||
# A fairly good method is to simply round the observed TTL up to
|
||||
# 32, 64, 128, or 255, but it should be noted that some obscure devices
|
||||
# might not use round TTLs (in particular, some shoddy appliances and
|
||||
# IRIX and Tru64 are known to use "original" initial TTL settings). If not
|
||||
# sure, use traceroute or mtr to see how far you are from the host.
|
||||
#
|
||||
# Note that -F option overrides this check if no signature can be found.
|
||||
#
|
||||
# - Don't fragment flag (DF) - some modern OSes set this to implement PMTU
|
||||
# discovery. Others do not bother.
|
||||
#
|
||||
# NEW SIGNATURE: Copy this value literally. Note: this setting is
|
||||
# sometimes cleared by firewalls and/or certain connectivity clients.
|
||||
# Try to find out what's the actual state for a given OS if you see both,
|
||||
# and add the right one. P0f will automatically detect a case when a
|
||||
# firewall removed the DF flag and will append "(firewall!)" suffix to
|
||||
# the signature, so if the DF version is the right one, don't add no-DF
|
||||
# variant, unless it has a different meaning.
|
||||
#
|
||||
# - Maximum segment size (MSS) - this setting is usually link-dependent. P0f
|
||||
# uses it to determine link type of the remote host.
|
||||
#
|
||||
# NEW SIGNATURE: Always wildcard this value, except for rare cases when
|
||||
# you have an appliance with a fixed value, know the system supports only
|
||||
# a very limited number of network interface types, or know the system
|
||||
# is using a value it pulled out of nowhere. I use specific unique MSS
|
||||
# to tell Google crawlbots from the rest of Linux population, for example.
|
||||
#
|
||||
# If a specific MSS/MTU is unique to a certain link type, be sure to
|
||||
# add it to mtu.h instead of creating several variants of each signature.
|
||||
#
|
||||
# - Window scaling (WSCALE) - this feature is used to scale WSS.
|
||||
# It extends the size of a TCP/IP window to 32 bits, of sorts. Some modern
|
||||
# systems implement this feature.
|
||||
#
|
||||
# NEW SIGNATURE: Observe several signatures. Initial WSCALE is often set
|
||||
# to zero or other low value. There's usually no need to wildcard this
|
||||
# parameter.
|
||||
#
|
||||
# - Timestamp - some systems that implement timestamps set them to
|
||||
# zero in the initial SYN. This case is detected and handled appropriately.
|
||||
#
|
||||
# NEW SIGNATURE: Copy T or T0 option literally.
|
||||
#
|
||||
# - Selective ACK permitted - a flag set by systems that implement
|
||||
# selective ACK functionality,
|
||||
#
|
||||
# NEW SIGNATURE: copy S option literally.
|
||||
#
|
||||
# - NOP option - its presence, count and sequence is a useful OS-dependent
|
||||
# characteristic,
|
||||
#
|
||||
# NEW SIGNATURE: copy N options literally.
|
||||
#
|
||||
# - Other and unrecognized options (TTCP-related and such) - implemented by
|
||||
# some eccentric or very buggy TCP/IP stacks ;-),
|
||||
#
|
||||
# NEW SIGNATURE: copy ? options literally.
|
||||
#
|
||||
# - EOL option. Contrary to the popular belief, the presence of EOL
|
||||
# option is actually quite rare, most systems just NOP-pad to the
|
||||
# packet boundary.
|
||||
#
|
||||
# NEW SIGNATURE: copy E option literally.
|
||||
#
|
||||
# - The sequence of TCP all options mentioned above - this is very
|
||||
# specific to the implementation,
|
||||
#
|
||||
# NEW SIGNATURE: Copy the sequence literally.
|
||||
#
|
||||
# - Quirks. Some buggy stacks set certain values that should be zeroed in a
|
||||
# TCP packet to non-zero values. This has no effect as of today, but is
|
||||
# a valuable source of information. Some systems actually seem to leak
|
||||
# memory there. Other systems just exhibit harmful but very specific
|
||||
# behavior. This section captures all unusual yes-no properties not
|
||||
# related to the main and expected header layout. We detect the following:
|
||||
#
|
||||
# - Data past the headers. Neither SYN nor SYN+ACK packets are supposed
|
||||
# to carry any payload. If they do, we should take notice. The actual
|
||||
# payload is not examined, but will be displayed if use the -X option.
|
||||
# Note that payload is not unusual in RST+ mode (see p0fr.fp), very
|
||||
# rare otherwise.
|
||||
#
|
||||
# - Options past EOL. Some systems have some trailing data past EOL
|
||||
# in the options section of TCP/IP headers. P0f does not examine this
|
||||
# data as of today, simply detects its presence. If there is a
|
||||
# confirmed sizable population of systems that have data past EOL, it
|
||||
# might be a good idea to look at it. Until then, you have to recompile
|
||||
# p0f with DEBUG_EXTRAS set or use -x to display this data,
|
||||
#
|
||||
# - Zero IP ID. This again is a (mostly) harmless setting to use a fixed
|
||||
# IP ID for packets with DF set. Some systems reportedly use zero ID,
|
||||
# most OSes do not. There is a very slight probability of a false
|
||||
# positive when IP ID is "naturally" chosen to be zero on a system
|
||||
# that otherwise does set proper values, but the probability is
|
||||
# neglible (if it becomes a problem, recompile p0f with IGNORE_ZEROID
|
||||
# set in the sources).
|
||||
#
|
||||
# - IP options specified. Usually, packets do not have any IP options
|
||||
# set, but there can be some. Until there is a confirmed sizable
|
||||
# population of systems that do have IP options in a packet, p0f
|
||||
# does not examine those in detail, but it might change (use
|
||||
# DEBUG_EXTRAS or -x to display IP options if any found),
|
||||
#
|
||||
# - URG pointer value. SYN packets do not have URG flag set, so the
|
||||
# value in URG pointer in TCP header is ignored. Most systems set it
|
||||
# to zero, but some OSes (some versions of Windows, for example) do
|
||||
# not zero this field or even simply leak memory; the actual value is
|
||||
# not examined, because most cases seem to be just random garbage
|
||||
# (you can use DEBUG_EXTRAS or -x to report this information though);
|
||||
# see doc/win-memleak.txt for more information,
|
||||
#
|
||||
# - "Unused" field value. This should be always zero, but some systems
|
||||
# forget to clear it. This might result in some funny issues in the
|
||||
# future. P0f checks for non-zero value (and will display it if
|
||||
# DEBUG_EXTRAS is set, or you can use -x),
|
||||
#
|
||||
# - ACK number non-zero. ACK value in SYN packets with no ACK flag
|
||||
# is disregarded and is usually set to zero (just like with URG
|
||||
# pointer), but some systems forget to do it. The exact value is
|
||||
# not examined (but will be displayed with DEBUG_EXTRAS, or you can
|
||||
# use -x). Note that this is not an anomaly in SYN+ACK and RST+ modes,
|
||||
#
|
||||
# - Non-zero second timestamp. The initial SYN packet should have the
|
||||
# second timestamp always zeroed. SYN+ACK and RST+ may "legally" have
|
||||
# this quirk though,
|
||||
#
|
||||
# - Unusual flags. If, in addition to SYN (or SYN+ACK), there are some
|
||||
# auxilinary flags that do not modify the very meaning of a packet,
|
||||
# p0f records this (this can be URG, PUSH, or something else).
|
||||
#
|
||||
# Note: ECN flags (ECE and CWR) are ignored and denoted in a separate
|
||||
# way. ECN is never by default, because some systems can't handle it,
|
||||
# and it probably does not make much sense to include it in signatures
|
||||
# right now.
|
||||
#
|
||||
# - TCP option segment parsing problems. If p0f fails to decode options
|
||||
# because of a badly broken packet, it records this fact.
|
||||
#
|
||||
# There are several other quirks valid only in RST+ mode, see p0fr.fp for
|
||||
# more information. Those quirks are unheard of in SYN and SYN+ACK
|
||||
# modes.
|
||||
#
|
||||
# NEW SIGNATURE: Copy "quirks" section literally.
|
||||
#
|
||||
# We DO NOT use ToS for fingerprinting. While the original TCP/IP
|
||||
# fingerprinting research believed this value would be useful for this
|
||||
# purpose, it is not. The setting is way too often tweaked by network
|
||||
# devices.
|
||||
#
|
||||
# To wildcard MSS, WSS or WSCALE, replace it with '*'. You can also use a
|
||||
# modulo operator to match any values that divide by nnn - '%nnn' (and,
|
||||
# as stated above, WSS also supports special values Snn and Tnn).
|
||||
#
|
||||
# Fingerprint entry format:
|
||||
#
|
||||
# wwww:ttt:D:ss:OOO...:QQ:OS:Details
|
||||
#
|
||||
# wwww - window size (can be * or %nnn or Sxx or Txx)
|
||||
# "Snn" (multiple of MSS) and "Tnn" (multiple of MTU) are allowed.
|
||||
# ttt - initial TTL
|
||||
# D - don't fragment bit (0 - not set, 1 - set)
|
||||
# ss - overall SYN packet size (* has a special meaning)
|
||||
# OOO - option value and order specification (see below)
|
||||
# QQ - quirks list (see below)
|
||||
# OS - OS genre (Linux, Solaris, Windows)
|
||||
# details - OS description (2.0.27 on x86, etc)
|
||||
#
|
||||
# If OS genre starts with '*', p0f will not show distance, link type
|
||||
# and timestamp data. It is useful for userland TCP/IP stacks of
|
||||
# network scanners and so on, where many settings are randomized or
|
||||
# bogus.
|
||||
#
|
||||
# If OS genre starts with @, it denotes an approximate hit for a group
|
||||
# of operating systems (signature reporting still enabled in this case).
|
||||
# Use this feature at the end of this file to catch cases for which
|
||||
# you don't have a precise match, but can tell it's Windows or FreeBSD
|
||||
# or whatnot by looking at, say, flag layout alone.
|
||||
#
|
||||
# If OS genre starts with - (which can prefix @ or *), the entry is
|
||||
# not considered to be a real operating system (but userland stack
|
||||
# instead). It is important to mark all scanners and so on with -,
|
||||
# so that they are not used for masquerade detection (also add this
|
||||
# prefix for signatures of application-induced behavior, such as
|
||||
# increased window size with Opera browser).
|
||||
#
|
||||
# Option block description is a list of comma or space separated
|
||||
# options in the order they appear in the packet:
|
||||
#
|
||||
# N - NOP option
|
||||
# E - EOL option
|
||||
# Wnnn - window scaling option, value nnn (or * or %nnn)
|
||||
# Mnnn - maximum segment size option, value nnn (or * or %nnn)
|
||||
# S - selective ACK OK
|
||||
# T - timestamp
|
||||
# T0 - timestamp with zero value
|
||||
# ?n - unrecognized option number n.
|
||||
#
|
||||
# P0f can sometimes report ?nn among the options. This means it couldn't
|
||||
# recognize this option (option number nn). It's either a bug in p0f, or
|
||||
# a faulty TCP/IP stack, or, if the number is listed here:
|
||||
#
|
||||
# http://www.iana.org/assignments/tcp-parameters
|
||||
#
|
||||
# ...the stack might be simply quite exotic.
|
||||
#
|
||||
# To denote no TCP options, use a single '.'.
|
||||
#
|
||||
# Quirks section is usually an empty list ('.') of oddities or bugs of this
|
||||
# particular stack. List items are not separated in any way. Possible values:
|
||||
#
|
||||
# P - options past EOL,
|
||||
# Z - zero IP ID,
|
||||
# I - IP options specified,
|
||||
# U - urg pointer non-zero,
|
||||
# X - unused (x2) field non-zero,
|
||||
# A - ACK number non-zero,
|
||||
# T - non-zero second timestamp,
|
||||
# F - unusual flags (PUSH, URG, etc),
|
||||
# D - data payload,
|
||||
# ! - broken options segment.
|
||||
#
|
||||
# WARNING WARNING WARNING
|
||||
# -----------------------
|
||||
#
|
||||
# Do not add a system X as OS Y just because NMAP says so. It is often
|
||||
# the case that X is a NAT firewall. While nmap is talking to the
|
||||
# device itself, p0f is fingerprinting the guy behind the firewall
|
||||
# instead.
|
||||
#
|
||||
# When in doubt, use common sense, don't add something that looks like
|
||||
# a completely different system as Linux or FreeBSD or LinkSys router.
|
||||
# Check DNS name, establish a connection to the remote host and look
|
||||
# at SYN+ACK (p0f -A -S should do) - does it look similar?
|
||||
#
|
||||
# Some users tweak their TCP/IP settings - enable or disable RFC1323,
|
||||
# RFC1644 or RFC2018 support, disable PMTU discovery, change MTU, initial
|
||||
# TTL and so on. Always compare a new rule to other fingerprints for
|
||||
# this system, and verify the system isn't "customized". It is OK to
|
||||
# add signature variants caused by commonly used software (PFs, security
|
||||
# packages, etc), but it makes no sense to try to add every single
|
||||
# possible /proc/sys/net/ipv4/* tweak on Linux or so.
|
||||
#
|
||||
# KEEP IN MIND: Some packet firewalls configured to normalize outgoing
|
||||
# traffic (OpenBSD pf with "scrub" enabled, for example) will, well,
|
||||
# normalize packets. Signatures will not correspond to the originating
|
||||
# system (and probably not quite to the firewall either).
|
||||
#
|
||||
# NOTE: Try to keep this file in some reasonable order, from most to
|
||||
# least likely systems. This will speed up operation. Also keep most
|
||||
# generic and broad rules near ehe end.
|
||||
#
|
||||
# Still decided to add signature? Let us know - mail a copy of your discovery
|
||||
# to lcamtuf@coredump.cx. You can help make p0f better, and I can help you
|
||||
# make your signature more accurate.
|
||||
#
|
||||
|
||||
##########################
|
||||
# Standard OS signatures #
|
||||
##########################
|
||||
|
||||
# ----------------- AIX ---------------------
|
||||
|
||||
# AIX is first because its signatures are close to NetBSD, MacOS X and
|
||||
# Linux 2.0, but it uses a fairly rare MSSes, at least sometimes...
|
||||
# This is a shoddy hack, though.
|
||||
|
||||
45046:64:0:44:M*:.:AIX:4.3
|
||||
|
||||
16384:64:0:44:M512:.:AIX:4.3.2 and earlier
|
||||
|
||||
16384:64:0:60:M512,N,W%2,N,N,T:.:AIX:4.3.3-5.2 (1)
|
||||
32768:64:0:60:M512,N,W%2,N,N,T:.:AIX:4.3.3-5.2 (2)
|
||||
65535:64:0:60:M512,N,W%2,N,N,T:.:AIX:4.3.3-5.2 (3)
|
||||
|
||||
65535:64:0:64:M*,N,W1,N,N,T,N,N,S:.:AIX:5.3 ML1
|
||||
|
||||
# ----------------- Linux -------------------
|
||||
|
||||
S1:64:0:44:M*:A:Linux:1.2.x
|
||||
512:64:0:44:M*:.:Linux:2.0.3x (1)
|
||||
16384:64:0:44:M*:.:Linux:2.0.3x (2)
|
||||
|
||||
# Endian snafu! Nelson says "ha-ha":
|
||||
2:64:0:44:M*:.:Linux:2.0.3x (MkLinux) on Mac (1)
|
||||
64:64:0:44:M*:.:Linux:2.0.3x (MkLinux) on Mac (2)
|
||||
|
||||
S4:64:1:60:M1360,S,T,N,W0:.:Linux:2.4 (Google crawlbot)
|
||||
S4:64:1:60:M1430,S,T,N,W0:.:Linux:2.4-2.6 (Google crawlbot)
|
||||
|
||||
S2:64:1:60:M*,S,T,N,W0:.:Linux:2.4 (large MTU?)
|
||||
S3:64:1:60:M*,S,T,N,W0:.:Linux:2.4 (newer)
|
||||
S4:64:1:60:M*,S,T,N,W0:.:Linux:2.4-2.6
|
||||
|
||||
S3:64:1:60:M*,S,T,N,W1:.:Linux:2.6, seldom 2.4 (older, 1)
|
||||
S4:64:1:60:M*,S,T,N,W1:.:Linux:2.6, seldom 2.4 (older, 2)
|
||||
S3:64:1:60:M*,S,T,N,W2:.:Linux:2.6, seldom 2.4 (older, 3)
|
||||
S4:64:1:60:M*,S,T,N,W2:.:Linux:2.6, seldom 2.4 (older, 4)
|
||||
T4:64:1:60:M*,S,T,N,W2:.:Linux:2.6 (older, 5)
|
||||
|
||||
S4:64:1:60:M*,S,T,N,W5:.:Linux:2.6 (newer, 1)
|
||||
S4:64:1:60:M*,S,T,N,W6:.:Linux:2.6 (newer, 2)
|
||||
S4:64:1:60:M*,S,T,N,W7:.:Linux:2.6 (newer, 3)
|
||||
T4:64:1:60:M*,S,T,N,W7:.:Linux:2.6 (newer, 4)
|
||||
|
||||
|
||||
S20:64:1:60:M*,S,T,N,W0:.:Linux:2.2 (1)
|
||||
S22:64:1:60:M*,S,T,N,W0:.:Linux:2.2 (2)
|
||||
S11:64:1:60:M*,S,T,N,W0:.:Linux:2.2 (3)
|
||||
|
||||
# Popular cluster config scripts disable timestamps and
|
||||
# selective ACK:
|
||||
|
||||
S4:64:1:48:M1460,N,W0:.:Linux:2.4 in cluster
|
||||
|
||||
# This happens only over loopback, but let's make folks happy:
|
||||
32767:64:1:60:M16396,S,T,N,W0:.:Linux:2.4 (loopback)
|
||||
32767:64:1:60:M16396,S,T,N,W2:.:Linux:2.6 (newer, loopback)
|
||||
S8:64:1:60:M3884,S,T,N,W0:.:Linux:2.2 (loopback)
|
||||
|
||||
# Opera visitors:
|
||||
16384:64:1:60:M*,S,T,N,W0:.:-Linux:2.2 (Opera?)
|
||||
32767:64:1:60:M*,S,T,N,W0:.:-Linux:2.4 (Opera?)
|
||||
|
||||
# Some fairly common mods & oddities:
|
||||
S22:64:1:52:M*,N,N,S,N,W0:.:Linux:2.2 (tstamp-)
|
||||
S4:64:1:52:M*,N,N,S,N,W0:.:Linux:2.4 (tstamp-)
|
||||
S4:64:1:52:M*,N,N,S,N,W2:.:Linux:2.6 (tstamp-)
|
||||
S4:64:1:44:M*:.:Linux:2.6? (barebone, rare!)
|
||||
T4:64:1:60:M1412,S,T,N,W0:.:Linux:2.4 (rare!)
|
||||
|
||||
# ----------------- FreeBSD -----------------
|
||||
|
||||
16384:64:1:44:M*:.:FreeBSD:2.0-4.2
|
||||
16384:64:1:60:M*,N,W0,N,N,T:.:FreeBSD:4.4 (1)
|
||||
|
||||
1024:64:1:60:M*,N,W0,N,N,T:.:FreeBSD:4.4 (2)
|
||||
|
||||
57344:64:1:44:M*:.:FreeBSD:4.6-4.8 (RFC1323-)
|
||||
57344:64:1:60:M*,N,W0,N,N,T:.:FreeBSD:4.6-4.9
|
||||
|
||||
32768:64:1:60:M*,N,W0,N,N,T:.:FreeBSD:4.8-5.1 (or MacOS X 10.2-10.3)
|
||||
65535:64:1:60:M*,N,W0,N,N,T:.:FreeBSD:4.7-5.2 (or MacOS X 10.2-10.4) (1)
|
||||
65535:64:1:60:M*,N,W1,N,N,T:.:FreeBSD:4.7-5.2 (or MacOS X 10.2-10.4) (2)
|
||||
|
||||
65535:64:1:60:M*,N,W0,N,N,T:Z:FreeBSD:5.1 (1)
|
||||
65535:64:1:60:M*,N,W1,N,N,T:Z:FreeBSD:5.1 (2)
|
||||
65535:64:1:60:M*,N,W2,N,N,T:Z:FreeBSD:5.1 (3)
|
||||
65535:64:1:64:M*,N,N,S,N,W1,N,N,T:.:FreeBSD:5.3-5.4
|
||||
65535:64:1:64:M*,N,W1,N,N,T,S,E:P:FreeBSD:6.x (1)
|
||||
65535:64:1:64:M*,N,W0,N,N,T,S,E:P:FreeBSD:6.x (2)
|
||||
|
||||
65535:64:1:44:M*:Z:FreeBSD:5.2 (RFC1323-)
|
||||
|
||||
# 16384:64:1:60:M*,N,N,N,N,N,N,T:.:FreeBSD:4.4 (tstamp-)
|
||||
|
||||
# ----------------- NetBSD ------------------
|
||||
|
||||
16384:64:0:60:M*,N,W0,N,N,T:.:NetBSD:1.3
|
||||
65535:64:0:60:M*,N,W0,N,N,T0:.:-NetBSD:1.6 (Opera)
|
||||
16384:64:1:60:M*,N,W0,N,N,T0:.:NetBSD:1.6
|
||||
65535:64:1:60:M*,N,W1,N,N,T0:.:NetBSD:1.6W-current (DF)
|
||||
65535:64:1:60:M*,N,W0,N,N,T0:.:NetBSD:1.6X (DF)
|
||||
32768:64:1:60:M*,N,W0,N,N,T0:.:NetBSD:1.6Z or 2.0 (DF)
|
||||
32768:64:1:64:M1416,N,W0,S,N,N,N,N,T0:.:NetBSD:2.0G (DF)
|
||||
32768:64:1:64:M*,N,W0,S,N,N,N,N,T0:.:NetBSD:3.0 (DF)
|
||||
|
||||
# ----------------- OpenBSD -----------------
|
||||
|
||||
16384:64:1:64:M*,N,N,S,N,W0,N,N,T:.:OpenBSD:3.0-3.9
|
||||
57344:64:1:64:M*,N,N,S,N,W0,N,N,T:.:OpenBSD:3.3-3.4
|
||||
16384:64:0:64:M*,N,N,S,N,W0,N,N,T:.:OpenBSD:3.0-3.4 (scrub)
|
||||
65535:64:1:64:M*,N,N,S,N,W0,N,N,T:.:-OpenBSD:3.0-3.4 (Opera?)
|
||||
32768:64:1:64:M*,N,N,S,N,W0,N,N,T:.:OpenBSD:3.7
|
||||
|
||||
# ----------------- Solaris -----------------
|
||||
|
||||
S17:64:1:64:N,W3,N,N,T0,N,N,S,M*:.:Solaris:8 (RFC1323 on)
|
||||
S17:64:1:48:N,N,S,M*:.:Solaris:8 (1)
|
||||
S17:255:1:44:M*:.:Solaris:2.5-7 (1)
|
||||
|
||||
# Sometimes, just sometimes, Solaris feels like coming up with
|
||||
# rather arbitrary MSS values ;-)
|
||||
|
||||
S6:255:1:44:M*:.:Solaris:2.5-7 (2)
|
||||
S23:64:1:48:N,N,S,M*:.:Solaris:8 (2)
|
||||
S34:64:1:48:M*,N,N,S:.:Solaris:9
|
||||
S34:64:1:48:M*,N,N,N,N:.:Solaris:9 (no sack)
|
||||
S44:255:1:44:M*:.:Solaris:7
|
||||
|
||||
4096:64:0:44:M1460:.:SunOS:4.1.x
|
||||
|
||||
S34:64:1:52:M*,N,W0,N,N,S:.:Solaris:10 (beta)
|
||||
32850:64:1:64:M*,N,N,T,N,W1,N,N,S:.:Solaris:10 (1203?)
|
||||
32850:64:1:64:M*,N,W1,N,N,T,N,N,S:.:Solaris:9.1
|
||||
|
||||
# ----------------- IRIX --------------------
|
||||
|
||||
49152:60:0:44:M*:.:IRIX:6.2-6.4
|
||||
61440:60:0:44:M*:.:IRIX:6.2-6.5
|
||||
49152:60:0:52:M*,N,W2,N,N,S:.:IRIX:6.5 (RFC1323+) (1)
|
||||
49152:60:0:52:M*,N,W3,N,N,S:.:IRIX:6.5 (RFC1323+) (2)
|
||||
|
||||
61440:60:0:48:M*,N,N,S:.:IRIX:6.5.12-6.5.21 (1)
|
||||
49152:60:0:48:M*,N,N,S:.:IRIX:6.5.12-6.5.21 (2)
|
||||
|
||||
49152:60:0:64:M*,N,W2,N,N,T,N,N,S:.:IRIX:6.5 IP27
|
||||
|
||||
# ----------------- Tru64 -------------------
|
||||
# Tru64 and OpenVMS share the same stack on occassions.
|
||||
# Relax.
|
||||
|
||||
32768:60:1:48:M*,N,W0:.:Tru64:4.0 (or OS/2 Warp 4)
|
||||
32768:60:0:48:M*,N,W0:.:Tru64:5.0 (or OpenVMS 7.x on Compaq 5.0 stack)
|
||||
8192:60:0:44:M1460:.:Tru64:5.1 (no RFC1323) (or QNX 6)
|
||||
61440:60:0:48:M*,N,W0:.:Tru64:v5.1a JP4 (or OpenVMS 7.x on Compaq 5.x stack)
|
||||
|
||||
# ----------------- OpenVMS -----------------
|
||||
|
||||
6144:64:1:60:M*,N,W0,N,N,T:.:OpenVMS:7.2 (Multinet 4.3-4.4 stack)
|
||||
|
||||
# ----------------- MacOS -------------------
|
||||
|
||||
S2:255:1:48:M*,W0,E:.:MacOS:8.6 classic
|
||||
|
||||
16616:255:1:48:M*,W0,E:.:MacOS:7.3-8.6 (OTTCP)
|
||||
16616:255:1:48:M*,N,N,N,E:.:MacOS:8.1-8.6 (OTTCP)
|
||||
32768:255:1:48:M*,W0,N:.:MacOS:9.0-9.2
|
||||
|
||||
32768:255:1:48:M1380,N,N,N,N:.:MacOS:9.1 (OT 2.7.4) (1)
|
||||
65535:255:1:48:M*,N,N,N,N:.:MacOS:9.1 (OT 2.7.4) (2)
|
||||
|
||||
# ----------------- Windows -----------------
|
||||
|
||||
# Windows TCP/IP stack is a mess. For most recent XP, 2000 and
|
||||
# even 98, the pathlevel, not the actual OS version, is more
|
||||
# relevant to the signature. They share the same code, so it would
|
||||
# seem. Luckily for us, almost all Windows 9x boxes have an
|
||||
# awkward MSS of 536, which I use to tell one from another
|
||||
# in most difficult cases.
|
||||
|
||||
8192:32:1:44:M*:.:Windows:3.11 (Tucows)
|
||||
S44:64:1:64:M*,N,W0,N,N,T0,N,N,S:.:Windows:95
|
||||
8192:128:1:64:M*,N,W0,N,N,T0,N,N,S:.:Windows:95b
|
||||
|
||||
# There were so many tweaking tools and so many stack versions for
|
||||
# Windows 98 it is no longer possible to tell them from each other
|
||||
# without some very serious research. Until then, there's an insane
|
||||
# number of signatures, for your amusement:
|
||||
|
||||
S44:32:1:48:M*,N,N,S:.:Windows:98 (low TTL) (1)
|
||||
8192:32:1:48:M*,N,N,S:.:Windows:98 (low TTL) (2)
|
||||
%8192:64:1:48:M536,N,N,S:.:Windows:98 (13)
|
||||
%8192:128:1:48:M536,N,N,S:.:Windows:98 (15)
|
||||
S4:64:1:48:M*,N,N,S:.:Windows:98 (1)
|
||||
S6:64:1:48:M*,N,N,S:.:Windows:98 (2)
|
||||
S12:64:1:48:M*,N,N,S:.:Windows:98 (3
|
||||
T30:64:1:64:M1460,N,W0,N,N,T0,N,N,S:.:Windows:98 (16)
|
||||
32767:64:1:48:M*,N,N,S:.:Windows:98 (4)
|
||||
37300:64:1:48:M*,N,N,S:.:Windows:98 (5)
|
||||
46080:64:1:52:M*,N,W3,N,N,S:.:Windows:98 (RFC1323+)
|
||||
65535:64:1:44:M*:.:Windows:98 (no sack)
|
||||
S16:128:1:48:M*,N,N,S:.:Windows:98 (6)
|
||||
S16:128:1:64:M*,N,W0,N,N,T0,N,N,S:.:Windows:98 (7)
|
||||
S26:128:1:48:M*,N,N,S:.:Windows:98 (8)
|
||||
T30:128:1:48:M*,N,N,S:.:Windows:98 (9)
|
||||
32767:128:1:52:M*,N,W0,N,N,S:.:Windows:98 (10)
|
||||
60352:128:1:48:M*,N,N,S:.:Windows:98 (11)
|
||||
60352:128:1:64:M*,N,W2,N,N,T0,N,N,S:.:Windows:98 (12)
|
||||
|
||||
# What's with 1414 on NT?
|
||||
T31:128:1:44:M1414:.:Windows:NT 4.0 SP6a (1)
|
||||
64512:128:1:44:M1414:.:Windows:NT 4.0 SP6a (2)
|
||||
8192:128:1:44:M*:.:Windows:NT 4.0 (older)
|
||||
|
||||
# Windows XP and 2000. Most of the signatures that were
|
||||
# either dubious or non-specific (no service pack data)
|
||||
# were deleted and replaced with generics at the end.
|
||||
|
||||
65535:128:1:48:M*,N,N,S:.:Windows:2000 SP4, XP SP1+
|
||||
%8192:128:1:48:M*,N,N,S:.:Windows:2000 SP2+, XP SP1+ (seldom 98)
|
||||
S20:128:1:48:M*,N,N,S:.:Windows:SP3
|
||||
S45:128:1:48:M*,N,N,S:.:Windows:2000 SP4, XP SP1+ (2)
|
||||
40320:128:1:48:M*,N,N,S:.:Windows:2000 SP4
|
||||
|
||||
S6:128:1:48:M*,N,N,S:.:Windows:XP, 2000 SP2+
|
||||
S12:128:1:48:M*,N,N,S:.:Windows:XP SP1+ (1)
|
||||
S44:128:1:48:M*,N,N,S:.:Windows:XP SP1+, 2000 SP3
|
||||
64512:128:1:48:M*,N,N,S:.:Windows:XP SP1+, 2000 SP3 (2)
|
||||
32767:128:1:48:M*,N,N,S:.:Windows:XP SP1+, 2000 SP4 (3)
|
||||
|
||||
# Windows 2003 & Vista
|
||||
|
||||
8192:128:1:52:M*,W8,N,N,N,S:.:Windows:Vista (beta)
|
||||
32768:32:1:52:M1460,N,W0,N,N,S:.:Windows:2003 AS
|
||||
65535:64:1:52:M1460,N,W2,N,N,S:.:Windows:2003 (1)
|
||||
65535:64:1:48:M1460,N,N,S:.:Windows:2003 (2)
|
||||
|
||||
# Odds, ends, mods:
|
||||
|
||||
S52:128:1:48:M1260,N,N,S:.:Windows:XP/2000 via Cisco
|
||||
65520:128:1:48:M*,N,N,S:.:Windows:XP bare-bone
|
||||
16384:128:1:52:M536,N,W0,N,N,S:.:Windows:2000 w/ZoneAlarm?
|
||||
2048:255:0:40:.:.:Windows:.NET Enterprise Server
|
||||
44620:64:0:48:M*,N,N,S:.:Windows:ME no SP (?)
|
||||
S6:255:1:48:M536,N,N,S:.:Windows:95 winsock 2
|
||||
32000:128:0:48:M*,N,N,S:.:Windows:XP w/Winroute?
|
||||
16384:64:1:48:M1452,N,N,S:.:Windows:XP w/Sygate? (1)
|
||||
17256:64:1:48:M1460,N,N,S:.:Windows:XP w/Sygate? (2)
|
||||
|
||||
# No need to be more specific, it passes:
|
||||
*:128:1:48:M*,N,N,S:U:-Windows:XP/2000 while downloading (leak!)
|
||||
|
||||
# ----------------- HP/UX -------------------
|
||||
|
||||
32768:64:1:44:M*:.:HP-UX:B.10.20
|
||||
32768:64:1:48:M*,W0,N:.:HP-UX:11.00-11.11
|
||||
|
||||
# Whoa. Hardcore WSS.
|
||||
0:64:0:48:M*,W0,N:.:HP-UX:B.11.00 A (RFC1323+)
|
||||
|
||||
# ----------------- RiscOS ------------------
|
||||
|
||||
16384:64:1:68:M1460,N,W0,N,N,T,N,N,?12:.:RISC OS:3.70-4.36 (inet 5.04)
|
||||
12288:32:0:44:M536:.:RISC OS:3.70 inet 4.10
|
||||
4096:64:1:56:M1460,N,N,T:T:RISC OS:3.70 freenet 2.00
|
||||
|
||||
# ----------------- BSD/OS ------------------
|
||||
|
||||
8192:64:1:60:M1460,N,W0,N,N,T:.:BSD/OS:3.1-4.3 (or MacOS X 10.2)
|
||||
|
||||
# ---------------- NetwonOS -----------------
|
||||
|
||||
4096:64:0:44:M1420:.:NewtonOS:2.1
|
||||
|
||||
# ---------------- NeXTSTEP -----------------
|
||||
|
||||
S8:64:0:44:M512:.:NeXTSTEP:3.3 (1)
|
||||
S4:64:0:44:M1024:.:NeXTSTEP:3.3 (2)
|
||||
|
||||
# ------------------ BeOS -------------------
|
||||
|
||||
1024:255:0:48:M*,N,W0:.:BeOS:5.0-5.1
|
||||
12288:255:0:44:M*:.:BeOS:5.0.x
|
||||
|
||||
# ------------------ OS/400 -----------------
|
||||
|
||||
8192:64:1:60:M1440,N,W0,N,N,T:.:OS/400:V4R4/R5
|
||||
8192:64:0:44:M536:.:OS/400:V4R3/M0
|
||||
4096:64:1:60:M1440,N,W0,N,N,T:.:OS/400:V4R5 + CF67032
|
||||
|
||||
28672:64:0:44:M1460:A:OS/390:?
|
||||
|
||||
# ------------------ ULTRIX -----------------
|
||||
|
||||
16384:64:0:40:.:.:ULTRIX:4.5
|
||||
|
||||
# ------------------- QNX -------------------
|
||||
|
||||
S16:64:0:44:M512:.:QNX:demodisk
|
||||
16384:64:0:60:M1460,N,W0,N,N,T0:.:QNX:6.x
|
||||
|
||||
# ------------------ Novell -----------------
|
||||
|
||||
16384:128:1:44:M1460:.:Novell:NetWare 5.0
|
||||
6144:128:1:44:M1460:.:Novell:IntranetWare 4.11
|
||||
6144:128:1:44:M1368:.:Novell:BorderManager ?
|
||||
|
||||
# According to rfp:
|
||||
6144:128:1:52:M*,W0,N,S,N,N:.:Novell:Netware 6 SP3
|
||||
|
||||
# -------------- SCO UnixWare ---------------
|
||||
|
||||
S3:64:1:60:M1460,N,W0,N,N,T:.:SCO:UnixWare 7.1
|
||||
S17:64:1:60:M*,N,W0,N,N,T:.:SCO:UnixWare 7.1.x
|
||||
S23:64:1:44:M1380:.:SCO:OpenServer 5.0
|
||||
|
||||
# ------------------- DOS -------------------
|
||||
|
||||
2048:255:0:44:M536:.:DOS:Arachne via WATTCP/1.05
|
||||
T2:255:0:44:M984:.:DOS:Arachne via WATTCP/1.05 (eepro)
|
||||
16383:64:0:44:M536:.:DOS:Unknown via WATTCP (epppd)
|
||||
|
||||
# ------------------ OS/2 -------------------
|
||||
|
||||
S56:64:0:44:M512:.:OS/2:4
|
||||
28672:64:0:44:M1460:.:OS/2:Warp 4.0
|
||||
|
||||
# ----------------- TOPS-20 -----------------
|
||||
|
||||
# Another hardcore MSS, one of the ACK leakers hunted down.
|
||||
0:64:0:44:M1460:A:TOPS-20:version 7
|
||||
|
||||
# ------------------ AMIGA ------------------
|
||||
|
||||
S32:64:1:56:M*,N,N,S,N,N,?12:.:AMIGA:3.9 BB2 with Miami stack
|
||||
|
||||
# ------------------ Minix ------------------
|
||||
|
||||
# Not quite sure.
|
||||
# 8192:210:0:44:M1460:X:@Minix:?
|
||||
|
||||
# ------------------ Plan9 ------------------
|
||||
|
||||
65535:255:0:48:M1460,W0,N:.:Plan9:edition 4
|
||||
|
||||
# ----------------- AMIGAOS -----------------
|
||||
|
||||
16384:64:1:48:M1560,N,N,S:.:AMIGAOS:3.9 BB2 MiamiDX
|
||||
|
||||
# ----------------- FreeMiNT ----------------
|
||||
|
||||
S44:255:0:44:M536:.:FreeMiNT:1 patch 16A (Atari)
|
||||
|
||||
###########################################
|
||||
# Appliance / embedded / other signatures #
|
||||
###########################################
|
||||
|
||||
# ---------- Firewalls / routers ------------
|
||||
|
||||
S12:64:1:44:M1460:.:@Checkpoint:(unknown 1)
|
||||
S12:64:1:48:N,N,S,M1460:.:@Checkpoint:(unknown 2)
|
||||
4096:32:0:44:M1460:.:ExtremeWare:4.x
|
||||
|
||||
S32:64:0:68:M512,N,W0,N,N,T,N,N,?12:.:Nokia:IPSO w/Checkpoint NG FP3
|
||||
S16:64:0:68:M1024,N,W0,N,N,T,N,N,?12:.:Nokia:IPSO 3.7 build 026
|
||||
|
||||
S4:64:1:60:W0,N,S,T,M1460:.:FortiNet:FortiGate 50
|
||||
|
||||
8192:64:1:44:M1460:.:@Eagle:Secure Gateway
|
||||
|
||||
# ------- Switches and other stuff ----------
|
||||
|
||||
4128:255:0:44:M*:Z:Cisco:7200, Catalyst 3500, etc
|
||||
S8:255:0:44:M*:.:Cisco:12008
|
||||
S4:255:0:44:M536:Z:Cisco:IOS 11.0
|
||||
60352:128:1:64:M1460,N,W2,N,N,T,N,N,S:.:Alteon:ACEswitch
|
||||
64512:128:1:44:M1370:.:Nortel:Contivity Client
|
||||
|
||||
# ---------- Caches and whatnots ------------
|
||||
|
||||
8190:255:0:44:M1428:.:Google:Wireless Transcoder (1)
|
||||
8190:255:0:44:M1460:.:Google:Wireless Transcoder (2)
|
||||
8192:64:1:64:M1460,N,N,S,N,W0,N,N,T:.:NetCache:5.2
|
||||
16384:64:1:64:M1460,N,N,S,N,W0,N:.:NetCache:5.3
|
||||
65535:64:1:64:M1460,N,N,S,N,W*,N,N,T:.:NetCache:5.3-5.5 (or FreeBSD 5.4)
|
||||
20480:64:1:64:M1460,N,N,S,N,W0,N,N,T:.:NetCache:4.1
|
||||
S44:64:1:64:M1460,N,N,S,N,W0,N,N,T:.:NetCache:5.5
|
||||
|
||||
32850:64:1:64:N,W1,N,N,T,N,N,S,M*:.:NetCache:Data OnTap 5.x
|
||||
|
||||
65535:64:0:60:M1460,N,W0,N,N,T:.:CacheFlow:CacheOS 4.1
|
||||
8192:64:0:60:M1380,N,N,N,N,N,N,T:.:CacheFlow:CacheOS 1.1
|
||||
|
||||
S4:64:0:48:M1460,N,N,S:.:Cisco:Content Engine
|
||||
|
||||
27085:128:0:40:.:.:Dell:PowerApp cache (Linux-based)
|
||||
|
||||
65535:255:1:48:N,W1,M1460:.:Inktomi:crawler
|
||||
S1:255:1:60:M1460,S,T,N,W0:.:LookSmart:ZyBorg
|
||||
|
||||
16384:255:0:40:.:.:Proxyblocker:(what's this?)
|
||||
|
||||
65535:255:0:48:M*,N,N,S:.:Redline: T|X 2200
|
||||
|
||||
# ----------- Embedded systems --------------
|
||||
|
||||
S9:255:0:44:M536:.:PalmOS:Tungsten T3/C
|
||||
S5:255:0:44:M536:.:PalmOS:3/4
|
||||
S4:255:0:44:M536:.:PalmOS:3.5
|
||||
2948:255:0:44:M536:.:PalmOS:3.5.3 (Handera)
|
||||
S29:255:0:44:M536:.:PalmOS:5.0
|
||||
16384:255:0:44:M1398:.:PalmOS:5.2 (Clie)
|
||||
S14:255:0:44:M1350:.:PalmOS:5.2.1 (Treo)
|
||||
16384:255:0:44:M1400:.:PalmOS:5.2 (Sony)
|
||||
|
||||
S23:64:1:64:N,W1,N,N,T,N,N,S,M1460:.:SymbianOS:7
|
||||
8192:255:0:44:M1460:.:SymbianOS:6048 (Nokia 7650?)
|
||||
8192:255:0:44:M536:.:SymbianOS:(Nokia 9210?)
|
||||
S22:64:1:56:M1460,T,S:.:SymbianOS:? (SE P800?)
|
||||
S36:64:1:56:M1360,T,S:.:SymbianOS:60xx (Nokia 6600?)
|
||||
S36:64:1:60:M1360,T,S,W0,E:.:SymbianOS:60xx
|
||||
|
||||
32768:32:1:44:M1460:.:Windows:CE 3
|
||||
|
||||
# Perhaps S4?
|
||||
5840:64:1:60:M1452,S,T,N,W1:.:Zaurus:3.10
|
||||
|
||||
32768:128:1:64:M1460,N,W0,N,N,T0,N,N,S:.:PocketPC:2002
|
||||
|
||||
S1:255:0:44:M346:.:Contiki:1.1-rc0
|
||||
|
||||
4096:128:0:44:M1460:.:Sega:Dreamcast Dreamkey 3.0
|
||||
T5:64:0:44:M536:.:Sega:Dreamcast HKT-3020 (browser disc 51027)
|
||||
S22:64:1:44:M1460:.:Sony:Playstation 2 (SOCOM?)
|
||||
|
||||
S12:64:0:44:M1452:.:AXIS:Printer Server 5600 v5.64
|
||||
|
||||
3100:32:1:44:M1460:.:Windows:CE 2.0
|
||||
|
||||
####################
|
||||
# Fancy signatures #
|
||||
####################
|
||||
|
||||
1024:64:0:40:.:.:-*NMAP:syn scan (1)
|
||||
2048:64:0:40:.:.:-*NMAP:syn scan (2)
|
||||
3072:64:0:40:.:.:-*NMAP:syn scan (3)
|
||||
4096:64:0:40:.:.:-*NMAP:syn scan (4)
|
||||
|
||||
1024:64:0:40:.:A:-*NMAP:TCP sweep probe (1)
|
||||
2048:64:0:40:.:A:-*NMAP:TCP sweep probe (2)
|
||||
3072:64:0:40:.:A:-*NMAP:TCP sweep probe (3)
|
||||
4096:64:0:40:.:A:-*NMAP:TCP sweep probe (4)
|
||||
|
||||
1024:64:0:60:W10,N,M265,T,E:P:-*NMAP:OS detection probe (1)
|
||||
2048:64:0:60:W10,N,M265,T,E:P:-*NMAP:OS detection probe (2)
|
||||
3072:64:0:60:W10,N,M265,T,E:P:-*NMAP:OS detection probe (3)
|
||||
4096:64:0:60:W10,N,M265,T,E:P:-*NMAP:OS detection probe (4)
|
||||
|
||||
1024:64:0:60:W10,N,M265,T,E:PF:-*NMAP:OS detection probe w/flags (1)
|
||||
2048:64:0:60:W10,N,M265,T,E:PF:-*NMAP:OS detection probe w/flags (2)
|
||||
3072:64:0:60:W10,N,M265,T,E:PF:-*NMAP:OS detection probe w/flags (3)
|
||||
4096:64:0:60:W10,N,M265,T,E:PF:-*NMAP:OS detection probe w/flags (4)
|
||||
|
||||
32767:64:0:40:.:.:-*NAST:syn scan
|
||||
|
||||
12345:255:0:40:.:A:-p0f:sendsyn utility
|
||||
|
||||
# UFO - see tmp/*:
|
||||
56922:128:0:40:.:A:-@Mysterious:port scanner (?)
|
||||
5792:64:1:60:M1460,S,T,N,W0:T:-@Mysterious:NAT device (2nd tstamp)
|
||||
S12:128:1:48:M1460,E:P:@Mysterious:Chello proxy (?)
|
||||
S23:64:1:64:N,W1,N,N,T,N,N,S,M1380:.:@Mysterious:GPRS gateway (?)
|
||||
|
||||
#####################################
|
||||
# Generic signatures - just in case #
|
||||
#####################################
|
||||
|
||||
*:128:1:52:M*,N,W0,N,N,S:.:@Windows:XP/2000 (RFC1323+, w, tstamp-)
|
||||
*:128:1:52:M*,N,W*,N,N,S:.:@Windows:XP/2000 (RFC1323+, w+, tstamp-)
|
||||
*:128:1:52:M*,N,N,T0,N,N,S:.:@Windows:XP/2000 (RFC1323+, w-, tstamp+)
|
||||
*:128:1:64:M*,N,W0,N,N,T0,N,N,S:.:@Windows:XP/2000 (RFC1323+, w, tstamp+)
|
||||
*:128:1:64:M*,N,W*,N,N,T0,N,N,S:.:@Windows:XP/2000 (RFC1323+, w+, tstamp+)
|
||||
|
||||
*:128:1:48:M536,N,N,S:.:@Windows:98
|
||||
*:128:1:48:M*,N,N,S:.:@Windows:XP/2000
|
||||
|
||||
|
||||
208
decnet/prober/osfp/p0f/data/p0fa.fp
Normal file
208
decnet/prober/osfp/p0f/data/p0fa.fp
Normal file
@@ -0,0 +1,208 @@
|
||||
#
|
||||
# p0f - SYN+ACK fingerprints
|
||||
# --------------------------
|
||||
#
|
||||
# .-------------------------------------------------------------------------.
|
||||
# | The purpose of this file is to cover signatures for outgoing TCP/IP |
|
||||
# | connections (SYN+ACK packets). This mode of operation can be enabled |
|
||||
# | with -A option. Please refer to p0f.fp for information on the metrics |
|
||||
# | used to create a signature, and for a guide on adding new entries to |
|
||||
# | those files. This database is somewhat neglected, and is looking for a |
|
||||
# | caring maintainer. |
|
||||
# `-------------------------------------------------------------------------'
|
||||
#
|
||||
# (C) Copyright 2000-2006 by Michal Zalewski <lcamtuf@coredump.cx>
|
||||
#
|
||||
# Plenty of signatures contributed in bulk by rain forest puppy, Paul Woo and
|
||||
# Michael Bauer.
|
||||
#
|
||||
# Submit all additions to the authors. Read p0f.fp before adding any
|
||||
# signatures. Run p0f -A -C after making any modifications. This file is
|
||||
# NOT compatible with SYN, RST+, or stray ACK modes. Use only with -A option.
|
||||
#
|
||||
# Feel like contributing? You can run p0f -A -K, then test/tryid -iR nnn...
|
||||
#
|
||||
# IMPORTANT INFORMATION ABOUT THE INTERDEPENDENCY OF SYNs AND SYN+ACKs
|
||||
# --------------------------------------------------------------------
|
||||
#
|
||||
# Some systems would have different SYN+ACK fingerprints depending on
|
||||
# the system that sent SYN. More specifically, RFC1323, RFC2018 and
|
||||
# RFC1644 extensions sometimes show up only if SYN had them enabled.
|
||||
#
|
||||
# Also, some silly systems may copy WSS from the SYN packet you've sent,
|
||||
# in which case, you need to wildcard the value. Use test/sendsyn.c, which
|
||||
# uses a distinct WSS of 12345, to test for this condition if unsure.
|
||||
#
|
||||
# IMPORTANT INFORMATION ABOUT DIFFERENCES IN COMPARISON TO p0f.fp:
|
||||
# ----------------------------------------------------------------
|
||||
#
|
||||
# - 'A' quirk would be present on almost every signature here. ACK number
|
||||
# is unusual for SYN packets, but is a commonplace in SYN+ACK packets,
|
||||
# of course. It is still possible to have a signature without 'A', when
|
||||
# the ACK flag is present but the value is zero - this, however, is
|
||||
# very uncommon.
|
||||
#
|
||||
# - 'T' quirk would show up on almost all signatures for systems implementing
|
||||
# RFC1323. The second timestamp is only unusual for SYN packets. SYN+ACK
|
||||
# are expected to have it set.
|
||||
#
|
||||
|
||||
##########################
|
||||
# Standard OS signatures #
|
||||
##########################
|
||||
|
||||
# ---------------- Linux -------------------
|
||||
|
||||
32736:64:0:44:M*:A:Linux:2.0
|
||||
S22:64:1:60:M*,S,T,N,W0:AT:Linux:2.2
|
||||
S22:64:1:52:M*,N,N,S,N,W0:A:Linux:2.2 w/o timestamps
|
||||
|
||||
5792:64:1:60:M*,S,T,N,W0:AT:Linux:older 2.4
|
||||
5792:64:1:60:M*,S,T,N,W0:ZAT:Linux:recent 2.4 (1)
|
||||
S4:64:1:44:M*:ZA:Linux:recent 2.4 (2)
|
||||
5792:64:1:44:M*:ZA:Linux:recent 2.4 (3)
|
||||
|
||||
S4:64:1:52:M*,N,N,S,N,W0:ZA:Linux:2.4 w/o timestamps
|
||||
|
||||
# --------------- Windows ------------------
|
||||
|
||||
65535:128:1:64:M*,N,W0,N,N,T0,N,N,S:A:Windows:2000 SP4
|
||||
S44:128:1:64:M*,N,W0,N,N,T0,N,N,S:A:Windows:XP SP1
|
||||
S12:128:1:64:M*,N,W0,N,N,T0,N,N,S:A:Windows:2000 (SP1+)
|
||||
S6:128:1:44:M*:A:Windows:NT 4.0 SP1+
|
||||
65535:128:1:48:M*,N,N,S:A:Windows:98 (SE)
|
||||
65535:128:1:44:M*:A:Windows:2000 (1)
|
||||
16616:128:1:44:M*:A:Windows:2003
|
||||
16384:128:1:44:M*:A:Windows:2000 (2)
|
||||
S16:128:1:44:M*:A:Windows:2000 (3)
|
||||
|
||||
# ------------------- OpenBSD --------------
|
||||
|
||||
17376:64:1:64:M*,N,N,S,N,W0,N,N,T:AT:OpenBSD:3.3
|
||||
|
||||
# ------------------- NetBSD ----------------
|
||||
|
||||
16384:64:0:60:M*,N,W0,N,N,T0:AT:NetBSD:1.6
|
||||
|
||||
# ----------------- HP/UX ------------------
|
||||
|
||||
32768:64:1:44:M*:A:HPUX:10.20
|
||||
|
||||
# ----------------- Tru64 ------------------
|
||||
|
||||
S23:60:0:48:M*,N,W0:A:Tru64:5.0 (1)
|
||||
65535:64:0:44:M*:A:Tru64:5.0 (2)
|
||||
|
||||
# ----------------- Novell -----------------
|
||||
|
||||
6144:128:1:52:M*,W0,N,S,N,N:A:Novell:Netware 6.0 (SP3)
|
||||
32768:128:1:44:M*:A:Novell:Netware 5.1
|
||||
|
||||
# ------------------ IRIX ------------------
|
||||
|
||||
60816:60:1:60:M*,N,W0,N,N,T:AT:IRIX:6.5.0
|
||||
|
||||
# ----------------- Solaris ----------------
|
||||
|
||||
49232:64:1:64:N,N,T,M*,N,W0,N,N,S:AT:Solaris:9 (1)
|
||||
S1:255:1:60:N,N,T,N,W0,M*:AT:Solaris:7
|
||||
24656:64:1:44:M*:A:Solaris:8
|
||||
33304:64:1:60:N,N,T,M*,N,W1:AT:Solaris:9 (2)
|
||||
|
||||
# ----------------- FreeBSD ----------------
|
||||
|
||||
65535:64:1:60:M*,N,W1,N,N,T:AT:FreeBSD:5.0
|
||||
57344:64:1:44:M*:A:FreeBSD:4.6-4.8
|
||||
65535:64:1:44:M*:A:FreeBSD:4.4
|
||||
|
||||
57344:64:1:48:M1460,N,W0:A:FreeBSD:4.6-4.8 (wscale)
|
||||
57344:64:1:60:M1460,N,W0,N,N,T:AT:FreeBSD:4.6-4.8 (RFC1323)
|
||||
|
||||
# ------------------- AIX ------------------
|
||||
|
||||
S17:255:1:44:M536:A:AIX:4.2
|
||||
|
||||
S12:64:0:44:M1460:A:AIX:5.2 ML04 (1)
|
||||
S42:64:0:44:M1460:A:AIX:5.2 ML04 (2)
|
||||
|
||||
# ------------------ BSD/OS ----------------
|
||||
|
||||
S6:64:1:60:M1460,N,W0,N,N,T:AT:BSD/OS:4.0.x
|
||||
|
||||
# ------------------ OS/390 ----------------
|
||||
|
||||
2048:64:0:44:M1460:A:OS/390:?
|
||||
|
||||
# ------------------ Novell ----------------
|
||||
|
||||
6144:128:1:44:M1400:A:Novell:iChain 2.2
|
||||
|
||||
# ------------------ MacOS -----------------
|
||||
|
||||
33304:64:1:60:M*,N,W0,N,N,T:AT:MacOS:X 10.2.6
|
||||
|
||||
#################################################################
|
||||
# Contributed by Ryan Kruse <rkruse@alterpoint.com> - trial run #
|
||||
#################################################################
|
||||
|
||||
# S4:255:0:44:M1024:A:Cisco:LocalDirector
|
||||
# 1024:255:0:44:M536:A:Cisco,3COM,Nortel:CatIOS,SuperStack,BayStack
|
||||
# S16:64:0:44:M512:A:Nortel:Contivity
|
||||
# 8192:64:0:44:M1460:A:Cisco,Nortel,SonicWall,Tasman:Aironet,BayStack Switch,Soho,1200
|
||||
# 4096:255:0:44:M1460:A:Cisco:PIX,CatOS
|
||||
# 8192:128:0:44:M1460:A:Cisco:VPN Concentrator
|
||||
# 8192:128:0:60:M1460,N,W0,N,N,T:AT:Cisco:VPN Concentrator
|
||||
# 4096:32:0:44:M1460:A:Cisco,3COM,Extreme,Nortel:Catalyst Switch CatOS,CoreBuilder,Summit,Passport
|
||||
# S4:255:0:44:M536:ZA:Cisco:IOS
|
||||
# 1024:32:0:44:M1480:UA:Nortel:BayStack Switch
|
||||
# 4096:60:0:44:M1460:A:Adtran:NetVanta
|
||||
# 4096:64:0:44:M1008:A:Adtran:TSU
|
||||
# S4:32:0:44:M1024:A:Alcatel:Switch
|
||||
# S8:255:0:44:M536:ZA:Cisco:IOS
|
||||
# 50:255:0:44:M536:ZA:Cisco:CatIOS
|
||||
# 512:64:0:40:.:A:Dell:Switch
|
||||
# 4096:64:0:40:.:A:Enterasys:Vertical Horizon Switch
|
||||
# 17640:64:1:44:M1460:A:F5,Juniper,RiverStone:BigIP,Juniper OS,Router 7.0+
|
||||
# 16384:64:0:44:M1460:A:Foundry,SonicWall:BigIron,TZ
|
||||
# 4096:64:0:44:M1452:A:HP:ProCurve Switch
|
||||
# 1024:64:0:44:M1260:A:Marconi:ES
|
||||
# 10240:30:0:44:M1460:A:Milan:Switch
|
||||
# 4096:64:0:44:M1380:A:NetScreen:Firewall
|
||||
# S32:64:0:44:M512:A:Nokia:CheckPoint
|
||||
# 1024:64:0:44:M536:A:Nortel:BayStack Switch
|
||||
# 4128:255:0:44:M*:ZA:Cisco:IOS
|
||||
# 1024:16:0:44:M536:A:Nortel:BayStack Switch
|
||||
# 1024:30:0:44:M1480:A:Nortel:BayStack Switch
|
||||
# S4:64:0:44:M1460:A:Symbol:Spectrum Access Point
|
||||
# S2:255:0:44:M512:A:ZyXEL:Prestige
|
||||
# S16:255:0:44:M1024:A:ZyXEL:ZyAI
|
||||
|
||||
###########################################
|
||||
# Appliance / embedded / other signatures #
|
||||
###########################################
|
||||
|
||||
16384:64:1:44:M1460:A:F5:BigIP LB 4.1.x (sometimes FreeBSD)
|
||||
4128:255:0:44:M*:ZA:Cisco:Catalyst 2900 12.0(5)
|
||||
4096:60:0:44:M*:A:Brother:HL-1270N
|
||||
S1:30:0:44:M1730:A:Cyclades:PR3000
|
||||
8192:64:1:44:M1460:A:NetApp:Data OnTap 6.x
|
||||
5792:64:1:60:W0,N,N,N,T,M1460:ZAT:FortiNet:FortiGate 50
|
||||
S1:64:1:44:M1460:A:NetCache:5.3.1
|
||||
S1:64:0:44:M512:A:Printer:controller (?)
|
||||
4096:128:0:40:.:A:Sequent:DYNIX 4.2.x
|
||||
S16:64:0:44:M512:A:3Com:NBX PBX (BSD/OS 2.1)
|
||||
16000:64:0:44:M1442:A:CastleNet:DSL router
|
||||
S2:64:0:44:M32728:A:D-Link:DSL-500
|
||||
S4:60:0:44:M1460:A:HP:JetDirect A.05.32
|
||||
8576:64:1:44:M*:A:Raptor:firewall
|
||||
S12:64:1:44:M1400:A:Cequrux Firewall:4.x
|
||||
2048:255:0:44:M1400:A:Netgear:MR814
|
||||
16384:128:0:64:M1460,N,W0,N,N,T0,N,N,S:A:Akamai:??? (1)
|
||||
16384:128:0:60:M1460,N,W0,N,N,T0:A:Akamai:??? (2)
|
||||
|
||||
8190:255:0:44:M1452:A:Citrix:Netscaler 6.1
|
||||
|
||||
# Whatever they run. EOL boys...
|
||||
S6:128:1:48:M1460,E:PA:@Slashdot:or BusinessWeek (???)
|
||||
|
||||
|
||||
48
decnet/prober/osfp/p0f/data/p0fo.fp
Normal file
48
decnet/prober/osfp/p0f/data/p0fo.fp
Normal file
@@ -0,0 +1,48 @@
|
||||
#
|
||||
# p0f - stray ACK signatures
|
||||
# --------------------------
|
||||
#
|
||||
# .-------------------------------------------------------------------------.
|
||||
# | The purpose of this file is to cover signatures for stray ACK packets |
|
||||
# | (established session data). This mode of operation is enabled with -O |
|
||||
# | option and is HIGHLY EXPERIMENTAL. Please refer to p0f.fp for more |
|
||||
# | information on the metrics used and for a guide on adding new entries |
|
||||
# | to this file. This database is looking for a caring maintainer. |
|
||||
# `-------------------------------------------------------------------------'
|
||||
#
|
||||
# (C) Copyright 2000-2006 by Michal Zalewski <lcamtuf@coredump.cx>
|
||||
#
|
||||
# Submit all additions to the authors. Read p0f.fp before adding any
|
||||
# signatures. Run p0f -O -C after making any modifications. This file is
|
||||
# NOT compatible with SYN, SYN+ACK or RST+ modes. Use only with -O option.
|
||||
#
|
||||
# IMPORTANT INFORMATION ABOUT THE INTERDEPENDENCY OF SYNs AND ACKs
|
||||
# ----------------------------------------------------------------
|
||||
#
|
||||
# Some systems would have different ACK fingerprints depending on the initial
|
||||
# SYN or SYN+ACK received from the other party. More specifically, RFC1323,
|
||||
# RFC2018 and RFC1644 extensions sometimes show up only if the other party had
|
||||
# them enabled. Hence, the reliability of ACK fingerprints may be affected.
|
||||
#
|
||||
# IMPORTANT INFORMATION ABOUT DIFFERENCES IN COMPARISON TO p0f.fp:
|
||||
# ----------------------------------------------------------------
|
||||
#
|
||||
# - Packet size MUST be wildcarded. ACK packets, by their nature, have
|
||||
# variable sizes, depending on the amount of data carried as a payload.
|
||||
#
|
||||
# - Similarly, 'D' quirk is not checked for, and is not allowed in signatures
|
||||
# in this file. A good number of ACK packets have payloads.
|
||||
#
|
||||
# - PUSH flag is excluded from 'F' quirk checks in this mode.
|
||||
#
|
||||
# - 'A' quirk is not a bug; all AC packets should have it set; also,
|
||||
# 'T' quirk is not an anomaly; its absence on systems with T option is.
|
||||
#
|
||||
|
||||
32767:64:1:*:N,N,T:AT:Linux:2.4.2x (local?)
|
||||
*:64:1:*:.:A:Linux:2.4.2x
|
||||
32736:64:0:*:.:A:Linux:2.0.3x
|
||||
|
||||
57600:64:1:*:N,N,T:AT:FreeBSD:4.8
|
||||
%12:128:1:*:.:A:Windows:XP
|
||||
S44:128:1:*:.:A:Windows:XP
|
||||
193
decnet/prober/osfp/p0f/data/p0fr.fp
Normal file
193
decnet/prober/osfp/p0f/data/p0fr.fp
Normal file
@@ -0,0 +1,193 @@
|
||||
#
|
||||
# p0f - RST+ signatures
|
||||
# ---------------------
|
||||
#
|
||||
# .-------------------------------------------------------------------------.
|
||||
# | The purpose of this file is to cover signatures for reset packets |
|
||||
# | (RST and RST+ACK). This mode of operation can be enabled with -A option |
|
||||
# | and is considered to be least accurate. Please refer to p0f.fp for more |
|
||||
# | information on the metrics used and for a guide on adding new entries |
|
||||
# | to this file. This database is looking for a caring maintainer. |
|
||||
# `-------------------------------------------------------------------------'
|
||||
#
|
||||
# (C) Copyright 2000-2006 by Michal Zalewski <lcamtuf@coredump.cx>
|
||||
#
|
||||
# Submit all additions to the authors. Read p0f.fp before adding any
|
||||
# signatures. Run p0f -R -C after making any modifications. This file is
|
||||
# NOT compatible with SYN, SYN+ACK, or stray ACK modes. Use only with -R
|
||||
# option.
|
||||
#
|
||||
# IMPORTANT INFORMATION ABOUT THE INTERDEPENDENCY OF SYNs AND RST+ACKs
|
||||
# --------------------------------------------------------------------
|
||||
#
|
||||
# Some silly systems may copy WSS from the SYN packet you've sent,
|
||||
# in which case, you need to wildcard the value. Use test/sendsyn.c for
|
||||
# "connection refused" and test/sendack.c for "connection dropped" signatures
|
||||
# - both tools use a distinct WSS of 12345, which is an easy way to tell
|
||||
# if WSS should be wildcarded.
|
||||
#
|
||||
# IMPORTANT INFORMATION ABOUT COMMON IMPLEMENTATION FLAWS
|
||||
# -------------------------------------------------------
|
||||
#
|
||||
# There are several types of RST packets you will surely encounter.
|
||||
# Some systems, including most reputable ones, are severily brain-damaged
|
||||
# and generate some illegal combinations from time to time. This is WAY
|
||||
# more common than with other packet types, because a broken RST does not
|
||||
# have any immediately noticable consequences; besides, the RFC793 is fairly
|
||||
# difficult to comprehend when it comes to this type of responses.
|
||||
#
|
||||
# P0f will give you a hint on new RST signatures, but it is your duty to
|
||||
# diagnose the problem and append the proper description when adding the
|
||||
# signature. Below is a list of valid and invalid states:
|
||||
#
|
||||
# - "Connection refused" message: this is a RST+ACK packet, SEQ number
|
||||
# set to zero, ACK number non-zero. This is a valid response and
|
||||
# is denoted by p0f as "refused" (quirk combination: K, 0, A).
|
||||
#
|
||||
# There are some very cases when this is incorrectly sent in response
|
||||
# to an unexpected ACK packet.
|
||||
#
|
||||
# - Illegal combination: RST+ACK packet, SEQ number set to zero, ACK
|
||||
# number zero. This is denoted by p0f as "invalid-K0" (quirk combination:
|
||||
# K and 0, no A).
|
||||
#
|
||||
# - Illegal combination: RST+ACK, SEQ number non-zero, ACK number zero
|
||||
# or non-zero. This is denoted by p0f as "invalid-K" and
|
||||
# "invalid-KA", respectively (quirk combinations, K, sometimes A, no 0).
|
||||
#
|
||||
# This combination is frequently generated by Cisco routers in certain
|
||||
# configurations in response to ACK (!). Brain dead, by all means, and
|
||||
# usually a result of (incorrectly) setting ACK flag on a valid RST packet.
|
||||
#
|
||||
# - "Connection dropped": RST, sequence number non-zero, ACK zero or
|
||||
# non-zero. This is denoted as "dropped" and "dropped 2" respectively
|
||||
# (quirk combinations: no K, sometimes A, no 0). While the ACK value should
|
||||
# be zeroed, it is not strictly against the RFC, and some systems either
|
||||
# leak memory there or set it to the value of SEQ.
|
||||
#
|
||||
# The latter variant, with non-zero ACK, is particularly common on
|
||||
# Windows.
|
||||
#
|
||||
# - Ilegal combination: RST, SEQ number zero, ACK zero or non-zero.
|
||||
# Denoted as "invalid-0" and "invalid-0A". Obviously incorrect, and
|
||||
# will not have the desired effect.
|
||||
#
|
||||
# Ok. That's it. RFC793 does not get much respect nowadays.
|
||||
#
|
||||
# IMPORTANT INFORMATION ABOUT DIFFERENCES IN COMPARISON TO p0f.fp:
|
||||
# ----------------------------------------------------------------
|
||||
#
|
||||
# - Packet size may be wildcarded. The meaning of wildcard is, however,
|
||||
# hardcoded as 'size > PACKET_BIG' (defined as 100 in config.h). This is
|
||||
# because some stupid devices (including Ciscos) tend to send back RST
|
||||
# packets quoting anything you have sent them in ACK packet previously.
|
||||
# Use sparingly, only if -X confirms the device actually bounces back
|
||||
# whatever you send.
|
||||
#
|
||||
# - A new quirk, 'K', is introduced to denote RST+ACK packets (as opposed
|
||||
# to plain RST). This quirk is only compatible with this mode.
|
||||
#
|
||||
# - A new quirk, 'Q', is used to denote SEQ number equal to ACK number.
|
||||
# This happens from time to time in RST and RST+ACK packets, but
|
||||
# is practically unheard of in other modes.
|
||||
#
|
||||
# - A new quirk, '0', is used to denote packets with SEQ number set to 0.
|
||||
# This happens on some RSTs, and is once again unheard of in other modes.
|
||||
#
|
||||
# - 'D' quirk is not a bug; some devices send verbose text messages
|
||||
# describing why a connection got dropped; it's actually suggested
|
||||
# by RFC1122. Of course, some systems have their own standards, and
|
||||
# put all kinds of crap in their RST responses (including FreeBSD and
|
||||
# Cisco). Use -X to examine those values.
|
||||
#
|
||||
# - 'A' and 'T' quirks are not an anomaly in certain cases for the reasons
|
||||
# described in p0fa.fp.
|
||||
#
|
||||
|
||||
################################
|
||||
# Connection refused - RST+ACK #
|
||||
################################
|
||||
|
||||
0:255:0:40:.:K0A:Linux:2.0/2.2 (refused)
|
||||
0:64:1:40:.:K0A:FreeBSD:4.8 (refused)
|
||||
0:64:1:40:.:K0ZA:Linux:recent 2.4 (refused)
|
||||
|
||||
0:128:0:40:.:K0A:Windows:XP/2000 (refused)
|
||||
0:128:0:40:.:K0UA:-Windows:XP/2000 while browsing (refused)
|
||||
|
||||
######################################
|
||||
# Connection dropped / timeout - RST #
|
||||
######################################
|
||||
|
||||
0:64:1:40:.:.:FreeBSD:4.8 (dropped)
|
||||
0:255:0:40:.:.:Linux:2.0/2.2 or IOS 12.x (dropped)
|
||||
0:64:1:40:.:Z:Linux:recent 2.4 (dropped)
|
||||
0:255:1:40:.:Z:Linux:early 2.4 (dropped)
|
||||
0:32:0:40:.:.:Xylan:OmniSwitch / Linksys WAP11 AP (dropped)
|
||||
0:64:1:40:.:U:NetIron:load balancer (dropped)
|
||||
|
||||
0:128:1:40:.:QA:Windows:XP/2000 (dropped 2)
|
||||
0:128:1:40:.:A:-Windows:XP/2000 while browsing (1) (dropped 2)
|
||||
0:128:1:40:.:QUA:-Windows:XP/2000 while browsing (2) (dropped 2)
|
||||
0:128:1:40:.:UA:-Windows:XP/2000 while browsing a lot (dropped 2)
|
||||
0:128:1:40:.:.:@Windows:98 (?) (dropped)
|
||||
|
||||
0:64:0:40:.:A:Ascend:TAOS or BayTech (dropped 2)
|
||||
|
||||
*:255:0:40:.:QA:Cisco:LocalDirector (dropped 2)
|
||||
|
||||
0:64:1:40:.:A:Hasbani:WindWeb (dropped 2)
|
||||
S23:255:1:40:.:.:Solaris:2.5 (dropped)
|
||||
|
||||
#######################################################
|
||||
# Connection dropped / timeout - RST with description #
|
||||
#######################################################
|
||||
|
||||
0:255:1:58:.:D:MacOS:9.x "No TCP/No listener" (seldom SunOS 5.x) (dropped)
|
||||
0:255:1:53:.:D:MacOS:8.5 "no tcp, reset" (dropped)
|
||||
0:255:1:65:.:D:MacOS:X "tcp_close, during connect" (dropped)
|
||||
0:255:1:54:.:D:MacOS:X "tcp_disconnect" (dropped)
|
||||
0:255:1:62:.:D:HP/UX:? "tcp_fin_wait_2_timeout" (dropped)
|
||||
32768:255:1:54:.:D:MacOS:8.5 "tcp_disconnect" (dropped)
|
||||
0:255:1:63:.:D:@Unknown: "Go away" device (dropped)
|
||||
|
||||
0:255:0:62:.:D:SunOS:5.x "new data when detached" (1) (dropped)
|
||||
32768:255:1:62:.:D:SunOS:5.x "new data when detached" (2) (dropped)
|
||||
0:255:1:67:.:D:SunOS:5.x "tcp_lift_anchor, can't wait" (dropped)
|
||||
|
||||
0:255:0:46:.:D:HP/UX:11.00 "No TCP" (dropped)
|
||||
|
||||
# More obscure ones:
|
||||
# 648:255:1:54:.:D:MacOS:??? "tcp_disconnect" (dropped)
|
||||
# 0:45:1:53:.:D:MacOS:7.x "no tcp, reset" (dropped)
|
||||
|
||||
##############################################
|
||||
# Connection dropped / timeout - broken RSTs #
|
||||
##############################################
|
||||
|
||||
S12:255:1:58:.:KAD:Solaris:2.x "tcp_disconnect" (dropped, lame)
|
||||
S43:64:1:40:.:KA:AOL:proxy (dropped, lame)
|
||||
*:64:1:40:.:KA:FreeBSD:4.8 (dropped, lame)
|
||||
*:64:1:52:N,N,T:KAT:Linux:2.4 (?) (dropped, lame)
|
||||
0:255:0:40:.:KAF:3Com:SuperStack II (dropped, lame)
|
||||
*:255:0:40:.:KA:Intel:Netport print server (dropped, lame)
|
||||
*:150:0:40:.:KA:Linksys:BEF router (dropped, lame)
|
||||
|
||||
*:32:0:44:.:KZD:@NetWare:??? "ehnc" (dropped, lame)
|
||||
0:64:0:40:.:KQ0:BayTech:RPC-3 telnet host (dropped, lame)
|
||||
|
||||
#############################################
|
||||
# Connection dropped / timeout - extra data #
|
||||
#############################################
|
||||
|
||||
*:255:0:*:.:KAD:Cisco:IOS/PIX NAT + data (1) (dropped, lame)
|
||||
0:255:0:*:.:D:Windows:NT 4.0 SP6a + data (dropped)
|
||||
0:255:0:*:.:K0AD:Isolation:Infocrypt accelerator + data (dropped, lame)
|
||||
|
||||
*:255:0:*:.:AD:Cisco:IOS/PIX NAT + data (2) (dropped)
|
||||
|
||||
*:64:1:*:N,N,T:KATD:Linux:2.4 (?) + data (dropped, lame)
|
||||
*:64:1:*:.:KAD:FreeBSD:4.8 + data (dropped, lame)
|
||||
|
||||
|
||||
|
||||
243
decnet/prober/osfp/p0f/format.py
Normal file
243
decnet/prober/osfp/p0f/format.py
Normal file
@@ -0,0 +1,243 @@
|
||||
"""p0f v2 ``.fp`` file parser.
|
||||
|
||||
Format (from the DSL spec at the top of every shipped ``.fp`` file):
|
||||
|
||||
wwww:ttt:D:ss:OOO:QQ:OS:Details
|
||||
|
||||
Where:
|
||||
wwww — window size: literal int | '*' | '%nnn' | 'Snn' | 'Tnn'
|
||||
ttt — initial TTL (literal int: 32/64/128/255 typically)
|
||||
D — DF bit: '0' or '1'
|
||||
ss — total IP packet length: literal int | '*' | '%nnn'
|
||||
OOO — option order: comma/space-separated tokens, or '.' for none.
|
||||
Tokens: N, E, S, T, T0, P, Wnnn/W*/W%nnn, Mnnn/M*/M%nnn, ?n
|
||||
QQ — quirks: concatenated single-letter flags, or '.' for none.
|
||||
Flags: P, Z, I, U, X, A, T, F, D, !, K, Q, 0, R
|
||||
OS — genre, optionally prefixed '-' (userland), '@' (group),
|
||||
'*' (random/bogus), or combinations (e.g. '-@Windows').
|
||||
Details — free-text flavor/version.
|
||||
|
||||
Lines starting with '#' and blank lines are skipped.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from decnet.prober.osfp.p0f.signature import (
|
||||
IntSpec,
|
||||
OptionToken,
|
||||
Signature,
|
||||
WindowSpec,
|
||||
precompute_specificity,
|
||||
)
|
||||
|
||||
logger = logging.getLogger("decnet.prober.osfp.p0f.format")
|
||||
|
||||
_OPTION_TOKEN_RE = re.compile(r"^([NESTPE]|T0|[MW\?])(\*|%\d+|\d+)?$")
|
||||
|
||||
|
||||
class P0fParseError(ValueError):
|
||||
"""Raised on genuinely malformed signature lines. The loader
|
||||
catches these and skips the offending line with a logger warning —
|
||||
one bad row doesn't disable the whole DB."""
|
||||
|
||||
|
||||
def parse_p0f_v2(path: Path) -> list[Signature]:
|
||||
"""Parse a p0f v2 ``.fp`` file and return a list of Signatures.
|
||||
|
||||
Malformed lines are logged at WARNING and skipped rather than
|
||||
aborting the whole load — the vendored DB has ~375 entries and one
|
||||
corrupt row shouldn't prevent the other 374 from being usable.
|
||||
"""
|
||||
out: list[Signature] = []
|
||||
with path.open("r", encoding="utf-8", errors="replace") as fh:
|
||||
for lineno, raw in enumerate(fh, 1):
|
||||
line = raw.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
try:
|
||||
sig = _parse_line(line)
|
||||
except P0fParseError as exc:
|
||||
logger.warning(
|
||||
"p0f parse: skipping %s:%d — %s", path.name, lineno, exc,
|
||||
)
|
||||
continue
|
||||
out.append(sig)
|
||||
logger.debug("p0f parse: loaded %d signatures from %s", len(out), path.name)
|
||||
return out
|
||||
|
||||
|
||||
def _parse_line(line: str) -> Signature:
|
||||
parts = line.split(":", 7)
|
||||
if len(parts) < 7:
|
||||
raise P0fParseError(f"expected 7+ colon-delimited fields, got {len(parts)}")
|
||||
if len(parts) == 7:
|
||||
parts = [*parts, ""] # empty details
|
||||
wss_s, ttl_s, df_s, tot_s, opts_s, quirks_s, os_s, details = parts
|
||||
|
||||
wss = _parse_wss(wss_s)
|
||||
ttl = _parse_int_field(ttl_s, field="ttl")
|
||||
df = _parse_df(df_s)
|
||||
total_len = _parse_int_spec(tot_s)
|
||||
options = _parse_options(opts_s)
|
||||
quirks = _parse_quirks(quirks_s)
|
||||
os_name, is_userland, is_approx, is_random = _parse_os_genre(os_s)
|
||||
|
||||
sig = Signature(
|
||||
wss=wss,
|
||||
ttl=ttl,
|
||||
df=df,
|
||||
total_len=total_len,
|
||||
options=options,
|
||||
quirks=quirks,
|
||||
os=os_name,
|
||||
flavor=details.strip(),
|
||||
notes="",
|
||||
is_userland=is_userland,
|
||||
is_approximate=is_approx,
|
||||
is_random=is_random,
|
||||
)
|
||||
# Replace specificity (frozen dataclass field default) with the
|
||||
# computed value via dataclasses.replace.
|
||||
from dataclasses import replace
|
||||
return replace(sig, specificity=precompute_specificity(sig))
|
||||
|
||||
|
||||
def _parse_wss(s: str) -> WindowSpec:
|
||||
s = s.strip()
|
||||
if s == "*":
|
||||
return WindowSpec("any")
|
||||
if s.startswith("%"):
|
||||
try:
|
||||
return WindowSpec("mod", int(s[1:]))
|
||||
except ValueError as exc:
|
||||
raise P0fParseError(f"bad mod window {s!r}") from exc
|
||||
if s.startswith("S"):
|
||||
try:
|
||||
return WindowSpec("mss_mul", int(s[1:]))
|
||||
except ValueError as exc:
|
||||
raise P0fParseError(f"bad Snn window {s!r}") from exc
|
||||
if s.startswith("T"):
|
||||
try:
|
||||
return WindowSpec("mtu_mul", int(s[1:]))
|
||||
except ValueError as exc:
|
||||
raise P0fParseError(f"bad Tnn window {s!r}") from exc
|
||||
try:
|
||||
return WindowSpec("literal", int(s))
|
||||
except ValueError as exc:
|
||||
raise P0fParseError(f"bad literal window {s!r}") from exc
|
||||
|
||||
|
||||
def _parse_int_field(s: str, *, field: str) -> int:
|
||||
"""Parse a bare int field (used for TTL). No wildcards allowed."""
|
||||
try:
|
||||
return int(s.strip())
|
||||
except ValueError as exc:
|
||||
raise P0fParseError(f"bad {field}: {s!r}") from exc
|
||||
|
||||
|
||||
def _parse_df(s: str) -> Optional[bool]:
|
||||
s = s.strip()
|
||||
if s == "*":
|
||||
return None
|
||||
if s == "0":
|
||||
return False
|
||||
if s == "1":
|
||||
return True
|
||||
raise P0fParseError(f"bad DF {s!r}; expected 0/1/*")
|
||||
|
||||
|
||||
def _parse_int_spec(s: str) -> IntSpec:
|
||||
s = s.strip()
|
||||
if s == "*":
|
||||
return IntSpec("any")
|
||||
if s.startswith("%"):
|
||||
try:
|
||||
return IntSpec("mod", int(s[1:]))
|
||||
except ValueError as exc:
|
||||
raise P0fParseError(f"bad mod int {s!r}") from exc
|
||||
try:
|
||||
return IntSpec("literal", int(s))
|
||||
except ValueError as exc:
|
||||
raise P0fParseError(f"bad literal int {s!r}") from exc
|
||||
|
||||
|
||||
def _parse_options(s: str) -> tuple[OptionToken, ...]:
|
||||
s = s.strip()
|
||||
if s in (".", ""):
|
||||
return (OptionToken("."),)
|
||||
normalized = s.replace(",", " ")
|
||||
tokens: list[OptionToken] = []
|
||||
for raw in normalized.split():
|
||||
tok = raw.strip()
|
||||
if not tok:
|
||||
continue
|
||||
tokens.append(_parse_option_token(tok))
|
||||
if not tokens:
|
||||
return (OptionToken("."),)
|
||||
return tuple(tokens)
|
||||
|
||||
|
||||
def _parse_option_token(raw: str) -> OptionToken:
|
||||
# T0 — timestamp zero (not the TCP option '?0').
|
||||
if raw == "T0":
|
||||
return OptionToken("T0")
|
||||
m = _OPTION_TOKEN_RE.match(raw)
|
||||
if not m:
|
||||
raise P0fParseError(f"bad option token {raw!r}")
|
||||
kind, val_raw = m.group(1), m.group(2)
|
||||
if kind in ("N", "E", "S", "T", "P"):
|
||||
return OptionToken(kind)
|
||||
# M / W / ? expect a numeric predicate (or wildcard).
|
||||
if val_raw is None:
|
||||
raise P0fParseError(f"option {kind!r} missing required value")
|
||||
if val_raw == "*":
|
||||
spec = IntSpec("any")
|
||||
elif val_raw.startswith("%"):
|
||||
try:
|
||||
spec = IntSpec("mod", int(val_raw[1:]))
|
||||
except ValueError as exc:
|
||||
raise P0fParseError(f"bad {kind} mod value {val_raw!r}") from exc
|
||||
else:
|
||||
try:
|
||||
spec = IntSpec("literal", int(val_raw))
|
||||
except ValueError as exc:
|
||||
raise P0fParseError(f"bad {kind} literal value {val_raw!r}") from exc
|
||||
return OptionToken(kind, spec)
|
||||
|
||||
|
||||
def _parse_quirks(s: str) -> frozenset[str]:
|
||||
s = s.strip()
|
||||
if s == "." or not s:
|
||||
return frozenset()
|
||||
# Quirks are a concatenated string of single-letter flags. '!' is a
|
||||
# valid quirk too.
|
||||
return frozenset(c for c in s if not c.isspace())
|
||||
|
||||
|
||||
def _parse_os_genre(s: str) -> tuple[str, bool, bool, bool]:
|
||||
"""Strip p0f's genre-prefix modifiers and return (os_name, is_userland, is_approx, is_random)."""
|
||||
is_userland = False
|
||||
is_approx = False
|
||||
is_random = False
|
||||
s = s.strip()
|
||||
# Prefixes can stack in any order — strip them all.
|
||||
changed = True
|
||||
while changed and s:
|
||||
changed = False
|
||||
if s.startswith("-"):
|
||||
is_userland = True
|
||||
s = s[1:]
|
||||
changed = True
|
||||
elif s.startswith("@"):
|
||||
is_approx = True
|
||||
s = s[1:]
|
||||
changed = True
|
||||
elif s.startswith("*"):
|
||||
is_random = True
|
||||
s = s[1:]
|
||||
changed = True
|
||||
return s, is_userland, is_approx, is_random
|
||||
109
decnet/prober/osfp/p0f/provider.py
Normal file
109
decnet/prober/osfp/p0f/provider.py
Normal file
@@ -0,0 +1,109 @@
|
||||
"""p0f v2 Provider — loads the vendored .fp databases and matches
|
||||
against observed TCP quirks.
|
||||
|
||||
Four databases ship under ``data/``:
|
||||
|
||||
p0f.fp — SYN fingerprints (passive / sniffer-captured inbound).
|
||||
p0fa.fp — SYN-ACK fingerprints (prober active-probe responses).
|
||||
p0fr.fp — RST+ fingerprints (reset-response quirks).
|
||||
p0fo.fp — "stray" packet fingerprints.
|
||||
|
||||
The provider routes incoming observations to the right sig list based
|
||||
on ``obs["context"]`` — see :meth:`P0fV2Provider.match` — and returns
|
||||
the highest-specificity matching :class:`OsMatch` or ``None``.
|
||||
|
||||
DECNET-authored additions can land in ``p0f-decnet.fp`` (same
|
||||
directory, loaded if present) under GPL-3.0. None exist today — the
|
||||
plan deferred writing any to a later commit — but the provider
|
||||
already picks it up when it appears.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
from decnet.prober.osfp.base import OsMatch, Provider
|
||||
from decnet.prober.osfp.p0f.format import parse_p0f_v2
|
||||
from decnet.prober.osfp.p0f.signature import Signature
|
||||
|
||||
logger = logging.getLogger("decnet.prober.osfp.p0f.provider")
|
||||
|
||||
|
||||
# Directory containing the vendored .fp files.
|
||||
_DATA_DIR: Path = Path(__file__).resolve().parent / "data"
|
||||
|
||||
# Which .fp files feed each observation context.
|
||||
_CONTEXT_DBS: dict[str, tuple[str, ...]] = {
|
||||
"syn": ("p0f.fp", "p0f-decnet.fp"),
|
||||
"synack": ("p0fa.fp",),
|
||||
"rst": ("p0fr.fp",),
|
||||
"stray": ("p0fo.fp",),
|
||||
}
|
||||
|
||||
|
||||
class P0fV2Provider(Provider):
|
||||
"""Match observations against the p0f v2 database."""
|
||||
|
||||
name = "p0f-v2"
|
||||
|
||||
def __init__(self, data_dir: Optional[Path] = None) -> None:
|
||||
self._data_dir = (data_dir or _DATA_DIR).resolve()
|
||||
self._sigs_by_context: dict[str, list[Signature]] = {}
|
||||
self._load()
|
||||
|
||||
def _load(self) -> None:
|
||||
for context, filenames in _CONTEXT_DBS.items():
|
||||
merged: list[Signature] = []
|
||||
for name in filenames:
|
||||
path = self._data_dir / name
|
||||
if not path.is_file():
|
||||
# p0f-decnet.fp is optional; all others are required.
|
||||
if name.startswith("p0f-decnet"):
|
||||
continue
|
||||
logger.warning("p0f-v2: missing required DB file %s", path)
|
||||
continue
|
||||
try:
|
||||
merged.extend(parse_p0f_v2(path))
|
||||
except OSError as exc:
|
||||
logger.warning("p0f-v2: could not load %s: %s", path, exc)
|
||||
self._sigs_by_context[context] = merged
|
||||
logger.debug("p0f-v2: %s context loaded %d signatures", context, len(merged))
|
||||
|
||||
def match(self, obs: dict[str, Any]) -> Optional[OsMatch]:
|
||||
"""Return the highest-specificity matching signature, or None.
|
||||
|
||||
``obs["context"]`` selects the DB slice; default is "syn"
|
||||
(passive observation, which is 80%+ of the event stream).
|
||||
Invalid contexts return None rather than raising.
|
||||
"""
|
||||
context = obs.get("context", "syn")
|
||||
sigs = self._sigs_by_context.get(context)
|
||||
if not sigs:
|
||||
return None
|
||||
|
||||
best: tuple[float, Signature] | None = None
|
||||
for sig in sigs:
|
||||
score = sig.score(obs)
|
||||
if score is None:
|
||||
continue
|
||||
if best is None or score > best[0]:
|
||||
best = (score, sig)
|
||||
# Short-circuit on a perfect match — can't beat 1.0.
|
||||
if best[0] >= 1.0:
|
||||
break
|
||||
|
||||
if best is None:
|
||||
return None
|
||||
score, sig = best
|
||||
return OsMatch(
|
||||
os=sig.os,
|
||||
flavor=sig.flavor,
|
||||
confidence=score,
|
||||
provider=self.name,
|
||||
is_userland=sig.is_userland,
|
||||
)
|
||||
|
||||
def signature_counts(self) -> dict[str, int]:
|
||||
"""For diagnostics / tests — how many sigs loaded per context."""
|
||||
return {ctx: len(sigs) for ctx, sigs in self._sigs_by_context.items()}
|
||||
287
decnet/prober/osfp/p0f/signature.py
Normal file
287
decnet/prober/osfp/p0f/signature.py
Normal file
@@ -0,0 +1,287 @@
|
||||
"""p0f v2 signature + observation matching/scoring.
|
||||
|
||||
A :class:`Signature` is one parsed row from a ``.fp`` file. A match
|
||||
against an observation dict (the kind ``sniffer_rollup`` hands us)
|
||||
returns a confidence score in [0, 1], with higher scores indicating
|
||||
more-specific matches. Wildcards and modulo predicates match but
|
||||
contribute less to the confidence than an exact literal match, so
|
||||
when multiple signatures fire against one observation we can pick the
|
||||
most-specific one.
|
||||
|
||||
Observation dict shape (all keys optional — a provider returns None
|
||||
if too few match-relevant fields are present):
|
||||
|
||||
{
|
||||
"window": int | None, # TCP window size
|
||||
"mss": int | None, # TCP MSS option value
|
||||
"wscale": int | None, # TCP window-scale option value
|
||||
"ttl": int | None, # initial-TTL bucket (32/64/128/255)
|
||||
"df": bool | None, # IP Don't-Fragment flag
|
||||
"total_len": int | None, # IP total length (SYN)
|
||||
"options_sig": str | None, # e.g. "M,N,W,T" or "M1460,N,W7,S"
|
||||
"quirks": frozenset[str] | None, # e.g. {"Z", "P"}
|
||||
}
|
||||
|
||||
The scoring is our extension — upstream p0f is "first match wins"
|
||||
using the order of entries in ``.fp``. We score so the factory can
|
||||
compare across multiple DB files (p0f.fp + p0fa.fp) and return the
|
||||
winner objectively.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Optional
|
||||
|
||||
|
||||
# ─── Field predicates ──────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WindowSpec:
|
||||
"""Parsed 'wss' field. Encodes p0f v2's window-size predicate DSL:
|
||||
|
||||
- 'literal' → observed window == value
|
||||
- 'mss_mul' → observed window == MSS * value (p0f "Snn")
|
||||
- 'mtu_mul' → observed window == (MSS+40) * value (p0f "Tnn")
|
||||
- 'mod' → observed window % value == 0 (p0f "%nnn")
|
||||
- 'any' → wildcard (p0f "*")
|
||||
"""
|
||||
|
||||
kind: str
|
||||
value: Optional[int] = None
|
||||
|
||||
def matches(self, window: Optional[int], mss: Optional[int]) -> bool:
|
||||
if self.kind == "any":
|
||||
return True
|
||||
if window is None:
|
||||
return False
|
||||
if self.kind == "literal":
|
||||
return window == self.value
|
||||
if self.kind == "mod":
|
||||
return self.value is not None and self.value > 0 and (window % self.value == 0)
|
||||
if self.kind == "mss_mul":
|
||||
return mss is not None and self.value is not None and window == mss * self.value
|
||||
if self.kind == "mtu_mul":
|
||||
return mss is not None and self.value is not None and window == (mss + 40) * self.value
|
||||
return False
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class IntSpec:
|
||||
"""Wildcard-or-modulo int predicate, used for MSS / wscale / total_len."""
|
||||
|
||||
kind: str # 'literal' | 'mod' | 'any'
|
||||
value: Optional[int] = None
|
||||
|
||||
def matches(self, observed: Optional[int]) -> bool:
|
||||
if self.kind == "any":
|
||||
return True
|
||||
if observed is None:
|
||||
return False
|
||||
if self.kind == "literal":
|
||||
return observed == self.value
|
||||
if self.kind == "mod":
|
||||
return self.value is not None and self.value > 0 and (observed % self.value == 0)
|
||||
return False
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class OptionToken:
|
||||
"""One TCP option as it appears in a signature's options list.
|
||||
|
||||
- kind='N' EOL 'E' SACK-permitted 'S' timestamp 'T' zero-timestamp 'T0'
|
||||
- kind='M' MSS option, value = IntSpec
|
||||
- kind='W' window-scale option, value = IntSpec
|
||||
- kind='?' unknown option number, value = IntSpec (literal = option number)
|
||||
- kind='.' no-options sentinel (singleton — matches only empty option list)
|
||||
"""
|
||||
|
||||
kind: str
|
||||
value: Optional[IntSpec] = None
|
||||
|
||||
def matches_literal(self, token: "OptionToken") -> bool:
|
||||
"""True when *this* signature token matches an observed *token*.
|
||||
|
||||
Signature-side carries the wildcard/modulo predicate; observed
|
||||
side is always a literal (or kind-only for flag options).
|
||||
"""
|
||||
if self.kind != token.kind:
|
||||
return False
|
||||
if self.value is None:
|
||||
return True
|
||||
if token.value is None:
|
||||
return False
|
||||
# Both have IntSpecs — match via predicate.
|
||||
return self.value.matches(token.value.value)
|
||||
|
||||
|
||||
# ─── Signature ─────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Signature:
|
||||
"""One parsed row from a p0f v2 .fp file.
|
||||
|
||||
``label_prefix`` captures p0f's os-genre modifiers:
|
||||
- ``-`` userland stack (not a real OS; flagged scanner/browser)
|
||||
- ``@`` approximate / group match
|
||||
- ``*`` random or bogus userland
|
||||
These prefixes are stripped from ``os``; the flags survive here
|
||||
for the profiler to decide e.g. "do I promote nmap to tool_guesses?"
|
||||
"""
|
||||
|
||||
wss: WindowSpec
|
||||
ttl: int
|
||||
df: Optional[bool]
|
||||
total_len: IntSpec
|
||||
options: tuple[OptionToken, ...] # in order; use (OptionToken('.'),) for none
|
||||
quirks: frozenset[str]
|
||||
os: str
|
||||
flavor: str
|
||||
notes: str
|
||||
is_userland: bool = False # '-' prefix
|
||||
is_approximate: bool = False # '@' prefix
|
||||
is_random: bool = False # '*' prefix (distinct from wildcard)
|
||||
|
||||
# Cache: a crude "specificity budget" precomputed at parse time.
|
||||
# Higher = more constrained fields, used as a tie-breaker when two
|
||||
# signatures match the same observation.
|
||||
specificity: float = field(default=0.0)
|
||||
|
||||
def score(self, obs: dict[str, Any]) -> Optional[float]:
|
||||
"""Return a confidence in [0, 1] on match, or None if any field
|
||||
rejects the observation.
|
||||
|
||||
Soft-field semantics: ``df`` and ``total_len`` are treated as
|
||||
"skip check when observation is missing" — the sniffer doesn't
|
||||
currently emit either, and a literal-constraint sig shouldn't
|
||||
reject a match solely because the observation is upstream-
|
||||
incomplete. Hard fields (``window``, ``ttl``, ``options_sig``,
|
||||
``quirks``) still hard-reject on absent or mismatched input —
|
||||
those are the real discriminators."""
|
||||
mss = obs.get("mss")
|
||||
# Window (hard)
|
||||
if not self.wss.matches(obs.get("window"), mss):
|
||||
return None
|
||||
# TTL — initial-TTL bucket must match exactly. The profiler is
|
||||
# expected to have rounded the observed TTL up to the nearest
|
||||
# bucket already via decnet.sniffer.p0f.initial_ttl. (hard)
|
||||
obs_ttl = obs.get("ttl")
|
||||
if obs_ttl is None or obs_ttl != self.ttl:
|
||||
return None
|
||||
# DF (soft — skip when unknown)
|
||||
if self.df is not None:
|
||||
obs_df = obs.get("df")
|
||||
if obs_df is not None and bool(obs_df) != self.df:
|
||||
return None
|
||||
# Total length (soft — skip when unknown)
|
||||
obs_total = obs.get("total_len")
|
||||
if obs_total is not None and not self.total_len.matches(obs_total):
|
||||
return None
|
||||
# Options (hard)
|
||||
if not _options_match(self.options, obs.get("options_sig")):
|
||||
return None
|
||||
# Quirks — must match as a set. (hard)
|
||||
obs_quirks = obs.get("quirks") or frozenset()
|
||||
if not isinstance(obs_quirks, frozenset):
|
||||
obs_quirks = frozenset(obs_quirks)
|
||||
if self.quirks != obs_quirks:
|
||||
return None
|
||||
# All fields matched — return the precomputed specificity.
|
||||
return self.specificity
|
||||
|
||||
|
||||
def _options_match(sig_opts: tuple[OptionToken, ...], obs_sig: Optional[str]) -> bool:
|
||||
"""Match signature option sequence against observation's comma/space-
|
||||
separated option string."""
|
||||
obs_tokens = _parse_observation_options(obs_sig)
|
||||
# Special case: signature is '.' (no-options sentinel).
|
||||
if len(sig_opts) == 1 and sig_opts[0].kind == ".":
|
||||
return len(obs_tokens) == 0
|
||||
if len(sig_opts) != len(obs_tokens):
|
||||
return False
|
||||
return all(s.matches_literal(o) for s, o in zip(sig_opts, obs_tokens))
|
||||
|
||||
|
||||
_OBS_TOKEN_RE = re.compile(r"^([A-Z\?])(\d+)?$")
|
||||
|
||||
|
||||
def _parse_observation_options(opts_sig: Optional[str]) -> list[OptionToken]:
|
||||
"""Convert the observation-side options string (from
|
||||
tcp_syn_fingerprint / tcpfp_fingerprint SD fields) into a list of
|
||||
literal OptionTokens. Accepts comma or space delimiters and tokens
|
||||
like 'M1460', 'W7', 'T', 'T0', 'N', 'E', '?47'.
|
||||
"""
|
||||
if not opts_sig:
|
||||
return []
|
||||
normalized = opts_sig.replace(",", " ")
|
||||
out: list[OptionToken] = []
|
||||
for raw in normalized.split():
|
||||
token = raw.strip()
|
||||
if not token:
|
||||
continue
|
||||
if token == "T0": # nosec B105 — TCP option name ("Timestamp zero"), not a credential
|
||||
out.append(OptionToken("T0"))
|
||||
continue
|
||||
m = _OBS_TOKEN_RE.match(token)
|
||||
if not m:
|
||||
# Unknown token — represent as opaque "?" with no value so
|
||||
# nothing matches it. Better than raising.
|
||||
out.append(OptionToken("?", IntSpec("literal", -1)))
|
||||
continue
|
||||
kind, num = m.group(1), m.group(2)
|
||||
if num is None:
|
||||
out.append(OptionToken(kind))
|
||||
else:
|
||||
out.append(OptionToken(kind, IntSpec("literal", int(num))))
|
||||
return out
|
||||
|
||||
|
||||
def precompute_specificity(sig: Signature) -> float:
|
||||
"""Crude specificity score used when comparing matching signatures.
|
||||
|
||||
Each field contributes a weight; wildcards and modulo predicates
|
||||
contribute less. Tuned so a fully-literal signature scores ~1.0 and
|
||||
a near-wildcard signature scores ~0.1.
|
||||
"""
|
||||
w = 0.0
|
||||
total = 0.0
|
||||
# Window (weight 3 — very discriminating)
|
||||
total += 3
|
||||
if sig.wss.kind == "literal":
|
||||
w += 3.0
|
||||
elif sig.wss.kind in ("mss_mul", "mtu_mul"):
|
||||
w += 2.5
|
||||
elif sig.wss.kind == "mod":
|
||||
w += 1.5
|
||||
# TTL — always literal, contributes a flat 1
|
||||
total += 1
|
||||
w += 1.0
|
||||
# DF (weight 1)
|
||||
total += 1
|
||||
if sig.df is not None:
|
||||
w += 1.0
|
||||
# Total length (weight 1)
|
||||
total += 1
|
||||
if sig.total_len.kind == "literal":
|
||||
w += 1.0
|
||||
elif sig.total_len.kind == "mod":
|
||||
w += 0.5
|
||||
# Options (weight 3 — highly discriminating when literal)
|
||||
total += 3
|
||||
if not (len(sig.options) == 1 and sig.options[0].kind == "."):
|
||||
literal_opts = sum(
|
||||
1 for o in sig.options
|
||||
if o.value is None or o.value.kind == "literal"
|
||||
)
|
||||
if sig.options:
|
||||
w += 3.0 * (literal_opts / len(sig.options))
|
||||
else:
|
||||
# "no options" is itself a signal.
|
||||
w += 2.0
|
||||
# Quirks (weight 1 — most sigs have no quirks so this is a small edge)
|
||||
total += 1
|
||||
if sig.quirks:
|
||||
w += 1.0
|
||||
return round(w / total, 4)
|
||||
239
decnet/prober/tcpfp.py
Normal file
239
decnet/prober/tcpfp.py
Normal file
@@ -0,0 +1,239 @@
|
||||
"""
|
||||
TCP/IP stack fingerprinting via SYN-ACK analysis.
|
||||
|
||||
Sends a crafted TCP SYN packet to a target host:port, captures the
|
||||
SYN-ACK response, and extracts OS/tool-identifying characteristics:
|
||||
TTL, window size, DF bit, MSS, window scale, SACK support, timestamps,
|
||||
and TCP options ordering.
|
||||
|
||||
Uses scapy for packet crafting and parsing. Requires root/CAP_NET_RAW.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import random
|
||||
from typing import Any
|
||||
|
||||
from decnet.telemetry import traced as _traced
|
||||
|
||||
# Lazy-import scapy to avoid breaking non-root usage of HASSH/JARM.
|
||||
# The actual import happens inside functions that need it.
|
||||
|
||||
# ─── TCP option short codes ─────────────────────────────────────────────────
|
||||
|
||||
_OPT_CODES: dict[str, str] = {
|
||||
"MSS": "M",
|
||||
"WScale": "W",
|
||||
"SAckOK": "S",
|
||||
"SAck": "S",
|
||||
"Timestamp": "T",
|
||||
"NOP": "N",
|
||||
"EOL": "E",
|
||||
"AltChkSum": "A",
|
||||
"AltChkSumOpt": "A",
|
||||
"UTO": "U",
|
||||
}
|
||||
|
||||
|
||||
# ─── Packet construction ───────────────────────────────────────────────────
|
||||
|
||||
@_traced("prober.tcpfp_send_syn")
|
||||
def _send_syn(
|
||||
host: str,
|
||||
port: int,
|
||||
timeout: float,
|
||||
) -> Any | None:
|
||||
"""
|
||||
Craft a TCP SYN with common options and send it. Returns the
|
||||
SYN-ACK response packet or None on timeout/failure.
|
||||
"""
|
||||
from scapy.all import IP, TCP, conf, sr1
|
||||
|
||||
# Suppress scapy's noisy output
|
||||
conf.verb = 0
|
||||
|
||||
src_port = random.randint(49152, 65535) # nosec B311 — ephemeral port, not crypto
|
||||
|
||||
pkt = (
|
||||
IP(dst=host)
|
||||
/ TCP(
|
||||
sport=src_port,
|
||||
dport=port,
|
||||
flags="S",
|
||||
options=[
|
||||
("MSS", 1460),
|
||||
("NOP", None),
|
||||
("WScale", 7),
|
||||
("NOP", None),
|
||||
("NOP", None),
|
||||
("Timestamp", (0, 0)),
|
||||
("SAckOK", b""),
|
||||
("EOL", None),
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
resp = sr1(pkt, timeout=timeout, verbose=0)
|
||||
except (OSError, PermissionError):
|
||||
return None
|
||||
|
||||
if resp is None:
|
||||
return None
|
||||
|
||||
# Verify it's a SYN-ACK (flags == 0x12)
|
||||
from scapy.all import TCP as TCPLayer
|
||||
if not resp.haslayer(TCPLayer):
|
||||
return None
|
||||
if resp[TCPLayer].flags != 0x12: # SYN-ACK
|
||||
return None
|
||||
|
||||
# Send RST to clean up half-open connection
|
||||
_send_rst(host, port, src_port, resp)
|
||||
|
||||
return resp
|
||||
|
||||
|
||||
def _send_rst(
|
||||
host: str,
|
||||
dport: int,
|
||||
sport: int,
|
||||
resp: Any,
|
||||
) -> None:
|
||||
"""Send RST to clean up the half-open connection."""
|
||||
try:
|
||||
from scapy.all import IP, TCP, send
|
||||
rst = (
|
||||
IP(dst=host)
|
||||
/ TCP(
|
||||
sport=sport,
|
||||
dport=dport,
|
||||
flags="R",
|
||||
seq=resp.ack,
|
||||
)
|
||||
)
|
||||
send(rst, verbose=0)
|
||||
except Exception: # nosec B110 — best-effort RST cleanup
|
||||
pass
|
||||
|
||||
|
||||
# ─── Response parsing ───────────────────────────────────────────────────────
|
||||
|
||||
def _parse_synack(resp: Any) -> dict[str, Any]:
|
||||
"""
|
||||
Extract fingerprint fields from a scapy SYN-ACK response packet.
|
||||
"""
|
||||
from scapy.all import IP, TCP
|
||||
|
||||
ip_layer = resp[IP]
|
||||
tcp_layer = resp[TCP]
|
||||
|
||||
# IP fields
|
||||
ttl = ip_layer.ttl
|
||||
df_bit = 1 if (ip_layer.flags & 0x2) else 0 # DF = bit 1
|
||||
ip_id = ip_layer.id
|
||||
tos = int(getattr(ip_layer, "tos", 0))
|
||||
dscp = (tos >> 2) & 0x3F
|
||||
ecn = tos & 0x3
|
||||
|
||||
# TCP fields
|
||||
window_size = tcp_layer.window
|
||||
# Server ISN: single sample from one probe — not classified here, but
|
||||
# exported so a downstream consumer correlating multiple probes against
|
||||
# the same target can apply seq_class.classify_sequence().
|
||||
server_isn = int(getattr(tcp_layer, "seq", 0))
|
||||
|
||||
# Parse TCP options
|
||||
mss = 0
|
||||
window_scale = -1
|
||||
sack_ok = 0
|
||||
timestamp = 0
|
||||
options_order = _extract_options_order(tcp_layer.options)
|
||||
|
||||
for opt_name, opt_value in tcp_layer.options:
|
||||
if opt_name == "MSS":
|
||||
mss = opt_value
|
||||
elif opt_name == "WScale":
|
||||
window_scale = opt_value
|
||||
elif opt_name in ("SAckOK", "SAck"):
|
||||
sack_ok = 1
|
||||
elif opt_name == "Timestamp":
|
||||
timestamp = 1
|
||||
|
||||
return {
|
||||
"ttl": ttl,
|
||||
"window_size": window_size,
|
||||
"df_bit": df_bit,
|
||||
"ip_id": ip_id,
|
||||
"tos": tos,
|
||||
"dscp": dscp,
|
||||
"ecn": ecn,
|
||||
"server_isn": server_isn,
|
||||
"mss": mss,
|
||||
"window_scale": window_scale,
|
||||
"sack_ok": sack_ok,
|
||||
"timestamp": timestamp,
|
||||
"options_order": options_order,
|
||||
}
|
||||
|
||||
|
||||
def _extract_options_order(options: list[tuple[str, Any]]) -> str:
|
||||
"""
|
||||
Map scapy TCP option tuples to a short-code string.
|
||||
|
||||
E.g. [("MSS", 1460), ("NOP", None), ("WScale", 7)] → "M,N,W"
|
||||
"""
|
||||
codes = []
|
||||
for opt_name, _ in options:
|
||||
code = _OPT_CODES.get(opt_name, "?")
|
||||
codes.append(code)
|
||||
return ",".join(codes)
|
||||
|
||||
|
||||
# ─── Fingerprint computation ───────────────────────────────────────────────
|
||||
|
||||
def _compute_fingerprint(fields: dict[str, Any]) -> tuple[str, str]:
|
||||
"""
|
||||
Compute fingerprint raw string and SHA256 hash from parsed fields.
|
||||
|
||||
Returns (raw_string, hash_hex_32).
|
||||
"""
|
||||
raw = (
|
||||
f"{fields['ttl']}:{fields['window_size']}:{fields['df_bit']}:"
|
||||
f"{fields['mss']}:{fields['window_scale']}:{fields['sack_ok']}:"
|
||||
f"{fields['timestamp']}:{fields['options_order']}:"
|
||||
f"{fields.get('dscp', 0)}:{fields.get('ecn', 0)}"
|
||||
)
|
||||
h = hashlib.sha256(raw.encode("utf-8")).hexdigest()[:32]
|
||||
return raw, h
|
||||
|
||||
|
||||
# ─── Public API ─────────────────────────────────────────────────────────────
|
||||
|
||||
@_traced("prober.tcp_fingerprint")
|
||||
def tcp_fingerprint(
|
||||
host: str,
|
||||
port: int,
|
||||
timeout: float = 5.0,
|
||||
) -> dict[str, Any] | None:
|
||||
"""
|
||||
Send a TCP SYN to host:port and fingerprint the SYN-ACK response.
|
||||
|
||||
Returns a dict with the hash, raw fingerprint string, and individual
|
||||
fields, or None if no SYN-ACK was received.
|
||||
|
||||
Requires root/CAP_NET_RAW.
|
||||
"""
|
||||
resp = _send_syn(host, port, timeout)
|
||||
if resp is None:
|
||||
return None
|
||||
|
||||
fields = _parse_synack(resp)
|
||||
raw, h = _compute_fingerprint(fields)
|
||||
|
||||
return {
|
||||
"tcpfp_hash": h,
|
||||
"tcpfp_raw": raw,
|
||||
**fields,
|
||||
}
|
||||
131
decnet/prober/tlscert.py
Normal file
131
decnet/prober/tlscert.py
Normal file
@@ -0,0 +1,131 @@
|
||||
"""
|
||||
TLS leaf-certificate capture from attacker-run servers.
|
||||
|
||||
Companion to ``decnet.prober.jarm``: JARM probes are crafted ClientHellos
|
||||
that never complete a real handshake (raw byte parsing only), so the
|
||||
peer certificate is never available from those sockets. This module does
|
||||
a separate :func:`ssl.wrap_socket` against the same ``(host, port)``
|
||||
solely to fetch and parse the leaf cert.
|
||||
|
||||
The cert is intentionally NOT verified — attacker-presented certs are
|
||||
inherently untrusted, and rejecting self-signed ones would defeat the
|
||||
whole point of the capture (most C2 infra runs self-signed certs).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import socket
|
||||
import ssl
|
||||
from typing import Any
|
||||
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.x509.oid import NameOID
|
||||
|
||||
from decnet.telemetry import traced as _traced
|
||||
|
||||
|
||||
def _cn_or_empty(name: x509.Name) -> str:
|
||||
"""Return the first CN attribute as a plain string, or ``""``."""
|
||||
attrs = name.get_attributes_for_oid(NameOID.COMMON_NAME)
|
||||
if not attrs:
|
||||
return ""
|
||||
return str(attrs[0].value)
|
||||
|
||||
|
||||
def _iso_utc(dt: Any) -> str:
|
||||
"""Cert validity timestamps as ``YYYY-MM-DDTHH:MM:SSZ``.
|
||||
|
||||
``cryptography`` exposes ``not_valid_before`` (deprecated, naive UTC)
|
||||
and ``not_valid_before_utc`` (timezone-aware) — prefer the latter
|
||||
when available so we always emit explicit-Z ISO strings.
|
||||
"""
|
||||
return dt.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
|
||||
def _extract_sans(cert: x509.Certificate) -> list[str]:
|
||||
"""All DNS / IP SANs as a flat list of strings; empty when absent."""
|
||||
try:
|
||||
ext = cert.extensions.get_extension_for_class(
|
||||
x509.SubjectAlternativeName
|
||||
)
|
||||
except x509.ExtensionNotFound:
|
||||
return []
|
||||
sans: list[str] = []
|
||||
san: x509.SubjectAlternativeName = ext.value
|
||||
sans.extend(str(v) for v in san.get_values_for_type(x509.DNSName))
|
||||
sans.extend(str(v) for v in san.get_values_for_type(x509.IPAddress))
|
||||
return sans
|
||||
|
||||
|
||||
@_traced("prober.tls_cert_parse")
|
||||
def parse_leaf_cert(der: bytes) -> dict[str, Any] | None:
|
||||
"""Parse a DER-encoded leaf cert into the prober's flat field shape.
|
||||
|
||||
Returns ``None`` if parsing fails for any reason — the caller treats
|
||||
that the same as a connect failure.
|
||||
"""
|
||||
try:
|
||||
cert = x509.load_der_x509_certificate(der, default_backend())
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
try:
|
||||
subject_cn = _cn_or_empty(cert.subject)
|
||||
issuer = cert.issuer.rfc4514_string()
|
||||
issuer_cn = _cn_or_empty(cert.issuer)
|
||||
try:
|
||||
nb = cert.not_valid_before_utc
|
||||
na = cert.not_valid_after_utc
|
||||
except AttributeError: # cryptography < 42
|
||||
nb = cert.not_valid_before
|
||||
na = cert.not_valid_after
|
||||
not_before = _iso_utc(nb)
|
||||
not_after = _iso_utc(na)
|
||||
self_signed = bool(subject_cn) and subject_cn == issuer_cn
|
||||
sans = _extract_sans(cert)
|
||||
cert_sha256 = hashlib.sha256(der).hexdigest()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
return {
|
||||
"subject_cn": subject_cn,
|
||||
"issuer": issuer,
|
||||
"self_signed": self_signed,
|
||||
"not_before": not_before,
|
||||
"not_after": not_after,
|
||||
"sans": sans,
|
||||
"cert_sha256": cert_sha256,
|
||||
}
|
||||
|
||||
|
||||
@_traced("prober.tls_cert_fetch")
|
||||
def fetch_leaf_cert(
|
||||
host: str, port: int, timeout: float = 5.0
|
||||
) -> dict[str, Any] | None:
|
||||
"""Open a TLS connection and return the parsed leaf cert.
|
||||
|
||||
Returns ``None`` on any connect / handshake / parse failure. Never
|
||||
raises — failures must collapse silently so the prober's outer loop
|
||||
can keep moving through targets.
|
||||
"""
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
# Some attacker C2 servers gate on weak ciphers; don't constrain.
|
||||
ctx.set_ciphers("ALL:@SECLEVEL=0")
|
||||
|
||||
try:
|
||||
with socket.create_connection((host, port), timeout=timeout) as raw:
|
||||
raw.settimeout(timeout)
|
||||
with ctx.wrap_socket(raw, server_hostname=None) as tls:
|
||||
der = tls.getpeercert(binary_form=True)
|
||||
except (OSError, ssl.SSLError, socket.timeout):
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
if not der:
|
||||
return None
|
||||
return parse_leaf_cert(der)
|
||||
628
decnet/prober/worker.py
Normal file
628
decnet/prober/worker.py
Normal file
@@ -0,0 +1,628 @@
|
||||
"""
|
||||
DECNET-PROBER standalone worker.
|
||||
|
||||
Runs as a detached host-level process. Discovers attacker IPs by tailing the
|
||||
collector's JSON log file, then fingerprints them via multiple active probes:
|
||||
- JARM (TLS server fingerprinting)
|
||||
- HASSHServer (SSH server fingerprinting)
|
||||
- TCP/IP stack fingerprinting (OS/tool identification)
|
||||
|
||||
Results are written as RFC 5424 syslog + JSON to the same log files.
|
||||
|
||||
Target discovery is fully automatic — every unique attacker IP seen in the
|
||||
log stream gets probed. No manual target list required.
|
||||
|
||||
Tech debt: writing directly to the collector's log files couples the
|
||||
prober to the collector's file format. A future refactor should introduce
|
||||
a shared log-sink abstraction.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
import json
|
||||
import re
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable
|
||||
|
||||
from decnet.bus import topics as _topics
|
||||
from decnet.bus.base import BaseBus
|
||||
from decnet.bus.factory import get_bus
|
||||
from decnet.bus.publish import (
|
||||
make_thread_safe_publisher,
|
||||
run_control_listener,
|
||||
run_health_heartbeat,
|
||||
)
|
||||
from decnet.logging import get_logger
|
||||
from decnet.prober.hassh import hassh_server
|
||||
from decnet.prober.jarm import JARM_EMPTY_HASH, jarm_hash
|
||||
from decnet.prober.tcpfp import tcp_fingerprint
|
||||
from decnet.prober.tlscert import fetch_leaf_cert
|
||||
from decnet.telemetry import traced as _traced
|
||||
|
||||
logger = get_logger("prober")
|
||||
|
||||
# ─── Default ports per probe type ───────────────────────────────────────────
|
||||
|
||||
# JARM: common C2 callback / TLS server ports
|
||||
DEFAULT_PROBE_PORTS: list[int] = [
|
||||
443, 8443, 8080, 4443, 50050, 2222, 993, 995, 8888, 9001,
|
||||
]
|
||||
|
||||
# HASSHServer: common SSH server ports
|
||||
DEFAULT_SSH_PORTS: list[int] = [22, 2222, 22222, 2022]
|
||||
|
||||
# TCP/IP stack: probe on ports commonly open on attacker machines.
|
||||
# Wide spread gives the best chance of a SYN-ACK for TTL/fingerprint extraction.
|
||||
DEFAULT_TCPFP_PORTS: list[int] = [22, 80, 443, 8080, 8443, 445, 3389]
|
||||
|
||||
# ─── RFC 5424 formatting (inline, mirrors templates/*/decnet_logging.py) ─────
|
||||
|
||||
_FACILITY_LOCAL0 = 16
|
||||
_SD_ID = "relay@55555"
|
||||
_SEVERITY_INFO = 6
|
||||
_SEVERITY_WARNING = 4
|
||||
|
||||
_MAX_HOSTNAME = 255
|
||||
_MAX_APPNAME = 48
|
||||
_MAX_MSGID = 32
|
||||
|
||||
|
||||
def _sd_escape(value: str) -> str:
|
||||
return value.replace("\\", "\\\\").replace('"', '\\"').replace("]", "\\]")
|
||||
|
||||
|
||||
def _sd_element(fields: dict[str, Any]) -> str:
|
||||
if not fields:
|
||||
return "-"
|
||||
params = " ".join(f'{k}="{_sd_escape(str(v))}"' for k, v in fields.items())
|
||||
return f"[{_SD_ID} {params}]"
|
||||
|
||||
|
||||
def _syslog_line(
|
||||
event_type: str,
|
||||
severity: int = _SEVERITY_INFO,
|
||||
msg: str | None = None,
|
||||
**fields: Any,
|
||||
) -> str:
|
||||
pri = f"<{_FACILITY_LOCAL0 * 8 + severity}>"
|
||||
ts = datetime.now(timezone.utc).isoformat()
|
||||
hostname = "decnet-prober"
|
||||
appname = "prober"
|
||||
msgid = (event_type or "-")[:_MAX_MSGID]
|
||||
sd = _sd_element(fields)
|
||||
message = f" {msg}" if msg else ""
|
||||
return f"{pri}1 {ts} {hostname} {appname} - {msgid} {sd}{message}"
|
||||
|
||||
|
||||
# ─── RFC 5424 parser (subset of collector's, for JSON generation) ─────────────
|
||||
|
||||
_RFC5424_RE = re.compile(
|
||||
r"^<\d+>1 "
|
||||
r"(\S+) " # 1: TIMESTAMP
|
||||
r"(\S+) " # 2: HOSTNAME
|
||||
r"(\S+) " # 3: APP-NAME
|
||||
r"- " # PROCID
|
||||
r"(\S+) " # 4: MSGID (event_type)
|
||||
r"(.+)$", # 5: SD + MSG
|
||||
)
|
||||
_SD_BLOCK_RE = re.compile(r'\[relay@55555\s+(.*?)\]', re.DOTALL)
|
||||
_PARAM_RE = re.compile(r'(\w+)="((?:[^"\\]|\\.)*)"')
|
||||
_IP_FIELDS = ("src_ip", "src", "client_ip", "remote_ip", "ip", "target_ip")
|
||||
|
||||
|
||||
def _parse_to_json(line: str) -> dict[str, Any] | None:
|
||||
m = _RFC5424_RE.match(line)
|
||||
if not m:
|
||||
return None
|
||||
ts_raw, decky, service, event_type, sd_rest = m.groups()
|
||||
|
||||
fields: dict[str, str] = {}
|
||||
msg = ""
|
||||
|
||||
if sd_rest.startswith("["):
|
||||
block = _SD_BLOCK_RE.search(sd_rest)
|
||||
if block:
|
||||
for k, v in _PARAM_RE.findall(block.group(1)):
|
||||
fields[k] = v.replace('\\"', '"').replace("\\\\", "\\").replace("\\]", "]")
|
||||
msg_match = re.search(r'\]\s+(.+)$', sd_rest)
|
||||
if msg_match:
|
||||
msg = msg_match.group(1).strip()
|
||||
|
||||
attacker_ip = "Unknown"
|
||||
for fname in _IP_FIELDS:
|
||||
if fname in fields:
|
||||
attacker_ip = fields[fname]
|
||||
break
|
||||
|
||||
try:
|
||||
ts_formatted = datetime.fromisoformat(ts_raw).strftime("%Y-%m-%d %H:%M:%S")
|
||||
except ValueError:
|
||||
ts_formatted = ts_raw
|
||||
|
||||
return {
|
||||
"timestamp": ts_formatted,
|
||||
"decky": decky,
|
||||
"service": service,
|
||||
"event_type": event_type,
|
||||
"attacker_ip": attacker_ip,
|
||||
"fields": fields,
|
||||
"msg": msg,
|
||||
"raw_line": line,
|
||||
}
|
||||
|
||||
|
||||
# ─── Log writer ──────────────────────────────────────────────────────────────
|
||||
|
||||
def _write_event(
|
||||
log_path: Path,
|
||||
json_path: Path,
|
||||
event_type: str,
|
||||
severity: int = _SEVERITY_INFO,
|
||||
msg: str | None = None,
|
||||
**fields: Any,
|
||||
) -> None:
|
||||
line = _syslog_line(event_type, severity=severity, msg=msg, **fields)
|
||||
|
||||
with open(log_path, "a", encoding="utf-8") as f:
|
||||
f.write(line + "\n")
|
||||
f.flush()
|
||||
|
||||
parsed = _parse_to_json(line)
|
||||
if parsed:
|
||||
with open(json_path, "a", encoding="utf-8") as f:
|
||||
f.write(json.dumps(parsed) + "\n")
|
||||
f.flush()
|
||||
|
||||
|
||||
# ─── Target discovery from log stream ────────────────────────────────────────
|
||||
|
||||
@_traced("prober.discover_attackers")
|
||||
def _discover_attackers(json_path: Path, position: int) -> tuple[set[str], int]:
|
||||
"""
|
||||
Read new JSON log lines from the given position and extract unique
|
||||
attacker IPs. Returns (new_ips, new_position).
|
||||
|
||||
Only considers IPs that are not "Unknown" and come from events that
|
||||
indicate real attacker interaction (not prober's own events).
|
||||
"""
|
||||
new_ips: set[str] = set()
|
||||
|
||||
if not json_path.exists():
|
||||
return new_ips, position
|
||||
|
||||
size = json_path.stat().st_size
|
||||
if size < position:
|
||||
position = 0 # file rotated
|
||||
|
||||
if size == position:
|
||||
return new_ips, position
|
||||
|
||||
with open(json_path, "r", encoding="utf-8", errors="replace") as f:
|
||||
f.seek(position)
|
||||
while True:
|
||||
line = f.readline()
|
||||
if not line:
|
||||
break
|
||||
if not line.endswith("\n"):
|
||||
break # partial line
|
||||
|
||||
try:
|
||||
record = json.loads(line.strip())
|
||||
except json.JSONDecodeError:
|
||||
position = f.tell()
|
||||
continue
|
||||
|
||||
# Skip our own events
|
||||
if record.get("service") == "prober":
|
||||
position = f.tell()
|
||||
continue
|
||||
|
||||
ip = record.get("attacker_ip", "Unknown")
|
||||
if ip != "Unknown" and ip:
|
||||
new_ips.add(ip)
|
||||
|
||||
position = f.tell()
|
||||
|
||||
return new_ips, position
|
||||
|
||||
|
||||
# ─── Probe cycle ─────────────────────────────────────────────────────────────
|
||||
|
||||
ProbePublishFn = Callable[[str, dict[str, Any]], None]
|
||||
|
||||
|
||||
@_traced("prober.probe_cycle")
|
||||
def _probe_cycle(
|
||||
targets: set[str],
|
||||
probed: dict[str, dict[str, set[int]]],
|
||||
jarm_ports: list[int],
|
||||
ssh_ports: list[int],
|
||||
tcpfp_ports: list[int],
|
||||
log_path: Path,
|
||||
json_path: Path,
|
||||
timeout: float = 5.0,
|
||||
publish_fn: ProbePublishFn | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Probe all known attacker IPs with JARM, HASSH, and TCP/IP fingerprinting.
|
||||
|
||||
Args:
|
||||
targets: set of attacker IPs to probe
|
||||
probed: dict mapping IP -> {probe_type -> set of ports already probed}
|
||||
jarm_ports: TLS ports for JARM fingerprinting
|
||||
ssh_ports: SSH ports for HASSHServer fingerprinting
|
||||
tcpfp_ports: ports for TCP/IP stack fingerprinting
|
||||
log_path: RFC 5424 log file
|
||||
json_path: JSON log file
|
||||
timeout: per-probe TCP timeout
|
||||
"""
|
||||
for ip in sorted(targets):
|
||||
ip_probed = probed.setdefault(ip, {})
|
||||
|
||||
# Phase 1: JARM (TLS fingerprinting)
|
||||
_jarm_phase(ip, ip_probed, jarm_ports, log_path, json_path, timeout, publish_fn)
|
||||
|
||||
# Phase 2: HASSHServer (SSH fingerprinting)
|
||||
_hassh_phase(ip, ip_probed, ssh_ports, log_path, json_path, timeout, publish_fn)
|
||||
|
||||
# Phase 3: TCP/IP stack fingerprinting
|
||||
_tcpfp_phase(ip, ip_probed, tcpfp_ports, log_path, json_path, timeout, publish_fn)
|
||||
|
||||
|
||||
@_traced("prober.jarm_phase")
|
||||
def _jarm_phase(
|
||||
ip: str,
|
||||
ip_probed: dict[str, set[int]],
|
||||
ports: list[int],
|
||||
log_path: Path,
|
||||
json_path: Path,
|
||||
timeout: float,
|
||||
publish_fn: ProbePublishFn | None = None,
|
||||
) -> None:
|
||||
"""JARM-fingerprint an IP on the given TLS ports."""
|
||||
done = ip_probed.setdefault("jarm", set())
|
||||
for port in ports:
|
||||
if port in done:
|
||||
continue
|
||||
try:
|
||||
h = jarm_hash(ip, port, timeout=timeout)
|
||||
done.add(port)
|
||||
if h == JARM_EMPTY_HASH:
|
||||
continue
|
||||
_write_event(
|
||||
log_path, json_path,
|
||||
"jarm_fingerprint",
|
||||
target_ip=ip,
|
||||
target_port=str(port),
|
||||
jarm_hash=h,
|
||||
msg=f"JARM {ip}:{port} = {h}",
|
||||
)
|
||||
logger.info("prober: JARM %s:%d = %s", ip, port, h)
|
||||
if publish_fn is not None:
|
||||
publish_fn(
|
||||
"jarm",
|
||||
{"attacker_ip": ip, "port": port, "jarm_hash": h},
|
||||
)
|
||||
# Cert capture: a non-empty JARM hash proves the port speaks
|
||||
# TLS, so a follow-up real handshake is worth attempting.
|
||||
# Failures are silent — the next probe target must not stall.
|
||||
_capture_tls_cert(ip, port, log_path, json_path, timeout, publish_fn)
|
||||
except Exception as exc:
|
||||
done.add(port)
|
||||
_write_event(
|
||||
log_path, json_path,
|
||||
"prober_error",
|
||||
severity=_SEVERITY_WARNING,
|
||||
target_ip=ip,
|
||||
target_port=str(port),
|
||||
error=str(exc),
|
||||
msg=f"JARM probe failed for {ip}:{port}: {exc}",
|
||||
)
|
||||
logger.warning("prober: JARM probe failed %s:%d: %s", ip, port, exc)
|
||||
|
||||
|
||||
@_traced("prober.tls_cert_capture")
|
||||
def _capture_tls_cert(
|
||||
ip: str,
|
||||
port: int,
|
||||
log_path: Path,
|
||||
json_path: Path,
|
||||
timeout: float,
|
||||
publish_fn: ProbePublishFn | None,
|
||||
) -> None:
|
||||
"""Fetch the leaf TLS cert from ``ip:port`` and emit a tls_certificate
|
||||
event. No-op when the handshake fails (silent — JARM already proved
|
||||
the port responds, but the real handshake can still fail for many
|
||||
reasons: cipher mismatch, SNI gating, mTLS requirement)."""
|
||||
try:
|
||||
cert = fetch_leaf_cert(ip, port, timeout=timeout)
|
||||
except Exception as exc:
|
||||
# fetch_leaf_cert is supposed to swallow errors; defense in depth.
|
||||
logger.warning("prober: TLS cert fetch crashed %s:%d: %s", ip, port, exc)
|
||||
return
|
||||
if cert is None:
|
||||
return
|
||||
|
||||
sans_csv = ",".join(cert["sans"])
|
||||
_write_event(
|
||||
log_path, json_path,
|
||||
"tls_certificate",
|
||||
target_ip=ip,
|
||||
target_port=str(port),
|
||||
subject_cn=cert["subject_cn"],
|
||||
issuer=cert["issuer"],
|
||||
self_signed=str(cert["self_signed"]).lower(),
|
||||
not_before=cert["not_before"],
|
||||
not_after=cert["not_after"],
|
||||
sans=sans_csv,
|
||||
cert_sha256=cert["cert_sha256"],
|
||||
msg=f"TLS cert {ip}:{port} CN={cert['subject_cn']} sha256={cert['cert_sha256'][:16]}...",
|
||||
)
|
||||
logger.info(
|
||||
"prober: TLS cert %s:%d CN=%s sha256=%s",
|
||||
ip, port, cert["subject_cn"], cert["cert_sha256"],
|
||||
)
|
||||
if publish_fn is not None:
|
||||
publish_fn(
|
||||
"tls_certificate",
|
||||
{
|
||||
"attacker_ip": ip,
|
||||
"port": port,
|
||||
"subject_cn": cert["subject_cn"],
|
||||
"cert_sha256": cert["cert_sha256"],
|
||||
"self_signed": cert["self_signed"],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@_traced("prober.hassh_phase")
|
||||
def _hassh_phase(
|
||||
ip: str,
|
||||
ip_probed: dict[str, set[int]],
|
||||
ports: list[int],
|
||||
log_path: Path,
|
||||
json_path: Path,
|
||||
timeout: float,
|
||||
publish_fn: ProbePublishFn | None = None,
|
||||
) -> None:
|
||||
"""HASSHServer-fingerprint an IP on the given SSH ports."""
|
||||
done = ip_probed.setdefault("hassh", set())
|
||||
for port in ports:
|
||||
if port in done:
|
||||
continue
|
||||
try:
|
||||
result = hassh_server(ip, port, timeout=timeout)
|
||||
done.add(port)
|
||||
if result is None:
|
||||
continue
|
||||
_write_event(
|
||||
log_path, json_path,
|
||||
"hassh_fingerprint",
|
||||
target_ip=ip,
|
||||
target_port=str(port),
|
||||
hassh_server_hash=result["hassh_server"],
|
||||
ssh_banner=result["banner"],
|
||||
kex_algorithms=result["kex_algorithms"],
|
||||
encryption_s2c=result["encryption_s2c"],
|
||||
mac_s2c=result["mac_s2c"],
|
||||
compression_s2c=result["compression_s2c"],
|
||||
msg=f"HASSH {ip}:{port} = {result['hassh_server']}",
|
||||
)
|
||||
logger.info("prober: HASSH %s:%d = %s", ip, port, result["hassh_server"])
|
||||
if publish_fn is not None:
|
||||
publish_fn(
|
||||
"hassh",
|
||||
{
|
||||
"attacker_ip": ip,
|
||||
"port": port,
|
||||
"hassh_server": result["hassh_server"],
|
||||
"ssh_banner": result["banner"],
|
||||
},
|
||||
)
|
||||
except Exception as exc:
|
||||
done.add(port)
|
||||
_write_event(
|
||||
log_path, json_path,
|
||||
"prober_error",
|
||||
severity=_SEVERITY_WARNING,
|
||||
target_ip=ip,
|
||||
target_port=str(port),
|
||||
error=str(exc),
|
||||
msg=f"HASSH probe failed for {ip}:{port}: {exc}",
|
||||
)
|
||||
logger.warning("prober: HASSH probe failed %s:%d: %s", ip, port, exc)
|
||||
|
||||
|
||||
@_traced("prober.tcpfp_phase")
|
||||
def _tcpfp_phase(
|
||||
ip: str,
|
||||
ip_probed: dict[str, set[int]],
|
||||
ports: list[int],
|
||||
log_path: Path,
|
||||
json_path: Path,
|
||||
timeout: float,
|
||||
publish_fn: ProbePublishFn | None = None,
|
||||
) -> None:
|
||||
"""TCP/IP stack fingerprint an IP on the given ports."""
|
||||
done = ip_probed.setdefault("tcpfp", set())
|
||||
for port in ports:
|
||||
if port in done:
|
||||
continue
|
||||
try:
|
||||
result = tcp_fingerprint(ip, port, timeout=timeout)
|
||||
done.add(port)
|
||||
if result is None:
|
||||
continue
|
||||
_write_event(
|
||||
log_path, json_path,
|
||||
"tcpfp_fingerprint",
|
||||
target_ip=ip,
|
||||
target_port=str(port),
|
||||
tcpfp_hash=result["tcpfp_hash"],
|
||||
tcpfp_raw=result["tcpfp_raw"],
|
||||
ttl=str(result["ttl"]),
|
||||
window_size=str(result["window_size"]),
|
||||
df_bit=str(result["df_bit"]),
|
||||
mss=str(result["mss"]),
|
||||
window_scale=str(result["window_scale"]),
|
||||
sack_ok=str(result["sack_ok"]),
|
||||
timestamp=str(result["timestamp"]),
|
||||
options_order=result["options_order"],
|
||||
tos=str(result["tos"]),
|
||||
dscp=str(result["dscp"]),
|
||||
ecn=str(result["ecn"]),
|
||||
server_isn=str(result["server_isn"]),
|
||||
msg=f"TCPFP {ip}:{port} = {result['tcpfp_hash']}",
|
||||
)
|
||||
logger.info("prober: TCPFP %s:%d = %s", ip, port, result["tcpfp_hash"])
|
||||
if publish_fn is not None:
|
||||
publish_fn(
|
||||
"tcpfp",
|
||||
{
|
||||
"attacker_ip": ip,
|
||||
"port": port,
|
||||
"tcpfp_hash": result["tcpfp_hash"],
|
||||
"ttl": result["ttl"],
|
||||
"mss": result["mss"],
|
||||
},
|
||||
)
|
||||
except Exception as exc:
|
||||
done.add(port)
|
||||
_write_event(
|
||||
log_path, json_path,
|
||||
"prober_error",
|
||||
severity=_SEVERITY_WARNING,
|
||||
target_ip=ip,
|
||||
target_port=str(port),
|
||||
error=str(exc),
|
||||
msg=f"TCPFP probe failed for {ip}:{port}: {exc}",
|
||||
)
|
||||
logger.warning("prober: TCPFP probe failed %s:%d: %s", ip, port, exc)
|
||||
|
||||
|
||||
# ─── Main worker ─────────────────────────────────────────────────────────────
|
||||
|
||||
@_traced("prober.worker")
|
||||
async def prober_worker(
|
||||
log_file: str,
|
||||
interval: int = 300,
|
||||
timeout: float = 5.0,
|
||||
ports: list[int] | None = None,
|
||||
ssh_ports: list[int] | None = None,
|
||||
tcpfp_ports: list[int] | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Main entry point for the standalone prober process.
|
||||
|
||||
Discovers attacker IPs automatically by tailing the JSON log file,
|
||||
then fingerprints each IP via JARM, HASSH, and TCP/IP stack probes.
|
||||
|
||||
Args:
|
||||
log_file: base path for log files (RFC 5424 to .log, JSON to .json)
|
||||
interval: seconds between probe cycles
|
||||
timeout: per-probe TCP timeout
|
||||
ports: JARM TLS ports (defaults to DEFAULT_PROBE_PORTS)
|
||||
ssh_ports: HASSH SSH ports (defaults to DEFAULT_SSH_PORTS)
|
||||
tcpfp_ports: TCP fingerprint ports (defaults to DEFAULT_TCPFP_PORTS)
|
||||
"""
|
||||
jarm_ports = ports or DEFAULT_PROBE_PORTS
|
||||
hassh_ports = ssh_ports or DEFAULT_SSH_PORTS
|
||||
tcp_ports = tcpfp_ports or DEFAULT_TCPFP_PORTS
|
||||
|
||||
all_ports_str = (
|
||||
f"jarm={','.join(str(p) for p in jarm_ports)} "
|
||||
f"ssh={','.join(str(p) for p in hassh_ports)} "
|
||||
f"tcpfp={','.join(str(p) for p in tcp_ports)}"
|
||||
)
|
||||
|
||||
log_path = Path(log_file)
|
||||
json_path = log_path.with_suffix(".json")
|
||||
log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
logger.info(
|
||||
"prober started interval=%ds %s log=%s",
|
||||
interval, all_ports_str, log_path,
|
||||
)
|
||||
|
||||
_write_event(
|
||||
log_path, json_path,
|
||||
"prober_startup",
|
||||
interval=str(interval),
|
||||
probe_ports=all_ports_str,
|
||||
msg=f"DECNET-PROBER started, interval {interval}s, {all_ports_str}",
|
||||
)
|
||||
|
||||
known_attackers: set[str] = set()
|
||||
probed: dict[str, dict[str, set[int]]] = {} # IP -> {type -> ports}
|
||||
log_position: int = 0
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
# Connect to the bus for attacker.fingerprinted fan-out. Failure is
|
||||
# non-fatal: probes still run, results still land in the log file,
|
||||
# they just don't push notifications to downstream consumers.
|
||||
bus: BaseBus | None = None
|
||||
try:
|
||||
candidate = get_bus(client_name="prober")
|
||||
await candidate.connect()
|
||||
bus = candidate
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logger.warning(
|
||||
"prober: bus unavailable, running in publish-off mode: %s", exc,
|
||||
)
|
||||
|
||||
raw_publish = make_thread_safe_publisher(bus, loop)
|
||||
|
||||
def _publish_attacker(event_type: str, payload: dict[str, Any]) -> None:
|
||||
# Every successful probe fans out under the same topic; the probe
|
||||
# family (jarm/hassh/tcpfp) goes in event_type so consumers can
|
||||
# filter in-memory without needing a dedicated subscription each.
|
||||
raw_publish(
|
||||
_topics.attacker(_topics.ATTACKER_FINGERPRINTED),
|
||||
payload,
|
||||
event_type,
|
||||
)
|
||||
|
||||
shutdown = asyncio.Event()
|
||||
heartbeat_task = asyncio.create_task(run_health_heartbeat(bus, "prober"))
|
||||
control_task = asyncio.create_task(
|
||||
run_control_listener(bus, "prober", shutdown),
|
||||
)
|
||||
try:
|
||||
while not shutdown.is_set():
|
||||
# Discover new attacker IPs from the log stream
|
||||
new_ips, log_position = await asyncio.to_thread(
|
||||
_discover_attackers, json_path, log_position,
|
||||
)
|
||||
|
||||
if new_ips - known_attackers:
|
||||
fresh = new_ips - known_attackers
|
||||
known_attackers.update(fresh)
|
||||
logger.info(
|
||||
"prober: discovered %d new attacker(s), total=%d",
|
||||
len(fresh), len(known_attackers),
|
||||
)
|
||||
|
||||
if known_attackers:
|
||||
await asyncio.to_thread(
|
||||
_probe_cycle, known_attackers, probed,
|
||||
jarm_ports, hassh_ports, tcp_ports,
|
||||
log_path, json_path, timeout,
|
||||
_publish_attacker,
|
||||
)
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(shutdown.wait(), timeout=interval)
|
||||
except asyncio.TimeoutError:
|
||||
pass
|
||||
finally:
|
||||
for t in (heartbeat_task, control_task):
|
||||
t.cancel()
|
||||
with contextlib.suppress(Exception, asyncio.CancelledError):
|
||||
await t
|
||||
if bus is not None:
|
||||
with contextlib.suppress(Exception):
|
||||
await bus.close()
|
||||
Reference in New Issue
Block a user