首页
社区
课程
招聘
[原创]对一个crackme.exe的算法分析
发表于: 2010-8-30 18:20 8079

[原创]对一个crackme.exe的算法分析

2010-8-30 18:20
8079

题外话:二个多月前,一个朋友叫我修改一个DLL引出函数的功能(没有源代码),当时正在研究嵌入式软件的动态加载,与这个事情有一定的相关性,于是就帮着一起做了,却没想到对软件的破解产生了极大的兴趣,于是暂时停止了动态加载的研究,利用周末时间研究破解一段时间后,想试试学习效果,就选择了http://bbs.pediy.com/showthread.php?t=111376 上的crackme.exe做试验,当对算法的跟踪陷入停顿的时候,在上周五的晚上,突然灵光一现,明白了算法的精要之处,第二天写完了一张A4纸后,SERIAL就被推导出来。

运行crackme.exe,随便输入NAME和SERIAL,出现对话框提示:Try again. 用OD载入程序,查找字符串参考:

找到Try again,并双击,转到代码:
00401126                       68 3C604000     push crackme.0040603C                    ; try again.
往上走,到代码:
004010D0                       83EC 2C         sub esp,2C  处,再往上走就是nop指令,很明显,这个就是验证函数的开始,在004010D0处下断点,按shift+f9运行,出现界面后输入name:chglyq,随便输入序列号202002,按TEST按钮断了下来,然后单步跟踪。

004010D0                       83EC 2C         sub esp,2C                  
004010D3                       8D4424 00       lea eax,dword ptr ss:[esp]
004010D7                       56              push esi
004010D8                       8B7424 34       mov esi,dword ptr ss:[esp+34]
004010DC                       57              push edi
004010DD                       8B3D 9C504000   mov edi,dword ptr ds:[<&USER32.GetDlgIte>; USER32.GetDlgItemTextA
004010E3                       6A 0A           push 0A
004010E5                       50              push eax
004010E6                       68 F2030000     push 3F2
004010EB                       56              push esi
004010EC                       FFD7            call edi                                 ; USER32.GetDlgItemTextA
按F7进入,进入的是系统领空,过程忽略,得到的是输入name的长度大小
004010EE                       83F8 06         cmp eax,6
004010F1                       72 2C           jb short crackme.0040111F         ; 是否小于6位,小于6位则跳转出错
004010F3                       8D4C24 14       lea ecx,dword ptr ss:[esp+14]
004010F7                       6A 1E           push 1E
004010F9                       51              push ecx
004010FA                       68 F3030000     push 3F3
004010FF                       56              push esi
00401100                       FFD7            call edi                                 ; 得到SERIAL的长度
00401102                       83F8 06         cmp eax,6
00401105                       72 18           jb short crackme.0040111F                ; 是否小于6位
00401107                       8D5424 14       lea edx,dword ptr ss:[esp+14]            ; EDX存输入SERIAL
0040110B                       8D4424 08       lea eax,dword ptr ss:[esp+8]             ; EAX存输入NAME
0040110F                       52              push edx
00401110                       50              push eax                            ; NAME和SERIAL作为函数参数
00401111                       E8 2A000000     call crackme.00401140                    ; 关键验证函数,F7进入

为方便叙述,以下的跟踪有些不重要的代码的解释将忽略,另外,有些叙述看了,也许会问,你怎么知道的?其实没有先知先觉的事情,在跟踪的过程中经历了很多次失败后,悟出来的。这里纯粹是为了叙述的方便。

******00401140处的代码是对输入用户名(chglyq)进行某种运算,并对一个数据区的的内存值进行位置变动,同时修改[0040603B]处的值*******
数据区初始:
00406030  33323130
00406034  37363534
00406038  09003038
从00406030到00406039地址,依次存储0-9,0的ASCII码,0040603B地址存储数字9。

-----------------对用户名第一位C(ascii:63)做处理------------------
00401141                       8B5C24 08       mov ebx,dword ptr ss:[esp+8]             ; EBX得到用户名
00401148                       8BFB            mov edi,ebx                           ; EBX转存到EDI
0040114A                       83C9 FF         or ecx,FFFFFFFF                       ; ECX全部位置1
0040114D                       33C0            xor eax,eax                            ; EAX清0
0040114F                       F2:AE           repne scas byte ptr es:[edi]             ; 循环取用户名每一位
00401151                       8B7C24 18       mov edi,dword ptr ss:[esp+18]            ; EDI得到SERIAL
00401155                       33F6            xor esi,esi                              ; ESI清0
00401157                       F7D1            not ecx
00401159                       49              dec ecx                                  ; ECX得到NAME长度
0040115A                       8BE9            mov ebp,ecx                              ; EBP得到长度
0040115C                       83C9 FF         or ecx,FFFFFFFF                          ; ECX全部位置1
0040115F                       F2:AE           repne scas byte ptr es:[edi]           ; 循环取SERIAL每一位
00401161                       F7D1            not ecx
00401163                       49              dec ecx                                ; 得到SERIAL长度---ECX
************开始运算*************
0040116A                       0FBE041E        movsx eax,byte ptr ds:[esi+ebx]          ; EAX得到用户名一字符:63('c')
0040116E                       C1F8 06         sar eax,6                         ; EAX算术右移6位,得到最高2位-01
00401171                       6A 00           push 0                          ; 0012FA28变成0----这个内存有2个取值0或1,表明是在用户名过程中还是在SERIAL运算的过程中(这个也是在多次调试后才明白的)
00401173                       50              push eax                        ; 用户名一字符右移6位的值压栈-作为函数参数---01
00401174                       E8 B7000000     call crackme.00401230                ; EAX为返回值=1
按F7进入.00401230函数
00401230                       8B4424 04       mov eax,dword ptr ss:[esp+4]             ; 取参数
00401235                       83F8 03         cmp eax,3                        ; 参数值是否大于3?一个字节右移6位后的值是不可能大于3的(0、1、2、3)
0040123F                      /FF2485 54134000 jmp dword ptr ds:[eax*4+401354]       ; EAX=0、1、2、3,本例为1,也就是根据参数的值,跳转到不同的分支去处理

00401282                       8A0D 3B604000   mov cl,byte ptr ds:[40603B]             ; ds:[40603B] 存储09
00401288                       80F9 07         cmp cl,7                              ; ds:[40603B]的09是否大于等于7?第一次肯定大于7的,因为初始值为9
004012F7                       8B4424 10       mov eax,dword ptr ss:[esp+10]           ; 将0012FA28中的内容存EAX=0
004012FB                       85C0            test eax,eax                         ; EAX为0,判断是在用户名运算状态还是SERIAL运算状态
004012EF                       B8 01000000     mov eax,1                           ;返回值设为1
004012F4                       5E              pop esi
004012F5                       5B              pop ebx
004012F6                       C3              retn
**********未对数据区做任何修改就返回*********
数据区
00406030  33323130
00406034  37363534
00406038  09003038

如果在代码处:
00401288                       80F9 07         cmp cl,7
是小于7的话,则处理的流程是:
0040128D                       0FBEC1        movsx eax,cl                             ; EAX获得[40603B]中的值
00401290                       80C1 03         add cl,3
00401294                       0FBE90 30604000 movsx edx,byte ptr ds:[eax+406030]
0040129B                       8A98 33604000   mov bl,byte ptr ds:[eax+406033]
004012A1                       880D 3B604000   mov byte ptr ds:[40603B],cl             ; 等同于将[40603B]原先的值加3
004012A7                       8898 30604000   mov byte ptr ds:[eax+406030],bl
004012AD                       8890 33604000   mov byte ptr ds:[eax+406033],dl          ; 将基于406030和406033偏移原先[40603B]值的两个内存互换
004012B3                       B8 01000000     mov eax,1
004012B8                       5B              pop ebx
004012B9                       C3              retn
**********总结:根据参数的值1,跳转到相应的处理代码,本例中的处理是:将[40603B]中的值与7比较,如果大于等于7,则不做任何处理返回;如果小于7,则将基于基于406030和406033偏移为[40603B]值的两个内存互换,再将[40603B]中的值加3,返回1。

*******************函数返回**********************

00401179                       8A0C1E          mov cl,byte ptr ds:[esi+ebx]             ; 再取用户名同一位字符c存cl
0040117C                       6A 00           push 0                               ;运算状态标志为函数参数
0040117E                       C1F9 04         sar ecx,4                              ; ECX算术右移4位
00401181                       83E1 03         and ecx,3                              ; ECX与3(00000011)相与,就是为得到第4、5位(从0开始)BIT值,本例为2
00401184                       51              push ecx
00401185                       E8 A6000000     call crackme.00401230                    ;
按F7进入函数
00401235                       83F8 03         cmp eax,3                        ; 参数的值是否大于3,---不可能出现
0040123F                      /FF2485 54134000 jmp dword ptr ds:[eax*4+401354]          ; EAX=2
ds:[0040135C]=004012BA (crackme.004012BA)
004012BA                       0FBE0D 3B604000 movsx ecx,byte ptr ds:[40603B]           ; ds:[40603B] 存储09,上次参数为1,未对这个内存值做修改
004012C1                       8BC1            mov eax,ecx                             ; EAX得到09
004012C3                       BE 03000000     mov esi,3                                ; ESI做为除数
004012C8                       99              cdq                                     ; 把EAX扩展成EDX:EAX,EDX变成0
004012C9                       F7FE            idiv esi                                 ; 把EDX:EAX 除以ESI,余数放在EDX=0,商在EAX=3
004012CB                       83FA 01         cmp edx,1                                ; 余数是否等于1
****被3除,余数只能是0、1、2,余数等于1的话,[40603B]的值为1、4、7三个****
如果等于1的话,则:
004012F7                       8B4424 10       mov eax,dword ptr ss:[esp+10]            ; 将0012FA28中的内容存EAX
004012FB                       85C0            test eax,eax
004012EF                       B8 01000000     mov eax,1                             ; 设置返回值为1
004012F4                       5E              pop esi
004012F5                       5B              pop ebx
004012F6                       C3              retn
本例不等于1(余数为0)
004012D0                       0FBE81 30604000 movsx eax,byte ptr ds:[ecx+406030]       ; ECX=09,ds:[00406039]=30 ('0'),[40603B]中的值作为偏移量
004012D7                       8A91 2F604000   mov dl,byte ptr ds:[ecx+40602F]          ; ds:[00406038]=38 ('8'),EDX=38
004012DD                       8891 30604000   mov byte ptr ds:[ecx+406030],dl          ; ds:[00406039]=38 ('8')
004012E3                       8881 2F604000   mov byte ptr ds:[ecx+40602F],al          ; ds:[00406038]=30 ('0') 38和39内存值交换
004012E9                       FE0D 3B604000   dec byte ptr ds:[40603B]                 ; 原先的09变成08,[40603B]中的值减1
004012EF                       B8 01000000     mov eax,1                                ; 返回值为1
004012F4                       5E              pop esi
004012F5                       5B              pop ebx
**********总结:根据参数的值2,跳转到相应的处理代码,本例中的处理是将[40603B]中的值除以3,根据余数是否等于1,做处理,若等于1,则直接返回,不等于1,则根据[40603B]中的值做为偏移量,将数据区406030和40602F的连续两个内存地址的值互换,并将[40603B]中的值减1*************
数据区:
00406030  33323130
00406034  37363534
00406038  08003830

************函数返回***********

0040118A                       8A141E          mov dl,byte ptr ds:[esi+ebx]             ; 再取用户名同一位字符c--EDX
0040118D                       6A 00           push 0
0040118F                       C1FA 02         sar edx,2                             ; ECX算术右移2位
00401192                       83E2 03         and edx,3                             ; edX与3(00000011)相与,得到第2、3位BIT,本例为0
00401195                       52              push edx
00401196                       E8 95000000     call crackme.00401230                   ; EAX为返回值=1
按F7进入函数
0040123F                      /FF2485 54134000 jmp dword ptr ds:[eax*4+401354]          ; crackme.00401246
ds:[00401354]=00401246 (crackme.00401246)

00401246                       8A0D 3B604000   mov cl,byte ptr ds:[40603B]             ; ECX=08,[40603B]中的值作为偏移量
0040124C                       80F9 03         cmp cl,3                              ;[ 40603B]中的值是否小于等于3
本例是大于,则走:
00401255                       0FBEC1          movsx eax,cl                         ; EAX=08,即将[40603B]中原先的值保存在EAX中
00401258                       80C1 FD         add cl,0FD                            ; 08+FD(253)---CL=05,等同减3
0040125C                      0FBE90 30604000 movsx edx,byte ptr ds:[eax+406030]       ; ds:[00406038]=30 ('0')---EAX=08
00401263                       8A98 2D604000   mov bl,byte ptr ds:[eax+40602D]          ; ds:[00406035]=35 ('5')
00401269                       880D 3B604000   mov byte ptr ds:[40603B],cl              ; ds:[0040603B]=05
0040126F                       8898 30604000   mov byte ptr ds:[eax+406030],bl          ; ds:[00406038]=35 ('5')
00401275                       8890 2D604000   mov byte ptr ds:[eax+40602D],dl          ; ds:[00406035]=30 ('0')
0040127B                       B8 01000000     mov eax,1
00401280                       5B              pop ebx
00401281                       C3              retn
如果是小于等于3,则:
004012F7                       8B4424 10       mov eax,dword ptr ss:[esp+10]            ; 将0012FA28中的内容存EAX
004012FB                       85C0            test eax,eax
004012EF                       B8 01000000     mov eax,1                            ;  置返回值为1
004012F4                       5E              pop esi
004012F5                       5B              pop ebx
004012F6                       C3              retn
未对数据区做任何改变就返回。

***********总结:根据参数的值0,跳转到响应的处理代码,本例中的处理是,取[40603B]中的值与3比较,如果值小于等于3,则不做任何处理返回1;本例是大于3(等于8),将基于406030和40602D,偏移为[40603B]中的值的两个内存互换(本例是38和35两个内存),并将[40603B]中的值与FD相加,得到05。*****************
数据区:
00406030  33323130
00406034  37363034
00406038  05003835
*********函数返回**********

0040119B                       8A041E          mov al,byte ptr ds:[esi+ebx]             ; 再取用户名同一位字符c--EAX
0040119E                       6A 00           push 0
004011A0                       83E0 03         and eax,3                             ; EaX与3(00000011)相与,得到第0、1位,本例是3
004011A3                       50              push eax
004011A4                       E8 87000000     call crackme.00401230                    ;
F7进入函数
0040123F                      /FF2485 54134000 jmp dword ptr ds:[eax*4+401354]          ; crackme.00401304
ds:[00401360]=00401304 (crackme.00401304)
00401304                       0FBE0D 3B604000 movsx ecx,byte ptr ds:[40603B]           ; ds:[0040603B]=05,ecx=00000005
0040130B                       8BC1            mov eax,ecx                          ; EAX=ECX=5=[0040603B]
0040130D                       BE 03000000     mov esi,3
00401312                       99              cdq                                   ; 把EAX扩展成EDX:EAX变,EDX变成0
00401313                       F7FE            idiv esi                               ; EDX:EDX/3,余数在EDX=2,商在EAX=1
00401315                       85D2            test edx,edx                            ; 余数EDX是否为0,本例为2

如果余数为0,则走:
00401346                       8B4424 10       mov eax,dword ptr ss:[esp+10]
0040134A                       85C0            test eax,eax
004012EF                       B8 01000000     mov eax,1                                ;
004012F4                       5E              pop esi
004012F5                       5B              pop ebx
004012F6                       C3              retn
未对数据区做任何修改就返回1

本例余数为2,则走:
00401319                       0FBE81 30604000 movsx eax,byte ptr ds:[ecx+406030]       ; ds:[00406035]=30 ('0')
00401320                       8A91 31604000   mov dl,byte ptr ds:[ecx+406031]          ; ds:[00406036]=36 ('6')
00401326                       5E              pop esi
00401327                       8891 30604000   mov byte ptr ds:[ecx+406030],dl          ; ds:[00406035]=36 ('6')
0040132D                       8881 31604000   mov byte ptr ds:[ecx+406031],al          ; ds:[00406035]=30 ('5')
00401333                       A0 3B604000     mov al,byte ptr ds:[40603B]              ; EAX=05
00401338                       5B              pop ebx
00401339                       FEC0            inc al                                   ; EAX=6
0040133B                       A2 3B604000     mov byte ptr ds:[40603B],al              ; [0040603B]=06
00401340                       B8 01000000     mov eax,1
00401345                       C3              retn

*************总结:根据参数的值3,跳转到相应的处理代码,本例中的处理是:将[40603B]中的值除以3,判断余数是否等于0,如果等于0,则不对数据区做任何修改返回1,如果余数不等于0,则将基于内存406030和406031,偏移为[40603B]中的值的两个内存单元值互换,并将[40603B]中的值加1。******************
数据区:
00406030  33323130
00406034  37303634
00406038  06003835

*************函数返回*************
004011A9                       83C4 20         add esp,20                          ; esp=0012FA0C
004011AC                       46              inc esi                             ; 循环次数+1
004011AD                       3BF5            cmp esi,ebp                        ; 循环次数是否达到NAME长度
004011AF                     ^ 7C B9           jl short crackme.0040116A     ; 若未达到NAME长度,继续循环。这个循环不断修改00406030-0040603B数据区的数据

进行下一位用户名字符的验证。。。。。。运算完毕,数据区为:
00406030  33323130
00406034  37383634
00406038  09003035

至此,对NAME的运算完毕,接着就是对SERIAL的检验运算。

004011BB                      6A 01           push 1          ; 使得0012FA28为1,表示处于SERIAL验证运算状态
004011BD                      0FBE140E        movsx edx,byte ptr ds:[esi+ecx]     ; 取2到,EDX第一位SERIAL
004011C1                      81E2 03000080   and edx,80000003                    ; 32(ASCII)与80000003与,EDX=2。得到的结果只能是4种:0,1,2,3,也就是得到输入字符的最低两位,本例为数字2
004011CE                      52              push edx                            ; 将运算后的结果压栈,做为函数参数
004011CF                      E8 5C000000     call crackme.00401230               ;  密码验证函数
非常奇怪,为什么跟检查NAME的函数一致?这个检验NAME和SERIAL算法的精要之处究竟在哪里?先跟下去再说。
按F7进入函数

00401230                       8B4424 04       mov eax,dword ptr ss:[esp+4]        ; 取参数,就是刚才压栈的2---EAX
00401235                       83F8 03         cmp eax,3                       ; 值是否大于3,不可能为3,为3就出错
0040123F                        /FF2485 54134000 jmp dword ptr ds:[eax*4+401354]     ; crackme.004012BA
ds:[0040135C]=004012BA (crackme.004012BA)
004012BA                       0FBE0D 3B604000 movsx ecx,byte ptr ds:[40603B]      ; ds:[40603B] 存储09
004012C1                       8BC1            mov eax,ecx                       ; EAX得到09
004012C3                       BE 03000000     mov esi,3
004012C9                       F7FE            idiv esi             ; 把EDX:EAX 除以ESI--3,余数放在EDX,商在EAX
                                                                   ; EAX=3,EDX=0
004012CB                       83FA 01         cmp edx,1           ; 余数是否等于1,本例不等于1
EDX不等于1,则走下面的流程
004012D0                       0FBE81 30604000 movsx eax,byte ptr ds:[ecx+406030]  ; EAX=ECX=09,ds:[00406039]=30 ('0'),406030是数据区初始地址,[40603B]是最后一个地址
004012D7                       8A91 2F604000   mov dl,byte ptr ds:[ecx+40602F]     ; ds:[00406038]=35 ('5'),EDX=35
004012DD                       8891 30604000   mov byte ptr ds:[ecx+406030],dl     ; ds:[00406039]=35 ('5')
004012E3                       8881 2F604000   mov byte ptr ds:[ecx+40602F],al     ; ds:[00406038]=30 ('0'),38和39两个内存空间的值交换
004012E9                       FE0D 3B604000   dec byte ptr ds:[40603B]            ; [40603B]原先的09变成08
004012EF                       B8 01000000     mov eax,1                        ; EAX=1,则返回值正确,下一个循环可以继续
004012F4                       5E              pop esi
004012F5                       5B              pop ebx
004012F6                       C3              retn
如果余数EDX=1,则走下面的流程
004012F7                       8B4424 10       mov eax,dword ptr ss:[esp+10]       ; 将0012FA28中的内容存EAX=1
004012FB                       85C0            test eax,eax                      ; 检测EAX是否等于0,在SERIAL验证状态,EAX是等于1的
004012FF                       5E              pop esi
00401300                       33C0            xor eax,eax                      ; EAX返回值为0,SERIAL验证出错
00401302                       5B              pop ebx
00401303                       C3              retn

**********总结:根据参数的值2,跳转到相应的处理代码,本例中的处理是将[40603B]中的值除以3,根据余数是否等于1做处理,若等于1,则SERIAL验证出错,不等于1,则根据[40603B]中的值做为偏移量,将数据区406030和40602F的连续两个内存地址的值互换,并将[40603B]中的值减1。************

数据区:
00406030  33323130
00406034  37383634
00406038  08003530
*****************函数返回****************
难道只要每位SERIAL的验证,通过了这个函数,只要返回1之后,验证就可以通过?回答不了,还是先继续下一位的SERIAL的验证吧!

检测第二位SERIAL,EDX=0
004011CF                      E8 5C000000     call crackme.00401230        ;将密码逐一验证,任何一位有问题返回错误
00401235                       83F8 03         cmp eax,3                   ;
00401246                    8A0D 3B604000     mov cl,byte ptr ds:[40603B]              ; ds:[0040603B]=08 (Backspace)
cl=5C ('\')
0040124C                       80F9 03         cmp cl,3   ;0040603B中的值是否小于等于3

如果小于等于3,则走下面的流程:
004012F7                     8B4424 10       mov eax,dword ptr ss:[esp+10]          ; 将0012FA28中的内容存EAX=1
004012FB                     85C0           test eax,eax
004012FD                     ^ 74 F0           je short crackme.004012EF            ; 检查EAX是否为0?

如果EAX=0,则走:
004012FF                       5E              pop esi
00401300                       33C0            xor eax,eax                              ; EAX清零,出错
00401302                       5B              pop ebx

如果EAX不为0,也就是0012FA28中的值为1,则走:
004012EF                       B8 01000000     mov eax,1                                ; EAX为1,则密码验证正确
004012F4                       5E              pop esi
004012F5                       5B              pop ebx
004012F6                       C3              retn

本例中,CL=8,则走:
00401255                       0FBEC1          movsx eax,cl                             ; EAX=08,就是[0040603B]中的值
00401258                       80C1 FD         add cl,0FD                               ; 08+FD(253)=05-存CL
0040125C                       0FBE90 30604000 movsx edx,byte ptr ds:[eax+406030]       ; ds:[00406038]=30 ('0')---EAX是一个基于406030偏移量,每次循环值不同
00401263                       8A98 2D604000   mov bl,byte ptr ds:[eax+40602D]          ; ds:[00406035]=36 ('6')
00401269                       880D 3B604000   mov byte ptr ds:[40603B],cl              ; ds:[0040603B]=08--->05,本次是将05存入,上次是减一操作
0040126F                       8898 30604000   mov byte ptr ds:[eax+406030],bl          ; ds:[00406038]=30 ('0')--->36('6')
00401275                   8890 2D604000   mov byte ptr ds:[eax+40602D],dl          ; ds:[00406035]=36 ('6')--->30('0')---38和35两个内存空间交换
0040127B                       B8 01000000     mov eax,1                ;返回值为1,可以进行下一个SERIAL的验证
00401280                       5B              pop ebx
00401281                       C3              retn

----------------------验证第三位SERIAL,为2,ASCii为32,与0X80000003相与后,得到数字2。
00401235                       83F8 03         cmp eax,3                                ; 参数的值是否大于3
跳转到:
004012BA                       0FBE0D 3B604000 movsx ecx,byte ptr ds:[40603B]           ; ds:[40603B]存储05存ECX
004012C1                       8BC1            mov eax,ecx                              ; EAX得到05
004012C3                       BE 03000000     mov esi,3
004012C8                       99              cdq                       ; 把EAX扩展成EDX:EAX,EDX变成0
004012C9                       F7FE            idiv esi                    ; 把EDX:EAX 除以ESI,余数放在EDX,商在EAX,EAX=1,EDX=2
004012CB                       83FA 01         cmp edx,1                                ; 余数是否等于1
如果等于1:
004012F7                       8B4424 10       mov eax,dword ptr ss:[esp+10]          ; 将0012FA28中的内容存EAX=1
004012FB                       85C0            test eax,eax                         ; 是否为0
等于0,则返回值为0,出错。不等于0,则:
004012EF                       B8 01000000     mov eax,1                        ; 返回值为1,可以继续下一位验证
004012F4                       5E              pop esi
004012F5                       5B              pop ebx
004012F6                       C3              retn
在本例中,余数等于2,则走:
004012D0                       0FBE81 30604000 movsx eax,byte ptr ds:[ecx+406030]       ; ECX=05,ds:[00406035]=30 ('0')
004012D7                       8A91 2F604000   mov dl,byte ptr ds:[ecx+40602F]          ; ds:[00406034]=34 ('4'),EDX=34
004012DD                       8891 30604000   mov byte ptr ds:[ecx+406030],dl          ; ds:[00406035]=34 ('4')
004012E3                       8881 2F604000   mov byte ptr ds:[ecx+40602F],al          ; ds:[00406034]=30 ('0')   34和35两个内存交换
004012E9                       FE0D 3B604000   dec byte ptr ds:[40603B]                 ; 原先的05变成04
004012EF                       B8 01000000     mov eax,1                ; 返回值为1,可以继续下一个SERIAL的验证
004012F4                       5E              pop esi
004012F5                       5B              pop ebx
004012F6                       C3              retn

-----------第四位SERIAL的验证-----------
00401235                       83F8 03         cmp eax,3                             ; 参数的值是否大于3,本例为0
00401246                       8A0D 3B604000   mov cl,byte ptr ds:[40603B]              ; [40603B]=4
0040124C                       80F9 03         cmp cl,3                              ; 0040603B中的值是否大于3,本例是大于

如果小于等于3,则:
004012F7                       8B4424 10       mov eax,dword ptr ss:[esp+10]            ; 将0012FA28中的内容存EAX
004012FB                       85C0            test eax,eax
004012FD                     ^ 74 F0           je short crackme.004012EF

004012EF                       B8 01000000     mov eax,1                                ;
004012F4                       5E              pop esi
004012F5                       5B              pop ebx
004012F6                       C3              retn
返回值为1,可以继续下一位密码验证。本例中CL=4,则:
00401255                       0FBEC1          movsx eax,cl                             ; EAX=04
00401258                       80C1 FD         add cl,0FD                               ; 08+FD(253)---CL=01
0040125C                       0FBE90 30604000 movsx edx,byte ptr ds:[eax+406030]       ;
00401263                       8A98 2D604000   mov bl,byte ptr ds:[eax+40602D]          ;
00401269                       880D 3B604000   mov byte ptr ds:[40603B],cl              ; 0040603B变成01
0040126F                       8898 30604000   mov byte ptr ds:[eax+406030],bl          ;
00401275                       8890 2D604000   mov byte ptr ds:[eax+40602D],dl        ; 34和30两个地址内容互换,[34]=31('1') [30]=30('0')
0040127B                       B8 01000000     mov eax,1                                ; 返回值为1

----------第五位SERIAL的验证-------
00401235                       83F8 03         cmp eax,3                             ; 参数的值是否大于3,本例为0
00401246                       8A0D 3B604000   mov cl,byte ptr ds:[40603B]              ; [40603B]里为01
0040124C                       80F9 03         cmp cl,3                               ; 0040603B中的值是否小于等于3,本例小于

004012F7                       8B4424 10       mov eax,dword ptr ss:[esp+10]           ; 将0012FA28中的内容存EAX=1
004012FB                       85C0            test eax,eax                             ; 检测EAX是否为0

本例为1,则:
004012FF                       5E              pop esi
00401300                       33C0            xor eax,eax                              ; EAX=0,出错了
00401302                       5B              pop ebx
00401303                       C3              retn

在验证到第五位SERIAL的时候,出错了。到目前为止,只看出检测SERIAL的时候,保证EAX返回值为1,则就可以保证可以继续检测下一位SERIAL了,但没想明白为什么在验证到第五位SERIAL的时候,出错了?还有,检测NAME和SERIAL的函数为同一个,运算过程就是依据一些条件,将00406030到00406039中的一些值两两交换,并将0040603B的值进行加1、减1、加3,减3运算,但这里头包含了哪些奥妙呢?思维陷入了停顿,虽然每行代码已经看懂,但不明白算法的玄机,陷入停顿……

后来跳出了检测SERIAL的循环,找到了下面的代码,明白了为什么将NAME和SERIAL放在同一个函数下检测的原因。

004011E0                       B8 01000000     mov eax,1
004011E5                       8A88 30604000   mov cl,byte ptr ds:[eax+406030]
004011EB                       8A90 2F604000   mov dl,byte ptr ds:[eax+40602F]
004011F1                       3ACA            cmp cl,dl                  ; 后一内存中的值是否比前一内存中的值小
004011F3                       7C 1F           jl short crackme.00401214                ; 小的话,就出错
004011F5                       40              inc eax                                ; EAX加1
004011F6                       83F8 09         cmp eax,9                           ; 小的话继续循环,循环到EAX=8,最后比较到38与37两个地址
004011F9                     ^ 7C EA           jl short crackme.004011E5            ; 这个循环检查说明,SERIAL每一位验证完后,数据区的排列顺序必须与初始排列一样
004011FB                       6A 00           push 0
004011FD                       68 5C604000     push crackme.0040605C                    ; good
00401202                       68 50604000     push crackme.00406050                    ; well done.

至此,终于明白为什么要将NAME和SERIAL放在同一函数下进行验证,对NAME的运算,是将00406030到00406039中的内存值排列顺序按照一定的规则打乱,对SERIAL的运算,是将打乱的顺序,按照一定的规则恢复成初始排列。

所以,要想通过SERIAL的检测,问题就变成:如果根据NAME,构造一个SERIAL,将打乱的顺序恢复?要想解决这个问题,还是得对00401230这个函数的算法进行研究。对算法重新整理下:

1、        假设参数为0,如果[0040603B]中的值<=3,则不做处理,直接返回,否则,就进行内存交换:
[00406030]+[0040603B]----------- [0040602D]+[0040603B]
[0040603B]= [0040603B]+0XFD    等同于-3

2、        假设参数为1,如果[0040603B]中的值>=7,则不做处理,直接返回,否则,就进行内存交换:
[00406030]+[0040603B]----------- [00406033]+[0040603B]
[0040603B]= [0040603B]+3

3、        假设参数为2,如果[0040603B]中的值被3除,余数等于1,则不做处理,直接返回,否则,就进行内存交换:
[00406030]+[0040603B]----------- [0040602F]+[0040603B]
[0040603B]= [0040603B]-1

4、        假设参数为3,如果[0040603B]中的值被3除,余数等于0,则不做处理,直接返回,否则,就进行内存交换:
[00406030]+[0040603B]----------- [00406031]+[0040603B]
[0040603B]= [0040603B]+1

把算法整理成这样的形式,基本上就很清晰了。
假设第一次参数为0(或1),那第二次参数为1(或0),就可以将打乱的次序恢复;假设第一次参数为2(或3),那第二次参数为3(或2)就可以恢复顺序。具体看下例子,有兴趣自己可以在纸上比划下。

1、        假设第一次参数为0,则进行交换的是[00406039]和[00406036]两个内存,[0040603B]变成6,第二次参数为1,则进行交换的内存为[00406036] [00406039],[0040603B]变成9;
2、        假设第一次参数为2,则进行交换的是[00406039]和[00406038],[0040603B]变成8,第二次参数为3,则进行交换的是[00406038]和[00406039],[0040603B]变成9;
3、        假设第一次参数为1,则不进行交换直接返回,同理,也不需要恢复顺序;
4、        假设第一次参数为3,则不进行交换直接返回,同理,也不需要恢复顺序。
5、        假设第一次参数为0,第二次参数为2,先看内存顺序的变化:
[00406039]------ [00406036], [0040603B]=6
[00406036]------ [00406035], [0040603B]=5
要想恢复顺序,必须按照相反相对称的路径进行恢复,即第三次参数为3,第四次参数为1,具体看下演算:
[00406035]------ [00406036], [0040603B]=6
[00406036]------ [00406039], [0040603B]=9
顺序及[0040603B]的值都得到恢复。

相对称的含义见示意图:


6、        假设第一次参数为2,第二次参数为0,那么第三次参数为1,第四次参数为3,可以将顺序恢复。
7、        再复杂点,回到刚才我们的NAME的第一个字符c,ASCII为63,那么参数序列为1、2、0、3,则对应的数据区的变化为:
参数为1,不做处理,直接返回;
参数为2:[00406039]------ [00406038],[0040603B]=8
参数为0:[00406038]------ [00406035],[0040603B]=5
参数为3:[00406035]------ [00406036],[0040603B]=6

如果接下来的参数序列为2、1、3,则可以恢复顺序,验证:
参数为2:[00406036]------ [00406035],[0040603B]=5
参数为1:[00406035]------ [00406038],[0040603B]=8
参数为3:[00406038]------ [00406039],[0040603B]=9

8、        分析了这么多了,我们输入的NAME:chglyq,就不难推导出SERIAL了
Chglyq对应的ASCII码为:63 68 67 6C 79 71(十六进制),因算法中是对每位字符,每次取2个BIT,作为参数,输入到函数中,因此,我们将ASCII序列拆解下,变成一个参数序列为:
1203 1220 1213 1230 1321 1301,参数顺序见下面的箭头
<----------------------------------------
第一步,将那些不用处理而直接返回的参数找出来过滤掉,通过OD追踪可以记录下来。
203 1220 1  3  230 132  301
根据0和1,2和3可以两两抵消的原则,再次过滤参数序列
203 122         3      3,位数为8位,可以保留至6位(函数验证需保证SERIAL为6位),再次过滤:
203 12                 3,再根据相互抵消原则,可以得到一个序列为:
31203                  2,按照对称原则,SERIAL序列为:230213
因在检测SERIAL的过程中,对每个SERIAL的每个位与3相与,见指令:
004011C1     81E2 03000080   and edx,80000003     ; KEY每一位与80000003与,得到的结果是4种:0,1,2,3
因此,SERIAL只需要用0、1、2、3四个数字组合就可以了,当然也说明了,符合条件的SERIAL序列不只一个。

SERIAL的推导过程就是这样,写一个象注册机的程序,就不是太难了。谁有兴趣帮写下上传?


[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

上传的附件:
收藏
免费 7
支持
分享
最新回复 (4)
雪    币: 555
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
不错,lz真有耐心~~~
2010-8-31 12:11
0
雪    币: 338
活跃值: (103)
能力值: ( LV7,RANK:110 )
在线值:
发帖
回帖
粉丝
3
不错 支持
2010-8-31 12:53
0
雪    币: 5
活跃值: (374)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
膜拜。。。标签之    极好的耐心
2010-8-31 12:59
0
雪    币: 114
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
看雪第一贴。希望早日达到LZ水平
2010-8-31 22:30
0
游客
登录 | 注册 方可回帖
返回
//