JIT引擎觸發RowHammer可行性研究

wyzsk發表於2020-08-19
作者: xlab · 2015/05/26 15:46

0x00 前言


作者: R3dF09@騰訊玄武實驗室

2015 年 3 月 Google Project Zero 發表文章 Exploiting the DRAM rowhammer bug to gain kernel privileges。由於文中提到的缺陷比較難以修復,需要更新 BIOS 來提 高記憶體重新整理的速度,引起了人們的擔憂。然而由於 RowHammer 的執行需要在目 標主機上執行特定的彙編程式碼,實施攻擊存在很大的難度。

本文旨在研究能否透過 Javascript 等指令碼語言的動態執行觸發 RowHammer, 如果能夠成功將極大增加 RowHammer 的攻擊性。為了驗證該思路,本文分析了 Java Hotspot、Chrome V8、.NET CoreCLR 以及 Firefox SpiderMonkey 的實現機制並 給出了可行性分析。

遺憾的是我們在這幾個程式中,沒有找到最優的利用方式。要麼不存在相關 的指令,要麼指令無法達到 RowHammer 要求,要麼需要有額外的操作更改執行 環境才能觸發,缺乏實際的攻擊意義。

0x01 RowHammer


本節將簡要回顧 RowHammer 存在的原理,其觸發的機制,已經在利用時將 面臨到的一些挑戰。

