新160个crackMe算法分析-42-crackme.exe

最近在做一个很有意思的CrackMe程序,这个程序的算法分析有一点难度

1.首先我们查壳,无壳,
PixPin_2026-04-22_19-04-25

2.然后从程序本身获取信息
PixPin_2026-04-22_19-05-19
程序需要一个key.dat文件
我们在当前目录下随便创建一个,然后打开程序
PixPin_2026-04-22_19-06-52
现在显示我们的key.det不是正确的keyfile

根据字符串搜索我们可以直接定位到关键的判断以及处理处
PixPin_2026-04-22_19-09-08

我们开始分析整个函数

检测key.dat文件是否存在
PixPin_2026-04-22_19-12-37

PixPin_2026-04-22_14-09-45

PixPin_2026-04-22_14-07-09

程序的算法分析
这个程序的算法有两个部分

.text:004011EA cmp     eax, 2A8BF4h    
.text:004011EF jz      short loc_4011F8

我们先分析第一部分
1.读取key.dat字符然后异或key.det文件字符的长度

PixPin_2026-04-22_14-11-21

2.把前三位字符与固定的数进行异或,分别是0x54 0x4D 0x47
PixPin_2026-04-22_19-19-42

3.从key.dat读取的数据存放在局部变量[ebp-158], IDA里面把我们注释了也就是[ebp + buffer], 根据此处的代码分析,我们知道程序把第4、5、6 三位字符依次与1、2、3位异或,通过分析在0x0040118E处第一次运行到这里esi=3,edi=3, 所以这个循环就是从第4位开始,每次用1、2、3位依次异或三位
0x004011AD处 bl=0,ecx是数据长度,由于前面是三位三位异或,但是数据长度并不一定是3的倍数,这一段汇编是把第ecx位赋值位0,让结果和数据长度一致。
PixPin_2026-04-22_19-21-22

此处还有一个地方要注意,下面的代码开始从0x00405030用1、2、3位每次依次异或三位,直到遇到FF
PixPin_2026-04-22_19-39-38

然后就到我们开始确定的判断位置
PixPin_2026-04-22_19-43-17

这里的思考我认为很巧妙,我们倒着可以推出,1,2,3位分别为何值时乘积为0x2A8BF4通过暴力枚举,我们可以得到以下结果。

for (int i = 0; i < 256; i ++ )
	for (int j = 0; j < 256; j ++ )
		for (int z = 0; z < 256; z++)
		{
			f (i * j * z == 0x2A8BF4)
			{
				printf("%d %d %d\n", i, j, z);
			}
		}

/*
85 139 236
85 236 139
118 139 170
118 170 139
139 85 236
139 118 170
139 170 118
139 236 85
170 118 139
170 139 118
236 85 139
236 139 85
*/

我们现在假设我们key,dat文件只有三位
我们根据之前的分析可以得到
初始第1位字符 ^ 数据字符的长度 ^ 0x54 ^ 0x00405030 = 我们通过枚举得到的结果
初始第2位字符 ^ 数据字符的长度 ^ 0x4D ^ 0x00405031 = 我们通过枚举得到的结果
初始第3位字符 ^ 数据字符的长度 ^ 0x47 ^ 0x00405032 = 我们通过枚举得到的结果

因为异或运算是可逆的我们完全可以倒着推出初始字符
假设我们现在的结果是85 139 236
初始第1位字符 = 85 ^ 数据字符的长度 ^ 0x54 ^ 0x00405030 = 1c
初始第2位字符 = 139 ^ 数据字符的长度 ^ 0x4D ^ 0x00405031 = 7a
初始第3位字符 = 236 ^ 数据字符的长度 ^ 0x47 ^ 0x00405032 = 0a

我们用HxD把key.dat数据改成1c 7a 0a;
PixPin_2026-04-22_20-06-39

在OD里面也可以看到程序以跳过了错误处理
我们按f9,会发现程序崩掉了,我们往下看
PixPin_2026-04-22_20-07-05

通过分析我们可以判断出,他在判断经过前面异或之后存储在局部变量buffer里面是否存在0x20
局部变量并没有0x20 所以程序到这里会一直循环然后崩掉
PixPin_2026-04-22_20-11-58
所以我们需要一个0x20
根据之前的分析 我们现在的字符长度位4 第四位在经过一系列异或的结果是0x20
len = 数据长度 = 4
0x20 = 第4位 ^ len ^ 第1位字符 ^ len ^ 0x54
这里的第一位的值由
第1位字符 = 85 ^ len ^ 0x54 ^ 0x00405030 计算出
这样我们就可以得到第4位字符
我们开始写代码验证一下

    char str[4];
	str[0] = 85;
	str[1] = 139;
	str[2] = 236;
	int len = 4;
	str[0] = str[0] ^ len ^ 0x54 ^ 0x1E;
	str[1] = str[1] ^ len ^ 0x4D ^ 0xBF;
	str[2] = str[2] ^ len ^ 0x47 ^ 0xA2;
	str[3] = 0x20 ^ len ^ str[0] ^ len ^ 0x54;
	printf("%02x %02x %02x %02x", str[0], str[1], str[2], str[3]);

得到的结果1b 7d 0d 6f, 我们更改key.dat文件
od运行程序会发现判断0x20之后程序可以正常运行,我们可以按f9运行试一下
会发现出现了弹窗
PixPin_2026-04-23_18-53-37
显示来自谁的注册,后续我们可以直到这后面是可以接上字符串的,此处位空白

