Skip to main content

Intro

When learning basic JavaScript (JS) engine exploitation, I found that I really struggled with understanding a common JS engine exploit primitive referred to as the fake object, or fakeobj, primitive. This primitive works in tandem with another primitive called the address of, or addrof, primitive. My struggle wasn’t due to a lack of available learning resources, as these primitives have been well explained in detail in plenty of other places; I simply found that none of the explanations fully clicked for me, and I had to spend quite a bit of time piecing things together from many different resources until I’d built a patchwork understanding of these topics.

My goal with this blog post is to put together a clear, step-by-step explanation of the addrof and fakeobj primitives and build a guide that would’ve been really helpful for me, with the hope that it’ll be helpful for other people who also just can’t quite wrap their brains around the many existing explanations. We’ll discuss what these exploit primitives are, some important architectural details in V8, and finally we’ll see a method of implementing these primitives.

While these primitives are not specific to any one JS engine, I’ll be concentrating on V8, the engine that powers Chrome, for this blog post. When examining the memory layouts of objects, keep in mind that if you’re interested in a target other than V8, you’ll likely need to do some research to learn how different JS engines handle things.

Prerequisite knowledge:

You don’t need to already be familiar with browser exploitation or any specific JavaScript engine. However, to get much out of this post, you should:

– Be comfortable using a debugger such as GDB. I will be using GDB with the GEF (https://github.com/hugsy/gef) extension throughout this post.

– Be familiar with common exploitation concepts such as out-of-bounds access, information leaks, and arbitrary read/write.

– Be comfortable writing very simple JavaScript.

Important resources:

As mentioned earlier, there are a lot of existing resources that explain the addrof and fakeobj primitives. I’ve included some of these resources below, along with other generally helpful resources. I’ll be referring to some of them, or the code they provide, during this post, so I recommend taking a look at them.

https://faraz.faith/2019-12-13-starctf-oob-v8-indepth/ — This is a helpful post that also explains the addrof and fakeobj primitives, though the post is from fairly long ago, and some V8 internals have changed since then.

https://phrack.org/issues/70/3 — This paper from saelo describes these primitives, although in the context of JavaScriptCore (JSC), the JS engine that powers Safari.

https://lyra.horse/blog/2024/05/exploiting-v8-at-openecsc/ – A very thorough post that features some useful interactive breakdowns of how V8 lays out objects in memory.

https://blog.infosectcbr.com.au/2020/02/pointer-compression-in-v8.html – This post explains an optimizing technique used by V8.

Setup to follow along:

Throughout this post, I’ll be using a CTF challenge from PicoCTF 2021 called Horsepower to show examples. The challenge files, especially the custom build of D8 (a standalone version of V8), can be downloaded from the following link after signing up: https://play.picoctf.org/practice/challenge/135

I strongly recommend downloading the challenge yourself so that you can follow along with this post.

Aligning the Compass

Before we dive into specific implementation details, let’s talk more generally about what the addrof and fakeobj primitives do and where they come into play in an exploit. This will help us understand what we’re working towards as we implement these primitives. To begin, these primitives are used after a bug has been triggered, but before arbitrary read/write has been achieved. A typical exploitation process in a JS engine might look like this:

– A vulnerability in the renderer process is triggered (type confusion, out-of-bounds write, etc.).

– Using the vulnerability, the addrof and fakeobj primitives are constructed.

– Using the addrof and fakeobj primitives, arbitrary read/write is achieved.

– Using arbitrary read/write, code execution is achieved (for example, by overwriting the backing store pointer of an ArrayBuffer to point to a memory page marked RWX and then using the ArrayBuffer to write shellcode to the page).

– Note that in modern V8, the V8 heap sandbox will need to be escaped to achieve code execution in the renderer; this mitigation is beyond the scope of this post.

– At this point, the renderer is compromised and future steps may involve escaping the renderer sandbox; this is also beyond the scope of this post, but if you’re interested in this stage of exploitation, I’ve written some about it before here: https://trustfoundry.net/2024/04/11/firefox-sandbox-vulnerability-research-introduction-and-environment-setup/

In this post, we’ll be discussing the second and third steps in that list. The other steps are helpful for context, but are not the focus of this post.

Now it’s time to actually talk about the primitives themselves. Let’s start with the addrof primitive, as I think it’s a lot simpler to understand than the fakeobj primitive, and the fakeobj primitive relies on the information provided by the addrof primitive.

The addrof primitive allows an attacker to disclose the memory address of any JavaScript object. This doesn’t only refer to the literal key-value pairs you may be used to seeing ( example = {"x":1} ) — this extends to things like arrays and WebAssembly (WASM) instances as well. This is basically a fancy info leak. We’re going to leak the starting address of an object on the heap, which we can then use to calculate things like the offset to the elements of the object. In other words, the addrof primitive lets us find out where important things are in memory, and we’ll use this knowledge to construct a pointer to something useful in memory. We’ll then use this pointer with our fakeobj primitive. What does “important” mean in this case? We’ll get to that as we discuss the next primitive.

So, an info leak is a good start, but on its own, this doesn’t give us much. That’s where the fakeobj primitive comes in. But before we get into the concepts behind this primitive, I think it’s good to keep in mind that the fakeobj primitive is really a stepping stone for achieving arbitrary read/write. It helps to keep this context in mind to avoid getting lost wondering why this primitive matters.

The fakeobj primitive allows an attacker to obtain a pointer that the JS engine believes points to a JS object. Crucially, though, the pointer doesn’t necessarily point to an object — the pointer could point to anywhere on the heap (in modern V8, it couldn’t point to somewhere outside the heap due to an optimizing technique called pointer compression, which we’ll briefly discuss later).

The part of this primitive I struggled so much to understand was why being able to point to an object that didn’t even exist mattered. We have a pointer that points to somewhere arbitrary and the JS engine thinks it’s an object — great, so what? If we attempt to use this pointer when it doesn’t point to a real object, won’t we just get a crash?

The idea behind this primitive is that we’re going to make our fake object point to some memory that is already in use by something we control, like the elements of an array. What we’ll do is create an array of floats (why floats? We’ll discuss that in the next section) and assign some very specific values to its elements. These values will create an entire fake object in memory, with the important values that the JS engine expects to see. Then we’ll use the addrof primitive to determine the address of our array’s elements, so that we know where our specially crafted fake object resides in memory. Then we’ll use our fakeobj primitive to get a pointer to the start of that crafted fake object.

This may still sound confusing. Why does it matter that we made a fake object? How is this any better than just getting a pointer to a real object through normal means? The crucial detail here is something we’ll see in more detail in the next section, but the idea is that some objects have an elements pointer, which in other words is where in memory our elements will get written when we add them. Normally, we have no control over this pointer; the JS engine decides where the elements will be stored in memory, and as a normal JS developer you likely wouldn’t need to care about this.

However, when we create a fake object in memory, we get to decide where the pointer points to! Remember how I mentioned earlier that the fakeobj primitive is a stepping stone for achieving arbitrary read/write? We can make the elements pointer of our faked object point to something in memory we’d like to read from or write to. Then when we interact with the object returned by the fakeobj primitive, we can use that elements pointer from the faked object, finally giving us arbitrary read/write (note that in V8, it’s actually still not really arbitrary, once again due to pointer compression, but this is a close enough description for now).

That’s quite a few pieces on the board! Now that we’ve discussed both primitives, let’s take a step back and summarize what we’ve learned.

– The addrof primitive gives us an info leak for the starting address of an object, such as an array or WASM instance.

– We’ll use this info leak to figure out where important things in memory reside, like the elements of a float array.

– The fakeobj primitive yields a pointer to an arbitrary heap address; the JS engine will believe that this points to an object, whether it really does or not.

– To make use of these primitives, we’ll create a float array and set some of its elements to very specific values so that we can fake an object in memory.

– One crucial part of the fake object will be the elements pointer, which will point to a location we want to read from or write to.

– We’ll use the addrof primitive to get the address of our float array and then calculate the address of the elements where we created the fake object.

– Then we’ll use the fakeobj primitive to get a pointer to the elements so that it points to the fake object in memory.

– When we interact with the pointer returned by our fakeobj primitive, we’ll read from and write to wherever our faked object’s elements pointer is pointing to. This means that at long last, we’ve achieved arbitrary read/write.

This still sounds pretty involved, but hopefully it’ll start to make more sense as we grab a debugger and walk through these steps. Before we do that, though, we need to take a detour to understand some JS engine internals.

Float Arrays, Object Arrays, and Array Memory Layout

For the implementations of our addrof and fakeobj primitives, it’s crucial to understand some details about float arrays and object arrays. We’ll want to see how arrays are laid out in memory in V8, and we’ll discuss some architectural details specific to V8 that’ll be important to keep in mind.

Let’s start off with float arrays. By this, we mean an array composed of only floating-point numbers, like in this example:

For us as exploit developers, floats are important because they’re stored as true 64-bit values, which isn’t the case for everything in JS. If we want to, for example, overwrite a 64-bit pointer in an object, we can use a float to do so. If this all sounds pretty abstract, let’s fire up a debugger and actually see what’s going on.

First, let’s create a JS file called demo.js and add the following code to it. Explaining every bit of this code is beyond the scope of this post, but essentially, it’s adding some helper functions that will allow us to gracefully convert values between integers and floats, which will be important for our primitives later:

Now we’re going to launch D8 within GDB by using the following command:

The argument –allow-natives-syntax will enable some special functions within D8 that will be helpful for debugging; we’re specifically interested in the %DebugPrint() function, which we’ll see in action shortly. The –shell argument just keeps the JS shell open after our JS file has been run.

After launching D8 in GDB, you can issue the command “r” to begin running D8. To get started, let’s create a float array and then check it out using %DebugPrint(). We want to see its memory layout, since that will be important for constructing our exploit primitives later. We can do this using the following code:

The output from %DebugPrint() will look something like this:

Don’t get overwhelmed by all the output. Most of it isn’t important for the purposes of this post. In fact, there are only three sections you need to care about right now:

0xbe1080853a1: [JSArray] — This is the address of the object itself, which means this is the address where the object begins. We’ll see the memory layout of the float array shortly, but this address is what we’ll be leaking with the addrof primitive.

map: 0x0be1082439f1 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties] — In V8, an object map is a data structure that basically dictates the type of the object. In this case, the map shows that the object’s type is PACKED_DOUBLE_ELEMENTS, because we’ve created an array that contains only doubles (floats) and is considered packed because there are no “holes”, or empty indexes, in the array. V8 relies on the map to know how to treat an object. This same concept exists in other JS engines as well under various names. If you want to dig into JS engine exploitation more, it’s an important concept to understand. I highly recommend reading the following blog post to get a more thorough introduction to this topic: https://mathiasbynens.be/notes/shapes-ics

