如何制作VB的P-Code调试器
作者:佚名 文章来源:不详 点击数: 更新时间:2008-8-12
// 这里是 Debugger的控制代码部分
// ...
// ...
// 这里我们修改跳转地址。由于它们不能在编码时修改,因此我们在VB虚拟机运行时采用自修改代码(SMC self-modifying code)设置跳转地址表
_asm {
mov eax, offset JmpOffset
add eax, 3
mov ebx, [VBDebugger.RedirTableAddr]
mov [eax], ebx
}
mov esp, ebp // 我们保存的堆栈的初始框架
pop ebp
popfd // 恢复标志
popad // 恢复寄存器
// 最后
// 如果我们要以内存编辑模式修改被调试的VB应用程序中的伪指令操作码,我们可以在AL中改变它。因此这样的改变只有当我们要修改的操作码将要跳转入VB虚拟机中相应的控制程序时才可进行。
_asm {
cmp [VBDebugger.OPCODE_CHANGE],0
je NoChange
mov al,VBDebugger.Opcode
NoChange:
mov [VBDebugger.VMAddress],0
}
JmpOffset:
// 将程序的控制权返还VB虚拟机。 注意这个代码是在运行时间自修改的
jmp [eax*4+VBDebugger.RedirTableAddr]
}
看到这个程序我们可以发问:谁说C不够强大? 就像你看到的,通过使用 “Naked” 指示,我们能凭着我们自己的品味要求创建程序。 事实上,这个指示(“Naked”)经常在为Window设计的驱动程序(VxD)中使用。 这个程序的行为有如一个hook(钩子程序)存在于原来的代码和虚拟机之间。 就像你所见的,这个程序除了返回控制到原始的跳转表什么也没做, 但它可以控制那些我们需要它控制的VB虚拟机执行的伪指令。
当我们开始有关P-Code的研究时,我们缺少有关资料,Microsft保留了有关技术规范说明。如果你想获知P-Code的秘密,你必须和微软签订一份叫做NDA (non disclosure Agreement)的保密协议,你保证不散布任何有关信息(这些信息会使得某些公司能够通过合法行对微软产生威胁)。因此你只能找到如下的可怜的一点零星资料,你可以在这里发现它们:
http://msdn.microsoft.com/archive/default.asp?url=/archive/en-us/dnarvc/html/msdn_c7pcode2.asp
此外 Exdec, 是一个由Josephco制作的 P-Code 反编译器。 Exdec可以反编译任何P-Code文件,但操作码的显示不够完善和全面(仅仅第一个字节)。 我们稍后会知道原因。
跳转地址表提供了一个基础。 我们准备了一个代码补丁,它将作为一个新的区段部分(section)被加到VB虚拟机(DLL)中。 这个补丁会获得初始数据并且调用我们的Debugger。 一旦Debugger准备好(安装好钩子程序“hooking”到虚拟机, 我们的补丁程序将继续载入VB的虚拟机,重置控制权到VB虚拟机DLL的OEP(程序入口)。 这个方法有一个主要的缺陷: 必须修改VB的虚拟机DLL。
我们可以通过创建一个载入器(loader)消除必须修改VB虚拟机代码的必要性。 一个小应用程序(载入器)以悬挂方式(suspended mode)开始一个可执行VB程序,使用 GetThreadContext获得其入口地址, 拷贝一个代码补丁(它负责装入Debugger的 DLL )。 一旦补丁程序被执行, 它使用synchronism APIs SetEvent 和 WaitForSingleObject通知主进程。 一旦补丁程序完成,他将恢复原始程序代码,并且通过调用SetThreadContext返回到原程序的OEP,就象什么都没有发生过。
补丁程序运行后,当我们的Debugger得到控制时产生了一个问题。当原始应用程序代码和部分VB数据被临时替换成我们的Debugger需要的数据时,我们必须运行我们的Debugger在一个独立的线程中,这个线程会检查一个内存位置是否包含VB5的签名,它(我们的移植检查过程)用于指示载入器是否成功执行,并且进行了我们预设的(各种替换)工作。
我们Debugger中的移植检查过程也会校验被装入的是否为一个可执行的VB 应用程序,这是通过察看输入表是否包含MSVBMXX。DLL(其中 XX 可能是 05或 06)。
另一个问题是不同版本的VB虚拟机操作码地址表存在差异。因此我们必须针对不同版本的虚拟机进行个案分析和处理。 这是个麻烦的方法。因为一旦有新版本的虚拟机被发行,我们就要修改我们自己的载入器代码。 这个问题一直没能得到解决,直到最近,我们才想出了比较理想的解决方案。 就如我以前说的,操作码地址表在VB虚拟机的引擎区域(在名为 .ENGINE 的区段中)。 这个表的特性之一就是它所包含的所有地址必须指向相同的区段空间, 因此我们设计了一个算法,它将定位第一组256个相邻的双字(DWORDS),这些值包含在.ENGINE 区段, 并且这将确定地址表。
* 第二步
恢复操作码与重获VB伪指令操作码助记符
如果我们开始就注意到了虚拟机的符号文件(DBG),这一步对我们来说就简单了。 一个办法是利用JosephCo的 Exdec反编译器获得助记符。 这并不容易搞定。 我没有一个个去找操作码, 而是假设Exdec(它的DLL)包含了一个操作码列表。 它也正是那样(我用一个十六进制编辑器定位和确定每个助记符的位置)。关键点:我发现部分操作码使用了前缀-FF,FE,FD,FC,FB (Lead0, Lead1, Lead2, Lead3 and Lead4)字节。每个前缀产生一个新的操作码表,共有:
( 5 前缀prefixes + 一组标准操作码设置) * 256 伪指令操作码 = 1536 个伪指令操作码
正如你所见,VB的操作码不少,不过许多没有作用,而且一些是多余的。例如:在VB6的虚拟机中,执行同样的操作 Lead4 前缀不使用自己的操作码处理程序跳转表而是直接转到操作码46h的处理程序中(这可以通过反编译VB虚拟机得到验证)。 就象我上面提到过的,当我们已经确定了一些操作码的行为时,我们认识到VB的虚拟机Debugger 文件包含全部它自己的助记符信息(处理程序名称,地址,每个助记符的名称)。我们是使用SoftICE转存(dumping)DBG内容到文本文件获得这些信息的。
这里列出一小部分:
相对虚拟地址 符号名称
RVA Size Symbol name
0F103D8Bh 34 CCyR4
0F103DADh 19 CCyVar
0F103DC0h 9 CBoolCy
0F103DC9h 0 CBoolR8
0F103DC9h 38 CBoolR4
0F103DEFh 32 CStrVar
0F103E0Fh 18 CStrBool
0F103E21h 34 CStrR8
通过上面的例子,你看到了一些P-Code助记符; RVA是它们在虚拟机中的相对地址偏移。 不幸的是,这些地址在不同版本的VB虚拟机中不相同,但我们的Debugger可以通过启发式搜索方法处理地址跳转表。 不同版本中的一些操作码助记符可能不同,但功能不变。 应用我们已经掌握的信息我们初步的做了一个伪指令操作码反编译器。它还仅能显示正在被执行的操作码,我们必须发现每一条指令的精确长度。这是全部工作中最艰巨的任务: 必须分析每一个操作码处理程序,在其开始和结束,检查操作码缓存区尺寸。
我们分析了1000多个操作码处理子程序,虽然有些很短,但花费了大量时间。 起初,我们假定操作码的缓存区尺寸是固定不变的,但不幸的是它们不同。 一些指令需要许多参数入栈,这使得它们的尺寸总是变化的。 这样的操作码不多,但如果我们不够小心谨慎就会在对相应处理过程的分析中产生错误的认识。大概这就是为什么JosephCo(在他的反编译器Exdec中)选择了不完全反编译的理由。 反向工程质量的保证就是必须对需要分析的目标(具体到每一个基础指令)进行最深入的理解,认识和解释。
我们将工作进行了分解; 每一部分基于不同指令组的尺寸。 这个工作结束后,我们有了全部的伪代码的尺寸,我们作出了一个能够产生比较象样的反编译代码的工具(WKTVBDE)。 勿容置疑地说,由于不可避免的人为错误,我们必须不断修正操作码的尺寸定义。甚至今天,我们觉得仍有一些错误存在。因为一些指令并没有在所有的应用程序中被使用,全部测试它们是不可能的。到目前为止,对于我们的Debugger (WKTVBDE 1.3)中还没有错误的操作码被报告。1
* 第三步
增加Debugger的基本功能特性
这一步有更复杂的编码和需要研究的问题。添加输出表(Export tTble)并不太难,这一步由载入器(loader)通过分析PE 文件头来实现。 然后Debugger 将获得跳转地址表。 随后,我们构造并建立一个断点状态表(激活/非激活/空)。 如此,我们可以在VB虚拟机的任何API处设置断点。 一个类似的方法是在伪指令上设置断点。 与常见的一般调试器(Debuggers)不同: 我们可以在任何一个给定的指令上设置断点。因为我们的Debugger在每一个操作码被虚拟机执行前已经全面控制了它们。
随后,我们在实际的代码中加入断点。比如在一个给定了地址的操作码上设置断点。 这些断点被保存在我们的Debugger中一个动态连接表中。这有几个优势:断点数量不受限制;对于大量的断点信息,可以动态调整内存的使用。
断点的基本功能并不复杂。简单地说,当Debugger获得控制,它为了自身的应用,保存操作码地址缓存区内容。 随后Debugger显示这个地址,并将存储的断点与之比较。如果这个操作码地址已经存在于用户断点设置列表中(状态为:活动),Debugger则停止被调试的VB应用程序的执行。 内存编辑器/查看器允许被调试的程序被编辑、查看和内存内容转存。
我们已经尝试尽量优化和确认指针操作的有效性。但我们依然无法全部避免和排除那些无效的内存访问操作(虽然象这样的非法操作在Debugger的内存编辑器中几乎是不可能的发生的)。 有一种可能,当你在调试程序进行内存修改时,毫无理由的程序突然中止。 这种情况可能发生在我们使用不当的操作码替换原来的操作码的时候(比如操作码要求的参数个数不同),这种行为可能导致虚拟机在解释执行时发生运行错误中断。
当用户清楚自己的调试行为时这种状况一般不会发生。在Debugger的新版中,我们将控制这种状况的出现,保存发生错误之前的各种状态,并允许程序继续处理随后的正常指令。无论如何这种状况仅发生在使用SoftICE替换了错误的代码时。 如果我们搞乱了什么,出现错误的结果是合乎逻辑的。
现在,所有的处理过程已经被图形化地表现在Debuggers Window窗口中。我们使用不同的颜色代码来标识我们已经设置了断点的伪指令行。 我们的Debugger由显示一个控制列表框开始。 有如你所见,我们采用了怀旧式的颜色调配方案。 为了方便用户使用,快速上手,我们尝试采用一种符合用户平常使用习惯的主程序界面设计方案(我们的Debugger采用了基于绿色,黑色的配色方案)。
Mr. Silver