Patching the Internals
In this section, we’ll complete the attack and modify the first instruction
of AmsiOpenSession directly from PowerShell with the help of Win32 APIs.
To implement the attack, we’ll need to perform three actions.
We’ll obtain the memory address of AmsiOpenSession,
modify the memory permissions where AmsiOpenSession is located,
and modify the three bytes at that location.
In order to resolve the address of AmsiOpenSession, we would typically call
GetModuleHandle to obtain the base address of AMSI.DLL, then call GetProcAddress.
We previously used reflection to accomplish this
with the in-memory PowerShell shellcode runner.
As part of the shellcode runner we created, the LookupFunc method called
both GetModuleHandle and GetProcAddress from the
Microsoft.Win32.UnsafeNativeMethods namespace.
We can use this function like any other Win32 API to locate AmsiOpenSession
by opening a 64-bit instance of PowerShell_ISE,
and executing the following code.
To verify this address, we’ll open WinDbg,
attach to the PowerShell_ISE process,
and quickly translate the address to hexadecimal with the ? command,
prepending the address with 0n.
With the value converted, we can then unassemble the instructions at that
address to check if it is correct.
Clearly, we have located the address of AmsiOpenSession,
This solves our first challenge.
Now we must consider memory protections.
In Windows, all memory is divided into 0x1000-byte pages.
A memory protection setting is applied to each page,
describing the permissions of data on that page.
Normally, code pages are set to PAGE_EXECUTE_READ, or 0x20,
which means we can read and execute this code, but not write to it.
This obviously presents a problem.
Let’s verify this in WinDbg with !vprot,
which displays memory protection information for a given memory address:
This shows the current memory protection for the memory page,
which is indeed PAGE_EXECUTE_READ.
Since we want to overwrite three bytes on this page,
we must first change the memory protection.
This can be done with the Win32 VirtualProtect API,
which has the following function prototype.
The first argument is the page address.
The second argument is the size of the area we wish to modify.
This is largely irrelevant
since APIs like VirtualProtect operate on an entire memory page.
Setting this parameter to any value between 1 and 0xFFF
will produce the same result.
However, for clarity we will set this to “3”.
The third argument is the most important
since it dictates the memory protection we want to apply to the page.
In our case, we want to set this to PAGE_EXECUTE_READWRITE
represented by 0x40.
This will ensure that we retain the original read and execute permissions
and also enable our overwrite.
The final argument is a variable where the current memory protection will be
stored by the operating system API.
The first three arguments can easily be translated from the C data types to
corresponding types in .NET of [IntPtr], [UInt32], and [UInt32] respectively.
The output value is a pointer to a DWORD.
In C# we can specify this as a reference with the MakeByRefType method,
which can be used together with the [ref] keyword when invoking the function.
Additionally, the value itself is suppled as a [UInt32].
To invoke VirtualProtect from PowerShell, we pass its memory address,
found through LookupFunc,
and its arguments types, found through getDelegateType,
and combine them with GetDelegateForFunctionPointer.
We combined LookupFunc and getDelegateType into one statement
along with the argument types to create the $vp_function variable,
from which we call the Invoke method.
The $oldProtectionBuffer variable
is used to store the old memory protection setting as required
The code itself should simply return the value “True” if successful,
but we can verify it in WinDbg,
by pausing execution through Debug > Break,
and then repeating the !vprot command.
However, the new memory protection is set to PAGE_EXECUTE_WRITECOPY
instead of PAGE_EXECUTE_READWRITE.
In order to conserve memory,
Windows shares AMSI.DLL between processes that use it.
PAGE_EXECUTE_WRITECOPY is equivalent to PAGE_EXECUTE_READWRITE
but it is a private copy used only in the current process.
Now that we have located AmsiOpenSession and modified its memory protections,
we can overwrite the required three bytes.
We can use the Copy method from the System.Runtime.InteropServices namespace
to copy the assembly instruction, XOR RAX,RAX, represented as 0x48, 0x31,
0xC0 from a managed array to unmanaged memory.
This should disable AMSI as soon as it is used,
but we’ll restore the original memory protection to cover our tracks.
To restore the memory protections, we'll use VirtualProtect again
and specify the previous memory protection value 0x20.
Since we stored the function delegate in the $vp variable,
we do not have to resolve it twice.
To verify the modifications, we’ll again use WinDbg:
Notice the modified assembly instructions,
as well as the restored memory protections.
As a final test, we will enter the ‘amsiutils’ string,
which would normally trigger AMSI.
Very nice. The bypass indeed works and AMSI is disabled.
We can now execute arbitrary malicious PowerShell code.