A C-style approach to Visual Basic 6

December 1, 2024

An homage to a programming language which’s grammar and ideas don’t get much acclaim nowadays.

I grew up with Visual Basic 6. As terrible as it was, I still have a soft spot for it. For all of its shortcomings, VB6 taught me a surprising lot about computer internals.

Let’s say it is the year 2005, and you are into ricing your Windows XP setup. You decide that customizing your wallpaper switching logic would totally rock. And you decide to do that in VB6.

Now, generally VB6 would be a good choice in this ecosystem since anything Windows-related usually exposes itself through COM interfaces. VB6 makes using COM a breeze.

However, one would have to get access to the Active Desktop, which exposes itself through the IActiveDesktop interface. And VB6 does not support it, because IActiveDesktop does not inherit the IDispatch interface. Without it, VB6 can’t dynamically call the interface’s methods.

You’d have two choices: Hand-craft a COM type library and import it into your project. Or abuse and bend the language in horrific ways to avoid a single dependency. Since there’s no build system and manual build steps are unacceptable an… - option 2 it is.

The goal

If this were to be accomplished with C, this is what it would take. Straight forward.

IActiveDesktop* pActiveDesktop = NULL;

// Get an instance of IActiveDesktop
HRESULT hr = CoCreateInstance(&CLSID_ActiveDesktop, NULL, CLSCTX_INPROC_SERVER,
                              &IID_IActiveDesktop, (void**)&pActiveDesktop);

pActiveDesktop->lpVtbl->SetWallpaper(pActiveDesktop, pathToWallaper, 0);
pActiveDesktop->lpVtbl->ApplyChanges(pActiveDesktop, AD_APPLY_ALL);

// Decrement the ref counter, effectively destroying the instance
pActiveDesktop->lpVtbl->Release(pActiveDesktop);

Port it to Visual Basic

So C aside, let’s get us an instance of IActiveDesktop in our process:

Const CLSID_ActiveDesktop As String = _
  "{75048700-EF1F-11D0-9888-006097DEACF9}"
 
Const IID_Unknown As String = _
  "{00000000-0000-0000-C000-000000000046}"

Const CLSCTX_INPROC_SERVER As Long = &H1

Private Declare Function IIDFromString Lib "ole32.dll" ( _
  ByVal lpszIID As Long, _
  iid As Any) As Long

Private Declare Function CoCreateInstance Lib "ole32.dll" ( _
  rclsid As Any, _
  ByVal pUnkOuter As Long, _
  ByVal dwClsContext As Long, _
  riid As Any, _
  ppv As Long) As Long

Private Type GUID
  data1            As Long
  data2            As Integer
  data3            As Integer
  data4(7)         As Byte
End Type

Function GetActiveDesktop()
  Dim iidUnknown   As GUID
  Dim classid      As GUID
  Dim hRes         As Long
  Dim obj          As Long

  IIDFromString StrPtr(CLSID_ActiveDesktop), classid
  IIDFromString StrPtr(IID_UNKNOWN), iidUnknown

  hRes = CoCreateInstance(classid, 0, CLSCTX_INPROC_SERVER, IID_IUnknown, obj)
  If hRes <> 0 Then
    Debug.Print "This should never happen TM"
  End If
  GetActiveDesktop = obj
End Function

The first difference is, there’s no predefined class/interface GUIDs. But there’s a Windows API function for that! IIDFromString will happily take a string and spit out a parsed GUID.

To make things short: VB6 strings are BSTR . A wide string with its length as a 32 bit integer preceding it. IIDFromString expects a LPCOLESTR, so just a wide string. Passing a BSTR will work if one skips the length field. Conveniently, this is what StrPtr does.

VB also allows passing a string directly to API functions, either ByVal or ByRef. However, when we pass by value, VB will convert the wide string to an Ansi string, and if we pass by reference, VB will pass it as a BSTR. Therefore the only proper option is to use the StrPtr function.

Anyways, you now have a pointer to an IActiveDesktop interface, typed as a Long, or rather a 32-bit integer. So what really is such an interface? It’s a VTable consisting of two interfaces, or expressed in C:

