Реверс‑инжиниринг и криптография: заставляем десктопные USB‑токены работать на Android

Всем привет. Я уже довольно давно увлекаюсь темой криптографии, и недавно мне удалось реализовать весьма интересный кейс. Начался он как интересный спор в одном ИТ‑чатике, а перерос в весьма интересный (хотя пока еще не примененный) проект. Но обо всем по порядку.
USB‑токены, что это и с чем едятся
Когда речь заходит о криптографии, первыми на ум приходят HTTPS‑сертификаты, но это веб‑безопасность. Помимо защиты соединений, у криптографии есть еще одна сторона: электронная подпись. Я думаю, об этом очень часто слышат даже за пределами ИТ, ведь электронные подписи сейчас используются в госуслугах, при подписании бизнес‑контрактов, даже при сделках с недвижимостью.
Но так как электронные подписи приравниваются к рукописной подписи, с соответсвующей юридической значимостью, их нужно очень хорошо защитить. И вот защита электронных подписей как раз обеспечивается специальными, похожими на флешки, физическими устройствами, так называемыми токенами.
Чаще всего они выглядят как обычные флешки, иногда как брелки, или даже Bluetooth‑ или NFC‑модули. На картинке один из примеров, токен российского производства.

В отличие от Bluetooth‑ и NFC‑токенов, которые чаще всего ориентированы на использование на мобильных устройствах, USB‑токены обычно ориентированы на использование исключительно на десктопе. Есть другой вариант — TypeC‑токены, они нередко бывают кроссплатформенными, и вендоры часто поставляют как десктопные, так и мобильные SDK.
В чем же проблема?
Вот допустим есть N‑ое количество пользователей, у которых на руках есть исключительно USB‑токены. Эти токены выдавались пользователям еще очень давно, когда даже в мыслях у бизнеса не было пытаться работать с электронной подписью на мобилках. И тут приходит понимание: мобильные устройства очень удобны, их всегда можно таскать с собой, а те же самые ноутбуки (на которых как раз эти токены и работают) таскать с собой везде и всюду как‑то не охота. И было бы классно если бы пользователи могли бы работать с документами, в том числе и подписывать их, прямо с мобильного устройства.
И как же достичь этого? С одной стороны, можно просто закупить партию новых токенов, которые можно использовать с мобилками — возможно, по такому сценарию и пошли бы большинство бизнесов. А можно написать свои драйвера и подружить уже имеющиеся у пользователей токены с мобилками, но тогда придется пользователям самим покупать себе OTG‑переходники (либо бизнес может сам раскошелиться на покупку этих переходников).
Честно, я не маркетолог и не предприниматель, и рассуждать, насколько любое из этих решений рентабельно, тут не буду. Я лишь хочу показать примерный процесс интеграции токенов в приложения и сервисы тех бизнесов, которые все же захотели бы пойти по второму сценарию.
Какие именно токены будем интегрировать?
Конкретно в моей стране в ходу есть две модели токенов:

Этот токен оказался самым простым в интеграции, и именно его мы «препарируем» в этой статье.

