在看雪潜水很长时间,今天吐个泡,发一篇自己写的CrackMe破解,难度不高,高手就不用看了,感谢看雪论坛和所有朋友对我的帮助。
【文章标题】: haunte's Crackme #1 破解+注册算法
【文章作者】: megadeath
【作者邮箱】: massacre@163.com
【作者QQ号】: 84227716
【软件名称】: haunte's Crackme #1
【软件大小】: 14,336
【下载地址】: http://www.crackmes.de/users/haunte/crachme_1_by_haunted/
【加壳方式】: 无
【保护方式】: 无
【编写语言】: ASM
【使用工具】: OllyICE
【操作平台】: WinXPsp2
【软件介绍】: 用户名+密码 验证
【作者声明】: 只是感兴趣,没有其他目的。失误之处敬请诸位大侠赐教!
--------------------------------------------------------------------------------
【详细过程】
MD5:39e1b194a26009aa91f6dd42e326453f Crackme.exe
首先拿到这个CrackMe,初次执行竟然没有反映,将在XP中设置为兼容win95模式后可以启动,程序很小,只有14k,但结构比我想象中的复杂,有不少干扰代码,花了一些时间读懂,破解过程如下。
程序是老套,输入用户名和密码,点击确认,但是错误注册码不会弹出对话框,用OD加载,在串式参考中找不到注册失败或成功的注册码,但是刚才在输入错误注册信息时弹出的对话框的标题是“crackme by haunted”,所以直接从这里找,在程序段往上一点点可以找到注册成功或失败的字样(幸运),可以断定注册判注册在这附近,再往上翻看,可以查到从004010D0 处开始进行注册码判断,我们从这里开始逐步分析,重新下断点在004010D0 处,然后往下走 004010CB > \B9 1E000000 mov ecx, 1E
004010D0 . 31C0 xor eax, eax ; 这里是开始点
004010D2 . BF B8204000 mov edi, 004020B8 ; 这里是最后放入真正注册码的
004010D7 . F3:AA rep stos byte ptr es:[edi]
004010D9 . 31FF xor edi, edi
004010DB . 47 inc edi
004010DC . C1E7 0B shl edi, 0B
004010DF . 6A 2D push 2D ; 用户名最长不能超过0x2d
004010E1 . 68 4F204000 push 0040204F ; 这个指针用于存放用户名
004010E6 . 57 push edi
004010E7 . 56 push esi
004010E8 . FF15 90214000 call dword ptr [402190] ; USER32.GetDlgItemTextA
004010EE . 09C0 or eax, eax ; 判断用户名长度
004010F0 . 0F85 01000000 jnz 004010F7 ; 如果用户名长度不为0则跳转
004010F6 . C3 retn ; 否则就结束
004010F7 > D1EF shr edi, 1
004010F9 . 6A 1E push 1E ; 输入的注册码最长不能超过0x1E
004010FB . 68 7C204000 push 0040207C ; 这个指针用于存放注册码
00401100 . 57 push edi
00401101 . 56 push esi
00401102 . FF15 90214000 call dword ptr [402190] ; USER32.GetDlgItemTextA
00401108 . 09C0 or eax, eax
0040110A . 0F85 01000000 jnz 00401111 ; 判断注册码长度
00401110 . C3 retn ; 长度如果为0就Over
00401111 > B8 4F204000 mov eax, 0040204F ; 这里放的是输入的用户名
00401116 . E8 1D020000 call 00401338 ; 进到里面看,这是计算字符串长度的
0040111B . A3 A0214000 mov dword ptr [4021A0], eax ; 字符串长度放到[4021A0],从后面来看,这步操作似乎没有太多作用,只是暂存了字符串长度
00401120 . BB 4F204000 mov ebx, 0040204F ; 输入的用户名字符串
00401125 . 0FB613 movzx edx, byte ptr [ebx] ; 取用户名的第一个字符放到edx中
00401128 . 0FB64C03 FF movzx ecx, byte ptr [ebx+eax-1] ; 取用户名的最后一个字符到ecx中
0040112D . 0FAFD1 imul edx, ecx ; edx = edx * ecx,即第一个字和最后一个字的乘积
00401130 . 50 push eax
00401131 . B9 02000000 mov ecx, 2
00401136 . 52 push edx
00401137 . 31D2 xor edx, edx ; edx清零
00401139 . F7F9 idiv ecx ; eax = 用户名长度 / ecx
0040113B . 5A pop edx ; edx是刚才的乘积
0040113C . 0FB60C03 movzx ecx, byte ptr [ebx+eax] ; 取用户名[用户名长度/2]的字符
00401140 . 0FAFD1 imul edx, ecx ; 刚才的乘积再乘以上面的字符
00401143 . BB B8204000 mov ebx, 004020B8 ; 这个地址是放真正注册码的,现在是空的
00401148 . C703 484E542D mov dword ptr [ebx], 2D544E48 ; 将 2D544E48 放到 数据区 004020B8 处,即字符串“HNT-”
0040114E . 89D0 mov eax, edx ; 将刚才的乘积放到eax
00401150 . 83C3 04 add ebx, 4 ; 这里ebx是放注册码的新位置,就是在 HNT- 这几个字符的后面
00401153 . E8 F7010000 call 0040134F ; 对eax进行一系列运算,算出四个值,添加到注册码尾部
00401158 . C705 00204000>mov dword ptr [402000], 1E ; 这步的确不知道是做什么用的,可能是迷惑代码
00401162 . 68 00204000 push 00402000 ; /pBufferSize = Crackme.00402000
00401167 . 68 9A204000 push 0040209A ; |Buffer = Crackme.0040209A
0040116C . E8 B34E0000 call <jmp.&KERNEL32.GetComputerNameA> ; \GetComputerNameA
00401171 . B8 9A204000 mov eax, 0040209A ; 这里eax中放的是计算机的名称
00401176 . E8 F2010000 call 0040136D ; 对计算机名称每个字符进行累加,结果返回eax
0040117B . BB 4F204000 mov ebx, 0040204F ; 这里是用户名,地址传给ebx
00401180 . E8 00020000 call 00401385 ; 对字符串一系列运算
00401185 . 50 push eax ; 对刚才的计算结果压栈,后面还有用到这个值
00401186 . 31C0 xor eax, eax ; eax清零
00401188 . 0FA2 cpuid ; 取得CPUID
0040118A . 58 pop eax ; eax是刚才计算后在 00401185 处压栈的结果
0040118B . 51 push ecx
0040118C . 52 push edx
0040118D . 21D8 and eax, ebx ; eax 的值更新为 eax 和CPUID后ebx的与结果
0040118F . BB B8204000 mov ebx, 004020B8 ; 这是放注册码的地方
00401194 . 89DF mov edi, ebx ; 取得注册码首地址位置放到edi中
00401196 . 50 push eax ; 将刚才eax的计算结果保存起来
00401197 . 66:31C0 xor ax, ax ; ax清零
0040119A . F2:AE repne scas byte ptr es:[edi] ; 统计现在注册码的长度,edi步进到字符串的末尾
0040119C . 89FB mov ebx, edi ; 字符串末尾位置赋值给ebx
0040119E . 58 pop eax ; 回复eax的值
0040119F . 4B dec ebx ; 注意这里的ebx自减1
004011A0 . C603 2D mov byte ptr [ebx], 2D ; 在注册码中增加 - 这个分割符号
004011A3 . 43 inc ebx ; 步进新的位置填充注册码
004011A4 . E8 A6010000 call 0040134F ; eax的值是0040118D时的eax值,计算后增加注册码
004011A9 . B8 4F204000 mov eax, 0040204F ; 这里是输入的用户名
004011AE . E8 BA010000 call 0040136D ; 对输入的用户名的每个字符进行累加,结果返回eax
004011B3 . 59 pop ecx ; ecx中是刚才CPUID调用时取得的edx返回的值
004011B4 . 31C8 xor eax, ecx ; eax 异或 ecx
004011B6 . 59 pop ecx ; 这个返回的是刚才CPUID调用时ecx的值
004011B7 . 09C8 or eax, ecx ; eax 或 ecx
004011B9 . 50 push eax ; 保存eax
004011BA . BB 4F204000 mov ebx, 0040204F ; 这是放输入用户名的地方
004011BF . 0FB603 movzx eax, byte ptr [ebx] ; 注册码的第一个字符
004011C2 . 0FB64B 01 movzx ecx, byte ptr [ebx+1] ; 注册码的第二个字符
004011C6 . 0FAFC1 imul eax, ecx ; 两者相乘结果放入到eax
004011C9 . 59 pop ecx ; ecx 是刚才上面计算的结果
004011CA . 0FAFC1 imul eax, ecx
004011CD . BF B8204000 mov edi, 004020B8 ; 放注册码的地方
004011D2 . 50 push eax
004011D3 . 30C0 xor al, al
004011D5 . F2:AE repne scas byte ptr es:[edi] ; 扫描注册码字符串
004011D7 . 58 pop eax
004011D8 . 89FB mov ebx, edi
004011DA . 4B dec ebx
004011DB . C603 2D mov byte ptr [ebx], 2D ; 再次增加 - 分隔符
004011DE . 43 inc ebx
004011DF . E8 6B010000 call 0040134F ; 对eax进行运算得到 - 后的新注册码
004011E4 . B8 4F204000 mov eax, 0040204F ; 这里存放的是用户名
004011E9 . E8 7F010000 call 0040136D ; 累加用户名值
004011EE . B9 1A000000 mov ecx, 1A
004011F3 . 31D2 xor edx, edx ; edx清零
004011F5 . F7F9 idiv ecx ; 累加值除以1a
004011F7 . B8 41000000 mov eax, 41 ; eax 放入 41
004011FC . 01D0 add eax, edx ; 再和刚才的余数相加
004011FE . 89C7 mov edi, eax ; 将结果存入到edi,一会儿要用到
00401200 . BB 4F204000 mov ebx, 0040204F ; 这里存放的是用户名
00401205 . E8 7B010000 call 00401385 ; 对用户名进行一系列计算
0040120A . 31D2 xor edx, edx ; edx清零
0040120C . F7F9 idiv ecx ; 得到的结果除以1A
0040120E . 83C2 41 add edx, 41 ; 将余数加0x41
00401211 . BB B8204000 mov ebx, 004020B8 ; 这是存放注册码
00401216 . 50 push eax
00401217 . 31C0 xor eax, eax
00401219 . 57 push edi
0040121A . 89DF mov edi, ebx
0040121C . F2:AE repne scas byte ptr es:[edi] ; 到注册码的末尾处
0040121E . 58 pop eax
0040121F . 89FB mov ebx, edi
00401221 . 5F pop edi
00401222 . 4B dec ebx
00401223 . C603 2D mov byte ptr [ebx], 2D ; 注册码增加短线分隔符
00401226 . 8843 01 mov byte ptr [ebx+1], al ; 把al作为字符增加
00401229 . 8853 02 mov byte ptr [ebx+2], dl ; 把dl作为字符增加
0040122C . C643 03 00 mov byte ptr [ebx+3], 0 ; 最后添加结束符,这时注册码已经完整
00401230 . 58 pop eax
00401231 . 56 push esi
00401232 . B8 B8204000 mov eax, 004020B8 ; 真正注册码
00401237 . E8 FC000000 call 00401338 ; 统计真正注册码长度
0040123C . 89C2 mov edx, eax ; 将长度放到edx中
0040123E . B8 7C204000 mov eax, 0040207C ; 输入的伪注册码
00401243 . E8 F0000000 call 00401338 ; 统计输入注册码长度,将注册码长度放到eax
00401248 . 39D0 cmp eax, edx ; 比较一下
0040124A . 0F85 5F000000 jnz 004012AF ; 长度不相等就跳转到失败处
00401250 . BE B8204000 mov esi, 004020B8 ; 真正注册码
00401255 . BF 7C204000 mov edi, 0040207C ; 输入的伪注册码
0040125A . 89C1 mov ecx, eax
0040125C > 8A06 mov al, byte ptr [esi]
0040125E . 8A27 mov ah, byte ptr [edi]
00401260 . 46 inc esi
00401261 . 47 inc edi
00401262 . 30C4 xor ah, al ; 两个注册码逐个比较
00401264 . 0F85 45000000 jnz 004012AF ; 如果不相等则跳到失败处
0040126A . 49 dec ecx
0040126B .^ 75 EF jnz short 0040125C ; 两个注册码逐个字符比较,循环
0040126D . 5E pop esi ; 后面就是显示正确注册的提示信息了
0040126E . B9 C8000000 mov ecx, 0C8
00401273 . B8 DC000000 mov eax, 0DC
00401278 . BF 23204000 mov edi, 00402023 ; ASCII "Right Code , Well Done",CR,LF," -hari code eka-?
0040127D . F2:AE repne scas byte ptr es:[edi]
0040127F . 4F dec edi
00401280 . 8827 mov byte ptr [edi], ah
00401282 . B8 DD204000 mov eax, 004020DD
00401287 . 83C0 64 add eax, 64
0040128A . 6A 40 push 40
0040128C . 50 push eax
0040128D . 68 23204000 push 00402023 ; ASCII "Right Code , Well Done",CR,LF," -hari code eka-?
00401292 . 56 push esi
00401293 . FF15 08204000 call dword ptr [402008] ; USER32.MessageBoxA
00401299 . B9 C8000000 mov ecx, 0C8
0040129E . B8 00000000 mov eax, 0
004012A3 . BF 23204000 mov edi, 00402023 ; ASCII "Right Code , Well Done",CR,LF," -hari code eka-?
004012A8 . F2:AE repne scas byte ptr es:[edi]
004012AA . 4F dec edi
004012AB . C607 DC mov byte ptr [edi], 0DC
004012AE . C3 retn
004012AF > B9 C8000000 mov ecx, 0C8
004012B4 . B8 DC000000 mov eax, 0DC
004012B9 . BF 0C204000 mov edi, 0040200C ; ASCII "Wrong Code",CR,LF,"-weradiyi-躌ight Code , Well Done",CR,LF," -hari code eka-?
004012BE . F2:AE repne scas byte ptr es:[edi]
004012C0 . 4F dec edi
004012C1 . 8827 mov byte ptr [edi], ah
004012C3 . B8 DD204000 mov eax, 004020DD
004012C8 . 83C0 64 add eax, 64
004012CB . 6A 30 push 30
004012CD . 50 push eax
004012CE . 68 0C204000 push 0040200C ; ASCII "Wrong Code",CR,LF,"-weradiyi-躌ight Code , Well Done",CR,LF," -hari code eka-?
004012D3 . 56 push esi
004012D4 . FF15 08204000 call dword ptr [402008] ; USER32.MessageBoxA
004012DA . B9 C8000000 mov ecx, 0C8
004012DF . B8 00000000 mov eax, 0
004012E4 . BF 0C204000 mov edi, 0040200C ; ASCII "Wrong Code",CR,LF,"-weradiyi-躌ight Code , Well Done",CR,LF," -hari code eka-?
004012E9 . F2:AE repne scas byte ptr es:[edi]
004012EB . 4F dec edi
004012EC . C607 DC mov byte ptr [edi], 0DC
004012EF . 5E pop esi
004012F0 . C3 retn
004012F1 > 6A 30 push 30
004012F3 . 68 41214000 push 00402141 ; ASCII "Crackme By Haunted"
004012F8 . 68 06214000 push 00402106 ; ASCII "Crackme",CR,CR,"Rules :",CR,"Find a serial(Write a keygen if you want)"
004012FD . 56 push esi
004012FE . FF15 04204000 call dword ptr [402004] ; USER32.MessageBoxA
00401304 . C3 retn
------------------------------------------------------------------------------------------
几点说明:
1.在00401188 处调用的CPUID指令,在AMD的CPU上也有这个指令,但求得的结果和Intel的不同,这里按照Intel的来写
如果eax初始值为0x00,调用CPUID后会往ebx,ecx和edx中写入一些值,查Intel手册的话可以了解到是寄存器中放的是一些字符串,ebx写的是“Genu”,ecx是“ntel”,edx中是“ineI”,也就是说如果是Intel的CPU的话必然是ebx=756E6547h,ecx=6c65746eh,edx=49656e69h。 2.说明一下几个call的调用,这几个call基本上都是靠eax和ebx传递参数的,刚开始看的时候觉得很混乱,多看几次就能理解了,不是很难
call 00401338
00401338 /$ 51 push ecx
00401339 |. 57 push edi
0040133A |. B9 64000000 mov ecx, 64 ; 最多扫描64个字符
0040133F |. 89C7 mov edi, eax ; eax中放的是字符串,赋值给edi,为后面的扫描
00401341 |. 31C0 xor eax, eax ; eax 清零,al 也是0
00401343 |. F2:AE repne scas byte ptr es:[edi] ; 对注册码扫描,碰到al就结束
00401345 |. F7D1 not ecx ; 反转ecx,ecx是减少的次数
00401347 |. 83C1 64 add ecx, 64 ; ecx 再加上64,ok,这样ecx中存放的是字符串的长度
0040134A |. 89C8 mov eax, ecx ; 结果就是用户名长度,放到eax中
0040134C |. 5F pop edi
0040134D |. 59 pop ecx
0040134E \. C3 retn ; 返回
这个是个很简单的函数,目的是统计输入字符串的长度,我们只要记得 00401338 是统计字符串长度的函数就可以了
call 0040134F
0040134F /$ 51 push ecx
00401350 |. 56 push esi
00401351 |. 4B dec ebx
00401352 |. B9 04000000 mov ecx, 4 ; ecx赋值4,要计算4个新值
00401357 |. BE 0A000000 mov esi, 0A ; esi赋值0xA,就是10进制的10
0040135C |. 52 push edx
0040135D |> 31D2 xor edx, edx ; edx清零
0040135F |. F7FE idiv esi ; eax = eax / esi,等于除以10
00401361 |. 80C2 30 add dl, 30 ; 余数加30
00401364 |. 88140B mov byte ptr [ebx+ecx], dl ; 放到一个数据区中
00401367 |.^ E2 F4 loopd short 0040135D
00401369 |. 5A pop edx
0040136A |. 5E pop esi
0040136B |. 59 pop ecx
0040136C \. C3 retn
这段是要计算四个值,将刚才的eax传入后不停除以0xA,每次得到的余数转换成数字字符,然后在刚才的注册码尾部增加上去,但要注意的是增加数字字符是倒着着加进去的,也就是从后往前加的 call 0040136D
0040136D /$ 56 push esi
0040136E |. 51 push ecx
0040136F |. 89C6 mov esi, eax ; 将字符串传给esi
00401371 |. 31C9 xor ecx, ecx
00401373 |. 31C0 xor eax, eax
00401375 |> 8A06 /mov al, byte ptr [esi] ; 取得字符串的每个字符
00401377 |. 84C0 |test al, al ; 看看是否有值
00401379 |. 74 05 |je short 00401380
0040137B |. 46 |inc esi
0040137C |. 01C1 |add ecx, eax ; 然后进行累加
0040137E |.^ EB F5 \jmp short 00401375
00401380 |> 89C8 mov eax, ecx ; 将结果返回到eax返回
00401382 |. 59 pop ecx
00401383 |. 5E pop esi
00401384 \. C3 retn
直接就是对计算机名称进行累加计算,结果放到eax后返回 call 00401385
00401385 /$ 51 push ecx ;
00401386 |. 52 push edx
00401387 |. 89D8 mov eax, ebx ; 这里把eax清除掉了
00401389 |. E8 AAFFFFFF call 00401338 ; 计算字符串长度的函数
0040138E |. 89C1 mov ecx, eax ; 字符串长度放到ecx
00401390 |. 4B dec ebx
00401391 |. 31D2 xor edx, edx ; edx清零
00401393 |> 8A140B /mov dl, byte ptr [ebx+ecx] ; 取得字符串每个字符,倒序
00401396 |. 0FAFD1 |imul edx, ecx ; edx = edx * ecx
00401399 |. 01D0 |add eax, edx ; eax = edx + eax
0040139B |. 49 |dec ecx
0040139C |.^ 75 F5 \jnz short 00401393
0040139E |. 5A pop edx
0040139F |. 59 pop ecx
004013A0 \. C3 retn
这段代码就比较有意思,这里是把用户名传入,然后先计算长度用来做循环次数,循环是倒序的,也是倒序取得用户名字符,然后将用户名字符乘以循环循环值,和eax累积,eax中初始值为用户名长度,最终返回eax的值,但有意思的是每次取得用户名的一个字符时是要放到edx中的,edx初始值为0x00,但每次循环都是将取得的字符是放在edx的低8位上的,其余位不变,这个要注意
最后到00401232 处的时候可以直接看内存中004020B8 ,这时注册码已经被计算出来了,爆破就从这边下手就可以了。
到这里我们把重新把思路整理一下,在计算注册码的时候总共有4个关键的call,00401338、0040134F、0040136D和00401385,其中00401338是统计字符串长度的,0040134F是将eax的值进行运算求得4个0-9的数字字符并增加到注册码末尾的,0040136D是对字符串的每个字符进行累加求和,00401385是对字符串每个字符求一个特殊的累积值。知道这四个函数的功能,我们把注册机写出来:
#include <iostream>
#include <string>
using namespace std;
//将字符串转换为4个数字字符并增加到注册码末尾,注意是倒序放入的
void Func0040134F(string & strCode, unsigned iNumber)
{
strCode.resize(4 + strCode.length());
for (int ii = 0; ii < 4; ++ii)
{
strCode[strCode.length() - ii - 1] = (char)((iNumber % 0x0A) + 0x30);
iNumber /= 0xA;
}
return;
}
//对字符串的每个字符进行一系列特殊的计算,返回一个特殊的乘积结果
unsigned Func00401385(const string & strString)
{
unsigned iResult = 0;
unsigned iStringLength = strString.length();
unsigned iAccumulate = iStringLength;
for (int ii = iStringLength - 1; ii >= 0; --ii)
{
iResult = iResult & 0xFFFFFF00;
iResult = iResult + (((int)strString[ii]) & 0xFF);
iResult = iResult * (ii + 1);
iAccumulate = iResult + iAccumulate;
}
return iAccumulate;
}
//累加字符串操作
unsigned Func0040136D(const string & strString)
{
unsigned iSum = 0;
for (int ii = 0; ii < strString.length(); ++ ii)
{
iSum += (int)strString[ii];
}
return iSum;
}
int main(int,char **)
{
string strName;
string strSn;
strSn = "HNT-";//注册码的第一部分
unsigned iNameLength = 0;
unsigned iResult = 0;
unsigned iEax, iEbx, iEcx, iEdx;
iEax = iEbx = iEcx = iEdx = 0;
do
{
cout << "Please input your name:" << endl;
cin >> strName;
if((0 != (iNameLength = strName.length())) && (0x2D >= iNameLength))
{
goto KeyGen;
}
} while(1);
KeyGen:
//增加注册码的第二部分
iResult = strName[0] * strName[iNameLength - 1];
iResult *= strName[iNameLength/2];
Func0040134F(strSn, iResult);
//增加注册码的第三部分
iResult = Func00401385(strName);
__asm
{
pushad
xor eax, eax
cpuid
mov iEax, eax
mov iEbx, ebx
mov iEcx, ecx
mov iEdx, edx
popad
}
iResult = iResult & iEbx;
strSn.push_back('-');
Func0040134F(strSn, iResult);
//增加注册码的第四部分
iResult = Func0040136D(strName);
iResult ^= iEdx;
iResult |= iEcx;
iResult *= (strName[0] * strName[1]);
strSn.push_back('-');
Func0040134F(strSn, iResult);
//增加注册码的第五部分
strSn.push_back('-');
iResult = Func0040136D(strName);
iResult = (iResult % 0x1A) + 0x41;
strSn.push_back((char)iResult);
iResult = (Func00401385(strName) % 0x1A) + 0x41;
strSn.push_back((char)iResult);
cout << strSn <<endl;
cout << "all right!" << endl;
cin.get();
return 0;
}
--------------------------------------------------------------------------------
【经验总结】
其中在找注册码的过程中,有一些迷惑代码,比如GetComputerNameAPI的调用,还有在 00401158 处存放0x1E,这个到最终也没有用上,在对CPUID调用后压栈后再重新取值进行计算也要小心,需要记下压栈的顺序以便后面运算的,否则很容易被搞乱计算步骤。在函数调用的时候也不是一般的压栈传参数,而是通过eax和ebx来传值,这个在开始的时候被弄得很迷惑,但多看几次就能明白怎么回事了。
另外对于明码比较的CrackMe还是比较好破解的,用爆破是最简单的办法,而且注册码也比较容易得到,今后将多练习看看非明码比对的CrackMe来学习学习。
--------------------------------------------------------------------------------
【版权声明】: 转载请注明作者并保持文章的完整, 谢谢!
2007年11月04日 22:50:15
------------上传附件到本地了,方便大家下载和以后整理。
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)