Share
## https://sploitus.com/exploit?id=31E02D09-0758-5A03-80F6-AE37C141D3EF
# Stopping Meshtastic `from`-field spoof attacks โ€” shape-detection defenses + hardware-bound identity

**Author:** mattdeering
**Research window:** 2026-04-23 through 2026-04-24
**Scope:** authorized firmware-security research on a small local-mesh fleet โ†’ four firmware patches proposed upstream, with live-attack evidence

---

## TL;DR

Meshtastic broadcast text packets have **no signature**. Anyone holding the channel PSK can rewrite the `from` field and transmit impersonation packets that most of the mesh accepts as legitimate. This is the attack class behind:

- the **MKE mesh bot** Discord complaints (one rogue node impersonating named community members),
- **CVE-2025-55292** (the DEF CON 33 "๐Ÿฅท" incident โ€” ~2000 nodes' longnames overwritten via NodeInfo forge),
- **GHSA-45vg-3f35-7ch2** (Cezar Lungu Feb-2026 disclosure โ€” NodeInfo spoof forces HAM-mode flag, downgrading PKC DMs to plaintext).

This repo is the public reference for **four small upstream patches** that together stop the attack today and unblock the permanent fix:

| # | Patch | Target | Blocks |
|---|---|---|---|
| A | **self-RX anomaly drop** | `meshtastic/firmware:develop` | spoofs claiming `from = our own nodeNum` |
| B | **hop_start anomaly drop** | `meshtastic/firmware:develop` | every spoof going through standard `Router::send` โ€” stops propagation at every defended hop mesh-wide |
| C | **HWIdentity Tier A** (no eFuse burn) | `meshtastic/firmware:develop`, fixes #8211 | factory reset + firmware flash preserve identity โ†’ safe rollout path for signing |
| D | **Two fix-ups for PR #9610** | `weebl2000/meshtastic-firmware:fix/xeddsa-review` | unblocks the XEdDSA signing work that is the long-term permanent fix |

Patches A + B catch 100% of the attack shapes the in-wild attackers (MKE bot, DEF CON ๐Ÿฅท, Cezar Lungu PoC) use today. Patch C makes the rollout of signing (via upstream #7602 + #9610) safe without breaking peer first-seen-wins caches. Patch D helps land the signing work upstream.

Every patch is โ‰ค250 LOC, ships with a compile-time opt-out, and has been built + flashed + live-attacked on real hardware.

---

## The attack

`meshtastic_MeshPacket`'s `from` field is unsigned. On a broadcast packet (`TEXT_MESSAGE_APP`, `NODEINFO_APP`, `POSITION_APP`, `TELEMETRY_APP`, `TRACEROUTE_APP`), the channel PSK provides confidentiality but **no authenticity**. Any node holding the PSK โ€” which includes every node on the default `AQ==` PSK across the entire global public mesh โ€” can construct a valid encrypted packet claiming to be from any other node.

**Upstream state:** the canonical fix is XEdDSA packet signing. Jonathan Bennett's PR #7602 introduced the skeleton in 2025 (still draft, stale since Feb 2026). Weebl2000's PR #9610 has real substance (signature covers `fromNode + packetId + portnum`, drops on verify failure, sets `HAS_XEDDSA_SIGNED` bit) but stuck in CHANGES_REQUESTED over two small UX items that Patch D here resolves.

**What shape-detection buys you in the meantime:** both defenses target a specific structural artifact that the standard `meshtastic-python` + `sendText(fromId=...)` attack path cannot avoid โ€” the attacker's `Router::send()` contains:

```cpp
if (isFromUs(p)) p->hop_start = p->hop_limit;
```

For a legitimate sender, `isFromUs(p)` is true โ†’ `hop_start` is stamped equal to `hop_limit`. For a spoofer who writes `p->from = VICTIM_NODENUM`, `isFromUs()` returns false at the attacker's own Router::send โ†’ `hop_start` stays 0 on the wire. Legit relayed packets preserve the original stamp (only `hop_limit` decrements), so `hop_start > 0` at every hop. `hop_start == 0 AND hop_limit > 0` is shape-impossible for a legitimate origination and is what every real-world attacker we've seen produces.

The defenses live in `Router::perhapsHandleReceived()`, fire on any OTA packet, are portnum-agnostic (catch NodeInfo, Text, Position, Telemetry alike), and work at every defended node โ€” not just the impersonated victim. ROUTER_LATE running this firmware refuses to relay spoofed packets โ†’ the 5W propagation pathway that makes spoofs mesh-wide in the first place is cut.

---

## Corpus validation (zero false positive claim)

Before deploying, we ran the hop_start check against a multi-day capture of real local-mesh traffic:

- **670 legitimate OTA packets** โ†’ `hop_start` distribution `{1, 2, 3, 4, 5, 6, 7}` uniformly
- **30 packets with `hop_start = 0`** โ†’ every one was an attack packet we generated ourselves during research, cross-verified by packet ID + payload length

**Zero legitimate packets** would have been blocked by the shape-detection defenses across that corpus. The selectivity is empirical, not theoretical.

`tools/extract_baseline_metrics.py` reproduces the analysis on any meshlogger JSONL archive.

---

## Live attack evidence

The full write-ups live in `evidence/` with per-packet timestamps, packet IDs, and A/B cross-matches against undefended witnesses.

- **`evidence/phase_0/BASELINE.md`** โ€” unmitigated attack succeeding. Shows the attack propagating via a 5W ROUTER_LATE to nodes in a different physical location (cross-matched packet ID proves relay).
- **`evidence/phase_1/SUMMARY.md`** โ€” Phase 1a self-RX test: 21 SPOOF drops, 10/10 unique packet IDs dropped, 0 false positives.
- **`evidence/phase_1/MESH_PREVENTION.md`** โ€” Phase 1e hop_start test: 20 drops, 10/10 unique packets dropped (including relayed copies at `hop_limit=2`), 0 false positives. Also includes A/B cross-match with an undefended witness node in the same RF neighborhood.
- **`evidence/phase_3/SUMMARY.md`** โ€” HWIdentity Tier A persistence test: pubkey bit-identical across two factory-reset cycles.
- **`evidence/phase_3/COMBINED_5LAYER_TEST.md`** โ€” combined firmware (all 4 patches + upstream XEdDSA base) live attack: **19 drops / 9 unique packet IDs caught / 0 false positives / both Phase 1a and Phase 1e firing in the same burst**. Identity stable.

---

## Proposed PRs

All four PR bodies are ready-to-paste in `pr-bodies/`:

- [`pr-bodies/PR_A_self_rx.md`](pr-bodies/PR_A_self_rx.md) โ€” self-RX anomaly drop
- [`pr-bodies/PR_B_hop_start.md`](pr-bodies/PR_B_hop_start.md) โ€” hop_start anomaly drop
- [`pr-bodies/PR_C_hw_identity.md`](pr-bodies/PR_C_hw_identity.md) โ€” HWIdentity Tier A
- [`pr-bodies/PR_D_xeddsa_fixup.md`](pr-bodies/PR_D_xeddsa_fixup.md) โ€” fix-ups for upstream #9610

The firmware branches these PRs open from are pushed to the companion fork: [`nightjoker7/firmware`](https://github.com/nightjoker7/firmware) (branches `defense/self-rx-drop`, `defense/hw-identity-tier-a`, `xeddsa-fixup`).

PRs are independent โ€” reviewers can take any subset in any order.

---

## Reproducing the tests

See [`REPRODUCIBILITY.md`](REPRODUCIBILITY.md). Requires two Meshtastic dev boards (ESP32-S3 and/or nRF52840), matching region/preset, and 30 minutes.

`tools/` contains the minimal helper scripts:

- `attack_test_capture.py` โ€” dual-node pubsub watcher for the attack test
- `test_hwidentity_v2.py` โ€” two-cycle factory-reset persistence test for Patch C
- `extract_baseline_metrics.py` โ€” corpus-validation script

---

## Honest limitations

- **Two-node test setup.** Not fleet-tested at scale. Shape-detection logic is unchanged across node count, but soak-time observation on a large fleet would be valuable.
- **Legacy-firmware risk for hop_start drop.** If firmware old enough to not stamp `hop_start` on origination exists in a fleet, its legit traffic would be blocked. We observed 0 such devices in our corpus. `-DMESHTASTIC_DISABLE_SPOOF_HOPSTART_DROP=1` opts out per-device.
- **Sophisticated attackers who hand-craft raw protobuf and manually stamp `hop_start = hop_limit`** defeat the shape check. XEdDSA signing (via #9610) catches that case; Patch D helps unblock the upstream work to get there.
- **Tier A HWIdentity doesn't resist a firmware dump attacker** (same threat surface as today โ€” today the private key is already in `/prefs`). Tier B (eFuse HMAC burn) is the future follow-up, out of scope here.
- **Tested only on ESP32-S3 + nRF52840.** Portduino, STM32, and RP2040 paths compile clean but aren't hardware-tested. Patches fall through gracefully on targets missing the relevant primitives.

---

## What's not in this repo

- **Attack firmware source code.** It's in a clearly-labeled research branch on the firmware fork, gated behind `-DSPOOFTEST_ENABLED=1`, and has never been merged into any tracked branch. Not duplicated here.
- **Raw meshlogger JSONLs.** Contain traffic from real community members whose packets we incidentally captured; not ours to redistribute.
- **Firmware binaries.** Reviewers build from source; binary MD5s are noted in the evidence files but the binaries are not hosted here.
- **HackRF IQ captures.** Multi-GB files, host-specific.

---

## Thanks

To the Meshtastic maintainers and contributors already working on the permanent fix โ€” especially jp-bennett (XEdDSA skeleton #7602) and weebl2000 (#9610). This work is meant to complement those efforts, not replace them: ship protection for the live attack class today, unblock signing for tomorrow.