160cm-cm056
0、前言 & 工具
0.1 前言
开始在论坛上查找关于crackme056
的资料时,发现标注crackme056
无法打开,尝试一番无果。
又在网上查找相关的内容,发现CSDN
上一篇关于crackme056
的文章,链接:“https://blog.csdn.net/zfy1996/article/details/107402027 ”,感谢文章提供的思路。
参考该文章后,慢慢研究下来,发现crackme056
很有意思,亮点及难点如下:动态解密使得程序能够正常运行、注册码算法更是在动态解密的基础上增加了难度。
从开始接触的无从下手,到后面的参考经验循序渐进,以至最后成功crack
;这个crackme
慢慢的在提升难度,且难度系数提升平滑;所以想把自己分析过的程记录一下,和大家一起交流学习。
0.2 分析工具
xdbg、ollydbg
动态调试;
IDA
静态分析;
DIE
查壳。
1、无法运行
初始拿到cm
时,都是查壳、运行观察、调试分析。
查壳,发现程序无壳;运行程序,程序没有反应,也没有报错异常;
1.1 初步静态分析
没办法了,直接上xdbg
调试,暂停在OEP
处,并且有GetVersion
调用,进一步说明程序没有加壳;
01-调试器调试
初步分析OEP
处的汇编码,发现程序最开始设置SEH
(结构化异常处理函数),回调函数的地址为0x4019F4
,可以先在回调函数处打上断点,添加注释等。
00401AD6 | 68 F4194000 | push <elraizer.2.sub_4019F4> | 压入SEH链回调函数
00401ADB | 64:A1 00000000 | mov eax, dword ptr fs:[0] | 获取当前SEH结构体的指针
00401AE1 | 50 | push eax | 压入指针,在esp处形成了一个标准的SEH结构体
00401AE2 | 64:8925 00000000 | mov dword ptr fs:[0], esp | 将标准的SEH结构体置为当前的SEH链表表头
往后分析,在调用GetVersion
后,将结果进行运算,并存储到内存变量中,且操作的内存地址处于data
区段,猜测是在初始化一些全局变量。
00401AE9 | 83EC 58 | sub esp, 0x58 |
00401AEC | 53 | push ebx |
00401AED | 56 | push esi |
00401AEE | 57 | push edi |
00401AEF | 8965 E8 | mov dword ptr ss:[ebp-0x18], esp |
00401AF2 | FF15 80914000 | call dword ptr ds:[<&GetVersion>] |
00401AF8 | 33D2 | xor edx, edx |
00401AFA | 8AD4 | mov dl, ah |
00401AFC | 8915 8C7A4000 | mov dword ptr ds:[0x407A8C], edx |
00401B02 | 8BC8 | mov ecx, eax |
00401B04 | 81E1 FF000000 | and ecx, 0xFF |
00401B0A | 890D 887A4000 | mov dword ptr ds:[0x407A88], ecx |
00401B10 | C1E1 08 | shl ecx, 0x8 |
00401B13 | 03CA | add ecx, edx |
00401B15 | 890D 847A4000 | mov dword ptr ds:[0x407A84], ecx |
00401B1B | C1E8 10 | shr eax, 0x10 |
00401B1E | A3 807A4000 | mov dword ptr ds:[0x407A80], eax |
再后面就是一些自定义的函数了,这里仅做了部分分析:
00401B23 | 6A 01 | push 0x1 |
00401B25 | E8 670B0000 | call <elraizer.2.sub_402691> | 申请内存空间
00401B2A | 59 | pop ecx |
00401B2B | 85C0 | test eax, eax |
00401B2D | 75 08 | jne elraizer.2.401B37 | 判断是否申请成功
00401B2F | 6A 1C | push 0x1C |
00401B31 | E8 C3000000 | call <elraizer.2.sub_401BF9> | 准备exitProcess
00401B36 | 59 | pop ecx |
00401B37 | E8 870A0000 | call <elraizer.2.sub_4025C3> | 填充线程数据到申请的空间中
00401B3C | 85C0 | test eax, eax |
00401B3E | 75 08 | jne elraizer.2.401B48 |
00401B40 | 6A 10 | push 0x10 |
00401B42 | E8 B2000000 | call <elraizer.2.sub_401BF9> | 准备exitProcess
00401B47 | 59 | pop ecx |
00401B48 | 33F6 | xor esi, esi |
其他的地方都是成片的call
函数地址,静态分析较为困难,所以开始动态调试分析。
1.2 动态调试查找可疑call
首先要搞清楚是哪里导致程序没有运行起来,所以前面那些已经分析过的call
在动态调试中可直接F8
步过。
调试后,发现程序退出的地址为0x00401BA7
,相应的汇编语句为call <elraizer.2.sub_401680>
。
00401B7C | 50 | push eax |
00401B7D | FF15 78914000 | call dword ptr ds:[<&GetStartupInfoA>] |
00401B83 | E8 EF030000 | call <elraizer.2.sub_401F77> | 对路径字符进行了未知运算,作用应该不大
00401B88 | 8945 9C | mov dword ptr ss:[ebp-0x64], eax |
00401B8B | F645 D0 01 | test byte ptr ss:[ebp-0x30], 0x1 |
00401B8F | 74 06 | je elraizer.2.401B97 |
00401B91 | 0FB745 D4 | movzx eax, word ptr ss:[ebp-0x2C] |
00401B95 | EB 03 | jmp elraizer.2.401B9A |
00401B97 | 6A 0A | push 0xA |
00401B99 | 58 | pop eax |
00401B9A | 50 | push eax |
00401B9B | FF75 9C | push dword ptr ss:[ebp-0x64] |
00401B9E | 56 | push esi |
00401B9F | 56 | push esi |
00401BA0 | FF15 74914000 | call dword ptr ds:[<&GetModuleHandleA>] |
00401BA6 | 50 | push eax |
00401BA7 | E8 D4FAFFFF | call <elraizer.2.sub_401680> | 退出!
说明这个call
的嫌疑很大,跟进去,运行到返回,查看ret
语句后面的数据。
发现内部有两个ret
,其中一个直接就是ret
,不过前面有jmp
跳过该ret
;第二个为ret 0x10
,说明这个call应该是带有4个参数。
00401711 | EB 0D | jmp elraizer.2.401720 |
00401713 | 8B55 EC | mov edx, dword ptr ss:[ebp-0x14] |
00401716 | 52 | push edx |
00401717 | E8 24000000 | call <elraizer.2.sub_401740> |
0040171C | C3 | ret |
0040171D | 8B65 E8 | mov esp, dword ptr ss:[ebp-0x18] |
00401720 | C745 FC FFFFFFFF | mov dword ptr ss:[ebp-0x4], 0xFFFFFFFF |
00401727 | 33C0 | xor eax, eax |
00401729 | 8B4D F0 | mov ecx, dword ptr ss:[ebp-0x10] |
0040172C | 64:890D 00000000 | mov dword ptr fs:[0], ecx |
00401733 | 5F | pop edi |
00401734 | 5E | pop esi |
00401735 | 5B | pop ebx |
00401736 | 8BE5 | mov esp, ebp |
00401738 | 5D | pop ebp |
00401739 | C2 1000 | ret 0x10 |
1.3 可疑call分析
这部分的分析有点困难,在参考资料后,才发现导致程序退出的call
其实是WinMain
,前面的GetStartupInfoA
、GetModuleHandleA
都是获取进程的信息。
//参数部分
00401B8F | 74 06 | je elraizer.2.401B97 |
00401B91 | 0FB745 D4 | movzx eax, word ptr ss:[ebp-0x2C] |
00401B95 | EB 03 | jmp elraizer.2.401B9A |
00401B97 | 6A 0A | push 0xA |
00401B99 | 58 | pop eax |
00401B9A | 50 | push eax | 参数4
00401B9B | FF75 9C | push dword ptr ss:[ebp-0x64] | 参数3
00401B9E | 56 | push esi | 参数2
00401B9F | 56 | push esi |
00401BA0 | FF15 74914000 | call dword ptr ds:[<&GetModuleHandleA>] |
00401BA6 | 50 | push eax | 参数1
//call调用地址
00401BA7 | E8 D4FAFFFF | call <elraizer.2.sub_401680> | 退出!
进入WinMain
内部分析,这里直接先给出分析后的结果,再慢慢说明。
02-WinMain内部分析
1.3.1 call sub_4017E0
分析
先分析第一个call
,外层调用查看可能传入了3个参数,初步记录为sub_4017E0(0x401620,eax,0x14)
;第一个参数可能为地址,第二个参数是是WinMain
外部传入的数据,第三个参数为常数0x14
。
004016AD | 8B45 14 | mov eax, dword ptr ss:[ebp+0x14] | 从堆栈获取传入的参数
004016B0 | A3 20784000 | mov dword ptr ds:[0x407820], eax |
004016B5 | 6A 14 | push 0x14 | 参数3
004016B7 | 50 | push eax | 参数2
004016B8 | 68 20164000 | push <elraizer.2.sub_401620> | 参数1
004016BD | E8 1E010000 | call <elraizer.2.sub_4017E0> | call
F7
跟入call
内部,查看返回为ret 0xC
,证实这个call
确实是带有3个参数;
004017E0 <elr | 8B4C24 04 | mov ecx, dword ptr ss:[esp+0x4] |
004017E4 | 8B4424 0C | mov eax, dword ptr ss:[esp+0xC] |
004017E8 | 56 | push esi |
004017E9 | 33F6 | xor esi, esi |
004017EB | 8D1401 | lea edx, dword ptr ds:[ecx+eax] |
004017EE | 3BCA | cmp ecx, edx |
004017F0 | 73 1E | jae elraizer.2.401810 |
004017F2 | 8A4424 0C | mov al, byte ptr ss:[esp+0xC] |
004017F6 | 53 | push ebx |
004017F7 | 57 | push edi |
004017F8 | 8A19 | mov bl, byte ptr ds:[ecx] |
004017FA | 32D8 | xor bl, al |
004017FC | 0FBEFB | movsx edi, bl |
004017FF | 8819 | mov byte ptr ds:[ecx], bl |
00401801 | 03F7 | add esi, edi |
00401803 | 41 | inc ecx |
00401804 | 3BCA | cmp ecx, edx |
00401806 | 72 F0 | jb elraizer.2.4017F8 |
00401808 | 5F | pop edi |
00401809 | 8BC6 | mov eax, esi |
0040180B | 5B | pop ebx |
0040180C | 5E | pop esi |
0040180D | C2 0C00 | ret 0xC //ret 0xC
00401810 | 8BC6 | mov eax, esi |
00401812 | 5E | pop esi |
00401813 | C2 0C00 | ret 0xC //ret 0xC
call sub_4017E0
内部代码不长,且没有其他的call
跳转,可单步着手分析,大致跟了几遍,发现重点汇编代码如下:
//call sub_4017E0内部
004017E9 | 33F6 | xor esi, esi //esi初始化为0
...
004017F8 | 8A19 | mov bl, byte ptr ds:[ecx]//取出ecx地址数据
004017FA | 32D8 | xor bl, al //进行异或运算
004017FC | 0FBEFB | movsx edi, bl //存储结果到edi
004017FF | 8819 | mov byte ptr ds:[ecx], bl//将结果存到原来地址处
00401801 | 03F7 | add esi, edi //esi=esi+edi
...
00401809 | 8BC6 | mov eax, esi
...
00401810 | 8BC6 | mov eax, esi
//call sub_4017E0调用处
004016B5 | 6A 14 | push 0x14 | 参数3
004016B7 | 50 | push eax | 参数2
004016B8 | 68 20164000 | push <elraizer.2.sub_401620> | 参数1
004016BD | E8 1E010000 | call <elraizer.2.sub_4017E0> | call
004016C2 | 8945 E4 | mov dword ptr ss:[ebp-0x1C], eax |
004016C5 | 3D AC020000 | cmp eax, 0x2AC |
004016CA | 75 54 | jne elraizer.2.401720 | 此处跳过了DialogBoxParamA
004016CC | E8 4FFFFFFF | call <elraizer.2.sub_401620> |
相信脱壳的朋友们很熟悉这种类型汇编码,解密数据,且esi
存储每个解密结果的和,最后将esi
的值赋给eax
,外部call sub_4017E0
调用后紧接着就对eax
的值进行判断,不相等则跳过了DialogBoxParamA
;且下一个call
的地址刚好是call sub_4017E0
的第一个参数。
为了显示窗口,所以call sub_4017E0
外部不能执行跳转;分析到这里,可以确定call sub_4017E0
是解密函数,解密完成后,判断解密标志是否正确,正确则执行刚才解密完成的内容。
1.3.2 call sub_4017E0
解密函数 IDA伪代码
方便理解,可使用IDA
进行静态分析,查看call sub_4017E0
的C++
伪代码如下:
//call sub_4017E0 IDA伪代码
int __stdcall sub_4017E0(unsigned int a1, char a2, int a3)
{
_BYTE *v3; // ecx
int v4; // esi
char v5; // bl
v3 = (_BYTE *)a1;
v4 = 0;
if ( a1 >= a1 + a3 )
return 0;
do
{
v5 = a2 ^ *v3;
*v3 = v5;
v4 += v5;
++v3;
}
while ( (unsigned int)v3 < a1 + a3 );
return v4;
}
结合IDA
伪代码,可得到call sub_4017E0
的函数原型为:decode(addr,key,count)
;addr
为解密的起始地址,key
为异或运算的数值,count
为解密区段的大小,函数返回4字节大小的flag
标志,用于验证是否正确解密。
1.3.3 call sub_4017E0
解密函数 C++ 代码 实现
可以很容易的实现decode(addr,key,count)
,这里给出C++
版本的解密代码:
#include<iostream>
//解密程序段
void decodeByte(char* decodeAddr,char key,char count,long& flag)
//char* decodeAddr(解密起始地址),char key(解密key,进行异或运算,所以该函数既能加密又能解密),char count(解密区段大小)
{
flag=0;
for(long i=0;i<count;i++)
{
decodeAddr[i]=decodeAddr[i]^key;
flag=flag+decodeAddr[i];
}
}
//从xdbg中复制解密段的字节码
//count=0x14 addr=401620 key=unknown
char _401620[]={
0x54, 0x8A, 0xEC, 0x52, 0x57, 0x56, 0xEA, 0x04,
0xAC, 0x22, 0xCD, 0x54, 0xFE, 0x6B, 0x01, 0x69,
0x7C, 0x71, 0x41, 0x01
};
//调用示例
void demo()
{
long flag=0;
char _401620_[0x14]={0};
//经过观察,key是使用al存储的,所以最大为0xFF
for(long i=0;i++;i<0x100)
{
//每次解密前重置flag为0
flag=0;
//由于解密会更改数据内容,所以也需要每次重置
memcpy(_401620_,_401620,0x14);
decodeByte(_402160,i,0x14,flag);
if(flag==0x2AC)
{
printf("the correct key is: 0x%02X\t",i);
}
}
}
最终发现正确的解密key
为0x1
,跟踪堆栈值,发现解密key
来源于WinMain
的参数4,所以只需将push 0xA
该为push 0x1
即可。
//参数部分
00401B8F | 74 06 | je elraizer.2.401B97
00401B91 | 0FB745 D4 | movzx eax, word ptr ss:[ebp-0x2C]
00401B95 | EB 03 | jmp elraizer.2.401B9A
00401B97 | 6A 0A | push 0xA //更改为push 0x1
00401B99 | 58 | pop eax
00401B9A | 50 | push eax //参数4,更改的参数
00401B9B | FF75 9C | push dword ptr ss:[ebp-0x64] //参数3
00401B9E | 56 | push esi //参数2
00401B9F | 56 | push esi
00401BA0 | FF15 74914000 | call dword ptr ds:[<&GetModuleHandleA>]
00401BA6 | 50 | push eax //参数1
//call调用地址
00401BA7 | E8 D4FAFFFF | call <elraizer.2.sub_401680> WinMain
解密成功时如下:
03-解密区段01
2、梅开二度-无法运行02
打上补丁后,准备运行看看程序界面,程序还是没反应;一番调试无果,再次查找资料,发现前面只是这个CrackMe
的“开胃菜”,后面还有更多“惊喜”。
2.1 int 3 异常
查阅参考资料发现程序还有一个int 3
异常位于0x40153C
,且该异常是由decode(addr,key,count)
解密函数解密后产生,所以需要再次分析解密函数,使得解密后的字节码[0x40153C]
变为0x90 nop
。虽说有点麻烦,但还是能够继续分析,不至于无从下手。
//int 3异常外部调用
004016E4 | E8 B7FDFFFF | call <elraizer.2.sub_4014A0> //解密int 3异常
004016E9 | 3D 12000000 | cmp eax, 0x12 //解密falg比较
004016EE | 75 05 | jne elraizer.2.4016F5
004016F0 | E8 1BFEFFFF | call <elraizer.2.sub_401510> //内部存在int3异常
...
//调用call内部int 3异常,存在花指令,干扰静态分析
0040153C | CC | int3 //int3异常处
0040153D | EB 05 | jmp elraizer.2.401544 //jmp 花指令
分析发现在call sub_4014A0
内部存在解密函数,动态单步调试,得到解密函数decode(0x401530,0X1,0X14)
,囊括了异常字节码[0x40153C]
,解密过程[0x40153C]
由0xCD->0xCC
,对应[0x40153C] xor 0x1=0xCD xor 0x1=0xCC
,所以可以反推0x90 xor 0x1=0x91=[0x40153C]
;直接补丁修改即可。
解决int 3
后,对应解密函数的flag
也会变化,这里由两个思路:①nop
解密flag
的判断跳转即可;②修改比较的flag
值,这里得到的flag=0xFFFFFFD6
。
004014DA | B9 10154000 | mov ecx, <elraizer.2.sub_401510> |
004014DF | 0BC2 | or eax, edx |
004014E1 | 6A 14 | push 0x14 | 解密区段大小count为0x14
004014E3 | 83C1 20 | add ecx, 0x20 |
004014E6 | 50 | push eax | 解密key为0x1
004014E7 | 51 | push ecx | 解密起始地址addr为:0x401530
004014E8 | E8 F3020000 | call <elraizer.2.sub_4017E0> | 解密04:0x401530~0x401544;共计0x14个字节,^0x1
一番修正补丁后,程序已经可以正常运行。
04-程序正常运行
2.2 补丁方法
像前面的动态解密int 3
异常,补丁方式可能不同于平常的直接修改,不过基本思想方法大同小异。
2.2.1 使用调试器修正补丁
使用调试器修正要在解密函数未进行解密之前,xdbg->右键->二进制编辑
修正字节码,应用补丁即可。
05-xdbg 修正补丁
2.2.2 使用010 edit修正文件
同样先使用调试器获取文件偏移,xdbg->右键->复制->文件偏移
复制汇编指令的文件偏移,这里的偏移=0x153C
,打开010 edit
定位修正即可。
06-010 edit 修正补丁
3、 其他分析补充
以上部分仅针对程序无法运行处分析,还有其他有意思的地方没有涉及,这部分做出补充。
3.1 解密函数补充
到目前为止,程序的解密函数总共有两个,分别为:call sub_4017E0、call sub_401000
。
上面仅分析了call sub_4017E0
,由于是动态解密汇编字节码,以字节为一个解密单位,所以这里更改函数原型为:decodeByte(addr,key,count)
,具体作用与实现可参考IDA
伪代码和在上面的C++
示例代码。
3.1.1 call sub_401000
补充分析
这里分析call sub_401000
,查看call sub_401000
内部返回处为ret 0x8
,可知带有两个参数;第一个参数仍然为内存地址,第二个参数可能为0、1。
//外部调用01
0040162D | 6A 00 | push 0x0 | 参数2
0040162F | 68 7D704000 | push elraizer.2.40707D | 参数1
00401634 | E8 C7F9FFFF | call <elraizer.2.sub_401000> | call
...
//外部调用02
00401650 | 6A 01 | push 0x1 | 参数2
00401652 | 68 7D704000 | push elraizer.2.40707D | 参数1
00401657 | E8 A4F9FFFF | call <elraizer.2.sub_401000> | call
...
//call sub_401000 内部
00401000 <elr | 56 | push esi |
00401001 | 8B7424 08 | mov esi, dword ptr ss:[esp+0x8] |
00401005 | 56 | push esi |
00401006 | FF15 10784000 | call dword ptr ds:[<&lstrlenA>] |
0040100C | 03C6 | add eax, esi |
0040100E | 3BF0 | cmp esi, eax |
00401010 | 73 22 | jae elraizer.2.401034 |
00401012 | 53 | push ebx |
00401013 | 8A5C24 10 | mov bl, byte ptr ss:[esp+0x10] |
00401017 | 8A06 | mov al, byte ptr ds:[esi] |
00401019 | 84DB | test bl, bl |
0040101B | 75 04 | jne elraizer.2.401021 |
0040101D | 04 40 | add al, 0x40 |
0040101F | EB 02 | jmp elraizer.2.401023 |
00401021 | 04 C0 | add al, 0xC0 |
00401023 | 8806 | mov byte ptr ds:[esi], al |
00401025 | 46 | inc esi |
00401026 | 56 | push esi |
00401027 | FF15 10784000 | call dword ptr ds:[<&lstrlenA>] |
0040102D | 03C6 | add eax, esi |
0040102F | 3BF0 | cmp esi, eax |
00401031 | 72 E4 | jb elraizer.2.401017 |
00401033 | 5B | pop ebx |
00401034 | 5E | pop esi |
00401035 | C2 0800 | ret 0x8 | 0x8
查看call sub_401000
内部,发现lstrlenA
,返回字符串长度,且传入的参数从ss:[esp+0x8]
中获取,正是传入的内存地址参数,这里可以猜测call sub_401000
与字符串相关。
查看[0x40707D]
地址处的内容如下,为不可识别的字符内容,这里可直接上手分析call sub_401000
内部;由于call sub_401000
内部除了call lstrlenA
外没有其他的函数调用,可运行到返回后在观察[0x40707D]
地址处的内容。
//call sub_401000之前,0x40707D
0040707D 05 29 30 00 .)0.
...
//call sub_401000运行到返回,0x40707D
0040707D 45 69 70 00 Eip.
对比前后[0x40707D]
地址处的内容,不难发现call sub_401000
的作用就是解码字符串,结合call sub_401000
内部字节码分析,可得到关键解码语句如下。
00401013 | 8A5C24 10 | mov bl, byte ptr ss:[esp+0x10]//获取参入参数2
00401017 | 8A06 | mov al, byte ptr ds:[esi] //取出地址内容
00401019 | 84DB | test bl, bl //判断参数2是否为0
0040101B | 75 04 | jne elraizer.2.401021
0040101D | 04 40 | add al, 0x40 //参数2等于0
0040101F | EB 02 | jmp elraizer.2.401023
00401021 | 04 C0 | add al, 0xC0 //参数2不等于0
00401023 | 8806 | mov byte ptr ds:[esi], al //解码后填回
根据参数2的值,分别加上不同的数值解码:参数2等于0时,字符串逐字符+0x40
,否则逐字符+0xC0
。
刚好有0x40+0xC0=0x100
,对于字节码来说,可以认为+0x40
为解码,结果再+0xC0
为加密,所以参数2就是一个标志位,标识字符串加解密的方式。如果觉得难以理解,可借助IDA
的伪代码帮助分析。
//call sub_401000 IDA伪代码
char *__stdcall sub_401000(char *a1, char a2)
{
char *v2; // esi
char *result; // eax
char v4; // al
char v5; // al
v2 = a1;
result = &a1[dword_407810(a1)];
if ( a1 < result )
{
do
{
v4 = *v2;
if ( a2 )
v5 = v4 - 64; // a2即参数2等于1,-64相当于+0xC0
else
v5 = v4 + 64; // a2即参数2等于0,+64相当于+0x40
*v2++ = v5;
result = &v2[dword_407810(v2)];
}
while ( v2 < result );
}
return result;
}
3.1.2 call sub_401000
解密函数 C++ 代码 实现
所以可得到call sub_401000
的函数原型为decodeStr(addr,mode)
,这里给出C++
版本的解码代码:
#include<iostream>
//解码字符串
void decodeStr(char* decodeAddr,char decodeMode)
{
long len=strlen(decodeAddr);
char key=(decodeMode==0?0x40:0xC0);
for(long i=0;i<len;i++)
{
decodeAddr[i]=decodeAddr[i]+key;
}
}
//从xdbg中复制解码段的字符串
//addr=40707D "Eip"
char _40707D[]={
0x05, 0x29, 0x30, 0x00
};
//addr=407051 "FrogsICE"
char _407051[]={
0x06, 0x32, 0x2F, 0x27, 0x33, 0x09, 0x03, 0x05, 0x00
};
//调用示例
void demoStr()
{
printf("[40707D] raw content: %s\n",_40707D);
decodeStr(_40707D,0);
printf("[40707D] decode: %s\n",_40707D);
decodeStr(_40707D,1);
printf("[40707D] encode: %s\n",_40707D);
printf("\n[407051] raw content: %s\n",_407051);
decodeStr(_407051,0);
printf("[407051] decode: %s\n",_407051);
decodeStr(_407051,1);
printf("[407051] encode: %s\n",_407051);
}
分析了decodeStr(addr,mode)
字符串解码函数,解码作用猜测是反调试,影响后续解密key
的数值,并且解码使用之后马上又是加密成原始字节码!
07-字符解码反调试
3.2 解密区段整理
到目前为止,所有解密函数解密的区段、函数原型、调用等都整合在下表。
序号 |
函数原型 |
备注 |
外部调用 |
|
解密01 |
decodeByte(0x401620,0x1,0x14) |
解密下一个执行的函数 |
0x4016BD |
|
解密02 |
decodebyte(0x4014F0,0x1,0x13) |
解密下一个执行的函数、Eip 反调试 |
0x4016CC |
|
解密03 |
decodebyte(0x4014A0,0x1,0x14) |
解密下一个执行的函数 |
0x4016D8 |
|
解密04 |
decodebyte(0x401510+0x20,0x1,0x14) |
解密下一个执行的函数、FrogsICE 反调试 |
0x4016E4 |
|
解密05 |
decodebyte(0x401620,0x2D,0x32) |
解密下一个执行的函数 |
0x4016F0 |
|
解密06 |
decodebyte(0x401524,0xC,0x14) |
破坏关键已执行代码,干扰分析(后面提及) |
0x4016F0 |
|
程序的无法运行问题已经解决,现在可以着手分析注册算法了。
4、注册算法初步探索
经过上面的分析,程序能够正常运行,而且是通过DialogBoxParam
创建的,直接定位窗口消息处理回调函数,分析注册算法。
08-窗口创建参数分析
4.1 分析窗口回调函数
回调函数原型为:LRESULT DefWindowProc(HWND hwnd,UINT uMsg,WPARAM wParam,LPARAM lParam)
定位0x4013F0
,回调函数的过程很清晰,取出uMsg
参数,判断窗口消息类型;粗略分析,部分消息类型都在下图做了标注。
09-窗口回调函数分析_0x4013F0
从按钮控件消息处开始进一步分析,首先便是两个常用API
,GetDlgItemTextA、lstrcmp
分别获取输入、比较字符,在lstrcmp
处下断点,查看传入参数。
0040144F | A1 24784000 | mov eax, dword ptr ds:[0x407824] //控件按钮消息开始
00401454 | 68 04010000 | push 0x104
00401459 | 68 3C784000 | push elraizer.2.40783C
0040145E | 6A 64 | push 0x64
00401460 | 50 | push eax
00401461 | FF15 30784000 | call dword ptr ds:[<&GetDlgItemTextA>]//获取输入
00401467 | 8B0D 90704000 | mov ecx, dword ptr ds:[0x407090]
0040146D | 41 | inc ecx
0040146E | 51 | push ecx //参数2
0040146F | 68 3C784000 | push elraizer.2.40783C //参数1
00401474 | FF15 04784000 | call dword ptr ds:[<&lstrcmp>] //字符比较
0040147A | 8B15 88704000 | mov edx, dword ptr ds:[0x407088]
00401480 | A1 8C704000 | mov eax, dword ptr ds:[0x40708C]
00401485 | 3BD0 | cmp edx, eax
00401487 | 75 0E | jne elraizer.2.401497
00401489 | A1 24784000 | mov eax, dword ptr ds:[0x407824]
0040148E | 6A 01 | push 0x1
00401490 | 50 | push eax
00401491 | FF15 48794000 | call dword ptr ds:[<&EndDialog>] //退出消息事件
00401497 | 33C0 | xor eax, eax
00401499 | C2 1000 | ret 0x10
4.2 下断分析lstrcmp
参数
随意输入字符,单击按钮,断在lstrcmp
处,参数内容如下图;多次调试,发现除去输入的字符外,另一个参数始终为"Kernel32.dll"
,所以可以大胆猜测serial=“Kernel32.dll”
。
10-lstrcmp下断查看传入参数
当输入"Kernel32.dll"
尝试后,发现程序没有任何反应;回到按钮消息事件中,发现比较之后,紧接着比较的结果eax
被[0x40708C]
地址处的数据覆盖,再去比较,判断是否需要退出窗口。
进过多次单步调试后,也发现没有其他的语句被执行,难道这个crack
是没有写成功的界面?做了这么久的crack
就要止步于此吗?在这里陷入了死循环,纠结无果后,再次参考资料寻求帮助,发现这个crack
的设计“别有洞天”!
5、梅开三度-再度分析解密函数解密区段
5.1 真假窗口消息回调函数分析
参考资料后发现,主窗口的回调信息处理函数存储在[0x407084]
中,而[0x407084]
的值有两种情况;一种是默认值,即[0x407084]
初始为0x4013F0
;另一种是上面的解密05解密正确,则执行mov [0x407084],0x401310
,将[0x407084]
更改为0x401310
。
前面显示窗口的回调函数便是[0x407084]
的初始值0x4013F0
,到最后仅仅是调用lstrcmpA
比较,然后就没有下文了;显然可知0x401310
才是正确的回调函数地址。
11-解密WinProc回调函数
而0x401310
又经过一次解密,解密函数原型为decodeByte(0x401310,key,0x32)
;
所以需要回头去分析解密函数,获取正确解密key
,解密key
的初始化关键位置如下:
①:0x40152B:mov dword ptr ds:[0x4070C4], 0x5A2D
,其中0x401531=0x2D,0x4015320x5A
;
②:0x401563:mov dword ptr ds:[0x407944], eax
。
大致流程代码如下:
0040152B | C705 C4704000 2D5A000 | mov dword ptr ds:[0x4070C4], 0x5A2D //[0x4070C4]=0x5A2D,设置解密key的关键代码
...
0040155D | 33C0 | xor eax, eax //eax=0
0040155F | 90 | nop
00401560 | 90 | nop
00401561 | 90 | nop
00401562 | 90 | nop
00401563 | A3 44794000 | mov dword ptr ds:[0x407944], eax//[0x407944]=0
...
0040156F | 6A 32 | push 0x32 //count=0x32
00401571 | 8B0D 44794000 | mov ecx, dword ptr ds:[0x407944]//ecx=0
00401577 | 030D C4704000 | add ecx, dword ptr ds:[0x4070C4]//ecx=0+0x5A2D
0040157D | 51 | push ecx //key=ecx=0x5A2D
0040157E | 68 10134000 | push <elraizer.2.sub_401310> //addr=0x401310
00401583 | E8 58020000 | call <elraizer.2.sub_4017E0> //call
5.2 C++ 代码遍历得出正确解密key
解密函数和前面都是相同,所以也可使用前面遍历的方法,比对解密flag 与 0xFFFFFD8C
,这里结合前面给出解密的C++
代码,稍作修改,遍历寻找正确解密key
的代码:
#include<iostream>
//解密程序段
void decodeByte(char* decodeAddr,char key,char count,long& flag)
//char* decodeAddr(解密起始地址),char key(解密key,进行异或运算,所以该函数既能加密又能解密),char count(解密区段大小)
{
flag=0;
for(long i=0;i<count;i++)
{
decodeAddr[i]=decodeAddr[i]^key;
flag=flag+decodeAddr[i];
}
}
//从xdbg中复制解密段的字节码
//count=0x32 addr=401310 key=unknown
char _401310[]={
0x43, 0x9D, 0xFA, 0x95, 0xFA, 0x02, 0x45, 0x40,
0x41, 0x9D, 0x53, 0x1A, 0x9F, 0x53, 0xEE, 0x9D,
0x5B, 0xEE, 0x9F, 0x5B, 0xE2, 0x95, 0x7B, 0xE2,
0x06, 0x95, 0x6B, 0xE2, 0x16, 0x19, 0x92, 0x98,
0x16, 0x16, 0x16, 0x97, 0x7B, 0xE2, 0x16, 0x17,
0x16, 0x16, 0x95, 0x6B, 0xE2, 0x16, 0x19, 0x92,
0x98, 0x16
};
//调用示例
void demoByte()
{
long flag=0;
char _401310_[0x32]={0};
//经过观察,key是使用al存储的,所以最大为0xFF
for(long i=0;i<0x100;i++)
{
//每次解密前重置flag为0
flag=0;
//由于解密会更改数据内容,所以也需要每次重置
memcpy(_401310_,_401310,0x32);
decodeByte(_401310_,i,0x32,flag);
if(flag==0xFFFFFD8C)
{
printf("the correct key is: 0x%02X\n",i);
}
}
}
运行得到两个解密key
:key1=0x16、key2=0xD6
,最后解密的flag
均等于0xFFFFFD8C
;经过测试试验,仅当key=0x16
时才能够正常显示窗口。
所以最终回调函数WinProc
的解密函数原型为:decodeByte(0x401310,0x16,0x32)
;
5.3 补丁修正
已经得出正确解密key=0x16
,且影响key
数值的地址在0x401531、0x401563
,由于该程序后面的解密call
均是由前面的解密call
解密出来的,所以还要去分析解密0x401531、0x401563
的call
。
分析发现解密处位于下图处,解密原型为:decodeByte(0x401530,0x1,0x14)
;
12-解密WinProc函数key赋值处
这里打补丁与前面的一致,不过这里由于影响key
数值有两个地方:一个是修改初始值为0x5A16
;另一个是修改随后的eax
为0x??????E9
,"?"
表示任意值,只需使得相加后的cl=0x16
即可。
这里仅演示第一种补丁方式,第二种修改部分与修改方式较多,不过原理与第一种相同,可自己尝试修改。
解密过程[0x401531]
由0x2C->0x2D
,对应[0x401531] xor 0x1=0x2C xor 0x1=0x2D
,所以可以反推反推0x16 xor 0x1=0x17=[0x401531]
。
所以最终补丁为[0x401531]:0x2C->0x17
。
针对第二种补丁方法,最终修改成果如下,仅供参考:
//修改前
00401558 | A1 44794000 | mov eax, dword ptr ds:[0x407944] |
0040155D | 33C0 | xor eax, eax |
0040155F | 90 | nop |
00401560 | 90 | nop |
00401561 | 90 | nop |
00401562 | 90 | nop |
00401563 | A3 44794000 | mov dword ptr ds:[0x407944], eax |
//修改后
00401558 | A1 44794000 | mov eax, dword ptr ds:[0x407944] |
0040155D | B0 E9 | mov al, 0xE9 |
0040155F | 90 | nop |
00401560 | 90 | nop |
00401561 | 90 | nop |
00401562 | 90 | nop |
00401563 | A3 44794000 | mov dword ptr ds:[0x407944], eax |
经过二次补丁后,还需要注意解密flag
的校验,如果前面int 3
异常的解密flag
判断没有使用nop
填充,这里还需要再次更改解密flag
的比对值,第一种补丁方法的比对值为0xFFFFFFBF
。
成功显示主窗口界面,且窗口回调处理函数为0x401310
。
13-回调处理函数为0x401310
6、梅开二度-注册算法再度探索
程序初始无法运行->成功显示窗口但主窗口回调函数错误->成功显示窗口且回调函数正确,一路曲折蜿蜒,到达最关键的注册算法分析,有种手撕程序外壳的感觉:(
6.1 动静态结合分析
前面已经分析得到正确的WinProc
窗口回调函数[0x407084]=0x401310
,重点分析0x401310
处即可。
首先要确定按钮事件,粗略分析如下图:
14-回调函数窗口消息类型分析
也可通过GetDlgItemTextA
定位关键处,接着就是两个必然执行call sub_0x4011F0、call sub_0x401180
与可能执行的call sub_0x4012C0
;
第一个call sub_0x4011F0
,接收输入serial
作为唯一参数,函数原型为sub_0x4011F0(serial)
,单步步过调试发现返回一个整数,存储在eax
中,初步猜测为加密输入的serial
,返回signOfSerial
。
第二个call sub_0x401180
,接收三个参数;第一个参数为固定内存地址0x4012E6
,第二个参数为sub_0x4011F0
计算的结果,第三个参数为固定常数0x6
,函数原型为sub_0x401180(0x4012E6,sub_0x4011F0(serial),0x6)
。
第三个call sub_0x4012C0
,没有参数,且调用了4次字符加解码函数decodeStr(addr,mode)
,开始时分别对[0x407065]、[0x40705D]
处的两个字符串统一解码,最后又统一加码;完成call sub_0x4012C0
之后便会jmp
到退出住窗口的消息处理过程。
0040136B | 68 04010000 | push 0x104 //参数4:maxCount
00401370 | 68 3C784000 | push elraizer.2.40783C //参数3:buffer
00401375 | 6A 64 | push 0x64 //参数2:控件ID
00401377 | 8B0D 24784000 | mov ecx, dword ptr ds:[0x407824]
0040137D | 51 | push ecx //参数1:hwnd
0040137E | FF15 30784000 | call dword ptr ds:[<&GetDlgItemTextA>]
00401384 | 68 3C784000 | push elraizer.2.40783C //参数1:buffer
00401389 | E8 62FEFFFF | call elraizer.2.4011F0 //call_01:4011F0
0040138E | 8945 FC | mov dword ptr ss:[ebp-0x4], eax //存储结果
00401391 | 6A 06 | push 0x6 //参数3:0x6
00401393 | 8B55 FC | mov edx, dword ptr ss:[ebp-0x4]
00401396 | 52 | push edx //参数2:call_01结果
00401397 | B8 C0124000 | mov eax, <elraizer.2.sub_4012C0>
0040139C | 83C0 26 | add eax, 0x26
0040139F | 50 | push eax //参数1:0x4012E6
004013A0 | E8 DBFDFFFF | call elraizer.2.401180 //call_02:401180
004013A5 | 8945 FC | mov dword ptr ss:[ebp-0x4], eax
004013A8 | 817D FC 5B8E0000 | cmp dword ptr ss:[ebp-0x4], 0x8E5B
004013AF | 75 0C | jne elraizer.2.4013BD
004013B1 | EB 05 | jmp elraizer.2.4013B8
...
004013B8 | E8 03FFFFFF | call elraizer.2.4012C0 //call_03:0x4012C0
经过前面的分析,其实对于第二个call sub_0x401180
的函数原型应该是比较熟悉的,又发现传入的内存地址参数0x4012E6
,正好在第三个call sub_0x4012C0
内部。
所以,可以猜测第二个call sub_0x401180
为解密参数;动态调试后,分析关键解密段如下:
004011B3 | 8B45 FC | mov eax, dword ptr ss:[ebp-0x4]//局部变量01
004011B6 | 0FBF08 | movsx ecx, word ptr ds:[eax] //取出地址数据
004011B9 | 334D 0C | xor ecx, dword ptr ss:[ebp+0xC]//与参数2异或
004011BC | 8B55 FC | mov edx, dword ptr ss:[ebp-0x4]//局部变量01
004011BF | 66:890A | mov word ptr ds:[edx], cx //更新数据到地址
004011C2 | EB 05 | jmp elraizer.2.4011C9
//jmp 跳转处
004011C9 | 8B45 FC | mov eax, dword ptr ss:[ebp-0x4]//局部变量01
004011CC | 0FBF08 | movsx ecx, word ptr ds:[eax] //取出已更新数据
004011CF | 8B55 F8 | mov edx, dword ptr ss:[ebp-0x8]//局部变量02
004011D2 | 03D1 | add edx, ecx //相加
004011D4 | 8955 F8 | mov dword ptr ss:[ebp-0x8], edx//结果存储
...
004011D9 | 8B45 F8 | mov eax, dword ptr ss:[ebp-0x8]//局部变量02
进一步完善函数原型为:decodeWord(addr,key,count)
;最后的flag
为已解密数据的总和,校验值为0x8E5B
。
到目前为止,想要成功,必须要获取正确的解密,关键点就在解密key
的获取方式上!
6.2 爆破补丁修改
对于这种校验flag
的解密方式,仍然采用遍历的方法得到正确的解密key
,和前面的代码模式都差不多,这里给出C++
版本的代码以供参考:
#include<iostream>
//解密程序段
void decodeWord(short* decodeAddr,short key,char count,long& flag)
//short* decodeAddr(解密起始地址),short key(解密key,进行异或运算,所以该函数既能加密又能解密),char count(解密区段大小)
{
flag=0;
for(long i=0;i<count;i++)
{
decodeAddr[i]=decodeAddr[i]^key;
//printf("%04X ",decodeAddr[i] & 0xFFFF);
flag=flag+decodeAddr[i];
}
}
//从xdbg中复制解密段的字节码
//count=0x6 addr=4012E6 key=unknown
short _4012E6[]={
0xEDB4, 0x8057, 0xF80B
};
//调用示例
void demoWord()
{
long flag=0;
short _4012E6_[0x3]={0};
//经过观察,key是使用al存储的,所以最大为0xFF
for(long i=0;i<0x10000;i++)
{
//每次解密前重置flag为0
flag=0;
//由于解密会更改数据内容,所以也需要每次重置
memcpy(_4012E6_,_4012E6,0x3*sizeof(short));
decodeWord(_4012E6_,i,0x3,flag);
if(flag==0x8E5B)
{
printf("the correct key is: 0x%04X\n",i);
}
}
}
最终得出正确的解密key=0xF84B
,咱们可以先手动在调试器中更改key
。
15-更改解密key为0xF84B并查看结果
看看会出现什么效果?并且观察0x4012E6
处解密解密出的汇编代码。
16-解密成功0x4012E6处汇编码
6.3 C++
实现 signOfSerial
算法
前面的爆破方法,虽然绕过了signOfName
的生成算法,但也获取了正确的signOfSerial
,即上面解密函数的key=0xF84B
,所以只要输入的serial
经过计算获取的signOfSerial=0xF84B
。
大致方向确定了,开始着手分析call sub_0x4011F0
,函数原型可修正为 long getSign(char* serial)
;
定位到0x4011F0
,发现内部的运算并不是太复杂,仅仅是一个大循环,也没有其他call
调用;
唯一算得上麻烦的就是jmp xxxx
类型的花指令,不过都只是单方向的简单跳转,干扰并不大,可直接nop
掉干扰的部分再分析;
虽然jmp xxxx
指令有一点干扰,但是,初步分析后发现每个jmp xxxx
后的内容都是一个可独立的运算,这样更容易分析算法;相当于是把几个算式分开放置,却并没有打乱计算的大致逻辑,仅仅是按块分隔罢了。
当然也可借助IDA
辅助分析,也是可以过掉jmp xxxx
的干扰指令。
大致流程:遍历字符串,取当前循环字符的ascii
码,分六个小块按顺序进行一系列运算。
这部分的分析并不难,直接给出C++
版本的算法以供参考:
#include<iostream>
long SAR4(long num)
{
long sarResult=0;
_asm
{
//sar 带符号右移,结果均存储到sarResult,可考虑编写出单独的函数以方便调用
push eax
mov eax,num
sar eax,0x4
mov sarResult,eax
pop eax
}
return sarResult;
}
void getSignOfSerial(char* serial,short* signOfSerial)//通过serial获取signOfSerial
{
long result=0;
long temp=0;
char asciiCodeOfSerialI=0;
do
{
asciiCodeOfSerialI=*serial;
serial++;
//Code_01
asciiCodeOfSerialI=asciiCodeOfSerialI & 0x7F;
//Code_02
temp=(result ^ asciiCodeOfSerialI) & 0xF;
//Code_03
//_asm
//{
// //sar 带符号右移,结果均存储到sarResult,可考虑编写出单独的函数以方便调用
// push eax
// mov eax,result
// sar eax,0x4
// mov sarResult,eax
// pop eax
//}
result=(temp * 0x1081) ^ SAR4(result);
//Code_04
temp=((asciiCodeOfSerialI >> 0x4) ^ result) & 0xF;
//Code_05
result=(temp * 0x1081) ^ SAR4(result);
//Code_06
result=( (temp * 0x1081) ^ SAR4(result));
} while ((*serial)!=0);
*signOfSerial=result;
}
最后,分析signOfSerial
算法的可逆性是否成立。
第一点:首先是Code_1、Code_2
中的与运算&
,不存在可逆性;
第二点:Code_3、Code_4
中的带符号右移运算sar
、右移运算>>
,均不存在可逆性;
所以,该程序只能通过遍历来获取serial
,对应的signOfSerial=0x8E5B
,则serial
是正确的。
这里提供五组key
以供测试:OFCX、OFRH、OICW、OIRG、OWBH
。
7、尾声 & 致谢 & 注册机代码
7.1 尾声
到这里为止,整个程序的分析已经接近尾声,最开始分析时遇到很多难点,中间又是断断续续的分析,不过参考了资料,最终还是成功完成,虽然后面部分的记录有些仓促,但也是很有趣的,所以又花了点时间记录一下分析过程。
最终注册码生成代码会贴在最后,供大家参考。
7.2 致谢
再次感谢本文所参考的资料:
160crack之056 Elraizer.2:https://blog.csdn.net/zfy1996/article/details/107402027
7.3 C++
版本 注册机代码
注册机代码,注释部分都保留了,主要是根据给出的char
数组,遍历所有的组合情况作为serial
,再去计算signOfSerial/key
是否等于0xF84B
;核心部分在于遍历所有组合情况。
需手动加上前面的getSerial
函数部分,直接调用startGenerateSerial
即。
#include <time.h>
//纯数字
char visualNum[]={
0xA, //第一个数据标识大小
0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39
};
//纯大写
char visualUpperCase[]={
0x1A, //第一个数据标识大小
0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48,
0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x50,
0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58,
0x59, 0x5A
};
//纯小写
char visualLowerCase[]={
0x1A, //第一个数据标识大小
0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68,
0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, 0x70,
0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78,
0x79, 0x7A
};
//可见特殊符号
char visualSpecialSymbol[]={
0x20, //第一个数据标识大小
0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28,
0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x3A,
0x3B, 0x3C, 0x3D, 0x3E, 0x3F, 0x40, 0x5B, 0x5C,
0x5D, 0x5E, 0x5F, 0x60, 0x7B, 0x7C, 0x7D, 0x7E
};
char updateSerial(char* baseStr,char* serial,char* index,long& curLen,long maxLen,long curOperateIndex,char carryFlag=0)
{
//baseStr为字符基表,即serial将由基表中的字符组成
//serial为最终更新的字符串
//index存储每一位数据在buffer中的序号
//curLen为当前serial的长度
//maxLenx为字符串的最大长度,标识退出条件
//curOperateIndex为当前操作运算的位index
//carryFlag为进位标识,标识是否发生进位
if (curLen==maxLen)//长度已达到最大长度,返回false,标识此次运算结束
{
return 0;
}
//判断是否发生进位,即当前操作位等于addr中的最后一项
if (serial[maxLen-curOperateIndex-1]==baseStr[baseStr[0]])//√
{
//进位时的局部变量更新,结果未更新,留存到递归调用中更新,以便应对多个位数据需要进位的情况,如:9999
index[curOperateIndex]=1;//将当前操作位的序号重置为1,首位存储着数组的大小
serial[maxLen-curOperateIndex-1]=baseStr[index[curOperateIndex]];//将当前操作数重置为对应数组序列的初始值
//发生进位时,且当前操作位==当前长度时,当前长度需要自增,否则保持不变
//curLen=(curLen==curOperateIndex?++curLen:curLen);
if (curLen==curOperateIndex)
{
curLen++;
}
curOperateIndex++;//当前操作位自增
//递归调用
return updateSerial(baseStr,serial,index,curLen,maxLen,curOperateIndex,1);
}else
{
//没有发生进位时,数据更新到serial中
serial[maxLen-curOperateIndex-1]=baseStr[index[curOperateIndex]+1];//更新数据到serial中
index[curOperateIndex]++;//当前操作位的序号自增
return 1;
}
}
void generateSerial(char* baseStr,long maxLen)
{
srand(time(NULL));
long randStartIndex=rand()%(baseStr[0]-1);//获取随机数
char* index=new char[maxLen];//存储每一位的数据序号,所以需要serialLen的大小
memset(index,1,maxLen);
index[0]=randStartIndex;
printf("本次随机生成的序号为:0x%02X\n",randStartIndex);
char* serial=new char[maxLen+1];//由于末尾需要添加标识符'\0',所以长度大小+1
memset(serial,baseStr[1],maxLen);
serial[0]=baseStr[index[0]];
serial[maxLen]=0;
long curLen=0;//记录当前serial的长度
long curOperateIndex=0;//记录当前操作位的index
short signOfSerial=0;
char* correctSerial[0x5]={0};
//分块申请内存,执行效率不如连续的内存
for (long i=0;i<0x5;i++)
{
correctSerial[i]=new char[maxLen+1];
memset(correctSerial[i],0,maxLen+1);
}
char correctSerialCount=0;
do
{
getSignOfSerial(serial,&signOfSerial);
if ((signOfSerial & 0xFFFF) ==0xF84B)
{
memcpy(correctSerial[correctSerialCount],serial,maxLen);
correctSerialCount++;
if (correctSerialCount==0x5)
{
break;
}
}
} while (updateSerial(baseStr,serial,index,curLen,maxLen,curOperateIndex));
if (index)
{
delete[] index;
}
if (serial)
{
delete[] serial;
}
printf("the correct serial: \n");
for (long i=0;i<correctSerialCount;i++)
{
printf("%s\n",correctSerial[i]);
}
for (long i=0;i<0x5;i++)
{
if (correctSerial[i])
{
delete[] correctSerial[i];
}
}
}
void startGenerateSerial()
{
long maxLen=0;
printf("请输入要生成的serial长度(建议取值为4~8):\n");
scanf_s("%d",&maxLen);
printf("请输入serial的组成字符(默认选项为6):\n");
printf("1、仅数字\n");
printf("2、仅小写字母\n");
printf("3、仅大写字母\n");
printf("4、大写+小写字母\n");
printf("5、大写+小写字母+数字\n");
printf("6、仅可见特殊字符(英文输入情况下!)\n");
int inputType=0;
char baseStrLen=0;//最大长度上限为0x60,所以char类型也是可以的
char* baseStr=NULL;
scanf_s("%d",&inputType);
printf("注意:本程序一次性最多生成5个serial以供选中\n");
printf("您输入的serial长度为:\"%d\",组成字符选项为:\"%d\";\n",maxLen,inputType);
//拼接对应选项的字符数组
switch (inputType)
{
case 1:
baseStrLen=visualNum[0];
baseStr=new char[baseStrLen+1];
memset(baseStr,0,baseStrLen+1);
memcpy(baseStr+1,visualNum+1,baseStrLen);
break;
case 2:
baseStrLen=visualLowerCase[0];
baseStr=new char[baseStrLen+1];
memset(baseStr,0,baseStrLen+1);
memcpy(baseStr+1,visualLowerCase+1,baseStrLen);
break;
case 3:
baseStrLen=visualUpperCase[0];
baseStr=new char[baseStrLen+1];
memset(baseStr,0,baseStrLen+1);
memcpy(baseStr+1,visualUpperCase+1,baseStrLen);
break;
case 4:
baseStrLen=visualLowerCase[0]+visualUpperCase[0];
baseStr=new char[baseStrLen+1];
memset(baseStr,0,baseStrLen+1);
memcpy(baseStr+1,visualLowerCase+1,visualLowerCase[0]);
memcpy(baseStr+visualLowerCase[0]+1,visualUpperCase+1,visualUpperCase[0]);
break;
case 5:
baseStrLen=visualLowerCase[0]+visualUpperCase[0]+visualNum[0];
baseStr=new char[baseStrLen+1];
memset(baseStr,0,baseStrLen+1);
memcpy(baseStr+1,visualLowerCase+1,visualLowerCase[0]);
memcpy(baseStr+visualLowerCase[0]+1,visualUpperCase+1,visualUpperCase[0]);
memcpy(baseStr+visualLowerCase[0]+visualUpperCase[0]+1,visualNum+1,visualNum[0]);
break;
default:
baseStrLen=visualSpecialSymbol[0];
baseStr=new char[baseStrLen+1];
memset(baseStr,0,baseStrLen+1);
memcpy(baseStr+1,visualSpecialSymbol+1,baseStrLen);
break;
}
if (baseStr==NULL)
{
printf("初始化错误,即将退出!\n");
}else
{
baseStr[0]=baseStrLen;
generateSerial(baseStr,maxLen);
delete[] baseStr;
}
}