
Названием «глухарь» птица обязана известной особенности токующего в брачный период самца утрачивать чуткость и бдительность, чем часто пользуются охотники.
Защита программного кода — извечная битва меча и щита. Одни люди стараются создать устойчивые ко взлому программные и аппаратные продукты, другие пытаются эти решения сломать. Но что происходит, когда команда, специализирующаяся на взломе, выпускает на рынок свой продукт? Можно ли в этих обстоятельствах разработать устройство, защиту которого невозможно обойти?
Сегодня мы посмотрим внутрь флеш-картриджа для Nintendo Switch под названием MIG Switch и раскроем тайну его происхождения! Ну и в качестве побочного квеста победим защиту одного из самых современных микроконтроллеров на рынке.
1. Есть только MIG, за него и держись
В общем, тема такая. Игровая консоль Nintendo Switch использует для хранения игр специальные носители — картриджи:

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

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


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

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

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

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

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

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

Три из них в виде 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) приводят к тому, что становится невозможным уследить за битами и провернуть фарш назад, не зная ключа.

Если шифрование реализовано «в лоб», то где-то внутри устройства в рамках самой первой операции 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 модель помогает математика, а именно, корреляция. Если есть зависимость между набором значений (предсказание/реальные замеры), то этот математический аппарат покажет, где именно эта зависимость наблюдается.
Конечно, это очень утрированный пример, но уже здесь видна проблема — прежде чем анализировать трассы и вычислять ключ, нужно сначала найти тот момент во времени, где происходит нужная операция.
В случае с общедоступными микроконтроллерами это просто — берём точно такой же (в идеале даже из той же партии) чип, прошиваем туда свой известный ключ, подставляем в модель те же данные, что подавали на чип при замерах и проверяем на практике:

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

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

Здесь на графике исследователи одновременно нашли как временные позиции, так и конкретные значения для ключа, но для этого потребовалось вычислять корреляцию для всех возможных моделей во всех предполагаемых временных позициях. Это может быть долго.
Примерную позицию, когда входные данные используются в микропроцессоре, можно найти и другими способами, например, SNR, NICV. Возвращаясь к игровым приставкам, такой подход использовала команда fail0verflow в своей статье про получение ключа южного моста PS4. Что они сделали:
записали много трасс питания с разными payload на входе
-
для каждого из 128 бит входных данных:
разделили трассы на две группы (с 0 и 1 в этой позиции)
вычислили среднее значение каждой из групп
посчитали абсолютную разницу между этими группами
-
построили график этих разниц для всех 128 бит и моментов времени

Получился график, показывающий в какие моменты времени CPU использует биты входных данных
Итак, подытоживая всю эту теорию, план атаки ESP32-S2 выходит следующий:
набрать «трасс» и соответствующих им данных
найти конкретные места, где происходят операции над входными данными
корреляцией найти, собственно, ключ
???
PROFIT

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

Здесь уже были реализованы запись осциллограмм питания, эмуляция SPI-флешки, математика для вычисления корреляций и утечек AES и даже программный PID-регулятор для поддержания постоянной температуры ESP32.
Конечно же, поддержки нужного мне ESP32-S2 в нём не было, но благодаря похожести всех ESP32 между собой, относительно малой кровью удалось адаптировать проект под себя, заодно прокачав его возможности:
Поднял частоту семплирования с 12 МГц аж до 48 МГц
Ускорил запись в 4 раза за счёт реорганизации замеров температуры
Сделал плату для ESP32-S2

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

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

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



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

На ней показаны моменты времени, где происходит утечка информации для различных моделей. Красным и оранжевым показаны модели утечек по 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) — модификатор для данных, который накладывается до и после шифрования:

Каждые 0x80 байт генерируется полностью новый Tweak, для этого через AES шифруется текущий адрес флешки. Внутри блока же Tweak каждый раз перед шифрованием следующих 0x10 байт умножается на α (полином x128+x7+x2+x+1, почти что умножение на 2).
Можно ли с помощью CPA узнать ключ KT? Проблема в том, что для корреляционной атаки нужно иметь возможность менять входные данные, что в этом случае сделать затруднительно — диапазон plaintext’а ограничен количеством блоков загрузчика (0-512), такой вариативности слишком мало для хоть какого-то результата.
Всё, что остаётся — атаковать каждый блок в 128 байт независимо. Обычно это не имеет смысла — этими атаками мы расшифровываем не саму прошивку, а только ESP32 загрузчик, который в подавляющем большинстве устройств стандартный и не представляет интереса. Но не в этом случае! Похоже, что в MIG загрузчик тоже самописный:

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


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 МГц в промежутках между измерениями, пока происходит чтение очередного блока данных:

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


В инструменте установлен 12-битный АЦП, значения хранятся в uint16_t — имеем целых 4 свободных бита, а значит, можно суммировать и усреднять трассы прямо в оперативке Pico, ещё до передачи на ПК (до 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 зависел от положения байта в ключе. Одни части ключа восстанавливались хорошо, другие наоборот, прятались до последнего.

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

Если хотя бы один байт из группы был неправильно угадан в 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() скопировать больше, чем нужно, перезаписать стек и тем самым выполнить загруженный код.

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

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

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

Что произошло — несмотря на два разных memcpy, SPI флешка имеет кэш, и при повторных чтениях этот самый кэш возвращает те же значения, не читая флешку повторно. Перед проверкой Secure Boot кэш намеренно инвалидируется, поэтому там заменить данные возможно, но как-то эксплуатировать это не получилось.
А это значит, остаётся лишь глич-атака. Кстати, это тоже считается и атакой по побочным каналам, и атакой по питанию ?. При разработке vESPa я заложил возможность проведения таких атак, причём не только с помощью транзистора, но и за счёт питания целевого чипа прямо от GPIO:

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

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

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

И мне действительно удалось подставить свой 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 образ прямо с флешки и исполняется. Ну а из шелл-кода уже делаем что угодно. Например, читаем фьюзы, расшифровываем ключевой сектор МИГа... Это успех!

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

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

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

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


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

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

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

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

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

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

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

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

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

Конечно, вся эта процедура достаточно сложная и времязатратная. Но иногда другого варианта не существует — по слухам, именно так команда Team Xecuter и разработала свой MIG — ключи шифрования были извлечены из ROM микросхемы Lotus3 как раз с помощью этой техники.
P.S.
Все результаты исследований были координированно переданы в Espressif. Последние результаты по ESP32-C5 отражены в их недавней рекомендации с благодарностями Positive Technologies.
Для интересующихся другими техническими подробностями, которые не вошли в статью - есть видео моего выступления с OFFZONE-2025:
Спрятал под спойлер
Также благодарю моих коллег из Positive Labs и всех, кто так или иначе помогал в исследовании и ревью данной статьи и помог сделать этот текст ещё лучше: Алексей Усанов (@Benonline), Павел Иванников (@Ivannikovp), Юрий Васин (@y0v1737), Егор Тишин (@Daterion) и Дмитрий Ватолин (@3Dvideo)