查看原文
其他

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

Michal Necasek CSDN 2018-10-26


【CSDN编者按】很多年前的问题,竟可引起 2018 年的电脑操作崩溃。这究竟是什么原因呢?事情要从30多年前的一个安全漏洞说起。

前不久我注意到一个奇怪的问题,它会令一台32位i386 OpenBSD 6.3上运行的用户进程导致操作系统崩溃(仅限于i386,AMD64没有这个问题)。这个问题似乎跟三十多年前的一个安全漏洞的历史有关。

崩溃的代码看上去跟崩溃没有任何关系,但CPU却会进入一种极其奇怪的状态,无法访问内核栈和GDT(这种状态极其不健康,因为异常和终端会导致三重错误,导致CPU挂起)。

经过一番冥思苦想后,我注意到(虚拟)CPU的A20门是关闭的。这是完全错误的,因为CPU在保护模式下关闭A20门会导致非常不可预测的后果。

这就是那些所谓的“别去碰”的东西。但一个用户进程怎么可能关闭A20门呢?这完全没道理啊。

结果发现,i386的OpenBSD 6.3中,用户进程的确能做到这一点(同样,仅限于i386,AMD64没事)。一个安全漏洞允许正常的用户进程读写许多I/O端口,这显然是极其危险的。

而导致一连串的事件的正是英特尔,再加上许多NetBSD和OpenBSD的开发者的贡献。感谢开源代码,现在我们可以跟踪到到底发生了什么事情,还能从这些错误中学到一点东西。


始作俑者英特尔


1982年80286芯片发布,它引入了硬件任务切换,从某种角度来说在当时很先进。任务的基本状态保存在任务状态段(TSS)中。

TSS记录了非当前(“换出”)任务的寄存器状态,还记录了当切换到其他更高优先级的任务时需要使用的栈(出于这个原因,支持保护模式的操作系统必须有合法的TSS)。

80386的设计者之一John Crawford,将286/386的任务切换描述为“一大堆从未正确工作过的微代码”,这句评价很符合后来的情况。

但这种机制被刻在了X86架构中,而且即使不使用硬件任务切换,TSS也是必要的(见AMD64架构,它没有硬件任务切换,但依然需要TSS)。

1985年80386在硅谷问世时,TSS被略微扩展了一下(与286相比)以支持32位寄存器,使得操作系统可以中断特定的端口访问,同时允许其他进程全速运行;众所周知,权限位图不是原始386规范的一部分。

这对于v86模式非常有用,而康柏的CEMM最早从1986年起就开始使用这个功能了。

注意到I/O的权限位图对于任何保护模式下的(使用386的TSS的)任务都有效,而不仅是v86任务。

而问题是,对于v86任务,任何I/O端口访问都会使用权限位图,而对于非v86任务,只有当CPL从数值上大于IOPL时(也就是说不这样做I/O就不被允许时)才如此。

英特尔决定将I/O权限位图(IOPB)放在TSS上,并对每个任务设置I/O特权。但由于这是对已有设计的修改,所以不可避免地会有一些纰漏。

TSS的最后一个DWORD(32位)最初只包含一个比特,表示在切换任务和TSS的时候是否应当触发调试断点,这个比特位于比特0。

英特尔重新定义了最后一个DWORD的高16比特,以包含TSS中的IOPB的偏移量。

TSS中并没有显式指定IOPB的大小,仅指定了它的起始地址,因此IOPB的大小是由TSS本身的大小暗示的(段的大小记录在了GDT,即全局描述表中)。

换句话说,IOPB起始于给定的偏移量,并一直延伸到TSS的末尾,或者说,一直延伸到全部65536个可能的I/O端口全部被位图覆盖为止。

这样操作系统就可以将自己的数据结构放在TSS的固定部分之后、IOPB开始之前。任何IOPB没有覆盖到的65536个端口之外的端口都默认为不可访问。

整个IOPB的大小为8KB,这点内存对于内存有限的系统(1~2MB)来说是非常值得节省的,还能省下开始处的一些I/O端口,甚至可能省下很多TSS。

AMD的文档说,合法的IOPB偏移量必须为68h或更大(68h是TSS的固定部分的大小),一面覆盖TSS的固定部分。

