一台可信的计算机需要有可信的设计、制造和分发,这个可以参阅1985年彩虹丛书的安全等级的定义(商用计算机操作系统一般只能做到C2级安全性,某些领域可以达到B级安全性)。我们不可能像波音那样对飞控进行形式化验证,所以A级是肯定没希望的,但是尽可能把计算机向B级提高是有现实意义的。现代的商用计算机一方面在缓缓地提高性能,另一方面在安全性上获得了长足的进步,而这些进步绝大多数都是对最终用户不可见的,只有在特定环境中启用完全的强化才能感受到和20年前的巨大差异。在商用领域,这些努力一般被归结为可信计算(或类似的商标术语),其基本目标是确保计算机能够按照设计的使用方式完成给定的指令,而不会出现所有者无法控制甚至无法感知的非预期行为。遗憾的是这些努力一方面提高了商用计算机的可靠性,另一方面也引入了各种问题,主要有:1、高权限的安全管理系统本身设计与实现的错误可能导致系统可靠性出现额外的弱点。2、这类系统可能被供应链滥用/盗用而引入系统所有者不愿意接受的限制。因此部分技能水平比较高的机器所有者会选择性地禁用这些安全系统,以自己的方式来实现同级别或更高级别的安全要求。
现代X86计算机非常复杂,其主要原因是计算机的组件复杂性太高以及因为通用性要求而引入的大量标准接口。因此要启动一台现代X86计算机,必须遵守非常严格的时序,执行非常复杂的上电初始化操作。举一个例子就是现代CPU都是多核心的,上电的时候通常只会有一个核心上电,利用这个核心完成整个系统的自举之后其他核心才会启动。这类动作越来越多,越来越复杂,使得系统自举的流程复杂到难以对照着芯片datasheet直接写出自举程序。这种情况加速了自举程序的供应链封闭,即很多自举代码已经难以由芯片设计者以外的企业/用户自行实现,也使得系统更加黑盒,更加难以信任。
背景说到这里。
虽然现在的趋势是将支持芯片集成进CPU内部,或者将若干支持芯片合并在一起,但是这里还是按照传统结构,CPU->北桥(northbridge)->南桥(southbridge)->嵌入式控制器(embedded controller)和超级I/O芯片(Super I/O)。
现代BIOS都是存放在SPI闪存中,速度仍然是蜗牛级的,因此南桥负责将BIOS映射给CPU可见地址。也就是说,上电的时候,南北桥一定是早于CPU初始化的。南桥启动完成之后会读取BIOS芯片的前4K,Intel Firmware Descriptor,来确定这块芯片的内部区域布局。IFD记录着BIOS芯片内部是否存在若干区域、这些区域的位置、是否可以被读写等基本信息。由于IFD的存在,同一块闪存上就可以有BIOS、ME、GBE、EC等多种ROM并存。南桥依据这些信息将BIOS区域映射到内存地址的最后位置,此时CPU就可以通过读取内存的最高地址来访问BIOS了。如果制造商启用了BootGuard,CPU会对BIOS内容进行签名校验,从而阻止任何不可信的启动代码执行。BootGuard实际上是在ME的BUP(Bring-Up Process)当中,但是因为ME是数字签名的你没法去掉它。
现在CPU开始上电。虽然Intel在努力淘汰历史包袱,然而截止到目前,CPU(通常)仍然是以16位模式单核启动的。此时因为地址空间是16位的,最大寻址只有1M(FFFF:0000,实际代表的物理地址是0xFFFF0),因此BIOS的最后1M是唯一可以访问的内容,而最后16字节就是CPU可以寻址的最高位置,这个位置叫做Reset Vector。如果CPU被配置为仅以32位或者64位模式启动,CPU也会先将地址线内部写死使得其寻址空间最高位指向BIOS(当然就可以映射不止1M了)。CPU总是将指令指针指向Reset Vector然后将控制权交给BIOS。(这里不讨论最新的架构中引入的Firmware Information Table,其位置是最后64字节处并且早于Reset Vector执行。)因为16字节什么都做不了,BIOS通常只会在Reset Vector做一个跳转跳到真正的bootblock,其主要目的就是尽快将CPU切换到32位或64位模式下。在这个跳转之后,CPU的地址线完全解锁,BIOS就可以开始真正的初始化了。
第一个初始化阶段叫做romstage,此时BIOS完全运行在只读状态下,因为这个时候内存还没有启动,没有内存时所有的代码都只能通过寄存器工作。幸运的是现代CPU通常有巨大的高速缓存,以M为单位,完全足够高级语言作为内存使用。因此romstage通常最大的任务就是配置CPU使得其将高速缓存显示为可用的内存,这个状态称为CAR(Cache As Ram)。
CAR阶段高级语言就可以运行了,因此这个时候的BIOS代码通常就不再是汇编了。这个阶段最重要的事就是启动内存,因为DRAM和SRAM最大的不同就是它需要内存控制器准确地与之刷新频率同步。然而不同主板的内存物理位置会有区别(从而导致延迟不一致),也可能内存条本身的频率和时序有差异,因此BIOS必须要对内存控制器进行训练使得它能够稳定控制内存。这个训练通常需要很长的时间(数百毫秒)才能找到稳定的频率和延迟,可以说BIOS启动耗时里面最主要的部分就在这里。一旦内存控制器训练完成,通常BIOS都会选择保存训练数据,从而在冷启动和从S3唤醒的时候可以尝试快速启动内存。
这里插入一个经常被热议的模块,Intel Management Engine。ME是CPU中的一个单独核心,运行着单独的操作系统,其权限极高而对主系统不可见,然而其却有能力控制CPU行为和读写内存。为了实现ME的设计目的,内存控制器启用之后,BIOS必须告知ME内存已经就绪,使得ME进入运行状态并且继续运行BUP代码(Bring-Up Process),否则CPU就罢工了,这就是为什么ME是不可以被完全禁用的。同样的,因为ME可以通过板载有线网卡在关机状态接受远程控制指令,因此BIOS芯片中也保存有GBE的ROM,所以ME加载自身的时候也同时激活了网卡。这也方便了BIOS因为不需要针对网卡进行初始化了。
有了内存,CPU就可以启动高速缓存并进入完整的运行状态了,开机过程就完成了。接下来BIOS需要考虑是否进一步初始化其他的板载设备,比如显卡,然后就可以显示一个漂亮的Logo了。所以当你看到Logo的时候你认为开机过程刚刚开始,实际上却是已经结束。
继续插入一个板载组件,嵌入式控制器EC。在早期的台式机主板上通常是不存在EC的,因为主板就是主板,没有什么其他组件了。但是在笔记本(以及现代台式机)上,通常有很多指示灯、开关、键盘、显示器转轴、入侵检测螺丝等,这些设备并不通过USB或者PCIe和CPU连接而是直接连接在Super I/O或者南桥上,因此也是需要非通用的初始化的。这些设备通常由一块EC芯片控制,这块芯片独立于CPU,是主板上的第二块CPU,其启动是完全自主的。很不幸,这也意味着EC不但拥有自己的BIOS,而且往往是闭源而缺少标准的。
在EC的协助下,BIOS可以进行各种可选的硬件初始化,例如硬盘控制器和无线网卡。BIOS最后一件事就是决定将控制权交给操作系统,然而取决于操作系统本身的特性,BIOS需要提供更多的服务。早期的BIOS是通过提供16位模式中断来将已经初始化的硬件暴露给操作系统的,如果操作系统比较古老,依赖于这些中断,那么BIOS就需要有相关代码实现。如果操作系统依赖UEFI标准,那么BIOS也需要实现。如果操作系统什么都不需要,那么直接把内核加载到特定位置然后跳转就可以了。商用主板通常会同时提供BIOS模式和UEFI模式,而定制硬件例如Chromebook则可能选择直接加载内核。
接下来我们可以通过开源BIOS实现coreboot来更深入一些,这也是我这段时间折腾的一个归纳。
coreboot是目前历史最悠久名气最大的开源BIOS实现,在2012年以前基本上统治着自定义BIOS这个领域。然而2013年Intel Boot Guard的出现使得任何非主板制造商生成的BIOS都无法启动系统(制造商的公钥被烧录在南桥中,CPU启动的时候会读取并校验BIOS的签名)。这个措施推出之后coreboot社区基本上鸟兽散,因为认为随着时间推移所有的制造商都将会启用BootGuard,开源BIOS也就没有足够的市场来维持其延续了。在这之后,Google事实上接管了coreboot(核心开发者基本上都是Google员工了),并主要成为Chromebook的BIOS实现。对商用计算机的支持也就停留在了2012年的Sandy Bridge(gen2),以及并未完成的Ivy Bridge(gen3)。因此coreboot玩家如果不是购买制造商主动支持的主板(例如Chromebook、System76等),基本上能支持的最新的型号就是ThinkPad xx30(如T430,X230)了。基本上现在还在坚持玩coreboot的玩家多多少少都会有玩X230的历史/情结,毕竟别的牌子也不大可能坚持10年还能用了。
虽然没有BootGuard存在,ThinkPad也是不允许用户随便刷入非OEM发布的BIOS的,因此主流的操作是拆开笔记本,解焊BIOS芯片并且使用编程器直接烧录。因为不是所有人都有热风台和精密焊接的条件,所以也衍生出不解焊直接烧录,以及利用安全漏洞进行软件烧录的路线。其中软件烧录虽然看似安全,第一无法完全控制BIOS芯片,第二也存在失败而不得不硬刷救砖的风险,因此不管准备走哪条路线,都应该准备好编程器。
xx30有两块BIOS芯片,拆下键盘和掌托之后撕开主板保护膜的一角就可以看到,习惯上将两块芯片叫做top(4M)和bottom(8M)。其中bottom被映射为0-8M,而top被映射为8-12M。这12M空间的布局为:
Flash Region 0 (Flash Descriptor): 00000000 - 00000fff
Flash Region 1 (BIOS): 00500000 - 00bfffff
Flash Region 2 (Intel ME): 00003000 - 004fffff
Flash Region 3 (GbE): 00001000 - 00002fff
不要尝试去把这几个区域按地址排序,因为Intel对每个区域的序号是有要求的(例如IFD和BIOS必须是第一第二位)。你也可以看出来,BIOS一共有7M,前3M是在bottom的尾部,后4M是在top全部。
如果选择软刷,因为BIOS由南桥管理,南桥会依据IFD去决定怎么映射BIOS,然而IFD是锁住的,即你无法用软件刷新IFD本身(只读)和ME区域(不可读写),所以软件刷新绝对没有办法完全控制BIOS。而ThinkPad的BIOS启动之后会要求南桥对若干关键区域如Reset Vector和bootblock进行写保护,因此即使恶意软件获取系统控制权也没有办法直接刷写。然而,因为Lenovo早期版本的BIOS并没有完全保护好所有的寄存器,所以如果降级BIOS到老版本是可以绕过保护刷写BIOS区域的,而且这种方法可以刷写最后7M空间,某种意义上比编程器单芯片刷写更有优势。
还有一种不建议的方案是通过上电时短接HDA_SDO脚,这是Intel的一个维护模式使得南桥不对IFD进行写保护,从而实现软件刷写IFD来解锁整个BIOS区域。然而这个引脚处于主板背面,你需要把整个笔记本大卸八块,如果你对自己的手艺这么有信心,还是看下面的硬刷。
因为Reset Vector位于地址的最高位置,即在top当中,而coreboot可以被配置为BIOS总大小不超过4M,因此只刷写top就可以完成(一个小型的)coreboot烧录,之后就可以在coreboot下刷新整个BIOS的7M。这种方式没有解锁IFD,因此ME区域仍然是不可读写的。
最完善的方案则是双芯片烧录,即可实现修改IFD和去除ME的目标,但是要实现bottom的生成,你需要先将系统当前的bottom读出并备份,并将其写入新生成的固件中。如果你破坏了IFD,砖。如果你破坏了ME,砖。如果你破坏了GBE,网卡砖或者丢失硬件MAC地址。幸运的是,coreboot的一个衍生版Heads提供了xx30正确的IFD、ME和GBE镜像,这些镜像和我自己从X230原厂BIOS提取的内容一致,因此你也可以完全放心地刷入Heads或者基于Heads提供的内容生成自己的BIOS。
在详细说明coreboot生成的BIOS结构之前,一个预备知识就是即使IFD确定了一个区域,这个区域内部的结构是完全由这个区域的所有者自行决定的。因此GBE其实分为两个4K的镜像(内容一样),而ME和BIOS区域则有内部的分区结构。
由于ME的分区结构被解读出来,ME Cleaner就有能力对ME进行分区删除和重定位,从而将除了启动时必要的BUP和FTPR等分区保留而其他分区删除,而把空间从5M左右缩减到100K左右,不但彻底禁用了ME,同时也把空间释放给BIOS使得你可以利用超过11M的空间。修改ME区域大小需要修改IFD。虽然理论上ME精简后连同IFD和GBE可能只有100K出头,你应该在修改IFD时给出额外空间使得BIOS对齐到64K,这有利于coreboot更有效率地读取内容(早期coreboot没对齐的话干脆启不动或者卡到50秒超时能启动)。
DRAM训练这个启动过程中难度最大的环节,Intel提供了固件(MRC:Memory Reference Code)来帮助OEM完成。这个固件,巨大、完善、不透明,因此也是被认为一个不可信的环节。因此coreboot自己实现了开源的训练代码,这个代码轻巧、开源、不可靠。所以刷入coreboot之后启不动、两条内存只认出来一条、内存没有运行在最高频率等是coreboot社区的老生常谈。出现这种情况一般的建议都是把内存条换个槽,或者换内存条,因为coreboot会检查训练缓存(MRC cache)是否存在有效数据,如果有的话就不会再次训练了,换位置可以迫使coreboot重新训练。但是有的时候换位置也不起作用的时候,可能就需要重新刷一次BIOS把训练数据清空了。反之,如果coreboot挑剔你的内存条但是你成功训练出来一次,那你就会希望每次刷BIOS的时候都把这个缓存刷进去,避免重新训练。
BIOS区域则一般来说会有会有3-4个分区。上面提到DRAM训练结果的缓存,这个缓存一般是一个64K的区域保存在BIOS头部(你可以理解为什么要对齐了么,不对齐读不出来就启不动DRAM)。之后可能会有一个可写的区域作为NVRAM,通常在128K到256K。为什么不用CMOS?因为UEFI规范要求NVRAM有足够大的空间以及可以确保任何情况不失,所以如果你开启UEFI就会在闪存上分配一个空间,真正实现永久保留(思考一下Secure Boot Keys)。此外BIOS的最后部分是bootblock,最后剩下的空间就是位于BIOS区域中间的其他所有空间。
coreboot使用了一个轻量级(即假的)文件系统称为CBFS,这使得coreboot在剩余的空间中保存不定数量的文件并且可以在一定程度上进行热修改。CBFS中保存着各种coreboot自身的模块,payload,以及payload自己的数据。如果payload自身没有操作CBFS的能力(毕竟是和硬件相关的),coreboot还可以在BIOS区域中划分一个分区作为存储区,并向payload提供一个API来读写这个分区。
coreboot的payload是开机完成之后转移控制权的目标,因为coreboot真的就只负责开机,之后的操作系统启动什么的完全不管。如果没有payload,你就实现了开机即死。很遗憾的是因为社区分崩离析,现在除了几个元老级payload还没死,基本上就没什么真正实用的payload了。这几个元老包括并不限于:
- SeaBIOS:提供16位BIOS中断,可以启动任何能在传统BIOS下启动的操作系统。够简单纯粹,不挑食,用在什么地方都行。
- Tianocore edk II:UEFI的代表实现之一,在OEM进行开发的情况下有独立启动的能力,然而对coreboot的支持是比较晚和一言难尽的。但是MrChomebox对Tianocore进行了惨绝人寰的patch之后,可用性得到了一定提升。主要在Chromebook上用,也能在ThinkPad上提供Secure Boot。
- Heads:严格来讲这是一个基于coreboot的完整的BIOS项目,只支持ThinkPad部分型号,提供较强安全保证,包括验证启动过程的完整性以及通过YubiKey、NitroKey等进行额外保护。
- LinuxBoot、grub2、Linux内核直接启动:都是有相当局限性的方案,例如你要更新Linux kernel就得刷BIOS。Heads实现就是一个BIOS内的精简Linux,然后做完所有验证之后kexec启动硬盘上的Linux。但是普通人走这个方案需要相当的技术实力。
那么为什么要折腾coreboot呢?虽然这似乎应该是这篇文章的开头,但是纸上得来终觉浅,折腾完了再总结更合理一点。我可以看到的和原厂BIOS的若干区别有:
- 启动速度更快。这个是在牺牲了原厂BIOS的皮实耐操的代价下换来的,即coreboot更容易启不动或者不稳定(主要是内存),对主板上组件的POST和对EC的控制均没有原厂可靠/完整。实际上这里节省个1-2秒在整个启动过程中也占不了多大比重,可以认为这是一个没什么实用价值的优势。
- coreboot提供了更详细的启动日志,如果有硬件串行口可以看到启动每一步做了什么,但是这个对最终用户没有任何价值。
- 不会对你的硬件进行限制。ThinkPad等臭名昭著的硬件白名单在coreboot下不复存在,你可以随意更换无线网卡、WWAN卡或者电池。
- 略微不同的配置项。原厂BIOS通常允许你进行非常细节的配置,哪怕是你觉得什么有用的选项都没有的精简版BIOS菜单也比coreboot提供的选项多多了。但是coreboot提供少量原厂不提供的选项,例如软禁用ME,嗯大概也就这个了。
- 不同的安全性。coreboot实际上不提供任何有意义的安全性,其所谓的安全性功能vboot和measured boot都依赖于payload实现。如果你刷Heads这种可以利用measured boot的payload,或者ChromeOS这种vboot的真正使用者,那么coreboot可以比原厂提升一点安全性。但是如果你刷SeaBIOS或者Tianocore,基本上你的电脑就门户大开,因为这两者都是没有BIOS密码功能的,物理接触然后一顿改就完了。
额外说一句软禁用ME。ME设计来就是上电即启动,不依赖主系统而可以操作板载有线网卡,控制CPU电源状态和读写内存。因此在不使用ME Cleaner的情况下,你插上电源和网线,或者允许电池下接受管理并插上网线,ME就启动并接受控制了,这个时候BIOS甚至没有开始执行。但是BIOS启动的时候是可以请求ME停止工作的,这个功能称为Soft Temporarily Disable,请注意这个功能是“请求”而不是“强制”,因此存在可能ME不服从,不过至少在早期的系统上ME还是会接受的。所以如果使用了coreboot进行软禁用,你被ME操纵的条件就是主板有电、插着网线、主机处于关机状态。