多读书多实践,勤思考善领悟

VB6 P-code伪代码的解读

本文于2088天之前发表,文中内容可能已经过时。

解读

由于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,得到伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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。看看我们中断在哪里:

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

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

1
2
3
4
5
6
7
8
9
10
6A37D39F  MOVSX EDI,WORD PTR DS:[ESI]           ;把字操作数带符号扩展到edi
6A37D3A2 ADD ESI,2esi指向下一句伪指令的操作码
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 ESIesi指向下一句伪指令的次级操作码或操作数
6A37D3BB JMP DWORD PTR DS:[EAX*4+6A37DA58] ;根据地址跳转表和操作码寻址解释单元

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

1
2
3
4
5
6
7
8
9
10
11
12
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中断在下面的地方:

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

执行跳转来到:

1
2
3
4
5
6
7
8
9
10
11
12
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,EBPedi(堆栈区偏移量)指向堆栈区即将保存数据的地方
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]

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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中断在下面的地方:

1
2
3
4
5
6
7
8
9
10
11
12
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
  1. 注意我所注释的这句伪指令,当我们单步走过这句指令时,右上角堆栈窗口显示如下(为了便于观察,在右侧的单选框中选择DWORD):
1
2
3
4
5
6
0012F424: 0012F524 0012F4F4         ;注意这里的栈顶0012F524
0012F41C: 0012FE3C 0000004E
……………………
……………………
0012F3B4: 00000000 00000000
0012F3AC: 77E6780F 0000008C
  1. 按下Ctrl+M打开转存窗口,在Address to Dump组合框中输入0012F524,我们看到:
1
2
3
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
  1. 记下这个指针,输入到Address to Dump组合框,现在这个Unicode字符串终于露出了真面目:
1
2
3
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伪指令又怎会遥不可及呢?

例子

我选择了由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抓取):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
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码),还能以什么形式存在呢?说了那么多,就是希望大家不要丧失信心,过了这心理一关,其他什么都好办。下面继续我们的旅程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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!!!我们停在下面的地方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
7637D9AA  MOV AL,BYTE PTR DS:[ESI+2]            ;注意这里esi2=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的寄存器面板中看,这个形式不再令您困惑了吧!

如果您是使用SoftICE来跟踪的,那么就不必这么费神了:SoftICE中有一条专门的DUMP命令DL ADDRESS可以直接解析内存中的浮点数:)

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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: [email]CyberBlade@gmx.net[/email]
;你愿意吗:)
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

供您调试时参考:)