Содержание статьи
Как ты понимаешь, за годы существования проблемы способов реализации придумано уже достаточно много. Это может быть как классический, хорошо всем известный Rubber Ducky, так и весьма экзотический вариант с перепрошивкой флешки с подходящим контроллером. Также народ придумал некоторое количество реализаций на Arduino и совместимом Digispark.
Кроме того, однозначно стоит упомянуть и о Pill Duck, так как своей концепцией именно этот проект наиболее близок к тому, что я покажу в статье. У Pill Duck есть хорошее и подробное описание, так что всячески рекомендую тебе ознакомиться с ним, если ты настроен в деталях разобраться в проблеме.
Сразу скажу, что я не ставил перед собой цель превзойти упомянутые устройства. Скорее это мой личный эксперимент на тему дистанционного пульта управления для компьютера, так что оценивать его стоит в первую очередь именно с такой точки зрения.
USB HID
USB (Universal Serial Bus), как ясно из названия, представляет собой универсальную последовательную шину, которая де-факто является стандартом в настоящее время (вернее, даже целым семейством стандартов). Она практически полностью заменила собой RS-232, LPT, PS/2 и используется преимущественно для связи ПК с периферийными устройствами.
INFO
Следует заметить, что рабочие места для наиболее ответственных задач до сих пор оснащаются средствами ввода с интерфейсами 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, в котором эмулируется движение мыши. Наибольший интерес для нас представляет именно дескриптор, вот как он выглядит:
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). Изменив их, можно притвориться практически любым устройством любого производителя (правда, есть сомнения в правовом статусе такого притворства, так что подумай дважды).
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[]
. В данном случае это четыре байта.
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
. Следующий байт зарезервирован для совместимости, в принципе его можно выкинуть. Дальше идут шесть байт, каждый из которых отвечает одной нажатой клавише: такой мультитач на шесть касаний, не считая модификаторов. Дескриптор клавиатуры выглядит следующим образом:
...
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-дескриптор выглядит так:
...
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 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, и о раскладке клавиатура тоже ничего не знает, это все происходит уровнем выше. Так как же выглядит нажатие клавиши а
? Это два последовательных отчета — первый о нажатии клавиши, а второй о ее отпускании (если забыть про то, что клавишу надо отпустить, выйдет конфуз).
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);
Продолжение доступно только участникам
Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
Вариант 2. Открой один материал
Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.
Я уже участник «Xakep.ru»