Хабрахабр

[Перевод] Пишем USB-драйверы для заброшенных устройств

Меня настолько обрадовала идея, что больше никогда не придётся возиться с VGA-мониторами, и учитывая заявленную поддержку Linux, я рискнул и купил всю партию примерно за 20 фунтов (25 долларов США). Недавно на eBay мне попалась партия интересных USB-девайсов (Epiphan VGA2USB LR), которые принимают на вход VGA и отдают видео на USB как веб-камера.

Что не так? Получив посылку, я подключил устройство, но оно даже не подумало появиться в системе как UVC.

Для меня это была новая концепция, ведь в ядре моего дистрибутива Linux обычно есть драйверы для всех устройств.
К сожалению, поддержка драйверов именно для этих устройств закончилась в Linux 4. Я изучил сайт производителя и обнаружил, что для работы требуется специальный драйвер. Таким образом, его не увидит ни одна из моих систем (Debian 10 на Linux 4. 9. 0). 19 или последняя версия LTS Ubuntu на Linux 5.

Конечно, файлы ведь идут в пакете DKMS, который по требованию собирает драйвер из исходного кода, как и многие обычные драйверы… Но ведь это можно исправить, верно?

Но здесь не так. Печально.

Я начал его изучать, прикидывая сложность реверс-инжиниринга, и нашёл несколько интересных строк: Внутри пакета оказался только предварительно скомпилированный бинарник vga2usb.o.

$ strings vga2usb.ko | grep 'v2uco' | sort | uniq
v2ucom_autofirmware
v2ucom_autofirmware_ezusb
v2ucom_autofirmware_fpga

Так это на самом деле FPGA-on-a-stick? Как же заставить работать нечто подобное?

Это заставило меня задуматься: что же он может защищать внутри драйвера? Ещё одной забавной и слегка тревожной находкой стали строки с параметрами закрытого ключа DSA.

$ strings vga2usb.ko | grep 'epiphan' | sort | uniq
epiphan_dsa_G
epiphan_dsa_P
epiphan_dsa_Q

Чтобы изучить драйвер в его нормальной среде, я поднял виртуальную машину с Debian 9 (последний поддерживаемый релиз) и сделал KVM USB Passthrough, чтобы дать прямой доступ к устройству. Затем установил драйвер и убедился, что он работает.

Я надеялся, что устройство отправляет необработанные или почти необработанные фреймы, поскольку это облегчило бы написание драйвера для пользовательского пространства. После этого мне захотелось посмотреть, как выглядит протокол связи.

Для этого я загрузил на хост виртуальной машины модуль usbmon и запустил Wireshark для захвата USB-трафика на устройство и с него во время запуска и захвата видео.

Вероятно, оно действительно основано на платформе FPGA без хранилища данных. Я обнаружил, что при запуске на устройство передаётся большое количество мелких пакетов, прежде чем оно начинает захватывать картинку. Каждый раз после подключения драйвер передавал на устройство прошивку в виде битстрима FPGA.

Я убедился в этом, открыв одну из коробок:

Поскольку для «загрузки» устройства нужно отправить ему битстрим/прошивку, придётся поискать его в предварительно скомпилированных бинарниках. Я запустил binwalk -x и начал искать какие-нибудь сжатые объекты (zlib). Для этого я написал скрипт поиска hex-последовательностей — и указал три байта из перехваченного пакета.

$ bash scan.sh "03 3f 55"
trying 0.elf
trying 30020
trying 30020.zlib
trying 30020.zlib.decompressed
...
trying 84BB0
trying 84BB0.zlib
trying 84BB0.zlib.decompressed
trying AA240
trying AA240.zlib
trying AA240.zlib.decompressed
000288d0 07 2f 03 3f 55 50 7d 7c 00 00 00 00 00 00 00 00 |./.?UP}|........|
trying C6860
trying C6860.zlib

После распаковки файла AA240.zlib оказалось, что там недостаточно данных для полного битстрима. Поэтому я решил захватить прошивку из пакетов USB.

Поскольку у каждой утилиты были разные части головоломки, я написал небольшую программу, которая объединяет выходные данные обеих программ в структуры go, чтобы воспроизвести пакеты обратно на устройство. Считывать USB-пакеты из файлов pcap может и tshark, и tcpdump, но обе сохраняют их лишь частично.

В этот момент я заметил, что загрузка происходит в два этапа: сначала USB-контроллер, а затем FPGA.

Я застрял на несколько дней: казалось, весь битстрим загружается, но устройство не запускалось, хотя пакеты от реального драйвера и моей симуляции выглядят вроде идентично.

В итоге я решил проблему, тщательно изучив pcap с учётом времени ответа на каждый пакет — и заметил большую разницу во времени одного конкретного пакета:

Будет мне уроком, как вводить значения вручную… Оказалось, что из-за небольшой опечатки запись происходила в неправильную область устройства.

Огромное достижение! Тем не менее, на устройстве наконец-то замигал светодиод!

Your browser does not support HTML5 video.

Было относительно просто реплицировать те же пакеты, которые запускали передачу данных, так что я смог написать конечную точку USB Bulk и мгновенно сбросить данные на диск!

Потому что после анализа оказалось, что данные не были явно закодированы каким-либо образом. Вот тут и начались настоящие сложности.

