最近我在研究 Visual Basic 6 格式,试图了解 IDispatch 实现如何解析函数的 vtable 偏移量。
在深入研究代码的过程中,我发现了几个我之前未曾探索过的结构体。这些结构体揭示了对于逆向工程师非常有价值的大量信息。
正如我们在本文中将展示的,来自内部结构的类型信息将使我们能够恢复标准 VB6 可执行文件中公开对象方法的函数原型。
这些信息与 IDispatch 的内部实现相关,并不需要像 ActiveX 组件那样的类型库。
在我们的恶意软件分析任务中,静态解析的方法将是主要关注点。
大多数 VB6 代码单元都是支持 IDispatch 接口的 COM 对象。这使它们能够作为通用类型方便地传递并与脚本客户端一起使用。
IDispatch 允许根据名称动态调用函数。以下 VB6 代码将通过 IDispatch 方法 进行操作:
这段简单的代码展示了几个要点:
VB6 可以从通用对象类型中调用正确的方法不当顺序的命名参数得到正确处理参数如果可行将自动转换为正确的类型此处字符串 “5” 转换为长整型如果我们添加一个 DebugBreak 语句,我们可以看到字符串 “myFunc” 被作为参数传递到底层的 vbaLateMemNamedCallLd 函数。这使得 IDispatchInvoke 能够通过字符串名称调用正确的函数。
由此我们可以推断,IDispatch 的实现必须知道完整的函数原型以及参数名称和类型,以便进行正确的调用。
我们还可以进一步测试这一点,通过更改参数名称、添加意外参数,或者将参数类型设置为无法转换为长整型的类型。
这些条件中的每一个都会在函数被调用之前引发错误。
需要注意的是,当这段代码被编译为标准可执行文件时,并不存在正式的类型库。这些类型信息则嵌入在二进制文件的某处。
那么,这些类型信息存储在哪里,我们又该如何检索它?
如果我们开始探测 BASICCLASSInvoke 运行时,我们将遇到内部函数,比如 FuncSigOfMember、EpiGetInvokeArgs、CoerceArg 等等。随著调试代码,我们可以捕捉对已知 VB6 结构的访问,并观察代码从那里的走向。
在更深入之前,我将简要概述 Visual Basic 6 文件格式的高层次结构。
VB6 二进制文件包含一系列嵌套结构,布局所有代码单元、外部引用、表单等。它们由 VBHeader 开始,然后分支出去,涵盖文件的各个方面。
与 Windows 加载器读取 PE 文件以设置正确的内存布局的方式类似,VB 运行时msvbvm60dll会读入 VB6 文件结构,以便在其环境中做好执行准备。
关于 VB6 文件格式的良好参考资料包括:
Visual Basic Image Internal Structure Format Alex IonescuSemiVBDecompiler 源码 VBGamer45vbdecompilertheautomaterscom 论坛本文所使用的结构和字段名称主要来自 SemiVBDecompiler 源码。一个有用的结构浏览器是免费的 vbdec 反汇编工具:http//sandspritecom/vbdec/
对于我们的工作,我们将从 VBHeadergtProjectInfogtObjectTable 开始找到的 ObjectTable 开始。
ObjectTablegtObjectArray 持有由其 ObjectCount 字段定义的物件结构数量。下面是一个示例:
这是一个单独代码对象的顶层结构。在这里我们可以找到包含 ProcCount 项的 ProcNamesArray。这个数组揭示了对象中定义的公共方法名称。
ObjectgtObjInfo 结构还将引导我们获取更多有趣的信息。ObjInfogtPrivateObject 将引导我们找到本篇博客文章所关注的主要结构。
我没有找到该结构或其下方结构的公共文档。以下信息是我通过分析所整理出的内容。
为了开发这些信息,我同时在四个不同的窗口中工作。
vb 运行时的反汇编目标可执行文件的反汇编调试器逐步执行代码并同步反汇编视图专门的工具来查看和搜索 VB6 结构以下是我目前对 PrivateObj 结构的定义:
在调试器中,我看到 vb 运行时代码访问此结构中的成员以获取 Invoke 调用的 vtable 偏移量。然后我编译了多个源代码变体,同时观察结构的变化效果。一些成员仍然未知,但我们已经识别出主要的感兴趣字段。
PrivateObj 结构引导我们看到描述每个代码对象的公共函数、变量和事件类型信息的数组。
事件和变量数组的计数直接保存在 PrivateObj 本身。要获得公共函数的计数,我们必须引用顶层的 ObjectgtProcCount 字段。
注意,在上面的 FuncType 数组中有一个空指针。这对应于源代码中的一个私有函数。如果你回头看看,你也会看到之前提到的 ProcNames 数组中有同样的空条目。
这些类型信息结构略有不同。首先,我们将看看我称之为 FuncTypDesc 结构。
我目前将它定义如下:
显示的结构对应于以下原型:
这个结构是本文文章的重点所在。为方法定义的类型数量保存在 argSize 字段中。前三位仅在属性类型中被设置。
111 代表属性 Set010 代表属性 Let001 代表属性 GetbFlags 位一也将被设置类型计数将是其余位数除以四。
如果 bFlags 位一被设置,最后一个条目代表方法的返回值。在所有其他情况下,这些类型表示从一到参数计数的参数。 VB6 支持每个方法最多 59 个用户参数。
argSize 为 0 是可能的,表示不带参数且没有返回值的子例程。
vOff 字段是成员的 vtable 偏移量。最低位被用作运行时中的标志,并在使用前被清除。最终调整的值将始终为 32 位对齐。
如果方法具有包含默认值的可选参数,则会设置 optionalVals 字段。
LpAryArgNames 是一个字符串数组的指针。它不是空终止的,因此在遍历之前应计算参数的数量。
在结构之后,我们看到几个额外的字节。这些代表了原型的类型信息。这个缓冲区的大小是动态的。
通过编译变体并比较变化的输出,我们确定了这些值的映射。以下值已被识别。
上述代码显示 0x20、0x40 和 0x80 位被设置以表示 ByRef、Array 和 Optional 修饰符。这些与在枚举中识别的基本类型结合使用。这些并不符合标准 VARIANT VARENUM 类型值。
comobj 和内部标志是特殊情况,在类型修饰符字节后嵌入另外的 32 位值。
这将链接到目标的 ObjInfo 结构,对于内部对象来说。
如果遇到 comobj 类型,将找到一个结构的偏移量,该结构指定对象的库 guid、clsid、库名和 dll 路径。当我们涵盖公共变量时,会展示这个结构。
需要注意的是,这些偏移量似乎总是 32 位对齐的。这可能在类型字节和数据偏移量之间引入零填充。各类型字节之间也观察到了零填充。解析器必须能够容忍这些变化。
以下是对以下原型的类型定义:
接下来快速看看 PubVarDesc 结构。这里是以下原型的结构:
这个示例显示标志 0x1D 表示外部 COM 对象类型。然后偏移量跟随后面是一个结构,该结构定义了 COM 对象本身的详细信息。
varOffset 字段中的值 0x3c 是此变量存储数据的位置。对于 VB6 COM 对象,ObjPtr() 返回一个结构,其第一个成员是对象的 Vtable 指针。其下方的区域包含类的实例数据。在这里,myPublicVar 将位于 ObjPtr()0x3c。
最后,我们将查看一个 EventDesc 结构,对应于以下原型:
该结构的布局与 PubFuncDesc 类型非常相似。不过,有一件事需要注意的是,我目前尚未找到链接到嵌入编译二进制文件中的事件名称字符串的方法。
加速器国外梯子在实际情况中,它们是嵌入在 ProcNamesArray 的字符串之后。
注意,类可以引发这些事件来回调消费者。定义事件的代码对象中并不存在事件例程的实现。
在本文中,我们详细介绍了几个结构,它们将允许分析师解析 VB 对象结构并提取公共对象成员的函数原型。
这类型信息是作为每个用户生成的 VB6 表单、类、高级控制等的标准 IDispatch 工序的一部分被包含进来的。这不适用于 BAS 代码模块中的函数,因为它们在内部并不是 COM 对象。
有趣的是,VB6 将这些类型数据与 text 部分的其他内部结构一起嵌入到 PE 文件中。这并不需要类型库,且无法禁用这一功能。
虽然 text 部分的结构可以包含多种编译器生成的本地代码存根,但所有用户代码都位于由 VBHeaderProjectInfoStartOfCode 和 EndOfCode 偏移量定义的内存范围内。
分析 VB6 二进制文件以解析本文中所列出的信息所需的框架可能相当复杂。幸运的是,已经存在开源实现来帮助减轻这一负担。
结构和原型提取例程已在免费 vbdec 反汇编工具中包含。这个工具还可以生成 IDC 脚本,以将适当的结构定义应用于反汇编内容。
VB6 二进制文件通常被认为难以分析。提取内部函数原型将对逆向工程师是一个非常受欢迎的补充。
标签:分析、研究、系列、VB
分享:X Facebook
2024-11-29 18:22:26