我的个人博客:https://www.kn0sky.com
微信公众号:我可是会飞的啊
前言
初学逆向,目前以这网上盛传的“适合破解新手的160个CreakMe”为目标,来进行实践练习与学习
本次我所使用的环境和工具如下:
操作系统:Windows 10 2004(物理机)
工具:x32dbg
程序分析
第1个程序是一个验证的程序,提供了两种验证方式,分别是:通过序列号和用户名验证,和单独通过序列号验证
这次分析的目标是:实现验证的破解,以及分析出序列号的算法
分析目标01:暴力破解两种验证方式
1.1 用户名/序列号验证暴力破解
根据字符串搜索定位到弹窗那块的代码部分
修改判断逻辑
0042FAF8 | 8B55 F0 | mov edx,dword ptr ss:[ebp-10] | [ebp-10]:"Enter your serial here !"
0042FAFB | 8B45 F4 | mov eax,dword ptr ss:[ebp-C] | [ebp-C]:"CW-6560-CRACKED"
0042FAFE | E8 F93EFDFF | call acid burn.4039FC |
;和正确的序列号进行对比
0042FB03 | 75 1A | jne acid burn.42FB1F |
;这里改成|0x9090,也就是nop填充这个751A即可强行跳过判断
0042FB05 | 6A 00 | push 0 |
0042FB07 | B9 CCFB4200 | mov ecx,acid burn.42FBCC | 42FBCC:"Congratz !!"
0042FB0C | BA D8FB4200 | mov edx,acid burn.42FBD8 | 42FBD8:"Good job dude =)"
0042FB11 | A1 480A4300 | mov eax,dword ptr ds:[430A48] |
0042FB16 | 8B00 | mov eax,dword ptr ds:[eax] |
0042FB18 | E8 53A6FFFF | call acid burn.42A170 |
0042FB1D | EB 18 | jmp acid burn.42FB37 |
0042FB1F | 6A 00 | push 0 |
0042FB21 | B9 74FB4200 | mov ecx,acid burn.42FB74 | 42FB74:"Try Again!"
0042FB26 | BA 80FB4200 | mov edx,acid burn.42FB80 | 42FB80:"Sorry , The serial is incorect !"
0042FB2B | A1 480A4300 | mov eax,dword ptr ds:[430A48] |
0042FB30 | 8B00 | mov eax,dword ptr ds:[eax] |
0042FB32 | E8 39A6FFFF | call acid burn.42A170 |
0042FB37 | 33C0 | xor eax,eax |
1.2 序列号单独验证暴力破解
搜索字符串,定位到弹窗的代码部分
修改判断逻辑:跟上面那个判断方法和修改方法一样
0042F4CA | 8B45 F0 | mov eax,dword ptr ss:[ebp-10] | [ebp-10]:"666"
0042F4CD | 8B55 F4 | mov edx,dword ptr ss:[ebp-C] | [ebp-C]:"Hello Dude!"
0042F4D0 | E8 2745FDFF | call acid burn.4039FC |
0042F4D5 | 75 1A | jne acid burn.42F4F1 |
; 用nop填充这个75 1A即可无条件判断成功
0042F4D7 | 6A 00 | push 0 |
0042F4D9 | B9 64F54200 | mov ecx,acid burn.42F564 | ecx:"Failed!", 42F564:"Congratz!"
0042F4DE | BA 70F54200 | mov edx,acid burn.42F570 | 42F570:"God Job dude !! =)"
0042F4E3 | A1 480A4300 | mov eax,dword ptr ds:[430A48] |
0042F4E8 | 8B00 | mov eax,dword ptr ds:[eax] |
0042F4EA | E8 81ACFFFF | call acid burn.42A170 |
0042F4EF | EB 18 | jmp acid burn.42F509 |
0042F4F1 | 6A 00 | push 0 |
0042F4F3 | B9 84F54200 | mov ecx,acid burn.42F584 | ecx:"Failed!", 42F584:"Failed!"
0042F4F8 | BA 8CF54200 | mov edx,acid burn.42F58C | 42F58C:"Try Again!!"
0042F4FD | A1 480A4300 | mov eax,dword ptr ds:[430A48] |
0042F502 | 8B00 | mov eax,dword ptr ds:[eax] |
0042F504 | E8 67ACFFFF | call acid burn.42A170 |
0042F509 | 33C0 | xor eax,eax |
分析目标02:分析出两种序列号的算法
2.1 用户名/序列号的序列号分析
搜索字符串Sorry
,发现存在两处地方用到了这个字符串:一个一个判断
第一个字符串是在登录是否成功这里判断的,这一块完整代码在1.1部分有,这里就只放关键部分了:
0042FAF8 | 8B55 F0 | mov edx,dword ptr ss:[ebp-10] | [ebp-10]:"123456"
0042FAFB | 8B45 F4 | mov eax,dword ptr ss:[ebp-C] | [ebp-C]:"CW-8528-CRACKED"
0042FAFE | E8 F93EFDFF | call acid burn.4039FC |
0042FB03 | 75 1A | jne acid burn.42FB1F | 用户名/序列号验证
这里进行对比的那个字符串应该就是正确的值,用这个序列号去登录,即可登录成功
另一个字符串的位置在:
0042FA4D | A1 6C174300 | mov eax,dword ptr ds:[43176C] | 0043176C:&"hello"
0042FA52 | E8 D96EFDFF | call acid burn.406930 | 获取字符串长度
0042FA57 | 83F8 04 | cmp eax,4 | 如果大于4就跳转进行序列号验证,如果小于4则直接Sorry
0042FA5A | 7D 1D | jge acid burn.42FA79 |
0042FA5C | 6A 00 | push 0 |
0042FA5E | B9 74FB4200 | mov ecx,acid burn.42FB74 | 42FB74:"Try Again!"
0042FA63 | BA 80FB4200 | mov edx,acid burn.42FB80 | edx:&"d稝", 42FB80:"Sorry , The serial is incorect !"
0042FA68 | A1 480A4300 | mov eax,dword ptr ds:[430A48] |
0042FA6D | 8B00 | mov eax,dword ptr ds:[eax] |
0042FA6F | E8 FCA6FFFF | call acid burn.42A170 | MessageBox
0042FA74 | E9 BE000000 | jmp acid burn.42FB37 |
判断用户名长度的,长度大于4则进行验证,长度小于4则直接返回验证失败
将jge改成jmp即可绕过用户名长度限制
经过反复测试发现,序列号的生成与用户名的首字符有关,比如1:4018,2:4100,a:7954,等
以及发现计算的数字会出现在内存地址0019E71E的位置上
继续测试,找到该地址是在哪里被填充的
是在这里,4018被填充到0019E71E的,这里又是movsb指令,是从0019E6AC地方复制来的,下面来找找这个地址的值是哪里来的
经过又一遍的步进,发现上面不远处的这个函数执行完会在0019E6AC填充中间数字:
进入函数接着步进发现了一个循环在填充中间数字:
这里的EAX里的值就是4018,中间数值依然是之前就算好的,这里的代码是通过对10取余一个一个数字填到内存中去,要找到中间数字是怎么算的还得往前找,找EAX的值是哪来的:
值是ESI地址里传给EAX 的,接着往上找ESI的地址(0019F72C)的值是哪来的:
生气,往里面找了半天,结果这个值就在最外面一层就算好了,计算方法是:
最终结果(十六进制) = 首字符的ASCII码(十六进制) * 29h * 2h
这么算的话,2开头的用户名应该是 32h * 29h * 2h = 1004h = 4100,和之前测试结果吻合
注册机代码:
#include<stdio.h>
#include<windows.h>
#include<string.h>
int main() {
char Username[10];
printf_s("请输入用户名:");
scanf_s("%s",Username,(unsigned int)sizeof(Username));
int FirstASCII = Username[0];
int Midcode = FirstASCII * 82;
char* Front = (char*)"序列号为: CW";
char Middle[10] = {0};
sprintf_s(Middle,"%d",Midcode);
char* Behind = (char*)"CRACKED";
char Res[50] = {0};
strcpy_s(Res,Front);
strcat_s(Res, "-");
strcat_s(Res, Middle);
strcat_s(Res, "-");
strcat_s(Res, Behind);
printf("%s",Res);
Sleep(10000);
return 0;
}
效果展示:
2.2 序列号单独验证序列号分析
通过修改不同的序列号进行验证,看见反汇编窗口中都跟同样的值进行比较,所以判断这个序列号单独验证是一个固定的值,是硬编码进去的验证,值为:Hello Dude!
总结
第一次独自分析破解CreakMe,遇到了挺多坑,这次让我学到了要去找某一个数值是怎么冒出来的,不妨先看看函数调用之前的参数是什么,也许这会有帮助(对应2.1部分)