Module refinery.units.scripting.deobfuscate

Expand source code Browse git
from __future__ import annotations

from typing import TYPE_CHECKING, Callable, NamedTuple

if TYPE_CHECKING:
    from refinery.lib.scripts import Node
    from refinery.lib.scripts.js.model import JsErrorNode
    from refinery.lib.scripts.js.parser import JsParser
    from refinery.lib.scripts.js.synth import JsSynthesizer
    from refinery.lib.scripts.ps1.model import Ps1ErrorNode
    from refinery.lib.scripts.ps1.parser import Ps1Parser
    from refinery.lib.scripts.ps1.synth import Ps1Synthesizer
    from refinery.lib.scripts.vba.model import VbaErrorNode
    from refinery.lib.scripts.vba.parser import VbaParser
    from refinery.lib.scripts.vba.synth import VbaSynthesizer

from refinery.lib.types import INF, Param
from refinery.units import Arg
from refinery.units.scripting import IterativeDeobfuscator


class _Backend(NamedTuple):
    name: str
    parser: type[JsParser] | type[Ps1Parser] | type[VbaParser]
    deobfuscate: Callable[..., int]
    synthesizer: type[JsSynthesizer] | type[Ps1Synthesizer] | type[VbaSynthesizer]
    error: type[JsErrorNode] | type[Ps1ErrorNode] | type[VbaErrorNode]
    #: The keyword this backend takes for the unit's output-preserving switch, or `None` where the
    #: language has no statement whose only effect is to write a value out. Named per backend rather
    #: than passed to all of them, because the same switch is spelled differently by each pipeline
    #: and a backend that has never heard of it must not be handed one.
    keep_output: str | None = None


class defu(IterativeDeobfuscator):
    """
    Universal script deobfuscator supporting JavaScript, PowerShell, and VBA.

    Attempts to parse the input as JavaScript, PowerShell, and VBA, then selects the language
    whose parser produces the fewest error nodes and applies the corresponding deobfuscation
    pipeline. The deobfuscation is executed iteratively until the output does not change any
    more; running the unit twice does not change the output.

    Where the selected language has a notion of a statement whose only effect is to write a value to
    the console, such a statement is deleted by default and the switch below keeps it; see
    `refinery.ps1` for what that costs and the assumption it rests on. A language without that
    notion ignores the switch.
    """

    _backend: _Backend

    def __init__(
        self,
        timeout=500,
        keep_output: Param[bool, Arg.Switch('-k', help=(
            'Keep every statement whose only effect is to write a value to the output stream, '
            'including values an obfuscator injected as noise. Use this when the input is a module '
            'or a fragment of a larger script.'))] = False,
    ):
        super().__init__(timeout=timeout, keep_output=keep_output)

    @classmethod
    def _backends(cls):
        from refinery.lib.scripts.ps1.deobfuscation import deobfuscate as ps1_deobfuscate
        from refinery.lib.scripts.ps1.model import Ps1ErrorNode
        from refinery.lib.scripts.ps1.parser import Ps1Parser
        from refinery.lib.scripts.ps1.synth import Ps1Synthesizer
        yield _Backend(
            'ps1',
            Ps1Parser,
            ps1_deobfuscate,
            Ps1Synthesizer,
            Ps1ErrorNode,
            keep_output='preserve_bare_output',
        )

        from refinery.lib.scripts.vba.deobfuscation import deobfuscate as vba_deobfuscate
        from refinery.lib.scripts.vba.model import VbaErrorNode
        from refinery.lib.scripts.vba.parser import VbaParser
        from refinery.lib.scripts.vba.synth import VbaSynthesizer
        yield _Backend('vba', VbaParser, vba_deobfuscate, VbaSynthesizer, VbaErrorNode)

        from refinery.lib.scripts.js.deobfuscation import deobfuscate as js_deobfuscate
        from refinery.lib.scripts.js.model import JsErrorNode
        from refinery.lib.scripts.js.parser import JsParser
        from refinery.lib.scripts.js.synth import JsSynthesizer
        yield _Backend('js', JsParser, js_deobfuscate, JsSynthesizer, JsErrorNode)

    def parse(self, data: str) -> Node:
        best_ast: Node | None = None
        best_errors = INF()
        best_backend = None
        for backend in self._backends():
            try:
                ast = backend.parser(data).parse()
                errors = sum(
                    len(n.text) for n in ast.walk() if isinstance(n, backend.error))
            except Exception:
                continue
            if errors < best_errors:
                best_errors = errors
                best_ast = ast
                best_backend = backend
                if errors == 0:
                    break
        if best_backend is None or best_ast is None or best_errors * 2 > len(data):
            raise ValueError('none of the available parsers was able to parse the input')
        self._backend = best_backend
        self.log_info(F'using {best_backend.name} with {best_errors / len(data) * 100:.2f}% errors')
        return best_ast

    def transform(self, ast: Node) -> int:
        keyword = self._backend.keep_output
        options = {} if keyword is None else {keyword: self.args.keep_output}
        return self._backend.deobfuscate(ast, **options)

    def synthesize(self, ast: Node) -> str:
        return self._backend.synthesizer().convert(ast)

Classes

class defu (timeout=500, keep_output=False)

Universal script deobfuscator supporting JavaScript, PowerShell, and VBA.

Attempts to parse the input as JavaScript, PowerShell, and VBA, then selects the language whose parser produces the fewest error nodes and applies the corresponding deobfuscation pipeline. The deobfuscation is executed iteratively until the output does not change any more; running the unit twice does not change the output.

Where the selected language has a notion of a statement whose only effect is to write a value to the console, such a statement is deleted by default and the switch below keeps it; see ps1 for what that costs and the assumption it rests on. A language without that notion ignores the switch.

Expand source code Browse git
class defu(IterativeDeobfuscator):
    """
    Universal script deobfuscator supporting JavaScript, PowerShell, and VBA.

    Attempts to parse the input as JavaScript, PowerShell, and VBA, then selects the language
    whose parser produces the fewest error nodes and applies the corresponding deobfuscation
    pipeline. The deobfuscation is executed iteratively until the output does not change any
    more; running the unit twice does not change the output.

    Where the selected language has a notion of a statement whose only effect is to write a value to
    the console, such a statement is deleted by default and the switch below keeps it; see
    `refinery.ps1` for what that costs and the assumption it rests on. A language without that
    notion ignores the switch.
    """

    _backend: _Backend

    def __init__(
        self,
        timeout=500,
        keep_output: Param[bool, Arg.Switch('-k', help=(
            'Keep every statement whose only effect is to write a value to the output stream, '
            'including values an obfuscator injected as noise. Use this when the input is a module '
            'or a fragment of a larger script.'))] = False,
    ):
        super().__init__(timeout=timeout, keep_output=keep_output)

    @classmethod
    def _backends(cls):
        from refinery.lib.scripts.ps1.deobfuscation import deobfuscate as ps1_deobfuscate
        from refinery.lib.scripts.ps1.model import Ps1ErrorNode
        from refinery.lib.scripts.ps1.parser import Ps1Parser
        from refinery.lib.scripts.ps1.synth import Ps1Synthesizer
        yield _Backend(
            'ps1',
            Ps1Parser,
            ps1_deobfuscate,
            Ps1Synthesizer,
            Ps1ErrorNode,
            keep_output='preserve_bare_output',
        )

        from refinery.lib.scripts.vba.deobfuscation import deobfuscate as vba_deobfuscate
        from refinery.lib.scripts.vba.model import VbaErrorNode
        from refinery.lib.scripts.vba.parser import VbaParser
        from refinery.lib.scripts.vba.synth import VbaSynthesizer
        yield _Backend('vba', VbaParser, vba_deobfuscate, VbaSynthesizer, VbaErrorNode)

        from refinery.lib.scripts.js.deobfuscation import deobfuscate as js_deobfuscate
        from refinery.lib.scripts.js.model import JsErrorNode
        from refinery.lib.scripts.js.parser import JsParser
        from refinery.lib.scripts.js.synth import JsSynthesizer
        yield _Backend('js', JsParser, js_deobfuscate, JsSynthesizer, JsErrorNode)

    def parse(self, data: str) -> Node:
        best_ast: Node | None = None
        best_errors = INF()
        best_backend = None
        for backend in self._backends():
            try:
                ast = backend.parser(data).parse()
                errors = sum(
                    len(n.text) for n in ast.walk() if isinstance(n, backend.error))
            except Exception:
                continue
            if errors < best_errors:
                best_errors = errors
                best_ast = ast
                best_backend = backend
                if errors == 0:
                    break
        if best_backend is None or best_ast is None or best_errors * 2 > len(data):
            raise ValueError('none of the available parsers was able to parse the input')
        self._backend = best_backend
        self.log_info(F'using {best_backend.name} with {best_errors / len(data) * 100:.2f}% errors')
        return best_ast

    def transform(self, ast: Node) -> int:
        keyword = self._backend.keep_output
        options = {} if keyword is None else {keyword: self.args.keep_output}
        return self._backend.deobfuscate(ast, **options)

    def synthesize(self, ast: Node) -> str:
        return self._backend.synthesizer().convert(ast)

Ancestors

Subclasses

Class variables

var reverse

The type of the None singleton.

Methods

def parse(self, data)
Expand source code Browse git
def parse(self, data: str) -> Node:
    best_ast: Node | None = None
    best_errors = INF()
    best_backend = None
    for backend in self._backends():
        try:
            ast = backend.parser(data).parse()
            errors = sum(
                len(n.text) for n in ast.walk() if isinstance(n, backend.error))
        except Exception:
            continue
        if errors < best_errors:
            best_errors = errors
            best_ast = ast
            best_backend = backend
            if errors == 0:
                break
    if best_backend is None or best_ast is None or best_errors * 2 > len(data):
        raise ValueError('none of the available parsers was able to parse the input')
    self._backend = best_backend
    self.log_info(F'using {best_backend.name} with {best_errors / len(data) * 100:.2f}% errors')
    return best_ast
def transform(self, ast)
Expand source code Browse git
def transform(self, ast: Node) -> int:
    keyword = self._backend.keep_output
    options = {} if keyword is None else {keyword: self.args.keep_output}
    return self._backend.deobfuscate(ast, **options)
def synthesize(self, ast)
Expand source code Browse git
def synthesize(self, ast: Node) -> str:
    return self._backend.synthesizer().convert(ast)

Inherited members