• 标 题:【原创】VB P-code -- 最后的战役
  • 作 者:cyclotron
  • 时 间:2004-12-26,11:05
  • 链 接:http://bbs.pediy.com

*岁末大盘点*
VB P-code -- 最后的战役
作者:cyclotron

   首先感谢您对这个VB P-code系列的关注。在前几期的文章中,我已经为大家详细介绍了关于对VB P-code程序动、静态结合的调试方法,相信大家对这部分内容已经非常熟悉了。如果您自己写过一些小程序进行调试研究,我相信您已经能够独立地完成本文提及的这个CrackMe的破解了。
   在本文中,我选择了由CyberBlade编写的一个中级VB P-code CrackMe作为范例来为大家介绍VB P-code程序的调试过程,希望能帮助大家熟悉一部分P-code伪指令,为将来的研究学习打下基础。由于我在前几期中已经讲解了对陌生的VB P-code伪指令的处理方法(用OllyDBG跟踪其解释过程),这次我将完全从伪代码的层面上对程序进行调试,也就是说,我将以伪指令为单位说明程序各部分所实现的功能。这种方法乍看起来可能不太直观,但是请相信,一旦您熟悉了这样的调试方法,将给您今后的学习带来极大的方便。
   好了,我们这就开始了。这次我首先使用Josephco的Exdec来生成该CrackMe的反编译代码,然后结合WKTVBDebugger和OllyDBG来进行调试。像往常一样,我们在WKTVBDebugger的Form Manager中对Check按钮下断点,记住这是个不同于其他程序调试的极其有效的断点方式,对于事件驱动的注册验证过程,这种断点是百分之百有效的。点下Check按钮以后,我们停在下面的代码上(从Exdec抓取):

