处理器微码操作以更改操作码?

信息安全 朦胧 混淆 部件
2021-09-06 15:32:39

我最近想到了一种通过默默无闻来实现安全性的极端方法,并想问你们是否可能。

一个无法访问特殊处理器文档的人是否能够更改 CPU 的微码以混淆机器的指令集?

为了让机器使用这样的处理器启动,还需要更改什么 - BIOS 操作就足够了吗?

4个回答

尽管现代 x86 处理器允许上传运行时微码,但格式是特定于模型的、未记录的,并且由校验和和可能的签名控制。此外,现在微码的范围有些有限,因为大多数指令都是硬连线的。有关详细信息,请参阅此答案现代操作系统在启动时会上传微码块,但这些块是由 CPU 供应商自己提供的,用于修复错误。

(请注意,上传的微码保存在内部专用 RAM 块中,不是 Flash 或 EEPROM;断电时会丢失。)


更新:对于什么是微码以及它可以做什么似乎存在一些误解和/或术语混淆,所以这里有一些更长的解释。

在第一个微处理器的时代,晶体管很昂贵:它们占用了大量的硅面积,这是芯片代工厂的稀缺资源(芯片越大,故障率越高,因为每个错误位置的灰尘颗粒都会使整个芯片不起作用)。所以芯片设计者不得不使用许多技巧,其中之一就是微码。那个时代的芯片架构大概是这样的:

Z80架构

(这张图片是无耻地从本站掠夺而来的)。CPU 被分割成许多单独的单元,这些单元通过数据总线连接在一起。让我们看看虚构的 " add B, C" 指令会包含什么(寄存器 B 和寄存器 C 的内容相加,结果存储回 B):

  1. 寄存器组必须将 B 寄存器的内容放在内部数据总线上。在同一周期结束时,“TEMP”存储单元应从数据总线读取值并将其存储。
  2. 寄存器组必须将 C 寄存器的内容放在内部数据总线上。在同一周期结束时,“A”存储单元应从数据总线读取值并将其存储。
  3. 算术和逻辑单元(ALU)改为其两个输入(其为TEMP和A),并计算加法。结果将在总线的下一个周期在其输出上可用。
  4. 寄存器组必须读取内部数据总线上的字节,并将其存储到 B 寄存器中。

整个过程将花费四个时钟周期。CPU 中的每个单元都必须按适当的顺序接收其特定的命令。向每个 CPU 单元发送激活信号的控制单元必须“知道”所有指令的所有序列。这就是微码介入的地方。微码是这个过程中基本步骤的一种表示,作为位字。每个 CPU 单元在每个微码中都有一些保留位。例如,每个字中的位 0 到 3 将用于寄存器组,对要操作的寄存器进行编码,以及操作是读取还是写入;第 4 到 6 位将用于 ALU,告诉它必须执行哪种算术或逻辑运算。

使用微码,控制逻辑变成了一个相当简单的电路:它由微码中的一个指针(它是一个 ROM 块)组成;在每个周期,控制单元读取下一个微码字,并通过专用线向每个 CPU 单元发送其微码位。然后,指令解码器是从操作码(程序员看到并存储在 RAM 中的“机器代码指令”)到微码块中的偏移量的映射:解码器将微码指针设置为执行序列的第一个微码字操作码。

这个过程的一种描述是CPU真正处理微码;并且微码为程序员认为是“机器码”的实际操作码实现了一个模拟器。

ROM 很紧凑:每个 ROM 位的大小与一个晶体管差不多,甚至略小。这使 CPU 设计人员能够在一个小的硅空间中存储许多复杂的不同行为。因此,备受推崇的摩托罗拉 68000 CPU是 Atari ST、Amiga 和 Sega Megadrive 的核心处理器,可容纳约 40000 个晶体管等效空间,其中约三分之一由微码组成;在那个狭小的区域内,它可以容纳 15 个 32 位寄存器,并实现一整套著名的寻址模式。操作码相当紧凑(从而节省了 RAM);微码字更大,但从外面看不到。


这一切都随着RISC处理器的出现而改变。RISC 源于认识到虽然微码允许具有复杂行为的操作码,但它也意味着指令解码中的大量开销。正如我们在上面看到的,一个简单的加法需要几个时钟周期。另一方面,那个时代(1980 年代后期)的程序员会越来越多地回避汇编,更喜欢使用编译器编译器将某种编程语言翻译成一系列操作码。恰好编译器使用相对简单的操作码;具有复杂行为的操作码很难集成到编译器的逻辑中。所以最终结果是微码隐含了开销,因此执行效率低下,因为程序员没有使用复杂的操作码!

