Gadget构造:从JIT-ROP到对抗XnR
*本文原创作者:cuteme,本文属FreeBuf原创奖励计划,未经许可禁止转载
写在前面
简单地说,本文介绍了两种构造gadget的思路。(在浏览器支持JIT的情况下)
演进:关于JIT-ROP
我们知道,最初DEP的出现是为了对抗的栈溢出、堆溢出等这类劫持程序执行流的攻击手法。在这之后,攻击者为了绕过DEP,开始利用libc.so中的函数获取shell,也就是我们所说的Ret2libc攻击。更有效的防御手段还有ASLR地址随机化,使得libc.so加载的基址每次都会发生变化。当然,如果存在地址泄露等,ASLR仍可以被绕过。
于是乎,学术界前几年开始研究细粒度的ASLR,即使基址被泄露,攻击者也得不到准确的地址。
2013年s&p上发表了一篇文章提出JIT-ROP,这项工作极具创新性。其意义在于它提出了在运行时寻找gadget同时构造ROP链的概念。详细的过程是这样的:在存在一个内存泄露漏洞的情况下,得到一个运行时代码指针,它泄露当前4k内存页,因为当前页可能会有分支指向其他页,所以通过这个指针可以得到尽可能多的页。将当前页运行时反汇编后,获得所需要的gadget并构造ROP链。
也就是说,JIT-ROP是通过即时扫描有效内存,即时反汇编搜寻rop gadgets,即时拼接shellcode,从而绕过dep/aslr的,它在程序运行过程中动态执行,所依赖的资源在进程地址空间中获取。所以它和传统rop最大的不同在于:它可以使用任何代码指针初始化攻击环境,而传统的rop需要泄露函数的具体地址。
这种攻击手段可以绕过学术界过去提出的任意粒度的ASLR。
演进:关于XnR
为了针对JIT-ROP,14年ccs有一篇文章提出了XnR,利用软件模拟的方式实现了页的不可读。
既然页不可读,那么JIT-ROP所提出的运行时读内存页然后搜寻可用gadget这一思想就行不通了。
那么如何对抗不可读,提出使JIT-ROP攻击不需要“可读”这一条件,也可达成呢?这就是下面要讨论的内容了。
相关:关于JIT
在进入正题之前,先简单说说JIT引擎是什么。
2008年,浏览器的JavaScript引擎引入JIT技术。JIT是一种即时编译技术,是一种解决浏览器效率低下的方案。例如Javascript,它是典型的解释型语言。程序员写了一个for循环,解释器是不知道这是一个循环的,所以它将会一句一句的去循环执行这个循环。这也是为什么解释型语言的效率特别低,因为它不会预先编译。
JIT引擎的出现就是为了改善这一情况。它通过在运行时将部分语句编译为机器码,于是下次执行不需要再翻译,从而省去了解释开销,使得浏览器性能大幅度改善。
JIT与攻击
早期利用JIT新特性进行攻击的方案是在2010年blackhat大会上提出的**Pointer Inference and JIT Spraying**,将 ActionScript代码中进行大量的XOR操作。然后编译成字节码,并且多次更新到Flash VM中,这样它会建立很多带有恶意Xor操作的内存块。
但一方面这样的方法比较旧,另一方面,学术界中早已提出防御方法JITDefender。
准备工作
攻击模型
(1)存在内存泄露漏洞
(2)不考虑CFI
(3)JavaScript环境
之所以如此,因为这篇文章的中心思想是,攻击者可以通过自己注入gadget实现绕过其他限制手段如XnR。关注点并非实现真实过程的控制流转移。
防御环境
(1)DEP:NX,可写不可执行
(2)ASLR: fine-grained,XnR应用于可执行文件,库,JIT编译而成的代码
(3)Non-Readable Code: 所有代码段不可读
(4)Hidden Code Pointers: 除了JIT-compiled的指针均已匿名
(5)JIT Hardening: JIT上的防御手段,比如randomized JIT pages, constant blinding,guard pages
攻击方法
方法一:利用条件跳转语句
原理:构造if/else,for/while,编译成包含可预知的条件跳转指令。
举例说明一下,如果我们需要的gadget是int 0×80;ret ,翻译成机器码也就是0xcd80c3。
那么,则往if中写入0xc380cd字节大小的JS代码。
如上右图,攻击者是可以确定,编译结果中肯定包含‘je 0xc380cd’这一句的(x86/x64 架构采用小端存储)。所以说,通过变化if中js代码的大小,我们可以修改代码长度,改变跳转的距离。而每句JS代码编译后的大小是已知固定的。所以攻击者可以精确的控制所生成的代码长度。即,je后面的值,可以随意控制。
举个更具体的例子,在Chrome 33 (32-bit)/Chrome 51(64-bit)上,如果需要一个gadget:0xcd80c3。
S1: v=v1+v2, compiling to 0×10 bytes(64位 v=v)
S2: v=0×01010101, compiling to 0xd bytes.
使用S1 0c380c次(得到结果0xc380c0),
再使用S2 一次,求和即得到0xc380cd。
以上步骤可以注入自己想要的gadget,接下来就是要找到gadget所在的位置。
(1)首先通过内存泄露漏洞,得到JIT所编译的JavaScript代码的函数地址
(2)将这个函数的作为参数传给另一个函数,这样它就被push到stack中了(javascript的特性)。
(3)得到栈的信息,得到函数对象的指针,它包含了指向实际地址的指针。
方法二:利用直接调用语句
上一种方法对IE基本是无效的。IE部署了JIT-hardening策略,会随机插入NOP,改变条件跳转语句的值。这样,编译后的语句就不可控了。于是作者又提出使用直接调用指令实现攻击。如:call 0×1234560
原理:任何两个直接调用指令即时调用同一个函数,机器码是不一样的。其实和条件跳转的原理差不多。之前控制的是je指令后边的值,那它控制的是什么呢。其实利用的是call后面的这个值是一个相对地址,第一次调用产生的机器码是被调用者和下一次所调用的差。如下图。
假设FUN函数在2000的位置。那么在图中,连续CALL FUN_1,所以这个常数代表FUN1和第二条CALL FUN1(即0×5)的距离。即**0×2000-0×5=0x1ffb**。(0×5是一条call指令的大小)。这个例子里,攻击者得到的是0xe8fb1f0000。
嗯,以上是大概思路。具体攻击流程有三步:
(1)找到call指令后那个被调用函数的地址。
再列一遍这个式子,0×2000-0×5=0x1ffb。在这里0×2000就是被调用函数的地址。
总之,一般来说,在javascript环境下,可以利用当然是地址已知的helper函数或built-in函数(比如Math.random,String.substring)。
另外不得不说到IE,它的built-in函数在library中,而且采用了细粒度的ASLR。但是它们所产生的JS对象,未启用随机化,而且包含指向这个函数的指针。所以对于IE也是可行的。
(2)把call指令放到指定位置。
再列一遍这个式子,0×2000-0×5=0x1ffb。
还是假设我们需要的gadget是int 0×80;ret (即0xcd80c3)。
0xcd80c3,包含三个字节。也就是说,上面那个减法式子的结果必须保证位数满足要求,要是你的距离才两位是怎么也构造不出三位的gadget的。于是为保证三字节距离,需要创建一个由直接调用语句组成的JS函数,使它在编译后覆盖0×100 00 00字节。即我们要求第一次和最后一次CALL之间的距离,至少为0×1 00 00 00字节指令。
(3)检查机制
还是要说到IE,它在编译时会随机插入NOP,所以基本上不能知道函数会被编译到哪个地址。而且,前提条件存在XnR,我们是不可以通过读取代码段来确定那个位置是否是所需要的gadget的。
那么,只能通过取得足够大小的代码空间,全部填满call指令,从而确保计算结果在距离范围内。
Javascript中的check address函数会读栈中数据,直到找到call指令放置的ret,可知是否放置正确。如果没有,重新编译,重复之前的步骤。
其他
这攻击主要针对的是x86 (32- or 64-bit)架构。在RISC架构中,如ARM,MIPS,由于硬件层次的原因,利用上文所述的方法不能成功攻击。除非在ARM架构下,攻击者强制切换为16-bit THUMB模式。
另外,虽然论文中作者是以浏览器为例,但其实采用jit引擎的东西应该都适用(?)。比如pdf阅读器是可以的。
总结
实际上本文的核心思想很简单:
(1)攻击者通过构造JS代码产生一个jit-compile gadget
(2)由于所产生的gadget是攻击者控制的,所以不需要搜索,不需要读页,即可被攻击者用来构造ROP链
(3)以上过程发生在运行时
参考文献
[1] What Cannot Be Read, Cannot Be Leveraged? Revisiting Assumptions of JIT-ROP Defenses. USENIX 2016
[2] Just-In-Time Code Reuse: On the Effectiveness of Fine-Grained Address Space Layout Randomization. IEEE S&P 2013
*本文原创作者:cuteme,本文属FreeBuf原创奖励计划,未经许可禁止转载