Несмотря на то, что драйвер второго токена вполне себе опенсорсный, его интеграция оказалась напорядок сложнее, и пока что я даже ее не завершил, позже в статье я объясню почему.
Оба токена китайского производства, доступны на самых разных маркетплейсах. И далее мы углубимся в дивный мир криптографических устройств и стандартов, поймем почему токены такие защищенные, и как даже под закрытое устройство, не имея в руках ничего кроме десктопного драйвера от вендора, токена и ноутбука (даже реальный смартфон опционален, по крайней мере если речь идет про Android) можно написать (почти) полноценный драйвер для столь серьезного средства защиты информации:)
Почему токены такие защищенные?
Особенность этих токенов в том, что они стандартизированы по FIPS (Federal Information Processing Standard) — американский стандарт по обработке информации, в котором расписаны требования к средствам защиты информации, правилам работы с закрытыми ключами и так далее — своего рода аналог стандартов по СЗИ в РФ.
Помним как я выше писал о том, что интегрировать второй токен (ePass) оказалось значительно сложнее? А все потому что токены могут оперировать в нескольких режимах: FIPS mode и nonFIPS mode. Токен от Longmai по‑умолчанию работает в nonFIPS режиме, что значит что соединение между токеном и компьютером не шифруется. В то время как ePass наоборот, по‑умолчанию работает в FIPS режиме, что значит, что нам ОБЯЗАТЕЛЬНО нужно будет реализовывать логику Secure Messaging в драйвере, из‑за чего, собственно, я пока что и отложил интеграцию ePass.
Также маркером режима работы является цвет индикатора на самом токене, на nonFIPS токене горит красный индикатор, а на FIPS токене — зеленый. Эти режимы может менять поставщик токенов, и делается это с помощью специализированных утилит, либо дерганием методов из драйвера вендора вручную.
Главная задача токена — защитить закрытый ключ пользователя. Для этого, согласно тому же стандарту FIPS, применяются самые разные методы, такие как противодествие вскрытию (обнуление закрытых ключей в случае нарушения), защита пин‑кодом (токен блокируется после нескольких неверных вводов), а также противодействие энергетическим воздействиям на корпус или порт токена (в англоязычной литературе это называется fault injection, что можно перевести как «внедрение ошибок»). Все эти меры позволяют предотвратить атаки по сторонним каналами и сделать устройство бесполезным в руках вора.
Как токен общается с компьютером?
В идеале хотелось бы SDK от вендора, но так как его нет, мы напишем нужные нам компоненты самостоятельно. Внизу диаграмма взаимодействия между токеном и комьютером, и некоторую ее часть нам придется перенести в свой драйвер, так как этой логики нет в ядре Android (в отличие от Linux, где это может работать чуть ли не из коробки).

