那是一个温暖的夏末下午,一位老客户打来电话。他们需要有人帮助他们摆脱困境,而且要快。该客户端构建在世界各地的办公室中可以找到的嵌入式设备。他们最新的产品具有所有正确的安全特性,考虑到他们的硬件限制,这是最好的可能了。这些设备由运行在微控制器上的固件驱动,提供健壮的无线通信;我就知道这是我的工作。
客户的工程师试图在他们的固件中构建正确的安全特性:一个通常不能更新的引导加载程序,其中包含一个静态信任根,以及加密签名的二进制固件更新。他们甚至加固了引导加载程序,以防御任何直接攻击固件更新协议的人——针对像我这样的人的对策。客户对设计的安全性感到自豪;毕竟,当时的大多数消费产品几乎没有解决固件读取保护问题。
一个倒霉的胖手指引发了当前的危机:客户端不小心删除了签署新固件更新所需的私钥。他们有一些令人兴奋的新特性要发布,以及通常的可靠性改进。他们的客户越来越不耐烦,但当被问及发布日期时,我的客户不得不拖延。他们怎么能想出一个有意义的约会呢?他们失去了签署新固件版本的能力。
考虑到这些设备在现场的绝对数量,以及每台设备的成本,替换程序是绝对最后的选择。经济损失将是巨大的,而策划这样一个项目将是令人难以置信的艰巨。逆向工程的尝试是在必须召回之前的最后一次“万福玛利亚”努力。
任务似乎很简单:找到一种方法,将新的静态信任根补丁到引导加载程序中(一个哲学问题:是这样吗?静态?),从而使客户端能够使用新密钥对固件更新进行签名。由于引导加载程序已被加固(我的客户声称如此),直接攻击固件更新程序是不可能的。
该设备具有多个串口,一旦客户机的应用程序启动,这些串口就会公开各种复杂的协议。设计中的主微控制器包括一个现成的Arm Cortex-M3核心,内置闪存和板载RAM。该微控制器没有信任的硬件根,因此引导加载程序提供了所有固件安全特性。值得注意的是,没有任何东西可以确保引导加载程序不被修改。
客户端没有设备上的固件的确切源代码,因为整个版本在fat-fingering期间丢失了。更糟糕的是,我不能轻易地使用生产设备来测试这一点可以但是这会增加大量客户端无法承受的时间),并且客户端花了很大的力气来保护设备不受固件读取的影响,甚至禁用了联合测试动作组(JTAG)调试端口。更有挑战性的是,版本固件映像不会发出调试日志消息。不过,有一个事实对我有利:固件是建立在开源组件之上的。许多这样的项目都使用了引导加载程序和实时操作系统。
因为时间是最重要的,对生产二进制文件的静态分析将花费大量时间,所以我需要找到一种快速执行代码的方法。我所拥有的只是编译后的固件版本和客户想要安装在现场部署的设备上的源代码的新版本。新代码是寻找通信协议处理代码缺陷的切入点;客户声称这段代码没有实质性的改变。我习惯了工作量少。
一个通用异步接收/发送器(UART),或串行端口,暴露了一个框架命令协议。处理逻辑如下:
有几个有趣的缺陷。首先,中断处理程序代码没有检查字节是否写过了活动缓冲区的末尾的逻辑。第二,协议处理程序线程盲目地从输入缓冲区接受有效负载长度,复制它被告知的任何内容,而不进行检查。此错误可用于将恶意消息复制到与工作缓冲区相邻的其他数据上。
更有挑战性的是,版本固件映像不会发出调试日志消息。
下一步是将发布的固件映像加载到微控制器供应商出售的标准开发工具包中。我需要想办法加载我自己的代码。一些快速实验表明,发送一个值为0x4f的10 KB的过长的消息会导致设备卡住。成功!但它为什么会崩溃呢?
0 x4f4f4f4f
手头有某个版本的源代码,在开发工具包上运行另一个类似版本的固件,这意味着我可以开始调试。快速检查显示,该设备在故障时抛出了总线故障。当试图读、写或试图执行来自无效地址的指令时,会在Cortex-M CPU上发生这种情况。设备的状态寄存器指示在处发生了无效的指令获取0 x4f4f4f4f
.这对我来说是个好消息,因为我现在可以控制程序计数器了。
Cortex-M系列微控制器被设计成用普通的C代码就能轻松瞄准的对象,只需要最少的汇编语言。中断服务例程(ISRs)是由硬件直接调用的普通C函数。当处理中断请求时,CPU核心内建的逻辑在正在运行的进程的堆栈上准备一个堆栈帧。该帧存储被中断进程的寄存器,以及关于CPU状态和中断时指令指针值的信息。然后,CPU切换到一个单独的中断模式堆栈指针,并调用ISR。如果您可以覆盖已保存的进程上下文的内容——特别是在中断进入时保存的指令指针,那么您可以告诉CPU稍后返回一些不同的代码。
现在,我要做的就是替换我的缓冲区0 x4f
字节和一些我想运行的代码的地址。下一次进程被唤醒时,CPU将跳转到它从这个保存的上下文中读取的地址。
控制代码执行是一个问题,但我还需要将重写信任根的代码存储在某个地方。许多微控制器提供高达1MB的闪存,对于嵌入式系统开发人员来说,这是一个很大的存储空间。RAM是一种更宝贵的商品——在许多情况下只有几十KB。该设备上的所有代码都可以直接从flash运行,将RAM留给CPU状态和数据结构。然而,这意味着在执行固件更新时没有足够的内存来保存完整的程序映像。
闪存在这个设备上被分成两个分区:引导加载程序内存和应用程序映像内存。图1描述闪存的使用。(注意固件更新程序内置在引导加载程序中,不能更新自己。)图2显示来自合法应用程序映像更新的应用程序映像结构。图3显示应用程序映像的安全特性,以及一些添加的代码和数据。
图2。应用图像结构。
引导加载程序中内置的更新程序会擦除整个应用程序映像内存,并在其位置写入一个新映像。在flash中准备好完整的图像后,引导加载程序检查应用程序图像的签名,包括头部的内容,以验证其真实性。如果签名检查失败,引导加载程序将立即删除刚刚下载的数据。修改固件映像是不可能的,因为签名检查会失败。我还能做什么?
更新程序很简单。只要你不断给它输入数据,它就会把传入的数据写入flash。
检查客户端使用的开源引导加载程序的代码,发现了一个有用的错误:签名检查只在头中指定的代码区域上执行。只要原始的标头、代码和签名未被修改,引导加载程序就会引导映像。一个快速的测试证明了这一点。附加了额外数据的映像成功引导,额外数据被忽略。由于该设备上的所有闪存都是可执行的,我可以简单地跳到附加到有效更新映像的额外代码。
关于引导加载程序的加固就讲到这里…
最后一步是编写一个有效负载,它将“增强”引导加载程序,以使用来自客户机的新公钥验证应用程序映像签名。我的有效负载很简单:从flash中删除原来的公钥,并在其位置写入新的密钥。在随后的重新启动中,引导加载程序将接受用新密钥签名的新固件映像(客户机现在保存在两个密钥中)安全的地方。
我委托人的孤注一掷得到了回报。他们很快发布了带有新功能和补丁的固件版本。他们的客户毫不知情,原来的签名密钥已经丢失,而新的固件映像是利用我在对设备进行逆向工程时发现的错误安装的。新的固件版本还包括对这些错误的修复。
如果我告诉你这份工作不是独一无二的,你会相信吗?我至少有三个不同的客户遇到过这种情况,他们都遇到了同样的麻烦。在交付我的“修复”之后,我总是建议我的客户如何存储和管理固件签名密钥。因为这些是它们的设备的关键,它们应该受到尊重——无论是出于设备生命周期还是出于安全原因。
今天,许多商品微控制器提供了一个静态的信任根,内置于硅引导ROM中。在这种情况下,实施这样的黑客攻击要困难得多,这使得保护密钥变得更加重要。此外,如果客户的设计使用了Cortex-M系列提供的内存保护功能,那么这项工作将更加具有挑战性。
幸运的是,虽然我帮助过很多客户解决这个问题,但没有一个人不止一次地要求做这类工作。课学到了什么?
数字图书馆是由计算机协会出版的。版权所有©2022 ACM, Inc.
没有发现记录