某天(前天:),看了两个小时高数(具体不详:)之后,想放松一下,找到《是男人就下100层》,打开,玩了两把,突然发现今天怎么看那个“注册”的按钮特别不爽,OK,Crack it!

常规步骤,用PEid侦壳,用VC4.x写的,这样方便了,脱壳都不用。接着祭出用户级调试法宝——Ollydbg,加载《是男人就下100层》,万事俱备,踏上征途!

按F9运行程序,等程序窗口出来,点一下任务栏上按钮,结果竟然中断了,CPU窗口一看,原来是一条INT3指令,麻烦的东西,NOP掉。继续F9运行,好,这下可以把程序窗口调到前面了。切换到程序窗口,点注册,出现填写用户名和序列号的窗口。用户名填HotHeart,序列号填一个123456,确定,当然不会成功,要不我就可以去买彩票了^_^。弹出一个错误提示框,内容是日文的……意思大概是序列号无效。既然有提示框,那就好办,回到Ollydbg,下断点bp MessageBoxA,再次换到程序窗口点确定。YES!顺利中断,切回Ollydbg看看代码:

; 获取对话框中控件文本

00407A78  PUSH 100

00407A7D  LEA EAX,DWORD PTR SS:[EBP-204]

00407A83  PUSH EAX

00407A84  PUSH 3EB

00407A89  MOV EAX,DWORD PTR SS:[EBP+8]

00407A8C  PUSH EAX

00407A8D  CALL DWORD PTR DS:[<&USER32.GetDlgItemText>]

00407A93  LEA EAX,DWORD PTR SS:[EBP-204]

00407A99  PUSH EAX

00407A9A  CALL 是男人就.00407C5F

00407A9F  ADD ESP,4

00407AA2  TEST EAX,EAX

00407AA4  JNZ 是男人就.00407AE1

; 从资源中载入字符串,是注册失败的提示信息

00407AAA  PUSH 100

00407AAF  LEA EAX,DWORD PTR SS:[EBP-104]

00407AB5  PUSH EAX

00407AB6  PUSH 4

00407AB8  MOV EAX,DWORD PTR DS:[40E200]

00407ABD  PUSH EAX

00407ABE  CALL DWORD PTR DS:[<&USER32.LoadStringA>]

; 下面就是弹出出错提示框的代码了

00407AC4  PUSH 10

00407AC6  PUSH 是男人就.0040D270

00407ACB  LEA EAX,DWORD PTR SS:[EBP-104]

00407AD1  PUSH EAX

00407AD2  MOV EAX,DWORD PTR SS:[EBP+8]

00407AD5  PUSH EAX

00407AD6  CALL DWORD PTR DS:[<&USER32.MessageBoxA>]

00407ADC  JMP 是男人就.00407B22

看到407AD6这里调用了MessageBoxA,往上看,很舒服地看到这一句:

00407AA4  JNZ 是男人就.00407AE1

一个很值得注意的跳转,是不是关键跳转呢,把JNZ改成JZ,再试试,点确定,出错提示是没有了,不过那个注册按钮还是在的,也就是说这句不是关键跳转,没有涉及到序列号的部分。再往上看,发现看在这个跳转之前有获取对话框中数据:

00407A8D  CALL DWORD PTR DS:[<&USER32.GetDlgItemText>]

那么在这里中断看看它得到了什么。F2下断点,切到程序窗口,输入用户名和序列号,确定,预料之中断下来。接着F8单步运行,再来一个YES!它得到的是123456,也就是我填的序列号,接着把序列号所在的地址存到EAX中,再将EAX压入堆栈,紧接着又一个call,嗯,这个call很值得怀疑,跟进!下面就是整个call的函数的代码了,有点长:)

00407C5F  PUSH EBP ; C函数标准开头

00407C60  MOV EBP,ESP

00407C62  SUB ESP,4   ; 局部变量,分析后面的代码可知是用来作计数器的

00407C65  PUSH EBX ; 保护寄存器

00407C66  PUSH ESI

00407C67  PUSH EDI

从代码开始可明显看出这是一个标准的C函数,接着往下看。碰到堆栈操作,这值得注意,因为call过来时有压入过一个参数,指向序列号起始地址:

00407C68  MOV EAX,DWORD PTR SS:[EBP+8]  ; 这句之后EAX指向序列号

00407C6B  XOR ECX,ECX

00407C6D  MOV CL,BYTE PTR DS:[EAX+7]    ; 得到序列号第8位

00407C70  TEST ECX,ECX ; 判断是否为0

00407C72  JE 是男人就.00407C7F    ; 是刚跳转

