Недавно на eBay мне попалась партия интересных USB-девайсов (Epiphan VGA2USB LR), которые принимают на вход VGA и отдают видео на USB как веб-камера. Меня настолько обрадовала идея, что больше никогда не придётся возиться с VGA-мониторами, и учитывая заявленную поддержку Linux, я рискнул и купил всю партию примерно за 20 фунтов (25 долларов США).
Получив посылку, я подключил устройство, но оно даже не подумало появиться в системе как UVC. Что не так?
Я изучил сайт производителя и обнаружил, что для работы требуется специальный драйвер. Для меня это была новая концепция, ведь в ядре моего дистрибутива Linux обычно есть драйверы для всех устройств.
К сожалению, поддержка драйверов именно для этих устройств закончилась в Linux 4.9. Таким образом, его не увидит ни одна из моих систем (Debian 10 на Linux 4.19 или последняя версия LTS Ubuntu на Linux 5.0).
Но ведь это можно исправить, верно? Конечно, файлы ведь идут в пакете 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.
Я убедился в этом, открыв одну из коробок:
Красный |
ISL98002CRZ-170 — работает как ADC для сигналов VGA |
Жёлтый |
XC6SLX16 — Xilinx Spartan 6 FPGA |
Циан |
64 МБ DDR3 |
Маджента |
CY7C68013A — USB-контроллер / фронтенд |
Поскольку для «загрузки» устройства нужно отправить ему битстрим/прошивку, придётся поискать его в предварительно скомпилированных бинарниках. Я запустил 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.
Считывать USB-пакеты из файлов pcap может и tshark, и tcpdump, но обе сохраняют их лишь частично. Поскольку у каждой утилиты были разные части головоломки, я написал небольшую программу, которая объединяет выходные данные обеих программ в структуры go, чтобы воспроизвести пакеты обратно на устройство.
В этот момент я заметил, что загрузка происходит в два этапа: сначала USB-контроллер, а затем FPGA.
Я застрял на несколько дней: казалось, весь битстрим загружается, но устройство не запускалось, хотя пакеты от реального драйвера и моей симуляции выглядят вроде идентично.
В итоге я решил проблему, тщательно изучив pcap с учётом времени ответа на каждый пакет — и заметил большую разницу во времени одного конкретного пакета:
Оказалось, что из-за небольшой опечатки запись происходила в неправильную область устройства. Будет мне уроком, как вводить значения вручную…
Тем не менее, на устройстве наконец-то замигал светодиод! Огромное достижение!
Было относительно просто реплицировать те же пакеты, которые запускали передачу данных, так что я смог написать конечную точку USB Bulk и мгновенно сбросить данные на диск!
Вот тут и начались настоящие сложности. Потому что после анализа оказалось, что данные не были явно закодированы каким-либо образом.
Для начала я запустил perf для общего представления о трассировке стека драйверов во время работы:
Хотя я мог выловить функции с данными фреймов, но понять кодировку самих данных никак не удавалось.
Чтобы лучше понять, что происходит внутри настоящего драйвера, я даже попробовал инструмент Ghidra от АНБ:
Хотя Ghidra невероятна (когда я впервые использовал её вместо IDA Pro), но всё ещё недостаточно хороша, чтобы помочь мне понять драйвер. Для реверс-инжиниринга требовался другой путь.
Я решил поднять виртуальную машину Windows 7 и взглянуть на драйвер Windows, вдруг он подбросит идеи. И тогда заметил, что для устройств имеется SDK. Один из инструментов оказался особенно интересным:
PS> ls
Directory: epiphan_sdk-3.30.3.0007epiphanbin
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 [format] [compression level]
- sets compression level [1..5],
- captures and saves compressed frames to a file
v2u_dec x [format]
- decompresses frames from the file to separate BMP files
Этот инструмент позволяет «выхватывать» единичные фреймы, причём изначально они не сжимаются, чтобы была возможность обработать фреймы позже на более быстрой машине. Это практически идеально, и я реплицировал последовательность пакетов USB, чтобы получить эти несжатые блобы. Количество байтов соответствовало примерно трём (RGB) на пиксель!
Первоначальная обработка этих изображений (просто принимая вывод и записывая его как пиксели RGB) дала нечто отдалённо напоминающее реальную картинку, которое устройство получало через VGA:
После некоторой отладки в hex-редакторе выяснилось, что каждые 1028 байт повторяется какой-то маркер. Немного стыдно, как много времени я потратил на написание фильтра. С другой стороны, в процессе можно было насладиться некоторыми образцами современного искусства.
Затем я понял, что наклон и искажение изображения вызваны пропуском и переносом пикселя на каждой строке (x=799 не равно x=800). И тогда, наконец, у меня получилось почти правильное изображение, если не считать цвета:
Сначала я думал, что проблема с калибровкой из-за выборки данных, когда вход VGA застрял на сплошном цвете. Для исправления я сделал новое тестовое изображение, чтобы выявить такие проблемы. Задним числом понимаю, что надо было использовать что-то вроде тестовой карты Philips PM5544.
Я загрузил изображение на ноутбук, и тот выдал такую картинку VGA:
Тут мне пришло воспоминание о какой-то давней работе по 3D-рендерингу/шейдеру. Это было очень похоже на цветовую схему YUV.
В итоге я погрузился в чтение литературы по YUV и вспомнил, что во время реверс-инжиниринга официального драйвера ядра, если я ставил точку останова на функции под названием v2ucom_convertI420toBGR24
, то система зависала без возможности возобновления. Так что, может, на входе была кодировка I420 (от -pix_fmt yuv420p
), а выход RGB?
После применения встроенной в Go функции YCbCrToRGB изображение внезапно стало намного ближе к оригиналу.
Мы сделали это! Даже сырой драйвер выдавал 7 кадров в секунду. Честно говоря, мне этого достаточно, так как я использую VGA только в случае аварии как резервный дисплей.
Итак, теперь мы знаем это устройство достаточно хорошо, чтобы объяснить алгоритм его запуска с самого начала:
- Нужно инициализировать USB-контроллер. Судя по объёму информации, на самом деле драйвер передаёт на него код для загрузки.
- Когда вы закончите загрузку USB, устройство отключится от шины USB и через мгновение вернётся с одной конечной точкой USB.
- Теперь можно отправлять битстрим FPGA, по одному 64-байтовому пакету USB за каждую контрольную передачу.
- По окончании передачи индикатор на устройстве начнёт мигать зелёным цветом. На этом этапе можно отправить то, что кажется последовательностью параметров (overscan и другие свойства).
- Затем запускаем контрольный пакет для получения фрейма, в пакете указано разрешение. Если отправить запрос фрейма 4:3 на широкоэкранный вход, то это обычно приведёт к повреждению фрейма.
Для максимальной простоты использования я внедрил в драйвер небольшой веб-сервер. Через браузерные MediaRecorder API он легко записывает поток с экрана в видеофайл.
Предупреждая неизбежные претензии к качеству экспериментального кода, скажу сразу: я им не горжусь. Наверное, он в таком состоянии, какого мне достаточно для приемлемого использования.
Код и готовые сборки для Linux и OSX лежат на GitHub.
Даже если программу никто никогда не запустит, для меня это было чертовски увлекательное путешествие в дебрях протокола USB, отладки ядра, реверс-инжиниринга модуля и формата декодирования видео! Если вам нравятся такие вещи, можете посмотреть другие статьи в блоге.