Malicious VBA Macro’s: Trials and Tribulations
Introduction
Over this past winter break, I wanted to go back to learning more about malicious Word/Excel Macros and what the potential is there. I made a blog post over a year ago where I talked about a technique I haven’t seen used very often involving linking a remote VBA template to a word doc, which was then downloaded and ran only when the document is open. In that same blog post, I also added a self-deletion technique, making it harder for the blue team to run forensics on the malicious doc. In this post, I’ll be talking about other techniques I’ve learned, including calling Windows API functions, and I’ll be discussing my (somewhat) failed attempt to write VBA that dumps the LSASS process, but also my successful attempt at writing a reverse shell completely in VBA (no shellcode injection or dropping exe’s).
Calling the Windows API
Something cool I for some reason only found out about recently is the fact that you are able to call Windows API functions from VBA. All you have to do is declare the specific function you want to use, including the library it comes from, and we can do anything a normal C++/C#/<insert language here> program could do. Well almost anything (we’ll get to that). Below is an example of how one would declare a function for the Windows API “Sleep” function which originates in the kernel32.lib:
Private Declare PtrSafe Function Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)
Now you can call it as you would any other function (the below code will sleep for 5 seconds):
Dim seconds As Long
seconds = 5000
Sleep(seconds)
Some things to note here:
- You MUST declare the function as “PtrSafe” for VBA version 7 and later. If you’re writing VBA version 6 and earlier, you don’t use PtrSafe (although I don’t know why you’d be writing in an old version in the first place).
- When declaring the function parameters, you must “cast” the parameter type to one that is understood by VBA. The above example “casts” dwMilliseconds from DWORD to Long, since VBA doesn’t understand DWORD, but it understands Long.
That last part can be a little confusing, because how are you supposed to know the parallel VBA types to the Windows API types, especially for types such as pointers and handles, which at face value, doesn’t seem to be supported at all by VBA. Well, in 2018, Microsoft made an update to VBA that introduced the type LongPtr, which can be used for all handles and pointer types in Windows API. This cheatsheet table shows the VBA equivalent type for Windows API declarations, making it wayyyy easier to write VBA that calls Windows API functions. This link explains in good detail everything I talked about above and more.
Dumping Memory With VBA
Now that we understand how to call Windows API functions with VBA, theres no limit to the malicious tools we can write in VBA. Right?
Well not exactly, and I found that out after spending about week writing a program to dump the LSASS process, which can contain credentials of users who are or have logged into the victim Windows machine. The VBA code I wrote would simply dump the process memory into a .dmp file, which could then be used by mimikatz to extract credentials from the dump. I didn’t add code to exfiltratre the dump file, but you could pretty easily add code to email it to yourself.
The reason I stopped before adding exfiltration code is because I came to a harsh realization. Throughout the entire process, I had a test process I was dumping from, which was notepad to make sure it was actually able to dump the process correctly. Once I had everything working and was able to succesfully dump notepad.exe into a .dmp file, it was time to test it out on lsass.exe.
The first thing I hadn’t realized until that point was that you needed to be NT AUTHORITY/SYSTEM in order to dump the lsass process. This was a dumb oversight on my part, as I already knew that but hadn’t thought of it while writing my code. Just for fun though, I thought I would run the VBA code as NT AUTHORITY/SYSTEM just to see if it works. That’s when the second realization hit me: Windows does not allow the execution of Microsoft Office applications as NT AUTHORITY/SYSTEM, due to Software Restriction Policies.
face palm
I don’t regret writing that code though for a couple reasons: First, I learned a lot about writing VBA, specifically with how it relates to memory manipulation. And second, this code could actaully prove useful, because it can still dump a lot of other non-system processes. I haven’t looked into it yet, but I imagine there could be some use in dumping browser processes like Chrome and Firefox, so maybe stay tuned for that ;).
Writing A VBA Reverse Shell
Now for the success story. After making my realizations above, I then went back to the drawing board to think of a tool that I could make that didn’t need to be run as SYSTEM at all. The first thing that popped in my head was a reverse shell. There are probably hundreds, if not thousands, of various reverse shells out there on the internet written in a lot of different languages. To my surprise, however, I could not find anything on a reverse shell written entirely in VBA. Most of the VBA code online would have various methods to either inject shellcode or run powershell that would download a malicious executable and run it. So it looked like a reverse shell written entirely in VBA was the best option because it checked all 3 of my boxes: Something that not many people have done (in this case noone), something that was actually possible to make (smh lsass dumper), and something that helped me learn more about calling Windows API functions from VBA.
The 3 Horsemen Of My Headache
I definitely didn’t expect it to be as difficult as it was. I learned a lot along the way, but it took A LOT of time to get there. A ran into 3 major issues that I had to overcome: realizing I had to use WSASocketA()
instead of socket()
, actually getting WSASocketA()
to work, and finally getting CreateProcessA()
to work.
When I started writing this tool, the first thing I focused on was getting the sockets to work. Luckily for me, there was enough information online about using sockets in VBA, that I was able to make a barebones socket using Ws2_32’s socket()
function that could send and recieve data. Once I had this done, I thought it was smooth sailing. All I would have to do is call CreateProcessA()
and redirect stdout, stdin, and stderror to my socket and I’d have a reverse shell! Well turns out it wasn’t that simple.
I began working on CreateProcessA()
, which at face value didn’t seem to hard. I even got to the point where when I ran the code, a cmd prompt popped up on the local machine. So I edited the STARTUPINFOA
object to redirect the stdout, stdin, and stderror to the socket I created and……..nothing. My remote machine was getting a connection, but there was no cmd prompt or any actual data being sent or recieved. At this point, I knew writing this tool was going to be a lot harder than I anticipated. So I started digging around my code, trying different things, but nothing was giving me what I hoped for. I dug online for hours and hours, looking for an answer and I finally found it. As it turns out, you can’t redirect I/O to a socket if that socket was created using the socket()
function. I needed to use WSASocketA()
instead.
another face palm
That was the first horsemen I had overcome.
And so I made the definitions for WSASocketA()
and replaced socket()
with it and………..nothing again. This time, my remote machine wasn’t even getting an initial connection. So I knew I was doing something wrong with WSASocketA()
. I used API Monitor to see the exact API calls the VBA process was calling along with the parameter values that were being passed. Thankfully, I had a fully functional reverse shell written in C++ already, so I ran API Monitor on that as well and simply compared it to my VBA process calls. This is where I found that I was trying to pass NULL into lpProtocolInfo, but I was actually passing a random pointer. A little googling and I found how to actually pass NULL to Windows API functions. You first declare the parameter as ByVal lpProtocolInfo As Any
and then pass in the value ByVal 0&
and that will be the correct NULL you’re looking for. After making that change, I finally got a connection to the remote machine!
Second horsemen done.
But there was still a problem. I recieved a connection, but on the cmd prompt on the local machine, there was the error message “The handle is invalid”. The weirdest part was, I could actually send commands from the remote machine and they would get executed on the local machine, but no data was being sent back to the remote machine. So I could essentially pop calc all day, but I couldn’t get any actual information from the victim, and if I didn’t see it for myself, I would have no confirmation that any of my commands were actually getting executed. I knew there was a bug in my CreateProcessA()
function, so I did the same thing as before and popped open API Monitor to see what the difference was between the VBA CreateProcessA()
call and the C++ CreateProcessA()
call. That’s when I noticed that one of the STARTUPINFOA
parameters, lpReserved2
, was passing in a bad pointer, when it should have been passing in NULL. This was very similar to the problem I had before, but this time I wasn’t initializing lpReserved2
at all, so the default value had to be NULL. I had initially declared lpReserved2
as a Byte
type, but apparently that wasn’t the right move. Doing a little more research, I noticed that if you declare a String
in VBA, it will be initialized as NULL. So I switched out lpReserved2 As Byte
to lpReserved2 As String
and……………..IT WORKED!
I had a fully functioning reverse shell written entirely in VBA!
But my work wasn’t done yet. I knew this project was mostly a meme and didn’t have much practical use in the real world, but I still wanted to see if I could make it less detectable by AV (at least for static analysis). Let’s throw our VBA into VirusTotal as is and see how it does:
Actually not too bad. It was only detected as malicious by 7 AV engines. But I think we can do a little better. So I used EvilClippy to stomp the VBA so it would only get stored in the P-Code of the Word document.
As you can see, it brought the detection down to just 1 engine! Even still, I wanted that number to 0. So I used both EvilClippy and macro_pack to first obfuscate and then stomp the VBA and…….
We got it to 0!
Conclusion
I wrote this post to show my learning process of VBA and Windows API calls. I’m hoping with what I’ve learned, I can continue to build more useful (and maybe just fun) tools in VBA. If you found any of this information useful or interesting, let me know on Twitter! Until next time ;)
References
- https://codekabinett.com/download/win32api-data-types-vba.pdf
- https://codekabinett.com/rdumps.php?Lang=2&targetDoc=windows-api-declaration-vba-64-bit
- https://stackoverflow.com/questions/4993119/redirect-io-of-process-to-windows-socket/5725609#5725609
- http://www.rohitab.com/apimonitor
- https://github.com/outflanknl/EvilClippy
- https://github.com/sevagas/macro_pack
- https://github.com/JohnWoodman/VBA-Macro-Reverse-Shell
- https://github.com/JohnWoodman/VBA-Macro-Dump-Process