00407C78  XOR EAX,EAX ; EAX清零

00407C7A  JMP 是男人就.00407DC4    ; 这里跳到函数结尾

00407C7F  MOV DWORD PTR SS:[EBP-4],0    ; 计数器清零

00407C86  JMP 是男人就.00407C8E

上面的代码的作用是得到序列号第8位,并判断是否为0,学过C的人都知道在C里字符串是以0表示结束的,那么就知道这里三句是用来判断用户输入的序列号是否大于7位的了,如果大于7位,就会将EAX清零并跳到函数结束的地方,而前面已经知道,在调用这个函数之后,如果返回值为0则序列号验证失败,所以序列号是7位或7位以内的。我填的123456,是6位,所以没有跳到函数结束,接着往下分析:

00407C8B  INC DWORD PTR SS:[EBP-4]    ; 计数器加1

00407C8E  CMP DWORD PTR SS:[EBP-4],7  ; 判断计数器是否大于7

00407C92  JGE 是男人就.00407CC3       ; 大于等于就

00407C98  MOV EAX,DWORD PTR SS:[EBP-4] ; 取计数器值到EAX

00407C9B  MOV ECX,DWORD PTR SS:[EBP+8] ; 取序列号地址到ECX

00407C9E  MOV AL,BYTE PTR DS:[EAX+ECX; 取序列号第N位到AL,N为计数器值

00407CA1  PUSH EAX ; 压入堆栈,调用处理函数

00407CA2  CALL 是男人就.00407DC9  ; 将1位序列号从ASCII转换成二进制值

00407CA7  ADD ESP,4   ; 恢复堆栈

00407CAA  XOR ECX,ECX ; ECX清零

00407CAC  MOV CL,AL ; 将处理过的1位序列号存到CL

00407CAE  CMP ECX,24  ; 判断是否大于0x24

00407CB1  JLE 是男人就.00407CBE  ; 大于就出错了

00407CB7  XOR EAX,EAX ; 大于0x24,跳到函数结束,返回0

00407CB9  JMP 是男人就.00407DC4

00407CBE  JMP 是男人就.00407C8B  ; 继续处理下一位

很清楚的,上面这段代码是用来判断序列号中每一位是否符合要求,是什么要求呢,看到在每取得1位序列号之后有一句call:

00407CA2  CALL 是男人就.00407DC9

这个调用得看看,F7跟进:

00407DC9  PUSH EBP

00407DCA  MOV EBP,ESP

00407DCC  PUSH EBX

00407DCD  PUSH ESI

00407DCE  PUSH EDI

00407DCF  XOR EAX,EAX

00407DD1  MOV AL,BYTE PTR SS:[EBP+8]    ; 取参数,即1位序列号

00407DD4  CMP EAX,61

00407DD7  JL 是男人就.00407DE8

; 判断是否大于0x61,如果对ASCII码表熟悉的话可以知道0x61对应的是a

; 小于则跳转,好,上面那个判断应该不是用来判断这个字符是否为小写字母了

; 不是则跳到后面继续处理,是则进行下面的处理过程

00407DDD  XOR EAX,EAX ; EAX清零

00407DDF  MOV AL,BYTE PTR SS:[EBP+8]    ; 取字符

00407DE2  SUB EAX,20     ; 减0x20,变大写字母

00407DE5  MOV BYTE PTR SS:[EBP+8],AL ; 保存

; 不管是否为小字字母,经过上面的判断和处理都会变成大写字母,继续进行处理

00407DE8  XOR EAX,EAX ; EAX清零

00407DEA  MOV AL,BYTE PTR SS:[EBP+8]    ; 取字符

00407DED  CMP EAX,41

00407DF0  JL 是男人就.00407E01

; 判断是否大于,0x41对应的ASCII字符为A

; 小于则跳转,说明这里是判断字符是否为大写字母

; 不是则跳到后面继续处理,是则进行下面的处理过程

00407DF6  XOR EAX,EAX ; EAX清零

00407DF8  MOV AL,BYTE PTR SS:[EBP+8]    ; 取字符

00407DFB  SUB EAX,7      ; 减去7

00407DFE  MOV BYTE PTR SS:[EBP+8],AL ; 保存

; 如果不是大写字母就跳到这里了,当然如果是大写字母的话

; 经过上面的处理过程同样要进行下面的处理过程

00407E01  XOR EAX,EAX ; EAX清零

00407E03  MOV AL,BYTE PTR SS:[EBP+8]    ; 取字符

00407E06  SUB EAX,30     ; 减去0x30

00407E09  JMP 是男人就.00407E0E  ; 函数结束

00407E0E  POP EDI

00407E0F  POP ESI

00407E10  POP EBX

00407E11  LEAVE

00407E12  RETN

连贯起来看可以知道这个处理函数的过程是这样的:

char是否为小写字母 -> 是则减去0x20变成大写字母,不是则保持不变 -> char是否为大写字母 -> 是则减去7,不是则保持不变 -> char减0x30

再联系数字和字母的ASCII码值,9为0x39,A为0x41,0x41-0x39=7,OK,可以知道这个函数是把1位序列号变成二进制值,字符0到9对应数值0到9,字母全部转换成大写字母,并且字母A对应10,B对应11,后面依次类推。

弄明白了字符到数字的函数,继续看序列号验证的代码,可以看到在得到二进制值之后,又来了一句:

00407CB1  JLE 是男人就.00407CBE  ; 大于就出错了

00407CB7  XOR EAX,EAX ; 大于0x24,跳到函数结束,返回0

00407CB9  JMP 是男人就.00407DC4

00407CBE  JMP 是男人就.00407C8B  ; 继续处理下一位

0x24转换成十进制就是36,刚好是10个数字加26个字母,也就是说序列号只允许数字与字母,并且要7位,否则就通不过这个验证了。弄明白了这个,继续往下看:

00407CC3  MOV EAX,DWORD PTR SS:[EBP+8]

00407CC6  MOV AL,BYTE PTR DS:[EAX+5]  ; 取序列号第6位

00407CC9  PUSH EAX

00407CCA  CALL 是男人就.00407DC9      ; 转换成数值

00407CCF  ADD ESP,4

00407CD2  XOR EBX,EBX

00407CD4  MOV BL,AL ; 序列号第6位值保存到EBX

00407CD6  MOV EAX,DWORD PTR SS:[EBP+8]

00407CD9  MOV AL,BYTE PTR DS:[EAX+2]  ; 取序列号第3位

00407CDC  PUSH EAX

00407CDD  CALL 是男人就.00407DC9      ; 转换成数值

00407CE2  ADD ESP,4

00407CE5  XOR ECX,ECX

00407CE7  MOV CL,AL ; 序列号第3位值保存到ECX

00407CE9  MOV ESI,24   ; ESI=0x24

00407CEE  LEA EAX,DWORD PTR DS:[ECX+EBX*2+1D] ; EAX=ECX+EBX*2+1D

00407CF2  CDQ ; 扩展成64位

00407CF3  IDIV ESI ; 除以0x24

00407CF5  MOV EBX,EDX ; EDX为余数,存到EBX

00407CF7  MOV EAX,DWORD PTR SS:[EBP+8]

00407CFA  MOV AL,BYTE PTR DS:[EAX]  ; 取序列号第1位

00407CFC  PUSH EAX

00407CFD  CALL 是男人就.00407DC9    ; 转换成数值

00407D02  ADD ESP,4

00407D05  XOR ECX,ECX

00407D07  MOV CL,AL ; 序列号第1位值存到ECX

00407D09  CMP EBX,ECX ; EBX=ECX?

00407D0B  JNZ 是男人就.00407DBD  ; 不等,验证失败,跳到函数结束

; 头有些大了,用心看发现这里在判断序列号是否符合规则

; (第3位+第6位*2+0x1D)%0x24==第1位

; 跟着又是两段代码类似的,不难发现也是判断,不过规则有小小的不同

; 下面这段的判断规则是

; (第2位+第5位*2+0x1D)%0x24==第7位

00407D11  MOV EAX,DWORD PTR SS:[EBP+8]

00407D14  MOV AL,BYTE PTR DS:[EAX+4]

00407D17  PUSH EAX

00407D18  CALL 是男人就.00407DC9

00407D1D  ADD ESP,4

00407D20  XOR EBX,EBX

00407D22  MOV BL,AL

00407D24  MOV EAX,DWORD PTR SS:[EBP+8]

00407D27  MOV AL,BYTE PTR DS:[EAX+1]

00407D2A  PUSH EAX

00407D2B  CALL 是男人就.00407DC9

00407D30  ADD ESP,4

00407D33  XOR ECX,ECX

00407D35  MOV CL,AL

00407D37  MOV ESI,24

00407D3C  LEA EAX,DWORD PTR DS:[ECX+EBX*2+1D]

00407D40  CDQ

00407D41  IDIV ESI

00407D43  MOV EBX,EDX

00407D45  MOV EAX,DWORD PTR SS:[EBP+8]

00407D48  MOV AL,BYTE PTR DS:[EAX+6]

00407D4B  PUSH EAX

00407D4C  CALL 是男人就.00407DC9

00407D51  ADD ESP,4

00407D54  XOR ECX,ECX

00407D56  MOV CL,AL

00407D58  CMP EBX,ECX

00407D5A  JNZ 是男人就.00407DBD

; 下面这段的判断规则是

; (第1位+第7位*2+0x1D)%0x24==第4位

00407D60  MOV EAX,DWORD PTR SS:[EBP+8]

00407D63  MOV AL,BYTE PTR DS:[EAX+6]

00407D66  PUSH EAX

00407D67  CALL 是男人就.00407DC9

00407D6C  ADD ESP,4

00407D6F  XOR EBX,EBX

00407D71  MOV BL,AL

00407D73  MOV EAX,DWORD PTR SS:[EBP+8]

00407D76  MOV AL,BYTE PTR DS:[EAX]

00407D78  PUSH EAX

00407D79  CALL 是男人就.00407DC9

00407D7E  ADD ESP,4

00407D81  XOR ECX,ECX

00407D83  MOV CL,AL

00407D85  MOV ESI,24

00407D8A  LEA EAX,DWORD PTR DS:[ECX+EBX*2+1D]

00407D8E  CDQ

00407D8F  IDIV ESI

00407D91  MOV EBX,EDX

00407D93  MOV EAX,DWORD PTR SS:[EBP+8]

00407D96  MOV AL,BYTE PTR DS:[EAX+3]

00407D99  PUSH EAX

00407D9A  CALL 是男人就.00407DC9

00407D9F  ADD ESP,4

00407DA2  XOR ECX,ECX

00407DA4  MOV CL,AL

00407DA6  CMP EBX,ECX

00407DA8  JNZ 是男人就.00407DBD

上面这三段就是序列号验证的主要部分了,写个示意表达式,用b1~b7表示序列号第1至7位转换后的数值,T为临时变量:

T = b3 + b6*2 + 1D

b1 = T mod 24 ?

T = b2 + b5*2 + 1D

b7 = T mod 24 ?

T = b1 + b7*2 + 1D

b4 = T mod 24 ?

如果三次判断均通过,那么整个序列号验证函数就回返回1,表示验证通过:

00407DAE  MOV EAX,1   ; EAX=1

00407DB3  JMP 是男人就.00407DC4  ; 跳到返回

00407DB8  JMP 是男人就.00407DC4

00407DBD  XOR EAX,EAX ; 任何验证的情况下均返回0

00407DBF  JMP 是男人就.00407DC4

00407DC4  POP EDI

00407DC5  POP ESI

00407DC6  POP EBX

00407DC7  LEAVE

00407DC8  RETN

到这里就差不多了,序列号验证函数已经分析完成,它的验证规则也知道了,接着是写注册机了,不过我一开始没想到怎么随机产生一个序列号,只好把正确的序列号全列出来了,看了一下,有4万多个……我用C写了个列序列号的程序,用的穷举法,列出每一个序列号,判断是否可用,如果可用就打印出来,程序清单如下:

#include <stdio.h>

int main()

{

char bin2char(int bin);

int tmp,b1,b2,b3,b4,b5,b6,b7;

printf("register codes:\n");

for(b1=0;b1<36;b1++)

for(b2=0;b2<36;b2++)

for(b3=0;b3<36;b3++)

for(b4=0;b4<36;b4++)

for(b5=0;b5<36;b5++)

for(b6=0;b6<36;b6++)

for(b7=0;b7<36;b7++)

{

tmp = b3 + b6*2 + 0x1D;

if (b1!=tmp%0x24) break;

tmp = b2 + b5*2 + 0x1D;

if (b7!=tmp%0x24) break;

tmp = b1 + b7*2 + 0x1D;

if (b4!=tmp%0x24) break;

printf("%c%c%c%c%c%c%c\n",

bin2char(b1),bin2char(b2),bin2char(b3),bin2char(b4),bin2char(b5),bin2char(b6),bin2char(b7));

}

}

char bin2char(int bin)

{

if(bin<10) return(bin+0x30);

else return(bin-10+0x41);

}

函数bin2char是序列号验证过程中字符转数值的逆过程。

是男人就下100层的破解就到此结束了,后来我又看了一下序列号的规律,发现第7位总是为0,而第4位决定第1位,而第3位与第6位,第2位与第5位分别为一组。这样,随机产生b2,b3,b4就可以生成一个序列号了。另外,也可用暴力破解的方式,在序列号验证函数中直接将EXA=1并返回,这样不论什么序列号都能通过验证了:)

OK,大功告成。写的有些啰嗦,第一次写破解的文章,有错误也是难免的,欢迎指正^_^,vipxjw#163.com。