Skip to content

Exceptions

Santiago Hormazabal edited this page Feb 12, 2026 · 1 revision

VB exceptions are handled by a SEH (Structured Error Handler). It's possible to use these SEH in different ways. Microsoft lets you use them in C++ with special keywords, like __try and __catch.

However, SEH comes in another flavor, which is SEH in stack frames. Although not commonly known, or even documented officially, they exist. See here for more info.

All the sample EXE files I've made set up the SEH by registering the handler on the function prologue. The usual code involves obtaining the address of the current Thread Information Block or TIB, and then setting up an ad-hoc EXCEPTION_REGISTRATION structure.

Let's use as a reference an EXE file that contains the test case with the OnErrorResumeNextTest subroutine. The code is as follows:

Private Sub OnErrorResumeNextTest()
    Dim l As Integer
    'Dim o As Object
    On Error Resume Next
    'o.test
    MsgBox "This shouldn't be visible " & 1 / 0
    
    MsgBox "This should be visible"
End Sub

The disassembly of said function could look like this:

.text:004033E0 ?OnErrorResumeNextTest@Module1@@AAGXXZ proc near
.text:004033E0 var_D0          = qword ptr -0D0h
.text:004033E0 var_78          = dword ptr -78h
.text:004033E0 var_70          = dword ptr -70h
.text:004033E0 var_68          = dword ptr -68h
.text:004033E0 var_60          = dword ptr -60h
.text:004033E0 var_58          = dword ptr -58h
.text:004033E0 var_50          = dword ptr -50h
.text:004033E0 var_48          = dword ptr -48h
.text:004033E0 var_40          = dword ptr -40h
.text:004033E0 var_38          = dword ptr -38h
.text:004033E0 var_30          = dword ptr -30h
.text:004033E0 var_28          = byte ptr -28h
.text:004033E0 var_20          = dword ptr -20h
.text:004033E0 var_18          = dword ptr -18h
.text:004033E0 var_14          = dword ptr -14h
.text:004033E0 var_10          = dword ptr -10h
.text:004033E0 var_C           = dword ptr -0Ch
.text:004033E0 var_4           = dword ptr -4
.text:004033E0 this            = dword ptr  8
.text:004033E0
.text:004033E0                 push    ebp
.text:004033E1                 mov     ebp, esp
.text:004033E3                 sub     esp, 18h
.text:004033E6                 push    offset ___vbaExceptHandler
.text:004033EB                 mov     eax, large fs:0
.text:004033F1                 push    eax
.text:004033F2                 mov     large fs:0, esp
.text:004033F9                 mov     eax, 9Ch
.text:004033FE                 call    ___vbaChkstk
.text:00403403                 push    ebx
.text:00403404                 push    esi
.text:00403405                 push    edi
.text:00403406                 mov     [ebp-18h], esp
.text:00403409                 mov     dword ptr [ebp-14h], offset dword_4011D0
.text:00403410                 mov     dword ptr [ebp-10h], 0
.text:00403417                 mov     dword ptr [ebp-0Ch], 0
.text:0040341E                 mov     dword ptr [ebp-4], 1
.text:00403425                 mov     dword ptr [ebp-4], 2
.text:0040342C                 push    0FFFFFFFFh
.text:0040342E                 call    ds:__imp_@__vbaOnError
; .... more ....

Prologue/Setup of SEH

Let's dig a little bit on the first instructions. The first 3 instructions create a local frame, which is a common thing to see. Usually this is called a function prologue, as it creates space on the stack, and sets up register ebp for local addressing of local variables.