虽然这很有道理,但英特尔的文档并没有提到这个限制,而且实际上英特尔的CPU允许IOPB从TSS中偏移量为0的地方开始,所以如果操作系统的设计者没考虑到这一点,就会引发一些“奇怪”的结果。

听起来似乎已经很奇怪了,但我们的英特尔当然不会止步于此。I/O端口访问可以为1、2或4字节,因此每次访问可能会考虑到IOPB中的1、2或4个比特。

由于端口访问可能不是对其的,因此CPU在测试IOPB时可能需要跨越字节边界读取2或4比特。

似乎由于这种设计是后来加上的,所以并没有足够的空间容纳更复杂的微指令,这种现象呈现给用户的就是CPU永远都从IOPB中读取两个字节。

因此,英特尔要求IOPB必须以一个填充字节结束,该字节的所有比特都为1(即“禁止访问”)。这样就能保证任何情况下都能读到合法数据。

而并没有文档描述当这个要求未能满足时的行为(即当IOPB的最后一个字节不全为1时的情况),因此你可能猜到了,如果填充字节是0,那就会允许访问字或双字的端口I/O,而这些区域本应是不能访问的。

更妙的是,这个要求(要求所有比特为1的填充字节)并没有出现在广为流传的80386 PRM原始版本的文档中(1986年)。

它被写在在1986年4月的80386芯片手册中(Intel档案号231630-002),并提供了相当的细节,但早期的软件开发者并不会总想着阅读这个文档。

在1989年的386SX PRM文档及以后的英特尔编程手册中详细记录了关于填充字节的要求。

注意sandpile.org声称最后一个字节只有最低的三个比特需要设置。这是完全有道理的,因为最多只需一次检查4个权限比特(对于双字大小的I/O),其中最低比特为IOPM的最后一个比特,而最坏的情况就是有三个比特溢出到填充字节中。换句话说,填充字节的高五比特从来不会被用到。

实际上,1986年的原始386 PRM不仅不完整,而且还是错误的:“例如,如果TSS限制等于I/O映射表的基址+31,那么表明前256个I/O端口已映射。

这句话暗示着映射256个端口需要32个字节,而实际上则需要33个。更新后的1989年参考手册说:“例如,如果TSS段的限制超过了比特映射的基址10个字节,那么映射有11个字节,前80个I/O端口已映射。”这段更新后的文字明确表示需要一个额外的填充字节。

有意思的是,1986年1月20日的一份英特尔备忘录第一次描述了I/O权限比特映射,它的内容如下:“例如,将TSS限制设置为 {BitmapBase + 32} 将允许比特映射前256个I/O端口”。

公开的PRM的文本与此相似,但不知为何32变成了31,所以是错误的。也许,英特尔编写文档的人也像所有人一样混淆了段大小和段限制之间的关系,也许,文档是在实现完成之前编写的,直到7年之后才得到更正。

而另一方面,奔腾处理器在实现v86增强时又打了更多补丁。对于增强的v86任务,TSS中的IOPB偏移量同时用作中断重定向映射的结束,后者位于紧挨着IOPB的前32字节处(256个软件中断需要256比特,即32个字节)。

没有任何用户可设置的标志来指明TSS中是否存在中断重定向映射,可以认为,只要CR4寄存器启用了v86模式增强,中断重定向映射就一定存在。

英特尔的386有个关于TSS中的IOPB偏移量字段的更正。处理器应该拒绝切换到任何限制小于103(67H)的TSS,但实际上只会拒绝切换到小于101(65H)的TSS。

遇到I/O指令时,386会尝试读取IOPB偏移量,如果TSS限制不够大,就会触发#TS错误。这种现象再次印证了IOPB是个后来才加入到386设计中的补丁。

英特尔的文档在设置没有IOPB的TSS方面说得并不清楚。英特尔的386文档(1986)说:如果I/O映射基址大于或等于TSS限制,那么TSS段就没有I/O权限映射,并且80386程序中的所有I/O指令都会在CPL > IOPL时引发异常。

而另一方面,AMD的文档说:映射可以位于TSS的前64K字节中的任何位置,只要它位于103字节之上。换句话说,在AMD CPU上,如果IOPB的偏移量小于68h,就表明没有IOPB。

