前言
这是160个CreakMe系列的第002个CM,难度:☆;
国庆给自己放了个小假期,现在开始恢复学习!
本次分析使用到的环境和工具如下:
-
操作系统:Windows 10 2004(物理机)
-
工具:x32dbg
-
IDE:Visual Studio 2019
程序分析
这个CM程序界面很直白,输入用户名和序列号进行验证,以及程序是用VB语言编写的
那么这次的分析目标是:实现验证的破解,以及分析出来序列号的算法
分析目标1:暴力破解
随便输入个用户名和序列号,点击OK:
根据错误提示,搜索字符串:
根据字符串所在代码的位置,可以很明显看出来,这里的je是用来判断完序列号进行跳转的,如果把je改成jne,当我们再次点击OK时,则:
序列号验证成功,暴力破解成功(其实设置je为nop可能更好一些,万一瞎填的序列号填对了呢哈哈哈)
分析目标2:序列号分析(新)
看了别人的分析之后,发现我傻了,我分析了半天的东西就是个16-10的进制转换。。。原文我保留了,以后再来回顾当下愚蠢的我吧哈哈哈
这里重新分析一下
经过测试,由于序列号的生成与字符串长度有关,所以查一下导入的函数有没有字符串长度有关的,嗯
真是非常的巧,刚好有一个,下断点,看看这一块代码:
这里算完长度之后,拿长度*17CFB+首字母ascii码值,这就是真正的序列号了,只不过要进制转换一下,之前我还傻傻的步进进去分析这个进制转换是怎么转换的了。。
例子还用原理的例子:用户名abc
3*17CBF + 61 = 47752
47752h转换成10进制就是:292690,就是真正的序列号了
编写注册机程序(新)
#include<stdio.h>
#include<string.h>
#include<Windows.h>
int main() {
//获取用户名
char username[10];
printf("请输入用户名:");
scanf_s("%s",username,10);
//计算序列号
char szbuf[MAXCHAR] = { 0 };
int series_num = strlen(username) * 0x17CFB + username[0];
sprintf_s(szbuf, "AKA-%d", series_num);
printf("序列号是:%s\r\n", szbuf);
system("pause");
return 0;
}
分割线,下面是我之前愚蠢的解法,有兴趣可以看个乐,算是分析进制转换流程了哈哈哈
分析目标2:序列号分析(旧)
在判断验证成功与否的je跳转那里下断点,往上找找看有没有验证函数调用啥的
上面有一个被判断为strcmp的函数很可疑,下断点看看:
经过测试,这就是是正确的序列号,接着往上面找,这个序列号是哪里算出来的,在上面所有可疑函数都下个断点看看:
这里是字符串AKA-和序列号的数字部分,在这之前ecx里就已经存有了正确的序列号,说明序列号的计算还在前面,接着去看看ecx的值是哪来的:ecx的值来自[ebp-1c],往上翻翻接着看看这个值是哪里来的:
这里把那个栈的地址取走了,通过这个函数之后,栈中地址里存储着序列号的地址,想到上一关的折腾,我打算这次换个方式找下去
经过反复测试,序列号的值与用户名首字母和长度有关,相同首字母和长度的用户名序列号是一样的
那么这可能调用了一个函数来判断字符串长度,去搜一搜导入函数里有没有一个跟len相关的函数:
真是非常的巧,刚好有一个,下断点,看看这一块代码:
再跟进这个函数层层深入探寻真相:
继续深入:
然后就找到了生成序列号的算法了:
之前有一段是,用字符串长度乘以17CFB,这三数相乘,然后加上首字符的ascii码,这个数字就是序列号生成的关键,以下简称为数字a
序列号的生成是从后往前生成的,用数字a/Ah,得到的数字放在ecx,余数放在edx,最后一个字符为edx+30对应的ascii码,然后不断循环,直到数字被除到0
举个例子吧,比如用户名是abc
那么计算流程如下:
3*17CBF + 61 = 47752
47752/A = 7255 余数是0
7255 /A = B6E 余数是9
B6E /A = 124 余数是6
124 /A = 1D 余数是2
1D /A = 2 余数是9
2 /A = 0 余数是2
把余数加上30h(30h 是 0),然后倒序排列是:292690
我们来验证一下:
编写注册机程序(旧)
效果演示
源代码
用C语言写个注册机:
#include<stdio.h>
#include<string.h>
#include<Windows.h>
int main() {
//获取用户名
char username[10];
printf("请输入用户名:");
scanf_s("%s",username,10);
//获取用户名长度
int u_length = strlen(username);
//获取首字母ascii
int first = username[0];
//计算乘积
int num = u_length * 0x17CFB + first;
//计算序列号
char series[10] = {0};
int i = 0;
int remainder = 0;
do {
remainder = num % 0xA;
num = num / 0xA;
series[i++] = remainder + 0x30;
} while (num!=0);
//打印完整的序列号
int s_length = strlen(series);
printf("\r\n序列号为:AKA-");
for (i = s_length - 1; i >= 0; i--) {
printf("%c", series[i]);
}
printf("\r\n");
system("pause");
return 0;
}
总结
这次的教训是,下次分析的时候,先用PE工具查看一下导入函数,这样也许可以从函数下手,更精准的定位到目标位置,从而减少了一个一个看CALL的过程,提高了准确率和效率