Following these, a certainly weird pattern is found.

  1. push offset ___vbaExceptHandler is pushing into the stack the address of the imported ___vbaExceptHandler function from the MSVBVM60.DLL.
  2. mov eax, large fs:0 is obtaining the value of the first DWORD element of the TIB, because the TIB is stored in fs:[0]. It turns out that the first element of the TIB is the current SEH frame. This is followed by a push eax.
  3. mov large fs:0, esp is actually overwriting the current SEH frame pointer with the one created in steps 1 and 2. Even if it doesn't look like, by pushing the exception handler and then the previous SEH frame pointer, the code has created a EXCEPTION_REGISTRATION structure on the stack, whose first DWORD element is the new exception handler function, and the second one is the pointer to the old SEH frame. See Figure 2 _except_handler_function.
  4. mov eax, 9Ch and the subsequent call ___vbaChkstk are not related to the exceptions, HOWEVER, they play a fundamental role in VB applications. This should be covered in another entry on this Wiki, but in short, it allocates the requested size in bytes (0x9c in this example) on the stack, but it also zeroes it.
  5. push ebx, push esi, push edi are irrelevant.
  6. The following 6 mov instructions are setting up a structure stored in the stack frame. I've figured that this is a structure, and I called it vbaProcedureLocalStorage.
    1. mov [ebp-18h], esp stores the current ESP (stack pointer).
    2. mov dword ptr [ebp-14h], offset dword_4011D0 stores the pointer of another structure, which I called vbaProcedureInfo.
    3. mov dword ptr [ebp-10h], 0 unknown at this time, otherInfo1
    4. mov dword ptr [ebp-0Ch], 0 unknown at this time, otherInfo2
    5. mov dword ptr [ebp-4], 1 this stores the index of the current VB instruction that precedes a call of a function that could raise an exception
    6. mov dword ptr [ebp-4], 2 same as above, but it's important to note that this starts at index 2, as it'll become clear later

Now, the structure looks like this:

typedef struct
{
	LPVOID						* uhm1;
	LPVOID						* uhm2;
	LPVOID						* ESP;
	vbaProcedureInfo			* lpProcedureInfo;
	DWORD						otherInfo1;
	DWORD						otherInfo2;
	DWORD						otherInfo3;
	DWORD						OnErrorNextInstructionIndex;
} vbaProcedureLocalStorage;

As seen before, the code set up 5 32-bit values on the stack, but it's been proven that the actual structure contains two additional pointers. These are named uhm1 and uhm2, and their meaning is still unknown.

Before calling a function that could raise an exception

VB's compiler updates the OnErrorNextInstructionIndex on the vbaProcedureLocalStorage structure every single time before it calls a function that could raise an exception.

On the example code provided above, there are two calls to a MsgBox, the first one looks like this:

.text:00403434                 mov     dword ptr [ebp-4], 3 ; <----------- here OnErrorNextInstructionIndex is updated to "3"
.text:0040343B                 mov     [ebp+var_60], 80020004h
.text:00403442                 mov     [ebp+var_68], 0Ah
.text:00403449                 mov     [ebp+var_50], 80020004h
.text:00403450                 mov     [ebp+var_58], 0Ah
.text:00403457                 mov     [ebp+var_40], 80020004h
.text:0040345E                 mov     [ebp+var_48], 0Ah
.text:00403465                 push    offset ___vba@04570278 ; "This shouldn't be visible "
.text:0040346A                 fld     ds:__real@8@3fff8000000000000000
.text:00403470                 cmp     __adjust_fdiv, 0
.text:00403477                 jnz     short loc_403481
.text:00403479                 fdiv    ds:__real@8@00000000000000000000
.text:0040347F                 jmp     short loc_403492
.text:00403481 ; ---------------------------------------------------------------------------
.text:00403481
.text:00403481 loc_403481:                             ; CODE XREF: Module1::OnErrorResumeNextTest(void)+97↑j
.text:00403481                 push    dword ptr ds:__real@8@00000000000000000000+4
.text:00403487                 push    dword ptr ds:__real@8@00000000000000000000
.text:0040348D                 call    __adj_fdiv_m64
.text:00403492
.text:00403492 loc_403492:                             ; CODE XREF: Module1::OnErrorResumeNextTest(void)+9F↑j
.text:00403492                 fnstsw  ax
.text:00403494                 test    al, 0Dh
.text:00403496                 jnz     loc_4035BD
.text:0040349C                 sub     esp, 8
.text:0040349F                 fstp    [esp+0D0h+var_D0]
.text:004034A2                 call    ds:__imp____vbaStrR8
.text:004034A8                 mov     edx, eax
.text:004034AA                 lea     ecx, [ebp+var_28]
.text:004034AD                 call    ds:__imp____vbaStrMove
.text:004034B3                 push    eax
.text:004034B4                 call    ds:__imp____vbaStrCat
.text:004034BA                 mov     [ebp+var_30], eax
.text:004034BD                 mov     [ebp+var_38], 8
.text:004034C4                 lea     eax, [ebp+var_68]
.text:004034C7                 push    eax
.text:004034C8                 lea     ecx, [ebp+var_58]
.text:004034CB                 push    ecx
.text:004034CC                 lea     edx, [ebp+var_48]
.text:004034CF                 push    edx
.text:004034D0                 push    0
.text:004034D2                 lea     eax, [ebp+var_38]
.text:004034D5                 push    eax
.text:004034D6                 call    ds:__imp____vba@rtcMsgBox ; <----------- MsgBox call here
.text:004034DC                 lea     ecx, [ebp+var_28]
.text:004034DF                 call    ds:__imp_@__vbaFreeStr
.text:004034E5                 lea     ecx, [ebp+var_68]
.text:004034E8                 push    ecx
.text:004034E9                 lea     edx, [ebp+var_58]
.text:004034EC                 push    edx
.text:004034ED                 lea     eax, [ebp+var_48]
.text:004034F0                 push    eax
.text:004034F1                 lea     ecx, [ebp+var_38]
.text:004034F4                 push    ecx
.text:004034F5                 push    4
.text:004034F7                 call    ds:__imp_@__vbaFreeVarList
.text:004034FD                 add     esp, 14h

