去年初开始从 Evernote 向外迁移笔记,今年才完成了个七七八八,绝大部分自己写的东西是迁出来了,剩下一部分是网摘之类的,也还有个上百可能数百篇,只好就先将就着缓缓吧。今年有个新目标,那就是整理一下在网络平台里的收藏或者书签。如果真要算起来,把一些古早的犄角旮旯都算上,光是平台的数量可能也少不了。暂时没计划动那么大的干戈,这些天从 V2EX 开始。
整理过程中发现有一篇帖子的内容不是索引型,而是知识型,作者的 ID 是 imbushuo。看评论应该是在相关领域的一位知名人物,好多人将其又称呼为 ben。随着某个索引型的评论才知道,他是向 Nokia Lumia 950 XL 上移植 Windows 的领导者。该贴的地址在这里。原帖内容如下。
用 UEFI 下的 Windows Boot Manager 来加载诸如 GRUB 之类的启动器
背景知识
曾经在 Legacy BIOS 时代,你可以很容易地从 Windows Boot Manager 切换到其他引导器(比如 ntldr,比如 GRUB ),因为有现实的兼容需求存在。只需要添加一个实模式启动项即可。
然而在 UEFI 时代,Windows Boot Manager 并没有提供这个功能。你可能尝试过将 grub.efi 直接添加为 Windows Boot Manager 的启动项目,但是发现这样并没有什么卵用 —— 尽管 winload.efi, bootmgr.efi 之类的程序都是以 .efi 为扩展名。关了 Secure Boot 也是如此。
那么这是为什么呢
因为实际上 Windows 那些玩意并不是标准的 EFI 程序!观察 MSVC Linker 的选项,你就会注意到一些神奇的东西:

