Глухарь ESPшный — мемуары охотника

в 9:28, , рубрики: bugbounty, correlation attack, esp32, offzone2025, positive technologies, raspberry pi pico, реверс-инжиниринг
Глухарь ESPшный — мемуары охотника - 1

Названием «глухарь» птица обязана известной особенности токующего в брачный период самца утрачивать чуткость и бдительность, чем часто пользуются охотники.

Защита программного кода — извечная битва меча и щита. Одни люди стараются создать устойчивые ко взлому программные и аппаратные продукты, другие пытаются эти решения сломать. Но что происходит, когда команда, специализирующаяся на взломе, выпускает на рынок свой продукт? Можно ли в этих обстоятельствах разработать устройство, защиту которого невозможно обойти?

Сегодня мы посмотрим внутрь флеш-картриджа для Nintendo Switch под названием MIG Switch и раскроем тайну его происхождения! Ну и в качестве побочного квеста победим защиту одного из самых современных микроконтроллеров на рынке.

1. Есть только MIG, за него и держись

В общем, тема такая. Игровая консоль Nintendo Switch использует для хранения игр специальные носители — картриджи:

Размером каждый картридж примерно с SD-карту памяти

Размером каждый картридж примерно с SD-карту памяти

Внутри него находится обычная на вид микросхема памяти от известного вендора — MXIC, Winbond или Toshiba:

Есть “монолиты”, есть TSOP48 - прямо как у современных флешек

Есть «монолиты», есть TSOP48 — прямо как у современных флешек

Вот только это не совсем обычная память — с внешним миром она общается по проприетарному протоколу с двойным шифрованием (по непубличной информации, в ней применяются алгоритмы AES-CCM и SNOW 2), так что ни прочитать её невозможно, ни сделать свой собственный накопитель.

Ну как невозможно, недавно кому-то это всё же удалось и на рынке появился «эмулятор» картриджей в комплекте со специальным «дампером» к нему:

В картридж вставляется microSD с образами резервными копиями игр

В картридж вставляется microSD с образами резервными копиями игр
А эта штука, наоборот, умеет делать резервные копии

А эта штука, наоборот, умеет делать резервные копии

Удивительно, но утверждается, что это российская разработка, даже есть (был, уже отключили) красиво оформленный сайт, где можно оформить заказ:

Более чем уверен, что вам ничего не пришлют, но сам не пробовал

Более чем уверен, что вам ничего не пришлют, но сам не пробовал

Увы, это лишь повод свалить вину на «плохих русских» и выйти за пределы сферы влияния Nintendo — все прекрасно понимают, что сделали этот девайс выходцы из Team Xecuter, ведь даже в DNS сайта однажды засветился (но всячески отрицает) Gary "Opa" Bowser, которого в прошлом засудили на $14млн за разработку и продажу модчипов на тот же Nintendo Switch. Тем не менее, прямых доказательств этому не было. Я решил исправить это недоразумение, а заодно посмотреть «что там внутри».

И я не про содержимое корпуса устройства, это как раз-таки самое простое. В первых партиях маркировка на чипах была затерта, но довольно скоро обнаружили и незатёртые экземпляры. Внутри оказались микроконтроллер ESP32 и FPGA Lattice ICE40:

То ли передумали, то ли поленились

То ли передумали, то ли поленились

В наше время, когда перепрошивается даже штекер в USB кабеле, самый сложным и затратным этапом исследования может оказаться не сам реверс-инжиниринг, а получение прошивки. Файлы обновлений практически всегда зашифрованы, а микрочипы имеют защиты и блокировки от вычитывания.

2. ̶Г̶а̶р̶р̶и̶ ̶П̶о̶т̶т̶е̶р̶ ̶и̶ атаки по побочным каналам

Как обычно защищаются микроконтроллеры? Прячут флешку подальше от длинных ручонок и всеми правдами и неправдами запрещают её читать. Espressif поступили иначе, у них флешка может быть и снаружи чипа, но читать её бесполезно. Данные зашифрованы AES ключом (он лежит во фьюзах) и на лету аппаратно расшифровываются при обращении к флешке через кэш:

Такое поддерживается не только для флешки, но и для PSRAM

Такое поддерживается не только для флешки, но и для PSRAM

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

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

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

Краткая схема атаки. Ну совсем краткая.

Краткая схема атаки. Ну совсем краткая.

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

Пример записанной осциллограммы. Видно, что пики потребления (внизу) происходят на фронтах клока CPU

Пример записанной осциллограммы. Видно, что пики потребления (внизу) происходят на фронтах клока CPU

Идея проста — найти зависимость графика питания от входных данных и за счёт этого определить, какой AES ключ был использован. Как именно?