The first instruction of this snippet has updated the index to 3, prior to calling the rtcMsgBox (VB did some behind-the-scenes variable duplication before calling rtcMsgBox). Please note that this causes an exception inside

The next update of this index is to 4:

.text:00403500                 mov     dword ptr [ebp-4], 4 ; <----------- here OnErrorNextInstructionIndex is updated to "4"
.text:00403507                 mov     [ebp+var_60], 80020004h
.text:0040350E                 mov     [ebp+var_68], 0Ah
.text:00403515                 mov     [ebp+var_50], 80020004h
.text:0040351C                 mov     [ebp+var_58], 0Ah
.text:00403523                 mov     [ebp+var_40], 80020004h
.text:0040352A                 mov     [ebp+var_48], 0Ah
.text:00403531                 mov     [ebp+var_70], offset ___vba@045702B8 ; "This should be visible"
.text:00403538                 mov     [ebp+var_78], 8
.text:0040353F                 lea     edx, [ebp+var_78]
.text:00403542                 lea     ecx, [ebp+var_38]
.text:00403545                 call    ds:__imp_@__vbaVarDup
.text:0040354B                 lea     edx, [ebp+var_68]
.text:0040354E                 push    edx
.text:0040354F                 lea     eax, [ebp+var_58]
.text:00403552                 push    eax
.text:00403553                 lea     ecx, [ebp+var_48]
.text:00403556                 push    ecx
.text:00403557                 push    0
.text:00403559                 lea     edx, [ebp+var_38]
.text:0040355C                 push    edx
.text:0040355D                 call    ds:__imp____vba@rtcMsgBox
.text:00403563                 lea     eax, [ebp+var_68]
.text:00403566                 push    eax
.text:00403567                 lea     ecx, [ebp+var_58]
.text:0040356A                 push    ecx
.text:0040356B                 lea     edx, [ebp+var_48]
.text:0040356E                 push    edx
.text:0040356F                 lea     eax, [ebp+var_38]
.text:00403572                 push    eax
.text:00403573                 push    4
.text:00403575                 call    ds:__imp_@__vbaFreeVarList
.text:0040357B                 add     esp, 14h

When an exception is raised

Windows will call the error handlers in the SEH frame in order. As calls to most of MSVBVM60.DLL's can cause an exception, this handler should handle the case when that happens.

The handler has to have the following declaration:

EXCEPTION_DISPOSITION __cdecl __vbaExceptHandler(
	struct _EXCEPTION_RECORD	*ExceptionRecord,
	void						*EstablisherFrame,
	struct _CONTEXT				*ContextRecord,
	void						*DispatcherContext
)

MSVBVM60.DLL raises VB-exceptions with an ExceptionCode of 0xC000008F, two arguments, and both of them are 0xdeadcafe.

This means, if we receive an exception, it's possible to figure out if it was raised by MSVBVM60.DLL by checking the value of the ExceptionRecord.

To be continued!

Clone this wiki locally