题外话:二个多月前,一个朋友叫我修改一个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期)