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.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.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 参数来设置阈值。
1编译器共包含如下几个步骤
1 | typedef enum { |
C1编译器执行流程大致如图2.2所示:
1) 生成HIR (build_hir)
C1编译器首先分析JVM字节码流,并将其转换成控制流图的形式,控制流图的基本块使用SSA的形式来表示。HIR是一个层级比较高的中间语言表示形式,离机器相关的代码还有一定的距离。
2) 生成LIR (emit_lir)
遍历控制流图的各个基本块,以及基本块中个各个语句,生成相应的LIR形式,LIR是一个比较接近机器语言的表现形式,但是还不是机器可以理解的代码。
3) 寄存器分配
LIR中使用的是虚拟寄存器,在该阶段必须为其分配真实可用的寄存器。C1为了保证编译的速度采用了基于线性扫描的寄存器分配算法
4) 生成目标代码
真正生成平台相关的机器代码的过程,在该阶段遍历LIR中的所有指令,并生成指令相关的汇编代码。主要是使用了LIR_Assembler类。
1 | LIR_Assembler lir_asm(this); |
通过遍历LIR_List依次调用,依次调用各个指令相关的emit_code(如表2-3),LIR中的指令都是继承自LIR_Op
1 | op->emit_code(this); |
以LIR_Op1为例,其emit_code方法为
1 | void LIR_Op1::emit_code(LIR_Assembler* masm) { //emit_code |
以LIR_Op1为例,其emit_code方法为
1 | case lir_prefetchr: |
最终会调用 prefetchr函数,该函数为平台相关的,不同的平台下实现不同,以x86平台为例,其实现位于assembler_x86.cpp
1 | void Assembler::prefetchr(Address 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的初始化阶段,其会生成一个代码块。如下所示:
1 | __ bind(flush_line); |
该部分代码在C1编译器编译完成之后有调用
1 | // done |
对当前代码存储的区域进行cache flush
1 | void AbstractAssembler::flush() { |
这种方法可以对内存做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的函数为
1 | MemMoveFunction CreateMemMoveFunction() { |
该函数的主要作用是当缓冲区无法满足指令的存储时,需要将缓冲区扩大一倍,在该过程中会调用一次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)。
1 | void gc_heap::relocate_survivor_helper (BYTE* plug, BYTE* plug_end) |
1 | //#define PREFETCH |
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的要求。
参考资料
- Google Project Zero
http://googleprojectzero.blogspot.com/2015/03/exploiting-dram-rowhammer-bug-to-gain.html - 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
- 高级语言虚拟机群组 http://hllvm.group.iteye.com/
- 各语言开放的源代码