这非常合理,因为这样才能保持与IOPB程序出现之前编写的软件的向后兼容,并避免了IOPB会覆盖TSS的固定部分的异常情况。而英特尔的CPU并没有阻止这种异常情况,不设置IOPB的软件基址的软件就可能会遇到无法预测的IOPB。

显然,正确使用IOPB并不容易。而且,虽然IOPB设置不正确不太可能会造成明显的问题,但可能会禁止访问特定端口,或允许访问意料之外的端口,最坏的情况下可能会导致安全问题。


386BSD拉开序幕


在上世纪八十年代后期,Bill Jolitz开始将BSD Unix移植到广泛使用的386架构上。386 BSD 0.0诞生于1992年早期。内部处理的数据保存在struct PCB中,定义在src/usr/src/sys.386bsd/include/pcb.h中,大致内容如下(仅摘要):

struct pcb {
    struct i386tss pcb_tss;
#ifdef notyet
    u_char pcb_iomap[NPORT/sizeof(u_char)]; /* i/o port bitmap */
#endif
    struct save87 pcb_savefpu; /* floating point state for 287/387 */
    struct emcsts pcb_saveemc; /* Cyrix EMC state */
/*
* Software pcb (extension)
*/

    int     pcb_flags;
    short   pcb_iml;     /* interrupt mask level */
    caddr_t pcb_onfault; /* copyin/out fault recovery */
    long    pcb_sigc[8]; /* XXX signal code trampoline */
    int     pcb_cmap2;   /* XXX temporary PTE - will prefault instead */
};

这个结构定义对于理解整个故事很有帮助。值得一提的是,PCB_IOMAP(即IOPB)并没有定义,但会放在紧挨着PCB_TSS之后,这会导致struct PCB不适合直接放在硬件TSS中,因为IOPB需要位于TSS的末尾(除非它覆盖了所有64K端口)。

还有一点,这个“软件PCB”实际上的确只包含软件定义的项目。


NetBSD的微小Bug和地雷


在九十年代中期,386BSD变成了NetBSD(与许多其他东西一起)。

1995年,NetBSD的开发者重写了操作系统的任务管理,使得每个进程都有自己的TSS。目标之一就是允许任务拥有自定义的IOPB,以便用户进程可以自己选择要访问的I/O端口。这种方式下只能开放前1024个端口。

struct PCB映射到TSS,它包含了固定的TSS部分、自定义的NetBSD字段,最后是个IOPB。它长这样(摘录于src/sys/arch/i386/include/pcb.h):

src/sys/arch/i386/include/pcb.h):
struct pcb {
    struct  i386tss pcb_tss;
    int pcb_tss_sel;
        union    descriptor *pcb_ldt;    /* per process (user) LDT */
        int    pcb_ldt_len;        /*      number of LDT entries */
    int pcb_cr0;        /* saved image of CR0 */
    struct  save87 pcb_savefpu; /* floating point state for 287/387 */
    struct  emcsts pcb_saveemc; /* Cyrix EMC state */
/*
 * Software pcb (extension)
 */

    int pcb_flags;
    caddr_t pcb_onfault;        /* copyin/out fault recovery */
    u_long  pcb_iomap[1024/32]; /* I/O bitmap */
};

必须指出,这里已经定义了PCB_IOMAP,而且它已经无缝移动到了“软件PCB”中,尽管它并不是完全软件定义的,这很可能是为了让整个结构能放到TSS中。这使得已有的“软件PCB(扩展)”的概念变得相当有误导性。

新的任务状态段通过src/sys/arch/i386/i386/gdt.c中的以下代码设置:

src/sys/arch/i386/i386/gdt.c:
void
tss_alloc(pcb)
    struct pcb *pcb
;
{
    int slot;

    slot = gdt_get_slot();
    setsegment(&dynamic_gdt[slot].sd, &pcb->pcb_tss, sizeof(struct pcb) - 1,
        SDT_SYS386TSS, SEL_KPL, 00);
    pcb->pcb_tss_sel = GSEL(slot, SEL_KPL);
}