Важно сказать, что в рамках работы над драйвером мы можем назвать токен смарт‑картой, так как по сути все это работает через один и тот же стек. Если бы у нас на руках была бы реальная смарт‑карта и отдельно от нее ридер, логика работы была бы той же самой, но нельзя было бы сказать что токен == ридер, а сейчас мы так сказать можем, поскольку токен в рамках данной диаграммы можно представить как ридер с одной единственной смарт‑картой.
Другой важной оговоркой является то, что на диаграмме PKCS#11-модули работают над CCID‑драйвером, это не значит что нам обязательно нужен PKCS#11 для работы с токеном. CCID позволяет работать с токеном напрямую, то есть из диаграммы можно даже убрать слой PKCS#11. Но мы его оставим, просто чтобы не нарушать целостность картины, ну и еще потому что одним из главных источников команд для токена будет именно вендорский PKCS#11-модуль, потому что он все равно работает с CCID, только через него и никак иначе.
Что нам нужно, чтобы токен заработал на Android?
Для того, чтобы токен мог успешно «общаться» с токеном, нам нужно реализовать логику работы CCID, которой нет ядре Android, а потребителям нашего драйвера можно просто поставлять библиотеку, которую можно интегрировать в приложение. То есть, по сути, мы пишем user‑space драйвер.
Начиная с Android 3.0, в системе уже имеется реализация USB‑стека, которая нам нужна для физического обмена сообщениями с токеном, нам же остается просто отправлять на токен корректные команды и правильно интерпретировать ответы от него.
Выбираем стек
Надеюсь меня не закидают тут тапками, но никакого хардкора с указателями и сильно глубокой системщиной не будет:)
А все потому что на своей текущей работе я пишу на c#, и именно его я выбрал в качестве инструмента для реализации этой задачи. Это возможно благодаря таким технологиям, как MAUI, которые делают.NET кроссплатформенным. Более того, вся разработка осуществлялась на windows.
Но разумеется, все было написано не совсем на голом дотнете, так как опенсорс‑сообщество уже предлагает определенные готовые решения для низкоуровневой работы со смарт‑картами на десктопе, и нам нужно будет всего‑навсего адаптировать их под свои нужды на мобилках.
У нас есть:
Pcsc‑sharp — библиотека, позволяющая отправлять APDU‑команды на токен напрямую, работает на windows, linux и macOS, мы адаптируем ее под android.
Device.net — комплект библиотек для работы с USB‑устройствами напрямую, позволяет отправлять сырые байты на самые разные устройства, но не для смарт‑карт на десктопе, так как смарт‑карты обычно эксклюзивно блокируются операционной системой на уровне ядра; мы будем использовать ее для бесшовного переключения между десктопным (во время интеграционных тестов) и мобильным (работа на реальном смартфоне) устройствами.
LibWDI — потребуется для интеграционного тестирования CCID‑слоя без использования смартфона/эмулятора; как было сказано ранее, ОС блокирует токен исключительно для себя, и для тестирования наших CCID‑пакетов нам придется на время теста подменять драйвер токена на обычный win‑usb драйвер, чтобы система думала что токен — это просто generic usb device и не блокировала его.
PKCS11interop — используется в юнит‑тестах, для проведения тестовых операций с токеном, как если бы мы работали с ним в обычном приложении, тем самым мы создаем и анализируем трафик между вендорским драйвером и нашим компьютером, это нужно для извлечения APDU‑команд.
PKCS#11-модуль токена — именно его мы будем использовать для общения с токеном в тестах, генерации трафика между токеном и компьютером, и именно его мы декомпилируем для извлечения нужных нам команд из него.
Chilkat ScMinidriver — используется в интеграционных тестах для тестирования CCID‑слоя драйвера, позволяя нам отправлять сырые CCID‑пакеты прямо на токен.
Это что касается используемых проектов в коде, помимо этого нам понадобится некоторый инструментарий на самом компьютере, это все не входит непосредственно в проект:
Ghidra — очень мощная программа для декомпиляции исполняемых бинарников; она понадобится нам для извлечения всего лишь одной, но очень важной APDU‑команды.
Wireshark + USBPcap‑драйвер — нужно для анализа трафика между токеном и компьютером.
Google AI или любой другой ИИ агент — хоть и опционально, но так как я вкатывался в работу со смарт‑картами с нуля, я использовал его для анализа трафика и превращения команд, отловленных с помощью Wireshark, в рабочий код.
Android‑эмулятор — нужен нам для запуска мини‑приложения, которое мы напишем специально для тестирования работы нашего драйвера на реальном устройстве.
Основные усилия были направлены на написание тестов, которые позволят нам убедиться в корректности работы как самих APDU‑команд, так и CCID‑части драйвера. Примечательно, что всю работу можно вести прямо на компьютере, даже не беря в руки смартфон, для этого нам нужно соответствующим образом настроить android‑эмулятор, об этом будет рассказано ниже.
Разберём основные подводные камни и сложности, с которыми столкнулся проект.
Для чтения сертификатов с токена не нужен пин‑код, и вот почему
Я ранее работал с PKCS11, и там на каждый чих нужно было вызывать Login(), а вот при работе с CCID этого вообще не нужно! А все потому что PKCS11 и CCID слои работают с токеном на разных уровнях, и если PKCS11 драйвер сохраняет сессии внутри себя, то CCID вообще не знает о сессиях ничего, и работает напрямую с файлами токена напрямую.
Для лучшего понимания разницы между этими слоями подойдет вот это:

Таблица взята из спецификации ScMinidriver [прямая ссылка на скачивание.docx — прим. модератора] от майкрософт, страница 103. Иными словами, с точки зрения CCID, токен — просто своего рода флешка с файлами, доступ к которым можно получить определенными командами (теми самыми APDU‑командами, которые мы прослушиваем с помощью Wireshark).
Различия между вводом пин‑кода через PKCS11.Login и CCID.VerifyPin
У токена состояния только два: залогинен или разлогинен. И если после завершения работы с драйвером не сбрасывается состояние на самом токене, это оставляет его залогиненным для других процессов пока он не будет извлечен из компьютера и вставлен снова.
Сам PKCS11-модуль корректно обрабатывает эти вещи, и нам тоже нужно вручную отправлять APDU‑команду Logout на токен, иначе операции с токеном могут выполнять другие процессы на компьютере/телефоне, не требуя ввода пин‑кода. Это как оставить ключи от сейфа с миллионом долларов под ковриком на пороге.
Тестирование CCID‑слоя
Самая интересная часть работы, так как если протестировать работу APDU‑команд на десктопе легко, тот тут уже имеем дело с уровнем драйверов самой ОС, и чтобы протестировать работу этого слоя, нам придется «подвинуть» системный CCID‑драйвер, чтобы получить сырой доступ к токену и иметь возможность отправлять ему свои собственные CCID‑пакеты.
Делается это с помощью libwdi, мы просто подменяем системный CCID‑драйвер на обычный WinUSB, а из кода работаем с токеном как с обычном USB‑устройством с помощью библиотек device.net.
К примеру, этот код сначала подменяет ccid‑драйвер winusb, а потом возвращает все обратно:
private async Task SwitchToWinUsbAsync(string vid, string pid)
{
Console.WriteLine($"[Setup] Переключаем устройство {HardwareId} на WinUSB через wdi-simple...");
// Ищем wdi-simple.exe в директории запуска тестов
string wdiPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "wdi-simple.exe");
if (!File.Exists(wdiPath))
{
throw new FileNotFoundException("Файл wdi-simple.exe не найден в директории сборки. Убедитесь, что он копируется в Output Directory.");
}
// Параметры для wdi-simple:
// --vid и --pid указывают на наше целевое устройство.
// --driver=0 указывает libwdi установить именно WinUSB (1 = libusb-win32, 2 = libusbK).
// --name задает кастомное имя устройства в диспетчере.
string arguments = $"--vid=0x{vid} --pid=0x{pid} --type=0 --name=\"Raw USB Smart Card Token\"";
await ScriptRunner.RunProcessAsync(wdiPath, arguments);
// Даем Windows время чтобы окончательно перестроить USB-стек после Zadig-инсталляции
await Task.Delay(4000);
Console.WriteLine("[Setup] Устройство успешно переведено в режим WinUSB.");
}
[SupportedOSPlatform("windows")]
public async Task RestoreDefaultDriverAsync()
{
Console.WriteLine($"[TearDown] Откатываем устройство {HardwareId} к стандартному CCID драйверу...");
var resultCleanup = await ScriptRunner.RunScriptAsync("wdi-cleanup.ps1");
Console.WriteLine($"Результат удаления устройства:\n[Ошибки]: {resultCleanup.Error}\n[Вывод]:{resultCleanup.Output}\n");
var resultInstall = await ScriptRunner.RunScriptAsync(Path.Combine("install.bat"), workDir: "drivers_mtoken");
Console.WriteLine($"Результат установки устройства:\n[Ошибки]: {resultInstall.Error}\n[Вывод]:{resultInstall.Output}\n");
await Task.Delay(4000);
SmartCardDeviceManager.ResetAllSmartCardReaders();
Console.WriteLine("[TearDown] Стандартный CCID драйвер смарт-карт успешно восстановлен.");
}Возврат токена обратно к классу смарт‑карт происходит с помощью специального powershell‑скрипта, иначе мне это сделать просто не удалось + тут была другая проблема: из‑за моих других экспериментов я чуть не сломал дерево устройств, так как в нем было слишком много dummy‑устройств, из‑за которых мой токен не хотел видеться как обычно.
Вот кстати сам скрипт, если кому‑то тоже понадобится:
Get-WindowsDriver -Online -All | Where-Object { $_.ProviderName -eq "libwdi" -and $_.OriginalFileName -like "*usb_device.inf" } | ForEach-Object { Write-Host "Удаление $_.Driver..."; pnputil /delete-driver $_.Driver /uninstall /force }Но у таких тестов есть еще одна нерешенная проблема: после каждого теста, отдельно (то есть такие тесты нельзя запускать на последовательное выполнение, только по одному), нужно извлекать токен и вставлять обратно, чтобы система его увидела, иначе токен не хочеть видеться.
Тест драйвера на эмуляторе
Тоже одна из самых интересных и сложных фич проета. Дело в том, что эмулятор андроида из коробки не поддерживает работу с USB‑устройствами, хотя у самого эмулятора есть флаг --usb-passthrough, который как раз для этого и предназначен.
Для того, чтобы можно было подключаться к токену прямо из эмулятора, нужно его пропатчить, добавив такое содержимое: <permissions><feature name="android.hardware.usb.host" /></permissions>, в файл: /system/etc/permissions/android.hardware.usb.host.xml.
Делается это с помощью вот примерно такого кода:
Write-Output "Launching ADB with root..."
& $adbPath root | Out-Null
Start-Sleep -Seconds 2
Write-Output "Disabling dm-verity..."
$verityResult = & $adbPath disable-verity 2>&1
Write-Output $verityResult
if ($verityResult -match "disabled" -or $verityResult -match "already") {
Write-Output "Rebooting to disable dm-verity..."
& $adbPath reboot
Start-Sleep -Seconds 5
Wait-For-Device "Verity disabled succesfully"
} else {
Write-Warning "Warning: disable-verity gave unexpected response. Probably bootloader is locked."
}Этот код отключает verity, позволяя записывать нужные нам разрешения в системные директории.

