
Уже несколько лет в Positive Labs мы исследуем механизмы безопасности в SoC RK35xx от Rockchip. За это время нам и другим исследователям безопасности удалось весьма неплохо проанализировать их. В результате было обнаружено несколько уязвимостей, две из которых были интересны нам, как людям, причастным к “железу”.
В первом случае нам пришлось разработать эмулятор eMMC памяти, чтобы эксплуатировать TOC-TOU уязвимость в момент загрузки прошивки.
Во втором — мы атаковали SoC с помощью Power Glitch, воздействуя на процесс проверки прошивки.
В обоих случаях нам удалось обойти механизм Secure Boot, а платформа RK35xx стала поистине хрестоматийным примером с точки зрения аппаратного хакинга, поэтому мы решили поделиться техническими деталями.
Предыстория
Orange Pi 5 на базе RK3588 попала в поле нашего зрения два года назад. Нам, как исследователям безопасности, в первую очередь было интересно включить защиты на данном чипе. Популярная история, что защита в чипе есть, но воспользоваться ей, как и протестировать, могут не все. В данном случае все было именно так, потому что защита в виде Secure Boot была заявлена производителем, но информации о том, как её включать, у нас не было.
Мы решили исправить эту ситуацию. Так у коллеги родилась подборка статей и выступлений на тему того, как устроен Secure & Encrypted boot у Rockchip RK35xx. Конечно, попутно мы немного смотрели и в сторону обхода этих же защит. Одним из интересных результатов исследования стало получение BootROM. И не только его. Но для начала немного напомню, как загружается RK35xx.
У чипа есть BootROM, который стартует после подачи питания, а после этого, в зависимости от конфигурации в OTP, запускается код с одного из носителей: SPI Flash, eMMC, SD-card.

Интересно, что если Secure Boot включен, то вместо штатного BootROM стартует Secure BootROM. Так вот, его тоже удалось достать! Все это подогрело комьюнити исследователей безопасности. Ведь имея Secure BootROM для RK3588, внутри которого реализована логика механизмов защиты, появилась возможность реверсить самую интересную часть загрузки… и тут понеслось)
P.S: Кстати, два BootROM есть только у RK3588, в то время как у RK3566 и RK3576 загрузчик один и Secure Boot реализован в нем.
Немного про TOC-TOU
В ноябре 2024 года “девочки на форуме” писали, что независимый исследователь безопасности обнаружил уязвимость типа TOC-TOU (Time-of-Check to Time-of-Use) в Secure BootROM от RK3588S. Также в мае 2025 другой исследователь выступал на нашем треке Positive Labs на Positive Hack Days с этой же уязвимостью. Оба выступления проходили в узких кругах и не записывались. По этой же причине мы не можем на них сослаться. Так или иначе про уязвимость было доложено в Rockchip и багу поправили в новой версии чипа RK3576.
Но на тот момент важным для нас было другое. Интересно было то, что эксплуатация уязвимости требует довольно хитрого устройства - eMMC эмулятора. Мы о нем давно говорили внутри своего коллектива, а тут появился такой повод все-таки приступить к разработке. Про разработку и технические особенности можете прочитать статью “Проект eMMC-emu” моего коллеги Димы. Первой целью для нашего эмулятора, как для инструмента хакера, стал RK3566.
Обход Secure Boot при помощи eMMC эмулятора
Когда информация об уязвимости TOC-TOU стала публичной, мы сразу проверили имеющиеся у нас BootROM для RK3566 и RK3588. Оказалось, что оба чипа содержат эту ошибку.
Чтобы понять, как она эксплуатируется, сначала разберем процесс загрузки этих SoC.

При загрузке Secure BootROM считывает данные с внешнего носителя — SPI Flash, eMMC или SD-карты. Сначала он ищет заголовок SPL по нескольким фиксированным смещениям. Для чтения используется буфер размером 0x200 байт, а наличие заголовка определяется по сигнатуре RKNS или RKSS в первых четырех байтах.

