← All papers

Solving the VBA Upconversion Problem

[Bontchev2000a] Proc. 10th Int. Virus Bull. Conf., 2000, pp. 273–299.

Abstract: When Office 97 appeared, its automatic upconversion of Office 95 macros (and macro viruses) posed several significant technical and ethical problems to the anti-virus researchers. The main problem was caused by the dilemma between the need to provide protection against the new, automatically created by the upconversion viruses and the desire to avoid the creation of new viruses by the anti-virus people. Office 2000 re-introduces the same problems all over again—this time additionally complicated by the fact that the upconverts created automatically by it are tantalizingly similar to their Office 97 originators and that, at source code level, they are even undistinguishable from them. Several attempts have been made to solve this dilemma with different success. This paper presents a particularly successful new method developed by the author. The method allows a transformation to be applied on any VBA program from any existing version of Office (VBA3, VBA5 or VBA6) which transformation would convert that program into an invariant form, thus allowing all possible upconverts (or downconverts) of a VBA virus to be identified with a single database entry. Particular source code is given which allows any anti-virus producer to benefit from the new method.

1 The Problem

The appearance of Microsoft Office 97 marked the beginning of a particular kind of nightmare problem for the developers of anti-virus software. The applications in this version of Office sported a common macro programming language—Visual Basic for Applications version 5.0, or VBA5 for short. In order to make the transition from the previous versions of Office easier, Microsoft included in the Office 97 applications the ability to convert automatically and without consulting the user the macro programs written in the macro programming languages supported by the previous versions of Office—WordBasic and Visual Basic for Applications version 3.0 (VBA3 for short). Unfortunately, this also often resulted in the automatic upconversion of the macro viruses written for the previous versions of the Office applications. The upconversion process was so successful that the resulting new programs often were viruses on their own—but different from their originators.

As a language, VBA5 is very different from WordBasic—the macro programming language used by Microsoft Word in the previous versions of Microsoft Office. (The main differences are outlined in [Bontchev98].) Although it looks extremely similar to VBA3—the macro programming language used by Microsoft Excel in the previous versions of Microsoft Office—internally it is different enough from it too. The differences between VBA3 and VBA5 are substantial enough for the anti-virus producers to consider, for instance, as a different virus the VBA5 upconversion of a virus written in VBA3.

Essentially, the applications of Office 97 began acting like automatic virus construction kits—taking old, known viruses, and automatically producing new ones from them. Needless to say, this presented a huge problem to the developers of anti-virus software.

On the one hand, it was not easy to explain to the users all the subtleties of the language difference and automatic upconversion—from the point of view of the user, s/he is simply opening an old document with one of the applications in the new version of Office. It is counter-intuitive that, if the document contained an old, known virus, after saving it with the new applications it will contain a new, unknown one. Furthermore, since this was a relatively often occurring and rather natural and unambiguous (most of the time) process, the users had the right to expect that their anti-virus product would protect them from the new virus, if the old one was known to its developers.

On the other hand, the new viruses looked quite different; even behaved differently sometimes. Although the upconversion process was straightforward and unambiguous most of the time, it occasionally presented nasty and undocumented quirks which could easily cause serious mistakes and irritation to the anti-virus producer. Furthermore, the creation of a new, unknown virus, even for the purposes of implementing protection from it, is considered a major ethical no-no by some anti-virus researchers, including the author of this paper ([Bontchev98]).

Two years ago, in [Bontchev98] we presented a method which allowed the anti-virus producer to implement detection of the upconverts without creating a working new virus at any point of the process. Although the paper concentrates on the WordBasic VBA5 upconversion issue, exactly the same methods can be applied to the VBA3 VBA5 issue. In the past three years we have successfully used these methods to provide the users of our scanners with adequate protection from the upconverted viruses.

Unfortunately, one of the major drawbacks of the methods mentioned above is that they are rather time-consumming and somewhat difficult to automate completely. Furthermore, the similarity between VBA3 and VBA5 is such that one is tempted to expect that there exists a simple algorithm which implements a one-to-one mapping between these two languages. If the practical realization of such an algorithm is possible, the anti-virus producers would not have to resort to the time– and effort-consumming methods presented in [Bontchev98]; they could perform this mapping on-the-fly. This would mean that implementing the detection of a VBA3 virus would automatically result in the detection of its VBA5 upconvert.

The appearance of Microsoft Office 2000 resurrected the old upconversion problem. The applications in this version of Office use a common macro programming language too—VBA6. Although showing a striking similarity with VBA5, it is noticeably different from it internally.

Finally, both the VBA5 modules and their VBA6 upconversion contain the source of the original VBA program in compressed form. Although normally unused (i.e., not compiled or executed), the sources are one and the same in the two cases—regardless that they result in differently looking p-code when compiled by the two VBA versions.

The things were further complicated by the fact that several anti-virus products implemented Microsoft’s suggestion to identify VBA5 viruses from their source code. Although it is an extremely poor and dangerous idea to identify a virus by some part of it which is never used (instead of by the part that is actually executed), this approach had one major advantage. It allowed the anti-virus products which employed it not to care about the VBA6 VBA6 upconversion issue (because the sources look the same in both cases) and to automatically have detection of the VBA6 upconverts of the VBA5 viruses they knew. In fact, for many of them it was impossible to distinguish between the two. In order to accommodate this, it was decided to define the VBA6 upconverts of the macro viruses written in VBA5 as undistinguishable from their originators. Of course, this resulted in all kinds of problems for those anti-virus products that identified macro viruses by their p-code (i.e., the reasonable thing to do).

All these issues raised the sharp need for implementing the one-to-one mapping algorithm mentioned above. It clearly existed, at some level, for the VBA5 VBA6 case—at a source code level the two are undistinguishable. The problem was to make them look undistinguishable at a p-code level (in order to accommodate the anti-virus products relying on the p-code for virus identification) and, if possible, to apply the same ideas for developing a mapping algorithm for the VBA3 VBA5 case. (The latter is more difficult, because it is not immediately obvious that such an algorithm exists at some level—VBA3 modules do not carry their own source.)

After several months of research and reverse-engineering we managed to come up with such a mapping algorithm applicable both to the VBA3 VBA5 and to the VBA5 VBA6 case. The purpose of this paper is to present our algorithm to the public.

2 Solutions That Don’t Work

Before we describe our method in details, we shall consider the possible alternative methods used by some anti-virus producers to solve the problem outlined in the previous section. We shall also explain what are the main flaws of these alternative methods and why ours is superior to them.

2.1 Upconversion Using the Office Applications

Probably the most straightforward approach one could think of for solving the problem described in the previous section is to simply use the corresponding Office application, upconvert the virus with it, and implement detection of the upconvert. For instance, one could use Excel 97 to upconvert an Excel 95 virus, in order to implement detection of the upconvert. This method works in the opposite direction too (although only for VBA3 viruses)—one could use Excel 97 to downconvert an Excel 97 virus to Excel 93 form and implement detection of the downconvert.

Unfortunately, the simplest approach to solving a complex problem is usually a wrong one. A detailed explanation of all the drawbacks of the approach outlined above is outside the scope of this paper—it is covered well enough in [Bontchev98]. In short, the problems are:

  1. Ethical problems. The process outlined above results in the creation of a new, previously not existing virus which is different from the original (see [VBull] and also Appendix A in [Bontchev98] for explanation of why it is different). Even if the creation of this virus is likely to occur naturally during the normal work of the user and even if it is being done in order to implement improved protection for the user, we consider the intentional creation of new, previously unknown viruses by the anti-virus people to be very unethical ([Bontchev97a]). Especially if alternatives exist to achieve the same goal, as is the case here.

  2. Technical problems. The upconvertors implemented in Office 97 have several quirks which can cause unexpected results. Especially if the upconversion is performed in a pseudo-automated way, without thinking, it can result in parts of the virus being lost, the result not being a virus any more, the result being slightly different than what would normally occur on a user’s machine and so on. The net result is that creating the upconvert and implementing detection of it might not necessarily result in providing protection from the real thing when/if it appears on the user’s machine.

