Module refinery.lib.scripts.au3.deobfuscation.casing
Builtin case normalization: printing every reference to a builtin function the official documentation spells it with.
AutoIt resolves function names without regard to case, so an obfuscator may spell a builtin any
way it likes and the call still resolves; the wide builtin table maps every lowercased name to
the official spelling, and this pass prints that one, so case-scrambling obfuscation leaves no
fingerprint in the output. Two shapes are rewritten: the direct callee of a call whose name the
table holds, and the string-literal first argument of a Call that names a builtin — the docs
say Call invokes builtins too, and no earlier pass resolves that name.
A spelling is rewritten only where it differs from the official one in ASCII case. Python's
str.lower also folds characters outside ASCII into ASCII letters — U+212A KELVIN SIGN becomes
k — and whether the engine folds them too is unpinned; the source spelling is right under
either answer, so the rewrite refuses such a name.
The callee rewrite changes only the printed spelling, never what the call resolves to, so it
carries no gate over what the tree holds, only the shadowing rule: a declared script function
owning the name keeps the call site's spelling, because printing a builtin's name there would
tell the reader the call is one. The Call argument is data the invoked function may read, so
its rewrite carries the same refusals the dynamic-name flattener reads — a script function of
the name Call shadows the builtin, an include may hide a Func Call no walk of the tree sees,
and a parse error leaves text no walk reads — and the same shadowing rule for the named
function. A registration builtin's string argument names a script function, never a builtin, and
no rule here reads it: only a Call holds a name this pass rewrites.
The pass runs in the final pipeline group, after every splice, fold, flatten, and alias rewrite has produced its final call shapes, so nothing it prints is later re-shaped. Its edits change no lowercased name and no declaration, so the scope snapshot the pass holds stays true mid-pass, and every spelling it writes is one a later round refuses, so the group runs it once.
Expand source code Browse git
"""
Builtin case normalization: printing every reference to a builtin function the official
documentation spells it with.
AutoIt resolves function names without regard to case, so an obfuscator may spell a builtin any
way it likes and the call still resolves; the wide builtin table maps every lowercased name to
the official spelling, and this pass prints that one, so case-scrambling obfuscation leaves no
fingerprint in the output. Two shapes are rewritten: the direct callee of a call whose name the
table holds, and the string-literal first argument of a `Call` that names a builtin — the docs
say `Call` invokes builtins too, and no earlier pass resolves that name.
A spelling is rewritten only where it differs from the official one in ASCII case. Python's
`str.lower` also folds characters outside ASCII into ASCII letters — U+212A KELVIN SIGN becomes
`k` — and whether the engine folds them too is unpinned; the source spelling is right under
either answer, so the rewrite refuses such a name.
The callee rewrite changes only the printed spelling, never what the call resolves to, so it
carries no gate over what the tree holds, only the shadowing rule: a declared script function
owning the name keeps the call site's spelling, because printing a builtin's name there would
tell the reader the call is one. The `Call` argument is data the invoked function may read, so
its rewrite carries the same refusals the dynamic-name flattener reads — a script function of
the name `Call` shadows the builtin, an include may hide a `Func Call` no walk of the tree sees,
and a parse error leaves text no walk reads — and the same shadowing rule for the named
function. A registration builtin's string argument names a script function, never a builtin, and
no rule here reads it: only a `Call` holds a name this pass rewrites.
The pass runs in the final pipeline group, after every splice, fold, flatten, and alias rewrite
has produced its final call shapes, so nothing it prints is later re-shaped. Its edits change no
lowercased name and no declaration, so the scope snapshot the pass holds stays true mid-pass,
and every spelling it writes is one a later round refuses, so the group runs it once.
"""
from __future__ import annotations
from refinery.lib.scripts import Node, Transformer, set_value
from refinery.lib.scripts.au3 import ast
from refinery.lib.scripts.au3.analysis.cache import model_cache
from refinery.lib.scripts.au3.analysis.scopes import Au3ScopeModel
from refinery.lib.scripts.au3.functions import AUTOIT_FUNCTION_NAMES
from refinery.lib.scripts.au3.lexer import ascii_lower, encode_au3_string
from refinery.lib.scripts.au3.model import (
Au3Call,
Au3Identifier,
Au3Script,
Au3StringLiteral,
)
class Au3BuiltinCaseNormalization(Transformer):
"""
Prints every reference to a builtin function — a call's callee, or the name a `Call` holds —
in the official documentation spelling.
"""
self_converging = True
def __init__(self):
super().__init__()
self._scopes = Au3ScopeModel({})
def visit(self, node: Node) -> Node | None:
if isinstance(node, Au3Script):
self._scopes = model_cache(self, node).scopes
self.generic_visit(node)
if not isinstance(node, Au3Call):
return None
callee = ast.direct_callee(node)
if callee is None:
return None
if callee.name not in self._scopes.functions:
self._normalize_callee(callee)
if callee.name == 'call':
self._normalize_call_name(node)
return None
def _normalize_callee(self, callee: Au3Identifier) -> None:
official = _official_spelling(callee.raw)
if official is None:
return
set_value(callee, 'raw', official)
self.mark_changed()
def _normalize_call_name(self, call: Au3Call) -> None:
if self._scopes.holds_an_include or self._scopes.holds_a_parse_error:
return
if not call.arguments:
return
literal = ast.unwrap_parens(call.arguments[0])
if not isinstance(literal, Au3StringLiteral):
return
if literal.value.lower() in self._scopes.functions:
return
official = _official_spelling(literal.value)
if official is None:
return
set_value(literal, 'value', official)
set_value(literal, 'raw', encode_au3_string(official))
self.mark_changed()
def _official_spelling(spelling: str) -> str | None:
"""
The official spelling of the builtin *spelling* names where the two differ in ASCII case
alone, or `None` where they are equal or no builtin is named. The lookup folds through
`refinery.lib.scripts.au3.lexer.ascii_lower`, so a spelling that only a fold beyond ASCII case
maps onto a builtin keeps a character no table key holds and names none.
"""
official = AUTOIT_FUNCTION_NAMES.get(ascii_lower(spelling))
if official is None or official == spelling:
return None
return official
Classes
class Au3BuiltinCaseNormalization-
Prints every reference to a builtin function — a call's callee, or the name a
Callholds — in the official documentation spelling.Expand source code Browse git
class Au3BuiltinCaseNormalization(Transformer): """ Prints every reference to a builtin function — a call's callee, or the name a `Call` holds — in the official documentation spelling. """ self_converging = True def __init__(self): super().__init__() self._scopes = Au3ScopeModel({}) def visit(self, node: Node) -> Node | None: if isinstance(node, Au3Script): self._scopes = model_cache(self, node).scopes self.generic_visit(node) if not isinstance(node, Au3Call): return None callee = ast.direct_callee(node) if callee is None: return None if callee.name not in self._scopes.functions: self._normalize_callee(callee) if callee.name == 'call': self._normalize_call_name(node) return None def _normalize_callee(self, callee: Au3Identifier) -> None: official = _official_spelling(callee.raw) if official is None: return set_value(callee, 'raw', official) self.mark_changed() def _normalize_call_name(self, call: Au3Call) -> None: if self._scopes.holds_an_include or self._scopes.holds_a_parse_error: return if not call.arguments: return literal = ast.unwrap_parens(call.arguments[0]) if not isinstance(literal, Au3StringLiteral): return if literal.value.lower() in self._scopes.functions: return official = _official_spelling(literal.value) if official is None: return set_value(literal, 'value', official) set_value(literal, 'raw', encode_au3_string(official)) self.mark_changed()Ancestors
Methods
def visit(self, node)-
Expand source code Browse git
def visit(self, node: Node) -> Node | None: if isinstance(node, Au3Script): self._scopes = model_cache(self, node).scopes self.generic_visit(node) if not isinstance(node, Au3Call): return None callee = ast.direct_callee(node) if callee is None: return None if callee.name not in self._scopes.functions: self._normalize_callee(callee) if callee.name == 'call': self._normalize_call_name(node) return None
Inherited members