CrackMe
9unk Lv5

简介

这篇笔记收集(参考)各个地方的 CrackMe 程序,用来作为练习。文章主要是分析算法,并编写注册机。

新 160 个 CrackMe

参考: 新160个CrackMe算法分析

001-abexcm5

查看程序基本信息

54
55

搜索字符串

  1. F9 运行到程序领空
    56

  2. 在当前模块搜索字符串
    57
    58

程序分析(静态+动态)

  1. 分析关键指令
    59

  2. 在关键函数出设置断点,F9进行动态调试,找到序列号
    60

  3. 向上溯源,找到第一个处理字符串的函数
    61

程序分为3段:

  • 第一步:将字符串 “OS” 和 “4562-ABEX” 拼接,结果为”OS4562-ABEX”
  • 第二步:将 “OS45” 各字节+2,结果为 “QU6762-ABEX”
  • 第三步:将 4023FD(“L2C-5781”) 和 40225C(“QU6762-ABEX”) 处的字符串拼接到 402000 地址处,结果为 “L2C-5781QU6762-ABEX”

编写注册机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <windows.h>

int main()
{
char str1[12] = "OS";
char str2[] = "4562-ABEX";
char str3[] = "L2C-5781";
char result[20] = {0};
strcat(str1,str2);
for (int i = 0; i < 4; i++)
{
str1[i] = str1[i] + 2;
}
strcat(result,str3);
strcat(result, str1);
printf("%s", result);
return 0;
}

002-Cruehead-CrackMe-3

查看程序基本信息

62
63

exeinfo未识别使用的编程语言,打开程序也没有发现任何注册点,只有一个 exit 按钮。

动态调试程序

  1. 运行到程序领空
    64

我们运行到程序领空后,直接就找到了CreateFile、ReadFile等API函数和关键字符串,未发现 main 函数的初始化操作,说明这个程序使用汇编写的。

程序分析

GetModuleHandle 函数

GetModuleHandle(NULL):获取 exe 进程的基地址

1
2
push 0
call <JMP.&GetModuleHandleA>

CreateFile 函数

CreateFile 函数可以创建新文件或打开现有文件。 必须指定文件名、创建说明和其他属性。 当应用程序创建新文件时,操作系统会将其添加到指定的目录。

CreateFile(CRACKME3.KEY,GENERIC_READ | GENERIC_WRITE,FILE_SHARE_READ|FILE_SHARE_WRITE,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL)

1
2
3
4
5
6
7
8
push    0               ; hTemplateFile
push 80h ; dwFlagsAndAttributes
push 3 ; dwCreationDisposition
push 0 ; lpSecurityAttributes
push 3 ; dwShareMode
push 0C0000000h ; dwDesiredAccess
push offset FileName ; "CRACKME3.KEY"
call CreateFileA
  • CreateFileA 结构体
    1
    2
    3
    4
    5
    6
    7
    8
    9
    HANDLE CreateFileA(
    [in] LPCSTR lpFileName,
    [in] DWORD dwDesiredAccess,
    [in] DWORD dwShareMode,
    [in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes,
    [in] DWORD dwCreationDisposition,
    [in] DWORD dwFlagsAndAttributes,
    [in, optional] HANDLE hTemplateFile
    );

参数:

  • lpFileName:需要打开或者创建的文件名

  • dwDesiredAccess
    设置文件访问权限(下图是文件访问掩码格式)
    65

在此格式中,低顺序 16 位用于特定于对象的访问权限,接下来的 8 位用于 标准访问权限,这些权限适用于大多数类型的对象,4 个高阶位用于指定每个对象类型可以映射到一组标准权限和特定于对象的 泛型访问权限 。 ACCESS_SYSTEM_SECURITY位对应于 访问对象的 SACL 的权限。

这个程序用的是泛型访问权限,其中第 31 和 30 位置 1,其余位置 0,最终值为 C0000000h 。

  • dwShareMode
    文件或设备的请求共享模式,可以读取、写入、删除所有这些或无 (引用下表) 。 对属性或扩展属性的访问请求不受此标志的影响。
    66

  • [in, optional] lpSecurityAttributes
    指向一个 SECURITYATTRIBUTES 结构的指针,该结构包含两个独立但相关的数据成员:可选的安全描述符,以及一个布尔值,该值确定返回的句柄是否可以由子进程继承。
    此参数可以为 NULL。
    如果此参数为 NULL,则应用程序可能创建的任何子进程都无法继承 CreateFile 返回的句柄,并且与返回的句柄关联的文件或设备获取默认的安全描述符。

  • dwCreationDisposition
    对存在或不存在的文件或设备执行的操作。
    对于文件以外的设备,此参数通常设置为 OPEN_EXISTING。
    67

  • lpSecurityAttributes
    文件或设备属性和标志, FILE_ATTRIBUTE_NORMAL 是文件最常见的默认值。
    68

  • [in, optional] hTemplateFile
    具有 GENERIC_READ 访问权限的模板文件的有效句柄。 模板文件为正在创建的文件提供文件属性和扩展属性。
    此参数可以为 NULL。
    打开现有文件时, CreateFile 将忽略此参数。
    打开新的加密文件时,该文件将从其父目录继承任意访问控制列表。

从上面函数分析,这里的 CreateFile 函数是在尝试打开一个名为 CRACKME3.KEY 的文件。

动态调试

经过动态调试,发现后续代码是用来创建、更新 win32 窗口。

  1. 创建 CRACKME3.KEY 文件,再重新调试程序
    69

判断 CRACKME3.KEY 文件的字符长度

  1. 将 key 长度改为 12h,再次调试
    70

  2. 继续步过调试,发现两个关键函数
    71

  3. 单步调试进入第一个关键call
    72

根据汇编写下伪代码

1
2
3
4
5
6
7
8
9
10
//代码逻辑
esi=key指针;bl=0x41;cl=1;sum = 0;times=0
do{
key[i]=key[i] xor bl;
sum += key[i];
bl++;
i++;
cl++;
}while(bl<0x4F||key[i]!=0);
times = cl;
  1. 紧接着还有1个关键指令

    1
    sum = sum XOR 12345678h
  2. 调试进入第二个关键 call
    73

1
2
//把末尾的4个字节传给eax
eax = key[esi+Eh]
  1. sum 值和 eax 进行比较,并且将 ZF 标志位的值传给 al 寄存器
    74

从上图可以分析出这块代码的流程,再从代码 sete altest al,al 这两条指令和下图代码来看,基本可以确认这是错误的执行流程。

75

现在基本可以确认指令 cmp eax,dword ptr ds:[4020F9] 是一个关键判断,执行该指令处,修改eax值后再继续调试看看程序的运行情况。

76

  1. 调试后发现程序成功运行
    78

编写注册机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int main()
{
char inputBuffer[15] = { 0 };
char key[5] = { 0 };
int sum = 0;
int Count = 0x41;
int i = 0;
printf("请输入 14 位的 key:");
scanf("%s", inputBuffer);

do {
inputBuffer[i] ^= Count;
sum += inputBuffer[i];
Count++;
i++;
} while (Count < 0x4F || inputBuffer[i] != 0);

sum ^= 0x12345678;
printf("CRACKME3.KEY 文件的内容是:%s\n末尾4位是:0x%x\n", inputBuffer,sum);
return 0;
}

补充

上面的程序执行的流程之分析了一半,当我重新调试一遍后,发现之前分析程序流程有问题。下面重新补充并纠正。

  1. 调用 GetModuleHandle 获取 EXE 进程的基地址
  2. 调用 CreateFile 读取文件
  3. 调用 ReadFile 读取文件内容
  4. 比较读取数据的长度(数据长度必须是18个字节)
  5. 处理前 14 个数据,并把结果存储到 sum( [0x004020F9] ) 中
  6. 判断 sum 和 eax(后4字节)数据是否相等
  7. 如果不相等,push eax再调用 call 0x004012F5 函数在参数(0x40210E) “CrackMe v3.0” 后面插入 “-Uncracked”; 如果相等就 push eax 此时 al 为 1
    83
    79

注意: 这里的 0x004012F5 函数并没有做堆栈平衡, 也就是说,如果key错误,栈顶值就是 0x40210E; 如果key正确,栈顶的值就是 0x12345401

  1. 调用 FindWindow 寻找包含字符串 “No need to disasm the code!” 的窗口,如果没有值为0(NULL),如果有返回窗口句柄。
  2. 调用 LoadIcon 和 LoadCursor 加载图标和光标
  3. 调用 RegisterClass 注册一个包含字符串 “No need to disasm the code!” 的窗口
  4. 调用 CreateWindowEx 创建包含字符串 “CrackMe v3.0 -Uncracked” 的窗口
  5. 调用 ShowWindow 显示窗口
  6. 调用 UpdateWindow 更新窗口,如果更新成功,返回值为1,如果更新失败,返回值为0
  7. 0x40210E 处的结果与 1 比较
    80

    这里的 pop eax 是来自函数 call 0x004012F5,这个函数中未使用堆栈平衡

  8. 最后这一段使用消息机制,等待用户对窗口进行操作
    81
  9. 最后在窗口标题就是字符串 “CrackMe v3.0 -Uncracked”
    82

    如果对上面创建窗口的函数熟悉的话,一看就知道 字符串 “CrackMe v3.0 -Uncracked” 是窗口标题

004-Acid Bytes.2

查看程序基本信息

使用PE工具,查看这是个32位可执行程序,且加了 UPX 壳
84

脱壳

85

重新查看程序信息
86
87

从上面的截图来看,程序使用的是 Delphi 语言,并且确定了程序的错误信息。

使用 IDR 加载脱壳程序,并进行分析

程序执行流程

  1. 使用 GetText 获取窗口数据
  2. 使用 StrCmp 判断窗口数据(serial)是不是 “12011982”
  3. 如果是就输出破解成功的消息框;如果serial为空,就输出消息框提示输入内容;如果不是就输出破解失败的消息框
    88

从上图中可以很清晰地判断出 serial 的值就是 “12011982”

005-Andrnalin.1

查看程序基本信息

89
90

程序分析

  1. F9运行程序到程序领空
    91

  2. 在当前模块搜索字符串
    92

  3. 双击字符串定位到相应指令处
    93

  4. 向上查找,看看有没有可疑的函数
    94

在strcmp指令处下断点

  1. 向 strcmp 函数后面查找第一个跳转指令
    95

根据跳转指令后面的字符串和call,可以判断这一块是正确破解后的输出,这里跳转指令是关键跳转,我么在这里下断点。

  1. 调试程序
    96

这里主要在就是看这两条指令 neg edisbb edi,edi 这两条指令:

  • 当 edi = -1 时,执行neg指令后,edi=1,CF标志位为1;执行 sbb 指令 edi=edi-edi-CF位=-1
  • 当 edi = 0 时,执行neg指令后,edi=0,CF标志位为0;执行 sbb 指令 edi=edi-edi-CF位=0

指令复习:neg指令,求补码指令,对操作数执行求补运算:用零减操作数,然后结果返回到操作数。neg指令对CF标志位有影响。

后面又执行了两条指令:inc edineg edi

  • 当 edi=-1 时,执行指令后 edi=0
  • 当 edi=0 时,执行指令后 edi=1

97

当 di=si=0 时,破解失败;当 di!=si时,破解成功。(esi 在之前就已经置为0了)
98

  1. 验证 key(SynTaX 2oo1)

99

006-AD_CM#2

查看程序基本信息

1
2

静态分析

3

该程序是用汇编写的,所以没有main函数。打开程序后,双击可疑函数 sub_4010FC,发现这个就是要分析的主函数。

4

5

  • 从上图的静态分析可以得到结果:
    serial(byte_403280) = name(string) - cl

cl 的值会根据 loop 循环,进行减1的操作。

  • 最终计算结果:
    name=9unkk
    serial=4qkij
1
2
3
4
5
9-5=4
u-4=q
n-3=k
k-2=i
k-1=j

6

编写注册机

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
#define _CRT_SECURE_NO_DEPRECATE

#include <stdio.h>
#include <stdlib.h>


int main(void)
{
char name[10] = { 0 };
char serial[10] = { 0 };
int len = 0;




printf("请输入Name(大于等于5个字符):");
scanf("%s", name);

//计算 Name 长度
for (int i = 0; name[i] != 0; i++)
{
len++;
}

int a = len;
printf("您的serial是:");
for (int i = 0,j=len; i < len; i++,j--)
{
serial[i] = name[i] - j;
printf("%c", serial[i]);
}

return 0;
}

007-Reg

查看程序基本信息

7
8

静态分析

  1. 搜索字符串,找到主程序的

9

通过IDA静态分析并没有找到中文字符串,这是因为IDA中文字符串搜索功能有点小问题,同时对于程序运行过程中产生的字符串是无法搜索出来的。

  1. 通过动态调试搜索字符串

10

  1. 在IDA中手动定位到字符串位置

11

  1. 按空格键查看程序流程图

12

经过多次手动调试之后发现,这一块的功能(TFmReg_btnOKClick)就是把 “UserName=9unk” 和 “SN=123456” 写入到 reg.dll 中,并没有验硬编码的功能。也就是说这个程序是通过读取 reg.dll 这个文件中的 “UserName” 和 “SN” 来验证硬编码的。

  1. 重新观察程序,发现左下角写着 “未注册”

13

  1. 重新在搜索字符串,并在 “提升栈顶” 处设置断点。

14
15

  1. 在 IDA 中定位到 “0x45D4A9” 位置中,静态程序。

16
17

复制字符串 “您的有效期” 和 “未注册” 到局部变量中,之后再查找 reg.dll 文件,如果该文件不存在就结束程序。

  1. 获取窗口 “UserName” 和 “SN” 窗口的值。

18

  1. 根据下面的 call 函数中使用了 “UserName” 和 “SN” 作为参数,这应该是关键函数。

19

delphi 逆向中的函数:

  • LStrLAsg 函数:将 EDX 指向的地址内容复制到EAX指向的地址中去。
  • FileExists 函数:查找文件是否存在,EAX作为入口点;返回值:al==”True” 或 “False”。
  • GetValue 函数:返回属性的串值,缺省时返回’(unknown)’,这应该被重载以返回适当的值。
  • StrCat(Dest: PChar; const Source: PChar) 函数:将参数source中的字符添加到Dest字符串的尾部,Dest缓冲区中必须有足够的空间。其中,Dest表示目的缓冲区;Source表示源缓冲区。
  • strcopy(Dest: PChar; const Source: PChar):将参数Source中的字符复制Dest字符的尾部,Dest缓冲区中必须有足够的空间。
  • strtoint 函数:将字符串转换为整数
  • strclr 函数:引用计数为0了,释放字符串

动态调试

  1. 从这里可以发现从 0x412500 地址往后存储的可能是自定义函数的数组指针。后面的这些函数比较多不必要一个个函数去分析,只要了解大概就行

20

  1. 关键函数暂时先跳过。在后面修改跳转指令,F9 运行程序,看看是否能正确注册程序。

21
22

可以看到,程序正常运行。后续就是分析这个关键函数再做什么操作。

  1. 经过第一轮分析可得出如下结果,修改 reg.dll 文件,重新调试程序。

23
24

循环计算 serial 中的每个字符,如果字符 (x + 0xD0(存储大小为字节) ) < 0xA 就会继续循环计算,如果不符合第一个要求 (上一个计算结果 + 0xF9) >= 6 跳出该子程序。

补充:这一段循环表示 ”输入的 SN 必须在 09 和 AF 之间“。

  1. 计算程序激活时间的函数

25

  1. 执行下一个函数后看到有一串16位的字符,这可能是正确的serial

26

  1. 继续调试,发现出现了一串新的字符,这很像 MD5 加密,我们尝试使用加密算法验证一下。

27
28

  1. 发现对输入的 serial 也进行了两次 MD5 加密,最后在对两个字符串进行比较

29
30

  1. 最终输入前面的serial,程序正确激活

31

算法分析:函数0x0045C5E0

  1. 入口参数:ECX=”正确的SN”,EDX=”激活日期期限”,EAX=”UserName”
    26

  2. 第一步将 “11”、”05”、”12” 转换为二进制字符串拼接为 “0001011010101100”

37

  1. 第二步:将“0001011010101100” 经过一系列操作变为 “01010110”,再转换为十六进制0x56

38

  1. 第三步:将“0001011010101100” 经过一系列操作变为 “00110100”,再转换为十六进制0x34

39

第二步和第三步不是重点可以忽略,只要知道大概在进行什么操作就行。

  1. “3456”+”MD5(9unk)” 通过函数 0x45C244 计算得出 “0x4E”

40

41

42

  1. 拼接为 “34564E”,再进行 MD5 加密,再取其中第8个和第9个字符 “59”
    43

44

45

  1. MD5(9unk)+MD5(MD5(9unk)+MD5(110512)) 通过函数 0x45C244 计算得出 “0xD4”

46

47

  1. MD5(110512)+MD5(MD5(9unk)+MD5(110512)) 通过函数 0x45C244 计算得出 “0xD8”

48

49

  1. 取 MD5(MD5(9unk)+MD5(110512)) 第7个字节,第14个字节,第23个字节,第11个字节

50

51

52

53

54

55

字节从0开始算,所以按字符个数算少了一个。

  1. 从下面的压入堆栈的值可以看出来这就是正确的 “SN”

57

函数 0x45C244 分析

call 0x45C244 将参数转换为字符串:入口参数 EAX=”3456”+”MD5(9unk)”

33

判断参数是否有错误

34

字符串转换为十六进制、通过函数 0x0045C09C 算出 “0x4E”、最后清空函数 0x45C244 使用的局部变量 [ebp-0x10C]

35

函数 0x0045C09C 分析

call 0x0045C09C 函数:入口参数 EAX=十六进制MD5值内存地址,ecx=参数长度,EDX=出口参数。

36

函数算法,使用内外两层循环进行进算,详细算法如下:

32

外循环

  1. 带入第n个字节,一共循环18次

内循环

  1. 循环将字节逻辑右移8次
  2. 每次循环得到的结果放入 cl
  3. cl xor dl
  4. cl and 1
  5. 如果 cl 不为0,dl=dl xor 0x18,edx=edx shr 1,dl or 0x80,eax = eax shr 1
  6. 如果 cl 为 0 ,edx=edx shr 1,eax = eax shr 1

总结

函数0x0045C5E0算法: 按顺序存储第5步得出的结果;存储固定值 “3456”,因为激活日期是固定的 ;存储第6步结果;存储第9步第8个字符;存储第9步第15个字符;存储第7步结果;存储第9步第24个字符;存储第9步第11个字符;存储第6步结果

编写注册机

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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
/*程序名:reg.c*/
/*
新160个CrackMe算法分析-007-Reg,注册机。
*/
#define _CRT_SECURE_NO_DEPRECATE
#pragma warning(disable:4996)
#pragma comment(lib,"libssl.lib")
#pragma comment(lib,"libcrypto.lib")

#include <stdio.h>
#include <string.h>
#include <openssl/md5.h>

/*-----------------------------------------------------------------------------
* 函数:md5hexToString
* 功能:将十六进制转换为字符串
* 入口参数:md=十六进制数的指针,result=存储十六进制字符串的数组,
* start=要转换为字符串的其实指针(例如要转换md,从第6位开始的字符串,start=5)
* len=要转换多少个字节为字符串
* 出口参数:无
-------------------------------------------------------------------------------*/
void HexToStr(unsigned char* md, char* result,int start,int len)
{
for (size_t i = start; i <= len; i++)
{
sprintf(result + i * 2, "%02x", md[i]);
}
}

/*-----------------------------------------------------------------------------
* 函数:xormd5(即关键算法函数:0x0045C09C)
* 功能:将十六进制数通过固定算法流程,循环计算并得出结果。
* 入口参数:a=存储十六进制值的数组,dest=存储结果的数组
* len=对数组a,需要循环计算多少次
* 出口参数:无
-------------------------------------------------------------------------------*/
void xormd5(char a[], unsigned char dest[],int len)
{
char num[1] = { 0 };
int result[1] = { 0 };
int x = 0;


for (int j = 0;j < len; j++)
{
num[0] = a[j];
for (int i = 0; i < 8; i++)
{
x = num[0];
x ^= result[0];
x &= 1;

if (x != 0)
{
result[0] ^= 0x18;
result[0] >>= 1;
result[0] |= 0x80;
num[0] >>= 1;
}
else
{
result[0] = result[0] >> 1;
num[0] = num[0] >> 1;
}
}
dest[0] = result[0];
}

}

int main(int argc,char* argv[])
{
unsigned char md[32] = { 0x34,0x56 };
char mds[65] = { 0 };
char mds1[33] = { 0 };
unsigned char md1[2] = { 0 };
char name[20] = {0};
char next1[6] = { 0 };
char result[18] = { 0 };
int len=0;
MD5_CTX c;

//输入用户名
printf("请输入用户名:");
scanf("%s", &name);

//判断用户名长度
len=strlen(name);


//第一步:获取 "4E"
//初始化MD5数据类型的数组
MD5_Init(&c);

//根据用户名生成 MD5 到变量c中
MD5_Update(&c, name, len);

//在MD5值前面加上3456
MD5_Final(md+2, &c);

//计算 "3456+MD5(UserName)" 的值,将结果传到 md1
xormd5(md,md1,18);

//转换为字符串存储到result中
HexToStr(md1, result,0,2);

//将result中的值转换为大写,后面的 MD5 值加密是用 "34564E" 而不是 "34564e"
for (int i = 0; i < 6; i++)
{
if (result[i] >= 0x61)
{
result[i] -= 0x20;
}
}

//存储3456到result=4E3456,同时next1=34564E,为下一步做准备。
int x = 0x33;
for (int i = 2; i < 6; i++,x++)
{
result[i] = x;
next1[i - 2] = x;
}



//第二步:获取 "59"
//将之前使用 xormd5 计算的结构存储到Next1中
next1[4] = result[0];
next1[5] = result[1];


//初始化MD5数据类型的数组
MD5_Init(&c);

//根据用户名生成 MD5 到变量c中
MD5_Update(&c, next1, 6);

//将MD5(34564E)存储到md
MD5_Final(md, &c);

//将md1转换为字符存储到result
md1[0] = md[3];
md1[1] = md[4];
HexToStr(md1, &result[6], 0, 4);
result[6] = result[7];
result[7] = result[8];


//第三步:获取 "8"

//初始化MD5数据类型的数组
MD5_Init(&c);

//根据用户名生成 MD5 到变量c中
MD5_Update(&c, name, len);
MD5_Final(md, &c);

//转换为字符串存储到mds
HexToStr(md, mds, 0, 15);


MD5_Init(&c);
//MD5(110512)到md中
MD5_Update(&c, "110512", 6);
MD5_Final(md, &c);

//转换为字符串存储到mds+32
HexToStr(md, mds+32, 0, 15);

//初始化MD5数据类型的数组
MD5_Init(&c);
MD5_Update(&c, mds, 64);

//将结果存储到 md 后32位,放到后面备用。
MD5_Final(md+16, &c);

//转换为字符串存储到 mds1 中
HexToStr(md+16, mds1, 0, 15);

//取md的第7个字节存储到 result 中
result[8] = mds1[7];

//第四步:获取 "b"
//取第14个字节存储到 result 中
result[9] = mds1[14];

//第九步:获取 "D8"(因为此时md的值正好是第九步的值,直接运算)
//此时 md = MD5("110512")+MD5(MD5("9unk")+MD5("110512"))

//计算 md 的值,将结果传到 md1
xormd5(md, md1,32);

//转换为字符串存储到result中
HexToStr(md1, result+14, 0, 0);


//第五步:获取 "D4"
//初始化MD5数据类型的数组
MD5_Init(&c);

//根据用户名生成 MD5 到变量c中
MD5_Update(&c, name, len);

//md前16位替换为MD5("9unk")
MD5_Final(md, &c);

//计算 md 的值,将结果传到 md1
xormd5(md, md1, 32);

//转换为字符串存储到result中
HexToStr(md1, result + 10, 0, 0);

//第六步:
//取第14个字节存储到 result 中
result[12] = mds1[23];

//第七步:
//取第11个字节存储到 result 中
result[13] = mds1[11];

printf("SN = ");
//打印测试,并将小写字母转换为大写
for (int i = 0; i < 17; i++)
{
if (result[i] >= 0x61)
{
result[i] -= 0x20;
}
printf("%c", result[i]);
}

printf("\n");

return 0; //程序结束
}

008-Afkayas.1

查看程序基本信息

58
59

静态分析

  1. 搜索字符串,没有找到相关有用的字符串

61

  1. 查看函数窗口,找到了三个自定义函数,依次点开查看,发现 sub_402191 函数和可能是关键函数。

60

  1. 浏览 sub_402191 函数

62
63

  1. 确认函数关键函数后,在 strcmp 函数处,按空格,找到偏移地址

64

动态调试

  1. 使用 ctrl+G 定位到 IDA 找到的偏移地址,并设置断点。

65

  1. 运行程序,断点后找到关键的 serial

66

  1. 验证 “AKA-390181” 是否为正确的serial

67

  1. 找到关键算法,从strcmp向上找第一个,且与 [ebp-0x1C][ebp-0x18] 的函数。
    68

  2. 设置断点,动态调试看看,该函数不属于程序领空不是算法函数

69

  1. 继续向下调试,确认 [ebp-0x1C] 存储的是serial

70

  1. 重新从函数开头找到与 [ebp-0x1C] 相关的函数,进行一步一步调试,最终在函数 vbastrI4 位置处返回了serial字符串

71

向上看还发现了两个函数:

  • vbaLenBstr 获得一个字符串的长度
  • rtcAnsiValueBstr —>传回字符码(返回第一个字符的字符代码)
  1. 重新调试程序,并找到关键算法
    72
    73
    74

算法: “AKA-“+(name长度*0x17CFB)+name[0]

编写注册机

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
/*程序名:Afkayas.c*/
/*
新160个CrackMe算法分析-008-Afkayas.1,注册机。
*/
#define _CRT_SECURE_NO_DEPRECATE
#pragma warning(disable:4996)
#pragma comment(lib,"libssl.lib")
#pragma comment(lib,"libcrypto.lib")

#include <stdio.h>
#include <string.h>
#include <openssl/md5.h>


int main(int argc,char* argv[])
{
char name[20] = { 0 };
int key = 0;

printf("请输入用户名:");
scanf("%s", name);

printf("serial = AKA-%d", strlen(name) * 0x17CFB + name[0]);

return 0; //程序结束
}

VBA逆向常用函数

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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
VB程序逆向常用的函数
1) 数据类型转换:

a)__vbaI2Str 将一个字符串转为8 位(1个字节)的数值形式(范围在 0 至 255 之间) 或2 个字节的数值形式(范围在 -32,768 到 32,767 之间)。
b)__vbaI4Str(vbastrI4) 将一个字符串转为长整型(4个字节)的数值形式(范围从-2,147,483,6482,147,483,647)
c)__vbar4Str 将一个字符串转为单精度单精度浮点型(4个字节)的数值形式
d)__vbar8Str 将一个字符串转为双精度单精度浮点型(8个字节)的数值形式
e) VarCyFromStr (仅VB6库. 要调试,则在WINICE.DAT里必须有 OLEAUT32.DLL)字符串到变比型数据类型
f) VarBstrFromI2 (仅VB6库. 要调试,则在WINICE.DAT里必须有 OLEAUT32.DLL)整型数据到字符串:

2) 数据移动:
a) __vbaStrCopy 将一个字符串拷贝到内存,类似于 Windows API HMEMCPY
b) __vbaVarCopy 将一个变量值串拷贝到内存
c) __vbaVarMove 变量在内存中移动,或将一个变量值串拷贝到内存

3) 数学运算:
a) __vbavaradd 两个变量值相加
b) __vbavarsub 第一个变量减去第二个变量
c) __vbavarmul 两个变量值相乘
d) __vbavaridiv 第一个变量除以第二个变量,得到一个整数商
e) __vbavarxor 两个变量值做异或运算

4) 程序设计杂项:
a) __vbavarfornext 这是VB程序里的循环结构, For... Next... (Loop)
b) __vbafreestr 释放出字符串所占的内存,也就是把内存某个位置的字符串给抹掉
c) __vbafreeobj 释放出VB一个对象(一个窗口,一个对话框)所占的内存,也就是把内存某个位置的一个窗口,一个对话框抹掉
d) __vbastrvarval 从字符串特点位置上获取其值
e) multibytetowidechar 将数据转换为宽字符格式,VB在处理数据之都要这样做,在TRW2000显示为7.8.7.8.7.8.7.8
f) rtcMsgBox 调用一个消息框,类似于WINDOWS里的messagebox/a/exa,此之前一定有个PUSH命令将要在消息框中显示的数据压入椎栈
g) __vbavarcat 将两个变量值相连,如果是两个字符串,就连在一起
h) __vbafreevar 释放出变量所占的内存,也就是把内存某个位置的变量给抹掉
i) __vbaobjset
j) __vbaLenBstr 获得一个字符串的长度,注:VB中一个汉字的长度也为1
k) rtcInputBox 显示一个VB标准的输入窗口,类似window's API getwindowtext/a, GetDlgItemtext/a
l) __vbaNew 调用显示一个对话框,类似 Windows' API Dialogbox
m) __vbaNew2 调用显示一个对话框,类似 Windows' API Dialogboxparam/a
n) rtcTrimBstr 将字串左右两边的空格去掉

5) 比较函数
a) __vbastrcomp 比较两个字符串,类似于 Window's API lstrcmp
b) __vbastrcmp 比较两个字符串,类似于 Window's API lstrcmp
c) __vbavartsteq 比较两个变量值是否相等
d)__vbaFpCmpCy - Compares Floating point to currency. sp; Compares Floating point to currency

6) 在动态跟踪,分析算法时,尤其要注意的函数:
rtcMidCharVar 从字符串中取相应字符,VB中的MID函数,用法MID("字符串","开始的位置","取几个字符")
rtcLeftCharVar 从字符串左边取相应字符,VB中的用法:left("字符串","从左边开始取几个字符")
rtcRightCharVar 从字符串右边取相应字符,VB中的用法:Right("字符串","从右边开始取几个字符")
__vbaStrCat 用字符串的操作,就是将两个字符串合起来,在VB中只有一个&或+
__vbaStrCmp 字符串比较,在VB中只有一个=或<>
ASC()函数 取一个字符的ASC值,在反汇编时,还是有的movsx 操作数

7) 在函数中的缩写:
bool 布尔型数据(TRUE 或 FALSE)
str 字符串型数据 STRING
i2 字节型数据或双字节整型数据 BYTE or Integer
ui2 无符号双字节整型数据
i4 长整型数据(4字节) Long
r4 单精度浮点型数据(4字节) Single
r8 双精度浮点型数据(8字节) Double
cy (8 个字节)整型的数值形式 Currency
var 变量 Variant
fp 浮点数据类型 Float Point
cmp 比较 compare
comp 比较 compare

8) Btw:
__vbavartsteq系列的还有__vbavartstne 不等于
__vbavartstGe,__vbavartstGt,__vbavartstLe,__vbavartstLt等,比较大于或小于

8) 拦截警告声:
bpx rtcBeep —>扬声器提示

9) 数据移动:
bpx vbaVarCopy —>数据移动将一个变量值串拷贝到内存
bpx vbaVarMove —>数据移动变量在内存中移动,或将一个变量值串拷贝到内存
bpx vbaStrMove —>移动字符串
bpx vbaStrCopy —>移动字符串 将一个字符串拷贝到内存,类似于 Windows API HMEMCPY

10) 数据类型转换:
bpx vbaI2Str —>将一个字符串转为8 位(1个字节)的数值形式(范围在 0 至 255 之间) 或2 个字节的数值形式(范围在 -32,768 到 32,767 之间)。
bpx vbaI4Str —>将一个字符串转为长整型(4个字节)的数值形式(范围从-2,147,483,6482,147,483,647)
bpx vbar4Str —>将一个字符串转为单精度单精度浮点型(4个字节)的数值形式
bpx vbar8Str —>将一个字符串转为双精度单精度浮点型(8个字节)的数值形式
bpx VarCyFromStr —>(仅VB6库. 要调试,则在WINICE.DAT里必须有 OLEAUT32.DLL)字符串到变比型数据类型
bpx VarBstrFromI2 —>(仅VB6库. 要调试,则在WINICE.DAT里必须有 OLEAUT32.DLL)整型数据到字符串:

11) 数值运算:
bpx vbaVarAdd —>两个变量值相加
bpx vbaVarIdiv —>除整,第一个变量除以第二个变量,得到一个整数商
bpx vbaVarSub —>第一个变量减去第二个变量
bpx vbaVarMul —>两个变量值相乘
bpx vbaVarDiv —>除
bpx vbaVarMod —>求余
bpx vbaVarNeg —>取负
bpx vbaVarPow —>指数
bpx vbavarxor —>两个变量值做异或运算

12) 针对变量:
bpx vbaVarCompEq —>比较局部变量是否相等
bpx vbaVarCompNe —>比较局部变量是否不等于
bpx vbaVarCompLe —>比较局部变量小于或等于
bpx vbaVarCompLt —>比较局部变量小于
bpx vbaVarCompGe —>比较局部变量大于或等于
bpx vbaVarCompGt —>比较局部变量大于

13) 程序结构:
bpx vbaVarForInit —>重复执行初始化
bpx vbaVarForNext —>重复执行循环结构, For... Next... (Loop)

14) 比较函数:
bpx vbaStrCmp —>比较字符串是否相等 ******
bpx vbaStrComp —>比较字符串是否相等 ******
bpx vbaVarTstEq —>检验指定变量是否相等
bpx vbaVarTstNe —>检验指定变量是否不相等
bpx vbaVarTstGt —>检验指定变量大于
bpx vbaVarTstGe —>检验指定变量大于或等于
bpx vbaVarTstLt —>检验指定变量小于
bpx vbaVarTstLe —>检验指定变量小于或等于


15) 字符串操作:
bpx vbaStrCat —>用字符串的操作,就是将两个字符串合起来,在VB中只有一个&或+
bpx vbaStrLike
bpx vbaStrTextComp —>与指定文本字符串比较
bpx vbaStrTextLike
bpx vbaLenBstr —>字符串长度
bpx vbaLenBstrB —>字符串长度
bpx vbaLenVar —>字符串长度
bpx vbaLenVarB —>字符串长度
bpx rtcLeftCharVar —>截取字符串,从字符串左边取相应字符,VB中的用法:left("字符串","从左边开始取几个字符")
bpx vbaI4Var —>截取字符串
bpx rtcRightCharVar —>截取字符串,从字符串右边取相应字符,VB中的用法:Right("字符串","从右边开始取几个字符")
bpx rtcMidCharVar —>截取字符串,VB中的MID函数,用法MID("字符串","开始的位置","取几个字符")
bpx vbaInStr —>查找字符串位置
bpx vbaInStrB —>查找字节位置
bpx vbaStrCopy —>复制字符串
bpx vbaStrMove —>移动字符串
bpx rtcLeftTrimVar —>删除字串的空白
bpx rtcRightTrimVar —>删除字串的空白
bpx rtcTrimVar —>删除字串的空白
bpx vbaRsetFixstrFree —>字符串往右对齐
bpx vbaRsetFixstr —>字符串往右对齐
bpx vbaLsetFixstrFree —>字符串往左对齐
bpx vbaLsetFixstr —>字符串往左对齐
bpx vbaStrComp —>字符串比较
bpx vbaStrCompVar —>字符串比较
bpx rtcStrConvVar2 —>字符串类型转换
bpx rtcR8ValFromBstr —>把字符串转换成浮点数
bpx MultiByteToWideChar —>ANSI字符串转换成Unicode字符串
bpx WideCharToMultiByte —>Unicode字符串转换成ANSI字符串
bpx rtcVarFromFormatVar —>格式化字符串
bpx rtcUpperCaseVar —>小写变大写
bpx rtcLowerCaseVar —>大写变小写
bpx rtcStringVar —>重复字符
bpx rtcSpaceVar —>指定数目空格
bpx rtcAnsiValueBstr —>传回字符码(返回第一个字符的字符代码)
bpx rtcByteValueBstr —>传回字符码(返回第一个字节的字符代码)
bpx rtcCharValueBstr —>传回字符码(返回第一个Unicode字符代码)
bpx rtcVarBstrFromAnsi —>传回字符(返回 String,其中包含有与指定的字符代码相关的字符 )
bpx rtcVarBstrFromByte —>传回字符(返回 String,其中包含有与指定的字符代码相关的单字节)
bpx rtcVarBstrFromChar —>传回字符(返回 String,其中包含有与指定Unicode 的 String)

16)其他函数:
vbaHresultCheckObj 对某个控件进行控件校验。
VbaGenerateBoundsError 数组下标越界
VbaErrorOverflow 错误 溢出
vbaExceptHandler,VB的万能断点,VB在每个过程的开始都要安装一个线程异常处理过程。

参考:VB程序逆向常用的函数

009-keygenme1

查看程序基本信息

  1. 运行程序后,并没有看到任何提示信息。

75

  1. 在窗口左下角分别有三个按钮 “check”、”about”、”exit”,点第一个按钮会出现如下错误。

81

静态分析

  1. 使用IDA分析程序,点开自定义函数,并没有找到任何有用信息

  2. 搜索字符串,发现关键字符串

76

  1. 双击,定位到字符串位置处

77

  1. 快捷键 ctrl+x 定位到代码位置

78

  1. 向上翻,找到 strcmp 函数,这很有可能是比较 serial 的
    79

动态调试

  1. 在IDA查询的 strcmp 位置处下断点,并运行

80

  1. 验证是否为正确的serial

82

算法分析

  1. 手动调试分析算法

83
84

编写注册机

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
/*程序名:keygenme.c*/
/*
新160个CrackMe算法分析-009-keygenme1,注册机。
*/
#define _CRT_SECURE_NO_DEPRECATE
#pragma warning(disable:4996)
#pragma comment(lib,"libssl.lib")
#pragma comment(lib,"libcrypto.lib")

#include <stdio.h>
#include <string.h>
#include <openssl/md5.h>


int main(int argc,char* argv[])
{
char name[20] = { 0 };
int x = 0;
int y = 0;

printf("请输入用户名:");
scanf("%s", name);

for (int i = 0; i < strlen(name); i++)
{
x -= name[i]-0x19;
}

y = x * x * x;

printf("serial = Bon-%X-%X-41720F48", x,y);

return 0; //程序结束
}

010-ceycey(UPX壳)

查看程序基本信息

  1. 程序有 upx 壳(压缩壳)

85

  1. 点击check,并未返回错误信息

86

脱壳基础

脱壳基础

手脱压缩壳

压缩壳

压缩壳的工作过程包括以下步骤:

  1. 压缩PE文件:使用数据压缩算法对PE文件进行处理,生成一个新的压缩PE文件。
  2. 解压内存中的数据:在执行程序时,壳内部解压缩代码启动,并在内存中创建一个新的节空间。
  3. 映射节数据:将之前压缩的节数据解压缩并映射回原来内存的位置,使程序能够在内存中正常运行。
  4. 保证内存内容一致:确保解压缩后的PE文件在内存中的内容与未压缩前的正常PE文件完全相同,以维持程序的行为一致性和功能的完整性。

简单来说就是将硬盘上的exe文件进行压缩,当运行程序时,会先解压缩exe并在内存中创建新的空间,再将解压缩数据放到刚才新建的内存中,最后再验证数据的完整性。执行完解压缩和验证过程后,程序会跳转到程序正常运行的入口点。

脱壳过程就是找到最后程序正常运行的入口点,然后再将改内存块数据导出另存。

脱壳

esp 定律脱壳(通常用于压缩壳): 一般加壳程序(压缩壳)在运行时,会先执行壳代码,然后在内存中恢复还原原程序,再跳转到原始OEP,执行原程序的代码。这些壳代码首先会使用 PUSHAD 指令保存寄存器环境,在解密各个区段完毕,跳往 OEP 之前会使用 POPAD 指令恢复寄存器环境。

  1. F8,执行 PUSHAD 指令

87

  1. 右键esp,设置硬件访问断点(在指令重新访问 12FFC4 后,会断点停下来)

88
89

  1. F9 运行程序,此时程序会停在 POPAD 下面

90

  1. F8 运行程序,跳转到OEP,程序入口点位置处

91

可以看到有一堆乱码,这是因为OD自动分析指令,而这些指令因压缩无法解析,所以变成乱码。解决方法就是删除OD的分析,将指令还原。

92
93

  1. 使用 ollydbg 脱壳进程,进行脱壳,并另存文件

94

  1. 验证脱壳是否成功

95

静态分析

  1. 程序运行中没有弹出任何提示,但还是按流程查找可疑字符串进行测试。

97

  1. 发现可疑字符串,双击定位到字符串,然后用 ctrl+x 定位到程序,并在上方找到可疑的 strcmp 函数

98
99

动态调试

  1. 动态调试后发现关键指令就是 strcmp 位置。

96

  1. 继续回到IDA分析,发现这只是一个字符串比较,没有算法。

100

  1. 验证注册码(ULTRADMA……………………………………………………)

101

011-wocy.1

查看程序基本信息

  1. 运行程序后,发现有个可疑的 Unregister
    1
    2

静态分析

  1. 使用IDA搜索字符串
    3

  2. 找到相关字符串,但是这两个字符串在两个不同的函数中
    4

  3. 分别看看这两个个函数中有没有比较字符串相关的函数

6

在 401600 函数中发现了一个宽字符函数 _mbscmp

5

动调试

  1. 在_mbscmp 函数位置处设置断点,看看这是不是我们需要的 id 值

7

  1. F9 运行程序

8

  1. 验证是否为正确 id

9

  1. 寻找与 [esp+0x4] 相关的指令,设置断点,找到关键算法

10
11
12

  1. 查找 MakeReverse 函数相关信息

13

编写注册机

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
/*程序名:011-wocy.1.c*/
/*
新160个CrackMe算法分析011-wocy.1,注册机。
*/
#define _CRT_SECURE_NO_DEPRECATE
#pragma warning(disable:4996)
#pragma comment(lib,"libssl.lib")
#pragma comment(lib,"libcrypto.lib")

#include <stdio.h>
#include <string.h>
#include <openssl/md5.h>


int main(int argc,char* argv[])
{
char name[20] = { 0 };
int x = 0;
int y = 0;

names:
printf("请输入用户名(长度不小于4):");
scanf("%s", name);

printf("id = ");

for (int i = strlen(name); i >=0 ; i--)
{
if (strlen(name) < 4)
{
goto names;
}
printf("%c", name[i]);

}
printf("\n");

return 0; //程序结束
}

012-crcme1

查看程序基本信息

  1. 程序没有加壳
    14

  2. 点击 sacan 分析程序
    15

  3. 打开程序,查看功能和信息,运行程序后发现错误弹窗
    16

  4. 点击旁边的info按钮,也发现一些信息,能大概猜出一些信息
    17

静态分析

分支一

  1. 使用IDA搜索字符串,发现IDA可以正常解析这些字符串(使用的IDA8.3),大概能分析出这个程序有两种解法。一种是通过输入name和serial,另一种是通过读取KEYFILE。

18

  1. 搜索对应的字符串,定位到代码位置,发现程序有三个输出结果

19

  1. 向上溯源,找到适合动态调试的断点位置

20

分支二

  1. 定位到字符串 ACG.KEY

25

  1. 定位到指令位置

26

动态调试

分支一

  1. 设置断点,动态调试

21

22

23

分支二

  1. 打开 ACG.key 文件失败

27

  1. 文件内容要 12 个字符

28

  1. 任意输入12个字符后继续调试程序

29
30
31

算法分析

算法一

  1. 累计name值,左移3位,异或 0x51A5
  2. serial值 异或 0x87CA
  3. name 结果 + serial 结果 = 0x797E7

24

算法二

  1. (key[0] xor 0x1B) rol 2 = 0x168
  2. (key[1] xor 0x1B) rol 2 = 0x160
  3. (key[2] xor 0x1B) rol 2 = 0x170
  4. (key[3] xor 0x1B) rol 2 = 0xEC
  5. (key[4] xor 0x1B) rol 2 = 0x13C
  6. (key[5] xor 0x1B) rol 2 = 0x1CC
  7. (key[6] xor 0x1B) rol 2 = 0x1F8
  8. (key[7] xor 0x1B) rol 2 = 0xEC
  9. (key[8] xor 0x1B) rol 2 = 0x164
  10. (key[9] xor 0x1B) rol 2 = 0x1F8
  11. (key[10] xor 0x1B) rol 2 = 0x1A0
  12. (key[11] xor 0x1B) rol 2 = 0x1BC

32

去除 NAG 窗口

NAG 窗口指的是不断弹出的窗口。这个程序中我们每次关闭程序时都会弹出窗口提示编写程序的作者。

  1. 搜索字符串,定位到函数为位置,并动态调试

33
34
35

  1. 运行程序到断点位置,看到该函数没有修改堆栈,直接从堆栈中找到函数返回地址 0x4011F6

36

  1. 跳转到地址 0x4011F6 地址处,记录修改指令地址 0x4011EF。
    37

  2. 通过手动修改exe文件去除 NAG

Image Base:表示程序在内存加载的基地址
Base Of Code:表示程序开始运行的位置,距离 Image Base 得到偏移量(即 程序入口点)。
File Offset:当PE文件储存在某个磁盘当中的时候,某个数据的位置相对于文件头的偏移量。

“Image Base” + “Base Of Code(Entry Point)” = 文件运行的代码在内存中的基地址(也可以理解为第一条指令在内存中的基地址)

“File Offset” 就表示文件在磁盘中(未运行)时,代码的相对的偏移地址(也可以理解为第一条指令在磁盘中的基地址)。

38

内存地址计算:内存中需要修改地址 - 内存中的入口点地址 = 0x4011EF - 0x401000 = 0x1EF

磁盘地址计算:文件偏移地址 + 0x1EF = 0x600 + 0x1F6 = 0x7EF(磁盘中需要修改的地址)

jmp的硬编码是 0xEB,复制文件并在 0x7EF 偏移处的 0x75 改为 0xEB,并保存。

40

  1. 最后验证程序,发现 NAG 窗口已去除。

编写注册机

注册机一

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
/*程序名:012-crcme1.c*/
/*
新160个CrackMe算法分析012-crcme1,注册机。
*/
#define _CRT_SECURE_NO_DEPRECATE
#pragma warning(disable:4996)
#pragma comment(lib,"libssl.lib")
#pragma comment(lib,"libcrypto.lib")

#include <stdio.h>
#include <string.h>
#include <openssl/md5.h>


int main(int argc,char* argv[])
{
char name[20] = { 0 };
int x = 0;
int y = 0;
int serial=0;

printf("请输入用户名(长度大于4):");
scanf("%s", name);

printf("serial = ");

// 累计 name 的值
for (int i = 0; i < strlen(name); i++)
{
x += name[i];
}

//循环左移三位
x <<= 3;

//name异或得出结果
x ^= 0x515A5;

//算出 serial xor 0x87CA 的值
y = 0x797E7 - x;

//y xor 0x87CA 得出 serial
serial = y ^ 0x87CA;

printf("%d\n",serial);

return 0; //程序结束
}

注册机二

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
/*程序名:012-crcme1_KEYFILE.c*/
/*
新160个CrackMe算法分析012-crcme1,注册机。
*/
#define _CRT_SECURE_NO_DEPRECATE
#pragma warning(disable:4996)
#pragma comment(lib,"libssl.lib")
#pragma comment(lib,"libcrypto.lib")

#include <stdio.h>
#include <string.h>
#include <openssl/md5.h>


int main(int argc,char* argv[])
{
int key[12] = { 0 };
key[0] = (0x168 >> 2) ^ 0x1B;
key[1] = (0x160 >> 2) ^ 0x1B;
key[2] = (0x170 >> 2) ^ 0x1B;
key[3] = (0xEC >> 2) ^ 0x1B;
key[4] = (0x13C >> 2) ^ 0x1B;
key[5] = (0x1CC >> 2) ^ 0x1B;
key[6] = (0x1F8 >> 2) ^ 0x1B;
key[7] = (0xEC >> 2) ^ 0x1B;
key[8] = (0x164 >> 2) ^ 0x1B;
key[9] = (0x1F8 >> 2) ^ 0x1B;
key[10] = (0x1A0 >> 2) ^ 0x1B;
key[11] = (0x1BC >> 2) ^ 0x1B;

printf("ACG.key = ");
for (int i = 0; i < 12; i++)
{
printf("%c", key[i]);
}
return 0; //程序结束
}

013-Acid_burn

查看程序基本信息

  1. 程序没有夹克,使用的是 Delphi 语言编写的

41
42
43
44

静态分析

分支一

  1. 搜搜字符串

45

  1. 定位到 “Try Agsin!!”,定位到关键跳转位置

46
47

分支二

  1. 通过观察流程图,确认两个关键跳转的位置

51
52
53
54

动态调试

分支一

  1. 动态调试发现函数 “CALL 004039FC” 的返回值,和入口参数,猜测这是比较函数

48
49

  1. 输入另一个参数字符串 “Hello Dude!” 进行测试

50

分支二

  1. 在两个关键跳转位置处设置断点

55

  1. 第一个断点判断name长度

56

  1. 同样是使用函数 “CALL 004039FC” 比较字符串

57

  1. 动态调试,找到关键算法,这是将 (name[0] * 0x29) * 2 的值转换为十进制。

58
59

逆向过程中先别急着分析关键函数,尽量先查看入口参数和结果的关系,然后再重新验证,就能确认函数功能。闷着头分析算法会浪费很多时间。

编写注册机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*程序名:013-Acid_burn.c*/
/*
新160个CrackMe算法分析013-Acid_burn,注册机。
*/
#define _CRT_SECURE_NO_DEPRECATE
#pragma warning(disable:4996)
#pragma comment(lib,"libssl.lib")
#pragma comment(lib,"libcrypto.lib")

#include <stdio.h>
#include <string.h>
#include <openssl/md5.h>


int main(int argc,char* argv[])
{
char name[20] = { 0 };
printf("请输入用户名:");
scanf("%s", name);

printf("serial = CW-%d-CRACKED\n", (name[0] * 0x29) * 2);

return 0; //程序结束
}

014-Splish

查看程序基本信息

60
61

静态分析

  1. 硬编码流程图

62
63

66

  1. 验证硬编码

67

  1. name 和 serial 流程图

65
64

动态调试

  1. 在关键跳转处,和 GetWindowsTextA 后面设置断点,并分析算法

68
69
70

  1. 算法

key = ((name[n] % 10) ^ n) + 2

if(key > 10);key = key - 10

key = serial[n] % 10

编写注册机

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
/*程序名:014-Splish.c*/
/*
新160个CrackMe算法分析014-Splish,注册机。
*/
#define _CRT_SECURE_NO_DEPRECATE
#pragma warning(disable:4996)
#pragma comment(lib,"libssl.lib")
#pragma comment(lib,"libcrypto.lib")

#include <stdio.h>
#include <string.h>
#include <openssl/md5.h>


int main(int argc, char* argv[])
{
int key;
char serial[20] = {0};
char name[20] = { 0 };
printf("请输入用户名:");
scanf("%s", name);

for (int i = 0,n = 0; i < strlen(name); i++)
{
//key = ((name[n] % 10) ^ n) + 2
key = ((name[i] % 10) ^ i) + 2;

//if(key > 10);key = key - 10
if (key > 10)
{
key -= 10;
}

//key = serial[n] % 10
for (int j = 0, num = 0x30; j < 10; j++,num++)
{
if ((serial[n] == 0)&&(num % 10 == key)&&(n==i))
{
serial[n] = num;
n++;
}
}

}

for (int k = 0; k < strlen(name); k++)
{
printf("%c", serial[k]);
}

return 0; //程序结束
}

015-Brad Soblesky.1

查看程序基本信息

71

静态分析

  1. IDA搜索字符串找到关键函数

72

动态调试

  1. 在关键函数位置设置断点,并进行调试

73

  1. 验证 key = “

74

  1. 经动态调试验证,发现这个就是一个常量字符串比较

75

IDA 中未搜索到字符串 ““ 是因为字符串设置问题,右键字符串窗口,点击 “setup” ,勾选 “Ignore instructions/data definitions”

76

Ignore instructions/data definitions(忽略指令/数据定义)。这个选项会使IDA扫描指令和现有数据定义中的字符串。使用这个选项,可以让IDA扫描二进制代码中错误地转换成指令的字符串,或扫描数据中非字符串格式。使用这个选项的效果类似于使用strings -a命令。

016-fty_crkme3

查看程序基本信息

1
2
3

脱upx壳

  1. 使用 esp 定律脱壳

4
5

静态分析

  1. 搜索字符串

6

  1. 定位到代码位置,分析程序

7
8

动态调试

  1. 动态调试后发现程序并在在关键位置断下来,说明在这之前已经有其他代码判断出输入的秘钥错误。

9

  1. 因为其中的代码有点多,只能从头开始分析其中的关键算法位置

10
11

  1. 经过第一轮的分析,找到serial写法规则

![15](https://github.com/9unkk/tuchuang/assets/25861639/
ad32ca8a-f420-4da2-9060-a756be5412b9)

  1. 重新输入秘钥 “12-345-67” ,并进行调试。分析出上半段指令的功能,找到了当serial长度为9时的关键跳转和函数

14
12

  1. 向上翻会发现有多处地方都用了这个关键函数且后面的指令都是一样的,我们依次设置好断点。一共发现了7个这样的函数,而我们 serial 中的数字也是7个。

13

  1. 重新调试分析关键函数算法

16

  1. 最终回到关键跳转位置,分析算法

17

  1. 最终再验证serial的长度为10的算法是否也是一样的

18
19

编写注册机

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
/*程序名:016-fty_crkme3.c*/
/*
新160个CrackMe算法分析016-fty_crkme3,注册机。
*/
#define _CRT_SECURE_NO_DEPRECATE
#pragma warning(disable:4996)
#pragma comment(lib,"libssl.lib")
#pragma comment(lib,"libcrypto.lib")

#include <stdio.h>
#include <string.h>
#include <openssl/md5.h>


int main(int argc, char* argv[])
{
int num = 1;
int num_x = 0; //存储商
int num_y = 0; //存储余数
int cj;
int cum;
int ws;

for (int i = 0; i < 999999999; i++,num++)
{
cum = 0;
ws = 0;
num_x = num;
//判断数字有几位
for (int j = 0; j < 9&&num_x>0; j++)
{
num_x /= 10;
ws++;
}


//计算乘积
num_x = num;
for (int k = 0; k < ws; k++)
{
cj = 1;
num_y = num_x % 10;
num_x /= 10;

// 程序中最小的位数是7位,其中包括0
if (ws < 7)
{
ws = 7;
}

// 计算乘积
for (int m = 0; m < ws; m++)
{
cj *= num_y;
}

//计算累计值
cum += cj;
}

// 打印结果
if (num == cum&&ws==7)
{
printf("%d%d-%d%d%d-%d%d\n", num/1000000,(num/100000)%10, (num / 10000)%10, (num / 1000)%10, (num / 100)%10, (num / 10) % 10,num %10);
}
else if (num == cum && ws == 8)
{
printf("%d%d-%d%d%d-%d%d%d\n", num / 10000000, (num / 1000000) % 10, (num / 100000) % 10, (num / 10000) % 10, (num / 1000) % 10, (num / 100) % 10, (num / 10) % 10,num % 10);
}
else if (num == cum && ws == 9)
{
printf("%d%d-%d%d%d-%d%d%d%d\n", num / 100000000, (num / 10000000) % 10, (num / 1000000) % 10, (num / 100000) % 10, (num / 10000) % 10, (num / 1000) % 10, (num / 100) % 10, (num / 10) % 10, num % 10);
}
}
return 0; //程序结束
}

017-Cabeca

查看程序基本信息

20

静态分析

  1. 找到关键跳转

21
22

  1. 因为IDA没有分析出具体函数名,我们只能先看最上面,将无用的指令跳过

23

  1. 记录想要调试指令的位置

24

动态调试

  1. 设置断点,并进行调试

25

  1. 对关键的函数进行断点调试

26
27

  1. 验证是否为正确的 serial

28

  1. 找到关键算法函数,分析发现这里面并没有对 0x42F714 和 0x42F718 赋值的指令,说明关键算法不在这

29
30

  1. 重新运行程序,看到 0x42F714 和 0x42F718 默认是 0 。我们在该地址处设置硬件写入(word)断点,之前设置的断点都删除掉。

31

因为最终结果大小是一个字,所以这里设置字大小的硬件断点

  1. 当我们输入两个字节后,程序断在如下位置,可以看到这一段都是 case 语句。

32

  1. F8 继续执行程序,找到关键算法函数

33

  1. 删除设置的硬件断点,并在算法函数位置处设置断点

    算法分析

  2. 通过动态调试找到关键算法 cese 语句。

35
34

  1. case语句在动态窗口看着有点累,我们在IDA中定位到该地址处,再按 F5 生成伪代码。

36

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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
int __fastcall TForm1_Edit1KeyPress(int result, int a2, _BYTE *a3)
{
switch ( *a3 )
{
case 8:
sub_419E10(*(_DWORD *)(result + 480), 0);
dword_42F714 = 0;
result = 0;
dword_42F718 = 0;
break;
case 0x41:
dword_42F714 += 1064;
dword_42F718 += 5648;
break;
case 0x42:
dword_42F714 += 726576;
dword_42F718 += 2;
break;
case 0x43:
dword_42F714 += 3462;
dword_42F718 += 9999;
break;
case 0x44:
dword_42F714 += 4516;
dword_42F718 += 74445628;
break;
case 0x45:
dword_42F714 += 73482;
dword_42F718 += 35644;
break;
case 0x46:
dword_42F714 += 15554;
dword_42F718 += 34328;
break;
case 0x47:
dword_42F714 += 254376;
dword_42F718 += 444444;
break;
case 0x48:
dword_42F714 += 37348;
dword_42F718 += 2615621;
break;
case 0x49:
dword_42F714 += 27458;
dword_42F718 += 3131331;
break;
case 0x4A:
dword_42F714 += 333476;
dword_42F718 += 12121212;
break;
case 0x4B:
dword_42F714 += 275546;
dword_42F718 += 71111;
break;
case 0x4C:
dword_42F714 += 1834457;
dword_42F718 += 76628;
break;
case 0x4D:
dword_42F714 += 10349;
dword_42F718 += 734348;
break;
case 0x4E:
dword_42F714 += 1025;
dword_42F718 += 897376628;
break;
case 0x4F:
dword_42F714 += 1652;
dword_42F718 += 3243223;
break;
case 0x50:
dword_42F714 += 156;
dword_42F718 += 8247348;
break;
case 0x51:
dword_42F714 += 342;
dword_42F718 += 236752;
break;
case 0x52:
dword_42F714 += 34343;
dword_42F718 += 783434;
break;
case 0x53:
dword_42F714 += 7635344;
dword_42F718 += 8734342;
break;
case 0x54:
dword_42F714 += 42344;
dword_42F718 += 78368;
break;
case 0x55:
dword_42F714 += 87442;
dword_42F718 += 12334;
break;
case 0x56:
dword_42F714 += 7641;
dword_42F718 += 7235;
break;
case 0x57:
dword_42F714 += 15552;
dword_42F718 += 323528;
break;
case 0x58:
dword_42F714 += 9834;
dword_42F718 += 732523528;
break;
case 0x59:
dword_42F714 += 33553;
dword_42F718 += 7238;
break;
case 0x5A:
dword_42F714 += 52763;
dword_42F718 += 726628;
break;
case 0x61:
dword_42F714 += 1063;
dword_42F718 += 121;
break;
case 0x62:
dword_42F714 += 1724;
dword_42F718 += 111;
break;
case 0x63:
dword_42F714 += 1169;
dword_42F718 += 738;
break;
case 0x64:
dword_42F714 += 18253;
dword_42F718 += 762;
break;
case 0x65:
dword_42F714 += 1024;
dword_42F718 += 14;
break;
case 0x66:
dword_42F714 += 1744;
dword_42F718 += 13;
break;
case 0x67:
dword_42F714 += 1661;
dword_42F718 += 12;
break;
case 0x68:
dword_42F714 += 1872;
dword_42F718 += 11;
break;
case 0x69:
dword_42F714 += 1084;
dword_42F718 += 99;
break;
case 0x6A:
dword_42F714 += 1892;
dword_42F718 += 888;
break;
case 0x6B:
dword_42F714 += 192;
dword_42F718 += 77;
break;
case 0x6C:
dword_42F714 += 10109;
dword_42F718 += 555;
break;
case 0x6D:
dword_42F714 += 2078;
dword_42F718 += 90;
break;
case 0x6E:
dword_42F714 += 3591;
dword_42F718 += 98;
break;
case 0x6F:
dword_42F714 += 142;
dword_42F718 += 7468;
break;
case 0x70:
dword_42F714 += 632432;
dword_42F718 += 575475;
break;
case 0x71:
dword_42F714 += 3415;
dword_42F718 += 648;
break;
case 0x72:
dword_42F714 += 24555;
dword_42F718 += 538;
break;
case 0x73:
dword_42F714 += 2224;
++dword_42F718;
break;
case 0x74:
dword_42F714 += 1211;
dword_42F718 += 64;
break;
case 0x75:
dword_42F714 += 2242;
dword_42F718 += 75;
break;
case 0x76:
dword_42F714 += 7334;
dword_42F718 += 78;
break;
case 0x77:
dword_42F714 += 9502;
dword_42F718 += 5;
break;
case 0x78:
dword_42F714 += 917;
dword_42F718 += 38;
break;
case 0x79:
dword_42F714 += 11539;
dword_42F718 += 8;
break;
case 0x7A:
dword_42F714 += 6400;
dword_42F718 += 456;
break;
default:
return result;
}
return result;
}
  1. 手动计算得出如下结果:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    name = "9unk"
    serial-1 = 6025
    serial-2 = 250

    计算过程:
    39 = default(不进行任何计算)
    75 = 2242 ; 75
    6E = 2242+3591; 75+98
    6B = 2242+3591+192;75+98+77

编写注册机

关键算法就是上面的伪代码,我们稍微修改优化一下就行。

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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
/*程序名:017-Cabeca.c*/
/*
新160个CrackMe算法分析017-Cabeca,注册机。
*/
#define _CRT_SECURE_NO_DEPRECATE
#pragma warning(disable:4996)
#pragma comment(lib,"libssl.lib")
#pragma comment(lib,"libcrypto.lib")

#include <stdio.h>
#include <string.h>
#include <openssl/md5.h>


void __fastcall EditKeyPress(int* result1,int* result2,int a)
{
switch (a)
{
case 0x41:
*result1 += 1064;
*result2 += 5648;
break;
case 0x42:
*result1 += 726576;
*result2 += 2;
break;
case 0x43:
*result1 += 3462;
*result2 += 9999;
break;
case 0x44:
*result1 += 4516;
*result2 += 74445628;
break;
case 0x45:
*result1 += 73482;
*result2 += 35644;
break;
case 0x46:
*result1 += 15554;
*result2 += 34328;
break;
case 0x47:
*result1 += 254376;
*result2 += 444444;
break;
case 0x48:
*result1 += 37348;
*result2 += 2615621;
break;
case 0x49:
*result1 += 27458;
*result2 += 3131331;
break;
case 0x4A:
*result1 += 333476;
*result2 += 12121212;
break;
case 0x4B:
*result1 += 275546;
*result2 += 71111;
break;
case 0x4C:
*result1 += 1834457;
*result2 += 76628;
break;
case 0x4D:
*result1 += 10349;
*result2 += 734348;
break;
case 0x4E:
*result1 += 1025;
*result2 += 897376628;
break;
case 0x4F:
*result1 += 1652;
*result2 += 3243223;
break;
case 0x50:
*result1 += 156;
*result2 += 8247348;
break;
case 0x51:
*result1 += 342;
*result2 += 236752;
break;
case 0x52:
*result1 += 34343;
*result2 += 783434;
break;
case 0x53:
*result1 += 7635344;
*result2 += 8734342;
break;
case 0x54:
*result1 += 42344;
*result2 += 78368;
break;
case 0x55:
*result1 += 87442;
*result2 += 12334;
break;
case 0x56:
*result1 += 7641;
*result2 += 7235;
break;
case 0x57:
*result1 += 15552;
*result2 += 323528;
break;
case 0x58:
*result1 += 9834;
*result2 += 732523528;
break;
case 0x59:
*result1 += 33553;
*result2 += 7238;
break;
case 0x5A:
*result1 += 52763;
*result2 += 726628;
break;
case 0x61:
*result1 += 1063;
*result2 += 121;
break;
case 0x62:
*result1 += 1724;
*result2 += 111;
break;
case 0x63:
*result1 += 1169;
*result2 += 738;
break;
case 0x64:
*result1 += 18253;
*result2 += 762;
break;
case 0x65:
*result1 += 1024;
*result2 += 14;
break;
case 0x66:
*result1 += 1744;
*result2 += 13;
break;
case 0x67:
*result1 += 1661;
*result2 += 12;
break;
case 0x68:
*result1 += 1872;
*result2 += 11;
break;
case 0x69:
*result1 += 1084;
*result2 += 99;
break;
case 0x6A:
*result1 += 1892;
*result2 += 888;
break;
case 0x6B:
*result1 += 192;
*result2 += 77;
break;
case 0x6C:
*result1 += 10109;
*result2 += 555;
break;
case 0x6D:
*result1 += 2078;
*result2 += 90;
break;
case 0x6E:
*result1 += 3591;
*result2 += 98;
break;
case 0x6F:
*result1 += 142;
*result2 += 7468;
break;
case 0x70:
*result1 += 632432;
*result2 += 575475;
break;
case 0x71:
*result1 += 3415;
*result2 += 648;
break;
case 0x72:
*result1 += 24555;
*result2 += 538;
break;
case 0x73:
*result1 += 2224;
++*result2;
break;
case 0x74:
*result1 += 1211;
*result2 += 64;
break;
case 0x75:
*result1 += 2242;
*result2 += 75;
break;
case 0x76:
*result1 += 7334;
*result2 += 78;
break;
case 0x77:
*result1 += 9502;
*result2 += 5;
break;
case 0x78:
*result1 += 917;
*result2 += 38;
break;
case 0x79:
*result1 += 11539;
*result2 += 8;
break;
case 0x7A:
*result1 += 6400;
*result2 += 456;
break;
default:
break;
}
}

int main(int argc, char* argv[])
{
char name[20] = { 0 };
int a=0;
int b=0;
printf("请输入用户名:");
scanf("%s", name);

for (int i = 0; i < strlen(name); i++)
{
EditKeyPress(&a, &b, name[i]);
}

printf("serial-1 = %d\nserial-2 = %d\n", a, b);

return 0; //程序结束
}

018-crackme_0006

查看程序基本信息

37

静态分析

  1. 搜索字符串

38

  1. 找到关键函数记录地址

39
40
41

  1. 向上找到 GetDlgItemTextA 函数,并记录地址

42
43

动态调试

  1. 获取输入的 name

44

  1. 经过第一次动态调试,发现了两个可疑的函数。经过二次调试分析出第一个函数是计算 name 的乘积,第二个函数是将结果 循环左移 1 位

45
46

  1. 再次经过调试分析程序

47

  1. 最终根据关键函数 strcmp 验证确认 serial

48
49

  1. 但是还有一个疑问,其中有一个局部变量 [ebp-0x10]=0x22347010,这个我们并不确定是固定值,还是由其他某个算法函数生成的。再次换另一种 name 和 serial 动态调试,确认一下。

51

  1. 但是在 win11 中测试程序时发现并未成功,重新调试后,发现两个不同的系统 [ebp+0x10] 的值不一样

52

  1. 向上找关于 [ebp-0x10] 的指令,发现[ebp-0x10]存储的是函数的返回值,其入口参数是 [ebp-8] 和 [ebp-4],继续向上找,最终定位到 0x4011D5 处

54

  1. 找到关键函数并分析:将 “C盘” 和 “D盘” 的 pVolumeSerialNum 参数值分别转为浮点数;再分别 * 2;最后相加、开方,转换成整数。

62
55
56
57
58
59
60
61
63

  1. 算法分析

第一步:将 “C盘” 和 “D盘” 的 pVolumeSerialNum 参数值分别转为浮点数;再分别 * 2;最后相加、开方,转换成整数 key 。

第二步:num = ((name[i] 的乘积 ) << 1) or key;第 8 位置 0;循环从 071362de9f8ab45c 中取第 (num % 0x10) 位,num /= 4

编写注册机

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
/*程序名:018-crackme_0006.c*/
/*
新160个CrackMe算法分析018-crackme_0006,注册机。
*/
#define _CRT_SECURE_NO_DEPRECATE
#pragma warning(disable:4996)


#include <stdio.h>
#include <stdlib.h>
#include <windows.h>

#define B_PATH 0x80

//浮点算法
int AlAn(int x, int y)
{
int a = 0;

__asm
{
fwait
finit
fild x
fld st(0)
fmulp st(1), st(0)
fild y
fld st(0)
fmulp st(1), st(0)
faddp st(1), st(0)
fsqrt
fistp a
}
return a;
}

int main(void)
{
unsigned int num1 = 0;
unsigned int num2 = 0;
unsigned long key1 = 0;
unsigned long key2 = 1;
unsigned long key3 = 0;
char name[] = { 0 };
char str[16] = { '0','7','1','3','6','2','d','e','9','f','8','a','b','4','5','c' };

char szVolumeNameBuf[MAX_PATH] = { 0 };
DWORD dwVolumeSerialNum;
DWORD dwMaxComponetLength=0xFF;
DWORD dwSysFlags;
char szFileSystemBuf[MAX_PATH] = { 0 };

//获取C盘和D盘的的信息,并存储返回值
GetVolumeInformationA("c:\\ ", szVolumeNameBuf, B_PATH,&dwVolumeSerialNum, &dwMaxComponetLength, &dwSysFlags, szFileSystemBuf, B_PATH);
num1 = dwVolumeSerialNum;

GetVolumeInformationA("d:\\ ", szVolumeNameBuf, B_PATH, &dwVolumeSerialNum, &dwMaxComponetLength, &dwSysFlags, szFileSystemBuf, B_PATH);
num2 = dwVolumeSerialNum;

//经过浮点算法函数,得出结果
key1=AlAn(num1, num2);


printf("请输入一个用户名:");
scanf("%s", name);

//循环计算 name 的乘积
for (int i = 0; i < strlen(name); i++)
{
key2 *= name[i];
}

//计算 key3
key3 = ((key2 << 1) | key1) & 0x0FFFFFFF;

//输出结果
for (unsigned long j = key3; j > 0; j /= 4)
{
printf("%c", str[j % 0x10]);
}

printf("\n");

system("pause");
return 0; //程序结束
}

019-Acid Bytes

  1. 程序有 upx 壳

64

  1. 程序有 nag 窗口

65

esp 定律脱壳

  1. F8 执行指令 pushad,对 esp 设置硬件断点

66

  1. F9 运行程序到断点处

67

  1. F8 运行到程序真正入口点,并进行脱壳

68

  1. 验证是否脱壳成功

69

去除 nag 窗口

  1. IDA 搜索字符串

70

  1. 定位,记录内存地址

71

  1. 设置在内存地址处设置断点

72

  1. 动态调试,返回到父函数

73

  1. 经过多次动态调试,发现 ebx 是一个动态值,根据 ebx 不同值输出不同的结果。所以最简单去 nag 的方式是将 SpeedButton2Click 函数 nop 掉。

78

分析程序

  1. 搜索字符串,找到关键函数,记录相关内存地址

79

  1. 动态调试后发现就是字符串比较:name = “Registered User”,serial = “GFX-754-IER-954”

80

020-cosh.3

查看程序基本信息

81

静态分析

  1. 搜索关键字,定位到程序位置

82

  1. 一直向上找到第一个跳转错误的位置 0x004014F5

83

动态调试

  1. 动态调试分析程序

84

  1. 找到算法部分,进行分析

85

  1. 验证算法
1
2
3
4
5
6
(39 xor 1) xor 0xA = 2
(75 xor 2) xor 0xB = |
(6E xor 3) xor 0xC = a
(6B xor 4) xor 0xD = b
(6B xor 5) xor 0xE = `
(6B xor 6) xor 0xF = b

86

编写注册机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*程序名:020-cosh.3.c*/
/*
新160个CrackMe算法分析 020-cosh.3,注册机。
*/

#define _CRT_SECURE_NO_DEPRECATE
#include <stdio.h>

int main(void)
{
char name[20] = { 0 };

printf("请输入用户名:");
scanf("%s", name);


for (int i = 1, j = 0xA; i <= strlen(name); i++, j++)
{
printf("%c",(name[i-1] ^ i) ^ j);

}
return 0; //程序结束
}

021-DIS[IP]-Serialme

  1. 程序无壳

1

2

静态分析

  1. 搜索字符串,定位到主程序

3

  1. 找到疑似算法部分

4

动态调试

  1. 设置断点,动态调试分析算法

5

编写注册机

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
/*程序名:021-DIS[IP]-Serialme.c*/
/*
新 160 个 CrackMe 算法分析 021-DIS[IP]-Serialme,注册机。
*/

#define _CRT_SECURE_NO_DEPRECATE
#include <stdio.h>

int main(void)
{
char name[20] = { 0 };
char num = 0;
char result[2] = { 0 };

//输入用户名
printf("请输入用户名:");
scanf("%s", name);

//算法
for (int i = 0; i < strlen(name); i++)
{
num = 0x61;

if ((name[i] != 'Z') && (name[i] != 'z') && (name[i] != '9'))
{
name[i] += 1;
}
num += i;

result[0] = num;
result[1] = name[i];

//打印serial
for (int j = 1; j >=0; j--)
{
printf("%c", result[j]);
}

}

return 0; //程序结束
}

022-CM

  1. 程序无壳

6

7

静态分析

  1. 搜索字符串定位到主程序

8

  1. 找到最上面的指令记录地址

9

动态调试

  1. 在地址处设置断点,并分析算法

10

serial = 6287-A

023-TraceMe

  1. 程序无壳

11

12

静态分析

  1. 搜索字符串,定位到程序字符串位置

13

对程序分析后发现这个程序看不懂,并不是我们正常的程序运行流程,报错字符串前面也看不到任何的跳转和call指令。

  1. 既然有输入对话框,就一定会使用 GetDlgItemTextA 函数来获取字符串。

14

记录地址:0x40119C

  1. 既然要验证 serial,一般都会使用 strcmp 函数来比较。尝试点开其他自定义函数进行查询。

15

记录地址:0x401379

动态调试

  1. 在两个 GetDlgItemTextA 函数位置设置断点

16

  1. 在 strcmp 函数位置处设置断点

17

  1. 动态调试,分析算法

18

19

20

  1. 验证 serial

21

复习:neg 和 sbb 指令

1
2
3
4
5
6
7
8
neg:用零减去操作数
当操作数为0时,置CF位为0
当操作数不为0时,置CF位为1

SBB:带进位(CF)减法
sbb eax,eax(eax-eax-CF)

strcmp 函数,通常会连起来使用这两个指令

注册机

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
/*程序名:023-TraceMe.c*/
/*
新 160 个 CrackMe 算法分析 023-TraceMe,注册机。
*/

#define _CRT_SECURE_NO_DEPRECATE
#include <stdio.h>

int main(void)
{
char name[20] = { 0 };
int key[8] = { 0x0C,0x0A,0x13,0x09,0x0C,0x0B,0x0A,0x08};
int result = 0;

//输入用户名
printf("请输入用户名:");
scanf("%s", name);

//算法:循环 i < strlen(name),并且 j > 7 要重置
for (int i = 3, j = 0; i < strlen(name); i++, j++)
{
//如果 key 遍历完就重置
if (j > 7)
{
j = 0;
}

//result = key[j] * name[i]
result += key[j] * name[i];
}

printf("serial = %d", result);
return 0; //程序结束
}

024-reverseMe

查看程序

  1. 程序无壳

22

  1. 打开程序后提示:评估期已过期。购买新许可证

23

IDA 静态分析

  1. IDA 搜索字符串找到 keyfile.dat 文件名

24

  1. 新建 keyfile.dat,并输入任意值,重新打开文件后层序提示:密钥文件无效。

25

  1. 分析出至少输入 16 个 key 值。

26

  1. 再次分析,发现算法部分,keyfile 中至少要有8个字母 ‘G’

27

  1. 验证算法

28

025-CRC-32crackme

CRC 校验

CRC的目的是保证数据的完整性,其方法是在发送数据的后面再增加多余的若干位数据,接收方使用同样的CRC计算方法,检查接收到的数据CRC是否为0:

  • 如果为0,则表示数据是完整的,接收方可以开开心心的去处理这个数据。
  • 如果不为0,则表示数据不完整/出错,接收方就需要处理下这个数据(一般是丢弃/要求重发)。

CRC校验有多种类型如:

  • CRC12:用于某些电信标准。
  • CRC16:常见的变体有多用途链路层CRC(PLCC)和CRC-16-IBM等。
  • CRC32:广泛用于网络通信和文件校验。
  • CRC64:用于处理64位数据,例如在某些网络协议中。

不同的应用可能会根据其特定需求选择不同的CRC算法。而每种类型的CRC校验算法都有其固定的多项式,最后根据模 2 除法生成多项式表。

如果想要计算某一数据的校验码,程序会使用循环得出校验和,其步骤如下:

  1. 按字节异或 0xFF
  2. 根据返回值查CRC表
  3. 对查到的CRC值进行累加

模 2 除法: 也称模 2 运算,其本质就是异或(xor)算法。

多项式: 在二进制形式下,多项式中的1表示对应的位被设置,而0表示未设置。例如多项式:x^3 + x^2 +x^0 = 1101,按顺序排列其中少了 x^1 ,所以有一个为 0 。

CRC 表的生成

CRC 校验表是通过循环依次计算 0~255 校验值作为表的元素。

这里用 CRC8 校验算法作为案例生成CRC表中 “0x01” 的校验值:

  • 第一步:确认 CRC8 多项式的二进制值

  • 第二步:多项式的值,最高位 1 隐藏(忽略),然后再将该值颠倒。

  • 第三步:声明一个变量 temp 存储整数值

  • 第四步:判断 temp 最低位。如果最低位为 1 ,先将 temp 右移 1 位,然后将 temp 和 颠倒后的多项式进行异或运算,结果保存到 temp。如果最低位为 0 ,则将 temp 右移 1 位。

  • 第五步:重复第二步,直到的 temp 中的 8 位数全部右移出去(简单来说就是一共要右移 8 次)。

  • 第六步:循环结束后,最终 temp 中存储的就是 temp原来整数的 CRC8 的校验值。

确认多项式值
  1. CRC8 多项式 X^8+X^5+X^4+X^0 = 0x131= 1 0011 0001
  2. 隐藏最高位 1 = 0011 0001
  3. 按位颠倒后得到 1000 1100 = 0x8c
计算整数 1(temp)的 CRC8 校验值

如果 temp 末尾为1,右移 1 位,然后与 0x8C 异或,存储结果到 temp;如果 temp 最低位为 0 ,则 temp 右移 1 位

temp = 0000 0001

  • 第 1 次右移:temp(0000 0001) >> 1 = 0000 0000

temp = temp(0000 0000) xor 1000 1100 = 1000 1100

  • 第 2 次右移:temp = temp(1000 1100) >> 1 = 0100 0110

  • 第 3 次右移:temp = temp(0100 0110) >> 1 = 0010 0011

  • 第 4 次右移:temp = temp(0010 0011) >> 1 = 0001 0001

temp = temp(0001 0001) xor 1000 1100 = 1001 1101

  • 第 5 次右移:temp = temp(1001 1101) >> 1 = 0100 1110

temp = temp(0100 1110) xor 1000 1100 = 1100 0010

  • 第 6 次右移:temp = temp(1100 0010) >> 1 = 0110 0001

  • 第 7 次右移:temp = temp(0110 0001) >> 1 = 0011 0000

temp = temp(0011 0000) xor 1000 1100 = 1011 1100

  • 第 8 次右移:temp = temp(1011 1100) >> 1 = 0101 1110 = 0x5E
生成 CRC 表

由于CRC的计算过程中需要不停的循环做异或运算,占用CPU较多,算法上有一种空间换时间的做法:提前把0x00-0xFF共256个数据的CRC码提前算好保存,那么计算时可以节省CPU,这个提前算好的表叫CRC表

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
/*程序名:CRC_Tab.c*/
/*
程序演示如何生成 CRC 多项式表。
*/
#define _CRT_SECURE_NO_DEPRECATE


#include <stdio.h>

//入口参数:x = 初始多项式指针(最大支持32位),y = 多项式有几个字节
int CRC_Poly(int x, int y)
{
int temp=0;
_asm
{
mov ecx, y
mov edx, x
xor eax, eax
shl ecx, 3
dec ecx
CRC_Rev:
shr edx, 1
adc eax, 0
shl eax, 1
loop CRC_Rev
mov temp,eax
}

return temp;
}


// x 表示需要校验的数据,y 表示处理后的多项式值
int CRC_Tab(int x, int y)
{
int temp=0;
_asm
{
//计数器置0
xor ecx,ecx
//生成CRC表时x的最大值是255,一共8bit
mov eax,x


CRC_Tab1:
//
cmp ecx, 8
jz end1

//如果末尾bit为0,先右移一位,如果为1,右移一位,再与多项式异或
inc ecx
shr eax,1

//如果CF=1,说明数值末尾是1
jc CRTab_CF
jmp CRC_Tab1
//如果溢出 与多项式进行异或
CRTab_CF:
xor eax,y
jmp CRC_Tab1
end1:
mov temp,eax
}

return temp;
}



int main(void)
{
int crc8[256] = { 0 };
int poly = 0x07; //0x04C11DB7,注意多项式最高位隐藏后,最前面的0别忽略了。
int proc_poly = 0;
int temp1 = 0;

//翻转生成项
proc_poly = CRC_Poly(poly, 1);

//循环255次
for (int i = 0; i < 256; i++)
{

if (i > 0)
{
crc8[i] = CRC_Tab(i, proc_poly);
}

printf("0x%x ", crc8[i]);
}

return 0; //程序结束
}

34

参考:

通过查CRC表法,计算校验和

  1. 先确认需要计算的数据串的指针和数据串长度(字节)

  2. 确认初始异或值,也可认为是CRC初始值。再确认最终需要异或的值

  3. 计算CRC表下标,并保留低字节:(数据串字节 xor CRC)& 0xFF

  4. 移出处理过的 CRC 值:CRC >> = 8

  5. 未处理过的 CRC xor CRC_Tab[下标]

  6. 循环重复 3~5 步

  7. CRC 检验和 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
26
27
28
int CRCSum(int* Dat, int Len, int Init_CRC, int END_XOR)
{
//首先CRC有个初始值,一般为全 0 或 -1
uint32_t CRC = Init_CRC;//初始值,要定义为无符号数,不然后面右移操作会出错

//初始化下表值
int Zj = 0;

//循环读取字符串各字节进行处理
for (int i = 0; i < Len; i++)
{
//第一步:(Dat[i] & 0xFF) 将数据串的高位清0;保留低字节;
//与 CRC 进行异或(异或之后高位变为1);& 0xFF 再重新将高位清零,八六低字节。
Zj = ((Dat[i] & 0xFF)^ CRC) & 0xFF;//生成标号


//将 CRC 处理过的字节移出,右移8位
CRC >>= 8;

// 与 CRC 表中的值进行异或,得到新的 CRC 值
CRC = CRC ^ crc_tab32[Zj];
}

//异或 END_XOR
CRC ^= END_XOR;

return (CRC);
}

参考:

这里是为了理解CRC校验,所以参考别人的代码手写的。后面会直接利用

查看程序

  1. 查看程序基本信息

29

  1. 运行程序,未发现任何错误提示

30

IDA 静态分析

  1. 搜索字符串找到关键跳转,并记录地址 0x404388

31

  1. 继续分析上面的代码

32

  1. 这里我们字符串长度唯一不超过 5 的就是 name,修改后看到程序提示错误信息

33

在程序逻辑框架中还有不少自定义函数,我们需要通过动态调试才能分析程序算法。

动态调试

  1. 经过动态调试,找到了2个可疑的函数,这基本可以确定是两个关键算法函数
    35
    36

算法分析

  1. 分析第一个函数算法

37
38

  1. 分析第二个算法

39

我们将第二个值放到计算器中看一下,发现这个算法就是将字符串转换为整数

40

  1. 验证serial,将CRC32检验和(0x21495E8D)转为十进制输入看一下

41

注册机

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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
/*程序名:025-CRC-32crackme.c*/
/*
新 160 个 CrackMe 算法分析 025-CRC-32crackme,注册机。
*/
#define _CRT_SECURE_NO_DEPRECATE
#include <stdio.h>
#include <stdlib.h>


#define CRC_POLY_32 0xEDB88320L
#define CRC_START_32 0xFFFFFFFFL

static int crc_tab32_init = 0;
static uint32_t crc_tab32[256];

static void init_crc32_tab();

uint32_t crc_32(const unsigned char* input_str, size_t num_bytes);

uint32_t crc_32(const unsigned char* input_str, size_t num_bytes)
{

uint32_t crc;
uint32_t tmp;
uint32_t long_c;
const unsigned char* ptr;
size_t a;

//如果CRC表未初始化就生成CRC表
if (!crc_tab32_init)
init_crc32_tab();

crc = CRC_START_32;
ptr = input_str;

//判断输入字符串指针指针是否为NULL
if (ptr != NULL)
for (a = 0; a < num_bytes; a++)
{
//字符串值与0x000000FF做and运算,将高位初始化为0
long_c = 0x000000FFL & (uint32_t)*ptr;
//第一步:字符串字节与0xFFFFFFFF(初始项)进行异或,其中有效值是低8位。
tmp = crc ^ long_c;
//第二步:将处理过的CRC低8位移出,等待下一步处理,同时保证CRC最高位与字节最高位对齐
//第三步:tmp & 0xFF 保证高位置0,提取CRC表中对应的值与处理过的CRC进行异或

crc = (crc >> 8) ^ crc_tab32[tmp & 0xFF];
ptr++;
}

//CRC 再次异或结束项
crc ^= 0xFFFFFFFFL;

return crc & 0xFFFFFFFFL;

} /* crc_32 */




uint32_t update_crc_32(uint32_t crc, unsigned char c)
{

uint32_t tmp;
uint32_t long_c;

long_c = 0x000000FFL & (uint32_t)c;

if (!crc_tab32_init)
init_crc32_tab();

tmp = crc ^ long_c;
crc = (crc >> 8) ^ crc_tab32[tmp & 0xff];

return crc & 0xFFFFFFFFL;

} /* update_crc_32 */

static void init_crc32_tab()
{

uint32_t i;
uint32_t j;
uint32_t crc;

for (i = 0; i < 256; i++)
{
crc = i;
for (j = 0; j < 8; j++)
{
if (crc & 0x00000001L)
crc = (crc >> 1) ^ CRC_POLY_32;
else
crc = crc >> 1;
}
crc_tab32[i] = crc;
}

crc_tab32_init = 1;
} /* init_crc32_tab */



int main(void)
{
char name[20] = { 0 };
char str[26] = { "DiKeN"};

printf("请输入用户名:");
scanf("%s", name);

strcat(str, name);

printf("serial = %u\n", crc_32(str, strlen(str)));
return 0; //程序结束
}

026-KeygenMe

查看程序

  1. 程序是用汇编写的,程序未加壳。

1

静态分析

  1. IDA 静态分析定位的程序

2

3

  1. 内存地址处设置断点,使用 OD 分析程序

4

6

我们可以直接静态分析出算法,分析不出来就动态调试一下。

还有一个指令需要注意: CMP ESI,DWORD PTR DS:[0x403138] 这里表示我们输入的name生成了正确的十六进制后,还要再将各字节反转,输出正确的字符才能注册成功。而我们name生成的十六进制值最大值值是 4 字节。

  1. 而我们seial的字符串可以使用系统中相应的GBK编码作为字符表,同样name也可以输入中文

7

编写注册机

ASCII 可打印 ASCII 32126(0x207E)
GBK:可打印 0x8140 - 0xFEFE

注册机:

  1. 通过输入 name 来生成十六进制值
  2. 将生成的十六进制值进行反转
  3. 要求1:判断第一个字节是否在 0x20~0x7E 之间
  4. 要求2:如果不符合要求就判断 2 字节是否在0x8140 - 0xFEFE中
  5. 使用循环判断字符,如果符合要求1,就继续判断下一个字符。如果不符合要求1,就判断是否符合要求2。如果两个都不符合就报错。
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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
/*程序名:026-KeygenMe.c*/
/*
新 160 个 CrackMe 算法分析 026-KeygenMe,注册机。
*/

#define _CRT_SECURE_NO_DEPRECATE
#include <stdio.h>
#include <stdlib.h>


//判断*x(源字符)是否符合正常输出的ASCII码,同时把字符串传到 *y(目的字符)中
//如果执行错误返回值为-1(不传输字符)
int Ascii_Check(char* x,char* y)
{
//先判断第一个字节是否符合英文字符、符号标准
if ((*x >= 32) && (*x <= 0x7E))
{
*y = *x;
}
else
{
return -1;
}
}

//判断*x(源字符)是否符合正常输出的GBK码,同时把字符串传到 *y(目的字符)中
//如果执行错误返回值为-1(不传输字符)
int Gbk_Check(int x,char* y)
{
if ((x >= 0x8140) && (x <= 0xFEFE))
{

*y = x / 0x100;
*(y+1) = x % 0x100;
}
else
{
return -1;
}

}


int main(void)
{
unsigned int num = 0;
char rev_str[5] = { 0 };
char name[20] = { 0 };
char A_result[2] = { 0 };
char G_result[3] = { 0 };
int bytes = 0;
printf("请输入用户名:");
scanf("%s", name);

//通过算法计算结果
for (int i = 0; i < strlen(name); i++)
{
num += name[i] * name[i];
num += (((name[i] / 2) + 3) * name[i]) - name[i];
num *= 2;
}


//将字符串反向存储到
for (int i = num,j = 0; i > 0; i >>= 8,j++)
{
rev_str[j] = i % 0x100;
}


bytes = strlen(rev_str);
//判断一个十六进制值是否是字节数,不是字节数就报错


for (int i = 0; i <bytes;)
{
//首先判断第一个字节是否符合ASCII,如果不符合,就后退一步判断是否符合GBK编码
if (Ascii_Check(rev_str+i, A_result) != -1)
{
printf("%c", A_result[0]);
i++;
}
else
{
//如果剩余的字节不满2字节,i就需要减1。i从0开始,要多减1
if ((bytes-i) < 2)
{
//还需要提前确认后续计算的字节本能超过数值的长度,如果超出了就报错
if ((i + 2) <= bytes)
{
i--;
}
else
{
printf("Serial gen Error\n");
break;
}

}

//还原再次判断是否符合GBK编码
num = (rev_str[i] << 8);
num += (unsigned char)rev_str[i + 1];
num &= 0x0000FFFF;
if (Gbk_Check(num, G_result) != -1)
{
printf("%s", G_result);
i += 2;
}
else
{
printf("Serial gen Error\n");
break;
}
}


}
return 0; //程序结束
}

当然还可以写一个内存注册机直接将结果写入程序中,暂时还不会写,先放着留着以后写。

027-MexeliteCRK1

查看程序

  1. 程序是使用 Delphi 写的,无壳

8

静态分析

  1. 根据关键字搜索字符串

9

  1. 双击搜索字符串,从这里我们就可以猜到这个字符串大概率就是 serial

10

delphi逆向,通常找按钮触发事件一般在 “_TForm1_Button1Click” 中。

动态调试

  1. 动态调试验证一下

11

  1. 输入 name = Benadryl 进行验证

12

028-ArturDents-CrackMe#3

  1. 程序有 PEtitle2.x 壳

13

ESP 定律脱壳

  1. 在 ESP 第一次变动的时候设置断点

14

15

  1. F9 运行程序,此时程序在系统领空

16

  1. Ctrl+F9 运行到程序领空,此时看见我们跳到的指令是一个 SEH 异常指令。

17

经过前几次执行会发现,直接 F9 继续运行程序,程序会进入异常终止程序运行。

  1. 按 shift+F9 忽略异常,运行程序。

18

  1. 继续按 shift+F9 忽略异常运行程序

19

  1. Ctrl+F9 运行到程序领空

20

  1. 当运行程序到一段看不懂的指令时,基本就说明已经运行到程序的入口地址。这里从模块中删除分析的代码,即可还原代码。

21

22

  1. 直接脱壳

23

24

  1. 验证程序是否能正常运行

25

静态分析

  1. 查看程序

26

  1. 搜索字符串,发现一个 “Well done Cracker, You did it!”,直接定位到相应的代码位置

27

因为 IDA 无法调用出流程图,我们直接动态调试,看看关键函数。

动态调试

  1. 设关键节点设置断点

28

  1. 基本可以断定这是strcmp函数

29

  1. 在报错:请求输入 name 或 serial 的后面,设置断点

30

  1. 分析suanfa

31

  1. 验证结果

32

编写注册机

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
/*程序名:028-ArturDents-CrackMe#3.c*/
/*
新 160 个 CrackMe 算法分析 028-ArturDents-CrackMe#3,注册机。
*/

#define _CRT_SECURE_NO_DEPRECATE

#include <stdio.h>
#include <stdlib.h>



int main(void)
{
char name[20] = { 0 };

printf("请输入用户名:");
scanf("%s", name);

printf("Serial:ADCM3-");

for (int i = 0; i < strlen(name); i++)
{
printf("%d", name[i] / 3) ;
}

printf("\n");

return 0; //程序结束
}

029-figugegl.1

  1. 查看程序基本信息,程序使用的 LCC-Win32 编译器编译的。

33

静态分析

  1. 搜索字符串,找到程序关键跳转

34

35

  1. 分析出算法

36

37

  1. 验证结果
1
2
3
4
5
6
7
8
9
9unkk = 0x39,0x75,0x6E,0x6B,0x6B

serial = 9tlhg

39-0=39
75-1=74
6E-2=6C
6B-3=68
6B-4=67

38

编写注册机

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
/*程序名:029-figugegl.1.c*/
/*
新 160 个 CrackMe 算法分析 029-figugegl.1,注册机。
*/

#define _CRT_SECURE_NO_DEPRECATE

#include <stdio.h>
#include <stdlib.h>



int main(void)
{
char name[20] = { 0 };

printf("请输入用户名(name长度 >= 4):");
scanf("%s", name);

printf("Serial:");

for (int i = 0; i < strlen(name); i++)
{
printf("%c", name[i] - i) ;
}

printf("\n");

return 0; //程序结束
}

030-Acid Bytes.4

  1. 查看程序基本信息:程序有UPX壳;用户名至少 6 位;提示报错信息。

39

脱壳

  1. ESP 定律脱壳

40

  1. F9 运行程序到硬件断点处

41

  1. F8运行程序,到入口地址处

42

  1. 脱壳,并验证程序是否能正常运行。

43

静态分析

  1. IDA 无法展开流程图,我们直接在关键跳转和strcmp函数处设置断点

44

  1. 使用 OD 断点,运行程序,在配合 IDA 向上找可疑函数

45

  1. 执行程序后发现程序并未断点在可以函数位置,重新分析发现函数前面都有一个跳转。

46

  1. 重新分析,在第一个函数的跳转处设置断点,并查看上面几行指令,并对照IDA分析 eax 初始值。

47

动态调试

  1. 经过一遍循环下来基本就能理清算法逻辑了:第一个部分循环计算 name[i]*2(i<6) 的累计值

48

49

  1. 第二部分:strlen(name)*2;最终将这两个值相加,转换成十进制字符串。

50

  1. 验证算法是否正确

51

编写注册机

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
/*程序名:030-Acid Bytes.4.c*/
/*
新 160 个 CrackMe 算法分析 030-Acid Bytes.4,注册机。
*/

#define _CRT_SECURE_NO_DEPRECATE

#include <stdio.h>
#include <stdlib.h>



int main(void)
{
char name[20] = { 0 };
int result = 0;

printf("请输入用户名(name长度 >= 6):");
scanf("%s", name);



for (int i = 0; i < strlen(name); i++)
{
result += name[i] * 2;
}

result += strlen(name) * 2;

printf("Serial:%d\n",result);

return 0; //程序结束
}

031-Cruehead.1

  1. 查看程序基本信息

1

静态分析

  1. 搜索字符串,定位到函数位置

2

3

  1. 继续使用 ctrl+x 定位函数 sub_40134D 的位置,此时我们已经进入主程序位置

4

  1. 找到关键跳转,发现这里并没有使用到循环,而且有可疑函数 sub_4013D8 和 syb_40137E

5

  1. 分析 sub_40137E 函数

7

如果 esi 指向的字符大于字母 Z,该字符减 20h

8

将 esi 指向的字符串累加,最终异或 5678h

9

  1. 分析 sub_4013D8

6

  1. 定位内存地址

10

动态调试

  1. 在内存地址处设置断点,并调试

11

我们直接看到 sub_40137E 函数的参数是name;sub_4013D8 的参数是 serial

  1. 之前已经通过静态分析出算法,我们直接设置一个符合程序的 name,并计算 serial

name:JUNKK

serial:JUNKK xor 5678h xor 1234h = 17871

12

编写注册机

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
/*程序名:031-Cruehead.1.c*/
/*
新 160 个 CrackMe 算法分析 031-Cruehead.1,注册机。
*/

#define _CRT_SECURE_NO_DEPRECATE


#include <stdio.h>
#include <stdlib.h>

int main(void)
{
char name[20] = { 0 };
int sum = 0;

printf("请属于用户名(只允许A~Z之间的字母):");

scanf("%s", name);

for (int i = 0; i < strlen(name); i++)
{
sum += name[i];
}

printf("%d", sum ^ 0x5678 ^ 0x1234);
return 0; //程序结束
}

032-Bengaly-Crackme2

  1. 查看程序基本信息,发现有 upx 压缩壳

13

  1. esp 定律脱壳

14

  1. IDA 静态分析

15

16

图片中的算法标记错误,第一个是 dl * dl

  1. 动态调试确认两个 算法输入的值

17

18

  1. 编写注册机
    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
    /*程序名:032-Bengaly-Crackme2.c*/
    /*
    新 160 个 CrackMe 算法分析 032-Bengaly-Crackme2,注册机。
    */

    #define _CRT_SECURE_NO_DEPRECATE

    #include <stdio.h>
    #include <stdlib.h>

    int main(void)
    {
    char name[20] = { 0 };
    int sum = 0;

    printf("请输入用户名:");

    scanf("%s", name);

    for (int i = 0; i < strlen(name); i++)
    {
    sum += (name[i] * name[i])+ (name[i] >> 1)- name[i];
    }

    printf("%d", sum);
    return 0; //程序结束
    }

19

033-dccrackme1

  1. 查看程序基本信息

20

  1. 使用 IDA 和 OD 搜索字符串,都没找到。

21
22

  1. delphi 程序事件通常是在双击按钮 _TForm1_Button1Click 触发,IDA 找到该函数并查看,发现有个 strcmp 函数

23

  1. OD调试,尝试在 strmp 函数处设置断点,确认是不是我们要找的关键函数

24

25

  1. 静态分析算法

26

  1. 编写注册机验证
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
/*程序名:033-dccrackme1.c*/
/*
新 160 个 CrackMe 算法分析 033-dccrackme1,注册机。
*/

#define _CRT_SECURE_NO_DEPRECATE

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
char name[20] = { 0 };
int sum = 0;

printf("请输入用户名:");

scanf("%s", name);

for (int i = 0; i < strlen(name); i++)
{
sum += (name[i] - 0x17) * (name[i] - 0x11);
}

printf("%d\n", sum);

return 0; //程序结束
}

27

034-fireworx.5

  1. 查看程序基本信息,依然是没有弹出任何报错信息

28

静态分析

  1. 使用 IDA 和 OD 都没有搜索到任何可疑的字符串

29

30

  1. 在 IDA 中找到了两个 Tform1 窗体

31

  1. 在第一个窗体中发现了 strcmp 函数,这个很可疑,记录一下内存地址 0x00441A24

32

  1. 第二个是点击按钮的窗体,这个应该就是显示 about 按钮输出的内容。

33

动态调试

  1. 打开OD,并在该内存地址处设置断点,运行程序后可以看到输入的字符串在与 “Regcode” 做比较

34

  1. 修改输入字符串,看看字符串是否会根据我们输入不同的值而变动。调试后发现需要比较的字符串无任何变动,说明这是一个固定字符串比较。而这个字符串应该是通过上面的 call 来实现的。

35

  1. 验证 serial

36

035-Dope2112.2

  1. 查看程序基本信息

37

  1. 使用 IDA 搜索字符串和函数,因IDA反编译delphi存在缺陷,未搜索到相关字符串和函数

38

39

  1. 使用 PE Explorer 工具,反编译程序

40

41

动态调试

  1. 动态调试分析程序

42

  1. 运行程序验证分析结果

43

编写注册机

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
/*程序名:035-Dope2112.2.c*/
/*
新 160 个 CrackMe 算法分析 035-Dope2112.2.c,注册机。
*/

#define _CRT_SECURE_NO_DEPRECATE

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
char name[20] = { 0 };
int result = 0x37;

printf("请输入用户名:");

scanf("%s", name);

for (int i = 0; i < strlen(name); i++)
{
result += name[i] << 9;
}

printf("%d\n", result);

return 0; //程序结束
}

036-Andrnalin.2

  1. 查看程序基本信息

1
2

  1. 使用 IDA 搜索字符串(勾选搜索unicode字符串)
    4
    5

  2. OD 运行程序,定位到关键函数和跳转位置处,然后向下查看堆栈数据,发现可疑的字符串
    8

  3. 验证一下,看看是不是我们需要的key
    7

算法分析

  1. 但不跟踪调试,找到一段循环在处理 name 字符串

9

  1. 在执行第二次循环执行到vbaVarAdd函数时发现,ECX 和 栈地址 0x0012F45C 的值都是 0xAE。第一次执行的时候这两个值是0x39 加上 0x57 等于 0xAE。这里的循环就是将 name[i] 转换成整数相加,最终计算结果是 0x187

10

11

  1. 循环结束后继续跟踪调试,发现有个 vbaVarMul 函数,执行后未找到乘积的结果。

13

  1. 重新调试并通过 CE 找到了有变动可疑的内存数据

14
15

  1. 执行 vbaMidStmtVar 函数发现出现了未处理完整的 key 值,这里将第4个字符替换为 ‘-‘

16

  1. 这里将第9个字符替换为 ‘-‘

17

编写注册机

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
/*程序名:036-Andrnalin.2.c*/
/*
新 160 个 CrackMe 算法分析 035-Dope2112.2.c,注册机。
*/

#define _CRT_SECURE_NO_DEPRECATE

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
char name[20] = { 0 };
long long num = 0;
int result[20] = {0};

printf("请输入用户名:");

scanf("%s", name);

for (int i = 0; i < strlen(name); i++)
{
num += name[i];
}

num *= 0x499602D2;


for (int k=0; num > 0; num /= 10,k++)
{


if (k == 3 || k == 8)
{
result[k] = 0x2D;
}
else
{
result[k] = num % 10;
}
}

printf("key:");

for (int m = (sizeof(result)/8)+1; m >= 0; m--)
{
if (m == 3 || m == 8)
{
printf("%c", result[m]);
}
else
{
printf("%d", result[m]);
}
}

printf("\n");

return 0; //程序结束
}

037-fireworx.2

  1. 查看程序基本信息

18

  1. 搜索click,找到关键函数

19

  1. 找到关键函数 strcmp,定位到内存地址 0x0044175A

20

21

  1. 发现一个可疑的字符串,手动再验证一遍

22

23

  1. 发现 serial 是拼接字符串 :”name”+”name”+”625g72”

24

  1. 再次验证上面的分析是正确的

25

038-Eternal Bliss.3

  1. 查看程序基本信息

26

  1. 搜索字符串,找到程序关键指令,并定位到内存地址处

27

28

  1. 动态调试未被断点拦截,我们将上面跳转到错误信息的地址都记录设置断点:00403115、00402DD2

29

30

  1. 动态调试后仍然看不懂是这些比较的二进制含义,无法找到程序算法。

31

32

  1. 找到所有跳转到报错的关键函数处,结合IDA分析处的函数,进行多次调试后分析出算法
    34
    35
    36

算法分析

  1. 将输入的字符串值相加,并与 0x2DC 向比较,如果不相等则报错
    37
    38

  2. 读取字符串中的2、4、7位字符,并且都要等于 0x65 = “e”
    39
    40

  3. 用固定数值做的障眼法,无任何意义
    41

编写注册机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*程序名:038-Eternal Bliss.3*/
/*
新 160 个 CrackMe 算法分析 038-Eternal Bliss.3.c,注册机。
这个注册机要匹配各种字符,写起来比较花时间,这里就手写一个key
*/

#define _CRT_SECURE_NO_DEPRECATE

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
char* str = "AedecdeA";
printf("Key = %s\n", str);
return 0; //程序结束
}

补充:类似于下面这段汇编,是用来比较 esi 和 [ebp-8] 的值是否相等

1
2
3
4
5
sub esi,[ebp-8]
neg esi
sbb esi,esi
inc esi
neg esi

039-eKH.1

  1. 查看程序基本信息

42

  1. IDA 找到双击事件函数

43

  1. 动态调试

44

最终找到另一个可疑函数

45

跟进去发现可疑的比较

46

验证可疑字符串,发现这个是正确的 serial

47

重新调试程序,分析关键算法

48

49

算法分析

1
2
sum = (((sum+name[i]) << 8) | string + i)
if(sum<0){sum *= -1}

50

1
2
serial[i] = key[sum % 0xA]
sum /= 0xA

51

注册机

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
#include <stdio.h>
#include <stdlib.h>

int main(void) {

char name[]={0};
int sum=0;
char* string = "LANNYDIBANDINGINANAKEKHYANGNGENTOT";
char* key="LANNY5646521";
int len;


printf("请输入用户名:");
scanf("%s",name);

len = strlen(name);

//循环计算name的累加值
for(int i=0;i<len;i++)
{
sum += (int)name[i];
if(sum > 0 )
{
sum <<= 8;
sum |= *(string+i);
if(sum<0)
{
sum *= -1;
}
}
}

sum ^= 0x12345678;

printf("你的密钥是: ");
//开始计算 serial
for(int pkey=0;sum>0;sum /= 0xA)
{
pkey=sum%0xA;
printf("%c",*(key+pkey));
}

return 0;
}

40-DaNiEl-RJ.1

  1. 查看程序基本信息

52

  1. 静态分析找到关键跳转 0x0042D56F

53

  1. 动态调试

54

55

尝试输入 “>zsp” 发现破解成功

56

0x3E == “>”,0x7A == “z”

57

注册机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <stdlib.h>

int main(void) {

char name[]={0};


printf("请输入用户名:");
scanf("%s",name);

int len = strlen(name);

printf("你的密钥是: ");
//循环计算serial
for(int i=0;i<len;i++)
{
printf("%c",name[i]+5);
}

return 0;
}
  • 本文标题:CrackMe
  • 本文作者:9unk
  • 创建时间:2023-03-07 14:00:07
  • 本文链接:https://9unkk.github.io/2023/03/07/crackme/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!