elements: 0x0be108085381 <FixedDoubleArray[3]> [PACKED_DOUBLE_ELEMENTS] — This is the elements pointer. It tells us where the elements associated with the object are stored. Recall that the way we leverage the fakeobj primitive is to eventually forge an object in memory where we control the elements pointer.

Let’s actually take a look at this object with GDB now and keep an eye out for those values. But first we’ve got a very quick detour to make.

Detour – pointer tagging

V8 uses an optimization technique called pointer tagging. There are a lot of places where you can read about this technique already, so I’m not going to go in depth, but the basic idea is that this technique uses the least significant bit of a pointer to determine whether it refers to an object or a small integer. When a tagged pointer refers to an object, you need to deduct 1 from it to get the actual address it’s referring to. Throughout this post, you’ll see GDB commands that deduct 1 from an address; this is to compensate for tagged pointers. It’s good to understand this optimization a bit more, but for the purposes of this post, it doesn’t need to be any more complicated than remembering that when you see an object address in %DebugPrint(), you need to subtract 1 from it to inspect it properly in GDB.

Back to the main thread

Let’s check out that object address in GDB:

In the first qword, we see that the lower 32 bits are 082439f1, which matches with the lower bits we see in the map value shown in the %DebugPrint() output.

This tells us that an object begins with a map. We’ll want to keep that in mind for when we fake an object in memory later.