Для начала я запустил perf для общего представления о трассировке стека драйверов во время работы:

Хотя я мог выловить функции с данными фреймов, но понять кодировку самих данных никак не удавалось.

Чтобы лучше понять, что происходит внутри настоящего драйвера, я даже попробовал инструмент Ghidra от АНБ:

Для реверс-инжиниринга требовался другой путь. Хотя Ghidra невероятна (когда я впервые использовал её вместо IDA Pro), но всё ещё недостаточно хороша, чтобы помочь мне понять драйвер.

И тогда заметил, что для устройств имеется SDK. Я решил поднять виртуальную машину Windows 7 и взглянуть на драйвер Windows, вдруг он подбросит идеи. Один из инструментов оказался особенно интересным:

PS> ls Directory: epiphan_sdk-3.30.3.0007\epiphan\bin Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 10/26/2019 10:57 AM 528384 frmgrab.dll
-a--- 10/27/2019 5:41 PM 1449548 out.aw
-a--- 10/26/2019 10:57 AM 245760 v2u.exe
-a--- 10/26/2019 10:57 AM 94208 v2u_avi.exe
-a--- 10/26/2019 10:57 AM 102400 v2u_dec.exe
-a--- 10/26/2019 10:57 AM 106496 v2u_dshow.exe
-a--- 10/26/2019 10:57 AM 176128 v2u_ds_decoder.ax
-a--- 10/26/2019 10:57 AM 90112 v2u_edid.exe
-a--- 10/26/2019 10:57 AM 73728 v2u_kvm.exe
-a--- 10/26/2019 10:57 AM 77824 v2u_libdec.dll PS> .\v2u_dec.exe
Usage: v2u_dec <number of frames> [format] [compression level] <filename> - sets compression level [1..5], - captures and saves compressed frames to a file v2u_dec x [format] <filename> - decompresses frames from the file to separate BMP files

Этот инструмент позволяет «выхватывать» единичные фреймы, причём изначально они не сжимаются, чтобы была возможность обработать фреймы позже на более быстрой машине. Это практически идеально, и я реплицировал последовательность пакетов USB, чтобы получить эти несжатые блобы. Количество байтов соответствовало примерно трём (RGB) на пиксель!

Первоначальная обработка этих изображений (просто принимая вывод и записывая его как пиксели RGB) дала нечто отдалённо напоминающее реальную картинку, которое устройство получало через VGA:

Немного стыдно, как много времени я потратил на написание фильтра. После некоторой отладки в hex-редакторе выяснилось, что каждые 1028 байт повторяется какой-то маркер. С другой стороны, в процессе можно было насладиться некоторыми образцами современного искусства.

И тогда, наконец, у меня получилось почти правильное изображение, если не считать цвета: Затем я понял, что наклон и искажение изображения вызваны пропуском и переносом пикселя на каждой строке (x=799 не равно x=800).

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

Я загрузил изображение на ноутбук, и тот выдал такую картинку VGA:

Это было очень похоже на цветовую схему YUV. Тут мне пришло воспоминание о какой-то давней работе по 3D-рендерингу/шейдеру.

Так что, может, на входе была кодировка I420 (от -pix_fmt yuv420p), а выход RGB? В итоге я погрузился в чтение литературы по YUV и вспомнил, что во время реверс-инжиниринга официального драйвера ядра, если я ставил точку останова на функции под названием v2ucom_convertI420toBGR24, то система зависала без возможности возобновления.

После применения встроенной в Go функции YCbCrToRGB изображение внезапно стало намного ближе к оригиналу.

Даже сырой драйвер выдавал 7 кадров в секунду. Мы сделали это! Честно говоря, мне этого достаточно, так как я использую VGA только в случае аварии как резервный дисплей.

Итак, теперь мы знаем это устройство достаточно хорошо, чтобы объяснить алгоритм его запуска с самого начала:

  1. Нужно инициализировать USB-контроллер. Судя по объёму информации, на самом деле драйвер передаёт на него код для загрузки.
  2. Когда вы закончите загрузку USB, устройство отключится от шины USB и через мгновение вернётся с одной конечной точкой USB.
  3. Теперь можно отправлять битстрим FPGA, по одному 64-байтовому пакету USB за каждую контрольную передачу.
  4. По окончании передачи индикатор на устройстве начнёт мигать зелёным цветом. На этом этапе можно отправить то, что кажется последовательностью параметров (overscan и другие свойства).
  5. Затем запускаем контрольный пакет для получения фрейма, в пакете указано разрешение. Если отправить запрос фрейма 4:3 на широкоэкранный вход, то это обычно приведёт к повреждению фрейма.

Для максимальной простоты использования я внедрил в драйвер небольшой веб-сервер. Через браузерные MediaRecorder API он легко записывает поток с экрана в видеофайл.

Наверное, он в таком состоянии, какого мне достаточно для приемлемого использования. Предупреждая неизбежные претензии к качеству экспериментального кода, скажу сразу: я им не горжусь.

Код и готовые сборки для Linux и OSX лежат на GitHub.

Если вам нравятся такие вещи, можете посмотреть другие статьи в блоге. Даже если программу никто никогда не запустит, для меня это было чертовски увлекательное путешествие в дебрях протокола USB, отладки ядра, реверс-инжиниринга модуля и формата декодирования видео!

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть