Crackme中的游戏-----贪吃蛇
某日,闲来无聊,从C盘浏览到F盘,又从F盘浏览到C盘。无意中翻出个CrackMe。反正无事,就当复习好了。不看不知道,一看还真不错,不只是单调的函数拉,贪吃蛇的小游戏一定玩过吧?注册的过程就是个贪吃蛇游戏。(本文适合和我一样入门级的朋友,所以注释得很罗嗦,高手莫笑。)
废话不多说~,FI检查,无壳。OllyDbg载入。这种CrackMe就是很适合我们菜鸟,无壳无花样。下断,到下面地址:
00401092 >>pushad //从这里开始,取得我们添入的信息,开始处理。
00401093 .>push 7F
00401095 .>push Snake.00401E00
0040109A .>push 3E8
0040109F .>push dword ptr ds:[401F00]
004010A5 .>call dword ptr ds:[<&USER32.Get> ; \GetDlgItemTextA
004010AB .>test eax,eax ;下面一小段是判断是否输入姓名和注册码的,我们跳过一些代码
004010AD .>jnz short Snake.004010CB
…………
00401103 >>call Snake.004012AA ; 这个CALL对输入的注册码进行形式上判断
00401108 .>test al,al ; 跟进很容易知道注册码只能是0-9,A-F
0040110A .>jnz short Snake.00401128 ;注册码不合要求,不跳出错
0040110C .>push 30
0040110E .>push Snake.00401400 ; |Title = "Error"
00401113 .>push Snake.00401464 ; |Text = "There is something wrong with your Serial..."
00401118 .>push dword ptr ds:[401F00] ; |hOwner = 000B01A8 ('Snake ',class='#32770')
0040111E .>call dword ptr ds:[<&USER32.Mes>; \MessageBoxA
00401124 .>popad
00401125 .>xor eax,eax
00401127 .>retn
00401128 >>call Snake.00401173 ; 这个CALL是将401B00到401BFF清零,在这区域两边添加16个FF,作为边界
0040112D .>call Snake.0040119B ;对名字进行处理的关键call
00401132 .>call Snake.0040120C ;对注册码进行处理的关键call
00401137 .>test al,al
00401139 .>jnz short Snake.00401157 ; 跳则成功,不跳OVER。爆破这里OK,不过我们的目的是找算法。
我们先跟进对注册名处理的CALL:
0040119B /$>push eax
0040119C |.>push ecx
0040119D |.>push edx
0040119E |.>push esi
0040119F |.>push edi
004011A0 |.>xor ecx,ecx
004011A2 |.>mov esi,Snake.00401E00 ; 这是保存注册名的地址
004011A7 |.>mov edi,Snake.00401B00 ;记的前面1个CALL清零的区域不?这就是这区域的开始地址,也就是我们贪吃蛇的游戏范围
004011AC |.>push esi
004011AD |.>mov dl,0
004011AF |>>/mov al,byte ptr ds:[esi] ;[esi]保存的注册名
004011B1 |.>|inc esi
004011B2 |.>|add dl,al
004011B4 |.>|test al,al ; 对名字各位求和,取低8位,记为S,即求和后与0x000000FF作与运算
004011B6 |.>\jnz short Snake.004011AF
004011B8 |.>pop esi
循环1:这个循环作用是产生名字长度个“CC”标志
004011B9 |>>/movzx eax,byte ptr ds:[esi] ; 取名字的1位
004011BC |.>|xor al,dl ; 记T=Name[n] XOR S;
004011BE |>>|/sub dl,al ; S=(S-T)AND 000000FF(注意是低8位运算,始终只取低8位,所以我们算的时候要与0x000000FF作与运算
004011C0 |.>||or byte ptr ds:[edi+eax],0CC ; [401b00+T]=CC;
004011C4 |.>||jnz short Snake.004011CA ;??上一步结果不可能为0
004011C6 |.>||dec dl
004011C8 |.>|\jmp short Snake.004011BE
004011CA |>>|inc ecx ;记数器,注册名长度为几,ecx+几次
004011CB |.>|inc esi
004011CC |.>|cmp byte ptr ds:[esi],0 ; 注册名是否取完
004011CF |.>\jnz short Snake.004011B9
上面这循环,在401B00-401BFF这段地址产生CC标志,即我们这游戏中蛇要吃的果子,其实,只要多看下就知道这段程序有问题,我在文末还会说明下。
循环2:这个循环是找个位置放‘DD’标志
004011D1 |.>mov dword ptr ds:[401F04],ecx ; 名字长度
004011D7 |.>xor dl,al ; S=S XOR T;
004011D9 |>>/sub al,dl ; T=(T-S) AND 0x000000FF;
004011DB |.>|cmp byte ptr ds:[edi+eax],0CC ; 比较是不是已经放了CC标志了
004011DF |.>|jnz short Snake.004011E5 ; 不是就存放DD标志
004011E1 |.>|dec dl ; 是CC标志就再找位置
004011E3 |.>\jmp short Snake.004011D9 ;
004011E5 |>>mov byte ptr ds:[edi+eax],0DD ; [401B00+T]=DD
004011E9 |.>mov al,dl ; T=S;
这个小循环是找个没存放CC标志的位置放DD标志,后面我们可以知道,DD是最后1个果子
循环3:找个位置放99标志,从下面可知,99是蛇头
004011EB |>>/cmp byte ptr ds:[edi+eax],0CC ;
004011EF |.>|je short Snake.004011F7 ; 寻找1个既没存放CC,也没存放DD标志的位置
004011F1 |.>|cmp byte ptr ds:[edi+eax],0DD ;
004011F5 |.>|jnz short Snake.004011FB ;
004011F7 |>>|dec al
004011F9 |.>\jmp short Snake.004011EB
004011FB |>>lea eax,dword ptr ds:[edi+eax]
004011FE |.>mov byte ptr ds:[eax],99 ; [401BDD+T]=99;
00401201 |.>mov dword ptr ds:[401700],eax
00401206 |.>pop edi
00401207 |.>pop esi
00401208 |.>pop edx
00401209 |.>pop ecx
0040120A |.>pop eax
0040120B \.>retn
从上面的分析可以知道,对注册名的处理结果是在401B00至401BFF的空间中产生数量与注册名长度相等的’CC’标志以及1个’DD’标志和1个’99’标志.
下面我们再跟进对注册码进行处理的CALL:
0040120C /$>pushad ; 游戏开始,准备好吃果子了。
0040120D |.>mov esi,Snake.00401D00 ; 注册码地址
00401212 |.>mov edi,Snake.00401700 ; "99"标志的地址
00401217 |>>/movzx eax,byte ptr ds:[esi] ; 取注册码的1位 记作PASS[n]
0040121A |.>|test al,al ;
0040121C |.>|je short Snake.00401260
0040121E |.>|sub al,30 ;这几句ASCII码变成0-15的数字
00401220 |.>|cmp al,0A ;
00401222 |.>|jb short Snake.00401226 ; PASS[n]<10在401226处理
00401224 |.>|sub al,7 ; 若是字母,就-7,结果是A=10,B=11…F=15
00401226 |>>|mov ecx,eax ;
00401228 |.>|and al,3 ; 即PASS[n] MOD 4
0040122A |.>|shr cl,2 ; PASS[n]/4,是整数相除,ECX即下面的K
0040122D |.>|mov edx,10 ;edx是每次蛇移动的距离
00401232 |.>|test al,al ;看PASS[n] MOD 4是否为0
00401234 |.>|je short Snake.00401241 ; 4的倍数跳
00401236 |.>|dec eax
00401237 |.>|je short Snake.0040123F ; 4的倍数+1 跳
00401239 |.>|shr edx,4 ;若PASS[n]=4*K+2或PASS[n]=4*K+3,则edx==1
0040123C |.>|dec eax ;
0040123D |.>|jnz short Snake.00401241 ; PASS[n]= 4*K+3跳
0040123F |>>|neg edx ; 蛇移动距离取反
上面这段是根据PASS[n]计算出每次蛇移动的距离,具体是:(PASS[n]=0~F,即0~15)
PASS[n]=4*K,edx=10,移动(K+1)*10 PASS[n]=4*K+1,edx=-10,位移-(K+1)*10
PASS[n]=4*K+2,edx=-1,位移-(K+1) PASS[n]=4*K+3,edx=1,位移K+1
00401241 |>>|mov eax,dword ptr ds:[edi] ; ‘99’标志地址,这个‘99’始终是蛇头
00401243 |.>|add eax,edx ;蛇头原来位置+移动距离
00401245 |.>|mov al,byte ptr ds:[eax] ; 取蛇头将要移动到的位置的值
00401247 |.>|test al,al ;是否为0?
00401249 |.>|je short Snake.0040127A ;是0则跳到下面移动蛇身
0040124B |.>|cmp al,99 ;“99”标志代表蛇身的,看蛇头是否吃到蛇身了
0040124D |.>|je short Snake.00401260 ; 是,则跳,OVER
0040124F |.>|cmp al,0CC ;是否是果子?
00401251 |.>|je short Snake.00401268 ;是则跳到下面增长蛇身
00401253 |.>|cmp al,0DD ;是否是最后一个果子
00401255 |.>|jnz short Snake.00401260 ; 跳则OVER
00401257 |.>|cmp dword ptr ds:[401F04],0 ;[401F04]即剩余果子数,在处理注册名的那CALL里被初始化为注册名长度
0040125E |.>|je short Snake.00401264 ; 不=0,即果子没吃完,不跳OVER
00401260 |>>|popad
00401261 |.>|mov al,0 ;失败标志位
00401263 |.>|retn
00401264 |>>|popad
00401265 |.>|mov al,1 ;成功标志位
00401267 |.>|retn
00401268 |>>|dec dword ptr ds:[401F04] ; 吃到果子跳到这里处理。剩余果子个数-1。
0040126E |.>|call Snake.00401289 ;
00401273 |.>|mov dword ptr ds:[eax],ebx ; 这是吃中果子时执行的语句
00401275 |.>|mov byte ptr ds:[ebx],99 ; 将果子所在地址变为蛇身
00401278 |.>|jmp short Snake.0040127F ;
0040127A |>>|call Snake.00401289
0040127F |>>|test ecx,ecx ; ecx即上面的K
00401281 |.>|je short Snake.00401286 ; 看K=0不?不就再移动EDX距离。即每个字符移动EDX*(K+1)距离。
00401283 |.>|dec ecx ;K=K-1
00401284 |.>|jmp short Snake.00401241
00401286 |>>|inc esi ;取下一个注册码
00401287 \.>\jmp short Snake.00401217
上面有个CALL是用来处理蛇身移动的。
00401289 /$>push edi ; 蛇身各节地址,从EDI开始为蛇头有几个地址蛇身就有几节
0040128A |.>mov eax,dword ptr ds:[edi] ;
0040128C |.>mov ebx,eax
0040128E |.>add eax,edx ; 蛇身位置+EDX
00401290 |>>/mov dword ptr ds:[edi],eax ;保存新的蛇身位置
00401292 |.>|mov byte ptr ds:[eax],99 ;
00401295 |.>|mov byte ptr ds:[ebx],0 ; 将‘99’移位,原蛇身位清零
00401298 |.>|add edi,4 ; edi+4,取蛇身的下一节
0040129B |.>|cmp dword ptr ds:[edi],0 ;看蛇身是否还有下一节。
0040129E |.>|je short Snake.004012A6 ;没有下一节即返回
004012A0 |.>|mov eax,ebx ; ebx即上一节蛇身移动前位置(99标志已被清零),下一节蛇身位置将移动到这里
004012A2 |.>|mov ebx,dword ptr ds:[edi] ;将要移动的下一节蛇身位置
004012A4 |.>\jmp short Snake.00401290 ;跳上去进行移动
004012A6 |>>mov eax,edi
004012A8 |.>pop edi
004012A9 \.>retn
到此,我们已经弄清楚该程序的验证过程了。先清零出255个空位,对注册名处理,在空位种,生成注册名长度个CC标志,再生成DD和99标志。对注册码处理,每一位注册码都代表蛇的一次移动。只要先把CC标志吃完,再吃DD标志,就OK啦,当然吃到蛇身自己(99标志)也OVER咯~~
注意上面生成CC标志的那几句程序。多看几眼就知道这几句是有问题的。因为,在生成CC标志时没有检查要生成的位置是否已经有标志了,结果是可能导致原CC标志被覆盖,所以CC标志个数也不=注册名长度,在这种情况下永远也不可能成功(如:注册名为aaaaaaa,长度为7,但算出CC标志只有3位。 )。所以,这里也就不给出注册机了。当然了,我只要弄清楚过程就行了。下面对生成CC,DD,99标志这段过程给出C语言描述
int main(int argc, char* argv[])
{
int num[256]={0},s=0,t=0,i;
char name[20];
printf("Enter the name:\n");
scanf("%s",name);
for(i=0;name[i]!='\0';i++)s+=name[i];
s=s&255;
for(i=0;name[i]!='\0';i++)
{
t=name[i]^s;
s=(s-t)&255;
num[t]=1;
}
s=s^t;
t=(t-s)&255;
while(num[t]==1)
{
s=(s-1)&255;
t=(t-s)&255;
}
num[t]=2;
t=s;
while(num[t]==1||num[t]==2)t-=1;
num[t]=99;//µ½´Ë´¦Î»ÎªÖ¹,ΪÉú³ÉCC£DD£99±êÖ¾
for(i=0,t=0;i<256;i++)
{
switch(num[i])
{
case 0:continue;
case 1:printf("[%p]=CC ",4201216+i);t++;continue;
case 2:printf("[%p]=DD ",4201216+i);continue;
case 99:printf("[%p]=99 ",4201216+i);continue;
}
}
printf("\ntotal of \"CC\":%d",t);
scanf("%s",name);
return 0;
}
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课