Academic project | Wireless Communications in ITS (WCI) · Master's in Wireless Communications · Universidad Politécnica de Madrid (UPM)
Author: Cristian Ayuso Ferrón
Hardware: 2× Ettus Research USRP B210 | Stack: Python 3, UHD 4.6, pyModeS v3, PySide6
This project is intended exclusively for educational and research purposes.
Transmitting on 1090 MHz or any aeronautical frequency without authorization is illegal in most jurisdictions. All RF transmission tests must be conducted in a controlled laboratory environment using a direct coaxial connection with appropriate attenuation — never radiating over the air.
The author is not responsible for any misuse of this software.
ADS-B (Automatic Dependent Surveillance-Broadcast) is the open protocol used by all modern commercial aircraft to broadcast their position, altitude, speed and identity on 1090 MHz. Because it has no encryption or authentication, it is vulnerable to spoofing and injection attacks.
This PoC implements a full real-time ADS-B receiver and Intrusion Detection System using software-defined radio. It has three functional phases:
- Reception — decode real ADS-B traffic from aircraft overhead
- Emulation — inject synthetic ADS-B frames via a second SDR over a coaxial loopback
- Detection — flag physically impossible or logically inconsistent frames in real time
The physical setup for the loopback spoofing test uses two Ettus B210 SDRs connected via coaxial cable and a 30 dB attenuator. This configuration allows the TX SDR to inject synthetic ADS-B frames directly into the RX SDR without radiating over the air, keeping the experiment safe and contained within the laboratory.
Two Ettus USRP B210 units in loopback configuration: TX/RX port → 30 dB attenuator → RX2 port. The attenuator prevents DAC saturation on the receiver while maintaining a clean signal path.
For real aircraft reception, the RX SDR is connected to a wideband 1–8 GHz antenna on the RX2 port. The image below shows the hardware ready for passive reception mode.
Ettus USRP B210 with wideband 1–8 GHz antenna connected to the RX2 port. USB 3.0 connection is mandatory for 4 Msps reception.
ADS-B uses PPM (Pulse Position Modulation) at 1 Mbps. Each bit occupies 1 µs and is encoded by the position of a 0.5 µs pulse within the symbol: a pulse in the first half encodes a 1, a pulse in the second half encodes a 0. Every frame is preceded by an 8 µs preamble consisting of four pulses at fixed positions (0, 1, 3.5 and 4.5 µs) that the demodulator uses for synchronisation.
PPM modulation scheme and demodulation pipeline: preamble detection via vectorized NumPy mask, intra-symbol alignment search across 4 possible offsets, and CRC-24 validation.
| Component | Specification |
|---|---|
| SDR RX | Ettus USRP B210 (serial <YOUR_RX_SERIAL>) — USB 3.0 port |
| SDR TX | Ettus USRP B210 (serial <YOUR_TX_SERIAL>) — USB 2.0 port |
| Antenna | Wideband 1–8 GHz, SMA male, connected to RX2 port |
| Coaxial cable | SMA male–male, for TX→RX loopback |
| Attenuator | 30 dB, inline on the coaxial loopback |
Important: The RX SDR must be on a USB 3.0 (blue) port. USB 2.0 does not have enough bandwidth for 4 Msps reception and will cause overflow errors.
These values were determined empirically during lab testing:
| Parameter | Value | Notes |
|---|---|---|
| Sample rate | 4 Msps | SPS=4, sufficient preamble resolution |
| RX gain | 70 dB | Signal/noise ratio ~36× |
| TX gain | 76 dB | With 30 dB attenuator on loopback |
| Detection threshold | mean × 8 | ~120 preamble candidates per block |
| Frequency | 1090 MHz | ADS-B Mode S band |
sudo apt update
sudo apt install uhd-host libuhd-dev python3-uhd gnuradio gr-osmosdr
# Download USRP firmware images (required)
sudo uhd_images_downloaderpip install pyModeS PySide6 numpy --break-system-packages
uhdPython bindings are provided bypython3-uhd(apt), not pip.
# Check UHD detects both SDRs
uhd_find_devices
# Check Python bindings
python3 -c "import uhd; uhd.usrp.MultiUSRP(); print('UHD OK')"
python3 -c "import pyModeS; print('pyModeS', pyModeS.__version__)"
python3 -c "from PySide6.QtWidgets import QApplication; print('PySide6 OK')"ADS-B_IDS_PoC/
├── adsb_receiver.py # RX pipeline + IDS anomaly detector
├── adsb_emulator.py # TX synthetic frame generator + modulator
├── app_gui.py # PySide6 GUI with Leaflet map
├── requirements.txt
├── LICENSE
└── docs/
python3 app_gui.pyThe GUI has three tabs:
Select the RX SDR serial and click ▶ Start Capture. The system will begin decoding ADS-B frames from real aircraft overhead. Decoded data (ICAO, callsign, altitude, speed, position) is shown in the log. When the IDS checkbox is enabled, only anomalous frames are displayed; disable it to see all traffic.
Receiver tab displaying real aircraft data decoded from 1090 MHz. Each line shows ICAO, callsign, altitude, and GPS coordinates. IDS alerts are highlighted when anomalies are detected.
Live Leaflet.js map rendered inside a QWebEngineView. Aircraft icons rotate dynamically based on their heading (or spherical bearing if the heading field is absent). Anomalous aircraft are highlighted in red. The map auto-centres on the first aircraft with a resolved position.
Leaflet map with live aircraft markers. Icons rotate to reflect heading. Red markers indicate aircraft flagged by the IDS.
Configure a synthetic aircraft profile and transmit it over the coaxial loopback. All flight parameters are editable at runtime — click ↻ Apply Profile to push changes to the transmitter without restarting.
Emulation panel. Left column: aircraft identity, flight parameters, and anomaly type selector with context-sensitive extra fields. Right column: TX log showing bootstrap warmup and active transmission status.
To run a loopback test:
- Connect TX/RX port of TX SDR → attenuator → RX2 port of RX SDR via coaxial cable
- Start the Receiver on Tab 1 first
- Go to Tab 3, select the TX SDR serial, configure the aircraft profile
- Click ▶ Start TX — the emulator sends a 6-frame warmup burst to unlock the CPR position decoder, then enters the normal transmission loop
- The synthetic aircraft appears in the log and map within ~5 seconds
- Enable Inject anomaly NOW to trigger the selected anomaly type and observe the IDS alert
| Field | Description |
|---|---|
| ICAO | 6 hex chars — unique aircraft identifier |
| Callsign | Up to 8 chars — flight number |
| Altitude (ft) | Cruising altitude |
| Speed (kts) | Groundspeed |
| Heading (°) | Track angle 0–360 |
| Vertical rate (fpm) | Climb/descent rate, 0 = level flight |
| TX interval (s) | Seconds between bursts |
| TX gain (dB) | Transmit power — use 76 dB with 30 dB attenuator |
The IDS evaluates every received state update against 6 rules divided into two categories:
| Anomaly | Trigger condition | Attack modelled |
|---|---|---|
IMPOSSIBLE_SPEED |
Reported speed > 700 kt (~Mach 1.05) | Direct speed spoofing |
POSITION_JUMP |
Implied speed via Haversine > 1400 kt | Position teleportation |
SPEED_MISMATCH |
Difference > 50 kt between reported and calculated speed | Inconsistent data injection |
ALTITUDE_SPIKE |
Implied vertical rate > 12,000 fpm with Δalt > 200 ft | Altitude spoofing |
| Anomaly | Trigger condition | Attack modelled |
|---|---|---|
FROZEN_POSITION |
Position unchanged > 30 s while speed > 50 kt | Ghost aircraft / replay attack |
ICAO_CALLSIGN_MISMATCH |
Callsign prefix (IBE, RYR, VLG...) does not match expected ICAO national registration prefix | ICAO spoofing / wrong callsign |
| Mode | What is injected |
|---|---|
altitude_spike |
Position frames report spike_altitude_ft instead of cruise altitude |
impossible_speed |
Velocity frame reports impossible_speed_kts |
position_jump |
Position frames alternate between two distant locations |
icao_spoofing |
Frames transmitted using spoof_icao instead of real ICAO |
frozen_position |
Position frames always report the same coordinates regardless of time |
wrong_callsign |
Identification frame reports spoof_callsign instead of real callsign |
- The ICAO/callsign mismatch detector uses a partial operator database (8 airlines). Unknown prefixes are not flagged. Extend
_CALLSIGN_ICAO_PREFIXinadsb_receiver.pyor replace with a full ICAO operator database. - Position decoding requires 5 consistent CPR pairs (PipeDecoder bootstrap). The TX emulator sends a 6-frame warmup at startup to satisfy this requirement — expect a ~2 second delay before lat/lon appears.
- The TX SDR on USB 2.0 handles 4 Msps burst transmission without underflows in testing, but sustained high-rate transmission may cause issues on some hosts.
- Real aircraft position (lat/lon) requires receiving both even and odd CPR frames from the same ICAO within 10 seconds.
- Modulation: PPM (Pulse Position Modulation) — 1 µs per bit, pulse in first half = bit 1, pulse in second half = bit 0
- Preamble: 4 pulses at µs 0, 1.0, 3.5, 4.5 — detected via vectorized NumPy mask with mandatory valley check at 2 µs
- CRC: 24-bit generator polynomial, verified on every decoded frame before passing to the tracker
- CPR decoding: Compact Position Reporting — requires even+odd frame pair to resolve absolute lat/lon; handled by
pyModeS.PipeDecoder - pyModeS version: v3.3.0 — uses
PipeDecoderfor stateful CPR resolution andpyModeS.utilfor CRC/ICAO helpers
GPL-3.0 — see LICENSE file.





