撰写一个 VB6 P-Code 除错器 – Avast

背景

在这篇文章中,我们将讨论如何为 VB6 P-code 编写一个除错器。自从我在2000年代初第一次看到 Mr SilverMr Snow
撰写的 WKTVBDE P-Code Debugger 以来,这一直是我非常想做的事情。

当我第一次看到那个除错器时,感觉有一种神奇的魔力。那时我刚开始我的职业生涯,我热爱以 VB6 编程,并且反向分析对我来说是一门神秘的黑暗艺术。

在休假期间,我终于找到了时间静下心来深入研究这个主题。现在,我将分享我在这个过程中所发现的内容。

这篇文章将大量依赖于之前的论文《VB P-Code 反组译》[1]。在这篇论文中,我们详细介绍了运行时如何处理 P-Code
并在不同处理程序之间转移执行。

我们将针对这一执行流程来控制我们的除错器。

这篇论文中详述的除错器架构示例可以在免费的
中找到。

探索

当我开始研究这个主题时,我首先想检查在 WKTVBDE P-Code Debugger 中运行的过程是什么样子的。

我将一个测试的 P-Code 可执行文件与带有调试符号的 VB 运行时放在一起。这个可执行文件在 WKTVBDE
下启动,然后附加了一个原生的除错器。

检查 0x66106D14P-Code 函数指针表显示,所有指针都已经被修补到 WKTVBDE.dll 中的同一个函数。

这让我们首次获得了他们如何实现其除错器的线索。此时值得注意的是,WKTVBDE 除错器完全在被除错的过程中运行,包括 GUI!

要启动除错器,您需要运行 loader.exe 并指定目标可执行文件。然后,它会启动该过程并在其中注入
WKTVBDE.dll。一旦加载,WKTVBDE.dll 将用自己的函数钩住整个基础的 P-Code 处理程序表,从而优先获取即将执行的任何
P-Code

除错器还包含:

  • 一个 P-Code 反组译器
  • 解析所有嵌套 VB 内部结构的能力
  • 列出所有代码对象和控制事件(如计时器或按钮点击)的能力

这还包括正常的除错器 UI 操作,如数据转储、断点管理、堆栈显示等。

这是一段相当复杂的代码要运行作为注入的 DLL。调试这所有内容肯定涉及大量工作。

在大致了解了除错器的运作方式后,我开始在网上搜索我能找到的其他信息。我很高兴找到了一篇 Mr SilverWoodmann
上的旧文章,我为了保存历史进行了镜像[3]

在这篇文章中,Mr Silver 阐述了他们撰写 P-Code
除错器的历史,并给出他们使用的钩子函数模板。这是一次非常有趣的阅读,为我提供了一个良好的起点。

设计考量

展望未来,我希望在这个架构中做出一些设计上的调整。

第一个改变是,我希望将所有结构解析、反组译引擎和用户界面代码移到一个独立的进程中。这些任务相当复杂,作为 DLL 注入 进行调试非常困难。

为了达成这一任务,我们需要一种简单易用、稳定的进程间通信(IPC)技术,并且本身是同步的。我在这个类别中最喜欢的技术是使用 WindowsMessage,这样外部进程在返回之前会自动等待窗口过程完成。

我已经广泛使用这项技术来延迟恶意软件在其自身解包后的行为[4]。我甚至将其与一个与 IDA 的远端实例接口的 Javascript 引擎联系起来[5]。

这种设计使我们能够自由撰写并调试文件格式解析、反组译引擎和用户界面代码,完全独立于除错器的核心。

此时,除错器的整合实质上成为了反组译器的附加功能。注入的 DLL 现在仅需拦截执行并与主界面进行通信。

在本论文的其余部分,我们将假设一个完全运作中的反组译器已经被创建,并仅专注于除错器特定的细节。

有关如何实现反组译器和结构解析的参考实现,请参考之前的论文[1]。

实现

现在拥有足够的资讯,该是开始尝试控制执行流程的时候了。

我们的第一个任务是弄清楚如何钩住 P-Code 函数指针表。在我们可以钩住它之前,我们实际上首先需要找到它!这可以通过几种方式完成。从
WKTVBDE 作者的论文来看,他们主要进行了三个阶段的进展。首先,他们以手动修补的方式开始了 VB 运行时和在导入表中引用的修改过的 DLL。