SetSegment()的第三个参数就是新的段限制。作者在这里埋了个很隐蔽的雷,他将struct PCB的大小和相应硬件TSS的大小隐含地联系在了一起。

这一点甚至都没出现在struct PCB的定义中,就等着不明真相的程序员踩雷了。

眼尖的读者可能已经注意到,这里的struct PCB中漏了些东西,即英特尔要求的最后一个填充字节。IOPB并没有像作者预想的那样覆盖所有400H个端口,而是仅覆盖了3F8H个。


OpenBSD的Bug修改带来了另一个Bug


人们注意到了错误的IOPB大小的问题,并于2000年5月在OpoenBSD中改正了这个错误。更新后的struct PCB如下所示:

#define    NIOPORTS    1024        /* # of ports we allow to be mapped */

struct pcb {
    struct  i386tss pcb_tss;
    int pcb_tss_sel;
        union    descriptor *pcb_ldt;    /* per process (user) LDT */
        int    pcb_ldt_len;        /*      number of LDT entries */
    int pcb_cr0;        /* saved image of CR0 */
    union   fsave87 pcb_savefpu;    /* floating point state for 287/387 */
    struct  emcsts pcb_saveemc; /* Cyrix EMC state */
/*
 * Software pcb (extension)
 */

    int pcb_flags;
    caddr_t pcb_onfault;        /* copyin/out fault recovery */
    int vm86_eflags;        /* virtual eflags for vm86 mode */
    int vm86_flagmask;      /* flag mask for vm86 mode */
    void    *vm86_userp;        /* XXX performance hack */
    u_long  pcb_iomap[NIOPORTS/32]; /* I/O bitmap */
    u_char  pcb_iomap_pad;  /* required; must be 0xff, says intel */
};

注释信息如下:

Add an extra byte to the end of struct pcb and make sure that it is set to
0xff.  Intel (vol1 section 9.5.2) says that there must be a byte inside the
TSS after the iomap because it always reads two bytes when checking
permissions for io accesses.  before this, bits 1016-1023 were ignored.

This means that the entire pcb_iomap (and i386_*_ioperm) are accurate;
pr#1190 fixed

粗看起来,这个修改似乎完全正确。然而很不幸,它并不正确,因为它正好踩上了1995年埋下的地雷。

因为struct PCB包含32位的成员,整个结构的大小会被C编译器向上取整到32位。

因此,这个修改并没有将原来的3F8H个端口修改成覆盖400h个端口,而是将IOPB的大小扩展到了覆盖418h个端口。

这不仅确保了IOPB的最后一个字节完全不会被设置,还使得端口408h-418h能够以无法控制的方式访问。

这是个极其严重的安全漏洞,因为该范围内可能有极其重要的系统端口,而任何进程都可能能够访问它们(说“可能”的原因是TSS的最后三个意料之外的填充字节并没有显式初始化,但很可能是0,从而允许访问)。

这个问题可以归咎于C语言和它对结构体进行的“隐含”调整(尽管这一点众所周知)……也许应该归咎于没能正确使用该语言的程序员。


NetBSD也在同一个Bug上中招


OpenBSDD的程序员并不是唯一遇到该结构体填充问题的人。NetBSD 4.x遇到了完全相同的问题,只不过后果要更严重一点。在2007年的4.0版中,他们的struct PCB的实现貌似很疯狂,但其实并没有:

#define    NIOPORTS    1024        /* # of ports we allow to be mapped */

struct pcb {
    struct  i386tss pcb_tss;
    int pcb_cr0;        /* saved image of CR0 */
    int pcb_cr2;        /* page fault address (CR2) */
    union   savefpu pcb_savefpu;    /* floating point state for FPU */

/*
 * Software pcb (extension)
 */

    int pcb_fsd[2];     /* %fs descriptor */
    int pcb_gsd[2];     /* %gs descriptor */
    void *  pcb_onfault;        /* copyin/out fault recovery */
    int vm86_eflags;        /* virtual eflags for vm86 mode */
    int vm86_flagmask;      /* flag mask for vm86 mode */
    void    *vm86_userp;        /* XXX performance hack */
    struct cpu_info *pcb_fpcpu; /* cpu holding our fp state. */
    u_long  pcb_iomap[NIOPORTS/32]; /* I/O bitmap */
};

