Исследование ANZEN: как мы взломали Keystone через уязвимость в USB

Ключевые выводы
• Отсутствие проверки контролируемой хостом длины в USB SDK приводит к чтению и записи за пределами фиксированного буфера
• Атакующий может перехватить таблицу обратных вызовов USB class, превратив запись за пределы буфера в произвольное исполнение кода на MCU
• Ошибочная конфигурация MPU позволяет напрямую выполнять shellcode в SRAM
• Получение Passcode, OTP и криптографических материалов из чипа безопасности позволяет восстановить AES Key и извлечь мнемоническую фразу BIP39
• Сторонние SDK также являются критической границей безопасности аппаратного кошелька и требуют обязательного полного аудита
Обновление безопасности (июль 2026 года): Проблема, рассматриваемая в этой статье, была обнаружена OneKey Anzen в марте 2026 года в ходе совместного аудита безопасности с Keystone и затрагивает прошивку Keystone 3 Pro с интегрированным USB SDK от поставщика MCU. Keystone выпустила прошивку v2.4.0 1 апреля 2026 года, полностью устранив данную проблему; на момент публикации совместного объявления обе стороны не обнаружили доказательств того, что данная уязвимость использовалась для атак на пользователей Keystone. Для успешной эксплуатации злоумышленнику требуется одновременно физически владеть устройством, знать правильный PIN, разблокировать устройство, получить подтверждение пользователя на подключение по USB и подключить устройство к контролируемому атакующим компьютеру. Пользователям прошивок ниже v2.4.0 следует немедленно обновиться до последней версии; использование изолированного подписания через QR позволяет избежать этого конкретного вектора атак через USB, однако обновление все равно рекомендуется выполнить. Подробнее см. в Совместное обновление безопасности Keystone и OneKey Anzen.
В начале августа в Лас-Вегасе прошла конференция Black Hat USA 2026. Из-за визовых сложностей наш доклад был переведен в формат on-demand. Темой доклада стал наш аудит кошелька Keystone, в ходе которого была обнаружена критическая уязвимость в USB SDK от поставщика чипа, позволившая в итоге извлечь мнемоническую фразу.
На самом деле проблемы в стеках протокола USB возникают далеко не впервые. За последние десять с лишним лет бесчисленное количество серьезных атак на устройства было связано именно со стеком USB: от уязвимостей в драйверах USB и удаленного перенаправления в 2014 году до Fusée Gelée, checkm8, Kamakiri и раскрытого в 2026 году usbliter8.
Хроника уязвимостей протокола USB
2014: Действительно ли для атак на USB необходим физический доступ?
В 2014 году NCC Group представила на Black Hat Asia доклад «USB Attacks Need Physical Access Right? Not Any More...».
В то время уязвимости USB обычно относили к категории «локальных физических атак»: злоумышленник должен был физически подключить вредоносное устройство к целевой машине, чтобы вызвать сбой в драйвере с помощью некорректного дескриптора или class-specific data.
Однако в этом докладе исследователи использовали RemoteFX USB Redirection для перенаправления низкоуровневых устройств USB на удаленный Windows Server. Вредоносное USB-устройство было физически подключено к клиенту атакующего, но драйвер, обрабатывающий вредоносные данные USB, выполнялся на удаленном сервере.
2018, Fusée Gelée: подмена длины объекта длиной запроса
Раскрытая в 2018 году уязвимость Fusée Gelée стала одним из самых классических примеров атак на USB BootROM. Уязвимость находилась в Recovery Mode (RCM) чипов NVIDIA Tegra. Устройство инициализировало стек USB на самом раннем этапе в среде BootROM, ожидая загрузки образа восстановления от хоста. Ошибка возникала в, казалось бы, стандартном управляющем запросе GET_STATUS.
Согласно семантике USB, объект status занимает всего 2 байта. Хост указывает в wLength, сколько байт он готов принять максимум, а устройство должно вернуть min(длина запроса хоста, фактическая длина доступных данных).
Однако при обработке получателя endpoint BootROM ошибочно выполнял логику следующего вида:
status = get_usb_endpoint_status(index);
/* 错误:使用主机给出的请求长度 */
size_to_tx = setup_packet.length;
memcpy(dma_buffer, &status, size_to_tx);
То есть фактическая длина 2-байтового объекта игнорировалась, а контролируемый атакующим wLength напрямую передавался в качестве размера в memcpy, достигая теоретически 65,535 байт.
Более того, до проверки подписи RCM уже позволял загрузить в память объемный payload; при этом область payload находилась в непосредственной близости от активного стека BootROM. За счет выбора DMA buffer и формирования структуры памяти копирование за пределы буфера позволяло перезаписать исполняемый стек контролируемыми атакующим данными.
Среда BootROM в то время не имела привычных механизмов stack canary, ASLR и эффективной защиты исполнения памяти. В итоге злоумышленник получал произвольное исполнение кода в контексте BootROM еще до срабатывания каких-либо блокировок безопасности и снижения привилегий.
Этот сценарий имеет прямое сходство с описываемой далее уязвимостью в аппаратном кошельке.
2019, checkm8: контроль не только над данными, но и над «незавершенностью»
Раскрытый в 2019 году checkm8 стал одной из самых масштабных уязвимостей в Apple SecureROM. Эксплойт затронул несколько поколений устройств iOS, позволив злоумышленникам выполнять код в контексте BootROM в режиме DFU, дампить SecureROM, расшифровывать keybag прошивки или переводить устройство в состояние demotion с поддержкой JTAG. Так как уязвимость находилась в аппаратно зашитом BootROM, ее невозможно было устранить обновлениями iOS на уже выпущенных устройствах.
В отличие от Fusée Gelée, checkm8 не был простой ошибкой проверки длины. Его суть заключалась в use-after-free в конечном автомате USB режима Apple DFU: фаза данных USB не завершалась штатно, глобальные переменные состояния фазы данных не очищались, при этом базовый DFU buffer освобождался при выходе из стека USB.
В штатном режиме при получении запроса DFU_DNLOAD с фазой DATA BootROM сохраняет приемный buffer и wLength. По завершении приема данных callback-функция очищает эти состояния, а при выходе из DFU освобождается соответствующий I/O buffer.
DFU_DNLOAD(setup)
{
ep0_buffer = dfu_io_buffer;
ep0_remaining = setup.wLength;
}
DATA_complete()
{
ep0_buffer = NULL;
ep0_remaining = 0;
}
DFU_exit()
{
free(dfu_io_buffer);
}
Эксплойт checkm8 использует зазор между этими шагами: атакующий отправляет запрос с фазой DATA, вынуждая BootROM сохранить указатель на buffer и оставшуюся длину, после чего намеренно не досылает данные до конца. Путем отмены передачи или создания таймаута callback завершения DATA не вызывается; затем инициируется смена состояния DFU или USB reset, что приводит к выходу из стека USB и освобождению исходного буфера.
В этот момент память уже освобождена, но в состоянии приема сохраняется старый указатель:
free(dfu_io_buffer);
ep0_buffer = old_dfu_io_buffer; // 悬空指针
Затем требуется организация heap. Публичный эксплойт выполняет утечку ряда объектов USB request, изменяя порядок последующих аллокаций так, чтобы новый DFU buffer разместился в другом месте, а старый адрес заняли объекты типа usb_device_io_request.
При следующей отправке данных USB BootROM продолжает запись по висячему ep0_buffer, перезаписывая уже новый объект. Эксплойт модифицирует поля вроде callback и next, направляя адрес обратного вызова на заранее подготовленный payload. При завершении следующего USB-запроса SecureROM передает управление коду атакующего.
2020, Kamakiri: как запрос USB превращается в косвенный переход
Уязвимость Kamakiri была направлена на режим загрузки BootROM в ряде SoC MediaTek. Она позволяла обойти аутентификацию, ограничивавшую неавторизованный Download Agent, и запустить неподписанный payload первой стадии в контексте BootROM. Получив эту возможность, последующие payload могли взаимодействовать с флеш-памятью, дампить память, изменять статус загрузки или восстанавливать окирпиченные устройства.
Цепочка эксплуатации Kamakiri очень коротка, поскольку BootROM MediaTek изначально предоставлял первую необходимую возможность: загрузку данных в известную область SRAM по протоколу загрузки. Атакующий помещает туда stage 1 payload, а затем триггерит уязвимость специальным USB control request.
Уязвимость находилась непосредственно в диспетчере USB control request. Независимый реверс-инжиниринг описывает эту логику следующим образом:
handler = handler_array[value * 13];
handler();
Ключевая проблема заключалась в том, что значение выбора из USB-запроса участвовало в индексации таблицы указателей функций без должной проверки диапазона. Атакующему было достаточно подобрать управляющее значение так, чтобы результат выборки из таблицы указывал на адрес ранее загруженного в SRAM payload, превращая USB-запрос в косвенный вызов payload. Расположение памяти BootROM в разных SoC различается, поэтому конкретные значения индекса и адреса payload могли требовать адаптации или перебора.
В публичном PoC после загрузки stage 1 отправлялась следующая управляющая передача:
ctrl_transfer(
0xA1, # class request, device-to-host, interface recipient
0,
0,
10, # wIndex
0
)
2026, usbliter8: контроллер USB смещает указатель записи DMA перед буфером
Раскрытый в июне 2026 года эксплойт usbliter8 вновь распространил неустранимые программно уязвимости BootROM на SoC Apple A12, A13, а также S4 и S5.
В случае успешной эксплуатации атакующий может выполнять код в контексте EL1 SecureROM, изменять поведение DFU, временно выполнять demote режима production mode чипа или обходить проверку подписи для запуска оригинального неподписанного iBoot. Это означает компрометацию цепочки доверия загрузки процессора приложений без возможности исправления через обновления iOS.
usbliter8 отличается от предыдущих примеров тем, что использует семантику указателя DMA самого USB-контроллера Synopsys DWC2 при обработке SETUP transaction.
По спецификации USB блок данных в SETUP transaction фиксирован и составляет 8 байт. Контроллер DWC2 допускает прием до трех пакетов SETUP подряд, записывая их последовательно в память через DMA. Три нормальных пакета SETUP занимают суммарно 24 bytes.
При поступлении четвертого пакета SETUP transaction контроллер фиксированно вычитает 24 байта из указателя записи DOEPDMA, возвращая адрес к началу этого пула SETUP buffer. Для штатных 8-байтовых пакетов SETUP эта логика работает корректно.
Однако контроллер также принимает аномальные пакеты SETUP короче 8 байт, продвигая указатель DMA на фактически записанную длину; при этом внутренняя гранулярность записи составляет 4 байта. Когда атакующий многократно отправляет короткие 4-байтовые пакеты, первые три пакета смещают адрес вперед всего на 12 байт, но при поступлении четвертого пакета контроллер по-прежнему вычитает фиксированные 24 байта. В результате следующая порция данных SETUP записывается не в исходный buffer, а на 12 байт левее (ниже) его начала. При циклическом повторении процесса позиция записи DMA с предсказуемым шагом смещается в сторону младших адресов, формируя стабильный buffer underflow.
Как правило, OOB DMA от контроллера USB не обязательно ведет к повреждению произвольной системной памяти. В современных SoC доступный диапазон адресов для USB-контроллера обычно ограничивается через IOMMU. Однако в среде SecureROM чипов A12 и A13 компонент USB DART находится в состоянии bypass, из-за чего у DMA контроллера отсутствует эффективная изоляция адресов. Это позволяет underflow выйти за рамки компактного SETUP buffer и перезаписывать другие объекты в SRAM.
На A12 буфер USB DMA соседствует со стеком USB task, что позволяет перезаписать сохраненный link register и перехватить управление счетчиком команд при переключении контекста задач. В A13 был добавлен механизм Pointer Authentication, поэтому прямая перезапись адреса возврата не работает; публичный эксплойт задействует более сложную цепочку повреждения памяти, перезаписывая указатель USB interrupt handler в BSS, что приводит к переходу на код атакующего при следующем прерывании USB. Оба пути в итоге обеспечивают исполнение в SecureROM EL1.
От OOB-чтения/записи по USB к произвольному исполнению кода
Первопричина уязвимости, представленной на Black Hat USA, также крылась в USB SDK.
Запросы line-coding в CDC требуют компактной структуры фиксированного размера, однако интегрированный USB SDK выделил под нее фиксированный 8-байтовый буфер CmdBuff, после чего напрямую передавал предоставленный хостом req->wLength в низкоуровневые функции отправки или приема. В коде отсутствовала проверка длины и требование соответствия фиксированному размеру протокола. В результате возникла возможность чтения и записи за пределами буфера.
USB SDK 越界读写漏洞代码
Разумеется, наличие OOB-чтения и записи — это лишь первый шаг и получение примитива повреждения памяти; конечной целью было достижение произвольного исполнения кода.
Так как уязвимость возникла в тракте USB CDC, я начал анализировать стек протокола USB по цепочке диспетчеризации текущего запроса. При изучении g_usbDev внимание привлекло поле dev.class_cb. Это был не обычный указатель на данные, а таблица обратных вызовов времени выполнения для USB class driver; при обработке запросов USB core вызывает через нее функции вроде Setup, DataIn и DataOut. Возможность перезаписи class_cb позволяла перенаправить последующий штатный USB-запрос через контролируемый указатель функции, что делало его идеальной целью для захвата управления.
Всю цепочку эксплуатации можно разделить на два USB-запроса.
Первый запрос использует SET_LINE_CODING для запуска записи за пределы буфера. В ходе этой записи требовалось одновременно решить три задачи: разместить shellcode в CmdBuff, сформировать поддельную таблицу обратных вызовов в соседней области SRAM и переписать class_cb, указав адрес этой поддельной таблицы.
Затем отправляется стандартный запрос GET_LINE_CODING. USB core обрабатывает его штатно и вызывает callback Setup драйвера класса через class_cb->Setup(...). Разница в том, что class_cb указывает уже не на оригинальную таблицу, а на нашу поддельную; а поддельный Setup ведет на shellcode в CmdBuff. В итоге второй USB-запрос передает PC напрямую на наш код, записанный в SRAM.
Поскольку shellcode находится в SRAM, обычно требуется преодолеть запрет на исполнение в области данных. Однако при настройке MPU в прошивке был использован некорректный формат size, из-за чего результирующий размер защищаемой области оказался равен 0. Регион MPU, предназначенный для ограничения прав на исполнение в BSS, фактически не работал. Таким образом, код внутри CmdBuff мог выполняться напрямую.
劫持 USB 回调表执行 shellcode
Убедившись в теоретической возможности атаки, я перешел к практике и протестировал реальные границы OOB-чтения и записи. Сформировав управляющую передачу размером 64 байта, с помощью OOB-чтения удалось непрерывно выгрузить около 24 KB SRAM, однако при OOB-записи на смещении 4,352 байта устройство стабильно падало в crash.
Дальнейший анализ показал, что на этом смещении перезаписывался активный указатель по адресу CmdBuff + 0x1130, в результате чего EP0 терял способность принимать последующие данные.
越界写导致设备崩溃的位置
Решение оказалось достаточно простым: благодаря наличию примитива OOB-чтения мы можем вычитать реальное состояние памяти, а затем внедрить сдампленные данные состояния в shellcode. Это позволило выстроить законченную логику произвольного исполнения кода.
Теперь, когда перезапись class_cb была отлажена, важно было не нарушить работу самого канала USB. Длинная запись сначала помещает shellcode и поддельную таблицу в SRAM и временно перенаправляет class_cb на эту таблицу; последующий запрос GET_LINE_CODING заставляет USB core перейти в shellcode через поддельный Setup. После выполнения полезной нагрузки shellcode записывает результат в область позади CmdBuff, восстанавливает исходный class_cb и вызывает оригинальный Setup, корректно завершая текущую передачу по EP0. В результате каждый перехват становится лишь кратковременным detour: USB сохраняет работоспособность и готов к внедрению следующей задачи.
Прошивка изначально пыталась настроить MPU так, чтобы запретить исполнение в BSS, однако в коде конфигурации использовался ошибочный формат size, из-за чего вычисленный диапазон защиты оказался равен нулю.
От произвольного исполнения на MCU к извлечению мнемонической фразы
Достижение произвольного исполнения на MCU еще не означает автоматического извлечения мнемонической фразы. Архитектура аппаратного кошелька устроена следующим образом: зашифрованные данные мнемоники хранятся в SE DS28S60, а в формировании AES KEY для их расшифровки участвуют OTP и два чипа SE: ATECC608 и DS28S60.
硬件钱包安全架构
Процесс генерации этого AES KEY представлен на схеме ниже:
AES Key 生成流程
Учетные данные, участвующие во взаимодействии с SE и генерации AES, формируются на основе passcode кошелька (пароля разблокировки) и OTP, как показано на схеме:
Passcode 与 OTP 凭证生成流程
При этом Passcode можно восстановить методом перебора после получения password hash, поэтому для завершения генерации AES key остается получить только salt для OTP. Что касается извлечения OTP, shellcode выполняется на том же уровне привилегий, что и штатная прошивка, поэтому через эксплойт USB можно записать значения в регистры MPU, отключить MPU и затем прочитать OTP.
Итоговая цепочка эксплуатации представлена ниже: после получения AES KEY расшифровывается энтропия BIP39 и восстанавливается мнемоническая фраза.
从 USB 漏洞到助记词提取的完整攻击链
Заключение
В ходе этого исследования технологии AI активно привлекались к анализу уязвимостей и разработке exploit. Появление AI значительно повысило продуктивность исследователей безопасности и снизило затраты ресурсов на изучение подобных уязвимостей. Если раньше путь от обнаружения бага до стабильного эксплойта требовал огромного количества ручного труда, то сегодня понимание кода, сопоставление структур данных, генерация скриптов и трассировка причин сбоев ускоряются в разы.
При этом как USB SDK, так и другие сторонние компоненты нередко упускаются из виду при аудитах безопасности продуктов, и зачастую именно это слабое звено приводит к полной компрометации всей системы защиты.






