Delen via


Wrap your PowerShell scripts as Executables

Every now and then while delivering one of our PowerShell workshops, my customers ask me "How can I create an exe from my PowerShell script?" And every time the same reasoning is being used: To obfuscate the use of clear text passwords, connection strings and other sensitive data from prying eyes. Sure, most will tell me "The user should not see the script running" but this never seems to be the real reason :)

While I hopefully won't have to tell you, my esteemed readers, the importance of not storing sensitive data in scripts at all, I still wanted to share my PowerShell wrapper (GitHub repo). After all, there might be reasons to use this like creating a transport vessel for your script and it's dependencies that even executes itself without having to be extracted before.

In order to do this I am leveraging the awesome capabilities of PowerShell, allowing me to locate the compiler at runtime and compiling my wrapper code into an executable. So let's examine this step by step.

First of all, the script content is read via Get-Content. This string will be placed in your .NET code later on, so we need to make sure that all weird characters are properly escaped.

 $scriptContent = (Get-Content $ScriptPath -Raw -Encoding UTF8) -replace "\\", "\\" -replace "`r`n", "\n" -replace '"', '\"'

The next order of business is to create a temporary file (*.cs) which we can actually compile later on. Easy - the .NET framework helps us with the Path class and it's static methods:

 $temp = [System.IO.Path]::GetTempFileName() -replace "\.tmp", ".cs"

Check out the other static members of System.IO.Path by applying a little Get-Member magic:

 [System.IO.Path] | Get-Member -Static

These methods will help you in many circumstances - just as a little sidenote.

We set the script contents to our actual .NET code - I would like to stress here that this is a very, very simple piece of code which does not take into account: Interactive scripts, scripts throwing errors like there is no tomorrow, return values that you might need, ... You might use it more as a base to build upon.

 @"
using System;
using System.Management.Automation;

namespace POSHRocks
{
    public class Wrapper
    {
        public static void Main(string[] args)
        {
            PowerShell ps = PowerShell.Create();
            ps.Commands.AddScript("$scriptContent");
            ps.Invoke();
        }
    }
}
"@ | Out-File $temp

This is all fine and dandy - and all of this should not be new to you if you have been using PowerShell for a little while. Observe the use of the here-string because I am too lazy to use the backtick to escape my quotation marks.

Next up: How will my code become an executable? Usually this is some black magic happening behind the scenes when you - the developer - hit "Build project" in Visual Studio. What actually happens at some point is: The C# compiler, csc.exe, will be called to build your assembly with all it's additional files, references and so on. For more information, check out /en-us/dotnet/csharp/language-reference/compiler-options/index.

To locate our compiler location we can make use of .NET once again:

 $compiler = Join-Path -Path ([Runtime.InteropServices.RuntimeEnvironment]::GetRuntimeDirectory() ) -ChildPath csc.exe

We basically locate the .NET runtime folder and assume that we can find the compiler there as well.

If you took a look at the linked article a couple of lines back you also saw the command-line options for our C# compiler. We use these to kick off a new process that will compile our script for us. Notice in the command-line options that the referenced assembly can also be easily found. Since we need to add a reference to the namespace System.Management.Automation, we can simply make use of reflection and get the Assembly location of a psobject, the one object inherently linked with PowerShell and the S.M.A namespace.

 # Compile the exe
$arguments = @(
    "/target:exe"
    "/out:$ExePath"
    "/r:`"$([psobject].Assembly.Location)`""
    $temp
)
$compilation = Start-Process -FilePath $compiler -ArgumentList $arguments -Wait -NoNewWindow -PassThru

Afterwards we check for success as defined by the property ExitCode of $compilation and return the FileInfo object of our new executable.

So there you go, creating an executable from a script (or any source code for that matter) from the shell, while dynamically retrieving our compiler and references.

Oh, and one little caveat: Remember me telling you not to store passwords in your scripts? This happens if you don't obey:

Namespace as seen in disassembler pointing to POSHRocks.Wrapper

The disassembled .NET code showing a clear-text password