3

Context: I know it is possible to execute assembly code in vba. A simple method is to overwrite the entry of a COM object's virtual table (vtable) with a function pointer to some place in memory that contains executable instructions. Then when you invoke the COM object's overwritten method, VBA uses the standard calling convention to execute whatever function the corresponding vtable points to.

Even though I understand the theory, I have never seen this done in real life. So I'm attempting to implement a "hello world" example that just shows a message box.

Class DummyThing
    Sub DoNothing()
    End Sub
End Class

'Uses mem manip functions from https://github.com/cristianbuse/VBA-MemoryTools/blob/master/src/LibMemory.bas

Module VBA
    Private Const MEM_COMMIT = &H1000
    Private Const MEM_RESERVE = &H2000
    Private Const PAGE_READWRITE = &H4
    Private Const PAGE_EXECUTE_READWRITE = &H40

    Declare PtrSafe Function VirtualAlloc Lib "kernel32" ( _
        ByVal lpAddress As LongPtr, _
        ByVal dwSize As Long, _
        ByVal flAllocationType As Long, _
        ByVal flProtect As Long) As LongPtr

    Declare PtrSafe Function LoadLibrary Lib "kernel32" Alias "LoadLibraryA" ( _
        ByVal lpLibFileName As String) As LongPtr

    Declare PtrSafe Function GetProcAddress Lib "kernel32" ( _
        ByVal hModule As LongPtr, _
        ByVal lpProcName As String) As LongPtr
    
    [ PackingAlignment (1) ] 'byte alignment instead of dword
    Type ShellcodeStruct
        push1 As Byte
        mb_ok As Byte

        push2 As Byte
        captionAddress As Long ' Address of 'Hello World' caption

        push3 As Byte
        textAddress As Long    ' Address of 'Hello World' text

        push4 As Byte
        hwnd As Byte

        callOp As Byte
        callType As Byte
        MessageBoxWAddress As Long ' Address of MessageBoxW
        popOp As Byte              ' POP EAX

        retOp As Byte              ' RET
    End Type
    
    Sub Main()
        Dim base As DummyThing = New DummyThing
        Dim vtable As LongPtr = MemLongPtr(ObjPtr(base)) 'deref objptr to get vtable ptr
        
        Dim title As String = "foo"
        Dim caption As String = "hello"
        
        Dim MessageBoxW As LongPtr = pMessageBoxW
        Dim code As ShellcodeStruct = GetShellCode(caption, title, VarPtr(MessageBoxW))
        Dim buffer As LongPtr = VirtualAlloc(0&, LenB(code), MEM_COMMIT Or MEM_RESERVE, PAGE_EXECUTE_READWRITE)
        
        If buffer = 0 Then
            Debug.Print ("    |__ VirtualAlloc() failed (Err:" + Str(Err.LastDllError) + ").")
            Exit Sub
        Else
            Debug.Print ("    |__ VirtualAlloc() OK - Got Addr: 0x" + Hex(buffer))
        End If
        
        CopyMemory ByVal buffer, code, LenB(code)
        
        MemLongPtr(vtable + PTR_SIZE * 7) = buffer 'overwrite vtable
        base.DoNothing 'invoke the overwritten vtable
    End Sub
    
    Function pMessageBoxW() As LongPtr
        Dim hLib As LongPtr
        Dim addrMessageBoxW As LongPtr

        hLib = LoadLibrary("User32.dll")
        Return GetProcAddress(hLib, "MessageBoxW")
    End Function
    
    Function GetShellCode(ByRef caption As String, ByRef text As String, ByVal addrMessageBoxW As Long) As ShellcodeStruct
        Dim sc As ShellcodeStruct

        ' Fill in the opcodes:
        sc.push1 = &H6A
        sc.mb_ok = &H0

        sc.push2 = &H68
        ' Assuming caption is a VBA string holding 'Hello World'
        sc.captionAddress = StrPtr(caption)

        sc.push3 = &H68
        ' Assuming text is another VBA string holding 'Hello World'
        sc.textAddress = StrPtr(text)

        sc.push4 = &H6A
        sc.hwnd = &H0

        sc.callOp = &HFF
        sc.callType = &H15
        sc.MessageBoxWAddress = addrMessageBoxW
        sc.popOp = &H58
        sc.retOp = &HC3
        Return sc
    End Function

End Module

Note I'm using twinBASIC rather than VBA because it does not crash the host when I get an ACCESS VIOLATION. It also lets me remove packing from the UDT which is convenient. But I expect this to work in 32 bit VBA too.

The x86 assembly code I'm trying to execute is the following:

push 0               ; MB_OK
push 'Hello World'   ; Caption (address to string in memory) - StrPtr(caption)
push 'Hello World'   ; Text (address to string in memory) - StrPtr(text)
push 0               ; hWnd
call MessageBoxW     ; VBA BSTRs are wide I think - the address is hardcoded
pop eax              ; discard return code of message box - same error without this line
ret

Problem

The code works 90% - it shows the message box with the full captions. However it then crashes with (runtime error -2147467259: NATIVE EXCEPTION: ACCESS_VIOLATION) presumably on the ret instruction.

I'm thinking about maybe the handling of the this pointer to a COM method - although that should be passed in a register and not something to worry about. Maybe there is a hresult? Any ideas how to get this working?

I'm guessing as my shellcode is being invoked as a COM method with stdcall, as the callee I need to leave the stack in a particular state and I'm not doing that correctly

9
  • 1
    Remove the pop eax, the result is not returned on the stack. Commented Sep 27, 2023 at 9:58
  • MessageBoxW is just an exported function, this has nothing to do with COM. And why do you pop eax? Commented Sep 27, 2023 at 9:59
  • @SimonMourier The method for invoking is using a COM vtable overwrite. I might need an hresult because it's COM. MessageBoxW I get using GetProcAddress then hardcode into the assembly once it is in opcode form - is there a different way to do this? pop eax I thought got rid of the returned value of the message box function? Commented Sep 27, 2023 at 10:44
  • @Jester I was getting the same violation before I added that. This is the return value of Message Box I am trying to discard. Separately I'm also wondering if I need to return an HRESULT somehow if I'm overwriting a VBA Sub... Commented Sep 27, 2023 at 10:50
  • 2
    Your code doesn't contain any COM, but yes for a method of a COM interface, this must be passed as the first parameter to the method. Most Windows API are stdcall so you don't need to pop anything, as the callee is responsible for cleaning up the stack (not the caller ie: your code) en.wikipedia.org/wiki/X86_calling_conventions Commented Sep 27, 2023 at 10:52

1 Answer 1

0

2 mistakes:

  1. The MessageBox function returns its value in eax not the stack. So pop eax pops the stack into eax; removing valid data from the stack. Incidentally we want to set eax to 0 for HRESULT_SUCCESS anyway.
  2. In stdcall, the callee must clean the stack up. When VBA invokes the shellcode in the line base.DoNothing, it pushes a hidden this pointer to the COM object onto the stack. It is the job of the shellcode to consume this item from the stack.

Therefore the correct assembly instructions are:

push 0              ; MB_OK
push DWORD PTR [captionAddress]   ; Address of 'Hello World' caption
push DWORD PTR [textAddress]      ; Address of 'Hello World' text
push 0              ; hWnd
call DWORD PTR [MessageBoxWAddress] ; Direct call to MessageBoxW
xor eax, eax        ; Clear EAX register, essentially setting HRESULT to S_OK (0)
ret 4               ; Return, and adjust stack by 4 bytes (for the "this" pointer)

Note ret 4 will remove the 4 byte this pointer from the stack, since we didn't consume it anywhere else. xor eax, eax sets the HRESULT (I think) to 0

Sign up to request clarification or add additional context in comments.

13 Comments

since we didn't consume it anywhere else - The other important part of ret 4 is that this extra adjustment of ESP by 4 happens after popping the return address. As you say, you can't pop earlier, that would pop your return address and leave the stack arg. (Unless you did something nasty like pop ecx / add esp, 4 / push ecx / ret. (Or even worse for performance, jmp ecx instead of push/ret, unbalancing the return-address predictor so future rets will mispredict.) At that point MessageBoxW has already cleaned up the 4 args you pushed before the calling it.
@PeterCordes Yes I began to realise that. So does that mean every single time you stdcall with parameters, you have to save the return address somewhere before accessing the params underneath it on the stack? Then restore the address to the stack afterwards before calling ret? Seems inefficient
No, MessageBoxW will access its stack args like [esp+4], or by setting up EBP as a frame pointer and using [ebp+8]. You don't normally use pop to read from stack memory, except when restoring stuff you pushed yourself. It's still random-access memory, stack operations just exist as a compact way to access the very end of the stack.
@PeterCordes Ah that makes sense. Is this always true that the Stack is in RAM, or is there ever a hardware stack that makes accessing elements by index a slow O(n) operation. Since the stack is used so often it might make sense to have dedicated hardware?
It's just RAM. Most accesses to it are random access via normal addressing modes. Cache and store-forwarding make it fast like other RAM; the stack is normally thread-private (although a function could pass pointers to local vars to other threads, if it won't return until after the threads end). Anyway, that means it can normally stay hot in L1d cache or at least L2, exclusively owned (MESI) so both loads and stores can hit in cache.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.