Алгоритм AES использует 4 метода «запутывания» и повторяет их в цикле несколько раз:

Весь AES в виде схемки. Первый и последний раунды неполные.

Весь AES в виде схемки. Первый и последний раунды неполные

Три из них в виде Python кода:

def add_round_key(s, k): # банальнейший xor с ключом
    for i in range(16):
        s[i] ^= k[i]

def sub_bytes(s):        # замена значений по таблице
    for i in range(16):
        s[i] = s_box[s[i]]

def shift_rows(s):       # перетасовка байт в зависимости от позиции
    in_s = copy(s)
    for i in range(16):
        s[i-4*(i % 4)] = in_s[i]

Такие операции, повторенные 10-14 раз с разными раундовыми ключами (генерируются из основного ключа отдельным алгоритмом Key Expansion) приводят к тому, что становится невозможным уследить за битами и провернуть фарш назад, не зная ключа.

Демонстрация AES-128 в средневековье

Демонстрация AES-128 в средневековье

Если шифрование реализовано «в лоб», то где-то внутри устройства в рамках самой первой операции add_round_key произойдёт действие s[i] ^= k[i] на результат которого мы влияем — например, если входные данные полностью совпадут с ключом, то во временном регистре окажутся только нули. Самое простое, что можно здесь придумать:

  • в качестве данных подать <xx 00 00 00 … 00> на вход

  • замерить потребление питания сразу после add_round_key

  • повторить для всех значений хх

  • выбрать вариант, где потребление минимально

  • мы нашли первый байт ключа!

Такой подход поиска различий потребления питания между запусками называется дифференциальная атака по энергопотреблению (Differential Power Attack, DPA). К сожалению, в реальной жизни описанный метод вряд ли сработает, и нужно использовать что-то посложнее, например корреляционную атаку (Correlation Power Attack).

Любителям бумажной литературы и аппаратного хакинга отдельно рекомендую заглянуть в хрестоматию The Hardware Hacking Handbook за авторством Колина О’Флинна. Есть и русская версия!

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

Шифрование теперь проходит до следующего этапа, а значит, затрагивает уже две операции — add_round_key и sub_bytes. Скомбинируем формулу:

out[0] = s_box[input_data[0] ^ key[0]]

Предположим, что нулевой байт ключа равен 0xBF. Тогда, подавая в качестве входных данных 0x17, 0x18, 0x19 и 0x1A во временном регистре должно получиться значение:

s_box[0x17 ^ 0xBF] = 0xC2
s_box[0x18 ^ 0xBF] = 0x5C
s_box[0x19 ^ 0xBF] = 0x24
s_box[0x1A ^ 0xBF] = 0x06

Раз, по нашему предположению, нулевые биты в регистре потребляют меньше питания, чем единичные, посчитаем, сколько после этого шага получилось единичных бит (вес Хэмминга):

s_box[0x17 ^ 0xBF] = 0xC2 # 3 бита
s_box[0x18 ^ 0xBF] = 0x5C # 4 бита
s_box[0x19 ^ 0xBF] = 0x24 # 2 бита
s_box[0x1A ^ 0xBF] = 0x06 # 2 бита

Итого получаем следующую зависимость (уникальный шаблон, отпечаток именно этого предположения “key[0] = 0xBF”):

Количество единичных бит в зависимости от входных данных

Количество единичных бит в зависимости от входных данных

Это наша (частичная) модель поведения потребления питания при key[0] = 0xBF. Причём для других предположений key[0] (моделей) картина будет другой. Осталось провести замеры реального потребления питания осциллографом и проверить, какой из 256 «моделей» больше всего они соответствуют. Это и будет правильный кусочек ключа.

Выбрать наиболее подходящий шаблон поведения aka модель помогает математика, а именно, корреляция. Если есть зависимость между набором значений (предсказание/реальные замеры), то этот математический аппарат покажет, где именно эта зависимость наблюдается.

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

В случае с общедоступными микроконтроллерами это просто — берём точно такой же (в идеале даже из той же партии) чип, прошиваем туда свой известный ключ, подставляем в модель те же данные, что подавали на чип при замерах и проверяем на практике:

Небольшой спойлер - проверка утечки с помощью корреляции на испытуемом ESP32-S2

Небольшой спойлер - проверка утечки с помощью корреляции на испытуемом ESP32-S2

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

Выяснили, что утечки AES начинаются с вон того третьего глубокого “пика”

Выяснили, что утечки AES начинаются с вон того третьего глубокого «пика»

