Sneaker Dev Logo
Back to Blog

ShapeSecurity's Javascript VM: Part 2

January 24, 2024

botting.rocks

In previous part, we talked about the VM Internals especifically about the VM Machinery. This second part we are going to talk about the VM Data and how they work together with the VM Machinery.
ShapeSecurity's Javascript VM: Part 2This article may have been republished from another source and might not have been originally written for this site.

⚠️ Some information, tools, or techniques discussed may have changed or evolved since the publishing of this article.

Originally published at https://web.archive.org/web/20250709224347/https://www.botting.rocks/shapesecuritys-javascript-vm-part-2/

ShapeSecurity's Javascript VM: Part 2

Intro

In previous part, we talked about the VM Internals especifically about the VM Machinery. This second part we are going to talk about the VM Data and how they work together with the VM Machinery.

VM Data

The VM Data contains 11 data objects that are used as configuration for starting new threads, what op to operate on next, the bytecode, ops functions, etc.

You can think of the VM Data parts as the things that hold mutable andimmutable data, control-flow logic and the ops used by ShapeSecurity's VM. Once again the naming that I gave to these 11 variables do not reflect their actual descriptive meaning and the names were kept for legacy purposes.

1. XOR_MAP

Every single string that is constructed inside ShapeSecurity's VM is stored in this map. The strings are decrypted using a xor byte inside ShapeSecurity's VM. The way this works is that every time ShapeSecurity's VM needs to construct a string, they do it via xoring two strings from the array OBFUSCATED using the getXorValue() function.

The getXorValue function was replaced from this:

1b1: {
2		var K = G;
3		var P = K + "," + E;
4		var s = p[P];
5		if (typeof s !== "undefined") {
6				var y = s;
7				break b1
8		}
9		var S = i[E];
10		var w = qp(S);
11		var X = qp(K);
12		var c = w[0] + X[0] & 255;
13		var J = "";
14		for (var b = 1; b < w.length; ++b) {
15				J += e(X[b] ^ w[b] ^ c)
16		}
17		var y = p[P] = J
18}
19var o = q.A.length;
20

into this:

1  function getXorValue(xorMap, obfuscatedStrings, strA, strB) {
2    const fullKeyName = strA + ',' + strB;
3    let value = xorMap[fullKeyName];
4    if (typeof value !== 'undefined') {
5      return value;
6    }
7    const strBDecoded = base64Decoder(obfuscatedStrings[strB]);
8    const strADecoded = base64Decoder(strA);
9    const thirdXor = strBDecoded[0] + strADecoded[0] & 255;
10    value = '';
11    for (let i = 1; i < strBDecoded.length; ++i) {
12      value += String.fromCharCode(strADecoded[i] ^ strBDecoded[i] ^ thirdXor);
13    }
14    xorMap[fullKeyName] = value;
15    return value;
16  }
17
18

Whenever getXorValue() is called inside ShapeSecurity's VM, the XOR_MAP is passed as the first parameter, OBFUSCATED(an array containing only strings) passed as second parameter, and strA and strB are passed as third and fourth parameter.

strA

is the actual raw string from one of the items of OBFUSCATED and strB

is not the actual raw string of another string inside OBFUSCATED but the index where it lies inside OBFUSCATED.

The fullKeyname

is a joint hashed name that is derived from the strA

+ ,

+ strB

indexed number. This is done to avoid wasting time xoring

the same string twice.

The xoring

mechanism is relatively straight forward.

-

Converts

strA

andstrB

to a base64 bytes.

Converts

-

Create an array of base64 bytes named

strADecoded

from the stringstrA

Create an array of base64 bytes named

-

Create an array of base64 bytes named

strBDecoded

from the stringstrB

Create an array of base64 bytes named

-

Create third xor value,

thirdXor

, that is derived from adding the first byte ofstrADecoded

, the first byte ofstrBDecoded

and apply& 255

to the result.

Create third xor value,

-

Create a string named

value

to stored thexoring

text.

Create a string named

-

Start from 1 and iterate through all the bytes from the array

strBDecoded

Start from 1 and iterate through all the bytes from the array

-

For each iteration, use the index to access each byte from

strADecoded

andstrADecoded

thenxor

them all together with thethirdXor

value.

For each iteration, use the index to access each byte from

-

Concatenate the result from the previous step into

value

Concatenate the result from the previous step into

-

Add the end of the loop, just add the final

value

intoXOR_MAPusing thefullKeyName

. Avoiding double computation whenxoring

the samestrA

andstrB

together.

Add the end of the loop, just add the final

2. OPS_FUNCTIONS

The OPS_FUNCTIONS was originally an array of ops functions but in the pretty version it was converted to an object with each key corresponding to the index in the original array. It would have been too difficult to find each op in an array of hundreds of ops visually when working along ShapeSecurity's VM. For this reason alone it was converted into an object instead of an array.

The functions inside OPS_FUNCTIONS were cleaned up to apply a simple conversion of 1 statement(ReturnStatement

, ExpressionStatement

,IfStatement

, etc.) represents 1 action. For the most part, all of the statements found inside each individual op

contained 1 action per statement, with the exception of a few:

throwIfTypeError()

1//ORIGINAL
2if (!(c in I)) {
3	throw new qd(c + " is not defined.")
4}
5
6//CONVERTED
7function throwIfTypeError(_$A) {
8	if (!(_$A in window)) {
9		throw new ReferenceError(_$A + " is not defined.");
10	}
11}
12

throwIfIsNotAnObject()

1//ORIGINAL
2if (w.A[w.A.length - 1] === null || w.A[w.A.length - 1] === void 0) {
3		throw new qJ(w.A[w.A.length - 1] + " is not an object")
4}
5
6//CONVERTED
7function throwIfIsNotAnObject(_$A) {
8	if (_$A === null || _$A === void 0) {
9		throw new TypeError(_$A + " is not an object");
10	}
11}
12

forInFunc()

1//ORIGINAL
2var q = [];
3for (var s in w.A[w.A.length - 1]) {
4	f(q, s)
5}
6
7//CONVERTED
8function forInFunc(stackValue) {
9	var arr = [];
10	for (var i in stackValue) {
11		arr.push(i);
12	}
13	return arr;
14}
15

pushWasExceptionHandled()()

1//ORIGINAL
2var q = w.M.N();
3var s = {
4		d: false,
5		Q: w.f,
6		r: w.r
7};
8w.x.q(s);
9w.f = q.W;
10w.r = q.r
11
12//CONVERTED
13function pushWasExceptionHandled(_vmContext) {
14	var errors = _vmContext.errors.pop();
15	var errorObj = {
16		wasExceptionHandled: false,
17		_errorOryIndex: _vmContext.yIndex,
18		_xIndex: _vmContext.xIndex
19	};
20	_vmContext.errorTracker.push(errorObj);
21	_vmContext.yIndex = errors._yIndex;
22	_vmContext.xIndex = errors._xIndex;
23}
24

errorTrackerPopWithThrow()

1//ORIGINAL
2var q = w.x.N();
3if (q.d) {
4		throw q.Q
5}
6w.f = q.Q;
7w.r = q.r
8
9//CONVERTED
10function errorTrackerPopWithThrow(_vmContext) {
11	var errorTrack = _vmContext.errorTracker.pop();
12	if (errorTrack.wasExceptionHandled) {
13		throw errorTrack._errorOryIndex;
14	}
15	_vmContext.yIndex = errorTrack._errorOryIndex;
16	_vmContext.xIndex = errorTrack._xIndex;
17}
18

The only argument that each op inside the array OPS_FUNCTIONS takes is an instance of a vmContext(). Each op function uses the vmContext() instance to do one or more of these actions:

-

Access items from the

HEAP,OBFUSCATED,ARRAY_OF_NUMBERSand/orARRAY_OF_MAP_FILTER_FOR_EACH

Access items from the

-

Increments the

yIndex

value by the number of uniquely referencedyIndex

instances

Increments the

-

Modifies the

stack

array by adding and/or removing items from it

Modifies the

-

Writes new keys into

_vmMemory

and/or reads keys from_vmMemory

by usinggetKey()

andsetKey()

methods

Writes new keys into

-

Makes conditional jumps by setting

yIndex

andxIndex

inside theconsequent

side of anIfStatement

Makes conditional jumps by setting

-

Creates new threads by calling

createThread()

Creates new threads by calling

-

Creates a new string using

getXorValue()

Creates a new string using

-

Starts a new "try catch mode" and/or ends a current "try catch mode"

-

Throws an exception

-

Defines a new value on an object and/or array by using

Object.defineProperty

Defines a new value on an object and/or array by using

-

Sets a new

xyIndex

from the currentyIndex

andxIndex

values and/or setsyIndex

toxyIndex.yIndex

andxIndex

toxyIndex.xIndex

Sets a new

-

Ends the thread execution bysetting the

returnValue

to something else thanexplicitReturn

Ends the thread execution bysetting the

-

Decreases the

stack

length

Decreases the

We can break these actions into three groups:

Actions 1 thru 2 are always done first

Actions 3 thru 12 are done second

Action 13 is done at the end

It is important to understand that not all ops will contains all these type of actions. Some ops will only contain actions from 3 thru 12, some might only contains actions 1 thru 2, and/or some might contain all of them in an op function.

Node: There are too many permutations of function ops to identify but there are a limited amount of lines that are used inside each function op.

In a later part, we will talk about ShapeSecurity's VM ops in more detail since they will require a full part to fully explained the inner workings.

3. OPS_SEQUENCE

ShapeSecurity VM's uses a dynamic mapping for determining two things:

The function op to execute based on the index value on

OPS_FUNCTIONS - The xIndex

value for the next op(unless is set inside the op by some action)

The OPS_SEQUENCE is a two dimensional array, you can think of the first dimension represents rows while the second dimension represents columns.

When the method next() on a vmContext instance is called it does 4 things:

-

Uses the current

xIndex

value to access the first dimensional index onOPS_SEQUENCE

Uses the current

-

Uses the

yIndex

value to access a byte(0-255) on theHEAParray as the second dimensional index

Uses the

-

Sets the next

xIndex

value to the first element in the returning array.

Sets the next

-

Returns the second element in the returning array which corresponds to the index value on the

OPS_FUNCTIONarray.

Returns the second element in the returning array which corresponds to the index value on the

1Object.defineProperty(L, "next", {
2	value: function () {
3		{
4			var w = OPS_SEQUENCE[this.xIndex][HEAP[this.yIndex++]];
5			this.xIndex = w[0];
6			return w[1];
7		}
8	}
9});
10

The xIndex

value is not changed unless a specific action sets the xIndex

inside the executed op to another value. This only occurs on specific actions that are used to set a conditional jump.

4. HEAP

In the previous part I kept calling the HEAP the bytecode. It took me a while to learn that what I was calling the HEAP truly represents the bytecode in a VM.

The HEAP is converted into an array of bytes(base64Decoder()) and in the previous versions of ShapeSecurity's VM the HEAP remained immutable. Later on, they introduced a new action into their function ops that allowed them to change any byte in the HEAP.

A lot of the configuration values that are used for setting a memory key, reading a memory key, getting the index of an item in OBFUSCATED, etc. come from the values in the HEAP. These are the values that are first read and set into a temporary variable(example:_$A

) for later use.

5. explicitReturn and 6. returnValue

As previously mentioned in the previous part, the ShapeSecurity's VM runs until returnValue

changes. In other words, when returnValue

is set to explicitReturn

the function vmRunner() continuously run until it sets to a different value than explicitReturn

.

Inside the ops returnValue

is changed to 3 different type of values:

- returnEmptyObjectwhich is basically an empty object({}

)

-

The last value in the

stack

array

The last value in the

- void 0

also known asundefined

The returnValue

value acts as a sort of ReturnStatement

from a regular function.

7. ARRAY_OF_NUMBERS and 8. OBFUSCATED

The ARRAY_OF_NUMBERS , as the name suggests, is simply an array that holds nothing but numbers in different formats. They are used for anything such as xoring bytes for encoding some values, encryption numbers, and numbers used for some of their signals.

OBFUSCATED should have really been called the ARRAY_OF_STRINGS since it complements ARRAY_OF_NUMBERS as it only contains an array of strings. However, when I first started labeling ShapeSecurity's VM internals I came across the getXorValue() function and noticed how this array was always used for decoding strings inside that function.

Nothing is deleted or added to these arrays as they both remain immutable the whole time.

9. NATIVE_FUNCTIONS

The NATIVE_FUNCTIONS is an array of multiple prototypes and functions that are used inside ShapeSecurity's VM as a way to reference native functions available as native code. ShapeSecurity's VM never adds any new items to this array and it remains immutable.

Inside one of the function ops, the NATIVE_FUNCTIONS is pushed to the stack

array and this remains the only way this NATIVE_FUNCTIONS is accessed.

10. ARRAY_OF_MAP_FILTER_FOR_EACH

This array contains 3 prototypes:

-

The prototype of

Array.prototype.map

The prototype of

-

The prototype of

Array.prototype.filter

The prototype of

-

The prototype of

Array.prototype.forEach

The prototype of

The prototypes are later used to check if a current object contains those methods defined. If they don't then the compiled code inside ShapeSecurity's VM implements an alternative function. This is not evident until later going thru the dissambled code.

11. THREAD_CONFIG

Every time a new thread is created, using the createThread()) inside one of the ops, the first parameter always corresponds to the index value of THREAD_CONFIG. Therefor, the array THREAD_CONFIG holds all the thread configurations used to set the keys for each newly created thread.

Each item in that array always contains an object with at-least 3 keys:

- keysFromArgs

: This represents the keys in order that will be set for each item inarguments

- initializeKeys

: All the keys that will be set tovoid 0

including the keys fromkeysFromArgs

- transferredKeys

: The keys transferred from the parent'svmMemory()instance.

Additionally, two other keys are sometimes set

- arksKey

: The key used to setarguments

to.

- workResultKey

: The key used to set itself, aka thethis

value.

With the exception of the first thread created, all future threads are created using the createThread() function with a value from the THREAD_CONFIG array as a configuring object.

Conclusion

We have barely scratched the surface in the inner details of ShapeSecurity's VM. In the next part we will dive into all the actions that make up the individual ops from OPS_FUNCTIONS and what kind of structures they produced. Make sure you tune in as there will be many more parts to come after Part 3 is done.


botting.rocks

Blog: https://web.archive.org/web/20250709224347/https://www.botting.rocks/