与UEFI一起勉强度日

2020-10-26 02:01:33

有些人可能会认为我过去的一些帖子陷入了泥潭,探索Windows Autopilot、Intune、Windows 10、ConfigMgr等的技术深度。但我要说的是,这些通常只达到300级-我会省略真正深层次的东西,因为它可能更难解释,有些人可能就是不在乎。但偶尔,为了保持理智,我喜欢跳进一个新话题,以证明我仍然有能力学习新东西,并在这个话题上花费超过30分钟。在这种情况下,我会跳到一个新话题中,以证明我仍然有能力学习新东西,并在这个话题上花费超过30分钟。)在这种情况下,我会忽略真正深层次的东西,因为它可能更难解释,有些人可能根本不在乎。但偶尔,为了保持理智,我喜欢进入一个新话题,以证明我仍然有能力学习新东西,并在这个话题上花费超过30分钟。任何阅读这篇博客文章的人都会一起来兜风。“我们看看有多少人一路走到了”路的尽头“。

那么主题是什么呢?这是我近10年前开始谈论的东西,统一可扩展固件接口(UEFI)。当时,它更多的是一个警告:“你部署Windows的方式将会改变。现在,它是一种生活方式(幸运的是,它不再像2010年我们第一次开始使用它时那样糟糕)。”

我不想重复UEFI背后的“为什么”,因为坦率地说,你已经没有太多的选择余地了:“所有新的Windows 10设备出厂时都默认启用了UEFI(如果你关闭了UEFI,这是你的耻辱)。”相反,我想更多地关注它是如何工作的,以及幕后发生了什么。

根据UEFI规范(这确实是一个很难遵循的文档),设备最初将启动并根据系统架构查找特定文件:

它将在可访问的FAT32卷上的\EFI\boot文件夹中查找此文件。*(固件只能读取FAT32文件系统。建议使用GPT磁盘,不过有趣的是,至少根据UEFI规范,足够的MBR磁盘可能也可以工作。)这实际上是一种后备机制--如果没有加载任何其他文件,则搜索某个文件。但是一旦您在设备上安装了操作系统,它就会创建固件条目,以提供替代选择;UEFI将沿着列表向下工作,直到它成功加载一个固件。以下是您将在已经运行Windows 10的Hyper-V虚拟机上看到的固件订单:

注意,列表中的第一个条目指向\EFI\Microsoft\Boot\bootmgfw.efi,这是Microsoft提供的引导管理器;然后它决定从BCD引导哪个操作系统。有趣的是:*如果您查看bootmgfw.efi文件并将其与bootx64.efi文件进行比较(您必须为EFI分区分配一个驱动器号才能看到这些文件),您会注意到它们的大小完全相同:

这不是巧合-它们实际上是完全相同的文件。因此,无论它是由固件(bootx64.efi)作为“最后手段”加载的,还是由显式固件条目加载的,无论哪种方式,它都做同样的事情。

然后,引导管理器将基于BCD条目引导操作系统。对于Windows 10操作系统,BCD中将有一个指向特定卷的条目,以及实际引导操作系统的Windows引导加载程序的路径,该路径通常是该卷上的\WINDOWS\system32\winload.efi。以下是我笔记本电脑中的条目:

在幕后,“PARTITION=c:”条目实际上指向卷的ID(因为驱动器号分配取决于操作系统映射,而不是BCD或加载器);BCDEDIT使其更加友好。当加载winload.efi时,它将找到实际的Windows内核并加载它,这将启动实际的操作系统引导过程。

还有另一个有趣的文件可以发挥作用,hvloader.efi。这个文件与“HypervisorLaunchtype”的BCD设置有关。“这个文件是用于Hyper-V虚拟机管理程序的。如果winload.efi引导加载程序将检查此设置,并自动加载hvloader.efi来启动虚拟机管理程序。但确切的机制对我来说并不是百分之百清楚-那是一个我不想掉下去的兔子洞。”

在此过程中值得一提的是:*如果启用了安全引导,将检查所有提到的文件,以确保它们已正确签名且未被修改。*如果引导过程深入到足以让NTOS内核运行,它还将执行关键引导文件的验证。*这是UEFI和Windows的一个关键安全功能,也是为什么您应该始终在运行UEFI和安全引导的情况下运行,除非是在测试或播放时(我们稍后会详细介绍)。

