Алгоритм работы эксплоита Fusée Gelée на базе уязвимости в чипах Tegra



В этом документе описывается суть уязвимости Fusée Gelée, которая используется в связке с холодной перезагрузкой и позволяет выполнять произвольный код без авторизации на ранних этапах работы bootROM через режим Tegra Recovery Mode (RCM) во встроенных процессорах Tegra от компании NVIDIA. Поскольку выполнение кода доступно в BPMP (Boot and Power Management Processor) прежде, чем будут активированы защитные функции, по сути, происходит полное компрометирование root-of-trust (корня доверия) для каждого процессора. Соответственно, становится возможной утечка конфиденциальной информации, которая может находиться на устройстве.

Краткая информация об уязвимости

Докладчик: Katherine Temkin (@ktemkin)

При поддержке: команда ReSwitched (https://reswitched.tech)

Электронная почта: k@ktemkin.com

Сфера действия: чипы Tegra; тип программного обеспечения значения не имеет.

Уязвимые версии: предположительно уязвимость присутствует во всех чипах Tegra, выпущенных до версии T186 / X2.

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

Планы по раскрытию: полное раскрытие бреши планируется 15 июня 2018 года.

Описание уязвимости

Функционал для работы с USB внутри загрузочного кода (IROM/bootROM) содержит операцию копирования, длину которой может контролировать злоумышленник. Сформировав управляющий запрос через USB определенным образом, злоумышленник может скопировать содержимое контролируемого буфера через активный стек выполнения, посредством перехвата управления BPMP (Boot and Power Management processor) прежде, чем начнут работать защитные функции и вступит в силу ограничение привилегий. В итоге злоумышленник может скопировать конфиденциальную информацию и загрузить произвольный код в прикладные процессоры главной группы на самый высокий уровень привилегий (обычно в качестве TrustZone Secure Monitor на уровне PL3/EL3).

Замечания, касающиеся публичного разглашения

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

Поскольку у других команд, по-видимому, уже есть схожий эксплоит – включая группу, которая заявляет о скорой продаже своей разработки - автор этой статьи и команда ReSwitched полагаем, что публичное раскрытие уязвимости наилучшим образом служит интересам общественности. На наш взгляд, уменьшение информационной асимметрии между рядовыми пользователями и владельцами эксплоитов приведет к тому, что каждый сможет провести оценку степени влияния данной бреши, исходя из используемых персональных стандартов безопасности.

Детальное описание бреши

Суть процесса загрузки чипов Tegra можно описать следующим псевдокодом, который был получен после извлечения и исследования прошивки IROM в уязвимой системе T210.

// If this is a warmboot (from "sleep"), restore the saved state from RAM.

if (read_scratch0_bit(1)) {

restore_warmboot_image(&load_addr);

}/

/ Otherwise, bootstrap the processor.

else

{

// Allow recovery mode to be forced by a PMC scratch bit or physical straps.

force_recovery = check_for_rcm_straps() || read_scratch0_bit(2);

// Determine whether to use USB2 or USB3 for RCM.

determine_rcm_usb_version(&usb_version);

usb_ops = set_up_usb_ops(usb_version);

usb_ops->initialize();

// If we're not forcing recovery, attempt to load an image from boot media.

if (!force_recovery)

{

// If we succeeded, don't fall back into recovery mode.

if (read_boot_configuration_and_images(&load_addr) == SUCCESS) {

goto boot_complete;

}

}

// In all other conditions

if (read_boot_images_via_usb_rcm(<snip>, &load_addr) != SUCCESS) {

/* load address is poisoned here */

}

}

boot_complete:

/* apply lock-outs, and boot the program at address load_address */

В процессорах Tegra есть режим восстановления через USB (USB Recovery Mode; RCM), который можно активировать при следующих условиях:

· Если процессор не может найти корректную таблицу Boot Control Table (BCT) и загрузчик на загрузочном носителе.

· На пины процессора подаются определенные сигналы (например, посредством нажатия комбинации клавиш).

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

Режим восстановления через USB присутствует на всех устройствах, включая те, в которых реализованы усиленные меры безопасности. Чтобы исключить неавторизованные коммуникации в режиме RCM, все команды должны быть подписаны ключом RSA или через AES-CMAC.

Реализация загрузчика протокола RCM в процессорах Tegra довольно проста и позволяет загружать небольшой кусок кода (называемый miniloader или applet) в локальную память Instruction RAM (IRAM). Обычно этот апплет называется nvtboot-recovery и представляет собой процедуру, позволяющую взаимодействовать через USB для начальной загрузки или инициализации системы.

Процесс RCM можно описать следующим псевдокодом, который также получен после исследования выгруженной прошивки IROM системы T210:

// Significantly simplified for clarity, with error checking omitted where

unimportant.

while (1) {

// Repeatedly handle USB standard events on the control endpoint EP0.

usb_ops->handle_control_requests(current_dma_buffer);

// Try to send the device ID over the main USB data pipe until we succeed.

if ( rcm_send_device_id() == USB_NOT_CONFIGURED ) {

usb_initialized = 0;

}

// Once we've made a USB connection, accept RCM commands on EP1.

else {

usb_initialized = 1;

// Read a full RCM command and any associated payload into a global buffer.

// (Error checking omitted for brevity.)

rcm_read_command_and_payload();

// Validate the received RCM command; e.g. by checking for signatures

// in RSA or AES_CMAC mode, or by trivially succeeding if we're not in

// a secure mode.

rc = rcm_validate_command();

if (rc != VALIDATION_PASS) {

return rc;

}

// Handle the received and validated command.

// For a "load miniloader" command, this sanity checks the (validated)

// miniloader image and takes steps to prevent re-use of signed data not

// intended to be used as an RCM command.

rcm_handle_command_complete(...);

}

}

Важно отметить, что полноценная RCM-команда и соответствующая полезная нагрузка передаются в глобальный буфер и по целевому адресу загрузки, соответственно, перед проверкой подписи, что позволяет злоумышленнику контролировать большой объем непроверенной памяти.

Наибольшая часть уязвимости приходится на функцию rcm_read_command_and_payload, которая принимает пакеты, связанные с RCM-командой и полезной нагрузкой, от оконечной точки, поддерживающей коммуникации через USB. В нашем случае подобной оконечной точкой будет простой канал для передачи блока бинарных данных, отделенный от стандартных USB-коммуникаций.

Функция rcm_read_command_and_payload содержит несколько проблем, одна из которых позволяет эксплуатировать данную уязвимость:

uint32_t total_rxd = 0;

uint32_t total_to_rx = 0x400;

// Loop until we've received our full command and payload.

while (total_rxd < total_to_rx) {

// Switch between two DMA buffers, so the USB is never DMA'ing into the same

// buffer that we're processing.

active_buffer = next_buffer;

next_buffer = switch_dma_buffers();

// Start a USB DMA transaction on the RCM bulk endpoint, which will hopefully

// receive data from the host in the background as we copy.

usb_ops->start_nonblocking_bulk_read(active_buffer, 0x1000);

// If we're in the first 680-bytes we're receiving, this is part of the RCM

// command, and we should read it into the command buffer.

if ( total_rxd < 680 ) {

/* copy data from the DMA buffer into the RCM command buffer until we've

read a full 680-byte RCM command */

// Once we've received the first four bytes of the RCM command,

// use that to figure out how much data should be received.

if ( total_rxd >= 4 )

{

// validate:

// -- the command won't exceed our total RAM

// (680 here, 0x30000 in upper IRAM)

// -- the command is >= 0x400 bytes

// -- the size ends in 8

if ( rcm_command_buffer[0] >= 0x302A8u

|| rcm_command_buffer[0] < 0x400u

|| (rcm_command_buffer[0] & 0xF) != 8 ) {

return ERROR_INVALID_SIZE;

} else {

left_to_rx = *((uint32_t *)rcm_command_buffer);

}

}

}

/* copy any data _past_ the command into a separate payload

buffer at 0x40010000 */

/* -code omitted for brevity - */

// Wait for the DMA transaction to complete.

// [This is, again, simplified to convey concepts.]

while(!usb_ops->bulk_read_complete()) {

// While we're blocking, it's still important that we respond to standard

// USB packets on the control endpoint, so do that here.

usb_ops->handle_control_requests(next_buffer);

}

}

Внимательный читатель может заметить проблему, которая не имеет отношения к эксплоиту Fusée Gelée: в коде нет проверки того, что DMA-буферы используются исключительно в одной операции. Соответственно, становится возможным параллельное использование DMA-буфера для обработки управляющего запроса и пересылки RCM-данных. В итоге поток выполнения RCM может прерываться, но поскольку обе операции содержат непроверенные данные, эта проблема не влияет на безопасность.

Чтобы найти уязвимость, нужно копнуть поглубже и проанализировать код, отвечающий за обработку стандартных управляющих запросов интерфейса USB. В основном этот код формирует ответы на управляющие USB-запросы.

Например, хост может запрашивать статус устройства посредством запроса GET_STATUS, на которой устройство должно отвечать коротким установочным пакетом. Особо следует упомянуть длину поля в запросе, которая должна ограничивать – но не определять – максимальный размер ответа. Согласно спецификации, устройство должно отправлять объем информации, который либо указан, либо доступен (в зависимости от того, какое из значений меньше).

В загрузчике этот функционал можно описать следующим псевдокодом:

// Temporary, automatic variables, located on the stack.

uint16_t status;

void *data_to_tx;

// The amount of data available to transmit.

uint16_t size_to_tx = 0;

// The amount of data the USB host requested.

uint16_t length_read = setup_packet.length;

/* Lots of handler cases have omitted for brevity. */

// Handle GET_STATUS requests.

if (setup_packet.request == REQUEST_GET_STATUS)

{

// If this is asking for the DEVICE's status, respond accordingly.

if(setup_packet.recipient == RECIPIENT_DEVICE) {

status = get_usb_device_status();

size_to_tx = sizeof(status);

}

// Otherwise, respond with the ENDPOINT status.

else if (setup_packet.recipient == RECIPIENT_ENDPOINT){

status = get_usb_endpoint_status(setup_packet.index);

size_to_tx = length_read; // <-- This is a critical error!

}

else {

/* ... */

}

// Send the status value, which we'll copy from the stack variable 'status'.

data_to_tx = &status;

}

// Copy the data we have into our DMA buffer for transmission.

// For a GET_STATUS request, this copies data from the stack into our DMA buffer.

memcpy(dma_buffer, data_to_tx, size_to_tx);

// If the host requested less data than we have, only send the amount requested.

// This effectively selects min(size_to_tx, length_read).

if (length_read < size_to_tx) {

size_to_tx = length_read;

}

// Transmit the response we've constructed back to the host.

respond_to_control_request(dma_buffer, length_to_send);

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

· Когда посылается запрос GET_CONFIGURATION и в качестве получателя указывается DEVICE.

· Когда посылается запрос GET_INTERFACE и в качестве получателя указывается INTERFACE.

· Когда посылается запрос GET_STATUS и в качестве получателя указывается ENDPOINT.

Вышеперечисленные условия являются критической ошибкой безопасности, поскольку хост может запросить до 65535 байт в одном управляющем запросе. В случаях, когда это значение помещается в переменную size_to_tx и далее поступает в качестве аргумента функции memcpy, 65535 байт копируются в текущий буфер dma_buffer. Поскольку DMA-буферы, используемые в протоколе USB, относительно небольшие, мы получаем переполнение.

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

На самом деле, на запрос GET_STATUS устройство должно сгенерировать двухбайтовый ответ – однако по факту в микросхеме Tegra отсылается ответ намного большего размера, что свидетельствует о присутствии уязвимости.

Чтобы понять последствия этой бреши, рассмотрим структуру памяти, используемую в bootROM. В нашем случае рассматривается bootROM в версии T210.


Основные области памяти, имеющие отношение к нашей уязвимости:

Стек выполнения разрастается, начиная с адреса 0x40010000. Таким образом, стек находится в памяти, которая находится сразу же за этим адресом.

DMA-буферы, используемые интерфейсом USB, находятся по адресам 0x40005000 и 0x40009000 соответственно. Поскольку в USB-стеке происходит чередование между этими двумя буферами во время передачи, хост может управлять выбором того или иного буфера во время отправки запроса.

Как только до RCM-кода в загрузчике доходит 680-байтовая команда, полученные данные размещаются в верхней секции IRAM по адресу 0x40010000. Максимально доступный объем полезной нагрузки - 0x30000 байт. Этот адрес примечателен тем, что находится сразу же за стеком выполнения.

Следует обратить внимание на смежность стека и полезной нагрузки, режима RCM, которая контролируется злоумышленником. Рассмотрим, что происходит при приеме слишком большого запроса GET_STATUS, где в качестве получателя указано ENDPOINT. Функция memcpy:

копирует 65535 байт;

берет данные, начиная с области, где находится переменная состояния в стеке, и далее затрагивает большую часть памяти после стека. В итоге копируется большая часть RCM-буфера с полезной нагрузкой, управляемого злоумышленником.

размещает целевой буфер начиная с адреса 0x40005000 или 0x40009000 (на выбор злоумышленника) до адреса 0x40014fff или 0x40018fff.

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

Данный эксплоит был бы эффективен для любой платформы. Однако в случае с bootROM эта атака особенно результативна, поскольку в bootROM:

· Не используются распространенные методы защиты, например, «осведомители» стека (stack canary) якобы потому, чтобы не усложнять функционал и из-за ограниченной памяти в IRAM и IROM.

· Не используется защита памяти. В итоге, стек можно читать, в стек можно писать и в стеке можно выполнять все, что угодно.

· Не используются защиты наподобие ASLR.

В итоге мы имеем:

1. Возможность загрузить произвольную полезную нагрузку в память в режиме RCM, поскольку в этом режиме проверяются только сигнатуры команд после того, как полезная нагрузка принята.

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

Вышеперечисленные возможности позволяют нам выполнять произвольный код во время загрузки устройства на базе процессора Tegra. Поскольку перехват управления происходит перед возвратом из функции read_boot_images_via_usb_rcm, ни одна защитная функция не начинает действовать. Соответственно, из полезной нагрузки можно получить доступ к фьюзам (настроечным битам) устройства T210 и конфиденциальной информации, которая там хранится, поскольку bootROM на этот момент еще не защищен.

Выполнение эксплоита

Fusée Launcher PoC эксплуатирует уязвимость системы T210 через последовательность следующих итераций:

1. Устройство загружается в режиме RCM. Способ активации этого режима зависит от устройства, но обычно нужно нажать комбинацию клавиш во время загрузки.

2. Со стороны хоста не возникает проблем с доступом к устройствам, находящимся в режиме RCM.

3. Хост считывает с пина EP1 IN 16 байт, в которых хранится ID устройства.

4. Хост собирает полезную нагрузку для эксплоита, которая содержит:

a. RCM-команду с указанием максимальной длины, чтобы мы смогли отправить полезную нагрузку максимально возможного размера без завершения получения полезной нагрузки в режиме RCM. До проверки используется только длина команды. Таким образом, мы можем отправить RCM-команду максимальной длины 0x30298 и оставшиеся 676 байт RCM-команды с произвольными значениями.

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

c. Выполняемую программу («конечную полезную нагрузку»), позиция которой в бинарном файле должна совпадать с точкой входа из предыдущего шага.

d. Полезную нагрузку, которая должна быть выровнена кратной размеру 0x1000, чтобы активный блок не перезаписался из-за ошибки, связанной с одновременным использованием DMA-буфера.

5. Полезная нагрузка эксплоита отправляется на устройство через пин EP1 OUT. Параллельно отслеживается количество 0x1000-байтовых «блоков», отосланных устройству. Если это число четное, следующая запись происходит в нижний DMA-буфер (по адресу 0x40005000), если нечетное – в верхний DMA-буфер (по адресу 0x40009000).

6. Если следующая акт записи нацелен на нижний DMA-буфер, дописываем блок из 0x1000 байт так, чтобы запись произошла в верхний DMA-буфер. Таким образом, мы снижаем общий объем данных для копирования.

7. Активируем уязвимый вызов функции memcpy посредством отсылки управляющего запроса GET_STATUS IN, где в качестве получателя указано ENDPOINT. Длина должна быть достаточной, чтобы перезаписать нужную область стека, но не больше, чем требуется.

Простейшее программа, запускаемая на хосте, которая активирует уязвимость приложена к отчету (см. файл fusee-launcher.py). Обратите внимание на ограничения функции, о которых рассказано в следующем разделе.

Программная реализация концепции

Вместе с этим отчетом идет 3 файла:

fusee-launcher.py – основной python-скрипт, который запускает простейшую бинарную полезную нагрузку в контексте bootROM через эксплоит.

intermezzo.bin – небольшая подпрограмма, предназначенная для перемещения полезной нагрузки от старшего адреса загрузки к стандартному адресу загрузки 0x40010000, используемому в режиме RCM, что позволяет запускать стандартные полезные нагрузки (например, nvtboot-recover.bin).

fusee.bin – пример полезной нагрузки для Nintendo Switch, популярного и хорошо защищенного устройство на базе серии T210. Эта полезная нагрузка выводит на экран информацию с фьюзов устройства и защищенной памяти IROM. Тем самым демонстрируется раннее выполнение в контексте bootROM.

Дополнение: многие стеки драйверов операционных систем, используемых на хосте, со скрипом позволяют формировать очень большие управляющие запросы. Опубликованный код работает в следующих средах:

В 64-битном Линуксе эксплоит работает через драйвер xhci_hcd. Экспериментальное приложение может вручную отправлять большие управляющие запросы, но не работает через драйвер ehci_hcd из-за ограничений. По общепризнанной традиции соединение через порт USB3 SuperSpeed (разъем синего цвета) практически всегда будет обрабатываться драйвером xhci_hcd.

В macOS эксплоит работает без каких-либо ограничений.

В случае с Windows требуется дополнительный модуль ядра и, соответственно, стандартной версией не обойтись.

Чтобы воспользоваться экспериментальной версией для Nintendo Switch, необходимо:

Установить среду с Linux или macOS, удовлетворяющую критериям выше. Должен быть установлен python3 и pyusb.

Подключить свитч к хосту через кабель USB A -> USB C.

Загрузить свитч в режиме RCM одним из трех способов. Первый способ, связанный с отключением платы eMMC, наиболее прост.

a. Отключить загрузку свитча через плату eMMC. Наиболее простой способ – открыть заднюю крышку и удалить плату eMMC. Альтернативный вариант: испортить таблицу Boot Configuration Table или загрузчик в загрузочном разделе платы eMMC.
b. Активировать пины, связанные с режимом RCM. Нажать кнопку VOL_DOWN, закоротить пин 10 на правом коннекторе JoyCon на землю и включить питание.
c. Обнулить бит 2 рабочего регистра PMC. В современных прошивках потребуется уровень исключений EL3 или активация функции pre-sleep в BPMP.
4. Запустить скрипт fusee-launcher.py с аргументом fusee.bin (необходимо, чтобы файл intermezzo.bin находится в одной папке со скриптом fusee-launcher.py).
sudo python3 ./fusee-launcher.py fusee.bin

Рекомендации по защите

В нашем случае, главная рекомендация – модификация обработчика управляющих запросов через интерфейс USB и ограничение размера ответа. В зависимости от типа устройства, шаги по защите могут быть следующими:

Для устройств, находящихся у пользователей, пока решение не придумано. К сожалению, доступ к фьюзам, отвечающим за конфигурирование ipatch-обновлений, был заблокирован. Фьюз ODM_PRODUCTION был удален, и обновление bootROM невозможно. Рекомендуется оповестить пользователей о проблеме и, по возможности, перейти на использование альтернативных девайсов.

Для новых устройств корректным является решение, связанное с выпуском новых ipatch-обновлений для ограничения размера ответа на управляющие запросы.

По-видимому, производители оборудования линейки T210 могут переключиться на использование линейки T214. Автор статьи надеется, что bootROM в серии T214 защищен от данной уязвимости так же, как и серия T186. В противном случае нужно выполнить модификацию MROM (Mask ROM) и / или ipatch-обновлений для линейки T214.


Комментариев нет:

Отправить комментарий

Wargroove — милые собачки, скучная война. Рецензия Игра протестирована на  PC Пару лет назад Wargroove стала одним из самых зап...