讀書時間:Meltdown 的攻擊方式

Meltdown 的論文可以在「Meltdown (PDF)」這邊看到。這個漏洞在 Intel 的 CPU 上影響最大,而在 AMD 是不受影響的。其他平台有零星的消息,不過不像 Intel 是這十五年來所有的 CPU 都中獎... (從 Pentium 4 以及之後的所有 CPU)

Meltdown 是基於這些前提,而達到記憶體任意位置的 memory dump:

  • 支援 µOP 方式的 out-of-order execution 以及當失敗時的 rollback 機制。
  • 因為 cache 機制造成的 side channel information leak。
  • 在 out-of-order execution 時對記憶體存取的 permission check 失效。

out-of-order execution 在大學時的計算機組織應該都會提到,不過我印象中當時只講「在確認不相干的指令才會有 out-of-order」。而現代 CPU 做的更深入,包括了兩個部份:

  • 第一個是 µOP 方式,將每個 assembly 拆成更細的 micro-operation,後面的 out-of-order execution 是對 µOP 做。
  • 第二個是可以先執行下去,如果發現搞錯了再 rollback。

像是下面的 access() 理論上不應該被執行到,但現代的 out-of-order execution 會讓 CPU 有機會先跑後面的指令,最後發現不該被執行到後,再將 register 與 memory 的資料 rollback 回來:

而 Meltdown 把後面不應該執行到 code 放上這段程式碼 (這是 Intel syntax assembly):

其中 mov al, byte [rcx] 應該要做記憶體檢查,確認使用者是否有權限存取那個位置。但這邊因為連記憶體檢查也拆成 µOP 平行跑,而產生 race condition:

Meltdown is some form of race condition between the fetch of a memory address and the corresponding permission check for this address.

而這導致後面這段不該被執行到的程式碼會先讀到資料放進 al register 裡。然後再去存取某個記憶體位置造成某塊記憶體位置被讀到 cache 裡。

造成 cache 內的資料改變後,就可以透過 FLUSH+RELOAD 技巧 (side channel) 而得知這段程式碼讀了哪一塊資料 (參考之前寫的「Meltdown 與 Spectre 都有用到的 FLUSH+RELOAD」),於是就能夠推出 al 的值...

而 Meltdown 在 mov al, byte [rcx] 這邊之所以可以成立,另外一個需要突破的地方是 [rcx]。這邊 [rcx] 存取時就算沒有權限檢查,在 virtual address 轉成 physical address 時應該會遇到問題?

原因是 LinuxOS X 上有 direct-physical map 的機制,會把整塊 physical memory 對應到 virtual memory 的固定位置上,這些位置不會再發給 user space 使用,所以是通的:

On Linux and OS X, this is done via a direct-physical map, i.e., the entire physical memory is directly mapped to a pre-defined virtual address (cf. Figure 2).

而在 Windows 上則是比較複雜,但大部分的 physical memory 都有對應到 kernel address space,而每個 process 裡面也都還是有完整的 kernel address space (只是受到權限控制),所以 Meltdown 的攻擊仍然有效:

Instead of a direct-physical map, Windows maintains a multiple so-called paged pools, non-paged pools, and the system cache. These pools are virtual memory regions in the kernel address space mapping physical pages to virtual addresses which are either required to remain in the memory (non-paged pool) or can be removed from the memory because a copy is already stored on the disk (paged pool). The system cache further contains mappings of all file-backed pages. Combined, these memory pools will typically map a large fraction of the physical memory into the kernel address space of every process.

這也是 workaround patch「Kernel page-table isolation」的原理 (看名字大概就知道方向了),藉由將 kernel 與 user 的區塊拆開來打掉 Meltdown 的攻擊途徑。

而 AMD 的硬體則是因為 mov al, byte [rcx] 這邊權限的檢查並沒有放進 out-of-order execution,所以就避開了 Meltdown 攻擊中很重要的一環。

Meltdown 與 Spectre 都有用到的 FLUSH+RELOAD

MeltdownSpectre 攻擊裡都有用到的 FLUSH+RELOAD 技巧。這個技巧是出自於 2013 年的「Flush+Reload: a High Resolution, Low Noise, L3 Cache Side-Channel Attack」。當時還因此對 GnuPG 發了一個 CVE-2013-4242

FLUSH+RELOAD 是希望透過 shared memory & cache 得到 side channel information,藉此突破安全機制。

論文裡面提到兩個攻擊模式,一種是在同一個 OS 裡面 (same-OS),另外一種是在同一台機器,但是是兩個不同的 VM (cross-VM)。攻擊的前提是要拿到與 GnuPG process 相同的 shared memory。兩個環境的作法都是透過 mmap() GnuPG 的執行檔以取得 shared memory。

在 same-OS 的情況下會使用同一個 process:

To achieve sharing, the spy mmaps the victim’s executable file into the spy’s virtual address space. As the Linux loader maps executable files into the process when executing them, the spy and the victim share the memory image of the mapped file.

在 cross-VM 的情況下會因為 hypervisor 會 dedup 而產生 shared memory:

For the cross-VM scenario we used two different hypervisors: VMware ESXi 5.1 on the HP machine and Centos 6.5 with KVM on the Dell machine. In each hypervisor we created two virtual machines, one for the victim and the other for the spy. The virtual machines run CentOS 6.5 Linux. In this scenario, the spy mmaps a copy of the victim’s executable file. Sharing is achieved through the page de-duplication mechanisms of the hypervisors.

