處理器高危漏洞無人倖免?樹莓派:我們不受影響

黃小天發表於2018-01-06

過去幾天,對 Meltdown 和 Spectre 安全漏洞的討論甚囂塵上。該漏洞影響了所有的現代英特爾處理器,Spectre 還影響了 AMD 處理器和 ARM 核心。Spectre 漏洞使得攻擊者可以繞過軟體檢查讀取當前地址空間中的任意位置資料;Meltdown 漏洞使得攻擊者可以讀取作業系統核地址空間的任意位置資料(使用者程式通常不可訪問該資料)。這兩種漏洞皆通過邊通道攻擊(side-channel attack)利用很多現代處理器都有的效能特徵(快取和推測執行)洩漏資料。近日,樹莓派創始人 Eben Upton 稱樹莓派不受這些漏洞的影響,並撰文詳解其原因。

谷歌 Project Zero 團隊發現的漏洞分別被稱為「Meltdown」和「Specter」。這些漏洞允許惡意程式從其它程式的記憶體中竊取資訊,這意味著惡意程式可以監聽密碼、賬戶資訊、金鑰及理論上儲存在程式中的任何內容。

其中,「Meltdown」影響英特爾處理器,它打破了使用者應用程式和作業系統之間最基本的隔離。這種攻擊允許程式訪問其它程式和作業系統的記憶體,這可能導致資料洩露。而「Spectre」除了能影響英特爾處理器外,還能影響 AMD 與 ARM 架構的大量處理器,這意味著除伺服器與個人電腦以外,智慧手機等終端裝置也會受到影響,幾乎所有現代計算機處理器均無法倖免。它打破了不同應用程式之間的隔離,這意味著,攻擊者可以使用惡意程式來獲取被隔離的私有資料。

英特爾近日表示,在未來數週內將有軟體補丁釋出。儘管大多數 PC 使用者不會受到影響,但安全補丁會導致處理器 0-30% 的運算速度下降。

處理器高危漏洞無人倖免?樹莓派:我們不受影響

本文介紹現代處理器設計的一些概念,使用簡單的 Python 程式解釋這些概念,比如:

t = a+b
u = c+d
v = e+f
w = v+g
x = h+i
y = j+k

儘管你的計算機處理器不會直接執行 Python,但這裡的語句足夠簡單,大致相當於簡單的機器指令。本文不詳述過多處理器設計中的重要細節(主要是 pipelining 和暫存器重新命名),它們對理解 Spectre 和 Meltdown 的工作原理不太重要。

想全面瞭解處理器設計和現代計算機架構,可參閱 Hennessy 和 Patterson 的經典著作《Computer Architecture: A Quantitative Approach》。

什麼是標量處理器?

最簡單的現代處理器每次迴圈執行一個指令,我們稱之為標量處理器。上述示例在標量處理器上需要執行六次迴圈。

樹莓派 1 和樹莓派 Zero 中使用的 Intel 486 和 ARM1176 都是標量處理器。

什麼是超標量處理器?

很明顯,加速標量處理器的方式就是提高其時脈頻率(clock speed)。但是,我們很快就到達處理器內部邏輯閘執行的極限;因此處理器設計人員開始尋找一次性處理多件事情的方式。

有序超標量處理器檢查收到的大批指令,嘗試在一個 pipeline 中一次性執行多個指令,這取決於指令之間的依賴關係。依賴關係很重要:你或許認為雙向超標量處理器可以將 6 個指令配對執行,如下所示:

t, u = a+b, c+d
v, w = e+f, v+g
x, y = h+i, j+k

但是這沒有作用:我們必須先計算 v 再計算 w,即第三個和第四個指令無法同時執行。雙向超標量處理器實際上無法找到與第三個指令配對的指令,因此,該示例將執行四個迴圈:

t, u = a+b, c+d
v    = e+f                   # second pipe does nothing here
w, x = v+g, h+i
y    = j+k

超標量處理器包括 Intel Pentium 以及樹莓派 2 和樹莓派 3 分別使用的 ARM Cortex-A7 和 Cortex-A53。樹莓派 3 的時脈頻率只比樹莓派 2 高 33%,但效能大約是後者的 2 倍:部分原因在於 Cortex-A53 超出 Cortex-A7 的對大量指令的配對執行能力。

什麼是無序處理器(out-of-order processor)?

回到我們的示例,我們可以看到即使 v 和 w 之間存在依賴關係,我們也可以找到其他獨立的指令填補第二次迴圈中空的 pipe。無序超標量處理器能夠打亂指令的順序(同樣受限於指令之間的依賴關係)以保持每個 pipeline 都處於忙碌狀態。

無序處理器可以有效交換 w 和 x 的順序:

t = a+b
u = c+d
v = e+f
x = h+i
w = v+g
y = j+k

允許執行三次迴圈:

t, u = a+b, c+d
v, x = e+f, h+i
w, y = v+g, j+k

無序處理器包括 Intel Pentium 2(以及大部分後續 Intel 和 AMD x86 處理器,除了一些 Atom 和 Quark 裝置)和很多近期的 ARM 處理器,如 Cortex-A9、-A15、-A17、-A57。

什麼是分支預測器(branch predictor)?

上述示例是直線式程式碼塊。真正的程式不是這樣的:他們還包括正向分支(用於實現條件運算,如 if 語句)、反向分支(用於實現 loop)。分支可能是無條件的(通常被採用),也可能是有條件的(是否採用取決於計算值)。

獲取指令時,處理器可能遇到依賴於計算值的條件分支(而該值目前尚未計算出)。為了避免停頓,處理器必須猜測下一個要獲取的指令:記憶體順序(對應未採用分支)或分支目標(對應採用分支)上的下一個指令。分支預測器通過收集某一個分支之前被採用頻率的相關統計資料,幫助處理器猜測該分支是否被採用。

現在分支預測器非常複雜,可以生成非常準確的預測。樹莓派 3 的額外效能部分是由於 Cortex-A7 和 Cortex-A53 之間分支預測的改進。但是,攻擊者也可以通過執行精心設計的一系列分支,誤訓練分支預測器作出較差的預測。

什麼是推測?

重排序順序指令(reordering sequential instruction)是一種恢復指令級別並行化的強大方法,但是由於處理器變得更寬(能夠一次執行三個或四個指令),保證所有 pipeline 處於忙碌狀態變得更難了。因此,現代處理器提高了推測能力。推測執行可以處理並不需要的指令:這樣就可以保證 pipeline 處於忙碌狀態,如果最後該指令沒有被執行,我們只需要放棄結果就可以了。

推測執行不必要的指令(以及支援推測和重排序的基礎架構)需要耗費大量能源,但是在很多情況下為了獲取單執行緒效能的提升,這種方法是值得的。分支預測器用於選擇通過程式最可能的路徑,最大化推測獲得收益的可能性。

為了展示推測的好處,我們可以看一下另一個示例:

t = a+b
u = t+c
v = u+d
if v:
   w = e+f
   x = w+g
   y = x+h

現在,我們具備從 t 到 u 到 v、從 w 到 x 到 y 的依賴關係,那麼沒有推測的雙向無序處理器無法填充第二個 pipeline。它用三次迴圈來計算 t、u 和 v,之後處理器知道 if 語句的主體是否被執行,然後用三次迴圈來計算 w、x 和 y。假設 if(由一個分支指令實現)使用了一次迴圈,那麼該示例可以執行四次(v 是零)或七次迴圈(v 不是零)。如果分支預測器表明 if 語句的主體很可能被執行,那麼推測可以有效打亂程式,如下:

t = a+b
u = t+c
v = u+d
w_ = e+f
x_ = w_+g
y_ = x_+h
if v:
   w, x, y = w_, x_, y_

因此現在我們有了額外的指令級別的並行來保持 pipeline 繁忙:

t, w_ = a+b, e+f
u, x_ = t+c, w_+g
v, y_ = u+d, x_+h
if v:
   w, x, y = w_, x_, y_

迴圈計數在推測性無序處理器中變得不太明確,但是 w、x 和 y 的分支和條件更新(幾乎)是空閒的,因此上述示例幾近於執行三個迴圈。

什麼是快取?