代码:
Proc: 40e680 40E26C: 04 FLdRfVar                local_009C 40E26F: 21 FLdPrThis               40E270: 0f VCallAd                 text 40E273: 19 FStAdFunc               local_0098 40E276: 08 FLdPr                   local_0098 40E279: 0d VCallHresult            get__ipropTEXTEDIT  ;这是一个很常见的调用,其功能是取得编辑框的字符串("TEXTEDIT"已经透露了秘密),很明显,这里取得的就是我们输入的用户名了 40E27E: 6c ILdRf                   local_009C 40E281: 1b LitStr:                   40E284: Lead0/30 EqStr             40E286: 2f FFree1Str               local_009C 40E289: 1a FFree1Ad                local_0098 40E28C: 1c BranchF:                40E2C1    ;上面这一段检验输入的用户名是否为空,没有输入的话当然就直接fail了,注意Branch这个词表示分支,F表示False ……………… ……………… 40E2CE: 0d VCallHresult            get__ipropTEXTEDIT  ;又来了,这回是取试炼码了 40E2D3: 6c ILdRf                   local_009C 40E2D6: 1b LitStr:                   40E2D9: Lead0/30 EqStr             40E2DB: 2f FFree1Str               local_009C 40E2DE: 1a FFree1Ad                local_0098 40E2E1: 1c BranchF:                40E316    ;试炼码是否为空? 40E2E4: 27 LitVar_Missing          40E2E7: 27 LitVar_Missing          40E2EA: 3a LitVarStr:              ( local_00CC ) Error 40E2EF: 4e FStVarCopyObj           local_00DC 40E2F2: 04 FLdRfVar                local_00DC 40E2F5: f5 LitI4:                  0x40  64  (...@) 40E2FA: 3a LitVarStr:              ( local_00AC ) You have to enter a key first.               ;这个提示很熟悉吧? ……………… ……………… 继续往下走,还有一次关于试炼码合法性的初步检验,跟上面的差不多: 40E323: 0d VCallHresult            get__ipropTEXTEDIT  ;取试炼码 40E328: 6c ILdRf                   local_009C 40E32B: 1b LitStr:                   40E32E: Lead0/30 EqStr             40E330: 2f FFree1Str               local_009C 40E333: 1a FFree1Ad                local_0098 40E336: 1c BranchF:                40E36B    ;不足5位就走下去 40E339: 27 LitVar_Missing          40E33C: 27 LitVar_Missing          40E33F: 3a LitVarStr:              ( local_00CC ) Error 40E344: 4e FStVarCopyObj           local_00DC 40E347: 04 FLdRfVar                local_00DC 40E34A: f5 LitI4:                  0x40  64  (...@) 40E34F: 3a LitVarStr:              ( local_00AC ) You have to enter at least 5 chars. 40E354: 4e FStVarCopyObj           local_00BC    ;试炼码必须大于等于5位 40E357: 04 FLdRfVar                local_00BC   注意,下面要开始真正的计算了: 40E36B: 28 LitVarI2:               ( local_00EC ) 0x1  (1);立即数1入栈,Lit表示立即数 40E370: 04 FLdRfVar                local_012C    ;FLd表示压栈,这里保存的是循环计数器 40E373: 04 FLdRfVar                local_009C 40E376: 21 FLdPrThis               40E377: 0f VCallAd                 text 40E37A: 19 FStAdFunc               local_0098 40E37D: 08 FLdPr                   local_0098 40E380: 0d VCallHresult            get__ipropTEXTEDIT  ;再次读取用户名 40E385: 6c ILdRf                   local_009C 40E388: 4a FnLenStr                     ;取用户名的长度,这个伪指令是很常见的 40E389: Lead2/69 CVarI4            local_00CC    ;转换(C-Convert)为变体型(Var就是Variant),这里要利用这个长度作为循环次数 40E38D: 2f FFree1Str               local_009C 40E390: 1a FFree1Ad                local_0098 40E393: Lead3/68 ForVar:           (when done) 40E3F5  ;ForVar,呵呵,循环从这里开始了 40E399: 04 FLdRfVar                local_009C 40E39C: 21 FLdPrThis               40E39D: 0f VCallAd                 text 40E3A0: 19 FStAdFunc               local_0098 40E3A3: 08 FLdPr                   local_0098 40E3A6: 0d VCallHresult            get__ipropTEXTEDIT  ;再取用户名…… 40E3AB: 04 FLdRfVar                local_0094    ;这是用户名的指针--我怎么知道?用OllyDBG调试一下您也就知道了 40E3AE: 28 LitVarI2:               ( local_00DC ) 0x1  (1)  ;立即数1入栈 40E3B3: 04 FLdRfVar                local_012C    ;记得吗,刚才的那个循环计数器 40E3B6: Lead1/22 CI4Var                 ;转换为整型 40E3B8: 3e FLdZeroAd               local_009C    ;取得用户名字符串一级指针 40E3BB: 46 CVarStr                 local_00BC    ;转换为变体型 40E3BE: 04 FLdRfVar                local_00FC    ;用户名字符串指针入栈 40E3C1: 0a ImpAdCallFPR4:                 ;这句调用rtcMidCharVar,调试的时候您就可以看到了,用意很明显:每轮循环取用户名的一个字符 40E3C6: 04 FLdRfVar                local_00FC    ;取得的字母入栈 40E3C9: Lead2/fe CStrVarVal        local_0150    ;转换为字符串型 40E3CD: 0b ImpAdCallI2                   ;调用rtcAnsiValueBstr转换为ASCII码 40E3D2: 44 CVarI2                  local_00CC    ;再转换为变体型 40E3D5: Lead0/ef ConcatVar              ;将每轮循环得到的十进制数作为字符串相连接,假定输入是cyclotron,那么循环最后得到的就是"9912199108111116114111110",当然这个字符串在内存中是以Unicode的形式出现的 40E3D9: Lead1/f6 FStVar                 ;保存字符串(St-Save to) 40E3DD: 2f FFree1Str               local_0150 40E3E0: 1a FFree1Ad                local_0098 40E3E3: 36 FFreeVar 40E3EC: 04 FLdRfVar                local_012C 40E3EF: Lead3/7e NextStepVar:      (continue) 40E399  ;下一轮循环


   好了,上面就是这个CrackMe的第一个循环,看到这里是不是有点疲惫了?其实我第一次调试的时候也觉得这P-code不知所云,上面我给大家的提示都是反复斟酌、研究的结果,有些可以从伪指令的命名上猜出来,还有一些就不得不进入OllyDBG跟踪汇编指令的解释过程了。比如像ConcatVar这样奇怪的指令,用惯了C和Asm的朋友可能无法理解--这么复杂的一个操作居然是一个指令而不是一个调用!?很遗憾,这些疑问只有在OllyDBG中才能解决了。另外,大家可能注意到了P-code代码中大量的变量类型转换,一会儿从整型变成变体型,一会儿又从变体型变成字符串型。其实如果在OllyDBG里面跟踪一下就会发现,很多类型转换其实根本就是换汤不换料,什么都没动,就是从一个地方移动到另一个地方了,所以大家跟踪的时候千万不要被它们迷惑。其实仔细想想,一个字母在内存里面除了ASCII码(当然也包括Unicode码),还能以什么形式存在呢?说了那么多,就是希望大家不要丧失信心,过了这心理一关,其他什么都好办。下面继续我们的旅程:

代码:
40E3F5: 04 FLdRfVar                local_0094    ;这里是把前面循环得到的字符串入栈 40E3F8: Lead0/eb FnLenVar               ;取它的长度 40E3FC: 28 LitVarI2:               ( local_00AC ) 0x9  (9);立即数9入栈 40E401: 5d HardType                40E402: Lead0/74 GtVarBool              ;是否大于9? 40E404: 1c BranchF:                40E425 40E407: 04 FLdRfVar                local_0094 40E40A: Lead3/c4 LitVarR8               ;浮点立即数3.1415926540000000000入栈,R8就表示Real of 8 bytes,如何得到这个浮点立即数的精确值?这里有一个小技巧,我待会儿会给大家介绍:) 40E416: Lead0/bc DivVar                 ;Div--很明显是做除法,记住:先入栈的是被除数,后入栈的是除数,其他算术运算指令也遵循这个规则 40E41A: Lead0/e1 FnFixVar             ;对除法的结果取整,学过VB的朋友应该熟悉这个指令     40E41E: Lead1/f6 FStVar                 ;保存结果为保存为Variant型 40E422: 1e Branch:                 40e3f5 40E425: 04 FLdRfVar                local_0094 40E428: Lead3/c1 LitVarI4:         ( local_param_5678FF54 ) 0x30f85678  (821581432)               ;立即数0x30f85678入栈 40E430: Lead0/17 XorVar                 ;Xor--异或运算 40E434: Lead1/f6 FStVar                 ;保存为Variant型 40E438: 04 FLdRfVar                local_0094    ;前面的运算结果入栈 40E43B: 08 FLdPr                   local_param_0008 40E43E: 8a MemLdStr                     ;这里调入一个内存操作数0D8B3h,可以用OllyDBG跟踪一下看看 40E441: Lead2/69 CVarI4            local_00AC    ;转换为变体型 40E445: Lead0/9c SubVar                 ;两数相减 40E449: Lead1/f6 FStVar                 ;保存为Variant型,我们记这个结果为S


   上面这一系列算术运算结束了,现在我来讲讲40E40A处的指令。这个指令压入一个8字节的浮点数到堆栈中,我们知道浮点数在内存中是以特定规则的科学计数法保存的,虽然通过仔细分析其每个bit的内容,我们可以推算出这个浮点数的值,但是这样未免太麻烦了,而要写注册机的话,不可避免地要用到这个值,这里我们将采用一种变通的方法让OllyDBG来替我们完成这一任务。
   首先我们用OllyDBG载入CyberBlade.exe,在40E40A处下内存访问断点。然后像往常一样,让程序跑起来,我们填入用户名和试炼码后点Check按钮,BOOM!!!我们停在下面的地方:

代码:
7637D9AA  MOV AL,BYTE PTR DS:[ESI+2]      ;注意这里esi+2=40E40A,开始读取目标伪指令了 7637D9AD  ADD ESI,3 7637D9B0  JMP DWORD PTR DS:[EAX*4+7637ED94]    ;这里进入解释引擎 ……………… ……………… 7637ECAB  XOR EAX,EAX 7637ECAD  MOV AL,BYTE PTR DS:[ESI] 7637ECAF  INC ESI 7637ECB0  JMP DWORD PTR DS:[EAX*4+7637FD94]    ;次级跳转 ……………… ………………   下面是关键了: 7637DF07  MOV EBX,5 7637DF0C  MOVSX EDI,WORD PTR DS:[ESI] 7637DF0F  MOV WORD PTR DS:[EDI+EBP],BX 7637DF13  MOV EAX,DWORD PTR DS:[ESI+2]      ;取第一个DWORD 7637DF16  MOV DWORD PTR DS:[EDI+EBP+8],EAX    ;第一个DWORD存入堆栈 7637DF1A  MOV EAX,DWORD PTR DS:[ESI+6]      ;取第二个DWORD 7637DF1D  ADD ESI,0A 7637DF20  MOV DWORD PTR DS:[EDI+EBP+C],EAX    ;第二个DWORD存入堆栈 7637DF24  JMP SHORT MSVBVM50.7637DEE0      ;修改这条指令! 7637DF26  POP EAX 7637DF27  ADD WORD PTR SS:[ESP],AX 7637DF2B  JO MSVBVM50.7637DAC4 7637DF31  XOR EAX,EAX 7637DF33  MOV AL,BYTE PTR DS:[ESI] 7637DF35  INC ESI 7637DF36  JMP DWORD PTR DS:[EAX*4+7637ED94]


   从上面的指令中,我们看出虚拟机把8字节的浮点数保存到12F52C指向的堆栈空间中。如果大家学过协处理器指令的话,应该知道这句:

  FLD mem32/64/80

   这句指令的功能是把实数装入到st(0),st(0)是FPU的一个浮点数寄存器,而OllyDBG的寄存器面板恰好可以监视所有的浮点数寄存器。说到这里,大家也许都明白了,是的,我们只要在7637DF20后面汇编一条FLD指令把12F52C处的8个字节装入到st(0),就可以看到浮点数的精确值了!下面就是这条指令的具体格式:

  FLD QWORD PTR [0012F52C]

   执行上面的指令以后,我们在OllyDBG的寄存器面板中看看(图1),
这个形式不再令您困惑了吧!
 
   如果您是使用SoftICE来跟踪的,那么就不必这么费神了:SoftICE中有一条专门的DUMP命令DL ADDRESS可以直接解析内存中的浮点数:)

   上面这段运算结束后,我们又来到一个循环:

代码:
40E44D: 28 LitVarI2:               ( local_00EC ) 0x1  (1)  ;立即数1入栈,作为循环的初始值 40E452: 04 FLdRfVar                local_012C      ;计数器 40E455: 28 LitVarI2:               ( local_00CC ) 0xa  (10)  ;循环终止值10 40E45A: Lead3/68 ForVar:           (when done) 40E495    ;进入循环 40E460: 04 FLdRfVar                local_009C 40E463: 21 FLdPrThis               40E464: 0f VCallAd                 text 40E467: 19 FStAdFunc               local_0098 40E46A: 08 FLdPr                   local_0098 40E46D: 0d VCallHresult            get__ipropTEXTEDIT    ;取得试炼码 40E472: 6c ILdRf                   local_009C 40E475: 04 FLdRfVar                local_012C 40E478: Lead1/22 CI4Var                   ;转换为整型 40E47A: 08 FLdPr                   local_param_0008 40E47D: 06 MemLdRfVar              local_param_0034   40E480: 9e Ary1LdI4                       ;这里是从一个Unicode字符串数组依次取出一系列字符串--   UNICODE "373703670"   UNICODE "684708686"   UNICODE "698673531"   UNICODE "391184533"   UNICODE "329528230"   UNICODE "654824169"   UNICODE "557168731"   UNICODE "387375850"   UNICODE "212298498"   UNICODE "851143730" 40E481: Lead0/30 EqStr                    ;上面这些字符串依次同同输入的试炼码比较。先不要急着高兴,看看下面的代码就知道这是迷魂阵:( 40E483: 2f FFree1Str               local_009C 40E486: 1a FFree1Ad                local_0098 40E489: 1c BranchF:                40E48C      ;奇怪的跳转,比较结果为False就直接跳到下一句--不跳也罢…… 40E48C: 04 FLdRfVar                local_012C 40E48F: Lead3/7e NextStepVar:      (continue) 40E460


   几圈下来,一点收获也没有,我们只能继续跟踪:

代码:
40E4A2: 0d VCallHresult            get__ipropTEXTEDIT    ;再一次取试炼码 40E4A7: 3e FLdZeroAd               local_009C 40E4AA: 46 CVarStr                 local_00BC      ;转换为变体型 40E4AD: 04 FLdRfVar                local_0094      ;还记得吗?这是前面用户名的计算结果S 40E4B0: Lead0/9c SubVar                   ;试炼码减去这个计算值得到M 40E4B4: 04 FLdRfVar                local_0150 40E4B7: 21 FLdPrThis               40E4B8: 0f VCallAd                 text 40E4BB: 19 FStAdFunc               local_0174 40E4BE: 08 FLdPr                   local_0174 40E4C1: 0d VCallHresult            get__ipropTEXTEDIT    ;又取用户名 40E4C6: 6c ILdRf                   local_0150 40E4C9: 4a FnLenStr                       ;取用户名长度 40E4CA: Lead2/69 CVarI4            local_00AC      ;转换为变体型 40E4CE: 5d HardType                40E4CF: Lead0/33 EqVarBool                ;是否与前面的计算值M相等? 40E4D1: 2f FFree1Str               local_0150 40E4D4: 29 FFreeAd:  40E4DB: 35 FFree1Var               local_00BC 40E4DE: 1c BranchF:                40E55B      ;这回是真正的关键跳转了! 40E4E1: 27 LitVar_Missing          40E4E4: 27 LitVar_Missing          40E4E7: 3a LitVarStr:              ( local_00CC ) Correct key 40E4EC: 4e FStVarCopyObj           local_00DC 40E4EF: 04 FLdRfVar                local_00DC 40E4F2: f5 LitI4:                  0x40  64  (...@) 40E4F7: 3a LitVarStr:              ( local_00AC ) Wow, you have found a correct key!                 ;作者的褒奖:) 40E4FC: 4e FStVarCopyObj           local_00BC 40E4FF: 04 FLdRfVar                local_00BC 40E502: 0a ImpAdCallFPR4:            40E507: 36 FFreeVar 40E512: 27 LitVar_Missing          40E515: 27 LitVar_Missing          40E518: 3a LitVarStr:              ( local_00CC ) Correct key! 40E51D: 4e FStVarCopyObj           local_00DC 40E520: 04 FLdRfVar                local_00DC 40E523: f5 LitI4:                  0x40  64  (...@) 40E528: 3a LitVarStr:              ( local_00AC ) Mail me, how you got it: CyberBlade@gmx.net                  ;你愿意吗:) 40E52D: 4e FStVarCopyObj           local_00BC 40E530: 04 FLdRfVar                local_00BC 40E533: 0a ImpAdCallFPR4:


   好了,最后我们总结一下这个CrackMe的算法:
(1)  用户名必须大于等于5位;
(2)  把用户名所有字符的十进制ASCII码连接起来得到一个数;
(3)  把上面这个数除以3.1415926540000000000,取整以后与30F85678h异或,再减去0D8B3h;
(4)  试炼码减去上面这个值的结果必须等于用户名的长度,也就是说我们只要把上面这个值加上strlen(用户名)就得到了注册码。

我的注册信息是:
用户名:cyclotron
注册码:667574641
供您调试时参考:)

   正如本文的标题所言,以上就是我的VB P-code系列的最后一站。虽然我们只经历了一场战役,但是VB P-code程序的调试方法我已经全部介绍给大家了,希望大家在读了这四篇拙文以后能有所收获。喜欢的朋友可以沿着这条思路继续走下去,不喜欢的朋友也请多多包涵。总体来说,VB P-code作为微软的机密之一,其详细的技术资料实在是非常罕见的,本系列只是从我业余研究的角度考察VB P-code程序,不当之处在所难免,望高手予以指正。最后我要说的是,微软的下一代平台.NET中的IL(Intermediate Language)实际上也是类似于VB P-code的伪指令集,但是其技术文档要比VB P-code丰富得多,这给我们的研究带来了方便,但同时也对这一架构上可执行文件的有效保护带来极大的威胁。从这个角度来说,未公开的VB P-code机制恐怕就是我在本系列开篇所提到的VB P-code程序逆向、破解的难度所在吧。

附件:CyberBlade.2.rar