ShapeSecurity's Javascript VM: Part 3

Intro

Okay, now that we have finished talking about the VM Machinery in part 1 and the VM Data in part 2, we have finished the preliminary parts. It's time to delve deeper into the ops from OPS_FUNCTIONS.

Diving Deeper Into the Ops

The ops from ShapeSecurity's VM were at first hard to reason with. The main reason behind that was that they didn't implement a one-to-one relationship from their virtual ops to what you see on their script. They had a lot of scrambled ops.

They had a lot of ops that were sort of like the middle pieces from a jig-saw puzzle. That meant that a lot of the ops appeared to be unique and/or were a combination of some sort of permutations that yielded "unique" ops. Instead of hashing every unique op that I encountered, I decided to go with a different approach. That approach was to analyze each line and understand what action it meant to do.

Eventually, I came up with thirteen different types of actions that I briefly mentioned on part 2 that will form the basis of this part.

I'm going to refer to each line of code inside the function op as an action .

1.

At the beginning of each op you had a beginning section where strings, numbers, and sometimes an array of prototype functions are constructed from the HEAP, OBFUSCATED, ARRAY_OF_NUMBERS and/or ARRAY_OF_MAP_FILTER_FOR_EACH arrays.

For the most part, everything in the beginning section was strings and numbers, everything that required the use of _vmContext.yIndex was used in this section.

  • HEAP
var _$A = HEAP[_vmContext.yIndex];
  • OBFUSCATED
var _$A = OBFUSCATED[HEAP[_vmContext.yIndex] << 8 | HEAP[_vmContext.yIndex + 1]];
  • ARRAY_OF_NUMBERS
var _$A = OBFUSCATED[HEAP[_vmContext.yIndex] << 8 | HEAP[_vmContext.yIndex + 1]];
  • ARRAY_OF_MAP_FILTER_FOR_EACH
var _$A = ARRAY_OF_MAP_FILTER_FOR_EACH[HEAP[_vmContext.yIndex]];

If the action was accessing an item from the HEAP or the ARRAY_OF_NUMBERS array then the value would be a number type.

If the action was accessing an item from the OBFUSCATED array then the value would be a string type

If the action was accessing an item from the ARRAY_OF_MAP_FILTER_FOR_EACH array then it was returning one of these three prototypes:

    1. Array.prototype.map
    1. Array.prototype.filter
    1. Array.prototype.forEach

The reason they used this array was to implement an alternative implementation in case these prototypes were not found on the current object. This is noticeable after tracing ShapeSecurity's VM and not from looking at the action in isolation.

One thing to look closely is how ShapeSecurity's VM was constructing the indexes to access these arrays.

The index for each array can be represented using 32 bits where you can split it into 4 bytes(8 bits per byte). They would use sometimes 2-3 numbers to set each section by Left-shifting by 8 or 16 bits and || the final 8 bits.

I'm not sure what their intentions were in doing this since it required almost no effort on my part to just replace all the _vmContext.yIndex values and evaluate the final expression.

2.

The _vmContext.yIndex value is increased based on how many values were used to access the previously mentioned arrays.

In the example below we can see that the highest value used was 4 so the increment value would be 5 because the increment value will always be the highest value + 1

var _$A = HEAP[_vmContext.yIndex];
var _$B = OBFUSCATED[HEAP[_vmContext.yIndex + 1] << 8 | HEAP[_vmContext.yIndex + 2]];
var _$C = HEAP[_vmContext.yIndex + 3] << 8 | HEAP[_vmContext.yIndex + 4];
_vmContext.yIndex += 5;       

3.

With the exception of one action:

_vmContext.stack.push(function () {
  null[0]();
});

ShapeSecurity's VM did not use the standard push() and pop() methods that you would usually find in most other Virtual Machines to modify the _vmContext.stack, which I will referred moving forward a the stack.

For accessing items in an array ShapeSecurity's VM used reversed indexes such as:

_vmContext.stack.length - 1
_vmContext.stack.length - 2
_vmContext.stack.length - 3

This meant that instead of popping from the stack and setting a new value it would overwrite the same index with a new value if it needed to.

When it came to adding an item to the stack it would use this peculiar method that I have started to adapt myself as well:

_vmContext.stack[_vmContext.stack.length] = window[_$A];

This method involves setting a new value in the stack by using the length of the stack as the index for the new value being set. This method was used instead of _vmContext.stack.push.

I didn't bother to build unique actions for every value being pushed into the stack such as these:

_vmContext.stack[_vmContext.stack.length - 2] = _vmContext.stack[_vmContext.stack.length - 2] >>> _vmContext.stack[_vmContext.stack.length - 1];

_vmContext.stack[_vmContext.stack.length - 2] = _vmContext.stack[_vmContext.stack.length - 2] * _vmContext.stack[_vmContext.stack.length - 1];

_vmContext.stack[_vmContext.stack.length - 2] = _vmContext.stack[_vmContext.stack.length - 2] % _vmContext.stack[_vmContext.stack.length - 1];

As long as it modified the stack array the value that went into the stack didn't matter. A single action was used for any type of modifications on the stack.

4.

There were two actions associated with the vmMemory()

For adding values into the memory, _vmContext._vmMemory.setKey() was used in a single line by itself. It didn't return any value so it was always found as a single ExpressionStatement.

When it came to accessing values from the memory, vmContext._vmMemory.getKey() was sometimes pushed to the stack or an index from the stack was overwritten with its value.

_vmContext.stack[_vmContext.stack.length] = _vmContext._vmMemory.getKey(_$A);

_vmContext.stack[_vmContext.stack.length - 1] = _vmContext._vmMemory.getKey(_$A);

It sometimes would be set to a temporary variable such as _$A and then used in another expression. It varied honestly.

var _$C = _vmContext._vmMemory.getKey(_$A);

The memory key is constructed from either the last item of the stack or a value from the HEAP.

Note: Only memory keys that were set at the thread creation or set before the thread is called can be accessed by the memory. Any memory key that is accessed or set that hasn't been defined with the method vmMemory.setKeyAsUndefined() will result in an error

5.

All jumps, or splits as I like to call them, would result from the value _vmContext.yIndex and _vmContext.xIndex being changed inside an op from an action. The only exception was when _vmContext.yIndex was incremented after using the HEAP, OBFUSCATED, ARRAY_OF_NUMBERS and/or ARRAY_OF_MAP_FILTER_FOR_EACH arrays.

There were 2 types of jumps:

A jump from the last item in the stack:

if (_vmContext.stack[_vmContext.stack.length - 1]) {
  _vmContext.yIndex = _$A;
  _vmContext.xIndex = _$B;
}

A jump from a hard-coded statement:

var _$F = (_$E in _$D);
if (_$F) {
  _vmContext.yIndex = _$B;
  _vmContext.xIndex = _$C;
}

If the test expression resulted in true then the consequent of the IfStatement would take a detour otherwise it would continue as usual.

These splits were not all used to represent If or If-then-else structs. Sometimes they were used to build other things such as:

A whole part will be dedicated to the control-flow structures and the jump components inside ShapeSecurity's VM.

6 and 7.

The createThread() and the getXorValue() functions were values that were simply implemented as values to actions that either modified the stack, or actions that created a temporary variable inside an op. We went over these 2 functions in part 1.

Please refer to part 1 if you need to brush up on the basics.

8.

The "try catch mode", that I briefly mentioned in part 1. was used by the ShapeSecurity's VM to implement the 3 different types of TryStatement:

    1. TryCatch
    1. TryCatchFinally
    1. TryFinally

