背景
在这篇文章中,我们将讨论如何为 VB6 P-code
编写一个除错器。自从我在2000年代初第一次看到 Mr Silver
和 Mr Snow
撰写的 WKTVBDE P-Code Debugger
以来,这一直是我非常想做的事情。
当我第一次看到那个除错器时,感觉有一种神奇的魔力。那时我刚开始我的职业生涯,我热爱以 VB6
编程,并且反向分析对我来说是一门神秘的黑暗艺术。
在休假期间,我终于找到了时间静下心来深入研究这个主题。现在,我将分享我在这个过程中所发现的内容。
这篇文章将大量依赖于之前的论文《VB P-Code 反组译》[1]。在这篇论文中,我们详细介绍了运行时如何处理 P-Code
并在不同处理程序之间转移执行。
我们将针对这一执行流程来控制我们的除错器。
这篇论文中详述的除错器架构示例可以在免费的
中找到。
探索
当我开始研究这个主题时,我首先想检查在 WKTVBDE P-Code Debugger
中运行的过程是什么样子的。
我将一个测试的 P-Code
可执行文件与带有调试符号的 VB
运行时放在一起。这个可执行文件在 WKTVBDE
下启动,然后附加了一个原生的除错器。
检查 0x66106D14
的 P-Code
函数指针表显示,所有指针都已经被修补到 WKTVBDE.dll
中的同一个函数。
这让我们首次获得了他们如何实现其除错器的线索。此时值得注意的是,WKTVBDE
除错器完全在被除错的过程中运行,包括 GUI!
要启动除错器,您需要运行 loader.exe
并指定目标可执行文件。然后,它会启动该过程并在其中注入
WKTVBDE.dll
。一旦加载,WKTVBDE.dll
将用自己的函数钩住整个基础的 P-Code
处理程序表,从而优先获取即将执行的任何
P-Code
。
除错器还包含:
- 一个
P-Code
反组译器 - 解析所有嵌套
VB
内部结构的能力 - 列出所有代码对象和控制事件(如计时器或按钮点击)的能力
这还包括正常的除错器 UI 操作,如数据转储、断点管理、堆栈显示等。
这是一段相当复杂的代码要运行作为注入的 DLL。调试这所有内容肯定涉及大量工作。
在大致了解了除错器的运作方式后,我开始在网上搜索我能找到的其他信息。我很高兴找到了一篇 Mr Silver
在 Woodmann
上的旧文章,我为了保存历史进行了镜像[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