2.2 Processing Only the Source

Since version 5, Visual Basic for Applications stores its modules in two forms—compiled to p-code and in source code, as entered by the user. (Actually, a third form, called execodes, is also present sometimes. However, its coverage is outside the scope of this paper.) Both are kept in different places of the same OLE2 stream that contains the module. The source code is stored in (rather trivially) compressed form at the end of the stream.

Most of the time the source code is left completely unused. Even when the module is examined with the VBA Editor, the editor de-compiles the p-code and displays the result of this decompilation, instead of displaying the source. It is possible to take a VBA module, wipe the source code area in it with a hex editor, and the resulting file will still work perfectly. If it is a virus, the contents of the source code area will be re-created (by de-compiling the p-code) in the replicants of the virus during its natural replication. So, what is the use of this source code area, then?

It is there for compatibility purposes—and the idea behind it is rather ingenious. Each VBA project (the complete set of VBA modules in an Office document) contains a version number in a particular place of the _VBA_PROJECT stream. (Unfortunately, we cannot reveal its exact location, because we have learned it from Microsoft under a non-disclosure agreement.) When an Office application opens a document and finds a VBA project in it, it passes it to the version of the VBA interpreter supported by that particular version of Microsoft Office.

The VBA interpreter examines the version number of the VBA project, as mentioned above. If it is a version number it recognizes, it simply begins to interpret the p-code instructions in the module(s) of the VBA project. If, however, the version of the VBA project in the document is not recognized by the local VBA interpreter, the latter simply fetches the sources of the module(s) in the project from the respective source code area(s), compiles them to p-code it can interpret, and begins interpreting this p-code. Microsoft guarantees that VBA will be upwards compatible at source code level—even if the underlying p-code differs wildly between the different VBA versions.

Due to the ingenuity of the idea described above, Office 2000 and later versions no longer needs to have the upconvertors equivalent to those that Office 97 has (so that it can still execute the programs written in the macro programming language(s) of the previous versions of Office). Instead of having to write a p-code upconvertor from VBA5 to VBA6 (and backwards), Microsoft have simply made VBA6 unable to recognize the VBA project version number of VBA5. As a result, when an Office 2000 application opens an Office 97 document containing a VBA project, it fails to recognize its version number and simply re-compiles its source(s) to VBA6 p-code—code which it understands and can begin to interpret.

The process works almost just as well in the opposite direction. When an Office 97 application opens an Office 2000 document containing a VBA6 project, it fails to recognize its version number, re-compiles its source(s) to VBA5 p-code and begins interpreting the latter. The process of locating the source code area is also guaranteed by Microsoft to remain constant in the versions of Office from 97 upwards.

When Office 97 was still in beta testing and the anti-virus producers were running scared because of the wildly different and horribly complicated (when compared to the previous versions of Office) macro-related structures, Microsoft suggested that we make our programs locate VBA viruses by locating the source code area of the VBA modules, decompressing it, and “scanning” it (whatever Microsoft thought that meant). As most of the time when Microsoft tries to tell the anti-virus producers how they ought to do their job, this seemed like a very bad idea at the time. After all, then we hadn’t even heard of the planned Office 2000 and how it was supposed to work and, of course, Microsoft failed to tell us why exactly we should look at the source, instead of at the stuff that really gets executed, the p-code.

We quickly determined that, as described at the beginning of this section, the source code was never used (remember, Office 2000 didn’t exist back then); that a VBA5 virus will work perfectly even if its source code is wiped (and that source code will be automatically re-created from the p-code in the replicants of the virus)—while it wouldn’t work if the p-code was wiped; that even the VBA Editor displayed what the p-code told it to, ignoring the source code completely. So, many anti-virus producers, including the author of this paper, dismissed the suggestion to scan the VBA source code area (instead of the p-code area) as the usual dumb anti-virus idea from Microsoft and promptly ignored it when designing their anti-virus programs.

Unfortunately, not all anti-virus producers did so. Some of them simply didn’t have the knowledge and/or resources to conduct themselves the extensive reverse-engineering needed to figure out how exactly to locate and cope with the p-code area (because, of course, Microsoft didn’t bother to tell us such “unnecessary” things—after all, they had already told us what we should do, according to them). Or maybe they had unhealthy faith in Microsoft’s competence as far as virus protection is concerned. Whatever the reason, the fact is that some anti-virus producers (including one of the major ones) implemented in their products Microsoft’s suggestion—instead of doing what the common sense dictated.

When Office 2000 appeared, those of us who had proceeded according to the “common sense” mentioned above were in for a nasty surprise. The surprise was not that its version of VBA used quite different p-code than the previous version of Office—that was, more or less, expected. The surprise was that our competitors who had followed Microsoft’s seemingly dumb suggestion were suddenly capable of detecting the VBA6 upconversions of the VBA5 viruses, without making any significant effort. The entries they had in their virus detection databases for the VBA5 viruses were also able to detect the VBA6 upconverts of these viruses—while, for the rest of us, these upconverts looked as different viruses (because of the different p-code) and required new entries in our databases. Furthermore, the creation of this new entry, if we had only the VBA5 virus, was far from trivial. Since we consider ourselves ethically bound (see [Bontchev98]) not to create anything that is a new virus (from our point of view), we would have had to resort to the effort– and time-consumming methods outlined in [Bontchev98]. So, for a change, it was we who looked stupid—not Microsoft.

Furthermore, since it was only fair to say that our problem was our own fault (for not listening to Microsoft) and that the VBA6 upconverts viruses were identical to their VBA5 originators at source code level (the only level at which Microsoft told us to look), it was decided to define these as the same viruses and say that those anti-virus producers who didn’t use the approach suggested by Microsoft should solve on their own the problems they might have with such a definition.

Surprisingly as it might look (or maybe not ), the author of this paper still believes that Microsoft’s suggestion was a bad idea. Despite any conveniences it brings, we maintain that it is plain dangerous to “identify” a virus by some area, the contents of which almost never has any impact whatsoever on the behavior of that virus. Furthermore, the VBA3 modules do not contain their own sources anywhere—they have only p-code areas, so the detection, recognition and identification of VBA3 viruses has to be performed according to the contents of the p-code area in the modules. Therefore, a better virus detection method was required for the Office 2000 macro viruses—a method which still used the p-code but which was able to treat the VBA6 upconvert of a VBA5 virus as exactly the same virus.

2.3 Processing Only the Opcodes

At a first glance, the VBA6 p-code looks quite similar to the VBA5 p-code. Some of the underlying structures (and sometimes—their location in the module) have changed somewhat, but the similarity is, nevertheless, significant. One of the major changes in VBA6 is the introduction of about a dozen new p-code instructions. Common sense dictates that if one is extending the instruction set of machine, even if it’s a virtual machine like the p-code stack machine used by VBA, one should put the opcodes of the new instructions either at the end of the existing opcode space (i.e., the new opcodes should be larger than any previously existing opcodes), or one should put them in the unused “gaps” of the opcode space (if, of course, such “gaps” exist).

Unfortunately, common sense seems to be somewhat on the lacking side at Microsoft. What Microsoft actually did was to insert the opcodes for the new instructions among the already existing opcodes, resulting in forward shifts of the majority of the already existing opcodes. So, as a whole and at a p-code level, the VBA6 upconversion of a VBA5 program looks quite different from the original.

Nevertheless, it is perfectly possible to come up with a translation (or a one-to-one mapping) between the opcodes of the equivalent p-code instructions in VBA5 and VBA6. Subsection 3.1.1 and Appendix A describe in details one such mapping. Several anti-virus producers (whose products were looking at the p-code area and not at the sources) opted for such on-the-fly translation of the opcodes only in their products. While this did allow them to handle automatically the VBA6 upconverts of the VBA5 viruses without additional efforts (and without additional entries in the virus detection databases of their scanners), this approach has, nevertheless, one major drawback.

The drawback comes from the fact that only the opcodes of the p-code instructions are largely insufficient to identify a VBA virus. Let us consider why this is the case.

