Skip to content

Releases: SpaceTrucker2196/sloth

sloth v1.8.5 — the architecture wave, slothweb, and a Bluetooth sideband

Choose a tag to compare

@SpaceTrucker2196 SpaceTrucker2196 released this 26 Oct 13:13

sloth v1.8.5 — the architecture wave, slothweb, and a Bluetooth sideband

Three weeks of work in one patch, not one: no release was cut on
2026-10-12 or 2026-10-19, so this tag carries 441 commits (336 of
them substantive) since v1.8.4 and 64 closed issues. The
alert-type enum goes 64 → 90; VIEW_COUNT goes 35 → 36 (the
[*] BLE view). Suite went 12254 → 33398 assertions.

Three things carry it. The first is #143, the October architecture
remediation: 30 issues (#108–#142) that rewrote parsing, state, build
and security discipline under one tracker — every 802.11 frame decoded
once, one bounded reader and one TLV iterator, one clock seam, one
alert-key builder, alerts.c split by domain, and the test binary
finally built the way the program ships. The second is slothweb,
a new top-level web/ tree and a second binary: a local browser
dashboard that reads sloth's data socket and sloth.db, built across
W0 and M0–M6 (#157–#164). The third is the charter widening of
2026-10-21
— sloth now observes Bluetooth Classic and BLE,
receive-only, with optional Ubertooth capture (#144), under the same
absolute passivity rule as 802.11.

Alongside those: camera identity became a classifier rather than an
OUI guess (#106, #155), the regulatory IE parsers that landed inert in
v1.8.4 grew their three rules (#101), the Multi-Link Element work
reached six rules and client MLDs (#104, #129), the surveillance-vendor
watchlist shipped (#105), WPS gained the two Pixie-Dust rules (#82),
Framing Frames reached PM_FORGE and QUEUE_EVICT (#107), the
evidence ring grew per-session manifests and alert citations (#92), and
the --db migration chain now runs the full v1 → v7 (#154, #145).


⚠️ Upgrade notes — read before upgrading

This is a patch tag by the owner's scheme, but six operator-visible
things changed. The schema bump and the privilege drop are the two
that can change what a working v1.8.4 invocation does.

1. DB_SCHEMA_VERSION 4 → 7, and the upgrade is one-way. An
existing v1–v6 --db file is migrated forward automatically inside
db_open(), one version at a time, each step in a single
BEGIN IMMEDIATE transaction tested against a file built at the
previous version — including the failure case, which leaves the file
unchanged. A migration never rewrites a recorded value: it adds columns
or tables with defaults, and where a change would alter the meaning of
stored data it keeps the old column beside the new one. But a file
this build has migrated to v7 is refused by v1.8.4 and older
, which
refuse any file from a newer build. If you may roll back, copy the
--db file before the first v1.8.5 run. v4 → v5 (#90) adds
karma_candidates.pnl_best; v5 → v6 and v6 → v7 (#155) add the camera
classifier columns (cam_score, cam_evidence, cam_vetoes,
cam_conflict) and the DHCP option 55/60 display fields.

2. A root run now drops privileges by default (#140). Owner ruling
2026-10-15. After the capture handles and netlink sockets are open,
sloth sets NO_NEW_PRIVS, drops to the sloth account when that
account exists (--user NAME overrides), and otherwise keeps the uid
but drops every capability and says so on stderr. CAP_NET_ADMIN is
retained only with --hop or --allow-active; CAP_NET_RAW never is.
A deployment that relied on root privileges persisting past capture
open — a root-owned --pcap-dir, --eapol-dir or data/ — will now
fail to write.
--no-privdrop is the announced escape hatch and
keeps every privilege for the whole run. --user with --no-privdrop
exits 2, as does a --user naming no account, or a drop that failed or
did not read back. Raw capset/prctl; no libcap linked.

3. Bare --data-socket now defaults to unix:/run/sloth/sloth.sock
and can exit 2 (#136).
Owner ruling 2026-10-15. If /run/sloth is
absent, a symlink, owned by neither root nor sloth's effective user, or
group/other-writable, the bare flag refuses at argv time and returns
2
with the remedy printed (RuntimeDirectory=sloth, or
install -d -m 0750 -o <uid> /run/sloth). There is deliberately no
fallback to TCP
— a flag that named no transport must never silently
become one every local account can read. Explicit specs are unchanged;
loopback TCP must now be typed in full.

4. JSONL is additive only, but three field meanings changed. No
record type or field was removed, renamed or re-typed, and 14 record
types remain 14. However: phy changes value on beacon, pnl_client
and wifi_assoc_req (#127 — extension IDs 106/107/108 are Wi-Fi 7, not
81, which the old mapping had wrong); wifi_ap.channel is corrected at
four frequency ranges (#167 — nl80211 now reports 32–35 for 5160–5175
instead of 0, both paths report 0 instead of 178–180 for 5890–5900,
5935 is 6 GHz channel 2, and 5960 reports 6 GHz with no channel); and
an RSNE advertising AKM 24/25 (SAE-EXT-KEY) is now labelled WPA3
where it was labelled WPA2 (#128). Record order within one poll tick
also changed
(#113): the declared phase table emits JSONL after the
snapshots, twins after beacons, and transit before alerts, fixing five
stale reads. A consumer that depended on the old intra-tick order will
see records in a different sequence.

5. Nine rules' alert dedup keys changed (#112). The typed key
builder replaced %.30s prefix truncation with a sanitised prefix plus
an FNV-1a hash of the full string, so remote-chosen key components can
no longer collide by sharing 30 characters. CLEARTEXT_CRED,
DGA_DOMAIN, CAPTIVE_PORTAL, SAE_PSK_SPLIT, SSID_CONFUSION, the
evil-twin chain, ROGUE_DHCP, WPS_PIN_BRUTE and BEACONING all emit
different key strings than v1.8.4 did. An archive joining incidents
across the upgrade boundary on key will not match. New additive
reopen_count on every alert.* record.

6. --replay --allow-active is refused (#119). Replay would
otherwise resolve addresses out of somebody else's savefile.

No CLI flag was renamed or removed. Seven were added:
--replay, --ticks, --user, --no-privdrop, --bt-monitor,
--bt-capture, --ble-rotation-hints. slothweb carries its own
separate flag set (--web-bind, --web-allow, --web-allow-sql,
--web-token, --sloth-db, --store, and the rest) on its own binary.


New alert types (64 → 90)

26 added, none removed or renamed. Every row has its research-corpus
source on file — an ALERT_TYPE_* without one turns
test_every_citable_alert_kind_is_cited red.

Bluetooth and BLE (#144, #156)

Alert Source
BT_OWN_TRANSMIT the second own-state alert kind: the Bluetooth host's own transmissions, held on/off until disabled (#144 slice 2)
BLE_ADV_FLOOD research/bluetooth/ble-spam-flood.md — Flipper/Marauder advertising flood; research/bluetooth/ble-address-rotation.md
BLE_WATCHLIST research/bluetooth/ble-watchlist-categories.md — surveillance-beacon categories
BLE_HID_UNEXPECTED research/cve/2023-45866.md (NVD, Apple HT214035) — unauthenticated HID advertisement; research/bluetooth/ble-address-rotation.md

Cameras (#106, #155)

Alert Source
CAM_WS_DISCOVERY research/papers/ws-discovery-onvif.md, camera-vendor-identity.md, camera-setup-softap.md
CAM_SSDP_DEVICE research/papers/camera-vendor-identity.md — UPnP DigitalSecurityCamera:1 NOTIFY, once per device
CAM_MDNS_SERVICE research/papers/camera-vendor-identity.md — DNS-SD service types IANA registers for cameras
CAM_SETUP_SSID research/papers/camera-setup-softap.md, camera-vendor-identity.md
CAM_SETUP_OUI research/papers/camera-vendor-identity.md, camera-router-role.md
CAM_RTSP_UNAUTH research/rfc/2326-7826-rtsp.md — a DESCRIBE answered with no challenge

Multi-Link / Wi-Fi 7 (#104, #129)

Alert Source
MLE_INVALID_LINK_ID research/cve/2026-58374.md, research/ieee/802.11be-9.4.2.312-mld-identity.md
MLE_COMMON_INFO_LEN_BAD same, plus 802.11be-2024-9.4.2.322-bss-mle-consistency.md
MLE_SUBELEM_OVERRUN research/cve/2026-58374.md, 802.11be-9.4.2.312-mld-identity.md
MLD_MAC_COLLISION 802.11be-9.4.2.312-mld-identity.md, 802.11be-2024-9.4.2.322
MLE_RECONFIG_NO_MLO 802.11be-2024-9.4.2.322 — a Reconfiguration MLE from a BSSID that has never sent a Basic MLE
MLE_PR_BEACON_DIFF 802.11be-2024-9.4.2.322 — a Probe Response whose Basic MLE disagrees with the same BSSID's newest Beacon within 30 s
MLE_CROSS_LINK_CIPHER_DOWNGRADE 802.11be-9.4.2.312, 802.11be-2024-9.4.2.322 — an MLD whose links disagree on cipher or PMF

Regulatory (#101)

Alert Source
COUNTRY_ABSENT research/ieee/802.11d-country-element.md
DFS_MASKING research/regulatory/dfs-15.407-en301893.md
TPC_ABSENT_HIGH_BAND 802.11d-country-element.md, 802.11-2020-11.8-spectrum-management.md

Framing Frames, WPS, surveillance (#107, #82, #105)

Alert Source
PM_FORGE research/cve/2022-47522.md — PM-bit forgery against a station's sleep declaration
QUEUE_EVICT research/cve/2022-47522.md — queued-frame release after a session change
WPS_PIXIE_PROBE research/papers/bongard-pixie-dust-2014.md, research/mitre/T1110.md — 60 s of silence after M3 with no M4 (--tune wps.pixie_silence_s=N)
WPS_PIXIE_LIKELY_SUCCESS same
SURV_DNS research/papers/surveillance-vendor-endpoints.md — DNS lookups of surveillance-vendor service domains
SURV_CLOUD_SNI same — TLS to a surveillance vendor's cloud, and nothing more

Two corrections went in behind these: SURV_DNS, SURV_CLOUD_SNI and
then THREAT_DOMAIN were all matching a cut DNS name and churning
updates (#105, #152), and the #152 docs claim that SNI and Host are
IOC-matched was wrong and is corrected rather than left stan...

Read more

sloth v1.8.4 — WPS attack detection and tunable thresholds

Choose a tag to compare

@SpaceTrucker2196 SpaceTrucker2196 released this 05 Oct 13:10

sloth v1.8.4 — WPS attack detection and tunable thresholds

Weekly patch release, and the first since v1.8.1 to add alert types:
the alert-type enum goes 61 → 64 with the three WPS rules (#82).
VIEW_COUNT stays 35 — no new views.

Two things carry this release. The first is that #82's WPS work reached
its verdicts: v1.8.3 shipped the EAP-WSC decoder as parse-only, waves 6
and 7 added the session table and the lockout ring as measurement, and
d652502 lands the three rules on top with their research-corpus
sources. The second is that every behavioural threshold in the tree —
23 of them, not just the WPS counts — moved from a #define to a
--tune registry (#82, 4206fe7), because the owner's acceptance of
the WPS thresholds was conditional on each being configurable and
nothing in the tree could satisfy that.

Alongside those: exporting crackable material is now opt-in with a
7-day retention sweep (#87), sensor health gained measured dwell, hop
activity and four more eviction counters (#91), the capture path is
finally reachable by the sanitizers through the real pcap_dispatch
callback (#95), the shipped binary builds hardened (#95), the capture
scope allow-list counts its refusals and refuses an interactive [m]
retarget (#85), and the regulatory IE parsers landed inert (#101).

No issues closed on this release. Twelve advanced and were
deliberately left open (#45, #82, #85, #86, #87, #90, #91, #92, #95,
#96, #101, #103). Suite went 11229 → 12254 assertions.


⚠️ Upgrade notes — read before upgrading

No schema bump. DB_SCHEMA_VERSION stays 4. An existing v4 --db
file opens unchanged. JSONL is additive only — new fields, no record
type or field removed, renamed or re-typed. No CLI flag was renamed
or removed
; eight were added.

Even so, two #87 changes can turn a working v1.8.3 invocation into a
refused one — or delete files it wrote:

1. --eapol-dir alone now exits 2 (#87). Writing PMKIDs and 4-way
handshakes is gated behind a new --collect-handshakes opt-in, off by
default. --eapol-dir DIR without it does not warn and continue; it
refuses at argv time and returns 2, because an operator who named a
destination for crackable material and got a running sloth would
reasonably read that as the export happening. Any init unit, script
or cron entry passing --eapol-dir today will fail to start on
v1.8.4 until --collect-handshakes is added.
Detection, alerting,
the [e] view and the JSONL/--db eapol_events records are
unaffected by the flag — only the .22000 lines and the per-handshake
pcaps are gated. The inverse (--collect-handshakes with no
--eapol-dir) warns and runs.

2. Exported handshake material is now deleted after 7 days (#87).
--handshake-retention DAYS defaults to 7 and sweeps at startup
and once a day while running. On the first v1.8.4 run with
--collect-handshakes --eapol-dir DIR, every artifact in DIR last
written before the window is unlinked. If you are keeping older
exports in that directory, copy them out first or pass
--handshake-retention 0 to keep forever. Granularity is whole-file by
mtime, so eapol.22000 goes only once nothing has been appended for
the whole window — the 22000 format has no per-line timestamp.
Deletion goes through the dirfd pinned at startup, entries are
fstatat'd AT_SYMLINK_NOFOLLOW, and only regular files are unlinked,
so a symlink planted at an artifact name cannot redirect a deletion out
of the export directory.

3. A bad --tune exits 2 (#82). An unknown knob name, a
non-integer, a missing value or an out-of-range number refuses the run
rather than falling back to the default. ssh.brute is not accepted as
an abbreviation of ssh.brute_force. This is the design point: a knob
the operator believes they set but did not is worse than no knob,
because the run looks tuned and behaves stock.

New flags this release, all additive: --tune, --tune-list,
--collect-handshakes, --handshake-retention,
--wps-pin-brute-cycles, --wps-lockout-cycles,
--wps-pbc-concurrent, and the --data-socket-allow-remote /
--eapol-dir / --pcap-dir spellings carried from v1.8.3.

Also worth knowing, not breaking: a raised --tune threshold can
silence a detector, which is indistinguishable from a quiet segment.
sensor_health therefore exports tuned_count and tuned (the knobs
themselves, not just a count — a count would not say which rule went
quiet), and the Interfaces health strip renders a fault-only
tuned N/M token.


New alert types (#82)

Three rules, each with its citation on file — ALERT_TYPE_* without a
research-corpus source turns test_every_citable_alert_kind_is_cited
red, which is why waves 6 and 7 stopped at measurement. Thresholds are
the owner's decision of 2026-09-30; only the counts are tunable,
because the 60 s, 1 h and 120 s windows are protocol facts rather than
preferences.

Alert Severity Rule Source
WPS_PIN_BRUTE CRIT (T1110.001) ≥5 M1→M3→EAP-NACK restart cycles in 60 s CERT/CC VU#723755 — WPS PIN brute force (Viehböck, 2011; CVE-2011-5053)
WPS_LOCKOUT_CYCLING WARN; CRIT on --my-bssid (T1110.001) ≥2 completed locked→unlocked cycles per hour same advisory, lockout clause
WPS_PBC_RACE CRIT (T1557) >2 live unfinished WSC registrations on one BSSID inside the 120 s walk time WSC 2.0 PBC walk time and session overlap, as implemented in hostap src/wps/wps_defs.h (WPS_PBC_WALK_TIME, WPS_CFG_MULTIPLE_PBC_DETECTED)

One WPS_PIN_BRUTE cycle is one refused PIN guess, and the EAP-NACK is
the oracle VU#723755 describes — which is exactly why it is countable
off the air without sloth sending anything. The rule fires in two
shapes: per station, and per UUID-E across ≥2 stations, because an
attacker rotating the MAC the AP rate-limits on leaves the enrollee
identity inside M1 alone. The rotating pass requires two or more
stations so a plain Reaver run is never reported twice.

WPS_LOCKOUT_CYCLING is the same attack read from the victim's side,
and it matters because the lock bit rides every beacon while the EAP
exchange is a brief unicast burst a hopping radio usually misses. An AP
that locks once and stays locked is a posture, not an attack, and never
fires.

WPS_PBC_RACE is gated on that AP's beacon advertising Device Password
ID 0x0004. WSC's own session-overlap rule makes the condition an abort
(Configuration Error 12, Multiple PBC sessions detected), so the CRIT
reports a protocol error rather than an inferred intent.

The substrate landed first and is unchanged by the rules:
440d514 added wps_track.c, a bounded LRU table keyed (BSSID, STA)
running the WSC 2.0 §7.7 state machine IDLE→M1_SEEN→M3_SEEN→NACKED/DONE
over the inner EAP packets eap_track already sees — a NACK completes a
restart cycle only when it follows M3, since M1→NACK is an ordinary
M2D-style refusal and counting it would inflate every brute-force figure
with normal traffic. 4a5d036 added a WPS_LOCK_RING(8) of transitions
between known lock states per BSSID; a beacon that merely omits the
attribute records nothing.


One tunable registry for every threshold (#82, 4206fe7)

Owner decision 2026-10-04 settled the surface: one repeatable flag over
a named registry, not ~20 flags, since one flag per threshold would mean
a new permanent CLI contract for every future detector.

sloth --tune dns.tunnel_label_thresh=40 --tune ssh.brute_force=20
sloth --tune-list

src/tune.{c,h} holds 23 rows indexed by a tune_id_t, so a detector
reads an array slot rather than comparing strings on the hot path; the
name exists for the CLI, --tune-list and the export. Each row carries
its shipped default, a range and a unit. Bounds exclude only values that
would make a rule meaningless — a count below 1, a zero-length window —
not values an operator who knows their segment might legitimately want.

The 22 #defines and KARMA_SSID_THRESH now expand to tune_val(...),
so every use site is untouched. That was deliberate: had any site needed
a compile-time constant it would have failed the build rather than
quietly changing meaning. Before this, every behavioural threshold in
src/alerts.c was a #define and sloth had exactly one getenv
(NO_COLOR).


Crackable material is opt-in, with retention (#87, f8fd5e8)

See upgrade notes 1 and 2 for the operator-visible half. The reasoning:
#87's earlier slices made the export private on disk (0700/0600, by
descriptor, validated never repaired), which bounds who can read the
file and does nothing about the file existing at all. A PMKID or a
paired M1+M2 supports offline password guessing by whoever gets a copy,
so its lifetime on disk is the remaining exposure.

The gate is about writing only, and that boundary was checked
against the code rather than assumed: the EAPOL-Key parser, the M1..M4
attempt state machine, the replay-counter pairing verdict, the
PTK-generation counter src/fragattack.c reads, M3 association
evidence, the [e] view and the JSONL / --db eapol_events records
all run unchanged with the gate closed. Only
append_22000_line_for_bssid() and write_handshake_pcap() are gated,
and both re-check g_collect at the last statement before material
becomes a file, so a future caller cannot route round
eapol_set_output_dir()'s refusal. Blinding the detector would have
cost detections and protected nothing, since nothing crackable leaves
the process.

Retention follows src/db.c's existing shape — a window, a last-run
clock, a scheduling wrapper the poll loop calls — rather than
introducing a second scheduler. A skipped entry or a failed unlinkat
is counted and surfaced through the existing sfile_fail path (stderr,
the [e] header, storage_eapol_failures) instead of being swallowed.


Sensor health: measured, not planned (#91)

Three slices, each replacing a number...

Read more

sloth v1.8.3 — strict by default

Choose a tag to compare

@SpaceTrucker2196 SpaceTrucker2196 released this 28 Sep 13:11

sloth v1.8.3 — strict by default

Weekly patch release. Like v1.8.2 it adds no new views and no new alert
types
— VIEW_COUNT (35) and the alert-type enum are byte-identical to
v1.8.2. What it changes is what the shipped binary does on its own.

Until this release, "sloth never transmits, never scans, never changes
kernel state" was a property of how you invoked it. Reverse DNS ran by
default, the nl80211 scan trigger fired from the ordinary poll loop with
no policy check, and the QUIC decoder resolved on the capture thread
where no operator toggle could reach it. Strict observation is now the
default
(#84), and there is one module that owns the policy every
subsystem asks.

Alongside that: the data socket gained an exposure guard and
close-on-exec (#86), the evil-twin family traded attacker-writable trust
anchors for an operator-supplied inventory (#89), capture scope is
pinned by ifindex instead of a stale name cache (#85), and the assurance
work (#95) put ASan/UBSan, ThreadSanitizer and cppcheck in CI as
blocking jobs — which then found and fixed four real defects.

Two issues closed on this release's code (#89, #94). Two more (#97, #98)
were closed administratively this week; their code shipped in v1.8.2.
Nine advanced and deliberately left open (#82, #84, #85, #86, #87, #90,
#92, #95, #96). Suite went 9304 → 11229 assertions.


⚠️ Upgrade notes — read before upgrading

No schema bump. DB_SCHEMA_VERSION stays 4. An existing v4 --db
file opens unchanged. JSONL is additive only — new fields, no record
type or field removed, renamed or re-typed. No CLI flag was renamed or
removed
; seven were added.

Four behaviour changes can turn a working v1.8.2 invocation into a
different — or refused — v1.8.3 run:

1. Reverse DNS is off by default (#84). Sloth now originates no
reverse-DNS traffic unless you pass --allow-active. The resolver
worker thread is not created at all in a strict run, so the guarantee is
a property of the process rather than a branch taken per lookup. If you
depended on numeric addresses resolving to names in the [n] toggle,
top hosts, or the packet detail pane, pass --allow-active. Names that
sloth observed passively — from the DNS/mDNS/NBNS/DHCP/SNI snoopers
— still resolve with no flag; only active lookups changed.

--allow-active deliberately does not restore resolution in the
UDP/443 QUIC decoder. That path runs per packet on the capture thread,
and re-enabling it behind a flag would reproduce the defect #84 names,
just less visibly.

2. A routable data socket is refused without an explicit opt-in
(#86).
A tcp: spec outside 127.0.0.0/8 — including the 0.0.0.0
wildcard — now requires --data-socket-allow-remote. The refusal names
the flag and offers an ssh -L one-liner; the permitted path warns,
naming address and port and the fact that the stream is unauthenticated
and unencrypted. If you bind the data socket to a routable address
today, v1.8.3 will refuse to start until you add the flag.
Loopback is
the whole /8, so an operator already on 127.0.0.2 needs nothing. unix:
specs are unaffected.

3. unix: data sockets are created 0600 (#86). They were created at
the process umask — 0755 on a default 0022 host, i.e. world-connectable.
A consumer running as a different uid than sloth will now be refused
by the kernel. That plus the uid-ownership check is the peer
authentication that replaces the crypto the Captain ruled out (#100
records the TLS design; it is not scheduled). Socket examples in the
quickstart, landing page and interfaces doc moved from
unix:/tmp/sloth.sock to unix:/run/sloth.sock — /tmp is
world-writable and should not be modelled as a recommended posture.

4. A replugged adapter under --iface/--monitor-only stays failed
closed until restart (#85).
The allow-list is pinned to
(ifindex, name) pairs before the capture thread starts. A rename, a
delete, or an index reused by a different device clears that pin for the
rest of the run. This is deliberate — the alternative is a stale cache
acting as an authorization source — but it means a hot-replug now needs
a restart rather than silently resuming. capture_scope_state() reports
none / enforced / no_capture / degraded so the blackout is
visible instead of silent.

New flags this release, all additive: --allow-active, --strict,
--inventory, --site, --no-correlate, --correlate-retain,
--data-socket-allow-remote.

Known asymmetry, stated rather than papered over: --strict locks
out the scan trigger but does not refuse --hop, and does not close
an opted-in routable data socket. --hop is parsed independently and
never consulted against the strict lock. sloth --help, README and
docs/wiki/data-socket-exposure.md were all corrected to say so
(baa9638, 6c33228) after each claimed full coverage. Whether
--strict should refuse --hop is an open owner decision.


Strict observation is the default (#84, slices 1–3 + docs)

Three slices, then two doc corrections, on the issue an external CISO/GRC
review filed as F02: the headline passivity claim was not backed by the
code.

Slice 1 (4315676) built the seam. dns_lookup() was the only
spelling of name resolution in the tree, so every caller that wanted "a
name if we have one" silently became a caller that generated
reverse-DNS egress on a cache miss. Now dns_lookup_cached() answers
only from metadata sloth already observed — and claims no PENDING cache
slot, because a refused miss must leave the cache indistinguishable from
one never taken, or the slot alone hands the next caller work it did not
ask for. dns_resolve() is the single choke point; getnameinfo(3)
appears exactly once in the tree behind it. dns_resolver_stats() is
the observable seam, and getnameinfo_calls is incremented inside
resolve_ip() rather than at the enqueue site, so the number means
"work that actually left the process" — a test asserts the two diverge.

Slice 2 (4374ff1) decided the policy, per the Captain's written
decision on the issue. dns_resolver_set_enabled() defaults off and the
worker thread is gated. test_resolver_enabled_by_default was inverted
to test_resolver_disabled_by_default — that assertion existed to make
this flip arrive as a deliberate edit rather than drift.

Slice 3 (b9aa686) extended the policy to the paths it did not
cover.
New src/observe.{c,h} owns the run's observation policy,
because the scan trigger lives in the platform layer and discovery in
its own module, neither of which can sensibly depend on the DNS module.
trigger_scan_async() fired NL80211_CMD_TRIGGER_SCAN from the poll
loop with no policy check; the decision, the limiter and the message
construction are split into linux_wifi_prepare_scan_trigger(), which
returns 0 when either refuses, and the sending path opens no socket when
nothing was built. Counting at the builder rather than at sendto(2) is
deliberate: a request built and then dropped is still one the code was
willing to make.

The scan limiter is also now genuinely per-interface. It was one
function-static time_t shared across every radio, behind a comment
claiming per-interface limiting, so the first interface enumerated
consumed the whole budget and the rest starved. A dedicated monitor
radio alongside an uplink is the deployment this tool targets, so
"global" did not mean fair — it meant one radio preferred.


Data socket hardening (#86)

The Captain's decision on #86 was no crypto in sloth — no OpenSSL,
no mbedTLS, no bearer tokens. That leaves the transport and lifecycle
half, and a boundary to write down in place of the crypto.

  • Listener lifecycle (f2bf0b5). init_unix() unconditionally
    unlink()'d whatever sat at the socket path: a regular file, a
    symlink, or another process's live listener were all removed the way a
    dead socket was. unix_path_removable() now lstat()s the path,
    never follows a symlink, refuses anything not S_ISSOCK or not owned
    by our uid, and probes liveness with a connect() — ECONNREFUSED
    means stale and safe, a successful connect leaves the path untouched.
    set_nonblock()'s return value was discarded at all three call sites,
    leaving a blocking fd behind — a hazard under g_mu, where
    accept()/send() could stall while the mutex is held. Both are now
    checked. init_tcp()'s port parse was strtol(s, NULL, 10), which
    took "8080x" as 8080; now checked against the full string.
  • Exposure guard (1d19bc8) — see upgrade note 2. The check runs
    before socket(), so a bind nobody opted into never reaches the
    kernel.
  • Close-on-exec (d3170da). Without FD_CLOEXEC any child sloth
    exec()s inherited the listener and every live client, and could
    accept() on or read the unauthenticated JSONL stream afterwards. The
    listener is created SOCK_CLOEXEC|SOCK_NONBLOCK and clients accepted
    with accept4(), so the flag is set atomically — a later fcntl()
    would leave a fork+exec window. Non-Linux targets use a checked
    fcntl(F_SETFD) and close the fd if it fails.
  • One exposure classifier (8fc0cb0). discovery_routable_tcp_port()
    carried its own drifted copy of "is this spec routable": it named only
    127.0.0.1 as loopback, so tcp:127.0.0.2:8765 bound without the
    flag and discovery then advertised it over mDNS as a LAN service no
    other host can reach. It also read the port with atoi(). It now
    delegates to data_socket_spec_is_remote() and
    data_socket_spec_tcp_port(), so the advertisement can only describe
    what the guard allowed.

Evil-twin findings get a trust anchor (#89, slices 1–3 — closed)

Slice 1 (09f7e2d) removed three things the code treated as proof of
ownership
, all of them values an attacker writes into a frame:

  • An 802.11k neighbour report dropped a pair outright if either AP
    advertised the other. Nothing authenticates ...
Read more

sloth v1.8.2 — say when you are not looking

Choose a tag to compare

@SpaceTrucker2196 SpaceTrucker2196 released this 23 Sep 15:33

sloth v1.8.2 — say when you are not looking

Weekly patch release, and an unusual one: no new detectors, no new
views, no new alert types.
Every line in it makes an existing claim
true — the sensor now reports when it has stopped observing, alerts
report transitions instead of repeating state, exports stop leaking
through the umask, and the documentation stops describing a version of
sloth that has not existed since v1.1.

Four issues closed (#83, #88, #93, #99), seven advanced and deliberately
left open (#82, #85, #87, #91, #96, #97, #98). Suite went 8358 → 9304
assertions
. The alert-type enum and VIEW_COUNT are byte-identical to
v1.8.1.


Upgrade notes

No schema bump — DB_SCHEMA_VERSION stays 4. An existing v4 --db
file opens unchanged.

No CLI change at all. The flag set is byte-identical to v1.8.1.
--db-max-mb is described differently (it is a pruning trigger, not a
hard cap — it always was) but behaves exactly as before.

JSONL is additive only. One new record type and one new record
family; nothing removed, renamed or re-typed:

  • sensor_health — one line per tick about the collector itself, the
    stream's only singleton record
  • alert.create / alert.escalate / alert.update / alert.resolve —
    the alert lifecycle family, emitted alongside the unchanged alert
    record
  • New fields on eapol: handshake_progress, replay_counter_ok,
    assoc_evidence. New fields on beacon for WPS Config Methods and
    Device Password ID. New channel fields on the interface record.

A consumer that ignores unknown record types and unknown fields needs no
changes.

One behaviour change worth reading before you upgrade: capture scope
now fails closed (#85). If you pass --monitor-only or --iface
and the restriction cannot be enforced, sloth refuses to start rather
than capturing wider than you asked. That is the correct behaviour for
an authorization boundary, but it can turn a previously-starting
configuration into a startup refusal. The refusal says why.


Security fixes

EAPOL-Key heap over-read (#83, 89e8ff0). The parser trusted the
frame to be as long as its own headers claimed. A truncated or crafted
EAPOL-Key frame read past the end of the buffer. Parsing is now bounded
by the declared span, with the contract written down in
docs/views/eapol.md so the next parser does not re-introduce it.
Attacker-reachable from any station in range.

Export artifacts were created under the process umask (#87,
c588ba9).
Handshake exports, JSONL, the DB and pcaps landed
world-readable on a default umask. New src/secure_file.c creates
directories 0700 and files 0600 by descriptor with
O_NOFOLLOW|O_CLOEXEC, so the umask is irrelevant, and a write that
fails is now reported instead of silently dropped.

Capture scope failed open (#85, cda85c5). --monitor-only and
--iface were advisory. They are now an authorization boundary: an
allow-list admits a frame only if every condition holds, and an
unenforceable restriction is a startup refusal.


The sensor now says when it is not looking (#91, slices 1–3)

Three separate paths made "healthy with no detections" and "not
observing" look identical:

  • chanhop_drive() discarded set_channel()'s return code, so a retune
    refused for want of CAP_NET_ADMIN left the --hop scan bar showing
    the channel it intended. The bar now renders an unconfirmed retune
    as [11?], and a lifetime failure count is kept that a later success
    does not clear — a flapping radio stays visible.
  • Both capture workers broke out of pcap_dispatch() on error and
    returned with the handle still open, so capture_is_open() kept
    saying yes. On a hopping radio an empty dwell is normal, so a dead
    monitor thread was indistinguishable from a quiet channel
    indefinitely. Each worker now classifies why it stopped — stopped,
    iface_gone, perm_lost, not_activated, error — and publishes
    libpcap's raw text beside the verdict.
  • pcap_stats() was never called, so NIC and libpcap buffer drops were
    invisible, and every bounded table discarded silently at capacity.
    Stats are polled per tick, with totals accumulated from deltas so a
    32-bit counter reset never shows a consumer a negative rate.

A healthy sensor renders exactly health: cap up mon up in the
interface view. Everything after those two words is a fault and appears
only when non-zero — a strip that printed drop 0 evict 0 every second
would train you to stop reading it.

Known limit, stated rather than papered over: the eviction tally
covers alerts, top hosts, PNL clients, per-client PNL SSIDs, DHCP
events, 802.1X sessions and the device table. It does not cover the
probe-client, beacon, seqnum, assoc or per-protocol rings. The docs say
which, because a tally that silently omits a table reads as "no loss"
when it means "not measured".

Slice 4 (occupancy benchmark) is not in this release, and the measured
adapter/driver/kernel support matrix needs hardware. #91 stays open.


Alerts report transitions, not just state (#98, 54568c8)

The alert record was emitted once per rule tick with no notion of a
run, so a WARN that became CRIT was byte-identical to the same WARN
repeating, and a condition that stopped simply went quiet. Consumers
could see state and never transitions.

A run of one dedup key is now an incident: one alert.create, any
number of alert.escalate / alert.update, one alert.resolve, all
carrying a stable incident_id and an ordered event_id.

Severity changes always emit, in both directions, never throttled —
severity is the paging signal. Detail-only changes are floored at one
per 60s against a signature of the detail, and the signature is not
advanced while throttled, so the next real change still fires with
current detail. Resolve is 300s with no rule re-asserting the key,
measured on last_evaluated rather than last_observed: rules
re-derive from retained state every poll, so keying on observation would
resolve a standing condition that is still true and immediately
re-create it.

The legacy alert record is untouched and count is still populated —
now documented as an explicit alias of evaluations (rule ticks). It
was never a packet or incident count.


Flood windows are real windows now (#88, 40b65dc)

Deauth and probe flood detection used a counter reset on a boundary, so
a burst straddling the reset could double its effective budget and an
attacker pacing to the boundary could stay under it forever. Both are
true sliding windows on CLOCK_MONOTONIC, with the evidence line
reporting the span it actually measured.


Data socket frames records whole (#93, aecf2ae)

Under backpressure a record could be split across writes, handing a
consumer half a JSON object. Records are framed whole, and a gap is
accounted and reported rather than silently papered over.


EAPOL handshake pairing (#97, ff96a71)

The (BSSID, STA) record carried sticky m1_seen / m2_seen flags
with no replay counter and no expiry, so an M1 cached at boot paired
with any later M2, across associations and rekeys — and that
half-exchange was then promoted to association evidence. Pairing now
requires equal Key Replay Counters within a 10s window, a new M1
discards everything that depended on the old one, and association
promotion moved from M2 to M3.

The hashcat 22000 export wrote a literal 02 message-pair byte
(hashcat's M2+M3 category) for what is actually an M1+M2 pair (00).
The byte is now derived from what was validated, with the
"not replaycount checked" bit set so the export reports what sloth
actually compared. Sloth still does not verify the MIC and never
cracks anything — §2.2 — so the offline validation against a known PSK
remains a lab step and #97 stays open.


Documentation that matches the code (#96, dd71a06)

From an external CISO/GRC review. Our own documentation was claiming
things the code does not back:

  • SECURITY.md named 1.4.x as current while the code was 1.8.1. It
    now names the real version, points at the #define as the source of
    truth, and states the actual policy: one branch, newest tag only, no
    LTS, no backports, no patch SLA. Inventing a support window we cannot
    keep would be the same defect pointing the other way.
  • The embedded IOC list is synthetic demo data, and an operator had
    no way to know. The disclosure now lives on three surfaces pinned by
    tests — the alert detail reads demo IOC, the help card has an
    "Embedded data" section, and the source says why no feed ships (a
    fetch is a network write, MISSION §2). THREAT_DOMAIN and
    THREAT_IP detect nothing until you replace it.
  • Retention was documented as a 30-day deletion and a hard disk cap.
    It is neither. New docs/wiki/retention.md, written from
    db_maintain(): 1x/3x/12x tiers, age from last_seen so a
    still-observed row never expires, hourly and only while sloth runs, a
    size guard that prunes only observation rows and is allowed to exceed
    target. It lists what nothing covers — JSONL, pcap, EAPOL exports,
    WAL, backups — and says plainly that row deletion is logical, not
    secure erasure.
  • Counts swept against source: 35 views, 61 alert rules, ~48.6k
    lines across 147 .c files. MISSION.md §3 still opened with "As of
    v1.1".

The licensing bullet of #96 is the maintainer's and is untouched here.


Build hygiene (#99, 741e6f1)

make WITH_WIFI=0 was the one build in the matrix that was not
warning-clean — two helpers in src/views/wifi.c sat outside the guard
that removes their only callers. Fixed, and the rule in
agents/AGENTS.md was widened from "make produces no warnings" to
name all six variants, since checking one build out of six is how it
drifted unnoticed in the first place.


Verification

Every variant built warning-clean and the suite green, on main, before
th...

Read more

sloth v1.8.1 — FragAttacks, WPA3 downgrade, and cited sources

Choose a tag to compare

@SpaceTrucker2196 SpaceTrucker2196 released this 21 Sep 13:04

sloth v1.8.1 — FragAttacks, WPA3 downgrade, and cited sources

Weekly patch release. Five issues closed (#73, #74, #75, #76, #80) plus
three buildable slices of #77, which stays open. Fourteen new detectors,
the in-tree research corpus that backs them, and a JSONL overflow fix
that matters if you run with -o or the data socket.

Suite went 6687 → 8358 assertions. Alert types 48 → 62. Views
33 → 35.


Upgrade notes

No schema bump — DB_SCHEMA_VERSION stays 4. An existing v4 --db
file opens as-is and gains one new table, ssid_akm_history (#74),
via CREATE TABLE IF NOT EXISTS. New alert types are appended to the
end of the enum, so every persisted alerts.type value is unchanged.

JSONL is additive only. New fields on beacon: wps_manufacturer,
wps_model_name, wps_model_number, wps_serial, ie_order_hash,
ie_order_count, tbtt_jitter_us, tbtt_jitter_samples,
tbtt_jitter_resets. Nothing removed, renamed or re-typed.

CLI is additive only. New: --version / -V, --with-research.

License. The repository now ships LICENSE — Sloth Source-Available
License 1.0 (#79): non-commercial, no derivatives, no enterprise or
law-enforcement use; packaging of unmodified source for distro
repositories is permitted. Earlier tags carry no license file.


Security fix — JSONL stack overflow (1f108a8)

Every JSONL builder advanced with off += snprintf(...). A record that
filled the buffer pushed off past LINEBUF, the remaining-size
calculation wrapped, and the next write landed out of bounds on the
stack. Attacker-reachable: SSID history, WPS strings and probed
SSIDs are chosen by the transmitter, and json_escape expands a control
byte sixfold. An AP cycling escape-heavy SSIDs crashed sloth whenever
-o or data-socket output was on.

All appends now go through a clamped appendf(). LINEBUF 2048 → 8192
(measured worst cases: beacon 4529 bytes, PNL 3436) and
EMIT_XFORM_MAX 2560 → 16384, so hostile records emit whole rather than
truncated to invalid JSON. Upgrade if you use JSONL output.


New detectors

FragAttacks family (#75, Vanhoef 2021) — nine alert types:

Alert CVE Signal
FRAG_PLAINTEXT CVE-2020-26140 / -26143 plaintext data after key install, per (BSSID, STA), ordered
FRAG_BCAST CVE-2020-26145 plaintext broadcast fragment on a protected BSS
FRAG_CACHE CVE-2020-24586 reassembly straddles a (re)association
FRAG_MIXED CVE-2020-26147 reassembly mixes encrypted and plaintext fragments
FRAG_AMSDU CVE-2020-24588 same CCMP PN replayed with the A-MSDU bit flipped
FRAG_AMSDU_EAPOL CVE-2020-26144 plaintext A-MSDU whose first subframe claims EAPOL
FRAG_MIXKEY CVE-2020-24587 reassembly spans a PTK rotation (M3 with a new ANonce)
FRAG_PN_GAP CVE-2020-26146 encrypted fragments with dPN ≠ dFN
FRAG_EAPOL_RELAY CVE-2020-26139 EAPOL with neither SA nor DA equal to the BSSID

The A-MSDU attack as originally specified is unobservable (subframe
headers are inside the ciphertext); FRAG_AMSDU detects the replay it
requires instead. New [c] FragAttacks view — per-BSSID counters.

WPA3 downgrade (#74) — SAE_PSK_SPLIT: one SSID SAE-only on one
BSSID and PSK-only on another, the gap between EVIL_TWIN's branches.
SAE_PSK_REGRESSION: one BSSID that sustained SAE-only now advertises
PSK-only, from persisted ssid_akm_history. Neither fires on transition
mode — that is WPA_DOWNGRADE. Basis: CVE-2023-52424, Dragonblood.

Action frames (#76) — SA_QUERY_FLOOD: the AP's MFP answer to a
spoofed-disassociation flood, visible even when the flood is not.
MFP_UNPROTECTED: unprotected robust action frame on an MFP-required
BSS (CVE-2019-16275). EVIL_TWIN gains a +btm-steered by <AP>
marker, escalating WARN → CRIT, when a Disassociation-Imminent BTM
Request named that twin within 300 s — a marker, not a sixth alert.

OPEN_SETUP_AP (#80) — WARN on an open SoftAP named like a device
onboarding surface (SETUP-, HP-Setup, HP-Print-, DIRECT-,
roborock-vacuum-, NETGEAR_EXT), behind a hotspot/guest allowlist
checked first. Under-flags on purpose. Basis: NIST SP 1800-36 Vol. A;
Wi-Fi P2P spec v1.5 §3.2.1 (DIRECT- mandates WPA2-PSK).

Tool fingerprints (#74, #68)

ESP32 Marauder and Pineapple MK7 rows land in the formerly empty
signature table, flagged unverified — sourced from research, not a
capture. An unverified row caps reported confidence at MED however many
fields agree.

New data (#77 slices)

Observables only — no thresholds, no alerts, no signatures, because the
attributions #77 asks for have no source verifiable without hardware.

  • WPS vendor strings — Manufacturer / Model Name / Model Number /
    Serial Number (WFA WPS 2.0 §12).
  • Beacon IE-ordering hash — element identity and order, bodies
    excluded (IEEE 802.11-2020 §9.3.3.2; Vanhoef et al., AsiaCCS 2016).
  • TBTT jitter — stddev of the TSF residual against whole Beacon
    Intervals, on the AP's own clock (IEEE 802.11-2020 §11.1.3, §9.4.1.10).

Research corpus (#73)

Curated per-source documents indexed into SQLite FTS5 (research.db,
committed, byte-reproducible). Query layer linked into sloth
(--with-research adds citations to --report); make research-mcp
builds a stdio MCP server over the same functions (not in all). New
[f] Research view — the sources behind each alert kind that has
fired, uncited kinds shown rather than hidden. The content pass took
coverage from 24/60 to 59/60 citable kinds; the coverage guard is now enforcing — a new uncited detector
fails the suite.

Fixed

  • ALERT_KEY_LEN 96 → 192: on IPv6 flows every cleartext-credential
    user on one flow deduped into a single alert (d7c23f8).
  • Build is warning-clean on gcc 13 across all four variants (was 8).
  • Research-view citation lookup keyed on the abbreviated display title
    and silently lost matches; now keyed on alert_type_name().

Known gaps

  • #77's per-vendor stack table, two-radio Pineapple correlation and
    verdict scorer need capture hardware.
  • Marauder / MK7 fingerprint rows remain unverified until captured.
  • The v1.8.0 gaps (chunked portal responses, --hop absence, DHCP
    option 114, request bodies) are unchanged.

Passive throughout. Nothing here transmits, scans, or modifies kernel
state — see MISSION.md §2.

sloth v1.8 — the 802.11 management surface

Choose a tag to compare

@SpaceTrucker2196 SpaceTrucker2196 released this 26 Aug 01:58

sloth v1.8.0 — the 802.11 management surface

Fourteen issues, closing the entire open backlog. Where v1.7 landed the
first passive detector batch, this release parses the 802.11 management
and control surface those detectors were guessing around — and adds ten
detectors on top of it.

Suite went 5906 → 6687 assertions. Alert types 33 → 48.


⚠️ Upgrade note — SQLite schema v3 → v4

An existing --db file will refuse to open, with:

sloth: db schema v3, this build writes v4 — use a separate file

That is deliberate and it is the designed behaviour, not a failure: the
version is checked before the schema is applied precisely so a mismatch
produces this message rather than an error on whatever statement first
hits a missing column.

Four columns wanted a bump across three issues, and each on its own was
a bad trade. Taken together it is one break instead of four:

Column Table Why
phy_confirmed pnl_clients a PHY tier corroborated by an association request rather than inferred from probes
downgrade_flags, akm_bits beacon_aps historical posture — "was this AP in transition mode in March"
nocert_sessions, nocert_no_hello, last_nocert_sta, last_nocert_ts rogue_radius CVE-2023-52160 evidence behind the alert

Start a new database file. The old one stays readable with any v1.7
build and with sqlite3 directly.

JSONL is additive only — every new field is new, nothing changed
shape, and a consumer ignoring them sees exactly what it saw before.
The CLI is unchanged.


New detectors

Ten alert types, each a passive rule over frames sloth now parses.

BTM_ABUSE (#59) — 802.11v BSS Transition Management used as a
deauth-equivalent. A Request with Disassociation Imminent set moves a
client with no deauth frame at all, so DEAUTH_FLOOD never saw this
attack. Gated on that bit rather than on rate alone: a Request without it
leaves the client free to decline, and rate alone fires on every busy
enterprise AP doing ordinary load balancing.

ASSOC_FLOOD and the association-request picture (#60) — what a
client asked for, beside what it settled for. There is no "granted
AKM" to compare against: an association response carries no RSN Element
outside FT and OWE (§9.3.3.7), so the downgrade is measured across
successive requests. A client that asked for SAE and came back asking for
PSK was moved, and that retry is the runtime signature of CVE-2023-52424
from the client side.

WPA_DOWNGRADE (#62) — an AP advertising a weaker lane beside its
primary one: PSK and SAE both on offer, an OWE BSS with an open
companion, MFP capable-but-not-required on SAE, or WPA1 beside RSN. None
is an attack; each is the prerequisite CVE-2023-52424 and Dragonblood
need. A 30-second observation floor keeps --hop from alerting on every
AP it brushes past.

PEAP_NO_SERVER_CERT (#65) — CVE-2023-52160. A TLS-in-EAP
session that reached EAP-Success with no ServerHello or Certificate ever
presented. ROGUE_RADIUS warns about attacker infrastructure; this warns
that your own fleet would fall for it, and stays silent until a device
actually does. Millions of Android and ChromeOS handsets shipped in that
state.

CSA_ABUSE (#63) — Channel Switch Announcement misused. Cheaper and
quieter than a deauth flood, and it works on firmware that ignores
deauth. Three shapes: a forged transmitter, a storm of distinct target
channels, or a destination hosting a known evil twin.

RRM_SURVEY_ABUSE (#61) — 802.11k Beacon Requests used to enumerate
the airspace through someone else's radio. The discriminator is not the
rate: a legitimate AP asks about networks it advertises.

RTS_FLOOD and BLOCKACK_ATTACK (#64, #70) — airtime denial of
service, and the Bl0ck attack (arXiv 2302.05899): a spoofed Block-Ack
Request that forces a peer's receive window past queued frames. Quiet by
design — what the operator sees is a client that stopped working.

CAPTIVE_PORTAL (#69) — a connectivity-check probe answered by
something other than the real endpoint. Three independent signals, so a
portal evading one still has to pass the others.


New data

HT / VHT / HE / EHT operation elements (#66) — channel width
(20/40/80/160/320, and non-contiguous 80+80) and the durable 6 GHz
channel fix
: the beacon channel came from the DS Parameter Set, which
much 6 GHz gear omits entirely. HE Operation's 6 GHz Operation Info is
authoritative and now wins. channel_source records which IE supplied
the number, so a wrong channel is attributable.

sloth previously treated every AP as 20 MHz, which makes any airtime
answer wrong by up to a factor of sixteen.

Multi-Link Element decode (#67) — not a new feature so much as a live
mis-count. Sequence-number correlation links a radio's randomised
addresses by their shared counter; a Wi-Fi 7 Multi-Link Device's radios
have independent sequence spaces and never correlate, so one handset
read as two or three devices. transit_canonical_mac() now consults the
MLE first — the protocol asserting the identity beats sloth inferring it.

802.11 data frames reach the IP decoder (#72) — the monitor radio and
the IP capture used to see disjoint worlds. On an open network they are
now one picture. Encrypted frames are rejected rather than parsed, per
MISSION §2.

HTTP response-side parsing (#71) — status, a bounded body, and
request/response pairing. The design decision that matters: sloth does
not reassemble TCP, so the parser reports complete, partial or
chunked-and-undecoded rather than pretending. "Different" and
"incomplete" are different answers.

Control-frame counters (#64) — RTS/CTS/ACK/Block-Ack per channel, the
measured half of occupancy against the QBSS Load an AP reports about
itself. CTS and ACK carry only a Receiver Address, so they attribute to a
channel and nothing finer.

Tool-fingerprint matcher (#68) — ships with an empty signature
table, on purpose
. A frame layout can be built from a specification; a
tool's fingerprint cannot. Invented rows would pass every test and match
nothing on the air, which is worse than an empty table because it looks
like coverage. The PMKID-harvest half needs no signatures and works
today.


Repo changes

  • agents/AGENTS.md now records the hand-built-frames test policy
    explicitly, and the needs-pcap-fixture label means "wanted as
    follow-up validation" rather than "blocked".
  • $(TEST_BIN) gained header prerequisites. A header-only change
    never rebuilt the test binary — every such change in this repo's
    history was tested against a stale build.

Fixed

  • Wi-Fi 7 PHY-tier labelling, broken mid-run by an operation-IE
    refactor that dropped the line setting has_eht. It compiled clean and
    the suite stayed green because nothing covered the PHY ladder from
    beacon IEs. Now one test per rung.
  • KARMA_SSID_THRESH was defined twice with the same value.

Known gaps

Named rather than hidden:

  • A rogue portal that chunks its HTTP response evades the
    captive-portal body check. It does not evade the DNS or TLS checks.
  • --hop makes absence a sample, not a fact. Every detector that
    could infer from "we never heard X" requires positive evidence first.
  • #68's signature rows need captures from a lab rig.
  • DHCP option 114 is not parsed, so the RFC 8910 portal-origin
    cross-check has nothing to compare against.
  • Request bodies are not captured, so the credential-POST hint in
    #69 is unbuilt — a request body is where passwords live, and that is a
    MISSION §2 conversation rather than a parser task.

Passive throughout. Nothing here transmits, scans, or modifies kernel
state — see MISSION.md §2.

sloth v1.7 — Wi-Fi attack-detection suite

Choose a tag to compare

@SpaceTrucker2196 SpaceTrucker2196 released this 05 Jul 13:47

Passive by charter. sloth still never injects, scans, cracks, or modifies kernel state. Two narrow, operator-authorised, opt-in/opt-out carve-outs exist (MISSION §2): --hop retunes sloth's own monitor receiver (no frame sent), and — new this release — mDNS discovery advertises the data socket via a separate daemon when the operator has bound it to a routable address.

This release lands the passive Wi-Fi attack-detection batch (issues #29–#34) on top of v1.6.0. Every detection signal is covered by hand-crafted RFC-byte and seeded-state tests — 3604 assertions, warning-clean.

New detectors (alerts)

  • SSID_CONFUSION (CRIT, T1557.004) — the on-air fingerprint for CVE-2023-52424: the same SSID on a second BSSID with a downgraded RSN posture (WPA3→WPA2, MFP required→off, GCMP→CCMP), or an 802.1X-Enterprise SSID cloned as PSK. (#32, #31)
  • MGMT_FUZZ (WARN/CRIT, T1499) — malformed beacon IEs (length overrun, oversize SSID, truncated RSN) from mdk4 mode m / crafted aireplay frames. The beacon parser now counts anomalies instead of silently dropping them. (#33)
  • ROGUE_RADIUS (WARN/CRIT, T1557.004) — a new EAP inner-frame parser (RFC 3748) decodes the 802.1X EAP conversation: CRIT when a weak inner method (EAP-MD5/GTC) is offered, WARN on a real-identity leak with no anonymous outer identity — the eaphammer / hostapd-wpe lure. (#31)

KARMA / PineAP (#30)

A dedicated VIEW_KARMA on key [y] — one ranked row per BSSID beaconing an abnormal SSID set, scoring:

  • PNL overlap + Jaccard % — how closely the advertised SSIDs mirror what nearby clients actually probe for (the PineAP beacon-response tell).
  • deauth-then-lure — a concurrent deauth flood (knock clients off, then answer their reconnection probes).
  • IE-uniformity — one identical IE fingerprint across every spoofed SSID (legit multi-VAP APs vary per VAP).

High-score lures also get a deep-red badge on the dashboard Beacons panel, the way THREAT_IP flags a hostile host.

Discovery (#29)

When --data-socket is bound to a routable address, sloth drops an Avahi service file so the sloth-ios client discovers _sloth._tcp by name. sloth transmits nothing itself (avahi-daemon announces); loopback/UNIX sockets never advertise; disable with --no-discovery.

Also

  • Passive sensor abstraction + factory converge loop groundwork (#28, #34).
  • New JSONL records / fields: sensor, wifi_merged, beacon fuzz_* counters.

Full per-alert reference in docs/views/alerts.md; KARMA view in docs/views/karma.md.

sloth v1.6 — Wi-Fi SIGINT expansion

Choose a tag to compare

@SpaceTrucker2196 SpaceTrucker2196 released this 05 Jul 05:45

Passive by charter. sloth still never injects, scans, or modifies kernel state — the one exception is the opt-in --hop scheduler retuning sloth's own monitor interface, gated behind an explicit MISSION §2 carve-out.

This release lands the full Wi-Fi SIGINT roadmap (issues #20–#28) on top of v1.5.1.

Channel coverage & multi-radio

  • Adaptive passive channel-hop scheduler (--hop, opt-in, off by default) — activity-scaled dwell with decay, an nl80211 set_channel platform op, and a live scan bar on the monitor-radio row. (#22)
  • Multi-radio passive observation merge — fold observations from more than one monitor adapter into one entity-keyed world model (AP BSSID / STA MAC), retaining per-radio provenance: seen_by, sensor_mask, best_rssi/best_sensor. Identity map with a single adapter. (#21)
  • Passive sensor abstraction layer — the typed registry every observation source registers through; the seam for future RF families (BLE / Zigbee / SDR / GPS / ADS-B / Meshtastic / CAN). (#28)

Detection

  • New alert rules: BEACON_FLOOD (T1498.001) and AUTH_FLOOD (T1499).
  • Beacon QBSS Load IE parsing — AP self-reported occupancy.
  • 6 GHz channel-number derivation on the monitor path.

Assessment & forensics

  • RF session baseline + drift detection. (#23)
  • Wireless-hygiene findings in the operator posture report (OPEN/WEP/TKIP/MFP-off/transition/WPS). (#24)
  • Passive site snapshot export/import for repeat assessments. (#27)

UX

  • Monitor-first: dashboard connections/packets bands pivot to STA↔AP associations and 802.11 frames when monitoring; full-screen frame view; launch on the dashboard with noise interfaces hidden. (#25)

Export

  • JSONL: emit each captured packet exactly once; additive sensor and wifi_merged records. (#20)

All builds warning-clean; 3447 test assertions pass.

sloth v1.5.1 — HTTP fingerprint + cleartext-cred breadth

Choose a tag to compare

@SpaceTrucker2196 SpaceTrucker2196 released this 03 Jul 04:31

sloth v1.5.1 — HTTP fingerprint + cleartext-cred breadth

Point release rolling up the follow-up work called out in the v1.5.0
notes: JA4H (HTTP client fingerprint), the remaining cleartext-cred
protocols, and the stateful AUTH LOGIN exchange for SMTP. No new
alert types, no new views — this is protocol breadth and provenance
depth on top of the v1.5 baseline.

JA4H — HTTP client fingerprint

TLS handshakes have carried JA3 + JA4 since v1.5.0. HTTP requests
now get JA4H, the same-family fingerprint for HTTP/1.x clients,
following the FoxIO spec. Emitted alongside the other HTTP fields
in JSONL as ja4h, and computed during the existing header walk
(no new capture surface).

Format is 49 chars: a(10) _ b(12) _ c(12) _ d(12).

ge 11 n n 02 en _ 8daaf6152771 _ 000000000000 _ 000000000000
│  │  │ │ │  │
│  │  │ │ │  └── first 2 chars of Accept-Language ("00" if absent)
│  │  │ │ └───── count of non-cookie / non-referer headers
│  │  │ └─────── 'r' if Referer header present, 'n' if not
│  │  └───────── 'c' if Cookie header present, 'n' if not
│  └──────────── HTTP version: "11" / "10" / "20"
└─────────────── first 2 chars of method, lowercased

Section b hashes header names in observed order (HTTP clients
don't reorder headers between runs, so order carries signal, unlike
JA4 for TLS which sorts). Sections c and d hash sorted cookie names
and sorted cookie name=value pairs, so cookie insertion order
doesn't affect the fingerprint.

See docs/views/http.md for the section
breakdown and docs/wiki/jsonl-schema.md
for the wire format.

Cleartext-cred coverage expanded

CLEARTEXT_CRED (T1040) fired on HTTP Basic and FTP in v1.5. It now
covers six protocols:

Protocol Port(s) Shape
HTTP Basic 80/8080/8000 Authorization: Basic <b64> header
FTP 21 USER <name> / PASS <value>
POP3 110 USER <name> / PASS <value>
IMAP 143 <tag> LOGIN <user> <pass> (quoted OK)
SMTP AUTH PLAIN 25/587 Single-shot AUTH PLAIN <b64> SASL
SMTP AUTH LOGIN 25/587 Three-line stateful SASL exchange

Each protocol has its own snoop module (src/pop3_snoop.c,
src/imap_snoop.c, src/smtp_snoop.c) modelled on the FTP shape.

AUTH LOGIN needed per-flow state because the username arrives on
a separate line after the AUTH LOGIN verb. Small ring of 16 flow
records inside smtp_snoop.c, keyed by (client, server, port),
LRU-evicted. Server-directed payloads (334 prompts) don't touch
state — the direction check is dst_port == 25 || dst_port == 587.

Guardrail preserved across all six: the password value is never
inspected past marking the flow. The recorder's public API has no
password parameter, enforcing the invariant at the module boundary.
The posture report and JSONL schema doc both call this out
explicitly.

Only Telnet remains uncovered — char-by-char across many small
packets, needs multi-packet reassembly, doesn't fit sloth's current
line-oriented snoop shape. Deferred to a follow-up when the snoop
layer gains per-flow byte reassembly.

Test hygiene

The updater tests hard-coded literal 1.4.0 / 1.5.0 strings when
they were written pre-v1.5. After the v1.5.0 version bump, one
assertion regressed. Decoupled the tests from SLOTH_VERSION — the
"up to date" test uses SLOTH_VERSION itself, the "newer" test uses
"99.99.0" and the "older" test uses "0.0.1", so future bumps
won't red the suite.

Stats

  • 3 substantive commits since v1.5.0
  • 6 cleartext-cred protocols (+4)
  • 3252 test assertions (+135)

Upgrade

git pull
make

Nothing removed; the JSONL schema is additive (new ja4h field on
http records, more protocol values on cleartext_cred). Existing
consumers keep working.

sloth v1.5 — synthesis, provenance, operator posture

Choose a tag to compare

@SpaceTrucker2196 SpaceTrucker2196 released this 03 Jul 02:09

sloth v1.5 — synthesis, provenance, operator posture

The synthesis release. v1.4 broadened protocol detection; v1.5
finishes the loop between "what did we see" and "what does it mean":
every alert now carries a MITRE ATT&CK technique, TLS handshakes get
JA4 alongside JA3, cleartext credentials are surfaced as first-class
events (with an ironclad "no password value, ever" guardrail),
devices carry an explainable risk bucket, and shutdown produces a
signed-off posture report. A locally-populated manifest also gives
the operator opt-in version-awareness without sloth ever touching
the network.

MITRE ATT&CK tagging

Every alert type now maps to a canonical ATT&CK technique via
alert_technique() in src/alerts.c. The value is populated on
fire(), shown on the alert detail panel, and emitted as a
"technique" field in the JSONL log. Host-posture alerts (like
NO_MONITOR_MODE) correctly map to the empty string so the field is
omitted rather than tagging the operator's own rig as an adversary
technique.

Coverage: the brute-force family maps to T1110.001; Kerberos
pre-auth spray to T1110.003; LDAP enumeration to T1087.002; the
threat-intel / DNS-tunnel family to T1071 / T1071.004; ARP spoof to
T1557.002; rogue DHCP / RA / evil-twin to T1557; weak TLS to T1600;
HTTP attack path to T1190. See docs/views/alerts.md for the full
table.

JA4 client fingerprinting

TLS ClientHellos now carry a JA4 fingerprint alongside the
existing JA3 (ja4 field in tls_log_entry_t, emitted in JSONL as
"ja4"). JA4 solves JA3's extension-order sensitivity by sorting the
cipher and extension lists before hashing, so Chrome/Firefox
extension randomization no longer changes the fingerprint.

Format (FoxIO spec) is 36 chars total:

t 13 d 15 16 h2 _ 8daaf6152771 _ b0da82dd1658

Section a encodes protocol / version / SNI flavour / cipher count /
extension count / ALPN edge chars. Sections b and c are truncated
SHA-256 over the sorted lists. New embedded SHA-256 module
(src/sha256.c) verified against RFC 6234 test vectors.

Cleartext credential exposure

New CLEARTEXT_CRED alert (WARN, T1040 Network Sniffing) fires when
sloth observes authentication material sent in the clear over:

  • HTTP Basic (TCP/80, 8080, 8000)
  • FTP USER/PASS (TCP/21)

Guardrail — no password field, ever. The recorder module has no
code path that accepts, stores, or hashes password bytes; the JSONL
schema has no field for it; the alerts detail panel doesn't show it.
The posture report calls the rule out explicitly. Adding a password
field would be a scope violation on the passive-observation contract
in MISSION.md §2. See docs/wiki/jsonl-schema.md for the schema
policy.

Additional protocols (POP3, IMAP, SMTP AUTH LOGIN, Telnet) are staged
for a follow-up landing.

Passive device risk scoring

Every device row in [g] Devices now carries a Risk column with
a heat-graded bucket: LOW / MED / HIGH / CRIT. The
score is a transparent weighted sum of six independently-observable
signals — no ML, no black box:

Signal Weight Meaning
RANDOM_MAC 1 locally-administered MAC (privacy MAC)
UNKNOWN_VENDOR 1 OUI not in the embedded table
NO_HOSTNAME 1 no DHCP/mDNS/NBNS resolution
PROBE_ONLY 1 probe frames, no STA/beacon association
CLEARTEXT_CRED 3 device leaked a credential in the clear
ALERT_TAGGED 3 an active alert names this device's IP

The signal bitmask is emitted in JSONL (risk_signals) so external
consumers can reproduce the score.

Posture report export

New --report FILE.md and --report-json FILE.json write a
session-summary artifact at shutdown. Rollup covers:

  • Alert counts by severity (LOW / WARN / CRIT).
  • MITRE ATT&CK technique breakdown with hit counts.
  • Cleartext credential exposures, with the "never a password value"
    reminder inline.
  • High-risk devices (HIGH + CRIT only, with signal bitmask).
  • Session start / end / duration.

Written once at shutdown after the main loop drains, so the file is a
signed-off session record rather than a mid-run snapshot. See
docs/wiki/posture-report.md.

Version check-in

New --check-manifest FILE reads a locally-populated JSON manifest
and displays "update available" in the help view when its "latest"
field exceeds SLOTH_VERSION. sloth never fetches from the network
itself
— populating the manifest is the operator's responsibility.
The reference script at examples/updater/check-latest.sh is the
recommended producer, but a systemd timer, cron job, or any other
policy-appropriate process works.

Reader is cache-aware: stat()s the manifest at most once per 60 s
and only re-reads on mtime change. Missing / malformed files set an
error flag and preserve last-good state so a transient failure
doesn't clobber the display.

Manifest format is documented in docs/wiki/manifest-format.md;
architecture in docs/wiki/version-checkin.md.

Platform parity

BSD / macOS now detects WiFi monitor-mode via SIOCGIFMEDIA +
IFM_IEEE80211_MONITOR, matching Linux's /sys/class/net/<if>/type
detection. NO_MONITOR_MODE fires identically across both platforms.

Bugs fixed

Three reporter-filed bugs closed pre-v1.4 tag (rolled up here for
completeness against the last release cadence):

  • #13 — active view gets first refusal on shadowed keys via a
    centralized view_claims_key table.
  • #14 — TLS supported_versions extension GREASE-filtered so
    Chrome/Firefox TLS 1.3 handshakes no longer mislabel as "TLS".
  • #15 — dashboard interfaces band honours the iface hide election.

Security policy

SECURITY.md replaced the stock GitHub template with a real
supported-version policy (1.4.x archived, 1.5.x now current),
vulnerability reporting instructions via the GitHub Security
Advisory flow, and a pointer to MISSION.md for the passive-only
guarantee.

Stats

  • 16 commits since v1.4.0
  • 28 alert rules (+1)
  • 31 views (unchanged)
  • 3117 test assertions (+371)
  • 2 new external modules: SHA-256, semver

Upgrade

git pull
make
sudo ./sloth --data-socket unix:/tmp/sloth.sock \
             --eapol-dir /tmp/sloth-eapol \
             --check-manifest /var/lib/sloth/version-manifest.json \
             --report /tmp/sloth-posture.md \
             -o /tmp/sloth.jsonl

Nothing removed; the JSONL schema is additive (technique, ja4,
risk, risk_signals fields, plus a new cleartext_cred record
type). Existing consumers keep working.