Passive intercept and decode system for ExpressLRS (ELRS) drone control links operating on 915 MHz LoRa. Detects active drone control and decodes RC channel commands (throttle, roll, pitch, yaw, arm state) transmitted from a ground control station (GCS) to a drone.
ELRS uses LoRa chirp spread spectrum modulation with Frequency Hopping Spread Spectrum (FHSS) to maximize range and resistance to interference. The hop sequence is seeded by the pilot's binding phrase (a 6-byte UID). This system:
- Passively characterizes the RF environment via SDR to determine spreading factor (SF)
- Sits on a fixed channel waiting for the FHSS sequence to cross it and deliver a SYNC packet
- Extracts 3 of 4 UID bytes from the SYNC packet, then brute-forces the 4th (256 guesses)
- Reconstructs the hop table via the ELRS Fisher-Yates/LCG algorithm
- Locks on in real-time using FreeRTOS hardware interrupts and a microsecond timer anchor
- Dewhitens and decodes every subsequent RC data packet
elrs-interceptor/
│
├── README.md ← this file
│
├── phase0/
│ └── sdr_characterize.py ← run on laptop with RTL-SDR before deploying hardware
│
└── firmware/
├── include/
│ ├── elrs_common.h ← shared constants, types, pin definitions
│ ├── phase1_net.h
│ ├── phase2_exploit.h
│ ├── phase3_lockons.h
│ └── phase4_autopsy.h
└── src/
├── main.cpp ← entry point, FreeRTOS task setup
├── phase1_net.cpp ← LR1121 init, CRC, OTA detection, UID extraction
├── phase2_exploit.cpp ← Fisher-Yates LCG, 256-guess brute force
├── phase3_lockons.cpp ← hop task, timer anchor, dead-reckoning
└── phase4_autopsy.cpp ← dewhiten, 11-bit channel unpack, output
| Component | Purpose | Notes |
|---|---|---|
| ESP32 (240 MHz) | Main MCU, FreeRTOS host | ESP32-S3 preferred for faster SPI |
| LR1121 or SX1262 breakout | LoRa transceiver | Semtech LR1121 covers 900MHz + 2.4GHz |
| 915 MHz antenna | Reception range | Yagi or patch for NLOS scenarios |
| RTL-SDR v3 (laptop) | Phase 0 characterization only | One-time setup step |
ESP32 GPIO LR1121 Pin Function
────────── ────────── ────────
GPIO 18 SCK SPI clock (16–18 MHz)
GPIO 19 MISO SPI data out
GPIO 23 MOSI SPI data in
GPIO 5 NSS SPI chip select
GPIO 26 DIO1 Packet interrupt (ISR trigger)
GPIO 14 NRESET Hardware reset
3.3V VCC
GND GND
pip install numpy scipy pyrtlsdr matplotlib# platformio.ini
[env:esp32dev]
platform = espressif32
board = esp32dev
framework = espidf
lib_deps =
jgromes/RadioLib@^6.4.0Connect RTL-SDR, tune to 902–928 MHz band, run:
cd phase0
python sdr_characterize.pyOutput:
Detected SF: 7
Packet rate: ~250 Hz
Hop window: 4000 µs
BW: 500 kHz (confirmed)
SyncWord: 0x12 (hardcoded)
Take note of the detected SF. Update elrs_common.h:
#define ELRS_SF 7 // set from sdr_characterize.py outputcd firmware
pio run --target upload
pio device monitor --baud 115200[PHASE1] Waiting for SYNC packet on 915.000 MHz...
[PHASE1] SYNC caught. OTA v2. UID: 0xA3 0x7F 0x2C [??]
[PHASE2] Brute forcing uid[3]... locked at 0x91 after 73 attempts
[PHASE3] Hop table generated. Anchored at t=0. Hopping...
[PHASE4] ACTIVE | Throttle: 68% | Roll: +12° | Pitch: -3° | Yaw: 0 | ARMED
[PHASE4] ACTIVE | Throttle: 71% | Roll: +8° | Pitch: +1° | Yaw: 0 | ARMED
┌─────────┐
boot → │ HUNTING │ ← fallback on >MAX_MISSED consecutive misses
└────┬────┘
│ SYNC packet received + CRC pass
┌────▼──────┐
│ EXPLOITING│ brute force uid[3] — avg ~128 hop intervals
└────┬──────┘
│ RC data packet CRC pass
┌────▼──────┐
│ LOCKED │ real-time hop tracking + decode
└────┬──────┘
│ >MAX_MISSED misses
└──────────────────► HUNTING
| Parameter | Value | Source |
|---|---|---|
| Physical sync word | 0x12 |
ELRS/src/lib/SX126XDriver/SX126XDriver.cpp |
| Bandwidth | 500 kHz | Fixed across all ELRS 900 modes |
| LCG multiplier | 2891336453 |
ELRS/src/lib/FHSS/FHSS.cpp |
| LCG increment | 1 |
Same |
| LFSR whitening poly | 0x69 |
ELRS/src/lib/FHSS/FHSS.cpp |
| CRC polynomial | 0x07 (CRC8/CCITT) |
ELRS packet spec |
| SYNC packet type | 0x01 |
ELRS OTA packet format |
| RC data packet type | 0x00 |
ELRS OTA packet format |
| OTA v2 UID offset | byte 3 |
ELRS v3.x source |
- This tool is for authorized security research and RF environment analysis only
- Intercepting drone control signals may be subject to local regulations
- ELRS is open-source; all constants above are sourced from the public GitHub repository
- Crossfire (TBS) uses a proprietary protocol — this system targets ELRS only