• 标 题:【原创】VB P-code -- 伪代码的奥秘
  • 作 者:cyclotron
  • 时 间:2004-12-26,11:02
  • 链 接:http://bbs.pediy.com

*岁末大盘点*
VB P-code -- 伪代码的奥秘
作者:cyclotron

   欢迎回到VB P-code伪代码世界。
   前两期我为大家介绍了VB P-code虚拟机的运行机制和P-code专用调试器WKTVBDebugger,相信大家已经对VB P-code程序有所了解了。我们的任务当然不仅仅局限于理论研究,解析VB P-code的最终目的还是加密解密和逆向工程。由于Microsoft没有公开VB P-code伪代码的技术文档,我们无法获得现成的伪代码指令说明,而单凭VB P-code反编译器给出的助记符信息是远远不够的,这就要求我们自行发掘伪代码执行的奥秘。
   可能有些朋友还不太明白,既然WKTVBDebugger作为一个伪代码级的调试器已经屏蔽了VB P-code虚拟机的解释过程,为什么还费神要去了解这些伪指令执行的细节呢?这里我请大家思考一个问题:假设你现在用WKTVBDebugger跟踪AddVar这句伪指令,你如何知道它的操作数和操作结果分别是多少?也许有人说,既然VB P-code虚拟机是基于堆栈的,那么操作数和操作结果一定存放在堆栈里了。不错,这些内容确实存在于堆栈中,但是你知道它的存放形式吗?是单个的操作数,是指针,还是其他复杂的数据结构?我敢说,如果你是第一次调试这句VB P-code指令,它一定会令你不知所措,即使你能最终解决这个问题,也一定会花上不少时间,结果也会令你觉得不可思议。此外,对于不同的P-code伪指令,其存放形式是各不相同的。如果你在调试一个软件时,不能看到每句指令的操作数和操作结果,那与静态反编译何异之有?WKTVBDebugger的调试功能还不是形同虚设?当然,这只是其中的一个方面。如果你连伪指令的抽象动作都不明白(毕竟不是所有的伪指令都能从其助记符看出其含义的),那又该如何呢?正如我前面所言,要理解这些伪指令的执行细节,是要费一番功夫的。

   既然如此,我们应当如何解决这些问题呢?答案将由黄金组合OllyDBG+WKTVBDebugger+VBParser来揭晓。为了方便,这次使用的例子程序还是上两篇使用的VB P-code.exe,我们将通过跟踪虚拟机的解释过程来研究P-code伪代码的执行细节。
首先使用ljtt的VBParser(非常专业的VB P-code反编译器)解析VB P-code.exe,得到伪代码如下:

代码:
FileName: D:\Contribution\VB P-code\VB P-code--伪代码的奥秘\VB P-code.exe -----=====-----=====-----=====--Pcode--=====-----=====-----=====----- [CommandButton] Private Sub Command1_Click() '-=-=-=-=-=-=-= ProcAddr Range: [004019C4 - 00401A54] , ProcSize: 90 =-=-=-=-=-=-=- 004019C4: 27 9C FE           LitVar_Missing          PushVarError 80020004 (missing)                                                   004019C7: 27 BC FE           LitVar_Missing          PushVarError 80020004 (missing)                                                   004019CA: 27 DC FE           LitVar_Missing          PushVarError 80020004 (missing)                                                    004019CD: 27 FC FE           LitVar_Missing          PushVarError 80020004 (missing)                                                    004019D0: 27 1C FF           LitVar_Missing          PushVarError 80020004 (missing)                                                    *********** Referent String: "Input" ***********                               | 004019D3: 3A 4C FF 00 00     LitVarStr               PushVarString Ptr_00401434 004019D8: 4E 3C FF           FStVarCopyObj           [local_C4]=vbaVarDup(Pop) 004019DB: 04 3C FF           FLdRfVar                Push local_C4 *********** Referent String: "Please input an integer" ***********                               | 004019DE: 3A 6C FF 01 00     LitVarStr               PushVarString Ptr_00401400 004019E3: 4E 5C FF           FStVarCopyObj           [local_A4]=vbaVarDup(Pop) 004019E6: 04 5C FF           FLdRfVar                Push local_A4 004019E9: 0B 02 00 1C 00     ImpAdCallI2             Call Ptr_00401020; check stack 001C; Push EAX 004019EE: 46 7C FE           CVarStr                  004019F1: FC F6 8C FE        FStVar                   (……省略)


   以上就是Command_Click事件响应代码的开头部分,是不是一下子觉得有些手足无措?没关系,我们一句一句来调试。如果你还记得我在第一篇中所描述的VB P-code虚拟机运行机制,那么你应该能想到下一步该怎么做。VB P-code虚拟机以流的形式顺序读入每一句伪指令,然后通过一个跳转地址表找到相应的解释代码,我们要跟踪它解释伪指令的细节,就必须在伪指令的操作码上下内存访问断点。现在我们看到第一句伪指令LitVar_Missing从004019C4(虚拟地址)开始,那么用OllyDBG加载VB P-code.exe,在转存窗口中来到004019C4,对第一个字节(操作码)下内存访问断点,然后按下F9让它执行,接着点OK。看看我们中断在哪里:

代码:
6A37D153  MOV AL,BYTE PTR DS:[ESI]      ;中断在这句,开始读操作码了,注意esi的值为004019C4 6A37D155  INC ESI          ;使esi指向操作数 6A37D156  JMP DWORD PTR DS:[EAX*4+6A37DA58]    ;根据跳转地址表和操作码寻址解释单元


   F8往下走,我们来到这句伪指令的解释单元:

代码:
6A37D39F  MOVSX EDI,WORD PTR DS:[ESI]      ;把字操作数带符号扩展到edi 6A37D3A2  ADD ESI,2          ;esi指向下一句伪指令的操作码 6A37D3A5  MOV WORD PTR DS:[EDI+EBP],0A      ;ebp显然是程序堆栈区某处的基址,但不是堆栈顶的指针,它把0A保存到edi指向的偏移地址处 6A37D3AB  MOV DWORD PTR DS:[EDI+EBP+8],80020004    ;向下8个字节处存入80020004,根据VBParser的说明,这个数字表示空参数(缺省参数),事实上我在源代码中确实没有提供这个参数 6A37D3B3  ADD EDI,EBP          ;这次edi得到0A所在的虚拟地址 6A37D3B5  PUSH EDI          ;在堆栈中压入这个虚拟地址 6A37D3B6  XOR EAX,EAX          ;清空eax,准备读取下一句伪指令 6A37D3B8  MOV AL,BYTE PTR DS:[ESI]      ;读取下一句伪指令的操作码 6A37D3BA  INC ESI          ;esi指向下一句伪指令的次级操作码或操作数 6A37D3BB  JMP DWORD PTR DS:[EAX*4+6A37DA58]    ;根据地址跳转表和操作码寻址解释单元


   我们来看一下这些指令执行完以后的堆栈:

代码:
0012F458   0012F494          ;栈顶 0012F45C   00000000 0012F460   00000000 …………………… …………………… 0012F488   00000000 0012F48C   00000000 0012F490   00000000 0012F494   0000000A          ;这就是刚才压入栈顶的数据了 0012F498   00000000 0012F49C   80020004 0012F4A0   00000000


   现在这个伪指令的动作很清楚了,LitVar_Missing执行以后,把一个虚拟地址压入堆栈,这个虚拟地址指向0000000A,00000000,80020004。实际上,这句伪指令的功能就是在堆栈中提供一个空参数,其堆栈完全没有参考价值。但我要据此说明的是,对所有的伪指令,我们都将使用这种方法来跟踪。在下面的说明中我将省略对虚拟机伪代码读取引擎的注释,因为这部分都是一样的。
下面我们来看看004019D3处的伪指令LitVarStr。老规矩,在004019D3处设内存访问断点,F9中断在下面的地方:

代码:
6A37D3B8  MOV AL,BYTE PTR DS:[ESI]      ;esi=004019D3 6A37D3BA  INC ESI 6A37D3BB  JMP DWORD PTR DS:[EAX*4+6A37DA58]


   执行跳转来到:

代码:
6A37D3C2  MOVSX EDI,WORD PTR DS:[ESI]      ;第一个字操作数FF4C(堆栈区偏移量)带符号扩展到edi 6A37D3C5  MOVZX EAX,WORD PTR DS:[ESI+2]      ;第二个字操作数0000(数据区偏移量)无符号扩展到eax 6A37D3C9  MOV EDX,DWORD PTR SS:[EBP-54]      ;根据下一句指令来看,这是P-code程序数据区的基址 6A37D3CC  MOV EAX,DWORD PTR DS:[EDX+EAX*4]    ;根据eax产生偏移量,取得数据区的数据,这里我们看到eax最后取得   一个虚拟地址,指向Unicode字符串"Input" 6A37D3CF  ADD EDI,EBP          ;edi(堆栈区偏移量)指向堆栈区即将保存数据的地方 6A37D3D1  MOV WORD PTR DS:[EDI],8      ;存入表示类型的数据 6A37D3D6  MOV DWORD PTR DS:[EDI+8],EAX      ;向下偏移8个字节处存入指向Unicode字符串"Input"的虚拟地址 6A37D3D9  PUSH EDI          ;最后堆栈区数据指针入栈 6A37D3DA  XOR EAX,EAX 6A37D3DC  MOV AL,BYTE PTR DS:[ESI+4] 6A37D3DF  ADD ESI,5 6A37D3E2  JMP DWORD PTR DS:[EAX*4+6A37DA58]


   同样地,有必要观察一下堆栈:

代码:
0012F444   0012F544          ;栈顶 0012F448   0012F514          ;下面是前面其他指令形成地堆栈 0012F44C   0012F4F4 0012F450   0012F4D4 0012F454   0012F4B4 0012F458   0012F494 0012F45C   00000000 …………………… …………………… 0012F540   00000000 0012F544   00000008 0012F548   00000000 0012F54C   00401434  UNICODE "Input" 0012F550   00000000


   结合上述跟踪,LitVarStr伪指令操作数的观察方法就很明显了:首先在dump窗口观察0012F544处的内容,向后移8个字节,得到虚拟地址00401434,再从dump窗口观察00401434处的内容,就是入栈的字符串参数了。
相应地,下面我们在WKTVBDebugger中演示一下操作的过程:
1.WKTVBDebugger加载VB P-code.exe;
2.在Form Manager中对Command1控件设断点;
3.点击OK;
4.WKTVBDebugger中断在下面的地方:

代码:
004019C4: 27 LitVar_Missing 0012F474h 004019C7: 27 LitVar_Missing 0012F494h 004019CA: 27 LitVar_Missing 0012F4B4h 004019CD: 27 LitVar_Missing 0012F4D4h 004019D0: 27 LitVar_Missing 0012F4F4h 004019D3: 3A LitVarStr 'Input'    ;这句就是我们在OllyDBG中跟踪的LitVarStr伪指令 004019D8: 4E FStVarCopyObj 0012F514h 004019DB: 04 FLdRfVar 0012F514h 004019DE: 3A LitVarStr 'Please input an integer'     004019E3: 4E FStVarCopyObj 0012F534h 004019E6: 04 FLdRfVar 0012F534h 004019E9: 0B ImpAdCallI2 rtcInputBox on address 73472265h


5.注意我所注释的这句伪指令,当我们单步走过这句指令时,右上角堆栈窗口显示如下(为了便于观察,在右侧的单选框中选择DWORD):

代码:
0012F424: 0012F524 0012F4F4      ;注意这里的栈顶0012F524 0012F41C: 0012FE3C 0000004E …………………… …………………… 0012F3B4: 00000000 00000000 0012F3AC: 77E6780F 0000008C


6.按下Ctrl+M打开转存窗口,在Address to Dump组合框中输入0012F524,我们看到:

代码:
0012F524:08 00 00 00 00 00 00 00 0012F52C:34 14 40 00 00 00 00 00    ;好了,还记得那个8个字节的偏移吗?00401434就是我们需要的那个数据区字符串的指针了! 0012F534:00 00 00 00 00 00 00 00


7.记下这个指针,输入到Address to Dump组合框,现在这个Unicode字符串终于露出了真面目:

代码:
00401434:49 00 6E 00 70 00 75 00         I.n.p.u. 0040143C:74 00 00 00 00 00 00 00         t....... 00401444:00 00 00 00 E1 4E AD 33         ....酦?


   当然,就这个指令本身而言,WKTVBDebugger已经在伪指令窗口中给出了其操作数,所以我们要观察这个字符串大可不必那么麻烦,但是对于其他没有注释的伪指令,这恐怕是唯一的办法了。

   由此可见,要熟练地调试VB P-code程序,整理出全部伪指令操作数的寻址方式是必要的一步。在OllyDBG和WKTVBDebugger面前,P-code伪指令并不神秘,它们只不过是一些人为定义的符号罢了。在WKTVBDebugger中,伪指令像是一个黑盒子,虚拟机隐藏了黑盒子的全部秘密。既然虚拟机的内部对OllyDBG是可见的,P-code伪指令又怎会遥不可及呢?下期为大家献上《VB P-code -- 最后的战役》,通过一个CrackMe的实战破解为我们的VB P-code之旅画上一个圆满的句号。