│破解作者│ 大菜一号(西哈`就是偶啦!)
│对 象│ 一个CrackMe->简单
│平 台│ 自己乱封装的D版XP
│目 的│ 很简单的CrackMe,主要分析算法,纯技术交流!
│声明声明│
`呵~~突然搞到这个小程序,就献丑下
------------------------------------------------------------------------
这个CrackMe好像有不只一个壳->我用脱壳机脱掉了~~汗一个!~手动偶脱不干净,用PEID查已经是无壳了``不过OD载入后会提示加壳,压缩...
有nag```
呵呵`脱壳后PEID查看说啥也没找到*->没找到就算了->一样破啦->西~~
传上来一个无壳的有nag的
Ctrl+n找函数,可以看到这个小程序只用了几个函数而已,唯一可下有用断点的就是"
"GetDlgItemText"->这个了,在每个函数上下断,;
输假信息: name:jiangwu55
code:12121212
------------------------------------------------------------------------------
004010E2 |. E8 43020000 call <jmp.&USER32.GetDlgItemTextA>---->断在这了
004010E7 |. 83F8 05 cmp eax, 5--------------------------->name长度和5比较
004010EA |. 7D 26 jge short 00401112------------------->name长度必须大于等于5
004010EC |. B8 66354000 mov eax, 00403566
004010F1 |. E8 05020000 call 004012FB
004010F6 |. 6A 40 push 40 ; /Style = MB_OK|MB_ICONASTERISK|MB_APPLMODAL
004010F8 |. 68 52354000 push 00403552 ; |Title = "TSRh CrackMe *Easy*"
004010FD |. 68 00304000 push 00403000 ; |Text = "Kill this Nag!!!"
00401102 |. 6A 00 push 0 ; |hOwner = NULL
00401104 |. E8 27020000 call <jmp.&USER32.MessageBoxA> ; \MessageBoxA->name长度小于5就在这里出错
00401109 |. 33C0 xor eax, eax
0040110B |. 5E pop esi
0040110C |. 5F pop edi
0040110D |. 5B pop ebx
0040110E |. C9 leave
0040110F |. C2 1000 retn 10
00401112 | E8 DC000000 call 004011F3------------------------->关键call1跟进
00401117 |. 6A 50 push 50
00401119 |. 68 90314000 push 00403190
0040111E |. 68 4D010000 push 14D
00401123 |. FF75 08 push dword ptr [ebp+8]
00401126 |. E8 FF010000 call <jmp.&USER32.GetDlgItemTextA>---->取code
0040112B |. A1 90314000 mov eax, dword ptr [403190]---------->code前四位到eax
我输的是"121212"->也就是"32313231"
00401130 |. 3D 74737268 cmp eax, 68727374-------------------->code前四位和68727374比较,(也就是trsh)
00401135 75 23 jnz short 0040115A------------------->我输入的不符合上面条件了不过为了看后面怎么算,这里就nop
00401137 |. 05 20320000 add eax, 3220
0040113C |. 50 push eax
0040113D |. 33C0 xor eax, eax
0040113F |. E8 EA000000 call 0040122E------------------------->关键call2跟进
00401144 |. 58 pop eax
00401145 |. 85C0 test eax, eax
00401147 |. 74 11 je short 0040115A
00401149 |. 33C2 xor eax, edx
0040114B |. 8BD0 mov edx, eax
0040114D |. 8BF0 mov esi, eax
0040114F |. 33C0 xor eax, eax
00401151 |. E8 31010000 call 00401287------------------------->关键call3跟进
00401156 |. 84C0 test al, al
00401158 |. 75 1F jnz short 00401179
0040115A |> B8 85354000 mov eax, 00403585
0040115F |. E8 97010000 call 004012FB
00401164 |. 6A 10 push 10 ; /Style = MB_OK|MB_ICONHAND|MB_APPLMODAL
00401166 |. 68 52354000 push 00403552 ; |Title = "TSRh CrackMe *Easy*"
0040116B |. 68 00304000 push 00403000 ; |Text = "Kill this Nag!!!"
00401170 |. 6A 00 push 0 ; |hOwner = NULL
00401172 |. E8 B9010000 call <jmp.&USER32.MessageBoxA> ; \MessageBoxA
00401177 |. EB 1D jmp short 00401196
00401179 |> B8 95354000 mov eax, 00403595
0040117E |. E8 78010000 call 004012FB
00401183 |. 6A 40 push 40 ; /Style = MB_OK|MB_ICONASTERISK|MB_APPLMODAL
00401185 |. 68 52354000 push 00403552 ; |Title = "TSRh CrackMe *Easy*"
0040118A |. 68 00304000 push 00403000 ; |Text = "Kill this Nag!!!"
0040118F |. 6A 00 push 0 ; |hOwner = NULL
00401191 |. E8 9A010000 call <jmp.&USER32.MessageBoxA> ; \MessageBoxA
---------------------------------------------------------------------------------------
关键call1:
004011F3 /$ A3 14354000 mov dword ptr [403514], eax---------->name长度到403514
004011F8 |. BF D3070000 mov edi, 7D3------------------------->0x7d3到edi
004011FD |. 033D 14354000 add edi, dword ptr [403514] --------->name长度加上0x7d3
00401203 |. 57 push edi ; /<%d>------>结果以十进制形式进栈
00401204 |. 68 1B354000 push 0040351B ; |Format = "tsrh-%d-"format呵``格式化,"tsrh-%d-"这种格式前面看到了,注册码前四位必须为"tsrh",这里就是了
00401209 |. 68 20334000 push 00403320 ; |s = tsrh-cra.00403320
0040120E |. E8 05010000 call <jmp.&USER32.wsprintfA>; \wsprintfA
00401213 |. 83C4 0C add esp, 0C
00401216 |. 68 20334000 push 00403320 ; /String = ""
0040121B |. E8 34010000 call <jmp.&KERNEL32.lstrlenA>; \lstrlenA-------->取得格式化字符串后的长度
00401220 |. 8B3D 14354000 mov edi, dword ptr [403514]
00401226 |. BE 01000000 mov esi, 1
0040122B |. 8BC8 mov ecx, eax----------------->格式化后字符串的长度传给ecx
0040122D \. C3 retn
---------------------------------------------------------------------------------------
关键call2:
0040122E /$ 56 push esi
0040122F |. 68 20334000 push 00403320 ; /String = "tsrh-2012-"->这个字符串,熟悉吧
00401234 |. E8 1B010000 call <jmp.&KERNEL32.lstrlenA>; \lstrlenA-------------->这个取长度函数,熟悉吧
00401239 |. 8B3D 14354000 mov edi, dword ptr [403514] ----->name长度传到edi
0040123F |. BE 01000000 mov esi, 1------------------->1到esi
00401244 |. 8BC8 mov ecx, eax----------------->格式化后字符串长度到ecx(后面该长度记做len,方便:))
00401246 |> B8 B0344000 mov eax, 004034B0 //从这里开始循环了
0040124B |. 0FB64406 FF movzx eax, byte ptr [esi+eax-1];-->取name字符
00401250 |. 04 0C add al, 0C --------------------->name字符加上0c(结果记作a,方便下面讲解)
00401252 |. 0FB6D0 movzx edx, al
00401255 |. 83EA 11 sub edx, 11--------------------->再减11
00401258 |. 03D0 add edx, eax-------------------->加上a
0040125A |. 2BD1 sub edx, ecx-------------------->再减去len
0040125C |. 33C2 xor eax, edx-------------------->结果与a异或
0040125E |. 50 push eax ; /<%X>->结果以大写十六进制形式进栈
0040125F |. 68 18354000 push 00403518 ; |Format = "%X"->又一个format格式化
00401264 |. 8D81 20334000 lea eax, dword ptr [ecx+403320] ; |
0040126A |. 50 push eax ; |s
0040126B |. E8 A8000000 call <jmp.&USER32.wsprintfA> ; \wsprintfA----->又一个wsprintf
00401270 |. 83C4 0C add esp, 0C
00401273 |. 68 20334000 push 00403320 ; /String = "tsrh-2012-"把上面结果格式化到这种格式的屁股后面了
00401278 |. E8 D7000000 call <jmp.&KERNEL32.lstrlenA> ; \lstrlenA->格式化后取其长度(记作len)
0040127D |. 8BC8 mov ecx, eax
0040127F |. 46 inc esi
00401280 |. 4F dec edi------------------------>记数器减一
00401281 |.^ 75 C3 jnz short 00401246------------->没算完就跳上去
00401283 |. 33C0 xor eax, eax
00401285 |. 5E pop esi
00401286 \. C3 retn
这里先总结一下:
算法最后再说->
前面我们知道第一个关键的call1是用来算出注册码"tsrh-XXXX-"->这个xxxx,只是简单的把name的长度加的0x7d3再以十进制形式格式化到xxxx里->得到"tsrh-2012-"这个字符串
再来第二个call,根据name长度来循环计算,这个循环结果也都格式化到上面那个字符串的后面,不过这个值只是临时的,因为还有第三个call,->通过每二次运算,我们得到"tsrh-2012-A7B8D6A9B0625A1816"
-----------------------------------------------------------------------
关键call3:
00401287 /$ 53 push ebx
00401288 |. BE 01000000 mov esi, 1
0040128D |> B8 B0344000 /mov eax, 004034B0
00401292 |. 0FB64406 FF |movzx eax, byte ptr [esi+eax-1]<-每次循环取name字符到eax
00401297 |. 85C0 |test eax, eax------>呃``这里说明一下
上一代码知道eax是name字符了,而下面那句可以跳出循环,
循环最后面还有一个cmp esi,10 jnz..这样,
在这里啥时候才会跳出循环呢,要等eax,也就是name字符取完了
eax为空,就跳,可是后面的0x10...这也是一个计数器啦,
第三个call中设了两个计数器,后面讲注册机时再解释
00401299 |. 74 34 |je short 004012CF
0040129B |. 40 |inc eax------------------------>name字符加一
0040129C |. BA 20334000 |mov edx, 00403320-------------->这串"tsrh-2012-A7B8D6A9B0625A1816"传到edx
004012A1 |. 0FB65416 0B |movzx edx, byte ptr [esi+edx+B]-->esi在循环之前给它赋了个1
edx上面知道是那串东东 byte->取字节,里面加了个0xb,那就是从1+0xb处取字符
也就是从1开始数,那串东东的第13位
004012A6 |. 33C2 |xor eax, edx------------------->与加一后的name字符异或
004012A8 |> 83F8 41 |/cmp eax, 41------------------->异或结果与41比较
004012AB |. 7D 05 ||jge short 004012B2------------>大于或等于41就和5a比较
004012AD |. 83C0 08 ||add eax, 8-------------------->小于41就加8
004012B0 |.^ EB F6 |\jmp short 004012A8------------>上面加8后再跳上去和41比较
必须加到结果大于0x41才跳出循环
004012B2 |> 83F8 5A |/cmp eax, 5A------------------->上面循环结束后,循环结果就和5a比较
004012B5 |. 7E 05 ||jle short 004012BC------------>小于或等于就跳出循环
004012B7 |. 83E8 03 ||sub eax, 3-------------------->大于就减去3
004012BA |.^ EB F6 |\jmp short 004012B2------------>减3再跳上去和5a比较
必须减到结果小于5a才跳出循环
004012BC |> 83C6 09 |add esi, 9--------------------->esi面前是计数器
这里+9只是为了在内存地址里存入结果
004012BF |. BB 20334000 |mov ebx, 00403320-------------->这串"tsrh-2012-A7B8D6A9B0625A1816"传到ebx
004012C4 |. 89041E |mov dword ptr [esi+ebx], eax--->esi前面+9了,ebx里是那串东东
这里就是把结果传到那串东东的第9位
当然,后面的循环分别是第10,11..位
004012C7 |. 83EE 08 |sub esi, 8------------------->前面也说了,esi在循环最后面作为第二个计数器
前面+9,这里再-8也就是+1了,计数器加一了,呵
004012CA |. 83FE 10 |cmp esi, 10------------------>第二个计数器和0x10比较
004012CD |.^ 75 BE \jnz short 0040128D----------->不等就跳上去
004012CF |> 33C0 xor eax, eax
004012D1 |. 5B pop ebx
004012D2 |. E8 01000000 call 004012D8--------------->这个是关键对比call4
004012D7 \. C3 retn
--------------------------------------------------------------------------------------
关键对比call4:
004012D8 /$ 53 push ebx
004012D9 |. 33F6 xor esi, esi
004012DB |> BB 90314000 /mov ebx, 00403190
004012E0 |. 8B1C1E |mov ebx, dword ptr [esi+ebx]----->每次循环都取假码的四位
004012E3 |. B8 20334000 |mov eax, 00403320
004012E8 |. 8B0406 |mov eax, dword ptr [esi+eax]----->每次循环都取真码的四位
004012EB |. 84C0 |test al, al----------------------->这里看比较完了没有
也就是前面说是取字符是不是为空
004012ED |. 74 09 |je short 004012F8--------------->是的话就跳
004012EF |. 46 |inc esi
004012F0 |. 3BD8 |cmp ebx, eax--------------------->比较看看
004012F2 |.^ 74 E7 \je short 004012DB--------------->相等就跳上面再比较
004012F4 |. 33C0 xor eax, eax---------------------->不相等eax就为0
004012F6 |. EB 01 jmp short 004012F9---------------->跳到pop返回
004012F8 |> 40 inc eax--------------------------->如果在4012f2处每次都相等,都跳上去
那么取完字符后4012ed处会跳到这,eax为1
004012F9 |> 5B pop ebx
004012FA \. C3 retn
这个对比也总结一下(爆破点很多:));
分析后知道eax其实是一个标志,指出真码和假码是否相同,相等就为1,不相等就为0
也就是注册成功eax为1,否则为0
爆破的话可以把4012ed改为jmp,或把4012f6给NOP掉,或返回后手动赋给eax一个1
还有就是返回后把关键跳转401158改为jmp或je,,呵,够多吧:)
那么最后得出:
name:jiangwu55
code:tsrh-2012-AXYZYZXFF
-------------------------------------------------------------------------------------
算法总结一下:
根据"tsrh-(%d)xxxx-yyyyyyy"这个格式来算
xxxx=取长度(name)+0x7d3
yyyyyyy有两次运算,烦!!~~偶偷懒一下,不说了,自己看看吧,呵~~~
(总结等于没说:) )
不过后面的注册机会讲到这个CrackMe第三个关键处的计数器
------------------------------------------------------------------------------------
C++源码注册机:
#include "stdio.h"
#include "string.h"
#include "iostream.h"
main()
{
char name[100],code[100],ch[5],lsCh,serial[100]="";
int i,len,len1,ls1,ls2,max;
cout<<"Your name:";
cin>>name;
len=strlen(name);
if(len<=5) //长度须大于等于5
{
cout<<"no!The name must big than 5";
}
else
{
max=len+0x7d3; //算出xxxxx
sprintf(code,"tsrh-%d-",max); //格式化,为了取长度
len1=strlen(code); //取长度
//-------------------------------------------------
for(i=0;i<len;i++) |
{ |
ls1=name[i]+0x0c; |
ls2=ls1-0x11; |
ls2+=ls1; 第二个call
ls2-=len1; |
ls2^=ls1; |
sprintf(ch,"%X",ls2); |
strcat(code,ch); |
len1=strlen(code); |
} |
//-------------------------------------------
lsCh=code[12]; /*取每二个call算出来的那串东东的第13位
前面分析过了(4012c4),因为传回到那串东东的"yyyyy.."的第一位处,后几位被空字节覆盖了
所以只有第一次循环取了第13位上的字符,其余循环则都是空
这里我简单的只取的其第13位/*
if(len<0x10) /*这个判断,前面也分析过,第三个call有两个计数器,
取完name字符后就跳出循环(也就是根据name长度的计数器)
还有就是到循环最后的16次计数*/
{
//-------------------------------------------
for(i=0;i<len;i++) |
{ |
ls1=(name[i]+1)^lsCh; |
while(ls1<0x41) |
{ |
ls1+=8; 第三个call的第一种情况
} |
while(ls1>0x5a) |
{ |
ls1-=3; |
} |
serial[i]=ls1; |
lsCh=NULL; |
}
//-------------------------------------------
}
else
{
//-------------------------------------------
for(i=0;i<0xf;i++) |
{ |
ls1=(name[i]+1)^lsCh; |
while(ls1<0x41) |
{ |
ls1+=8; |
} 第三个call的第二个情况
while(ls1>0x5a) |
{ |
ls1-=3; |
} |
serial[i]=ls1; |
lsCh=NULL; |
}
//-------------------------------------------
}
sprintf(name,"tsrh-%d-",max);
cout<<"Your code:"<<name<<serial<<endl;
}
}
第三个call的情况总结:
注册机上讲过了,这里再分析一下,,一种是根据name长度来循环,还有就是要循环16次,啥时候才会出现这种情况咧?
当name长度大于16的时候,在OD的代码中,第一个跳出循环的跳转就会比第二个计数器实现得晚,循环的最后一个跳转就不
跳了,直接走出循环,而name对于16来说多出来的那些字符也忽略不算了;
当name长度小于16的时候,在OD的代码中,因为16次循环还没完,但name的字符已经取完了,所以第一个跳出循环的跳转
就跳了,而(16-name长度)次的循环也就不算了
实在不明白,就跟一下程序吧!
-------------------------------------------------------------------------------
最后给出整体的注册机:
#include "stdio.h"
#include "string.h"
#include "iostream.h"
main()
{
char name[100],code[100],ch[5],lsCh,serial[100]="";
int i,len,len1,ls1,ls2,max;
cout<<"Your name:";
cin>>name;
len=strlen(name);
if(len<=5)
{
cout<<"no!The name must big than 5";
}
else
{
max=len+0x7d3;
sprintf(code,"tsrh-%d-",max);
len1=strlen(code);
for(i=0;i<len;i++)
{
ls1=name[i]+0x0c;
ls2=ls1-0x11;
ls2+=ls1;
ls2-=len1;
ls2^=ls1;
sprintf(ch,"%X",ls2);
strcat(code,ch);
len1=strlen(code);
}
lsCh=code[12];
if(len<0x10)
{
for(i=0;i<len;i++)
{
ls1=(name[i]+1)^lsCh;
while(ls1<0x41)
{
ls1+=8;
}
while(ls1>0x5a)
{
ls1-=3;
}
serial[i]=ls1;
lsCh=NULL;
}
}
else
{
for(i=0;i<0xf;i++)
{
ls1=(name[i]+1)^lsCh;
while(ls1<0x41)
{
ls1+=8;
}
while(ls1>0x5a)
{
ls1-=3;
}
serial[i]=ls1;
lsCh=NULL;
}
}
sprintf(name,"tsrh-%d-",max);
cout<<"Your code:"<<name<<serial<<endl;
}
}
--------------------------------------------------------------
对了,这个程序还有一个nag,启动时会出现:
重新载入程序,在每个MessageBox上设断,运行后就会断下:
00401062 |. B8 01000000 mov eax, 1---------------------->1传到eax
00401067 |. 84C0 test al, al---------------------->看看eax是否为空,不为空下面就不跳
所以在上面给eax赋个0,也就是把1改为0就行了
00401069 |. 74 13 je short 0040107E-------------->这个可以跳过nag,改为jmp也行
0040106B |. 6A 40 push 40
0040106D |. 68 52354000 push 00403552
00401072 |. 68 00304000 push 00403000
00401077 |. 6A 00 push 0
00401079 |. E8 B2020000 call <jmp.&USER32.MessageBoxA> ->断在这里,上面看看
--------------------------------------------------------------
│经验总结│
这个CrackMe算法不复杂,就是在写注册机时有这么个小陷阱
也就是两个计数器那个地方,注意一下,仔细分析一下就行了@~~~:->
CrackMe和注册机:
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)