• 标 题:【原创】VB P-code -- 虚拟机的艺术
  • 作 者:cyclotron
  • 时 间:2004-12-26,10:57
  • 链 接:http://bbs.pediy.com

*岁末大盘点*
VB P-code -- 虚拟机的艺术
作者:cyclotron

   初学crack的时候,总有不少大虾好心地提醒菜鸟们:别碰VB P-code的东东。不可否认,这确实是善意的提醒。VB P-code特殊的运作方式以及由此造成的天然的调试困难,是不少早期cracker难以忘却的恶梦。然而,随着各类专业的VB P-code Debugger和Decompiler层出不穷,VB P-code已经不再像以前那样遥不可及了。退一步来说,即使是使用通用调试器SoftICE或OllyDBG来跟踪VB P-code程序,只要掌握其原理,它也并非是不可战胜的。
   VB P-code到底是怎样一种机制?它与普通的可执行程序有何不同?下面由我为大家来揭开VB P-code神秘的面纱……

   众所周知,采用VB编写的应用程序有两种编译方式,一种是Native Code方式,另一种就是P-code方式。事实上,VB 4.0以前只采用P-code编译,VB Native Code是在VB 5.0以后发展起来的,其目的是为了在一定程度上改善VB应用程序的运行速度。VB P-code的运行速度较慢,这是由它本身的运行机制所决定的。
   P-code,即Pseudo Code(伪代码),这一概念最早出现在Pascal编译器中,它是为了提供跨平台可移植性而产生的,实现这一编译机制的Pascal编译器被称为"Pascal P Compiler"。Sun公司在其推出的Java语言上也成功地实现了这种机制。Java程序的伪编译代码由一系列代表一定意义的字节码(byte code)组成,它们同属于一套特定的指令集,这种字节码不能由不同的CPU直接执行,而是要通过特殊的解释器翻译为CPU可以识别的指令才能执行,这种解释器就是我们常说的"虚拟机"。只要在不同的平台上提供虚拟机,把字节码翻译为对应的CPU指令集,也就实现了所谓的跨平台特性。Microsoft推出的VB P-code,实际上也是一组自定义的指令集,必须通过基于堆栈的虚拟机翻译为80X86上的指令集才能执行,担任虚拟机任务的就是msvbvm50.dll和msvbvm60.dll这两个动态链接库文件。由于在文件执行过程中多出了这一个解释的步骤,自然要影响到其执行的速度。正如我们所看到的,VB P-code并没有实现所谓的跨平台运行特性,这对于Pseudo这个词的起源是不恰当的;另一方面,采用P-code形式编译的VB应用程序的体积要小于采用Native Code形式编译的同样程序(这是由于P-code指令集的每一条指令对应于一组80X86指令所完成的任务),所以VB P-code实际上意味着VB Packed-code(压缩代码),用以强调VB P-code程序较小的代码体积。

   作为程序员,他们现在已经拥有了充分的选择依据--速度,还是体积;然而,作为cracker,仅仅知道这些是远远不够的,正如本文的标题所道出的,我们需要了解的,是虚拟机的运作方式--这才是在调试中真正起作用的东西。

   为了说明虚拟机的运作方式,我们写个小程序来调试一下,下面是这个小程序的源码:

代码:
Private Sub Command1_Click() X = InputBox("Please input an integer", "Input") If X <> "" Then     Y = X + 2     X = Y * 3     Out.Text = X End If End Sub


   这个程序只处理Command_Click事件,同时用到了InputBox,便于我们用rtcInputBox函数设断,最后记得用P-code方式编译。
   现在拿出我们心爱的调试器OllyDBG加载这个程序,在命令行插件中输入bp rtcInputBox ↙(注意区分大小写),按下F9运行,点击按钮,我们中断在下面的地方:

代码:
6A360CF2 >PUSH EBP          ;这里就是rtcInputBox的第一句指令了 6A360CF3  MOV EBP,ESP 6A360CF5  SUB ESP,54 6A360CF8  MOV EAX,DWORD PTR SS:[EBP+1C] 6A360CFB  PUSH EBX 6A360CFC  PUSH ESI 6A360CFD  PUSH EDI 6A360CFE  CMP WORD PTR DS:[EAX],0A 6A360D02  MOV EDI,80020004 6A360D07  JNZ MSVBVM60.6A360E6C 6A360D0D  CMP DWORD PTR DS:[EAX+8],EDI 6A360D10  JNZ MSVBVM60.6A360E6C 6A360D16  OR DWORD PTR SS:[EBP-8],FFFFFFFF


   按下Ctrl+F9返回,当InputBox弹出以后随意输入一个整数,然后继续单步跟踪,直到我们来到下面的代码处:

代码:
6A37D2CD  MOVSX EAX,WORD PTR DS:[ESI] 6A37D2D0  PUSH DWORD PTR DS:[EAX+EBP] 6A37D2D3  XOR EAX,EAX 6A37D2D5  MOV AL,BYTE PTR DS:[ESI+2] 6A37D2D8  ADD ESI,3 6A37D2DB  JMP DWORD PTR DS:[EAX*4+6A37DA58]    ;注意这句 6A37D2E2  MOVSX EAX,WORD PTR DS:[ESI]      ;我们来到这里 6A37D2E5  ADD EAX,EBP 6A37D2E7  PUSH EAX 6A37D2E8  XOR EAX,EAX 6A37D2EA  MOV AL,BYTE PTR DS:[ESI+2] 6A37D2ED  ADD ESI,3 6A37D2F0  JMP DWORD PTR DS:[EAX*4+6A37DA58]     ;注意这句 6A37D2F7  MOVSX EAX,WORD PTR DS:[ESI] 6A37D2FA  POP EBX 6A37D2FB  MOV WORD PTR DS:[EAX+EBP],BX 6A37D2FF  XOR EAX,EAX 6A37D301  MOV AL,BYTE PTR DS:[ESI+2] 6A37D304  ADD ESI,3 6A37D307  JMP DWORD PTR DS:[EAX*4+6A37DA58]    ;注意这句


    你可能已经注意到了,每一段代码都包括了下面这些指令:

代码:
XOR EAX,EAX MOV AL,BYTE PTR DS:[ESI+2] ADD ESI,3 JMP DWORD PTR DS:[EAX*4+6A37DA58]


   如果不明白这些指令是干什么的,跟踪时就会觉得老是在原处打转转。事实上,这几条指令正是虚拟机读取P-code伪代码的引擎部分。为了说明这些指令的功能,我们先来看看VB P-code伪代码的格式。

3A 6C FF 03 00
操作码  操作数

   这是一句典型的VB P-code指令,其助记符为LitVarStr,表示将一个Variant型的字符串入栈。3A是指令的操作码,0003是操作数,是以某种形式标记的字符串地址,6CFF在虚拟机引擎中没有用到,暂不清楚起什么作用。现在我们可以来解释虚拟机所做的一切了:

代码:
6A37D2E2  MOVSX EAX,WORD PTR DS:[ESI]      ;esi指向待解释指令的操作数 6A37D2E5  ADD EAX,EBP          ;取得某种形式的字符串指针 6A37D2E7  PUSH EAX          ;压栈 6A37D2E8  XOR EAX,EAX          ;eax清零 6A37D2EA  MOV AL,BYTE PTR DS:[ESI+2]      ;取下一条指令的操作码 6A37D2ED  ADD ESI,3          ;移至下一条指令的操作数 6A37D2F0  JMP DWORD PTR DS:[EAX*4+6A37DA58]    ;根据跳转地址表到下一条指令的解释单元


   这里我们看到,虚拟机并没有用简单的条件判断来识别操作码,而是采用了一种很巧妙跳转地址表法,把解释流程直接导向相应的解释单元。6A37DA58是地址跳转表的首地址,eax保存了下一条指令的操作码,由于每一个跳转地址是一个dword,所以用eax乘以4的值加上跳转表的基地址来索引下一条指令的解释单元。当然,由于每一条指令的长度是不同的,所以前面提到的读取P-code伪代码的引擎部分并不完全相同。明白了虚拟机的解释原理,跟踪P-code程序就方便多了,一旦看到读取P-code伪代码的引擎部分,我们就知道虚拟机开始解释下一条指令了。尽管如此,调试P-code程序还是比调试普通的本地机器码编译的可执行程序困难得多,因为在虚拟机的流程下,我们已经无法随时回顾前面执行过的指令了。
   下面我们来看看加法在虚拟机中是如何被解释执行的。按F8跟踪代码来到:

