Crack之初体验-第三课
作 者: 四波
时 间: 2011-12-11,05:48:03
链 接: http://bbs.pediy.com/showthread.php?t=144053
Crack之初体验系列--第一课==第二课
=========================================
书接上文,言归正传。第一课的时候我说过,第二课的终极目标是写出注册机,或者起码要整理出注册代码,用你所熟悉的编程语言表述出来。可是实际上我们在第二课中并没有完成这个任务。
上一课我们仅仅是“侥幸”在寄存器以及堆栈中得知了正确的注册码,有个细节不知道大家注意到没有,就是程序其实只取了我们输入进去的字符的前9个字符,不管你在输入框中输入多少东西,它都只取了前9个字符进去,呵呵,细心点就应该发现了。对于这个程序来说,也许取几个字符进去并不重要,但是以后也许我们会遇到这种比较重要的问题。
因为上次那个crackme是非常very很简单的,它只有一个输入框,我们输入注册码后程序直接拿进去与正确的注册码比较,这种比较方式我们称之为:明码比较!现在我们初学的时候都先学习这种明码比较的例子,等技术成熟了我们再学习暗码比较(明码比较的对立面是不是叫暗码比较??不清楚。。。)。
这节课我们将学习一个name-serial的例子,我个人比较倾向于不讲纯理论,读了一二十年的书了,每次一读到理论性的东西就想睡觉,每次一看到举了个例子或者故事就马上看看。不知道大家是不是跟我一个情况,反正我就是这样,似乎没救了。。。。
老套路,首先运行来看看。运行后什么也不输入直接点check it,会弹出一个“Wrong serial-try again”的对话框。点击确定返回后name输入框中会出现nameless的字符串。
再随便输入点什么看看,就输入name:iloveswu serial:1234567吧,再check下,还是弹出跟刚才一样的错误对话框。
第二步:查壳。PEID查出是:EXECryptor 1.x.x -> SoftComplete Developement,这个是什么壳,没见过。。。不过没事,我们没见过的东西多了,大爷不稀罕,是吧?
OK。我们先别忙着破解,先在程序里面晃悠几圈,看看有什么值得注意的地方,顺便看看里面有苍老师的作品没有。。。(这个可以有!可是这个还真没有!!!)
好,大家都转回来吧,别转晕了。我相信大家眼神再不好,就算是1000度近视,都应该看到程序门口那句赤裸裸的挑衅了吧?
代码:
00401045 |. 6A 30 PUSH 30 ; /Style = MB_OK|MB_ICONEXCLAMATION|MB_APPLMODAL 00401047 |. 68 00604000 PUSH crackme2.00406000 ; |crackme2 0040104C |. 68 7A604000 PUSH crackme2.0040607A ; |what the hell are you doing in my app with a debugger? 00401051 |. 6A 00 PUSH 0 ; |hOwner = NULL 00401053 |. E8 9C030000 CALL <JMP.&user32.MessageBoxA> ; \MessageBoxA 00401058 |. 6A 00 PUSH 0 ; /ExitCode = 0 0040105A \. E8 A7030000 CALL <JMP.&kernel32.ExitProcess> ; \ExitProcess
这不是赤裸裸的挑衅是什么?它怎么知道我在用debugger拿它做实验?
这还不是关键,关键在于,这是个死程序,不信大家一步一步F8试试,试到最好什么结果,是不是下面这样的?
诶,这个比较新奇哦,从来没见过啊。用OD载入后居然只运行一个具有十足挑衅味道的对话框就完事,这算什么啊?
好吧,我猜,这就是传说中的anti-debugger,所谓的反调试是也。
那怎么办?
在这能够运行的仅仅几句代码上,我们该怎么来想办法破解呢?
额,大家想想办法啊?
我觉得办法有几个:
1.放弃吧,我连壳是什么东西都不知道,连汇编代码都认不完,这个难度肯定不适合初学者。
2.找找看,有什么线索没有,或许万一说不定有可能瞎猫碰到死耗子呢?
如果要放弃,我们还写这个教程干嘛?
所以,永不放弃!
那好,我们来个比较死板的办法,先查下参考字符串吧。
右键->超级字串参考->查找ASCII(或者右键->查找->所有参考文本字串)
哈哈,里面居然能找“Wrong serial-try again“这串字符,太神奇了,还有nameless
当然,还有注册成功的语句:valid serial - now write a keygen!
好,我们双击”valid serial - now write a keygen!“所在行跟随来到反汇编窗口,找到了这几句关键的代码。
代码:
00401246 |. 68 B1604000 PUSH crackme2.004060B1 ; / nameless 0040124B |. 68 B6624000 PUSH crackme2.004062B6 ; |String1 = "" 00401250 |. E8 DB010000 CALL <JMP.&kernel32.lstrcmpA> ; \lstrcmpA这儿是比较函数 00401255 75 16 JNZ SHORT crackme2.0040126D ;这儿根据比较结果进行相应的跳转 00401257 |. 6A 00 PUSH 0 ; /Style = MB_OK|MB_APPLMODAL 00401259 |. 68 00604000 PUSH crackme2.00406000 ; |crackme2 0040125E |. 68 3D604000 PUSH crackme2.0040603D ; |valid serial - now write a keygen!这儿是验证成功时执行的代码,其实就是弹出一个对话框 00401263 |. FF75 08 PUSH DWORD PTR SS:[EBP+8] ; |hOwner 00401266 |. E8 89010000 CALL <JMP.&user32.MessageBoxA> ; \MessageBoxA 0040126B |. EB 14 JMP SHORT crackme2.00401281 0040126D |> 6A 10 PUSH 10 ; /Style = MB_OK|MB_ICONHAND|MB_APPLMODAL 0040126F |. 68 00604000 PUSH crackme2.00406000 ; |crackme2 00401274 |. 68 60604000 PUSH crackme2.00406060 ; |wrong serial - try again!这儿是验证失败 00401279 |. FF75 08 PUSH DWORD PTR SS:[EBP+8] ; |hOwner 0040127C |. E8 73010000 CALL <JMP.&user32.MessageBoxA> ; \MessageBoxA 00401281 |> 5B POP EBX 00401282 |. 5E POP ESI 00401283 |. 5F POP EDI 00401284 |. C9 LEAVE 00401285 \. C2 0400 RETN 4
所以,就算它有anti-debugger那又如何?就算我们不能在OD里动态调试又如何?我们不是一样找到了它的关键代码吗?并且如果你愿意暴力破解,现在不改代码,更待何时?就像第一节课我们讲得那样,直接nop掉那句跳转指令也行,或者将JNZ改为JZ(JE也一样)也行。爆破就那么简单。
但是爆破已经不能满足我们的欲望了(此时我欲火焚身ing......),我们来完全破解它。
但是因为有anti-debugger我们不能在OD里面动态调试程序,也就不可能像上节课那样在CALL上下断点,然后拦截到正确的注册码。那既然不能动态调试,我们就彻底搞清楚它是怎么计算注册码的,这样的话我们就不用在OD里运行它,只需要分析出算法,然后算出正确的注册码就行了啊!
好,说到做到!我们就来学学如何分析算法。
今天我们要讲一个非常重要的函数,名字叫GetDlgItemTextA,这个函数是干嘛的呢?以下文字从百度上copy过来的:
2.函数的返回值是其拷贝的字节数目。
通常来说,程序就是通过这个函数将我们输入的东西读入进去的。我们通过跟踪这个函数,继而发现它把读入进去的name和serial怎么样了,然后就能找到其算法了,此所谓顺藤摸瓜也!
那么,我们怎么知道什么时候这个函数被调用了呢?
OK,要找到什么时候这个函数被调用了还是很简单的。
首先,在反汇编窗口中点击右键->查找->当前模块中得名称(标签),然后在名称窗口中找到这个函数GetDlgItemTextA,找到后在这个函数所在行点击右键->查找输入函数参考,快捷键为Enter。
如下图所示:
在我们这个例子中,我们看到,有两处调用了这个函数,一处是004011CD,另一处是00401241。很明显这两处就是读入name和serial的时候。
不妨先来看看004011CD。在”参考位于“那个小窗口里双击第一行就来到了反汇编窗口中004011CD这儿。
代码:
004011C3 |. 68 84624000 |PUSH crackme2.00406284 ; |Buffer = crackme2.00406284 004011C8 |. 6A 01 |PUSH 1 ; |ControlID = 1 004011CA |. FF75 08 |PUSH DWORD PTR SS:[EBP+8] ; |hWnd 004011CD |. E8 16020000 |CALL <JMP.&user32.GetDlgItemTextA> ; \GetDlgItemTextA 004011D2 |. 64:8B15 18000>|MOV EDX,DWORD PTR FS:[18] 004011D9 |. 8B52 30 |MOV EDX,DWORD PTR DS:[EDX+30] 004011DC |. 0FB652 02 |MOVZX EDX,BYTE PTR DS:[EDX+2] 004011E0 |. 83F8 00 |CMP EAX,0 004011E3 |. 7F 11 |JG SHORT crackme2.004011F6
还有一个问题就是,这个函数把读入后的字符串放到哪里了?关于函数的基本知识就只能靠大家自己去学了,我在这里只是告诉你们,这个函数的第三个参数(还记得参数入栈的顺序是从后向前吗?),也就是这一行:
代码:
004011C3 |. 68 84624000 |PUSH crackme2.00406284 ; |Buffer = crackme2.00406284
函数返回之后有几个MOV语句,这都不是我们关心的,关键是大家看到后面那两句了吗?如下:
代码:
004011E0 |. 83F8 00 |CMP EAX,0 004011E3 |. 7F 11 |JG SHORT crackme2.004011F6
OK。我们关心的是,如果输入的字符串个数大于0,然后怎么样了,那我们就到004011F6去看看。
代码:
004011F6 |> \8D35 84624000 LEA ESI,DWORD PTR DS:[406284] ;将存放name的缓冲区地址传给ESI 004011FC |. 33C9 XOR ECX,ECX ;清空ECX 004011FE |> 0FBE06 /MOVSX EAX,BYTE PTR DS:[ESI] ;将name的第一个字符放进EAX中 循环体开始 00401201 |. 8BD8 |MOV EBX,EAX ;这中间这段就是对name的每个字符进行处理 00401203 |. 2BF2 |SUB ESI,EDX 00401205 |. C1E0 04 |SHL EAX,4 00401208 |. C1EB 05 |SHR EBX,5 0040120B |. 33C3 |XOR EAX,EBX 0040120D |. 83C0 26 |ADD EAX,26 00401210 |. 33C1 |XOR EAX,ECX 00401212 |. 03C8 |ADD ECX,EAX 00401214 |. 46 |INC ESI ;将指针移向name的下一个字符 00401215 |. 803E 00 |CMP BYTE PTR DS:[ESI],0 ;判断是否为空,是则说明name中得每个字符都处理过了,否则跳到开头继续处理下一个字符 00401218 |.^ 75 E4 \JNZ SHORT crackme2.004011FE ;循环体结束 0040121A |. B8 EF0D0C00 MOV EAX,0C0DEF 0040121F |. 2BC1 SUB EAX,ECX 00401221 |. 0FAFC0 IMUL EAX,EAX
这里就引用前辈的文字:
最后将CM2,A,C连解成注册码,一组可行的注册码:CM2-C-A
比如:name:iloveswu serial:CM2-4879-9072D264
虽然说现在我们知道了算法,但是当真要人工来算这个注册码还真比较难,所以注册机是必需的。但是我们今天好像讲得有点多了,注册机怎么写就不讲了。
以上算是比较完全地破解了这个crackme,并且我们也知道了它的算法。
但是假如我要你在一分钟内将它破解呢,而且不能改掉判断注册码正确与否的那几行关键代码?有没有办法?
办法其实是有的。那就是,想办法让OD能够运行它。
前面我们已经看到了,这个程序有一个anti-debugger,它在OD里面不会运行到任何有用的代码处,只会显示一个”what the hell are you doing in my app with a debugger?“的对话框。那怎么样才能让它运行它的主功能那些代码呢?JMP?还是CALL?
我们仔细看看,在OD里面能够运行的几个语句里面没有跳转指令,但是有CALL指令,我们 可以用它来作为跳转。但是解决这个问题还得解决另外一个问题,跳到哪儿?到底哪儿才是我们需要的程序的起始点呢?呵呵,大家来看看这个程序的代码:
代码:
00401000 > E8 24000000 CALL crackme2.00401029 ; (初始 cpu 选择) 00401005 |. 8B4C24 0C MOV ECX,DWORD PTR SS:[ESP+C] 00401009 |. C701 17000100 MOV DWORD PTR DS:[ECX],10017 0040100F |. C781 B8000000>MOV DWORD PTR DS:[ECX+B8],0 00401019 |. 31C0 XOR EAX,EAX 0040101B |. 8941 14 MOV DWORD PTR DS:[ECX+14],EAX 0040101E |. 8941 18 MOV DWORD PTR DS:[ECX+18],EAX 00401021 |. 806A 00 E8 SUB BYTE PTR DS:[EDX],0E8 00401025 |. 33C0 XOR EAX,EAX 00401027 |. 33DB XOR EBX,EBX 00401029 |$ 68 60104000 PUSH crackme2.00401060 ; SE 处理程序安装 0040102E |. 64:FF35 00000>PUSH DWORD PTR FS:[0] 00401035 |. 64:8925 00000>MOV DWORD PTR FS:[0],ESP 0040103C |. 9C PUSHFD 0040103D |. 813424 540100>XOR DWORD PTR SS:[ESP],154 00401044 |. 9D POPFD 00401045 |. 6A 30 PUSH 30 ; /Style = MB_OK|MB_ICONEXCLAMATION|MB_APPLMODAL 00401047 |. 68 00604000 PUSH crackme2.00406000 ; |crackme2 0040104C |. 68 7A604000 PUSH crackme2.0040607A ; |what the hell are you doing in my app with a debugger? 00401051 |. 6A 00 PUSH 0 ; |hOwner = NULL 00401053 |. E8 9C030000 CALL <JMP.&user32.MessageBoxA> ; \MessageBoxA 00401058 |. 6A 00 PUSH 0 ; /(初始 cpu 选择) 0040105A \. E8 A7030000 CALL <JMP.&kernel32.ExitProcess> ; \ExitProcess 0040105F . C3 RETN 00401060 /$ 64:8F05 00000>POP DWORD PTR FS:[0] ; 结构异常处理程序 00401067 |. 83C4 04 ADD ESP,4 0040106A |. 6A 00 PUSH 0 ; /pModule = NULL 0040106C |. E8 A1030000 CALL <JMP.&kernel32.GetModuleHandleA> ; \GetModuleHandleA 00401071 |. A3 35604000 MOV DWORD PTR DS:[406035],EAX 00401076 |. 6A 00 PUSH 0 ; /lParam = NULL 00401078 |. 68 95104000 PUSH crackme2.00401095 ; |DlgProc = crackme2.00401095 0040107D |. 6A 00 PUSH 0 ; |hOwner = NULL 0040107F |. 68 00604000 PUSH crackme2.00406000 ; |crackme2 00401084 |. FF35 35604000 PUSH DWORD PTR DS:[406035] ; |hInst = NULL 0040108A |. E8 4D030000 CALL <JMP.&user32.DialogBoxParamA> ; \DialogBoxParamA 0040108F |. 50 PUSH EAX ; /ExitCode 00401090 \. E8 71030000 CALL <JMP.&kernel32.ExitProcess> ; \ExitProcess
这个程序的第一条语句就是CALL,那我们就不客气了,直接将CALL后面的地址改成00401060,也就是说,程序一进来就会去执行00401060那段程序。这种想法是好的,但是这样改了这个程序还能工作吗?试试就知道。大家把第一句那个CALL改了以后存盘试一下,如何保存修改过的程序不用我说吧,如果想不起来了,翻翻第一课,里面就有。
保存好以后运行一下刚刚改过的程序,呵呵居然能运行,而且一切正常,那么,接下来,要破解它就容易了。
直接OD载入,用上节课讲到的方法来吧(还记得上节课我们是怎么得到注册码的吗?不记得了我们再温习一遍)
我们在lstrcmpA这个函数上下断点。即在
00401250 |. E8 DB010000 CALL <JMP.&kernel32.lstrcmpA> ; \lstrcmpA
这行上下断点。如下图:
然后F9运行,输入name:iloveswu serial:随便输点什么或者干脆不输入也行,然后点击check,回到OD,程序就被断下来了,此时,在反汇编窗口中和对战窗口中看到一串类似CM2-4879-9072D264的字符串了吧,这就是我们的注册码了。
这个方法我说了那么多,其实总结一下,确实只要一分钟就可以破解了。首先观察到程序可能的起始点,然后直接将CALL指向这个起始点,然后OD载入,下断点,运行,输入name和serial后点击check,程序被断下,看到注册码。
就这么简单,当然,这个属于爆破!
啊,我要崩溃了,童鞋们,你们知道现在是几点吗?告诉你们,北京时间5:40,我想睡觉了,累啊,从两点写到快六点,爱学习的人你们伤不起啊!有责任心的人你们伤不起啊!长得帅的人你们也伤不起啊!单身的人你们更伤不起啊!