网友在贴子上说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位也可以。
  只是一种学习探讨,不对的地方,特别是源代码,敬请指正!