我记得很久以前在UEFI规范中读到UEFI使用“EFI字节码”(EBC)。这将意味着特殊的工具、编译器等。嗯,我错了。是的,确实存在这样的东西,但它主要是针对跨平台驱动程序的,而且显然不是经常使用。所以这些EFI文件(如bootmgfw.efi)只是典型的可执行文件,可以用任何语言编写并编译成非常接近标准二进制的东西。C和汇编语言是最常用的语言。两者都用在了UEFI本身的源代码中(EDK2存储库,天奥科尔项目的一部分)。

至少目前,我对编写UEFI代码不感兴趣。但有一段UEFI代码在探索UEFI的一些“幕后”操作时会非常有用:EFI shell。我认为这是一种“UEFI命令提示符”,它有许多用于处理UEFI本身的内置命令。虽然源代码包含在EDK2项目中,但只需要构建(编译/链接)它。

好的,那么如何建造它呢?*在天科Github的内容中有一些说明。这些说明是可以的,但不是完美的。*以下是我所做工作的高级总结:

安装Visual Studio 2019(即使它没有在EDK2文档中列出为官方支持-仅限于Visual Studio 2017)。请确保您安装了桌面开发工具。)(我包括了MSVC v141,这是VS2017版本。)我认为这是必要的,只是没有花时间来证明。)。

将Python 3.7或更高版本安装到C:\Program Files\Python37。将系统环境变量Python_HOME设置为“C:\Program Files\Python37”(不带引号,不带反斜杠)。

将NASM开源汇编编译器安装到C:\Program Files\NASM。将系统环境变量NASM_PREFIX设置为“C:\Program Files\NASM\”(不带引号,尾随反斜杠)。

从Git GUI克隆EDK2存储库,将源位置指定为https://github.com/tianocore/edk2.git,目标位置指定为C:\EDK2。

如果所有这些都有效,那么您应该拥有主UEFI代码的完整构建。但是您仍然不会拥有UEFI shell。所以还需要更多:

这样做之后,您应该会在C:\EDK2\Build\Shell\DEBUG_VS2017\X64\ShellPkg\Application\Shell\Shell\DEBUG.中找到生成的shell.efi文件,现在,让我们尝试使用它…。

让我们从非常简单的事情开始:*一个全新的虚拟机,上面没有操作系统。*根据前面的讨论,我们需要做的就是创建一个FAT32磁盘,将EFI文件放在正确的位置,然后启动。*所以让我们来尝试一下。以下是步骤:

在Hyper-V中创建一个新的虚拟机(4 GB RAM,一个小型扩展VHDX,第二代,两个处理器-典型的东西)。

在文件资源管理器中找到VHDX文件,然后双击以挂载它。您将收到如下消息:

按照它说的做:打开磁盘管理,初始化磁盘,然后创建一个小的FAT32分区。

导航到文件资源管理器中的驱动器(E:From My Example)。创建E:\efi\boot文件夹结构,然后将shell.efi文件复制到其中,将其重命名为bootx64.efi(还记得上面的吗?)。

2019年09月02日中午12:08:00<;DIR>;上午12:08:00:00:00:00。2019年09月02日中午12:08:00<;DIR>;上午12:08:00:00:00。2019年09月01日12:31分,文件管理器文件名为976,960 bootx64.efi文件名为976,960个字节;文件系统文件名为976,960字节;文件名为976,960个字节。

在文件资源管理器中右键单击驱动器(例如E:\),然后弹出它(这会卸载它)。

现在,您可以启动虚拟机,并看到它失败,错误如下:

这实际上是一个好兆头:UEFI固件试图加载EFI shell(shell.efi重命名为bootx64.efi),但失败了。您能猜到为什么屏幕上的错误消息显示“不允许使用未签名映像的哈希”吗?很简单,这是一个安全启动错误。EFI可执行文件未经Microsoft签名-实际上根本没有签名。因此要使其运行,我们需要关闭Secure Boot。要关闭VM并更改设置:

正如您在该输出中看到的,设备运行的是UEFI shell 2.2(规范的最新版本)、UEFI 2.7(相当新,现在有2.8版本),而Microsoft是供应商(正如Hyper-V中预期的那样)。如果您查看FS0:VOLUME:

您可以看到,它是我们之前准备的FAT32卷,重命名后的shell.efi由虚拟机自动加载。

有关接受的所有命令,请参阅UEFI Shell 2.2规范的第92页。我最感兴趣的是“dmpstore”,因为它将转储所有UEFI变量。*您可以指定需要所有变量(-all),并且希望在屏幕上翻页(-b,用于查看屏幕上的名称)。*下面是来自“dmpstore-all-b”的示例:

“NV+BS”、“NV+RT+BS”和类似的字符串表示变量的属性。这些值包括:RT=运行时访问(即使在UEFI引导服务完成后也可以查询,例如从Windows中)。

Bs=引导服务访问(可由引导服务可执行文件查询,例如EFI文件)。

下一个字段(在单引号之间)表示供应商GUID(这样值可以用“Owner”分隔)和变量名。那些说“EFIGlobalVariable”的字段使用众所周知的GUID(有关有效全局变量的列表,请参阅UEFI2.8规范的第80页-任何不在该列表中的内容都必须使用不同的供应商GUID)。

之后,您可以看到数据的长度和数据本身的十六进制转储。

注意到上面的SetupMode和SecureBoot变量了吗?Windows安全引导文档中提到了这些变量。

所以现在我们知道的足够危险了。“我们可以构建UEFI码,我们可以运行EFIshell,我们可以四处查看UEFI固件变量。”但是现在我们需要更进一步,…。

使用新的、空的虚拟机对测试很有用,但它不一定会显示典型设备上现有设置的真实画面,因为Windows还没有安装。要了解这一点,我们需要使用真实的设备或实际安装了操作系统的VM。由于我手头恰好有一台真实的设备,这是一款运行最新Windows 10 Insider预览版的Surface Book,我用它来测试Windows Autopilot场景,这似乎是完美的实验品。

对于物理机,我实际上可以从包含EFI shell的USB密钥启动设备。要做到这一点,我只需要将USB密钥格式化为FAT32,然后在其上创建与在空VM中相同的文件结构,其中bootx64.efi文件同样是先前构建的重命名的shell.efi:

2019年09月02日中午12:08:00<;DIR>;上午12:08:00:00:00:00。2019年09月02日中午12:08:00<;DIR>;上午12:08:00:00:00。2019年09月01日12:31分,文件管理器文件名为976,960 bootx64.efi文件名为976,960个字节;文件系统文件名为976,960字节;文件名为976,960个字节。

但在使用USB密钥启动设备之前,我们必须在物理机上执行与在空虚拟机上相同的操作:我们必须关闭安全引导。(如果设备是BitLocker加密的,请注意-在执行此操作之前,您需要暂停BitLocker,否则由于UEFI篡改,设备将进入BitLocker恢复模式。这是一个功能-如果您没有先挂起BitLocker,您将需要BitLocker恢复密钥才能通过它。)在Surface Book上,我们可以通过在打开设备电源的同时按住音量降低按钮进入UEFI设置。从那里开始,安全引导设置位于Security(安全)下:

从Surface UEFI设置中,我们还可以强制设备引导到USB闪存盘。如果您导航到引导配置,您可以在列表底部看到USB驱动器。在该驱动器上向左滑动(可能需要一些练习才能正确运动,这有点挑剔),现在就会出现一个从它引导的对话框:

点击确定后,它将引导至EFI外壳-但在这样的超高分辨率显示器上,您可能需要使用放大镜才能读取:

不用说,EFI shell对DPI的感知程度并不高。但它至少是有功能的。从那里我可以执行我在VM上使用的相同的“dmpstore-all-b”命令。)(如果您不使用-b开关,预计会有一些非常痛苦的屏幕滚动。*虽然EFI shell可以工作,但它绝对没有针对具有此分辨率的屏幕进行优化。)好的,现在的诀窍是将该列表保存到一个文件中,因为我不可能手动键入所有这些内容。幸运的是,EFI shell支持输出重定向(来自UEFI shell规范的第70页),并且dmpstore命令具有写出逗号分隔列表的选项,因此您可以这样做:

这更像是这样。*(FS0:在我的情况下是USB驱动器。我还有一个fs1:,这是Surface Book中SSD上的FAT32 EFI引导分区。*我假设FS0:也将是用于引导的设备,但请确保检查以防万一。)要做更多的“切片和切割”,我可以将其(重命名为.csv)导入到Excel中,以便进行排序、过滤等操作。以下是快速查看的内容:(FS0:在我的情况下是USB驱动器。还有fs1:,这是Surface Book中SSD上的FAT32 EFI引导分区。我假设FS0:也是用于引导的设备,但请确保进行检查以防万一。)。

现在,该列表中的第四列等同于我们在虚拟机中看到的相同属性。(第五列是数据的长度,第六列是十六进制编码的数据。)UEFI 2.8规范的第241页有关于这些属性值的详细信息:

#DEFINE EFI_VARIABLE_NON_VERIAL 0x00000001#DEFINE EFI_VARIABLE_BOOTSERVICE_ACCESS 0x00000002#DEFINE EFI_VARIABLE_RUNTIME_ACCESS 0x00000004#DEFINE EFI_VARIABLE_HARDARD_ERROR_RECORD 0x00000008#DEFINE EFI_VARIABLE_AUTIFICATED_WRITE_ACCESS 0x000010//注意:EFI_VARIABLE_AUTIFIATED_WRITE_ACCESS已弃用#DEFINE EFI_VARIABLE_TIME_BASED_AUTHENTICATED_WRITE_ACCESS 0x00000020#DEFINE EFI_VARIABLE_EVERIFY_APPEND_WRITE 0x00000040

起初我以为我只关心非易失性变量(第一位设置),但在看完列表后改变了主意。*设置了第二位的变量只能通过.EFI应用程序查询(在操作系统加载之前)。设置了第三位的变量可以从运行的操作系统访问。*所以包括那些值以及安全指示器是很有用的:

那么,这些UEFI供应商代码中有哪些是什么呢?*对于其中一些代码,我们可能永远不知道(任何人都可以选择任何值)。但有一些代码是我们可以弄清楚的:

77FA9ABD-0359-4D32-BD60-28F4E78F784B:这些是微软Windows相关的值,你可以在网上找到一些参考资料。

遗憾的是,我希望能够从运行的操作系统中看到的一个值,即UEFI版本,无法从操作系统中访问(未设置RT属性)。而且,由于我必须关闭安全引导才能运行EFI shell,这可能会更改其他变量(除了安全引导文档中明显的变量之外),因此可能会遗漏一些变量。

好的,我们可以看到物理设备上的UEFI外壳的值-目标实现了。那么下一步呢?

从理论上讲,任何运行时可访问的变量(上表中的RT)都可以从运行中的操作系统(如Windows)中查询。而且有一个名为GetFirmwareEnvironment mentVariable(有各种风格)的API可以做到这一点。我实际上几年前就在MDT中使用过这个API,因为它是用来确定设备是否使用UEFI启动的:*如果你调用API寻找随机/无效的ID,返回的错误代码将足以提供线索。如果它失败,并出现“找不到”错误,你就知道它是使用UEFI启动的:*如果你调用API寻找随机/无效的ID,返回的错误代码将足以作为线索。如果它失败并出现“找不到”错误,你就知道它是使用UEFI启动的;如果它失败并出现“不受支持”的错误,您就知道它正在使用传统BIOS(或UEFI CSM,它实际上是模拟的传统BIOS)。

但是现在我们想把它用于“真正的东西”。API最大的挑战是您必须知道您在寻找什么;没有API可以让您枚举所有可用的变量。所以上面的列表为这个问题提供了一个很好的起点。

有趣的是,有一个PowerShell脚本已经检索了一些UEFI变量,名为Get-SecureBootUEFI。

但它不能让你检索任意变量,所以回到绘图板上来。只要在PowerShell脚本中嵌入一点C#代码(附在这个博客上),你就可以查询任何你想要的东西(至少是那些在上表中标记为“RT”的变量)。下面是一个例子:

呼。对我来说,这是一次学习练习,积累知识供将来使用。对于任何真正读过整篇文章的人来说,希望你也能学到一些东西。