Module refinery.lib.scripts.js.deobfuscation.paramcopy
Fold a parameter into the local its function copies it to before anything else runs.
A function whose body opens by copying a parameter into a local of its own and never reads the parameter again,
function (a_1) { var a; a = a_1; ... a ... }
has the local hold the argument from the first statement on, which is what a parameter of that name holds. Giving the parameter the local's name and dropping the copy leaves the same function:
function (a) { ... a ... }
Recovering a function the obfuscator flattened into a state machine leaves this shape behind: the machine stores the arguments into a scope object, and flattening that object turns each store into a copy from the parameter the recovery gives the function.
Expand source code Browse git
"""
Fold a parameter into the local its function copies it to before anything else runs.
A function whose body opens by copying a parameter into a local of its own and never reads the
parameter again,
function (a_1) { var a; a = a_1; ... a ... }
has the local hold the argument from the first statement on, which is what a parameter of that name
holds. Giving the parameter the local's name and dropping the copy leaves the same function:
function (a) { ... a ... }
Recovering a function the obfuscator flattened into a state machine leaves this shape behind: the
machine stores the arguments into a scope object, and flattening that object turns each store into
a copy from the parameter the recovery gives the function.
"""
from __future__ import annotations
from typing import NamedTuple
from refinery.lib.scripts import Node, _remove_from_parent, _replace_in_parent
from refinery.lib.scripts.js.analysis.cache import model_cache
from refinery.lib.scripts.js.analysis.model import (
FUNCTION_NODES,
Binding,
BindingKind,
Scope,
SemanticModel,
references_own_arguments,
)
from refinery.lib.scripts.js.deobfuscation.helpers import ScriptLevelTransformer, remove_declarator
from refinery.lib.scripts.js.model import (
JsAssignmentExpression,
JsBlockStatement,
JsExpressionStatement,
JsFunctionDeclaration,
JsFunctionNode,
JsIdentifier,
JsRestElement,
JsScript,
JsSequenceExpression,
JsSwitchCase,
JsVariableDeclaration,
JsVariableDeclarator,
)
from refinery.lib.scripts.js.strict import directive_prologue, strict_mode_at
class _Copy(NamedTuple):
assignment: JsAssignmentExpression
param: JsIdentifier
local: Binding
def _parameters(function: JsFunctionNode) -> dict[int, JsIdentifier]:
"""
The plain and rest parameters of *function*, by the id of the identifier each one declares.
"""
params: dict[int, JsIdentifier] = {}
for param in function.params:
if isinstance(param, JsRestElement):
param = param.argument
if isinstance(param, JsIdentifier):
params[id(param)] = param
return params
def _entry_copies(function: JsFunctionNode, model: SemanticModel) -> list[JsAssignmentExpression]:
"""
The assignments that run first in *function*, before any other statement of its body, and copy
one of its parameters into a `var` of its body (`_copies_a_parameter`): those in the statements
that open the body, where the only statements before them are directives, `var` declarations
without an initializer and function declarations, none of which runs anything at its position.
A statement joining several such assignments with commas contributes each of them. The run ends
at the first assignment of any other kind, so no assignment before a copy reads a local or can
throw.
"""
body = function.body
if not isinstance(body, JsBlockStatement):
return []
params = _parameters(function)
body_scope = model.function_scope(function)
statements = body.body[len(directive_prologue(body)):]
copies: list[JsAssignmentExpression] = []
for statement in statements:
if isinstance(statement, JsFunctionDeclaration):
continue
if isinstance(statement, JsVariableDeclaration):
if all(
isinstance(declarator, JsVariableDeclarator) and declarator.init is None
for declarator in statement.declarations
):
continue
break
if not isinstance(statement, JsExpressionStatement):
break
expression = statement.expression
parts = [expression]
if isinstance(expression, JsSequenceExpression):
parts = expression.expressions
for part in parts:
if not _copies_a_parameter(part, params, body_scope, model):
return copies
assert isinstance(part, JsAssignmentExpression)
copies.append(part)
return copies
def _copies_a_parameter(
part: Node | None,
params: dict[int, JsIdentifier],
body_scope: Scope | None,
model: SemanticModel,
) -> bool:
"""
Whether *part* is `x = p` for one of the parameters *params* and a `var` `x` of the function
whose body scope is *body_scope*.
"""
if not isinstance(part, JsAssignmentExpression) or part.operator != '=':
return False
left = part.left
right = part.right
if not isinstance(left, JsIdentifier) or not isinstance(right, JsIdentifier):
return False
source = model.resolve(right)
local = model.resolve(left)
return (
source is not None
and source.kind is BindingKind.PARAM
and any(id(declaration) in params for declaration in source.declarations)
and local is not None
and local.kind is BindingKind.VAR
and local.scope is body_scope
)
def _is_sole_access(binding: Binding | None, reference: Node, model: SemanticModel) -> bool:
"""
Whether *reference* is the one access to *binding* the program can make: the binding records it
and nothing else, and no name the model cannot attribute can reach it.
"""
return (
binding is not None
and [*binding.reads, *binding.writes] == [reference]
and not binding.dynamic_refs
and not binding.indefinite_writes
and not binding.exported
and not model.reflection_can_reach(binding)
)
def _names_in_the_parameter_list(function: JsFunctionNode) -> set[str]:
"""
Every name spelled in the parameter list of *function*: the parameters it binds, and the names
its default values read, which are resolved in a scope of their own that does not see the body.
A parameter renamed to one of them would clash with a binding of the list or capture the read.
"""
return {
node.name
for param in function.params
for node in param.walk()
if isinstance(node, JsIdentifier)
}
def _declared_in_a_statement(declaration: Node) -> bool:
"""
Whether *declaration* is the name of a declarator without an initializer whose `var` statement
stands directly in a statement list, from which `remove_declarator` takes it without a gap.
"""
declarator = declaration.parent
if not isinstance(declarator, JsVariableDeclarator) or declarator.id is not declaration:
return False
statement = declarator.parent
return (
declarator.init is None
and isinstance(statement, JsVariableDeclaration)
and isinstance(statement.parent, (JsBlockStatement, JsScript, JsSwitchCase))
)
def _coalescible(function: JsFunctionNode, model: SemanticModel) -> list[_Copy]:
"""
The entry copies of *function* whose parameter can take the local's place. The parameter is a
plain or rest one of *function*'s own, declared by nothing else, and read by the copy and by
nothing else. The local holds the copied value and no other (`SemanticModel.singular_value`),
is declared only by declarators `remove_declarator` can take out, and its name is spelled
nowhere in the parameter list (`_names_in_the_parameter_list`). A sloppy body that reads its own
`arguments` sees the parameters through it, and moving an argument from one name to another
would change what a write through either one reaches.
"""
if not strict_mode_at(function) and references_own_arguments(function):
return []
params = _parameters(function)
spelled = _names_in_the_parameter_list(function)
copies: list[_Copy] = []
for assignment in _entry_copies(function, model):
left = assignment.left
right = assignment.right
assert isinstance(left, JsIdentifier) and isinstance(right, JsIdentifier)
source = model.resolve(right)
if (
source is None
or len(source.declarations) != 1
or id(source.declarations[0]) not in params
or not _is_sole_access(source, right, model)
):
continue
local = model.resolve(left)
if (
local is None
or local.name in spelled
or not local.reads
or model.singular_value(local) is not right
or local.dynamic_refs
or local.exported
or model.reflection_can_reach(local)
or not all(map(_declared_in_a_statement, local.declarations))
):
continue
spelled.add(local.name)
copies.append(_Copy(assignment, params[id(source.declarations[0])], local))
return copies
def _drop(assignment: JsAssignmentExpression) -> None:
"""
Remove an entry copy from the statement that holds it, and the statement with its last copy.
"""
parent = assignment.parent
if isinstance(parent, JsSequenceExpression):
_remove_from_parent(assignment)
if len(parent.expressions) == 1:
_replace_in_parent(parent, parent.expressions[0])
return
assert isinstance(parent, JsExpressionStatement)
_remove_from_parent(parent)
class JsParameterCopyCoalescing(ScriptLevelTransformer):
"""
Give a parameter the name of the local its function copies it to on entry, and drop the copy.
"""
def _process_script(self, node: JsScript) -> None:
model = model_cache(self, node).model
decided: list[_Copy] = []
for function in node.walk():
if isinstance(function, FUNCTION_NODES):
decided.extend(_coalescible(function, model))
for copy in decided:
for declaration in copy.local.declarations:
declarator = declaration.parent
assert isinstance(declarator, JsVariableDeclarator)
remove_declarator(declarator)
_replace_in_parent(copy.param, JsIdentifier(name=copy.local.name))
_drop(copy.assignment)
self.mark_changed()
Classes
class JsParameterCopyCoalescing-
Give a parameter the name of the local its function copies it to on entry, and drop the copy.
Expand source code Browse git
class JsParameterCopyCoalescing(ScriptLevelTransformer): """ Give a parameter the name of the local its function copies it to on entry, and drop the copy. """ def _process_script(self, node: JsScript) -> None: model = model_cache(self, node).model decided: list[_Copy] = [] for function in node.walk(): if isinstance(function, FUNCTION_NODES): decided.extend(_coalescible(function, model)) for copy in decided: for declaration in copy.local.declarations: declarator = declaration.parent assert isinstance(declarator, JsVariableDeclarator) remove_declarator(declarator) _replace_in_parent(copy.param, JsIdentifier(name=copy.local.name)) _drop(copy.assignment) self.mark_changed()Ancestors
Inherited members