其次,他们进展到一个单一的支持复本的运行时,并对要修补的硬编码偏移进行修补,然后加载器将调试器 DLL注入到目标过程中。最后,他们增加了能够动态定位和修补表的能力,无论运行时版本如何。

这是一个不错的实验进展,他们详细介绍了这个过程。第二个阶段对于任何能理解这篇论文的人都是可轻易访问的,且效果也相当好。我将留给读者去探索注入和钩子细节。

基本步骤是:

  • 将内存设置为可写
  • 复制原始函数指针表
  • 用自己的钩子程序替换原始处理程序

已发布的示例还利用了自我修改代码,而我们会尽量避免这样做。为了避免这样的情况,我们将引入单独的钩子存根,每个表一个,以记录一些额外数据。

在进入单独的钩子存根之前,我们注意到他们在一个全局结构中存储了一些运行时/状态信息。我们将根据以下内容进行扩展:

从钩子代码中,您会注意到第一个表中的所有基本操作码(不包括领导字节处理程序)都收到了相同的钩子。每个结尾的 Lead_X 字节则收到了自己的处理程序。

以下显示了前两个表的钩子处理程序示例,其他四个遵循相同的模式:

每个单独表的钩子配置了当前领导字节和表基础的全局 VM 结构字段。实现的实质部分现在从通用的钩子程序开始。

在主 PCodeHookProc 中,您会注意到我们调用了另一个函数:void NotifyUI()

在这个函数中,我们会检查断点、处理单步执行等等。这个函数然后使用同步 IPC 与外部进程的除错器用户界面进行对话。

除错器 UI 将接收步进通知,然后进入等待循环,直到用户给出步进/继续/停止的命令。这将使被除错过程冻结,直到 SendMessage
处理返回。您可以在 SysAnalyzer ApiLogger 的源代码中找到这方面的范例实现[6]。

我们之所以要从 PCodeHookProc 调用另一个函数,是因为它是以裸函数的形式用组合语言编写的。一旦摆脱这一点,我们现在可以在 C中轻松实现更复杂的逻辑。

进一步的步骤

一旦所有的钩子实现,您仍然需要一种方式来操控被除错程序。当代码远程冻结时,远端 GUI 仍然可以通过一个单独的 IPC 回道向冻结的过程发送新命令。

这样,您可以管理断点、变更步进模式,并通过运行时导出如 rtcTypeName 实现查找服务。

钩子 DLL 也可以修补自定义操作码。以下代码在未使用的 slot 0x01 中添加我们的单字节 NOP 指令。

如注释中所暗示的,当前操作码的实时修补以及“在此设置新原点”类型的功能都是可行的。这些是通过除错器直接对全局 VM 结构进行
WriteProcessMemory 调用来实现的。该结构的地址在启动时的初始化消息中透露。

结论

编写 P-Code 除错器是一个非常有趣的概念。这是我个人想做的事情,已经快20年了。

当您近距离看到所有运作零件时,这并不像乍看之下那样令人生畏。

拥有一个运作中的 P-Code 除错器也是学习 P-Code 指令集如何实际运作的基础步骤。能够实时观看 VB6 P-code的执行,并结合堆叠差异检测和数据查看器工具,这是相当有启发性的。在这么高的粒度水平进行单步执行,让您更清楚地了解发生了什么。

尽管钩子代码本身在技术上具有挑战性,但在开始之前您就已经需要完成一些庞大的工作。

这些前提包括:

  • 准确解析未知的文件格式
  • 一个稳健的反组译引擎,用于未知的 P-Code 指令集
  • 一个方便的用户界面,以实现数据显示和除错器控制

对于一名逆向工程师来说,这样的项目就像糖果一样。有如此多的方面可以分析和研究。如此多未记录的东西等待探索。这是一个由千千万万片段组成的谜题。

可以挤出什么能力?还有多少东西等待发现?

对我来说,这是一段相当吸引人的旅程,也让我更亲近我所喜爱的语言。希望这些文章能启发他人,引导他们探索的旅程。

[1] –
[2] – (MD5: EEBEB73979D0AD3C74B248EBF1B6E770)
[3] –
[4] –
[5] –
[6] –

标签:、、、、

分享:XFacebook

Leave a Reply

Your email address will not be published. Required fields are marked *