梅开x度-160cm之cm056 Elraizer.2 爆破与keygen分析
# `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`调用,进一步说明程序没有加壳;
初步分析`OEP`处的汇编码,发现程序最开始设置`SEH`(结构化异常处理函数),回调函数的地址为`0x4019F4`,可以先在回调函数处打上断点,添加注释等。
```tex
00401AD6 | 68 F4194000 | push <elraizer.2.sub_4019F4> | 压入SEH链回调函数
00401ADB | 64:A1 00000000 | mov eax, dword ptr fs: | 获取当前SEH结构体的指针
00401AE1 | 50 | push eax | 压入指针,在esp处形成了一个标准的SEH结构体
00401AE2 | 64:8925 00000000 | mov dword ptr fs:, esp | 将标准的SEH结构体置为当前的SEH链表表头
```
往后分析,在调用`GetVersion`后,将结果进行运算,并存储到内存变量中,且操作的内存地址处于`data`区段,猜测是在初始化一些全局变量。
```tex
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:, 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:, edx |
00401B02 | 8BC8 | mov ecx, eax |
00401B04 | 81E1 FF000000 | and ecx, 0xFF |
00401B0A | 890D 887A4000 | mov dword ptr ds:, ecx |
00401B10 | C1E1 08 | shl ecx, 0x8 |
00401B13 | 03CA | add ecx, edx |
00401B15 | 890D 847A4000 | mov dword ptr ds:, ecx |
00401B1B | C1E8 10 | shr eax, 0x10 |
00401B1E | A3 807A4000 | mov dword ptr ds:, eax |
```
再后面就是一些自定义的函数了,这里仅做了部分分析:
```tex
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>`。
```tex
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:, eax |
00401B8B | F645 D0 01 | test byte ptr ss:, 0x1 |
00401B8F | 74 06 | je elraizer.2.401B97 |
00401B91 | 0FB745 D4 | movzx eax, word ptr ss: |
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: |
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个参数。
```tex
00401711 | EB 0D | jmp elraizer.2.401720 |
00401713 | 8B55 EC | mov edx, dword ptr ss: |
00401716 | 52 | push edx |
00401717 | E8 24000000 | call <elraizer.2.sub_401740> |
0040171C | C3 | ret |
0040171D | 8B65 E8 | mov esp, dword ptr ss: |
00401720 | C745 FC FFFFFFFF | mov dword ptr ss:, 0xFFFFFFFF |
00401727 | 33C0 | xor eax, eax |
00401729 | 8B4D F0 | mov ecx, dword ptr ss: |
0040172C | 64:890D 00000000 | mov dword ptr fs:, 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`都是获取进程的信息。
```tex
//参数部分
00401B8F | 74 06 | je elraizer.2.401B97 |
00401B91 | 0FB745 D4 | movzx eax, word ptr ss: |
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: | 参数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`内部分析,这里直接先给出分析后的结果,再慢慢说明。
#### 1.3.1 `call sub_4017E0` 分析
先分析第一个`call`,外层调用查看可能传入了3个参数,初步记录为`sub_4017E0(0x401620,eax,0x14)`;第一个参数可能为地址,第二个参数是是`WinMain`外部传入的数据,第三个参数为常数`0x14`。
```tex
004016AD | 8B45 14 | mov eax, dword ptr ss: | 从堆栈获取传入的参数
004016B0 | A3 20784000 | mov dword ptr ds:, 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个参数;
```tex
004017E0 <elr | 8B4C24 04 | mov ecx, dword ptr ss: |
004017E4 | 8B4424 0C | mov eax, dword ptr ss: |
004017E8 | 56 | push esi |
004017E9 | 33F6 | xor esi, esi |
004017EB | 8D1401 | lea edx, dword ptr ds: |
004017EE | 3BCA | cmp ecx, edx |
004017F0 | 73 1E | jae elraizer.2.401810 |
004017F2 | 8A4424 0C | mov al, byte ptr ss: |
004017F6 | 53 | push ebx |
004017F7 | 57 | push edi |
004017F8 | 8A19 | mov bl, byte ptr ds: |
004017FA | 32D8 | xor bl, al |
004017FC | 0FBEFB | movsx edi, bl |
004017FF | 8819 | mov byte ptr ds:, 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`跳转,可单步着手分析,大致跟了几遍,发现重点汇编代码如下:
```tex
//call sub_4017E0内部
004017E9 | 33F6 | xor esi, esi //esi初始化为0
...
004017F8 | 8A19 | mov bl, byte ptr ds://取出ecx地址数据
004017FA | 32D8 | xor bl, al //进行异或运算
004017FC | 0FBEFB | movsx edi, bl //存储结果到edi
004017FF | 8819 | mov byte ptr ds:, 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:, 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++`伪代码如下:
```cpp
//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++`版本的解密代码:
```cpp
#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=decodeAddr^key;
flag=flag+decodeAddr;
}
}
//从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_={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`即可。
```tex
//参数部分
00401B8F | 74 06 | je elraizer.2.401B97
00401B91 | 0FB745 D4 | movzx eax, word ptr ss:
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: //参数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
```
解密成功时如下:
## 2、梅开二度-无法运行02
打上补丁后,准备运行看看程序界面,程序还是没反应;一番调试无果,再次查找资料,发现前面只是这个`CrackMe`的“开胃菜”,后面还有更多“惊喜”。
### 2.1 int 3 异常
查阅参考资料发现程序还有一个`int 3`异常位于`0x40153C`,且该异常是由`decode(addr,key,count)`解密函数解密后产生,所以需要再次分析解密函数,使得**解密后的字节码``变为`0x90 nop`**。虽说有点麻烦,但还是能够继续分析,不至于无从下手。
```tex
//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)`,囊括了异常字节码``,解密过程``由`0xCD->0xCC`,对应` xor 0x1=0xCD xor 0x1=0xCC`,所以可以反推`0x90 xor 0x1=0x91=`;直接补丁修改即可。
解决`int 3`后,**对应解密函数的`flag`也会变化**,这里由两个思路:①`nop`解密`flag`的判断跳转即可;②修改比较的`flag`值,这里得到的`flag=0xFFFFFFD6`。
```tex
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
```
一番修正补丁后,程序已经可以正常运行。
### 2.2 补丁方法
像前面的动态解密`int 3`异常,补丁方式可能不同于平常的直接修改,不过基本思想方法大同小异。
#### 2.2.1 使用调试器修正补丁
使用调试器修正要在解密函数未进行解密之前,`xdbg->右键->二进制编辑`修正字节码,应用补丁即可。
#### 2.2.2 使用010 edit修正文件
同样先使用调试器获取文件偏移,`xdbg->右键->复制->文件偏移`复制汇编指令的文件偏移,这里的偏移`=0x153C`,打开`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。
```tex
//外部调用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: |
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: |
00401017 | 8A06 | mov al, byte ptr ds: |
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:, 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:`中获取,正是传入的内存地址参数,这里可以猜测`call sub_401000`与字符串相关。
查看``地址处的内容如下,为不可识别的字符内容,这里可直接上手分析`call sub_401000`内部;由于`call sub_401000`内部除了`call lstrlenA`外没有其他的函数调用,可运行到返回后在观察``地址处的内容。
```tex
//call sub_401000之前,0x40707D
0040707D 05 29 30 00 .)0.
...
//call sub_401000运行到返回,0x40707D
0040707D 45 69 70 00 Eip.
```
对比前后``地址处的内容,不难发现`call sub_401000`的作用就是解码字符串,结合`call sub_401000`内部字节码分析,可得到关键解码语句如下。
```tex
00401013 | 8A5C24 10 | mov bl, byte ptr ss://获取参入参数2
00401017 | 8A06 | mov al, byte ptr ds: //取出地址内容
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:, al //解码后填回
```
根据参数2的值,分别加上不同的数值解码:参数2等于0时,字符串逐字符`+0x40`,否则逐字符`+0xC0`。
刚好有`0x40+0xC0=0x100`,对于字节码来说,可以认为`+0x40`为解码,结果再`+0xC0`为加密,所以参数2就是一个标志位,标识字符串加解密的方式。如果觉得难以理解,可借助`IDA`的伪代码帮助分析。
```cpp
//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;
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;
}
while ( v2 < result );
}
return result;
}
```
#### 3.1.2 `call sub_401000` 解密函数 C++ 代码 实现
所以可得到`call sub_401000`的函数原型为`decodeStr(addr,mode)`,这里给出`C++`版本的解码代码:
```cpp
#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=decodeAddr+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(" raw content: %s\n",_40707D);
decodeStr(_40707D,0);
printf(" decode: %s\n",_40707D);
decodeStr(_40707D,1);
printf(" encode: %s\n",_40707D);
printf("\n raw content: %s\n",_407051);
decodeStr(_407051,0);
printf(" decode: %s\n",_407051);
decodeStr(_407051,1);
printf(" encode: %s\n",_407051);
}
```
分析了`decodeStr(addr,mode)`字符串解码函数,解码作用猜测是反调试,影响后续解密`key`的数值,并且解码使用之后马上又是加密成原始字节码!
### 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`创建的,直接定位窗口消息处理回调函数,分析注册算法。
### 4.1 分析窗口回调函数
回调函数原型为:`LRESULT DefWindowProc(HWND hwnd,UINT uMsg,WPARAM wParam,LPARAM lParam)`
定位`0x4013F0`,回调函数的过程很清晰,取出`uMsg`参数,判断窗口消息类型;粗略分析,部分消息类型都在下图做了标注。
从按钮控件消息处开始进一步分析,首先便是两个常用`API`,`GetDlgItemTextA、lstrcmp`分别获取输入、比较字符,在`lstrcmp`处下断点,查看传入参数。
```
0040144F | A1 24784000 | mov eax, dword ptr ds: //控件按钮消息开始
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:
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:
00401480 | A1 8C704000 | mov eax, dword ptr ds:
00401485 | 3BD0 | cmp edx, eax
00401487 | 75 0E | jne elraizer.2.401497
00401489 | A1 24784000 | mov eax, dword ptr ds:
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”`。
当输入`"Kernel32.dll"`尝试后,发现程序没有任何反应;回到按钮消息事件中,发现比较之后,紧接着比较的结果`eax`被``地址处的数据覆盖,再去比较,判断是否需要退出窗口。
进过多次单步调试后,也发现没有其他的语句被执行,难道这个`crack`是没有写成功的界面?做了这么久的`crack`就要止步于此吗?在这里陷入了死循环,纠结无果后,再次参考资料寻求帮助,发现这个`crack`的设计“别有洞天”!
## 5、梅开三度-再度分析解密函数解密区段
### 5.1 真假窗口消息回调函数分析
参考资料后发现,主窗口的回调信息处理函数存储在``中,而``的值有两种情况;一种是默认值,**即``初始为`0x4013F0`**;另一种是上面的**解密05解密正确**,则**执行`mov ,0x401310`**,将**``更改为`0x401310`**。
前面显示窗口的回调函数便是``的初始值`0x4013F0`,到最后仅仅是调用`lstrcmpA`比较,然后就没有下文了;显然**可知`0x401310`才是正确的回调函数地址**。
而`0x401310`又经过一次解密,解密函数原型为`decodeByte(0x401310,key,0x32)`;
所以需要回头去分析解密函数,获取正确解密`key`,解密`key`的初始化关键位置如下:
①:`0x40152B:mov dword ptr ds:, 0x5A2D`,其中`0x401531=0x2D,0x4015320x5A`;
②:`0x401563:mov dword ptr ds:, eax`。
大致流程代码如下:
```tex
0040152B | C705 C4704000 2D5A000 | mov dword ptr ds:, 0x5A2D //=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:, eax//=0
...
0040156F | 6A 32 | push 0x32 //count=0x32
00401571 | 8B0D 44794000 | mov ecx, dword ptr ds://ecx=0
00401577 | 030D C4704000 | add ecx, dword ptr ds://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`的代码:
```cpp
#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=decodeAddr^key;
flag=flag+decodeAddr;
}
}
//从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_={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)`;
这里打补丁与前面的一致,不过这里由于影响`key`数值有两个地方:一个是修改初始值为`0x5A16`;另一个是修改随后的`eax`为`0x??????E9`,`"?"`表示任意值,只需使得相加后的`cl=0x16`即可。
这里仅演示第一种补丁方式,第二种修改部分与修改方式较多,不过原理与第一种相同,可自己尝试修改。
解密过程``由`0x2C->0x2D`,对应` xor 0x1=0x2C xor 0x1=0x2D`,所以可以反推反推`0x16 xor 0x1=0x17=`。
所以最终补丁为`:0x2C->0x17`。
针对第二种补丁方法,最终修改成果如下,仅供参考:
```tex
//修改前
00401558 | A1 44794000 | mov eax, dword ptr ds: |
0040155D | 33C0 | xor eax, eax |
0040155F | 90 | nop |
00401560 | 90 | nop |
00401561 | 90 | nop |
00401562 | 90 | nop |
00401563 | A3 44794000 | mov dword ptr ds:, eax |
//修改后
00401558 | A1 44794000 | mov eax, dword ptr ds: |
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:, eax |
```
经过二次补丁后,还需要注意解密`flag`的校验,如果前面`int 3`异常的解密`flag`判断没有使用`nop`填充,这里还需要再次更改解密`flag`的比对值,第一种补丁方法的比对值为`0xFFFFFFBF`。
成功显示主窗口界面,且窗口回调处理函数为`0x401310`。
## 6、梅开二度-注册算法再度探索
程序初始无法运行->成功显示窗口但主窗口回调函数错误->成功显示窗口且回调函数正确,一路曲折蜿蜒,到达最关键的注册算法分析,有种手撕程序外壳的感觉:(
### 6.1 动静态结合分析
前面已经分析得到正确的`WinProc`窗口回调函数`=0x401310`,重点分析`0x401310`处即可。
首先要确定按钮事件,粗略分析如下图:
也可通过`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)`,开始时分别对`、`处的两个字符串统一解码,最后又统一加码;完成`call sub_0x4012C0`之后便会`jmp`到退出住窗口的消息处理过程。
```tex
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:
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:, eax //存储结果
00401391 | 6A 06 | push 0x6 //参数3:0x6
00401393 | 8B55 FC | mov edx, dword ptr ss:
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:, eax
004013A8 | 817D FC 5B8E0000 | cmp dword ptr ss:, 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`为解密参数;动态调试后,分析关键解密段如下:
```tex
004011B3 | 8B45 FC | mov eax, dword ptr ss://局部变量01
004011B6 | 0FBF08 | movsx ecx, word ptr ds: //取出地址数据
004011B9 | 334D 0C | xor ecx, dword ptr ss://与参数2异或
004011BC | 8B55 FC | mov edx, dword ptr ss://局部变量01
004011BF | 66:890A | mov word ptr ds:, cx //更新数据到地址
004011C2 | EB 05 | jmp elraizer.2.4011C9
//jmp 跳转处
004011C9 | 8B45 FC | mov eax, dword ptr ss://局部变量01
004011CC | 0FBF08 | movsx ecx, word ptr ds://取出已更新数据
004011CF | 8B55 F8 | mov edx, dword ptr ss://局部变量02
004011D2 | 03D1 | add edx, ecx //相加
004011D4 | 8955 F8 | mov dword ptr ss:, edx//结果存储
...
004011D9 | 8B45 F8 | mov eax, dword ptr ss://局部变量02
```
进一步完善函数原型为:`decodeWord(addr,key,count)`;最后的`flag`为已解密数据的总和,校验值为`0x8E5B`。
到目前为止,想要成功,必须要获取正确的解密,关键点就在解密`key`的获取方式上!
### 6.2 爆破补丁修改
对于这种校验`flag`的解密方式,仍然采用遍历的方法得到正确的解密`key`,和前面的代码模式都差不多,这里给出`C++`版本的代码以供参考:
```cpp
#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=decodeAddr^key;
//printf("%04X ",decodeAddr & 0xFFFF);
flag=flag+decodeAddr;
}
}
//从xdbg中复制解密段的字节码
//count=0x6 addr=4012E6 key=unknown
short _4012E6[]={
0xEDB4, 0x8057, 0xF80B
};
//调用示例
void demoWord()
{
long flag=0;
short _4012E6_={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`。
看看会出现什么效果?并且观察`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++`版本的算法以供参考:
```cpp
#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`](https://blog.csdn.net/zfy1996/article/details/107402027 )
### 7.3 `C++` 版本 注册机代码
注册机代码,注释部分都保留了,主要是根据给出的`char`数组,遍历所有的组合情况作为`serial`,再去计算`signOfSerial/key`是否等于`0xF84B`;**核心部分在于遍历所有组合情况**。
需手动加上前面的`getSerial`函数部分,直接调用`startGenerateSerial`即。
```cpp
#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==baseStr])//√
{
//进位时的局部变量更新,结果未更新,留存到递归调用中更新,以便应对多个位数据需要进位的情况,如:9999
index=1;//将当前操作位的序号重置为1,首位存储着数组的大小
serial=baseStr];//将当前操作数重置为对应数组序列的初始值
//发生进位时,且当前操作位==当前长度时,当前长度需要自增,否则保持不变
//curLen=(curLen==curOperateIndex?++curLen:curLen);
if (curLen==curOperateIndex)
{
curLen++;
}
curOperateIndex++;//当前操作位自增
//递归调用
return updateSerial(baseStr,serial,index,curLen,maxLen,curOperateIndex,1);
}else
{
//没有发生进位时,数据更新到serial中
serial=baseStr+1];//更新数据到serial中
index++;//当前操作位的序号自增
return 1;
}
}
void generateSerial(char* baseStr,long maxLen)
{
srand(time(NULL));
long randStartIndex=rand()%(baseStr-1);//获取随机数
char* index=new char;//存储每一位的数据序号,所以需要serialLen的大小
memset(index,1,maxLen);
index=randStartIndex;
printf("本次随机生成的序号为:0x%02X\n",randStartIndex);
char* serial=new char;//由于末尾需要添加标识符'\0',所以长度大小+1
memset(serial,baseStr,maxLen);
serial=baseStr];
serial=0;
long curLen=0;//记录当前serial的长度
long curOperateIndex=0;//记录当前操作位的index
short signOfSerial=0;
char* correctSerial={0};
//分块申请内存,执行效率不如连续的内存
for (long i=0;i<0x5;i++)
{
correctSerial=new char;
memset(correctSerial,0,maxLen+1);
}
char correctSerialCount=0;
do
{
getSignOfSerial(serial,&signOfSerial);
if ((signOfSerial & 0xFFFF) ==0xF84B)
{
memcpy(correctSerial,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);
}
for (long i=0;i<0x5;i++)
{
if (correctSerial)
{
delete[] correctSerial;
}
}
}
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;
baseStr=new char;
memset(baseStr,0,baseStrLen+1);
memcpy(baseStr+1,visualNum+1,baseStrLen);
break;
case 2:
baseStrLen=visualLowerCase;
baseStr=new char;
memset(baseStr,0,baseStrLen+1);
memcpy(baseStr+1,visualLowerCase+1,baseStrLen);
break;
case 3:
baseStrLen=visualUpperCase;
baseStr=new char;
memset(baseStr,0,baseStrLen+1);
memcpy(baseStr+1,visualUpperCase+1,baseStrLen);
break;
case 4:
baseStrLen=visualLowerCase+visualUpperCase;
baseStr=new char;
memset(baseStr,0,baseStrLen+1);
memcpy(baseStr+1,visualLowerCase+1,visualLowerCase);
memcpy(baseStr+visualLowerCase+1,visualUpperCase+1,visualUpperCase);
break;
case 5:
baseStrLen=visualLowerCase+visualUpperCase+visualNum;
baseStr=new char;
memset(baseStr,0,baseStrLen+1);
memcpy(baseStr+1,visualLowerCase+1,visualLowerCase);
memcpy(baseStr+visualLowerCase+1,visualUpperCase+1,visualUpperCase);
memcpy(baseStr+visualLowerCase+visualUpperCase+1,visualNum+1,visualNum);
break;
default:
baseStrLen=visualSpecialSymbol;
baseStr=new char;
memset(baseStr,0,baseStrLen+1);
memcpy(baseStr+1,visualSpecialSymbol+1,baseStrLen);
break;
}
if (baseStr==NULL)
{
printf("初始化错误,即将退出!\n");
}else
{
baseStr=baseStrLen;
generateSerial(baseStr,maxLen);
delete[] baseStr;
}
}
```
附上练手程序:Elraizer.2.zip
补充练手程序和xdbg黑色主题配置文件:
https://wwa.lanzoul.com/b011bo9bi
密码:5sqa 论坛也有一篇:
160 个 CrackMe 之 056 Elraizer.2 解码代码破解、注册分析及注册机实现
https://www.52pojie.cn/thread-981822-1-1.html 学习了,感谢分享 调试器配色能分享吗 感谢楼主分享
hszt 发表于 2022-2-27 11:18
调试器配色能分享吗
补充连接在置顶楼层了 woflant 发表于 2022-2-27 14:42
补充练手程序和xdbg黑色主题配置文件:
https://wwa.lanzoul.com/b011bo9bi
谢谢分享 楼主厉害 会逆向安卓软件吗,会的麻烦联系我