引言
故障注入是一种用于评估设备安全性的技术,其原理是故意向硬件组件引入故障或错误,以绕过诸如调试保护或密码认证等安全功能。这些注入操作应在特定时刻进行,并持续受控的时间,以便破坏内存或跳过指令。实现故障注入的方法包括:
- 硬件设备
- 软件方法
- 结合硬件和软件的混合方法
这种攻击方式在支付卡或内容保护等敏感领域得到了广泛应用,并且在近几年变得易于实施。
让我们考虑以下场景:密码检查是通过一个返回 0 或 1 的 if 语句来实现的。在第一种情况下,用户被拒绝访问;而当返回 1 时,用户则可以登录。以下是故障注入时可能出现的情况:
- 可以注入特定值 1,从而绕过身份验证。
- 产生随机字节(这种情况更有可能发生)。根据实现方式的不同,一些保护措施仍可能被绕过。
- 跳过指令,例如 if 语句本身。
一些寄存器会被破坏,设备可能会以非标准状态运行。过去,故障注入攻击曾被用来突破安全措施,例如 Xbox360 的复位故障攻击、STM32 的闪存读取、MediaTek MT8163V3 的安全启动绕过,或者最近对 DJI 无人机的成功故障注入攻击。
时钟故障注入
时钟故障注入是一种针对配备外部时钟的设备的攻击方式。通过在正常时钟脉冲周期之间注入时钟故障脉冲,且在极其恰当的时刻进行,可以在算术逻辑单元(ALU)使用的原始时钟信号中,两个合法的时钟边沿之间移除或插入一个边沿。当这种情况发生时,可能会触发意外行为,例如处理器跳过一条指令,可能正是负责安全检查的指令。
ChipWhisperer 代码库中的故障注入课程给出了一个关于对 Atmel AVRM ATMega 328P进行此类攻击的非常清晰的示例:
该系统并非从 FLASH 中加载每条指令并执行整个过程,而是采用流水线来加快执行速度。这意味着一条指令正在被解码,而下一条指令正在被获取,如下图所示:
但如果对时钟进行修改,可能会出现系统没有足够时间来实际执行指令的情况。考虑以下情况,其中“Execute #1”实际上被跳过了。在系统实际执行该指令之前,另一个时钟边沿就到来了,导致微控制器开始执行下一条指令。
光学故障注入
光学故障注入领域实际上包含了多种不同的技术,从使用X射线的昂贵设备来定位闪存单元上的单个比特,到使用相机闪光灯的廉价方法,通过引发随机故障来恢复AES密钥。
在硬件安全评估中,用于安全评估的主要方法是激光故障注入(LFI)。使用脉冲激光来物理操纵和扭曲数据,从而在运行中的设备中引发故障。
主要目标是找到芯片上故障注入成功率最高的区域。在进行此类攻击时,需要考虑X、Y和Z坐标,以确定最佳的注入空间位置。
这种技术的标准设备包括:
- 控制设备
- 电动平台
- 激光源
- 物镜
然而,这种技术也有一些副作用,例如组装设备的成本较高,以及需要将芯片从电路板上移除,这可能会损坏设备。这是由于故障注入是从芯片的背面进行的,尽管已经开发出了从正面进行注入的新技术。
总的来说,光学故障注入技术提供了高精度和可重复性,但成本较高。
电磁故障注入
电磁(EM)辐射对模拟和数字模块均有影响,尽管它们的物理特性有所不同。为了改变数字模块(这些模块是时钟驱动的),可以利用短暂的电磁脉冲在特定的时钟周期内注入故障,这需要借助高压脉冲发生器和带有铁氧体磁芯的线圈(注入探针)来实现。这种注入方法在成本和精确度之间取得了良好的平衡。
与激光故障注入不同,电磁故障注入无需将芯片从电路板上拆焊,且需仅在两个维度上寻找合适的空间位置。此外,与时钟或电压故障注入不同,电磁故障注入无需在芯片上焊接或连接导线。
下图展示了一款PicoEMP设备正在对树莓派进行攻击:
电磁故障注入是一种对攻击者而言较为实用的技术。它具备良好的精准度,其成本较电压或时钟故障注入略高,但远低于激光故障注入。此外,该技术无需采用拆焊或其他侵入性方法,因此能够保持系统级芯片(SoC)的完整性。针对此类攻击,可采用的方法是运行一个无限循环,同时利用网格扫描CPU表面。首先对一个点进行多次测试,随后将探针移动到另一个点(通常相距约1毫米),依此类推,以完成对整个SoC的扫描。此外,还可以使用不同尺寸的探针。
电磁故障注入技术的一个应用实例是Riscure的相关工作,通过故障注入干扰ESP32 CPU,从而在启动时绕过安全启动摘要验证。
电压故障注入
电压故障注入的目标是精确控制微控制器的电源,且控制时间足够短。如果控制时间过长,将导致芯片复位;而正确地进行控制,则可使其进入一种未定义状态,从而引发不确定的行为。这意味着需要找到合适的故障脉冲宽度以及恰当的触发时机。
这种方法面临的一个主要问题是,存在一些旨在维持电压稳定的组件,例如去耦电容器,它们可能会干扰故障注入的效果,因此可能需要将这些电容器拆焊。此外,将注入装置尽可能靠近目标设备进行焊接,也有助于提高故障注入的效果。
Creating a simple glitch
为了详细介绍的不同目标上进行电压故障注入,我们使用了 ChipWhisperer Lite。
短路是通过一个 MOSFET 实现的,MOSFET 是一种晶体管,其主要功能是控制导电性,即根据其栅极所施加的电压量来控制电流在其源极和漏极之间流动的程度。它可以被建模为一个简单的受电控的开关。在此,我们将它并联在电源轨道上,以便短暂地将 VCC 与地短路。
首先,我们进行一个没有任何限制条件的简单故障注入。为此,我们使用了一块 Arduino Uno 开发板,并将系统级芯片(SoC)放置在面包板上。这样,我们可以非常方便地控制微控制器的地线,且不会受到任何干扰电容的影响。在其他情况下,为了确保我们的故障注入尽可能有效,我们可能需要移除连接到 Vcore 和复位线路上的去耦电容器。这可以通过使用标准的电烙铁和一些耐心来完成。
需要重新连接的引脚至少包括PIN 2、3(RX / TX)、7(VCC)、9和10(Clock)。这些引脚负责基本的电源供应和操作以及与微控制器的串行通信。如果需要将代码上传到Arduino,还需要连接Pin 1(Reset)。使用示波器来检查故障是否发生,其探头连接到Pin 7。下面是一个简单的示意图,以便更好地理解这些连接关系:
使用Arduino SDK编写并上传到Uno板上的是一个相当简单的C++脚本。
void setup() {
Serial.begin(115200);
}
void loop() {
int ctr = 0;
for(int i=0; i<2; i++){
for(int j=0; j<2; j++){
delay(100);
Serial.print("i: ");
Serial.print(i);
Serial.print(" j:");
Serial.print(j);
Serial.print(" ctr:");
Serial.println(ctr);
ctr++;
}
}
}
import chipwhisperer as cw
cw.set_all_log_levels(cw.logging.CRITICAL)
SCOPETYPE = 'OPENADC'
PLATFORM = 'CWLITEXMEGA'
scope = cw.scope()
# We adjust the clock to fit with the ATMega 328p frequency.
scope.clock.clkgen_freq = 8E6
# Set clock to internal chipwhisperer clock
scope.glitch.clk_src = "clkgen"
#"enable_only" - insert a glitch for an entire clock cycle based on the clock of the CW (here at 8MHz so 0,125 micro seconds)
scope.glitch.output = "enable_only"
# Enable Low power and High power transistors.
scope.io.glitch_lp = True
scope.io.glitch_hp = True
# LP provides a faster response, so sharper glitches. HP can provide better results for targets that have strong power supplies or decoupling capacitors that ca not be removed.
scope.io.vglitch_reset() #it simply sets scope.io.glitch_hp/lp to False, waits delay seconds, then returns to their original settings. In short, reset the glitching module.
# How many times the glitch is repeated
scope.glitch.repeat = 1
# Send the glitch
scope.glitch.manual_trigger()
scope.dis()
发送了一个持续整个时钟周期的故障信号,并将时钟频率调整为与ATMega 328p的时钟频率一致。其中,“repeat”参数用于控制故障重复的次数。起初,我们将该参数值设置得非常低,随后逐步增加(我们采用每次增加50次重复,但为了降低损坏设备的风险,也可以使用更小的增量值),以便观察Arduino在不同设置下的行为表现。
单片机的重复次数较多。
在示波器上,我们可以看到故障信号被发送出去:
我们尝试在故障信号的起始点和结束点放置光标,并将时钟频率设置为8MHz,因此一个时钟周期为1/(8×10⁶)=1.25×10⁻⁷秒。故障信号重复了500次,所以1.25×10⁻⁷×500=0.0000625秒=62.5微秒。测量结果显示为63.6微秒,因此考虑到测量误差,一切工作正常。
如您所见,由于故障信号过于强烈,Arduino Uno发生了重启。在故障注入中,正常行为与设备重启之间的界限非常微妙,在此区间内会发生一些不寻常的事情。经过一番尝试后,我们发现当故障信号重复380次时,设备开始重启。因此,我们将故障信号的重复次数从1次增加到380次,并检查Arduino的行为表现:
for i in range(380):
scope.io.vglitch_reset(0.5)
scope.glitch.repeat = i
scope.glitch.manual_trigger()
我们记录了 minicom 会话并启动了脚本。接下来,我们来分析其内容:
i: 0 j:0 ctr:0
i: 0 j:1 ctr:1
i: 1 j:0 ctr:2
i: 1 j:1 ctr:3
i: 0 j:0 ctr:0
i: 0 j:1 ctr:1
i: 1 j:0 ctr:2
i: 1 j:1 ctr:3
i: 0 j:0 ctr:0
i: 0 j:1 ctr:1
i: 1 j:0 ctr:2
i: 0 j:0 ctr:0
i: 0 j:1 ctr:1
i: 0 j:0 ctr:0
i: 0 j:1 ctr:1
i: 0 j:0 ctr:0
i: 0 j:1 ctr:1
i: 0 j:0 ctr:0
i: 0 j:1 ctr:1
i: 0 j:0 ctr:0
i: 0 j:1 ctr:1
如您所见,我们成功地跳过了一些指令。起初,计数器从0跳到3,然后跳到2,最后不再超过1。从示波器上可以看到,在脚本执行过程中,故障信号的重复次数越来越多:
示波器上显示了一个较大的故障信号。
如果我们要修改某些值,可以清楚地看到,此处的故障信号过于强烈,因为我们跳过了一些循环迭代。我们修改了脚本,以发送更窄的故障信号并减少重复次数。
import chipwhisperer as cw
[...]
scope.clock.clkgen_freq =192E6 # Maximum frequency of the internal clock of the CW
[...]
# insert a glitch for a portionof a clock cycle
scope.glitch.output = "glitch_only"
[...]
gc = cw.GlitchController(groups=["success", "reset", "normal"], parameters=["width", "repeat"])
gc.set_range("width", 0, 35)
gc.set_range("repeat", 1, 35)
# The steps could be reduced to be more precise
gc.set_global_step(1)
for glitch_setting in gc.glitch_values():
scope.glitch.width = glitch_setting[0]
scope.glitch.repeat = glitch_setting[1]
print(f"{scope.glitch.width} {scope.glitch.repeat}")
scope.glitch.manual_trigger()
scope.io.vglitch_reset()
scope.dis()
我们在脚本执行过程中成功地修改了某些值!
i: 0 j:-16777215 ctr:-16777215
[…]
i: -8023668 j:1 ctr:-805831672
i: 1 j:0 ctr:-805831671
i: 1 j:1 ctr:-805831670
[...]
Glitching a logging prompt
String PASSWORD = "passw";
boolcheckPass(String buffer) {
for (int i = 0; i < PASSWORD.length(); i++) {
if (buffer[i] != PASSWORD[i]) {
returnfalse;
}
}
returntrue;
}
voidsetup() {
Serial.begin(115200);
Serial.println("Password:");
}
voidloop() {
if (Serial.available() > 0) {
char pass[PASSWORD.length()];
Serial.readBytesUntil('n', pass, PASSWORD.length());
bool correct = checkPass(pass);
if (correct) {
Serial.println("Logged in!");
Serial.flush();
exit(0);
} else {
Serial.println("Incorrect password.");
Serial.println("Password:");
}
}
}
import chipwhisperer as cw
import time
import serial
import os
cw.set_all_log_levels(cw.logging.CRITICAL)
SCOPETYPE = 'OPENADC'
PLATFORM = 'CWLITEXMEGA'
scope = cw.scope()
scope.clock.clkgen_freq = 192E6
scope.glitch.clk_src = "clkgen"
scope.glitch.output = "glitch_only"
scope.io.glitch_lp = True
scope.io.glitch_hp = True
gc = cw.GlitchController(groups=["success", "reset", "normal"], parameters=["width", "repeat"])
gc.set_global_step(0.4)
gc.set_range("width", 1, 45)
gc.set_range("repeat", 1, 50)
gc.set_step("repeat", 1)
for glitch_setting in gc.glitch_values():
# Try to connect to the arduino uno using the serial connection:
try:
with serial.Serial("/dev/ttyACM1", 115200, timeout=1) as ser:
scope.glitch.width = glitch_setting[0]
scope.glitch.repeat = glitch_setting[1]
print(f"Width: {scope.glitch.width}, repeat: {scope.glitch.repeat}")
# Send the glitch and a wrong password
scope.glitch.manual_trigger()
ser.write(b'tatat')
scope.io.vglitch_reset()
# If the serial connection breaks, use uhubctl to poweroff / poweron the usb port on the USB hub where the Arduino Uno is plugged
except Exception as e:
os.system('/usr/sbin/uhubctl -S -p 2 -a cycle > /dev/null 2>&1')
time.sleep(5)
pass
scope.dis()
原文始发于微信公众号(安全脉脉):故障注入详解(一)
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论