Module refinery.lib.scripts.au3.deobfuscation

AutoIt3 AST deobfuscation transforms.

Five groups, in the order the obfuscated shapes need them. normalize rewrites what a call structurally is — an Execute call over a string literal becomes the expression that literal names, a call through an alias variable becomes the function that variable provably names, and a Call, Assign, or Eval over a string-literal name becomes the direct call, plain write, or plain read that name spells — refinery.lib.scripts.au3.deobfuscation.fold replaces what is known — an expression whose operands pin its value becomes the literal of that value, a variable read the propagation walk proves a value for becomes that literal, and a dispatcher loop whose state a provable constant pins becomes the one case body it ever runs — emulate replaces what a script-defined function returns: the interpreter runs the call over the value domain and the call becomes the literal of its result — cleanup removes what no live code reaches: a function no call names, a declarator no occurrence binds, an effect-free statement, and a plain assignment whose target nothing reads — each only when no dynamic-name channel could observe the removal — and canonicalize reprints what survived: a call whose callee names a builtin, or whose Call argument does, takes the official spelling of that name, a change no resolution reads.

refinery.lib.scripts.au3.deobfuscation.fold depends on normalize and re-triggers it: folding an Execute call's argument into the string literal it always was is what makes the next normalize pass see a flattenable call, and rewriting a callee through an alias is what makes the next refinery.lib.scripts.au3.deobfuscation.fold pass see the builtin the alias named. emulate depends on refinery.lib.scripts.au3.deobfuscation.fold — only literal arguments reach a decoded call — and every change it makes re-triggers the whole pipeline: a decoded "BinaryToString" is what the next normalize pass turns into an alias rewrite, and a decoded argument is what the next refinery.lib.scripts.au3.deobfuscation.fold pass folds. cleanup depends on emulate and takes no selective invalidation set: a removal changes everything the other models know, the emulate stance. canonicalize depends on cleanup and takes the empty one: it rewrites only spellings, every model keys lowercased names, and no group depends on it, so its changes re-run nothing. The group's internal loop is what iterates a sweep to its fixpoint — a function whose only callers an earlier sweep removed keeps its call sites until the next sweep re-census.

Expand source code Browse git
"""
AutoIt3 AST deobfuscation transforms.

Five groups, in the order the obfuscated shapes need them. `normalize` rewrites what a call
structurally is — an `Execute` call over a string literal becomes the expression that literal
names, a call through an alias variable becomes the function that variable provably names, and
a `Call`, `Assign`, or `Eval` over a string-literal name becomes the direct call, plain write,
or plain read that name spells — `fold` replaces what is known — an expression whose operands
pin its value becomes the literal of that value, a variable read the propagation walk proves a
value for becomes that literal, and a dispatcher loop whose state a provable constant pins becomes
the one case body it ever runs — `emulate` replaces what a script-defined function returns:
the interpreter runs the call over the value domain and the call becomes the literal of its
result — `cleanup` removes what no live code reaches: a function no call names, a declarator
no occurrence binds, an effect-free statement, and a plain assignment whose target nothing reads
— each only when no dynamic-name channel could observe the removal — and `canonicalize` reprints
what survived: a call whose callee names a builtin, or whose `Call` argument does, takes the
official spelling of that name, a change no resolution reads.

`fold` depends on `normalize` and re-triggers it: folding an `Execute` call's argument into the
string literal it always was is what makes the next `normalize` pass see a flattenable call, and
rewriting a callee through an alias is what makes the next `fold` pass see the builtin the alias
named. `emulate` depends on `fold` — only literal arguments reach a decoded call — and every
change it makes re-triggers the whole pipeline: a decoded `"BinaryToString"` is what the next
`normalize` pass turns into an alias rewrite, and a decoded argument is what the next `fold`
pass folds. `cleanup` depends on `emulate` and takes no selective invalidation set: a removal
changes everything the other models know, the `emulate` stance. `canonicalize` depends on
`cleanup` and takes the empty one: it rewrites only spellings, every model keys lowercased
names, and no group depends on it, so its changes re-run nothing. The group's internal loop is
what iterates a sweep to its fixpoint — a function whose only callers an earlier sweep removed
keeps its call sites until the next sweep re-census.
"""
from __future__ import annotations

