某天(前天:),看了两个小时高数(具体不详:)之后,想放松一下,找到《是男人就下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。
0 条评论。