Реверс-инжениринг драйверов USB-устройств на примере машинки на радиоуправлении

    Перевод статьи DRIVE IT YOURSELF: USB CAR

    image

    Один из аргументов любителей Windows перед любителями Linux – недостаток драйверов для оборудования под эту ОС. С течением времени ситуация выправляется. Сейчас она уже гораздо лучше, чем 10 лет назад. Но иногда можно встретить какое-то устройство, которое не распознаётся вашим любимым дистрибутивом. Обычно это будет какая-нибудь USB-периферия.

    Красота свободного софта в том, что эту проблему можно решить самостоятельно (если вы программист). Конечно, всё зависит от сложности оборудования. С трёхмерной веб-камерой у вас может и не получится – зато многие USB-устройства довольно просты, и вам не придётся нырять в глубины ядра или закапываться в С. В этом уроке мы с вами при помощи Python по шагам изготовим драйвер к игрушечной радиоуправляемой машинке.

    Процесс по сути будет реверс-инженирингом. Сначала мы подробно изучим устройство, затем сохраним данные, которыми оно обменивается с драйвером в Windows, и попытаемся понять, что они означают. Для нетривиальных протоколов вам может потребоваться как опыт, так и удача.

    Знакомство с USB


    USB – шина с управлением хостом. Хост (PC) решает, какое устройство отправляет данные по проводам, и когда именно. Даже в случае асинхронного события (нажатие кнопки на клаве) оно не отправляется хосту сейчас же. Поскольку на каждой шине может быть до 127 устройств (и ещё больше, если через хабы), такая схема работы облегчает управление.

    Также у USB есть многослойная система протоколов – примерно как у интернета. Самый нижний уровень обычно реализован в кремнии. Транспортный слой работает через туннели (pipe). Потоковые туннели передают разные данные, туннели сообщений – сообщения для управления устройствами. Каждое устройство поддерживает минимум один туннель сообщений. На высшем уровне приложения (или класса) есть протоколы вроде USB Mass Storage (флэшки) или Human Interface Devices (HID), устройства для взаимодействия человека с компьютером.

    В проводах


    USB-устройство можно рассматривать как набор конечных точек, или буферов ввода/вывода. У каждого есть направление данных (ввод или вывод) и тип передачи. По типам буферы бывают следующие: прерывания, изохронные, управляющие и пакетные.

    Прерывания передают данные по чуть-чуть в реальном времени. Если пользователь нажал клавишу, устройство ждёт, пока хост не спросит «не нажимали ли там кнопочки?». Хост не должен тормозить, и эти события не должны потеряться. Изохронные работают примерно так же, но не настолько жёстко – они разрешают передавать больше данных, при этом допуская их потерю, когда это не критично (например, веб-камеры).

    Пакетные предназначены для больших объёмов. Чтобы они не забивали канал, им отдаётся всё место, которое сейчас не занято другими данными. Управляющие используются для управления устройствами, и только у них есть жёстко заданный формат запросов и ответов. Набор буферов со связанными с ним метаданными называется интерфейсом.

    У любого USB-устройства есть буфер номер ноль – это конечная точка туннеля по умолчанию, который используется для управляющих данных. Но как хост узнаёт, сколько у устройства есть ещё буферов и какого они типа? Для этого используются разные дескрипторы, отправляемые по особым запросам по туннелю по умолчанию. Они могут быть стандартными для всех, особыми для конкретных классов устройств, или проприетарными.

    Дескрипторы составляют иерархию, которую можно посмотреть утилитами типа lsusb. Наверху сидит дескриптор устройства, где содержится Vendor ID (VID) и Product ID (PID). Эта пара уникальная для каждого устройства, по ней система находит нужный драйвер. У устройства может быть несколько конфигураций, каждое со своим интерфейсом (например, принтер, сканер и факс в МФУ). Но обычно определяется одна конфигурация с одним интерфейсом. Они описываются соответствующими дескрипторами. У каждой конечной точки есть дескриптор, содержащий её адрес (число), направление (ввод или вывод) и тип передачи.

    У спецификаций классов есть свои типы дескрипторов. Спецификация USB HID ожидает передачу данных в виде «отчётов», которые отправляются и принимаются по буферу управления или прерываний. Эти дескрипторы определяют формат отчёта (к примеру, «1 поле длиной 8 бит») и то, как его надо использовать («офсет в направлении Х»). Поэтому HID-устройство описывает само себя и его может поддерживать универсальный драйвер (usbhid в Linux). Иначе пришлось бы писать свой драйвер для каждой мыши.

    Не буду пытаться описывать в нескольких абзацах сотни страниц спецификаций. Интересующихся отправляю к книге O’Reilly «USB in a Nutshell», бесплатно лежащей по адресу www.beyondlogic.org/usbnutshell. Займёмся-ка лучше делом.

    Разбираемся с разрешениями


    По умолчанию с USB-устройствами можно работать только из-под рута. Чтобы не запускать таким образом тестовую программу, добавим правило udev:

    SUBSYSTEM=="usb", ATTRS{idVendor}=="0a81", ATTRS{idProduct}=="0702", GROUP="INSERT_HERE", MODE="0660"
    


    Вставьте имя группы, к которой принадлежит ваш пользователь, и добавьте это в /lib/udev/rules.d/99-usbcar.rules.

    Под капотом


    Посмотрим, как выглядит машинка по USB. lsusb – инструмент для подсчёта устройств и декодирования их дескрипторов. Входит в комплект usbutils.

    [val@y550p ~]$ lsusb
    Bus 002 Device 036: ID 0a81:0702 Chesen Electronics Corp.
    Bus 002 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
    ...
    


    Машинка – это Device 036 (чтобы быть уверенным, её можно отсоединить и снова запустить lsusb). Поле ID – это пара VID:PID. Для чтения дескрипторов запустите lsusb -v:

    [val@y550p ~]$ lsusb -vd 0a81:0702
    Bus 002 Device 036: ID 0a81:0702 Chesen Electronics Corp.
    Device Descriptor:
    idVendor 0x0a81 Chesen Electronics Corp.
    idProduct 0x0702
    ...
    bNumConfigurations 1
    Configuration Descriptor:
    ...
    Interface Descriptor:
    ...
    bInterfaceClass 3 Human Interface Device
    ...
    iInterface 0
    HID Device Descriptor:
    ...
    Report Descriptors:
    ** UNAVAILABLE **
    Endpoint Descriptor:
    ...
    bEndpointAddress 0x81 EP 1 IN
    bmAttributes 3
    Transfer Type Interrupt
    ...
    


    Стандартная иерархия. Как и у большинства устройств, у неё только одна конфигурация и интерфейс. Можно заметить одну конечную точку interrupt-in (кроме точки по умолчанию 0, которая есть всегда, и поэтому не выводится в списке). Поле bInterfaceClass сообщает о том, что это устройство HID. Это хорошо – протокол общения с HID открыт. Казалось бы, прочтём дескриптор отчётов, чтобы понять их формат и использование, и дело в шляпе. Однако у него стоит пометочка ** UNAVAILABLE **. ЧЗН? Поскольку машинка – устройство HID, драйвер usbhid присвоил её себе, но не знает, что с ней делать. Надо отвязать его от управления ею.

    Для начала надо найти адрес шины. Переподключим её, запустим dmesg | grep usb, и посмотрим в последнюю строчку, начинающуюся с usb X-Y.Z:. X, Y и Z – целые числа, уникальным образом определяющие порты на хосте. Затем запустим

    [root@y550p ~]# echo -n X-Y.Z:1.0 > /sys/bus/usb/drivers/usbhid/unbind
    


    1.0 – это конфигурация и интерфейс, которые должен отпустить драйвер usbhid. Чтобы подвязать всё обратно, запишите то же самое в /sys/bus/usb/drivers/usbhid/bind.

    Теперь поле Report descriptor выдаёт информацию:

    Report Descriptor: (length is 52)
    Item(Global): Usage Page, data= [ 0xa0 0xff ] 65440
    (null)
    Item(Local ): Usage, data= [ 0x01 ] 1
    (null)
    ...
    Item(Global): Report Size, data= [ 0x08 ] 8
    Item(Global): Report Count, data= [ 0x01 ] 1
    Item(Main ): Input, data= [ 0x02 ] 2
    ...
    Item(Global): Report Size, data= [ 0x08 ] 8
    Item(Global): Report Count, data= [ 0x01 ] 1
    Item(Main ): Output, data= [ 0x02 ] 2
    ...
    


    Задано два отчёта. Один читает с устройства (ввод), второй пишет (вывод). Оба размером в байт. Однако их использование не очевидно. Для сравнения, вот как выглядит дескриптор отчёта для мышки (не весь, но главные строчки):

    Report Descriptor: (length is 75)
    Item(Global): Usage Page, data= [ 0x01 ] 1
    Generic Desktop Controls
    Item(Local ): Usage, data= [ 0x02 ] 2
    Mouse
    Item(Local ): Usage, data= [ 0x01 ] 1
    Pointer
    Item(Global): Usage Page, data= [ 0x09 ] 9
    Buttons
    Item(Local ): Usage Minimum, data= [ 0x01 ] 1
    Button 1 (Primary)
    Item(Local ): Usage Maximum, data= [ 0x05 ] 5
    Button 5
    Item(Global): Report Count, data= [ 0x05 ] 5
    Item(Global): Report Size, data= [ 0x01 ] 1
    Item(Main ): Input, data= [ 0x02 ] 2
    


    Тут всё ясно. С машинкой – непонятно, и нам надо догадаться об использовании битов самостоятельно.

    Небольшой бонус


    Большинство радиоуправляемых игрушек просты и используют стандартные приёмники, работающие на одинаковых частотах. Значит, нашу программу можно будет использовать для управления другими игрушками, кроме этой машинки.

    Работа для детектива


    При анализе сетевого трафика используют снифер. И в нашем случае такая штука пригодится. Бывают специальные USB-мониторы для коммерческого использования, но для нашей задачи подойдёт и Wireshark.

    Настроим перехват USB в Wireshark. Сначала разрешим мониторинг USB в ядре. Загрузим модуль usbmon:

    [root@y550p ~]# modprobe usbmon
    


    Подмонтируем особую файловую систему debugfs:

    [root@y550p ~]# mount -t debugfs none /sys/kernel/debug
    


    Появится директория /sys/kernel/debug/usb/usbmon, которую можно использовать для записи трафика простыми средствами оболочки:

    [root@y550p ~]# ls /sys/kernel/debug/usb/usbmon
    0s 0u 1s 1t 1u 2s 2t 2u
    


    Там лежат файлы с загадочными именами. Целое число – номер шины (первая часть адреса шины USB); 0 означает все шины на хосте. s – statistics, t – transfers, u — URBs (USB Request Blocks логические сущности, представляющие происходящие транзакции). Чтобы сохранить все передачи на шине 2, введите:

    [root@y550p ~]# cat /sys/kernel/debug/usb/usbmon/2t
    ffff88007d57cb40 296194404 S Ii:036:01 -115 1 <
    ffff88007d57cb40 296195649 C Ii:036:01 0 1 = 05
    ffff8800446d4840 298081925 S Co:036:00 s 21 09 0200 0000 0001 1 = 01
    ffff8800446d4840 298082240 C Co:036:00 0 1 >
    ffff880114fd1780 298214432 S Co:036:00 s 21 09 0200 0000 0001 1 = 00
    


    Для нетренированного глаза тут ничего непонятно. Хорошо, что Wireshark будет декодировать данные.

    Теперь нам нужна Windows, которая будет работать с оригинальным драйвером. Лучше всего установить всё в VirtualBox (с Oracle Extension Pack, поскольку нам необходима поддержка USB). Убедитесь, что VirtualBox может использовать устройство, и запустите KeUsbCar, которая управляет машинкой в Windows. Запустите Wireshark, чтобы посмотреть, какие команды драйвер отправляет на устройство. На первом экране выберите интерфейс usbmonX, где X – это шина, к которой подключена машинка. Если Wireshark запускается не из-под рута, убедитесь, что узлы /dev/usbmon* имеют соответствующие разрешения.

    image

    Нажмём в KeUsbCar кнопочку Forward. Wireshark перехватит несколько исходящих управляющих пакетов. На скриншоте отмечен тот, который нужен нам. Судя по параметрам, это запрос SET_REPORT (bmRequestType = 0x21, bRequest = 0x09), который обычно используется, чтобы изменить статус устройства – такого, как лампочки на клавиатуре. Согласно тому Report Descriptor, что мы видели, длина данных составляет 1 байт, и сам отчёт содержит 0x01 (также подсвечено).

    Нажатие кнопки Right выливается в похожий запрос. Но отчёт уже содержит 0х02. Можно догадаться, что это означает направление движение. Таким же образом выясняем, что 0х04 – это правый реверс, 0х08 – задний ход, и т.д. Правило простое: код направления – это двоичная единичка, сдвинутая влево на позицию кнопки в интерфейсе KeUsbCar, если считать их по часовой стрелке.

    Также можно отметить периодические запросы прерываний от Endpoint 1 (0x81, 0x80 означает, что это точка ввода; 0x01 это её адрес). Что это? Кроме кнопок, у KeUsbCar есть индикатор заряда, так что это, возможно, данные по батарее. Их значение не меняется (0х05), если машина не выезжает из гаража. В противном случае запросов прерываний не происходит, но они возобновляются, если мы ставим её обратно. Тогда, видимо, 0х05 означает «идёт зарядка» (игрушка простая, поэтому уровень зарядки не передаётся). Когда батарея зарядится, прерывание начнёт возвращать 0x85 (0x05 с установленным 7 битом). Видимо, 7 бит означает «заряжено». Что делают биты 0 и 2, которые составляют 0х05, пока неясно.

    Пишем почти настоящий драйвер


    Заставить программу работать с устройством, которое ранее не поддерживалось – это хорошо, но иногда нужно сделать так, чтобы с ним работала и остальная система. Это значит, надо делать драйвер, а это требует программирования на уровне ядра (http://www.linuxvoice.com/be-a-kernel-hacker/), и вам это вряд ли сейчас нужно. Но возможно, нам удастся обойтись и без этого, если речь идёт про USB.

    Если у вас есть USB-сетевуха, можно использовать TUN/TAP для подключения программы PyUSB в сетевой стек Linux. Интерфейсы TUN/TAP работают как обычные сетевые, с именами типа tun0 или tap1, но через них все пакеты становятся доступны в узле /dev/net/tun. Модуль pytun делает работу с TUN/TAP простой. Быстродействие страдает, но можно переписать программу на С с использованием libusb.

    Ещё один кандидат – USB-дисплей. В Linux есть модуль vfb, который позволяет обращаться к фреймбуферу как к /dev/fbX. Можно использовать ioctls, чтобы перенаправить консоль на него, и закачивать содержимое /dev/fbX на USB-устройство. Это тоже не быстро, но ведь вы не собираетесь играть в 3D-шутеры через USB.

    Пишем код


    Сделаем такую же программу, как для Windows. 6 стрелочек и уровень зарядки, который мигает, когда машинка заряжается. Код лежит на GitHub github.com/vsinitsyn/usbcar.py

    Как нам работать в USB под Linux? Это возможно делать из пространства пользователя при помощи библиотеки libusb. Она написана на С и требует хороших знаний USB. Простая альтернатива – PyUSB. Для интерфейса пользователя я использовал PyGame.

    Исходники PyUSB скачайте с github.com/walac/pyusb, и установите через setup.py. Ещё вам понадобится установить библиотеку libusb. Поместим функциональность для управления машиной в класс с оригинальным названием USBCar.

    import usb.core
    import usb.util
    class USBCar(object):
      VID = 0x0a81
      PID = 0x0702
      FORWARD = 1
      RIGHT = 2
      REVERSE_RIGHT = 4
      REVERSE = 8
      REVERSE_LEFT = 16
      LEFT = 32
      STOP = 0
    


    Импортируем два главных модуля PyUSB и вставляем значения для управления машинкой, которые мы вычислили при просмотре трафика. VID и PID – это id машинки, взятые из вывода lsusb.

    def __init__(self):
      self._had_driver = False
      self._dev = usb.core.find(idVendor=USBCar.VID, idProduct=USBCar.PID)
      if self._dev is None:
        raise ValueError("Device not found")
    


    Функция usb.core.find() ищет устройство по его ID. Подробности см. github.com/walac/pyusb/blob/master/docs/tutorial.rst

      if self._dev.is_kernel_driver_active(0):
        self._dev.detach_kernel_driver(0)
        self._had_driver = True
    


    Мы отвязываем драйвер ядра, как мы делали в случае с lsusb. 0 – номер интерфейса. По выходу из программы его надо привязать обратно через release(), если он был активен. Поэтому мы запоминаем начальное состояние в self._had_driver.

      self._dev.set_configuration()
    


    Запускаем конфигурацию. Этот код эквивалентен следующему коду, который PyUSB скрывает от программиста:

      self._dev.set_configuration(1)
      usb.util.claim_interface(0)
    def release(self):
      usb.util.release_interface(self._dev, 0)
      if self._had_driver:
        self._dev.attach_kernel_driver(0)
    


    Этот метод надо вызвать перед завершением программы. Мы отпускаем использовавшийся интерфейс и присоединяем драйвер ядра обратно.

    Передвижение машинки:

    def move(self, direction):
      ret = self._dev.ctrl_transfer(0x21, 0x09, 0x0200, 0, [direction])
      return ret == 1
    


    direction – одно из значений, определённых в начале класса. ctrl_transfer() передаёт управляющие команды. Данные передаются как строка или как список. Возвращает метод количество записанных байт. Поскольку у нас всего один байт, мы возвращем True в этом случае, и False в ином.

    Метод для статуса батареи:

    def battery_status(self):
      try:
        ret = self._dev.read(0x81, 1, timeout=self.READ_TIMEOUT)
        if ret:
          res = ret.tolist()
          if res[0] == 0x05:
            return 'charging'
          elif res[0] == 0x85:
            return 'charged'
        return 'unknown'
      except usb.core.USBError:
        return 'out of the garage'
    


    Метод read() принимает адрес конечной точки и количество байт для чтения. Тип передачи определяется конечной точкой и хранится в дескрипторе. Также мы задаём нестандартное время таймаута, чтобы программа работала быстрее. Device.read() возвращает массив, который мы конвертируем в список. Мы проверяем его первый байт, чтобы определить статус зарядки. Если машинка не в гараже, то вызов read() не выполнится, и выбросит ошибку usb.core.USBError. Мы предполагаем, что эта ошибка происходит именно из-за этого. В остальных случаях мы возвращаем статус ‘unknown’.

    Класс UI инкапсулирует интерфейс пользователя. Пройдёмся по основным вещам. Главный цикл находится в UI.main_loop(). Мы задаём фон с картинкой, показываем уровень заряда, если машинка в гараже, и рисуем кнопки управления. Затем ждём события – если это клик, то двигаем машинку в заданном направлении через USBCar.move().

    Вся программа, включая GUI, занимает чуть больше 200 строк. Неплохо для устройства без документации.

    Конечно, мы специально взяли довольно простое устройство. Но в мире есть довольно много схожих с нашим устройств, и многие используют протоколы, не сильно отличающиеся от того, что мы расковыряли. Реверс-инжениринг сложного устройства – задача непростая, но уже сейчас вы можете добавить в Linux поддержку какой-нибудь безделушки вроде устройства, сообщающего о полученном e-mail. Если это и не сильно полезно – то, по крайней мере, это интересно.
    Support the author
    Share post

    Similar posts

    Comments 18

      +1
      Большое спасибо, может я, наконец, отреверсю свою клавиатуру razer blackwidow, что бы задействовать макроклавиши или вообще запись макросов.
        0
        Я пытался заставить работать китайско-индийские 3G модемы. Индусы берут китайский модем, меняют или оставляют корпус, что-то там мудрят, чтобы usb_modeswitch не работал. И eject тоже не работает. Поддержка фирм, выпустивших эти модемы, тоже помогать не хочет. «Наши модемы поддерживают только Windows».

        Какое-то время я пытался мониторить команды, которые Windows отправляет модему, чтобы перевести модем из режима виртуального CD-ROM в режим модема, но из-за отсутствия подобной статьи мои усилия не увенчались успехом. Теперь может быть попробую еще раз.
          +13
          Спасибо за перевод! Не нашел ссылку на оригинал, вдруг кому интересно будет — www.linuxvoice.com/drive-it-yourself-usb-car-6/
            +10
            Только почему-то автор как перевод это не оформил и источник не указал.
            Из-за этого сначала подумал, что это авторский материал.
              0
              Прошу прощения, я периодически забываю оформлять, как перевод. Добавил ссылку.
              • UFO just landed and posted this here
              0
              Перевод не плох, но оригинал никогда не помешает. Спасибо.
              +4
              «Я не искал. Я их сам написал.» ©
                +1
                А дальше что? его можно закоммитить в ядро или в дистрибутив?
                как это происходит?
                  0
                  Про дистрибутив не скажу, а в ядро тут комитить нечего т.к. автор для ядра ничего не делал.
                  Мало того, он сам пишет что в ядре уже есть драйвер HID который установился для его машинки, но он решил его отключить. Зачем это нужно было делать осталось загадкой.
                    0
                    Дело в том, что сам драйвер HID не зает о том что нужно/можно делать с машинкой.
                      0
                      Так libusb тоже не знает как управлять машинкой.
                      Драйвер HID делает файл в /dev и позволяет отправлять и читать HID репорты оттуда обычными средствами ОС. Фактически он уже делает все то, что автор сделал с помощью libusb и pyusb.
                  +9
                  Я как-то решал «обратную» задачку с реверс-инжинирингом (linux->windows). У меня есть очень старый USB сканер пятнадцатилетней давности, но рабочий. Драйверов по Windows 7 для него, понятное дело, нет. Только под ХР. Я запустил виртуальную машину с ХР, и сдёрнул там весь протокол обмена через оригинальный драйвер. Задачу сильно облегчило то, что нашёлся драйвер под линукс, вот оттуда я практически весь протокол и взял без нудного разбора логов (так что «реверса» как такового я практически избежал). Затем написал программку, общающуюся со сканером напрямую и теперь он у меня под семёркой работает на ура (причём даже лучше чем раньше — он отдаёт картинку в RAW, а калибровки в файле сохраняю, так что он к работе моментально готов и больше не елозит под крышкой перед сканированием. Я использовал довольно специфический инструмент разработки — LabVIEW, что позволило решить задачку практически за вечер. Если интересно, могу написать.
                    +2
                    Пишите, не спрашивайте.
                    0
                    Кто-нибудь понял зачем автор отключил от своего устройства драйвер usbhid и решил работать с устройством через libusb?
                    Разве python не позволяет открыть файл в /dev и делать с ним read/write?
                    • UFO just landed and posted this here
                        0
                        А нет статьи по разработке какого-нибудь USB-устройства?
                          +1
                          Я разрабатывал USB-джойстик + самопальное устройство ввода-вывода (composite device) на контроллере AT91SAM7. Не скажу, что это было просто, поскольку пришлось обрабатывать все транзакции, т.е. фактически, реализовывать весь стек USB со стороны контроллера.
                          Сейчас, полагаю, есть множество готовых фреймворков для контроллеров с USB, ну а если нужен просто HID, то точно есть, поскольку еще в 2008 году была на свете библиотека для AVRов, где дерганьем пинов реализовывался джойстик.

                          Only users with full accounts can post comments. Log in, please.