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.
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.AmsiScanBuffer
function is called.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.
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 theAmsiScanBuffer
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.