The main reason for the problem comes from the way VBA stores the identifiers used by the different VBA modules in a VBA project. Instead of storing them within the respective module that uses them (as, ahem, common sense would suggest), Microsoft has decided to pool them together in a single place, keeping a single instance of each identifier. This place is the dir stream in VBA3 and the _VBA_PROJECT stream in VBA5 and above. We call this place the pool of identifiers.

So, for instance, if two VBA modules both use the identifier Foo, this identifier is not kept in each one of these modules. Instead, a single instance of it is present in the pool of identifiers. The p-code instructions of each of the two modules that need to reference this identifier simply contain a pointer as their operand. This pointer acts as an index in the pool of identifiers.

It is not clear to us why Microsoft has chosen such a design. The obvious reason—in order to reduce the space occupied by the VBA module(s)—seems unlikely, given how bloated the rest of Office is, how many unnecessary structures are kept in the modules, and how weak the compression method is, when it is used at all. Whatever the reason, this approach causes several problems to the producers of anti-virus software.

For instance, this approach causes the “variable letter case” problem described in [Bontchev97b]. Since only one instance of an identifier common for more than one VBA modules is kept, modifying the letter case of this identifier in one of the modules automatically results in the same modification in the other module.

But much more dreadful is the problem that, due to the approach described above, two completely different programs can look exactly the same at p-code opcode-only level.

For instance, the VBA5 line

x = x + 1

translates to the following p-code:

20 00 [xx xx] ; PUSH the variable “x” on the stack
A4 00 01 00 ; PUSH the integer constant 0x0001 on the stack
0B 00 ; POP the top two arguments of the stack, add
; them together, and PUSH the result back on
; the stack
27 00 [xx xx] ; POP the top argument of the stack and assign
; it to the variable “x”

(VBA is compiled to pseudo code—called p-code for short—for a virtual p-machine. It is a stack machine, this is why the compiled instructions look as if the original code is expressed in Reverse Polish Notation.)

Here, for instance, “20 00” is the opcode (0x0020) of the corresponding p-code instruction and “[xx xx]” is the operand of that instruction. Some p-code instructions (like the one with opcode 0x00A4 in the above example) take literal operands (0x0001 in the above example). However, those instructions that reference identifiers have operands which, as explained above, are essentially indexes to a common pool of identifiers elsewhere. And, since this pool of identifiers is common among the VBA modules, this means that adding a new module (or modifying an existing one) can (and, in practice often does) result in the “re-shuffling” of the pool of identifiers and in the modification of the operands of some p-code instructions (like the instruction with opcode 0x0020) in completely unrelated modules.

So, an anti-virus product which examines only the opcodes of the p-code instructions has to skip (ignore) the operands of some of them. In particular, if in the above example an anti-virus product wants to use as a scan string the p-code translation of the VBA line “x = x + 1” (this is an extremely poor scan string for anti-virus purposes and, besides, exclusive reliance on scan strings for virus identification is a bad idea, but this is just an example, OK?), it will have to use the wildcard scan string

20 00 ?? ?? A4 00 01 00 0B 00 27 00 ?? ??

Unfortunately, this very same scan string will also match the line

Foo = Bar + 1

because that line will translate to the following p-code:

20 00 [yy yy]
A4 00 01 00
0B 00
27 00 [zz zz]

Therefore, the anti-virus product will be likely to cause false positives. The situation could be improved somewhat by making sure that the wildcard characters in the scan string (“?? ??”) do not match just anything but one and the same thing in both cases (while still ignoring what exactly that thing is and just making sure that it is one and the same in both cases). However, even with such an improvement the anti-virus product could still cause false positives because its scan string will still match a line like

Snafu = Snafu + 1

The only way to solve this problem is to look not only at the opcodes of the p-code instructions but also to resolve their operands and examine the relevant parts of the structures they happen to refer to. Unfortunately, this is far from trivial.

3 Our Solution

After considering the alternative solutions of the problem outlined in Section 1, we have decided pick the best qualities of them, while avoiding their flaws. As a consequence, we have decided to adopt the following approach in our product—an approach which we wholeheartedly recommend to the other anti-virus producers.

First, we have decided to base our VBA virus identification on the p-code area; not on the source code area. After all, it only makes sense to concentrate on the areas of the virus which are actually having the major impact on its behavior almost all of the time.

Second, we have decided to construct a one-to-one mapping which would allow us to see the different up– and downconversions of a VBA virus as one and the same virus.

Third, we have decided to cover with this mapping not only the opcodes (for reasons explained in the previous section) but also the data structures referenced by the operands of the p-code instructions.

Fourth, we have decided to make our mapping function general enough to cover not only the VBA5 VBA6 up/downconversion but also the VBA3 VBA5 up/downconversion.

As a consequence, our method allows us to consider as “the same virus” all possible up– or downconversions of a VBA virus—and to detect and identify all its up to three forms (VBA3, VBA5 and VBA6) with a single entry in our macro virus detection database. We do this without any significant loss of the exactness of identification (unlike the method which relies only on opcode translation, see subsection 2.3). Essentially, we are performing on-the-fly conversion to some invariant form (which resembles mostly to the VBA5 form, although is not completely identical to it) of the VBA programs as we scan them.

In order to achieve this, we must perform on-the-fly “translation” of two main things in the VBA module—the opcodes of the p-code instructions and the data structures, referenced by the operands of the p-code instructions. In the next two sections we shall describe in details how to perform these two tasks.

3.1 Translating the Opcodes

In this subsection we shall cover the one-to-one mapping function that performs the opcode translation, as well as several pitfalls one should be aware of when implementing it.

3.1.1 Opcode Translation

The opcode of a p-code instruction in any version of VBA resides in the low 12 bits of a 16-bit word. (The high 4 bits of the same 16-bit word contain different opcode modifiers or flags.) Our translation function is called Translate and is listed in Appendix A. It takes two input parameters—the 16-bit word mentioned above (named token in our listing) and a parameter specifying whether this is a VBA3, VBA5 or VBA6 instruction (this parameter is called VBAVersion in our listing and can assume the values 3, 5 or 6). The return value of the function is the p-code instruction translated to some invariant form.

Most of the time, this form is identical to the VBA5 form of the instruction. That is, we have decided to use VBA5 as the basis of the opcode translation. (This decision is arbitrary and any of the other two forms could be used as a basis instead. For instance, the anti-virus product AVP from Kaspersky Labs uses VBA3 as a basis for its opcode translation—although it performs only opcode translation as described in subsection 2.3; not the full translation we are describing as our solution here.) There is only one exception—for the opcode 0x0044—and it is due to one of the quirks described in the next subsection.

The bulk of our function’s code is used by the VBA6 VBA5 translation. This is not because the process is incredibly complex but because the new opcodes have been added in such illogical places in the VBA6 instruction space. Not only have they been inserted in the middle of the existing old instructions (thus shifting some of the existing instructions’ opcodes forwards), but they haven’t even been inserted in a single, compact area. Instead, they have been scattered all over the instruction space—a couple of new instructions here, a couple of new instructions there—making the translation process annoying, if not particularly difficult. Our function does the logical thing, shifting the opcodes of the old instructions back to where they were in VBA5, and placing all the new p-code instructions at the end of the instruction space.

The basic VBA3 VBA5 translation is relatively simple. Almost all p-code instructions that are new for VBA5 (new, when compared to VBA3) have been put rather logically at the end of the existing code space. However, it wouldn’t be Microsoft’s way to proceed in an entirely logical manner—which is probably why one new instruction has been inserted in the middle of the existing instruction space—between the VBA3 instructions with opcodes 0x43 and 0x44, resulting in a shift of half of the opcodes forwards. Our translation function handles this by logically correcting the VBA3 opcodes to make them look as if they have a one-instruction gap after the instruction with opcode 0x43.

3.1.2 Opcode Translation Quirks

The remaining code in our opcode translation function is there in order to handle some VBA3 VBA5 and VBA5 VBA6 opcode upconversion quirks.