DECLARE_INTERFACE_IID_(IActiveDesktop, IUnknown, "f490eb00-1240-11d1-9888-006097deacf9")
{
    // IUnknown methods
    STDMETHOD(QueryInterface) (THIS_ _In_ REFIID riid, _Outptr_ void **ppv) PURE;
    STDMETHOD_(ULONG,AddRef)  (THIS) PURE;
    STDMETHOD_(ULONG,Release) (THIS) PURE;

    // IActiveDesktop methods
    STDMETHOD (ApplyChanges)(THIS_ DWORD dwFlags) PURE;
    STDMETHOD (GetWallpaper)(THIS_ _Out_writes_(cchWallpaper) PWSTR pwszWallpaper, UINT cchWallpaper, DWORD dwFlags) PURE;
    STDMETHOD (SetWallpaper)(THIS_ _In_ PCWSTR pwszWallpaper, DWORD dwReserved) PURE;
    STDMETHOD (GetWallpaperOptions)(THIS_ _Inout_ LPWALLPAPEROPT pwpo, DWORD dwReserved) PURE;
    STDMETHOD (SetWallpaperOptions)(THIS_ _In_ LPCWALLPAPEROPT pwpo, DWORD dwReserved) PURE;
    STDMETHOD (GetPattern)(THIS_ _Out_writes_(cchPattern) PWSTR pwszPattern, UINT cchPattern, DWORD dwReserved) PURE;
    STDMETHOD (SetPattern)(THIS_ _In_ PCWSTR pwszPattern, DWORD dwReserved) PURE;
    STDMETHOD (GetDesktopItemOptions)(THIS_ _Inout_ LPCOMPONENTSOPT pco, DWORD dwReserved) PURE;
    STDMETHOD (SetDesktopItemOptions)(THIS_ _In_ LPCCOMPONENTSOPT pco, DWORD dwReserved) PURE;
    STDMETHOD (AddDesktopItem)(THIS_ _In_ LPCCOMPONENT pcomp, DWORD dwReserved) PURE;
    STDMETHOD (AddDesktopItemWithUI)(THIS_ _In_opt_ HWND hwnd, _In_ LPCOMPONENT pcomp, DWORD dwReserved) PURE;
    STDMETHOD (ModifyDesktopItem)(THIS_ _Inout_ LPCCOMPONENT pcomp, DWORD dwFlags) PURE;
    STDMETHOD (RemoveDesktopItem)(THIS_ _In_ LPCCOMPONENT pcomp, DWORD dwReserved) PURE;
    STDMETHOD (GetDesktopItemCount)(THIS_ _Out_ int *pcItems, DWORD dwReserved) PURE;
    STDMETHOD (GetDesktopItem)(THIS_ int nComponent, _Inout_ LPCOMPONENT pcomp, DWORD dwReserved) PURE;
    STDMETHOD (GetDesktopItemByID)(THIS_ ULONG_PTR dwID, _Inout_ LPCOMPONENT pcomp, DWORD dwReserved) PURE;
    STDMETHOD (GenerateDesktopItemHtml)(THIS_ _In_ PCWSTR pwszFileName, _In_ LPCOMPONENT pcomp, DWORD dwReserved) PURE;
    STDMETHOD (AddUrl)(THIS_ _In_opt_ HWND hwnd, _In_ PCWSTR pszSource, _In_ LPCOMPONENT pcomp, DWORD dwFlags) PURE;
    STDMETHOD (GetDesktopItemBySource)(THIS_ _In_ PCWSTR pwszSource, _Inout_ LPCOMPONENT pcomp, DWORD dwReserved) PURE;
};

VB6 has no concept of function pointers. So the closest we can get (in a 32 bit environment of course) is:

Type IActiveDesktop
  ' IUnknown
  QueryInterface          As Long
  AddRef                  As Long
  Release                 As Long
  ' IActiveDesktop
  ApplyChanges            As Long
  GetWallpaper            As Long
  SetWallpaper            As Long
  GetWallpaperOptions     As Long
  SetWallpaperOptions     As Long
  GetPattern              As Long
  SetPattern              As Long
  GetDesktopItemOptions   As Long
  SetDesktopItemOptions   As Long
  AddDesktopItem          As Long
  AddDesktopItemWithUI    As Long
  ModifyDesktopItem       As Long
  RemoveDesktopItem       As Long
  GetDesktopItemCount     As Long
  GetDesktopItem          As Long
  GetDesktopItemByID      As Long
  GenerateDesktopItemHtml As Long
  AddUrl                  As Long
  GetDesktopItemBySource  As Long
End Type

Using this knowledge, if one wanted to get the address of IActiveDesktop::SetWallpaper, one could read the 4 bytes at that exact memory offset. VB can’t do that though. But the Windows API can:

Declare Sub RtlMoveMemory Lib "kernel32" ( _
  pDst As Any, _
  pSrc As Any, _
  ByVal dlen As Long)

...

Dim obj     As Long
Dim vtbl    As IActiveDesktop
Dim vtblptr As Long

obj = GetActiveDesktop()
RtlMoveMemory vtblptr, ByVal obj, 4
RtlMoveMemory vtbl, ByVal vtblptr, Len(vtbl)
Debug.Print vtbl.SetWallpaper

