Preliminary Exploration of Software Reverse Engineering
对于 crackme 示例程序,我们选择 Bjanes Crackme V2.0a 序列号检测程序进行逆向。
一、项目结构分析
首先对程序界面进行初步了解:

由于此程序由 Visual Basic 语言编写,我们使用 VB Decompiler Lite 对此程序进行反编译,项目结构如图所示。

结合程序界面、反编译得到的项目结构及各部分代码,可以推测出:
Project/Forms/CrackmeV20a 对应主窗体
Project/Forms/Form1 对应 About 界面
因此,
Project/Code/CrackmeV20a 对应主窗体逻辑
Project/Code/Form1 对应 About 界面逻辑
接着分析主窗体的按钮,一共有Check it, Exit 和 About 三个,无输入直接点击 Check it 按钮弹出错误提示。因此,推测:
About_Click_403530 为 About 按钮逻辑函数
Command_1_Click_403620 和 Command_2_Click_403BC0 为 Check it 和 Exit 按钮逻辑函数
对这两个按钮的逻辑代码进行查看,在 Command_1_Click_403620 函数中发现:

如图,有进行 MsgBox 调用来输出错误信息的部分,因此 Command_1_Click_403620 为 Check it 按钮的逻辑函数,也是进行序列号判断的核心函数。
二、核心函数分析
对 Command_1_Click_403620 函数进行分析可以发现,函数结构可以被大体分为以下几个部分:
函数初始化和栈设置 (loc_00403620 - loc_0040366A)
1 2 3 4 5 6 7
| loc_00403620: push ebp ; 保存调用者的栈基址指针 loc_00403621: mov ebp, esp ; 设置新的栈基址指针 loc_00403623: sub esp, 0000000Ch ; 在栈上分配 12 字节空间 loc_00403626: push 00401116h ; 异常处理程序的地址 loc_0040362B: mov eax, fs:[00h] ; 获取当前 SEH 链 loc_00403631: push eax ; 保存当前 SEH 链 loc_00403632: mov fs:[00000000h], esp ; 设置新的 SEH 处理程序
|
这是标准的函数序言部分,设置了栈帧和结构化异常处理(SEH)。
1 2 3 4 5 6 7
| loc_00403639: sub esp, 000000E4h ; 为局部变量分配 228 字节空间 loc_0040363F: push ebx ; 保存寄存器值 loc_00403640: push esi loc_00403641: push edi loc_00403642: mov var_C, esp ; 保存栈指针 loc_00403645: mov var_8, 004010E0h ; 设置 SEH 信息 loc_0040364C: mov edi, Me ; 获取当前窗体对象
|
这部分为局部变量分配空间并保存重要寄存器。Me 是当前窗体对象的引用。
1 2 3 4 5 6 7 8
| loc_0040364F: mov eax, edi loc_00403651: and eax, 00000001h ; 检查对象引用的最低位 loc_00403654: mov var_4, eax loc_00403657: and edi, FFFFFFFEh ; 清除最低位 (VB 对象引用格式) loc_0040365A: push edi loc_0040365B: mov Me, edi loc_0040365E: mov ecx, [edi] ; 获取 vtable 指针 loc_00403660: call [ecx+04h] ; 调用 AddRef 方法
|
这部分代码处理 VB 对象引用格式并调用 AddRef 方法增加引用计数。
变量初始化 (loc_00403663 - loc_004036AC)
1 2 3 4 5 6
| loc_00403663: mov edx, [edi] loc_00403665: xor ebx, ebx ; 将 ebx 设为 0 loc_00403667: push edi loc_00403668: mov var_1C, ebx ; 初始化多个变量为 0 ... loc_0040369B: mov var_D0, ebx
|
这段代码将多个局部变量初始化为 0。
1 2 3 4 5
| loc_004036A1: call [edx+00000308h] ; 调用窗体的控件访问方法 loc_004036A7: push eax loc_004036A8: lea eax, var_2C loc_004036AB: push eax loc_004036AC: call [0040102Ch] ; Set 对象引用
|
这段代码访问窗体上的控件(很可能是 Text1 文本框),准备获取用户输入的序列号。
获取序列号文本 (loc_004036B2 - loc_004036D9)
1 2 3 4 5 6 7 8 9
| loc_004036B2: mov esi, eax ; 保存对象引用 loc_004036B4: lea edx, var_1C loc_004036B7: push edx loc_004036B8: push esi loc_004036B9: mov ecx, [esi] loc_004036BB: call [ecx+000000A0h] ; 调用对象的 Text 属性 getter loc_004036C1: cmp eax, ebx ; 检查返回值是否为 0 loc_004036C3: fclex ; 清除浮点异常标志 loc_004036C5: jnl 4036D9h ; 如果没有错误,跳转
|
这段代码获取文本框中的文本(用户输入的序列号),存入 var_1C。如果发生错误则处理它。
序列号长度检查 (loc_004036D9 - loc_00403704)
1 2 3 4 5 6 7 8
| loc_004036D9: mov eax, var_1C ; 获取序列号字符串 loc_004036DC: push eax loc_004036DD: call [00401008h] ; 调用 Len 函数 loc_004036E3: xor ecx, ecx ; 将 ecx 置为 0 loc_004036E5: cmp eax, 00000009h ; 比较长度与 9 loc_004036E8: setnz cl ; 如果不等于 9,设置 cl 为 1 loc_004036EB: neg ecx ; 对 ecx 求补 (如果 cl = 1, 结果为 -1, 否则为 0) loc_004036ED: mov esi, ecx ; 保存结果到 esi
|
这段核心代码检查序列号长度是否为 9。setnz cl 指令在长度不等于 9 时设置 cl 为 1。
1 2 3 4 5 6
| loc_004036EF: lea ecx, var_1C loc_004036F2: call [004010C0h] ; 释放字符串 loc_004036F8: lea ecx, var_2C loc_004036FB: call [004010C4h] ; 释放对象 loc_00403701: cmp si, bx ; 比较si与bx(0) loc_00403704: jnz 00403A24h ; 如果不等于0,跳转到失败处理
|
这段检查长度验证结果,如果长度不是 9,则跳转到地址 00403A24(错误处理)。
再次获取序列号并准备循环 (loc_0040370A - loc_00403787)
1 2 3 4 5 6 7 8 9 10 11 12
| loc_0040370A: mov edx, [edi] loc_0040370C: push edi loc_0040370D: call [edx+00000308h] ; 再次获取文本框控件 ... loc_00403745: mov eax, var_1C loc_00403748: push eax loc_00403749: call [00401008h] ; 再次调用 Len 函数 loc_0040374F: mov ecx, eax loc_00403751: call [00401050h] ; 将长度转换为整数 loc_00403757: lea ecx, var_1C loc_0040375A: mov var_EC, eax ; 存储序列号长度 loc_00403760: mov var_18, 00000001h ; 初始化循环计数器为 1
|
这段代码再次获取序列号,并将其长度保存在 var_EC 中。设置 var_18 为 1,这将作为循环计数器。
1 2 3
| loc_0040377C: mov cx, var_EC ; 获取序列号长度 loc_00403783: cmp var_18, cx ; 比较循环计数器与长度 loc_00403787: jnle 00403AA4h ; 如果循环计数器 > 长度,跳转到成功处理
|
这段代码是循环的入口点,比较循环计数器与序列号长度。如果已完成所有字符的验证,跳转到成功处理代码。
循环验证每个字符 (loc_0040378D - loc_00403A1D)
1 2 3 4 5 6 7 8
| loc_0040378D: mov edx, [edi] ... loc_004037A1: mov ebx, eax ; 获取文本框对象 loc_004037A3: lea edx, var_1C loc_004037A6: push edx loc_004037A7: push ebx loc_004037A8: mov ecx, [ebx] loc_004037AA: call [ecx+000000A0h] ; 获取文本框内容
|
这段代码再次获取序列号文本。
1 2 3 4 5 6 7 8 9 10 11 12
| loc_00403803: movsx edi, word ptr var_18 ; 将循环计数器扩展为 32 位 loc_00403807: mov edx, var_24 ... loc_00403826: call [00401044h] ; 调用 Mid$ 函数 ... loc_00403834: call [0040101Ch] ; 调用 Asc 函数 ... loc_0040383A: mov ecx, var_1C ... loc_0040384E: call [00401044h] ; 调用 Mid$ 函数 ... loc_0040385C: call [0040101Ch] ; 调用 Asc 函数
|
这段代码使用 Mid$ 和 Asc 函数获取序列号中特定位置字符的 ASCII 码,并进行一些检查。
1 2 3 4 5 6 7 8 9
| loc_00403862: xor edx, edx loc_00403864: cmp ax, 0030h ; 比较 ASCII 码与 '0' (48) loc_00403868: setl dl ; 如果小于'0',设置 dl 为 1 loc_0040386B: neg edx ; 对 edx 求补 loc_0040386D: lea eax, var_28 loc_00403870: and ebx, edx ; 与前面的结果进行 AND 运算 ... loc_004038AA: test bx, bx ; 测试结果 loc_004038AD: jnz 00403A22h ; 如果不为 0,跳转到失败处理
|
这部分代码检查序列号中的字符是否全部为数字。如果发现非数字字符,跳转到失败处理。
核心验证算法 (loc_004038B3 - loc_00403A04)
1 2 3 4 5 6
| loc_004038F1: mov ax, var_18 ; 获取循环计数器 loc_004038F5: mov ebx, [00401074h] ; Chr 函数地址 loc_004038FB: xor ax, 0002h ; 与 2 进行 XOR 操作 loc_004038FF: lea ecx, var_60 ... loc_00403914: call ebx ; 调用 Chr 函数
|
这是验证算法的核心部分。它获取当前位置索引(循环计数器),与数字 2 进行 XOR 操作,然后使用 Chr 函数将结果转换为字符。
1 2 3 4 5 6 7 8
| loc_0040391D: mov eax, var_1C ; 获取序列号 ... loc_00403934: call [00401044h] ; 调用 Mid$ 获取当前位置的字符 loc_0040393A: mov edx, eax loc_0040393C: lea ecx, var_20 loc_0040393F: call __vbaStrMove loc_00403941: push eax loc_00403942: call [0040101Ch] ; 调用 Asc 获取 ASCII 码
|
这部分获取序列号中当前位置的字符的 ASCII 码。
1
| loc_00403967: fsub real8 ptr [004010D8h] ; 减去 48
|
从字符的 ASCII 码中减去 48,表示获取数字的实际值。例如,字符 ‘5’ 的 ASCII 码是 53,减去 48 得到数值 5。
1 2 3 4 5 6 7
| loc_0040399D: call [004010B0h] ; 调用 Right 函数获取上面 XOR 结果的最右一位 loc_004039AB: lea ecx, var_D0 loc_004039B1: lea edx, var_80 loc_004039B4: push ecx loc_004039B5: push edx loc_004039B6: call [004010A0h] ; 比较两个值 loc_004039BC: mov edi, eax ; 结果存入 edi
|
这段代码获取 XOR 结果字符串的最右边一个字符,然后与序列号中对应位置的数字值进行比较。
1 2
| loc_00403A01: test di, di ; 测试比较结果 loc_00403A04: jnz 403A22h ; 如果不相等,跳转到失败处理
|
如果比较结果不相等,表示验证失败,跳转到失败处理代码。
循环计数器递增 (loc_00403A06 - loc_00403A1D)
1 2 3 4 5 6 7
| loc_00403A06: mov edi, Me loc_00403A09: mov eax, 00000001h ; eax = 1 loc_00403A0E: add ax, var_18 ; 循环计数器加 1 loc_00403A12: jo 00403BACh ; 如果溢出,跳转到异常处理 loc_00403A18: mov var_18, eax ; 保存新的循环计数器 loc_00403A1B: xor ebx, ebx ; ebx = 0 loc_00403A1D: jmp 0040377Ch ; 跳回循环开始处
|
这段代码将循环计数器递增 1,然后跳回循环的开始处,继续验证下一个字符。
失败处理 (loc_00403A24 - loc_00403AA2)
1 2 3 4 5 6 7
| loc_00403A24: mov esi, [004010A4h] ; __vbaVarDup 函数地址 ... loc_00403A4E: mov var_A8, 004022F0h ; "Wrong serial!" ... loc_00403A69: mov var_98, 004022C8h ; "Sorry, try again!" ... loc_00403A8C: call [00401030h] ; 调用 MsgBox 显示错误消息
|
这段代码在验证失败时显示错误消息框:”Wrong serial! Sorry, try again!”
成功处理 (loc_00403AA4 - loc_00403B24)
1 2 3 4 5 6 7
| loc_00403AA4: mov esi, [004010A4h] ; __vbaVarDup 函数地址 ... loc_00403ACE: mov var_A8, 004022A4h ; "Correct serial!" ... loc_00403AE9: mov var_98, 00402258h ; "Good job, tell me how you do that!" ... loc_00403B0C: call [00401030h] ; 调用 MsgBox 显示成功消息
|
这段代码在验证成功时显示成功消息框:”Correct serial! Good job, tell me how you do that!”
函数清理和返回 (loc_00403B12 - loc_00403BA4)
这部分代码释放所有使用的资源,恢复保存的寄存器值,并返回。
三、寄存器变化分析
下面是函数在进行各个部分处理时寄存器的变化。
序列号获取和长度检查
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| ; 获取序列号文本 loc_004036B2: mov esi, eax ; ESI = 文本框对象引用 loc_004036BB: call [ecx+000000A0h] ; 调用 Text 属性 getter ; 此时var_1C = 用户输入的序列号字符串
; 检查长度是否为 9 loc_004036D9: mov eax, var_1C ; EAX = 序列号字符串 loc_004036DD: call [00401008h] ; 调用 Len 函数 ; 此时 EAX = 用户输入的序列号长度
loc_004036E5: cmp eax, 00000009h ; 比较长度是否为 9 loc_004036E8: setnz cl ; 如果不是 9, CL = 1, 否则 CL = 0 loc_004036EB: neg ecx ; 如果 CL = 0, ECX = 0; 如果 CL = 1, ECX = -1 loc_004036ED: mov esi, ecx ; 保存结果到 ESI
|
循环准备
1 2 3 4 5 6 7 8
| ; 获取序列号长度 loc_00403745: mov eax, var_1C ; EAX = 序列号字符串 loc_00403749: call [00401008h] ; 调用 Len 函数 ; 此时 EAX = 序列号长度 = 9
loc_00403751: call [00401050h] ; 转换为整数 loc_0040375A: mov var_EC, eax ; var_EC = 序列号长度 = 9 loc_00403760: mov var_18, 00000001h ; var_18 = 循环计数器 = 1
|
循环开始 - 验证每个位置
1 2 3 4
| ; 循环条件检查 loc_0040377C: mov cx, var_EC ; CX = 序列号长度 = 9 loc_00403783: cmp var_18, cx ; 比较循环计数器与长度 ; 第一次迭代: var_18 = 1, CX = 9, 所以继续循环
|
获取序列号字符
1 2 3 4 5 6 7 8 9
| ; 获取序列号 loc_004037A1: mov ebx, eax ; EBX = 文本框对象引用 loc_004037AA: call [ecx+000000A0h] ; 获取文本内容 ; 此时 var_1C = 序列号字符串 = "301674501"
; 获取当前位置的字符 loc_00403803: movsx edi, word ptr var_18 ; EDI = 当前循环计数器 = 1 loc_00403826: call [00401044h] ; 调用 Mid$ 函数 ; 对于第一个位置, var_28 = "3"
|
检查字符是否为数字
1 2 3 4 5 6 7 8 9 10 11 12 13
| loc_00403834: call [0040101Ch] ; 调用 Asc 函数 ; 此时 AX = 第一个字符的 ASCII 码 = 51
loc_0040383F: cmp ax, 0039h ; 比较与 '9' 的 ASCII 码 loc_00403848: setnle bl ; 如果 > 57, BL = 1, 否则 BL = 0 ; 字符 '3' 不大于 '9', 所以 BL = 0
loc_00403864: cmp ax, 0030h ; 比较与 '0' 的 ASCII 码 loc_00403868: setl dl ; 如果 < '0', DL = 1, 否则 DL = 0 ; 字符 '3' 不小于 '0', 所以 DL = 0
loc_00403870: and ebx, edx ; EBX = BL & DL = 0 ; 这里检查字符是否在 '0' 到 '9' 之间, 如果是数字, EBX = 0
|
核心 XOR 验证算法
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
| ; 执行 XOR 操作 loc_004038F1: mov ax, var_18 ; AX = 循环计数器 = 1 loc_004038FB: xor ax, 0002h ; AX = 1 XOR 2 = 3 ; 此时 AX = 3
; 转换为字符并保存 loc_00403914: call ebx ; 调用 Chr 函数, 转换 XOR 结果 ; 此时 var_28 = "3", XOR 结果转换为字符
; 获取当前位置的序列号字符的值 loc_00403942: call [0040101Ch] ; 调用 Asc 函数获取 ASCII 码 ; 对于第一位 "3", AX = 51 loc_0040394B: mov var_48, ax ; var_48 = 51
; 减去 '0' 的 ASCII 码 loc_00403967: fsub real8 ptr [004010D8h] ; ST(0) = 51-48 = 3 ; 将字符转换为数值,此时 ST(0) = 3
; 获取 XOR 结果的最后一位字符 loc_004039A5: call [004010B0h] ; 调用 Right 函数 ; 此时 var_80 包含 XOR 结果的最后一位
; 比较序列号字符值与 XOR 结果 loc_004039B6: call [004010A0h] ; 比较两个值 ; 转换为数值后相等,所以 EDI = 0
|
检查比较结果和循环控制
1 2 3 4 5 6 7 8 9 10 11
| loc_00403A01: test di, di ; 测试 EDI 是否为 0 ; 如果相等, 继续执行下面的代码
; 增加循环计数器 loc_00403A09: mov eax, 00000001h ; EAX = 1 loc_00403A0E: add ax, var_18 ; AX = 1 + 1 = 2 loc_00403A18: mov var_18, eax ; var_18 = 2 ; 循环计数器增加到 2
; 循环返回 loc_00403A1D: jmp 0040377Ch ; 跳回循环开始处
|
四、结果推理与验证
通过对Command_1_Click_403620 函数的分析,我们可以确定序列号验证算法为:
- 序列号必须是 9 位长度
- 所有字符必须是数字
- 对于每个位置 i(从 1 到 9):
- 将位置索引 i 与 2 进行 XOR 运算:
i XOR 2
- 将结果转换为字符串
- 取字符串的最右边一位字符,记为 C
- 将序列号第i位字符的值(即 ASCII 码减去 48),记为 B
- 比较 B 与 C,必须相等
按照这个算法,唯一有效的序列号是:301674501。
每个位置的验证过程如下:
| 位置 (var_18) |
XOR 结果 (AX) |
转换为字符 |
序列号对应字符 |
序列号 ASCII |
字符值 |
比较结果 (EDI) |
| 1 |
1 ^ 2 = 3 |
‘3’ |
‘3’ |
51 |
3 |
0 (相等) |
| 2 |
2 ^ 2 = 0 |
‘0’ |
‘0’ |
48 |
0 |
0 (相等) |
| 3 |
3 ^ 2 = 1 |
‘1’ |
‘1’ |
49 |
1 |
0 (相等) |
| 4 |
4 ^ 2 = 6 |
‘6’ |
‘6’ |
54 |
6 |
0 (相等) |
| 5 |
5 ^ 2 = 7 |
‘7’ |
‘7’ |
55 |
7 |
0 (相等) |
| 6 |
6 ^ 2 = 4 |
‘4’ |
‘4’ |
52 |
4 |
0 (相等) |
| 7 |
7 ^ 2 = 5 |
‘5’ |
‘5’ |
53 |
5 |
0 (相等) |
| 8 |
8 ^ 2 = 10 |
‘10’ |
‘0’ |
48 |
0 |
0 (相等) |
| 9 |
9 ^ 2 = 11 |
‘11’ |
‘1’ |
49 |
1 |
0 (相等) |
输入有效序列号,结果如图所示,逆向成功。