Здесь важно правильно угадать с предположениями про то, как именно работает AES внутри исследуемого устройства. Например, в случае ESP32, это аппаратный модуль, обрабатывающий все 128 бит блока входных данных за один тик процессора, что мы и видим в результате — один основной пик на графике значений корреляции. Напротив, для программной реализации AES, где каждый байт обрабатывается отдельно, пики для каждого из 16 байт будут раскиданы во времени:

Взято из вот этой статьи

Взято из вот этой статьи

Здесь на графике исследователи одновременно нашли как временные позиции, так и конкретные значения для ключа, но для этого потребовалось вычислять корреляцию для всех возможных моделей во всех предполагаемых временных позициях. Это может быть долго.

Примерную позицию, когда входные данные используются в микропроцессоре, можно найти и другими способами, например, SNR, NICV. Возвращаясь к игровым приставкам, такой подход использовала команда fail0verflow в своей статье про получение ключа южного моста PS4. Что они сделали:

  • записали много трасс питания с разными payload на входе

  • для каждого из 128 бит входных данных:

    • разделили трассы на две группы (с 0 и 1 в этой позиции)

    • вычислили среднее значение каждой из групп

    • посчитали абсолютную разницу между этими группами

  • построили график этих разниц для всех 128 бит и моментов времени

    Получился график, показывающий в какие моменты времени CPU использует биты входных данных

    Получился график, показывающий в какие моменты времени CPU использует биты входных данных

Итак, подытоживая всю эту теорию, план атаки ESP32-S2 выходит следующий:

  • набрать «трасс» и соответствующих им данных

  • найти конкретные места, где происходят операции над входными данными

  • корреляцией найти, собственно, ключ

  • ???

  • PROFIT

Гениальные идеи преследовали его, но он был быстрее

Гениальные идеи преследовали его, но он был быстрее

3. Вперёд и с песней

Там, где это возможно, я стараюсь опираться на некоторый рабочий proof-of-concept. Куда легче сначала пощупать уже работающий девайс, понять его устройство, изучить все нюансы, и уже от этого отталкиваться в своём исследовании. Таким проектом в этой истории послужил esp-cpa, автор которого заморочился и написал лонгрид, где очень подробно рассказал, как достиг результатов и как пошагово всё это воспроизвести.

Готовый девайс с подключенным ESP32v3

Готовый девайс с подключенным ESP32v3

Здесь уже были реализованы запись осциллограмм питания, эмуляция SPI-флешки, математика для вычисления корреляций и утечек AES и даже программный PID-регулятор для поддержания постоянной температуры ESP32.

Конечно же, поддержки нужного мне ESP32-S2 в нём не было, но благодаря похожести всех ESP32 между собой, относительно малой кровью удалось адаптировать проект под себя, заодно прокачав его возможности:

  • Поднял частоту семплирования с 12 МГц аж до 48 МГц 

  • Ускорил запись в 4 раза за счёт реорганизации замеров температуры

  • Сделал плату для ESP32-S2

Да, добавил JTAG разъём для удобства отладки

Да, добавил JTAG разъём для удобства отладки

Хотя нет, «малой кровью» — это сильно заниженная оценка моих страданий. Всё, что могло пойти не так — пошло не так.

Когда что-то не получается

Когда что-то не получается

Начнём с того, что esp-cpa использует замедление ESP32 до 0.5 МГц. Автор делал это для того, чтобы при 12 МГц семплирования иметь адекватную осциллограмму питания. Но ESP32-S2 отказывается работать на столь низких частотах! Чтобы заставить чип работать на 1.1 МГц (меньше не вышло), понадобилось добавить в разрыв CLK конденсатор на 10 нФ и резистор на 20 КОм. Как до этого догадаться? Да никак, только эксперименты:

Очевидно, на 1.1 МГц (слева) график питания гораздо адекватнее, чем на 4.8 МГц (справа)

Очевидно, на 1.1 МГц (слева) график питания гораздо адекватнее, чем на 4.8 МГц (справа)

Дальше — больше. У меня в MIG стоял не просто ESP32-S2, а версия с флешкой внутри, ESP32-S2F. А для атаки нужно подключить SPI эмулятор вместо флешки, значит надо как-то отключить внутреннюю. К счастью, все выводы были по-прежнему подключены к пинам микросхемы, а значит, достаточно перерезать (и перехватить на себя) линию Chip Enable (CE):

Проводки, уходящие от контакта CE на флешку (синий) и проц (красный). Тут же сидит и PSRAM (зелёный)

Проводки, уходящие от контакта CE на флешку (синий) и проц (красный). Тут же сидит и PSRAM (зелёный)
Гравёр в руки и аккуратно пилим буром из центра в направлении нужной ножки

Гравёр в руки и аккуратно пилим буром из центра в направлении нужной ножки
После чего паяем провод на отрезанный волосок

