第一次分析VB可执行体内部数据结构是几年前的事了,当时代码写的很混乱,现在刚好了解相关知识的时候,发现之前的代码可读性太差,所以一边重构代码,一边写篇文章复习下以前的成果。
一般使用反汇编工具来分析VB程序的入口点,结果都是以下形式:
push 0x0040XXXX call MSVBVM60.ThunRTMain
首先push一个地址到栈里,紧接着调用MSVBVM60库里的ThunRTMain函数,并且不会立即返回。
这个地址指向的数据结构,我就管它叫做VB_HEADER,该数据结构是VB程序运行时至关重要的。
根据该结构,VB运行时(MSVBVM60)可以枚举该项目有多少个对象,对象有多少控件,控件又有多少的事件。程序该从何处执行,代码是否为P-Code等等。
下图简示了从VB_HEADER开始枚举每个对象的流程:
这里说的’对象‘就是指原VB项目中的每个编译单元(frm/bas/ctl/cls/…),在编译完成后,都会以’对象‘的形式存在于最终二进制文件中。
而窗体对象进而会指出一个数据结构来描述所拥有的控件表,控件又会指出一个数据结构来描述所拥有的事件表。
下面是用C语言写的上图相关的数据结构:
//VB 初始化头部结构
typedef struct _VB_HEADER
{
union {
char crSign[4]; // 四个字节的签名符号,和PEHEADER里的那个signature是类似性质的东西,VB文件都是"VB5!"
uint32_t dwSign;
} Sign;
uint16_t wRuntimeBuild; // 运行时创立的变量(类似编译的时间)
char szLangDll[14]; // 语言DLL文件的名字(如果是0x2A的话就代表是空或者是默认的)
char szSecLangDll[14]; // 备份DLL语言文件的名字(如果是0x7F的话就代表是空或者是默认的,改变这个值堆EXE文件的运行没有作用)
uint16_t wRuntimeRevision; // 运行时DLL文件的版本
LCID dwLCID; // 语言的ID
LCID dwSecLCID; // 备份语言的ID(只有当语言ID存在时它才存在)
PVOID lpSubMain; // VA sub main过程的地址指针(3.)(如果是0则代表这个EXE时从Form窗体文件开始运行的)
PVBPI lpProjInfo; // VA 工程信息的地址指针,指向一个VB_PROJECT_INFO结构(2.)
uint32_t fMdlIntCtls; // ?详细见"MDL 内部组建的标志表"
uint32_t fMdlIntCtls2; // ?详细见"MDL 内部组建的标志表"
uint32_t dwThreadFlags; // 线程的标志
uint32_t dwThreadCount; // 线程个数
uint16_t wFormCount; // 窗体个数
uint16_t wExtComponentCount; // VA 外部引用个数例如WINSOCK组件的引用
uint32_t dwThunkCount; // ?大概是内存对齐相关的东西
PVBGT lpGuiTable; // VA GUI元素表的地址指针(指向一个GUITable_t结构(4.四))
PVBEXTCOMP lpExtComponentTable; // VA 外部引用表的地址指针
PVBCRD lpComRegData; // VA COM注册数据的地址指针
uint32_t oSZProjectDescription; // Offset 指向工程EXE名字的字符串
uint32_t oSZProjectExeName; // Offset 指向工程标题的字符串
uint32_t oSZProjectHelpFile; // Offset 指向帮助文件的字符串
uint32_t oSZProjectName; // Offset 指向工程名的字符串
} VB_HEADER, VBHDR, *PVB_HEADER, *PVBHDR;
//VB工程信息
typedef struct _VB_PROJECT_INFO
{ //sizeof() = 0x23c
union {
char chSign[4]; // 结构的签名特性,和魔术字符类似
uint32_t dwSign;
} Sign;
PVBOT lpObjectTable; // VA 结构指向的组件列表的地址指针(很重要的!(7.))
uint32_t dwNull; // 没有用的东西
PVOID lpCodeStart; // VA 代码开始点,类似PEHEAD->EntryPoint这里告诉了VB代码实际的开始点
PVOID lpCodeEnd; // VA 代码结束点
uint32_t dwDataSize; // 标志1
PVOID lpThreadSpace; // 多线程的空间??
func__vbaExceptHandler lpVBAExceptHandler; // VA VBA异常处理器(SEH)地址指针
PVOID lpNativeCode; // VA 本地机器码开始位置的地址指针
WCHAR szPathInformation[264]; // 原文件地址,一个字符串,长度最长为264
PVBEXT lpExternalTable; // VA 引用表的地址
uint32_t dwExternalCount; // 引用表大小(个数)
} VB_PROJECT_INFO, VBPI, *PVB_PROJECT_INFO, *PVBPI;
//VB对象表
typedef struct _VB_OBJECT_TABLE //这个是OBJECT 的总表,可以索引以后的每个OBJECT
{ //sizeof() = 0x54
LPVOID lpHeapLink; //没有用的填充东西
LPVOID lpExecProj; //指向VB工程Exec COM对象
struct _VB_PROJECT_INFO2 * lpProjectInfo2; //VA指向Project Info 2
uint32_t dwReserved; //编译后总为-1,未使用
uint32_t dwNull; //编译后未使用
LPVOID lpProjectObject; //指向工程数据
GUID uuidObject; //对象表的GUID
uint16_t fCompileState; //在编译过程中使用的内部标志
uint16_t wTotalObjects; //目前在工程总计对象数量
uint16_t wCompiledObjects; //已编译对象数量
uint16_t wObjectsInUse; //通常等于上面的编译后的对象数量
PVBPUBOBJ lpObjectArray; //VA指向第一个VB公共对象描述符,很重要
uint32_t fIdeFlag; //标志/指针,仅在IDE模式下使用
uint32_t lpIdeData; //标志/指针,仅在IDE模式下使用
uint32_t lpIdeData2; //标志/指针,仅在IDE模式下使用
PCHAR lpProjectName; //指向工程名字
LCID dwLcid; //language ID1
LCID dwLcid2; //language ID2
LPVOID lpIdeData3; //标志/指针,仅在IDE模式下使用
uint32_t dwIdentifier; //结构的模板版本
} VB_OBJECT_TABLE, VBOBJTAB, *PVB_OBJECT_TABLE, *PVBOT;
//VB公共对象描述符
typedef struct _VB_PUBLIC_OBJECT_DESCRIPTOR //这个就是每个对象描述符的结构
{ // sizeof() = 0x30
struct _VB_OBJECT_INFO * lpObjectInfo; //VA 指向一个ObjectInfo_t类型,来显示这个OBJECT的数据
uint32_t dwReserved; //保留未使用,编译后总为-1
PSHORT lpPublicBytes; //VA 指向外部可见变量表大小 (integers)
PSHORT lpStaticBytes; //VA 指向外部不可见变量表大小 (integers)
LPVOID lpModulePublic; //VA 指向外部可见变量表
LPVOID lpModuleStatic; //VA 指向外部不可见变量表
PCHAR lpObjectName; //VA 字符串,这个OBJECT的名字
uint32_t dwMethodCount; //events, funcs, subs(事件\函数\过程)数目
PCHAR * lpMethodNamesArray; //指向方法名称指针数组
uint32_t oStaticVars; //OFFSET 从pModuleStatic指向的静态变量表偏移
uint32_t fObjectType; //比较重要显示了这个OBJECT的实现,具体见下表
uint32_t dwNull; //编译后无效
} VB_PUBLIC_OBJECT_DESCRIPTOR, VBPUBOBJDESCR, VBPUBOBJ, *PVB_PUB_OBJ_DESCR, *PVBPUBOBJ;因为VB内部的数据结构众多且较为复杂,所以本文只摘取相关的数据结构来讲解。
