JIT引擎触发RowHammer可行性研究

RowHammer漏洞是DDR3内存中存在的问题,通过频繁的访问内存中的一行数据,会导致临近行的数据发生反转。然而由于其运行需要在目标主机上大量运行特定的代码,实施攻击存在很大的难度。本文旨在研究能否通过Javascript等脚本语言的动态执行触发RowHammer,如果能够成功将极大增加RowHammer的攻击性。为了验证该思路,本文分析了Java Hotspot、Chrome V8、.NET CoreCLR以及Firefox SpiderMonkey的实现机制并给出了可行性分析。

0x00 前言

  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?

  RowHammer是DDR3内存中存在的问题,通过频繁的访问内存中的一行(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操作。

图1.1


图1.2

1.2 RowHammer触发的方法

  表1.1所示为Google Project Zero所给出的可以触发RowHammer的代码段。

  其中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 Hotspot、Chrome V8等执行引擎的运行机制。

2.1 Java Hotspot

  Hotspot是Oracle 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在不同平台下使用的虚拟机。


图2.1

2.1.1 模版解释器触发RowHammer?

a) 模版解释器工作原理

  模版解释器是一种比较靠近底层的解释器实现,每一个字节码对应一个模版,所有的模版组合在一起构成一个模板表。每一个模版实质上都是一段汇编代码,在虚拟机创建阶段进行初始化。在执行class文件的时候,遍历字节码,每检测到一个字节码就调用相应的汇编代码块进行执行,从而完成对于字节码的解释执行。
  为了完成对于字节码的解释执行,Hotspot在初始化时还会生成多种汇编代码块,用来辅助字节码的解释,比如函数入口代码块,异常处理代码块等。查看Hotspot中生成的代码块和模版可以采用命令 java –XX:+PrintInterpreter 指令来完成。
  针对各个字节码的模版中汇编代码比较庞大,比如字节码invokevirtual对应的代码块共有352 bytes,字节码putstatic有512 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 参数来设置阈值。

图2.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类。

    LIR_Assembler lir_asm(this);
    lir_asm.emit_code(hir()->code());

  通过遍历LIR_List依次调用,依次调用各个指令相关的emit_code(如表2-3),LIR中的指令都是继承自LIR_Op

op->emit_code(this);

  以LIR_Op1为例,其emit_code方法为

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

  以LIR_Op1为例,其emit_code方法为

case lir_prefetchr:
  prefetchr(op->in_opr());
  break;

  最终会调用 prefetchr函数,该函数为平台相关的,不同的平台下实现不同,以x86平台为例,其实现位于assembler_x86.cpp

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的初始化阶段,其会生成一个代码块。如下所示:

    __ 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编译器编译完成之后有调用

    // done
    masm()->flush();             //invoke ICache flush  

对当前代码存储的区域进行cache flush

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的函数为

MemMoveFunction CreateMemMoveFunction() {

    __ prefetch(Operand(src, 0), 1);
    __ cmp(count, kSmallCopySize);    //kSmallCopySize=8
    __ j(below_equal, &small_size);  
    __ cmp(count, kMediumCopySize);   //kMediumCopySize=63
    __ 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)。


图2.3

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.4

//#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.5

2.4 Firfox SpiderMonkey

  SpiderMonkey是Firfox默认使用的带有JIT的Javascript引擎,在SpiderMonkey中没有RowHammer所需要的指令出现。

0x03 总结

  本文研究的主要目的是希望通过JIT引擎来触发RowHammer的执行,为了提高脚本语言的执行效率,当前绝大多数脚本引擎都带有JIT编译器以提高运行的效率。本文研究了Hotspot、V8、RyuJIT和SpiderMonkey,其中并没有找到比较好的触发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. 各语言开放的源代码