Tl,dr; Toying with some AMSI bypasses from the internet were not working as expected, so I decided to walk through to see where it was failing and wrote my own AMSI bypass based off the works of [https://codewhitesec.blogspot.com/2019/07/heap-based-amsi-bypass-in-vba.html], [https://secureyourit.co.uk/wp/2019/05/10/dynamic-microsoft-office-365-amsi-in-memory-bypass-using-vba/], and [https://www.contextis.com/en/blog/amsi-bypass]. The purpose of the blog post is to bring light to this Bypass so defenders and Microsoft are aware of it, and for Penetration Testers/Security Professionals to use in engagements to yet again display that AMSI (and in broader context, AV and the like) is not enough alone to secure an environment. Ok, let’s get started…
As I am writing this, it should be made known that according to Dan @ [https://codewhitesec.blogspot.com/2019/07/heap-based-amsi-bypass-in-vba.html], Microsoft’s official position is that AMSI is not a security boundary, and numerous AMSI bypasses exist in the public realm. This journey began during the tail end of [silentbreaksec]‘s Malware Dark Side Ops course (great course I might add) I attended during the final DerbyCon. During the course, myself and another attendee were messing around with a Macro-based dropper and discovered that simple string mutation (which was the norm for evading Defender years ago) was no longer enough to ensure our payload would circumvent detection. While I didn’t spend too much more time mucking with the bypass during the rest of DerbyCon, I did have a strong desire to come back to this problem when I finally had the chance.
If you are reading this article, I’m certain you have to come to the same conclusion I quickly discovered once I began researching this issue. Thanks to the Anti-Malware Scan Interface library, my payload was no longer just getting triggered via static analysis, but behavioral analysis as well! As per [https://docs.microsoft.com/en-us/windows/win32/amsi/how-amsi-helps], as a user enables macros, a behavioral log is started, and any API, COM, etc calls deemed high risk are sent to AMSI to evaluate if the behavior is malicious or not.
AMSIScanBuffer and the Bypass Patch
Searches for “VBA AMSI bypass” yielded numerous results. Many, however, seemed to rely on calling the function “AMSIScanBuffer” itself, which causes Windows Defender to flag the macro as malicious.
This function is a popular target of AMSI Bypasses due to the nature that the user themselves can load the AMSI Library into memory, and the perform an in-memory “patch” to force “AMSIScanBuffer” to return “0”, thereby bypassing AMSI’s ability to perform it’s analysis on the malicious code. Interestingly, however, is that by knowing other functions to call in AMSI, you could derive the address of “AMSIScanBuffer”, by identifying it via the first dozen or so bytes of the function as written about [here]. In this article, the author uses “AmsiUacInitialize” as their base function to begin their search for “AMSIScanBuffer”. Walking through their code, and toying with some of the concepts still yielded a Windows Defender trigger. First, I attempted some very basic string substitution with anything containing AMSI (“Amsi.dll”, “AmsiUacInitialize”).
amdll = LoadLib("as"+"mi"+".dll", "Am"+"si"+"Uac"+"In"+"it"+"ial"+"ize")
Still caught… Ok, but I felt very confident that maybe if I avoided “AmsiUacInitialize”, this could potentially work. After scouring the internet further and in my own tests, I found that not only would “AmsiUacInitialize” potentially get caught as a static string, but so would “RtlMoveMemory” ([found here]), “CopyMemory”, and others. It was at this time I decided to write my own Amsi bypass in VBA, while borrowing heavily from others 😉 . Using a method very close to the [article] stated above, I decided I would find a function close to “AmsiScanBuffer”, use something other than “RtlMoveMemory” to bypass the function’s code by returning 0 as the function is called, and hopefully my “malicious” code would execute. Easy, right? Upon, reading this [blog] by Paul Laîné, I settled on using “DllCanUnloadNow” as the base to search for “AmsiScanBuffer”. Testing his powershell [gist], I was able to confirm that his AMSI bypass did indeed work for powershell.
The VBA I eventually wrote ended up resembling a true port of his powershell implementation. This was not by accident, as I ended up utilizing his powershell code in conjuction with WinDBG to check the debugged address I was seeing in my VBA code. While everything was there in the gist, I wanted to confirm on my own the proper “magic bytes”, if you will, via WinDBG. Upon launching Excel, I ran my macro with a breakpoint set shortly after loading asmi.dll into memory, then attached to the process via WinDBG. Next, I issued a “u !amsi!amsiscanbuffer L50” to view the first few bytes of the targeted function:
Sure enough, the first few bytes of “AmsiScanBuffer” are indeed “0x8b, 0xff, 0x55, 0x8b,0xec, 0x83 … ” just as found in the powershell gist referenced above. But I couldn’t use “RtlMoveMemory” to copy the memory over into a buffer for examination, so how would I do so? Again, as referenced by Dan in his [blogpost] on heap based amsi bypass in vba, I could use “CryptBinaryToStringA” from crypt32.dll. As provided [here], the function can copy data per a specified length to another buffer. In practice though, this was a bit of a headache trying to get running smoothly. For starters, I had to play with the VarPtr and ByVal modifiers a bit to get the data from amsi.dll buffer into my placeholder buffer. Additionally, if the final bytesWritten (pcchString) parameter was less than the length of what I passed to the function to write, the write did not always occur. These could have been isolated to me and my environment, but I wanted to ensure the reader was prepared for that fight just in case attempts to replicate this bypass presented these issues. Upon finally educating myself and fixing the issues that arose, I was able to read (and write) bytes from memory in VBA using this Crypt32 function.
Now on to my loop iteration. One of the things VBA is not known for is speed, so I knew I would have to try to optimize my for loop somehow in the hopes of speeding up finding the address of “AmsiScanBuffer”. Since I was walking memory one byte at a time looking for 10 consecutive bytes to match, I came to the conclusion that I could merely skip the next (up to) 9 bytes if I didn’t have a match on my magicbytes. From “DllCanUnloadNow” to “AmsiScanBuffer”, this may not make that much of difference, but future applications this could aid in optimization (left to the reader to figure out). The VBA loop ends up looking a bit like this (yes, it’s sloppy, but it works):
As seen above, I grab the address of “DllCanUnloadNow”, use a while loop to walk a byte at a time until the magic bytes (e3gg) are found, use a for loop to iterate to ensure all 10 bytes are there, if they aren’t I skip the next 8-n bytes and search again. In the case if they are found, I break the while loop to continue on to the patch (a goto would probably also work). I also check for 20k iterations, as most likely the magic bytes aren’t located (and most likely it is already patched), and exit the loop then. Great, now on to applying the patch!
Applying the patch of “0x31, 0xc0, 0xc3” initially seemed to work. A peek in WinDBG shows the patch present at the top of AmsiScanBuffer. Should be shell poppin’ time, right?
Or so I thought. Upon adding Shell “calc.exe” to the bottom, Excel would just… crash. Upon attaching to the process with WinDBG, we find an Access Denied Error, trying to run an instruction at 0x00001ac8. Huh?
After attempting to run down why this crash was occurring (remember this Bypass works 100% in the powershell gist), I eventually stumbled upon a difference in a previous screenshot of “AmsiScanString” [here] and the “AmsiScanString” from my environment. This led me to consider that maybe my “AmsiScanBuffer” was different as well:
Now, this is just a total shot in the dark, but maybe Excel is crashing as whatever AMSI is expected to return can no longer just be a simple “0”? It would appear that AmsiScanBuffer would now expect to pop 24 (0x18) bytes off the stack with the final “ret 18h“. Maybe if I modify my patch to match. Before I am able to, however, I quickly discovered that (duh) in x86 Office, I could only write up to 4 bytes at a time, so I would have to write the first 4 bytes, and then write the next byte. I elected to merely increment the target address by 1 and write another 4 bytes as this would accomplish the same thing. The updated patch looks something like this:
Ok, now to see if calc pops up:
And AmsiScanbuffer in memory looks like this:
Additional weaponization could be implemented to allow the VBA to figure out whether the old patch or new patch should be applied, which architecture the VBA is running in, etc. but I am leaving that as an exercise for the reader :-). A full copy of the VBA itself can be found here on my [github].
Bypassing static string analysis… still
So now that I have shown the world I’m a master hacker by popping calc.exe (/s), the real use case would be to get powershell to do something useful as it is seemingly the most targeted binary in malicious office docs these days. Simply calling powershell.exe itself was not enough to get Windows Defender to intercept my evil macro, I had to at least provide some arguments to it. My guinea pig string ended up being “powershell.exe -exec Bypass ping 127.0.0.1”, which flags.
Additionally, bypassing email filters and then like, it is probably not a great idea to leave the payload string un-obfuscated. The easiest obfuscation is by using the Chr() function of VBA and to transform the string into a collection of the numeric ASCII values of the characters and call the Chr() function on them. Also, it appears that the use of VBA’s “Shell” function may lead to detection as well, so using the “CreateProcess” function from kernel32.dll may be an alternative. Below is an example (FireFireFire is an alias to “CreateProcess”):
Private Declare PtrSafe Function FireFireFire Lib "kernel32" Alias "CreateProcessA" (ByVal lpApplicationName As String, ByVal lpCommandLine As String, lpProcessAttributes As Any, lpThreadAttributes As Any, ByVal bInheritHandles As Long, ByVal dwCreationFlags As Long, lpEnvironment As Any, ByVal lpCurrentDriectory As String, lpStartupInfo As STARTUPINFO, lpProcessInformation As PROCESS_INFORMATION) As Long ... Dim pInfo As PROCESS_INFORMATION Dim sInfo As STARTUPINFO Dim sNull As String Dim lSuccess As Long Dim lRetValue As Long woowoowoo = Chr(67) & Chr(58) & Chr(92) & Chr(119) & Chr(105) & Chr(110) & Chr(100) & Chr(111) & Chr(119) & Chr(115) & Chr(92) & Chr(115) & Chr(121) & Chr(115) & Chr(119) woowoowoo = woowoowoo & Chr(111) & Chr(119) & Chr(54) & Chr(52) & Chr(92) & Chr(119) & Chr(105) & Chr(110) & Chr(100) & Chr(111) & Chr(119) & Chr(115) & Chr(112) & Chr(111) & Chr(119) & Chr(101) & Chr(114) & Chr(115) & Chr(104) & Chr(101) & Chr(108) & Chr(108) & Chr(92) & Chr(118) & Chr(49) & Chr(46) & Chr(48) & Chr(92) & Chr(112) & Chr(111) & Chr(119) & Chr(101) & Chr(114) & Chr(115) & Chr(104) & Chr(101) & Chr(108) & Chr(108) & Chr(46) & Chr(101) & Chr(120) & Chr(101) & Chr(32) & Chr(45) & Chr(101) & Chr(120) & Chr(101) & Chr(99) & Chr(32) & Chr(66) & Chr(121) & Chr(112) woowoowoo = woowoowoo & Chr(97) & Chr(115) & Chr(115) & Chr(32) & Chr(45) & Chr(110) & Chr(111) & Chr(112) & Chr(32) & Chr(112) & Chr(105) & Chr(110) & Chr(103) woowoowoo = woowoowoo & Chr(32) & Chr(49) & Chr(50) & Chr(55) & Chr(46) & Chr(48) & Chr(46) & Chr(48) & Chr(46) & Chr(49) lSuccess = FireFireFire(sNull, woowoowoo, ByVal 0&, ByVal 0&, 1&, CREATE_NEW_CONSOLE, ByVal 0&, sNull, sInfo, pInfo) lRetValue = CloseHandle(pInfo.hThread) lRetValue = CloseHandle(pInfo.hProcess)
In summary, I have shown how relatively easy it may be to bypass current and potentially future AMSI protections. As is with most security products, it is a cat and mouse game that is always evolving. It had been suggested by many researchers to simply blacklist all of the functions of amsi.dll, but then a blog from [byte_St0rm] shows that such measures may not even be enough as they go through the process of digging for and finding the address of amsi.dll and the required “AmsiScanBuffer” function via the Process Environment Block – no longer requiring the string of “AMSI” anywhere in the VBA to perform a bypass. And it has been stated numerous times in the articles linked in this blog, that Microsoft themselves do not see Bypassing AMSI as a huge security issue, so it isn’t known if a response or not to this modification of an existing bypass will warrant any immediate attention.
Finally, I’d like to thank all the authors of the referenced material for their contributions.
This is a cross-post from Maveris’s Medium blog at https://medium.com/maverislabs.