from refinery.lib.scripts.au3.analysis import Au3ModelCache
from refinery.lib.scripts.au3.deobfuscation.casing import Au3BuiltinCaseNormalization
from refinery.lib.scripts.au3.deobfuscation.eliminate import Au3DeadCodeElimination
from refinery.lib.scripts.au3.deobfuscation.evaluate import Au3FunctionEvaluation
from refinery.lib.scripts.au3.deobfuscation.execute import Au3ExecuteFlattening
from refinery.lib.scripts.au3.deobfuscation.fold import Au3ConstantFolding
from refinery.lib.scripts.au3.deobfuscation.indirect import Au3IndirectCallRewrite
from refinery.lib.scripts.au3.deobfuscation.names import Au3DynamicNameFlattening
from refinery.lib.scripts.au3.deobfuscation.propagate import Au3ConstantPropagation
from refinery.lib.scripts.au3.deobfuscation.unflatten import Au3ControlFlowDeflattening
from refinery.lib.scripts.au3.deobfuscation.unused import Au3UnusedVariableRemoval
from refinery.lib.scripts.au3.model import Au3Script
from refinery.lib.scripts.pipeline import DeobfuscationPipeline, TransformerGroup

_normalize = TransformerGroup(
    'normalize',
    Au3ExecuteFlattening,
    Au3IndirectCallRewrite,
    Au3DynamicNameFlattening,
)

_fold = TransformerGroup(
    'fold',
    Au3ConstantFolding,
    Au3ConstantPropagation,
    Au3ControlFlowDeflattening,
)

_emulate = TransformerGroup(
    'emulate',
    Au3FunctionEvaluation,
)

_cleanup = TransformerGroup(
    'cleanup',
    Au3DeadCodeElimination,
    Au3UnusedVariableRemoval,
)

_canonicalize = TransformerGroup(
    'canonicalize',
    Au3BuiltinCaseNormalization,
)

_GROUP_DEPENDENCIES = {
    'fold': {'normalize'},
    'emulate': {'fold'},
    'cleanup': {'emulate'},
    'canonicalize': {'cleanup'},
}

_GROUP_INVALIDATORS = {
    'fold': {'normalize', 'emulate', 'cleanup', 'canonicalize'},
    'canonicalize': set(),
}

_pipeline = DeobfuscationPipeline(
    [_normalize, _fold, _emulate, _cleanup, _canonicalize],
    dependencies=_GROUP_DEPENDENCIES,
    invalidators=_GROUP_INVALIDATORS,
)


def deobfuscate(ast: Au3Script, max_steps: int = 5000) -> 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 set of
    transforms that fails to converge fails loudly instead of hanging. Pass `0` to disable the
    bound entirely.
    """
    return _pipeline.run(ast, max_steps=max_steps, models=Au3ModelCache(ast))

Sub-modules

refinery.lib.scripts.au3.deobfuscation.casing

Builtin case normalization: printing every reference to a builtin function the official documentation spells it with …

refinery.lib.scripts.au3.deobfuscation.eliminate

The Phase 4 transform: what no live code reaches leaves the tree. A function no call names and no string spells, a declarator no occurrence binds or …

refinery.lib.scripts.au3.deobfuscation.evaluate

The Phase 3 transform: what a script-defined decoder function returns becomes the literal its call site stands for. The interpreter runs the call over …

refinery.lib.scripts.au3.deobfuscation.execute

Execute flattening: splicing the one expression a string-literal argument provably names into the place of the call …

refinery.lib.scripts.au3.deobfuscation.fold

Constant folding: replacing an expression whose value every ingredient pins with the literal that spells it …

refinery.lib.scripts.au3.deobfuscation.helpers

Shared utilities for AutoIt3 deobfuscation transforms.

refinery.lib.scripts.au3.deobfuscation.indirect

Alias and indirection rewriting: replacing the callee of a call through an alias variable with the function that variable provably names …

refinery.lib.scripts.au3.deobfuscation.names

Dynamic-name flattening: replacing a Call, Assign, or Eval over a string-literal name with the direct call, plain write, or plain read that name …

refinery.lib.scripts.au3.deobfuscation.propagate

Constant propagation: replacing a variable read whose value the walk in refinery.lib.scripts.au3.analysis.propagation proves with the literal that …

refinery.lib.scripts.au3.deobfuscation.unflatten

Control-flow deflattening, degenerate form: a truthy-constant While whose body is one state assignment and one Switch over that state splices down …

refinery.lib.scripts.au3.deobfuscation.unused

The write-only store sweep: a plain assignment whose target nothing reads leaves the tree. The read census of …

Functions

def deobfuscate(ast, max_steps=5000)

Apply all available deobfuscators to the input. A non-zero max_steps bounds the total number of change-producing transformer passes and raises DeobfuscationTimeout once it is exceeded, so a set of transforms that fails to converge fails loudly instead of hanging. Pass 0 to disable the bound entirely.

Expand source code Browse git
def deobfuscate(ast: Au3Script, max_steps: int = 5000) -> 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 set of
    transforms that fails to converge fails loudly instead of hanging. Pass `0` to disable the
    bound entirely.
    """
    return _pipeline.run(ast, max_steps=max_steps, models=Au3ModelCache(ast))