首页
社区
课程
招聘
[原创]某型数据恢复软件序列号之破解
发表于: 2007-5-12 21:58 7895

[原创]某型数据恢复软件序列号之破解

2007-5-12 21:58
7895
 
  近日发现一个数据恢复软件,小巧,但是非常好用。唯一的不足就是在未注册的时候仅能使用部分功能,而且恢复的数据不能超过一定的大小限制。在使用是深感可惜,就萌生了破解的想法,一条路是破解其序列号的生成算法,自己算一个序列号出来;要不就干脆直接更改程序机器代码,突破功能上的限制。

  首选进行序列号的算法破解,用Peid查壳,发现输出结果是 “Microsoft Visual C++ 7.0”,一阵狂喜,该软件在VC++.Net环境下编写,但是没有加任何的壳。非常好,非常好,这样可以省去一大堆去壳的麻烦。

  这个数据恢复软件的注册,是提交用户名,采用付费后获得注册码的程序的方式,也就是说输入用户名,再输入开发公司给的对应序列号,就可以注册成功。那么传统的破解序列号的办法就再合适不过了。
  
  它详细的注册流程是这样:
  
  1. 出现程序主界面,点击注册按钮出现注册对话框;
  2. 输入用户名和序列号,完成注册;

  乍一看,注册方法还是比较传统的,现在推测,序列号的生成和输入的用户名有某种联系。破解的切入点可以定在这个注册对话框上,如果我们可以这个对话框背后,找到序列好的生成算法,那么一切问题都解决。

  在OllyICE下加载,一切顺利。花不多时间,就找到了隐藏在幕后的一片桃花源。以下是序列号生成函数的片段,前面省略掉了一段,这一段其实就干了一件事,初始化了一个7×4的二位数组,换成C语言的话说,是这样的:

  int base[7][4] = { {X,,X, X, X},{X,,X, X, X},{X,,X, X, X},{X,,X, X, X},{X,,X, X, X},{X,,X, X, X},{X,,X, X, X} };

  我们称数组base为基准数组,数组里的X,代表一个特定的值,这个值是固定好的,不变的,考虑到我们破解的目的仅仅是技术交流,恕不给出具体数值。

  初始化完成以后,就开始计算序列号。

00475A64  |.  E9 1D030000   jmp     00475D86
00475A69  |>  8B45 08       mov     eax, [ebp+8]
00475A6C  |.  50            push    eax                           ; |s
00475A6D  |.  E8 F0660000   call    <jmp.&MSVCRT.strlen>          ; |strlen
00475A72  |.  83C4 04       add     esp, 4
00475A75  |.  3D FD030000   cmp     eax, 3FD
00475A7A  |.  76 07         jbe     short 00475A83
00475A7C  |.  33C0          xor     eax, eax
00475A7E  |.  E9 03030000   jmp     00475D86
00475A83  |>  8B4D 08       mov     ecx, [ebp+8]
00475A86  |.  51            push    ecx                           ; |s
00475A87  |.  E8 D6660000   call    <jmp.&MSVCRT.strlen>          ; |strlen
00475A8C  |.  83C4 04       add     esp, 4
00475A8F  |.  8985 54F9FFFF mov     [ebp-6AC], eax
00475A95  |.  C685 68F9FFFF>mov     byte ptr [ebp-698], 0
00475A9C  |.  C785 F0FBFFFF>mov     dword ptr [ebp-410], 0
00475AA6  |.  C785 F8FBFFFF>mov     dword ptr [ebp-408], 0
00475AB0  |.  8B55 08       mov     edx, [ebp+8]
00475AB3  |.  52            push    edx                           ; |src
00475AB4  |.  8D85 FCFBFFFF lea     eax, [ebp-404]                ; |
00475ABA  |.  50            push    eax                           ; |dest
00475ABB  |.  E8 A8660000   call    <jmp.&MSVCRT.strcpy>          ; |strcpy
00475AC0  |.  83C4 08       add     esp, 8

  可能在静态情况下阅读汇编代码比较痛苦,但是在上面一段的注释中,还是可以看到,程序调用了msvcrt.dll动态连接库中的strlen和strcpy函数,熟悉C语言的朋友不会陌生,前者是求出一个字符串的长度,后者完成一次字符串的拷贝操作。而此时操作的对象是用户名字符串,看来以后要对用户名下手。

00475AC3  |.  C785 74FBFFFF>mov     dword ptr [ebp-48C], 0
00475ACD  |.  EB 0F         jmp     short 00475ADE
00475ACF  |>  8B8D 74FBFFFF /mov     ecx, [ebp-48C]
00475AD5  |.  83C1 01       |add     ecx, 1
00475AD8  |.  898D 74FBFFFF |mov     [ebp-48C], ecx
00475ADE  |>  8B95 74FBFFFF  mov     edx, [ebp-48C]
00475AE4  |.  3B95 54F9FFFF |cmp     edx, [ebp-6AC]
00475AEA  |.  7D 35         |jge     short 00475B21
00475AEC  |.  8B85 74FBFFFF |mov     eax, [ebp-48C]
00475AF2  |.  8A8D 68F9FFFF |mov     cl, [ebp-698]
00475AF8  |.  028C05 FCFBFF>|add     cl, [ebp+eax-404]
00475AFF  |.  888D 68F9FFFF |mov     [ebp-698], cl
00475B05  |.  8B95 68F9FFFF |mov     edx, [ebp-698]
00475B0B  |.  81E2 FF000000 |and     edx, 0FF
00475B11  |.  8B85 F0FBFFFF |mov     eax, [ebp-410]
00475B17  |.  03C2          |add     eax, edx
00475B19  |.  8985 F0FBFFFF |mov     [ebp-410], eax
00475B1F  |.^ EB AE         \jmp     short 00475ACF

  熟悉汇编的朋友一看,就知道这是一段循环代码,它的操作对象是用户名字符串。它计算用户名字符串usrname中各个字符的ASCII的累加和与重叠累加和。举个例子说吧,假如输入的用户名是“HQ”,那么它会得到两个计算结果,一个是“H”和“Q”字符ASCII码的累加和accumulation,accumulation=0x48+0x51=0x99;0x48和0x51分别是字符“H”和“Q”的ASCII码的十六进制形式;另一个结果是重叠累加和,sum=0x48+(0x48+0x51)=0xE1,发现不同了吗?其实这个结果是“H”+(“H”+“Q”),故谓之重叠累加,要是输入的用户名是“LHZ”,那么sum=‘L’+(‘L’+‘H’)+(‘L’+‘H’+‘Z’)。我们把这步操作换成C语言的形式,写在一个函数里:

      int ModifyID( char ID[], int length, int& accumulation )
      {
          int i = 0, sum = 0x00;

      accumulation = 0x00;
          for ( i=0; i<length; i++ )
          {      
            accumulation += ID[i];
      sum += accumulation;
          }     

      return sum;
      }

  这里的sum和accumulation分别对应计算得到的累加和与重叠累加和。

00475B21  |>  68 00020000   push    200                           ; |n = 200 (512.)
00475B26  |.  6A 00         push    0                             ; |c = 00
00475B28  |.  8D8D 70F9FFFF lea     ecx, [ebp-690]                ; |
00475B2E  |.  51            push    ecx                           ; |s
00475B2F  |.  E8 9C650000   call    <jmp.&MSVCRT.memset>          ; |memset
00475B34  |.  83C4 0C       add     esp, 0C
00475B37  |.  6A 08         push    8                             ; |n = 8
00475B39  |.  8B55 0C       mov     edx, [ebp+C]                  ; |
00475B3C  |.  52            push    edx                           ; |src
00475B3D  |.  8D85 70F9FFFF lea     eax, [ebp-690]                ; |
00475B43  |.  50            push    eax                           ; |dest
00475B44  |.  E8 13660000   call    <jmp.&MSVCRT.memcpy>          ; |memcpy
00475B49  |.  83C4 0C       add     esp, 0C
00475B4C  |.  8D8D 70F9FFFF lea     ecx, [ebp-690]                ;  // regi code ASC to NUM
00475B52  |.  51            push    ecx                           ; |Arg1
00475B53  |.  E8 FEFAFFFF   call    00475656                      ; |DRW.00475656

  这里在一系列准备之后,就开始对输入的序列号下手了,最后一句的call指令明显和其他几个call不一样,这个函数是开发公司自己写的,不能从名字推断出他做了些什么事情。在深入调试之后,原来它取了输入的序列号的前八个字符组成一个字符串,将这个字符串传换为数值。假如输入的前八个是“12345678”,转换结果就是0x12345678;要是“EF54BCAD”,结果就是0xEF54BCAD。这都是16进制数。我们将转换得到的数字保存在变量num里,方便下面的解说。

00475B58  |.  8985 F4FBFFFF mov     [ebp-40C], eax    ;  eax保存转换得到的num    
00475B5E  |.  8B95 F0FBFFFF mov     edx, [ebp-410]    ;  edx保存刚才计算得到的用户名重叠累加和sum
00475B64  |.  0395 F4FBFFFF add     edx, [ebp-40C]    ;  计算 sum+num  
00475B6A  |.  8995 F0FBFFFF mov     [ebp-410], edx    ;  计算结果保存于 [ebp-410],我们取变量名r1
00475B70  |.  8B85 F4FBFFFF mov     eax, [ebp-40C]    
00475B76  |.  0385 F0FBFFFF add     eax, [ebp-410]    ;  eax保存刚才计算得到r1
00475B7C  |.  8B8D 68F9FFFF mov     ecx, [ebp-698]    ;  ecx保存刚才计算得到的用户名累加和accumulation  
00475B82  |.  81E1 FF000000 and     ecx, 0FF      ;  ecx保存的值是刚才计算得到的sum的值,取低8位
00475B88  |.  03C1          add     eax, ecx      ;  r2 = accumulation + (ecx & 0xFF)
00475B8A  |.  8B95 74FBFFFF mov     edx, [ebp-48C]    
00475B90  |.  33C9          xor     ecx, ecx
00475B92  |.  8A8C15 FBFBFF>mov     cl, [ebp+edx-405]    ;  ecx是输入的用户名的最后一个字符的ASCII值,username[last]
00475B99  |.  03C1          add     eax, ecx      ;  r3=r2+(int)username[last]
00475B9B  |.  33D2          xor     edx, edx
00475B9D  |.  B9 07000000   mov     ecx, 7      
00475BA2  |.  F7F1          div     ecx        ;  计算r3 % 7,即r3除以7的余数
00475BA4  |.  8995 F8FBFFFF mov     [ebp-408], edx    ;  r3除以7的余数保存于[ebp-408]中,取变量名row
00475BAA  |.  8B95 F8FBFFFF mov     edx, [ebp-408]
00475BB0  |.  C1E2 04       shl     edx, 4      ;  计算 edx×16
00475BB3  |.  8D8415 78FBFF>lea     eax, [ebp+edx-488]    ;  完成在基准数组base的定位,即以下取base[row]这一行的数据
00475BBA  |.  8985 70FBFFFF mov     [ebp-490], eax

  计算序列号的过程终于拉开了序幕。上面这一段,就开始了序列号计算的一连串变换。程序大喊一声,“我要变形了!”,就开始变了。对照刚才的分析结果,具体的变法都写在了语句后面的注释里了。

00475BC0  |.  8B8D 70FBFFFF mov     ecx, [ebp-490]
00475BC6  |.  8B51 0C       mov     edx, [ecx+C]
00475BC9  |.  3395 F0FBFFFF xor     edx, [ebp-410]
00475BCF  |.  8995 58F9FFFF mov     [ebp-6A8], edx
00475BD5  |.  8B85 70FBFFFF mov     eax, [ebp-490]
00475BDB  |.  8B48 04       mov     ecx, [eax+4]
00475BDE  |.  338D F0FBFFFF xor     ecx, [ebp-410]
00475BE4  |.  898D 5CF9FFFF mov     [ebp-6A4], ecx
00475BEA  |.  8B95 70FBFFFF mov     edx, [ebp-490]
00475BF0  |.  8B42 08       mov     eax, [edx+8]
00475BF3  |.  3385 F0FBFFFF xor     eax, [ebp-410]
00475BF9  |.  8985 60F9FFFF mov     [ebp-6A0], eax
00475BFF  |.  8B8D 70FBFFFF mov     ecx, [ebp-490]
00475C05  |.  8B11          mov     edx, [ecx]
00475C07  |.  3395 F0FBFFFF xor     edx, [ebp-410]
00475C0D  |.  8995 64F9FFFF mov     [ebp-69C], edx

  这一段看着充满了一种对称的美感,就是序列号最后生成保存的地方。在base[row]中去数字,分别和刚才计算得到的变量r1做异或操作。

  第一个结果是 base[row][3]  XOR  r1
  第二个结果是 base[row][1]  XOR  r1
  第三个结果是 base[row][2]  XOR  r1
  第四个结果是 base[row][0]  XOR  r1
  
  我们把整个过程翻译成C语言,Transform(SN, num, strlen(SN))函数将输入序列号的前八个字符组成的字符串SN转换为对应的数值,保存于变量num中:

    int sum = ModifyID(ID, length, accumulation);
    
    if ( !Transform(SN, num, strlen(SN)) )
    {
      cerr<<" Illegal input of SN !"<<endl;
      exit(0);
    }
      
    int r1 = sum+num;
    int r2 = r1+num;
    r2 += (accumulation & 0xFF);
    r2 += ID[length-1];
    
    int row = r2 % 7;
    
    SerialNumber[0] = num;
    SerialNumber[1] = base[row][3] ^ r1;
    SerialNumber[2] = base[row][1] ^ r1;
    SerialNumber[3] = base[row][2] ^ r1;
    SerialNumber[4] = base[row][0] ^ r1;

  细心的你已经发现,SerialNumber数组是存放序列号的数组,但为什么这里它有5个元素呢?在对后来的代码的分析中,它将num,和刚才四次异或操作的结果都作为序列号的组成部分。而接下来,只是要将这些数值在转换回字符串而已。程序用了sprintf函数完成这一步。具体就不做分析。

  用户名和最后生成的序列号就具有下列的形式:

  Username    :  LHZ
  SerialNumbe  :  12345678-XXXXXXXX-XXXXXXXX-XXXXXXXX-XXXXXXXX

  用户名和序列号的前八个字符是可以任意输入的,而后面的四个数字是根据用户名和序列号的前八个字符算出来的,这就是这个数据恢复软件的序列号生成算法。

  在C语言下实现了算法以后,计算出一组序列号,一注册,果然成功!

  前前后后,总共花了三个小时,终于解除了这道题!^_^

[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

收藏
免费 0
支持
分享
最新回复 (2)
雪    币: 234
活跃值: (1659)
能力值: ( LV9,RANK:410 )
在线值:
发帖
回帖
粉丝
2
支持并学习
2007-5-12 22:36
0
雪    币: 201
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
真详细,努力学习~~
2007-5-13 01:17
0
游客
登录 | 注册 方可回帖
返回
//