网友在贴子上说AotuRun的注册码引起了他的困惑,我出于好奇下载并打开了在Round1中的bac.bin。在OD(可直接打开)监控下,我随便输入注册码,当弹出“错误注册码”框后,在堆栈栏中出现了字串“Jzsvvi1lkFid”,果然它也是一个正确的注册码。我当时认为这个程序只不过是小菜一碟,网友说真正的注册码是“3u8A-5DKU1-JDAf-3vvE15-KlsHW2”,并且说用不同的方式打开后,画面不同(我用不同的码打开后,看不出有什么不同。由于界面没有任何操作按钮,程序不能运行,我估计是网络游戏)。接着我分析了它的比较过程,发现它先用明码比较“Jzsvvi1lkFid”,如果不对,再将“3u8A-5DKU1-JDAf-3vvE15-KlsHW2”通过冗长的计算来比较。我只是猜想“Jzsvvi1lkFid”是方便作者对程序的调试,不对外,或者本身是个诱饵,使你进入一个功能不全的界面,忽略对真正注册码的研究?当时我认为这个问题已完美解决了,但当我试图写一个注册机来验证它的注册方法时,才发现该程序写得十分诡秘,远比想象的复杂得多。
它的诡秘大约在这几个方面:
1.程序所有的窗口都由CreateDialogIndirectParamA函数创建,包括出错警告框。它们共用一个回调过程,所有的消息和操作都通过该通道。想拦截到注册框,非常困难。
2.明码比较“Jzsvvi1lkFid”,或许是个诱饵?
3.注册码共25个字符,它的前18个字符由软件作者安排,第19个字符由Jzs-vvi-1lk-Fid计算值与3u8A-5DKU1-JDAf-3vvE1的计算值的商再除以3D的余数来定,最后6个字符由前19个字符逻辑运算产生。
4.当你的注册码符合第3条后,然后再四分组检验前18个字符的计算值,如果不符合它暗藏的条件并不报错,但程序立即崩溃。使你对崩溃的原因一头雾水。
由于要说清楚这些问题的来龙去脉不是一两句话的事,若你不感兴趣就此打住;否则就要稍有点耐心。下面分条细说:
一、 截注册框,查找“确定”按钮的入口地址是破解注册码的关键:
1.从查找注册窗口回调函数入口入手:
打开SoftICE和Spy&Cap,用后者找出注册框句柄,再用SoftICE找出注册框的回调函数入口是00443860。进入该入口,什么回调程序呀?竟然没有消息处理分支结构,无论怎样设断和跟踪都不会出现明显与注册框操作有关的代码,甚至一动鼠标程序就停在没断处,除非取消设断。单步跟踪也没有任何结果,累到你主动放弃跟踪。总之,它不是你熟习的窗口回调函数。这一招是失败了!
2.在OD中查找所有的分支,寻找111(WM_COMMAND)消息入手:
窗口的所有操作都必须有windows消息中的WM_COMMAND指令,该指令值为111。在OD中找到包含111的case (有多个窗口一般就有多个111,但本程序共用一个通道,就只有一个111。屏幕上表现为打开一个新窗口,就自动关闭原来的窗口),双击来到下列代码:
0047CC8B 81FB 11010000..cmp ebx, 111 ; Switch (cases 6..111)
0047CC91 56………………push esi
0047CC92 57………………push edi
0047CC93 8BF9……………mov edi, ecx
0047CC95 75 1B……………jnz short 0047CCB2
0047CC97 FF75 10…………push dword ptr [ebp+10] ; Case 111 (WM_COMMAND) of switch
0047CC9A 8B07……………mov eax, [edi]
0047CC9C FF75 0C…………push dword ptr [ebp+C]
0047CC9F FF90 E8000000…call [eax+E8]
注意0047CC95处转跳,当ebx=111时进入0047CC97,这就是窗口WM_COMMAND指令的入口。但问题远没有解决,所有的窗口(动作)操作都由它进入,还必须找出你需要的按钮操作。
3.在消息WM_COMMAND携带的参数wParam中找出“确定”按钮的ID值:
“确定”按钮的默认值ID=1,但必须通过Spy&Cap软件去验证一下。现在必需在0047CC97处将代码分流出来,当[ebp+c]=1时将它拦截下来。注意,常规情况下,消息传递中[ebp+8]是hwnd,[ebp+c]是uMsg,[ebp+10]是wParam。当然软件作者也可以任意安排。它隐藏了hwnd,将[ebp+8]=uMsg,[ebp+c]=wParam。这需要在0047CC9F处设断从OD的堆栈中得到确认(从数值上去判断,[bp+4]肯定是返回地址)。当这些都准备好后,对程序作如下手术:
改动0047CC97处代码为:
0047CC97 E9 AFDA0000 jmp 0048A74B ;转跳到空白处
在0048A74B处(空白地址)添加下列代码,并在nop处设断:
0048A74B FF75 10……….push dword ptr [ebp+10]
0048A74E 8B07……………mov eax, [edi]
0048A750 53………………push ebx
0048A751 8B5D 0C……….mov ebx, [ebp+C]
0048A754 83FB 01……….cmp ebx, 1
0048A757 75 01……………jnz short 0048A75A
0048A759 90………………nop ;(设断)
0048A75A 5B………………pop ebx
0048A75B E9 3C25FFFF….jmp 0047CC9C
当我们在0048A759处设断后,运行,输入假注册码,按下“确定”,立即被断了下来。然后跟踪,一律不跟进call,注意观察状态拦,看有无假注册码出现。一路小跑什么码也没出现,“嘭”的一声弹出密码错误警告框,按“确定”后程序就退出了。打开程序再试,居然还是没有发现是从什么位置弹出的警告框,第一次较量就这样败了下来!
二、跟踪技巧:
在windows自带的1000多条消息中,找到了携带“确定”按钮ID的WM_COMMAND,已经是很不容易了,接下来还要做深入细致的跟踪工作(0048A759断点不能取消)。
1.在所有的MessageBox函数处设断,创建窗口的CreateDialogIndirectParam函数处设断:
在MessageBox设断,是企图寻找什么时候调用它们的。设断后再试,在第二个函数第一次中断后没有出现注册框,继续,弹出了注册框,输入假码后第二次在CreateDialogIndirectParam中断,用F8跟踪一步就中断在MessageBox处,往回走发现它完全是个无厘头。它是一个独立的函数过程,根本不知道是从什么地址进入的!(没想到小小程序这么猾头)显然单步跟踪CreateDialogIndirectParam是惟一希望了。
单步进入CreateDialogIndirectParam函数(它是USER32中的一个函数)后,好象万里长征,对里面的call一个也不要跟进,一旦弹出出错框,立即右键打开OD弹出式菜单,选“转到——上个”,记下该call地址设断,下次跟进。在这个函数中的断点是:77D24E23,下一次跟踪时一定要跟进。
2.在77D24E23进入后,还是使用对call不跟进的方针,走了无数步的后稀里糊涂一下就转跳到了00404BC7,居然回到了主程序(在XP的很多*.dll中,都是用int 20中断弹回到主程序的,int 20是什么机制我不知道)。现在离目标近了。按继定方针,走到了00404CC2漏掉了,下次跟进,进入call 403E60后,才走几步就在状态拦中出现了输入的假注册码,注册码比较过程就在眼前了。注册过程藏得这么深,在众多软件中还是比较少见的。
3.进入call 403E60后,还是不跟进call,但要密切注意状态拦和堆栈。一路走下去,发现一连串的call 4013F0,每跳过一次堆栈中就出现一段注册码,原来注册码是分段存放在内存不同的地址的,需要时才一段段的将它取出(大家都知道搜索功能,他不防范你么?该软件内存中也根本没有比较码Jzsvvi1lkFid,那怕是它的一段)。接着是一连串的call 4026B0,每过一次,注册码就连接一段。当堆栈中出现了连接好了的注册码和比较码后,要警惕了!比较过程就在眼前,刚跳过call 469860,eax就返回了FFFFFFFF,这是比较不能通过的信号,call 469860是比较过程无疑了。真是走过了万水千山,终于逮住了这个小猾头。搞软件是苦职业,搞破解更是苦差事!幸好我两样都不沾。
三、注册算法原理:
1.00469888地址是明码比较过程,比较过不了就进入00405985,然后将比较字串和注册字串都转换成16进制数,显然它的算法原理就在00405985里面。然后在00404409: call 469E40处理两个计算结果,它将两数相除,再将商除以3D取余数,将余数转换成字符与注册码的第19位比较,不同就报错,然后将正确的前19个字符,进入00405903用另一种算法得到8位整数,通过call 405A10得到最后6个字符,将这6个字符与注册码的最后6个比较,正确后算是初步通过。(算法原理后面给出)
2.到此,我以为写个注册机也不困难了,还不知道程序后面暗藏杀机!试着按它的原理,手动的编出了一个25字符的注册码,输入后试验,没有报错,但程序一下子就崩溃了。弹出了一个C++系统的警告窗口,大意是程序执行了非法操作,强令退出,请与开发商联系。啊~啊!什么非法操作?分明是作者在402F14:call 46997B返回后设置的一个int 3死亡陷阱!
原来后面还有很长的计算和慢长的检验!它将初步过关的注册码前18个字符按4、5、4、5个分成四组,每组的计算值都被2710除,商和余数都必须小于19A1,但它不警告你,不管你的商和余数是多少都进入后面的校验。不满足商和余数条件的打入死亡陷阱,满足的则将商和余数查表(一个庞大的表)转换成6~8位的一个大数,然后通过循环运算归零(不归零怎样,没实验)。每一组都通过后,对注册码的检验才算完成!
啊~,一个不大程序的注册方法看起来都累,还不知注册保护今后如何发展?看来不累死几个破解者在计算机桌前,誓不罢休!(嘻!谁叫你没事专搞破坏?)
四、构造注册机的几个必要模块:
1.字符与数值的互换:
原程序的转换思路是查表。将字符的ASCII码减去30后,将其值乘4作为转跳距离,跳到指定位置后取该位置的值作为转换值返回。由于反过来将数值转换成字符时,该项表不能用,又搞了一个反查值表,整个程序显得臃肿(00404F80)。完全可以把字符排序,将序列号作为转换值返回,其程序结构将大为简化。
下面给出用汇编实现上述功能的模块。模块1是把字符串整体转换成数值串,目的是为后面的自动选择适合条件的字符运算简化了程序。
.const
szlist db '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-',0
.code
;(模块1)--------------------------------------------------
;字串转换成数串函数:(参数:_字(数)串地址,_串长度)
StringToNumber proc _esi,_lench
…………pushad
…………mov esi,_esi ;字串和数串的共用地址
…………xor ebx,ebx
………….while ebx < _lench
………………mov al,byte ptr [esi+ebx]
………………mov ecx,3fh
………………lea edi,szlist ;取转换表地址,即开始的那个排序表
………………repnz scasb ;注意,repnz返回的ecx自动减1,要从差值上调整
……………….if ecx = = 0 ;表中没有的字符或“-”,转换值为0
…………………………jz @F
……………….endif
………………sub ecx,3eh
………………neg ecx
……@@:...mov byte ptr [esi+ebx],cl
………………inc ebx
………….endw
…………popad
…………ret
StringToNumber endp
;(模块2)-------------------------------------------------------
;单个数值转换成单个字符函数:(无参数,al代入数值,al返回字符)
NumberToString proc uses esi
…………movzx eax,al
…………lea esi,szlist ;还是那个排序表,通用
…………mov al,byte ptr [esi+eax]
…………ret
NumberToString endp
2.字符串转换成大整数:(模块3)
严格的说,是数串转换成大整数,因为字符串经模块1已经变成了数值串。它的算法原理是:
N=x1*3D^0+x2*3D^1+x3*3D^3+……+xn*3D^n
其中,x1,x2等是转换后的数值。原程序写的算法过程还可以,只是转跳太多,读起来不方便,且有一个漏洞:即加数的进位被忽略了。好在是程序本身对结果的准确度要求不高,而且这种进位的几率太低。什么意思?所谓“准确度要求不高”是指运算超过了16位,高位会被舍去。相除取商,余数也被忽略。“几率太低”例如,167A38C4加上一个小于3D的数,在第8位上产生了进位,惟一的情况是前6位都是FFFFFFxx,好象永远都不会发生。但作为一个计算算法,还是加上恐怕才严谨些。下面给出一个源代码:
;----(模块3)-----------------------------------------------------
;数串转换成大整数函数:(参数:_结果地址(高位在前4字节),_数串地址,数串长度)
CharToNumb proc _IntH,_esi,_lench
…………pushad
…………xor ecx,ecx
…………mov edi,_IntH
…………mov [edi],ecx
…………mov [edi+4],ecx
…………mov ebx,3dh
…………mov esi,_esi
…………mov ecx,_lench
…………dec ecx
…………js @F ;数串长=0退出
………….while ecx != 0
………………mov al,byte ptr [esi+ecx]
………………movzx eax,al
………………add eax,[edi+4]
………………jae step
………………inc byte ptr [edi] ;若有进位,高位+1
…….step:.mul ebx
………………mov [edi+4],eax ;乘积结果低位
………………push edx
………………mov eax,[edi]
………………mul ebx
………………pop edx
………………add eax,edx
………………mov [edi],eax ;乘积结果高位
………………dec ecx
………….endw
…………mov al,byte ptr [esi]
…………movzx eax,al
…………add [edi+4],eax
…………jae @F
…………inc byte ptr [edi]
@@:…popad
…………ret
CharToNumb endp
3.检验码的生成:(模块4)
程序用注册码的前19个字符生成6个字符的检验码。原程序的这一段写得还可以,但转跳太多阅读容易看花眼。稍加改动后,少了两个转跳,代码行数不变,读起来清楚了许多(有兴趣的可以查看00405903处)。后半段是我添上去的,目的让字串一气呵成。顺便说几句,请不要评价C++与汇编谁个更优,各自发挥优势的地方不同。垃圾代码的多少,软件的大小、资源占有率、运行效率等在电脑硬件飞速发展的今天早已不是任何问题了。苦苦地追求程序的优化只是自找苦吃和自我陶醉!扯远了,回头来。生成检验码是一个逻辑运算,本来贴个图,逻辑流程一目了然,但我没有贴图权限,只有用嘴说了。将字符串排序, eax初值为0,(转入点)eax自加,若eax>=8000,则xor eax,1021,从最高位开始进行位测试,若该位为1,则xor eax,1021,否则测试下一位并回跳“转入点”。实现该逻辑运算代码如下:
;--(模块4)-----------------------------------------------------
;生成检验码函数:(参数:_生成码地址,_源字串地址,_字串长度)
TestChar proc _edi,_esi,_lench
…………pushad
…………mov edi,_lench
…………or edi,edi
…………jz _Off ;字串长为0退出
…………xor eax,eax
…………mov esi,_esi
step1:.mov dl,byte ptr [esi]
…………mov cl,80h
step2:.push eax
…………add eax,eax
…………pop ebx
…………test bh,bh
…………jns step3
…………xor eax,1021h
step3:.test cl,dl ;位测试
…………je step4
…………xor eax,1021h
step4:.shr cl,1
…………test cl,cl
…………jnz step2
…………inc esi
…………dec edi
…………jnz step1 ;以上是产生注册码前19个字符转换的数值
…………mov edi,_edi
…………mov ecx,3dh ;以下生成注册码的后6个字符
.repeat
…………xor edx,edx
…………div ecx
…………push eax
…………mov eax,edx
…………invoke NumberToString ;查字符表
…………mov byte ptr [edi],al
…………pop eax
…………inc edi
.until…eax = = 0
…………mov byte ptr [edi],al ;字串尾部添个0
_Off:.popad
…………ret
TestChar endp
4.自动调整注册码字符,使之适合苛刻条件:(模块5)
注册码前18个字符,按4、5、4、5分组,每组的计算值被2710除,商和余数都必需小于19A1。下列代码可以实现这一功能,在主程序模块中随机输入18个字符,先转换成数值串,再分别按4、5、4、5四次调用本模块,就会得到满足条件的前18个字符的数值串,最后整体还原为字符串。
;---------------------------------------------------------------------------------------------
;检验数值串是否符合要求并自动校正函数:(参数:_数值串地址,_数串长度)
TestNumber proc _esi,_lench
…………local @szIntHL[8]:byte
…………pushad
…………mov esi,_esi ;地址共用,返回值也用该地址
…………lea edi,@szIntHL
…………mov ecx,2710h
…………mov ebx,_lench
@@:...invoke CharToNumb,addr @szIntHL,esi,_lench
…………xor edx,edx
…………mov eax,[edi+4]
…………div ecx
………….if (eax >= 19a1h)||(edx >= 19a1h)
……………….if byte ptr [esi+ebx-1] = = 0
……………………dec ebx
……………….endif
………………dec byte ptr [esi+ebx-1]
………………jmp @B
………….endif
…………popad
…………ret
TestNumber endp
5.最后的组装:
注册机的所需功能函数全都有了,剩下只须组装即可。不过,我不再说了。再说下去就与本论坛的精神相悖了。只是被充一点,第19个字符由下列方法产生:A=第1组的计算值,B=第2组的计算值,C=第3组的计算值,D=第4组的计算值,E=Jzs-vvi-1lk-Fid(包含-号)的计算值。计算K=E/(A*B+C*D),K只取整数。K/3D取余数。该余数换成字符就是第19个字符。其实算K时,大整数只取高8位也可以。
只是一种学习探讨,不对的地方,特别是源代码,敬请指正!