代码:
6A37D3B3  ADD EDI,EBP 6A37D3B5  PUSH EDI 6A37D3B6  XOR EAX,EAX 6A37D3B8  MOV AL,BYTE PTR DS:[ESI] 6A37D3BA  INC ESI 6A37D3BB  JMP DWORD PTR DS:[EAX*4+6A37DA58]    ;al=FBh


   走到这里时al的值为FB,这是Variant型变量算术运算伪指令的操作码,esi则指向了该操作码后的一个次操作码94,代表加法运算。跟随跳转地址表我们来到:

代码:
6A37D9BA  XOR EAX,EAX 6A37D9BC  MOV AL,BYTE PTR DS:[ESI] 6A37D9BE  INC ESI 6A37D9BF  JMP DWORD PTR DS:[EAX*4+6A37DE58]    ;al=94h


   这里是一个二级跳转表,我们注意到6A37DE58这个值和前面的跳转地址表基址不同了,这说明VB P-code算术运算伪指令采用了二级跳转来解释执行。我们知道一个字节的操作码可以表示的操作指令种类最多为256个,为了解决指令数不够的问题,Microsoft对部分伪指令进行二级跳转解释执行。这次跳转以后,我们来到:

代码:
6A384628  LEA EBX,DWORD PTR DS:[__vbaVarSub] 6A38462E  JMP SHORT MSVBVM60.6A384610 6A384630  LEA EBX,DWORD PTR DS:[__vbaVarMul] 6A384636  JMP SHORT MSVBVM60.6A384610 6A384638  LEA EBX,DWORD PTR DS:[__vbaVarDiv] 6A38463E  JMP SHORT MSVBVM60.6A384610 6A384640  LEA EBX,DWORD PTR DS:[__vbaVarIdiv] 6A384646  JMP SHORT MSVBVM60.6A384610 6A384648  LEA EBX,DWORD PTR DS:[__vbaVarMod] 6A38464E  JMP SHORT MSVBVM60.6A384610 6A384650  LEA EBX,DWORD PTR DS:[__vbaVarAdd]    ;我们跳转到这里 6A384656  JMP SHORT MSVBVM60.6A384610 6A384658  LEA EBX,DWORD PTR DS:[__vbaVarAnd] 6A38465E  JMP SHORT MSVBVM60.6A384610 6A384660  LEA EBX,DWORD PTR DS:[__vbaVarOr] 6A384666  JMP SHORT MSVBVM60.6A384610 6A384668  LEA EBX,DWORD PTR DS:[__vbaVarXor] 6A38466E  JMP SHORT MSVBVM60.6A384610 6A384670  LEA EBX,DWORD PTR DS:[__vbaVarEqv] 6A384676  JMP SHORT MSVBVM60.6A384610 6A384678  LEA EBX,DWORD PTR DS:[__vbaVarImp] 6A38467E  JMP SHORT MSVBVM60.6A384610


   很明显,Variant变量的算术逻辑运算伪操作都分布在这里,即将调用的函数__vbaVarAdd地址被装入ebx,继续向下看到:

代码:
6A384610  MOVSX EDI,WORD PTR DS:[ESI]      ;取加法指令的操作数 6A384613  ADD EDI,EBP 6A384615  PUSH EDI          ;操作数入栈 6A384616  CALL EBX          ;这里调用了函数__vbaVarAdd执行加法操作 6A384618  PUSH EDI          ;运算结果保存在堆栈结构中 6A384619  XOR EAX,EAX 6A38461B  MOV AL,BYTE PTR DS:[ESI+2]      ;继续取下一条伪指令的操作码 6A38461E  ADD ESI,3          ;指向下一条伪指令的操作数 6A384621  JMP DWORD PTR DS:[EAX*4+6A37DA58]    ;跳向下一条伪指令的解释单元


   经过上面的跟踪,VB P-code虚拟机的解释过程已经很清楚了,其他的相关指令读者可以通过调试自行分析。
   现在我们知道SoftICE或者OllyDBG完全可以胜任调试VB P-code程序的任务,然而,在大多数情况下,它们并非是我们最好的选择,根据调试的习惯,我们总是希望调试器能够直接跟踪可执行文件本身的每一句指令,这样我们可以直观地了解程序实现的功能。从这个角度来说,SoftICE或者OllyDBG是调试VB P-code虚拟机的优秀工具,但是它们无法让我们将更多的注意力集中在P-code程序本身的功能上。伴随着这样一种想法,功能强大的VB P-code专用调试器WKTVBDebugger应运而生了。这又是一种怎样的调试器?它究竟给VB P-code程序调试带来了怎样一种变革?敬请关注《VB P-code -- 调试器的革命》。
附件:VB_P-code.rar