Module refinery.lib.scripts.ps1.deobfuscation.simplify
PowerShell syntax normalization transforms.
Expand source code Browse git
"""
PowerShell syntax normalization transforms.
"""
from __future__ import annotations
from refinery.lib.scripts import Node, Transformer, set_value
from refinery.lib.scripts.ps1.analysis.cache import model_cache
from refinery.lib.scripts.ps1.analysis.commands import CommandKind, Ps1CommandModel
from refinery.lib.scripts.ps1.analysis.dataflow import Ps1VariableFlow
from refinery.lib.scripts.ps1.analysis.separator import coerced_text_at
from refinery.lib.scripts.ps1.ast import get_command_name
from refinery.lib.scripts.ps1.data import (
ALL_PARAMETER_NAMES,
KNOWN_PS_OPERATORS,
KNOWN_PS_SWITCHES,
PS1_KNOWN_VARIABLES,
SIMPLE_IDENTIFIER,
TYPE_ARG_COMMANDS,
)
from refinery.lib.scripts.ps1.deobfuscation.helpers import (
is_bare_command_name,
make_string_literal,
)
from refinery.lib.scripts.ps1.deobfuscation.substitution import substitute_field, substitute_list
from refinery.lib.scripts.ps1.deobfuscation.typenames import canonical_type_name
from refinery.lib.scripts.ps1.model import (
Ps1BinaryExpression,
Ps1CastExpression,
Ps1ClassDefinition,
Ps1CommandArgument,
Ps1CommandArgumentKind,
Ps1CommandInvocation,
Ps1ExpandableString,
Ps1ExpressionStatement,
Ps1FunctionDefinition,
Ps1HereString,
Ps1IntegerLiteral,
Ps1InvokeMember,
Ps1MemberAccess,
Ps1ParenExpression,
Ps1RealLiteral,
Ps1ScopeModifier,
Ps1Script,
Ps1StringLiteral,
Ps1SubExpression,
Ps1TypeExpression,
Ps1UnaryExpression,
Ps1Variable,
)
from refinery.lib.scripts.ps1.token import _strip_backtick_noop
from refinery.lib.scripts.win32const import DEFAULT_ENVIRONMENT_TEMPLATE
_KNOWN_ENV_NAMES: dict[str, str] = {
name.lower(): name for name in DEFAULT_ENVIRONMENT_TEMPLATE
}
def _has_wildcard(name: str) -> bool:
return any(c in name for c in '*?[')
class Ps1Simplifications(Transformer):
def __init__(self):
super().__init__()
self._commands: Ps1CommandModel | None = None
self._flow: Ps1VariableFlow | None = None
self._entry = False
def visit(self, node: Node):
"""
The models are captured once at the root and dropped again when the walk they were captured
for ends, for the reason
`refinery.lib.scripts.ps1.deobfuscation.typenames.VariableTypeAwareTransformer` states: a
second walk over a tree the first one rewrote enters on the guarded arm and would otherwise
be answered from the first walk's graphs.
"""
if self._entry or not isinstance(node, Ps1Script):
return super().visit(node)
self._entry = True
try:
cache = model_cache(self, node)
self._commands = cache.commands
self._flow = cache.variable_flow
return super().visit(node)
finally:
self._entry = False
self._commands = None
self._flow = None
def visit_Ps1Variable(self, node: Ps1Variable):
self.generic_visit(node)
if '`' in node.name:
set_value(node, 'name', _strip_backtick_noop(node.name))
self.mark_changed()
if node.braced and SIMPLE_IDENTIFIER.match(node.name):
set_value(node, 'braced', False)
self.mark_changed()
canonical = PS1_KNOWN_VARIABLES.get(node.name.lower())
if canonical is not None and canonical != node.name:
set_value(node, 'name', canonical)
self.mark_changed()
if node.scope == Ps1ScopeModifier.ENV:
canonical = _KNOWN_ENV_NAMES.get(node.name.lower())
if canonical is not None and canonical != node.name:
set_value(node, 'name', canonical)
self.mark_changed()
return None
def visit_Ps1FunctionDefinition(self, node: Ps1FunctionDefinition):
self.generic_visit(node)
if '`' in node.name:
set_value(node, 'name', _strip_backtick_noop(node.name))
self.mark_changed()
return None
def visit_Ps1ClassDefinition(self, node: Ps1ClassDefinition):
self.generic_visit(node)
if '`' in node.name:
set_value(node, 'name', _strip_backtick_noop(node.name))
self.mark_changed()
return None
def visit_Ps1ParenExpression(self, node: Ps1ParenExpression):
self.generic_visit(node)
inner = node.expression
if isinstance(inner, (Ps1StringLiteral, Ps1HereString, Ps1IntegerLiteral, Ps1RealLiteral, Ps1TypeExpression)):
return inner
return None
def visit_Ps1SubExpression(self, node: Ps1SubExpression):
self.generic_visit(node)
if isinstance(node.parent, Ps1ExpandableString):
return None
if len(node.body) == 1:
stmt = node.body[0]
if isinstance(stmt, Ps1ExpressionStatement):
inner = stmt.expression
if isinstance(inner, (
Ps1Variable,
Ps1StringLiteral,
Ps1IntegerLiteral,
Ps1RealLiteral,
Ps1TypeExpression,
Ps1CastExpression,
)):
return inner
return None
def visit_Ps1ExpandableString(self, node: Ps1ExpandableString):
"""
An expandable string every part of which is a constant, written as the plain string it
produces.
What a subexpression contributes is what the value it holds *renders* to and not the way it
was written — measured, `"$(0xFF)"` is `255` and `"$([char]65)"` is `A` — which is the same
question `refinery.lib.scripts.ps1.deobfuscation.constants` answers for a variable
substituted into one of these. A collection contributes its elements separated by `$OFS`,
which is why the question is asked at the string rather than of the value alone.
"""
self.generic_visit(node)
if self._flow is None:
return None
parts: list[str] = []
for p in node.parts:
if isinstance(p, Ps1StringLiteral):
parts.append(p.value)
continue
if isinstance(p, Ps1SubExpression) and len(p.body) == 1:
stmt = p.body[0]
if isinstance(stmt, Ps1ExpressionStatement) and stmt.expression is not None:
sv = coerced_text_at(stmt.expression, node, self._flow)
if sv is not None:
parts.append(sv)
continue
return None
return make_string_literal(''.join(parts))
def visit_Ps1MemberAccess(self, node: Ps1MemberAccess):
self.generic_visit(node)
self._normalize_member(node)
return None
def visit_Ps1InvokeMember(self, node: Ps1InvokeMember):
self.generic_visit(node)
self._normalize_member(node)
return None
def _normalize_member(self, node: Ps1MemberAccess | Ps1InvokeMember):
if not isinstance(node.member, Ps1StringLiteral):
return
name = node.member.value
if node.member.raw and node.member.raw[0] == '"' and '`' in node.member.raw:
name = _strip_backtick_noop(node.member.raw[1:-1])
if SIMPLE_IDENTIFIER.match(name):
set_value(node, 'member', name)
self.mark_changed()
def visit_Ps1BinaryExpression(self, node: Ps1BinaryExpression):
self.generic_visit(node)
normalized = KNOWN_PS_OPERATORS.get(node.operator.lower(), node.operator)
if normalized != node.operator:
set_value(node, 'operator', normalized)
self.mark_changed()
return None
def visit_Ps1UnaryExpression(self, node: Ps1UnaryExpression):
self.generic_visit(node)
normalized = KNOWN_PS_OPERATORS.get(node.operator.lower(), node.operator)
if normalized != node.operator:
set_value(node, 'operator', normalized)
self.mark_changed()
return None
def visit_Ps1CommandArgument(self, node: Ps1CommandArgument):
self.generic_visit(node)
if node.kind in (Ps1CommandArgumentKind.SWITCH, Ps1CommandArgumentKind.NAMED):
if '`' in node.name:
set_value(node, 'name', _strip_backtick_noop(node.name))
self.mark_changed()
name_lower = node.name.lower()
normalized = KNOWN_PS_OPERATORS.get(name_lower)
if normalized is None:
normalized = KNOWN_PS_SWITCHES.get(name_lower)
if normalized is None:
bare = name_lower.lstrip('-')
if bare != name_lower:
canonical = ALL_PARAMETER_NAMES.get(bare)
if canonical is not None:
normalized = F'-{canonical}'
if normalized is not None and normalized != node.name:
set_value(node, 'name', normalized)
self.mark_changed()
return None
def visit_Ps1TypeExpression(self, node: Ps1TypeExpression):
self._normalize_type_field(node, 'name')
return None
def visit_Ps1CastExpression(self, node: Ps1CastExpression):
self.generic_visit(node)
self._normalize_type_field(node, 'type_name')
return None
def _normalize_type_field(self, node: Node, attr: str) -> None:
spelled = getattr(node, attr)
normalized = self._normalize_type_name(spelled)
if normalized != spelled:
set_value(node, attr, normalized)
def _normalize_type_name(self, name: str) -> str:
canonical = canonical_type_name(name)
if canonical is not None and canonical != name:
self.mark_changed()
return canonical
return name
def _operator_is_noise(self, node: Ps1CommandInvocation) -> bool:
"""
Whether dropping the invocation operator from `node` leaves the same program. `&` always
does: it only forces command position. `.` does not — it runs the target in the *caller's*
scope, so a script file or a function dot-sourced this way writes its definitions, variables
and type-system changes here rather than into a child scope. A compiled cmdlet has no such
body and cannot tell the two apart, so the dot may be dropped only from a name the command
model resolves to a cmdlet — never from one a `function`/`filter`, a `function:`/`alias:`
assignment, or an unresolved `Set-Alias` has taken over, each of which the model reports as
something other than a cmdlet.
The world reads the surviving dot as its evidence that off-tree code runs
(`refinery.lib.scripts.ps1.analysis.world.runs_another_script_file`), so dropping it from
`. helper` did not merely change scope: the world, rebuilt from the stripped tree, then read
closed and every grant in the script fired.
"""
if node.invocation_operator == '&':
return True
if self._commands is None:
return False
return self._commands.denotation(node).kind is CommandKind.CMDLET
def visit_Ps1CommandInvocation(self, node: Ps1CommandInvocation):
self.generic_visit(node)
old_name = node.name
if isinstance(node.name, Ps1ParenExpression) and node.name.expression is not None:
inner = node.name.expression
if isinstance(inner, Ps1StringLiteral):
substitute_field(node, 'name', inner)
elif isinstance(inner, Ps1CommandInvocation):
c = get_command_name(inner)
if c is not None and c.lower() in ('gcm', 'get-command'):
if len(inner.arguments) == 1:
arg = inner.arguments[0]
if isinstance(arg, Ps1CommandArgument):
arg = arg.value
if isinstance(arg, Ps1ParenExpression):
arg = arg.expression
# Only resolve a concrete command name; a wildcard pattern such as
# `gcm i*e-e*` must not be substituted verbatim as the command name.
if isinstance(arg, Ps1StringLiteral) and not _has_wildcard(arg.value):
substitute_field(node, 'name', arg)
if node.name is not old_name:
self.mark_changed()
if node.name and isinstance(node.name, Ps1StringLiteral):
if '`' in node.name.value:
stripped = _strip_backtick_noop(node.name.value)
substitute_field(node, 'name', Ps1StringLiteral(
offset=node.name.offset,
value=stripped,
raw=stripped,
))
self.mark_changed()
if is_bare_command_name(node.name.value) and node.name.raw != node.name.value:
substitute_field(node, 'name', Ps1StringLiteral(
offset=node.name.offset,
value=node.name.value,
raw=node.name.value,
))
self.mark_changed()
if node.invocation_operator in ('&', '.'):
if isinstance(node.name, Ps1StringLiteral):
name_val = node.name.value
if (
(SIMPLE_IDENTIFIER.match(name_val) or '-' in name_val)
and is_bare_command_name(name_val)
and not _has_wildcard(name_val)
and self._operator_is_noise(node)
):
substitute_field(node, 'name', Ps1StringLiteral(
offset=node.name.offset,
value=name_val,
raw=name_val,
))
set_value(node, 'invocation_operator', '')
self.mark_changed()
if (c := get_command_name(node)) and c.lower() in TYPE_ARG_COMMANDS:
self._normalize_first_positional_type_arg(node)
return None
def _normalize_first_positional_type_arg(self, node: Ps1CommandInvocation):
for arg in node.arguments:
if isinstance(arg, Ps1CommandArgument):
if arg.kind == Ps1CommandArgumentKind.NAMED:
if arg.name.lstrip('-').lower() == 'class' and isinstance(arg.value, Ps1StringLiteral):
normalized = self._normalize_type_name(arg.value.value)
if normalized != arg.value.value:
substitute_field(arg, 'value', Ps1StringLiteral(
offset=arg.value.offset, value=normalized, raw=normalized))
continue
if arg.kind != Ps1CommandArgumentKind.POSITIONAL:
continue
if isinstance(arg.value, Ps1StringLiteral):
normalized = self._normalize_type_name(arg.value.value)
if normalized != arg.value.value:
substitute_field(arg, 'value', Ps1StringLiteral(
offset=arg.value.offset, value=normalized, raw=normalized))
return
if isinstance(arg, Ps1StringLiteral):
normalized = self._normalize_type_name(arg.value)
if normalized != arg.value:
arguments = list(node.arguments)
arguments[arguments.index(arg)] = Ps1StringLiteral(
offset=arg.offset, value=normalized, raw=normalized)
substitute_list(node, 'arguments', arguments)
return
Classes
class Ps1Simplifications-
In-place tree rewriter. Each visit method may return a replacement node or
Noneto keep the original. Tracks whether any transformation was applied via thechangedflag.When a
modelscache is attached by the pipeline, settingchangedtruthy invalidates it, so a transform that mutates the tree never leaves a stale model behind for the next consumer.Expand source code Browse git
class Ps1Simplifications(Transformer): def __init__(self): super().__init__() self._commands: Ps1CommandModel | None = None self._flow: Ps1VariableFlow | None = None self._entry = False def visit(self, node: Node): """ The models are captured once at the root and dropped again when the walk they were captured for ends, for the reason `refinery.lib.scripts.ps1.deobfuscation.typenames.VariableTypeAwareTransformer` states: a second walk over a tree the first one rewrote enters on the guarded arm and would otherwise be answered from the first walk's graphs. """ if self._entry or not isinstance(node, Ps1Script): return super().visit(node) self._entry = True try: cache = model_cache(self, node) self._commands = cache.commands self._flow = cache.variable_flow return super().visit(node) finally: self._entry = False self._commands = None self._flow = None def visit_Ps1Variable(self, node: Ps1Variable): self.generic_visit(node) if '`' in node.name: set_value(node, 'name', _strip_backtick_noop(node.name)) self.mark_changed() if node.braced and SIMPLE_IDENTIFIER.match(node.name): set_value(node, 'braced', False) self.mark_changed() canonical = PS1_KNOWN_VARIABLES.get(node.name.lower()) if canonical is not None and canonical != node.name: set_value(node, 'name', canonical) self.mark_changed() if node.scope == Ps1ScopeModifier.ENV: canonical = _KNOWN_ENV_NAMES.get(node.name.lower()) if canonical is not None and canonical != node.name: set_value(node, 'name', canonical) self.mark_changed() return None def visit_Ps1FunctionDefinition(self, node: Ps1FunctionDefinition): self.generic_visit(node) if '`' in node.name: set_value(node, 'name', _strip_backtick_noop(node.name)) self.mark_changed() return None def visit_Ps1ClassDefinition(self, node: Ps1ClassDefinition): self.generic_visit(node) if '`' in node.name: set_value(node, 'name', _strip_backtick_noop(node.name)) self.mark_changed() return None def visit_Ps1ParenExpression(self, node: Ps1ParenExpression): self.generic_visit(node) inner = node.expression if isinstance(inner, (Ps1StringLiteral, Ps1HereString, Ps1IntegerLiteral, Ps1RealLiteral, Ps1TypeExpression)): return inner return None def visit_Ps1SubExpression(self, node: Ps1SubExpression): self.generic_visit(node) if isinstance(node.parent, Ps1ExpandableString): return None if len(node.body) == 1: stmt = node.body[0] if isinstance(stmt, Ps1ExpressionStatement): inner = stmt.expression if isinstance(inner, ( Ps1Variable, Ps1StringLiteral, Ps1IntegerLiteral, Ps1RealLiteral, Ps1TypeExpression, Ps1CastExpression, )): return inner return None def visit_Ps1ExpandableString(self, node: Ps1ExpandableString): """ An expandable string every part of which is a constant, written as the plain string it produces. What a subexpression contributes is what the value it holds *renders* to and not the way it was written — measured, `"$(0xFF)"` is `255` and `"$([char]65)"` is `A` — which is the same question `refinery.lib.scripts.ps1.deobfuscation.constants` answers for a variable substituted into one of these. A collection contributes its elements separated by `$OFS`, which is why the question is asked at the string rather than of the value alone. """ self.generic_visit(node) if self._flow is None: return None parts: list[str] = [] for p in node.parts: if isinstance(p, Ps1StringLiteral): parts.append(p.value) continue if isinstance(p, Ps1SubExpression) and len(p.body) == 1: stmt = p.body[0] if isinstance(stmt, Ps1ExpressionStatement) and stmt.expression is not None: sv = coerced_text_at(stmt.expression, node, self._flow) if sv is not None: parts.append(sv) continue return None return make_string_literal(''.join(parts)) def visit_Ps1MemberAccess(self, node: Ps1MemberAccess): self.generic_visit(node) self._normalize_member(node) return None def visit_Ps1InvokeMember(self, node: Ps1InvokeMember): self.generic_visit(node) self._normalize_member(node) return None def _normalize_member(self, node: Ps1MemberAccess | Ps1InvokeMember): if not isinstance(node.member, Ps1StringLiteral): return name = node.member.value if node.member.raw and node.member.raw[0] == '"' and '`' in node.member.raw: name = _strip_backtick_noop(node.member.raw[1:-1]) if SIMPLE_IDENTIFIER.match(name): set_value(node, 'member', name) self.mark_changed() def visit_Ps1BinaryExpression(self, node: Ps1BinaryExpression): self.generic_visit(node) normalized = KNOWN_PS_OPERATORS.get(node.operator.lower(), node.operator) if normalized != node.operator: set_value(node, 'operator', normalized) self.mark_changed() return None def visit_Ps1UnaryExpression(self, node: Ps1UnaryExpression): self.generic_visit(node) normalized = KNOWN_PS_OPERATORS.get(node.operator.lower(), node.operator) if normalized != node.operator: set_value(node, 'operator', normalized) self.mark_changed() return None def visit_Ps1CommandArgument(self, node: Ps1CommandArgument): self.generic_visit(node) if node.kind in (Ps1CommandArgumentKind.SWITCH, Ps1CommandArgumentKind.NAMED): if '`' in node.name: set_value(node, 'name', _strip_backtick_noop(node.name)) self.mark_changed() name_lower = node.name.lower() normalized = KNOWN_PS_OPERATORS.get(name_lower) if normalized is None: normalized = KNOWN_PS_SWITCHES.get(name_lower) if normalized is None: bare = name_lower.lstrip('-') if bare != name_lower: canonical = ALL_PARAMETER_NAMES.get(bare) if canonical is not None: normalized = F'-{canonical}' if normalized is not None and normalized != node.name: set_value(node, 'name', normalized) self.mark_changed() return None def visit_Ps1TypeExpression(self, node: Ps1TypeExpression): self._normalize_type_field(node, 'name') return None def visit_Ps1CastExpression(self, node: Ps1CastExpression): self.generic_visit(node) self._normalize_type_field(node, 'type_name') return None def _normalize_type_field(self, node: Node, attr: str) -> None: spelled = getattr(node, attr) normalized = self._normalize_type_name(spelled) if normalized != spelled: set_value(node, attr, normalized) def _normalize_type_name(self, name: str) -> str: canonical = canonical_type_name(name) if canonical is not None and canonical != name: self.mark_changed() return canonical return name def _operator_is_noise(self, node: Ps1CommandInvocation) -> bool: """ Whether dropping the invocation operator from `node` leaves the same program. `&` always does: it only forces command position. `.` does not — it runs the target in the *caller's* scope, so a script file or a function dot-sourced this way writes its definitions, variables and type-system changes here rather than into a child scope. A compiled cmdlet has no such body and cannot tell the two apart, so the dot may be dropped only from a name the command model resolves to a cmdlet — never from one a `function`/`filter`, a `function:`/`alias:` assignment, or an unresolved `Set-Alias` has taken over, each of which the model reports as something other than a cmdlet. The world reads the surviving dot as its evidence that off-tree code runs (`refinery.lib.scripts.ps1.analysis.world.runs_another_script_file`), so dropping it from `. helper` did not merely change scope: the world, rebuilt from the stripped tree, then read closed and every grant in the script fired. """ if node.invocation_operator == '&': return True if self._commands is None: return False return self._commands.denotation(node).kind is CommandKind.CMDLET def visit_Ps1CommandInvocation(self, node: Ps1CommandInvocation): self.generic_visit(node) old_name = node.name if isinstance(node.name, Ps1ParenExpression) and node.name.expression is not None: inner = node.name.expression if isinstance(inner, Ps1StringLiteral): substitute_field(node, 'name', inner) elif isinstance(inner, Ps1CommandInvocation): c = get_command_name(inner) if c is not None and c.lower() in ('gcm', 'get-command'): if len(inner.arguments) == 1: arg = inner.arguments[0] if isinstance(arg, Ps1CommandArgument): arg = arg.value if isinstance(arg, Ps1ParenExpression): arg = arg.expression # Only resolve a concrete command name; a wildcard pattern such as # `gcm i*e-e*` must not be substituted verbatim as the command name. if isinstance(arg, Ps1StringLiteral) and not _has_wildcard(arg.value): substitute_field(node, 'name', arg) if node.name is not old_name: self.mark_changed() if node.name and isinstance(node.name, Ps1StringLiteral): if '`' in node.name.value: stripped = _strip_backtick_noop(node.name.value) substitute_field(node, 'name', Ps1StringLiteral( offset=node.name.offset, value=stripped, raw=stripped, )) self.mark_changed() if is_bare_command_name(node.name.value) and node.name.raw != node.name.value: substitute_field(node, 'name', Ps1StringLiteral( offset=node.name.offset, value=node.name.value, raw=node.name.value, )) self.mark_changed() if node.invocation_operator in ('&', '.'): if isinstance(node.name, Ps1StringLiteral): name_val = node.name.value if ( (SIMPLE_IDENTIFIER.match(name_val) or '-' in name_val) and is_bare_command_name(name_val) and not _has_wildcard(name_val) and self._operator_is_noise(node) ): substitute_field(node, 'name', Ps1StringLiteral( offset=node.name.offset, value=name_val, raw=name_val, )) set_value(node, 'invocation_operator', '') self.mark_changed() if (c := get_command_name(node)) and c.lower() in TYPE_ARG_COMMANDS: self._normalize_first_positional_type_arg(node) return None def _normalize_first_positional_type_arg(self, node: Ps1CommandInvocation): for arg in node.arguments: if isinstance(arg, Ps1CommandArgument): if arg.kind == Ps1CommandArgumentKind.NAMED: if arg.name.lstrip('-').lower() == 'class' and isinstance(arg.value, Ps1StringLiteral): normalized = self._normalize_type_name(arg.value.value) if normalized != arg.value.value: substitute_field(arg, 'value', Ps1StringLiteral( offset=arg.value.offset, value=normalized, raw=normalized)) continue if arg.kind != Ps1CommandArgumentKind.POSITIONAL: continue if isinstance(arg.value, Ps1StringLiteral): normalized = self._normalize_type_name(arg.value.value) if normalized != arg.value.value: substitute_field(arg, 'value', Ps1StringLiteral( offset=arg.value.offset, value=normalized, raw=normalized)) return if isinstance(arg, Ps1StringLiteral): normalized = self._normalize_type_name(arg.value) if normalized != arg.value: arguments = list(node.arguments) arguments[arguments.index(arg)] = Ps1StringLiteral( offset=arg.offset, value=normalized, raw=normalized) substitute_list(node, 'arguments', arguments) returnAncestors
Methods
def visit(self, node)-
The models are captured once at the root and dropped again when the walk they were captured for ends, for the reason
VariableTypeAwareTransformerstates: a second walk over a tree the first one rewrote enters on the guarded arm and would otherwise be answered from the first walk's graphs.Expand source code Browse git
def visit(self, node: Node): """ The models are captured once at the root and dropped again when the walk they were captured for ends, for the reason `refinery.lib.scripts.ps1.deobfuscation.typenames.VariableTypeAwareTransformer` states: a second walk over a tree the first one rewrote enters on the guarded arm and would otherwise be answered from the first walk's graphs. """ if self._entry or not isinstance(node, Ps1Script): return super().visit(node) self._entry = True try: cache = model_cache(self, node) self._commands = cache.commands self._flow = cache.variable_flow return super().visit(node) finally: self._entry = False self._commands = None self._flow = None def visit_Ps1Variable(self, node)-
Expand source code Browse git
def visit_Ps1Variable(self, node: Ps1Variable): self.generic_visit(node) if '`' in node.name: set_value(node, 'name', _strip_backtick_noop(node.name)) self.mark_changed() if node.braced and SIMPLE_IDENTIFIER.match(node.name): set_value(node, 'braced', False) self.mark_changed() canonical = PS1_KNOWN_VARIABLES.get(node.name.lower()) if canonical is not None and canonical != node.name: set_value(node, 'name', canonical) self.mark_changed() if node.scope == Ps1ScopeModifier.ENV: canonical = _KNOWN_ENV_NAMES.get(node.name.lower()) if canonical is not None and canonical != node.name: set_value(node, 'name', canonical) self.mark_changed() return None def visit_Ps1FunctionDefinition(self, node)-
Expand source code Browse git
def visit_Ps1FunctionDefinition(self, node: Ps1FunctionDefinition): self.generic_visit(node) if '`' in node.name: set_value(node, 'name', _strip_backtick_noop(node.name)) self.mark_changed() return None def visit_Ps1ClassDefinition(self, node)-
Expand source code Browse git
def visit_Ps1ClassDefinition(self, node: Ps1ClassDefinition): self.generic_visit(node) if '`' in node.name: set_value(node, 'name', _strip_backtick_noop(node.name)) self.mark_changed() return None def visit_Ps1ParenExpression(self, node)-
Expand source code Browse git
def visit_Ps1ParenExpression(self, node: Ps1ParenExpression): self.generic_visit(node) inner = node.expression if isinstance(inner, (Ps1StringLiteral, Ps1HereString, Ps1IntegerLiteral, Ps1RealLiteral, Ps1TypeExpression)): return inner return None def visit_Ps1SubExpression(self, node)-
Expand source code Browse git
def visit_Ps1SubExpression(self, node: Ps1SubExpression): self.generic_visit(node) if isinstance(node.parent, Ps1ExpandableString): return None if len(node.body) == 1: stmt = node.body[0] if isinstance(stmt, Ps1ExpressionStatement): inner = stmt.expression if isinstance(inner, ( Ps1Variable, Ps1StringLiteral, Ps1IntegerLiteral, Ps1RealLiteral, Ps1TypeExpression, Ps1CastExpression, )): return inner return None def visit_Ps1ExpandableString(self, node)-
An expandable string every part of which is a constant, written as the plain string it produces.
What a subexpression contributes is what the value it holds renders to and not the way it was written — measured,
"$(0xFF)"is255and"$([char]65)"isA— which is the same questionrefinery.lib.scripts.ps1.deobfuscation.constantsanswers for a variable substituted into one of these. A collection contributes its elements separated by$OFS, which is why the question is asked at the string rather than of the value alone.Expand source code Browse git
def visit_Ps1ExpandableString(self, node: Ps1ExpandableString): """ An expandable string every part of which is a constant, written as the plain string it produces. What a subexpression contributes is what the value it holds *renders* to and not the way it was written — measured, `"$(0xFF)"` is `255` and `"$([char]65)"` is `A` — which is the same question `refinery.lib.scripts.ps1.deobfuscation.constants` answers for a variable substituted into one of these. A collection contributes its elements separated by `$OFS`, which is why the question is asked at the string rather than of the value alone. """ self.generic_visit(node) if self._flow is None: return None parts: list[str] = [] for p in node.parts: if isinstance(p, Ps1StringLiteral): parts.append(p.value) continue if isinstance(p, Ps1SubExpression) and len(p.body) == 1: stmt = p.body[0] if isinstance(stmt, Ps1ExpressionStatement) and stmt.expression is not None: sv = coerced_text_at(stmt.expression, node, self._flow) if sv is not None: parts.append(sv) continue return None return make_string_literal(''.join(parts)) def visit_Ps1MemberAccess(self, node)-
Expand source code Browse git
def visit_Ps1MemberAccess(self, node: Ps1MemberAccess): self.generic_visit(node) self._normalize_member(node) return None def visit_Ps1InvokeMember(self, node)-
Expand source code Browse git
def visit_Ps1InvokeMember(self, node: Ps1InvokeMember): self.generic_visit(node) self._normalize_member(node) return None def visit_Ps1BinaryExpression(self, node)-
Expand source code Browse git
def visit_Ps1BinaryExpression(self, node: Ps1BinaryExpression): self.generic_visit(node) normalized = KNOWN_PS_OPERATORS.get(node.operator.lower(), node.operator) if normalized != node.operator: set_value(node, 'operator', normalized) self.mark_changed() return None def visit_Ps1UnaryExpression(self, node)-
Expand source code Browse git
def visit_Ps1UnaryExpression(self, node: Ps1UnaryExpression): self.generic_visit(node) normalized = KNOWN_PS_OPERATORS.get(node.operator.lower(), node.operator) if normalized != node.operator: set_value(node, 'operator', normalized) self.mark_changed() return None def visit_Ps1CommandArgument(self, node)-
Expand source code Browse git
def visit_Ps1CommandArgument(self, node: Ps1CommandArgument): self.generic_visit(node) if node.kind in (Ps1CommandArgumentKind.SWITCH, Ps1CommandArgumentKind.NAMED): if '`' in node.name: set_value(node, 'name', _strip_backtick_noop(node.name)) self.mark_changed() name_lower = node.name.lower() normalized = KNOWN_PS_OPERATORS.get(name_lower) if normalized is None: normalized = KNOWN_PS_SWITCHES.get(name_lower) if normalized is None: bare = name_lower.lstrip('-') if bare != name_lower: canonical = ALL_PARAMETER_NAMES.get(bare) if canonical is not None: normalized = F'-{canonical}' if normalized is not None and normalized != node.name: set_value(node, 'name', normalized) self.mark_changed() return None def visit_Ps1TypeExpression(self, node)-
Expand source code Browse git
def visit_Ps1TypeExpression(self, node: Ps1TypeExpression): self._normalize_type_field(node, 'name') return None def visit_Ps1CastExpression(self, node)-
Expand source code Browse git
def visit_Ps1CastExpression(self, node: Ps1CastExpression): self.generic_visit(node) self._normalize_type_field(node, 'type_name') return None def visit_Ps1CommandInvocation(self, node)-
Expand source code Browse git
def visit_Ps1CommandInvocation(self, node: Ps1CommandInvocation): self.generic_visit(node) old_name = node.name if isinstance(node.name, Ps1ParenExpression) and node.name.expression is not None: inner = node.name.expression if isinstance(inner, Ps1StringLiteral): substitute_field(node, 'name', inner) elif isinstance(inner, Ps1CommandInvocation): c = get_command_name(inner) if c is not None and c.lower() in ('gcm', 'get-command'): if len(inner.arguments) == 1: arg = inner.arguments[0] if isinstance(arg, Ps1CommandArgument): arg = arg.value if isinstance(arg, Ps1ParenExpression): arg = arg.expression # Only resolve a concrete command name; a wildcard pattern such as # `gcm i*e-e*` must not be substituted verbatim as the command name. if isinstance(arg, Ps1StringLiteral) and not _has_wildcard(arg.value): substitute_field(node, 'name', arg) if node.name is not old_name: self.mark_changed() if node.name and isinstance(node.name, Ps1StringLiteral): if '`' in node.name.value: stripped = _strip_backtick_noop(node.name.value) substitute_field(node, 'name', Ps1StringLiteral( offset=node.name.offset, value=stripped, raw=stripped, )) self.mark_changed() if is_bare_command_name(node.name.value) and node.name.raw != node.name.value: substitute_field(node, 'name', Ps1StringLiteral( offset=node.name.offset, value=node.name.value, raw=node.name.value, )) self.mark_changed() if node.invocation_operator in ('&', '.'): if isinstance(node.name, Ps1StringLiteral): name_val = node.name.value if ( (SIMPLE_IDENTIFIER.match(name_val) or '-' in name_val) and is_bare_command_name(name_val) and not _has_wildcard(name_val) and self._operator_is_noise(node) ): substitute_field(node, 'name', Ps1StringLiteral( offset=node.name.offset, value=name_val, raw=name_val, )) set_value(node, 'invocation_operator', '') self.mark_changed() if (c := get_command_name(node)) and c.lower() in TYPE_ARG_COMMANDS: self._normalize_first_positional_type_arg(node) return None
Inherited members