Blog: Red Teaming
Patchless AMSI bypass using SharpBlock
For those that followed my personal blog posts on Creating an EDR and Bypassing It, I developed a new tool called SharpBlock. The tool implements a Windows debugger to prevent EDR’s or any other DLL from loading into a process that SharpBlock launches.
One feature that was missing from the initial release of SharpBlock was the ability to bypass AMSI.
Currently there are several AMSI bypasses available for evading your latest and greatest malware payload. Some involve modifying the payload itself like @HackingDave demonstrates in the first part of his video here, and others involve patching the AmsiScanBuffer code which CyberArk reported on back in 2018.
Initially my plan was to simply go with patching the AmsiScanBuffer code in the same way CyberArk demonstrated, but during my research I also came across F-Secure’s blog on Hunting for AMSI bypasses. So, in keeping with the spirit of bypassing EDR’s, I started on a path of bypassing AMSI without the need for patching code.
SharpBlock operates by implementing a debugger that listens for DLL load events and prevents the DLL entry point from executing. Up until now, it wasn’t really a full-fledged debugger, as it was only interested in specific events. This got me thinking, can we expand on SharpBlock’s debugger functionality and automate the behaviour of setting breakpoints within the code and altering the return values without the need to patch code.
For the non-developers reading this blog, a breakpoint is essentially a bookmark placed on a particular line of code or instruction that will pause the program when execution reaches that particular point. This allows developers to inspect what the program was doing at that time. Developers can then inspect and modify variables and return values within the program and then continue execution when they are ready.
For natively compiled programs on x86/x86_64, there are two methods for implementing a breakpoint, software and hardware. Software breakpoints are out of the question since this actually involves patching the code we intend to place a breakpoint on, making our ASMI bypass detectable by tools such as AMSIDetection. So that leaves us with hardware breakpoints. There are a special set of debug registers available on Intel/AMD CPU’s that permit up to 4 hardware breakpoints per thread.
The registers in question are numbered Dr0 to Dr7. Dr0-Dr3 store the addresses for our breakpoints and Dr6 and Dr7 are control registers to enable the breakpoints and determine which breakpoints where triggered when a breakpoint is hit. Once the DR registers have been set on a particular thread, the CPU will trigger an int 1 interrupt (Single Step) when the instruction pointer is about to execute the instruction at the breakpoint address, which is then captured by the debugger.
SharpBlock implementation of EnableBreakpoint:
AmsiScanBuffer patching is an effective bypass, and in fact can be used long after a process has started. SharpBlock on the other hand has control of the process from the very beginning, so with this in mind I wanted to see if I could disable AMSI early and clear up as much as possible to remain undetected.
AmsiScanBuffer requires a context to be created prior to calling, which is done using the AmsiInitialize API. As part of my EDR research, I had noticed that some solutions support disabling the AMSI scan interface altogether. When disabled, the AmsiInitialize function would return error 0x80070002, which meant that AmsiScanBuffer was never called since no valid context was created. That was the plan with SharpBlock, setting a breakpoint on AmsiInitialize then update the return value to prevent a valid context from being created.
When the breakpoint is reached, SharpBlock will be interrupted with the EXCEPTION_DEBUG_EVENT. The exception code contains the value 0x80000004, which is the constant for the SINGLE_STEP exception. At that point we can query the thread context that triggered the exception, which returns all register state at the time of the breakpoint.
State of relevant registers when AmsiInitialize breakpoint is hit:
The goal of our bypass is to change the current instruction pointer back to the caller of AmsiInitialize and update the return value with 0x8007002 to indicate that AMSI is currently disabled. What we are essentially simulating is a RET instruction so that AmsiInitialize is not actually called. To achieve this there are 3 separate tasks that we need to update on the thread context.
- The first is to update the current instruction pointer to the caller. This is achieved by reading the memory address where RSP is currently pointing to, since on the first instruction of a function this will be the return address.
- The second is to set the RAX register to our return value of 0x8007002
- And the third is to increment our stack pointer so that it points to the same location as if the RET instruction was actually executed.
SharpBlocks implementions of DisableAMSI:
The Context64 class used within the code above is a light wrapper around the native API’s GetThreadContext and SetThreadContext, which essentially enable applications to query and change the current state of a thread and its registers. The thread that called AmsiInitialize is now in a state indicating AMSI has been disabled and no context was created.
One method that I can think of for detecting this bypass is to periodically retrieve the context of all threads and see if there are any breakpoints set on AmsiInitialize. To combat this, before resuming the debugged program, we also need to clean up. Since we do not need the breakpoints anymore, we can iterate through all our debugee’s threads and clear the breakpoints.
Clearing all hardware breakpoints:
The demo video below shows CyberArk’s AMSIDetection PoC running within the bottom right window.
The tool outputs “hash matches” if no AMSI tampering has been detected within launched powershell processes. The main window demonstrates running Mimikatz via powershell using SharpBlock. The first run is without the AMSI bypass in place, which is blocked by AMSI and no tampering is detected by AMSIDetection. The second run is with SharpBlock’s AMSI bypass method in place which allows Mimikatz to run, but again, AMSIDetection does not detect any tampering present.
The latest code for SharpBlock can be found on GitHub which includes the AMSI bypass method.