reachtim

Technical Security

LoRecon: LoRa Reconnaissance on a $20 Board

A teaching walkthrough of a passive LoRa sniffer for cheap ESP32 hardware — the scanning problem it solves, exactly what it can and can’t decrypt, and how the pieces fit together.

My hacking partner and I spent an afternoon in a hotel listening to the LoRa traffic drifting through the building. Mesh nodes relaying each other’s messages. Someone’s sensor phoning home. By the time we packed up, we had 3,630 packets from 266 devices, and 57 messages we could read in plaintext — every one of them opened with a known or default channel key, nothing custom.

That’s what this article is about: what a key like that actually opens, and how LoRecon does the listening.

LoRecon is the listener. It’s firmware for a twenty-dollar ESP32 board that turns it into a self-contained LoRa reconnaissance tool, driven from your phone over its own web interface. We built it — I wrote the firmware, my buddy The-Foe built out the hardware and did the field testing — and I’ll teach it the way I’d want it explained to me, starting from the one fact that makes sniffing LoRa harder than sniffing WiFi.

Why you can’t just listen

On WiFi, one radio hears every other radio on the same channel. LoRa doesn’t work that way. “LoRa” is just the modulation; the protocol on top is left to each network, and every one picks its own frequency, spreading factor, bandwidth, and coding rate, and a receiver has to match all four before it can demodulate anything at all. Get one of the four wrong and you hear nothing at all — just silence, and you never even see it.

So a passive sniffer can’t just “listen.” It has to guess, methodically, across every combination anyone actually uses. LoRecon carries a table of 29 such combinations, covering five network families, and cycles through them one at a time, dwelling about twelve seconds on each before moving on. A full lap is roughly six minutes. Whatever’s in range and transmitting during its slice gets captured, classified, and — where possible — decrypted, before the radio retunes and moves to the next slot.

The 29-configuration scan cycle A row of 29 small blocks grouped into five clusters — Meshtastic (11), LoRaWAN/TTN (7), Helium (6), MeshCore (3), and ISM (2) — with one block highlighted to show the radio's current position in the sweep. SCAN CYCLE 0:00 ~6:00 Meshtastic 11 configs LoRaWAN / TTN 7 configs Helium 6 configs MeshCore 3 configs ISM 2 configs
Twenty-nine frequency/spreading-factor/bandwidth/coding-rate combinations, grouped by the network family that uses them. The highlighted block is wherever the sweep currently sits — roughly twelve seconds per config, about six minutes for a full loop.

That’s also why the scan takes minutes instead of being instant: there’s no shortcut around trying each combination in turn. It’s the same reason a Meshtastic node in your backpack and a LoRaWAN sensor on someone’s roof can share the same air without ever hearing each other — they were never listening on the same “channel” in the first place. Back in that hotel, the six-minute lap is exactly what turned a quiet band into 266 devices: give the sweep enough laps and everything transmitting eventually lands in a slice you’re tuned to.

What it does once it hears something