1.1 What`s RowHammer?

RowHammerDDR3 記憶體中存在的問題,透過頻繁的訪問記憶體中的一行(row) 資料,會導致臨近行(row)的資料發生位反轉。如圖 1.1(a)所示,記憶體是由一系列 記憶體單元構成的二維陣列。如圖 1.1(b)所示每一個記憶體單元由一個電晶體和一個 電容組成,電晶體與 wordline 相連,電容負責儲存資料。DRAM 的每一行(row) 有自己的 wordline,wordline 需要置高電壓,特定行(row)的資料才能夠訪問。當 某一行的 wordline 置高電壓時,該行的資料就會進入 row-buffer。當 wordline 頻 繁的充放電時,就可能會導致附近 row 的儲存單元中的電容放電,如果在其被刷 新之前,損失過多的電壓就會導致記憶體中的資料發生變化。

圖 1.2 所示是一塊記憶體,其中一個 row 為 64kb(8KB)大小, 32k 個 row 組成一 個 Bank, 8 個 Bank 組成一個 Rank, 該 Rank 為 2G。此處需要注意不同的 Bank 有 專用的 row-buffer,訪問不同 Bank 中的 row 不會導致 wordline 的充放電。

記憶體中的電壓是不能長期儲存的,需要不停的對其進行重新整理,重新整理的速度為 64ms,所以必須在 64ms 內完成 RowHammer 操作。

enter image description here

enter image description here

1.2 RowHammer 觸發的方法


表 1.1 所示為 Google Project Zero 所給出的可以觸發 RowHammer 的程式碼段。

表 1.1

code1a:
  mov (X), %eax // Read from address X
  mov (Y), %ebx // Read from address Y 
  clflush (X) // Flush cache for address X 
  clflush (Y) // Flush cache for address Y 
  jmp code1a

其中 x, y 地址的選擇非常重要,x, y 必須要在同一個 Bank,不同的 row 中。

因為不同的 Bank 有專用的 row-buffer。如果 x, y 在同一個 row 中就不會對 wordline 進行頻繁的充放電,也就不會觸發 RowHammer

上述程式碼只是一種有效的測試方法,但並不是惟一的,歸根到底我們所需要 的就是在 64ms 內讓一個 wordline 頻繁的充放電。

1.3 觸發 RowHammer 指令


為了頻繁的使 wordline 充放電,必須考慮 CPU 的 Cache, 如果當前地址在Cache 裡面就不會訪問記憶體,也就不會導致 wordline 的充放電情況。

表 1.2

指令 作用
CLFLUSH 將資料從 Cache 中擦除
PREFETCH 從記憶體中讀取資料並存放在 Cache 中
MOVNT* 不經過 Cache 直接運算元據

表 1.2 中的指令都可以用來頻繁的訪問一個記憶體地址,並使相應的 wordline 充放電,如果要觸發 RowHammer, 需要上述指令的配合才能完成。

(注: 這些指令並不是惟一的觸發方法,比如透過分析實體地址和 L3 Cache 的對映關係演算法(不同的 CPU 架構實現可能不同),找到對映到同一個 Cache set 的一系列地址,透過重複訪問這一系列的地址即可觸發 RowHammer。)

0x02 指令碼層面觸發 RowHammer


Google Project Zero 給出的 POC 是直接以彙編的方式來執行,可以用來驗證 記憶體是否存在安全問題。當前指令碼語言大都存在 JIT 引擎,如果能夠透過指令碼控 制 JIT 引擎直接觸發 RowHammer,將會具有更大的攻擊意義。為了分析其可行性,本節研究了 Java HotspotChrome V8 等執行引擎的執行機制。

2.1 Java Hotspot


HotspotOracle JDK 官方的預設虛擬機器,主要用來解釋執行 Java 位元組碼,其 原始碼位於 Openjdk 下 hotspot 目錄,可以獨立編譯。Java 位元組碼是堆疊式指令集, 指令數量少,共有 256 條指令,完成了資料傳送、型別轉換、程式控制、物件操 作、運算和函式呼叫等功能。Java 位元組碼儲存在 class 檔案中,作為 Hotspot 虛 擬機的輸入,其在一定程式上是使用者可控的。那麼能否透過構造 class 檔案,使 得 Hotspot 在執行時完成 RowHammer 呢?

Java Hotspot 預設對位元組碼進行解釋執行,當某方法被頻繁呼叫,並且達到一定的閾值,即會呼叫內建的 JIT 編譯器對其進行編譯,在下次執行時直接呼叫編 譯生成的程式碼。

Java 位元組碼直譯器有兩個實現,分別為模版直譯器和 C++直譯器,Hotspot 默 認使用模版直譯器。Java 的 JIT 編譯器有三個實現,分別為客戶端編譯器(C1 編 譯器)、伺服器端編譯器(C2 編譯器)以及 Shark 編譯器(基於 LLVM)的編譯 器。

圖 2.1 所示為 Java 在不同平臺下使用的虛擬機器。

enter image description here

enter image description here

2.1.1 模版直譯器觸發 RowHammer?

a) 模版直譯器工作原理

模版直譯器是一種比較靠近底層的直譯器實現,每一個位元組碼對應一個模版, 所有的模版組合在一起構成一個模板表。每一個模版實質上都是一段彙編程式碼, 在虛擬機器建立階段進行初始化。在執行 class 檔案的時候,遍歷位元組碼,每檢測 到一個位元組碼就呼叫相應的彙編程式碼塊進行執行,從而完成對於位元組碼的解釋執 行。

為了完成對於位元組碼的解釋執行,Hotspot 在初始化時還會生成多種彙編代 碼塊,用來輔助位元組碼的解釋,比如函式入口程式碼塊,異常處理程式碼塊等。檢視 Hotspot 中生成的程式碼塊和模版可以採用命令 java –XX:+PrintInterpreter 指令來 完成。

針對各個位元組碼的模版中彙編程式碼比較龐大,比如位元組碼 invokevirtual 對應 的程式碼塊共有 352 bytes,位元組碼 putstatic512 bytes

b) 直譯器能否觸發 RowHammer?

位元組碼在解釋執行的時候會產生彙編程式碼,那麼是否可以透過 class 檔案讓直譯器生成 RowHammer 需要的指令呢?

透過分析,位元組碼對應的模版和輔助程式碼塊的指令中沒有 prefetch, clflush 以 及 movnt*系列指令,所以直接透過構造位元組碼,然後使用模版直譯器來觸發 RowHammer 是不可行的。

***||2.1.2 JIT 編譯器觸發 RowHammer?

a) C1 編譯器工作原理  JIT 編譯器也是一種編譯器,只不過其是在程式動態執行過程中在需要的時候 對程式碼進行編譯。其編譯流程與一般編譯器基本相同。

C1 編譯器是客戶端使用的 JIT 編譯器實現,其主要追求編譯的速度,對於代 碼的最佳化等要相對保守。

Hotspot 編譯器預設是非同步編譯,有執行緒 CompilerThread 負責對特定的方法 進行呼叫,當方法呼叫次數達到一定閾值時將會呼叫 JIT 編譯器對方法進行編譯, 該閾值預設為 10000 次,可以透過 –XX:+CompileThreshold 引數來設定閾值。

enter image description here

圖 2.2 從程式碼級別看,C1 編譯器共包含如下幾個步驟

表 2.1

typedef enum {
  _t_compile,
  _t_setup,
  _t_optimizeIR,
  _t_buildIR,
  _t_emit_lir,
  _t_linearScan,
  _t_lirGeneration,
  _t_lir_schedule,
  _t_codeemit,
  _t_codeinstall,
  max_phase_timers
} TimerName;

C1 編譯器執行流程大致如圖 2.2 所示:

1) 生成 HIR (build_hir)

C1 編譯器首先分析 JVM 位元組碼流,並將其轉換成控制流圖的形式,控制流 圖的基本塊使用 SSA 的形式來表示。HIR 是一個層級比較高的中間語言表示 形式,離機器相關的程式碼還有一定的距離。

2) 生成 LIR (emit_lir)

遍歷控制流圖的各個基本塊,以及基本塊中個各個語句,生成相應的 LIR 形 式,LIR 是一個比較接近機器語言的表現形式,但是還不是機器可以理解的 程式碼。

3) 暫存器分配

LIR 中使用的是虛擬暫存器,在該階段必須為其分配真實可用的暫存器。C1為了保證編譯的速度採用了基於線性掃描的暫存器分配演算法

4) 生成目的碼

真正生成平臺相關的機器程式碼的過程,在該階段遍歷 LIR 中的所有指令,並 生成指令相關的彙編程式碼。主要是使用了 LIR_Assembler 類。

表 2.2

#!c++
LIR_Assembler lir_asm(this); 
lir_asm.emit_code(hir()->code());

透過遍歷 LIR_List 依次呼叫,依次呼叫各個指令相關的 emit_code(如表 2-3), LIR 中的指令都是繼承自 LIR_Op

表 2.3

#!c++
op->emit_code(this);

以 LIR_Op1 為例,其 emit_code 方法為

表 2.4

#!c++
void LIR_Op1::emit_code(LIR_Assembler* masm) { //emit_code 
    masm->emit_op1(this);
}

假設 LIR_Op1 的操作碼為 LIR_prefetchr

表 2.5

#!c++
case lir_prefetchr:
  prefetchr(op->in_opr());
  break;

最終會呼叫 prefetchr 函式,該函式為平臺相關的,不同的平臺下實現不同, 以 x86 平臺為例,其實現位於assembler_x86.cpp

表 2.6

#!c++
void Assembler::prefetchr(Address src) {
   assert(VM_Version::supports_3dnow_prefetch(), "must support");
   InstructionMark im(this);
   prefetch_prefix(src);
   emit_byte(0x0D);
   emit_operand(rax, src); // 0, src
}

最終將會生成相應的機器碼。

b) 能否觸發 RowHammer

C1 編譯器是否能夠觸發 RowHammer? 經過分析發現,在 x86 平臺下封裝了 prefetch 相關的指令,確實是有希望控制產生 prefetch 指令。

從底層向上分析,如果要生成 prefetch 指令,在 LIR 層需要出現 LIR_op1 操 作,且操作碼需要為 lir_prefetchr 或者 lir_prefetchw,進一步向上層分析,要在 LIR 層出現這樣的指令,在從位元組碼到 HIR 的過程中必須能夠呼叫到 GraphBuilder::append_unsafe_prefetch 函 數 。 該 方 法 在 GraphBuilder::try_inline_instrinsics 函 數 中 調 用 , 進 一 步 分 析 只 需 調 用 sun.misc.unsafe 的 prefetch 操作即可觸發。透過深入分析,Hotspot 確實是支援 prefetch 操作,然而在 Java 的執行庫 rt.jar 中,sun.misc.unsafe 並沒有宣告 prefetch 操作,導致無法直接呼叫,需要更改 rt.jar才能觸發成功。這樣就失去了攻擊的 意義。

在 Hotspot 中還存在 clflush 這種指令,在 hotspot 的初始化階段,其會生成 一個程式碼塊。如下所示:

表 2.7

#!c++
__ bind(flush_line);
__ clflush(Address(addr, 0)); //addr: address to flush
__ addptr(addr, ICache::line_size);
__ decrementl(lines); //lines: range to flush 
__ jcc(Assembler::notZero, flush_line);

該部分程式碼在 C1 編譯器編譯完成之後有呼叫

表 2.8

#!c++
// done
masm()->flush(); //invoke ICache flush

對當前程式碼儲存的區域進行 cache flush

表 2.9

#!c++
void AbstractAssembler::flush() {
    sync();
    ICache::invalidate_range(addr_at(0), offset());
}

這種方法可以對記憶體做 cache flush, 主要問題在於程式碼儲存的區域在堆中是 隨機分配的,無法直接指定 cache flush 的區域,而且由於涉及到編譯的操作,無 法在短時間內大量產生 clflush 指令。

c) 其它編譯器實現

C2 編譯器與 C1 編譯器有一定的相似性又有很大的不同,由於主要在伺服器 端使用所以 C2 編譯器更加註重編譯後程式碼的執行效率,所有在編譯過程中相對 C1 編譯器做了大量的最佳化操作,但是在生成彙編程式碼的時候兩者使用的是同一 個抽象彙編,所以 C2 編譯器與 C1 編譯器應該大體相同,能夠生成 prefetch 指令, 但是在預設的情形下無法直接使用。

Shark 編譯器是基於 LLVM 實現的,一般都不會開啟,沒有對該編譯器進行進 一步的分析。

2.2 Chrome V8


V8 是 Google 開源的 Javascript 引擎,採用 C++編寫,可獨立執行。V8 會直接 將 JavaScript 程式碼編譯成本地機器碼,沒有中間程式碼,沒有直譯器。其執行機制 是將 Javascript 程式碼轉換成抽象語法樹,然後直接 walk 抽象語法樹,生成相應的 機器碼。

在 V8 生成機器碼的過程中無法生成 prefetch, clflush, movnt*系列指令。但是 在 V8 執行的過程中可能會引入 prefetch 指令。

產生 prefetch 的函式為

表 2.10

#!c++
MemMoveFunction CreateMemMoveFunction() {

表 2.11

#!c++
__ prefetch(Operand(src, 0), 1); 
__ cmp(count, kSmallCopySize); 
__ j(below_equal, &small_size); 
__ cmp(count, kMediumCopySize); 
__ j(below_equal, &medium_size); 
__ cmp(dst, src);
__ j(above, &backward);

該函式的主要作用是當緩衝區無法滿足指令的儲存時,需要將緩衝區擴大一 倍,在該過程中會呼叫一次 prefetch 指令,但是呼叫的此處遠遠不足 RowHammer 觸發的條件。

2.3 .NET CoreCLIR


CoreCLR 是.NET 的執行引擎,RyuJIT 是.NET 的 JIT 實現,目前已經開源。作為 Java 的競爭對手,.NET 大量參考了 Java 的實現機制,從位元組碼的設計,到編譯 器的實現等,都與 Java 有幾分相似。在 RyuJIT 的指令集定義中只定義了一些常 見的指令(圖 2.3),但是沒有 RowHammer 需要的指令,所以無法直接觸發。但是 在 CoreCLR 的 gc 中存在 prefetch 操作(表 2.12),然而該指令預設是被置為無效的 (表 2.13)。

enter image description here

圖 2.3

表 2.12

#!c++
void gc_heap::relocate_survivor_helper (BYTE* plug, BYTE* plug_end) {
    BYTE* x = plug; 
    while (x < plug_end) {
        size_t s = size (x);
        BYTE* next_obj = x + Align (s); 
        Prefetch (next_obj); 
        relocate_obj_helper (x, s); 
        assert (s > 0);
        x = next_obj;
} }

表 2.13

#!c++
//#define PREFETCH
#ifdef PREFETCH
__declspec(naked) void __fastcall Prefetch(void* addr) {
   __asm {
      PREFETCHT0 [ECX]
      ret
   }; 
}
#else //PREFETCH
inline void Prefetch (void* addr) {
    UNREFERENCED_PARAMETER(addr);
}
#endif //PREFETCH

2.4 Firfox SpiderMonkey


SpiderMonkey 是 Firfox 預設使用的帶有 JIT 的 Javascript 引擎,在 SpiderMonkey中沒有 RowHammer 所需要的指令出現。

0x03 總結


本文研究的主要目的是希望透過 JIT 引擎來觸發 RowHammer 的執行,為了 提高指令碼語言的執行效率,當前絕大多數指令碼引擎都帶有 JIT 編譯器以提高執行 的效率。本文研究了 HotspotV8RyuJITSpiderMonkey,其中並沒有找到比 較好的觸發 RowHammer 的方法,當然依舊有一些 JIT 還沒被研究,不過透過以 上研究證明 JIT 觸發的方式非常困難,原因主要有以下幾點:

1) RowHammer 的觸發條件比較苛刻,64ms 內觸發成功也就意味著無關指令的 數目必須很少,否者在 64ms 內 wordline 無法充放電足夠的次數。

2) Cache 相關的指令並不常用,RowHammer 執行需要使用 CLFLUSH, PREFETCH, MOVNT*系列指令,這些指令在實際的使用過程中並不常見,在使用者態進行 Cache 相關操作的情形比較少見。

3) 站在 JIT 開發人員的角度考慮,為了實現跨平臺,一般會對指令進行抽象, 然後在各個平臺上具體實現。抽象的指令一般都儘可能少,因為每抽象一個 指令就需要再新增大量的程式碼。在分析的 JIT 引擎中只有 hotspot 抽象了 prefetch 指令,引擎都儘可能少的去抽象編譯器要用到的指令,想透過指令碼 直接生成需要的彙編指令很困難。(特例是如果採用了第三方引擎(比如 AsmJit),引擎會抽象所有的彙編指令,則有更大的可能性觸發,然而當前主 流語言的 JIT 部分大都是獨立開發,而第三方引擎則多是從這些程式碼中提取 並逐步完善的)。

4)在整個分析過程中發現指令出現的原因主要是輔助 JIT 編譯,比如使用 prefetch 提高某些資料存取的速度,使用 CLFLUSH 重新整理指令緩衝區等。指令 出現的次數與頻率,遠遠達不到 RowHammer 的要求。

參考資料


  1. Google Project Zero 部落格 http://googleprojectzero.blogspot.com/2015/03/exploiting-dram-rowhammer-bug-to-gain.html

  2. Paper: Flipping Bits in Memory Without Accessing Them: An Experimental Study of DRAM Disturbance Errors http://users.ece.cmu.edu/~yoonguk/papers/kim-isca14.pdf

  3. 高階語言虛擬機器群組 http://hllvm.group.iteye.com/ 

  4. 各語言開放的原始碼

本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章