今天的主角就是 BOOT_APPLICATION。上述提及的 winload.efi 之类的程序就是这个类型的。它和 EFI_APPLICATION 有着很大的区别。
入口点的区别
这是一个常见的 EFI 程序的入口点:
|
1 2 3 4 5 6 |
EFI_STATUS EFIAPI InitializeUserInterface( IN EFI_HANDLE ImageHandle, IN EFI_SYSTEM_TABLE *SystemTable ) |
而这是一个 Boot Application 的入口点:
|
1 2 3 4 5 6 |
// Boot Manager Application Entrypoint // Bootstraps environment and transfer control to EFI Application entry point. NTSTATUS BlApplicationEntry( _In_ PBOOT_APPLICATION_PARAMETER_BLOCK BootAppParameters, _In_ PBL_LIBRARY_PARAMETERS LibraryParameters ) |
入口点都不一样,怎么谈恋爱,当然加载不起来(。
环境上的区别
Windows Boot Manager 是一个 UEFI Application。从固件进入后,它会创建一套自己的环境(与固件相独立),配置自己的 MMU 映射和自己的异常向量,为加载 Windows Kernel 作准备。所以 前人的尝试 只能运行 EFI 的一部分功能,而且好像只在 VMware 的固件上工作。对此感兴趣的可以看一下 bootmgfw.efi 的 BlInitializeLibrary 函数的内容。
创建完自己的环境后,它会把相关信息放到一个结构体里供其他 Boot Application 作参考,这玩意大概长这样:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
typedef struct _BOOT_APPLICATION_PARAMETER_BLOCK { /* This header tells the library what image we're dealing with */ unsigned long Signature[2]; unsigned long Version; unsigned long Size; unsigned long ImageType; unsigned long MemoryTranslationType; /* Where is the image located */ unsigned __int64 ImageBase; unsigned long ImageSize; /* Offset to BL_MEMORY_DATA */ unsigned long MemoryDataOffset; /* Offset to BL_APPLICATION_ENTRY */ unsigned long AppEntryOffset; /* Offset to BL_DEVICE_DESCRPIPTOR */ unsigned long BootDeviceOffset; /* Offset to BL_FIRMWARE_PARAMETERS */ unsigned long FirmwareParametersOffset; /* Offset to BL_RETURN_ARGUMENTS */ unsigned long ReturnArgumentsOffset; } BOOT_APPLICATION_PARAMETER_BLOCK, *PBOOT_APPLICATION_PARAMETER_BLOCK; |
里面有原始固件环境信息结构体的指针(随体系结构而变)。最近相关内容好像进公开的 Windows SDK/DDK 里了,感兴趣的可以找一下。也可以看一下这个很刺激的演讲,Alex Ionescu 写的。
回到 UEFI 环境里
以下内容为在 ARMv7 上的情况。其他平台请自己分析,不过大同小异。直接贴代码。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
// 先去拿一个固件描述符。之后你要把这玩意传进下面的函数里 FirmwareDescriptor = (PBL_FIRMWARE_DESCRIPTOR) (ParamPointer + BootAppParameters->FirmwareParametersOffset); ... // Switch to "real" mode and never look back. // And we do not need boot application context in this application. void SwitchToRealModeContext(PBL_FIRMWARE_DESCRIPTOR FirmwareDescriptor) { // 切换到 Firmware Mode (代码里称之为 Real Mode, anyway ARM 上没有 Real Mode) // 从固件环境描述符里获得目前的中断状态 unsigned int InterruptState = FirmwareDescriptor->InterruptState; // 关掉中断 DisableInterrupt(); // 切换 MMU 状态,以下请参考 ARMv7 手册 unsigned long Value = FirmwareDescriptor->MmState.HardwarePageDirectory | FirmwareDescriptor->MmState.TTB_Config; ArmMoveToProcessor(Value, CP15_TTBR0); ArmInstructionSynchronizationBarrier(); ArmMoveToProcessor(0, CP15_TLBIALL); ArmInvalidateBTAC(); ArmDataSynchronizationBarrier(); ArmInstructionSynchronizationBarrier(); // 切换好了,下面切换异常状态。参考 ARMv7 手册 ArmMoveToProcessor(FirmwareDescriptor->ExceptionState.IdSvcRW, CP15_TPIDRPRW); ArmDataSynchronizationBarrier(); ArmMoveToProcessor(FirmwareDescriptor->ExceptionState.Control, CP15_SCTLR); ArmInvalidateBTAC(); ArmDataSynchronizationBarrier(); ArmInstructionSynchronizationBarrier(); ArmMoveToProcessor(FirmwareDescriptor->ExceptionState.Vbar, CP15_VBAR); ArmInstructionSynchronizationBarrier(); // 如果固件描述符之前报告开了中断,那么重新开中断 if (InterruptState) ArmEnableInterrupt(); } |
在完成这些步骤后你就可以把相关信息传送到标准的 EFI 程序入口点了:
|
1 2 3 4 |
SomeRandomEfiApplicationEntry( FirmwareDescriptor->ImageHandle, FirmwareDescriptor->SystemTable ); |
然后就可以做任何事情了。抛砖引玉,你可以自己分析一下在 x86 和 amd64 上这个事情上怎么实现的。
bootmgfw.efi 和 bootmgr.efi 的区别?
前者是 EFI Application,后者是 Boot Application。但是前者除了会设置环境外,和后者没有什么本质上的区别。
附录
Windows Phone 上的其他 ELF chainload (包括跑一些喜闻乐见的东西)就是用这个东西做的。因为那个漏洞的本质是 Windows Boot Manager 感觉没有打开 Secure Boot,但是固件依然会验证从固件本身加载的程序,所以需要一个 chainloader 来取得控制权。代码可以来这儿看。在 Windows Phone 上需要为这个启动项打开 NoIntegrityChecks 开关。
程序退出时不需要切换回 Boot Manager 状态,bootmgfw 会负责善后。退出后重新回到启动选项界面。
- 作者网站:https://imbushuo.net/
- 作者 X:https://twitter.com/imbushuo
- 作者 GitHub:https://github.com/imbushuo/
