Module refinery.lib.scripts.js.deobfuscation
JavaScript AST deobfuscation transforms.
Expand source code Browse git
"""
JavaScript AST deobfuscation transforms.
"""
from __future__ import annotations
from refinery.lib.scripts import tree_version
from refinery.lib.scripts.js.analysis.cache import ModelCache
from refinery.lib.scripts.js.analysis.conversions import ConversionSettlement
from refinery.lib.scripts.js.analysis.environment import HostEnvironment
from refinery.lib.scripts.js.deobfuscation.antidbg import JsRemoveSelfDefending
from refinery.lib.scripts.js.deobfuscation.argwrap import JsAssignmentsAsFunctionArgs
from refinery.lib.scripts.js.deobfuscation.b91strings import JsBase91StringDecoder
from refinery.lib.scripts.js.deobfuscation.cff import (
JsControlFlowUnflattening,
JsGeneratorCFFUnflattening,
)
from refinery.lib.scripts.js.deobfuscation.constants import JsConstantInlining
from refinery.lib.scripts.js.deobfuscation.deadcode import JsDeadCodeElimination
from refinery.lib.scripts.js.deobfuscation.dispatcher import JsDispatcherUnwrapper
from refinery.lib.scripts.js.deobfuscation.evaluator import JsFunctionEvaluator
from refinery.lib.scripts.js.deobfuscation.globalfinder import JsGlobalFinderInlining
from refinery.lib.scripts.js.deobfuscation.iifeaccessor import JsIIFEAccessorPromoter
from refinery.lib.scripts.js.deobfuscation.namespaces import JsNamespaceFlattening
from refinery.lib.scripts.js.deobfuscation.objectfold import JsObjectFold
from refinery.lib.scripts.js.deobfuscation.paramcopy import JsParameterCopyCoalescing
from refinery.lib.scripts.js.deobfuscation.protospelling import JsPrototypeSpellingNormalization
from refinery.lib.scripts.js.deobfuscation.reflection import JsReflectionInlining
from refinery.lib.scripts.js.deobfuscation.restunpack import JsRestArrayUnpacking
from refinery.lib.scripts.js.deobfuscation.scramble import JsScrambleStringDecoder
from refinery.lib.scripts.js.deobfuscation.selfchecks import JsSelfCheckFolding
from refinery.lib.scripts.js.deobfuscation.simplify import JsSimplifications
from refinery.lib.scripts.js.deobfuscation.singleuse import JsSingleUseFunctionInliner
from refinery.lib.scripts.js.deobfuscation.stringarray import JsStringArrayResolver
from refinery.lib.scripts.js.deobfuscation.unshuffle import JsArrayUnshuffle
from refinery.lib.scripts.js.deobfuscation.unused import JsUnusedCodeRemoval
from refinery.lib.scripts.js.deobfuscation.wrappers import JsCallWrapperInliner
from refinery.lib.scripts.js.model import JsScript
from refinery.lib.scripts.js.options import DeobfuscationOptions
from refinery.lib.scripts.pipeline import (
DeobfuscationPipeline,
PipelineObserver,
TransformerGroup,
)
_pipeline = DeobfuscationPipeline(
groups=[
TransformerGroup(
'unpack',
JsReflectionInlining,
),
TransformerGroup(
'normalize',
JsPrototypeSpellingNormalization,
JsAssignmentsAsFunctionArgs,
JsSimplifications,
JsDeadCodeElimination,
),
TransformerGroup(
'fold',
JsSelfCheckFolding,
JsNamespaceFlattening,
JsCallWrapperInliner,
JsDispatcherUnwrapper,
JsIIFEAccessorPromoter,
JsFunctionEvaluator,
JsObjectFold,
JsControlFlowUnflattening,
JsGeneratorCFFUnflattening,
JsRestArrayUnpacking,
JsParameterCopyCoalescing,
JsArrayUnshuffle,
JsConstantInlining,
JsGlobalFinderInlining,
),
TransformerGroup(
'resolve',
JsStringArrayResolver,
JsBase91StringDecoder,
JsScrambleStringDecoder,
),
TransformerGroup(
'cleanup',
JsRemoveSelfDefending,
JsUnusedCodeRemoval,
JsSingleUseFunctionInliner,
),
],
dependencies={
'normalize': {'unpack'},
'fold': {'normalize'},
'resolve': {'fold'},
'cleanup': {'fold'},
},
)
def deobfuscate(
ast: JsScript,
max_steps: int = 5000,
*,
module: bool = False,
entrypoints: tuple[str, ...] = (),
environment: HostEnvironment = HostEnvironment.universal,
trust_eval: bool = False,
preserve_script_return: bool = False,
observer: PipelineObserver | None = None,
) -> int:
"""
Apply all available deobfuscators to the input. A non-zero *max_steps* bounds the total number of
change-producing transformer passes and raises
`refinery.lib.scripts.pipeline.DeobfuscationTimeout` once it is exceeded, so a transform that fails
to converge fails loudly instead of hanging. The default is generous — real inputs settle in a few
to a few dozen passes (the differential corpus peaks in the low tens) — so it never bounds a
legitimate deobfuscation, only a runaway loop. Pass `0` to disable the bound entirely. *module*
selects the execution model the input is assumed to run under, *entrypoints* names top-level
functions a host calls by name, and *environment* pins the host whose global names are assumed
present; see `refinery.lib.scripts.js.options.DeobfuscationOptions`. *trust_eval* selects the
trusting model, which assumes code supplied as data is inert and is deliberately unsound; the
default is the suspecting model, so a caller that reads this function's output as equivalent to
its input keeps that claim. *observer* is called around
every transformer, which is how a property of the tree is attributed to the pass that moved it; see
`refinery.lib.scripts.js.deobfuscation.audit.StrictModeAudit`.
A conversion of a function that is handed on, with no evidence that the program installs a
conversion on it, is folded only on a tree the run has settled: a key a later pass decodes may
yet spell such an install. The run keeps a record of the tree it last refused such a
conversion on (`refinery.lib.scripts.js.analysis.conversions.ConversionSettlement`). When the
pipeline reaches its fixpoint on that tree, every pass that decodes has run to its end, so the
record settles the tree and the pipeline runs again over the same models, with the step budget
carried over. A change in that run leaves the settled tree behind, since a fold may let a pass
decode more, so the pipeline runs again as long as a run that changed the tree ends on one it
refused a conversion on.
"""
options = DeobfuscationOptions(
module=module,
entrypoints=tuple(entrypoints),
environment=environment,
trust_eval=trust_eval,
preserve_script_return=preserve_script_return,
)
settlement = ConversionSettlement()
models = ModelCache(ast, options, settlement)
steps = _pipeline.run(
ast,
max_steps=max_steps,
models=models,
options=options,
observer=observer,
)
while settlement.waits_on(tree_version(ast)):
settlement.settle(tree_version(ast))
settled_steps = _pipeline.run(
ast,
max_steps=max_steps,
initial_steps=steps,
models=models,
options=options,
observer=observer,
)
if settled_steps == steps:
break
steps = settled_steps
return steps
Sub-modules
refinery.lib.scripts.js.deobfuscation.antidbg-
Remove the self-defending anti-tamper pattern a common JavaScript obfuscator emits …
refinery.lib.scripts.js.deobfuscation.argwrap-
The obfuscator converts statement sequences into calls to a self-disabling no-op function whose arguments carry all side effects. This transformer …
refinery.lib.scripts.js.deobfuscation.audit-
Attribute a change of strict mode to the pass that made it …
refinery.lib.scripts.js.deobfuscation.b91strings-
Decode the strings an obfuscator conceals behind base91 decoders. Every string literal becomes a call of an accessor, which decodes an element of a …
refinery.lib.scripts.js.deobfuscation.cff-
Control-flow flattening recovery transforms.
refinery.lib.scripts.js.deobfuscation.constants-
Inline constant variable references in JavaScript.
refinery.lib.scripts.js.deobfuscation.deadcode-
Eliminate dead code branches guarded by constant conditions …
refinery.lib.scripts.js.deobfuscation.dispatcher-
The dispatcher obfuscation moves the functions of a program into the table of one routing function and turns every call into a call of that router: a …
refinery.lib.scripts.js.deobfuscation.evaluator-
Evaluate pure JavaScript functions called with constant arguments and replace call sites with computed results.
refinery.lib.scripts.js.deobfuscation.globalfinder-
Resolve global-object-finder functions to
globalThis… refinery.lib.scripts.js.deobfuscation.helpers-
Shared utilities for JavaScript deobfuscation transforms, and the runtime value domain they and the interpreter agree on: what a JavaScript value is …
refinery.lib.scripts.js.deobfuscation.iifeaccessor-
Promote IIFE-bound function accessors to plain function declarations …
refinery.lib.scripts.js.deobfuscation.interpreter-
Mini-interpreter for executing pure JavaScript functions with concrete arguments.
refinery.lib.scripts.js.deobfuscation.namespaces-
Flatten empty namespace objects into bare variable declarations.
refinery.lib.scripts.js.deobfuscation.objectfold-
Inline properties of locally-defined constant object literals …
refinery.lib.scripts.js.deobfuscation.paramcopy-
Fold a parameter into the local its function copies it to before anything else runs …
refinery.lib.scripts.js.deobfuscation.protospelling-
Rewrite the spellings that reach an intrinsic prototype without naming it to
Owner.prototype… refinery.lib.scripts.js.deobfuscation.reflection-
Inline reflectively executed JavaScript code: eval, Function constructor, constructor chains, and setTimeout/setInterval with string arguments. An …
refinery.lib.scripts.js.deobfuscation.restunpack-
Unpack rest-parameter arrays that pack multiple variables into a single parameter …
refinery.lib.scripts.js.deobfuscation.scramble-
Resolves string concealment using the Scramble cipher. Scramble uses PBKDF2 key derivation followed by multiple rounds of a permutation-based …
refinery.lib.scripts.js.deobfuscation.selfchecks-
Fold the two self-checks an obfuscator inserts that read the source text of a function …
refinery.lib.scripts.js.deobfuscation.simplify-
JavaScript syntax normalization transforms.
refinery.lib.scripts.js.deobfuscation.singleuse-
Unwrap a function the whole script exists to call once …
refinery.lib.scripts.js.deobfuscation.strict_divergence-
Runtime strict-vs-sloppy divergence detection for reflection inlining. The reflection transform inlines payloads from always-sloppy surfaces …
refinery.lib.scripts.js.deobfuscation.stringarray-
Resolve the string-array rotation pattern produced by popular JavaScript obfuscators …
refinery.lib.scripts.js.deobfuscation.templates-
Recognize a helper an obfuscator inserts by what it is: a function is the helper when its normal form equals the normal form of the helper's template, …
refinery.lib.scripts.js.deobfuscation.unshuffle-
Resolve statically-evaluable array rotation calls …
refinery.lib.scripts.js.deobfuscation.unused-
Remove unreachable function declarations and unused variable assignments …
refinery.lib.scripts.js.deobfuscation.wrappers-
Inline trivial function call wrappers …
Functions
def deobfuscate(ast, max_steps=5000, *, module=False, entrypoints=(), environment=HostEnvironment.universal, trust_eval=False, preserve_script_return=False, observer=None)-
Apply all available deobfuscators to the input. A non-zero max_steps bounds the total number of change-producing transformer passes and raises
DeobfuscationTimeoutonce it is exceeded, so a transform that fails to converge fails loudly instead of hanging. The default is generous — real inputs settle in a few to a few dozen passes (the differential corpus peaks in the low tens) — so it never bounds a legitimate deobfuscation, only a runaway loop. Pass0to disable the bound entirely. module selects the execution model the input is assumed to run under, entrypoints names top-level functions a host calls by name, and environment pins the host whose global names are assumed present; seeDeobfuscationOptions. trust_eval selects the trusting model, which assumes code supplied as data is inert and is deliberately unsound; the default is the suspecting model, so a caller that reads this function's output as equivalent to its input keeps that claim. observer is called around every transformer, which is how a property of the tree is attributed to the pass that moved it; seeStrictModeAudit.A conversion of a function that is handed on, with no evidence that the program installs a conversion on it, is folded only on a tree the run has settled: a key a later pass decodes may yet spell such an install. The run keeps a record of the tree it last refused such a conversion on (
ConversionSettlement). When the pipeline reaches its fixpoint on that tree, every pass that decodes has run to its end, so the record settles the tree and the pipeline runs again over the same models, with the step budget carried over. A change in that run leaves the settled tree behind, since a fold may let a pass decode more, so the pipeline runs again as long as a run that changed the tree ends on one it refused a conversion on.Expand source code Browse git
def deobfuscate( ast: JsScript, max_steps: int = 5000, *, module: bool = False, entrypoints: tuple[str, ...] = (), environment: HostEnvironment = HostEnvironment.universal, trust_eval: bool = False, preserve_script_return: bool = False, observer: PipelineObserver | None = None, ) -> int: """ Apply all available deobfuscators to the input. A non-zero *max_steps* bounds the total number of change-producing transformer passes and raises `refinery.lib.scripts.pipeline.DeobfuscationTimeout` once it is exceeded, so a transform that fails to converge fails loudly instead of hanging. The default is generous — real inputs settle in a few to a few dozen passes (the differential corpus peaks in the low tens) — so it never bounds a legitimate deobfuscation, only a runaway loop. Pass `0` to disable the bound entirely. *module* selects the execution model the input is assumed to run under, *entrypoints* names top-level functions a host calls by name, and *environment* pins the host whose global names are assumed present; see `refinery.lib.scripts.js.options.DeobfuscationOptions`. *trust_eval* selects the trusting model, which assumes code supplied as data is inert and is deliberately unsound; the default is the suspecting model, so a caller that reads this function's output as equivalent to its input keeps that claim. *observer* is called around every transformer, which is how a property of the tree is attributed to the pass that moved it; see `refinery.lib.scripts.js.deobfuscation.audit.StrictModeAudit`. A conversion of a function that is handed on, with no evidence that the program installs a conversion on it, is folded only on a tree the run has settled: a key a later pass decodes may yet spell such an install. The run keeps a record of the tree it last refused such a conversion on (`refinery.lib.scripts.js.analysis.conversions.ConversionSettlement`). When the pipeline reaches its fixpoint on that tree, every pass that decodes has run to its end, so the record settles the tree and the pipeline runs again over the same models, with the step budget carried over. A change in that run leaves the settled tree behind, since a fold may let a pass decode more, so the pipeline runs again as long as a run that changed the tree ends on one it refused a conversion on. """ options = DeobfuscationOptions( module=module, entrypoints=tuple(entrypoints), environment=environment, trust_eval=trust_eval, preserve_script_return=preserve_script_return, ) settlement = ConversionSettlement() models = ModelCache(ast, options, settlement) steps = _pipeline.run( ast, max_steps=max_steps, models=models, options=options, observer=observer, ) while settlement.waits_on(tree_version(ast)): settlement.settle(tree_version(ast)) settled_steps = _pipeline.run( ast, max_steps=max_steps, initial_steps=steps, models=models, options=options, observer=observer, ) if settled_steps == steps: break steps = settled_steps return steps