solly 发表于 2019-7-17 11:22

160 个 CrackMe 之 153 - The_q.1 的注册算法分析和注册机实现

本帖最后由 solly 于 2019-7-19 14:54 编辑

160 个 CrackMe 之 153 - The_q.1 是一个 Delphi 编译的 CrackMe 程序,文件信息如下,没有加壳:


既然是 Delphi 程序,直接先用 DeDeDark 打开看看,寻找事件入口:

可以看到到,有一个 Edit1Change 事件就是我们需要的事件,其入口地址为 0x00425F64,我们直接在 OD 中跳转到这个地址下断即可:

用 OD 载入并下好断点,F9 直接运行 CrackMe,显示界面如下图:



因为是 change 事件,我们先不要手动录入注册码。可以在记事本(notepad.exe)中录好,并复制,然后再到 CrackMe 中粘贴,这样可以一次性输入多个字符而只中断一次。如下图,我们输入的注册码:


我们前面已经在 0x00425F64 下了断点,程序就会断在这个位置,整个事件的代码如下:
00425F64/.55            push    ebp
00425F65|.8BEC          mov   ebp, esp
00425F67|.83C4 E8       add   esp, -18
00425F6A|.53            push    ebx
00425F6B|.56            push    esi
00425F6C|.57            push    edi
00425F6D|.33C9          xor   ecx, ecx
00425F6F|.894D F8       mov   dword ptr , ecx         ;regCode
00425F72|.894D F4       mov   dword ptr , ecx         ;temp1
00425F75|.894D F0       mov   dword ptr , ecx          ;temp2
00425F78|.8945 FC       mov   dword ptr , eax         ;Sender,文本控件对象(0x02331C9C)
00425F7B|.33C0          xor   eax, eax
00425F7D|.55            push    ebp
00425F7E|.68 8D614200   push    0042618D                         ;try
00425F83|.64:FF30       push    dword ptr fs:
00425F86|.64:8920       mov   dword ptr fs:, esp
00425F89|.C645 EB 00    mov   byte ptr , 0             ;设置为失败标志
00425F8D|.8D55 F8       lea   edx, dword ptr          ;edx == buffer ===> regCode
00425F90|.8B45 FC       mov   eax, dword ptr
00425F93|.8B80 EC010000 mov   eax, dword ptr
00425F99|.E8 AAF3FEFF   call    00415348                         ;Control.GetText()
00425F9E|.8B45 F8       mov   eax, dword ptr          ;eax ===> regCode
00425FA1|.E8 CAD5FDFF   call    00403570                         ;eax == strlen(regCode)
00425FA6|.8BD0          mov   edx, eax                         ;edx == eax == strlen(regCode)
00425FA8|.8D45 F4       lea   eax, dword ptr          ; ===> temp1
00425FAB|.E8 94D8FDFF   call    00403844                         ;SetLength(temp1, (strlen(regCode))
00425FB0|.8B45 F8       mov   eax, dword ptr          ;eax ===> regCode
00425FB3|.E8 B8D5FDFF   call    00403570                         ;eax == strlen(regCode)
00425FB8|.8BD0          mov   edx, eax                         ;edx == eax == strlen(regCode)
00425FBA|.8D45 F0       lea   eax, dword ptr           ; ===> temp2
00425FBD|.E8 82D8FDFF   call    00403844                         ;SetLength(temp2, strlen(regCode))
00425FC2|.8B45 F8       mov   eax, dword ptr          ;eax ===> regCode
00425FC5|.E8 A6D5FDFF   call    00403570                         ;strlen()
00425FCA|.83F8 08       cmp   eax, 8                           ;strlen(regCode)>=8 ???
00425FCD|.7D 0D         jge   short 00425FDC
00425FCF|.8D45 F4       lea   eax, dword ptr          ;temp1
00425FD2|.BA 08000000   mov   edx, 8
00425FD7|.E8 68D8FDFF   call    00403844                         ;SetLength(temp1, 8)
00425FDC|>C645 EF 01    mov   byte ptr , 1             ;int x = 1
00425FE0|.C645 EE 01    mov   byte ptr , 1             ;int k = 1
00425FE4|.8B45 F8       mov   eax, dword ptr          ;eax ===> regCode
00425FE7|.E8 84D5FDFF   call    00403570                         ;strlen()
00425FEC|.84C0          test    al, al
00425FEE|.76 64         jbe   short 00426054                   ;strlen(regCode) <= 0 ???
00425FF0|.8845 EA       mov   byte ptr , al            ;int n = == strlen(regCode)
00425FF3|.B3 01         mov   bl, 1                            ;int i = 1;
00425FF5|>33C0          /xor   eax, eax
00425FF7|.8AC3          |mov   al, bl                        ;i
00425FF9|.8B55 F8       |mov   edx, dword ptr           ;edx ===> regCode
00425FFC|.8A4402 FF   |mov   al, byte ptr       ;al = regCode
00426000|.25 FF000000   |and   eax, 0FF
00426005|.0FA305 407842>|bt      dword ptr , eax         ;位测试和设置指令, bt 0x00000000, (eax % 32)
0042600C|.73 21         |jnb   short 0042602F                  ;不低于跳转, al为偶数不跳转(即 ASCII 码为偶数),al为奇数则跳转
0042600E|.8D45 F4       |lea   eax, dword ptr           ;temp1
00426011|.E8 2AD7FDFF   |call    00403740                        ;eax ===> temp1, 奇
00426016|.33D2          |xor   edx, edx
00426018|.8A55 EF       |mov   dl, byte ptr          ;dl == x
0042601B|.33C9          |xor   ecx, ecx
0042601D|.8ACB          |mov   cl, bl                        ;cl == i
0042601F|.8B75 F8       |mov   esi, dword ptr           ;esi ===> regCode
00426022|.8A4C0E FF   |mov   cl, byte ptr       ;cl = regCode, i= 4
00426026|.884C10 FF   |mov   byte ptr , cl      ;temp1 == regCode
0042602A|.FE45 EF       |inc   byte ptr                ;x++
0042602D|.EB 1F         |jmp   short 0042604E
0042602F|>8D45 F0       |lea   eax, dword ptr          ; ===> temp2
00426032|.E8 09D7FDFF   |call    00403740                        ;eax ===> temp2,偶
00426037|.33D2          |xor   edx, edx
00426039|.8A55 EE       |mov   dl, byte ptr          ;k, 初始化为 1
0042603C|.33C9          |xor   ecx, ecx                        ;int j = 0
0042603E|.8ACB          |mov   cl, bl                        ;j=i
00426040|.8B75 F8       |mov   esi, dword ptr           ;esi ===> regCode
00426043|.8A4C0E FF   |mov   cl, byte ptr       ;cl = regCode
00426047|.884C10 FF   |mov   byte ptr , cl      ;temp1 == regCode, temp1== "bb7"
0042604B|.FE45 EE       |inc   byte ptr                ;k++
0042604E|>43            |inc   ebx                           ;i++
0042604F|.FE4D EA       |dec   byte ptr                ;n--, n == strlen(regCode)
00426052|.^ 75 A1         \jnz   short 00425FF5
00426054|>33FF          xor   edi, edi                         ;int xorSum = 0
00426056|.33D2          xor   edx, edx
00426058|.8A55 EE       mov   dl, byte ptr             ;dl == stelen(temp1) + 1
0042605B|.4A            dec   edx
0042605C|.8D45 F0       lea   eax, dword ptr           ;eax ===> temp1, 奇
0042605F|.E8 E0D7FDFF   call    00403844                         ;SetLength(), 写入字符串的长度值,成为 delphi格式字符串
00426064|.8A45 EE       mov   al, byte ptr             ;k = strlen(temp1) + 1
00426067|.48            dec   eax
00426068|.84C0          test    al, al
0042606A|.76 1A         jbe   short 00426086
0042606C|.8845 EA       mov   byte ptr , al            ;n = strlen(temp1) - 1, 奇
0042606F|.B3 01         mov   bl, 1                            ;int i = 1
00426071|>33C0          /xor   eax, eax
00426073|.8AC3          |mov   al, bl                        ;i
00426075|.8B55 F0       |mov   edx, dword ptr          ;edx ===> temp1, 奇
00426078|.0FB64402 FF   |movzx   eax, byte ptr
0042607D|.66:33F8       |xor   di, ax                        ;xorSum = xorSum ^ temp1
00426080|.43            |inc   ebx                           ;i++
00426081|.FE4D EA       |dec   byte ptr                ;n--
00426084|.^ 75 EB         \jnz   short 00426071
00426086|>8A45 EF       mov   al, byte ptr             ;j = strlen(temp2)+1, 偶
00426089|.F6D0          not   al                               ;al == -6
0042608B|.3C 08         cmp   al, 8
0042608D|.76 1F         jbe   short 004260AE
0042608F|.8A5D EF       mov   bl, byte ptr             ;j
00426092|.80FB 08       cmp   bl, 8
00426095|.77 17         ja      short 004260AE                   ;j>8
00426097|>8D45 F4       /lea   eax, dword ptr           ; ===> temp2, 偶
0042609A|.E8 A1D6FDFF   |call    00403740                        ;eax ===> temp2
0042609F|.33D2          |xor   edx, edx
004260A1|.8AD3          |mov   dl, bl                        ;x
004260A3|.C64410 FF 30|mov   byte ptr , 30      ;temp2 padding "0"
004260A8|.43            |inc   ebx
004260A9|.80FB 09       |cmp   bl, 9
004260AC|.^ 75 E9         \jnz   short 00426097
004260AE|>8D45 F4       lea   eax, dword ptr          ; ===> temp2
004260B1|.BA 08000000   mov   edx, 8
004260B6|.E8 89D7FDFF   call    00403844                         ;eax ===> SetLength(temp2, 8)
004260BB|.8D45 F8       lea   eax, dword ptr          ; ===> regCode
004260BE|.BA 04000000   mov   edx, 4
004260C3|.E8 7CD7FDFF   call    00403844                         ;eax ===> SetLength(regCode, 4)
004260C8|.B3 01         mov   bl, 1                            ;int i=1
004260CA|>8D45 F8       /lea   eax, dword ptr           ;regCode, 开始循环取temp2前4位
004260CD|.E8 6ED6FDFF   |call    00403740                        ;eax ===> temp0
004260D2|.8BF3          |mov   esi, ebx                        ;i
004260D4|.81E6 FF000000 |and   esi, 0FF
004260DA|.8B55 F4       |mov   edx, dword ptr           ;edx ===> temp2
004260DD|.8A5432 FF   |mov   dl, byte ptr       ;dl = temp2
004260E1|.885430 FF   |mov   byte ptr , dl      ;regCode == "0068"
004260E5|.43            |inc   ebx
004260E6|.80FB 05       |cmp   bl, 5
004260E9|.^ 75 DF         \jnz   short 004260CA
004260EB|.8B45 F8       mov   eax, dword ptr          ;eax ===> temp2_0-3
004260EE|.E8 B5FFFDFF   call    004060A8                         ;eax == StrToInt("6662") == 0x1A06
004260F3|.66:8945 EC    mov   word ptr , ax
004260F7|.B3 01         mov   bl, 1
004260F9|>8D45 F8       /lea   eax, dword ptr           ;regCode, 开始循环取 temp2 后4位
004260FC|.E8 3FD6FDFF   |call    00403740                        ;eax ===> temp0
00426101|.8BF3          |mov   esi, ebx
00426103|.81E6 FF000000 |and   esi, 0FF
00426109|.8B55 F4       |mov   edx, dword ptr
0042610C|.8A5432 03   |mov   dl, byte ptr
00426110|.885430 FF   |mov   byte ptr , dl
00426114|.43            |inc   ebx
00426115|.80FB 05       |cmp   bl, 5
00426118|.^ 75 DF         \jnz   short 004260F9
0042611A|.8B45 F8       mov   eax, dword ptr          ;eax ===> temp2_4-7
0042611D|.E8 86FFFDFF   call    004060A8                         ;eax == StrToInt("4680") == 0x1248
00426122|.8BC8          mov   ecx, eax
00426124|.66:85FF       test    di, di                           ;xorSum 不能为 0
00426127|.74 1B         je      short 00426144
00426129|.66:8B45 EC    mov   ax, word ptr             ;前4位 0x44
0042612D|.66:33C1       xor   ax, cx                           ;前4位 xor 后4位
00426130|.0FB7C0      movzx   eax, ax
00426133|.0FB7D7      movzx   edx, di
00426136|.8BCA          mov   ecx, edx
00426138|.99            cdq
00426139|.F7F9          idiv    ecx                              ;(前4位 xor 后4位) mod (xorSum(regCode前3位))
0042613B|.83FA 0D       cmp   edx, 0D                        ;余数为 0x0D, 13
0042613E|.75 04         jnz   short 00426144
00426140|.C645 EB 01    mov   byte ptr , 1             ;设置为成功标志
00426144|>807D EB 00    cmp   byte ptr , 0
00426148|.74 15         je      short 0042615F                   ; 0 - 失败,1 - 成功
0042614A|.8B45 FC       mov   eax, dword ptr
0042614D|.8B80 F4010000 mov   eax, dword ptr
00426153|.BA A4614200   mov   edx, 004261A4                  ;ASCII "R-E-G-I-S-T-E-R-E-D !!"
00426158|.E8 1BF2FEFF   call    00415378                         ;Control.SetText()
0042615D|.EB 13         jmp   short 00426172
0042615F|>8B45 FC       mov   eax, dword ptr
00426162|.8B80 F4010000 mov   eax, dword ptr
00426168|.BA C4614200   mov   edx, 004261C4                  ;ASCII "NOT Registered !"
0042616D|.E8 06F2FEFF   call    00415378                         ;Control.SetText()
00426172|>33C0          xor   eax, eax
00426174|.5A            pop   edx
00426175|.59            pop   ecx
00426176|.59            pop   ecx
00426177|.64:8910       mov   dword ptr fs:, edx
0042617A|.68 94614200   push    00426194
0042617F|>8D45 F0       lea   eax, dword ptr
00426182|.BA 03000000   mov   edx, 3
00426187|.E8 8CD1FDFF   call    00403318
0042618C\.C3            retn
0042618D   .^ E9 4ECCFDFF   jmp   00402DE0                         ;catch
00426192   .^ EB EB         jmp   short 0042617F
00426194   .5F            pop   edi
00426195   .5E            pop   esi
00426196   .5B            pop   ebx
00426197   .8BE5          mov   esp, ebp
00426199   .5D            pop   ebp
0042619A   .C3            retn

上面的代码中,我在每行代码后基本都有说明,不过可能流程不清晰,下面的图,更能説明问题:

CrackMe 首先将我们输入的序列号进行字符分组,分组原则就是序列号中每个字符的 ASCII 码值,按码值的奇偶性,分成两组字符串:str1 和 str2。
str1 所含字符的ascii 码值都是奇数的数字或其它非数字字符,str2 所含字符的 ascii 码值均为偶数的数字字符。
str1 的长度无限制,最少为1个字符。str2 最少要8个数字字符,如果不够,在后面补“0”字符,如果有多,则8个以后的字符用不到。
接下来就是运算,str1 中的每个字符依次进行 xor 运算,结果为 r1。如下所示,str1 的长度为 n:
r1 = str1 xor str1 xor str1 xor str1 xor str1 xor ...... xor str1;
str2 则将8个字符,分成两组,前4个一组形成32bits整数 n1,后4个一组形成另一个32bits整数 n2,然后两个整数进行 xor 运算,形成结果 r2。如下所示:
n1 = StrToInt(str2str2str2str2),
n2 = StrToInt(str2str2str2str2)


r2 = n1 xor n2;


最后,r1 和 r2 要满足以下条件:
r2 mod r1 == 0x0D

从上面分析可以看,r1 通过 xor 运算得到,最简单的情况是 r1 只由一个字符运算得到,即 str1 只有一个字符:
r1 = 0x00 xor str1;
我们的注册机就是按这个原则来进行注册码算码的,注册机的源码如下,使用 Dev-C++调试通过:
#include <iostream>
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <unistd.h>

int main(int argc, char** argv) {

      ///RAND_MAX = 47; // 0~46
      srand((unsigned)time(NULL)); //// 初始化随机数

      srand((unsigned)rand() * rand()); //// 初始化随机数

      printf("Keygen for The.q.1, please wait...\n");

      long o = 0x20;
      for(long k=0; k<100; k++) {//// 最多测试100次
                /// 生成两个由偶数数字组成的4位数字
                long n1 = 0;
                long n2 = 0;
                for(int i=0; i<3; i++) {
                        n1 = n1 * 10 + ((long)(4 * rand() / (RAND_MAX + 1))) * 2;
                        n2 = n2 * 10 + ((long)(3 * rand() / (RAND_MAX + 1))) * 2;
                }
                /// 增加成功机率
                n1 += 8000;
                n2 += 6000;
      
                long n = n2 xor n1;
               
                //// 生成 1 位除数,1位奇数即可,跳过xor操作
                for (long i=33; i<126; i+=2) { /// 可见字符 (0x21~0x7D)
                        if((n % i) == 13) {
                              o = i; /// 成功
                              //printf("o = %d\n", i);
                              break;
                        }
                }

                /// U08220640, k82446222, S82626242, G80406
                if(o != 0x20) {
                        printf("Serial: %c%04u%04u\n", o, n1, n2);
                        break;               
                }
               
                //sleep(5);
      }
      
      if(o == 0x20) {
                printf("Serial Not Found, please run again!\n");
      }
      
      return 0;
}
上面注释中 U08220640, k82446222, S82626242, G80406,都是计算出来的可用注册码。

下面对注册码规则进一步説明:
1、如果计算出来的注册码后面是‘0’,可以不用输入这些‘0’,如 G80406000,只要输入 G80406 即可,其余的 CrackMe 自己会补上。
2、由于两个相同的字符 xor 后结果为0,不会影响下一次 xor 运算,所以在 str1 中,可以成对(或成双,2,4,6,8....)地插入非数字字符或者奇数数字(1,3,5,7,9),并且位置不限,如上面 G80406,可以输入 ggG80406,G80g4g06, gGg80g4g06gkkg 等等,都是有效的。



分析完毕!!!


补充:
前面的注册机,首先确定n1,n2,形成 r2,然后测试 r1(即 str1),如果反过来考虑。我们先确定 r1(即 str1),然后强制设置 n2 = 0000,就是不考虑 n2 了,这样就可以根据条件算出 r2,就得到 n1 了,因为 n1 = r2 了。
新的注册机代码如下:
/**
* 方式 2
***/
long getSN2() {
//      for(int i=33; i<127; i++) {
//                printf("%c", i);    /// 生成 base 字符串,手动删除其中的0,2,4,6,8,放到后的base数组变量中。
//      }      
//      printf("\n");
      ///
      srand((unsigned)time(NULL)); //// 初始化随机数

      srand((unsigned)rand() * rand()); //// 初始化随机数

      printf("Keygen for The.q.1, please wait...\n");

      char base[] = "!\"#$%&'()*+,-./13579:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~";
      long n = strlen(base);
      
      //// 测试次数
      long n1 = 0;
      for (int k=0; k<100; k++) {
                int i = ((int)(n * rand() / (RAND_MAX + 1)));

                long o = (long)base;    //// 确定 str1
      
                //计算n1
                n1 = 1;
                long j = 0;
                while(n1<8888) {
                        j++;
                        n1 = o * j + 13;    //// 计算 n1
                        
                        //// 下面测试n1是否全由偶数数字组成
                        long m = n1 % 2;
                        if(m == 1) {
                              continue;
                        }
                        m = (n1/10) % 2;
                        if(m == 1) {
                              continue;
                        }
                        m = (n1/100) % 2;
                        if(m == 1) {
                              continue;
                        }
                        m == (n1/1000) % 2;
                        if(m == 0) {
                              break;   /// 成功找到 n1
                        }
                }
               
                if(n1 <= 8888) {
                        //long n2 = 0;
                        printf("Serial: %c%04d\n", o, n1);
                        break;
                }
      }

      if(n1>8888) {
                printf("Serial Not Found, please run again!\n");
      }
      
      return 0;
}

补充2:
如果对方式2进一步简化,设置 j==1,这样,r2 = o + 13 了。当计算的 r2 (或 n1)只有两位数时,就能满足条件了。
方式3如下:/**
* 方式 3
***/
long getSN3() {
//      for(int i=33; i<87; i++) {///控制 i+13<100
//                int x = i + 13;/// 计算r2, (j==1)
//                long m = x % 2;
//                if(m == 1) {
//                        continue;
//                }
//                m = (x/10) % 2;
//                if(m == 0) {
//                        printf("%c", i);//// 生成下面的 base 字符串
//                }
//      }
//      printf("\n");

      char base[] = "!#/1357CEGIK";
      //long i = (unsigned)time(NULL) % 12; ////随机在12个序列号中选一个
      for(long i=0; i<12; i++) //// 生成全部 12 个简单注册码
      {
                long o = (long)base;    //// r1+13<100
                ///
                printf("%c%04d\n", o, o+13);
      }
      
      return 0;
}

这样得到12个简单的注册码:
!0046
#0048
/0060
10062
30064
50066
70068
C0080
E0082
G0084
I0086
K0088


补充3:
前面是生成“计算简单”的注册码,基本按规则可口算得到。下面考虑一下最短的注册码,因为 CrackMe 会自己补0,我们可以考虑当r2 (或n1)为整百或整千时最简短。修改后的方式4如下:
/**
* 方式 4,控制输入的位数最少
***/
long getSN4() {
        char base[] = "!\"#$%&'()*+,-./13579:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~";
        long n = strlen(base);
       
        for(int r=2000; r<8800; r+=200) {///控制 r2 去除尾0后只有1~2位
                for(int i=0; i<n; i++) {
                        long x = (r - 13) % base;
                        if(x==0) {
                                long r2 = r/100;
                          long m = r2 % 2;
                          if(m == 1) {
                                        continue;
                                }
                                m = (r2/10) % 2;
                                if(m == 0) {
                                        printf("%c%d\n", base, r2);
                                }
                        }
                }
        }

        return 0;
}



运行后,输出如下:
Q22
M24
542
O42
)44
k44
!46
180

从上面结果还可以看出,180 后面的0可以去掉,只输入 18 即可,这一个应该是最简单的注册码了!!!:lol

yycoolrich 发表于 2019-7-17 11:53

{:1_893:}和高人还有很大的差距!!!!

社会峰哥 发表于 2019-7-17 12:24

数学不好怎么办不会算啊

hxp.china.sh 发表于 2019-7-17 12:46

有没有VB的教程呀,以前的方法不管用         

伽古拉 发表于 2019-7-17 13:19

好的 谢谢楼主的无私分享

wxjerry 发表于 2019-7-17 14:15

谢谢楼主的无私分享!{:1_893:}

天空藍 发表于 2019-7-17 14:48

社会峰哥 发表于 2019-7-17 12:24
数学不好怎么办不会算啊

这只是代码逻辑算法而已,不会算可以用计算器(软体工程型)。有错请指正。

夜夜思念 发表于 2019-7-17 19:58

学习了,感谢分享~

solly 发表于 2019-7-19 14:41

天空藍 发表于 2019-7-17 14:48
这只是代码逻辑算法而已,不会算可以用计算器(软体工程型)。有错请指正。

不是计算器,还是要先理解怎么算的,怎么算还是要有点数学逻辑滴,计算器只是验算你的理解。

天空藍 发表于 2019-7-19 16:47

solly 发表于 2019-7-19 14:41
不是计算器,还是要先理解怎么算的,怎么算还是要有点数学逻辑滴,计算器只是验算你的理解。

我的意思是说这个数学比例占得少,主要是代码逻辑问题,这些主要是靠调试来弄懂代码逻辑,而不是靠数学,靠数学的会是一堆数学符号。 xor像我就不晓得如何算,指令逻辑不懂,只要将指令和数字放入计算机运算,答案就出来了,接下来就是靠调试弄懂代码原理。有错在帮忙指正。thank
页: [1] 2
查看完整版本: 160 个 CrackMe 之 153 - The_q.1 的注册算法分析和注册机实现