Shedding light on creating VBA macros
Dear Fellowlship, today’s homily is about tricks to transcribe well-known attacks and TTPs to the VBA cursed language. Please, take a seat and listen to the story.
Prayers at the foot of the Altar a.k.a. disclaimer
There are high chances of invoking daemons from other dimensions while coding tools in the form of VBA macros. Please proceed with caution and always under adult supervision.
Introduction
As explained in our article Hacking in an epistolary way: implementing kerberoast in pure VBA, we are implementing well-known attacks as VBA macros. This task is extremely frustrating due to restrictions imposed by VBA, which often require workarounds and hacky tricks to address situations that are a nonissue in most other languages. Most of the times we have to google through old forums to find a suitable solution, so we decided to create this article where some of those tricks are collected, so that in 2020 you do not have to waste your time as we did.
Keep in mind that we are focused on implementing the attacks avoiding the usage of process injections, binary drops or PowerShell. We do it calling Windows APIs directly with pure VBA :)
An ode to offsetof
for the hours saved
One of the most common problems we had to face when creating VBA tools was creating the data structures used by the APIs. VBA types can be a bit tricky, but once you learn their sizes it is easier to mentally translate a C structure to VBA. Except when you have to deal with misalignments. That is a pain in the ass.
Recently, one of our owls created a VBA Macro to extract and decrypt passwords saved in Chrome. In the process of creating such Cronenberg’s abomination of code, a problem arised: calls to bcryptdecrypt()
for the AES-GCM decryption were failing with “INVALID_PARAMETERS” status. However, checking the call with API Monitor showed no issues, and after a few hours of practicing the ancient sport of hitting a wall with your head, the problem was located: the structure members were misplaced.
This function uses the BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO
structure, defined as:
typedef struct _BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO {
ULONG cbSize;
ULONG dwInfoVersion;
PUCHAR pbNonce;
ULONG cbNonce;
PUCHAR pbAuthData;
ULONG cbAuthData;
PUCHAR pbTag;
ULONG cbTag;
PUCHAR pbMacContext;
ULONG cbMacContext;
ULONG cbAAD;
ULONGLONG cbData;
ULONG dwFlags;
} BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO, *PBCRYPT_AUTHENTICATED_CIPHER_MODE_INFO;
If you start to blindly translate the structure to VBA, just matching its types, the structure will be misaligned. The easiest way to know where every member should be, aligning the appropiate types (with padding if needed), is to use offsetof
:
#include <windows.h>
#include <bcrypt.h>
#include <stdio.h>
int main()
{
printf("cbSize=%d\n", offsetof(BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO, cbSize));
printf("dwInfoVersion=%d\n", offsetof(BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO, dwInfoVersion));
printf("pbNonce=%d\n", offsetof(BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO, pbNonce));
printf("cbNonce=%d\n", offsetof(BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO, cbNonce));
printf("pbAuthData=%d\n", offsetof(BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO, pbAuthData));
printf("cbAuthData=%d\n", offsetof(BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO, cbAuthData));
printf("pbTag=%d\n", offsetof(BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO, pbTag));
printf("cbTag=%d\n", offsetof(BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO, cbTag));
printf("pbMacContext=%d\n", offsetof(BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO, pbMacContext));
printf("cbMacContext=%d\n", offsetof(BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO, cbMacContext));
printf("cbAAD=%d\n", offsetof(BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO, cbAAD));
printf("cbData=%d\n", offsetof(BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO, cbData));
printf("dwFlags=%d\n", offsetof(BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO, dwFlags));
printf("sizeof=%d\n", sizeof(BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO));
return 0;
}
Which returns the offset of each structure member:
cbSize=0
dwInfoVersion=4
pbNonce=8
cbNonce=16
pbAuthData=24
cbAuthData=32
pbTag=40
cbTag=48
pbMacContext=56
cbMacContext=64
cbAAD=68
cbData=72
dwFlags=80
sizeof=88
Now you can set the types and paddings needed to properly align the structure :)
Dealing with memory
Working with memory is pretty easy once you get used to do so. If no fancy stuff is needed, you can just declare an empty byte array (Dim stuff() as Bytes
) and then resize it as needed using redim (redim stuff(0 To Size-1)
). In order to copy memory we are going to call RtlMoveMemory
, and VarPtr
gives us a pointer to an element inside the array. Imagine a function call that returned a pointer to a memory structure from which we want to retrieve a value (let’s say it is at offset 64 with size 16):
Private Declare PtrSafe Sub CopyMemory Lib "KERNEL32" Alias "RtlMoveMemory" (ByVal Destination As LongPtr, ByVal Source As LongPtr, ByVal Length As Long)
'(...)
dim tmpBuf() as Byte
dim ReturnedPointer as LongPtr
'(...)
ReturnedPointer = something(arg1,arg2)
redim tmpBuf(0 To 15) 'size - 1
Call CopyMemory (VarPtr(tmpBuf(0)), ReturnedPointer + 64, 16)
'(...)
We can also work with the heap in the same way (code reused from the kerberoast post):
Private Declare PtrSafe Function GetProcessHeap Lib "KERNEL32" () As LongPtr
Private Declare PtrSafe Function HeapAlloc Lib "KERNEL32" (ByVal hHeap As LongPtr, ByVal dwFlags As Long, ByVal dwBytes As LongLong) As LongPtr
'(...)
Dim heap As LongPtr
Dim mem As LongPtr
heap = GetProcessHeap()
mem = HeapAlloc(heap, 0, LenB(KerbRetrieveRequest) + LenB(target))
Call CopyMemory(mem, VarPtr(tempToFix(0)), LenB(KerbRetrieveRequest) + LenB(target))
'''(...)
In case you want to retrieve a field that is a pointer, you can directly copy its value to a LongLong
or LongPtr
variable (this also applies to other numeric values like sizes, you only need to set the appropiate variable type).
Dim pointer As LongPtr
Call CopyMemory(VarPtr(pointer), VarPtr(something(144)), 8)
Keeping the value inside a LongPtr
instead of a byte array makes it easier to use it later (to do arithmetics or to pass it as a function argument)
Dealing with strings
If a function returns an LPSTR
or LWPSTR
and we need to use it in the VBA itself, we are copying its value to a byte array as done before, but this time calculating the string size using lstrlenA()
or lstrlenW()
. Then, if the string is ANSI, we use strconv(array,vbUnicode)
. There is a good example in this post:
'Converting an LPTSTR (ANSI) String Pointer to a VBA String
Private Declare PtrSafe Function lstrlenA Lib "kernel32.dll" (ByVal lpString As LongPtr) As Long
Private Declare PtrSafe Sub CopyMemory Lib "kernel32.dll" Alias "RtlMoveMemory" _
(ByVal Destination As LongPtr, ByVal Source As LongPtr, ByVal Length As Long)
Public Function StringFromPointerA(ByVal pointerToString As LongPtr) As String
Dim tmpBuffer() As Byte
Dim byteCount As Long
Dim retVal As String
' determine size of source string in bytes
byteCount = lstrlenA(pointerToString)
If byteCount > 0 Then
' Resize the buffer as required
ReDim tmpBuffer(0 To byteCount - 1) As Byte
' Copy the bytes from pointerToString to tmpBuffer
Call CopyMemory(VarPtr(tmpBuffer(0)), pointerToString, byteCount)
End If
' Convert (ANSI) buffer to VBA string
retVal = StrConv(tmpBuffer, vbUnicode)
StringFromPointerA = retVal
End Function
'Converting an LPWSTR (Unicode) String Pointer to a VBA String
Private Declare PtrSafe Function lstrlenW Lib "kernel32.dll" (ByVal lpString As LongPtr) As Long
Private Declare PtrSafe Sub CopyMemory Lib "kernel32.dll" Alias "RtlMoveMemory" _
(ByVal Destination As LongPtr, ByVal Source As LongPtr, ByVal Length As Long)
Public Function StringFromPointerW(ByVal pointerToString As LongPtr) As String
Const BYTES_PER_CHAR As Integer = 2
Dim tmpBuffer() As Byte
Dim byteCount As Long
' determine size of source string in bytes
byteCount = lstrlenW(pointerToString) * BYTES_PER_CHAR
If byteCount > 0 Then
' Resize the buffer as required
ReDim tmpBuffer(0 To byteCount - 1) As Byte
' Copy the bytes from pointerToString to tmpBuffer
Call CopyMemory(VarPtr(tmpBuffer(0)), pointerToString, byteCount)
End If
' Straigth assigment Byte() to String possible - Both are Unicode!
StringFromPointerW = tmpBuffer
End Function
EoF
This article is just an addendum to our previous article “Hacking in an epistolary way”. We wanted to share a few tricks to help others build their own macros.
We hope you enjoyed this reading! Feel free to give us feedback at our twitter @AdeptsOf0xCC.