WEBINAR: On-demand webcast
How to Boost Database Development Productivity on Linux, Docker, and Kubernetes with Microsoft SQL Server 2017 REGISTER >
Figure 1: Imagine such code being legal in VB.
The purpose of this investigation is to enable applications written in VB6 to use function pointers. Other advantages may be discovered, such as embedding native code in VB applications, thus extending the world of possibilities without the need of external DLLs. For the sake of keeping this as brief and concise as possible, other variations of the techniques used have been ignored and the focus have been maintained on detailing the one methodology that handles more common situations. Before reading a comprehensive examination, I'm assuming you'd like to actually see a working sample project (see the download at the end of this article).
Did You Say Function Pointers?
Because pointers in general aren't VB6 specific, it might sound crazy to talk about function pointers in VB6. So, you should first see how you usually get function pointers in VB6. With an AddressOf operator? Yes, but not only! There's also the GetProcAddress Win32 API from kernel32.dll that can be used at runtime to retrieve addresses of functions exported by other DLLs. Again, not only... There're other scenarios! But, why would one use runtime loading when you simply can use a Declare statement? For the same reasons a C/C++ programmer would, for example, by using different DLLs (or versions of the same DLL) depending on the environment in which the application is running. In fact, the whole concept of plugins is based on loading, probably on demand, external components that export the expected functionality. For example, some codecs are even downloaded on demand, and then loaded by the application that uses them. Also, a function pointer might be given by an external module as a callback (aka delegate) and its value may depend on the state of some objects. In OOP, a well-known behavioral pattern called 'Template Method' is characterized by changing control flow at runtime. Using function pointers may considerably reduce the effort put in its implementation by reducing the number of classes that need to be defined.
Using Pointers that Are Not Type Safe
If you ever used EnumWindows API in VB6, you may have noticed that there's nothing that enforced you to pass the address of a function with a correct prototype as a callback. The code will fail at runtime but compile without complaining. Delegates in VB .NET overcome this issue, although their main purpose might have been to ensure availability of code, because CLR may discard compiled code that is not referenced. Although other languages have a way to declare type safe pointers (as typedef in C/C++), in VB6 you can only treat them as signed 4-byte numbers. The developer will be responsible for type checking, without any help from the compiler.
Making the Call
The method of choice for calling addresses stored as Long is replacing entries in the 'Virtual Function Table' (VFT) of a class because it provides enough flexibility, while it is still easy to use. It has the same behaviour in IDE, Native code and P-Code; this helps debugging. A vftable is a mechanism used in programming language implementations to support dynamic polymorphism; in other words, runtime method binding. Where some online resources describe how the VC++ 6.0 compiler creates vftables for classes, I couldn't find anything regarding the VB 6.0 compiler. Typically, a compiler creates a separate vftable for each class and stores its pointer as a hidden member of each base class, often as the first member. The VC++ compiler builds the vftable for each class in a read-only page of the code section in a similar way to string literals.
To modify its contents, one needs to use VirtualProtect API to temporarily change the access rights. The first address in the vftable created by VC++ points to the scalar destructor function of the class, and it is followed by the addresses of virtual functions, in their order of declaration. Although VC++ supports multiple inheritance and handles pure virtual function calls, your objective can be simply achieved by identifying how VB retrieves the address of a public method, given the address of an instance from a class. A visual inspection of a class instance was required to establish the location and content of the vftable. By displaying the memory content from the addresses of two instances of the same class, you should be able to identify which is the pointer to the vftable. Because both instances point to the same vftable, the pointer value must be the same and must belong to your VB module (by default, the starting address is 0x00400000). As you can see, the pointer to the vftable is stored as the first 4 bytes within the class instances.
Figure 2: Two objects sharing the same VFT.
Figure 3: Addresses at offset &H1C belong to your module.
The first seven addresses in the vftable point to code within 'msvbvm60.dll'. Modifying the class definition by adding more public methods will change the vftable content starting with offset &H1C. To consolidate the theory, I wrote an external DLL for breaking into VB calls to methods of an object and reading the disassembly with the VC++ debugger. This is much easier than it sounds. Have a global procedure creating an instance of a VB class, and call its first public method. At runtime, display the address of the global function, the address of the class instance, and the pointer to the vftable. Load the process with the VC++ debugger by pressing F11. Write down the current instruction pointer (eip of the application's entry point) and change it to the address of the global procedure (type it in the Registers window and press Enter). Set a breakpoint at this address. Change the eip to the old value and resume execution. When the breakpoint is reached, step through the code and observe the Registry values until the method of the class is called. The screenshots show how the eax register is getting the address of the object (0x14BE70), from which 4 bytes are copied into the ecx register, representing the address of the VFT (0x4033B8). The instruction at 0x402391 is the call to the first public method of that class and, as you can see, its offset in the vftable is &H1C.
Figure 4: Disassembly showing how to get addresses from vftable.
My investigation continued to see whether private member data or methods affect the location or content of the vftable. The answer was no, but public member variables change the contents of the vftable by inserting accessors and modifiers that will be called when using the member data outside the class. For proof of concept, I wrote the following test:
VERSION 1.0 CLASS Attribute VB_Name = "DynamicVFT" 'Byte offset of first index in Virtual Function Table (VFT). Private Const OffsetToVFT = &H1C 'Swaps the addresses of 2 public methods with the same prototype. Private Sub SwapPlayers() Dim pVFT As Long CopyMemory pVFT, ByVal ObjPtr(Me), 4 'get the VFT address. Dim fnAddress As Long 'get AddressOf Play. CopyMemory fnAddress, ByVal pVFT + OffsetToVFT, 4 CopyMemory ByVal pVFT + OffsetToVFT, _ 'replace Play address with Replay address. ByVal pVFT + OffsetToVFT + 4, 4 'replace Replay address with Play address. CopyMemory ByVal pVFT + OffsetToVFT + 4, fnAddress, 4 End Sub 'Address of this method can be found at first index in the VFT. Public Sub Play() Debug.Print "Dynamic VFT plays: White move." Call SwapPlayers End Sub 'Address of this method can be found at second index in the VFT. Public Sub Replay() Debug.Print "Dynamic VFT plays: Black move." Call SwapPlayers End Sub
Sub Main() 'Phase 1: Making the call. Dim dynObj As New DynamicVFT Dim idx As Long For idx = 0 To 9 dynObj.Play Next idx End Sub
Note: These methods had the same prototype. Swapping the addresses in the vftable worked as expected and the output of the above code is shown below:Dynamic VFT: White move. Dynamic VFT: Black move. Dynamic VFT: White move. Dynamic VFT: Black move. ...
So, changing the values in the VFT of a VB6 class will replace the methods of that class. One more important thing to remember when modifying vftables is that they are shared by all instances of that class.