Once a packet is captured, LoRecon classifies it by protocol and tries to open it. Three things can happen, depending on what it heard.

  • Default-key decryption. It carries 23 known and historically common Meshtastic channel PSKs and tries each against what it captured. For MeshCore, the public channel key is fixed and shipped on every device, so that one’s automatic; a handful of common hashtag-room keys (#general, #emergency, and similar) are derived from the room name and tried too. For LoRaWAN, it checks 16 default AppKeys against any Join Request it catches.
  • Position extraction. A successful decrypt of a Meshtastic POSITION_APP packet yields GPS coordinates, exportable straight to KML or GeoJSON.
  • Replay. Ten storage slots, with a configurable repeat count and delay, for testing how your own gear reacts to a packet it’s seen before.

Everything captured is reachable from a six-tab web UI the board serves itself — over its own access point if there’s no WiFi around — and exportable as CSV or Wireshark-ready PCAP. From there, a Python toolkit that ships in the repo picks up the file: a report command that turns a capture into a prioritized findings page, a map command that plots extracted GPS onto an interactive map, and a topology command that draws the mesh as a graph, labeling nodes with whatever operator name it could decrypt.

The boundary: what a default key doesn’t get you

This is the part I’d want to know first if I were reading instead of writing: a cracked default key does not hand you someone’s private messages.

Meshtastic firmware from 2.5.0 onward split its encryption into two systems. A sweep like this treats them very differently.

Everything a node broadcasts automatically — its position, its telemetry, its node info, its traceroutes, routing packets, and anything typed into a channel (as opposed to a direct message) — is wrapped in one shared AES key per channel. If that key matches one of the 23 known and historically common values LoRecon carries, the database opens it in one pass.

That’s what happened to all 57 readable messages in the hotel capture: every one decrypted with a known key, none with a custom PSK. Two distinct channels were in use — a main one with 261 devices, and a separately named one with 17 — and both decrypted with the identical unmodified default key, which is what actually determined the outcome.

Position broadcasts aren’t uniformly precise, either. Firmware supports a range of reduced-precision settings, each defining a grid cell centered on the reported point rather than blurring the true location — a limit set locally by the app, not the radio link. Of the nodes broadcasting position in this same capture, 91% used some reduced-precision setting; the remaining 9% broadcast full, meter-level precision. Same channel, same key in both cases — just a different setting.

None of that says anything about why any particular node in the capture is set up the way it is. For a lot of Meshtastic’s user base, the public channel is exactly what they want: a way to reach and be reached by other operators nearby, on a network anyone can join without arranging a key first.

Direct messages and admin commands are a different story. Since firmware 2.5.0 they get a second, independent layer — Curve25519 public-key encryption, keyed to the recipient’s device, whose private key never leaves that device. No database of default values gets you through that layer, because there is no shared secret to guess. This is the correct primitive, used correctly.

What a default channel key does and doesn't open A LoRa packet passes through a shared channel-PSK layer. Automatic broadcasts such as position, telemetry, and node info become plaintext once that layer opens. Direct messages and admin commands carry a second Curve25519 layer that a channel key never opens. ENCRYPTION BOUNDARY LoRa packet, captured over the air CHANNEL PSK · AES-256-CTR shared key — often still the factory default AUTOMATIC BROADCASTS position · telemetry · nodeinfo routing · channel messages plaintext once the key hits DIRECT MESSAGES & ADMIN firmware 2.5.0 and newer Curve25519 key pair stays encrypted — no key for this
The channel PSK is one shared secret protecting everything a node sends automatically. A hit opens it all at once. Direct messages and admin commands sit behind a second, independent lock keyed to one device’s private key — a channel-PSK hit never reaches it.

If an operator sets a custom channel PSK, the 23-key database doesn’t match anything, and the broadcasts stay encrypted blobs too. A sweep like this checks the key, not the name — renaming a channel has no effect on it. Setting a custom PSK is a thirty-second job in the app, and it’s the one change that alters what a listener sees.

Before firmware 2.5.0, direct messages were just channel messages with a destination field, protected by the same shared key as everything else. Pre-2.5.0 devices don’t get the Curve25519 protection at all. So if you’re auditing an older deployment, keep in mind that “we’re on the default key” and “our DMs are private” cannot both be true at once.

From antenna to report

None of this needs a laptop in the field, and none of it needs a cloud service. The whole pipeline fits on the board and the phone in your pocket.

LoRecon's end-to-end data path LoRa RF flows one-way into the ESP32 board, which serves a web UI to a phone or laptop over WiFi. Captures export as CSV, PCAP, KML, or GeoJSON to Python tools that run entirely offline. END-TO-END DATA PATH no cloud in this picture — a phone is enough, and every analysis step after export runs offline LoRa RF passive reception receive only (no transmit path) ESP32-S3 + SX1262 scans & decrypts WiFi Phone or laptop web UI, no install export Python toolkit report · map · topology
CSV, PCAP, KML, and GeoJSON move the data from device to disk; `lorecon report`, `lorecon map`, and `lorecon topology` turn a saved capture into a findings page, a GPS map, and a mesh graph — all of it running on a machine that never has to touch the internet.

The RF leg is drawn one-way on purpose. LoRecon never transmits — replay writes to storage slots you trigger manually, but nothing goes out over the air unless you ask it to, and normal operation is receive-only from power-on. That’s a deliberate design line.

Choosing hardware

Four boards run the same firmware and the same web UI; the differences are about what you need in the field, not what the tool can do.

Board Why you’d pick it
Heltec WiFi LoRa 32 V3 Cheapest, most proven. Start here.
Heltec WiFi LoRa 32 V4 Optional L76K GPS, native USB
LilyGO T3-S3 SD card slot — log PCAP/CSV to disk in the field
LilyGO T-Beam Supreme SD, GPS, battery management, and 8 MB of PSRAM

That last column, PSRAM, earns its keep. A long unattended capture dies of heap fragmentation on a bare ESP32 long before anything conceptually interesting happens to it. The T-Beam Supreme with PSRAM enabled holds roughly 130 KB of minimum free heap after twenty hours with fifty devices in range, against about 53 KB without. Most of the recent release cycle was stability engineering of exactly that flavor — chunked API responses, heap guards, crash context that survives a reboot. It’s unglamorous work, and it’s the difference between a tool that runs on the bench and one that’s still running when you come back to it.

Try it yourself

There’s a browser-based installer, so getting a board running doesn’t require touching a toolchain:

https://haksht.github.io/lorecon/install/ — plug the board in, open the page in Chrome or Edge, click flash.

If you want the Python analysis toolkit too — report, map, topology, and the rest — that lives in the source repo, not the binary release, so you’ll want to clone it:

git clone https://github.com/haksht/lorecon.git
cd lorecon
python -m venv venv
venv\Scripts\activate.ps1        # or ./venv/bin/activate on macOS/Linux
pip install -e .

lorecon --help confirms the install worked. From there, lorecon report capture.csv is the first thing worth running against anything you capture.

Building the firmware yourself is PlatformIO, with OTA updates available after the first USB flash. MIT licensed. The current release is 2.6.0.

Note: This is for networks you own or have written permission to test. Passive reception is generally legal in the US; interception of content can implicate 18 U.S.C. § 2511, and the rules vary considerably by jurisdiction. The default-key testing exists to demonstrate a risk, not to hand anyone a way into someone else’s traffic.

Learn more

The repo’s docs/ folder is the fuller version of everything above:

Credit where it’s due

I wrote the firmware and the tooling. The hardware side — board selection, the antenna and power work, and the field testing that turned up most of the bugs worth fixing — is The-Foe, who I share the haksht org with. A sniffer that works on the bench and falls over after six hours in a bag is not a sniffer, and finding that out is its own skill.

The repo is at github.com/haksht/lorecon. If it’s useful to you, a star helps other people find it.