[原创]P-Code指令的误解[吹毛求疵]
最近在搞P-Code编码解码的引擎,于是对P-Code有了一些理解。
也发现了现在很多网络上关于P-Code文章的的一些错误。
首先,在昨天以前,我也一直认为P-Code只有一种形式的变长指令,在安于此生大牛的帖子里我也是这么说的,可是事实却不是如此。
目前关于P-Code指令格式的文章,国内好像基本都是转载国外的。
而国外的好像也都是同一出处,应该都是出自wktvbdebugger目录下那个chm文档。
那个文档里有应该算是当时最全面的P-Code指令的介绍了。我得承认wktvbdbg的作者真的很牛。
但是人都会犯错,大牛也不例外,今天在研究我那个引擎解析P-Code指令参数时,发现指令长度跟vbvm实际取的参数不一致。
起先一直认为是我错了。研究了好久,才发现是wktvbdbg的作者错了。先把代码发上来看看:

661007BF > /0FB706 movzx eax, word ptr ds:[esi] ; 取第2个WORD参数 661007C2 . |83C6 02 add esi, 0x2 ; 增加指令指针 661007C5 . |42 inc edx 661007C6 . |74 05 je short msvbvm60.661007CD 661007C8 . |8B75 A8 mov esi, dword ptr ss:[ebp-0x58] 661007CB . |03F0 add esi, eax 661007CD > |33C0 xor eax, eax ; eax置零 661007CF . |8A06 mov al, byte ptr ds:[esi] ; 取下个操作码 661007D1 . |46 inc esi ; 自增指令指针 661007D2 . |FF2485 5CD40F66 jmp dword ptr ds:[eax*4+0x660FD45C] ; 跳转到处理函数 661007D9 > |BB 9B631066 mov ebx, msvbvm60.6610639B 661007DE .^|EB D2 jmp short msvbvm60.661007B2 661007E0 > |BB CE5E1066 mov ebx, msvbvm60.__vbaForEachCollVar 661007E5 .^|EB CB jmp short msvbvm60.661007B2 661007E7 > |BB E7631066 mov ebx, msvbvm60.661063E7 ; ebx放入函数地址 661007EC > |0FBF06 movsx eax, word ptr ds:[esi] ; 取第1个WORD参数 661007EF . |83C6 02 add esi, 0x2 ; 增加指令指针 661007F2 . |03C5 add eax, ebp 661007F4 . |50 push eax 661007F5 . |FFD3 call ebx 661007F7 . |8BD0 mov edx, eax 661007F9 . |F7D2 not edx 661007FB .^ EB C2 jmp short msvbvm60.661007BF ; 无条件跳转
这段代码是vbvm处理NextEachVar指令的函数(函数入口:0x661007E7)。
NextEachVar指令前缀是:FE 8C。
关于当前指令的长度,可以在wktvbdbg文档中的Lead3 Prefix 里找到。
8Ch NextEachVar 9
文档中说这条指令长9 Bytes。
这里的9 Bytes不是实际的长度。
p-code指令格式:
<标准指令前缀> [扩展指令前缀] [定参] [变参]
因为NextEachVar是扩展指令,所以这里描述的9 Bytes是从扩展指令前缀开始算起的。
也就是说NextEachVar实际长10 Bytes(1 Byte标准指令前缀 + 9 Bytes)。
但是看上面的代码,很明显VB虚拟机只读取了2个WORD,合计4 Bytes。
加上1 Byte的扩展指令前缀,也就5 Bytes,这样算的话是文档中写错了。
可能有人会说在661007F5还call ebx了。这里万一取了4 Bytes呢?
在这里我也不想证明了,直接上个
VB Decompiler v3.6 has been released (Nov. 8, 2007)
What's new in this version:
- Displaying External API calls in Native Code
- New plugin LangeFree for editing language DLL name in VB programs (thanks to Executioner)
- All windows centered on the owner
- Methods parsing optimized
- Strings parser rewritten (now it also works with spaces after/before
strings, and with zero bytes in string)
- Decompilation of commands with length > 9 in mnemonics mode (P-Code)
- Calculation of VTable offset in P-Code for PropertyPages and WebClasses fixed
- Command length in P-Code (NextEachVar, ExitProcFrameCb, ExitProcFrameCbStack,
LateMemNamed*, LateIdNamed*, VarLateMemCallLdRfVar) fixed
- Decompilation of "On Error" command (with FFFE and FFFF parameters)
[URL="http://www.vb-decompiler.org/history.htm"]VB Decompiler History[/URL]
这些指令长度的错误,在VB Decompiler 3.6版中被修复,时间是2007年11月8号。
我测试的结果表示,Semi VB Decompiler、wktvbdbg、exdec、p32dasm等常用P-Code反汇编器全部都败给这些指令。
能正确识别出这些指令实际长度的反汇编器只有VB Decompiler(商业软件就是不一样
)。这些指令前几个就不讲解了,上面也介绍了一个,而且这些都是固定长度的,修改一下就好了。
重要的是LateMemNamed*、LateIdNamed*这两种指令,这两种是新的变长指令。
0xFE前缀: LateMemNamed* : /*A6h*/ LateMemNamedCall /*A7h*/ LateMemNamedCallLdVar /*A8h*/ LateMemNamedCallStVar /*A9h*/ LateMemNamedStAd LateIdNamed* : /*AAh*/ LateIdNamedCall /*ABh*/ LateIdNamedCallLdVar /*ACh*/ LateIdNamedCallStVar /*ADh*/ LateIdNamedStAd
先看LateMemNamedCall在wktvbdbg文档中的定义:
A6h LateMemNamedCall 7
LateMemNamedCall被定义成7 Bytes长,再说下变长指令:
在文档中变长指令长度被定义成-1,如下所示:
32h FFreeStr -1
而wktvbdbg的文档中也只有这一种变长指令。
这种变长指令格式如下:
<指令前缀 1或2 Byte(s)> [尾部长度 1个WORD] [变参 WORD数组,长度由尾部长度定义]
这里的尾部长度描述的起始地址是:1或2 + 2的地址,简单点说就是尾部长度后面的成员的起始地址。
正常情况下尾部长度一定能被2整除,如果有余数会被vbvm忽略。
我的分析结果是LateMemNamedCall指令也是变长的,但是这里LateMemNamedCall指令与-1表示的变长指令结构是不一样的,先上代码:

66101167 > 0FB716 movzx edx, word ptr ds:[esi] ; 后缀长度 6610116A . |0FB746 02 movzx eax, word ptr ds:[esi+0x2] 6610116E . |83EA 04 sub edx, 0x4 ; 后缀长度-4 66101171 . |0FB75E 04 movzx ebx, word ptr ds:[esi+0x4] 66101175 . |83C6 06 add esi, 0x6 ; 增加指令指针 66101178 . |33FF xor edi, edi 6610117A . |B9 01000000 mov ecx, 0x1 6610117F > |57 push edi 66101180 . |D1EA shr edx, 1 ; 后缀长度右移1位 (后缀长度 / 2) 66101182 . |52 push edx 66101183 . |56 push esi 66101184 . |D1E2 shl edx, 1 ; 后缀长度左移1位 (后缀长度 * 2) 66101186 . |03F2 add esi, edx ; 指令指针+后缀长度 66101188 . |53 push ebx 66101189 . |8BD4 mov edx, esp 6610118B . |83C2 10 add edx, 0x10 6610118E . |52 push edx 6610118F . |51 push ecx 66101190 . |FF75 AC push dword ptr ss:[ebp-0x54] 66101193 . |50 push eax 66101194 . |FF75 B4 push dword ptr ss:[ebp-0x4C] 66101197 . |E8 EC6D0000 call msvbvm60.66107F88 6610119C .^|E9 19FDFFFF jmp msvbvm60.66100EBA
以上代码就是vbvm处理LateMemNamedCall指令的函数(函数入口:0x66101167)。
从代码可以分析出LateMemNamedCall指令的格式:
2字节指令前缀 2字节后缀长度 2字节参数1 2字节参数2 变参 具体如下:
std lead tlen wd1 wd2 变参
FE A6 0006h 0000h 0000h ......
其中后缀长度(tlen)描述的大小是从Word1(wd1)开始算起的地址到变参结束的大小。
这里假定tlen=6,这里的6 Bytes包括了wd1、wd2,所以在6610116E要把tlen-4。
因为在66101175,已经算过定参的长度了。
tlen-4=2,所以这里判定变参为1个Word(2 Bytes),LateMemNamedCall总长为10 Bytes。
你也许想问如果tlen为0会怎么样呢?那样tlen-4后,edx=0xFFFFFFFC(-4)。
这样会导致指令指针指向已经处理过的数据。
剩下的变长指令就不一一列举了,个别的如LateMemNamedCallLdVar,定参不一样而已。
以上是本人的分析结果,如有错误,欢迎指出!