На скриншоте выше, я запустил свое тестовое приложение на эмуляторе, который я предварительно настроил для работы с токеном, и, как видно, эмулятор без проблем увидел сертификаты на токене.
Использование ИИ
Хочу это тоже выделить в отдельный пункт и рассказать, в каких местах я использовал ИИ, так как это тема очень актуальная, вся лента буквально пестрит этим, чего отставать от тренда:
Скрипты для автоматической настройки эмулятора — полностью написаны ИИ, я только свои пожелания ему кидал, сам я в PowerShell не очень соображаю.
CCID‑слой — в какой‑то степени тоже был написан ИИ, а именно: извлечение протокола (T0, T1) из ATR, обмен CCID‑пакетами, их отправка и чтение (но дочитку больших пакетов порциями — это писал я, так как там было слишком точечно), адаптация pcsc‑sharp для работы с мобилками (реализация ISCardApi для андроида).
Что не было написано ИИ
Адаптация device.net на Андроид. Дело в том, что этот проект старый (еще на Xamarin работал), свежих форков нет (еще бы, какие нишевые задачи), и мне нужно было перевести его на MAUI. За вечер я справился с этим сам, все для того, чтобы иметь возможность быстро писать тесты и отделить транспортную логику от CCID.
Собственный RSA‑провайдер для удобного использования с криптографическими библиотеками — тут нужно было всего лишь проставить соответствующие вызовы APDU‑команд, ИИ был бы избыточен.
Итого
Я покопался еще по проекту, вроде все основные и интересные моменты расписал, больше ничего в голову не приходит.
Данный проект наглядно показывает, как можно обойти вендорские ограничения даже по поддержке криптографического оборудования на разных платформах.
Работал над проектом я ~2 месяца: 1 месяц я тупил и стагнировал (мне просто хотелось извлечь все APDU‑команды из декомпилированного драйвера, я не хотел пользоваться wireshark), второй месяц я очень активно занимался этим проектом.
Пока что это мой самый интересный и сложный проект за мою карьеру:)
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.