3.1.2.1 Internal Function Names

In VBA3 there are 7 opcodes which represent calls to some often-used VBA3 functions. They are listed in the following table:

VBA3 p-code instruction Corresponding VBA3 function Number of arguments VBA5 upconversion
75 00 CurDir() up to 1 24 00 4C 00 xx xx
76 00 Dir() up to 2 24 00 7E 00 xx xx
79 00 Error() up to 1 24 00 9A 00 xx xx
7A 00 Format 0 24 00 AE 00 00 00
7B 00 FreeFile() up to 1 24 00 B2 00 xx xx
83 00 Mid() up to 3 24 00 FC 00 xx xx
84 00 MidB() up to 3 24 00 00 01 xx xx

When any of these VBA3 instructions is upconverted to VBA5, they are replaced with the instruction with opcode 0x0024 and an operand representing an internal name (see subsection 3.2.1); just like any user-defined function. For instance, the line

Foo = CurDir(Bar)

compiles to the following p-code in VBA3

20 00 [xx xx] ; Reference the variable "Bar"
75 00 ; CurDir()
27 00 [yy yy] ; Assign the result to "Foo"

and to the following p-code in VBA5:

20 00 [xx’ xx’] ; Reference the variable "Bar"
24 00 4C 00 00 01 ; CurDir(), 1 argument
27 00 [yy’ yy’] ; Assign the result to "Foo"

where “[xx xx]” (and “[xx’ xx’]”) is a reference to the identifier “Foo”, “[yy yy]” (and “[yy’ yy’]”) is a reference to the identifier “Bar” in the common pool of identifiers, and “4C 00” is the representation of the internal name “CurDir”. The additional two bytes in the second instruction (“00 01”) represent the fact that the function takes one argument in this case. (The argument of this function is optional; if it was missing, the VBA3 p-code would have looked slightly differently and the VBA5 would have had “00 00” instead of “00 01”. See more about this in subsection 3.1.2.2.) While this quirk cannot be expressed in our simple opcode-for-opcode translation function, the complete implementation of the on-the-fly translation should bear it in mind and accommodate for it.

3.1.2.2 The Mysterious Case of the Disappearing P-Code Instructions

During the VBA3 VBA5 upconversion or during the VBA5 VBA3 downconversion, some p-code instructions do not up– or downconvert—instead, they simply disappear.

  1. Instructions disappearing during the VBA5 VBA3 downconversion. In VBA5, there are three p-code instructions used in the “For” and “With” control structures. The p-code translation of the control structure “For” always begins with the p-code instruction 0x00F7. The p-code translation of the control structure “With” always begins with the p-code instruction 0x00F9. Finally, the control variable in a “For” loop is denoted by using the p-code instruction 0x00F6 immediately after referring to it in the definition of the loop. When such VBA5 p-code is downconverted to VBA3, these p-code instructions simply disappear. This is why a correct implementation of the on-the-fly p-code translation should simply skip these p-code instructions. The following is an example, illustrating what happens. The VBA line

For A = 1 To 2

translates to the following p-code in VBA5:

F7 00 ; “For” prefix
20 00 [xx xx] ; Reference to the variable “A”
F6 00 ; Control variable marker
A4 00 01 00 ; Integer constant 1
A4 00 02 00 ; Integer constant 2
8B 00 ; For… To

This same VBA5 p-code downconverts to the following VBA3 p-code:

20 00 [yy yy] ; Reference to the variable “A”
A3 00 01 00 ; Integer constant 1
A3 00 02 00 ; Integer constant 2
8A 00 ; For… To

  1. Instructions disappearing during the VBA3 VBA5 upconversion. Six of the seven instructions listed in the table in subsection 3.1.2.1 have optional arguments (the seventh, Format, has no arguments). This means that one or two of their arguments can be missing when the function is invoked. (Two of these functions have up to three arguments—but at least one of these three arguments must be present, so again at most two arguments can be missing.) In VBA3, the p-code instruction 0x00A3 is used to indicate a missing first argument and the p-code instruction 0x04A3 is used to indicate a missing second argument (if both arguments are missing, both p-code instructions will be used). In the later versions of VBA, however, there are no such instructions—in fact, as we saw in subsection 3.1.2.1, there are no special internal function names. Instead, these are regular functions, addressed by the p-code instruction with the opcode 0x0024. This p-code instruction takes an operand which specifies the number of arguments in the function, so there is no need for a “missing argument” opcode. Here is an example illustrating what happens. As we saw in subsection 3.1.2.1, the VBA line

Foo = CurDir(Bar)

compiles to the following p-code in VBA3

20 00 [xx xx] ; Reference to the variable “Bar”
75 00 ; CurDir()
27 00 [yy yy] ; Assignment to the variable “Foo”

and to the following p-code in VBA5:

20 00 [XX XX] ; Reference to the variable “Bar”
24 00 4C 00 00 01 ; CurDir(), 1 argument
27 00 [YY YY] ; Assignment to the variable “Foo”

However, if the original VBA line is just

Foo = CurDir()

it would translate, instead, to the following p-code in VBA3:

A2 00 ; Missing first (and only) argument
75 00 ; CurDir()
27 00 [yy yy] ; Assignment to the variable “Foo”

which would upconvert to the following VBA5 p-code:

24 00 4C 00 00 00 ; CurDir(), 0 arguments
27 00 [YY YY] ; Assignment to the variable “Foo”

Again, a correct implementation of the on-the-fly translation function should take this peculiarity into account and produce invariant results in all (VBA3, VBA5 and VBA6) cases.

3.1.2.3 The “Me” Reference

The special VBA object “Me” is referred to in two different ways in VBA3 and in the later VBA versions. In either case, it is not present in the common pool of identifiers. In VBA3 the reference 0xFFFF is used to address it (in the operand of the p-code instructions), while in the later versions of VBA the internal name 0x00FA is used (see subsection 3.2.1 for more information about the internal names).

3.1.2.4 Internal Function Calls

The other quirk comes from the fact that the VBA5 p-code instruction with opcode 0x44 (internal function call) is sometimes translated to the instruction with opcode 0x24 (regular function call) when upconverted to VBA6 or downconverted to VBA3.

3.2 Translating the Data Structures

As mentioned in Section 2.3, the opcode translation is relatively simple and straightforward—but far from sufficient. In addition to it, we must also translate the data structures referred to by the operands of the different p-code instructions—and this translation is far from simple.

Our task is further complicated by the fact that, while our knowledge of the formats of these data structures is the result of reverse-engineering, the location of the areas where these data structures reside in the module stream relies on information we have obtained from Microsoft under a non-disclosure agreement and are, therefore, not free to publish. So, regretfully, in this subsection we shall assume that the reader knows how to locate these areas and will only describe their formats and the translation we perform on them. If the reader does not know how to locate them, s/he is encouraged to request this information from Microsoft. We can only wish luck and advise patience in this quest—most likely, both of them will be needed.

3.2.1 Translating the Internal Names

As we saw in subsection 3.1.2.1, in VBA3 some function identifiers are not stored in the common pool of identifiers and are not referred to by a normal (variable) identifier index. Instead, special constants are used to denote them.

This idea is taken even further in VBA5. About 250 “often used” identifiers are not stored in the common pool of identifiers but are denoted, instead, with special constants. The table in Appendix B provides a complete list of them. (The selection of some of them cannot cause but puzzlement—why has the identifier “B”, for instance, been deemed more “often-used” than any other single-letter identifier—e.g., “I”? Ah, well, Microsoft. ) We call these “special” identifiers “internal identifiers”.

If a VBA5 or higher p-code instruction takes an operand which refers to some identifier, this operand is larger than 0x0200, if it is an index to the common pool of identifiers—and smaller than 0x0200, if it is an “internal” name.

Unfortunately, these “internal” identifiers aren’t quite the same in VBA6. A few new ones have been added—but instead of placing them in the “gaps” of the existing table, they have replaced some of the existing identifiers, causing the replaced identifiers to be shifted around. Sometimes the folks at Microsoft seem to be going out of their ways to create annoyances for the anti-virus people.

In order to handle this case, we use a function called TranslateID. This function is invoked only for the internal identifiers (i.e., with a reference smaller than 0x0200) and only when processing VBA6 p-code. It returns the translated invariant form of the respective internal identifier, which form is very close to the VBA5 form of this identifier. The code of this function is also provided in Appendix B.

These internal identifiers provide additional annoynce when VBA5 (or VBA6) p-code is downconverted to VBA3. Since VBA3 they are not present in the common pool of identifiers, a correct implementation of the on-the-fly translation has to use a table of literal strings corresponding to the names of these special identifiers and use the strings from this table each time when a special identifier is enountered in the examined VBA5 (or VBA6) p-code. In other words, we have to make it look as if there is nothing special with these identifiers and we have normal references to the common pool of identifiers.

3.2.2 The Line Definition Table

The p-code area in the VBA module stream is preceded by the so-called Line Definition Table. This is a structure beginning with a 16-bit identifier (which is always 0xCAFE—Microsoft humor), followed by a 16-bit version number, followed by a 16-bit number containing the number of lines in the module (including the empty lines). Finally, there is an array of 12-byte records, one record per line. Each of these records has the following format in the different versions of VBA:

Line Definition Record format in VBA3:

struct
{
UINT32 Flags; // flags
UINT8 NumLines // physical lines in this logical line
UINT8 Indent // spaces this line is indented
UINT16 wLineLength; // bytes of opcodes in the line
UINT16 wExcodeLength; // bytes of excodes in the line
UINT16 LineStart; // index to the actual opcode data
};

Line Definition Record format in VBA5 and VBA6:

struct
{
UINT32 Flags:19; // flags
UINT32 NumLines:5; // physical lines in this logical line
UINT32 Indent:8; // spaces this line is indented
UINT16 wLineLength; // bytes of opcodes in the line
UINT16 wExcodeLength; // bytes of excodes in the line
UINT32 LineStart; // index to the actual opcode data
};

The Line Definition Table is important for the processing of the p-code area for two reasons.

First, the p-code representation of the different VBA lines is not necessarily in the order in which they appear in the VBA module. Usually, it is in the order in which they have been entered by the user. For instance, the p-code representation of the 2nd line could appear in the p-code area after the p-code representation of the 3rd line and so on. However, the records of the Line Definition Table are always in the correct (increasing) order and one can use their LineStart field to locate the correct representation of each consecutive line in the p-code area.

Second, the p-code representation of each VBA5 or VBA6 line always occupies a number of bytes which is a multiple of 8. If the actual VBA line compiles to p-code, the length of which is not a multiple of 8, it is padded with random garbage, so that its length becomes a multiple of 8. Therefore, the records in the Line Definition Table are necessary in order to determine what actual length of the p-code representation of each VBA line—that is, minus the eventually present padding garbage. This length is indicated by the wLineLength field of each corresponding record in the Line Definition Table.

3.2.3 The External Declarations Table

Each externally declared function (with the Declare keyword) in VBA5 and VBA6 results in an entry in the first of tables in the VBA module stream. This table is referred to as DLLENTRY_DEFN in the documentation which Microsoft distributes under a non-disclosure agreement. (The reader should refer to this documentation for information about locating this table.) According to our reverse-engineering, the records of this table have the following format:

struct
{
UINT16 wHasOrdinal;
UINT16 wDllName;
union
{
UINT16 wDllEntry;
UINT16 wDllOrdinal;
};
UINT16 wParamSize;
UINT16 wDllEntryDefnNext;
UINT32 dwDllEntryPoint;
UINT32 dwLibrary;
};

The field wDllName is a reference to the common pool of identifiers and, normally, the corresponding identifier should be included in the identification algorithm. However, there is no Declare operator in VBA3—and our goal is to achieve a uniform VBA3/VBA5/VBA6 translation algorithm. Therefore, we suggest that the references to the External Declarations Table are simply ignored; we consider the resulting loss of the exactness of the identification to be insignificant.

3.2.4 The Object Table

The second table in the module stream is the so-called Object Table. It is referred to as the “unbound import description array” in the documentation Microsoft gives under a non-disclosure agreement and the exact method of its location can be found in that documentation.

Only one p-code instruction has an operand which refers to this table. This is the p-code instruction with opcode 0x00BE in VBA5. It is generated when compiling the VBA operator New. The operand of this instruction is an offset from the beginning of the Object Table. The table itself is an array of records with the following format:

struct
{
UINT16 wRefs;
union
{
struct
{
UINT16 refkind:2;
UINT16 isDeclRef:1;
UINT16 isExcodeRef:1;
UINT16 depkind:3;
UINT16 isInternalRef:1;
UINT16 isSelfFree:1;
UINT16 isBtinfo:1;
UINT16 tkind:4;
UINT16 isDual:1;
UINT16 isCreatable:1;
};
UINT16 wInitialize;
};
UINT16 wTypeNext;
union
{
UINT16 m_hlnam;
UINT16 m_hrghlnam;
};
UINT16 wSwitchedOut;
};

If the lowest bit of the field m_refkind in a record is set, the m_hrghlnam field of the same record is an offset from the beginning of another area in the module stream. The first 16-bit word at that offset is a number indicating how many 16-bit symbolic references follow (usually only one). Each such symbolic reference is just a reference to the common pool of identifiers. The corresponding identifier should be included in the algorithm which tries to identify the VBA program in the module.

3.2.5 The Indirect Table

The majority of the important data structures used by the p-code of a VBA module are stored in a single table in the same module. We call it “the Indirect Table”, because it provides a kind of indirect addressing of the different fields used by the p-code instructions. Microsoft uses the term “array of type definitions” in the documentation it distributes under a non-disclosure agreement.

This documentation provides sufficient information for locating the indirect table in the module stream (information which we, regretfully, cannot provide here, because it is covered by the NDA). However, it doesn’t provide any information about the format of the structures used in this table. We have determined the most important formats by the means of reverse-engineering and will share our knowledge on this matter with the reader in the next few subsections.

The Indirect Table is a horrible mess. It looks as if somebody has constructed several linked lists of records of different types and has dumped the memory containing these lists into a file. The elements of the different lists are often intermixed and most records have a field which indicates where the next record of the same type is, so the lists can be traversed element-by-element.

The size of each element in each record is important. It would be a mistake for the anti-virus product to attempt to read the complete record as a whole; as a sequence of bytes. The reason for this is because the VBA structures used by Mac Office 98 have exactly the same formats as the VBA5 structures in Office 97—but the multi-byte records in them have a big-endian order of their bytes. Therefore, if the anti-virus product wants to have its invariant translation function handle not only VBA modules from Office 95, Office 97 and Office 2000—but also from MacOffice 98, it has to read the records field-by-field and perform any necessary big endian to little endian byte swapping. (This is not important for the earlier versions of MacOffice, because the only kind of VBA used in them is VBA3 and the structures in it are always in little-endian format.)

3.2.5.1 The Subroutine Definition Structure

This structure is pointed at by the operand of the p-code instruction for defining subroutines, functions and properties—0x008F. (From now on we shall use the opcodes of the p-code instructions, as already translated by the function Translate in Appendix A.) The operand of this instruction is a 16-bit word in VBA3 and a 32-bit word in VBA5 and VBA6. This word contains an offset from the beginning of the Indirect Table. At that offset in the table resides the subroutine definition record for the subroutine, function or property definition that has generated this p-code instruction.

The subroutine definition record has the following structure in VBA3:

struct
{
UINT16 wFlags;
UINT16 NextOffset;
UINT16 wSymbol;
UINT16 wUnknown0;
UINT16 wUnknown1;
UINT16 wUnknown2;
UINT16 wUnknown3;
UINT16 ArgumentOffset;
UINT16 wUnknown4;
UINT16 ReturnType;
UINT16 wUnknown5;
UINT16 wUnknown6;
UINT16 wUnknown7;
UINT16 wUnknown8;
UINT16 wUnknown9;
UINT16 wUnknown10;
}

It has the following structure in VBA5:

struct
{
UINT8 bFlags1;
UINT8 bFlags2;
UINT16 wSymbol;
UINT32 NextOffset;
UINT32 dwUnknown0;
UINT32 dwUnknown1;
UINT32 dwUnknown2;
UINT32 dwUnknown3;
UINT32 dwUnknown4;
UINT32 dwUnknown5;
UINT32 dwUnknown6;
UINT32 ArgumentOffset;
UINT32 ReturnType;
UINT16 wDeclare;
UINT16 wUnknown7;
UINT16 wUnknown8;
UINT16 wUnknown9;
UINT16 wUnknown10;
UINT8 cOptions;
UINT16 wArgumentCount;
UINT16 wUnknown12;
UINT8 cUnknown13;
UINT16 wUnknown14;
}

And, finally, it has the following (slightly different) structure in VBA6:

struct
{
UINT8 bFlags1;
UINT8 bFlags2;
UINT16 wSymbol;
UINT32 NextOffset;
UINT32 dwUnknown0;
UINT32 dwUnknown1;
UINT32 dwUnknown2;
UINT32 dwUnknown3;
UINT32 dwUnknown4;
UINT32 dwUnknown5;
UINT32 dwUnknown6;
UINT32 dwUnknown6a;
UINT32 ArgumentOffset;
UINT32 ReturnType;
UINT16 wDeclare;
UINT16 wUnknown7;
UINT16 wUnknown8;
UINT16 wUnknown9;
UINT16 wUnknown10;
UINT8 cOptions;
UINT16 wArgumentCount;
UINT16 wUnknown12;
UINT8 cUnknown13;
}

The meaning of the most important fields is as follows:

  • bFlags2. This seems to be a set of bit flags. Bit 15 is set if this subroutine is declared as Static, bit 13 is set if the type of the function is specified explicitly with the keyword “As”, bit 11 is set or not depending on whether the subroutine is declared Public or Private and bits 4–7 are set depending on whether this is a subroutine or function definition (bit 4), a Property Get definition (bit 5), a Property Let definition (bit 6) or a Property Set definition (bit 7). The meaning of the other bits is unknown.

  • wSymbol. This is a normal reference to the common pool of identifiers for the name of the subroutine, function or property defined by this record.

  • NextOffset. This is the offset, from the beginning of the Indirect Table, of the next subroutine definition record—or -1, if this is the last such record on the list. Note that this field has different sizes in VBA3 and in the later versions of VBA.

  • ArgumentOffset. This is the offset, from the beginning of the Indirect Table, of the list of records defining the arguments of the currently defined subroutine, function or property (see section 3.2.5.2)—or -1, if the there are no arguments. Note that this field has different sizes in VBA3 and in the later versions of VBA.

  • ReturnType. In VBA5 and above, if this record defines a subroutine, this field contains -1. Otherwise, if its high half contains -1, its low half contains the return type of the function (0—default, 1—Null, 2—Integer, 3—Long, 4—Single, 5—Double, 6—Currency, 7—Date, 8—String, 9—Object, 10—Error, 11—Boolean, 12—Variant, 14—Decimal, 17—Byte; some of these are not available in VBA3). If the high half of this field does not contain -1, the whole field is an index to an Object Definition record (see subsection 3.2.5.6) in the Indirect Table and the object defined there is the return type of the function. In VBA3 the field contains directly the value indicating the return type. Note that this field has different sizes in VBA3 and in the later versions of VBA.

  • wDeclare. Index in the External Declarations Table, if this is an externally declared subroutine or function—or -1 otherwise.

  • wArgumentCount. Number of arguments of this subroutine or function.

It is recommended that the VBA virus identification algorithm identifies at least the contents of wSymbol and ReturnType and processes the corresponding records of every argument of the function (see the next section).

3.2.5.2 The Subroutine Argument Definition Structure

The records defining each of the arguments of a function or subroutine are stored in the Indirect Table too—in linked lists; a different chain of records for every function or subroutine definition.

Each record has the following structure in VBA3:

struct
{
UINT16 wFlags;
UINT16 wUnknown0;
UINT16 wSymbol;
UINT16 wUnknown1;
UINT16 ArgType;
UINT16 wUnknown2;
UINT16 NextOffset;
UINT16 wUnknown3;
}

It has the following structure in VBA5 and VBA6:

struct
{
UINT8 bFlags1;
UINT8 bFlags2;
UINT16 wSymbol;
UINT32 dwUnknown0;
UINT32 dwUnknown1;
UINT32 ArgType;
UINT32 dwUnknown2;
UINT32 NextOffset;
UINT8 bOptions1;
UINT8 bOptions2;
UINT16 wUnknown3;
UINT32 dwUnknown4;
}

The meaning of some of the fields is:

  • bFlags2. This field contains bit flags. Bit 14 is set if the argument is an array. Bit 13 is set if the type of the argument is specified explicitly with the keyword “As”. The meaning of the other bits is unknown.

  • wSymbol. This is a normal reference to the common pool of identifiers for the name of the argument.

  • ArgType. Type of the argument. If its high half contains -1, its low half contains the type of the current argument of the current function or subroutine (0—default, 1—Null, 2—Integer, 3—Long, 4—Single, 5—Double, 6—Currency, 7—Date, 8—String, 9—Object, 10—Error, 11—Boolean, 12—Variant, 14—Decimal, 17—Byte; some of these are not available in VBA3). If the high half of this field does not contain -1, the whole field is an index to an Object Definition record (see section 3.2.5.6) in the Indirect Table and the object defined there is the type of the argument. In VBA3 the field contains directly the value indicating the type of the argument. Note that this field has different sizes in VBA3 and in the later versions of VBA.

  • NextOffset. This is the offset, from the beginning of the Indirect Table, of the argument definition record of the next argument of the current function or subroutine—or -1, if the current record has defined the last argument. Note that this field has different sizes in VBA3 and in the later versions of VBA.

  • bOptions2. This field contains bit flags. Bit 9 is set if this argument is declared Optional. Bit 2 is set if this argument is declared to be passed ByVal. Bit 1 is set if this argument is declared to be passed ByRef. The meaning of the other bits is unknown.

It is recommended that the VBA virus identification algorithm identifies at least the contents of wSymbol.

3.2.5.3 The Dim Definition Structure

This structure is pointed at by the operand of the p-code instruction for explicit variable declaration (with the VBA keyword Dim)—0x00E9. The operand of this instruction is a 16-bit word in VBA3 and a 32-bit word in VBA5 and VBA6. This word contains an offset from the beginning of the Indirect Table. At that offset in the table resides the definition record of the variable that is being defined.

This record has the following structure in VBA3:

struct
{
UINT16 wFlags;
UINT16 wUnknown0;
UINT16 wSymbol;
UINT16 wUnknown1;
UINT16 ArrayOffset;
UINT16 wUnknown2;
}

It has the following structure in VBA5 and VBA6:

struct
{
UINT8 wFlags1;
UINT8 wFlags2;
UINT16 wSymbol;
UINT32 dwNextOffset;
UINT32 dwUnknown0;
UINT32 ArrayOffset;
}

The meaning of some of the fields is:

  • bFlags2. This field contains bit flags. Bit 13 is set if the type of the declared variable is specified explicitly with the keyword “As”. Bit 5 is set if the Dim declaration creates a new object via the VBA keyword New. The meaning of the other bits is unknown.

  • wSymbol. This is a normal reference to the common pool of identifiers for the name of the type.

  • dwNextOffset. This is the offset, from the beginning of the Indirect Table, of the next variable declaration record—or -1, if the there are no more such records.

  • ArrayOffset. This is the offset, from the beginning of the Indirect Table, of the Array Definition record (see subsection 3.2.5.4), if this is a variable declaration record of an array variable, or -1 otherwise. Note that this field has different sizes in VBA3 and in the later versions of VBA.

It is recommended that the VBA virus identification algorithm identifies at least the contents of wSymbol.

3.2.5.4 The Array Definition Structure

This structure is pointed at by the field ArrayOffset in the Dim Definition structure described in section 3.2.5.3. Also, the third operand of the p-code instructions 0x00D9 and 0x00DA (generated by the VBA operator ReDim) is an index (16-bit in VBA3 and 32-bit in VBA5 and VBA6) from the beginning of the Indirect Table to a record of this kind.

The record has the following structure in VBA3:

struct
{
UINT16 wUnknown0;
UINT16 ArrayDimensionsOffset;
UINT16 Type;
UINT16 wUnknown1;
}

In VBA5 and VBA6, it has the following structure:

struct
{
UINT8 wFlags1;
UINT8 wFlags2;
UINT32 ArrayDimensionsOffset;
UINT8 Type;
UINT8 bUnknown1;
}

The meaning of some of the fields is:

  • Type. Type of the elements of the array (0—default, 1—Null, 2—Integer, 3—Long, 4—Single, 5—Double, 6—Currency, 7—Date, 8—String, 9—Object, 10—Error, 11—Boolean, 12—Variant, 14—Decimal, 17—Byte; some of these are not available in VBA3). Note that this field has different sizes in VBA3 and in the later versions of VBA. In VBA5 and later, if this field contains 0x1D, the record is followed immediately by an Object Definition record (see subsection 3.2.5.6).

  • ArrayDimensionsOffset. This is the offset, from the beginning of the Indirect Table, of the data structures describing the dimensions of the array (see subsection 3.2.5.5). Note that this field has different sizes in VBA3 and in the later versions of VBA.

The format of the structures used for array definition (described in this and in the next subsections) is too different between VBA3 and the higher VBA versions. It would be too difficult to perform on-the-fly translation from one of the formats to the other. Besides, it is probably not worth the effort, because the structures can be simply ignored without a significant loss of the exactness of identification.

3.2.5.5 The Array Dimension Definition Structure

This structure, too, resides in the Indirect Table. It is pointed at by the field ArrayDimensionsOffset in the Array Definition structure described in subsection 3.2.5.4. It has the following format in VBA3:

struct
{
UINT16 DimensionCount;
UINT16 wUnknown0;
UINT16 Type;
UINT16 wUnknown1;
}

In VBA5 and VBA6, the structure has the following format:

struct
{
UINT8 DimensionCount;
UINT8 bUnknown0;
UINT32 dwUnknown1;
UINT8 Type;
UINT8 bUnknown2;
}

In all versions of VBA, this structure is immediately followed by DimensionCount records. Each record is 8 bytes large and, supposedly, contains information about the limits of each dimension of the array. Unfortunately, the exact format of these records is not known.

3.2.5.6 Object Definition Structure

This structure is used only in VBA5 and VBA6. It is not referred to directly by any of the operands of the p-code instructions. Instead, it is referred to occasionally by some of the fields in other structures (e.g., the Array Dimensions definition record). This structure, too, resides in the Indirect Table. Its format looks like this:

struct
{
UINT8 wFlags1;
UINT8 wFlags2;
UINT16 wObject;
UINT16 wUnknown0;
UINT16 wUnknown1;
}

The field wObject is an index from the beginning of the Object Definition Table (see subsection 3.2.4) of the record, containing the necessary information for the object used in this definition.

3.2.5.7 The Type Definition Structure

This structure is pointed at by the operand of the p-code instruction for defining types—0x00E8. The operand of this instruction is a 16-bit word in VBA3 and a 32-bit word in VBA5 and VBA6. This word contains an offset from the beginning of the Indirect Table. At that offset in the table resides the type definition record of the type that is being defined.

This record has the following structure in VBA3:

struct
{
UINT16 wFlags;
UINT16 wUnknown0;
UINT16 wSymbol;
UINT16 wUnknown1;
UINT16 wUnknown2;
UINT16 wOptions;
}

It has the following structure in VBA5 and VBA6:

struct
{
UINT8 wFlags1;
UINT8 wFlags2;
UINT16 wSymbol;
UINT32 dwUnknown0;
UINT32 dwUnknown1;
UINT32 dwUnknown2;
UINT16 wUnknown3;
UINT16 wOptions;
UINT16 wUnknown4;
UINT16 wUnknown5;
UINT32 dwUnknown6;
UINT32 dwUnknown7;
}

The meaning of some of the fields is:

  • wSymbol. This is a normal reference to the common pool of identifiers for the name of the type.

  • wOptions. This field contains bit flags. Bit 0 is set to 1 if this is a Public type definition and to 0 otherwise. The meaning of the other bits is unknown.

It is recommended that the VBA virus identification algorithm identifies at least the contents of wSymbol.

4 Conclusion

The method described in this paper is relatively easy to implement. It allows a scanner to have uniform handling of all possible up– and downconversions of a VBA virus without any significant loss of exactness of identification. It is strongly recommended that the anti-virus producers adopt this approach. The method is easily extensible for the future versions of VBA.

It is not quite clear to the author of this paper whether the implementation of a similar translation function for the WordBasic VBA5 upconversion case is practically possible. If it is, it would result in the complete solution of the macro virus upconversion problem. However, having in mind that Office 95 is rarely used nowadays and that the WordMacro viruses (i.e., the macro viruses written in WordBasic) have all but disappeared, the point is probably moot.

Appendix A. The Opcode Translation Function

The following function takes two arguments—a 16-bit p-code token (opcode plus modifiers) and another argument, indicating the VBA version number. It returns the translated p-code token. This translated p-code token is invariant across the different VBA versions.

UINT16 Translate (UINT16 token, int VBAVersion)
{
UINT16 opcode;

opcode = token & 0x3FF;
if (VBAVersion == 3) // VBA3
{
if (opcode == 0x75 || opcode == 0x76 ||
opcode == 0x79 || opcode == 0x7A ||
opcode == 0x7B || opcode == 0x83 ||
opcode == 0x84)
opcode = 0x24;
if (opcode >= 0x44)
opcode++;
}
else if (VBAVersion == 5) // VBA5
{
if (opcode == 0x44)
opcode = 0x24;
}
else if (VBAVersion == 6) // VBA6
{
if (opcode > 0x44)
{
opcode––;
if (opcode < 0x45)
{
opcode = 0xFC;
goto quit;
}
}
if (opcode > 0x47)
{
opcode –= 2;
if (opcode == 0x46)
{
opcode = 0xFD;
goto quit;
}
if (opcode == 0x47)
{
opcode = 0xFE;
goto quit;
}
}
if (opcode > 0x70)
{
opcode –= 4;
if (opcode == 0x6D)
{
opcode = 0xFF;
goto quit;
}
if (opcode == 0x6E)
{
opcode = 0x100;
goto quit;
}
if (opcode == 0x6F)
{
opcode = 0x101;
goto quit;
}
if (opcode == 0x70)
{
opcode = 0x102;
goto quit;
}
}
if (opcode > 0x97)
{
opcode––;
if (opcode < 0x98)
opcode = 0x103;
}
}
else
{
// Error—neither VBA3 nor VBA5 nor VBA6
}
quit:
token &= 0xFC00;
token |= opcode;
return token;
}

Appendix B. Internal Identifiers

The following table lists the special internal identifiers in VBA5 and the names they correspond to.

