BLE Advertisement Replay Attack & Spoof Detection
Problem
In order to maximize battery life, BLE sensors may send data as non-connectable undirected advertisements, the classic beacon type transmission. In this use case, there is a vulnerability that advertisements can be captured by a third party and replayed. MAC addresses can be easily spoofed, leading to unreliable data on the observer end.
Project
The example BLE Gateway and BLE Sensor is an attempt at mitigating the vulnerability to replay attacks when using beacon type advertisements. It's written for nRF51822 hardware with no onboard NVRAM. The lack of permanent storage weakens this scheme because of start-up conditions. I'll also point out where this scheme is not a proof against replay attacks, only detecting and mitigating certain types of attempts
Import programBLE_Gateway
BLE ADV Gateway, converts advertisement to proper JSON serial output
Import programBLE_Sensor
BLE ADV sensor for 2-pin interrupt (i.e. window/door sensor w/ reed switch)
The assumption is that sensors even in sleep mode retain memory and timers. On the nRF51822, this is reasonable, as the sleep current is only 4uA. On power-on, sensors capture their power-on time as zero. Subsequent advertisements embed a 2-byte value indicating the number of seconds since power-on. This "spoof clock" resets at 1800 seconds (or 30 minutes), due to the limitations of the mbed implementation of timers on the nRF51822. Every new advertisement is basically timestamped with when that advertisement event happened relative to the sensor's "birthday", with a birthday periodicity of 1800 seconds.
When the observer reads the sensor data and timestamp, it can calculate when the sensor's birthday is relative to it's own timer. The observer stores an array of MAC address and birthday pairs. A sensor's advertisement is considered non-suspicious if the sensor's birthday calculated from the advertisement's timestamp matches this array. On powerup with a blank array, the gateway has no other option than to trust all new MAC address advertisements.
But it's expected that sensors would need to have their batteries switched out eventually. Any power off-to-on cycle will reset the birthday of the sensor and so its advertisements would have timestamps that conflict with the observer's recorded birthday for that MAC address. I extended the observer's birthday records with a secondary set of birthdays. When there is a conflict, the new birthday is recorded into this second record. If there are 3 consecutive transmissions where the birthdays are consistent, then I can consider this sensor's birthday as the new primary birthday and start accepting data timed at this new birthday. The birthday records are implemented as a FIFO.
I don't change the time values on every repeated advertisement, and the gateway may receive any of the several repeated advertisements during the advertisement interval. As a result, the gateway must accept a several seconds long range around the birthday time. This also accounts for timer drift between sensors and gateways before resyncing. It does mean that the spoof detection is weaker than a scheme where every advertisement packet has an updated time and modules have an actual external real time clocks with greater accuracy.
Here's my advertisement organization
AES Array | ADV Array | Full Advertisement | Designation | Value | Function |
---|---|---|---|---|---|
na | na | Byte 0 | AD1 Length | 0x02 | AD1 is 2 bytes long |
na | na | Byte 1 | AD1 Type | 0x01 | AD1 Data interpreted as flag |
na | na | Byte 2 | AD1 Data 0 | 0x06 | AD1 flag indicates non-connectable undirected |
na | na | Byte 3 | AD2 Length | 0x1B | AD2 is 27 bytes (0x1B) long (rest of this data) |
na | na | Byte 4 | AD2 Type | 0xFF | 0xFF mean Manufacturer Specific Data |
na | 0 | Byte 5 | AD2 Data 0 | ADV_Data[0] | "our device" flag, MAC[3] |
na | 1 | Byte 6 | AD2 Data 1 | ADV_Data[1] | "out device" flag, MAC[2] |
na | 2 | Byte 7 | AD2 Data 2 | ADV_Data[2] | "out device" flag, MAC[1] |
na | 3 | Byte 8 | AD2 Data 3 | ADV_Data[3] | "out device" flag, MAC[0] |
na | 4 | Byte 9 | AD2 Data 4 | ADV_Data[4] | battery voltage json MSB, ie 3 in 3.14 |
na | 5 | Byte 10 | AD2 Data 5 | ADV_Data[5] | battery voltage json |
na | 6 | Byte 11 | AD2 Data 6 | ADV_Data[6] | battery voltage json |
na | 7 | Byte 12 | AD2 Data 7 | ADV_Data[7] | battery voltage json LSB, ie 4 in 3.14 |
na | 8 | Byte 13 | AD2 Data 8 | ADV_Data[8] | reserved |
na | 9 | Byte 14 | AD2 Data 9 | ADV_Data[9] | reserved |
0 | 10 | Byte 15 | AD2 Data 10 | ADV_Data[10] Encrypted | spoof - clock high byte |
1 | 11 | Byte 16 | AD2 Data 11 | ADV_Data[11] Encrypted | spoof - clock low byte |
2 | 12 | Byte 17 | AD2 Data 12 | ADV_Data[12] Encrypted | Xmit_Cnt - increments per transmit event, 0-255 |
3 | 13 | Byte 18 | AD2 Data 13 | ADV_Data[13] Encrypted | JSON[0] |
4 | 14 | Byte 19 | AD2 Data 14 | ADV_Data[14] Encrypted | JSON[1] |
5 | 15 | Byte 20 | AD2 Data 15 | ADV_Data[15] Encrypted | JSON[2] |
6 | 16 | Byte 21 | AD2 Data 16 | ADV_Data[16] Encrypted | JSON[3] |
7 | 17 | Byte 22 | AD2 Data 17 | ADV_Data[17] Encrypted | JSON[4] |
8 | 18 | Byte 23 | AD2 Data 18 | ADV_Data[18] Encrypted | JSON[5] |
9 | 19 | Byte 24 | AD2 Data 19 | ADV_Data[19] Encrypted | JSON[6] |
10 | 20 | Byte 25 | AD2 Data 20 | ADV_Data[20] Encrypted | JSON[7] |
11 | 21 | Byte 26 | AD2 Data 21 | ADV_Data[21] Encrypted | JSON[8] |
12 | 22 | Byte 27 | AD2 Data 22 | ADV_Data[22] Encrypted | JSON[9] |
13 | 23 | Byte 28 | AD2 Data 23 | ADV_Data[23] Encrypted | JSON[10] |
14 | 24 | Byte 29 | AD2 Data 24 | ADV_Data[24] Encrypted | JSON[11] |
15 | 25 | Byte 30 | AD2 Data 25 | ADV_Data[25] Encrypted | JSON[12] |
The first 5 bytes are the standard AD1 block for beacons, indicating the advertisement is unconnectable undirected. AD2 block is our manufacturer specific custom advertisement where we pack data. Bytes 5-8 are our indicator for the observer being able to identify "our sensor" vs. other beacons using a pattern matching scheme. The observer will only process the data (perform AES decrypt and output data over serial) if these 4 bytes match our pattern. The alternative is to know which MAC addresses to look for ahead of time. This would make deployment more complicated. The pattern matching is a compromise between security and ease of deployment. It imposes a weakness to the replay detection because the size of the spoof checking array is finite. We can only keep track of, say 50, unique MAC's and their birthdays. Beyond that, the FIFO starts dropping old MAC addresses, and malicious advertisements can then be injected. We'll detect when this happens, but we're not able to protect against it in an automated way. A solution for this is to have an external step to validate MAC addresses and protect those from dropping off the FIFO. But that's a lot of work.
Bytes 9-12 hold the battery voltage. I didn't want to take up more of the AES encrypted block for something that's not very sensitive.
The first two encrypted bytes, Byte 15-16, hold the number of seconds since the sensor's birthday.
Byte 17 holds the transmit counter. This helps the gateway differentiate between repeated transmissions of the same advertisement data, and not send those out the uart.
Byte 18-30 hold JSON formatted sensor data. Using JSON isn't very byte-efficient. But it pretty flexible and the scheme works well with MQTT topic naming.
Please log in to post comments.