SDR: Testing LoRa with SDR and some handy tools

Sébastien Dudek
January 24, 2020


  • Some basics in RF: For that you can also join our next training from 10th to the 13th of February 2020 in Berlin.
  • A set LoRa targets: an end device and a gateway could be great;
  • A SDR device (USRP, HackRF, RTL-SDR, or any other at least with RX support and supported by GNU Radio);
  • (Optionally) A LoRa dongle that can transmit arbitrary MAC payloads like MK002 series, or the Microchip RN2483 that is supported by python-loranode;
  • Our resources for interpreting decoded LoRa signal: LoRa Craft;
  • A song for the good mood:


LoRA (Long Range) is LPWAN (Low-Power Wide-Area Network) technology that is used in many cases:

  • Smart Parking;
  • IoT Network;
  • Smart meters;
  • Natural disaster communication;
  • Detection and tracking;
  • etc.

This technology uses license-free Sub-GigaHertz bands:

  • Europe: 433 MHz and 868 MHz;
  • Australia and North America: 915 MHz;
  • Asia: 923 MHz.

An exhaustive list of frequencies per country can be found here.

LoRa is used for long-range transmissions. Theoretically, we can expect the following ranges depending on different factors (indoor/outdoor gateways), as well as the environment:

  • 2-3 Km wide coverage outdoor in urban areas;
  • 5-7 Km in rural areas;
  • and sometimes very long range like 702 Km.

See also 11 Myths About LoRaWan.

LoRa devices generally use two layers. The first one is called LoRa PHY, and is the lower physical layer that is proprietary. This part was made by Semtech and is proprietary so there is no freely available documentation of this layer. However, Semtech has provided an overview of the modulation and some specifications. As this layer is proprietary and the only references are the provided specification, it is quite possible to encounter incompatible devices in practice.

The second layer is LoRaWAN and is the cloud-base MAC (Medium Access Control) layer protocol. This layer is used to communicate between LPWAN (Low-Power Wide-Area Network) gateways. The following picture shows a classic architecture a LoRaWAN network:

LoRa Join procedure in OTAA
LoRaWAN architecture (source:

LoRaWAN is not mandatory when using a LoRa device. It is also possible to use raw MAC communication to send commands and messages directly like in P2P (Peer-to-Peer) for example. But in this case, 100% of the security remains on people's hands, as it does not provide any encryption and integrity as LoRaWAN does.


LoRa equipment can have various security modes:

  • raw MAC;
  • LoRaWAN:

    • ABP (Activation by Personalization);
    • OTAA (Over-the-Air Activation).

The raw MAC does not provide any security by default. Device set to this configuration could be vulnerable to eavesdropping and injection. On the other hand, device using the LoRaWAN stack can be set with ABP or OTAA activation methods.

The rest of this section will briefly speak about LoRaWAN 1.0 and 1.1 security, specifically about its keys.


LoRaWAN 1.0

The preferred and most secure method is OTAA. Indeed, within this mode a Join procedure is mandatory to authenticate the LoRa end device:

LoRa Join procedure in OTAA
LoRaWAN 1.0 Join procedure in OTAA (source:

In this process, the end device first sends a Join-request with 3 parameters to derive the session keys:

  • DevEUI: unique end-device identifier in IEEE EUI64 address space;
  • AppEUI: the application identifier in IEEE EUI64 address space;
  • and a random DevNoce of 2 bytes.

These 3 parameters are sent in clear-text, but a MIC (Message Integrity Code) value is included to the message, and if we refer to LoRaWAN version 1.0, we see that it is computed as follows:

cmac = aes128_cmac(AppKey, MHDR | AppEUI | DevEUI | DevNonce)
MIC = cmac[0..3]

The MIC is a 4 bytes AES-128 CMAC and uses a AES-128 key, called AppKey, to cipher public information. The AppKey is only known by the end device and the application.

If the device is permitted to join the network, session keys are generated on the Network side. After that, a Join-accept is then sent by the network to the end device with 6 parameters that are encrypted with the AppKey:

  • AppNonce in LoRaWAN 1.0: random value (3 bytes);
  • NetID, called Home_NetID in 1.1: network ID (3 bytes);
  • DevAddr: Device ID (3 bytes);
  • DL Settings: downlink parameters;
  • RxDelay: delay between TX and RX (1 byte);
  • CFList: optional list of channel frequencies (16 bytes);

The Join-accept payload is encrypted as follows:

aes128_decrypt(AppKey, AppNonce | NetID | DevAddr | DLSettings | RxDelay | CFList | MIC)

Note: Network uses AES decrypt in ECB mode to encrypt messages, that the end device will decrypt with an encrypt method.

And session keys are computed as follows on the network and the end device sides to protect data confidentiality and integrity:

NwkSKey = aes128_encrypt(AppKey, 0x01 | AppNonce | NetID | DevNonce | pad16)
AppSKey = aes128_encrypt(AppKey, 0x02 | AppNonce | NetID | DevNonce | pad16)

At the end, the NwkSKey will be used to compute and verify messages integrity with the MIC field, and AppSKey will be used to encrypt and decrypt messages.

LoRaWAN 1.1

LoRaWAN 1.1 provides more robustness compared to version 1.0, as it ensures the rotation of keys and relies on multiple sessions keys to protect the data in confidentiality and integrity.

In fact, the Join procedure is the same, but use a dedicated key called NwkKey to generate the MIC value:

cmac = aes128_cmac(NwkKey, MHDR | JoinEUI | DevEUI | DevNonce)
MIC = cmac[0..3]

This same key is used to encrypt parameters of the Join Accept message:

  • JoinNonce in LoRaWAN 1.0: random value (3 bytes);
  • NetID, called Home_NetID in 1.1: network ID (3 bytes);
  • DevAddr: Device ID (3 bytes);
  • DL Settings: downlink parameters;
  • RxDelay: delay between TX and RX (1 byte);
  • CFList: optional list of channel frequencies (16 bytes).
aes128_decrypt(NwkKey, JoinNonce | NetID | DevAddr | DLSettings | RxDelay | CFList | MIC)

The AppSKey is generated like in LoRaWAN 1.0 if the OpNeg bit of the DLsettings field is set. In fact, the OpNeg bit is used to negotiate LoRaWAN protocol to version 1.0 when it is set, otherwise 1.1 version is used.

Compared to version 1.0, two other keys FNwkSIntKey and SNwkSIntKey are generated for uplink and downlink messages MIC integrity respectively. Moreover, NwkSEncKey replaces the NwkSKey to protect MAC commands transmitted as payload on port 0, or in the FOpts field.

When OpNeg is not used, these keys are computed as follows:

FNwkSIntKey = aes128_encrypt(NwkKey, 0x01 | JoinNonce | JoinEUI | DevNonce | pad16 )
SNwkSIntKey = aes128_encrypt(NwkKey, 0x03 | JoinNonce | JoinEUI | DevNonce | pad16)
NwkSEncKey = aes128_encrypt(NwkKey, 0x04 | JoinNonce | JoinEUI | DevNonce | pad16)

Version 1.1 also implements a ReJoin-request that will generate keys dedicated for this purpose:

JSIntKey = aes128_encrypt(NwkKey, 0x06 | DevEUI | pad16) # for integrity
JSEncKey = aes128_encrypt(NwkKey, 0x05 | DevEUI | pad16) # to encrypt Join Accept payload

And so the MIC be will computed as follows for next data exchanges:

cmac = aes128_cmac(JSIntKey, JoinReqType | JoinEUI | DevNonce | MHDR | JoinNonce | NetID | DevAddr | DLSettings | RxDelay | CFList )
MIC = cmac[0..3]
Sending results over UDP


The ABP method is simplier than OTAA, as there is no Join Procedure. Nevertheless, it as downsides in terms of security, as session keys are hardcoded on 1.0 version as for 1.1.

Indeed, session keys stay the same until we manually change it, or with a firmware update/upgrade, so ABP could be vulnerable to a cryptanalysis attack than OTAA.


Using ECB mode encryption is risky as the ciphertext can leak information about the plaintext (length, prefixes, common substrings, etc.). This problem has been discussed by Renaud Lifchitz at 2018 (link in French). During the talk Renaud also mentioned that some devices have unprotected memory, and it is possible to get the firmware and its configuration by interfacing ourselves to the device. He also mentionned that it is possible to impersonate gateways that are not authenticated.

In addition to these issues with LoRa version 1.0, we also have the use of weak keys AppKey and hardcoded AppSKey and NwkSKey. Indeed, in OTAA it is possible to enumerate weak/default AppKey key on Join-request's MIC field, and Join-accept payloads. After recovering the AppKey with a bruteforce, an attacker maybe be allowed to impersonize an end device, but also to eavesdrop communication, if his is able to intercept the whole Join procedure. In ABP mode, it is even worse, as an attacker retrieving the AppSKey and NwkSKey for devices can eavesdrop the communication anytime he wants.

The security of data exchanged in LoRaWAN generally relies on good key management procedures. About that, there is an excellent guide written by ex-MWR Labs, currently F-Secure Labs, that explains made mistakes in different components, and how to improve their security.

We can briefly highlight some general practices to apply on a LoRaWAN setup:

  • Use random generated keys;
  • Avoid the exposition of key management servers and services (exposed key management service accessible in the internet);
  • Preferably use HSM (Hardware Security Module) to keep the keys;
  • Preferably use OTAA mode and LoRa version 1.1.

Intercept LoRA signal

To intercept LoRa signal, we will make use of a Software-Defined Radio device and GNU Radio to make the signal acquisition. For our case, we use a USRP B205mini-i, but that could be pretty any device, even a RTL-SDR, that has a GNU Radio support.

When dealing with unknown targets, we have to go through several stages until we are able to capture and analyze its data.

So lets start by scoping targeted frequencies!

Identify frequencies

At this stage, we need to know frequencies used by our target. To do that, we can use a waterfall to have a representation of the signal across the frequency range, but also a FFT (Fast Fourier Transform) block to find the different frequency components of a signal. Here is a GNU Radio schema that can achieve that:

GNU Radio schema to find targeted signal
GNU Radio schema to find targeted signal

In this schema, the sampling rate samp_rate can be increased depending on your SDR device. Longer it is, more range you will cover, but then you'll have to process it with your computer (be wise). Moreover, to be able to cover the different bands dynamically, the central frequencies can be changed with a slider block implemented in GNU Radio.

Note: The identification of signals can also be performed with tools like GQRX or other RF explorer tools, but for this post we will keep using GNU Radio and evolve our schema to the end.

On the FFT display, it is also important to fill the Max Hold box to keep track of frequency components. Here is the result when a signal is triggered:

LoRa signal triggered in GNU Radio FFT and waterfalls
LoRa signal triggered in GNU Radio FFT and waterfall displays

In the above screenshots, we can observe that a signal was sent 868.2 MHz central frequency. Looking at lower and highest frequency components of the FFT, this signal should have a bandwidth of 125 KHz:

Zoom on low to high components
Zoom on low to high components

It should be noted that LoRa devices generally use multiple channels, and so it is better to identify all of them the same way before going further.

Demodulating and decoding captured signals

In the previous section, we have been able to identify an interesting signal with multiple short chirps that we can observe on the waterfall part, above the FFT. Lets now demodulate and decode it.

To transmit signal LoRA uses a proprietary spread spectrum method derivative to CSS (Chirp Speark Spectrum) and FEC (Forward Error Coding) to improve resilience against interference. A very good resource can be found on this RevSpace page. Thanks to gr-lora of Bastille and gr-lora of rpp0, the demodulating and decoding of LoRA PHY signal can be done quickly with one of the 2 modules. For this post we will make use of gr-lora of rpp0 as it is well and nicely maintained, but also easy to install with GNU Radio 3.8 .

To continue, we add a LoRa Receiver from gr-lora to our schema, but at this stage, we have to fill different parameters:

Adding gr-lora to the schema
Adding gr-lora to the schema

We already know the frequency channel used by our captured signal, which is 868.2 MHz and the BW (bandwidth). However, there is still the SF (Spreading Factor) value left. To find what kind of values it could take, we can look at SF and BW used for LoRa online table and focuse on European countries for our case:

Data Rate       	Configuration	bits/s 	Max payload
DR0	                SF12/125kHz	    250	    59
DR1	                SF11/125kHz	    440	    59
DR2	                SF10/125kHz	    980	    59
DR3	                SF9/125kHz	    1 760	123
DR4	                SF8/125kHz	    3 125	230
DR5	                SF7/125kHz	    5 470	230
DR6	                SF7/250kHz	    11 000	230
DR7	                FSK: 50kpbs	    50 000	230

Naively bruteforcing this missing SF parameter is a way among others, but we can think about smarter moves. Indeed, as seen in the table above, LoRa operates with SF from 7 to 12. The shorted chirp is SF7 with a data rate of 11 kilobits/s, and 12 the longest one with a data rate of 250 bits/s. So by consequences, this will have an effect on time, and this can be observed if we compare two configurations, one with a SF7 and 125 kHz BW (on the left) and our target (on the right):

Comparing chirps with two different configuratons
Comparing chirps with two different configuratons

As shown in the above figure, chirps are sent really fast in the SF7 configuration compared to our target, so it is probable that our target use one of the highest SF configurations.

So by changing the receiver to SF12BW125 configuration, we are finally able to see packets in GNU Radio console as follows:

18 31 10 40 ad 15 00 60 00 00 00 03 ca fe ff ff ff ff ff ff ff ff ff 6e 5a d7 0d 59 2e

But these hexadecimal does not mean anything to us now, apart from the word 0xcafe (french word for coffee - Deuh).

So for further analysis, we have made few tools to parse these packets.

Interpret packets

Our resources

After being able to capture, demodulate and decode LoRa PHY signal, we need to understand the structure of these packets. For LoRaWAB 1.0 a tool called lorawan-packet-decoder has been published, but this is not very flexible for our case, and does not allow us to generate packet either for further purposes.

To overcome these limits, we have made Scapy layers to parse LoRa PHY packets decoded by the GNU Radio g-lora receiver. This layer can be fetched on our GitHub here.

As an example, if we take the previous packet retrieved by the receiver, we are able to decode it as follows:

>>> import binascii
>>> from layers.loraphy import *
>>> pkt = "18 31 10 40 ad 15 00 60 00 00 00 03 ca fe ff ff ff ff ff ff ff ff ff 6e 5a d7 0d 59 2e"
>>> l_pkt = LoRa(binascii.unhexlify(pkt.replace(" ", ""))
... )
###[ LoRa ]### 
  Preamble  = 0x1
  PHDR      = 0x8311
  PHDR_CRC  = 0x0
  MType     = Unconfirmed Data Up
  RFU       = 0
  Major     = 0
  \DevAddr   \
   |###[ DevAddrElem ]### 
   |  NwkID     = 0xad
   |  NwkAddr   = 0x600015
  \FCtrl     \
   |###[ FCtrl_UpLink ]### 
   |  ADR       = 0
   |  ADRACKReq = 0
   |  ACK       = 0
   |  ClassB    = 0
   |  FOptsLen  = 0
  FCnt      = 0
  FPort     = 3
  DataPayload= '\xca\xfe\xff\xff\xff\xff\xff\xff\xff\xff\xff'
  MIC       = 0x6e5ad70d
  CRC       = 0x592e

Chaining the interpreter

The module gr-lora also implement a block to quickly send packets other UDP to an arbitrary host on port, so we will use to send packets to a port that will be catched by the Scapy sniff() function later:

Sending results over UDP
Using Message Socket Sink to send results over UDP

To get UDP packet locally and interpret them automatically, we can use a script called of the Lora Craft repository:

$ python
<LoRa  Preamble=0x1 PHDR=0x631e PHDR_CRC=0x0 MType=Unconfirmed Data Up RFU=0 Major=0 DevAddr=[<DevAddrElem  NwkID=0xad NwkAddr=0x600015 |>] FCtrl=[<FCtrl_UpLink  ADR=0 ADRACKReq=0 ACK=0 ClassB=0 FOptsLen=0 |>] FCnt=0 FPort=2 DataPayload='i\x06D\x94\x97\x08\xce!\xd9' MIC=0x4b516899 CRC=0x96e1 |>
<LoRa  Preamble=0x1 PHDR=0x631e PHDR_CRC=0x0 MType=Unconfirmed Data Up RFU=0 Major=0 DevAddr=[<DevAddrElem  NwkID=0xad NwkAddr=0x600015 |>] FCtrl=[<FCtrl_UpLink  ADR=0 ADRACKReq=0 ACK=0 ClassB=0 FOptsLen=0 |>] FCnt=0 FPort=2 DataPayload='penthertz' MIC=0x20a5fcba CRC=0xcdc |>
<LoRa  Preamble=0x0 PHDR=0xd30c PHDR_CRC=0x0 MType=Confirmed Data Up RFU=0 Major=0 DevAddr=[<DevAddrElem  NwkID=0xad NwkAddr=0x600015 |>] FCtrl=[<FCtrl_UpLink  ADR=0 ADRACKReq=0 ACK=0 ClassB=0 FOptsLen=1 |>] FCnt=0 FOpts_up=[<MACCommand_up  CID=LinkCheckReq LinkCheck=[''] |>] FOpts_down=[<MACCommand_down  CID=222 |>] FPort=92 DataPayload='' MIC=0x31c753f |>

Testing OTAA and ABP keys

In OTAA, the Join procedure could be interesting to capture to brutforce MIC fields on Join-request messages. As we saw earlier, this MIC is generated by the AppKey in version 1.0 of LoRaWAN and the NmkKey for 1.1.

As an example, if we are able to intercept the following Join-request packet, we can test if a weak key like 000102030405060708090A0B0C0D0E0F with our lutil/ helper checkMIC:

>>> from layers.loraphy import *
>>> from lutil.crypto import *
>>> key = "000102030405060708090A0B0C0D0E0F"
>>> p = '000000006c6f7665636166656d656565746f6f00696953024c49'
>>> pkt = LoRa(binascii.unhexlify(p))
>>> pkt
<LoRa  Preamble=0x0 PHDR=0x0 PHDR_CRC=0x0 MType=Join-request RFU=0 Major=0 Join_Request_Field=[<Join_Request  AppEUI='lovecafe' DevEUI='meeetoo' DevNonce=26985 |>] MIC=0x53024c49 |>
>>> checkMIC(binascii.unhexlify(key), str(pkt))

Moreover, for Join-Accept messages, the AppKey or NwkKey can be tested by combining JoinAcceptPayload_decrypt and checkMIC:

>>> pkt = "000000200836e287a9805cb7ee9e5fff7c9ee97a"
>>> ja = JoinAcceptPayload_decrypt(binascii.unhexlify(key), binascii.unhexlify(pkt))
>>> ja
>>> Join_Accept(ja)
<Join_Accept  JoinAppNonce=0x6fe14a NetID=0x10203 DevAddr=0x68e8cb1 OptNeg=0 RX1DRoffset=0x0 RX2_Data_rate=0x0 RxDelay=0x0 |<Padding  load='\xbejsu' |>>
>>> p = "\x00\x00\x00\x20"+ja # adding headers
>>> checkMIC(key.decode("hex"), p)
>>> True

In ABP, bruteforce attacks against NwkSKey and AppSKey in version 1.0 can be performed against encrypted data payloads. However in 1.1 version, bruteforcing would require more computing, as the MIC is generated by dedicated session keys for integrity, especially if we could not be able to recover known fields of a unencrypted Join-accept.

To test these sessions keys, you can use lorakeys as suggested in an interesting article written by Sébastien ROY (in French).


Identifying, capturing and decoding LoRa signal in practice seems basic thanks to provided tools and right methodologies. Nevertheless, in a real scenario, attackers will face with many other signals in targeted frequency ranges, but also noise and other elements that could disturb us during an assessment. Generally speaking with RF, it is recommended to have a sufficient feedback in closed environment before going into the wild and try to attack a target in production. Also, attacking keys can be a very fruitful in some cases when there are manually set and weak, otherwise the attacker could also try to identify exposed services to the internet, and then find vulnerabilities in one of them to compromise nodes' keys.

We hope that tools and ressources we made for this post will help to assess LoRa setups, and invite you to contribute if you have some ideas ;)!

Wanna learn more about SDR hacks?

Don't miss our next training from the 10th to the 13th of February 2020 in Berlin!

About the Author

Sébastien Dudek

Sébastien is a security researcher focusing on flaws in radio-communication systems. He has published attacks against mobile device baseband, Power-Line devices, as well as intercom systems.