第一次分析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内部的数据结构众多且较为复杂,所以本文只摘取相关的数据结构来讲解。