Writing stealth code in PowerShell

What happens in module, stays in module.

Most of my scripts are using Import-Component function to bulk-import dependencies (PS1 files with functions, modules, source code, .Net assemblies).

To import PS1 files with functions, they have to be dot-sourced and that provided me with some challenge: if PS1 is dot-sourced inside the function, it will be available only in that function’s scope. To overcome this, I could scope each contained function, alias, and variable as global (nasty!) or call Import-Component function itself using dot-sourcing (yes, you can dot-source more than just files).

For a while, dot-sourcing Import-Component seemed to work fine, until one day, I realized, that this effectively pollutes caller’s scope with all Import-Component‘s internal variables. Consider this example:

function DotSource-Me
{
    $MyString = 'Internal variable'
}

$MyString = 'External variable'

# Calling function as usual
DotSource-Me
Write-Host "Function was called, 'MyString' contains: $MyString"

# Dot-sourcing function
. DotSource-Me
Write-Host "Function was dot-sourced, 'MyString' contains: $MyString"

If we run this script, the output will be:

Function was called, 'MyString' contains: External variable
Function was dot-sourced, 'MyString' contains: Internal variable

As you can see, when the DotSource-Me function is called as usual, it’s internal variable is restricted to the function’s scope and doesn’t affect the caller’s scope. But when it’s dot-sourced, variable in the caller’s scope is overwritten.

To remedy this, we could take advantage of the fact, that creating a new module creates an entirely new SessionState. It means that everything that happens inside the module is completely isolated. So if we place all code inside the function in the dynamically generated module, it wouldn’t affect anything outside, even if dot-sourced. Also we don’t want to actually pollute caller’s scope with newly created module object. Luckily for us, the New-Module cmdlet has ReturnResult parameter, that runs the script block and returns the results instead of returning a module object. So lets modify our example:

function DotSource-Me
{
    New-Module -ReturnResult -ScriptBlock {
        $MyString = 'Internal variable'
    }
}

$MyString = 'External variable'

# Calling function as usual
DotSource-Me
Write-Host "Function was called, 'MyString' contains: $MyString"

# Dot-sourcing function
. DotSource-Me
Write-Host "Function was dot-sourced, 'MyString' contains: $MyString"

And then run it and observe the results:

Function was called, 'MyString' contains: External variable
Function was dot-sourced, 'MyString' contains: External variable

That’s so much better!

But what if our function that has to be dot-sourced has parameters? Unfortunately, PowerShell will create variable for each parameter, and because function is dot-sourced, those variables will be created in the callers scope:

function DotSource-Me
{
    Param
    (
        $MyString
    )
}

$MyString = 'External variable'

# Calling function as usual
DotSource-Me
Write-Host "Function was called, 'MyString' contains: $MyString"

# Dot-sourcing function
. DotSource-Me
Write-Host "Function was dot-sourced, 'MyString' contains: $MyString"

And they will pollute and\or overwrite variables in callers scope:

Function was called, 'MyString' contains: External variable
Function was dot-sourced, 'MyString' contains:

To mitigate this issue, we can exploit the fact that PowerShell doesn’t create corresponding variables for DynamicParameters. Note, that code in the DynamicParam block has to be wrapped in the New-Module too, otherwise it will be executed in caller’s scope

function DotSource-Me
{
    [CmdletBinding()]
    Param()
    DynamicParam
    {
        New-Module -ReturnResult -ScriptBlock {
            # Set the dynamic parameters name
            $ParameterName = 'MyString'

            # Create the dictionary
            $RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary

            # Create the collection of attributes
            $AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]

            # Create and set the parameters' attributes
            $ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute

            # Add the attributes to the attributes collection
            $AttributeCollection.Add($ParameterAttribute)

            # Create and return the dynamic parameter
            $RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter(
                        $ParameterName,
                        [string],
                        $AttributeCollection)
            $RuntimeParameterDictionary.Add($ParameterName, $RuntimeParameter)
            $RuntimeParameterDictionary
        }
    }
}

$MyString = "External variable"

# Calling function as usual
DotSource-Me
Write-Host "Function was called, 'MyString' contains: $MyString"

# Dot-sourcing function
. DotSource-Me
Write-Host "Function was dot-sourced, 'MyString' contains: $MyString"

And the result is:

Function was called, 'MyString' contains: External variable
Function was dot-sourced, 'MyString' contains: External variable

To make things easier, you can put my New-DynamicParameter function inside the New-Module‘ scriptblock and use it like this:

function DotSource-Me
{
    [CmdletBinding()]
    Param()
    DynamicParam
    {
        New-Module -ReturnResult -ScriptBlock {
            Function New-DynamicParameter
            {
                # function body here...
            }

            New-DynamicParameter -Name MyString -Type ([string])
        }
    }
}

$MyString = "External variable"

# Calling function as usual
DotSource-Me
Write-Host "Function was called, 'MyString' contains: $MyString"

# Dot-sourcing function
. DotSource-Me
Write-Host "Function was dot-sourced, 'MyString' contains: $MyString"

Bonus chapter

What if we really need to execute something in caller’s scope from the New-Module‘s scriptblock? In Import-Component function, dot-sourcing command itself has to be executed in the caller’s scope, while all other code should be well-hidden in New-Module. To achieve a desired result I’m using a not-so-well-know fact, that scriptblocks are bound to the session state:

Any script block that’s defined in a script or script module (in literal form, not dynamically created with something like [scriptblock]::Create()) is bound to the session state of that module (or to the “main” session state, if not executing inside a script module.) There is also information specific to the file that the script block came from, so things like breakpoints will work when the script block is invoked.

When you pass in such a script block as a parameter across script module boundaries, it is still bound to its original scope, even if you invoke it from inside the module.

Here is the final example:

function DotSource-Me
{
    [CmdletBinding()]
    Param()
    DynamicParam
    {
        New-Module -ReturnResult -ScriptBlock {
            # Set the dynamic parameters name
            $ParameterName = 'ScriptBlock'

            # Create the dictionary
            $RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary

            # Create the collection of attributes
            $AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]

            # Create and set the parameters' attributes
            $ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute

            # Add the attributes to the attributes collection
            $AttributeCollection.Add($ParameterAttribute)

            # Create and return the dynamic parameter
            $RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter(
                        $ParameterName,
                        [scriptblock],
                        $AttributeCollection)
            $RuntimeParameterDictionary.Add($ParameterName, $RuntimeParameter)
            $RuntimeParameterDictionary
        }
    }

    Process
    {
        New-Module -ReturnResult -ScriptBlock {
            # Assign internal variable
            $MyString = "Internal variable"

            # Execute scriptblock
            & $PSBoundParameters.ScriptBlock
        }
    }
}

$MyString = "External variable"
$MyScriptBlock = {Write-Host "Scriptblock, 'MyString' contains: $MyString"}

Write-Host "Script, 'MyString' contains: $MyString"

# Dot-sourcing function
. DotSource-Me -ScriptBlock $MyScriptblock

Note that although $MyString variable is defined inside the New-Module‘s scriptblock, the code in the MyScriptBlock‘s parameter’s scriptblock is executed in the caller’s scope and accesses $MyString variable from there:

Script. 'MyString' contains: External variable
Scriptblock. 'MyString' contains: External variable
Advertisements