What Context Mom?
When we examined each of the AMSI Win32 APIs, we found that they all use the
context structure that is created by calling AmsiInitialize.
However, Microsoft has not documented this context structure.
Undocumented functions, structures, and objects are often prone to error,
and provide a golden opportunity for security researchers
and exploit developers.
In this particular case,
if we can force some sort of error in this context structure,
we may discover a way to crash or bypass AMSI without impacting PowerShell.
Since this context structure is undocumented, we will use Frida
to locate its address in memory and then use WinDbg to inspect its content.
As before, we’ll use the PowerShell prompt and trace it with Frida.
Let’s enter another “test” string
to obtain the address of the context structure.
Recall that amsiContext is created when AMSI is initialized so its memory address
does not change between scans, allowing us to inspect it easily with WinDbg.
As a next step, we’ll open WinDbg,
attach to the PowerShell process,
and dump the memory contents of the context structure.
We don’t know the size of the context structure but we find that the first four
bytes equate to the ASCII representation of “AMSI”.
This seems rather interesting and might be usable
since this string is likely static between processes.
If we can observe the context structure in action in the AMSI APIs,
we may be able to determine if the first four bytes are being referenced in any way
To do this, we’ll use the unassemble command in WinDbg along with
the AmsiOpenSession function from the AMSI module:
The fourth line of assembly code is interesting as it compares the contents
of a memory location
to the four static bytes we just found inside the context structure.
According to the 64-bit calling convention,
we know that RCX will contain the function’s first argument.
The first argument of AmsiOpenSession is exactly the context structure
according to its function prototype, which means that a comparison is performed
to check the header of the buffer.
Although we don’t know much about this context structure, we observe
that the first four bytes equate to the ASCII representation of “AMSI”.
After the comparison instruction, we find a conditional jump instruction,
JNE, or “jump if not equal”.
If the header bytes are not equal to this static DWORD, the conditional jump
is triggered and execution goes to offset 0x4B inside the function.
Let’s use WinDbg to display the instructions at that address
The conditional jump leads directly to an exit of the function where the static
value is placed in the EAX register.
On both the 32-bit and 64-bit architectures,
the function return values are returned through the EAX or RAX register.
If we revisit the function prototype of AmsiOpenSession,
we notice that the return value type is HRESULT.
HRESULT values are documented and can be referenced on MSDN where we find
that the numerical value placed in the EAX register
corresponds to the message text E_INVALIDARG.
The message text, while not especially verbose, indicates that an argument,
which we would assume to be amsiContext, is invalid.
In short, this error occurs if the context structure has been corrupted.
If the the first four bytes of amsiContext do not match the header values,
AmsiOpenSession will return an error.
What we don’t know is what effect that error will cause.
In a situation like this, there are typically two ways forward.
The first is to trace the call to AmsiOpenSession that returns this error
and try to figure out where that leads.
This could become very time-consuming and complex.
The second, and much simpler, approach is to force a failed AmsiOpenSession call,
let the execution continue, and observe what happens in our Frida trace.
Let’s try this approach first
In order to force an error, we’ll place a breakpoint on AmsiOpenSession,
and trigger it by entering a PowerShell command.
Once the breakpoint has been triggered,
we’ll use ed to modify the first four bytes of the context structure,
and let execution continue.
After overwriting the AMSI header value,
we’ll continue execution, which generates exceptions
According to this output, AmsiOpenSession() has exited.
This could indicate that AMSI has been shut down.
To test this, we’ll enter the ‘amsiutils’ string
that was previously flagged as malicious:
This time, none of the hooked AMSI APIs are called and our command is not flagged.
By corrupting the amsiContext header,
we have effectively shut down AMSI without affecting PowerShell.
We have effectively bypassed AMSI. Very nice.
Although this method is effective, it relies on manual intervention with WinDbg.
Let’s try to implement this bypass directly from PowerShell with reflection.
PowerShell stores information about AMSI in managed code inside the
System.Management.Automation.AmsiUtils class,
which we can enumerate and interact with through reflection.
As previously discussed, a key element of reflection is the GetType method,
which we’ll invoke through System. Management.Automation.PSReference,
also called [Ref].
GetType accepts the name of the assembly to resolve,
which in this case is System.Management.Automation.AmsiUtils.
Before we execute any code, we’ll close the current PowerShell session
and open a new one to re- enable AMSI.
Sadly, Windows Defender and AMSI are blocking us from obtaining a reference
to the class due to the malicious ‘AmsiUtils’ string.
We could again attempt to bypass Windows Defender with a split string,
as some public bypasses do, but Microsoft regularly updates the signatures
and this simple bypass may eventually fail.
Instead, we’ll attempt another approach and loop the GetTypes method,
searching for all types containing the string “iUtils” in its name:
Armed with a handle to the AmsiUtils class, we can now invoke the GetFields
method to enumerate all objects and variables contained in the class.
Since GetFields accepts filtering modifiers, we’ll apply the
NonPublic and Static filters to help narrow the results.
As we will soon realize,
the amsiContext field contains the unmananged amsiContext buffer.
However, we can not reference the field directly since amsiContext
also contains a malicious “amsi” string.
We’ll again loop through all the fields, searching for a name containing “Context”:
Although we managed to obtain the amsiContext field without triggering AMSI,
the output contains a very large integer.
Converting this to hexadecimal produces a value
which looks like a valid memory address.
To verify our theory that this is indeed the address of the amsiContext buffer,
we’ll open and attach WinDbg,
and dump the memory at that address:
The first four bytes at the dumped address contain the AMSI header values,
indicating that this is very likely the amsiContext buffer.
Now, let’s put this all together
We’ll recreate each of the steps and use Copy to overwrite the amsiContext header
by copying data, in this case, four zeros, from managed to unmanaged memory:
The executed commands do not produce any output,
but we can inspect our work by switching to WinDbg,
forcing a break through Debug > Break,
and dumping the content of the amsiContext buffer.
This output indicates that the context structure header was indeed overwritten,
which should force AmsiOpenSession to error out.
Next, let’s continue execution in the debugger,
switch back to PowerShell,
and enter the malicious ‘amsiutils’ string again:
The string was not flagged. Excellent.
We can combine this bypass into a PowerShell one-liner:
Not only is this bypass working, but it is difficult to blacklist now that we’ve
removed explicit signature strings and dynamically resolved the types and fields.
This is working well, but it’s not the only approach
We’ll work through another bypass in the next section