0x0004 ABS
0x0006 ACCESS
0x0008 ADDRESSOF
0x000A ALIAS
0x000C AND
0x000E ANY
0x0010 APPEND
0x0012 ARRAY
0x0014 AS
0x0016 ASSERT
0x0018 B
0x001A BASE
0x001C BF
0x001E BINARY
0x0020 BOOLEAN
0x0022 BYREF
0x0024 BYTE
0x0026 BYVAL
0x0028 CALL
0x002A CASE
0x002C CBOOL
0x002E CBYTE
0x0030 CCUR
0x0032 CDATE
0x0034 CDEC
0x0036 CDBL
0x0038 CDECL
0x003A CHDIR
0x003C CINT
0x003E CIRCLE
0x0040 CLNG
0x0042 CLOSE
0x0044 COMPARE
0x0046 CONST
0x0048 CSNG
0x004A CSTR
0x004C CURDIR
0x004E CURDIR$
0x0050 CVAR
0x0052 CVDATE
0x0054 CVERR
0x0056 CURRENCY
0x0058 DATABASE
0x005A DATE
0x005C DATE$
0x005E DEBUG
0x0060 DECIMAL
0x0062 DECLARE
0x0064 DEFBOOL
0x0066 DEFBYTE
0x0068 DEFCUR
0x006A DEFDATE
0x006C DEFDEC
0x006E DEFDBL
0x0070 DEFINT
0x0072 DEFLNG
0x0074 DEFOBJ
0x0076 DEFSNG
0x0078 DEFSTR
0x007A DEFVAR
0x007C DIM
0x007E DIR
0x0080 DIR$
0x0082 DO
0x0084 DOEVENTS
0x0086 DOUBLE
0x0088 EACH
0x008A ELSE
0x008C ELSEIF
0x008E EMPTY
0x0090 END
0x0092 ENDIF
0x0094 ENUM
0x0096 EQV
0x0098 ERASE
0x009A ERROR
0x009C ERROR$
0x009E EVENT
0x00A0 WITHEVENTS
0x00A2 EXIT
0x00A4 EXPLICIT
0x00A6 F
0x00A8 FALSE
0x00AA FIX
0x00AC FOR
0x00AE FORMAT
0x00B0 FORMAT$
0x00B2 FREEFILE
0x00B4 FRIEND
0x00B6 FUNCTION
0x00B8 GET
0x00BA GLOBAL
0x00BC GO
0x00BE GOSUB
0x00C0 GOTO
0x00C2 IF
0x00C4 IMP
0x00C6 IMPLEMENTS
0x00C8 IN
0x00CA INPUT
0x00CC INPUT$
0x00CE INPUTB
0x00D0 INPUTB
0x00D2 INSTR
0x00D4 INPUTB$
0x00D6 INT
0x00D8 INSTRB
0x00DA IS
0x00DC INTEGER
0x00DE LEFT
0x00E0 LBOUND
0x00E2 LENB
0x00E4 LEN
0x00E6 LIB
0x00E8 LET
0x00EA LINE
0x00EC LIKE
0x00EE LOAD
0x00F0 LOCAL
0x00F2 LOCK
0x00F4 LONG
0x00F6 LOOP
0x00F8 LSET
0x00FA ME
0x00FC MID
0x00FE MID$
0x0100 MIDB
0x0102 MIDB$
0x0104 MOD
0x0106 MODULE
0x0108 NAME
0x010A NEW
0x010C NEXT
0x010E NOT
0x0110 NOTHING
0x0112 NULL
0x0114 OBJECT
0x0116 ON
0x0118 OPEN
0x011A OPTION
0x011C OPTIONAL
0x011E OR
0x0120 OUTPUT
0x0122 PARAMARRAY
0x0124 PRESERVE
0x0126 PRINT
0x0128 PRIVATE
0x012A PROPERTY
0x012C PSET
0x012E PUBLIC
0x0130 PUT
0x0132 RAISEEVENT
0x0134 RANDOM
0x0136 RANDOMIZE
0x0138 READ
0x013A REDIM
0x013C REM
0x013E RESUME
0x0140 RETURN
0x0142 RGB
0x0144 RSET
0x0146 SCALE
0x0148 SEEK
0x014A SELECT
0x014C SET
0x014E SGN
0x0150 SHARED
0x0152 SINGLE
0x0154 SPC
0x0156 STATIC
0x0158 STEP
0x015A STOP
0x015C STRCOMP
0x015E STRING
0x0160 STRING$
0x0162 SUB
0x0164 TAB
0x0166 TEXT
0x0168 THEN
0x016A TO
0x016C TRUE
0x016E TYPE
0x0170 TYPEOF
0x0172 UBOUND
0x0174 UNLOAD
0x0176 UNLOCK
0x0178 UNKNOWN
0x017A UNTIL
0x017C VARIANT
0x017E WEND
0x0180 WHILE
0x0182 WIDTH
0x0184 WITH
0x0186 WRITE
0x0188 XOR
0x018A #CONST
0x018C #ELSE
0x018E #ELSEIF
0x0190 #END
0x0192 #IF
0x0194 ATTRIBUTE
0x0196 VB_BASE
0x0198 VB_CONTROL
0x019A VB_CREATABLE
0x019C VB_CUSTOMIZABLE
0x019E VB_DESCRIPTION
0x01A0 VB_EXPOSED
0x01A2 VB_EXT_KEY
0x01A4 VB_HELPID
0x01A6 VB_INVOKE_FUNC
0x01A8 VB_INVOKE_PROPERTY
0x01AA VB_INVOKE_PROPERTYPUT
0x01AC VB_INVOKE_PROPERTYPUTREF
0x01AE VB_MEMBERFLAGS
0x01B0 VB_NAME
0x01B2 VB_PREDECLAREDID
0x01B4 VB_PROCDATA
0x01B6 VB_TEMPLATEDERIVED
0x01B8 VB_VARDESCRIPTION
0x01BA VB_VARHELPID
0x01BC VB_VARMEMBERFLAGS
0x01BE VB_VARPROCDATA
0x01C0 VB_USERMEMID
0x01C2 VB_VARUSERMEMID
0x01C4 VB_GLOBALNAMESPACE
0x01C6 ,
0x01C8 .
0x01CA \
0x01CC _
0x01CE !
0x01D0 #
0x01D2 &
0x01D4 '
0x01D6 (
0x01D8 )
0x01DA *
0x01DC +
0x01DE –
0x01E0 /
0x01E2 :
0x01E4 ;
0x01E6 <
0x01E8 <=
0x01EA <>
0x01EC =
0x01EE =<
0x01F0 =>
0x01F2 >
0x01F4 ><
0x01F6 >=
0x01F8 ?
0x01FA \\
0x01FC ^
0x01FE :=

For instance, the VBA line

BF = B + F

translates to the following VBA5 p-code:

20 00 18 00 ; Reference to the variable “B”
20 00 A6 00 ; Reference to the variable “F”
0B 00 ; +
27 00 1C 00 ; Assignment to the variable “BF”

Please note that, although many of the identifiers in the second column of the above table look like VBA keywords, the corresponding constant in the first column is not used instead of this VBA keyword.

The following function takes a VBA6 internal identifier and returns the translated (invariant) internal identifier which can be used to look up the name of the identifier in the above table both in VBA5 and VBA6.

UINT16 TranslateID (UINT16 IntID)

switch (IntID)

case 0x00CE:
IntID = 0x00D2;
break;
case 0x00D2:
IntID = 0x00D6;
break;
case 0x00D6:
IntID = 0x00DA;
break;
case 0x00DA:
IntID = 0x00DE;
break;
case 0x00DE:
IntID = 0x00E2;
break;
case 0x00E2:
IntID = 0x00E6;
break;
case 0x00E6:
IntID = 0x00EA;
break;
case 0x00EA:
IntID = 0x00CE;
break;
case 0x0184:
IntID = 0x00A0;
break;
}
if (IntID >= 0x00A2 && IntID <= 0x0182)
IntID += 2;
return IntID;
}

5 References

  • [Bontchev97a] Vesselin Bontchev, “The Wrong Way”, Virus Bull., Oct. 1997, pp. 14-17.
  • [Bontchev97b] Vesselin Bontchev, “Macro Virus Identification Problems”, Proc. 7th Virus Bull. Conf., 1997, San Francisco, pp. 175-196.
  • [Bontchev98] Vesselin Bontchev, “The ‘Pros’ and ‘Cons’ of Macro Virus Upconversion”, Proc. 8th Int. Virus Bull. Conf, 1998, München, Germany, pp. 153-172.
  • [FitzGerald] Nick FitzGerald, “Do you Know the Way to VBA?”, Virus Bull., August 1997, pp. 17-19.
  • [VBull] “The Word of the Day”, Virus Bulletin, March 1997, pp. 6–7.