Module refinery.lib.scripts.ps1.options
Caller-supplied options controlling PowerShell deobfuscation.
They sit above both the analysis and the transform layers because one run is configured once: the analysis layer cannot import the transform layer, so an option held there would be a second place a caller has to configure and a setting the two layers could disagree about.
Expand source code Browse git
"""
Caller-supplied options controlling PowerShell deobfuscation.
They sit above both the analysis and the transform layers because one run is configured once: the
analysis layer cannot import the transform layer, so an option held there would be a second place a
caller has to configure and a setting the two layers could disagree about.
"""
from __future__ import annotations
from dataclasses import dataclass
@dataclass(frozen=True)
class Ps1DeobfuscationOptions:
"""
Options that steer PowerShell deobfuscation. Each is the caller's answer to a question the file
itself cannot settle, which is why neither is derived.
*preserve_bare_output* selects what a statement that only writes a value to the success output
stream is worth.
- Stripping model (default, `preserve_bare_output=False`): such a statement is deleted wherever
the analysis can prove three things: that evaluating it cannot raise, that its value reaches
the process output and nothing more, and that no redirection moves it away. What is lost is
text on a console nobody is watching, and what is bought is the removal of the junk an
obfuscator pads a script with. The proof rests on one assumption no file can settle: that the
input is a standalone script and not a library some other file imports, whose functions are
then called from call sites this analysis never sees.
- Preserving model (`preserve_bare_output=True`): no such statement is ever deleted. This is the
answer for a script whose printed output *is* the artifact, and for a `.psm1` or any other
fragment that runs as part of something larger.
Neither model touches a statement whose value is captured, whose evaluation does anything, or
that this analysis cannot read; those are kept under both, and the switch is not what protects
them.
*trust_eval* selects what code this analysis cannot read is assumed to do to the .NET type
system and the command table — the two things
`refinery.lib.scripts.ps1.analysis.world.Ps1TypeWorld` calls the world.
- Suspecting model (default, `trust_eval=False`): every construct that runs code supplied as
data opens the world where it stands, and every purity grant below it is refused. This is the
only sound answer, because the code being run can do anything the runtime allows.
- Trusting model (`trust_eval=True`): such a construct is assumed to leave both intact, so the
junk written around it is removed as if it were not there. **This is unsound, deliberately.**
Measured on Windows PowerShell 5.1, an `Invoke-Expression` payload can register a type
accelerator named `System.Guid`, after which a later `[System.Guid]::NewGuid()` throws where
the rewritten script computes a value; it can re-point a property through `Update-TypeData`,
so `[Diagnostics.Process]::GetCurrentProcess().ProcessName` yields something else; and it can
define `function Get-Date` in the calling scope. The switch is for triage, where reading the
script matters more than being able to run the output.
The trusting model assumes the same of what such code *reads*, which costs more than the type
system does. A `function` this script defines and no statement in it calls is deleted, so a
payload whose only job is to call one is left calling a name the output no longer defines; a
bare value inside a function nothing but the console reads goes the same way, so a payload that
captures the call gets nothing; and a fault the payload can arm — `Set-StrictMode` reaching a
bare read below it — stops being reachable, so a statement that terminated the input runs on.
None of the three is a change to the type system or the command table, and each is a way the
output can behave differently from a payload that changed neither.
What the trusting model does *not* excuse is a change the script performs in plain sight.
`Add-Type`, `Update-TypeData`, `Add-Member`, `Import-Module`, `New-Module`, a `class` or `enum`
definition, a type-accelerator remap and a PSObject member mutation still open the world under
both models, and so does a statement that spells out both a command name it takes over and what
it binds that name to — `New-Alias Get-Date Stop-Process`, `Set-Item function:Get-Date { ... }`.
The assumption is about code that cannot be read, not about every way a script can reach the
world.
"""
preserve_bare_output: bool = False
trust_eval: bool = False
def bare_output_is_preserved(options: object | None) -> bool:
"""
Whether *options* asks for every write to the success output stream to be kept. Any value that
is not a `Ps1DeobfuscationOptions` — a transformer run standalone, or one with no options
attached — defaults to the stripping model, which is what the pipeline does unless told.
"""
return isinstance(options, Ps1DeobfuscationOptions) and options.preserve_bare_output
def eval_is_trusted(options: object | None) -> bool:
"""
Whether *options* asks for code this analysis cannot read to be assumed inert. Any value that is
not a `Ps1DeobfuscationOptions` — a model built standalone, or one with no options attached —
defaults to the suspecting model, which is the only sound one and what the pipeline does unless
told.
"""
return isinstance(options, Ps1DeobfuscationOptions) and options.trust_eval
Functions
def bare_output_is_preserved(options)-
Whether options asks for every write to the success output stream to be kept. Any value that is not a
Ps1DeobfuscationOptions— a transformer run standalone, or one with no options attached — defaults to the stripping model, which is what the pipeline does unless told.Expand source code Browse git
def bare_output_is_preserved(options: object | None) -> bool: """ Whether *options* asks for every write to the success output stream to be kept. Any value that is not a `Ps1DeobfuscationOptions` — a transformer run standalone, or one with no options attached — defaults to the stripping model, which is what the pipeline does unless told. """ return isinstance(options, Ps1DeobfuscationOptions) and options.preserve_bare_output def eval_is_trusted(options)-
Whether options asks for code this analysis cannot read to be assumed inert. Any value that is not a
Ps1DeobfuscationOptions— a model built standalone, or one with no options attached — defaults to the suspecting model, which is the only sound one and what the pipeline does unless told.Expand source code Browse git
def eval_is_trusted(options: object | None) -> bool: """ Whether *options* asks for code this analysis cannot read to be assumed inert. Any value that is not a `Ps1DeobfuscationOptions` — a model built standalone, or one with no options attached — defaults to the suspecting model, which is the only sound one and what the pipeline does unless told. """ return isinstance(options, Ps1DeobfuscationOptions) and options.trust_eval
Classes
class Ps1DeobfuscationOptions (preserve_bare_output=False, trust_eval=False)-
Options that steer PowerShell deobfuscation. Each is the caller's answer to a question the file itself cannot settle, which is why neither is derived.
preserve_bare_output selects what a statement that only writes a value to the success output stream is worth.
-
Stripping model (default,
preserve_bare_output=False): such a statement is deleted wherever the analysis can prove three things: that evaluating it cannot raise, that its value reaches the process output and nothing more, and that no redirection moves it away. What is lost is text on a console nobody is watching, and what is bought is the removal of the junk an obfuscator pads a script with. The proof rests on one assumption no file can settle: that the input is a standalone script and not a library some other file imports, whose functions are then called from call sites this analysis never sees. -
Preserving model (
preserve_bare_output=True): no such statement is ever deleted. This is the answer for a script whose printed output is the artifact, and for a.psm1or any other fragment that runs as part of something larger.
Neither model touches a statement whose value is captured, whose evaluation does anything, or that this analysis cannot read; those are kept under both, and the switch is not what protects them.
trust_eval selects what code this analysis cannot read is assumed to do to the .NET type system and the command table — the two things
Ps1TypeWorldcalls the world.-
Suspecting model (default,
trust_eval=False): every construct that runs code supplied as data opens the world where it stands, and every purity grant below it is refused. This is the only sound answer, because the code being run can do anything the runtime allows. -
Trusting model (
trust_eval=True): such a construct is assumed to leave both intact, so the junk written around it is removed as if it were not there. This is unsound, deliberately. Measured on Windows PowerShell 5.1, anInvoke-Expressionpayload can register a type accelerator namedSystem.Guid, after which a later[System.Guid]::NewGuid()throws where the rewritten script computes a value; it can re-point a property throughUpdate-TypeData, so[Diagnostics.Process]::GetCurrentProcess().ProcessNameyields something else; and it can definefunction Get-Datein the calling scope. The switch is for triage, where reading the script matters more than being able to run the output.
The trusting model assumes the same of what such code reads, which costs more than the type system does. A
functionthis script defines and no statement in it calls is deleted, so a payload whose only job is to call one is left calling a name the output no longer defines; a bare value inside a function nothing but the console reads goes the same way, so a payload that captures the call gets nothing; and a fault the payload can arm —Set-StrictModereaching a bare read below it — stops being reachable, so a statement that terminated the input runs on. None of the three is a change to the type system or the command table, and each is a way the output can behave differently from a payload that changed neither.What the trusting model does not excuse is a change the script performs in plain sight.
Add-Type,Update-TypeData,Add-Member,Import-Module,New-Module, aclassorenumdefinition, a type-accelerator remap and a PSObject member mutation still open the world under both models, and so does a statement that spells out both a command name it takes over and what it binds that name to —New-Alias Get-Date Stop-Process,Set-Item function:Get-Date { ... }. The assumption is about code that cannot be read, not about every way a script can reach the world.Expand source code Browse git
@dataclass(frozen=True) class Ps1DeobfuscationOptions: """ Options that steer PowerShell deobfuscation. Each is the caller's answer to a question the file itself cannot settle, which is why neither is derived. *preserve_bare_output* selects what a statement that only writes a value to the success output stream is worth. - Stripping model (default, `preserve_bare_output=False`): such a statement is deleted wherever the analysis can prove three things: that evaluating it cannot raise, that its value reaches the process output and nothing more, and that no redirection moves it away. What is lost is text on a console nobody is watching, and what is bought is the removal of the junk an obfuscator pads a script with. The proof rests on one assumption no file can settle: that the input is a standalone script and not a library some other file imports, whose functions are then called from call sites this analysis never sees. - Preserving model (`preserve_bare_output=True`): no such statement is ever deleted. This is the answer for a script whose printed output *is* the artifact, and for a `.psm1` or any other fragment that runs as part of something larger. Neither model touches a statement whose value is captured, whose evaluation does anything, or that this analysis cannot read; those are kept under both, and the switch is not what protects them. *trust_eval* selects what code this analysis cannot read is assumed to do to the .NET type system and the command table — the two things `refinery.lib.scripts.ps1.analysis.world.Ps1TypeWorld` calls the world. - Suspecting model (default, `trust_eval=False`): every construct that runs code supplied as data opens the world where it stands, and every purity grant below it is refused. This is the only sound answer, because the code being run can do anything the runtime allows. - Trusting model (`trust_eval=True`): such a construct is assumed to leave both intact, so the junk written around it is removed as if it were not there. **This is unsound, deliberately.** Measured on Windows PowerShell 5.1, an `Invoke-Expression` payload can register a type accelerator named `System.Guid`, after which a later `[System.Guid]::NewGuid()` throws where the rewritten script computes a value; it can re-point a property through `Update-TypeData`, so `[Diagnostics.Process]::GetCurrentProcess().ProcessName` yields something else; and it can define `function Get-Date` in the calling scope. The switch is for triage, where reading the script matters more than being able to run the output. The trusting model assumes the same of what such code *reads*, which costs more than the type system does. A `function` this script defines and no statement in it calls is deleted, so a payload whose only job is to call one is left calling a name the output no longer defines; a bare value inside a function nothing but the console reads goes the same way, so a payload that captures the call gets nothing; and a fault the payload can arm — `Set-StrictMode` reaching a bare read below it — stops being reachable, so a statement that terminated the input runs on. None of the three is a change to the type system or the command table, and each is a way the output can behave differently from a payload that changed neither. What the trusting model does *not* excuse is a change the script performs in plain sight. `Add-Type`, `Update-TypeData`, `Add-Member`, `Import-Module`, `New-Module`, a `class` or `enum` definition, a type-accelerator remap and a PSObject member mutation still open the world under both models, and so does a statement that spells out both a command name it takes over and what it binds that name to — `New-Alias Get-Date Stop-Process`, `Set-Item function:Get-Date { ... }`. The assumption is about code that cannot be read, not about every way a script can reach the world. """ preserve_bare_output: bool = False trust_eval: bool = FalseInstance variables
var preserve_bare_output-
The type of the None singleton.
var trust_eval-
The type of the None singleton.
-