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:
-
Array.prototype.map
-
Array.prototype.filter
-
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:
-
- Logical Expressions with the
&&
or||
operators
- Logical Expressions with the
-
- Fake Branches
-
- Pre-Loop test conditions
-
- Post-Loop test conditions
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
:
-
- TryCatch
-
- TryCatchFinally
-
- 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.
-
- 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.
-
- 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
});
-
- _yIndex : This is the y index value that will be used for jumping into the catch side if an error occurs.
-
- _xIndex : This is the x index value that will be used for jumping into the catch side if an error occurs.
-
- 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:
-
- Call the action
_vmContext.errors.pop
which pops out the object containing the catch coordinates.
- Call the action
-
- Call the action
pushWasExceptionHandled()
.
- Call the action
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 tofalse
signaling that no error occurrederrorOryIndex
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
:
-
returnValue = void 0;
-
returnValue = returnEmptyObject;
-
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!