Мы провели исследование микроконтроллера Espressif ESP32 на предмет устойчивости к атакам, выполняемым методом внесения сбоев в работу чипов (Fault Injection). Мы постепенно шли к тому, чтобы найти уязвимости, которые позволят нам обойти механизмы защищённой загрузки (Secure Boot) и шифрования флеш-памяти (Flash Encryption) посредством всего одного сбоя, вызванного электромагнитным полем. Более того, мы, успешно проведя атаку, не только смогли выполнить произвольный код, но и получили данные флеш-памяти в расшифрованном виде.
Компания Espressif зарегистрировала эту уязвимость в базе данных CVE под кодом CVE-2020-13629. Знакомясь с атакой, описанной в этой статье, учитывайте то, что она применима к чипам ESP32 ревизий 0 и 1. Более новые ESP32 V3 поддерживают функционал отключения загрузчика UART, который использован в этой атаке.
Загрузчик UART
В ESP32 загрузчик UART реализован в коде ROM. Это позволяет, кроме прочего, записывать программы во внешнюю флеш-память. Реализация загрузчика UART в виде кода, хранящегося в ROM, — это распространённое решение. Оно достаточно надёжно из-за того, что подобный код нелегко повредить. Если бы данный функционал был бы основан на коде, хранящемся во внешней флеш-памяти, то любое повреждение такой памяти привело бы к полной неработоспособности микроконтроллера.
Обычно доступ к подобному функционалу организуется при загрузке чипа в специальном режиме, в режиме загрузки (boot mode). Выбор этого режима осуществляется с помощью контактных перемычек (или перемычки), устанавливаемых перед перезагрузкой устройства. В ESP32 именно для этого используется вывод G0
.
Загрузчик UART поддерживает множество интересных команд, которые можно использовать для чтения/записи памяти и регистров и даже для выполнения программ из SRAM.
▍Выполнение произвольного кода
Загрузчик UART поддерживает загрузку и выполнение произвольного кода с использованием команды load_ram
. В SDK ESP32 входят все инструменты, необходимые для компиляции кода, который может быть выполнен из SRAM. Например, в следующем фрагменте кода выполняется вывод строки SRAM CODEn
в последовательный интерфейс.
void __attribute__((noreturn)) call_start_cpu0()
{
ets_printf("SRAM CODEn");
while (1);
}
Инструмент esptool.py
, являющийся частью SDK ESP32, можно использовать для загрузки скомпилированных бинарных файлов в SRAM. Потом эти файлы можно запускать.
esptool.py --chip esp32 --no-stub --port COM3 load_ram code.bin
Интересно то, что загрузчик UART нельзя отключить. Поэтому доступ к нему есть всегда, даже в том случае, если включены защищённая загрузка и шифрование флеш-памяти.
▍Дополнительные меры безопасности
Очевидно, если не предпринять дополнительных мер безопасности, то постоянная доступность загрузчика UART сделает практически бесполезными механизмы защищённой загрузки и шифрования флеш-памяти. Поэтому компания Espressif реализовала дополнительные механизмы безопасности, которые основаны на технологии eFuse.
Речь идёт о битах, используемых для настройки параметров безопасности, хранящихся в особой памяти, которую часто называют OTP-памятью (One-Time-Programmable Memory, однократно программируемая память). Биты в такой памяти могут меняться лишь с 0 на 1, но не в обратном направлении. Это гарантирует то, что если бит, включающий какую-то возможность, был установлен, он уже никогда не будет сброшен. Во время работы ESP32 в режиме функционирования загрузчика UART для отключения определённых возможностей используются следующие биты OTP-памяти:
DISABLE_DL_ENCRYPT
: отключение шифрования флеш-памяти.DISABLE_DL_DECRYPT
: отключение прозрачного дешифрования флеш-памяти.DISABLE_DL_CACHE
: отключение всего MMU-кеша флеш-памяти.
Нас больше всего интересует бит OTP-памяти DISABLE_DL_DECRYPT
, так как он отключает прозрачное дешифрование данных, хранящихся во флеш-памяти.
Если этот бит не установлен, то, при загрузке микроконтроллера с помощью загрузчика UART, можно организовать простой доступ к данным, хранящимся во флеш-памяти, работая с ними как с обычным текстом.
Если этот бит установлен, то, в режиме загрузки с помощью загрузчика UART, из памяти можно прочитать лишь зашифрованные данные. Функционал шифрования флеш-памяти, полностью реализованный на аппаратном уровне и прозрачный для процессора, включается лишь тогда, когда ESP32 загружается в обычном (Normal) режиме.
При выполнении атаки, о которой мы тут говорим, все эти биты установлены в 1.
Данные в SRAM, сохраняющиеся после горячей перезагрузки устройства
SRAM-память, используемая микроконтроллером ESP32, вполне обычна. Такая же используется многими чипами. Обычно она применяется совместно с ROM и отвечает за запуск первого загрузчика из флеш-памяти. Такую память удобно применять на ранних стадиях загрузки, так как перед её использованием не нужно ничего настраивать.
Опыт предыдущих исследований говорит нам о том, что данные, хранящиеся в SRAM, не изменяются до тех пор, пока их не перезапишут, или до тех пор, пока к ячейкам памяти не перестанет подводиться электричество. После холодной перезагрузки (то есть — цикла включения/выключения питания) чипа содержимое SRAM будет сброшено к состоянию, которое она имеет по умолчанию. Каждый чип такой памяти при этом отличается уникальным (можно сказать, полуслучайным) состоянием битов, устанавливаемых в значения 0 и 1.
Но после горячей перезагрузки, когда чип перезагружают, не отключая питание, может случиться так, что данные, хранящиеся в SRAM, останутся такими же, как были. Это показано на следующем рисунке.
Воздействие холодной (выше) и горячей (ниже) перезагрузки на содержимое SRAM
Мы решили узнать о том, справедливо ли вышесказанное для ESP32. Мы выяснили, что для выполнения программной горячей перезагрузки можно воспользоваться аппаратным сторожевым таймером. Можно заставить сработать этот таймер и в том случае, когда чип находится в режиме загрузки с использованием загрузчика UART. В результате можно воспользоваться этим механизмом для перевода ESP32 в обычный режим загрузки.
Воспользовавшись тестовым кодом, загрузка которого в SRAM и выполнение которого были организованы с использованием загрузчика UART, мы определили, что данные в SRAM, и правда, сохраняются после горячей перезагрузки, инициированной сторожевым таймером. А это означает, что мы, записав в SRAM то, что нам нужно, можем загрузить ESP32 в обычном режиме.
Тут перед нами возник вопрос о том, как нам этим воспользоваться.
Дорога к сбою
Мы предположили, что, возможно, нам удастся воспользоваться для атаки фактом сохранения данных в SRAM после горячей перезагрузки. Первая наша атака заключалась в том, что мы записали в SRAM, используя загрузчик UART, некий код, а потом, пользуясь сторожевым таймером, провели горячую перезагрузку устройства. Затем мы устроили сбой, проведя его в тот момент, когда код из ROM перезаписывает этот код кодом загрузчика из флеш-памяти в процессе обычной загрузки.
Эта идея у нас появилась после того, как в процессе ранее проведённых экспериментов мы превратили процесс передачи данных в процесс выполнения кода. Тогда мы заметили, что чип начинает выполнять код со стартового адреса до того, как загрузчик завершит копирование.
Иногда, чтобы чего-то добиться, нужно лишь это попробовать…
▍Код, загружаемый в SRAM и используемый для проведения атаки
Вот код, который мы, используя загрузчик UART, записали в SRAM.
#define a "addi a6, a6, 1;"
#define t a a a a a a a a a a
#define h t t t t t t t t t t
#define d h h h h h h h h h h
void __attribute__((noreturn)) call_start_cpu0() {
uint8_t cmd;
ets_printf("SRAM CODEn");
while (1) {
cmd = 0;
uart_rx_one_char(&cmd);
if(cmd == 'A') { // 1
*(unsigned int *)(0x3ff4808c) = 0x4001f880;
*(unsigned int *)(0x3ff48090) = 0x00003a98;
*(unsigned int *)(0x3ff4808c) = 0xc001f880;
}
}
asm volatile ( d ); // 2
"movi a6, 0x40; slli a6, a6, 24;" // 3
"movi a7, 0x00; slli a7, a7, 16;"
"xor a6, a6, a7;"
"movi a7, 0x7c; slli a7, a7, 8;"
"xor a6, a6, a7;"
"movi a7, 0xf8;"
"xor a6, a6, a7;"
"movi a10, 0x52; callx8 a6;" // R
"movi a10, 0x61; callx8 a6;" // a
"movi a10, 0x65; callx8 a6;" // e
"movi a10, 0x6C; callx8 a6;" // l
"movi a10, 0x69; callx8 a6;" // i
"movi a10, 0x7A; callx8 a6;" // z
"movi a10, 0x65; callx8 a6;" // e
"movi a10, 0x21; callx8 a6;" // !
"movi a10, 0x0a; callx8 a6;" // n
while(1);
}
В этом коде реализовано следующее (номера пунктов списка соответствуют номерам, указанным в комментариях):
- Обработчик команд с единственной командой, который выполняет сброс сторожевого таймера.
- Аналог
NOP
, реализованный на базе инструкцийaddi
. - Ассемблерный код, выводящий в последовательный интерфейс строку
Raelize!
.
▍Выбор времени проведения атаки
В нашем распоряжении было сравнительно небольшое окно атаки, начинающееся с F
, как показано на следующем рисунке. Из предыдущих экспериментов нам было известно, что в этот момент выполняется копирование кода загрузчика из флеш-памяти.
Окно атаки представлено фрагментом F
Сбой нужно устроить до того, как содержимое SRAM будет полностью перезаписано правильным кодом загрузчика из флеш-памяти.
▍Цикл атаки
В каждом из наших экспериментов мы предпринимали следующие шаги для того чтобы проверить работоспособность идеи атаки. Успешная организация сбоя должна была привести к выводу в последовательный интерфейс строки Raelize!
.
- Установка пина
G0
в низкое состояние и выполнение холодной перезагрузки для входа в режим загрузчика UART. - Использование команды
load_ram
для выполнения из SRAM кода, производящего атаку. - Отправка программе
A
для выполнения горячей перезагрузки и для перехода к обычному режиму загрузки. - Организация сбоя в процессе копирования загрузчика из флеш-памяти с помощью кода из ROM.
▍Результаты
После того, как мы выполняли этот эксперимент больше суток, проведя его более миллиона раз, мы так и не добились успеха.
▍Неожиданный результат
Но, несмотря на то, что желаемого нам достичь не удалось, мы, анализируя результаты экспериментов, нашли кое-что неожиданное.
В одном из экспериментов в последовательный интерфейс вывелись данные, указывающие на то, что сбой привёл к исключению IllegalInstruction
(недопустимая инструкция). Вот как это выглядело:
ets Jun 8 2016 00:22:57
rst:0x10 (RTCWDT_RTC_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
configsip: 0, SPIWP:0xee
clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
mode:DIO, clock div:2
load:0x3fff0008,len:4
load:0x3fff000c,len:3220
load:0x40078000,len:4816
load:0x40080400,len:18640
entry 0x40080740
Fatal exception (0): IllegalInstruction
epc1=0x661b661b, epc2=0x00000000, epc3=0x00000000,
excvaddr=0x00000000, depc=0x00000000
При попытках вызвать сбой чипа подобные исключения возникают достаточно часто. Это справедливо и для ESP32. При возникновении большинства таких исключений регистр PC
устанавливается в ожидаемое значение (то есть — там находится правильный адрес). Нечасто бывает так, чтобы в PC
появилось бы такое интересное значение.
Исключение IllegalInstruction
возникает из-за того, что по адресу 0x661b661b
нет правильной инструкции. Мы решили, что это значение в регистр PC
должно откуда-то попадать, и что само по себе оно там появиться не может.
Мы, в поисках объяснения, проанализировали код, который загрузили в SRAM. Просмотр бинарного кода, фрагмент которого показан ниже, позволил нам быстро узнать ответ на наш вопрос. А именно, тут легко найти значение 0x661b661b
. Оно представлено двумя инструкциями addi a6, a6, 1
, с помощью которых в коде реализован аналог NOP
.
00000000 e9 02 02 10 28 04 08 40 ee 00 00 00 00 00 00 00 |....(..@........|
00000010 00 00 00 00 00 00 00 01 00 00 ff 3f 0c 00 00 00 |...........?....|
00000020 53 52 41 4d 20 43 4f 44 45 0a 00 00 00 04 08 40 |SRAM CODE......@|
00000030 50 09 00 00 00 00 ff 3f 04 04 fe 3f 4d 04 08 40 |P......?...?M..@|
00000040 00 04 fe 3f 8c 80 f4 3f 90 80 f4 3f 98 3a 00 00 |...?...?...?.:..|
00000050 80 f8 01 c0 54 7d 00 40 d0 92 00 40 36 61 00 a1 |....T}.@...@6a..|
00000060 f5 ff 81 fc ff e0 08 00 0c 08 82 41 00 ad 01 81 |...........A....|
00000070 fa ff e0 08 00 82 01 00 4c 19 97 98 1f 81 ef ff |........L.......|
00000080 91 ee ff 89 09 91 ee ff 89 09 91 f0 ff 81 ee ff |................|
00000090 99 08 91 ef ff 81 eb ff 99 08 86 f2 ff 5c a9 97 |...............|
000000a0 98 c5 1b 66 1b 66 1b 66 1b 66 1b 66 1b 66 3e 0c |...f.f.f.f.f.f>.|
000000b0 1b 66 1b 66 1b 66 1b 66 1b 66 1b 66 1b 66 1b 66 |.f.f.f.f.f.f.f.f|
000000c0 1b 66 1b 66 1b 66 1b 66 1b 66 1b 66 1b 66 1b 66 |.f.f.f.f.f.f.f.f|
000000d0 1b 66 1b 66 1b 66 1b 66 1b 66 1b 66 1b 66 1b 66 |.f.f.f.f.f.f.f.f|
...
00000330 1b 66 1b 66 1b 66 1b 66 1b 66 1b 66 1b 66 1b 66 |.f.f.f.f.f.f.f.f|
00000340 1b 66 1b 66 1b 66 1b 66 1b 66 1b 66 1b 66 1b 66 |.f.f.f.f.f.f.f.f|
00000350 1b 66 1b 66 1b 66 1b 66 1b 66 1b 66 1b 66 1b 66 |.f.f.f.f.f.f.f.f|
Мы готовили с помощью этих инструкций «место для манёвра», используя их аналогично тому, как последовательности команд NOP
часто используются в эксплойтах для откладывания выполнения кода до нужного момента. Мы не ожидали того, что эти инструкции, в итоге, окажутся в регистре PC
.
Но мы, конечно, были не против этим воспользоваться. Мы решили, что сможем загрузить данные из SRAM в регистр PC
во время сбоя, устроенного тогда, когда данные из флеш-памяти копируются средствами ROM-кода.
Мы быстро поняли, что теперь у нас есть все ингредиенты для приготовления атаки, позволяющей, устроив единственный сбой, обойти системы защищённой загрузки и шифрования флеш-памяти. Тут мы использовали опыт, полученный в ходе выполнения ранее описанной нами атаки, когда нам удалось получить контроль над регистром PC
.
Путь к успеху
При проведении этой атаки мы использовали большую часть того кода, который ранее загружали в SRAM с помощью загрузчика UART. Из этого кода убраны лишь команды вывода символов в последовательный интерфейс, так как теперь нашей целью была установка регистра PC
в нужное нам значение, то есть — получение возможности контролировать систему.
#define a "addi a6, a6, 1;"
#define t a a a a a a a a a a
#define h t t t t t t t t t t
#define d h h h h h h h h h h
void __attribute__((noreturn)) call_start_cpu0() {
uint8_t cmd;
ets_printf("SRAM CODEn");
while (1) {
cmd = 0;
uart_rx_one_char(&cmd);
if(cmd == 'A') {
*(unsigned int *)(0x3ff4808c) = 0x4001f880;
*(unsigned int *)(0x3ff48090) = 0x00003a98;
*(unsigned int *)(0x3ff4808c) = 0xc001f880;
}
}
asm volatile ( d );
while(1);
}
После компиляции этого кода мы, прямо в его бинарном варианте, заменили инструкции addi
на адрес 0x4005a980
. По этому адресу находится функция в ROM, которая выводит данные в последовательный интерфейс. Успешный вызов этой функции позволил бы нам узнать об успешно проведённой атаке.
Мы подготовились к тому, чтобы устраивать такие сбои, параметры которых соответствовали бы тому, что, в одном из предыдущих экспериментов, вызвал исключение IllegalInstruction
. Через некоторое время мы обнаружили успешное завершение нескольких попыток эксперимента по загрузке в регистр PC
заданного адреса. Возможность контроля регистра PC
с высокой долей вероятности означает то, что мы сможем выполнять произвольный код.
▍Почему это возможно?
В заголовок этого раздела вынесен хороший вопрос, ответить на который нелегко.
У нас, к сожалению, нет чёткого ответа. Мы, определённо, не ожидали того, что манипуляции с данными могут позволить контролировать регистр PC
. У нас есть несколько объяснений этого, но мы не можем заявлять с полной уверенностью о том, что какое-то из них истинно.
Одно из объяснений заключается в том, что во время сбоя меняются оба операнда инструкции ldr
, используемой для загрузки значения в a0
. Это похоже на то, что мы видели при проведении этой атаки, где мы получали непрямой контроль над регистром PC
, модифицируя данные.
Более того, возможно, что в коде, хранящемся в ROM, реализован функционал, который способствует успешности проведения данной атаки. Другими словами, мы, из-за сбоя, можем выполнить правильный код из ROM, что приводит к тому, что данные из SRAM загружаются в регистр PC
.
Для того чтобы узнать о том, что именно позволило нам выполнить эту атаку, нужно провести более тщательные исследования. Но, если смотреть на дело глазами того, кто решил взломать чип, имеющихся у нас знаний достаточно для того, чтобы создать эксплойт, основанный на возможности воздействия на регистр PC
.
Извлечение содержимого флеш-памяти в виде обычного текста
Мы можем писать в регистр PC
то, что нам нужно, но мы пока не можем извлечь содержимое флеш-памяти в виде обычного текста. Поэтому решено было воспользоваться возможностями загрузчика UART.
А именно, мы решили перейти прямо к загрузчику UART в то время, когда чип находится в режиме обычной загрузки. Для проведения этой атаки мы перезаписали инструкции addi
в коде, загруженном в RAM, использовав вместо них адрес начала кода загрузчика UART (0x40007a19
).
Загрузчик UART выводит в последовательный интерфейс строку, показанную ниже. Этот факт мы можем использовать для определения успешности атаки.
waiting for downloadn"
После того, как подобный эксперимент окажется успешным, мы можем просто воспользоваться esptool.py
для запуска команды read_mem
и для получения доступа к данным флеш-памяти, представленным в виде обычного текста. Например, следующая команда читает 4 байта из адресного пространства внешней флеш-памяти (0x3f400000
).
esptool.py --no-stub --before no_reset --after no_reset read_mem 0x3f400000
К сожалению, такая команда не сработала. По какой-то причине ответ процессора выглядел как 0xbad00bad
, что указывало на то, что мы пытаемся читать данные из неразмеченной памяти.
esptool.py v2.8
Serial port COM8
Connecting....
Detecting chip type... ESP32
Chip is ESP32D0WDQ6 (revision 1)
Crystal is 40MHz
MAC: 24:6f:28:24:75:08
Enabling default SPI flash mode...
0x3f400000 = 0xbad00bad
Staying in bootloader.
Мы обратили внимание на то, что в начале загрузчика UART выполняется довольно много настроек. Мы предположили, что эти настройки могут воздействовать и на MMU.
Просто для того чтобы попробовать что-то ещё, мы решили перейти прямо к обработчику команд самого загрузчика UART (0x40007a4e
). После того, как мы окажемся в обработчике, мы сможем самостоятельно отправить команду read_mem
прямо в последовательный интерфейс:
target.write(b'xc0x00x0ax04x00x00x00x00x00x00x00x40x3fxc0')
К сожалению, если перейти прямо в обработчик, то строка, которая выводится после входа в загрузчик UART (то есть — waiting for downloadn
), не выведется. Из-за этого мы теряем простой и удобный способ идентификации удачных экспериментов. В результате мы решили отправлять вышеописанную команду во всех экспериментах, не обращая внимания на то, удачными они были или нет. Мы использовали очень короткий тайм-аут последовательного интерфейса для того чтобы минимизировать дополнительные затраты времени, связанные с этим тайм-аутом, возникающие практически всегда.
Через некоторое время мы увидели результаты первых успешных экспериментов!
Итоги
В этом материале мы описали атаку на ESP32, при проведении которой мы обходим системы защищённой загрузки и шифрования флеш-памяти, устраивая всего один сбой в работе микроконтроллера. Более того, мы использовали уязвимость, эксплуатируемую при проведении атаки, для извлечения содержимого зашифрованной флеш-памяти в виде обычного текста.
Для того чтобы пошагово представить ход этой атаки, мы можем использовать FIRM.
Ход атаки
Вот краткое описание того, что происходит на разных шагах вышеописанной атаки:
- Activate (выбор инструментов для проведения атаки) — здесь используется комплекс Riscure Inspector FI.
- Inject (проведение атаки) — осуществляется электромагнитное воздействие на исследуемый микроконтроллер.
- Glitch (нарушение работы устройства) — происходит нарушение работы устройства, зависящее от параметров воздействия (время, место, мощность).
- Fault (неправильная работа устройства) — устройство начинает работать неправильно, но то, что именно случится на этом шаге, заранее неизвестно. Например, могут быть повреждены какие-то инструкции в памяти.
- Exploit (эксплуатация уязвимости) — использование загрузчика UART для выполнения кода, производящего запись данных в SRAM, и для организации горячей перезагрузки устройства. Осуществляется запись адреса обработчика команд загрузчика UART в регистр
PC
для вызова командыread_mem
. - Goal (достижение цели атаки) — осуществляется чтение данных из флеш-памяти в виде обычного текста.
Интересно то, что успех этой атаки зависит от двух слабостей ESP32. Первая слабость заключается в том, что загрузчик UART нельзя отключить. В результате он доступен всегда. Вторая слабость — это сохранение данных в SRAM после горячей перезагрузки устройства. Это позволяет, используя загрузчик UART, заполнить SRAM произвольными данными.
В информационном сообщении, которое относится к этой атаке, компания Espressif сообщает о том, что в более новых версиях ESP32 присутствуют механизмы, делающие подобную атаку невозможной.
Все стандартные встраиваемые системы уязвимы к атакам, выполняемым путём нарушения нормальной работы устройств. Поэтому неудивительно то, что к атакам по сторонним каналам уязвим и микроконтроллер ESP32. Подобные чипы попросту не спроектированы с расчётом на устойчивость к таким атакам. Но, что важно, это не значит, что подобные атаки не несут в себе никакого риска.
Наше исследование показало, что использование слабых мест чипа позволяет проводить успешные атаки, устраивая сбои. Большинство атак, о которых можно узнать из открытых источников, используют традиционные подходы, где основное внимание направлено на обход проверок. Мы встречали не особенно много сообщений об атаках, подобных той, которую описали.
Мы уверены в том, что полный потенциал подобных атак всё ещё исследован недостаточно. Большинство исследователей до последнего времени занималось лишь изучением методов нарушения работы чипов (шаги Activate, Inject, Glitch), а мы же пошли дальше, рассмотрев возможность работы с уязвимым чипом после сбоя (шаги Fault, Exploit, Goal).
Исследования до 2020 и после 2020 года
Мы уверены в том, что креативное использование новых моделей отказов микросхем приведёт к увеличению числа способов атак, в которых, для достижения самых разных целей, будут использованы интересные стратегии эксплуатации уязвимостей.
Если вам интересна тема, затронутая в этом материале, то вот, вот и вот — другие материалы, посвящённые исследованию ESP32.
Сталкивались ли вы на практике с взломом каких-либо устройств методами, близкими к тем, о которых шла речь в этой статье?
Автор: ru_vds