
Экстрактор прошивки для микропроцессоров CH55x
Экстрактор прошивки CH55x используется для чтения прошивки из интегральных схем CH55x. В устройствах нет встроенной поддержки для прямого чтения прошивки через загрузчик. Однако существует функция проверки содержимого прошивки по 8 байт за раз с предоставленными данными. Эта функция уязвима для классической атаки по времени, что и используется здесь для извлечения прошивки из этих устройств. Загрузчик можно получить через USB или UART. Этот экстрактор прошивки работает только через UART, потому что более высокая задержка USB сделала бы эту атаку сложной. Протестированные чипы: CH552 и CH554, версии загрузчика 2.4 и 2.5. Аппаратное решение для экстрактора прошивки основано на STM32 Blue Pill, поскольку они доступны, дешевы и обладают необходимой производительностью.
Загрузчик ранее был прочитан из устройства, а его протокол связи был восстановлен. Ниже приведена приблизительно правильная команда проверки и функция проверки, используемая в загрузчике. Существуют некоторые ограничения, которые требуют, чтобы длина была кратна 8, адрес был выровнен по границе 8 байт, адрес был меньше 0x3800 и чтобы не было предыдущих ошибок проверки. Последнее ограничение означает, что после каждой неудачной проверки требуется перезапуск CH55x.
Мы видим, что функция проверки возвращается немедленно, если один байт проверки неверен. Это означает, что чем больше байтов верны, тем дольше будет выполняться функция проверки. Это классический пример атаки по времени, которую можно использовать.
unsigned char verifycmd[] = {
// 0x57, 0xab, // UART magic not included to verify function
0xa6, // Verify command
5 + len, // Constant 5 plus length of data to verify
0, // Unused
addr_low, // Low byte of address
addr_high, // High byte of address
0, 0, 0, // Unused
0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, // Data to verify against
checksum
}
unsigned char verify(unsignec char *cmdbuffer)
{
static char prev_verify_error;
unsigned char len = cmdbuffer[1]-5
unsigned short addr = cmdbuffer[3] + cmdbuffer[4] << 8;
if (len & 0x07 || addr & 0x07 || addr > 0x3800 || prev_verify_error) {
return 0xfe;
}
for (int i=0; i < len; i++) {
// Key can be set through bootloader, and CBYTE[] means code memory
if(key[i & 0x07] ^ cmdbuffer[8+i] ^ CBYTE[addr+i]) {
prev_verify_error = 1;
return 0xf5;
}
}
return 0;
}
Путем проб и ошибок мы обнаружили, что каждый правильный байт увеличивает время выполнения функции проверки примерно на 4,2 мкс. Скорость передачи, используемая загрузчиком для связи UART, равна 57600 (независимо от того, что вы прочитаете в других местах), что означает, что передача одного бита занимает примерно 17,4 мкс. Соотношение между этими двумя временами важно, потому что, похоже, ответ отправляется с джиттером также около 17 мкс. Затем мы пытаемся различить ответы, время отклика которых отличается на 4,2 мкс от функции проверки, но может отличаться до 17 мкс из-за джиттера UART (тактовой синхронизации). Это кажется сложной задачей, но её можно решить с помощью статистических методов.
Мы можем определить, был ли байт правильным или нет, попробовав проверить его несколько раз и записывая результаты. Скажем, например, что минимально возможное время получения ответа, если первый байт неверен, составляет 30 мкс. Тогда с учетом джиттера UART мы можем ожидать максимальное время ответа 30 мкс + 17,4 мкс = 47,4 мкс для неверного первого байта. В то же время, верный первый байт и неверный второй байт сдвинут этот «диапазон» до 34,2 мкс – 51,6 мкс. Добавив некоторые запасы, мы можем заключить, что первый символ был неверным, если время ответа меньше ~33 мкс. Аналогично, мы можем сказать, что первый символ был верным, если время ответа больше 48 мкс. Это основа атаки по времени, используемой для извлечения прошивки.
Это не полностью автоматический инструмент для извлечения прошивки — потребуется модификация исходного кода и перекомпиляция (с использованием VS Code и PlatformIO). Основная причина в том, что точные временные характеристики для каждого байта различаются в зависимости от конфигурации и требуют настройки. Процесс настройки можно было бы автоматизировать, но это не было целью данного проекта. Основная настройка выполняется через переменную prober_limits. Например, prober_limits[0] содержит ограничения для байта 0 из 8 проверяемых байтов. Если время ответа меньше .invalid_under_time, мы знаем, что байт был неверным. Если оно больше .valid_over_time, мы знаем, что байт правильный. Также есть .min_delta, позволяющий перейти к следующему байту до того, как мы точно узнаем, правильный байт или нет.
struct ProberByteLimits prober_limits[8] = {
{
// Byte 0
.invalid_under_time = 33,
.valid_over_time = 50,
.min_delta = 30
}, // ...
Чтобы найти подходящие значения, рекомендуется установить .invalid_under_time в 0, .valid_over_time в, например, 100, а .min_delta можно оставить равным 30. Это означает, что пробник не сможет найти первый правильный байт, но прогресс будет выводиться на UART хост-ПК. Вы увидите что-то вроде:
[0x0000]=0x01? min=31 max=47 tries=63
min=31 max=48 tries=127
min=31 max=48 tries=191
min=30 max=48 tries=255
min=30 max=48 tries=319
При условии, что первый проверяемый символ неверен, мы можем использовать эти минимальные/максимальные значения со смещением 1 или 2, чтобы определить подходящие ограничения. Например, выше мы могли бы установить .invalid_under_time в 33 и .valid_over_time в 50. Затем пробник будет пробовать разные значения, пока не найдет правильное, что будет выглядеть примерно так:
...
[0x0000]=0x7d? min=31 max=39 tries= 7
[0x0000]=0x02? min=40 max=56 tries= 7
[0x0001]=0x01? min=35 max=52 tries=34
Мы видим, что последняя попытка по адресу 0x0000 имела максимальное время ответа 56 мкс, что означает, что это был правильный байт. Затем пробник переходит к следующему байту и продолжает процесс. Обратите внимание, что все 8 ограничений для байтов должны быть настроены отдельно, но после настройки они будут работать для всей памяти. Когда будут найдены 8 правильных байтов, они будут выведены в формате ihex:
:0800000002002932ffffffff9f
Запись вывода UART в текстовый файл позволяет использовать grep для ^:, чтобы извлечь полное содержимое памяти в формате ihex.
Пример схемы для извлечения прошивки показан ниже. Два транзистора позволяют отключать питание CH55x от Blue Pill. Это рекомендуется по сравнению с использованием только программного сброса, поскольку программный сброс работает только когда CH55x находится в режиме загрузчика (а загрузчик имеет тайм-аут, после которого запускает код приложения). Резисторы на UART к CH55x включены, потому что есть подозрение, что внутренние подтягивающие резисторы на CH55x могут запитывать его от UART даже при отключенном питании. Резистор 10 кОм между V33 и P3.6 необходим для перевода CH55x в режим загрузчика. На Blue Pill вывод PA11 подключен к выводу RX UART, чтобы иметь возможность измерять время ответа с помощью таймера 1.

С помощью настройки и использования этого инструмента можно извлечь прошивку устройств CH55X. Процесс извлечения не быстрый, но позволяет извлечь 14 кб за один-два дня. Это немного зависит от настройки и того, насколько хорошо используемая таблица частот соответствует реальному ассемблеру прошивки. К сожалению, исходный код немного запутан. Я создал этот инструмент, потому что мне понадобилась прошивка от устройства CH554, и теперь, когда я её получил, нет реальной причины дорабатывать сам инструмент. Тем не менее, он должен оказаться полезным, если кому-то понадобится извлечь прошивку из устройств CH55x.