我们现在可以继续初始分析,假如有5个数,由于程序会通过一系列异或得到我们真正需要的字符,所以此处我们也需要倒着推一下,假如第5位是1(0x31)
我们写代码验证一下

    char str[5];
	str[0] = 85;
	str[1] = 139;
	str[2] = 236;

	int len = 5;
	str[0] = str[0] ^ len ^ 0x54 ^ 0x1E;
	str[1] = str[1] ^ len ^ 0x4D ^ 0xBF;
	str[2] = str[2] ^ len ^ 0x47 ^ 0xA2;
	str[3] = 0x20 ^ len ^ str[0] ^ len ^ 0x54;
	str[4] = 0x31 ^ len ^ str[1] ^ len ^ 0x4D;
	
	printf("%02x %02x %02x %02x %02x", str[0], str[1], str[2], str[3], str[4]);

结果是1a 7c 0c 6e 00 我们再去修改key.dat文件
PixPin_2026-04-23_19-05-43
会发现我们出现了1

我们回到od,分析一下这些数据是从哪里来的
PixPin_2026-04-23_19-13-54
[ebp - 38]
PixPin_2026-04-23_19-14-15
[ebp - 58]
PixPin_2026-04-23_19-14-15

我们需要分析一下此处的call esi ,我们先不往里面分析,观察压栈的数据和f8运行过的返回值和堆栈的变化,如果看不出什么名堂,再去详细看看这个函数里面干了什么事
运行前堆栈
PixPin_2026-04-23_19-18-20
运行后堆栈
PixPin_2026-04-23_19-18-28

我们会发现出现了我们看到的弹出信息,再往下运行就会出现弹窗
到这里我们其实可以开始写注册机了
但是这个程序还有很多我们不清楚的过程,比如这个call esi 究竟干了什么,细心的人可能会发现,这个call的地址是0x00405030,是我们之前分析用1、2、3位每次依次异或三位的地方,在借鉴别人的思路和阅读别人的博客之后,我们可以知道这里面用到了shellcode,我们的前3位是密钥开始,开始解密从0x00405030的数据,在分析过程中,我们遗漏了一个
PixPin_2026-04-23_19-25-43
通过查阅我们可以知道这个函数
VirtualProtect 是 Windows API 中用于更改当前进程虚拟地址空间中已提交页面区域访问保护属性的函数,常用于内存管理、代码注入、反调试等场景。它只能作用于当前进程,若需修改其他进程内存保护,应使用 VirtualProtectEx。
关于代码注入就可以是shell code,具体怎么做,以我目前的知识,完全不知道该怎么说,还没学过,后续等知识储备足够分析这个过程,我应该会把这一部分补充一下

关于注册机的编写,可以尝试完成一下,根据上面的分析应该可以完成

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string>
char str[20];
char strName[20];
int main()
{
	printf("输入我们需要显示数据: ");//其实就是用户名
	scanf("%s", strName);

	str[0] = 85;
	str[1] = 139;
	str[2] = 236;
	str[3] = 0x20;
	strcat(str, strName);
	int len = strlen(str);
	str[0] = str[0] ^ len ^ 0x54 ^ 0x1E;
	str[1] = str[1] ^ len ^ 0x4D ^ 0xBF;
	str[2] = str[2] ^ len ^ 0x47 ^ 0xA2;

	int tmp = 3;
	for (int i = 0; i < (len - 3) / 3 + 1; i++)
	{
		str[tmp] = str[tmp] ^ len ^ str[0] ^ len ^ 0x54;
		str[tmp + 1] = str[tmp + 1] ^ len ^ str[1] ^ len ^ 0x4D;
		str[tmp + 2] = str[tmp + 2] ^ len ^ str[2] ^ len ^ 0x47;
		tmp += 3;
	}
	for (int i = 0; i < len; i++)
	{
		printf("%02x", str[i]);
	}
	//printf("%02x %02x %02x %02x %02x", str[0], str[1], str[2], str[3], str[4]);
	return 0;
}

其实这是我在写博客的时候又去写了一遍,比之前写的精简一点。再重写的过程里我也发现了自己的一些不足,把冗余的地方修改了一下。
我们测试一下
PixPin_2026-04-23_19-59-03

输入key.dat
PixPin_2026-04-23_19-59-57
运行
PixPin_2026-04-23_20-00-25
大功告成

总结:这个CrackMe是周一晚上开始做的,由于白天要上课,磨磨蹭蹭今天才把博客写出了,这个程序我真的花了好久的时间啊,完成之后,成就感还是满满的,这个程序的思路我是从https://www.kn0sky.com/?p=241 这个人那边学习的,这个up的思路真的非常棒,很精简,虽然我写得博客很冗余,但是我感觉不差,必定我把我想写的东西写出来了,没有放弃这点可是很重要的啊,可能看起来会乱乱的,我相信我以后会越写越好的,我当时想开始做这个160crackme程序也是因为看了这个up的博客,也想借此锻炼一下自己的调试水平,我感觉在工具的使用很分析上,我是有进步的,换以前,我完全不会做一些很难的题目,看到汇编代码就想跑。

posted @ 2026-04-23 20:01  书与兔子  阅读(24)  评论(0)    收藏  举报