Shark恒 发表于 2015-1-20 16:43

使用OllyDbg从零开始Cracking 第二十九章-P-CODE-Part1

                        第二十九章-P-CODE-Part1(本章的CrackMe需要支持库MSVBVM50.DLL)前面章节我们已经介绍了Visual Basic逆向相关的基础知识,如果大家还想更加深入的研究VB应用程序逆向的话,可以继续学习高级篇中的关于VB逆向的相关内容。(PS:就是我打包上传到百度网盘的西班牙逆向文集系列)COCO写的VB逆向教程就比较好,其中可能会涉及到比较复杂的处理技巧,拿来练习做好不过了。还有一些其他优秀教程可供我们更加深入的研究VB Cracking。接着下来我们将讨论下一个话题-P-CODE。我们知道VB编写的应用程序有两种编译方式:一种是Native方式,我们前面章节已经讨论过了,两一种就是P-CODE方式-我们接下来将讨论的话题。Native编译的代码和P-CODE的主要区别在于:Native是直接执行代码段中的代码的。而对于P-CODE,如果我们使用之前那个Patch过的OD加载它,并且给代码段设置内存访问断点的话(实际上是内存执行断点),除非是直接调用API函数,否则不会断下来。说明,该区段没有直接可供CPU执行的代码。P-code,实际上是一组自定义的指令集,必须通过基于堆栈的虚拟机翻译为80X86上的指令集才能执行,即通过msvbvm50.dll和msvbvm60.dll这两个动态库来解释执行。也可以理解为通过P-CODE告诉虚拟机将要进行什么操作。例如:1e表示 执行无条件跳转(JMP)1e意味着执行无条件跳转,该无条件跳转是通过虚拟机(msvbvm50.dll,msvbvm60.dll)来执行的,也就是说P-CODE程序会读取代码段中的值,然后由这些值来告诉虚拟机需要执行什么操作。这也就是为什么我们说P-CODE程序的代码段中没有可执行代码的缘故。现在就让我们一起拿起“手术刀”来跟踪,剖析一个CrackMe的P-CODE的奥秘吧。我们实验的这个CrackMe名字叫做clave1,这个CrackMe是我朋友JB DUC写的,用来介绍P-CODE相关的内容正好合适,我们需要找到该CrackMe的正确序列号。很多人调试P-CODE可能喜欢用WKT,如果大家想了解WKT怎么调试P-CODE的话,可以看JB DUC关于P-CODE的逆向教程,我们这里还是用OllyDbg来调试了,由于微软官方并没有提供操作码的清单,所以还会用到另外一个工具EXDEC-这个工具可以识别操作码的名称。我们使用原版的OD,配置好反反调试插件,加载该CrackMe。可以看到停在了入口点处。说明一点,之前介绍的用于剔除Native程序的NAG窗口的4C法同样适用于P-CODE。现在我们来看看跟Native的不同的地方:我们从入口点往下看,会发现没有几行代码。我们只看到了大片的字节码,这里不要试图去分析这些字节码(毫无意义),我们应该还记得对于Native方式编译的VB应用程序,入口点往下还会有大量的代码吧。也有相似的字节码,我们继续往下。这里我们可以看到大段的代码,而P-CODE的程序的话OllyDbg可能也会显示处少量的代码,OD识别成了指令,但其实这些地方也是纯字节码。我们继续来看刚刚那个P-CODE的CrackMe。另外一个特征就是MethCallEngine这个API函数,该函数我们在P-CODE方式编译的VB应用程序中都能看到,所以我们甄别一个VB应用程序是不是以P-CODE方式编译的,一般有两步:1:看代码段中入口点以下是不是大片的字节码 2:看有没有MethCallEngine这个函数。好了,现在我们来看看字符串列表中有没有什么有价值的字符串。貌似没看到什么有用的字符串。好,那我们直接给JMPMethCallEngine这一行下一个断点吧。接下来我们选中JMP MethCallEngine这一行,单击鼠标右键选择-Follow,转入MethCallEngine内部,接着在MethCallEngine入口点处设置一个断点。运行起来。弹出了注册窗口,等待我们输入序列号。对于Native的VB程序,我们可以断API函数,但是P-CODE就搞不定了,但4C法对P-CODE程序依然有用。现在我们随便输入一个错误的序列号。我们单击Registrar(西班牙语:注册)按钮。断在了JMP MethCallEngine处,MethCallEngine函数对P-CODE进行初始化。我们继续运行,断在了MethCallEngine的入口处,我们来看看它做了些什么。我们可以看到跳转到了7413D243处。现在我们用ExDec打开该CrackMe。ExDec是一款专门针对P-CODE的反编译器。我们来看看它显示了些什么。我们可以看到将被读取的第一个字节是04,位于401BD0地址处,应该在第一次读取代码段指令的附近,我们可以给该字节设置一个内存访问断点。我们单击Registrar按钮后就会断在了MethCallEngine处,我们给代码段设置内存访问断点。运行起来的话,将断在了读取代码段的指令处,读取401BD0内存单元中04的指令应该就在附近,所以我们接着给该字节设置内存访问断点,继续运行,就能马上定位到。我们按F9运行起来。断了下来,这个时候我们给刚刚那个04字节设置内存访问断点,运行起来,又断了下来,我们可以看到ESI指向的就是401BD0内存单元。(PS:这里下断点的顺序我换了次序,一次就可以定位到,作者10次才定位到)这里读取的04字节值保存到AL中,这里是读取到的第一个P-CODE操作码。接着我们来看看ExDec中显示的其他操作码。我们将ExDec跟OD的数据窗口显示的内容对应起来看,会发现这些操作码并不连续,这是因为中间夹杂着操作码需要的参数。正如你所看到的,这里正在读取第一个字节。我们可以看到当前ESI指向了401BD0,下一行,ESI值递增1,以便读取操作码的参数。接着我们就到了间接跳转JMP指令这里,这一行将去执行这个操作码(我们在ExDec中看到的04)。我们可以看到一个陌生的操作码。这里我们可以看到将执行操作码04(即FLdRfVar),就只有几行代码,也没实现什么很神奇的操作,嘿嘿。还可以看到XOREAX,EAX,然后就是读取后面操作码。这里首先读取紧跟在04后面两个字节的参数。通过MOVSX指令将FF74(这是个负数,前面汇编章节介绍过)保存到EAX中,我们继续跟踪。EAX的值为-8C(十六进制),我们双击EAX值的话可以看到:我们可以看到FF74对应的十进制是-140也就是十六进制的-8C。我们可以看到ExDec中显示的是8C。接下来一行,操作码的参数值被加上EBP寄存器的值。接下来一行使用PUSH指令将刚刚运算的结果压入堆栈。这里相当于PUSH EBP - 8C (EBP - 8C:标识着堆栈中的局部变量),我的机器上,EBP的值为12F4E0,减去8C就得到了12F454。即当前EAX中的值。也就是使用PUSH指令压入堆栈中的值。好,我们继续往下跟。我们可以看到通过XOR EAX,EAX指令将EAX清零了,这就意味着操作码被清零了,该操作完成了,重置寄存器的值,然后接下来一行就可以读取下一个操作码了。第二个操作码是21,在接下来的一行读取它。操作码跟之前一样依然被是保存在AL。现在ESI被加上3,指向当前操作码的参数,接着通过间接跳转JMP去执行操作码21。我们来Google一下它的含义。好,这里我们可以看到有些前辈做了注释,虽然我们不知道它具体是干什么用的,但是根据字面的意思来理解就是加载一个指针,并且指向一个数据项。我们继续往下跟。这里是将EBP + 8指向内存单元的内容读取出来并保存到EBP - 4C指向的内存单元中。这个值在我的机器上是15B000,我们在数据窗口中定位到这个地址。该地址中保存的是4022E8,我们继续在数据窗口中定位到4022E8。这里我们可以推断出15B000其实是一个指针。该指针指向了一张表,虽然对我们的逆向起不到什么实质性的帮助,但起码我们还是看出一点门道了。还有一点就是可以看出该操作码没有参数。我们继续跟。这里EAX又被清零了,下一行读取第三个操作码。从ExDec中我们可以看出该操作码是0F。VcallAd以上是0F这个操作码具体的解释,我们可以看到它有一个占两个字节的参数。该参数我这里显示的0300,其表示句柄表中数据元素的偏移。接下来是一个间接跳转JMP,我们跟进去看看。又是读取EBP - 4C的内容,保存到EBX中。这里我们可以看到是15B000,并使用PUSH指令压入到堆栈中。接着是将参数值300保存到EAX中。这里EBX的值为15B000(我们已经知道了它指向了一张表),该表起始地址为4022E8,我们姑且将这张表称之为DescriptionItem Table。这里由该表的起始地址偏移300。表的起始地址偏移300就得到了4025E8,保存到EAX中。该值指向了表的这里。这里操作码0F就是根据参数值指明的偏移量来定位前一个操作码获取到的表中的数值。这里是使用CALL指令间接调用EAX内存单元中保存的值处。该值为7414C348,这里我们不跟进去,结果会被保存在堆栈中的。我们直接按F8键单步步过这个CALL。我们可以看到堆栈中保存了结果,我们需要弄明白它表示什么意思。接下来的操作码是19,参数值是88,代表一个局部变量。这里我们直接跟进。我们看到这里。通过MOVSX指令读取出占两个字节的参数值,将其保存到EAX中。FF开头表明该参数值是一个负数。该值对应的十六进制为-88,跟ExDec中显示的刚好对应起来了。十进制的-139正好等于十六进制的-88。接着ESI 加2,然后刚刚计算出的-88加上EBP的值,即将EBP - 88保存到EAX中。这里我们可以看到到达了一个CALL处,根据堆栈的来看其有三个参数。第一个参数是前一个操作码执行的结果,第二个参数我这里是12F458,即EBP - 88-表示一个局部变量。第三个参数是-1。这里我们不跟进这个CALL,直接按F8键单步步过这个CALL,看看会发生什么。我们会发现堆栈发生了变化,ECX被清零了。EBP-88内存单元保存了前一个操作码执行的结果。接下来一个操作码是08,它也将局部变量EBP -88作为参数。我们跟进这个JMP。这下面并不是我们之前看到的XOR EAX,EAX结束,而是OR EAX,EAX,接着使用条件跳转判断EAX是否为零。我们来看看它具体干了些什么。首先将操作码的参数FF78保存到EAX中,注意这里使用的是MOVSX,FF开头表示是负数,十六进制值为-88。这一行是将EAX + EBP指向内存单元的值保存到EAX中,即EBP - 88这个局部变量的值。这里判断EAX是否为零,如果为零就跳转到74145A15地址处。如果不为零就继续往下执行。这里将EAX的值保存到EBP -4C中。我们应该还记得之前读取EBP + 8的内容,接着将其保存到EBP - 4C中。所以说EBP - 4C的值不为零。所以我们将EBP - 4C称为指针数据元素。接下来是下一个操作码。这里我们来Google一下这个操作码0dVCallHresult。表示获取文本框中输入的文本。这里将读取我们输入的错误序列号,我们继续跟,看看是不是这样。我们可以看到该操作码跟之前一样还是以XOR EAX,EAX结束。首先读取EBP - 4C的内容(指向数据项的指针)保存到EAX中。接下来将这个值压入堆栈。接着读取操作码的参数。这里将参数值保存到EDI中。然后读取EAX指向的内存单元的内容,这是另一个表的起始内容。我们看到该表的00A0偏移处。这里依然是间接CALL表中内容,我们不跟进这个CALL,直接按F8键单步步过这个CALL,然后看堆栈的结果。我们按F8键执行这个CALL。接着EBP - 44的内容保存到EDX中。这里判断某个值,接着读取下一个操作码。这里你可能会问读取的是什么,是我们输入的错误序列号吗?我们看看ExDec先。我们可以看到该操作码的参数是8C,也就是EBP - 8C,我们看看EBP - 8C的值是多少。这里我们可以看到是12F454。其保存的是15D3BC是我们输入的错误序列号的指针。嘿嘿,终于找到了我们输入的错误序列号。接下来一个操作码是6C ILdRf。这里的解释是该操作码加载一个引用的值。我们跟进这个操作码看看。这里。这里通过MOVSX指令读取参数的值,是个负数。参数是FF74,所以保存到EAX中是:对应的十六进制是-8C。这里将EAX + EBP指向的内容压入堆栈,实际上是将EBP - 8C的内容压入堆栈。这里我们可以看到EBP - 8C指向了我们输入的错误序列号,堆栈中也保存了这个指针。我们在数据窗口中清楚地看到指向了我们输入的错误序列号。下一个操作码是:1b LitStr,根据字面上的意思来理解是”字符串”。我们来看看它会干些什么。我们跟进该操作码。可以看到参数为0001,将被保存到EAX中,是个正数。接下来一行我们可以看到4017E4被保存到了EDX中,这个值是什么,我们暂时还无从知晓。压入这个4016F8是干嘛的呢?ExDec中只显示了两个单引号,表明将一个空字符串压入堆栈。通过数据窗口我们也能看出是一个空字符串,即当我们单击注册按钮时,下一个操作码会检查我们输入的是否为空。ExDec中显示如下:这里Lead0是第一个操作,30EqStr是第二个操作。我们来Google一下它的意思。Lead0/30 EqStr - 比较两个字符串。也就是说这里将比较两个字符串。这是一个双操作码的操作。第一个操作码的操作数是FB,我们跟进这个操作码。这里直接以XOR EAX,EAX结束,什么也没做,接着读取第二个操作码。第二个操作码是30,接着读取参数。操作执行完后以XOREAX,EAX结束。我们跟进这个操作码。这第二个操作码PUSH 0。接下来是一个CALL,有三个参数。我们按F8键单步步过这个CALL,看看会发生什么。堆栈移动了,里面值没有变,只是堆栈被抬高了。下一行CMP AL,0,这里AL保存的是上一个CALL的结果,我这里的值是:AL = 01表示两个字符串不相等。比较完以后首先将EAX置零。如果刚刚比较的结果不为零,就将零压入堆栈,如果比较的结果相等就将FFFFFFFF压入堆栈。说明在做检查,嘿嘿。我们接着看下一个操作码。我们Google一下它的意思会发现跟SysFreeString类似,就是释放字符串所占的内存空间。我们可以看到这里要释放的内存空间是EBP - 8C。这里首先将EDX赋值为1,接着通过MOVSX将参数值保存到EBX中,接着将EBX - FF74(十六进制的-8C)压入堆栈。这里EBX + EBP即EBP - 8C,所以压入堆栈的是错误的序列号。这里我们可以看到下面的CALL里面会调用一个API函数SysFreeString,然后返回。这个时候我们输入的错误序列号被清空了。这里我们输入的错误系列号其实还在15D3BC这个内存单元中,只不过它的指针EBP - 8C被清空了而已。接着看下一个操作码。这里将清除局部变量EBP - 88的内容。EBP - 88的值是多少?这不是表中的数据项吗,将被清除,我们接着往下看。这里是获取操作码的参数。FF78对应十六进制的-88。这里又是将EBP - 88的内容保存到EAX中,接着判断它是否为零。这里不为零,我们继续。这里调用这个CALL释放EBP - 88局部变量的内存空间。这里EAX被清零了。下一个操作码是:该操作码是一个条件跳转,所有的Branch开头的都是跳转操作:指令                   操作码                      跳转条件Branch               1e                        无条件跳转BranchF                1c                        栈顶数据为false则跳转BranchT                1d                        栈顶数据为True则跳转这是一个条件跳转操作,如果栈顶数据为假就跳转,这里检查文本框中的序列号是否正确。如果跳转了的话,下个操作码将是:这里的条件跳转直接越过了401BF3处的无条件跳转。这个操作就结束了,我们继续看下一个。这里到达了401BF6这个分支,条件跳转成立了,越过了401BF3处的无条件跳转,嘿嘿。Lead3/c1 LitVarl4这是个双操作码的操作,我们来看一看。这里第一个操作码结束了,继续读取第二个操作码。我们继续跟踪。这里读取操作码的参数。FF54以FF开头,所以是一个负数。这次参数占4个字节,被保存到EAX中。这是一个局部变量。EBP + FFFFFF54 + 8即第一个参数加上8,结果是12F43C,参数的值被保存到12F43C中。接着我们跳转到了这里。这里将12F434压入堆栈。12F434是一个结构体的指针。而这个结构体里面又保存了其他3个结构体。我们跟到了这里,嘿嘿。如果该CrackMe采用的是硬编码的话,我们可以切换到小数形式,看看是不是正确的序列号。我们直接再打开一个这个CrackMe。我们在OD中看看十进制值为多少。我们可以看到十进制值为246810,我们在CrackMe中输入它。嘿嘿,提示输入的序列号正确。我们接着看比较的过程。下一个操作是双操作码。第一个操作码是FC,我们跟进这个操作码,直接就结束了,接着读取第二个操作码。该操作码是F6,我们跟进去,ExDec中显示的是EBP - 9C,即local_009c。这里读取参数,是个负数。对应十六进制的-9C。这里EBP + EBX,即EBP - 9C,值为12F444,这里面是空的。这里判断是否小于8,如果小于8则跳转。接着又跳转,这里我们就不深究了,直接看到该操作码的最后一行。这里将3保存到EBP - 9C中。执行后。下一行拷贝这里的内容到EBP - 9C中。EBP - 9C结构里保存了正确的序列号,还有这个数字3。接着看下一个操作码。这里跟前面的介绍基本上是一样的了。相同的地方我们直接略过,直接看到我们还没有跟过的操作码。这里我们跳过前面的,直接给401C17这个操作码设置一个内存访问断点,当读取到这个操作码的时候就会断下来。断在了这里,这里读取操作码0A,我们看看ExDec中显示的:ImpAdCallFPR4,表示调用一个API函数。例如:4017F5: 0aImpAdCallFPR4:   _rtcMsgBox这个例子是调用__rtcMsgBox这个API函数。再来看401C17: 0aImpAdCallFPR4: _rtcR8ValFromBstr这里读取该操作码的参数。参数被保存到ECX中。EAX被赋值为401000,接着判断EAX是否为零。接着读取第二个参数。我们到了CALL EAX这里,此时EAX值为401000,我们看看这个地址是哪个API函数。传递给该函数的参数在堆栈中:可以看到是我们输入的错误序列号。这里该操作序列号被装载到了浮点寄存器ST0中去了,浮点寄存器我们还没有介绍过,下面还有一些寄存器。如果你看不到浮点寄存器的话,你可以在寄存器窗口中单击鼠标右键选择-View FPU registers。加载我们错误序列号的位置在这:我们看到下一个操作码。401C1C:Lead2/6bCVarR8。我们跟进这个操作码。这是一个双操作码的操作,先读取第一个操作码,接着读取第二个。接下来,是一些浮点指令,我们还没有介绍过。但是我们可看到读取的参数。这里:EBP + EAX这里FSTP指令将ST0中的内容保存EAX+8指向的1内存单元中,即12F43C中。然后执行一次出栈操作。我们后面章节再详细讨论。执行以后:这里有可能是我们输入的错误序列号,我们将其转化为64位双精度小数看看,单击鼠标右键。可以看到正好是我们输入的错误序列号,但是这里占的是8个字节,即64位。从逻辑上来讲,一个占4个字节,32位,一个占8个字节,64位,看起来不一样。切换为正常模式显示。该指令将浮点寄存器的状态字保存到AX中,我们执行这一行。这里该操作就结束了。这个操作码我们不知道是干什么用的,我们还是跟一跟吧。这一行是将ESP指向的内容保存到EAX中。这个内存单元位于转换后的错误序列号的上面。下一个操作码这里将EBP - 9C压入堆栈:接下来又是一个双操作码的操作。这里读取第二个操作码。这里我们到了一个CALL处,堆栈中参数如下:其中一个指向了正确的序列号,一个指向了错误的序列号,将它们进行比较吗?这里我们下一个断点。我们可以看到这个CALL返回的结果EAX值为1。这里FFFFFFFF被压入堆栈,为了看到正确的序列号是多少,我们在比较这里设置一个断点。03C41A对应的十进制值为246810,我们在文本框中输入这个值。我们按下Registrar按钮,断了下来,我们看到比较处。246810对应的十六进制,可能会以不同格式存储,在不同的CALL中会被转化然后进行比较,我们看看结果是什么。我们可以看到EAX = 0。栈顶元素也被置零了。接下来的BranchF操作码就会根据栈顶元素值来决定显示什么提示框了。你可能会说怎么这么长啊,因为这里是初次介绍P-CODE,所以我们给大家逐一介绍了每个操作码,后面章节我们就不会这么赘述了。我们可以根据ExDec反编译器得知每个操作码的名称,然后用OllyDbg来定位调试。下一章节,我们将介绍clave2这个例子,大家可以先试试。
