iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 18
1
Security

網路安全概述系列 第 18

番外篇: CPU 預設執行 bug, Spectre, CVE-2017-5753

  • 分享至 

  • xImage
  •  

前言

最近火紅的議題即是「CPU 預設執行的 bug」,然後常常聽到像是「效能會掉 30%」,「AMD 重返榮耀」等等的詞彙。

Google 有養了一個團隊,Project Zero。Project Zero 的功用是「透過黑別人,幫人家除錯,使大家變得安全」。Project Zero 在去年中左右就將這一波問題回報給各廠家(Intel/AMD/ARM)了,不過,直到最近才能夠公開討論。
這叫做 Responsible disclosure,先幫大家保護好使用者,不公開講 bug。不過若對方不願意修 bug,那就是另一回事了。例如 DJI 就是一個很優秀的示範,想必以後沒有人願意幫他找 bug,而寧願把 bug 拿去別的地方賣掉,或乾脆直接公開,附上 PoC,讓大家打爆你。不管選哪個,都不用受鳥氣,棒棒。

這次的問題包含了:

  • Spectre (CVE-2017-5753, CVE-2017-5715)
  • Meltdown (CVE-2017-5754)

以上兩個 bug 都會造成資訊的外洩(例如,讀別人程式的內容,從 user space 讀屬於作業系統核心的記憶體),然後都沒有資料竄改的問題。另外,兩個 bug 都必須要有「在目標機器上執行程式」的前置條件。(瀏覽器基本上都是在別人的機器上執行 JS,但不一定會 GG,見文末)

目前已知 Spectre 是影響 Intel/ARM/AMD 的處理器,而 Meltdown 只會影響 Intel 處理器。不過兩個 bug,都是打預測執行(下面會提到)這塊。

講解

處理器執行指令的流程

處理器當然是用來執行指令的。每個指令有四個階段:

古早時期的處理器,都是依據指令順序來執行指令。可以想成高階語言中「同步」的概念,所以如果一個指令是要等超級久的 I/O 動作,大家都要一起等超級久。
後來,有一種的設計模式叫做 "out-of-order execution",意思就是可以不依據順序來執行指令。這種設計模式下,處理指令的順序大概是這樣子的:

  1. 先讀下一個指令
  2. 將讀到的指令放入排程器
  3. 指令在排程器裡面等,直到該指令的目標都是可使用的狀態了,才從排程裡面拿出來。指令可以不依據當初進去的順序拿出來
  4. 將指令分配給負責該指令的運算單元,然後把指令「執行
  5. 將結果放到「結果池」(放了一堆運算結果的地方)內
  6. 結果池中的結果,一定要依照當初指令進來的順序來依序拿出去。如果指令有什麼寫東西的動作的話,在拿出來的時候,才可以將結果寫到目標去。這才能真的算是「執行完成」。

總而言之,東西可以不依據順序執行,但是一定要依據順序完成。

保護環

1200px-Priv_rings.svg.png

在資訊工程裡面,有一種叫做「保護環」的機制。
保護環的概念,提供了程式們不同的權限等級。從數字小到大,小的數字代表能力越大,大的數字代表能力越少。
各個環之間通常會有一個特殊的閘門,可以讓外層的環用某種方式去訪問內層的環。舉例來說,基本上,使用者執行的程式,都是在 Ring 3。音效卡驅動是在 Ring 2/Ring 1,所以使用者的程式不可以直接打開麥克風,必須要透過閘門才能夠去叫內層的東西做事情。

Meltdown 比較嚴重,可以一路從 Ring 3 突破到 Ring 0。但是 Spectre 理論上影響範圍只有 Ring 3。

預測執行

目前大部分的處理器都有 預測執行 的能力。預測執行是指說,在接收到某個作業之前,先猜出會做什麼東西,然後先做那件事。這樣一來,當那件事情真的出現時,執行速度看起來就會變快。

有預測執行能力的處理器,會依據之前執行的模式等等,來猜之後會走哪個路線,然後先把那條路線上的東西都給執行完。不過,因為是依據之前執行的模式,所以可以某程度上故意讓處理器的猜測失準(或是控制會猜測的路線)。
另外,因為這是用猜的,所以有可能猜錯。如果猜錯了,要直到一陣子之後,才會知道猜錯。從預測執行錯誤的指令到發現猜錯之間的時間,叫做 預測錯誤區間

Spectre

這次 Spectre 的問題,是因為預設執行在某些狀況下會出問題,所以可以用某些方式,去讀取屬於別人程式記憶體中的資料。(程式理論上是只能讀取屬於自己的資料的)。

