Module refinery.lib.scripts.js.deobfuscation.antidbg
Remove the obfuscator.io self-defending anti-tamper pattern.
The obfuscator emits a run-once wrapper factory and one or more guard sites. Each guard hands the global object and a payload function to the factory and invokes the result:
FACTORY = (function () {
var flag = true;
return function (recv, payload) {
var run = flag
? function () { if (payload) { var x = payload.apply(recv, arguments); return payload = null, x; } }
: function () {};
return flag = false, run;
};
}());
var guard = FACTORY(this, function () { /* anti-analysis payload */ });
guard();
This transformer detects the pattern in two independent ways and removes the factory and each guard invocation:
- By the ReDoS signature string
(((.+)+)+)+$carried by the payload. - Structurally, by the run-once
apply-payload factory template, which covers payloads that do not carry the ReDoS string (e.g. a console-hijack or a RegExp self-test).
Both detectors feed the same shared removal executor.
Expand source code Browse git
"""
Remove the obfuscator.io self-defending anti-tamper pattern.
The obfuscator emits a run-once wrapper factory and one or more guard sites. Each guard hands the
global object and a payload function to the factory and invokes the result:
FACTORY = (function () {
var flag = true;
return function (recv, payload) {
var run = flag
? function () { if (payload) { var x = payload.apply(recv, arguments); return payload = null, x; } }
: function () {};
return flag = false, run;
};
}());
var guard = FACTORY(this, function () { /* anti-analysis payload */ });
guard();
This transformer detects the pattern in two independent ways and removes the factory and each guard
invocation:
- By the ReDoS signature string `(((.+)+)+)+$` carried by the payload.
- Structurally, by the run-once `apply`-payload factory template, which covers payloads that do not
carry the ReDoS string (e.g. a console-hijack or a RegExp self-test).
Both detectors feed the same shared removal executor.
"""
from __future__ import annotations
from refinery.lib.scripts import _remove_from_parent
from refinery.lib.scripts.js.analysis.cache import model_cache
from refinery.lib.scripts.js.deobfuscation.helpers import (
ScriptLevelTransformer,
binding_has_references,
remove_declarator,
)
from refinery.lib.scripts.js.model import (
JsAssignmentExpression,
JsBlockStatement,
JsCallExpression,
JsConditionalExpression,
JsExpressionStatement,
JsFunctionExpression,
JsIdentifier,
JsMemberExpression,
JsNullLiteral,
JsParenthesizedExpression,
JsScript,
JsSequenceExpression,
JsStringLiteral,
JsThisExpression,
JsVariableDeclaration,
JsVariableDeclarator,
FUNCTION_NODES,
strip_parens,
)
_REDOS_SIGNATURE = '(((.+)+)+)+$'
def _is_global_receiver(node) -> bool:
node = strip_parens(node)
if isinstance(node, JsThisExpression):
return True
return isinstance(node, JsIdentifier) and node.name in ('globalThis', 'window', 'self', 'global')
def _matches_self_defending_factory(model, fn) -> bool:
"""
True when `fn` matches the run-once `apply`-payload factory template: exactly two plain-identifier
params `(recv, payload)`; a conditional whose alternate is an empty function expression; a call of
the form `payload.apply(recv, ...)` or `payload.call(recv, ...)` resolved through the param bindings.
"""
if not isinstance(fn, FUNCTION_NODES) or len(fn.params) != 2:
return False
if not all(isinstance(p, JsIdentifier) for p in fn.params):
return False
recv_b = model.binding_of(fn.params[0])
payload_b = model.binding_of(fn.params[1])
if recv_b is None or payload_b is None:
return False
empty_alt = False
apply_shape = False
payload_nulled = False
for n in fn.walk():
if isinstance(n, JsConditionalExpression):
alt = strip_parens(n.alternate)
if isinstance(alt, JsFunctionExpression) and alt.body is not None and not alt.body.body:
empty_alt = True
if isinstance(n, JsCallExpression):
callee = strip_parens(n.callee)
if not isinstance(callee, JsMemberExpression):
continue
prop = callee.property
prop_name = getattr(prop, 'name', None) or getattr(prop, 'value', None)
if prop_name not in ('apply', 'call'):
continue
base = strip_parens(callee.object)
arg0 = strip_parens(n.arguments[0]) if n.arguments else None
if (
isinstance(base, JsIdentifier)
and model.resolve(base) is payload_b
and isinstance(arg0, JsIdentifier)
and model.resolve(arg0) is recv_b
):
apply_shape = True
if isinstance(n, JsAssignmentExpression) and n.operator == '=':
lhs = strip_parens(n.left)
rhs = strip_parens(n.right)
if (
isinstance(lhs, JsIdentifier)
and model.resolve(lhs) is payload_b
and isinstance(rhs, JsNullLiteral)
):
payload_nulled = True
return empty_alt and apply_shape and payload_nulled
def _removal_unit(call: JsCallExpression) -> JsCallExpression:
"""
Return the IIFE call that wraps `call` when `call` is the sole statement of an immediately-invoked
function body, otherwise return `call` itself.
"""
es = call.parent
if not isinstance(es, JsExpressionStatement):
return call
block = es.parent
if not isinstance(block, JsBlockStatement) or len(block.body) != 1:
return call
fn = block.parent
if not isinstance(fn, FUNCTION_NODES):
return call
outer = fn.parent
while isinstance(outer, JsParenthesizedExpression):
outer = outer.parent
if isinstance(outer, JsCallExpression) and strip_parens(outer.callee) is fn:
return outer
return call
def _remove_expr(node) -> None:
"""
Remove `node` as an expression: strips to the innermost non-paren ancestor, then removes the
sequence operand, the enclosing expression statement, or the node itself.
"""
cur = node
p = cur.parent
while isinstance(p, JsParenthesizedExpression):
cur, p = p, p.parent
if isinstance(p, JsSequenceExpression):
_remove_from_parent(cur)
elif isinstance(p, JsExpressionStatement):
_remove_from_parent(p)
else:
_remove_from_parent(cur)
class JsRemoveSelfDefending(ScriptLevelTransformer):
"""
Detect and remove the obfuscator.io self-defending factory+guard pattern, keyed both by the ReDoS
signature string and by the structural run-once `apply`-payload template.
"""
def _process_script(self, node: JsScript):
for literal in list(node.walk()):
if isinstance(literal, JsStringLiteral) and _REDOS_SIGNATURE in literal.value:
self._remove_redos(literal, node)
self._remove_structural(node)
def _remove_redos(self, redos_literal: JsStringLiteral, root: JsScript) -> None:
guard_decl = redos_literal.parent
while guard_decl is not None and not isinstance(guard_decl, JsVariableDeclarator):
guard_decl = guard_decl.parent
if guard_decl is None or not isinstance(guard_decl.id, JsIdentifier):
return
if not isinstance(guard_decl.init, JsCallExpression):
return
callee = guard_decl.init.callee
if isinstance(callee, JsIdentifier):
factory_name = callee.name
elif isinstance(callee, JsFunctionExpression):
factory_name = None
else:
return
guard_name = guard_decl.id.name
co_names: set[str] = set()
if factory_name is None:
for arg in guard_decl.init.arguments:
if isinstance(arg, JsIdentifier):
co_names.add(arg.name)
var_decl = guard_decl.parent
if not isinstance(var_decl, JsVariableDeclaration):
return
body_parent = var_decl.parent
if isinstance(body_parent, JsScript):
body = body_parent.body
elif isinstance(body_parent, JsBlockStatement):
body = body_parent.body
else:
return
for stmt in list(body):
if (
isinstance(stmt, JsExpressionStatement)
and isinstance(stmt.expression, JsCallExpression)
and isinstance(stmt.expression.callee, JsIdentifier)
and stmt.expression.callee.name == guard_name
and not stmt.expression.arguments
):
_remove_from_parent(stmt)
remove_declarator(guard_decl)
cleanup_names = {factory_name} if factory_name is not None else co_names
for name in cleanup_names:
model = model_cache(self, root).model
for stmt in list(body):
if not isinstance(stmt, JsVariableDeclaration):
continue
for d in list(stmt.declarations):
if (
isinstance(d, JsVariableDeclarator)
and isinstance(d.id, JsIdentifier)
and d.id.name == name
):
binding = model.binding_of(d.id)
if not binding_has_references(model, binding):
remove_declarator(d)
self.mark_changed()
def _remove_structural(self, root: JsScript) -> None:
model = model_cache(self, root).model
immediate_guards: list[JsCallExpression] = []
stored_guard_bindings: list = []
factory_names: set[str] = set()
for node in list(root.walk()):
if not isinstance(node, JsCallExpression) or len(node.arguments) < 2:
continue
if not _is_global_receiver(node.arguments[0]):
continue
fn = model._target_function_of_call(node)
if fn is None or not _matches_self_defending_factory(model, fn):
continue
callee = strip_parens(node.callee)
if isinstance(callee, JsIdentifier):
factory_names.add(callee.name)
parent = node.parent
while isinstance(parent, JsParenthesizedExpression):
parent = parent.parent
if isinstance(parent, JsCallExpression) and strip_parens(parent.callee) is node:
immediate_guards.append(parent)
elif isinstance(parent, JsVariableDeclarator) and isinstance(parent.id, JsIdentifier):
binding = model.binding_of(parent.id)
if binding is None:
continue
guard_called = any(
isinstance(ref.parent, JsCallExpression)
and strip_parens(ref.parent.callee) is ref
for ref in model.references(binding)
)
if guard_called:
stored_guard_bindings.append(binding)
if not immediate_guards and not stored_guard_bindings:
return
for guard_call in immediate_guards:
_remove_expr(_removal_unit(guard_call))
for gb in stored_guard_bindings:
for ref in list(model.references(gb)):
call = ref.parent
while isinstance(call, JsParenthesizedExpression):
call = call.parent
if isinstance(call, JsCallExpression) and strip_parens(call.callee) is ref:
_remove_expr(call)
for decl_site in list(gb.declarations):
d = decl_site.parent
if isinstance(d, JsVariableDeclarator):
remove_declarator(d)
model = model_cache(self, root).model
for binding in list(model.root_scope.bindings.values()):
if binding.name not in factory_names:
continue
if not binding_has_references(model, binding):
for decl_site in list(binding.declarations):
d = decl_site.parent
if isinstance(d, JsVariableDeclarator):
remove_declarator(d)
self.mark_changed()
Classes
class JsRemoveSelfDefending-
Detect and remove the obfuscator.io self-defending factory+guard pattern, keyed both by the ReDoS signature string and by the structural run-once
apply-payload template.Expand source code Browse git
class JsRemoveSelfDefending(ScriptLevelTransformer): """ Detect and remove the obfuscator.io self-defending factory+guard pattern, keyed both by the ReDoS signature string and by the structural run-once `apply`-payload template. """ def _process_script(self, node: JsScript): for literal in list(node.walk()): if isinstance(literal, JsStringLiteral) and _REDOS_SIGNATURE in literal.value: self._remove_redos(literal, node) self._remove_structural(node) def _remove_redos(self, redos_literal: JsStringLiteral, root: JsScript) -> None: guard_decl = redos_literal.parent while guard_decl is not None and not isinstance(guard_decl, JsVariableDeclarator): guard_decl = guard_decl.parent if guard_decl is None or not isinstance(guard_decl.id, JsIdentifier): return if not isinstance(guard_decl.init, JsCallExpression): return callee = guard_decl.init.callee if isinstance(callee, JsIdentifier): factory_name = callee.name elif isinstance(callee, JsFunctionExpression): factory_name = None else: return guard_name = guard_decl.id.name co_names: set[str] = set() if factory_name is None: for arg in guard_decl.init.arguments: if isinstance(arg, JsIdentifier): co_names.add(arg.name) var_decl = guard_decl.parent if not isinstance(var_decl, JsVariableDeclaration): return body_parent = var_decl.parent if isinstance(body_parent, JsScript): body = body_parent.body elif isinstance(body_parent, JsBlockStatement): body = body_parent.body else: return for stmt in list(body): if ( isinstance(stmt, JsExpressionStatement) and isinstance(stmt.expression, JsCallExpression) and isinstance(stmt.expression.callee, JsIdentifier) and stmt.expression.callee.name == guard_name and not stmt.expression.arguments ): _remove_from_parent(stmt) remove_declarator(guard_decl) cleanup_names = {factory_name} if factory_name is not None else co_names for name in cleanup_names: model = model_cache(self, root).model for stmt in list(body): if not isinstance(stmt, JsVariableDeclaration): continue for d in list(stmt.declarations): if ( isinstance(d, JsVariableDeclarator) and isinstance(d.id, JsIdentifier) and d.id.name == name ): binding = model.binding_of(d.id) if not binding_has_references(model, binding): remove_declarator(d) self.mark_changed() def _remove_structural(self, root: JsScript) -> None: model = model_cache(self, root).model immediate_guards: list[JsCallExpression] = [] stored_guard_bindings: list = [] factory_names: set[str] = set() for node in list(root.walk()): if not isinstance(node, JsCallExpression) or len(node.arguments) < 2: continue if not _is_global_receiver(node.arguments[0]): continue fn = model._target_function_of_call(node) if fn is None or not _matches_self_defending_factory(model, fn): continue callee = strip_parens(node.callee) if isinstance(callee, JsIdentifier): factory_names.add(callee.name) parent = node.parent while isinstance(parent, JsParenthesizedExpression): parent = parent.parent if isinstance(parent, JsCallExpression) and strip_parens(parent.callee) is node: immediate_guards.append(parent) elif isinstance(parent, JsVariableDeclarator) and isinstance(parent.id, JsIdentifier): binding = model.binding_of(parent.id) if binding is None: continue guard_called = any( isinstance(ref.parent, JsCallExpression) and strip_parens(ref.parent.callee) is ref for ref in model.references(binding) ) if guard_called: stored_guard_bindings.append(binding) if not immediate_guards and not stored_guard_bindings: return for guard_call in immediate_guards: _remove_expr(_removal_unit(guard_call)) for gb in stored_guard_bindings: for ref in list(model.references(gb)): call = ref.parent while isinstance(call, JsParenthesizedExpression): call = call.parent if isinstance(call, JsCallExpression) and strip_parens(call.callee) is ref: _remove_expr(call) for decl_site in list(gb.declarations): d = decl_site.parent if isinstance(d, JsVariableDeclarator): remove_declarator(d) model = model_cache(self, root).model for binding in list(model.root_scope.bindings.values()): if binding.name not in factory_names: continue if not binding_has_references(model, binding): for decl_site in list(binding.declarations): d = decl_site.parent if isinstance(d, JsVariableDeclarator): remove_declarator(d) self.mark_changed()Ancestors
Inherited members