简单地说,RISC 就是对 CPU 中微码的抑制。程序员(或编译器)看到的操作码微码,或者足够接近。这意味着 RISC 操作码更大(通常每个操作码 32 位,如在原始 ARM、Sparc、Mips、Alpha 和 PowerPC 处理器中)具有更规则的编码。然后,RISC CPU 每个周期可以处理一条指令。当然,指令做的事情比它们的 CISC 对应物少(“CISC”是非 RISC 处理器所做的,比如 68000)。

因此,如果您想用微码编程,请使用 RISC 处理器。在真正的 RISC 处理器中,没有严格意义上的微码;有一些操作码以一一对应的方式转换为所有 CPU 单元的激活位。这为编译器提供了更多优化代码的选项,同时节省了 CPU 空间。第一个ARM只使用了 30000 个晶体管,少于 68000 个,同时在相同的时钟频率下提供了更多的计算能力。付出的代价是更大的代码,但当时 RAM 越来越便宜(那时计算机 RAM 的大小开始以兆字节计算,而不是仅仅以千字节计算)。


然后事情又变了,变得更加混乱。RISC 并没有杀死 CISC 处理器。原来,向后兼容是计算行业中一股极其强大的力量。这就是为什么现代 x86 处理器(如 Intel i7 或更高版本)仍然能够运行为 1970 年代后期的 8086 设计的代码的原因。因此 x86 处理器必须实现具有复杂行为的操作码。结果是现代处理器有一个指令解码器,它将操作码分为两类:

  • 编译器使用的通常的、简单的操作码会像 RISC 一样执行,“硬连线”成固定的行为。加法、乘法、内存访问、控制流操作码……都是这样处理的。
  • 为了兼容性而保留的不寻常、复杂的操作码使用微码进行解释,微码仅限于 CPU 中单元的子集,以免在处理简单操作码时干扰和诱导延迟。现代 x86 中微编码指令的一个示例是fsin,它计算浮点操作数上的正弦函数。

由于晶体管缩小了很多(2008 年的四核i7使用了 7.31亿个晶体管),用RAM块替换微码的 ROM 块变得相当容易该 RAM 块仍位于 CPU 内部,用户代码无法访问,但可以对其进行更新毕竟微码是一种软件,所以有bug。CPU 供应商发布其 CPU 微码的更新。操作系统可以使用某些特定的操作码上传此类更新(这需要内核级权限)。由于我们谈论的是 RAM,因此这不是永久性的,必须在每次启动后再次执行。

这些微码更新的内容根本没有记录;它们非常特定于 CPU 精确模型,并且没有标准。此外,还有被认为是MAC甚至可能是数字签名的校验和:供应商希望严格控制进入微码区域的内容。可以想象,恶意制作的微码可能会通过触发 CPU 内部的“短路”来损坏 CPU。


总结:微码并不像人们常说的那么棒。现在,微码黑客是一个封闭的领域。CPU供应商为自己保留它。但即使您可以编写自己的微码,您也可能会感到失望:在现代 CPU 中,微码只影响 CPU 的外围单元。

至于最初的问题,在微码中实现的“晦涩的操作码行为”实际上与自定义虚拟机模拟器没有什么不同,就像@Christian 链接到的那样。这将是最好的“通过默默无闻的安全”,即不是很好。这样的事情很容易受到逆向工程的影响。

如果传说中的微码可以实现一个完整的解密引擎,带有一个防篡改的密钥存储区,那么你就可以拥有一个非常强大的反逆向工程解决方案。但微码无法做到这一点。这需要更多的硬件。Cell CPU可以做到这一点它已在 Sony PS3 中使用(不过,Sony 在其他领域搞砸了它——CPU 在系统中并不孤单,它本身并不能确保完全的安全性)。

当你研究像这样的硬件操作时,你就进入了“这里是龙”的领域。我不知道任何对此进行过任何实际实验的研究或野外攻击,所以我的回答将纯粹是学术性的。

