SecOC的是为了在车辆的CAN总线消息中增加真实性验证。增加良好的真实性验证需要涉及一些权衡。首先是在加密可靠性与计算时间之间的权衡,因为验证器的计算不应影响ECU所进行的实时计算。其次,是在抵抗暴力破解与可用有效载荷之间的平衡,因为只有验证器的一部分可以包含在消息中,以便为有效载荷留出空间。标准CAN消息的可用有效载荷仅为8字节,因此可用空间有限。
对CAN消息进行真实性验证并非新概念,但通常基于专有校验和算法。然而,一旦算法被逆向,就可以生成有效的校验和。例如,可以参考Charlie Miller和Chris Valasek的研究成果。作为这些专有算法的改进,AUTOSAR定义了SecOC标准,该标准提出了一种基于AES CMAC的身份验证方案。AES计算速度快,并且得到许多处理器硬件的支持。该标准建议根据抵抗暴力破解攻击的强度,采用不同的配置文件,但最终由开发者决定他们愿意为有效载荷牺牲多少空间。
下图展示了SecOC在发送端和接收端的整体实现概览。在接下来的段落中,我们将详细介绍各个不同的部分。
-
Trip Counter:行程计数器-每次车辆启动时递增。此计数器由SecOC协调器(例如网关)管理,并作为同步消息的一部分广播到网络的其余部分。 -
Reset Counter:复位计数器-以固定间隔递增。此计数器也是同步消息的一部分。 -
Message Counter:消息计数器-每次特定ECU发送消息时递增。当复位计数器递增时,它复位为零。 -
Reset Flag:复位标志-复位标志由复位计数器的最少几个有效位组成。这些被再次添加到新鲜度值,这确保了如果新鲜度值被截断,它包含复位计数器和消息计数器的最低有效位。
计算MAC的第一步是构建用于身份验证的数据。该数据通过连接CAN仲裁ID、有效载荷和新鲜度值来构造。然后使用128位AES CMAC算法来计算MAC本身。有关算法的详细信息,请参阅NIST SP 800- 38 B或RFC 4493。pyccryptodome包有一个Python实现,也可以用来理解算法。
在计算MAC之后,需要构造将通过网络发送的消息。这通过将有效载荷、新鲜度值的几个低位比特和MAC的高位比特拼接来完成的。通过添加新鲜度值的低位,接收节点接收复位计数器和消息计数器的低位。通过将其与接收器的内部状态相结合,这足以构建发送方的完整新鲜度值。在重构新鲜度值之后,可以计算MAC并将其与随消息发送的截断MAC进行比较。
由于计算MAC所涉及的AES加密基于对称加密,因此所有ECU都需要知道相同的密钥,同时重要的是这些密钥不能被攻击者轻易知晓。当车间在汽车中安装新组件时,车间需要能够更新 新ECU上的钥匙,以便它们与汽车的其他部分相匹配。由于攻击者可以模拟安装新的ECU,因此重要的是不能通过观察此密钥更新操作来获得密钥。
这种密钥管理是不同AUTOSAR标准的一部分,即安全硬件扩展(SHE)规范。下图概述了这一过程。这个过程中最重要的一点是,新密钥是使用ECU中已经存在的密钥之一加密的。该钥匙可以是当前的SecOC钥匙,也可以是MASTER_ECU_KEY
(在ECU的使用寿命期间不应更改)。这样,新的密钥可以由新的ECU解密,而攻击者无法获得密钥。有关更多信息,请参阅AUTOSAR SHE标准或Vector KB 0012486。
这项研究始于2021年初,当时第一批配备SecOC的丰田汽车开始上市。当时,最常见的车辆是2021年的Rav4 Prime,但由于芯片短缺,仍然很难找到。后来,SecOC开始向更多的丰田汽车推出,最初只针对日本工厂生产的汽车,但现在已经进入美国生产的卡罗拉。
除了目标车辆之外,我们还需要选择在接收或发送的消息中使用SecOC的目标ECU。这意味着动力转向(EPS),前向摄像头和动力传动系(PCM)ECU都是可能的目标。
我们决定从动力转向(EPS)开始。其内部的微控制器需要符合诸如ASIL-D等特定安全标准,希望这意味着它使用较旧的技术,安全功能较少。此外,EPS上的固件通常不会太大,这应该使逆向工程不那么繁琐。
Firmware Layout
了解SecOC是如何实现的第一步是开始对应用程序固件进行逆向。在逆向中,找到并确定你试图解决的问题的不同部分,然后尝试从中找到关联。在本案例中,我们实际上有三条线索,可以尝试将它们联系起来。
我们的第一个线索是统一诊断服务(UDS)4例程控制0x0110,它涉及更新SecOC密钥。首先,我们通过查找用于UDS错误代码的常量来识别UDS处理程序。寻找一个同时具有0x35(无效密钥)和0x36(超出尝试次数)的函数通常会找到安全访问例程的UDS处理程序。从那里你可以找到你的方式找到例行控制处理程序。
第二个线索是我们知道传入的CAN消息需要验证。因此,我们查阅了数据手册中的相关CAN寄存器,并识别出从CAN总线读取数据的代码部分。从那里开始,我们可以跟踪数据流,直至计算内容的消息认证码(MAC)。
当我们意识到特定的SecOC实现没有使用硬件安全模块(HSM)时,最后一部分就来了,所有的AES计算都是在软件中完成的。使用ghidra-findcrypt,我们可以很容易地找到AES SBoxes的位置。从那里开始,我们可以识别出更高级别的函数,例如加密数据块的函数。
在花了很多个晚上进行逆向,并在固件中标记了1000多个函数之后,我们终于能够将所有内容拼凑在一起。我们对SecOC的实现方式以及所有相关密钥在RAM中的位置进行了完全逆向(记住,没有使用HSM)。现在我们只需要找到一个任意读取漏洞来实际提取密钥,或者找到一种禁用SecOC验证的方法。不过,这说起来容易做起来难。
为了找到绕过SecOC的方法,我们对代码进行了一般审计。我们检查了SecOC处理程序、UDS处理程序相关的代码,并寻找其他调试协议,如CCP/XCP。然而,即使花了相当长的时间,我们也没有发现任何有用的东西。我们有一些发现值得一提:
-
在启动后的第一秒内,SecOC检查是禁用的。在这段启动期间,带有错误MAC的消息会被完全解析和处理。我们寻找了延长这一时间的方法,但似乎它是固件中硬编码的。 -
UDS处理程序实现了非标准SID 0xAB和0xBA。它们处理一些5字符的命令,如BAENA、JTEKM、JTRM1等。确切用途未知。 -
有一组CAN仲裁ID(0x13,0x14,0x15,0x16,0x17,0x18,0x19,0x1a),允许临时设置新的SecOC密钥。然而,看起来需要知道 ECU_MASTER_KEY
,就像实际的密钥更新一样。 -
行程计数器上似乎没有防回滚保护
在应用程序本身没有找到任何方法来获取密钥之后,我们将注意力集中在引导加载程序上。引导加载程序的主要目的是刷新ECU的更新,并且是一个独立的应用程序。我们将注意力转向了引导程序。引导程序的主要功能是向ECU刷入更新,它是一个独立的应用程序。如果因某种原因刷入更新失败,ECU会停留在引导程序中,以便可以再次尝试刷入。这意味着我们不得不重新开始逆向工程,并寻找所有与UDS相关的代码。
在逆向了大部分与刷入相关的UDS处理程序后,我们注意到ECU允许刷入应用的闪存区域,以及将数据上传到RAM的某个区域。有一些例程会调用位于这个RAM区域的函数,因此这看起来非常有希望。然而,还需要更多的逆向才能弄清楚这个RAM区域的确切用途。记住,实际上并没有这个ECU的更新,因此我们无法观察丰田软件是如何进行重新刷入的。
在某个时候,我们意识到引导程序代码不包含任何函数来处理写闪存扇区或擦除闪存扇区的低级操作。相反,它将准备数据闪存到缓冲区中,然后调用神秘RAM区域中的一个函数,该函数带有指向该缓冲区的指针。这意味着,如果我们能以某种方式在神秘的RAM区域中获得一些代码,我们就有一种方法让它执行。
选择这种重新刷入方法的原因对我们来说并不完全清楚。一个可能的原因是,为了使ECU更加防呆,不包含擦除应用所需的代码,因为如果没有可用的更新文件,如果应用被擦除,就需要更换ECU来修复它。或者,开发者希望保持一些灵活性,以防引导程序中存在否则无法在现场修复的错误
尽管我们知道可以通过请求刷入或擦除闪存区域来触发RAM中的代码,但我们仍然需要一种方法来实际上传这些代码。事实证明,这相当繁琐,因为ECU只接受具有正确CRC和CMAC的加密数据块。此外,我们还需要通过安全访问算法才能首先访问刷入功能。幸运的是,所有加密都基于AES,因此我们可以从提取的固件中获取所有所需的密钥材料
通过对引导程序进行逆向,并梳理刷入逻辑的所有不同状态机,我们设法重建了以下更新过程。
-
使用UDS SID 0x10(诊断会话控制)跳转到引导程序。 -
使用UDS SID 0x27(安全访问)进行身份验证,请求种子并发送相应的密钥以登录。 -
使用UDS SID 0x2E(按标识符写入数据)设置一些AES密钥/初始化向量(IV)。具体包括DID 0x203(5字节,用途不明)、DID 0x201(用于派生加密密钥的AES密钥)和DID 0x202(AES IV)。 -
使用UDS SID 0x34(请求下载)、0x36(传输数据)、0x37(请求传输退出)将4kb的刷入例程上传到RAM地址0xFEBF0000。在我们的案例中,我们将上传一个利用程序/外壳代码,而不是刷入例程。 -
运行例程控制以验证上传数据的CRC和CMAC。UDS SID 0x31(例程控制),具体例程标识符为0x10f0。我们将上传数据的地址和大小传递给例程控制。 -
使用例程控制0xff00请求擦除一个随机区域。我们将要擦除区域的地址和大小传递给该例程控制。这将触发我们的外壳代码。
在允许上传任何数据之前,我们需要与ECU进行身份验证。在UDS中,这是使用SID 0x27安全访问完成的。正常的过程是请求一个(随机)种子,我们对该种子运行某种加密操作以产生一个密钥。然后将该密钥发送到ECU,如果正确,我们就登录了。
然而,在这个ECU上,程序略有不同。当请求种子时,ECU还期望16个字节的数据,这些数据也涉及密钥的计算。ECU包含一个16字节的秘密,它被用作AES密钥来加密我们在请求种子时发送的任何数据。其结果然后被用作第二AES操作中的导出密钥,该第二AES操作将随机生成的种子解密为登录密钥。另见下图的示意图。
在构造payload 之前,我们需要向ECU发送两个值,用于导出用于解密和CMAC计算的密钥。这是使用SID 0x2E(按标识符写入数据)完成的。我们需要写一个AES密钥(DID 0x201)和一个AES IV(DID 0x202)。
上传到RAM的payload需要有一个非常特定的格式,以通过ECU所做的所有检查。如果这些检查不通过,代码将无法执行。payload的第一部分是直接的,包含用于闪烁例程或shellcode的代码。在偏移量0xFD0
处,一个指针被放置到主刷入函数的入口点,在我们的例子中,我们指向有效负载的起始。在偏移0xFE0
处放置与CRC 32校验相关的一些值,即被校验的块的起始地址和大小。地址和大小不能自由选择,因为它们会与引导加载程序中的某些预期值进行比较。引导加载程序计算该块的CRC 32,并期望其为0xFFFFFFFF
。通过在起始地址和大小之后放置一个特制的填充值,我们可以确保这是正确的。
除了CRC32检查,还有一个CMAC检查需要通过。首先,以与安全访问算法类似的方式导出密钥。固件中的另一个密钥(与安全访问密钥不同)用于加密DID 0x201以创建派生密钥。使用该导出的密钥,为包括CRC值的payload计算CMAC,该CRC值以IV(DID 0x202)为前缀。CMAC被放置在payload的末端。在组装好payload后,还需要使用派生密钥和IV(DID 0x202)通过AES CBC进行加密。下图是payload布局和加密步骤的示意图。
Building Shellcode to Extract the Keys
正如我们在应用程序逆向中所确定的那样,密钥可以从数据闪存或RAM中读取(引导加载程序在启动时不会清除RAM),我们只需要找到一种方便的方法来提取它们。我们决定写一个小payload,允许通过CAN总线转储内存区域,然后在完成后重启ECU。
为了构建payload,我们编写了一个Dockerfile,用于准备交叉编译器,通过为v850-elf目标构建binutils和gcc来实现。我们只需构建gcc的第一阶段,因为我们不会使用任何标准库或头文件。
然后,我们可以用C编写一个shellcode,将数据写入相应的寄存器并通过CAN总线发送出。CAN设备的使用可以在芯片的用户手册中阅读,或者通过Ghidra查看ECU固件的CAN总线代码。然后使用v850-elf-gcc -ffreestanding编译payload,并使用v850-elf-objcopy将其转换为原始二进制文件。创建payload后,我们仍然需要通过上面列出的步骤将其转换为可以发送到ECU的payload。
Conclusion
在这篇博客文章中,我们讨论了让车主完全访问其CAN总线并生成有效的SecOC MAC所需的不同步骤。总结起来,我们采取了以下步骤:
-
通过故障注入绕过锁定的调试端口,从ECU中提取固件。 -
逆向应用代码,以了解SecOC的实现方式以及在RAM中找到密钥的位置。 -
逆向工程引导程序,以了解更新过程如何工作以及我们如何上传和运行shellcode。 -
我们构建了一个shellcode,从RAM中提取密钥并通过CAN发送出去,然后重新启动设备。
这使我们能够在几秒钟内提取SecOC密钥,而无需对车辆进行任何更改。有了这个钥匙,我们就可以向任何我们想要的ECU发送消息,并控制所有的线控功能,如车道保持辅助(LKA),自适应巡航控制(ACC)或自动紧急制动(AEB)。
这之所以成为可能,是因为ECU开发者犯了两个错误。主要问题是上传到引导程序的有效载荷缺乏适当的加密签名。这需要使用类似PKCS#1 RSA签名的方式进行,其中提取固件并不能让你生成有效的签名。第二个问题是未使用硬件安全模块(HSM)来存储SecOC中使用的密钥。
原文始发于微信公众号(安全脉脉):2021 Toyota RAV4 Prime SecOC 密钥提取
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论