После чего паяем провод на отрезанный волосок

Уже опущу, сколько понадобилось, чтобы заставить все подсистемы правильно работать, подобрать параметры, чтобы корреляция считалась, а ключи восстанавливались, упомяну только про один неожиданный момент, который всплыл, когда я проводил серию экспериментов с различными параметрами захвата трасс питания. Я уже показывал эту картинку ранее:

Всякие модели и корреляции

Всякие модели и корреляции

На ней показаны моменты времени, где происходит утечка информации для различных моделей. Красным и оранжевым показаны модели утечек по AES, раунды 0 и 1 соответственно:

  • AES_Round0 = HW(inv_sbox(input ^ key))

  • AES_Round1 = HW(inv_sbox(after_r0 ^ key))

На графике видно, что у каждой модели утечка происходит в двух местах. Так вот, я попробовал проводить корреляционный анализ как в первой точке (P1), так и во второй (P2), а ещё попробовал скомбинировать результаты, банально перемножив полученные значения (P1+P2):

В ячейках – ошибка вычислений ключа. В скобках – количество успешно угаданных байт

В ячейках — ошибка вычислений ключа. В скобках — количество успешно угаданных байт

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

С нагревом и без, при тех же настройках

С нагревом и без, при тех же настройках

Наконец, после многочисленных замеров, объединений результатов и даже перебора, мне удалось восстановить ключ для первого блока MIG:

Расшифрованный первый блок. Тут есть интересности, мы к этому ещё вернёмся

Расшифрованный первый блок. Тут есть интересности, мы к этому ещё вернёмся

Почему только для первого блока? А это уже связано с особенностями применяемой в ESP32-S2 схеме шифрования AES-XTS.

4. AES’ы бывают разные — белые, синие, красные

Самый простой вариант блочного применения алгоритма AES (ECB) работает с данными независимо. Одним и тем же ключом каждый блок данных размером в 16 байт преобразуется в соответствующие 16 байт шифротекста. Значит, если узнать ключ для любого блока, им же можно расшифровать и всё остальное.

И так и было в ESP32 (упоминалось в моей статье про ёлку) — несмотря на то, что ключ модифицируется в зависимости от адреса блока, модификация эта обратима.

Чтобы усложнить задачу потенциальным злоумышленникам, в Espressif во всех последующих чипах сменили алгоритм шифрования на AES-XTS. Теперь в схему, помимо основного ключа (KE), добавляется второй (KT), с помощью которого генерируется Tweak (Ti) — модификатор для данных, который накладывается до и после шифрования:

Изначально этот алгоритм создавался для поддержки шифрования блоками, не кратными блоку AES

Изначально этот алгоритм создавался для поддержки шифрования блоками, не кратными блоку AES

Каждые 0x80 байт генерируется полностью новый Tweak, для этого через AES шифруется текущий адрес флешки. Внутри блока же Tweak каждый раз перед шифрованием следующих 0x10 байт умножается на α (полином x128+x7+x2+x+1, почти что умножение на 2).

Можно ли с помощью CPA узнать ключ KT? Проблема в том, что для корреляционной атаки нужно иметь возможность менять входные данные, что в этом случае сделать затруднительно — диапазон plaintext’а ограничен количеством блоков загрузчика (0-512), такой вариативности слишком мало для хоть какого-то результата. 

Всё, что остаётся — атаковать каждый блок в 128 байт независимо. Обычно это не имеет смысла — этими атаками мы расшифровываем не саму прошивку, а только ESP32 загрузчик, который в подавляющем большинстве устройств стандартный и не представляет интереса. Но не в этом случае! Похоже, что в MIG загрузчик тоже самописный:

Всего два сегмента - это точно что-то нестандартное

Всего два сегмента — это точно что-то нестандартное

А значит, можно попробовать выйти за рамки адекватного и выполнить одновременную атаку на 84 блока (по анализу происходящего на SPI шине, размер бутлоадера MIG был оценен в 11 КБ). Для этого потребовалось полностью переделать инструментарий.

Знакомьтесь, vESPa:

Да, это WeAct RP2350B, скрещенная с переосмысленным esp-cpa

Да, это WeAct RP2350B, скрещенная с переосмысленным esp-cpa
Скутер-пасхалка

Скутер-пасхалка

Raspberry Pi Pico — это лучшее, что можно найти, если вы хотите сделать нечто очень быстрое и очень нестандартно шевелящее ножками, при этом не залезая в дебри Verilog. Мне удалось засунуть в эту небольшую платку под десяток крутых фич:

  • Полная эмуляция 16 КБ SPI-Flash (в одно- и двух-битном режиме) до 40 МГц

  • Журналирование обращений по SPI, подмена данных на лету по триггеру

  • Динамическое управление частотой тактирования чипа

  • Запись осциллограмм питания, в том числе кусочно

  • Нагрев и поддержание температуры изучаемого чипа

  • Предварительная обработка и усреднение осциллограмм перед передачей

Например, логирование SPI выглядит так:

|WAKE_UP
|READ_03:	001000
|JEDECID:	5E4016
|READ_03:	001010
|STATUS0:	00
|STATUS0:	00
|READ_BB:	001020
|READ_BB:	001030
|READ_BB:	001040
|READ_BB:	001050
|READ_BB:	001060
|READ_BB:	001070
|READ_BB:	001080
..

Это удобно, чтобы узнать, к примеру, тип микроконтроллера (разные модели по-разному общаются с флешкой), размер бутлоадера, место хранения прошивки, без необходимости подключать серьёзный логический анализатор.

Поддержка двухбитного SPI, как и динамическое управление частотой тактирования, сильно ускорило сбор данных. Да, перед замером питания процессор стоит замедлять до 1 МГц, но ни к чему держать 1 МГц в промежутках между измерениями, пока происходит чтение очередного блока данных:

Стартуем на 50 МГц, дальше 10 вне записи и 1 при записи

Стартуем на 50 МГц, дальше 10 вне записи и 1 при записи

Но что реально круто - кусочный сбор данных. Если мы заранее знаем, какой временной участок нас интересует, зачем хранить остальное? Можно ещё на этапе сбора отбрасывать всё лишнее:

Оставляем только места, где есть утечки

Оставляем только места, где есть утечки
Обрезаем только их, итого было 1024 точки, стало 48, экономия!

Обрезаем только их, итого было 1024 точки, стало 48, экономия!

В инструменте установлен 12-битный АЦП, значения хранятся в uint16_t — имеем целых 4 свободных бита, а значит, можно суммировать и усреднять трассы прямо в оперативке Pico, ещё до передачи на ПК (до 16 раз):

Без усреднения (слева) и с усреднением по 16 трасс (справа)

Без усреднения (слева) и с усреднением по 16 трасс (справа)

Все параметры для запуска замеров вывел в конфиг, плюс дублирую в бинарный файл с результатами. Выглядит как-то так:

{
    "base_clock": 80000, # начальная частота CPU для быстрого перехода к чтению прошивки
    "mid_clock": 16000,  # частота CPU вне замера осциллограммы
    "slow_clock": 1400,  # частота при замере осциллограммы
    "trigger": 3,        # после этой команды переключаться на выдачу рандома
    "clk_pos": 160,      # начинать запись после этого клока SPI
    "mode": 3,           # режим эмуляции SPI
    "adc_del": 483,      # дополнительная задержка записи
    "adc_num": 12,       # количество точек в каждом сегменте
    "btw_del": 111,      # пауза между сегментами
    "btw_num": 3,        # количество записываемых сегментов на блок
    "spi_num": 15,       # количество записываемых блоков
    "multi": 1           # режим записи множества блоков
}

Огорчало лишь одно — качество восстановления AES ключей. На тестовых чипах стабильно получалось восстановить только половину ключа. Интересно, что success rate зависел от положения байта в ключе. Одни части ключа восстанавливались хорошо, другие наоборот, прятались до последнего.

Типичные результаты корреляции для раунда 0, только на 4 из 16 результат вышел хорошо (одна выделяющаяся отдельно отстоящая от остальных линия на графике)

Типичные результаты корреляции для раунда 0, только на 4 из 16 результат вышел хорошо
(одна выделяющаяся отдельно отстоящая от остальных линия на графике)

Здесь стоит упомянуть интересную особенность. Полное восстановление ключа для AES-XTS требует атаки на два раунда алгоритма AES — в первом раунде мы получаем модифицированный Tweak, на втором раунде получаем ключ шифрования. Объединяя эти кусочки можно расшифровать блок. При этом сложности возникают только с раундом 0, в то время как раунд 1, напротив, очень хорошо атакуется (конечно, при условии правильно проведённой предыдущей атаки):

12 из 16 успешно показались, но целый ряд зафейлился

12 из 16 успешно показались, но целый ряд зафейлился

Если хотя бы один байт из группы был неправильно угадан в 0 раунде, целый ряд не показывал результатов на раунде 1. И напротив, если 4 соответствующих байта оказались верными, это отлично видно на графике. Благодаря такому отлично различимому результату, атаку на раунд 1 можно использовать в качестве оракула! А именно, для проверки, какие части Tweak были получены верно.

