Сливаем дамп флешки STM32 стандартными инструментами
Как считать прошивку контроллера который не был залочен? До очень просто.
Подключаем плату через st-link и запускаем программу STM32CubeProgrammer или ST-LINK Utility.
Обе программы имеют вполне годный консольный help и документацию, в которой он, по сути, дублируется.
STM32CubeProgrammer. Сохраняет прошивку в bin, hex, srec
В port выбирается используемый интерфейс, далее идет адрес старта прошивки (0x08000000), размер прошивки (0x20000) и название файла куда будет сохранена прошивка.
ST-LINK Utility. Сохраняет прошивку в файл.
Ключ -c выполняет подключение, ключ -Dump переводит программу ST-LINK в режим чтения прошивки, далее идет адрес старта прошивки (0x08000000), размер прошивки (0x20000) и название файла куда будет сохранена прошивка.
Сравнить слитый файл с прошивкой на микроконтроллере можно через следующую команду в ST-LINK Utility. Выведет первый не совпавший адрес.
Конечно, удобнее сравнивать файл прошивки с дампом в GUI. Он выводит два окошка с файлами где красным подсвечены не совпавшие секции.
STM32 Shellcode: firmware dump over UART
In one of the previous articles, we talked about stack overflow and overwriting the stack pointer to the desired function address — Stack Buffer overflow in STM32.
RCE (remote code execution) is a complete attack that uses such an exploit. In order to perform it, one writes shellcode functions to the buffer and puts the shellcode address to the stack pointer. As a result, the code that is written to the buffer gets executed.
And there the fun begins. To be honest, when preparing the article, we didn’t have an objective to write a complete shellcode solution, and that’s why we selected the buffer size randomly. Randomly small 🙂
O_o buffer size is 20 bytes plus some stack space for local variables. Later, we ended up with 32 bytes total (to be the same as that 32 x “.” from python script)
What can be packed into 32 bytes?
Challenge accepted, let’s do some shellcode. What is gonna be the purpose of shellcode? Let’s try to dump the whole chip firmware. We have USB and UART interfaces initialized. The latter is easier to work with, so let’s stick with UART as a channel to be used for dumping the firmware.
Algorithm for working with UART is the following:
- wait for the flag UART_FLAG_TXE (Transmit Data Register Empty)
- write the next byte of the firmware to the register UART->DR (data register)
- increment the pointer to the next byte of the firmware
- return to step one
In addition, we need to ensure the operability of our code:
- need to allocate some space on the stack (decrement stack pointer)
- write the valid address to the link register
In order to avoid checking the flag UART_FLAG_TXE in a loop, we can call HAL_Delay(1). The UART will work at 115200 kbps and 1-millisecond delay is just enough.
We could’ve looked up and used the function HAL_UART_Transmit() but then our shellcode will be dependant on the location of a certain function in the chip’s memory. We can do the same to replace HAL_Delay() with a waiting loop.
If we work directly with peripheral registers then our code will work independently from whether certain functions are present in the firmware or not. With literally 2–3 register writes we can enable UART and start transmitting data.
So, the final version of the shellcode will look something like this:
The code is easier to write code using C (even with assembly insertions), compile it and check the disassembly result. Then you can tweak it the way you want:
After packing the shellcode into the python script and some testing, we get the following code:
vanbwodonk / read-stm32-firnware.md
OpenOCD (Open On-Chip Debugger) is open-source software that interfaces with a hardware debugger’s JTAG port. OpenOCD provides debugging and in-system programming for embedded target devices. OpenOCD provides the ability to flash NAND and NOR FLASH memory devices that are attached to the processor on the target system. Flash programming is supported for external CFI compatible flashes (Intel and AMD/Spansion command set) and several internal flashes (LPC2000, AT91SAM7, STR7x, STR9x, LM3 and STM32x).
OpenOCD was originally developed by Dominic Rath at the University of Applied Sciences Augsburg. The OpenOCD source code is now available through the GNU General Public License (GPL).
Считывание защищенной прошивки из флеш-памяти STM32F1xx с использованием ChipWhisperer
В предыдущей статье мы разбирались с Vcc-glitch-атаками при помощи ChipWhisperer. Нашей дальнейшей целью стало поэтапное изучение процесса считывания защищенной прошивки микроконтроллеров. С помощью подобных атак злоумышленник может получить доступ ко всем паролям устройства и программным алгоритмам. Яркий пример – взлом аппаратного криптокошелька Ledger Nano S с платой МК STM32F042 при помощи Vcc-glitch-атак.
Интересно? Давайте смотреть под кат.
О возможности считывания защищенной прошивки мы узнали из статьи, в которой приведены результаты выполнения Vcc-glitch-атаки – обхода байта защиты RDP через масочный загрузчик (bootloader) для нескольких микроконтроллеров (далее – МК). Также рекомендуем к прочтению статью о взломе ESP32.
Теоретической базой исследования послужило руководство успешного считывания защищенной прошивки для LPC1114 через масочный загрузчик с использованием ChipWhisperer.
Так же, как и в первой статье, мы решили проводить эксперименты на плате МК STM32F103RBT6:
Возможность записи данных в сектор флеш-памяти и RAM-памяти или их чтения, а также выполнения других действий с памятью МК определяется значением байта защиты (для STM32 – RDP). Для разных МК значения и назначение байтов защиты, а также алгоритм их проверки различается.
Аппаратная настройка
Приступим к проведению эксперимента. Для начала необходимо подключить ChipWhisperer к МК согласно рисунку:
Схема подключения ChipWhisperer к STM32 для считывания защищенной прошивки через масочный загрузчик
На схеме зачеркнуты элементы, которые следует удалить из платы STM32F103RBT6 (в отличие от стандартного подключения МК). Стрелками обозначены места подключения ChipWhisperer, а подписями – его пины.
Наличие внешнего кварца, представленного на схеме, не обязательно, поскольку при работе с масочным загрузчиком плата МК STM32F103RBT6 использует внутренний CLOCK с частотой 24 МГц, поэтому синхронизация между ChipWhisperer и МК отсутствует.
Перейдем к настройке ChipWhisperer. Как уже было отмечено выше, рекомендуемая частота работы ChipWhisperer – 24 МГц (или другое кратное значение). Чем выше кратность этой частоты, тем точнее можно настроить момент атаки. Из-за отсутствия синхронизации подбор параметра scope.glitch.offset необязателен, ему можно присвоить любое значение.
Параметры scope.glitch.repeat и scope.glitch.width необходимо подбирать в зависимости от установленной частоты ChipWhisperer. При большом значении частоты все кратковременные импульсы, количество которых устанавливается при помощи scope.glitch.repeat, сливаются в один длительный импульс. Поэтому можно подбирать значение параметра scope.glitch.width, а scope.glitch.repeat зафиксировать, либо наоборот. Мы обнаружили, что оптимальная длительность импульса должна составлять около 80 нс (определяется как ширина импульса на его полувысоте).
Осталось подобрать значение параметра scope.glitch.ext_offset.
Подбор scope.glitch.ext_offset
Сначала необходимо выбрать момент атаки. Согласно схеме, представленной в документе компании STM, проверка значения байта защиты выполняется после получения запроса на чтение данных сектора флеш-памяти:
Алгоритм ответа на запрос о чтении данных сектора флеш-памяти
Чтобы удостовериться в верности такой схемы проверки, мы считали исполняемый код загрузчика подобного МК без защиты RDP через ST-Link. На рисунках ниже показаны части алгоритма обработки команды Read Memory command.
Общий вид обработки команды чтения памяти (хорошо видны вызов функции проверки RDP и посылка NACK в случае неудачной проверки)
Тело функции проверки RDP
Обратим внимание на тело функции проверки RDP: видно, что происходит чтение регистра по адресу 0x40022000 + 0x1C , логический сдвиг на 30 разрядов и ветвление. Из документации PM0075 Programming manual (STM32F10xxx Flash memory microcontrollers) становится понятно, что 0x40022000 – это базовый адрес контроллера flash memory, а 0x1C – это смещение регистра FLASH_OBR, в котором нас интересует второй бит RDPRT: Read protection, в котором содержится статус защиты RDP.
Необходимый момент проведения атаки – отработка инструкции LDR (загрузки из памяти). Эта инструкция располагается между запросом на чтение прошивки (отправление байта 0x11 с контрольной суммой 0xEE ) и ответом ACK / NOACK МК по UART. Для того чтобы визуально зафиксировать этот момент, необходимо подключить осциллограф к UART1_RX (пин PA10) и UART1_TX (пин PA9), а затем отслеживать изменение напряжения по UART1. В результате осциллограмма атаки по питанию с подобранным значением scope.glitch.ext_offset должна выглядеть примерно так:
Выбор момента проведения атаки
Скрипт считывания прошивки
Теперь необходимо указать момент срабатывания триггера CW_TRIG в коде Python с целью перехвата момента передачи контрольной суммы по UART1_RX. У ChipWhisperer есть библиотека для общения с масочным загрузчиком МК STM32. В штатном режиме эта библиотека используется для загрузки на МК прошивок из руководств при помощи класса class STM32FSerial(object) , расположенного в файле programmer_stm32fserial.py по пути software/chipwhisperer/hardware/naeusb/ . Для активации срабатывания триггера необходимо скопировать этот класс в главный исполняемый скрипт, чтобы метод класса CmdGeneric(self, cmd) стал глобально доступным, и добавить команду scope.arm() до передачи контрольной суммы (0xEE) запроса на считывание сектора памяти. Итоговый класс приведен в спойлере ниже.
Следует обратить внимание на то, что масочный загрузчик STM32F1хх позволяет считывать за один запрос не более 256 байт прошивки из указанного сектора флеш-памяти. Поэтому при считывании всей прошивки МК необходимо в ходе Vcc-glitch-атаки выполнить несколько запросов на чтение. Затем полученные 256 байт следует разбить на восемь 32-байтных массивов и сформировать из них файл формата HEX.
Настройка параметров ChipWhisperer завершена. Итоговый скрипт на считывание прошивки выглядит следующим образом:
Все закомментированные сообщения print() после строчки except Exception as помогают отслеживать состояние МК при поиске оптимальных параметров glitch-импульса. Для отслеживания конкретного состояния МК достаточно раскомментировать необходимое сообщение print() .
Результаты считывания
На видео продемонстрирована загрузка прошивки на МК через программатор ST-LINK, перевод RDP в состояние защиты и последующее считывание прошивки:
Успешному проведению Vcc-glitch-атаки могут помешать следующие ошибки:
• считывание неверного сектора памяти;
• самопроизвольное удаление прошивки.
Избежать подобных ошибок поможет точный выбор момента атаки путем увеличения частоты работы ChipWhisperer.
После разработки и отладки алгоритма считывания защищенной прошивки мы провели тестовое считывание прошивки программатора ST-LINK-V2.1, который работает на МК STM32F103CBT6. Считанную прошивку мы зашили на «чистый» МК STM32F103CBT6 и установили его вместо заводского. В результате ST-LINK-V2.1 с замененным МК работал в нормальном режиме, будто подмены не было.
Также мы попробовали провести серию атак на STM32F303RCT7. Этот МК в ходе атаки вел себя идентично STM32F103RBT6, но ответ на запрос чтения памяти содержал байт, равный 0х00, что не совпадало с ожидаемым нами результатом. Причина такой неудачи заключалась в более сложном и развитом принципе организации защиты этих МК.
В МК STM32F1xx существует два состояния защиты: защита выключена (Level 0) и включена (Level 1). В старших моделях предусмотрено три состояния защиты: защита отключена (Level 0, RDP = 0x55AA), защита флеш- и SRAM-памяти (Level 2, RDP = 0x33CC) и защита только флеш-памяти (Level 1, RDP принимает любые значения, отличные от 0x55AA и 0x33CC). Поскольку Level 1 может принимать множество значений RDP, установить Level 0 достаточно тяжело. С другой стороны, существует возможность понижения уровня защиты с Level 2 на Level 1 сбиванием одного бита в байте RDP (показано на рисунке ниже), что открывает доступ к SRAM-памяти.
Сравнение значений RDP для разных уровней защиты прошивки
Остается только понять, как этим может воспользоваться злоумышленник. Например, с помощью метода CBS (Cold-Boot Stepping), описанного в этой статье. Этот метод основан на поэтапном снимке состояния SRAM-памяти (периодичность выполнения каждого снимка была в районе микросекунды) после загрузки МК с целью получения ключей шифрования, скрытых паролей или любой другой ценной информации. Авторы предполагают, что метод CBS сработает на всех сериях МК STM32.
Выводы
Подведем итоги наших экспериментов. Выполнение Vcc-glitch-атаки с использованием данных, полученных в результате предыдущего исследования (о котором можно прочитать здесь), заняло у нас несколько дней. А значит, научиться проводить подобные атаки достаточно легко.
Vcc-glitch-атаки опасны тем, что от них сложно защититься. Для уменьшения вероятности успешного проведения подобных атак предлагается использовать МК с более высоким уровнем защиты.

Raccoon Security – специальная команда экспертов НТЦ «Вулкан» в области практической информационной безопасности, криптографии, схемотехники, обратной разработки и создания низкоуровневого программного обеспечения.