接下來就能夠利用 cache 表演了。基本原理是「存取某一塊記憶體內容,然後計算花了多久取得,就能知道這次存取是從 L1、L2、L3 還是記憶體取得」。所以 FLUSH+RELOAD 就設計了三個步驟:

  • During the first phase, the monitored memory line is flushed from the cache hierarchy.
  • The spy, then, waits to allow the victim time to access the memory line before the third phase.
  • In the third phase, the spy reloads the memory line, measuring the time to load it.

先 flush 掉要觀察的記憶體位置 (用 clflush),然後等待一小段時間,接著掃記憶體區塊,透過時間得知有哪些被存取過 (就會比較快)。這邊跟 cache 架構有關,你不能想要偷看超過 cache 大小的量 (這樣會被 purge 出去),所以通常是盯著關鍵的部份就好。

接著是要搞 GnuPG,先看他在使用 RSA private key 計算的程式碼:

而依照這段程式碼挑好位置觀察後,就開始攻擊收資訊。隨著時間變化就可以看到這樣的資訊:

然後可以觀察出執行的順序:

於是就能夠依照執行順序推敲出 RSA key 了,而實際測試的成果是這樣,在一次的 decrypt 或是 sign 就把 RSA key 還原的差不多了 (96.7%):

We demonstrate the efficacy of the FLUSH+RELOAD attack by using it to extract the private encryption keys from a victim program running GnuPG 1.4.13. We tested the attack both between two unrelated processes in a single operating system and between processes running in separate virtual machines. On average, the attack is able to recover 96.7% of the bits of the secret key by observing a single signature or decryption round.

知道了這個方法後,看 Meltdown 或是 Spectre 才會知道他們用 FLUSH+RELOAD 的原因... (因為在 Meltdown 與 Spectre 裡面就只有帶過去)

Facebook 與 Google Chrome 以及 Firefox 的人合作降低 Reload 使用的資源

Facebook 花了不少時間對付 reload 這件事情:「This browser tweak saved 60% of requests to Facebook」。

Facebook 的人發現有大量對靜態資源的 request 都是 304 (not modified) 回應:

In 2014 we found that 60% of requests for static resources resulted in a 304. Since content addressed URLs never change, this means there was an opportunity to optimize away 60% of static resource requests.

Google Chrome 很明顯偏高:

於是他們找出原因後,發現 Google Chrome 只要 POST 後的頁面都會 revalidate:

A piece of code in Chrome hinted at the answer to our question. This line of code listed a few reasons, including reload, for why Chrome might ask to revalidate resources on a page. For example, we found that Chrome would revalidate all resources on pages that were loaded from making a POST request.

然後在討論後認為這個行為不必要,就修掉了,可以看到降了非常多:

We worked with Chrome product managers and engineers and determined that this behavior was unique to Chrome and unnecessary. After fixing this, Chrome went from having 63% of its requests being conditional to 24% of them being conditional.

但還是很明顯比起其他瀏覽器偏高不少,在追問題後發現當輸入同樣的 url 時 (像是 Ctrl-L 或是 Cmd-L 然後直接按 enter),Google Chrome 會當作 reload:

The fact that the percentage of conditional requests from Chrome was still higher than other browsers seemed to indicate that we still had some opportunity here. We started looking into reloads and discovered that Chrome was treating same URL navigations as reloads while other browsers weren't.

不過這次推出修正後發現沒有大改變:(拿 production 測試 XDDD)

Chrome fixed the same URL behavior, but we didn't see a huge metric change. We began to discuss changing the behavior of the reload button with the Chrome team.

後來是針對 reload button 的行為修改,max-age 很長的就不 reload,比較短的就 reload。算是一種 workaround:

There was some debate about what to do, and we proposed a compromise where resources with a long max-age would never get revalidated, but that for resources with a shorter max-age the old behavior would apply. The Chrome team thought about this and decided to apply the change for all cached resources, not just the long-lived ones.

Google 也發了一篇說明這個新功能:「Reload, reloaded: faster and leaner page reloads」。

當 Facebook 的人找 Firefox 的人時,Firefox 決定另外定義哪些東西在 reload 時不需要 revalidate,而不像 Google Chrome 的 workaround:

Firefox chose to implement this directive in the form of a cache-control: immutable header.

Firefox 的人也寫了一篇「Using Immutable Caching To Speed Up The Web」解釋這個新功能。

所以之後規劃前後端的架構時又有東西要考慮進去...

用 BrowserSync 測試多個平台...

BrowserSync 是用 node.js 寫的工具,可以同時測試一堆 device,修改後不用按 reload,印象中已經有套件可以做類似的事情?

一般用 npm 裝就可以了:

npm install browser-sync

最簡單的方法是直接執行 browser-sync,執行後會出現像這樣的訊息:

<script src='//192.168.1.1:3000/socket.io/socket.io.js'></script>
<script>var ___socket___ = io.connect('http://192.168.1.1:3000');</script>
<script src='//192.168.1.1:3001/client/browser-sync-client.0.6.0.js'></script>

把這段程式碼貼到 body 的最後面就可以了,當 BrowerSync 偵測到檔案有更新時會透過 server push 機制重刷頁面。

另外,也可以產生 bs-config.js 修改設定:

browser-sync --init

更完整的說明可以從「Options」這頁找到。