Итак, имеем ситуацию:

  • Общий ключ шифрования уже выстрадали со взломом первого блока, он везде одинаковый и именно он выплывает на CPA раунда 1

  • Tweak каждый раз различаются поэтому ключи на CPA раунда 0 везде разные

  • Можно узнать, какие кусочки Tweak правильно восстановились, через CPA раунда 1

  • Некоторые позиции восстанавливаются лучше других, обычно удается подтвердить 4-8 байт

  • В рамках блока, Tweak зависят друг от друга как Tn = Tn-1 ⊗ α

Стоп, что?

  • В рамках блока, Tweak зависят друг от друга как Tn = Tn-1 ⊗ α !!

То самое умножение, которое входило в стандарт AES-XTS вместо усиления защиты сыграло в нашу пользу. Следите за руками, теперь мы играем в судоку:

  • Запускаем CPA на раунд 0, получаем 8 частично валидных ключей одного XTS блока

  • Получаем подтверждения, какие из байт валидны, через CPA раунда 1 (если повезет)

  • XOR'им их с раундовым ключом шифрования, чтоб получить частично валидный Tweak

  • Собираем паззл из 100% валидных кусочков, протягивая их в соседние ключи

  • Брутфорсим оставшиеся части, используя «подтверждение через раунд 1» и протягивание

Сложный кейс алгоритма «судоку» — восстанавливаем ключ всего блока практически из ничего

Сложный кейс алгоритма «судоку» — восстанавливаем ключ всего блока практически из ничего

Изначально я пытался использовать статистические методы и выбирать наиболее часто встречающиеся биты при сдвиге и XORе твиков, но в итоге пришёл к выводу, что нужно работать только со 100% подтверждёнными данными, иначе на одних предположениях алгоритм легко уводит в далёкие дали.

Короче, всё получилось, сутки сбора трасс, несколько часов судоку и брутфорса — и у меня есть все блоки бутлоадера МИГ!!

Процесс работы алгоритма вычисления ключей для всех блоков

Процесс работы алгоритма вычисления ключей для всех блоков
Привет, бутлоадер МИГа

Привет, бутлоадер МИГа

Бутлоадер оказался интересным, пошли первые подтверждения, что это творчество команды TX — коды ошибок вида 0xBAD00006 и код успеха 0x900D0000 точно так же используются в прошивке модчипа SX Core разработки Team Xecuter.

Основная же прошивка загружалась из SPI флешки в обход прозрачного шифрования ESP, зато с ручным использованием AES и странного проприетарного шифрования. От получения прошивки меня отделял последний ключ, который либо напрямую читался из фьюзов, либо получался через аппаратный HMAC из рандомных 16 байт, обнаруженных мной ещё в первом расшифрованном блоке.

Та самая штука, которую я изначально принял за секретный ключ

Та самая штука, которую я изначально принял за секретный ключ

Этот самый финальный ключ открывал доступ к небольшому блоку в самом конце флешки, блоку ключей, которыми зашифрованы прошивки МИГа. Долго я пробовал к нему подобраться... Мешались новые проблемы:

  • чип здесь уже работает от внутреннего PLL на частоте 240 MHz, замедлить не получается

  • AES уже запускается не аппаратно, а программным кодом — нет четкого триггера

  • до момента расшифровки проходит много времени, сбор «трасс» получается очень долгим

Различными экспериментами удалось добиться восстановления 5-6 байт пробного ключа в раунде 0. К сожалению, в этот раз раунд 1 совсем никак не помогал, да и метод «судоку» был неприменим из-за обычного AES + всего одного AES блока.

5. Глич, похититель кода

На самом деле был и другой вариант вычитать прошивку — через ошибку в коде ROM чипа. Более того, порой это можно сделать даже, если ошибки никакой нет! Например, здесь автор через кратковременный сбой питания заставляет memcpy() скопировать больше, чем нужно, перезаписать стек и тем самым выполнить загруженный код.

Плата для проведения этой атаки на ESP32

Плата для проведения этой атаки на ESP32

Конечно, куда проще действительно найти ошибку в программном коде. Я пробовал, даже думал, что нашёл одну:

То самое место в коде ROM

То самое место в коде ROM

Что тут происходит:

  1. Entry Point из заголовка флешки копируется на стек

  2. Заголовок проверяется на валидность

  3. Заголовок читается повторно для проверки хеш-суммы

  4. Заголовок читается в третий раз в рамках проверки secure boot

  5. После всех проверок, код исполняет Entry Point из копии в стеке!

Повторные чтения одних и тех же данных это заявка на уязвимость вида TOCTOU (Time of Check — Time of Use), когда между проверкой и реальным использованием значение можно подменить «снаружи», в данном случай на флешке. А если подменить Entry Point, сами понимаете, можно прыгнуть на любой код и перехватить управление.

