本帖最后由 pk8900 于 2017-12-9 09:49 编辑
昨天晚上,继续研究 了【适合破解新手的160个crackme练手】,本文内容为第34个CrackMe: Cruehead.3.exe,有些收获,分析并编写注册机后,在论坛里搜索了一下关于这个CrackMe的帖子进行参照,找到了两篇,有一篇未分析出算法,有一篇已基本将算法分析出来,但注册机编写的不是很完美,参考地址:https://www.52pojie.cn/thread-371674-1-1.html,所以写了这篇帖子,分享一下我的分析过程及注册机编写方法。
【crackme简介】
下载地址:http://pan.baidu.com/share/link?shareid=541269&uk=4146939145
MASM32 / TASM32编写,无壳,是一个Keyfile验证方式,界面打开窗口空白,file菜单只有一个exit,help菜单也只有一个About说明。
分析工具:X64dbg,OD,(因X64DBG不能自动注释API函数调用参数,所以和OD结合使用)
【crackme截图】
【算法分析过程】
因为在下载crackme的时候,看见后面标注的验证方式是Keyfile,所以分析起来就省了不少事,直接搜索字符串,可以搜索到“CRACKME3.KEY”,明显的Key文件,同时还有"Good work cracker!"及"Cracked by: Now try the next crackme!"字符串,估计是成功后标志。直接双击“CRACKME3.KEY”来到代码处:
[Asm] 纯文本查看 复制代码 0040100C |. C705 F9204000>mov dword ptr ds:[0x4020F9],0x0
00401016 |. 6A 00 push 0x0 ; /hTemplateFile = NULL
00401018 |. 68 80000000 push 0x80 ; |Attributes = NORMAL
0040101D |. 6A 03 push 0x3 ; |Mode = OPEN_EXISTING
0040101F |. 6A 00 push 0x0 ; |pSecurity = NULL
00401021 |. 6A 03 push 0x3 ; |ShareMode = FILE_SHARE_READ|FILE_SHARE_WRITE
00401023 |. 68 000000C0 push 0xC0000000 ; |Access = GENERIC_READ|GENERIC_WRITE
00401028 |. 68 D7204000 push Cruehead.004020D7 ; |FileName = "CRACKME3.KEY"
0040102D |. E8 76040000 call <jmp.&KERNEL32.CreateFileA> ; \CreateFileA
00401032 |. 83F8 FF cmp eax,-0x1
00401035 |. 75 0C jnz short Cruehead.00401043 ; 文件不存在跳走
00401037 |> 68 0E214000 push Cruehead.0040210E ; ASCII "CrackMe v3.0 "
0040103C |. E8 B4020000 call Cruehead.004012F5
00401041 |. EB 6B jmp short Cruehead.004010AE
00401043 |> A3 F5204000 mov dword ptr ds:[0x4020F5],eax ; kernel32.BaseThreadInitThunk
00401048 |. B8 12000000 mov eax,0x12
0040104D |. BB 08204000 mov ebx,Cruehead.00402008
00401052 |. 6A 00 push 0x0 ; /pOverlapped = NULL
00401054 |. 68 A0214000 push Cruehead.004021A0 ; |pBytesRead = Cruehead.004021A0
00401059 |. 50 push eax ; |BytesToRead = 76753358 (1987392344.)
0040105A |. 53 push ebx ; |Buffer = 7EFDE000
0040105B |. FF35 F5204000 push dword ptr ds:[0x4020F5] ; |hFile = NULL
00401061 |. E8 30040000 call <jmp.&KERNEL32.ReadFile> ; \ReadFile
00401066 |. 833D A0214000>cmp dword ptr ds:[0x4021A0],0x12 ; 读取字节数不为0x12(18)字节,跳走
0040106D |.^ 75 C8 jnz short Cruehead.00401037
0040106F |. 68 08204000 push Cruehead.00402008
00401074 |. E8 98020000 call Cruehead.00401311
00401079 |. 8135 F9204000>xor dword ptr ds:[0x4020F9],0x12345678
分析此块代码:
1、CreateFileA参数Mode = OPEN_EXISTING,也就是打开已存在的文件,若文件不存在,函数返回-1,是用来验证Key文件是否存在的。
2、ReadFile函数读取文件内容,参数pBytesRead = Cruehead.004021A0是读取文件返回的字节数,接下来一句代码cmp dword ptr ds:[0x4021A0],0x12,如果读取的字节数少于0x12(18)个,则跳至失败处。
于是我们在CrackMe目录下新建一个文件CRACKME3.KEY,并写入不少于18个字节的数据,内容随意。
再次载入程序,在ReadFile后代码进行跟踪。
[Asm] 纯文本查看 复制代码 00401066 |. 833D A0214000>cmp dword ptr ds:[0x4021A0],0x12 ; 读取字节数不为0x12(18)字节,跳走
0040106D |.^ 75 C8 jnz short Cruehead.00401037
0040106F |. 68 08204000 push Cruehead.00402008 ; 123123123123111111
00401074 |. E8 98020000 call Cruehead.00401311 ; 子程序1
00401079 |. 8135 F9204000>xor dword ptr ds:[0x4020F9],0x12345678 ; [0x4020F9] XOR 0x12345678
00401083 |. 83C4 04 add esp,0x4
00401086 |. 68 08204000 push Cruehead.00402008 ; 123123123123111111
0040108B |. E8 AC020000 call Cruehead.0040133C ; 子程序2
00401090 |. 83C4 04 add esp,0x4
00401093 |. 3B05 F9204000 cmp eax,dword ptr ds:[0x4020F9] ; 比较
00401099 |. 0F94C0 sete al 比较标志存入AL
0040109C |. 50 push eax ; kernel32.BaseThreadInitThunk
0040109D |. 84C0 test al,al 测试AL值
0040109F |.^ 74 96 je short Cruehead.00401037 ; 跳转
004010A1 |. 68 0E214000 push Cruehead.0040210E ; CrackMe v3.0 - Uncracked
004010A6 |. E8 9B020000 call Cruehead.00401346
上面这段代码可以看出,程序先后两次对文件中读取的字符串地址压栈,调用了两个子程序处理,然后是一个cmp比较和一个JE跳转。关键算法就是这里了,先分析子程序1(call Cruehead.00401311)
[Asm] 纯文本查看 复制代码 00401311 /$ 33C9 xor ecx,ecx
00401313 |. 33C0 xor eax,eax
00401315 |. 8B7424 04 mov esi,dword ptr ss:[esp+0x4] ; 读入key字符串指针
00401319 |. B3 41 mov bl,0x41 ; 用于异或解密的字符
0040131B |> 8A06 /mov al,byte ptr ds:[esi]
0040131D |. 32C3 |xor al,bl
0040131F |. 8806 |mov byte ptr ds:[esi],al
00401321 |. 46 |inc esi ; key字符串指针+1
00401322 |. FEC3 |inc bl ; 异或解密字符+1
00401324 |. 0105 F9204000 |add dword ptr ds:[0x4020F9],eax ; 累加解密后字符ASCII值至0x4020F9
0040132A |. 3C 00 |cmp al,0x0
0040132C |. 74 07 |je short Cruehead.00401335 ; 解密后为0(空字符)则结束
0040132E |. FEC1 |inc cl
00401330 |. 80FB 4F |cmp bl,0x4F ; 解密字符累加至4F结束
00401333 |.^ 75 E6 \jnz short Cruehead.0040131B
00401335 |> 890D 49214000 mov dword ptr ds:[0x402149],ecx
0040133B \. C3 retn
此段代码把从KEY文件中读取的字符串进行异或解密,解密字符从0x41开始,一直到0X4E共14位,中间如果解密出来的字符若是\0(空字符)则结束,(注意:这里在判断字符为\0时,字符与解密字符的异或值已经被累加,存入[0x4020F9]中,因此写注册机的时候要注意判断时机),取出的共18个字节,后4个是不进行解密的。
子程序调用结束返回后,将[0x4020F9]地址的累加值与0x12345678 异或。
接下来进入子程序2:
[Asm] 纯文本查看 复制代码 0040133C /$ 8B7424 04 mov esi,dword ptr ss:[esp+0x4] ; Cruehead.00402008
00401340 |. 83C6 0E add esi,0xE ; 从KEY字符串偏移14
00401343 |. 8B06 mov eax,dword ptr ds:[esi] ; 取key字符串后4位
00401345 \. C3 retn
子程序2的代码非常简单,就是从读取的Key字符串中取出后4个字节。子程序2返回后,将后4个字节值与子程序1累加并异或的值比较,来确定是否真的注册成功,注册成功后,程序将解密的14字节用户名写入字符串"Cracked by: Now try the next crackme!"中间的14个空格位置,调用信息框显示成功信息。
【注册机编写】
经分析得知,程序读取Key文件中前18个字节得到一个字符串(准确点说应该是字节集),解密前14个字节并累加其ASCII值,遇0结束,也就是正好是我们的用户名部分。所以我们可以自己指定用户名,通过异或加密0x41开始的字符后,写入KEY文件1-14字节,长则截去,短则添0,然后累加ASCII值,后与0x12345678 异或,将这个值写到KEY文件第15-18字节,至此注册机即可实现写KEY文件,完成注册。
注册机C++代码:
[C++] 纯文本查看 复制代码 #include<iostream>
#include <windows.h>
using namespace std;
int main()
{
char * path = "E:\\crackme\\CRACKME3.KEY";
FILE * keyfile;
char * youkey = new char[260]; //输入用户名
unsigned long retvalue=0; //后4字节检验值
memset(youkey, 0, 260);
cout << "Enter your name:";
gets(youkey);
for (int x = 0; x < 0xE; x++)
{
retvalue += (youkey[x]);
youkey[x] = youkey[x] ^ (0x41 + x);
if ((youkey[x] ^ (0x41 + x ))== 0) //这里要先累加,然后进行判断
break;
}
retvalue ^= 0x12345678;
errno_t err = fopen_s(&keyfile, path, "w+b");
if (err != 0)
{
cout << "file open or create failed!" << endl;
system("pause");
return -1;
}
rewind(keyfile);
fwrite(youkey, sizeof(byte), 0xE, keyfile);
fseek(keyfile, 0xE, 0);
fwrite((char *)&retvalue, sizeof(byte), 4, keyfile);
fclose(keyfile);
cout <<"write file:"<< youkey<< hex << retvalue << endl;
delete[] youkey;
system("pause");
return 1;
}
附个成功注册的截图图:
欢迎大家回贴交流,共同探讨。 |