看起来似乎有道理,除了union savefpu包含了struct savexmm,后者由于某些明显的理由,有个__aligned(16)的属性。完全有可能没有任何人注意到这一点。这个问题在NetBSD 5.0中不再存在。

在OpenBSD的情况中,这个问题的直接原因是在硬件相关的代码中依赖sizeof(struct PCB),而这些代码完全没有处理通常的C结构体的填充问题。

而NetBSD的情况与OpenBSD不同,前者更不明显,因为它是由结构体内的联合体内的结构体造成的。这正是一段看似良好的代码修改在完全不相关的地方导致问题的教科书般的例子。


OpenBSD继续挖坑


在2007年10月,OpenBSD把坑挖得更大了。经过一系列修改之后,struct PCB变成了这样:

#define    NIOPORTS    1024        /* # of ports we allow to be mapped */

struct pcb {
    struct  i386tss pcb_tss;
    int pcb_tss_sel;
    union   descriptor *pcb_ldt;    /* per process (user) LDT */
    int pcb_ldt_len;        /*      number of LDT entries */
    int pcb_cr0;        /* saved image of CR0 */
    int pcb_pad[2];     /* savefpu on 16-byte boundary */
    union   savefpu pcb_savefpu;    /* floating point state for FPU */
    struct  emcsts pcb_saveemc; /* Cyrix EMC state */
/*
 * Software pcb (extension)
 */

    caddr_t pcb_onfault;        /* copyin/out fault recovery */
    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 */
    u_long  pcb_iomap[NIOPORTS/32]; /* I/O bitmap */
    u_char  pcb_iomap_pad;  /* required; must be 0xff, says intel */
    int pcb_flags;
};

IOPB后面增加了另一个成员,也就是说它将IOPB的大小再次扩展了32比特,即32个端口。

pcb_flags的实际值决定了哪些端口可以被访问,但这次一些端口是必然能被访问的(因为该值永远不会为-1)。

这个Bug无法再归咎于C语言了,它显然是个编程错误。但是,九十年代的代码帮了它很大的忙。

有了结构体最后一段之前的关于“软件PCB(扩展)”的注释,人们很难注意到所谓的“软件PCB”实际上是由硬件定义的。


越陷越深


到现在为止,我们有了1985年的灾难式的设计,加上1986年不完整的文档,1995年充满Bug的代码,2000年的重大错误,以及2007年不那么重要的错误。还能再坏吗?嗯……

2016年3月(OpenBSD 6.0),我们依然能看到下面的注释:

Delete i386_{get,set}_ioperm(2) APIs and underlying sysarch(2) bits.
They're no longer used by anything and should let us simplify the TSS
handling.

看起来没问题,对吧?我们可以扔掉整个IOPB,不会再有打开的I/O端口了。嗯……俗话说得好,好心难免办坏事。更新后的struct PCB如下:

struct pcb {
    struct  i386tss pcb_tss;
    int pcb_cr0;        /* saved image of CR0 */
    caddr_t pcb_onfault;        /* copyin/out fault recovery */
    union   savefpu pcb_savefpu;    /* floating point state for FPU */
    struct  segment_descriptor pcb_threadsegs[2];
                    /* 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, 00);

所以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读写你的用户进程本不能访问的物理内存。


无关的改变


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

其中,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, 00);
     return GSEL(slot, SEL_KPL);
 }

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

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


其他人怎么处理?


为了完整,也许我们应该看看其他操作系统是如何表示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中的注释。


书籍评论


许多关于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页的伪代码并没有清晰地表明这一点。


结论


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

  • 不完整或误导性的文档很危险;

  • 不充分或步误导性的源代码注释很危险;

  • 复杂、难懂的硬件设计很危险;

  • 最后时刻的硬件改变,会导致不可预测的问题;

  • 使用编程语言时不理解细节很危险;

  • 你不懂的东西最终一定会给你造成伤害;

  • 长时间来看,小错误会在人们意识不到的情况下变成大错误。

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

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

作者:Michal Necasek

译者:弯月,责编:胡巍巍

文章已于修改

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存