Module refinery.lib.scripts.au3.deobfuscation.execute
Execute flattening: splicing the one expression a string-literal argument provably names into
the place of the call.
The interpreter evaluates exactly one expression there — an assignment inside the argument
parses as a comparison and writes nothing, declarations and statement sequences are parse errors
that answer "" — so the literal is read with the parser's expression entry, where = climbs as
a comparison. A literal that parses to a bare identifier never splices, in any position: what the
name evaluates to is unpinned, and the alias map in
refinery.lib.scripts.au3.analysis.aliases owns the declaration instead. A literal that parses
to any other expression splices in expression position — including a top-level = comparison,
which assignment duality reads back exactly as Execute did — and in statement position every
shape splices except that comparison: the synthesized line would re-parse as an assignment.
Every call inside the spliced expression must be a direct call to a script function or a table
builtin: a call an unseen #include resolves, or one through a variable, leaves the Execute
alone, because an unknown function is one of Execute's pinned failure modes and the spliced
form would make that error fatal where the original answered "". A literal that fails to parse
never ran at runtime, so it is left standing for the same reason, and so is one that parses to
an expression holding an array literal: an array literal is legal AutoIt only where a declaration
initializes, which no splice position is.
The splice also refuses wherever a read of @error or @extended may see the state the
Execute stood over: a successful Call, Assign, and Eval all clear both flags (probe9
A17, probe9f A21/A22), and whether a successful Execute clears them is unpinned, so a tree
that may read either — by macro, through a computed callee, through another Execute's
payload, or through an include — keeps every wrapper standing rather than guess. The model
cache owns that question; the decode pass over script functions refuses on the same answer.
The accepted behavior change — a runtime error the original Execute would have swallowed
becoming fatal where it happens — is bounded by these gates and owned by the Phase 2 plan.
Expand source code Browse git
"""
`Execute` flattening: splicing the one expression a string-literal argument provably names into
the place of the call.
The interpreter evaluates exactly one expression there — an assignment inside the argument
parses as a *comparison* and writes nothing, declarations and statement sequences are parse errors
that answer `""` — so the literal is read with the parser's expression entry, where `=` climbs as
a comparison. A literal that parses to a bare identifier never splices, in any position: what the
name evaluates to is unpinned, and the alias map in
`refinery.lib.scripts.au3.analysis.aliases` owns the declaration instead. A literal that parses
to any other expression splices in expression position — including a top-level `=` comparison,
which assignment duality reads back exactly as `Execute` did — and in statement position every
shape splices except that comparison: the synthesized line would re-parse as an assignment.
Every call inside the spliced expression must be a direct call to a script function or a table
builtin: a call an unseen `#include` resolves, or one through a variable, leaves the `Execute`
alone, because an unknown function is one of `Execute`'s pinned failure modes and the spliced
form would make that error fatal where the original answered `""`. A literal that fails to parse
never ran at runtime, so it is left standing for the same reason, and so is one that parses to
an expression holding an array literal: an array literal is legal AutoIt only where a declaration
initializes, which no splice position is.
The splice also refuses wherever a read of `@error` or `@extended` may see the state the
`Execute` stood over: a successful `Call`, `Assign`, and `Eval` all clear both flags (probe9
A17, probe9f A21/A22), and whether a successful `Execute` clears them is unpinned, so a tree
that may read either — by macro, through a computed callee, through another `Execute`'s
payload, or through an include — keeps every wrapper standing rather than guess. The model
cache owns that question; the decode pass over script functions refuses on the same answer.
The accepted behavior change — a runtime error the original `Execute` would have swallowed
becoming fatal where it happens — is bounded by these gates and owned by the Phase 2 plan.
"""
from __future__ import annotations
from refinery.lib.scripts import Node, Transformer, _replace_in_parent, set_child
from refinery.lib.scripts.au3 import ast
from refinery.lib.scripts.au3.analysis.cache import model_cache
from refinery.lib.scripts.au3.functions import AUTOIT_FUNCTION_NAMES
from refinery.lib.scripts.au3.model import (
Au3BinaryExpression,
Au3Call,
Au3ExpressionStatement,
Au3Identifier,
Au3Script,
Au3StringLiteral,
Expression,
)
from refinery.lib.scripts.au3.parser import Au3Parser
class Au3ExecuteFlattening(Transformer):
"""
Replaces an `Execute` call whose single argument is a string literal with the expression
that literal parses to, in every position the pinned semantics of `Execute` cover.
"""
def __init__(self):
super().__init__()
self._functions: frozenset[str] = frozenset()
self._error_state_is_observable = False
def visit(self, node: Node) -> Node | None:
if isinstance(node, Au3Script):
cache = model_cache(self, node)
self._functions = cache.scopes.function_names
self._error_state_is_observable = cache.may_read_error_state
self.generic_visit(node)
if not isinstance(node, Au3Call):
return None
expression = self._flattenable_expression(node)
if expression is None:
return None
return self._splice(node, expression)
def _flattenable_expression(self, call: Au3Call) -> Expression | None:
"""
The expression the call's literal argument names, or `None` where the call is not a
flattenable `Execute`: a shadowed name, a non-literal or missing argument, a parse
failure, a bare identifier, an array literal inside the parsed expression, a call
inside it this pass cannot resolve, or a tree whose error state a read may observe.
"""
callee = ast.direct_callee(call)
if callee is None or callee.name != 'execute':
return None
if 'execute' in self._functions:
return None
if self._error_state_is_observable:
return None
if len(call.arguments) != 1:
return None
literal = ast.unwrap_parens(call.arguments[0])
if not isinstance(literal, Au3StringLiteral):
return None
expression = Au3Parser(literal.value).parse_expression()
if expression is None:
return None
if isinstance(expression, Au3Identifier):
return None
if ast.contains_an_array_literal(expression):
return None
if not self._calls_resolve(expression):
return None
return expression
def _calls_resolve(self, expression: Expression) -> bool:
"""
Whether every call in *expression* is a direct call to a script function or a table
builtin.
"""
for node in expression.walk():
if not isinstance(node, Au3Call):
continue
callee = ast.direct_callee(node)
if callee is None:
return False
if callee.name not in self._functions and callee.name not in AUTOIT_FUNCTION_NAMES:
return False
return True
def _splice(self, call: Au3Call, expression: Expression) -> Node | None:
"""
Put *expression* where *call* stands: directly in expression position, and in statement
position as an expression statement — unless it is a top-level `=` comparison, which the
parser would read back as an assignment.
"""
statement = ast.owning_statement(call)
if (
isinstance(statement, Au3ExpressionStatement)
and ast.unwrap_parens(statement.expression) is call
):
if self._is_top_level_equality(expression):
return None
set_child(statement, 'expression', expression)
self.mark_changed()
return None
if not _replace_in_parent(call, expression):
return None
self.mark_changed()
return None
@staticmethod
def _is_top_level_equality(expression: Expression) -> bool:
return (
isinstance(expression, Au3BinaryExpression)
and expression.operator == '='
)
Classes
class Au3ExecuteFlattening-
Replaces an
Executecall whose single argument is a string literal with the expression that literal parses to, in every position the pinned semantics ofExecutecover.Expand source code Browse git
class Au3ExecuteFlattening(Transformer): """ Replaces an `Execute` call whose single argument is a string literal with the expression that literal parses to, in every position the pinned semantics of `Execute` cover. """ def __init__(self): super().__init__() self._functions: frozenset[str] = frozenset() self._error_state_is_observable = False def visit(self, node: Node) -> Node | None: if isinstance(node, Au3Script): cache = model_cache(self, node) self._functions = cache.scopes.function_names self._error_state_is_observable = cache.may_read_error_state self.generic_visit(node) if not isinstance(node, Au3Call): return None expression = self._flattenable_expression(node) if expression is None: return None return self._splice(node, expression) def _flattenable_expression(self, call: Au3Call) -> Expression | None: """ The expression the call's literal argument names, or `None` where the call is not a flattenable `Execute`: a shadowed name, a non-literal or missing argument, a parse failure, a bare identifier, an array literal inside the parsed expression, a call inside it this pass cannot resolve, or a tree whose error state a read may observe. """ callee = ast.direct_callee(call) if callee is None or callee.name != 'execute': return None if 'execute' in self._functions: return None if self._error_state_is_observable: return None if len(call.arguments) != 1: return None literal = ast.unwrap_parens(call.arguments[0]) if not isinstance(literal, Au3StringLiteral): return None expression = Au3Parser(literal.value).parse_expression() if expression is None: return None if isinstance(expression, Au3Identifier): return None if ast.contains_an_array_literal(expression): return None if not self._calls_resolve(expression): return None return expression def _calls_resolve(self, expression: Expression) -> bool: """ Whether every call in *expression* is a direct call to a script function or a table builtin. """ for node in expression.walk(): if not isinstance(node, Au3Call): continue callee = ast.direct_callee(node) if callee is None: return False if callee.name not in self._functions and callee.name not in AUTOIT_FUNCTION_NAMES: return False return True def _splice(self, call: Au3Call, expression: Expression) -> Node | None: """ Put *expression* where *call* stands: directly in expression position, and in statement position as an expression statement — unless it is a top-level `=` comparison, which the parser would read back as an assignment. """ statement = ast.owning_statement(call) if ( isinstance(statement, Au3ExpressionStatement) and ast.unwrap_parens(statement.expression) is call ): if self._is_top_level_equality(expression): return None set_child(statement, 'expression', expression) self.mark_changed() return None if not _replace_in_parent(call, expression): return None self.mark_changed() return None @staticmethod def _is_top_level_equality(expression: Expression) -> bool: return ( isinstance(expression, Au3BinaryExpression) and expression.operator == '=' )Ancestors
Methods
def visit(self, node)-
Expand source code Browse git
def visit(self, node: Node) -> Node | None: if isinstance(node, Au3Script): cache = model_cache(self, node) self._functions = cache.scopes.function_names self._error_state_is_observable = cache.may_read_error_state self.generic_visit(node) if not isinstance(node, Au3Call): return None expression = self._flattenable_expression(node) if expression is None: return None return self._splice(node, expression)
Inherited members