Module refinery.lib.scripts.ps1.deobfuscation.typecast
PowerShell type cast simplification transforms.
Expand source code Browse git
"""
PowerShell type cast simplification transforms.
"""
from __future__ import annotations
from refinery.lib.scripts import Node, Transformer, canonical
from refinery.lib.scripts.ps1.analysis.cache import model_cache
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.analysis.values import (
Ps1Outcome,
convert,
make_string_literal,
read,
render,
text_of,
)
from refinery.lib.scripts.ps1.data import named_type, resolve_type
from refinery.lib.scripts.ps1.deobfuscation.helpers import unwrap_single_paren
from refinery.lib.scripts.ps1.model import (
Expression,
Ps1BinaryExpression,
Ps1CastExpression,
Ps1Script,
Ps1TypeExpression,
)
#: The two targets this pass treats as something other than a value conversion: one that names a
#: type rather than producing a value of one, and one whose answer the conversion grid does not
#: carry, because it was captured over scalar targets only. See the arm each belongs to.
_TYPE = named_type('System.Type')
_STRING = named_type('System.String')
class Ps1TypeCasts(Transformer):
"""
A cast written back as the value it produces, asked of
`refinery.lib.scripts.ps1.analysis.values` rather than decided here.
`-as` is **not** rewritten into a cast: the two are different expressions, measured. `'abc' -as
[int]` is `$null` where `[int] 'abc'` throws, and `300 -as [byte]` is `$null` where `[byte] 300`
throws. `Ps1Outcome` tells them apart, and a conversion that may throw folds neither, so an `-as`
this cannot answer is left standing rather than turned into the cast that stops the script.
`[string]` of a collection is read outside the value grid: its elements 5.1 joins with `$OFS`,
which `_joined_collection` answers. `[char[]]` of a list of numbers is a `Char[]`, which has no
literal, so `_spelled` writes none and the cast stands while an operator over it reads its type.
Every question here is asked of one step: the operand has already been visited, so `read` names
what it came to and `convert` answers the cast over that; `evaluate` would walk the operand again
at every node, quadratic over a tree a visitor is already descending.
"""
def __init__(self):
super().__init__()
self._flow: Ps1VariableFlow | None = None
self._entry = False
def visit(self, node: Node):
"""
Captured once rather than per cast: every fold below marks the pass changed, which drops the
cache, so a per-site lookup would rebuild the control-flow graphs of the whole script once
per folded cast. This pass replaces an expression with the value it produces and neither
adds nor removes a statement, so the graphs it would rebuild are the graphs it already has.
Dropped again when the walk it was captured for ends, so that a second walk over a tree the
first one rewrote cannot be answered from the first walk's graphs.
"""
if self._entry or not isinstance(node, Ps1Script):
return super().visit(node)
self._entry = True
try:
self._flow = model_cache(self, node).variable_flow
return super().visit(node)
finally:
self._entry = False
self._flow = None
def visit_Ps1BinaryExpression(self, node: Ps1BinaryExpression):
self.generic_visit(node)
if node.operator.lower() != '-as' or node.left is None:
return None
if not isinstance(node.right, Ps1TypeExpression):
return None
target = resolve_type(node.right.name)
if target is None:
return None
return _spelled(node, convert(read(node.left), target))
def visit_Ps1CastExpression(self, node: Ps1CastExpression):
self.generic_visit(node)
target = resolve_type(node.type_name)
if target is None:
return None
return (
self._named_type(node, target)
or _spelled(node, convert(read(node.operand), target))
or self._joined_collection(node, target)
)
@staticmethod
def _named_type(node: Ps1CastExpression, target) -> Expression | None:
"""
`[type] 'X'`, the one arm here that names a type rather than producing a value of one. What
the expression evaluates to is a `System.RuntimeType`, which the domain deliberately carries
no element for, so what this reads out of the operand is a name and what it writes is the
type literal naming the same thing.
"""
if target != _TYPE:
return None
named = text_of(read(node.operand))
return None if named is None else Ps1TypeExpression(offset=node.offset, name=named)
def _joined_collection(self, node: Ps1CastExpression, target) -> Expression | None:
"""
The conversion grid was captured over scalar targets only, so the domain has no cell for a
`[string]` of a collection, and the separator is not a property of the value in any case:
`refinery.lib.scripts.ps1.analysis.separator` answers it, at the point the cast stands, and
refuses wherever a run could have written the name something else.
"""
if target != _STRING or node.operand is None or self._flow is None:
return None
text = coerced_text_at(unwrap_single_paren(node.operand), node, self._flow)
return None if text is None else make_string_literal(text)
def _spelled(node: Expression, outcome: Ps1Outcome) -> Expression | None:
"""
The expression an outcome is written as, or `None` where nothing is written.
An outcome that may throw is never folded, because the script's throw is part of what it does
and replacing it with the value the operation would have had deletes that. A fact that names no
value has no spelling. And a value the node *already* spells is left alone: replacing a tree
with an equal one is a rewrite that never converges, and `refinery.lib.scripts.canonical` is the
model's own answer to whether two trees spell the same program.
"""
if outcome.may_throw:
return None
spelled = render(outcome.value)
if spelled is None or canonical(spelled) == canonical(node):
return None
return spelled
Classes
class Ps1TypeCasts-
A cast written back as the value it produces, asked of
refinery.lib.scripts.ps1.analysis.valuesrather than decided here.-asis not rewritten into a cast: the two are different expressions, measured.'abc' -as [int]<code> is </code>$null<code> where </code>[int] 'abc'<code> throws, and </code>300 -as [byte]<code> is </code>$null<code> where </code>[byte] 300throws.Ps1Outcometells them apart, and a conversion that may throw folds neither, so an-asthis cannot answer is left standing rather than turned into the cast that stops the script.[string]of a collection is read outside the value grid: its elements 5.1 joins with$OFS, which_joined_collectionanswers.[char[]]of a list of numbers is aChar[], which has no literal, so_spelledwrites none and the cast stands while an operator over it reads its type.Every question here is asked of one step: the operand has already been visited, so
readnames what it came to andconvertanswers the cast over that;evaluatewould walk the operand again at every node, quadratic over a tree a visitor is already descending.Expand source code Browse git
class Ps1TypeCasts(Transformer): """ A cast written back as the value it produces, asked of `refinery.lib.scripts.ps1.analysis.values` rather than decided here. `-as` is **not** rewritten into a cast: the two are different expressions, measured. `'abc' -as [int]` is `$null` where `[int] 'abc'` throws, and `300 -as [byte]` is `$null` where `[byte] 300` throws. `Ps1Outcome` tells them apart, and a conversion that may throw folds neither, so an `-as` this cannot answer is left standing rather than turned into the cast that stops the script. `[string]` of a collection is read outside the value grid: its elements 5.1 joins with `$OFS`, which `_joined_collection` answers. `[char[]]` of a list of numbers is a `Char[]`, which has no literal, so `_spelled` writes none and the cast stands while an operator over it reads its type. Every question here is asked of one step: the operand has already been visited, so `read` names what it came to and `convert` answers the cast over that; `evaluate` would walk the operand again at every node, quadratic over a tree a visitor is already descending. """ def __init__(self): super().__init__() self._flow: Ps1VariableFlow | None = None self._entry = False def visit(self, node: Node): """ Captured once rather than per cast: every fold below marks the pass changed, which drops the cache, so a per-site lookup would rebuild the control-flow graphs of the whole script once per folded cast. This pass replaces an expression with the value it produces and neither adds nor removes a statement, so the graphs it would rebuild are the graphs it already has. Dropped again when the walk it was captured for ends, so that a second walk over a tree the first one rewrote cannot be answered from the first walk's graphs. """ if self._entry or not isinstance(node, Ps1Script): return super().visit(node) self._entry = True try: self._flow = model_cache(self, node).variable_flow return super().visit(node) finally: self._entry = False self._flow = None def visit_Ps1BinaryExpression(self, node: Ps1BinaryExpression): self.generic_visit(node) if node.operator.lower() != '-as' or node.left is None: return None if not isinstance(node.right, Ps1TypeExpression): return None target = resolve_type(node.right.name) if target is None: return None return _spelled(node, convert(read(node.left), target)) def visit_Ps1CastExpression(self, node: Ps1CastExpression): self.generic_visit(node) target = resolve_type(node.type_name) if target is None: return None return ( self._named_type(node, target) or _spelled(node, convert(read(node.operand), target)) or self._joined_collection(node, target) ) @staticmethod def _named_type(node: Ps1CastExpression, target) -> Expression | None: """ `[type] 'X'`, the one arm here that names a type rather than producing a value of one. What the expression evaluates to is a `System.RuntimeType`, which the domain deliberately carries no element for, so what this reads out of the operand is a name and what it writes is the type literal naming the same thing. """ if target != _TYPE: return None named = text_of(read(node.operand)) return None if named is None else Ps1TypeExpression(offset=node.offset, name=named) def _joined_collection(self, node: Ps1CastExpression, target) -> Expression | None: """ The conversion grid was captured over scalar targets only, so the domain has no cell for a `[string]` of a collection, and the separator is not a property of the value in any case: `refinery.lib.scripts.ps1.analysis.separator` answers it, at the point the cast stands, and refuses wherever a run could have written the name something else. """ if target != _STRING or node.operand is None or self._flow is None: return None text = coerced_text_at(unwrap_single_paren(node.operand), node, self._flow) return None if text is None else make_string_literal(text)Ancestors
Methods
def visit(self, node)-
Captured once rather than per cast: every fold below marks the pass changed, which drops the cache, so a per-site lookup would rebuild the control-flow graphs of the whole script once per folded cast. This pass replaces an expression with the value it produces and neither adds nor removes a statement, so the graphs it would rebuild are the graphs it already has.
Dropped again when the walk it was captured for ends, so that a second walk over a tree the first one rewrote cannot be answered from the first walk's graphs.
Expand source code Browse git
def visit(self, node: Node): """ Captured once rather than per cast: every fold below marks the pass changed, which drops the cache, so a per-site lookup would rebuild the control-flow graphs of the whole script once per folded cast. This pass replaces an expression with the value it produces and neither adds nor removes a statement, so the graphs it would rebuild are the graphs it already has. Dropped again when the walk it was captured for ends, so that a second walk over a tree the first one rewrote cannot be answered from the first walk's graphs. """ if self._entry or not isinstance(node, Ps1Script): return super().visit(node) self._entry = True try: self._flow = model_cache(self, node).variable_flow return super().visit(node) finally: self._entry = False self._flow = None def visit_Ps1BinaryExpression(self, node)-
Expand source code Browse git
def visit_Ps1BinaryExpression(self, node: Ps1BinaryExpression): self.generic_visit(node) if node.operator.lower() != '-as' or node.left is None: return None if not isinstance(node.right, Ps1TypeExpression): return None target = resolve_type(node.right.name) if target is None: return None return _spelled(node, convert(read(node.left), target)) def visit_Ps1CastExpression(self, node)-
Expand source code Browse git
def visit_Ps1CastExpression(self, node: Ps1CastExpression): self.generic_visit(node) target = resolve_type(node.type_name) if target is None: return None return ( self._named_type(node, target) or _spelled(node, convert(read(node.operand), target)) or self._joined_collection(node, target) )
Inherited members