vb6 API and call backs
Author: David Zimmer
Date: 08.29.19 - 1:03pm
Next lets look at how vb6 handles call back routines. Consider the following code:
Private Declare Function CallWindowProc Lib "user32" _ Alias "CallWindowProcA" ( _ ByVal lpPrevWndFunc As Long, _ ByVal hwnd As Long, _ ByVal msg As Long, _ ByVal wParam As Long, _ ByVal lParam As Long _ ) As Long Private Sub Main() CallWindowProc AddressOf WindowProc, 1, 2, 3, 4 End End Sub Function WindowProc(ByVal hwnd As Long, ByVal msg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long MsgBox "In WindowProc : " & Join(Array(hwnd, msg, wParam, lParam), ",") End FunctionLets start with a pcode compile:
401D88 Form1.Form_Load: 401D88 F5 04000000 LitI4 0x4 401D8D F5 03000000 LitI4 0x3 401D92 F5 02000000 LitI4 0x2 401D97 F5 01000000 LitI4 0x1 401D9C 05 0000 ImpAdLdRf Module1.Proc_401DD8 401D9F 0A 01001400 ImpAdCallFPR4 user32.CallWindowProcA 401DA4 3C GetLastError 401DA5 FCC8 End 401DA7 13 ExitProcHresultHere is a screen shot of vbdec stopped just before the api call:
Module1.Proc_401DD8 is the address of the start of the pcode function, however we notice that ImpAdLdRf actually pushed a different value onto the stack. In my test case it actually pushed 4012D0. We then look at that address in our memdump pane and view the disassembly. Here we find:
4012D0 mov edx 401e84 ;last offset of Module1.Proc_401DD8 function 4012D5 mov ecx 40104e 4012DA jmp ecx401e84 is the last offset of Module1.Proc_401DD8 function
40104e = jmp  = msvbvm60.ProcCallEngine
So here is the code flow. CallWindowProcA will transfer execution to 4012D0. This is a custom stub embedded in the vb6 executable .text section.
This stub moves the target pcode functions raw addr into edx and then through two jmp's sends execution to the vb runtime export ProcCallEngine.
After note: So I looked into the disassembler guts and ImpAdLdRf handler. The 0000 arg is actually a const pool index for...4012D0..the const pool viewer detects it an external api stub and resolves it to the pcode function automatically. The disassembler engine also recognized it as such and auto resolved it to the pcode function.
In this case did the disassembler actual do to much work and hide info on us? Tough call..I guess I have to say no, I rather have it resolved to the actual function to be called, but its good to know the nitty gritty of whats going on too!.
To be thorough I might as well also throw in how execution gets to the actual api.
401D9F 0A 01001400 ImpAdCallFPR4 user32.CallWindowProcAImpAdCallFPR4 takes two two byte arguments. 0014 is the stack adjustment that takes place. If you look at our prototype its 5 longs, 20 bytes, or 0x14 in hex. The first arg of 0001 is a const pool offset, here resolving to 00401C7C. This is used used in a direct call eax. Again this points to a small native stub:
.text:00401C7C mov eax, dword_40233C .text:00401C81 or eax, eax .text:00401C83 jz short loc_401C87 .text:00401C85 jmp eax .text:00401C87 ; ------------------------------------------- .text:00401C87 .text:00401C87 loc_401C87: .text:00401C87 push 401C64h .text:00401C8C mov eax, offset DllFunctionCall .text:00401C91 call eax ; DllFunctionCall .text:00401C93 jmp eaxIn this clip of code we see a pointer address being check to see if its empty, if not we jmp to it. If it has not yet been loaded then it pushs the offset of some string pointers:
.text:00401C44 dd 7 .text:00401C48 aUser32 db 'user32',0 .text:00401C4F db 0 .text:00401C50 dd 10h .text:00401C54 aCallwindowproc db 'CallWindowProcA',0 .text:00401C64 dd offset aUser32 ; "user32" .text:00401C68 dd offset aCallwindowproc ; "CallWindowProcA" .text:00401C6C dd 40000h .text:00401C70 dd offset unk_402334 .text:00401C74 db 0 .tIt then calls the msvbvm export DllFunctionCall export to load the dll, resolve the target function address then does a jmp to the api. Something in DllFunctionCall must know where to cache the lookup. The 402334 after the string pointers is probably the key since its very close to the known 40233C address. I might dig into that detail somewhere along the way, but we can observe it in action so would not be to hard to find if we really wanted explicit knowledge of where.
Since the stubs used jmp's, execution naturally returns from the win api call back to the lower portion of our ImpAdCallFPR4 handler. I will include the entire commented handler below:
ENGINE:6610664E _lblEX_ImpAdCallFPR4: ENGINE:6610664E ENGINE:6610664E movzx ecx, word ptr [esi] ; pool index ENGINE:66106651 movzx edi, word ptr [esi+2] ; stack check arg ENGINE:66106655 add esi, 4 ENGINE:66106658 add edi, esp ; expected stack value on function return ENGINE:6610665A mov edx, [ebp-54h] ; const pool ENGINE:6610665D mov eax, [edx+ecx*4] ; get value from pool ENGINE:66106660 or eax, eax ENGINE:66106662 nop ENGINE:66106663 jz errorVal ; null check on pool value ENGINE:66106669 ENGINE:66106669 cmp _g_EventMonitorsEnabled, 0 ENGINE:66106670 jnz ??0002200 ENGINE:66106676 call eax ; call our embedded stub to look up api or used cached addr ENGINE:66106678 ENGINE:66106678 cmp edi, esp ; is the stack what we expected or corrupt? ENGINE:6610667A jnz StackErr_0 ENGINE:66106680 xor eax, eax ENGINE:66106682 mov al, [esi] ; get next pcode index ENGINE:66106684 inc esi ; inc pcode to next byte ENGINE:66106685 jmp ds:_tblByteDisp[eax*4] ; now jump to next pcode handler from tableSo now you have seen the entire chain for a call back.