Preliminary Exploration of Software Reverse Engineering

Preliminary Exploration of Software Reverse Engineering

Astrid Stark Lv. ∞

对于 crackme 示例程序,我们选择 Bjanes Crackme V2.0a 序列号检测程序进行逆向。

一、项目结构分析

首先对程序界面进行初步了解:

Program Interface

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

Project Structure

结合程序界面、反编译得到的项目结构及各部分代码,可以推测出:

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_403620Command_2_Click_403BC0 为 Check it 和 Exit 按钮逻辑函数

对这两个按钮的逻辑代码进行查看,在 Command_1_Click_403620 函数中发现:

False Message

如图,有进行 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 函数的分析,我们可以确定序列号验证算法为:

  1. 序列号必须是 9 位长度
  2. 所有字符必须是数字
  3. 对于每个位置 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 (相等)

输入有效序列号,结果如图所示,逆向成功。

Correct Serial!