30 年前的漏洞,竟可引起 2018 年的电脑操作崩溃!

int pcb_ldt_len; /* number of LDT entries */ int pcb_ldt_len; /* number of LDT entries */ int pcb_gsd[2]; /* %gs descriptor */ int pcb_ldt_len; /* number of LDT entries */ /* per-thread descriptors */
int vm86_eflags; /* virtual eflags for vm86 mode */
int vm86_flagmask; /* flag mask for vm86 mode */
void *vm86_userp; /* XXX performance hack */
struct pmap *pcb_pmap; /* back pointer to our pmap */


struct cpu_info *pcb_fpcpu; /* cpu holding our fpu state */
int pcb_flags;
};

现在完全没有IOPB了。也就是说,struct PCB中不再有IOPB,但并没有从真正的TSS中消失。因为设置TSS的代码包含了下面一行:

pcb->pcb_tss.tss_ioopt = sizeof(pcb->pcb_tss) << 16;

换句话说,操作系统告诉CPU,固定的TSS部分之后(偏移量68H)紧接着就是IOPB。这意味着,struct PCB中的所有软件定义的字段都会被CPU解释为IOPB。而且的确会如此,因为设置TSS limit的代码依然是:

setgdt(slot, &pcb->pcb_tss, sizeof(struct pcb) - 1,
SDT_SYS386TSS, SEL_KPL, 0, 0);

所以TSS会变得很大。

现在,尽管始作俑者是英特尔,但Bug是在OpenBSD中出现的。TSS中的IOPB偏移量应该比TSS限制更大(以同时对应英特尔和AMD的CPU)。

这个Bug的结果就是,i386版OpenBSD 6.0并没有完全移除IOPB,而是创造了一个更大、更无法控制的IOPB。

而且0-11B8h范围内的许多端口都必然能够访问(在特定的OpenBSD版本中)。同样,比特值为0表示“允许访问”,而0比特会有很多。

这个范围覆盖了许多系统端口,包括旧的中断控制器、DMA控制器、计时器、各种系统端口、VGA、IDE驱动器、PCI配置空间,还有许多鬼才知道的东西。任何用户进程都能读写这些端口。

从好的方面来说,这并不是太大的安全漏洞。如果你能访问PCI配置空间的端口,那么也许你可以访问IDE或AHCI的旧访问端口,也许可以读写磁盘,也许还能使用DMA读写你的用户进程本不能访问的物理内存。

30 年前的漏洞,竟可引起 2018 年的电脑操作崩溃!

无关的改变

2018年春天,OpenBSD在i386内核上加入了Meltdown的不定。不幸的是,这些代码并不是为OpenBSD 6.3准备的,在6.3发布之前被回滚了。

30 年前的漏洞,竟可引起 2018 年的电脑操作崩溃!

其中,Meltdown补丁废除了每个进程一个TSS的做法,对每个CPU仅使用一个TSS。其结果就是,struct PCB完全不再映射到TSS了。

当问题被报告至OpenBSD开发者后,他们迅速对OpenBSD 6.2和6.3做出了修正。

实际的修改很简单,可以完全引用在这里:

Index: sys/arch/i386/i386/gdt.c
===================================================================
RCS file: /cvs/src/sys/arch/i386/i386/gdt.c,v
diff -u -p -u -r1.37 gdt.c
--- sys/arch/i386/i386/gdt.c 7 Mar 2016 05:32:46 -0000 1.37
+++ sys/arch/i386/i386/gdt.c 23 Jul 2018 23:53:28 -0000
@@ -210,7 +210,7 @@ tss_alloc(struct pcb *pcb)
int slot;
slot = gdt_get_slot();
- setgdt(slot, &pcb->pcb_tss, sizeof(struct pcb) - 1,
+ setgdt(slot, &pcb->pcb_tss, sizeof(struct i386tss) - 1,
SDT_SYS386TSS, SEL_KPL, 0, 0);
return GSEL(slot, SEL_KPL);
}

TSS限制现在只是设置为必须的最小值,这样就没有留出空间给IOPB,因此不会导致错误的权限问题……

只要IOPB的偏移量位于TSS限制之后,这正是实际情况。现在不再有IOPB,用户模式的应用程序也无法再访问I/O端口了。

30 年前的漏洞,竟可引起 2018 年的电脑操作崩溃!

其他人怎么处理?

为了完整,也许我们应该看看其他操作系统是如何表示TSS中没有IOPB的。

例如,在386增强版本的Windows 3.1中没有这个问题,因为IOPB覆盖了所有64K I/O端口。Windows 3.0、EMM386(至少在4.50版本中)、386MAX 6.02或Windows 9x中也是如此。

Windows NT 3.1将IOPB的偏移量设置为与TSS大小相等,即比TSS限制大1。这也是Windows 7的做法(32位和64位都是如此),其他派生于Windows NT的操作系统(如Windows 10)都是如此。

OS/2 2.0(及后续版本)使用0DFFFh作为IOPB偏移量(并使用最小尺寸的68H字节的TSS)。