In the second qword, we see the value 0x08085381 in the lower bits. The 0x6 in the upper bits is a little more confusing, but essentially V8 is storing the number of elements in the array — but it doubles the length because of a pointer tagging particularity (this is a bit more in the weeds than we need to get for this post, but if you want to know more about this, check out footnote 4 here: https://lyra.horse/blog/2024/05/exploiting-v8-at-openecsc/#fn:4)

But wait! The elements pointer in the %DebugPrint() output shows 0x0be108085381; the lower bits match what we see in memory in the second qword, but the upper bits don’t, and that 0x6 value isn’t present at all; compare them:

 

That doesn’t seem right. Now comes our second detour — this is the last one, I promise!

Another detour – pointer compression

Another optimizing technique leveraged by V8 is called pointer compression. This technique essentially uses some upper bits of an address to refer to the heap base (that is, the beginning of the heap). The lower bits of an address refer to the offset from the heap base. By making use of this technique, V8 doesn’t need to store so many full 64-bit pointers on the heap, since those consume more memory. Instead, just the lower bits can be used, and when a compressed pointer is accessed, the heap base will be calculated and added to the upper bits to construct the full pointer.

For example, remember that %DebugPrint() showed the elements pointer 0x0be108085381? Well, 0x0be1 in this case is the heap base. It’ll be added to our compressed pointer value 0x08085381 to get the full pointer. %DebugPrint() shows us the heap base, but when inspecting with GDB, we just see the compressed pointer. An effect of this optimization is that full 64-bit pointers are actually uncommon to see on the heap (and in fact, modern V8 has a whole security mitigation called the heap sandbox that builds on this property!).

For our purposes, this is pretty much all you need to know about pointer compression for now. Just remember that when you want to inspect something in GDB, %DebugPrint() will let you see the full pointer (including the heap base), so you’ll want that value. When you’re just looking in memory, often you’ll see compressed pointers, where you’re just getting the lower 32 bits. You can read more about pointer compression in the following posts: https://v8.dev/blog/pointer-compression, https://blog.infosectcbr.com.au/2020/02/pointer-compression-in-v8.html

Back to the main thread for real this time

Okay, so now we understand why there’s a discrepancy between the %DebugPrint() output and the stuff we’re seeing in memory. Let’s check out the full pointer 0x0be108085381 to see our float array’s elements. Don’t forget that we need to deduct 1 to compensate for pointer tagging.

The first qword in this output shows the length and type of the array. Then the next three qwords are our actual floats in memory! Note that these are not compressed pointers — they’re full 64-bit values. This is an interesting property, because if we want to read from or write to memory, we want to use a full 64-bit value to do so. Then the next qword is the start of our object! This shows that the elements are stored right before the beginning of the object in memory.

Let’s go back to our demo.js file and append the following line:

If we re-run D8 with our updated file, get the elements pointer with %DebugPrint(), and investigate the element of floatarray, we can see our full controlled 64-bit value:

Great, so this is a useful property to remember — elements in a float array are considered a full 64-bit value. Next up, let’s talk about object arrays. Here’s some code to append to demo.js:

Note that we’re creating an object and then creating an array that has just one element, the newly created object. Let’s re-run D8 and then check out the address of the test object:

And now let’s check out the element of objarray:

Note that the lower bits of the second qword match the lower bits of the object address we saw using %DebugPrint(). Unlike float arrays, object arrays are arrays of pointers. This distinction between float arrays and object arrays is going to become crucial when implementing the addrof and fakeobj primitives.

We finally have enough background information, so now let’s discuss actually implementing the primitives and achieving arbitrary read/write.

From Zero to Addrof/Fakeobj to Arbitrary R/W

We’ll begin by implementing the addrof primitive. Go ahead and write the following code to a file called exploit.js:

Of interest here is the addrof_primitive() function, so let’s talk over that and then see it in action. It takes one argument, an object. Then it creates two arrays, one of which is a float array and one of which is an object array whose lone element is the object provided via argument.

Next there’s an array builtin called setHorsepower() getting called. This is not normally an available method and has been added to the JS engine for the purposes of the CTF challenge. Because this isn’t actually a walkthrough of the CTF challenge, we won’t spend much time on the introduced bug, but basically this is just a simple method that modifies the length of the calling array, granting out-of-bounds (OOB) access. By invoking this on vuln_array, we can then access heap content beyond the bounds of vuln_array, such as the memory of victim_array.

Why are we allocating a float array and an object array right next to each other? Well, let’s think back to what we learned in the previous section. Float array elements are just true 64-bit values. Object array elements, on the other hand, are pointers. If we could just read an object array element, but have V8 believe the element is part of a float array, we could leak a pointer to an object! This is exactly what we’re doing here: we’re going to use the OOB access provided by the bug to read beyond the bounds of our float array and into the memory of our object array — but crucially, since we’re accessing that memory from a float array, the content we access will be interpreted as full 64-bit values.

So on the line return ftoi(vuln_array[7]) & 0xffffffffn , we access outside the bounds of vuln_array (the exact index is just calculated by finding the distance in memory between vuln_array’s elements and the element we want to read from victim_array). Specifically, we access the stored element of victim_array, which will be a pointer to obj. Then we use a bitwise operation to remove the upper bits, since as we’ve learned, only the lower bits are used for a compressed pointer.

Enough preamble — let’s see the addrof primitive in action. As a reminder, you can run your script in D8 under GDB like this:

After running the script, we can give our primitive a try:

The object address is returned as a BigInt, but if we convert it to hex, we get 0x80856b9. Let’s see what %DebugPrint() has to say about our testobj:

Hey, that looks right! Remember that the upper bits of the %DebugPrint() output are the heap base, but the lower bits match the pointer we just leaked. So, we’ve got our addrof primitive implemented successfully — we use OOB access from our float array to leak a pointer stored in an object array.

Now comes the trickier part, which is implementing the fakeobj primitive. Let’s take a look at some code that implements it.

This time around, our fakeobj primitive accepts an address as an argument, instead of an object. The idea here is that we’ll leak an address using the addrof primitive first. Then we’ll do any math we might need to do in order to adjust the address by a certain offset — for example, we might deduct a little from the address to make it point to the elements of an object instead of the start of the object. Then we’ll pass that address as an argument to fakeobj_primitive().

The function itself starts off in a familiar way, by creating a float array and an object array adjacent to one another, then triggering the provided OOB bug. However, this time, instead of just reading from the float array, we’re going to write beyond the bounds of the float array and modify the very first element (that is, element 0) of the object array. Crucially, because we’re writing via the float array, we can provide a true 64-bit value. In this case we only really care about the lower bits, since the pointers in the object array will be compressed. We’re writing the address we want to use for our fake object to the lower bits of our 64-bit value, and we’re just going to place 0x0 in the upper bits.

Finally, we simply return the modified element of the object array. Remember that the object array interprets its elements as pointers. So, what we did here was modify an element to make it point somewhere arbitrary and then return the element. V8 thinks this modified value is a pointer to an object. However, since we got to set the pointer, it can point to anything! To demonstrate this, let’s try using this primitive and make the faked object pointer point to something invalid. We’ll trigger a crash to prove that our primitive is working. Underneath the code for the fakeobj primitive, let’s add this:

Here we’re setting up an array of floats and then using our addrof primitive to calculate the address of the 0th element in the array (the fakeobj will start at the beginning of the array, where we saw the map pointer before; we’re calculating by going -40 bytes before that value, which is where the elements are stored in memory). Then we use our fakeobj primitive to get back a fake object that is pointing to that 0th element.

So, in memory, what’s happening with our fake object right now? Here’s what V8 thinks our fake object pointer should be pointing to (assuming it’s pointing to a float array object):

Object map –> our fake object pointer points to here

Elements pointer –> this then points to the elements of the object in memory

However, the pointer we wrote actually points to the 0th element of our target array! Here’s what the actual memory layout is like for our fake object:

0x3ff199999999999a –> Our fake object pointer points to here (the value 1.1)

0x400199999999999a –> This will be interpreted as the elements pointer, but this is definitely not a valid elements pointer right now (it’s the value 2.2)

The floats 3.3, 4.4, 5.5 occupy the next 3 qwords

As you can see, V8 has specific expectations about what should be in memory for the start of an object — in the case of a float array, a map and then an elements pointer. But right now, our fake object just points into some pre-existing elements that don’t match its expectations. So, what’ll happen if we try accessing our fake object? Give it a try with this code:

A screenshot showing a crash being triggered.

We get a crash! This is actually good news, because it shows that we’ve implemented our primitive correctly. We’re getting the behavior we’d expect. Now we just need to finesse the memory layout where our fake object points to get control over the object.

Let’s make that happen. Remember how we said earlier that the point of the fakeobj primitive is to achieve arbitrary read/write? We’re going to use our fakeobj primitive to achieve arbitrary read and write next. To start, replace the earlier code underneath the fakeobj primitive implementation with this code instead:

This looks complicated, but it’s not so bad once you know what we’re trying to achieve.

First, to demonstrate our arbitrary read, we’re going to read the backing store buffer pointer of an ArrayBuffer. To avoid making things even more confusing, we won’t discuss the point of this too much; suffice it to say that ArrayBuffers make use of something called a “backing store” which is basically a region of memory used to store the ArrayBuffer contents. ArrayBuffers have a pointer that points to the backing store location. This pointer is a very attractive target for attackers, because overwriting it to point to somewhere else could allow overwriting legitimate memory contents with shellcode to ultimately achieve code execution. We won’t be going that far in this demonstration, however; we just want to show that we can read something arbitrary from heap memory.

Also, in modern V8, the previously mentioned heap sandbox kills this technique — the version of V8 we’re working with doesn’t have this mitigation, so you don’t have to worry about it, but if you decide you want to try the same thing in a modern version of V8, you’ll have to contend with the heap sandbox and this approach won’t work anymore.

After allocating an ArrayBuffer, we create a couple of values that are going to be part of our forged object in memory. This time, when we point our fake object into the elements of a float array, there’ll be a carefully crafted forged object waiting. It’ll have the general structure V8 expects, so the object will be considered valid — it’ll just so happen that its elements pointer points to somewhere in memory we decide.

To start, there’s this line:

All we’re doing here is writing a float into memory with the value of a float array map — remember that an object is expected to begin with a map pointer! This value will be constant across runs, so we don’t need to worry about leaking it at runtime. Next up, we’ve got these lines:

Remember that the second qword in an object, the one directly following the map pointer, is expected to be an elements pointer (for float array objects). So here we’re crafting a fake elements pointer that points to something we want to read from. Specifically, we’re getting the address of the ArrayBuffer using the addrof primitive, and then we’re doing a little math to get the offset from the start of the ArrayBuffer to its backing store pointer. Also, we want the fake elements pointer to point to 8 bytes before the value we want to read, since the elements pointer of a float array points to one initial qword and then the actual elements in memory immediately follow it. After calculating the address we want, we’re just writing that into memory as the elements pointer, and we’re setting the upper bits to reflect the number of elements (doubled, as discussed earlier).

Then we’ve got these lines:

We create target_array and make its first two elements be the forged object we’ve created — the float array map followed by our faked elements pointer, which points to a part of the ArrayBuffer’s memory. Then we use the same trick as before, where we calculate the offset from the start of our target_array object to its elements and use the addrof primitive to get that address. Then we get a fake object that points to the 0th element in target_array, only this time, there’s a nicely forged object waiting there.

Finally, we interact with our fake object and read its 0th element. This will cause the object to use our faked elements pointer, so it’ll read one 64-bit value (since we’ve faked a float array) from wherever our faked elements pointer points to. In this case, this will be the location in memory holding the ArrayBuffer’s backing store pointer. Let’s try running this in a debugger to see it all together.

We can see that our exploit is saying the backing store pointer is 93825016177040, which converted to hex is 0x555556c2a190. We’ll confirm that’s right in a moment, but first, let’s use %DebugPrint() and GDB to check out the elements of target_array and see our forged object:

Hey, that looks promising! The second qword, which is the 0th element, looks exactly like the next-to-last qword, which is the very beginning of our object. In both cases, they’re the map for a float array. That’s exactly what we were going for. Then the next qword at 0x2e6b08085c88 looks like the fake elements pointer we created. Now let’s use %DebugPrint() on target_buf and see if its backing store pointer matches what we leaked using our arbitrary read, 0x555556c2a190:

It’s a perfect match! Our arbitrary read is working. If we wanted to read something else, we’d just follow this same approach but write a different forged elements pointer into our faked object.

And what about arbitrary write? Let’s give it a try. Remember that our fake object is considered a float array, so we can try writing a float into it:

That should’ve modified target_buf’s backing store pointer. Did it?

It did! We can demonstrate that the backing store is now pointing somewhere attacker-defined by making a TypedArray that uses the ArrayBuffer and then attempting to write to it:

A screenshot showing a crash being triggered.

As you’d expect, this triggers a segfault, since we overwrote the backing store pointer to be a nonsense value that absolutely does not point to any valid memory.

As an aside, you may be thinking “Wait, don’t we already have OOB write because of the bug introduced for this challenge? Can’t we just use that to corrupt the backing store pointer of an ArrayBuffer, instead of implementing arbitrary write through the addrof and fakeobj primitives?” The answer is yes, absolutely we could, and if we were working toward solving a CTF challenge, probably we’d just go for that approach instead. It’s easier and faster to implement. However, the goal of this post is to help explain these primitives, so I wanted to focus on showcasing them.

Conclusion

At this point, we’ve gone through the whole process of implementing the addrof and fakeobj primitives and then achieving arbitrary read/write using them. We covered a lot of ground, so let’s recap what we learned:

– The addrof and fakeobj primitives act as stepping stones to achieving arbitrary read/write.

– To implement these primitives, we made use of the fact that float arrays store their elements as true 64-bit values, whereas object arrays store compressed pointers to objects.

– To implement the addrof primitive, we placed an object in an object array and then used a float array with OOB access to read the element, disclosing the object’s compressed pointer.

– We learned that a key part of the fakeobj primitive is to overlay the fake object pointer on top of memory an attacker can already control (the elements of a float array).

– To implement the fakeobj primitive, we used OOB access from a float array to write a compressed pointer into an object array. This compressed pointer pointed to an entire object we forged in memory.

– The forged object was created by using a float array and setting two elements to some crafted values. The first element, where the fake object pointed, was a pointer to the map for the desired fake object type — in this case, a float array.

– The second element in the forged object was a faked elements pointer. This would point to what we wanted to read from and write to.

– Finally, we could interact with our faked object to actually perform the read and write operations; it would see the faked elements pointer we crafted and dereference it.

I hope this post has helped make the purpose and implementation of the addrof and fakeobj exploit primitives clearer, especially for those who found themselves as befuddled by them as I was. If you enjoyed this post and would like to read more about browser security, you may enjoy this previous blog post introducing Just-in-Time compilers and their relevance to browser security: https://trustfoundry.net/2025/01/14/a-mere-mortals-introduction-to-jit-vulnerabilities-in-javascript-engines/

Josiah Pierce

Josiah enjoys competing in Capture the Flag (CTF) competitions in his spare time and is interested in exploit development and reverse engineering.

Leave a Reply