• 标 题:中级实例教程—之疯狂单词v1.70 
  • 作 者:afanty
  • 时 间:2003/03/26 09:36pm
  • 链 接:http://bbs.pediy.com

前言:

俺学破解其实也只有不到半个月的时间,之所以敢到这里班门弄斧写“中级”教程,主要是发现各种教程大多在重复入门知识,广大菜鸟缺乏进一步的指导,抱着“俺不挨臭鸡蛋谁挨臭鸡蛋”的信念,希望能够给菜鸟同志们深入学习带来一点点启发,如此足矣。

一、研究对象简介:

《疯狂单词》是俺收集到的最好的背单词共享软件,我想任何想钻研技术的人都会发现具备读懂 e文资料的能力是相当重要的,而学好 e文必须过词汇关,这也是俺要拿《疯狂单词》开刀的初衷,目前最新的版本是v1.7x,用google 可以找到很多提供下载的地方,建议下载语音压缩版,20兆,当然如果带宽不是问题,110 兆的语音清晰版发音会更好一些。

《疯狂单词》需要重新启动程序来验证注册码,好像是从“utraledit”学来的,这种方式对“爆破一族”有一定的防范能力,因为程序不再局限于在“请输入注册码”和“恭喜注册成功”之间对注册码进行验证,它可以在任何地方对注册码进行突击检查,一旦发现注册码非法,随时取消已注册状态。所以即使爆破注册成功,爆破者总是无法判断程序中是否还有地方埋有不知道什么时候会引爆的“地雷”。

《疯狂单词》不是用 注册码 = f(用户名) 的方式将正确的注册码算出来,再与输入的注册码做比较,而是构造了一个隐函数算法,程序总是判断 F(注册码,用户名)的值是否合法,这样,正确的注册码永远不会在程序的计算过程中出现,对于掌握了在寄存器和内存中找注册码这种初级技术的菜鸟有很好的防范作用。

《疯狂单词》运用了一些反跟踪的手段,特别是针对 bpx +静态地址的断点和 bpm断点,比如你跟踪到了验证注册码的函数的地址:0040c660,下 bpx 0040c660, 会发现下一次中断时0040c660处的代码变成了“INVALID”; 再比如你跟踪到存放假码的地址:024ab100,下 bpm 024ab100, 会发现程序会用各种方法反复将该地址里的值转移到其他地址去,甚至会分批几个字节几个字节地偷运,或者写进注册表然后再读进另一个地址,等等。

二、总体破解思路:

不管程序如何验证注册码,在验证之前它总要知道你输入的假码是什么,所以在验证之前它必然会完成两个动作:

a) 从注册界面读入假码
b) 对假码进行预处理,可能只是简单的格式验证也可能是做一个变换:变码 = f(假码),然后将处理过后的假码或变码放到一个内存地址,注册码验证函数一定会访问这个地址。

所以跟踪假码就成为了找到验证函数的一把钥匙:

bpx getwindowtexta (或 bpx getdlgitemtexta) do "d esp->8"
中断后esp存放的是函数执行完毕后的返回地址,因为32位系统中每个地址占4个字节空间,所以此时esp->4是传递给函数的第 1个参数,也就是调用函数前最后一个压栈的参数的地址,同理esp->8是第二个参数,如此类推。函数getwindowtexta的第二个参数就是函数取得的字符串的存放地址,也就是我们要跟踪的假码的地址,d esp->8可以得到这一地址,这个地址不是固定的,假设为xxxxxxxx。

bpmd xxxxxxxx rw
密切关注程序对这一地址的任何读写操作,如果程序将该地址中的假码复制到另一地址,马上 bpmd 另一地址,如果发现程序调用regsetvalueexa函数将该地址中的假码写入注册表,记下调用之前倒数第二个压栈的值(第二个参数),假设为aaaa(dword,四字节),马上下 bpx regqueryvalueex if *(esp->8)==aaaa do "d esp->14",如果程序从注册表将假码读入另一地址,则马上 bpmd 另一地址。这样你肯定会在假码的带领下找到程序的注册码验证函数!

由于程序往往会将 F(假码,用户名)分解成不止一个二元方程,即:
f1(假码) = f(用户名)
f2(假码) = f(用户名)
......
fn(假码) = f(用户名)

所以第一次跟踪到的验证函数一般只包含大F 算法的一部分,所以只能初步了解该算法。程序在完成第一次验证之后可能会告诉你“注册码正确,请重新启动程序完成注册”,请千万不要被它迷惑,你必须继续让“钥匙”带领你去找其他的验证函数,防止程序“偷偷地”做第二次、第三次甚至第n 次验证。即使关闭程序也要坚决搞清楚它将钥匙放到哪里保存起来了,防止程序重新运行后再次验证。在跟踪假码时程序可能会反复对假码进行变换,每一次变换后都要继续跟踪变码,即使程序将变码写入注册表,也要看看它会不会再读出来再做验证,即使程序什么事都不做了等着你关闭程序,你也要看看在你点击了“X” 按钮之后程序有没有处理OnDestroy 消息响应函数再次变换或验证变码,总之要对假码及假码的一切变码 bpmd 到底!

程序重启时要采用前面提到的 bpx regqueryvalyeexa 加 bpm 手段继续跟踪“钥匙”,直到你对大F 算法有了足够的了解,能够推算出正确的注册码甚至写出注册机为止。

三、验证算法分析:

跟踪到第一次验证函数为0040b880,代码分析如下:
:u 0040b880 L 271
001B:0040B880  PUSH      FF-----------------------------------------------------
001B:0040B882  PUSH      00467DFB
001B:0040B887  MOV       EAX,FS:[00000000]
001B:0040B88D  PUSH      EAX
001B:0040B88E  MOV       FS:[00000000],ESP     这一段是函数对调用环境的
                                              保存,不用深究
001B:0040B895  SUB       ESP,000001A4
001B:0040B89B  PUSH      EBX
001B:0040B89C  PUSH      EBP
001B:0040B89D  PUSH      ESI
001B:0040B89E  PUSH      EDI----------------------------------------------------

001B:0040B89F  MOV       ESI,00000008<---------循环控制变量,从假码的第9位开始
                                              验证      
001B:0040B8A4  MOV       DWORD PTR [ESP+1C],00000000
001B:0040B8AC  MOV       EBX,00000001<---------循环控制变量,验证到第20位时回
                                              到第2位继续验证

001B:0040B8B1  PUSH      0049141C-----------------------------------------------
001B:0040B8B6  LEA       ECX,[ESP+14]
001B:0040B8BA  CALL      00449D4A
001B:0040B8BF  MOV       ECX,00000064          将[ESP+24]之后的100个dword地址
001B:0040B8C4  MOV       EAX,EBX               赋值为“1”
001B:0040B8C6  LEA       EDI,[ESP+24]          将[ESP+24]这一地址记入EDI,这
001B:0040B8CA  XOR       EBP,EBP               一地址存放输入的注册码(假码)
001B:0040B8CC  REPZ STOSD                      的各位数字值,是程序在验证函数
                                              之前做的
001B:0040B8CE  MOV       EDI,[ESP+000001C8]    再将从[ESP+28]到[ESP+64]这16
001B:0040B8D5  MOV       EAX,00000008          个dword地址依次赋值为
001B:0040B8DA  MOV       ECX,00000005          “1975121885116918”,估计与
001B:0040B8DF  MOV       [ESP+40],EAX          作者生日有关,将用来作为加密
001B:0040B8E3  MOV       [ESP+44],EAX          算法的一个密码表
001B:0040B8E7  MOV       [ESP+60],EAX
001B:0040B8EB  LEA       EAX,[EDI+04]
001B:0040B8EE  MOV       EDX,00000009
001B:0040B8F3  MOV       [ESP+30],ECX
001B:0040B8F7  MOV       [ESP+48],ECX
001B:0040B8FB  PUSH      EAX                   注意这期间进行过一次压栈操作,所
                                              以ESP的值发生过变化,对后面的
001B:0040B8FC  LEA       ECX,[ESP+14]          [ESP+xx]的地址有所影响
001B:0040B900  MOV       [ESP+000001C0],EBP
001B:0040B907  MOV       [ESP+28],EBX
001B:0040B90B  MOV       [ESP+2C],EDX
001B:0040B90F  MOV       DWORD PTR [ESP+30],00000007
001B:0040B917  MOV       [ESP+38],EBX
001B:0040B91B  MOV       DWORD PTR [ESP+3C],00000002
001B:0040B923  MOV       [ESP+40],EBX
001B:0040B927  MOV       [ESP+50],EBX
001B:0040B92B  MOV       [ESP+54],EBX
001B:0040B92F  MOV       DWORD PTR [ESP+58],00000006----------------------------

001B:0040B937  MOV       [ESP+5C],EDX-------------------------------------------
001B:0040B93B  MOV       [ESP+60],EBX
001B:0040B93F  CALL      0044A0F4
001B:0040B944  LEA       EAX,[EDI+08]
001B:0040B947  LEA       ECX,[ESP+10]
001B:0040B94B  PUSH      EAX
001B:0040B94C  CALL      0044A0F4
001B:0040B951  LEA       EAX,[EDI+14]
001B:0040B954  LEA       ECX,[ESP+10]
001B:0040B958  PUSH      EAX
001B:0040B959  CALL      0044A0F4
001B:0040B95E  LEA       EAX,[EDI+0C]
001B:0040B961  LEA       ECX,[ESP+10]          这一段是将你输入的用户名、信箱
001B:0040B965  PUSH      EAX                   地址与软件的名称、版本号合并成
001B:0040B966  CALL      0044A0F4              为一个目标串,放入[ESP+10],
001B:0040B96B  LEA       EAX,[EDI+10]          程序实际上验证 F(注册码,目标串)
001B:0040B96E  LEA       ECX,[ESP+10]          目标串形如
001B:0040B972  PUSH      EAX                   yourmail+wordslover+yourname+1.60
001B:0040B973  CALL      0044A0F4              看来作者对这一算法非常满意,从
001B:0040B978  LEA       EAX,[EDI+1C]          1.6版沿用至今
001B:0040B97B  LEA       ECX,[ESP+10]
001B:0040B97F  PUSH      EAX
001B:0040B980  CALL      0044A0F4
001B:0040B985  LEA       EAX,[EDI+20]
001B:0040B988  LEA       ECX,[ESP+10]
001B:0040B98C  PUSH      EAX
001B:0040B98D  CALL      0044A0F4
001B:0040B992  LEA       EAX,[EDI+24]
001B:0040B995  LEA       ECX,[ESP+10]
001B:0040B999  PUSH      EAX
001B:0040B99A  CALL      0044A0F4-----------------------------------------------

001B:0040B99F  MOV       ECX,[ESP+10]<---------将目标串地址写入ECX
001B:0040B9A3  XOR       EAX,EAX<--------------EAX清零
001B:0040B9A5  MOV       ECX,[ECX-08]<---------取目标串的长度
001B:0040B9A8  CMP       ECX,EAX<--------------长度为零则跳转(目标串包含
                                              wordslover1.60,长度怎么可
                                              能是零?有病!)
001B:0040B9AA  JLE       0040B9F1
001B:0040B9AC  MOV       EDX,EDI<--------------将假码数字表地址写入EDX
001B:0040B9AE  MOV       [ESP+18],EAX<---------将[ESP+18]作为从零开始的一个
                                              临时变量
001B:0040B9B2  MOV       EDX,[ESI*4+EDX+30]<---将假码当前位(ESI)的数字值
                                              写入EDX
001B:0040B9B6  MOV       [ESP+20],EDX<---------将该值写入[ESP+20]保存

--------------------------------------------------------------------------------

001B:0040B9BA  MOV       EDX,[ESP+10]<---------将目标串地址写入EDX
001B:0040B9BE  MOV       EDI,[ESP+18]<---------将变量[ESP+18]的值写入EDI
001B:0040B9C2  AND       EDI,8000000F<---------变量[ESP+18] 除以16的余数
001B:0040B9C8  MOV       DL,[EDX+EAX]<---------将目标串的当前位的ASC码写入DL
001B:0040B9CB  JNS       0040B9D2<-------------若余数不为负值则跳转?[ESP+18]
                                              的所有赋值动作都在掌握之内,怎
                                              么可能大于80000000?有病!
001B:0040B9CD  DEC       EDI
001B:0040B9CE  OR        EDI,-10
001B:0040B9D1  INC       EDI
001B:0040B9D2  MOV       EDI,[EDI*4+ESP+24]<---根据变量[ESP+18]的值读入密码表
                                              (19751218...)的相应位的数值写
                                              入EDI
001B:0040B9D6  MOVSX     EDX,DL<---------------将DL中的目标串当前位的ASC码值写
                                              入EDX
001B:0040B9D9  IMUL      EDI,EDX<--------------当前密码值与目标串当前ASC码值的
                                              乘积
001B:0040B9DC  ADD       EDI,[ESP+20]<---------再加上当前假码数值
001B:0040B9E0  ADD       EBP,EDI<--------------在EBP中累计这一结果
001B:0040B9E2  MOV       EDI,[ESP+18]<---------将变量[ESP+18]的值写入EDI
001B:0040B9E6  INC       EAX<------------------目标串的当前位+1
001B:0040B9E7  ADD       EDI,ESI<--------------变量[ESP+18]+假码的当前位
001B:0040B9E9  CMP       EAX,ECX<--------------目标串的当前位是否等于目标串长度
001B:0040B9EB  MOV       [ESP+18],EDI<---------更新变量[ESP+18]
001B:0040B9EF  JL        0040B9BA<-------------循环直至目标串结束


这一段循环是针对假码的每一位p[i]计算一个值A[i] = f(p[i],目标串),(其中i=ESI
设目标串为d,密码表为c,则 A[i] = {
                                    int num = 0;
                                    for(int j=0; j<d.lenth(); j++)
                                        num = num + d[j]*c[j*i%16]+p[i];
                                    return num;
                                 }

--------------------------------------------------------------------------------

001B:0040B9F1  MOV       EAX,ESI
001B:0040B9F3  AND       EAX,80000007<---------EAX等于假码当前位除以8的余数
001B:0040B9F8  JNS       0040B9FF
001B:0040B9FA  DEC       EAX
001B:0040B9FB  OR        EAX,-08
001B:0040B9FE  INC       EAX
001B:0040B9FF  JNZ       0040BA61<-------------余数不为零则跳转
001B:0040BA01  CMP       ESI,13
001B:0040BA04  JNZ       0040BA10<-------------不为19则跳转,除以8余0怎么可能
                                              等于19?有病!
001B:0040BA06  MOV       DWORD PTR [ESP+18],00000000
001B:0040BA0E  JMP       0040BA17

--------------------------------------------------------------------------------
001B:0040BA10  LEA       ECX,[ESI+01]<---------ECX等于假码当前位+1
001B:0040BA13  MOV       [ESP+18],ECX<---------存到临时变量[ESP+18]
001B:0040BA17  MOV       EDX,[ESP+000001C4]<---字符串形式的假码的地址的指针写
                                              入EDX
001B:0040BA1E  MOV       EAX,ESI<--------------EAX等于假码当前位
001B:0040BA20  MOV       EDI,[EDX]<------------字符串形式的假码的地址写入EDI
001B:0040BA22  CDQ<----------------------------EDX清零
001B:0040BA23  MOV       ECX,[EDI-08]<---------假码长度写入ECX
001B:0040BA26  IDIV      ECX<------------------在EDX里得到假码当前位(EAX)除
                                              以假码长度(ECX)的余数,还不就
                                              是假码的当前位?毛病!
001B:0040BA28  MOV       AL,[EDI+EDX]<---------将假码当前位的ASC码值写入AL
001B:0040BA2B  MOV       EDI,0000000A<---------EDI等于10
001B:0040BA30  MOV       [ESP+17],AL<----------将假码当前位的ASC码值写入临时变
                                              量[ESP+17]
001B:0040BA34  MOV       EAX,EBP<--------------当前A值,参见前述
                                              A[i]=f(p[i],目标串)
001B:0040BA36  CDQ<----------------------------EDX清零
001B:0040BA37  IDIV      EDI<------------------在EDX里得到当前A值(EAX)除以10
                                              的余数
001B:0040BA39  MOV       EAX,[ESP+18]<---------EAX=假码当前位+1
001B:0040BA3D  MOV       EDI,[ESP+000001C8]<---ESP+1c8+30是数值形式的假码的地址
001B:0040BA44  SUB       DL,[EAX*4+EDI+30]<----将DL里的余数减去假码下一位的ASC
                                              码值
001B:0040BA48  MOV       AL,[ESP+17]<----------AL=假码当前位的ASC码值
001B:0040BA4C  ADD       DL,AL<----------------余数-假码下一位在加上当前位后的
                                              结果
001B:0040BA4E  MOV       EAX,ESI
001B:0040BA50  PUSH      EDX
001B:0040BA51  CDQ                              
001B:0040BA52  IDIV      ECX                   无用功,老毛病!
001B:0040BA54  MOV       ECX,[ESP+000001C8]    
001B:0040BA5B  PUSH      EDX
001B:0040BA5C  CALL      0044A234<-------------将结果替换字符串形式假码的当前位

这一段是将假码的第9、17位做变换:
p[8] = p[9] + A[8]%10
p[16] = p[17] + A[16]%10

--------------------------------------------------------------------------------

001B:0040BA61  MOV       EAX,ESI
001B:0040BA63  MOV       ECX,00000007
001B:0040BA68  CDQ
001B:0040BA69  IDIV      ECX
001B:0040BA6B  TEST      EDX,EDX
001B:0040BA6D  JNZ       0040BA96
001B:0040BA6F  CMP       ESI,13
001B:0040BA72  JNZ       0040BA78
001B:0040BA74  XOR       ECX,ECX
001B:0040BA76  JMP       0040BA7B
001B:0040BA78  LEA       ECX,[ESI+01]
001B:0040BA7B  MOV       EAX,EBP
001B:0040BA7D  MOV       EDI,0000000A
001B:0040BA82  CDQ
001B:0040BA83  IDIV      EDI
001B:0040BA85  MOV       EAX,[ESP+000001C8]
001B:0040BA8C  SUB       EDX,[ECX*4+EAX+30]
001B:0040BA90  JNZ       0040BADA

累死了!这一段就不祥加注释了,反正就是验证第8、15位(i%7==0)的A值必须为零,
否则就JNZ 0040bada,验证失败!
--------------------------------------------------------------------------------

001B:0040BA92  INC       DWORD PTR [ESP+1C]<---若A值为零则标志变量[ESP+14]+1
001B:0040BA96  INC       ESI<------------------假码当前位+1
001B:0040BA97  CMP       ESI,13
001B:0040BA9A  JLE       0040BA9E
001B:0040BA9C  MOV       ESI,EBX<--------------若当前位为19(从0开始计算,19是
                                              第20位),则当前位回到1
001B:0040BA9E  LEA       ECX,[ESP+10]
001B:0040BAA2  MOV       DWORD PTR [ESP+000001BC],FFFFFFFF
001B:0040BAAD  CALL      00449CDC
001B:0040BAB2  CMP       DWORD PTR [ESP+1C],14
001B:0040BAB7  JL        0040B8B1<-------------循环直至标志为等于20,相当于把
                                              所有工序重复10遍,因为ESI每从1
                                              到19一次,仅有两次机会修改标志位
001B:0040BABD  MOV       AL,BL<----------------函数返回值置1,表示验证成功  
001B:0040BABF  MOV       ECX,[ESP+000001B4]
001B:0040BAC6  POP       EDI
001B:0040BAC7  POP       ESI
001B:0040BAC8  POP       EBP
001B:0040BAC9  POP       EBX
001B:0040BACA  MOV       FS:[00000000],ECX
001B:0040BAD1  ADD       ESP,000001B0
001B:0040BAD7  RET       0008
001B:0040BADA  LEA       ECX,[ESP+10]
001B:0040BADE  MOV       DWORD PTR [ESP+000001BC],FFFFFFFF
001B:0040BAE9  CALL      00449CDC
001B:0040BAEE  XOR       AL,AL<---------------函数返回值置0,表示验证失败
001B:0040BAF0  JMP       0040BABF

这一验证函数仅仅验证了假码的两位,然后又将假码的另两位做了10次变换,然后就恭喜你注册成功!所以必须继续跟踪变码,发现果然在你下达了关闭程序的指令后它在程序窗口从屏幕上消失的同时偷偷又调用了一个类似的验证函数,这次它用同样的方法验证变码的其中一位而将变码的另六位做了20次变换!重新启动程序后你会发现它在很多各地方再次进行验证和变换,经过推理可知如果你输入的注册码的每一位的A值都等于0,而且每一位在变换后都可以保持原值,则不管程序如何变换,该注册码永远都能通过验证!

所以。。。俺的注册机可以诞生了!


结语:

疯狂单词构造了一个巧妙的算法,要求注册码满足:
注册码 = F(注册码,用户名)
然后他每次验证注册码之后,只有正确的注册码可以保持不变,并通过下一次的验证。
但不足的是该作者的离散数学功底太差,他将大F分解成多个小f时仅仅采用了分段函
数的形势,函数表达式本身是一样的,仅仅是作用域不同而已(验证不同的位),假如能
够将大F分解成多个函数表达式不同的小f,通过跟踪小f分析其大F的算法将是非常困难的!
当然,衷心希望此文不要被疯狂单词作者看见,即使看见,也衷心希望他不会马上疯狂:)
预告:俺的下一篇教程将是《楚汉棋缘》,据说是很不错的象棋共享软件,敬请关注!