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/


分享到:


相關文章: