Executing PowerShell without PowerShell.exe

The next important topic addresses one of the top myths around PowerShell security:

:

The first three of the top myths you have already learned about. The last one is about the problem that many defenders still think exists today: blocking PowerShell.exe will also block PowerShell in general. As you know, PowerShell is based on .NET, and in detail, it uses the System.Management.Automation namespace. Therefore, System.Management.Automation.dll will be loaded to execute PowerShell cmdlets.

The documentation for the API for System.Managamenent.Automation.dll can be found at the following link: https://docs.microsoft.com/en-us/dotnet/api/system.management.automation.

The first example shows how the dll can be loaded and used without PowerShell.exe. For this scenario, a small C# program is created. First, take a look at the C# code, which is saved as plain text to a *.cs file in your example prog.cs:

using System;
using System.Configuration.Install;
using System.Runtime.InteropServices;
//Loading the asembly
using System.Management.Automation.Runspaces;

public class Program
{
//Constructor
public static void Main( string[] args )
{
//Loading the executor class and executing it with the first
//gathered argument from the command line
//Example: prog.exe c: empMimiKatz.psm1
Executor.Execute( args[ 0 ] );
}
}

//Class to retrieve content of a file and execute it with .Invoke()
public class Executor
{
public static void Execute(string file)
{
//load file content into variable
string fileContent = System.IO.File.ReadAllText(file);

//create a config for the runspace
RunspaceConfiguration runspaceConfig = RunspaceConfiguration.Create();

//create a runspace with the config
Runspace runspace = RunspaceFactory.CreateRunspace(runspaceConfig);

//open the runspace
runspace.Open();

//create a new pipeline in the created runspace
Pipeline createdPipeline = runspace.CreatePipeline();

//add the content of the script as command to the pipeline
createdPipeline.Commands.AddScript(fileContent);

//invoke the pipeline with the inserted command
createdPipeline.Invoke();
}
}

You should recognize that, leaving the comments aside, the number of code lines will only end up at around 20 lines. It starts with showing the compiler the used namespaces, which is accomplished with the using keyword. This is followed by the command line program, which is created as a static class and receives arguments. The first argument is passed to the static method of the following class, Executor. Here, the content is saved in a variable, followed by the creation of a runspace with its pipeline. Finally, the content of the file is added as a command and executed. The classes used here were imported from the loaded assembly, System.Management.Automation.Runspaces. This approach should be possible on most of the machines in every enterprise company. The source code for our command line tool is now ready, but we still need to compile it to create an executable. Fortunately, there are a few ways to get your own code compiled on a Windows machine. One way is using the C# compiler that comes with .NET, which is named csc.exe:

#region Windows 10 x64
#Global Assembly Cache
C:WindowsMicrosoft.NETFramework64v4.0.30319csc.exe /r:C:WindowsassemblyGAC_MSILSystem.Management.Automation1.0.0.0__31bf3856ad364e35System.Management.Automation.dll /unsafe /platform:anycpu /out:"C: empprog.exe" "C: empprog.cs"

#Native dll
C:WindowsMicrosoft.NETFramework64v4.0.30319csc.exe /r:C:WindowsMicrosoft.NETassemblyGAC_MSILSystem.Management.Automationv4.0_3.0.0.0__31bf3856ad364e35System.Management.Automation.dll /unsafe /platform:anycpu /out:"C: empprog.exe" "C: empprog.cs"

#endregion

#region Windows 7 x64

C:WindowsMicrosoft.NETFramework64v2.0.50727csc.exe /r:C:WindowsassemblyGAC_MSILSystem.Management.Automation1.0.0.0__31bf3856ad364e35System.Management.Automation.dll /unsafe /platform:anycpu /out:"C: empprog.exe" "C: empprog.cs"

#endregion

#region Windows 7 x86

C:WindowsMicrosoft.NETFrameworkv2.0.50727csc.exe /r:C:WindowsassemblyGAC_MSILSystem.Management.Automation1.0.0.0__31bf3856ad364e35System.Management.Automation.dll /unsafe /platform:anycpu /out:"C: empprog.exe" "C: empprog.cs"

#endregion

#region Windows 10 x86

C:WindowsMicrosoft.NETFrameworkv4.0.30319csc.exe /r:C:WindowsassemblyGAC_MSILSystem.Management.Automation1.0.0.0__31bf3856ad364e35System.Management.Automation.dll /unsafe /platform:anycpu /out:"C: empprog.exe" "C: empprog.cs"

#endregion
Further information regarding the C# compiler, csc.exe, to help understand the command line properties in detail can be found at the following link: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-options/command-line-building-with-csc-exe.

The integrated C# compiler is now being used to create our compiled program, which is called prog.exe. Depending on the operating system, you will find csc.exe in different locations due to its architecture and you will also be able to use different assemblies from different locations for the System.Management.Automation namespace. We will take a dedicated look at these differentiations later on in the topic. Having now compiled your program, you can try to test it on your own. You can find the example files on GitHub. The example file, test.ps1, includes the following line of code:

Get-Service | Out-File c:	empServices.txt

The services should be retrieved and saved into a file in the specified location. The execution of the program now looks as follows: 

And as you can see, we already validated that this approach is working, as it created the services.txt file with the gathered services. 

As a result of this example, we have learned that PowerShell is executed with the System.Management.Automation namespace, and this can easily be achieved without the use of PowerShell.exe. To further prove this fact, you can make use of the Process Monitor tool from the Sysinternals tools.

The Sysinternals tools are a must-know toolset for troubleshooting and debugging problems and errors on Windows machines.

Process Monitor can be downloaded from the following link: https://docs.microsoft.com/en-us/sysinternals/downloads/procmon.

In this log excerpt from ProcMon, it can be seen that the System.Management.Automation.dll from the Global Assembly Cache (GAC) was loaded. There are different locations and version of the System.Management.Automation.dll. For Windows 10 machines up to version 1803, it is possible to install two different .NET versions: .NET 3.5, which includes 2.0 and 3.0, and .NET 4.7 (as of today). Each of the versions comes also packed with its DLLs, and most enterprise environments will have dependencies for both .NET versions and therefore have both turned on on their machines:

The problem with the older version is that it comes packed with the DLL for the PowerShell engine 2. This means that PowerShell can be started loading version 2 to execute commands and scripts. The problem, though, with the lower version is that logging capabilities were not introduced until version 3. This means that every executed PowerShell script would not be logged and would almost be hidden from a defensive perspective. A possible way to visualize this would be through the use of ProcMon, as shown previously.

At the moment, you should be in an upgrade process to Windows 10, or have already finished it. After Windows 8.1, it is possible to remove the old PowerShell version, which is strongly recommended. There might be some legacy scripts out there that will only work with PowerShell version 2.0 or under, such as scripts for Microsoft Exchange 2010. Make sure that no such dependencies exist and start removing the feature incrementally in the field:

The easiest way to execute PowerShell scripts with engine version 2 is as follows:

#Hiding against logging
powershell.exe -version 2 Get-Service

The executable for PowerShell is started with the argument -version 2. To identify these kinds of downgrade attacks, you could also make use of PowerShell logging, which we will dive into later. The following snippet retrieves all engine initiations from versions lower than 5.0:

#The event ID 400 provides lifecycle events
#The following query retrieves an initiated session, which has been started with an Engine version lower than 5.0

#This would catch typical downgrade attacks
Get-WinEvent -LogName "Windows PowerShell" |
Where-Object Id -eq 400 |
Foreach-Object {
$version = [Version] ($_.Message -replace '(?s).*EngineVersion=([d.]+)*.*','$1')
if($version -lt ([Version] "5.0")) { $_ }
}

But how can you find all of the possible Dlls on the machines that might be used? The following code retrieves all relevant dlls:

#Retrieving all dlls
(Get-ChildItem *.dll -rec -ea ig | ForEach-Object FullName).Where{ $_ -match 'System.Management.Automation.(ni.)?dll' }

And to show you how complex the result can be, take a look at the result for David's machine:

Let us try to classify all these dlls:

  • PowerShell Core 6 version and later (installed side by side)
  • External dlls from third- or first-party software such as Visual Studio
  • Ported dlls (OneDrive)
  • Native images
  • MSIL assemblies/Global Assembly Cache

But if you paid real attention, you will have recognized that the ones for PowerShell version 2 are missing. This is because PowerShell version 2 has just been removed via optional features on this machine. After starting PowerShell, it will always try to find fitting dlls in the GAC/MSIL first, before moving on to the native dlls. 

However, for your machine, you, might get a result that probably looks more like the following:

C:windowsassemblyNativeImages_v2.0.50727_64System.Management.A#8b1355a03394301941edcbb9190e165bSystem.Management.Automation.ni.dll
C:windowsassemblyNativeImages_v4.0.30319_32System.Manaa57fc8cc#8d9ad8b895949d2a5f247b63b94a9cdSystem.Management.Automation.ni.dll
C:windowsassemblyNativeImages_v4.0.30319_64System.Manaa57fc8cc#4072bc1c91e324a1f680e9536b50bad4System.Management.Automation.ni.dll

In this result, very important information is marked in bold. There are v2 assemblies for PowerShell engine 2 and the v4 assemblies, which would be the latest ones installed on the machines and therefore actually include the dlls for PowerShell engine 5.1. As long the v2 assemblies are available and not being blocked with AppLocker, for example, these kind of downgrade attacks will work.

In addition to this, PowerShell can be executed as a 32-bit or 64-bit process. To accomplish this on purpose, you can open the specific PowerShell executables. This might be necessary if you need to work with specific drivers, such as connecting to an Oracle database. Keep in mind that if a 32-bit executable is started with PowerShell 64-bit, it will recognize this and open the app as 32-bit, but not vice versa:

  • 64-bit PowerShell: c:windowssystem32windowspowershellv1.0powershell.exe
  • 32-bit PowerShell: c:windowssyswow64windowspowershellv1.0powershell.exe

It is very interesting to see that the folder with 32 in its name is actually calling the 64-bits version and vice versa. The syswow64 folder only exists in 64 bit Windows versions and represents the compatability mechanism on 64 bit versions. To validate this fact, you can just execute the following code in the opened PowerShell hosts:

[System.Environment]::Is64BitProcess

Some malware comes together with its own dlls, and unfortunately, it is not possible to remove the PowerShell engine 2 from Windows 7/Windows Server 2008 R2 machines. To accomplish this task, you will use the retrieved v2 dlls here and block them, or even better, whitelist the ones that you specifically allow to be executed. This can be accomplished with AppLocker or Windows Defender Application Control, which are described right after this topic.

We have added some more techniques to execute PowerShell commands without the use of PowerShell.exe on GitHub, such as working with a GUI and using DLL files with rundll32. Now, we want to take a look at how we can catch these kind of attacks. We want to catch all scenarios where a different PowerShell host was used to execute PowerShell code, or even worse, where a dedicated System.Management.Automation.dll was used for downgrade and possibly also upgrade attacks (the very first PowerShell Core 6 versions didn't include some security mechanisms and are therefore also used as an attack vector).

To accomplish this, we make use of the WMI events and create an event subscriber on the machine as follows:

function New-ForensicPSWMIEventSubScription
{
<#
.SYNOPSIS
Create an event subscription to catch suspicious PowerShell executions.
.DESCRIPTION
Create an event subscription to catch suspicious PowerShell executions.
Catches unusual PowerShell hosts and unusual loaded System.Management.Automation.dll files.

Derivated from the example - BlueHat 2016 - WMI attack detection demo by Matt Graeber
https://gist.github.com/mattifestation/fa2e3cea76f70b1e2267
.EXAMPLE
New-ForensicPSWMIEventSubScription
Use the default settings
.EXAMPLE
New-ForensicPSWMIEventSubScription
Write your own settings
#>
[CmdletBinding()]
param
(
#locally used naming variable
[Parameter(Mandatory=$false, Position=0)]
[System.String]
$SourceIdentifier = 'WMIEventHandler_PowerShellHostProcessStarted',

#Define whitelisted host processes
[Parameter(Mandatory=$false, Position=1)]
[System.String[]]
$WhitelistedProcesses = @('powershell_ise.exe','powershell.exe'),

#Define whitelisted dll substrings
[Parameter(Mandatory=$false, Position=2)]
[System.String[]]
$WhitelistedDllSubStrings = @('NativeImages_v4.0.30319_','GAC_MSILSystem.Management.Automationv4.0')
)

#The following scriptBlock is being executed, if the trigger is being fired
$PSHostProcessStarted = {
$Event = $EventArgs.NewEvent

$LoadTime = [DateTime]::FromFileTime($Event.TIME_CREATED)
$ProcessID = $Event.ProcessID

#Important: The process may already be exited
#It can possibly retrieve further information for the process
$ProcInfo = Get-WmiObject -Query "SELECT * FROM Win32_Process WHERE ProcessId=$PID" -ErrorAction SilentlyContinue

#Store process information
$CommandLine = $ProcInfo.CommandLine
$ProcessName = $ProcInfo.Name

#validate if process name is whitelisted
if ($ProcessName -in $WhitelistedProcesses) {
$stateUsedHost = 'good'
}
else {
$stateUsedHost = 'bad'
Write-EventLog -LogName "Windows PowerShell" -Source 'PowerShell' -EventID 1337 -EntryType Warning -Message 'An untypical PowerShell host has been used to execute PowerShell code.' -Category 1
}

#validate if dll name is whitelisted
$stateUsedDLL = 'bad'
$fileNameDLL = $($Event.FileName)
foreach ($substring in $WhitelistedDllSubStrings)
{
if ($fileNameDLL -like "*$subString*")
{
$stateUsedDLL = 'good'
#after the first occurence has been found, further looping becomes unnecessary
break
}
}
if ($stateUsedDLL -eq 'bad')
{
Write-EventLog -LogName "Windows PowerShell" -Source 'PowerShell' -EventID 1338 -EntryType Warning -Message 'An untypical Automation dll has been used to execute PowerShell code.' -Category 1
}

#Visualize
$furtherInformation = @"
SIGNATURE: Host PowerShell process started

Date/Time:
$LoadTime
Process ID: $ProcessID
Process Name: $ProcessName
Command Line: $CommandLine
StateUsedHost: $stateUsedHost
StateUsedDll: $stateUsedDll
Dll loaded: $fileNameDLL
"@

Write-Warning $furtherInformation

#write log entry with full information, if dll or host was unknown
if (($stateUsedDLL -eq 'bad') -or ($stateUsedHost -eq 'bad'))
{
Write-EventLog -LogName "Windows PowerShell" -Source 'PowerShell' -EventID 1339 -EntryType Information -Message $furtherInformation -Category 1

#Writing additional information for forensics
$EventArgs | Export-Clixml -Path C: empEventArgs.clixml
$ProcInfo | Export-Clixml -Path C: empProcInfo.clixml
#If you want to store all information added date to file
#$ProcInfo | Export-Clixml -Path ("c: empProcInfo_{0}.clixml" -f $(get-date -f yyyyMMdd_hhmmss))
#$EventArgs | Export-Clixml -Path ("c: empEventArgs_{0}.clixml" -f $(get-date -f yyyyMMdd_hhmmss))
}
}

# The following trigger is defined by its query on the Win32_ModuleLoadTrace class
# Every time the dll is being loaded from a script, the action $PSHostProcessStarted is started
$PSHostProcArgs = @{
Query = 'SELECT * FROM Win32_ModuleLoadTrace WHERE FileName LIKE "%System.Management.Automation%.dll%"' Action = $PSHostProcessStarted
SourceIdentifier = $sourceIdentifier
}

#Register alert for current session
Register-WmiEvent @PSHostProcArgs
}

We have two important parts in this script. First, take a look at the content of the script block $PSHostProcessStarted. It includes the action to be processed after the trigger has fired its event. Here, we extract further information from the process and validate the dlls and executable of the PowerShell host against our defined whitelisted values. The next important part is the trigger itself: $PSHostProcArgs. It includes the WMI query 'SELECT * FROM Win32_ModuleLoadTrace WHERE FileName LIKE "%System.Management.Automation%.dll%"', which searches for all loaded modules including an Automation.dll. The last line just registers this event, and afterwards it is just working in a hidden way. You can test this with VSCode and the demo Magic.exe from GitHub.

First, create a new subscription as follows: 

#Create new subscription
New-ForensicPSWMIEventSubScription -SourceIdentifier 'PS_ES' -WhitelistedProcesses @('PowerShell.exe') -WhitelistedDllSubStrings ('NativeImages_v4.0.30319_')

You can run this in ISE or Visual Studio Code, whichever you prefer. Next, launch the Magic.exe executable, which is a C# program, and also import System.Management.Autiomation.dll, as shown in the previous example. After executing the Get-process cmdlets, you should see these results:

Magic.exe:

VSCode output:

WARNING: SIGNATURE: Host PowerShell process started

Date/Time: 06/20/2018 17:43:03
Process ID: 31364
Process Name: Magic.exe
Command Line: "C:UsersdavidOneDrive - das NevesBooksLearning PowerShellGithubBook_Learn_PowerShellCh72_Executing PowerShell differently3_Magic.exeMagic.exe"
StateUsedHost: bad
StateUsedDll: good
Dll loaded: WindowsassemblyNativeImages_v4.0.30319_64System.Manaa57fc8cc#
d6592025a7ef3a065cf2e2c0455468e6System.Management.Automation.ni.dll

EventLog:

Keep in mind that you cannot create two subscriptions with the same event. Removing an event subscription works with the Unregister-Event cmdlet:

#Remove event
Unregister-Event -SourceIdentifier 'PS_ES'

In addition, you have also stored additional information to file, which can easily be retrieved:

#region loading additional information
$ProcInfo = import-Clixml c: empProcInfo.clixml
$EventArgs = import-Clixml c: empEventArgs.clixml
Get-ChildItem -Path c: emp -Filter *.clixml
#endregion

An interesting idea would be to also make an image dump from the executable used, to have more forensic material on hand.

Further information on the procdump tool can be found at the following link:
https://docs.microsoft.com/en-us/sysinternals/downloads/procdump

You could easily add your procdump line after the Export-Clixml cmdlets.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.226.4.239