Когда заголовок найден, BootROM повторно считывает его уже в другой буфер и проверяет цифровую подпись, при этом hash заголовка зашит в OTP. Если проверка прошла успешно, то едем дальше. Но есть один нюанс!
К этому моменту заголовок уже был прочитан дважды — в разные буферы. Данные во втором буфере проверены и считаются доверенными, а в первом по-прежнему находится лишь ранее прочитанная копия. Именно данные из первого буфера используются для дальнейшей загрузки бинарных файлов. И это проблема!
__int64 __fastcall load_from_storage(load_st_ctx *ctx) { // some lines omitted for clarity err = ctx->read_page(i, &fw_header_1, &page_addr); // read header to 0xff00_0040 // some lines omitted for clarity if ( fw_header_1.magic == "RKSS" || fw_header_1.magic == "RKNS") { // some lines omitted for clarity ctx->read(page_addr, data_buff, (v5 << 0xB) + 0x800); // read header to 0xff00_1000 err = verify(data_buff); // check signature of header at 0xff00_1000 if ( !err ) { // read binary to 0xff00_1000 err = (ctx->read)( page_addr + fw_header_1.binaries[0].off, data_buff, fw_header_1.binaries[0].data_size << 9); if ( !err ) { // use header at 0xff00_0040 err = load_binary__(&fw_header_1, data_buff, fw_header_1.binaries); // some lines omitted for clarity } } } }
Казалось бы, все в порядке: оба чтения выполняются с одного и того же адреса, а значит данные должны быть одинаковыми. Но в мире hardware hacking это предположение уже не работает. Если злоумышленник способен подменять данные «на лету» во время чтения с внешнего носителя, вполне корректная с точки зрения логики программа становится уязвимой.
Да, для такой атаки нужен и физический доступ, и ряд дополнительных устройств, но все это не беда.
Автор, обнаруживший уязвимость, предложил эксплуатировать ее через подмену данных с SD-карты. Мы же решили реализовать аналогичную атаку для eMMC. Для этого сначала разберем, как BootROM загружает и запускает DDR модуль.

Из схемы видно, что в первых 0x200 байтах заголовка находятся хеши двух исполняемых файлов: DDR и SPL. Именно их и нужно подменить, чтобы загрузить собственный код и обойти Secure Boot.
Идея проста: пока SoC проверяет корректный, подписанный заголовок, мы подменяем данные первого чтения. В результате BootROM продолжает считать, что использует доверенные хеши, хотя на самом деле получает наши, заранее вычисленные для модифицированного DDR, записанного в eMMC.
Модификация eMMC эмулятора для подмены данных
Описанная уязвимость в RK35xx стала для нас толчком к разработке эмулятора eMMC — устройства, способного полностью заменить штатную микросхему памяти. Такое устройство могло бы быть полезным для:
быстрого обновления и тестирования прошивок, хранящихся в eMMC;
обеспечения полного и быстрого контроля над данными в eMMC во время работы устройства.
Врать, что мы делали eMMC эмулятор, чтобы эмулировать eMMC и только, я не стану. Мы давно хотели такое устройство в лабораторию, ведь зачастую обходу защиты мешает один пресловутый бит или байт. Нам, конечно, был интересен именно такой функционал.
Первично нужно было сделать просто эмулятор, а уже потом наградить его дополнительными возможностями. Когда мой коллега Дима закончил устройство на базе FPGA, которое позволяло загружать Orange Pi до стадии U-Boot, настало время модификаций.
Проведение TOC-TOU атаки через eMMC эмулятор предполагает следующие условия:
наличие устройства с RK3566 с включенным Secure Boot;
оригинальный образ, содержащий правильно подписанный заголовок;
наличие eMMC эмулятора, протестированного с BootROM, SPL, U-Boot и kernel на аналогичной плате.
Так как эмулятор построен на FPGA, лучше всего разбить задачу на базовые логические составляющие, так проще представлять архитектуру. Атака TOC-TOU предполагает два действия на уровне эмулятора:
выбор обращений к определенному адресу памяти, где расположен заголовок - 0х8000;
перенаправление этих обращений на один адрес во время check и на другой - во время usage.
То есть, первое чтение адреса 0x8000 должно быть перенаправлено в дополнительную область памяти с нашими hash. Второе чтение штатное. При этом предполагается нормальная работа с остальным массивом памяти.
Получаем два элементарных логических действия: компаратор адреса и счетчик обращений, встроенный в механизм чтения памяти самого эмулятора. На основе них была сделана небольшая управляемая надстройка, настраиваемая перед атакой следующими регистрами:
Название регистра |
Назначение |
Значение для RK3566 TOC-TOU |
hack_count |
Номер обращения к адресу, по которому будет производиться замена. Т.е. значение 2 обеспечит замену при каждом втором обращении. |
0x2 |
hack_dstaddr |
Адрес на который будет производиться замена |
0xFFF00000 |
hack_opcode |
Включение механизма замены |
0x1 |
hack_srcaddr |
Адрес который подлежит замене |
0x00008000 |
Такая настройка готовит эмулятор к обходу Secure Boot. При этом образ, загруженный в эмулятор, также модифицируется:
1) в оригинальном образе модифицируется код, например, модуля SPL;
2) заголовок с hash значениями для модифицированного SPL отправляется в неразмеченную часть эмулируемой памяти.
Эти два изменения - всё, что нам нужно от образа. Вот как изменения в образе и изменения в логике работы eMMC эмулятора выглядят на одной схеме:

Обход Secure Boot
Как всегда, подготовка это 90% работы, далее дело за малым. В качестве подопытного для атаки и отладки мы выбрали одноплатник Orange Pi 3B на базе SoC RK3566. Такой выбор был сделан потому, что на данной плате есть разъем для внешних eMMC модулей. Это упрощает подключение нашего устройства.

В итоге получился вот такой стенд:

Про отладку тут речь не пойдет, поэтому просто сделаем вид, что все запустилось с первого раза. Мы “белые хакеры” и поэтому запускать вредоносный код, который сожжет SoC не станем. Просто поменяем строчки, которые выводятся в UART во время исполнения SPL.

В результате работы нам удалось обойти Secure Boot на RK3566. На RK3588 эта уязвимость также присутствует. А вот в RK3576 после проверки подписи заголовка первые 0x200 байт из data_buff копируются в header_struct, тем самым устраняя уязвимость.
На этой победной ноте мы закрыли тему с эмулятором, но не с Rockchip.
Just for fun: закроем багу сами
Так как patchROM функционала в RK3588/RK3566 похоже нет, можно считать, что эта уязвимость не может быть исправлена в уже выпущенных чипах. Никогда не попадать в функцию load_from_storage - единственный вариант избежать проблемы.
Для этого можно прошить в OTP (fuses) конфигурацию, позволяющую загружаться только по USB. Для этого нужно во фьюзы, отвечающие за выбор источника загрузки, записать значение 0xA, соответствующее загрузке сервисными командами по USB.
После этого при загрузке SoC будет попадать в функцию boot_USB (сюда не положили уязвимость). Так можно загрузить SPL сервисными командами с другого устройства.
void __noreturn init() { // some lines omitted for clarity if ( !get_boot_cfg() ) { // some lines omitted for clarity boot_storage(); boot_USB(); } while ( 1 ) ; } __int64 get_boot_cfg() { // some lines omitted for clarity err = OTP_Read(&otp_val, 0x44, 1u, 1u); if ( !err ) { otp_val &= 0xFu; if ( otp_val && otp_val < 6 || otp_val == 0xA ) BOOT_CTX.boot_src_code = otp_val; return 0; } return err; } __int64 boot_storage() { // some lines omitted for clarity if ( !BOOT_CTX.boot_src_code ) { // some lines omitted for clarity } i=0 while ( boot_srcs[i]->code != BOOT_CTX.boot_src_code ) // boot_srcs[i]->code is 1..5 { if ( ++i >= 5 ) return 0xFFFFFFFF; // so with boot_src_code == 0xA we go here and avoid vulnerable function } err = (boot_srcs[i]->check)(); if ( err ) return err; return load_from_storage(i); }
Немного про Power Glitch
Про уязвимость, которую будем использовать ниже, историй нет. Только одна заметка без подробностей от ребят из BI.ZONE Hardware Lab о том, что им удалось обойти Secure Boot на Rockchip при помощи атаки по питанию. Поэтому, раз уж публичных деталей нет, давайте разбираться.
Для начала про fault injection (частным случаем которого является Power Glitch) нужно знать следующее: если система подвержена атаке, то дело за малым.
Для этого анализируют код и ищут места, где кратковременный сбой может изменить логику выполнения. Обычно таких мест немало: пропуск инструкции, ошибочный результат сравнения или нарушение последовательности исполнения могут привести к обходу проверки. Поэтому в подобных атаках речь чаще идет не об одной уязвимости, а о совокупности небольших логических особенностей, которые становятся проблемой только в мире hardware hacking.
Конечно, существуют аппаратные и программные механизмы защиты от power fault injection: мониторы питания, повторные проверки, избыточные вычисления и другие. Их реализация требует дополнительных аппаратных ресурсов, усложняет программную логику и увеличивает стоимость разработки.
Вот Rockchip не делали этого, поэтому расскажем, чем это обернулось.

Обход Secure Boot при помощи Power Glitch
Атаку по питанию мы покажем на примере Orange Pi 5 Plus на базе RK3588.
Логика работы BootROM ранее упомянутого RK3566 и RK3588 практически идентична. Но чтобы понять, как использовать fault injection для обхода Secure Boot, нам придется немного детальнее рассмотреть один момент. А именно проверку hash для SPL или DDR бинарей и их дальнейшую загрузку.
__int64 __fastcall load_from_storage(load_st_ctx *ctx) { // some lines omitted for clarity err = verify(data_buff); // check header signature if ( !err ) { boot_stage = use_RKPK ? 3 : 2; err = check_boot_flow(0, boot_stage); if ( !err ) { err = load_binary__(&fw_header_1, data_buff, fw_header_1.binaries); // CHECK DDR Training if ( !err ) { boot_stage = use_RKPK ? 4 : 3; err = check_boot_flow(0, boot_stage); if ( !err ) { // some lines omitted for clarity err = run_binary(data_buff); if ( !err ) { err = load_binary__(&fw_header_1, 0, &fw_header_1.binaries[1]); // CHECK SPL if ( !err ) { boot_stage = use_RKPK ? 5 : 4; err = check_boot_flow(0, boot_stage); if ( !err ) { // some lines omitted for clarity err = run_binary(0); // some lines omitted for clarity }
Из предыдущей главы мы помним, что заголовок содержит hash каждого бинарного файла и подписан. По сути, это и есть реализация Secure Boot: заголовок проверяется, данные из него используются для верификации кода, которому будет передано управление.
С точки зрения обычной логики все выглядит корректно. Но в мире аппаратной безопасности снова появляются нюансы. Используя power fault injection можно влиять на поток исполнения программы. Конечно, не напрямую, а косвенно. Например, сбой по питанию может привести к неправильному исполнению инструкции.
Ремарка от автора
Тут у всех разное представление из-за различного уровня абстракции.
Для программиста на С это значит, что if в такой конструкции может работать неправильно.
if (protection_check_OK) edem_dalshe(); else ne_edem_nikuda();
Для разработчика на assembler эта конструкция будет выглядеть более многостадийной:
MOV W23, W0 CBNZ W23, ERR bl edem_dalshe ERR: bl ne_edem_nikuda
Тут уже и вычитка из памяти, и инструкция сравнения может отработать неверно.
А если на это посмотрит RTL разработчик, то вариантов, где сбойнуть систему найдет еще больше.
В функцию load_binary передается указатель на hash из заголовка, а также указатель на вычитанный модуль DDR. Внутри нее происходит подсчет реального hash для данных, а затем сравнение посчитанного и переданного hash значения. В случае успеха возвращается 0, если же сравнение показало разницу в hash, то 1.
Посмотрим, что внутри:
__int64 __fastcall load_binary(header_struct *header, char *data, header_2_struct *fw_desc) { // some lines omitted for clarity size = fw_desc->data_size << 9; if ( (header->boot_flag & ENCRYPTED) != 0 ) { // some lines omitted for clarity err = decrypt_and_hash(0, iv, data, size, calced_hash); } else { err = hash(data, size, calced_hash); } if ( !err ) { err = sec_memcmp(fw_desc->hash, calced_hash, 0x20); // TRY FAULT INJECTION here or inside nex_boot_stage(boot_stage + 1, ret); } return err; }
По сути всего две функции: hash и sec_memcmp. Посчитали hash и сравнили с оригиналом. Теперь можно взглянуть на assembler:



Как минимум, нарушение работы любой из этих шести инструкций, может привести к неправильной проверке hash. И это мы рассмотрели только процесс возврата результата из sec_memcmp до момента, когда ядро решает, исполнять ли загруженный в буфер код. Как я уже говорил, для С разработчика таких мест кажется меньше, для RTL-щика больше. Важно лишь то, что их есть некоторое количество.
Неправильное выполнение одной из этих инструкций может привести к обходу Secure Boot.
Идея атаки такая: необходимо, при помощи просадки напряжения на линии питания ядра вызвать сбой в работе чипа. Сбой следует проводить после чтения DDR из SPI Flash в память SoC. Именно тогда считается его hash и сравнивается с оригиналом в заголовке. Это позволит загрузить модуль DDR даже, если hash не сойдется.
Подготовка платы Orange Pi 5 Plus для атаки
Вообще, как уже было сказано, если есть где погличить, то дело за малым. То есть все остальное - задача инженерная. И первый шаг - это подготовка оснастки. Обычно под каждый новый микроконтроллер мы изготавливаем небольшую плату - аддон с минимально необходимой обвязкой.

Это существенно упрощает настройку параметров атаки и делает её воспроизводимой для других подобных МК. А в качестве управляющего контроллера для атаки и устройства, взаимодействующего с целевым чипом, мы используем платформу Chipolino.
В данном случае SoC крупный и сложный с точки зрения изготовления платы под него. Хотелось провернуть это факультативное дело по-быстрому. Поэтому, впервые за долгое время, мы немного отступили от наших принципов и решили провести атаку прямо на устройстве, а именно на Orange Pi плате.
Оценив документацию на SoC и схемотехнику для Orange Pi, стало ясно, что атаковать скорее всего нужно по линии питания VDD_CPU_LIT_S0. LIT от слова little, то есть little core. Именно на этом маленьком ядре исполняется код BootROM, который и нужно сбоить.
Еще одним доводом в пользу того, что BootROM работает на “мелком” ядре номер 0, является первая команда в нем. MRS X0, MPIDR_EL1 - команда, читающая системный регистр MPIDR_EL1 в регистр Х0. Далее идет проверка, что исполнение происходит именно на ядре 0, если нет, то зависаем в while (true).

Кроме того, анализ схемы показал еще несколько важных вещей, а именно:
наличие конденсаторов (не то чтоб мы их не ждали) и их количество на линии питания ядра
максимальный ток, который может выдавать источник питания ядра - 5А

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


А вот тот факт, что DC/DC преобразователь может выдавать большой ток, означает, что нужно использовать мощный транзистор c драйвером для его управления. Ведь форма и длительность импульса при атаке является одним из ключевых факторов для успеха. Для простых МК и подготовленных аддонов достаточно одного маломощного транзистора. Но не в этот раз, решили мы.
В итоге, в дополнение к Chipolino изготовили внешний транзисторный модуль по такой схеме и вот что получилось:

Для работы ему нужно питание 5V и подключение к общему GND, а управляющий вход нужно подключить к Chipolino.
Важной деталью для получения максимально крутой формы импульса является минимизация сопротивления соединения между модулем и входами питания ядра. Поэтому модуль расположен с обратной стороны платы, максимально близко к выводам SoC, а для подключения к Vcore используется толстый провод.

Кстати, такое количество конденсаторов большой ёмкости в схеме не просто так. Модуль то подключает Vcore к GND через транзистор, то Vcore к этим емкостям. При переключении от GND в нормальное состояние DC/DC преобразователь пытается компенсировать низкое напряжение, что приводит к скачку напряжения. От такого скачка чип может перезагружаться. Дополнительные емкости, подключаемые при переключении, решают эту проблему.
Glitch RK35xx
Когда целевая микросхема и плата, где она установлена, готовы, нужно лишь все подключить, настроить логику атаки и ждать результата.
Что такое логика атаки?
Включить устройство и спустя некоторое время замкнуть питание ядра на GND — самый простой вариант атаки. Но на практике логика часто оказывается значительно сложнее.
Почти всегда необходимо взаимодействовать с целевым чипом по UART, SPI или SWD, дождаться определённого этапа работы и только после этого кратковременно замкнуть питание ядра на GND.
Под логикой атаки мы как раз и подразумеваем последовательность предварительных действий с чипом: его корректное включение, перевод в нужный режим, ожидание необходимого события и непосредственно выполнение атаки.
В качестве платформы для атаки мы используем Chipolino и, в данном случае, универсальный аддон для удобства подключения к Orange Pi и драйверу. Выглядит это вот так:


Кстати, данная плата стартует с SPI Flash. То есть BootROM ищет заголовок и SPL именно там. По этой причине мы выстроили логику атаки с привязкой к обмену данными на SPI интерфейсе. Если посмотреть на лог загрузки:

то можно отметить следующее:
Примерный момент передачи управления модулю DDR можно определить по сообщениям, которые он начинает посылать в UART
DDR модуль стартует через ~550 мкс после окончания его чтения из SPI Flash
17 раз дергается CS (chip select) от подачи reset и до конца чтения DDR модуля
Все это наталкивает на мысли, что можно привязаться к линии CS и считать фронты до окончания чтения модуля DDR. Это позволит максимально точно приблизиться к моменту проверки hash от DDR бинаря. Конечно, потребуется небольшой перебор места для атаки, но точно видно, что нет смысла отодвигаться во времени дальше чем на 550 мкс от конца чтения - там модуль уже запущен.
Таким образом мы и логику сформировали, и сократили временное окно перебора до 550 мкс (что по опыту очень мало). Итак план:
Запуск Orange Pi по сигналу reset (заведен на Chipolino)
Ждем 17 фронтов на линии CS (заведен на GP28 Chipolino)
Атакуем - кратковременно подтягиваем линию питания ядра к GND
Ждем сообщений в UART от модифицированного модуля DDR
Если сообщения получены, то атака успешна, код запустился. Если нет - повторяем п.1-5 меняя место атаки
The End
Итак, включаем Secure Boot, запускаем корректно подписанный образ для исследования поведения системы. И после этого патчим DDR модуль. Не важно было, что менять, и поэтому для начала был изменен один байт в строке DDR. Поменяли ее на DDA. Как только этот образ был записан на SPI Flash, SoC перестал нормально загружаться, сообщения в UART пропали, а значит система была готова к проведению Power Glitch атаки.
Придерживаясь плана выше, мы реализовали алгоритм атаки в Chipolino и даже выложили его на GitHub. По сути, результатом наших изысканий стали несколько параметров успешной атаки. Вот они:
~1 us ширина импульса
~540 us от последнего фронта на линии CS при чтении DDR модуля
В результате получили:
запуск патченного DDR модуля
строку
DDAв UART


Таким образом, с использованием глича по питанию удалось обойти Secure Boot на RK3588 и запустить собственный код. Стоит отметить, что, хотя речь в статье идёт о RK3588, эту атаку также можно воспроизвести на RK3566 и более новом RK3576.
Заключение
Короткая сводка для тех, кто путается как я:
Чип |
ROM version |
TOC-TOU |
FI |
Secondary Secure BootROM |
RK3588 |
350B 20210512 V100 |
уязвим |
уязвим |
+ |
RK3566 |
350A 20210322 V300 |
уязвим |
уязвим |
- |
RK3576 |
350E 20231110 V100 |
исправлен |
уязвим |
- |
RK35xx оказался отличной платформой для демонстрации интересных техник, которые мы используем в лаборатории, а также устройств, которые разрабатываем для подобных исследований. Нам хотелось показать, что такое аппаратные атаки, hardware hacking и исследования в области аппаратной безопасности на практике. Надеюсь у нас получилось, а вам понравилось.
Статья является результатом работы коллектива Positive Labs.
------------------------------------------------------------------------------------------------------------------------------------------
Статья носит исключительно информационный характер и не является инструкцией или призывом к совершению противоправных действий. Наша цель — рассказать о существующих уязвимостях, тактиках и техниках, которыми могут воспользоваться злоумышленники, и дать рекомендации по защите. Авторы не несут ответственности за использование кем-то информации из статьи.