这符合英特尔的文档中的注释(如1990 i486 PRM),“I/O比特映射的基址不能超过DFFF(十六进制)”;这条注释依然存在于英特尔最新的SDM中。很显然,覆盖所有64K端口的IOPB不可能从偏移量0DFFFh之外开始,并且依然能放进64K中(因为它需要8K + 1个填充字节),尽管为何这与TSS限制要小于0EFFFh(例如为何IOPB不能超越64K边界)的原因并不明显。

不论如何,微软和IBM的OS/2程序员并不是唯一阅读了英特尔文档的人。例如,Solaris 2.4和Solaris 7也使用了同样的0DFFFh作为IOPB基址。

在BeOS 5.0(1999年)或NetBSD 5.0(2009年)中,IOPB被设置为0FFFFh以产生同样的效果(没有IOPB),尽管这可能违反了英特尔SDM中的注释。

30 年前的漏洞,竟可引起 2018 年的电脑操作崩溃!

书籍评论

许多关于X86架构的书籍都互相矛盾,有些甚至自相矛盾。如前所述,其中以英特尔的官方文档为首。

Hummel

如上所述,英特尔原始的386PRM没有提及任何关于设置额外的IOPB字节的问题,这导致一些作者写了一些没有事实根据的幻象。Robert L. Hummel的《PC Magazine Programmer's Technical Reference: The Processor and Coprocessor》(Ziff-Davis Press,1992年)在116页上称,“为了提高处理器在处理未对齐的端口时的效率,80386SX和80486的逻辑被重新设计过了(相对于80386DX),以保证永远从I/O权限比特映射中读取两个字节”。

而且继续说“……I/O权限比特映射的末尾必须用额外的字节做填充,该字节的值必须为FFh,以提供与80386DX的兼容性。”

这本书还称,“80386SX和80486处理器会忽略填充字节的值,并在计算I/O权限比特映射的限制时不考虑该字节。但是,80386DX的确会考虑该字节。”

有可能在那之前的CPU的确不会使用这个填充字节并认为它的值为FFH。但这个声明从逻辑上讲不通——如果CPU需要填充字节才能保证一次读两个字节,那为什么它的值不重要?

而且显然,这个生命直接与80386(DX)的使用手册矛盾,手册上明确记载了填充字节是必须的。这段文字似乎完全是作者的臆造。

Crawford & Gelsinger

还有John H. Crawford和Patrick P. Gelsinger的《Programming the 80386 》(SYBEX,1987)一书。作者是分别是386的首席架构师和386的设计者之一。490~495页提供了应对IOPB的详细做法,包含了伪代码(比官方的文档要详细得多)。

尽管这样一本权威的书,其中的文本也是值得质疑的。例如,491页说“为以最快的速度访问比特映射”,CPU一定会读取两个字节。

但书中并没有解释为何读取不对齐的字要比读单个字节要快(后者的情况下第二个字节无需读取)。

Crawford和Gelsinger说IOPB“可以存储在TSS的前64K字节中的任何地方”并且“可以从TSS的前56K中的任何地方开始”,这两个声明已经自相矛盾了(为什么不能从60K处开始并覆盖一半的端口?)。

493页的伪代码证明没有这种限制,IOPB位置的唯一限制来自于TSS中的IOPB偏移量是16位的这个事实。也就是说,伪代码允许IOPB从TSS的前64K字节中的任何地方开始,并有可能跨越64K。

《Programming the 80386》中的文本和伪代码必然有一个是错的,而且很可能两个都是错的。但即使如此,这本书对于IOPB的描述要比其他书详细、清晰得多。

Agarwal

同样相关的是Rakesh K. Agarwal的《80×86 Architecture & Programming Volume II: Architecture Reference》(Prentice Hall,1991)一书,作者也是一名与386设计有关的英特尔工程师(注意这本书没有卷一)。Agarwal的伪代码与Crawford & Gelsinger的类似,尽管不完全一样。

Agarwal称IOPB必须“不能超过TSS的最大限制,即0xFFFF”,但并没有明确地解释为什么TSS限制的最大值应当被限制在这个范围(TSS的描述符允许最大值4GB)。

但是,这本书还说如果I/O权限比特映射基址超过了0DFFFh,那么“I/O权限检查可能会在本应失败的时候成功”。

尽管并没有明确说明,但强烈地暗示了IOPB偏移量计算可以在386上使用16比特算数进行,如果IOPB过于接近64KB,就可能导致计算溢出,返回到TSS的最开头。不幸的是,120~121页的伪代码并没有清晰地表明这一点。

30 年前的漏洞,竟可引起 2018 年的电脑操作崩溃!

结论

在过去的几十年内,小错误和不准确可能会变成大错误,甚至严重的安全脆弱性。这个过程是隐藏的,因为绝大部分是不可见的。总结一下:

  • 不完整或误导性的文档很危险;
  • 不充分或步误导性的源代码注释很危险;
  • 复杂、难懂的硬件设计很危险;
  • 最后时刻的硬件改变,会导致不可预测的问题;
  • 使用编程语言时不理解细节很危险;
  • 你不懂的东西最终一定会给你造成伤害;
  • 长时间来看,小错误会在人们意识不到的情况下变成大错误。

据本文观察,Bug和安全漏洞大多数都是不可见的。正确编写的软件能够一直正确地工作,但恶意的程序总会找到出路。

原文:http://www.os2museum.com/wp/the-history-of-a-security-hole/


分享到:


相關文章: