
Названием «глухарь» птица обязана известной особенности токующего в брачный период самца утрачивать чуткость и бдительность, чем часто пользуются охотники.
Защита программного кода — извечная битва меча и щита. Одни люди стараются создать устойчивые ко взлому программные и аппаратные продукты, другие пытаются эти решения сломать. Но что происходит, когда команда, специализирующаяся на взломе, выпускает на рынок свой продукт? Можно ли в этих обстоятельствах разработать устройство, защиту которого невозможно обойти?
Сегодня мы посмотрим внутрь флеш-картриджа для 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 (уязвимости присвоен номер BDU:2026-05336).
Для интересующихся другими техническими подробностями, которые не вошли в статью - есть видео моего выступления с OFFZONE-2025:
Спрятал под спойлер
Также благодарю моих коллег из Positive Labs и всех, кто так или иначе помогал в исследовании и ревью данной статьи и помог сделать этот текст ещё лучше: Алексей Усанов (@Benonline), Павел Иванников (@Ivannikovp), Юрий Васин (@y0v1737), Егор Тишин (@Daterion) и Дмитрий Ватолин (@3Dvideo)
Автор: 15432