Настроил SPI эмулятор, подключил, получил такое:

Secure Boot проверка прошла!

Secure Boot проверка прошла!

А сразу затем такое:

Но не SHA-256 хеш

Но не SHA-256 хеш

Что произошло — несмотря на два разных memcpy, SPI флешка имеет кэш, и при повторных чтениях этот самый кэш возвращает те же значения, не читая флешку повторно. Перед проверкой Secure Boot кэш намеренно инвалидируется, поэтому там заменить данные возможно, но как-то эксплуатировать это не получилось.

А это значит, остаётся лишь глич-атака. Кстати, это тоже считается и атакой по побочным каналам, и атакой по питанию 🙂. При разработке vESPa я заложил возможность проведения таких атак, причём не только с помощью транзистора, но и за счёт питания целевого чипа прямо от GPIO:

Можно, например, что-то такое сформировать всего тремя положениями пинов

Соединяем 5 ножек между собой, получаем высокоскоростной ЦАП и формируем сигнал почти любой формы

Вообще, Raspberry Pi Pico прекрасно подходит для проведения voltage glitch атак, о чём даже в его даташите написано:

Естественно в разделе про защиту от подобных атак

Естественно в разделе про защиту от подобных атак

Мне очень хотелось развить идею с TOCTOU — достаточно в любом виде сбить работу SPI-кэша и CPU считает данные повторно, дальше дело техники. Самое простое место для этого нашлось сразу за чтением идентификатора флеша:

Всего в паре команд от него происходит чтение следующего куска конфигурации

Всего в паре команд от него происходит чтение следующего куска конфигурации

Не буду томить, это сработало, глич получился именно так, как я хотел:

Чип прочитал 0x1000 адрес повторно!

Чип прочитал 0x1000 адрес повторно!

И мне действительно удалось подставить свой Entry Point при включённом Secure Boot. Вот только куда прыгать-то? Единственный буфер, куда загружается прошивка, полностью очищается на старте.

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

Красное - менять нельзя, синее и белое - можно и нужно

Красное — менять нельзя, синее и белое — можно и нужно

Всего лишь 10 байт, ну 14, если считать с разрывами. Что можно накодить в 10 байтах? Разве что прыжок на существующую функцию. Глянул ROM на предмет интересностей и, представляете, нашёл целых две!

Вот эта функция принимает строку по UART в произвольную память:

Авторы просили не использовать эту функцию. А мы будем :)

Авторы просили не использовать эту функцию. А мы будем :)

Следующий код влезает в 10 байт и даже успевает передать «наружу» сигнал об успешном гличе по UART:

05 4A A4 call0  uart_tx_char
7C FB    movi.n a1, -1
FB A7    addi.n a10, a7, 0xF
45 5F A4 call0  UartRxString

После чего натравливает UartRxString на адрес в стеке. Здесь можно загрузить около 200 байт кода и сразу прыгнуть на него за счёт перетирания адреса возврата. Единственное ограничение — код не должен содержать байты 0xA и 0xD, поскольку это определяется как конец строки.

Я смог скомпилировать шеллкод с этими ограничениями, но мне это вообще не понравилось, поэтому я переделал атаку на вторую найденную лазейку. Это функция ets_unpack_flash_code_legacy:

Тут даже есть проверка-антиглич

Тут даже есть проверка-антиглич

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

1С 06    movi.n  a6, 0x10
69 81    s32i.n  a6, sp, 0x20 ; нужно, чтобы функция правильно отработала
3В 09    mov.n   a3, a9
45 DD A2 call0   0x40011464

Да, это тоже влезает в 10 байт. После прыжка шелл-код грузится как обычный ESP32 образ прямо с флешки и исполняется. Ну а из шелл-кода уже делаем что угодно. Например, читаем фьюзы, расшифровываем ключевой сектор МИГа... Это успех!

Тот самый сектор с ключами от прошивок MIG

Тот самый сектор с ключами от прошивок MIG

На основе извлечённых данных, исследователь hexkyz написал распаковщик файлов обновления MIG Switch, после чего и все остальные смогли изучить прошивку и убедиться, что это разработка команды Team Xecuter.

Доказательства? В прошивке нашлась, помимо уже знакомых кодов ошибок 0xBAD0000x, виртуальная машина, которую TX ранее использовали в других своих проектах:

hexkyz сделал декриптор и для неё, если что

hexkyz сделал декриптор и для неё, если что

6. Хороший тамада и конкурсы интересные

Посмотрели Espressif на достигнутые результаты и сказали — есть новые чипы, с новой фичей против DPA — псевдо-раундами:

Идея интересная — до и после настоящих раундов, чип шифрует всякий мусор

Идея интересная — до и после настоящих раундов, чип шифрует всякий мусор

Для старых же серий — рекомендуется использовать чипы с флешкой внутри, например, ESP32-H2, где наружных контактов интерфейса SPI не имеется, а значит, подключить эмулятор флешки уже не получится:

У S2 есть SPI-интерфейс, у H2 всё спрятано

У S2 есть SPI-интерфейс, у H2 всё спрятано

Но флешка же внутри у него осталась? Подержите мою шляпу!

Нанопроводки между чипом и флешкой внутри корпуса ESP32-H2

Нанопроводки между чипом и флешкой внутри корпуса ESP32-H2
Припаяно к vESPa обычным паяльником

Припаяно к vESPa обычным паяльником

Дальше всё по-накатанной, CPA на первый блок, код в заголовок, глич-атака на чтение:

Запустил простейший payload, выводящий в UART перевод строки

Запустил простейший payload, выводящий в UART перевод строки

Что до псевдо-раундов — на новейшем ESP32-C5 удалось обойти и их. Вся суть защиты заключается в том, что нельзя отличить псевдо-раунды от реальных, и, раз они добавлены случайно, нельзя определить момент, где записывать трассы.

Они же должны выглядеть так же, правда?

Нельзя ведь определить, правда?

Давайте вместе посмотрим на осциллограмму после включения псевдо-раундов на ESP32-C5:

Упс...

Упс...

Подозрительные двойные пики, не правда ли? Я предположил, что это места переключения псевдо/реальные данные. И действительно, если в каждой записи отфильтровать участок, оставив только эти подозрительные места (и чуток ближайшего окружения на всякий случай):

Отфильтровано и усреднено прямо внутри прошивки vESPa

Отфильтровано и усреднено прямо внутри прошивки vESPa

А потом натравить корреляционный анализ, используя известный ключ:

Есть попадание на втором двойном пике!

Есть попадание на втором двойном пике!

То получаем отчетливое совпадение с моделью! Дальше удалось и ключ восстановить, правда, брутфорсить пришлось не побайтно, а аж по 32 бита за раз из-за особенностей аппаратной реализации AES в чипе:

На моём ноуте - двое суток брутфорса на каждый ключ. На Nvidia 4090 - сутки

На моём ноуте — двое суток брутфорса на каждый ключ. На Nvidia 4090 — сутки

Выходит, защититься невозможно, все чипы подвержены тем или иным атакам? И да, и нет. В том же ESP32-C5 помимо псевдо-раундов есть по умолчанию включенные глич-детекторы, из-за которых помехами по питанию добиться исполнения кода не получится. Есть ещё способ глич-атаки электромагнитным импульсом, против которого глич-детекторы могут и не сработать, мы как раз сейчас пробуем её провести на базе проекта Chip’olino:

Высокое напряжение + катушка=нехилый импульс на чип

Высокое напряжение + катушка = нехилый импульс на чип

Помимо упомянутых малоинвазивных атак, есть метод «в лоб» — взять и прочитать те самые фьюзы, в которых хранится ключ шифрования. Конечно, для этого требуется дорогостоящее оборудование — как минимум, сканирующий электронный микроскоп:

Пока шла вся эта история, у нас в лабе такой появился

Пока шла вся эта история, у нас в лабе такой появился

И уже с его помощью можно фьюзы просто взять и «сфоткать»:

А это как раз многострадальный ESP32-S2, снято уже на нашем микроскопе

А это как раз многострадальный ESP32-S2, снято уже на нашем микроскопе

Конечно, вся эта процедура достаточно сложная и времязатратная. Но иногда другого варианта не существует — по слухам, именно так команда Team Xecuter и разработала свой MIG — ключи шифрования были извлечены из ROM микросхемы Lotus3 как раз с помощью этой техники.

P.S.

Все результаты исследований были координированно переданы в Espressif. Последние результаты по ESP32-C5 отражены в их недавней рекомендации с благодарностями Positive Technologies (уязвимости присвоен номер BDU:2026-05336).

Для интересующихся другими техническими подробностями, которые не вошли в статью - есть видео моего выступления с OFFZONE-2025:

Спрятал под спойлер

Также благодарю моих коллег из Positive Labs и всех, кто так или иначе помогал в исследовании и ревью данной статьи и помог сделать этот текст ещё лучше: Алексей Усанов (@Benonline), Павел Иванников (@Ivannikovp), Юрий Васин (@y0v1737), Егор Тишин (@Daterion) и Дмитрий Ватолин (@3Dvideo)

Автор: 15432

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js