這次文章內容參考 Project Zero

文件

我們先來看 Intel 自己的文件,這份有解釋「分支預測」。

2.3.2.3, Branch Prediction
分支預測是指,先猜接下來會遇到哪個分支,然後在遇到那個分支之前就先執行它。

2.3.5.2, L1 DCache
L1 暫存器可以在以下狀況下進行讀取:

  • 在預測執行的狀況下做讀取,毋須等待該未來分支執行完成
    ...

Intel's Software Developer's Manual 3A, 11.7, Implicit Caching (Pentium 4, Intel Xeon, and P6 family processors
當一塊記憶體變得有可能可以被快取時,即使該記憶體沒有依照標準的方式 (這裡原文是 _never have been accessed in the normal von Neumann sequence, 求大神幫翻) 被存取過,也可能會隱快取 (implict caching,意思就是自動幫你做快取)。P6 以後的處理器,因為有積極預取 (aggressive preferching)、分支預測、頁表 cache miss 的關係,所以都會做隱快取。由於軟體還沒有辦法在 386/486/Pentium 處理器上預測指令預取的行為的關係,所以隱快取在這些平台上都算是延伸行為,

因為很多詞在中文中找不太到,所以只能大略翻譯。我不確定翻譯是否正確的地方,會留下原文。

原理

這邊先借 Project Zero 的講解來用。

struct array {
    unsigned long length;
    unsigned char data[];
};
struct array *arr1 = ...;
unsigned long untrusted_offset_from_caller = ...;
if (untrusted_offset_from_caller < arr1->length) {
    unsigned char value = arr1->data[untrusted_offset_from_caller];
     ...
}

依據上方的 code,如果 arr1->length 沒有被快取到,處理器有可能就會將 arr1->data[untrusted_offset_from_caller] 的資料做預先讀取。這基本上是越界讀取了,不過在這狀況下沒差,因為執行到 if 那行時,處理器會發現這東東根本不會執行,所以會把所有預先執行的東西給扔了,把狀態回復。(所以這些指令都不會完成

但如果改一下,改成這樣:

struct array {
    unsigned long length;
    unsigned char data[];
};
struct array *arr1 = ...; /* small array */
struct array *arr2 = ...; /* array of size 0x400 */
/* >0x400 (OUT OF BOUNDS!) */
unsigned long untrusted_offset_from_caller = ...;
if (untrusted_offset_from_caller < arr1->length) {
    unsigned char value = arr1->data[untrusted_offset_from_caller]; /* value 即是越界讀到的值 */
    unsigned long index2 = ((value&1)*0x100)+0x200; /* index2 依據 value 產生兩個不同的數值, 0x200 和 0x300 */
    if (index2 < arr2->length) {
        unsigned char value2 = arr2->data[index2];
    }
}

假設執行到宣告 untrusted_offset_from_caller 附近之前,其他資料都被快取,除了 arr1->lengtharr2->data[0x200]arr2->data[0x300] 以外的狀況下,然後處理器又猜測這個片段會被執行的狀況下,處理器可能預先執行這些事情:

  • arr1->data[untrusted_offset_from_caller] 先做快取
  • arr2->data 也做快取,放進 L1 快取

然後繼續執行下去,因為會發現 if 根本成立不了,所以會跟之前一樣,把原本算完的東西給扔了,不過因為在預測執行錯誤的狀況下,不會把快取也砍了,所以這時候我們只要想辦法量測讀取 arr->data[0x200]arr->data[0x300] 的時間,看看哪個時間短,只要是時間短的,就是有被快取,然後就可以知道 value 的數值到底是 0 或是 1。

攻擊實務上的問題

前面提到,這東西要實作,一定要能夠在別人的機器上執行程式碼。所以通常有幾種狀況:

  • 真的寫這個樣子的 code,然後這樣執行
  • 直譯器或是 JIT 剛好產生出這種 code

像是 #2 的狀況,以瀏覽器執行 JS 來說,因為 JS 要轉成機器碼才能執行,所以要很剛好轉成這樣,才能成功。另外,這個攻擊必須要依靠準確的「計時」,所以目前最直接的防禦方式是直接把計時器的準確度變差。

另外,還要剛好分支預測都走到正確(發音:我們想要)的路線,才會成功。前面有提到如何惡搞預先執行,這個在之後會講到。


上一篇
番外篇: 談 CPU Bug、FDIV/F00F bug
下一篇
番外篇: CPU 預設執行 bug, Spectre: 進階版 combo 技
系列文
網路安全概述31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言