我们在办公室发现了一台启用了BIOS密码的笔记本电脑。最重要的是,笔记本电脑的安全启动已打开。我们想要运行未使用Microsoft密钥签名的操作系统,因此我们确实需要一种进入设置实用程序的方法。UEFI安全术语入门SEC-安全PEI-EFI预初始化DXE-驱动程序执行环境PEI模块/DXE驱动程序/UEFI应用程序-包含固件代码协议的MicrosoftPE格式化文件-GUID识别结构实例PCH-平台控制中央启动过程即使是今天的现代64位CPU也开始以16位模式执行。在UEFI中,这称为SEC阶段。SEC阶段配置一组最小的CPU寄存器,然后将CPU切换到32位模式。此模式切换标志着SEC阶段的结束和PEI阶段的开始。在过去,SEC阶段还充当系统信任的基础,但今天,该角色分配给了PCH。在CPU开始执行任何代码之前,PCH在SEC和PEI阶段验证固件。PEI阶段配置一些非CPU平台组件,并可选地验证DXE阶段代码的完整性。验证通过后,PEI阶段将CPU切换到64位模式,开始DXE阶段。DXE阶段包含操作系统启动前运行的所有驱动程序和应用程序,包括操作系统的“引导加载程序”。其中一些驱动程序甚至在您的操作系统启动后仍然存在。固件文件系统UEFI定义了自己的文件系统格式以用于闪存映像。闪存映像将包含一个或多个固件卷,每个卷将包含一个或多个固件文件。文件由GUID而不是名称标识,尽管某些文件类型定义了一种可选的方式来提供名称。一个文件也可以是另一个卷的容器,它支持嵌套卷(即一个卷在另一个卷中)。嵌套卷通常用于支持卷压缩。Flash引入闪存转储PC本质上只是功能强大的大型嵌入式设备,并且像大多数嵌入式设备一样,它们具有可以转储和重写的闪存芯片。闪存芯片通常是板上最大的封装芯片。我们快速识别了笔记本电脑主板上的闪存芯片,并立即使用一些夹子将其连接到SPIFlash编程器。解析Flashflash的内容被格式化为Intelimage,可以被UEFITool轻松解析https://github.com/LongSoft/UEFIToolIntelFlashimage分为几个区域。然而,我们唯一关心的区域是BIOS区域。BIOS区由几个固件文件系统卷和一个NVRAM变量存储区组成。我们本可以找到负责向用户显示设置屏幕并修补密码检查的文件,但这在这台笔记本电脑上是不可能的,因为启用了基于硬件的固件安全性。在上图中,红色标记的区域受IntelBootGuard保护。在CPU执行任何指令之前,会计算红色区域的哈希值,并根据存储在闪存中某处的签名进行检查。RSA公钥的哈希值,用于验证OEM在制造过程中融合到PCH中的签名。标记为青色的区域受OEM的代码验证机制保护。OEM必须在BootGuard之上实施自己的身份验证机制,因为BootGuard仅保护SEC和PEI阶段。DXE阶段也需要防止被修改。在大多数实现中,青色区域被散列,然后该散列与存储在红色区域文件中的散列进行匹配。由于红色区域中的所有内容都已受BootGuard保护,因此不需要额外的签名。处理NVRAM除了在NVRAM变量内部,我们看不到安全启动启用标志或BIOS密码。因此,首先要尝试的是完全清除所有NVRAM变量,并让板子对所有内容都使用其默认值。如果幸运的话,安全启动将被禁用并且BIOS密码将消失。然而,当我们在清除变量存储后尝试启动电路板时,我们收到以下错误消息:12B4:NVRAM变量中的SecSettings校验和错误。我们在转储中搜索了这个字符串的一部分,并将包含它的DXE驱动程序加载到IDA中。加载后,我们按照外部参照查找引用它的代码。它似乎会尝试获取具有特定GUID的协议的句柄,如果无法获取,它将获取日志记录协议的句柄并向其发送错误消息。我们跟踪了实现所需协议的驱动程序,并查看了它试图访问的所有NVRAM变量。其中一个具有SecConfig名称,因此我们尝试清除除该变量之外的所有NVRAM变量,并希望一切顺利。电路板成功启动,安全启动被禁用!但是,BIOS密码仍处于启用状态。它不能存储在SecConfig变量中,因为在查看其内容后,我们确定它只是一堆启用/禁用标志。变量中没有足够的数据来包含密码或密码的哈希值。基于这些发现,我们得出结论,BIOS密码必须存储在NVRAM以外的其他地方,甚至可以将闪存密码存储在完全不同的芯片上。通过退出正在运行的UEFI应用程序(在本例中为UEFIShell的副本)将固件修改加载到安装程序中,并且在引导至闪存驱动器后,您可以返回到引导设备选择菜单。然而,一旦完成,从引导菜单进入设置菜单的选项就会消失,我们查看了设置菜单的NVRAM引导条目,发现它正在引导到一个GUID为2AD48FB3-2E28-的UEFI应用程序42F2-88D5-A73EC922DCBA。通过在UEFITool中搜索该GUID,我们在固件中找到了应用程序的可执行文件。我们提取它并将其放在闪存驱动器上,尝试从shell执行应用程序,但由于某种原因,可执行文件被标记为DXE驱动程序,而不是UEFI应用程序。我们设法通过使用加载命令而不是直接运行它来做到这一点,但即使以这种方式启动它,我们也会看到密码提示。修改内存中的固件我们想要跟踪处理密码检查逻辑的驱动程序,以便可以在内存中对其进行修补。想要制作一个补丁,让它认为没有设置密码。这不是永久性修复,但如果它有效,它将允许我们进入设置菜单。除此之外,我们还浏览了固件映像中的所有DXE名称。BpwManager之所以特别,是因为我们认为Bpw可能是BIOS密码的缩写。我们将其加载到IDA中并查看其所有字符串。当我们看到正确的驱动程序时知道12AE:SysSecurity-字符串列表中超出了BIOS密码输入失败计数。驱动程序注册了一个由函数指针组成的协议。我们查看了设置实用程序使用该协议的所有地方,并找到了一个我们认为它正在确定是否启用BIOS密码的地方。它调用协议提供的函数之一,如果返回值设置了最低位,它将启用该字符串,否则将禁用该字符串。((void(__fastcall*)(BpwProtocol*,_QWORD,char*))bpwProtocol->GetBPWFlags)(bpwProtocol,0i64,&bpwFlags);v13=L"启用";if(!(bpwFlags&1))v13=L"禁用";假设这与显示BIOS密码菜单项的代码有关,并且它调用的GetFlags()函数是某种函数。此函数的代码只是从内存地址读取一个值并将其返回。我们使用UEFIShell编辑内存中的标志值并将其设置为0,然后尝试再次加载设置实用程序。我们甚至可以转到安全选项卡并取消/重置BIOS密码!可悲的是,在重新启动笔记本电脑并尝试正常进入设置实用程序后,它仍然提示我们输入旧密码。GUID仿真EEPROMBpwManager驱动程序中的几乎每个函数都会调用GUID为9FFA2362-7344-48AA-8208-4F7985A71B51的协议。我们使用UEFITool的GUID搜索功能来查找对该协议的所有引用。引起我兴趣的是一个名为EmuSecEepromDxe的驱动程序。将其加载到IDA中并确认这是注册相关协议的驱动程序。该协议由三个函数指针组成,其中一个除了返回错误值外什么都不做。根据其余两个函数的十六进制输出以及如何在BpwManager驱动程序中使用它们,我们构建了这个结构来描述协议:structEmuSecEepromProtocol{public:EFI_STATUS(*eepromRead)(EmuSecEepromProtocol*this,__int64eepromBankID,__int64byteIndex,unsignedchar*b);EFI_STATUS(*eepromWrite)(EmuSecEepromProtocol*this,__int64eepromBankID,__int64byteIndex,unsignedcharb);EFI_STATUS(*returnError)();};我们把仿真的EEPROM分成几个部分,我们称之为存储区。有8个存储区,每个存储区包含0x80字节。每个eepromBankID指向两个连续的库,第二个库用于大于0x80的字节索引。我们确定对BpwManagerDXE重要的信息存储在银行ID0x57中。我们编写了一个快速UEFI应用程序,它尝试从该银行ID读取所有0x100字节,但我们对eepromRead进行的每次调用都会返回前0x80字节的错误代码。这意味着我们无法从两个存储组中的第一个中读取数据,无法跟踪IDA中引用该错误编号的位置。通读代码,我们发现bankID0x5C是所有bank的访问权限数组。每次尝试从银行读取或写入内容时,都会根据正在访问的银行号(而非ID号)检查银行ID0x5C中的字节。BankID0x57对应banknumbers6和7,确保已经设置了足够的banknumber6不允许读写,banknumber7允许读取。这解释了为什么我们能够从后半部分读取字节而不是前半部分。我们试图更改bank编号6的允许字节,但这返回了另一个错误。我们发现权限字节中还有另一位锁定了进一步的权限更改。尝试修补导致错误返回码的跳转指令,但这也不起作用。为了追踪它,我们跟踪了所有读/写请求的路径,发现它们最终进入了CPUIOEFI协议,实际操作发生在CPU之外的某个地方。ShenanigansBoot-Time我猜测所有模拟的EEPROM操作实际上都由嵌入式控制器处理,但我没有花太多时间寻找实际处理它们的方法。了解它是如何工作的对我来说并不重要。板上几乎所有其他芯片都采用BGA封装,我们不知道其引脚排列,因此闪存存储在那里的任何芯片都是不切实际的。我们知道,在引导过程中的某个时刻,必须设置权限以允许至少一些操作,因为要求输入密码的提示需要与之进行比较,并且设置实用程序必须能够更改密码。我们需要的提示是,如果您启动到内置应用程序(例如诊断启动画面)然后退出,设置按钮仍将出现在启动菜单中。但是,如果您启动到外部应用程序(例如闪存驱动器上的UEFIShell),“进入设置”按钮将消失,直到下次重新启动。我们在转储中搜索内置应用程序的名称,以尝试查看是否可以将其重定向到通常无法访问但内置的UEFIShell。原来都是标准的NVRAMUEFI引导项,引导项的properties字段有一个flag表示是给应用程序的,变量包含的是内置应用程序的GUID而不是文件路径跑步。“问题”我们读取了所有银行的权限字节,发现它们允许每个银行的所有权限。然后,确定密码的散列值在EEPROM中的位置,并将0写入其中。如果读取到BpwManager密码的hash全为0,就会认为没有设置密码。重启后,我们可以进入启动菜单,但是在选择任何启动项时遇到这个错误:01240:BadBPWdata,stopboot。此错误仅显示约3秒,然后系统立即关机。与其直接在模拟的EEPROM中修补散列,我们实际上应该做与之前相同的修补,以绕过密码提示进入设置菜单并从那里进行更改。JTAG在这一点上,我们每个人想要拯救板子的唯一方法就是JTAG。大多数英特尔芯片组支持通过USB的JTAG,即使主板没有JTAG连接器也是如此。他们称之为直接连接接口或DCI,它有两种类型:DbC(USBDebug类)和OOB。OOB在USB引脚上实现了完全不同的有线协议,需要特殊的适配器,只能通过与英特尔签署NDA才能获得。使用交叉USB3.0AA电缆连接到要调试的电路板,它将作为USB设备枚举。要连接此USB设备,您可以使用IntelSystemStudio,这是一个没有NDA的免费下载,它提供了一个正常的调试器接口。要找到接口,我们需要弄清楚如何启用DCI。在大多数主板上,可访问的设置实用程序仅显示一小部分可用的配置选项。出于某种原因,其他选项通常仍然被编译,即使它们总是被隐藏。您在UEFI中看到的几乎每个界面都基于规范HII或人机界面基础设施。HII接口由VFR语言设计并编译成IFR。我们需要做的就是找到显示选项的DXE,并从中提取IFR。一旦我们有了IFR,我们就可以将其分解以使其更易于阅读。幸运的是,已经有人完成了编写工具来完成所有这些工作。我们使用了LongSoft的Universal-IFR-Extractor的一个分支。要找到正确的DXE,我们只需在设置实用程序中搜索其中一个选项的名称,IFR提取器的输出是字节码的反汇编版本。https://github.com/LongSoft/Universal-IFR-Extractor有很多VarStore对象定义如下:VarStoreEFI:VarStoreId:0x5[8D6355D7-9BD1-44FF-B02F-925BA85A0FAC],Attributes:3,Size:572,Name:PchSetup中的每个VarStore变量都对应一个NVRAM变量,由其名称和guid给出,用于在选项中引用它。选项定义如下:OneOf:DCIenable(HDCIEN),VarStoreInfo(VarOffset/VarName):0x8,VarStore:0x5,QuestionId:0x2D8,Size:1,Min:0x0,Max0x1,Step:0x0OneOfOption:Disabled,Value(8bit):0x0(default)OneOfOption:Enabled,Value(8bit):0x1EndOneOf{2902}DCI使选项存储在VarStore5中字节偏移8处,长度为1字节。将此字节设置为0以禁用,将其设置为1以启用。这些是我们更改的所有选项:DCI启用(HDCIEN)->启用调试接口->启用调试接口锁定->禁用启用/禁用IED(英特尔增强调试)->启用将FlashBoots中的NVRAM变量编辑为除启动菜单,因此编辑这些NVRAM变量的唯一选择是使用外部闪存编程器直接写入它们。从闪存芯片中检索到新的转储后,我们只会修改当前状态,不会将其恢复到较早的状态,这可能会导致更多故障。使用UEFITool在闪存中找到变量的偏移量。每个变量都有一个标头,因此需要跳过它才能获取变量的实际值。编辑所有需要更改选项的变量后,在UEFITool中重新加载转储以验证我们没有意外损坏的数据并且变量的值确实发生了变化。之后,重新刷新它,连接交叉线,然后给电路板通电。这是我们第一次在基于Intel的计算机上使用JTAG,这非常令人兴奋,尤其是事实证明它是如此简单。为了解决“问题”调试分析,我们进入启动菜单,然后进入调试器。不确定将代码分成哪一部分,更不用说我们感兴趣的代码或数据所在的位置了。不幸的是,在“问题”发生并导致电路板无法启动之前,我们没有想到保存任何相关DXE模块的加载地址。调试器有一个内置命令会列出所有加载的DXE模块,但它需要UEFI系统表的地址,自动扫描失败。在大多数使用EDK-II构建的UEFI二进制文件中,系统表在执行驱动程序/应用程序的入口点函数时存储在全局变量中。我们从RIP所在的地址转储了几个字节,然后在UEFITool中搜索这些字节以确定当前正在执行代码的模块。将该模块加载到IDA中,并根据模块的加载地址重新设置数据库。通过在IDA数据库中搜索我们之前提取的字节并根据RIP的值计算偏移量,可以找到模块的加载地址。现在有了一个对齐良好的IDA数据库,很容易获得包含指向系统表的指针的全局变量的地址。系统表的地址从该变量中检索并提供给调试器。Now,whenlistingcommandsforDXEmodules,itactuallygivesalist.输出类似于:ModuleIDBaseSizeName000010x00000000D5A320000x00018460000020x00000000D5A4B0000x00003B80000030x00000000D5A4F0000x00001200000040x00000000D5A510000x00002F40000050x00000000D5A540000x00000540000060x00000000D5A550000x00001620...在未加载调试信息的情况下,调试器不会给我们模块的名称甚至GUID。However,thesizeofthe模块可用于缩小可能的基地址范围。UEFITool提供每个模块的大小,检查位于每个剩余基地址的内存,并将其与从固件转储中提取的数据进行比较,以确定哪个基地址对应于我们感兴趣的模块。代码执行我们考虑了一会儿执行任意代码的最佳方式。我们意识到,任何需要运行的代码都会导致状态发生变化,这种变化会在重启后持续存在。这意味着我们不必将执行返回到固件,代码运行后,板子可以重新启动。我们回顾了BpwManager中的代码,确定有两个字节的校验和没有被删除。我们认为将校验和清零将允许电路板启动。要写入零,我们只需将RIP设置为eepromWrite函数的地址,然后设置所有其他寄存器以提供正确的函数参数。我们在eepromWrite函数中执行此操作,以便执行不会返回到固件代码。返回固件代码可能会导致崩溃,因为通过修改寄存器、将电路板置于未定义状态、覆盖两个字节并重置电路板,BIOS密码终于消失了!尝试通过设置实用程序设置我们自己的密码,以确保它实际上已经消失并具有完全控制权。