本系列文章汉化版转载看雪论坛
感谢原作者:RicardoNarvaja(西班牙人) 原作者个人主页:http://www.ricardonarvaja.info/
感谢热心翻译的朋友:1~3章译者:BGCoder4~58章译者:安于此生
全集配套程序下载地址:链接: http://pan.baidu.com/s/1eQzTWfo 密码: vytv


hackysh 发表于 2022-2-20 12:14


[快捷回复]-感谢楼主热心分享!

一道彩虹屁 发表于 2022-2-23 02:53

谢谢分享

不苦小和尚 发表于 2022-2-25 11:31


[快捷回复]-感谢楼主热心分享!

别管我了行 发表于 2022-3-2 03:41

zg2600 发表于 2022-7-10 11:18

[吾爱汇编论坛52HB.COM]-楼主,你是一个大好人!!nice,谢谢,给力非常感谢破解思路

天使替我爱你 发表于 2022-7-10 22:14

大佬腻害多谢大佬

zg2600 发表于 2022-7-26 11:23

[吾爱汇编论坛52HB.COM]-做的不错哦,楼主加油,期待更好的作品!

冷亦飞 发表于 2022-11-5 09:16

谢谢分享

曾经沧海 发表于 2022-11-19 17:38

感谢分享。正在试用。
页: [1] 2
查看完整版本: 第二十九章-P-CODE-Part1