Yet another update to bypass AMSI in VBA

Khris Tolbert
Maveris Labs
10 min readNov 19, 2019

--

Tl,dr; Toying with some VBA 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 are out there. 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. Initial tests against an up-to-date Win 10 v. 1903 system showed that existing bypasses would either crash or get flagged immediately by Defender (as they should). 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.

AMSI

If you are reading this article, I’m certain you have 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.

Microsoft’s AMSI integration with JS and VBA

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.

Office not liking the Amsi Bypass Macro

This function is a popular target of AMSI Bypasses due to the nature that the user themselves can load the AMSI Library into memory. Then, the user can perform an in-memory “patch” to force “AMSIScanBuffer” to return a score of “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")
Office not liking the Amsi Bypass Macro, again…

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.

Powershell AMSI Bypass

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 conjunction 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:

Breakpoint set after loading amsi.dll
Disassembly of AmsiScanBuffer loaded into memory

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 magic bytes. From “DllCanUnloadNow” to “AmsiScanBuffer”, this may not make that much of a difference, but this could aid in optimization for future applications (left to the reader to figure out). The VBA loop ends up looking a bit like this (yes, it’s sloppy, but it works):

Main loop searching for AmsiScanBuffer

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?

AmsiScanBuffer with patch applied
Patch logic in VBA with original patch

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?

WinDBG intercepting “Access Denied” Violation

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:

Side-by-side comparison of the differing AmsiScanString ret calls
AmsiScanBuffer ret call

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/return 24 (0x18) bytes off the stack with the final “ret 18h”. Maybe, modifying the patch to match could work. 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:

Updated patch in VBA

Ok, now to see if calc pops up:

Congrats! There’s a calc.exe!

And “AmsiScanbuffer” in memory looks like this:

AMSIScanBuffer with new patch applied

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 :-).

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, when bypassing email filters and the 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 (this gist may help). 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”):

VBA code snippet of an “encoded” powershell payload

Conclusion

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.

References

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/
https://www.contextis.com/en/blog/amsi-bypass
https://docs.microsoft.com/en-us/windows/win32/amsi/how-amsi-helps
https://medium.com/@byte_St0rm/adventures-in-the-wonderful-world-of-amsi-25d235eb749
https://ntopcode.wordpress.com/2018/02/26/anatomy-of-the-process-environment-block-peb-windows-internals/

This blog is cross-post of a post from my personal IT-related blog https://khr0x40sh.wordpress.com/

Website: www.maveris.com

Email: info@maveris.com

Maveris exists to help your organization reach its fullest potential by providing thought leadership in IT and cyber so you can connect fearlessly. To learn more visit us at: www.maveris.com

--

--

Khris Tolbert
Maveris Labs

Sometimes things break and I happen to be behind the keyboard. I’m just as confused as you are.