Getting so close! That’s a very promising looking memory address. It’s definitely not zero.

Again, VB6 has no concept of function pointers. How would one use this number to actually execute a call? You have one more trick up your sleeve: The Windows API. Namely CallWindowProcA. It takes in the address of what is hopefully a window message handling function and its 4 arguments and then simply calls it. You could abuse it to call any other function instead.

Looking at IActiveDesktop::SetWallpaper, you count 3 parameters (2 parameters + this pointer). This is an issue because of the calling convention of the function, stdcall.

You’re almost there, if there was just some solution to not have the program crash after setting the wallpaper.

An assembler style approach to Visual Basic 6

If there was some kind of proxy, something that could convert from 4 arguments to 3, and clean the stack up properly? Something like this:

proxy:
  POP EAX   ; save return address to EAX
  POP ECX   ; remove unused argument
  POP ECX   ; remove unused argument
  POP ECX   ; remove unused argument
  POP ECX   ; remove unused argument
  PUSH EAX  ; put return address back
  
  PUSH MY_ARG_3
  PUSH MY_ARG_2
  PUSH MY_ARG_1
  CALL FUNC
  RET

These instructions are not difficult to generate at runtime. One would simply have to call them then, using CallWindowProcA. Seems straight forward:

Private Function CallPointer(ByVal fnc As Long, ParamArray params()) As Long
  Dim btASM(1024)  As Byte
  Dim pASM         As Long
  Dim i            As Integer
 
  pASM = VarPtr(btASM(0))
 
  AddByte pASM, &H58                  ' POP EAX
  AddByte pASM, &H59                  ' POP ECX
  AddByte pASM, &H59                  ' POP ECX
  AddByte pASM, &H59                  ' POP ECX
  AddByte pASM, &H59                  ' POP ECX
  AddByte pASM, &H50                  ' PUSH EAX
 
  For i = UBound(params) To 0 Step -1
    AddPush pASM, CLng(params(i))     ' PUSH dword
  Next
 
  AddCall pASM, fnc                   ' CALL rel addr
  AddByte pASM, &HC3                  ' RET
 
  CallPointer = CallWindowProcA(VarPtr(btASM(0)), 0, 0, 0, 0)
End Function

Private Sub AddPush(pASM As Long, lng As Long)
  AddByte pASM, &H68
  AddLong pASM, lng
End Sub

Private Sub AddCall(pASM As Long, addr As Long)
  AddByte pASM, &HE8
  AddLong pASM, addr - pASM - 4
End Sub

Private Sub AddLong(pASM As Long, lng As Long)
  RtlMoveMemory ByVal pASM, lng, 4
  pASM = pASM + 4
End Sub

Private Sub AddByte(pASM As Long, bt As Byte)
  RtlMoveMemory ByVal pASM, bt, 1
  pASM = pASM + 1
End Sub

Note: This would crash nowadays. Instead of a local array, you should allocate an executable page using VirtualAlloc. Don’t jump into data sections.

Putting it all together

...

Dim vtbl               As IActiveDesktop
Dim vtblptr            As Long
Dim pathToJpeg         As String
Dim iidActiveDesktop   As GUID
Dim clsidActiveDesktop As GUID
Dim obj                As Long

pathToJpeg = "win.jpg"

IIDFromString StrPtr(CLSID_ActiveDesktop), clsidActiveDesktop
IIDFromString StrPtr(IID_ActiveDesktop), iidActiveDesktop

CoCreateInstance clsidActiveDesktop, 0, CLSCTX_INPROC_SERVER, iidActiveDesktop, obj

RtlMoveMemory vtblptr, ByVal obj, 4
RtlMoveMemory vtbl, ByVal vtblptr, Len(vtbl)

CallPointer(vtbl.SetWallpaper, obj, StrPtr(pathToJpeg), 0)
CallPointer(vtbl.ApplyChanges, obj, AD_APPLY_ALL)

CallPointer vtbl.Release, obj

And there we have it. The zero dependency VB translation of the initial C code snippet. And we’re not far off:

IActiveDesktop* pActiveDesktop = NULL;

CoCreateInstance(&CLSID_ActiveDesktop, NULL, CLSCTX_INPROC_SERVER,
                 &IID_IActiveDesktop, (void**)&pActiveDesktop);

pActiveDesktop->lpVtbl->SetWallpaper(pActiveDesktop, pathToWallaper, 0);
pActiveDesktop->lpVtbl->ApplyChanges(pActiveDesktop, AD_APPLY_ALL);

pActiveDesktop->lpVtbl->Release(pActiveDesktop);
A C-style approach to Visual Basic 6 - December 1, 2024 - Arne Elster