I understand that most readers would be familiar with a TryCatch which is a TryStatement with only the try side and the `catch side that is used to catch exceptions. Very few of those will be familiar with a TryCatchFinally, which in addition to having a catch side also has a finally side.

The finally side in a TryCatchFinally always gets executed after the try side runs without any exceptions or after finishing the catch side.

The last type of TryStatement was to me one that I have never used before or rarely needed, the TryFinally. Just like the TryCatchFinally the finally side always gets executed at the end, but with the case of a TryFinally which doesn't have a catch side, the finally side gets executed after the try side.

The next two pieces of information I'm going to present are crucial.

    1. When an exception is thrown on a catch side from a TryCatchFinally the exception DOES NOT get thrown until the end of the finally side.
    1. When an exception is thrown on a try side from a TryFinally the exception DOES NOT get thrown until the end of the finally side.

For more information please visit this link.

That being said, let's start with how ShapeSecurity's VM implements the TryCatch and move on to the other two.

The TryCatch is implemented by pushing an object into the _vmContext.errors array with 3 properties.

_vmContext.errors.push({
  _yIndex: _$A,
  _xIndex: _$B,
  totalErrors: 0
});
    1. _yIndex : This is the y index value that will be used for jumping into the catch side if an error occurs.
    1. _xIndex : This is the x index value that will be used for jumping into the catch side if an error occurs.
    1. totalErrors: An increment that is only used to count how many exceptions occurs which is not used on a TryCatch.

You should only worry about numbers 1 and 2 as number 3 doesn't matter for now.

When that object is pushed to the _vmContext.errors the "try catch mode" is activated and all the ops will be executed using the tryCatchSomething() function.

Once inside the try catch mode the try side will run until the action _vmContext.errors.pop() is used.

This will remove the last object stored inside the _vmContext.errors and the "try catch mode" is done.

In the case that an exception occurs during the execution of the try side, the TryStatement inside tryCatchSomething() will catch the exception and pass it as the first argument to errorHandling().

During the execution of the errorHandling() function, the last object on _vmContext.errors will be popped.
After that, it will push a new object into the _vmContext.errorTracker array to signal that an exception has occurred.

This will be done with an object containing two properties:


_vmContext.errorTracker.push({
  wasExceptionHandled: true,
  _errorOryIndex: error
});

This object contains the wasExceptionHandled property set to true indicating that an error occurred and the _errorOrIndex property set to the exception caught on the function tryCatchSomething().

Right after that, it will use the _yIndex and the _xIndex value from the last object popped from the _vmContext.errors array to jump to the catch side of the TryCatch.

This is essentially the gist of how a TryStatement is implemented in ShapeSecurity's VM.

When ShapeSecurity's VM implements a TryCatchFinally it will use 2 objects inside the _vmContext.errors array to implement it. This means it will call the action _vmContext.errors.push() to be used for the catch and finally side.

The first time it calls _vmContext.errors.push() it will contain the _xIndex and the _yIndex values of the catch side, the second time it will contain the _xIndex and the _yIndex values for the finally side. The actions _vmContext.errors.push() are called consecutively.

If the try side of the TryCatchFinally runs without throwing an exception it will do two things:

    1. Call the action _vmContext.errors.pop which pops out the object containing the catch coordinates.
    1. Call the action pushWasExceptionHandled().
function pushWasExceptionHandled(_vmContext) {
  var errors = _vmContext.errors.pop();
  var errorObj = {
    wasExceptionHandled: false,
    _errorOryIndex: _vmContext.yIndex,
    _xIndex: _vmContext.xIndex
  };
  _vmContext.errorTracker.push(errorObj);
  _vmContext.yIndex = errors._yIndex;
  _vmContext.xIndex = errors._xIndex;
}

pushWasExceptionHandled() is always called at the end of a try side inside a TryCatchFinally or a TryFinally. This function is used to jump to the finally side and is very similar to the function errorHandling().

The main difference are that it pushes a new object to the _vmContext.errorTracker array with:

  • wasExceptionHandled set to false signaling that no error occurred
  • errorOryIndex containing the _yIndex value to jump after the end of the finally side
  • _xIndex containing the _xIndex value to jump after the end of the finally side

If the try side of the TryCatchFinally ends up having an exception then the errorHandling() function will pop the last object from _vmContext.errors containing the catch cords which will be used to jump to the catch side.

Once the catch side is done executing,pushWasExceptionHandled() will be called at the end to jump to the finally side.

At the end of the finally side the action errorTrackerPopWithThrow() will be called.

function errorTrackerPopWithThrow(_vmContext) {
  var errorTrack = _vmContext.errorTracker.pop();
  if (errorTrack.wasExceptionHandled) {
    throw errorTrack._errorOryIndex;
  }
  _vmContext.yIndex = errorTrack._errorOryIndex;
  _vmContext.xIndex = errorTrack._xIndex;
  }

The function errorTrackerPopWithThrow() is used to implement the crucial part where any exceptions caught on the catch side of a TryCatchFinally or the try side of a TryFinally can be thrown after the finally side ends. This is the last action that represents the end of the finally side.

When wasExceptionHandled is set to false it will indicate that there weren't any exceptions left to throw and it will continue to the next op set by the last object on _vmContext.errors.

However, when wasExceptionHandled is set to true it will throw the exception that was caught using the errorHandling() function. ShapeSecurity's VM usually had another TryStatement that would be catching this exception.

To get out of the "try catch mode", the _vmContext.errors array must remain with zero items, and there are some occasions where multiple TryStatements are nested inside each other. Such as a TryCatchFinally being the parent of many TryCatch that live on the try side.

Last but not least, the TryFinally, is implemented similarly like a TryCatch where it only needs 1 object pushed to the _vmContext.errors array. The only noticeable difference is that at the end of a try inside a TryFinally the action pushWasExceptionHandled() is called instead of _vmContext.errors.pop() like in a normal TryCatch.

If an error occurs inside the try side in a TryFinally then the same rules will apply on the finally side like it would if it was a TryCatchFinally.

9.

There were usually two types of exceptions being thrown around inside ShapeSecurity's VM.

One type was used to make sure some keys existed inside an object while executing an op.

throwIfTypeError()
throwIfIsNotAnObject()
throwIfCannotAccessProperty()

These acted as assert statements that threw an exception if some keys were not defined in an object. I skipped these as they were not required for following the execution of the thread.

The other exception was an action that was using the last item on the stack to make a ThrowStatement. I did not skip this and implemented it as if it was an ending state like when returnValue is changed to some other value other than explicitReturn.

10.

ShapeSecurity's VM had a particular way of setting new items into an array, creating objects, and adding new properties to an object.

Instead of doing something like this:

var someArray = ["hello", "world", "f5"];

They would use an action that would call Object.defineProperty to set the properties of an object. Each time Object.defineProperty was called in an action it was setting one element in an array, or one property in an object.



//Pretend someArray is an array
Object.defineProperty(someArray, "0",{
  writable: true,
  writable: true,
  writable: true,
  value: "hello",
});
Object.defineProperty(someArray, "1",{
  writable: true,
  writable: true,
  writable: true,
  value: "world",
});
Object.defineProperty(someArray, "2",{
  writable: true,
  writable: true,
  writable: true,
  value: "beautiful",
});


//Pretend someObject is an object
Object.defineProperty(someObject, "name",{
  writable: true,
  writable: true,
  writable: true,
  value: "f5",
});
Object.defineProperty(someObject, "protection",{
  writable: true,
  writable: true,
  writable: true,
  value: "hard",
});
Object.defineProperty(someObject, "person",{
  writable: true,
  writable: true,
  writable: true,
  value: "tellmemore",
});

Notice how you can add an item to an array by using an index in the form of a string. Isn't that something, ShapeSecurity's VM is always pushing the boundaries of the kind of fuckery that you can do in Javascript.

11.

A unique state in ShapeSecurity's VM could be defined as the unique value of _vmContext.xIndex and _vmContext.yIndex because you needed two indexes to access the next op in var w = OPS_SEQUENCE[this.xIndex][HEAP[this.yIndex++]];.

The only exception to this rule was when the action that modified the _vmContext.xyIndex object happened.

_vmContext.xyIndex = {
  yIndex: _vmContext.yIndex,
  xIndex: _vmContext.xIndex
};

This action acted as a sort of mini function that ended when the _vmContext.xIndex and _vmContext.yIndex values were set to the xIndex and yIndex from _vmContext.xyIndex object set at the beginning of the mini function.

The start of this mini function will occur by the action setting _vmContext.xyIndex to the current _vmContext.yIndex and _vmContext.xIndex values. Then, on a later op, there will be an action that will return from this mini function by explicitly setting the _vmContext.xIndex and _vmContext.yIndex to the values previously set at the start of this mini function. Essentially causing an explicit jump.

All the ops between the start of the mini function and the end will use the same ops with the same xIndex and yIndex cords. The only thing different is that the return address of the mini function will be unique. The value that goes inside _vmContext.xyIndex will always be unique, just not the ops it follows before it sets _vmContext.xIndex and _vmContext.yIndex to the values from _vmContext.xyIndex at the end of the mini function.

12.

As previously said in part 1, the vmRunner() will continue to execute until returnValue is not set to explicitReturn.

This meant that there were only 3 types of actions that changed the value of returnValue:

    1. returnValue = void 0;
    1. returnValue = returnEmptyObject;
    1. returnValue = _vmContext.stack[_vmContext.stack.length - 1];

The first return action was used to represent an implicit return in a regular Javascript function since return without an argument results in the default value being undefined or void 0.

The second return action was used to return an empty object({}).

The third return action was used to represent an explicitly return in a regular Javascript function with a defined argument value.

When one of these 3 actions was executed it signaled the end of the current thread execution.

13.

Since ShapeSecurity's VM did not use the common popping and pushing mechanisms for modifying the stack, they decreased the stack length based on how many values were used but not set to any new values. This action was always the last action performed in an op when values from the stack were accessed or modified.

Conclusion

Well this concludes Part 3 of this series and I would say we are halfway there through the series. Part 4 is going to be dedicated to the fuckery that ShapeSecurity's VM uses and the different types of jump components inside ShapeSecurity's VM. It will most likely be shorter than Part 3, until then, see ya next time!