在過去,處理器速度與記憶體訪問速度成正比。我的 BBC Micro(2MHz 6502),可以每 2μs(微秒)執行一次指令,儲存週期為 0.25μs。在接下來的 35 年中,處理器已經變的非常快,但是記憶體幾乎沒變化:樹莓派 3 中的一個 Cortex-A53 可以每 0.5ns(納秒)執行一次指令,但是可能需要 100ns 才能訪問主存。

a = mem[0]
b = mem[1]

需要 200ns。

但在實踐中,程式傾向於以相對可預測的方式訪問記憶體,同時展示時間區域性性(如果我訪問一個定位,我很可能很快再次訪問它)和空間區域性性(如果我訪問一個定位,我很可能很快訪問附近的位置)。快取利用這些屬性來降低訪問記憶體的平均成本。

快取是一個小的片上記憶體,接近於處理器,儲存最近使用的位置(及其近鄰)內容的副本,以便在隨後的訪問中可以快速獲取。藉助快取,上述示例的執行將稍微超過 100ns:

a = mem[0]    # 100ns delay, copies mem[0:15] into cache
b = mem[1]    # mem[1] is in the cache

從 Spectre 和 Meltdown 的角度來看,最重要的一點是你可以對記憶體訪問的時間進行計時,你可以知道訪問的地址是在快取之中(短時)或者不在(長時)。

什麼是邊通道?

來自維基百科:

「邊通道攻擊是基於從密碼系統的物理實現獲得的資訊的任何攻擊,而不是演算法中的蠻力或理論弱點(相較於密碼分析學)。例如,定時資訊、功耗、電磁洩漏甚至聲音都可以提供額外的資訊源,這些資訊可被用來破解系統。」

Spectre 和 Meltdown 屬於邊通道攻擊,通過定時來觀察快取中是否有另一個可訪問的位置,以推斷記憶體位置的內容,這些內容通常不應該被訪問。

把它放在一起

現在讓我們看看如何結合推測和快取以允許類似 Meltdown 的攻擊。考慮下面這個示例,它是一個有時讀取所有非法(核心)地址的使用者程式,並導致錯誤(崩潰):

t = a+b
u = t+c
v = u+d
if v:
   w = kern_mem[address]   # if we get here, fault
   x = w&0x100
   y = user_mem[x]

現在,假設我們可以訓練分支預測器,使其相信 v 很可能是非零的,那麼我們的無序雙向超標量處理器就會混洗程式,像這樣:

t, w_ = a+b, kern_mem[address]
u, x_ = t+c, w_&0x100
v, y_ = u+d, user_mem[x_]

if v:
   # fault
   w, x, y = w_, x_, y_      # we never get here

即使處理器總是推測性地讀取核心地址,它必須推遲產生的錯誤,直到知道 v 是非零。從表面上看,這是安全的,因為:

  • v 是零,所以非法讀取的結果不會被提交給 w
  • v 是非零,但在讀取結果被提交給 w 之前發生了錯誤

然而,假設我們在執行程式碼之前重新整理快取,並排列 a、b、c、d 以使 v 實際上為零。現在第三個迴圈中的推測性讀取為:

v, y_ = u+d, user_mem[x_]

其將依賴非法讀取結果的第八位獲取使用者地址 0x000 或 0x100,並把地址及其近鄰載入進快取。由於 v 是零,推測性指令的結果將被擯棄,執行將繼續。如果我們隨後訪問其中一個地址,就可以決定哪個地址在快取之中。恭喜:你剛剛從核心地址空間讀取了一個位!

真正的 Meltdown 實際上要比這更復雜(特別是,為了避免錯誤訓練分支預測器,作者無條件地優先執行非法讀取,並處理產生的異常),但原理是相同的。Spectre 使用相似方法來顛覆軟體陣列邊界檢查。

結論

現代處理器竭盡全力保持抽象,從而成為直接訪問記憶體的有序標量機器,而事實上,使用包括快取、指令重排序和推測在內的大量技術來提供比簡單處理器更高的效能有望成為現實。Meltdown 和 Spectre 就是當我們在抽象的語境中推理安全性,然後在抽象與現實之間遇到細微差別時會發生的事情的例項。

樹莓派使用的 ARM1176、Cortex-A7 和 Cortex-A53 核心中推測的缺失使我們免於此類攻擊。處理器高危漏洞無人倖免?樹莓派:我們不受影響

原文連結:

相關文章