當前位置:
首頁 > 科技 > 30 年前的漏洞,竟可引起 2018 年的電腦操作崩潰!

30 年前的漏洞,竟可引起 2018 年的電腦操作崩潰!

【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限制設置為 將允許比特映射前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中,大致內容如下(僅摘要):

structpcb{

structi386tsspcb_tss;

#ifdefnotyet

u_char pcb_iomap[NPORT/sizeof(u_char)];/* i/o port bitmap */

#endif

structsave87pcb_savefpu;/* floating point state for 287/387 */

structemcstspcb_saveemc;/* Cyrix EMC state */

/*

* Software pcb (extension)

*/

intpcb_flags;

shortpcb_iml;/* interrupt mask level */

caddr_tpcb_onfault;/* copyin/out fault recovery */

longpcb_sigc[8];/* XXX signal code trampoline */

intpcb_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):

structpcb{

structi386tsspcb_tss;

intpcb_tss_sel;

uniondescriptor *pcb_ldt;/* per process (user) LDT */

intpcb_ldt_len;/* number of LDT entries */

intpcb_cr0;/* saved image of CR0 */

structsave87pcb_savefpu;/* floating point state for 287/387 */

structemcstspcb_saveemc;/* Cyrix EMC state */

/*

* Software pcb (extension)

*/

intpcb_flags;

caddr_tpcb_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)

structpcb *pcb;

{

intslot;

slot = gdt_get_slot();

setsegment(&dynamic_gdt[slot].sd, &pcb->pcb_tss,sizeof(structpcb) -1,

SDT_SYS386TSS, SEL_KPL,,);

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如下所示:

#defineNIOPORTS 1024/* # of ports we allow to be mapped */

structpcb{

structi386tsspcb_tss;

intpcb_tss_sel;

uniondescriptor *pcb_ldt;/* per process (user) LDT */

intpcb_ldt_len;/* number of LDT entries */

intpcb_cr0;/* saved image of CR0 */

unionfsave87 pcb_savefpu;/* floating point state for 287/387 */

structemcstspcb_saveemc;/* Cyrix EMC state */

/*

* Software pcb (extension)

*/

intpcb_flags;

caddr_tpcb_onfault;/* copyin/out fault recovery */

intvm86_eflags;/* virtual eflags for vm86 mode */

intvm86_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 theendofstructpcbandmake sure that itissetto

0xff. Intel (vol1section9.5.2) says that there must be abyteinside the

TSSafterthe iomap because italwaysreadstwobyteswhenchecking

permissionsforio accesses.beforethis, bits1016-1023were ignored.

This means that the entire pcb_iomap (andi386_*_ioperm)areaccurate;

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的實現貌似很瘋狂,但其實並沒有:

#defineNIOPORTS 1024/* # of ports we allow to be mapped */

structpcb{

structi386tsspcb_tss;

intpcb_cr0;/* saved image of CR0 */

intpcb_cr2;/* page fault address (CR2) */

unionsavefpu pcb_savefpu;/* floating point state for FPU */

/*

* Software pcb (extension)

*/

intpcb_fsd[2];/* %fs descriptor */

intpcb_gsd[2];/* %gs descriptor */

void* pcb_onfault;/* copyin/out fault recovery */

intvm86_eflags;/* virtual eflags for vm86 mode */

intvm86_flagmask;/* flag mask for vm86 mode */

void*vm86_userp;/* XXX performance hack */

structcpu_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變成了這樣:

#defineNIOPORTS 1024/* # of ports we allow to be mapped */

structpcb{

structi386tsspcb_tss;

intpcb_tss_sel;

uniondescriptor *pcb_ldt;/* per process (user) LDT */

intpcb_ldt_len;/* number of LDT entries */

intpcb_cr0;/* saved image of CR0 */

intpcb_pad[2];/* savefpu on 16-byte boundary */

unionsavefpu pcb_savefpu;/* floating point state for FPU */

structemcstspcb_saveemc;/* Cyrix EMC state */

/*

* Software pcb (extension)

*/

caddr_tpcb_onfault;/* copyin/out fault recovery */

intvm86_eflags;/* virtual eflags for vm86 mode */

intvm86_flagmask;/* flag mask for vm86 mode */

void*vm86_userp;/* XXX performance hack */

structpmap*pcb_pmap;/* back pointer to our pmap */

structcpu_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 */

intpcb_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) APIsandunderlying sysarch(2) bits.

They"re no longer used by anything and should let us simplify the TSS

handling.

看起來沒問題,對吧?我們可以扔掉整個IOPB,不會再有打開的I/O埠了。嗯……俗話說得好,好心難免辦壞事。更新後的struct PCB如下:

structpcb{

structi386tsspcb_tss;

intpcb_cr0;/* saved image of CR0 */

caddr_tpcb_onfault;/* copyin/out fault recovery */

unionsavefpu pcb_savefpu;/* floating point state for FPU */

structsegment_descriptorpcb_threadsegs[2];

/* per-thread descriptors */

intvm86_eflags;/* virtual eflags for vm86 mode */

intvm86_flagmask;/* flag mask for vm86 mode */

void*vm86_userp;/* XXX performance hack */

structpmap*pcb_pmap;/* back pointer to our pmap */

structcpu_info*pcb_fpcpu;/* cpu holding our fpu state */

intpcb_flags;

};

現在完全沒有IOPB了。也就是說,struct PCB中不再有IOPB,但並沒有從真正的TSS中小時。因為設置TSS的代碼包含了下面一行:

pcb->pcb_tss.tss_ioopt = sizeof(pcb->pcb_tss)

換句話說,操作系統告訴CPU,固定的TSS部分之後(偏移量68H)緊接著就是IOPB。這意味著,struct PCB中的所有軟體定義的欄位都會被CPU解釋為IOPB。而且的確會如此,因為設置TSS limit的代碼依然是:

setgdt(slot, &pcb->pcb_tss,sizeof(structpcb) -1,

SDT_SYS386TSS, SEL_KPL,,);

所以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。其結果就是,structPCB完全不再映射到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.37gdt.c

--- sys/arch/i386/i386/gdt.c7Mar201605:32:46-00001.37

+++ sys/arch/i386/i386/gdt.c23Jul201823:53:28-0000

@@-210,7+210,7@@ tss_alloc(structpcb *pcb)

intslot;

slot = gdt_get_slot();

- setgdt(slot, &pcb->pcb_tss,sizeof(structpcb) -1,

+ setgdt(slot, &pcb->pcb_tss,sizeof(structi386tss) -1,

SDT_SYS386TSS, SEL_KPL,,);

returnGSEL(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

譯者:彎月,責編:胡巍巍


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 CSDN 的精彩文章:

華為下調的公積金,是否讓自己人寒了心?
用機器學習預測誰將奪得世界盃冠軍?附完整代碼!

TAG:CSDN |