Skip to content

Instantly share code, notes, and snippets.

@Wh1terat
Created February 4, 2020 10:57
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Wh1terat/fa6fd357094b34a8c15c4c2a74488f9a to your computer and use it in GitHub Desktop.
Save Wh1terat/fa6fd357094b34a8c15c4c2a74488f9a to your computer and use it in GitHub Desktop.
Incapsula JS Deobfuscator (obfuscator.io) - Python
#!/usr/bin/env python3
import sys
import json
import string
import random
import visitor
import mytemplates
from pyjsparser import parse
from base64 import b64decode
def _rc4(message, key):
cipher_text = bytearray()
key = [ord(c) for c in key]
sbox = list(range(256))
j = 0
for i in range(256):
j = (j + sbox[i] + key[i % len(key)]) % 256
sbox[i], sbox[j] = sbox[j], sbox[i]
i = j = 0
for byte in message:
i = (i + 1) % 256
j = (j + sbox[i]) % 256
sbox[i], sbox[j] = sbox[j], sbox[i]
key_stream = sbox[(sbox[i] + sbox[j]) % 256]
cipher_text.append(key_stream ^ ord(byte))
return cipher_text
def unwrap(program):
for node in program.traverse():
if node.type == 'VariableDeclarator' and node.id.name == 'b':
return bytes.fromhex(node.init.value).decode('utf-8')
def rotate_sting_array(program):
for result in visitor.search(program, visitor.objectify(parse(mytemplates.STRING_ROTATE)).body[0]):
for node in program.traverse():
if node.type == 'VariableDeclarator' and node.id.name == result.expression.arguments[0].name:
n = result.expression.arguments[1].value % len(node.init.elements)
node.init.elements = node.init.elements[n:] + node.init.elements[:n]
yield {'name': node.id.name, 'elements':node.init.elements}
node.remove()
result.remove()
def fix_calls(program):
string_arrays = list(rotate_sting_array(program))
for result in visitor.search(program, visitor.objectify(parse(mytemplates.CALL_WRAPPER)).body[0]):
string_array = result.declarations[0].init.body.body[1].declarations[0].init.object.name
string_array = next(value['elements'] for value in string_arrays if value['name'] == string_array)
for node in program.traverse():
try:
if node.callee.name == result.declarations[0].id.name:
i = int(node.arguments[0].value, 16)
key = node.arguments[1].value
raw = string_array[i].value
value = _rc4(b64decode(raw).decode('utf-8'),key).decode('utf-8')
node.replace(visitor.objectify({'type':'Literal', 'value': value}))
except AttributeError:
pass
result.remove()
def fix_control_flow(program):
def fix_switch_statements():
for node in program.traverse():
try:
if node.declarations[0].init.callee.property.value == 'split' and '|' in node.declarations[0].init.callee.object.value:
name = node.declarations[0].id.name
order = [int(n) for n in node.declarations[0].init.callee.object.value.split('|')]
for child in node.parent:
if child.type == 'WhileStatement' and child.body.body[0].discriminant.object.name == name:
node.parent.extend(child.body.body[0].cases[idx].consequent[0] for idx in order)
child.remove()
break;
node.remove()
except (AttributeError,IndexError) as e:
pass
def fix_indirect_expressions():
# Ew. There's a level of indirection on the indirection, hacky workaround
for _ in range(2):
for node in program.traverse():
try:
if all(prop.value.type == 'FunctionExpression' and prop.value.body.body[0].type == 'ReturnStatement' for prop in node.declarations[0].init.properties):
func = next((x for x in node.declarations[0].init.properties))
for nodex in program.traverse():
try:
if nodex.callee.object.name == node.declarations[0].id.name and nodex.callee.property.value == func.key.value:
new_node = visitor.objectify(func.value.body.body[0].argument.dict())
if type(new_node) == visitor.BinaryExpression:
new_node.left = nodex.arguments[0]
new_node.right = nodex.arguments[1]
elif type(new_node) == visitor.CallExpression:
new_node.callee.name = nodex.arguments[0].name
new_node.arguments = nodex.arguments[1:]
nodex.replace(new_node)
except (AttributeError,IndexError):
pass
node.remove()
except (AttributeError,IndexError):
pass
fix_switch_statements()
fix_indirect_expressions()
def cleanup_debug(program):
for result in visitor.search(program, visitor.objectify(parse(mytemplates.DEBUG_INTERVAL)).body[0]):
for node in program.traverse():
try:
if node.expression.callee.name == result.id.name:
node.remove()
except (AttributeError,IndexError):
pass
result.remove()
def cleanup(program):
templates = [mytemplates.SINGLE_NODE, mytemplates.UNICODE_PROTECTION, mytemplates.DEBUG_PROTECTION]
for idx,template in enumerate(templates):
for result in visitor.search(program, visitor.objectify(parse(template)).body[0]):
result.remove()
def cleanup_calls(program):
for node in program.traverse():
if node.type == 'MemberExpression':
if (node.computed and
node.property.type == 'Literal' and
not isinstance(node.property.value, int)):
node.computed = False
node.property = visitor.objectify({'type':'Identifier','name':node.property.value})
def cleanup_truthy(program):
for node in program.traverse():
if node.type == 'UnaryExpression' and node.operator == '!':
if node.argument.type == 'UnaryExpression' and node.argument.operator == '!':
if node.argument.argument.type == 'ArrayExpression' and not node.argument.argument.elements:
node.replace(visitor.objectify({'type':'Literal', 'name':True}))
elif node.argument.type == 'ArrayExpression' and not node.argument.elements:
node.replace(visitor.objectify({'type':'Literal', 'name':False}))
def rename_idents(program):
# *WARNING* This is not scope aware **WARNING**
def generate_name():
ecma_reserved = ['break','case','catch','class','const','continue','debugger','default','delete','do','else','export',
'extends','finally','for','function','if','import','in','instanceof','new','return','super','switch',
'this','throw','try','typeof','var','void','while','with','yield','enum','implements','interface','let',
'package','private','protected','await','abstract','boolean','byte','char','double','final','float',
'goto','int','long','native','short']
while True:
#newname = ''.join(random.choices(string.ascii_letters, k=random.randint(1, 3)))
newname = ''.join(random.choices(string.ascii_letters, k=3))
if newname not in ecma_reserved and newname not in idents.values():
return newname
idents = {}
for node in program.traverse():
if node.type == 'Identifier' and node.name.startswith('_0x'):
if node.name not in idents.keys():
idents[node.name] = generate_name()
node.name = idents[node.name]
def main():
script = parse(sys.stdin.read())
program = visitor.objectify(script)
script = parse(unwrap(program))
program = visitor.objectify(script)
fix_calls(program)
fix_control_flow(program)
cleanup_debug(program)
cleanup(program)
cleanup_calls(program)
cleanup_truthy(program)
rename_idents(program)
print(json.dumps(program.dict()))
if __name__ == '__main__':
main()
CALL_WRAPPER = r'''
var stringArrayCallsWrapper = function (index, key) {
index = index - 0;
var value = stringArray[index];
if (stringArrayCallsWrapper['initialized'] === undefined) {
(function () {
var getGlobal = Function('return (function() ' + '{}.constructor("return this")( )' + ');');
var that = getGlobal();
var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
that['atob'] || (that['atob'] = function (input) {
var str = String(input)['replace'](/=+$/, '');
for (var bc = 0, bs, buffer, idx = 0, output = '';
buffer = str['charAt'](idx++);
~buffer && (bs = bc % 4 ? bs * 64 + buffer : buffer, bc++ % 4) ? output += String['fromCharCode'](255 & bs >> (-2 * bc & 6)) : 0) {
buffer = chars['indexOf'](buffer);
}
return output;
});
}());
var rc4 = function (str, key) {
var s = [], j = 0, x, res = '', newStr = '';
str = atob(str);
for (var k = 0, length = str['length']; k < length; k++) {
newStr += '%' + ('00' + str['charCodeAt'](k)['toString'](16))['slice'](-2);
}
str = decodeURIComponent(newStr);
for (var i = 0; i < 256; i++) {
s[i] = i;
}
for (i = 0; i < 256; i++) {
j = (j + s[i] + key['charCodeAt'](i % key['length'])) % 256;
x = s[i];
s[i] = s[j];
s[j] = x;
}
i = 0;
j = 0;
for (var y = 0; y < str['length']; y++) {
i = (i + 1) % 256;
j = (j + s[i]) % 256;
x = s[i];
s[i] = s[j];
s[j] = x;
res += String['fromCharCode'](str['charCodeAt'](y) ^ s[(s[i] + s[j]) % 256]);
}
return res;
};
stringArrayCallsWrapper['rc4'] = rc4;
stringArrayCallsWrapper['data'] = {};
stringArrayCallsWrapper['initialized'] = !![];
}
var cachedValue = stringArrayCallsWrapper['data'][index];
if (cachedValue === undefined) {
if (stringArrayCallsWrapper['once'] === undefined) {
var StatesClass = function (rc4Bytes) {
this['rc4Bytes'] = rc4Bytes;
this['states'] = [
1,
0,
0
];
this['newState'] = function () {
return 'newState';
};
this['firstState'] = '\\w+ *\\(\\) *{\\w+ *';
this['secondState'] = '[\'|"].+[\'|"];? *}';
};
StatesClass['prototype']['checkState'] = function () {
var regExp = new RegExp(this['firstState'] + this['secondState']);
return this['runState'](regExp['test'](this['newState']['toString']()) ? --this['states'][1] : --this['states'][0]);
};
StatesClass['prototype']['runState'] = function (stateResult) {
if (!Boolean(~stateResult)) {
return stateResult;
}
return this['getState'](this['rc4Bytes']);
};
StatesClass['prototype']['getState'] = function (rc4Bytes) {
for (var i = 0, len = this['states']['length']; i < len; i++) {
this['states']['push'](Math['round'](Math['random']()));
len = this['states']['length'];
}
return rc4Bytes(this['states'][0]);
};
new StatesClass(stringArrayCallsWrapper)['checkState']();
stringArrayCallsWrapper['once'] = !![];
}
value = stringArrayCallsWrapper['rc4'](value, key);
stringArrayCallsWrapper['data'][index] = value;
} else {
value = cachedValue;
}
return value;
};
'''
STRING_ROTATE = r'''
(function (array, times) {
var whileFunction = function (times) {
while (--times) {
array['push'](array['shift']());
}
};
var selfDefendingFunc = function () {
var object = {
'data': {
'key': 'cookie',
'value': 'timeout'
},
'setCookie': function (options, name, value, document) {
document = document || {};
var updatedCookie = name + '=' + value;
var i = 0;
for (var i = 0, len = options['length']; i < len; i++) {
var propName = options[i];
updatedCookie += '; ' + propName;
var propValue = options[propName];
options['push'](propValue);
len = options['length'];
if (propValue !== true) {
updatedCookie += '=' + propValue;
}
}
document['cookie'] = updatedCookie;
},
'removeCookie': function () {
return 'dev';
},
'getCookie': function (document, name) {
document = document || function (value) {
return value;
};
var matches = document(new RegExp('(?:^|; )' + name['replace'](/([\.$?*|{}\(\)\[\]\\\/+^])/g, '$1') + '=([^;\]*)'));
var func = function (param1, param2) {
param1(++param2);
};
func(whileFunction, times);
return matches ? decodeURIComponent(matches[1]) : undefined;
}
};
var test1 = function () {
var regExp = new RegExp('\\w+ *\\(\\) *{\\w+ *[\'|"].+[\'|"];? *}');
return regExp['test'](object['removeCookie']['toString']());
};
object['updateCookie'] = test1;
var cookie = '';
var result = object['updateCookie']();
if (!result) {
object['setCookie'](['*'], 'counter', 1);
} else if (result) {
cookie = object['getCookie'](null, 'counter');
} else {
object['removeCookie']();
}
};
selfDefendingFunc();
}(stringArray, 123));
'''
SINGLE_NODE= r'''
var singleNodeCallControllerFunction = function () {
var firstCall = !![];
return function (context, fn) {
var rfn = firstCall ? function () {
if (fn) {
var res = fn['apply'](context, arguments);
fn = null;
return res;
}
} : function () {
};
firstCall = ![];
return rfn;
};
}();
'''
UNICODE_PROTECTION=r'''
var selfDefendingFunction = singleNodeCallControllerFunction(this, function () {
var func1 = function () {
return 'dev';
}, func2 = function () {
return 'window';
};
var test1 = function () {
var regExp = new RegExp('\\w+ *\\(\\) *{\\w+ *[\'|"].+[\'|"];? *}');
return !regExp['test'](func1['toString']());
};
var test2 = function () {
var regExp = new RegExp('(\\\\[x|u](\\w){2,4})+');
return regExp['test'](func2['toString']());
};
var recursiveFunc1 = function (string) {
var i = ~-1 >> 1 + 255 % 0;
if (string['indexOf']('i' === i)) {
recursiveFunc2(string);
}
};
var recursiveFunc2 = function (string) {
var i = ~-4 >> 1 + 255 % 0;
if (string['indexOf']((!![] + '')[3]) !== i) {
recursiveFunc1(string);
}
};
if (!test1()) {
if (!test2()) {
recursiveFunc1('indеxOf');
} else {
recursiveFunc1('indexOf');
}
} else {
recursiveFunc1('indеxOf');
}
});
selfDefendingFunction();
'''
DEBUG_PROTECTION=r'''
function debugProtectionFunction() {
function debuggerProtection(counter) {
if (('' + counter / counter)['length'] !== 1 || counter % 20 === 0) {
(function () {
}['constructor']('debugger')());
} else {
(function () {
}['constructor']('debugger')());
}
return debuggerProtection(++counter);
}
try {
return debuggerProtection(0);
} catch (y) {
}
}
'''
DEBUG_INTERVAL=r'''
function debugProtectionFunctionInterval() {
if (new windowfoo['Date']()['getTime']() - initTime > 500) {
debugProtectionFunction();
}
}
'''
"""Trasnforms AST dictionary into a tree of Node objects.
Source: https://github.com/austinbyers/esprima-ast-visitor """
import abc
from collections import OrderedDict
from typing import Any, Dict, Generator, List, Union
class UnknownNodeTypeError(Exception):
"""Raised if we encounter a node with an unknown type."""
pass
class Node(abc.ABC):
"""Abstract Node class which defines node operations"""
@property
@abc.abstractmethod
def fields(self) -> List[str]:
"""List of field names associated with this node type, in canonical order."""
def __init__(self, data: Dict[str, Any]) -> None:
"""Sets one attribute in the Node for each field (e.g. self.body)."""
#self.parent = None
for field in self.fields:
setattr(self, field, objectify(data.get(field)))
def __eq__(self, other):
"""Compare objects"""
return self is other
def replace(self,node):
"""Super hacky way to replace Nodes."""
if isinstance(self.parent, list):
self.parent[self.parent.index(self)] = node
elif isinstance(self.parent, Node):
for field in self.grandparent.fields:
if self.parent == getattr(self.grandparent, field):
setattr(self.grandparent, field, node)
def remove(self):
"""Super hacky way to remove Nodes."""
if isinstance(self.parent, list):
if len(self.parent) > 1:
self.parent.remove(self)
else:
self.grandparent.parent.remove(self.grandparent)
def dict(self) -> Dict[str, Any]:
"""Transform the Node back into an Esprima-compatible AST dictionary."""
result = OrderedDict({'type': self.type}) # type: Dict[str, Any]
for field in self.fields:
val = getattr(self, field)
if isinstance(val, Node):
result[field] = val.dict()
elif isinstance(val, list):
result[field] = [x.dict() for x in val]
else:
result[field] = val
return result
def traverse(self) -> Generator['Node', None, None]:
"""Pre-order traversal of this node and all of its children."""
yield self
for field in self.fields:
val = getattr(self, field)
if isinstance(val, Node):
setattr(val, 'parent', val)
setattr(val, 'grandparent', self)
yield from val.traverse()
elif isinstance(val, list):
for node in val:
setattr(node, 'parent', val)
setattr(node, 'grandparent', self)
yield from node.traverse()
@property
def type(self) -> str:
"""The name of the node type, e.g. 'Identifier'."""
return self.__class__.__name__
def search(program, pattern, strict=False):
"""Search tree for AST pattern"""
for node in program.traverse():
if compare_nodes(node, pattern, strict):
yield node
def compare_nodes(node, pattern, strict):
"""Compare Node"""
if node == pattern:
return True
elif isinstance(node, list) and isinstance(pattern, list):
if len(pattern) != len(node):
return False
return all(compare_nodes(node[idx], child, strict) for idx,child in enumerate(pattern))
elif isinstance(node, Node) and isinstance(pattern, Node):
if node.type != pattern.type:
return False
if not strict:
skipfields = ['name','value','raw'];
return all(compare_nodes(getattr(node, field),getattr(pattern, field), strict) for field in pattern.fields if field not in skipfields)
else:
return all(compare_nodes(getattr(node, field),getattr(pattern, field), strict) for field in pattern.fields if field)
def objectify(data: Union[None, Dict[str, Any], List[Dict[str, Any]]]) -> Union[
None, Dict[str, Any], List[Any], Node]:
"""Recursively transform AST data into a Node object."""
if not isinstance(data, (dict, list)):
# Data is a basic type (None, string, number)
return data
if isinstance(data, dict):
if 'type' not in data:
# Literal values can be empty dictionaries, for example.
return data
# Transform the type into the appropriate class.
node_class = globals().get(data['type'])
if not node_class:
raise UnknownNodeTypeError(data['type'])
return node_class(data)
else:
# Data is a list of nodes.
return [objectify(x) for x in data]
# --- AST spec: https://github.com/estree/estree/blob/master/es5.md ---
# pylint: disable=missing-docstring,multiple-statements
class Identifier(Node):
@property
def fields(self): return ['name']
class Literal(Node):
@property
def fields(self): return ['value', 'regex']
class Program(Node):
@property
def fields(self): return ['body']
# ========== Statements ==========
class ExpressionStatement(Node):
@property
def fields(self): return ['expression']
class BlockStatement(Node):
@property
def fields(self): return ['body']
class EmptyStatement(Node):
@property
def fields(self): return []
class DebuggerStatement(Node):
@property
def fields(self): return []
class WithStatement(Node):
@property
def fields(self): return ['object', 'body']
# ----- Control Flow -----
class ReturnStatement(Node):
@property
def fields(self): return ['argument']
class LabeledStatement(Node):
@property
def fields(self): return ['label', 'body']
class BreakStatement(Node):
@property
def fields(self): return ['label']
class ContinueStatement(Node):
@property
def fields(self): return ['label']
# ----- Choice -----
class IfStatement(Node):
@property
def fields(self): return ['test', 'consequent', 'alternate']
class SwitchStatement(Node):
@property
def fields(self): return ['discriminant', 'cases']
class SwitchCase(Node):
@property
def fields(self): return ['test', 'consequent']
# ----- Exceptions -----
class ThrowStatement(Node):
@property
def fields(self): return ['argument']
class TryStatement(Node):
@property
def fields(self): return ['block', 'guardedHandlers', 'handlers', 'handler', 'finalizer']
class CatchClause(Node):
@property
def fields(self): return ['param', 'body']
# ----- Loops -----
class WhileStatement(Node):
@property
def fields(self): return ['test', 'body']
class DoWhileStatement(Node):
@property
def fields(self): return ['body', 'test']
class ForStatement(Node):
@property
def fields(self): return ['init', 'test', 'update', 'body']
class ForInStatement(Node):
@property
def fields(self): return ['left', 'right', 'body']
# ========== Declarations ==========
class FunctionDeclaration(Node):
@property
def fields(self): return ['id', 'params', 'body']
class VariableDeclaration(Node):
@property
def fields(self): return ['kind', 'declarations']
class VariableDeclarator(Node):
@property
def fields(self): return ['id', 'init']
# ========== Expressions ==========
class ThisExpression(Node):
@property
def fields(self): return []
class ArrayExpression(Node):
@property
def fields(self): return ['elements']
class ObjectExpression(Node):
@property
def fields(self): return ['properties']
class Property(Node):
@property
def fields(self): return ['key', 'value', 'kind']
class FunctionExpression(Node):
@property
def fields(self): return ['id', 'params', 'body']
class UnaryExpression(Node):
@property
def fields(self): return ['operator', 'prefix', 'argument']
class UpdateExpression(Node):
@property
def fields(self): return ['operator', 'argument', 'prefix']
class BinaryExpression(Node):
@property
def fields(self): return ['operator', 'left', 'right']
class AssignmentExpression(Node):
@property
def fields(self): return ['operator', 'left', 'right']
class LogicalExpression(Node):
@property
def fields(self): return ['operator', 'left', 'right']
class MemberExpression(Node):
@property
def fields(self): return ['object', 'property', 'computed']
class ConditionalExpression(Node):
@property
def fields(self): return ['test', 'consequent', 'alternate']
class CallExpression(Node):
@property
def fields(self): return ['callee', 'arguments']
class NewExpression(Node):
@property
def fields(self): return ['callee', 'arguments']
class SequenceExpression(Node):
@property
def fields(self): return ['expressions']
@Tavel
Copy link

Tavel commented Oct 26, 2020

@Wh1terat, can I pay you to deobfuscate single js source for me (~1200 lines)? I believe it's obfuscated by obfuscator.io, but your deob.py script outputs only this on it:
{"type": "Program", "body": [{"type": "ExpressionStatement", "expression": {"type": "Identifier", "name": "None"}}]}
If yes, please let me know how I can contact you further.
I apologize if this suggestion is offensive to you.

@Wh1terat
Copy link
Author

@Tavel

After reading about "mailbot", no. I don't want to assist with such a project - for money or not.

@Tavel
Copy link

Tavel commented Oct 26, 2020

@Tavel

After reading about "mailbot", no. I don't want to assist with such a project - for money or not.

Ok, sure, you are a white rat, I'm a black one. I just had to try. Thank you anyway and appreciate your quick reply, sir!

@atimofeev86
Copy link

@Tavel

After reading about "mailbot", no. I don't want to assist with such a project - for money or not.

maybe you read about another mailbot. Tavel is a good guy

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment