生物软件注册破解入门篇(上)

¾chromas 2.23的分析

 

WindSeven

 

(Ver. 1.0, 2004.03.28)

 

 

声明:本文仅供对软件加解密进行学习研究之用。转载时请保留作者信息。

 

 

对生物软件破解的是非曲直就不罗嗦了,我是把软件破解作为一种爱好和挑战来玩的。因为看到一些学生物的朋友想要了解软件加解密,所以决定写几篇东西来交流一下。

 

软件保护的方式很多(有关基础知识如果不太了解的话,可到下面给出的两个网站上去大概看一看),注册保护是其中常见的一种。比如说一个软件的演示版(demo)可能有功能上的限制,或者是使用时间、次数的限制等等,只有输入了正确的注册码或激活码,你才可能获得全部功能或能继续使用。正版软件使用前需要输入序列号或激活码,或者需要一个key文件或license 文件的,也可以归为此类。此类软件在运行时必然会对用户提供的注册码、序列号,或者key文件license 文件进行计算、比较,以检测其合法性。所以破解的基本思路就是设法找到程序中进行这些比较、检测的地方。找到后可以选择修改程序代码来达到破解的目的(即所谓¡°暴力破解¡±),不过一般不到万不得已我是不愿意修改原程序的代码的,最好是能找出注册码或key文件的算法。只要这些计算是在用户本地的电脑上进行的并且算法是可逆的,我们就可以据此写出注册机。

 

在进行实例分析前说一下软件破解需要具备哪些基本条件:

首先是要了解一些电脑的基本原理,比如寄存器、堆栈、中断等等。这个感兴趣的朋友大概都学过或知道吧,不太了解的可以随便找本电脑原理的书来补习一下。

其次是要有一定汇编基础。不论是动态跟踪还是静态分析,你要面对的都是由可执行文件反汇编出来的汇编过程。有人说看这些就跟看天书一样,其实软件破解远不需要你精通汇编语言,只要你有一定程序语言基础并知道每条汇编指令都是干什么的,完全就可以看得懂这些汇编过程。我想所有程序语言都是相通的,学会了一门,其它的都不难了解。不过也许最好是有一定C语言基础,因为C语言是和汇编最接近的高级语言。另外手边应该有一份汇编指令的¡°字典¡±。当年我是从一本书的附录里复印下来的,现在应该在网上很多地方都可以找得到。你可以直接到intel的网站上去找,象是浮点指令的介绍我就是从intel网上down下来的。

最后就是工具了。必备的工具包括一款动态跟踪调试软件和一款反汇编软件。用来动态跟踪的工具软件大家常用的就是NuMega/ CompuwareSoftice。作为单独软件的Softice的最后一个版本是4.05,在网上应该到处都可以找得到。现在SofticeDriverStudio的一部分,目前最新的版本是3.1Softice相当于4.31),不过支持Win9x的最终版本是2.7。现在不少人觉得另一款软件OllyDbg更好用,有兴趣的也可以试试。反汇编或者静态分析可以使用W32Dasm,到处流传的版本是8.93 Gold。另一款反汇编软件IDA Pro能够提供更多信息,并有许多附加功能,不过对于初学者来说还是建议先用W32Dasm吧。还有许多小工具我会在实例分析中提到。

 

所以说基础的软件破解并不需要有很高深的学问,关键是一要有耐心,这个对初学者尤其重要;二要有经验和感觉,这个会慢慢积累和培养出来。

 

下面再介绍两个破解网站,在这两个以及其它网站上你不仅可以看到有关软件破解的教程和实例分析,也可以下载到很多破解所需要的工具软件及使用简介:

1.    笨冬瓜(ddcrack)的破解网站: http://ddcrack.myetang.com/

有很多适合初学者的实例教程。

2.    看雪工作室 (KanXue Studio): http://www.pediy.com/

国内著名的软件破解专业网站。

 

好了,现在通过一个实例来具体介绍软件注册破解的基本方法。¡°受害者¡±Chromas (Ver. 2.23)是一款用来显示和输出DNA自动测序结果的小工具,可到以下网址下载:http://www.technelysium.com.au 。说实在,这些个人软件作者和小公司也挺不容易的,现在已经很不好意思破解这种软件了。不过既然是破解入门,就只好拿一个看上去简单的开刀了。再说好像alchemy很早以前就有了破解了,test123也给出过注册机,那我也不算是¡°首犯¡±了 :)

 

一.  首先看看软件的基本情况

 

Chromas安装后,可以看到这款软件有60天的全功能试用期,60天后如果没注册的话会转变为限制功能版。Help菜单中选Register,会出现如下注册窗口:

 

 

看上去注册码(Key)是根据用户名(Name)和单位(Organisation)算出来的那种,不过注册码有两个,不能算是最简单的。随便输入一个Name,两个注册码看起来都是5位的,不妨输入1234567890(在以后的分析中,试验用的注册码应该用比较独特的,以便在跟踪程序时可以确定是你输入的)。按OK后,软件弹出如下消息窗口告诉你输入的注册码不正确:


 

看来对注册码的计算和检测(准确点说,起码注册码计算和检测的一部分)就在这里了。


Text Box
 

 

顺便提一下一般软件检测、验证注册码的方式。一种就是根据输入的用户名等计算出正确的注册码,然后再和输入的注册码比较:

f(用户名) = 注册码

在软件保护最简单的情况中,正确的注册码甚至会在程序运行某一时刻出现在内存中。当然复杂的情况中,注册码的检测、验证并不仅只一处,可能会在软件的不同地方(甚至在软件的不同升级版本中)对注册码的不同字节或位置,使用不同的函数进行验证。

另一种是根据输入的用户名和注册码分别计算出一个值,然后对这两个值进行比较:

f1(用户名) = f2(注册码)

这种方式就比较隐蔽了。

还有一种更厉害的,由输入的用户名和注册码计算出一个特定值:

f(用户名, 注册码) = 特定值

然后再对这个特定值进行各种验证。这样的话想做注册机就比较困难了。

 

但道高一尺,魔高一丈,可以说各种保护方式总有其破解的方法。上面已经提到对注册保护破解的基本思路是设法找到程序中对注册码或key文件等进行检测、验证的地方。那么怎么找呢?有静态分析和动态跟踪两种方式。所谓静态分析就是对软件的可执行文件进行反汇编,然后对反汇编出来的程序进行分析。静态分析比较有利于掌握整个程序的思路。所谓动态跟踪是利用程序调式工具对目标软件的运行进行单步跟踪分析。动态跟踪可以分析程序的运行细节,但对经验不多的人的来说比较容易在跟踪中迷失方向。最好的方式是动静结合,往往能事半功倍

 

从理论上说,无论是静态分析还是动态跟踪,从软件运行的第一步(即程序入口)开始分析和跟踪,总可以找到程序中的关键点,即验证注册码或key文件的地方。但这样费时费力,相信没有人会这样做。所以有效的方法是在短时间内找到离关键点最近的地方。寻找这样的突破点对不同的目标软件应该用不同的方法,但一般有从静态分析开始突破和从动态跟踪开始突破两种基本方法。下面我会分别说明,以使大家对两种方式都有所了解。一般我喜欢先尝试从静态分析突破,所以先介绍这个方法。

 

二.  注册破解方法之一:从静态分析突破

 

从静态分析开始,首先要对目标程序进行反汇编。不过现在不少软件有反反汇编的措施,最常用就是对可执行文件加壳。所以一般反汇编前最好用FileInfo等工具察看软件是否加壳及加的什么壳,然后就可以找相应的脱壳工具来脱壳。找不到的话就只好自己动手了,或者改由动态跟踪开始突破。还有一种反反汇编措施是在程序中加入一段特殊代码,不影响程序的正常执行,但会在被反汇编时进入死循环。有时候你看到反汇编软件在反汇编某些程序死掉了,那么有可能就是中招了。当然还有所谓花指令,可干扰反汇编,使反汇编结果不正常等等。

 

Chromas没有加壳,可以用W32Dasm直接反汇编(Disassemble®Open File to Disassemble),然后最好将结果保存以备后用。下面的分析你可以在W32Dasm中进行,但有时候我对W32Dasm的一些功能(如搜索等)及速度不太满意,一般会用Ultraedit等软件对保存下来的程序清单(.alf文件)进行分析。

 

回到正题,一般你输入的注册码或序列号不能通过验证的话,软件会提示你输入不正确,相关提示信息一般会显示在一个消息窗口(MessageBox)里。比如对Chromas来说会提示你:¡±The keys do not match the name and organization¡­¡± 所以静态分析的主要突破方式就是从程序中对这些提示信息字符串的引用入手。下面是一个简单但典型的程序流程:

1. call xxxxxxxx               对输入注册码进行验证的函数,假设验证通过函数返回值为1,不通过则为0

2. 比较函数返回值(一般放在eax里)

cmp eax, 0

je yyyyyyyy         函数返回值为0,则跳到验证未通过的处理过程。

另一种更常见的比较方式如下,也是eax0则跳:

test eax, eax

je yyyyyyyy

3. 验证通过的处理过程

¡­

jmp zzzzzzzz        跳过下面验证未通过的处理过程

4. 验证未通过的处理过程,可能包含对提示信息字符串的引用:

* Possible Reference to String Resource ¡­

* Possible StringData Ref from ¡­

 

所以如果目标软件没有什么防范措施的话,找到对提示信息字符串的引用后,有可能很快就在前面不远找到验证过程。如果软件不对提示信息字符串进行直接引用,或者对提示信息字符串加密,或者干脆没有提示,那我们可能就只好用其它方法突破了。

 

现在我们对W32Dasm反汇编出来的Chromas程序清单进行分析,搜索¡±The keys¡±。注意有时W32Dasm报告的对字符串的引用并不一定准确,所以一般需要用softice跟踪到该程序行确认一下。不过我们现在遇到的问题是:程序清单中没有给出¡±The keys¡­¡±的引用!遇到这种情况我一般会转去试其它动态跟踪突破的方法了,不过现在为了写这个教程,只好硬着头皮上了:)

 

Ultraedit打开Chromas.exe文件,你可以搜索到¡±The keys¡­¡±字符串,说明软件对此并没有加密。那么很可能是W32Dasm没有找到对这一字符串的引用,我们只好自己动手了。Windows下消息窗口的显示通常会调用WindowsMessageBox函数(有关WindowsAPI函数下面一节再作简要介绍),搜索Chromas程序清单,可以找到一处对MessageBoxA函数的调用(其实通过动态跟踪,用bpx MessageBoxA应该也可以达到同样效果,参见下一节):

 

* Referenced by a (U)nconditional or (C)onditional Jump at Address:

|:0043CAC3(U)

|

:0043CADF 53                      push ebx

:0043CAE0 57                      push edi

:0043CAE1 FF7508                  push [ebp+08]

:0043CAE4 FF75F4                  push [ebp-0C]

 

* Reference To: USER32.MessageBoxA, Ord:01BEh

                                  |

:0043CAE7 FF15E8044500            Call dword ptr [004504E8]

 

这里顺便提一下手边最好还应该有一份Windows API函数的资料,这个在很多书里或网站上可以找到,最权威的可以去MicrosoftMSDN网站上查。我们可以知道MessageBoxA的第二个参数是显示信息字符串的指针(即倒数第二个压入堆栈的参数,函数参数的传递一般是通过堆栈进行的),不清楚也没关系,动态跟踪时对几个参数一一检查就是了。

 

接下来要借用动态跟踪进行确认了,softice的一般使用请参考下一节。用softiceSymbol loader打开Chromas.exe,装载(load),进入softice调试画面,Chromas程序应该停在第一行。输入g 43CAE1,即把断点设在了这一程序行,程序运行到这里会被暂时中断,被softice接管。Chromas被运行,Help菜单中选Register,随便输入用户名和注册码,点OK。程序被拦截下来了!看来注册码错误的信息确实是通过这里显示的。[ebp+08]中的值为00451600,应该是错误信息字符串的指针。用d 451600显示一下,果然是指向¡±The keys do not match ¡­¡±

 

回到程序清单,搜索00451600,很走运,可以找到如下一处(注意一般软件运行时可能对字符串进行多次拷贝,不会像这个例子一样能这样找到;如果接着刚才动态跟踪的话,按两次F12也可以回到00409D84这里):

 

* Referenced by a (U)nconditional or (C)onditional Jump at Addresses:

|:00409C51(C), :00409C5E(C)

|

:00409D76 6A00                    push 00000000

:00409D78 6A30                    push 00000030

:00409D7A 6800164500              push 00451600

:00409D7F E8982D0300              call 0043CB1C

:00409D84 5F                      pop edi

:00409D85 5E                      pop esi

:00409D86 5D                      pop ebp

:00409D87 83C414                  add esp, 00000014

:00409D8A C3                      ret

 

softice可以确认(g 409D7A)这里确实也是错误信息字符串的指针。现在来看看程序清单中跳转来的地方:

 

:00409C30 6A00                    push 00000000

:00409C32 6A00                    push 00000000

 

* Possible Reference to Dialog: DialogID_000A, CONTROL_ID:0067, ""

                                  |

:00409C34 6A67                    push 00000067

:00409C36 E89B8B0200              call 004327D6 ¬这里读取Key1

:00409C3B 6A00                    push 00000000

:00409C3D 6A00                    push 00000000

 

* Possible Reference to Dialog: DialogID_000A, CONTROL_ID:0068, ""

                                  |

:00409C3F 6A68                    push 00000068

:00409C41 8BCF                    mov ecx, edi

:00409C43 8BE8                    mov ebp, eax

:00409C45 E88C8B0200              call 004327D6¬这里读取Key2

:00409C4A 83FEFF                  cmp esi, FFFFFFFF ¬ esiName等算出

:00409C4D 8944240C                mov dword ptr [esp+0C], eax

:00409C51 0F841F010000            je 00409D76

:00409C57 C1E010                  shl eax, 10

:00409C5A 0BC5                    or eax, ebp

:00409C5C 3BC6                    cmp eax, esi

:00409C5E 0F8512010000            jne 00409D76

 

怎么样,和前面介绍的一般流程类似吧?不过这里验证过程和结果处理放在一起了。这一段程序下面应该是注册码验证正确后,向系统注册表或INI文件写入注册信息的过程。在程序清单开始有程序中所有对话框的信息(DIALOG INFORMATION),可以知道ID000A的对话框其实就是软件注册的对话框,而ID00670068的控制(control)应该分别是输入的Key1Key2。当然仍可以通过softice确认。所以我们应该可以确认这里就是软件注册的关键点了。

 

注册码的验证过程和算法我放到最后再讲,下面先分析从动态跟踪开始突破的方法。

 

三.  注册破解方法之二:从动态跟踪突破

 

动态跟踪要用到softice等调试工具,关于softice的安装、配制和使用请参考softice自带的手册或看雪的解密教程。这里只提几个初学者应注意的地方:

1. 由于跟踪时经常需要把断点设在WindowsAPI函数上,主要的API函数包含在kernel32.dlluser32.dllgdi32.dlladvapi32.dll等系统文件中,配制softice时应使其装入这些系统文件。可在Symbol loader(或Softice loader)中配制加入这些文件:EDIT SOFTICE Initialization Settings Exports Add。在Win9x下也可直接用任何文本编辑器编辑softice目录中的winice.dat文件,将以下几行前的分号(如果有的话)去掉:

EXP=c:\windows\system\kernel32.dll

EXP=c:\windows\system\user32.dll

EXP=c:\windows\system\gdi32.dll

EXP=c:\windows\system\advapi32.dll

2. Windows里按Ctrl-D可以呼出SoftICE的调试窗口,再按Ctrl-D可返回Windows系统,也可使用X命令或按F5键。如果嫌调试窗口太小的话,可用lineswidth等命令调整。缺省状态下调试窗口包括寄存器窗口、代码窗口和命令窗口,此外你还可选择显示数据窗口、浮点窗口等。下面一幅图是从看雪的教程里¡°借¡±来的,特别请注意¡°程序领空¡±这个概念。如果跟踪分析时,当前的程序领空不属于目标程序,那么基本是白费功夫。

3. Softice的操作命令很多,这里强调几个常用的:

G命令前面已提过,用来执行程序,后面如加地址,则执行到该地址为止。

D命令也提过,用来显示内存某区域的内容。

快捷键F8,相当于T命令,单步跟踪。

F10键,相当于P命令,单步执行。F10F8的区别是,遇到 CALL等指令时,F8会跟进去,F10则不会。

F12键,相当于组合命令P RET,效果是返回调用当前程序段的程序模块。

? 命令,计算一个表达式的值,SoftICE自带的16进制计算器。

断点相关命令:

BPX [地址] BPX [函数名],常用的下断点的命令;

BL,列出当前所有断点;


SoftICE调试窗口示意

 

 

BD,屏蔽指定的或所有断点,如BD 0,1,3BD *

BE,恢复被屏蔽掉的指定的或所有断点;

BC,清除指定的断点。

 

和前面的静态分析一样,动态跟踪时一般也不会从目标程序的第一步开始。有效的方法是利用softice等调试工具等在程序或系统中下断点,当程序运行到这一点(或地址)就会被暂时中断,从而我们可分析其运行细节和中间状态。所以由动态跟踪突破的关键就是设置一个好的断点,而该断点距离我们要寻找的关键程序模块尽可能近。我们知道对注册保护来说,关键程序模块就是程序验证注册码或序列号的地方。

 

那么对注册保护怎么设置断点呢?在Windows下,程序一般会调用Windows系统的API函数进行一些基本操作。比如在程序的注册窗口,程序读取用户在文字编辑框中输入的用户名和注册码等时,可能会用到以下API函数:GetWindowText (常见的形式是GetWindowTextA,另一种不常见的形式是GetWindowTextW)GetDlgItemText(A/W),还有两个不大常用的直接将输入转变为整数的函数GetWindowIntGetDlgItemInt。而需要读取key文件或license 文件的程序,可能会调用ReadFileAPI函数。所以将断点设在这些API函数的入口是由动态跟踪突破的主要方式

 

不过不少软件已经开始采取一些反跟踪的措施,比如在关键的程序模块中尽量少用或不用这些常用的Windows API函数。这时可以试试拦截相关的Windows消息,如点击OK按钮,BMSG xxxx WM_COMMAND xxxx为对应窗口的句柄,可用SoftICE提供的HWND命令获得,也可以利用Visual Studio中的Spy++实用工具得到),又如控件文本的读取,BMSG xxxx WM_GETTEXT。还可以试试拦截其它的函数,如拦截消息窗口函数BPX MessageBox(A/W) MessageBoxEx(A/W) MessageBoxIndirect(A/W),甚至是对话框函数(这个有很多,就不一一列举了)等等。

Win9x下可利用一个在系统很低层的API函数¾Hmemcpy,俗称万能断点,因为任何字符串等数据的处理都涉及到内存中数据的拷贝,即会调用该函数(非应用程序直接调用,所以可能需要按nF12返回应用程序)。在NT/2000/XP系统中,相关的函数是Memcpy,但这个函数在这些系统中很少被用到,所以基本没什么用了。

一些高级的软件还可能会采取一些反跟踪技术使你无法使用softice等调试工具,这里就不一一介绍了。

 

现在来分析chromas,当然你可以把所有可能用到的API函数都设为断点,但我们也可以做到有的放矢。打开反汇编出来的chromas.exe程序清单,在开始的MenuDialog信息后面是所有的引入函数(IMPORTED FUNCTIONS),这里我们可以查到该程序调用的所有API函数。在W32Sasm中,也可以通过Functions®Imports查看。另一种有效的方法是利用Softsnoop等工具实时监测目标程序调用了哪些API函数。

 

chromas.exe程序清单中我们可以知道该程序调用了GetWindowTextAGetDlgItemTextAGetDlgItemInt。按Ctrl-D呼出softice,下以下断点:

bpx GetWindowTextA

bpx GetDlgItemTextA

bpx GetDlgItemInt

由于这些API函数可能也会被程序中的非注册码验证模块或者是系统中的其它程序调用,所以需暂时将这些断点屏蔽掉,以免干扰(当然你也可以暂时不下断点,效果一样):

bd *

这时你用bl列断点的话,会看到被屏蔽掉的断点前都有*号。

 

Ctrl-D退出softice,运行ChromasHelp菜单中选Register,随便输入用户名和注册码。注意一般软件会在你点OK按钮时再去读取你输入的完整信息,然后加以验证。但¡°狡猾¡±的软件会是你一边输入,它就一边读取、计算了。所以开启断点的时机需要试试,如果输入完成后开启,再点OK按钮拦不下来的话,可以试试在输入最后一个字符前开启断点。有的软件甚至是对输入的各个字符实时进行不同计算的,这时就得从开始就拦了。我们先试完整输入后再开启断点,按Ctrl-D呼出softice,恢复刚才下的断点:

be *

退出softice,然后在Chromas中点OK按钮。程序被拦下来了!

 

程序被拦在GetDlgItemTextA的入口,注意现在的程序领空是在系统内部(USER32)。按F12键,直到返回CHROMAS的领空(这里只需按一次)。如果一直回不到的话,一般说明你跟错了。现在我们回到了这里:

 

:0043281E   CALL [USER32!GetDlgItemTextA]

:00432824   JMP 00432836¬回到这里

¡­

:00432836   POP EBP

:00432837   RET 000C

 

这里程序读取文本后没两步就返回(RET)了,所以这段程序除了读取文本外应该没有什么特殊的地方。按F12返回调用模块,我们来到了这里:

 

:00409BD6   PUSH 00000200 ¬这个应该是最大接收字节数

:00409BDB   PUSH 00467380 ¬接收缓冲区的指针/地址

:00409BE0   MOV EDI, ECX

:00409BE2   PUSH 00000065 ¬ Name控件ID

:00409BE4   CALL 00432808

:00409BE9   TEST EAX, EAX ¬回到这里

:00409BEB   JE 00409BFC

 

如果还对前面静态分析的结果有印象的话,可以知道这段程序已经是在注册码的验证模块里了。初次来到这里也不要紧,将此段程序前后看一看,再在softice里大概跟踪分析一下,也可以确认这整个程序模块是根据用户名和单位名计算出一个数值,再和两个Key算出的值比较。比如上面这一小段程序,你可以用softice重新装载 chromas.exe,执行到00409BD6这里中断,然后进行单步跟踪分析。00432808这个函数不必跟进去,只需观察传进去的参数和返回的结果,基本可以确定其作用是将你输入的用户名拷贝到(DS:)00467380所指的缓冲区,返回值eax为用户名的长度(字节数)。

 

所以对chromas这样简单的软件,由动态跟踪突破可能不到5分钟你就可以找到软件的注册码验证模块。由于W32Dasm没能分析出对注册码错误提示信息的引用,由静态分析突破需要多一点的时间。这也说明对不同软件应该灵活采取不同破解方法。


 

四.  注册码验证过程和算法分析

 

现在来分析chromas注册码的验证过程并推算出其注册码算法。从我们找到的验证模块开始:

 

:00409BD0 83EC14                  sub esp, 00000014

:00409BD3 55                      push ebp

:00409BD4 56                      push esi

:00409BD5 57                      push edi

:00409BD6 6800020000              push 00000200¬最大接收字节数

:00409BDB 6880734600              push 00467380¬接收缓冲区指针

:00409BE0 8BF9                    mov edi, ecx

 

* Possible Reference to Dialog: DialogID_0005, CONTROL_ID:0065, "Chromas 2.23"

                                  |

:00409BE2 6A65                    push 00000065 ¬这个其实应该是指注册窗口(DialogID_000A)Name编辑框(CONTROL_ID:0065),可见W32Dasm报告的引用也不一定是准确的。

:00409BE4 E81F8C0200              call 00432808¬读取Name

:00409BE9 85C0                    test eax, eax

:00409BEB 740F                    je 00409BFC ¬ Name长度是否为0

 

上面这段程序在上一节动态跟踪时已经分析过了,只是将你输入的用户名拷贝到(DS:)00467380所指的缓冲区,函数_00432808的返回值eax为用户名的长度(字节数)。如果用户名长度为零,则跳过下面一段程序:

 

:00409BED B980734600              mov ecx, 00467380

:00409BF2 49                      dec ecx

 

* Referenced by a (U)nconditional or (C)onditional Jump at Address:

|:00409BFA(C)

|

:00409BF3 803C0120                cmp byte ptr [ecx+eax], 20 ¬空格

:00409BF7 7503                    jne 00409BFC

:00409BF9 48                      dec eax

:00409BFA 75F7                    jne 00409BF3

 

上面这段程序的作用是找到用户名中最后一个非空格字符的位置,0x2016进制的20)即为空格的ASCII码。也就是说,输入的用户名末尾多余的空格将被忽略不计。

 

* Referenced by a (U)nconditional or (C)onditional Jump at Addresses:

|:00409BEB(C), :00409BF7(C)

|

:00409BFC C680807346002C          mov byte ptr [eax+00467380], 2C

:00409C03 C6808173460020          mov byte ptr [eax+00467381], 20

:00409C0A 8D8082734600            lea eax, dword ptr [eax+00467382]

:00409C10 6800010000              push 00000100¬最大接收字节数

:00409C15 50                      push eax¬接收缓冲区指针

 

* Possible Reference to Dialog: DialogID_000A, CONTROL_ID:0066, ""

                                  |

:00409C16 6A66                    push 00000066 ¬ Organisation控件ID

:00409C18 8BCF                    mov ecx, edi

:00409C1A E8E98B0200              call 00432808 ¬读取Organisation

 

这一段程序是在用户名后加了一个逗号(0x2C)和一个空格,然后将读取的单位名复制在用户名后。现在00467380所指向的字符串为¡°用户名, 单位名¡±。

 

:00409C1F 6880734600              push 00467380

:00409C24 E867BCFFFF              call 00405890 ¬计算N

:00409C29 83C404                  add esp, 00000004

:00409C2C 8BCF                    mov ecx, edi

:00409C2E 8BF0                    mov esi, eax ¬ N=>esi

 

上面这段调用了函数_00405890,在softiceF10一下,不难推测出该函数是根据用户名和/或单位名计算出一个特定值(不妨称之为N值)。关于此函数我们后面再分析,现在先接着向下看:

 

:00409C30 6A00                    push 00000000

:00409C32 6A00                    push 00000000

 

* Possible Reference to Dialog: DialogID_000A, CONTROL_ID:0067, ""

                                  |

:00409C34 6A67                    push 00000067 ¬ Key1控件ID

:00409C36 E89B8B0200              call 004327D6 ¬读取Key1

:00409C3B 6A00                    push 00000000

:00409C3D 6A00                    push 00000000

 

* Possible Reference to Dialog: DialogID_000A, CONTROL_ID:0068, ""

                                  |

:00409C3F 6A68                    push 00000068 ¬ Key2控件ID

:00409C41 8BCF                    mov ecx, edi

:00409C43 8BE8                    mov ebp, eax ¬ Key1=>ebp

:00409C45 E88C8B0200              call 004327D6 ¬读取Key2

 

:00409C4A 83FEFF                  cmp esi, FFFFFFFF

:00409C4D 8944240C                mov dword ptr [esp+0C], eax

:00409C51 0F841F010000            je 00409D76 ¬ N值为-1则验证不通过

:00409C57 C1E010                  shl eax, 10 ¬ Key2 * 216

:00409C5A 0BC5                    or eax, ebp ¬ (Key2*216) | Key1

:00409C5C 3BC6                    cmp eax, esi

:00409C5E 0F8512010000            jne 00409D76

 

这一段是我们在第二节已经看到过的注册码验证过程。函数_004327D6调用了GetDlgItemInt读取Key1Key2。注册码通过必须符合以下两个条件:

 

1)      N¹ -1

2)      N = Key1 | (Key2 * 216) |代表或运算)

 

下面的程序就是验证通过后或验证未通过的处理过程了:

 

//注册码验证通过,则向INI文件和系统注册表写入注册信息:

* Reference To: KERNEL32.WritePrivateProfileStringA, Ord:02E5h

                                  |

:00409C64 8B356C024500            mov esi, dword ptr [0045026C]

:00409C6A 53                      push ebx

:00409C6B 68B4774600              push 004677B4 ¬¡°CHROMAS.ini¡±

:00409C70 6880734600              push 00467380 ¬¡°用户名, 单位名¡±

:00409C75 682C1F4500              push 00451F2C ¬¡°Register¡±

:00409C7A 686C1C4500              push 00451C6C ¬¡°Chromas¡±

:00409C7F FFD6                    call esi

:00409C81 6A00                    push 00000000

:00409C83 68F0854600              push 004685F0

:00409C88 6A00                    push 00000000

:00409C8A 683F000F00              push 000F003F

:00409C8F 6A00                    push 00000000

:00409C91 6A00                    push 00000000

:00409C93 6A00                    push 00000000

:00409C95 68841C4500              push 00451C84

//¡±Software\Technelysium\Chromas¡±

:00409C9A 6802000080              push 80000002

 

* Reference To: ADVAPI32.RegCreateKeyExA, Ord:015Fh

                                  |

:00409C9F FF1504004500            Call dword ptr [00450004]

.

.

.

* Referenced by a (U)nconditional or (C)onditional Jump at Address:

|:00409D2F(C)

|

:00409D50 B968704600              mov ecx, 00467068

:00409D55 C605058B460001          mov byte ptr [00468B05], 01

:00409D5C C605048B460000          mov byte ptr [00468B04], 00

:00409D63 E828FBFFFF              call 00409890

:00409D68 8BCF                    mov ecx, edi

:00409D6A E849970200              call 004334B8

:00409D6F 5F                      pop edi

:00409D70 5E                      pop esi

:00409D71 5D                      pop ebp

:00409D72 83C414                  add esp, 00000014

:00409D75 C3                      ret

 

//注册码验证未通过,则显示注册码错误提示信息:

* Referenced by a (U)nconditional or (C)onditional Jump at Addresses:

|:00409C51(C), :00409C5E(C)

|

:00409D76 6A00                    push 00000000

:00409D78 6A30                    push 00000030

:00409D7A 6800164500              push 00451600 ¬¡°The Keys do not match...¡±

:00409D7F E8982D0300              call 0043CB1C

:00409D84 5F                      pop edi

:00409D85 5E                      pop esi

:00409D86 5D                      pop ebp

:00409D87 83C414                  add esp, 00000014

:00409D8A C3                      ret

 

现在我们回过头来看看函数_00405890,即N值是怎么根据用户名和/或单位名计算出来的:

 

* Referenced by a CALL at Addresses:

|:00404526   , :0040461C   , :00409C24  

|

:00405890 81EC04040000            sub esp, 00000404

//在分析函数时,初学者应注意堆栈的操作。我们知道,在函数开始时,堆栈指针esp指向返回地址,即[esp]= 返回地址。在这个例子中,传过来的参数为指向¡°用户名, 单位名¡±的指针(pName),即[esp+04]=pName。现在esp减掉404,则[esp+0408]=pName

:00405896 33C9                    xor ecx, ecx ¬ ecx0

:00405898 8D542404                lea edx, dword ptr [esp+04] ¬ M[]

:0040589C 56                      push esi

:0040589D 57                      push edi

//堆栈又下压了8个字节,目前[esp+0410] = pNameesp+0C®M[]。注意是16进制。

 

//以下一段程序在[esp+0C]起始的堆栈区域生成一个特定的整数数组(姑且称之为M[256]),可用C程序表示如下:

for ( i = 0; i < 256; i++ ) {

k = i;

for ( j = 8; j > 0; j-- ) {

if  ( ( k & 1 ) != 0 )

k ^= 0x410771DA;

k /= 2;

}

M[i] = k;

}

* Referenced by a (U)nconditional or (C)onditional Jump at Address:

|:004058BF(C)

|

:0040589E 8BC1                    mov eax, ecx ¬ ecx为计数器

:004058A0 BE08000000              mov esi, 00000008

 

* Referenced by a (U)nconditional or (C)onditional Jump at Address:

|:004058B1(C)

|

:004058A5 A801                    test al, 01

:004058A7 7405                    je 004058AE

:004058A9 35DA710741              xor eax, 410771DA

 

* Referenced by a (U)nconditional or (C)onditional Jump at Address:

|:004058A7(C)

|

:004058AE D1E8                    shr eax, 1 ¬相当于除以2

:004058B0 4E                      dec esi

:004058B1 75F2                    jne 004058A5

:004058B3 8902                    mov dword ptr [edx], eax

:004058B5 41                      inc ecx

:004058B6 83C204                  add edx, 00000004

:004058B9 81F900010000            cmp ecx, 00000100 ¬ 0x100=256

:004058BF 7CDD                    jl 0040589E ¬ ecx<256,回到上面继续

 

:004058C1 8BB42410040000          mov esi, dword ptr [esp+00000410]

//[esp+0410]=pName => esi

:004058C8 83CFFF                  or edi, FFFFFFFF ¬ edi置为-1

:004058CB 803E00                  cmp byte ptr [esi], 00

:004058CE 7453                    je 00405923

//如果¡°用户名, 单位名¡±的第一个字节为0(即为空字符串),直接返回-1

 

//以下为根据用户名和/或单位名计算N值的过程,可用C程序表示如下(假设用户名和/或单位名字符串为Name[],字符串长度为nLength):

N=-1;

for ( i = 0; i < nLength; i++ ) {

k = (BYTE) Name[i];

if ( ( k >= ¡®A¡¯ ) && ( k <= ¡®Z¡¯ ) )

k += 0x20; //0x20 = ¡®a¡¯-¡®A¡¯

if  ( ( k != ¡® ¡® ) &&  ( k != 0x09 ) && ( k != ¡®,¡¯ ) && ( k != ¡®.¡¯ ) ) {

j = ( (BYTE) N ) ^ k;

N >>= 8;

N ^= M[j];

}

}

:004058D0 53                      push ebx

//目前esp+10®M[]

* Reference To: USER32.CharLowerA, Ord:0021h

                                  |

:004058D1 8B1D84064500            mov ebx, dword ptr [00450684]

 

* Referenced by a (U)nconditional or (C)onditional Jump at Address:

|:00405915(C)

|

:004058D7 33C0                    xor eax, eax

:004058D9 8A06                    mov al, byte ptr [esi]

:004058DB 50                      push eax

:004058DC FFD3                    call ebx ¬大写字母转小写

 

//一些特殊符号将被忽略不计

:004058DE 3C20                    cmp al, 20 ¬空格

:004058E0 8844240C                mov byte ptr [esp+0C], al

:004058E4 7429                    je 0040590F

:004058E6 3C09                    cmp al, 09 ¬TAB

:004058E8 7425                    je 0040590F

:004058EA 3C2C                    cmp al, 2C ¬¡®,¡¯

:004058EC 7421                    je 0040590F

:004058EE 3C2E                    cmp al, 2E ¬¡®.¡¯

:004058F0 741D                    je 0040590F

 

:004058F2 8B54240C                mov edx, dword ptr [esp+0C]

//¡°用户名, 单位名¡±中的一个字符

:004058F6 8BCF                    mov ecx, edi ¬ ediN值,初始值-1

:004058F8 81E1FF000000            and ecx, 000000FF

:004058FE 81E2FF000000            and edx, 000000FF

:00405904 33CA                    xor ecx, edx

:00405906 C1EF08                  shr edi, 08

:00405909 8B448C10                mov eax, dword ptr [esp+4*ecx+10]

//M[]中取一个数

:0040590D 33F8                    xor edi, eax

 

* Referenced by a (U)nconditional or (C)onditional Jump at Addresses:

|:004058E4(C), :004058E8(C), :004058EC(C), :004058F0(C)

|

:0040590F 8A4601                  mov al, byte ptr [esi+01]

:00405912 46                      inc esi

:00405913 84C0                    test al, al ¬下一字符是否为结束符(¡®\0¡¯)

:00405915 75C0                    jne 004058D7

 

:00405917 8BC7                    mov eax, edi ¬返回N

:00405919 5B                      pop ebx

:0040591A 5F                      pop edi

:0040591B 5E                      pop esi

:0040591C 81C404040000            add esp, 00000404

:00405922 C3                      ret

 

//如果¡°用户名, 单位名¡±为空字符串,返回-1

* Referenced by a (U)nconditional or (C)onditional Jump at Address:

|:004058CE(C)

|

:00405923 8BC7                    mov eax, edi ¬ edi=-1

:00405925 5F                      pop edi

:00405926 5E                      pop esi

:00405927 81C404040000            add esp, 00000404

:0040592D C3                      ret

 

如果做注册机的话,对用户名和/或单位名的计算可以照抄,剩下的任务是从N值反推出Key1Key2。我们知道

N  =  Key1  |  (Key2 * 216)  (216 = 0x10000)

所以可以推测出N值(4字节、32位的长整数)可以拆成高16(Key2)和低16(Key1)。用C程序可表示如下:

Key1 = N & 0xFFFF;

Key2 = N >> 16;

 

呼,写教程可比破解费劲多了,希望能对初学者们有所帮助。如果有什么错漏的话,也请不吝赐教。

 

1