feat(fingerprint): ToS/DSCP/ECN extraction in active + passive TCP fingerprint
Active prober now reads ip.tos from the SYN-ACK and emits tos/dscp/ecn alongside the existing TTL/window/options fields. dscp is folded into the fingerprint hash so different DSCP markings produce distinct signatures. Passive sniffer logs the same three fields on tcp_syn_fingerprint events; profiler rollup carries them into the attacker tcp_fingerprint snapshot; AttackerDetail's TCP STACK panel now surfaces DSCP and ECN cells.
This commit is contained in:
@@ -133,6 +133,9 @@ def _parse_synack(resp: Any) -> dict[str, Any]:
|
||||
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
|
||||
@@ -159,6 +162,9 @@ def _parse_synack(resp: Any) -> dict[str, Any]:
|
||||
"window_size": window_size,
|
||||
"df_bit": df_bit,
|
||||
"ip_id": ip_id,
|
||||
"tos": tos,
|
||||
"dscp": dscp,
|
||||
"ecn": ecn,
|
||||
"mss": mss,
|
||||
"window_scale": window_scale,
|
||||
"sack_ok": sack_ok,
|
||||
@@ -191,7 +197,8 @@ def _compute_fingerprint(fields: dict[str, Any]) -> tuple[str, str]:
|
||||
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['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
|
||||
|
||||
@@ -412,6 +412,9 @@ def _tcpfp_phase(
|
||||
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"]),
|
||||
msg=f"TCPFP {ip}:{port} = {result['tcpfp_hash']}",
|
||||
)
|
||||
logger.info("prober: TCPFP %s:%d = %s", ip, port, result["tcpfp_hash"])
|
||||
|
||||
@@ -181,6 +181,9 @@ def sniffer_rollup(events: list[LogEvent]) -> dict[str, Any]:
|
||||
"options_sig": e.fields.get("options_sig", ""),
|
||||
"has_sack": e.fields.get("has_sack") == "true",
|
||||
"has_timestamps": e.fields.get("has_timestamps") == "true",
|
||||
"tos": _int_or_none(e.fields.get("tos")),
|
||||
"dscp": _int_or_none(e.fields.get("dscp")),
|
||||
"ecn": _int_or_none(e.fields.get("ecn")),
|
||||
}
|
||||
tcp_fp_context = "syn"
|
||||
|
||||
@@ -236,6 +239,9 @@ def sniffer_rollup(events: list[LogEvent]) -> dict[str, Any]:
|
||||
"options_sig": e.fields.get("options_order", ""),
|
||||
"has_sack": e.fields.get("sack_ok") == "1",
|
||||
"has_timestamps": e.fields.get("timestamp") == "1",
|
||||
"tos": _int_or_none(e.fields.get("tos")),
|
||||
"dscp": _int_or_none(e.fields.get("dscp")),
|
||||
"ecn": _int_or_none(e.fields.get("ecn")),
|
||||
}
|
||||
tcp_fp_context = "synack" # prober sent SYN, captured attacker's SYN-ACK
|
||||
|
||||
|
||||
@@ -1056,6 +1056,9 @@ class SnifferEngine:
|
||||
options_sig=tcp_fp["options_sig"],
|
||||
has_sack=str(tcp_fp["sack_ok"]).lower(),
|
||||
has_timestamps=str(tcp_fp["has_timestamps"]).lower(),
|
||||
tos=str(int(getattr(ip, "tos", 0))),
|
||||
dscp=str((int(getattr(ip, "tos", 0)) >> 2) & 0x3F),
|
||||
ecn=str(int(getattr(ip, "tos", 0)) & 0x3),
|
||||
os_guess=os_label,
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user