首先,我最好解释一下微码是如何工作的。如果您已经对这些东西有所了解,请随时跳过,但我宁愿为那些不知道的人提供详细信息。微处理器由硅芯片上的大量晶体管组成,这些晶体管以提供一组有用的基本功能的方式互连。这些晶体管根据电压的内部变化或转换来改变它们的状态电压电平之间。这些转换是由时钟信号触发的,时钟信号实际上是一个在高频下在高电压和低电压之间切换的方波——这是我们获得 CPU 的“速度”测量值的地方,例如 2GHz。每次时钟周期在低压和高压之间切换时,都会进行一次内部更改。这称为时钟滴答。在最简单的设备中,单个时钟滴答可能构成整个编程操作,但这些设备的功能极其有限。

随着处理器变得越来越复杂,需要在硬件级别完成以提供最基本的操作(例如两个 32 位整数相加)的工作量已经增加。一条本地汇编指令(例如add eax, ebx)可能涉及大量的内部工作,而微码是定义该工作的内容。每个时钟滴答执行一条微码指令,一条本机指令可能涉及数百条微码指令。

让我们看一个非常简单的内存读取版本,对于指令mov eax, [01234000],即从内存中的地址移动一个 32 位整数01234000到一个内部寄存器。首先,处理器必须从其内部指令缓存中读取指令,这本身就是一项复杂的任务。让我们暂时忽略这一点,但它涉及控制单元 (CU) 内部的许多各种操作,这些操作解析指令并启动各种其他内部单元。一旦控制单元解析了指令,它就必须执行一组微指令来执行操作。首先,它需要检查系统内存管道是否已准备好接收新指令(请记住,内存芯片也接受命令),以便它可以进行读取。接下来,它需要向管道发送读取命令并等待它被服务。DDR 是异步的,所以它必须等待一个中断来表示操作已经完成。一旦引发中断,CPU 继续执行该指令。下一个操作是将新值从内存移动到内部寄存器。这并不像听起来那么简单——您通常会识别的寄存器(eax、ebx、ecx、edx、ebp 等)并不固定在芯片中特定的物理晶体管组上。事实上,CPU 的物理内部寄存器比它公开的要多得多,它使用一种称为寄存器重命名的技术来优化传入、传出和处理数据的转换。因此,必须将来自内存总线的实际数据移动到物理寄存器中,然后必须将该寄存器映射到公开的寄存器名称。在这种情况下,我们会将其映射到 eax。t 听起来很简单——您通常会识别的寄存器(eax、ebx、ecx、edx、ebp 等)并不固定在芯片中特定的物理晶体管组上。事实上,CPU 的物理内部寄存器比它公开的要多得多,它使用一种称为寄存器重命名的技术来优化传入、传出和处理数据的转换。因此,必须将来自内存总线的实际数据移动到物理寄存器中,然后必须将该寄存器映射到公开的寄存器名称。在这种情况下,我们会将其映射到 eax。t 听起来很简单——您通常会识别的寄存器(eax、ebx、ecx、edx、ebp 等)并不固定在芯片中特定的物理晶体管组上。事实上,CPU 的物理内部寄存器比它公开的要多得多,它使用一种称为寄存器重命名的技术来优化传入、传出和处理数据的转换。因此,必须将来自内存总线的实际数据移动到物理寄存器中,然后必须将该寄存器映射到公开的寄存器名称。在这种情况下,我们会将其映射到 eax。传出和处理的数据。因此,必须将来自内存总线的实际数据移动到物理寄存器中,然后必须将该寄存器映射到公开的寄存器名称。在这种情况下,我们会将其映射到 eax。传出和处理的数据。因此,必须将来自内存总线的实际数据移动到物理寄存器中,然后必须将该寄存器映射到公开的寄存器名称。在这种情况下,我们会将其映射到 eax。

以上所有内容都是一种简化——实际操作可能涉及更多工作,或者可能由专用的内部设备处理。因此,您可能正在查看大量微指令,这些微指令本身做的很少,但加起来就是一条指令。在某些情况下,特殊的微指令用于触发处理特定操作的异步内部硬件操作,旨在提高性能。

如您所见,微码非常复杂。它不仅在 CPU 类型之间会有很大差异,而且在发布版本和修订版之间也会有很大差异。这使得定位变得困难——您无法真正分辨出设备中编程了哪些微码。不仅如此,微码被编程到芯片中的方式也因每个处理器而异。最重要的是,它没有记录和校验和,并且可能还需要一些签名检查。您需要一些严肃的硬件来对机制和检查进行逆向工程。

让我们假设你可以以有用的方式覆盖微码。你会如何让它做任何有用的事情?请记住,每个代码只是在硬件内部改变一些值,而不是真正的操作。通过杂耍微码来混淆操作码将需要一个完整的自定义操作系统和引导加载程序,但 BIOS (可能)仍然可以工作。不幸的是,更现代的系统使用 UEFI 而不是旧的 BIOS 规范,后者涉及在实模式下在 CPU 上执行一些代码。这意味着您需要一个全新的 BIOS 和操作系统,全部从头开始编写。几乎不是一种有用的混淆方法。最重要的是,您甚至可能无法重新映射指令,因为看似任意的字节值并不是那么任意 - 各个位映射到选择 CPU 内部不同区域的代码。更改它们可能会破坏 CPU'

一个更有趣的练习是实现一条新指令,将您从 ring3 转换到 ring0 并切换回另一个指令,所有这些都无需执行任何检查。这将允许您通过特权升级做一些有趣的事情,而无需特定于操作系统的后门。

是的,这是可能的,尽管与某些人想的不太一样。按照这些思路,我在 Schneier 的博客上提出了一些想法。有几种方法可以做到这一点:

  1. 您自己的微码以不会改变的处理器开头例如,这可以使用开放核心并冻结内部设计来实现。然后您(和其他用户)对其进行自定义微码。正如其他人所指出的,这是很多工作。但是,您可以对微代码/微指令编译器执行高级语言(使用这些关键字搜索它们)。这些的组合是重量级方法。一个更简单的概念版本是 Alpha 的 PALcode,它允许您创建由现有指令组成并以原子方式执行的新指令。不确定任何仍在生产中的处理器中是否存在该功能。

  2. 我的另一种方法是提出一个微码并简单地更改机器代码指令的标识符。编译器和微码签名者位于受保护的机器上,要么没有联网,要么坐在高度可靠的守卫后面。传入的 shell 代码具有随机效应,在类似的学术研究中几乎不会导致代码执行。(谷歌指令集随机化,因为这种东西甚至有 CPU 原型。)该方案还将产生一个带有编译器、调试器等的工具链。Tensilica 的 Xtensa 处理器 IP 已经为特定应用生成了 CPU 和工具链。这……比这简单得多。;)

  3. 最好的方法是修改架构,只允许对数据进行合理的操作。这些被称为“标记”、“能力”等架构。标记架构将标记添加到表示数据类型(例如整数、数组、代码)的内存片段。处理器类型在允许之前检查各个操作的完整性。Crash-safe.org 的安全设计就是这样做的。能力系统是关于使用指向代码和数据片段的安全指针来划分系统。Cambrige 的 CHERI 项目就是这样做的。这两种风格过去都用于开发具有出色安全性和/或跟踪记录的实用系统。下面是关于它们的权威书籍。我目前的设计利用这些作为构建 GEMSOS、KeyKOS 或 JX 系统的安全操作系统的坚实基础。

http://homes.cs.washington.edu/~levy/capabook/

之所以给出这个答案,是因为尽管给出了否定的答案,但按照您所描述的精神,已经完成了一些事情,并且针对常见攻击进行了一些测试。他们只是不完全做一个新的微码,尤其是手工。他们使用我提到的快捷方式或设计一个处理器来模拟这种效果。如果它成为主流但同时会停止大多数代码注入,它可能会被打败。出于这个原因,我很久以前就向一小部分用户推荐了在 PPC 上使用 Linux(删除了识别信息)作为商业应用程序。在稳定供应廉价硬件的情况下,它们仍然是恶意软件并且在 5 年多之后没有被黑客入侵。因此,我希望随机 ISA 或微编码方法能够更好地工作,比这更好的标记/功能(甚至针对专业人士),以及它们的组合甚至更好。

我认为更改 x86 的微码是不可能的,但是可以在上面运行具有不同微码的模拟器并且可以使用。该仿真器可以构建为在引导时启动,类似于 CPU 引导(是的,CPU 也需要引导)。

PE 保护器中使用了混淆操作码,这将生成一组独特的操作码和可以解释这些操作码的虚拟机。这种方法使静态分析变得困难,用于反盗版和恶意软件编写。该技术的一个例子是Themida