万里长征第三步:Posbase 1.04注册算法分析

解密者:冲天剑@pediy.com
工具:peid 0.94, ollyice 1.10


0. 导言

  Posbase 1.04是个国际象棋方面的应用程序,据其作者称它可以用来储存国际象棋
开局的特定局面,作成数据库以方便查询检索。至于这样做有什么特别好处,却未详加
介绍。一个引人注目的缺点是占用磁盘空间过大,譬如一个1,000,000盘对局的数据库
竟需要7GB,而相同数量对局的chessbase格式数据库只要一张CD就能装下了。

  下载地址:hxxp://www.wmlsoftware.com/download.html(请自行改正)


1. 解密

    (1)PEID查壳,结果显示Borland Delphi 4.0 - 5.0 [Overlay]。

  (2)运行原程序试注册,出现错误信息:"Invalid code"。用调试器载入,查找此
字串。


///////////    以下是代码    /////////////

00591B02   .  8B45 E8       mov     eax, [ebp-18]
00591B05   .  8D55 E4       lea     edx, [ebp-1C]
00591B08   .  E8 1370E7FF   call    00408B20
00591B0D   .  8B45 E4       mov     eax, [ebp-1C]
00591B10   .  5A            pop     edx
00591B11   .  E8 82060000   call    00592198
00591B16   .  84C0          test    al, al
00591B18   .  75 75         jnz     short 00591B8F                   ; 关键跳转
00591B1A   .  E8 C5B8F1FF   call    004AD3E4
00591B1F   .  33C0          xor     eax, eax
00591B21   .  55            push    ebp
00591B22   .  68 6C1B5900   push    00591B6C
00591B27   .  64:FF30       push    dword ptr fs:[eax]
00591B2A   .  64:8920       mov     fs:[eax], esp
00591B2D   .  E8 7652E7FF   call    <jmp.&kernel32.GetTickCount>     ; [GetTickCount
00591B32   .  8945 F8       mov     [ebp-8], eax
00591B35   >  E8 6E52E7FF   call    <jmp.&kernel32.GetTickCount>     ; [GetTickCount
00591B3A   .  8945 F4       mov     [ebp-C], eax
00591B3D   .  8B45 F4       mov     eax, [ebp-C]
00591B40   .  3B45 F8       cmp     eax, [ebp-8]
00591B43   .  72 14         jb      short 00591B59
00591B45   .  8B45 F8       mov     eax, [ebp-8]
00591B48   .  05 10270000   add     eax, 2710
00591B4D   .  73 05         jnb     short 00591B54
00591B4F   .  E8 6013E7FF   call    00402EB4
00591B54   >  3B45 F4       cmp     eax, [ebp-C]
00591B57   .^ 73 DC         jnb     short 00591B35
00591B59   >  33C0          xor     eax, eax
00591B5B   .  5A            pop     edx
00591B5C   .  59            pop     ecx
00591B5D   .  59            pop     ecx
00591B5E   .  64:8910       mov     fs:[eax], edx
00591B61   .  68 731B5900   push    00591B73
00591B66   >  E8 35B8F1FF   call    004AD3A0
00591B6B   .  C3            retn
00591B6C   .^ E9 B71AE7FF   jmp     00403628
00591B71   .^ EB F3         jmp     short 00591B66
00591B73   .  33C9          xor     ecx, ecx
00591B75   .  BA 101C5900   mov     edx, 00591C10                    ;  ASCII "Invalid code"
00591B7A   .  8B45 FC       mov     eax, [ebp-4]
00591B7D   .  E8 92B4F1FF   call    004AD014
00591B82   .  8B45 FC       mov     eax, [ebp-4]
00591B85   .  33D2          xor     edx, edx
00591B87   .  8990 2C020000 mov     [eax+22C], edx
00591B8D   .  EB 45         jmp     short 00591BD4
00591B8F   >  8D55 E8       lea     edx, [ebp-18]

///////////    以上是代码    /////////////


这次的关键跳转不太好找,00591B75以上的几行代码显得很没有逻辑性,不知道从程序
的什么地方能够跳到这里,而再往上的几个jb,jnb其跳转范围又局限在几行之内,不
象是关键跳转,好不容易再往前找到一个jnz跳过了错误信息,才敢推测它是关键跳转。
于是在它前面的00591B05处下断点,步过call 00592198这一行以后才从堆栈里翻出了
真注册码,跟进00592198一看,这还只是比较真码跟输入码的地方,算法还在里面一
层。不管怎样说,还是列出这个过程的一个小片段:


///////////    以下是代码    /////////////

005921F0  |.  8B45 FC       mov     eax, [ebp-4]
005921F3  |.  E8 2CFEFFFF   call    00592024                         ;  算密码的Call
005921F8  |.  8B45 E8       mov     eax, [ebp-18]
005921FB  |.  8D55 EC       lea     edx, [ebp-14]
005921FE  |.  E8 4567E7FF   call    00408948
00592203  |.  8B55 EC       mov     edx, [ebp-14]
00592206  |.  58            pop     eax
00592207  |.  E8 081EE7FF   call    00404014                         ;  比较真假码
0059220C  |.  74 04         je      short 00592212

///////////    以上是代码    /////////////


以下是跟进00592024的结果:


///////////    以下是代码    /////////////

00592024  /$  55            push    ebp
00592025  |.  8BEC          mov     ebp, esp
00592027  |.  81C4 28FFFFFF add     esp, -0D8
0059202D  |.  33C9          xor     ecx, ecx
0059202F  |.  898D 28FFFFFF mov     [ebp-D8], ecx
00592035  |.  8955 F8       mov     [ebp-8], edx
00592038  |.  8945 FC       mov     [ebp-4], eax                     ;  入口参数:EAX=用户名指针
0059203B  |.  8B45 FC       mov     eax, [ebp-4]
0059203E  |.  E8 7520E7FF   call    004040B8
00592043  |.  33C0          xor     eax, eax
00592045  |.  55            push    ebp
00592046  |.  68 6C215900   push    0059216C
0059204B  |.  64:FF30       push    dword ptr fs:[eax]
0059204E  |.  64:8920       mov     fs:[eax], esp
00592051  |.  837D FC 00    cmp     dword ptr [ebp-4], 0
00592055  |.  0F84 F0000000 je      0059214B                         ;  确保用户名指针有效
0059205B  |.  EB 0B         jmp     short 00592068
0059205D  |>  8D45 FC       /lea     eax, [ebp-4]
00592060  |.  8B55 FC       |mov     edx, [ebp-4]
00592063  |.  E8 A41EE7FF   |call    00403F0C                        ;  字符串连接
00592068  |>  8B45 FC        mov     eax, [ebp-4]
0059206B  |.  E8 941EE7FF   |call    00403F04                        ;  测串长
00592070  |.  83F8 17       |cmp     eax, 17
00592073  |.^ 7C E8         \jl      short 0059205D                  ;  与自身连接,直到串长大于23
00592075  |.  C745 F4 01000>mov     dword ptr [ebp-C], 1
0059207C  |>  8B45 F4       /mov     eax, [ebp-C]
0059207F  |.  8B55 FC       |mov     edx, [ebp-4]                    ;  连接后的串指针
00592082  |.  48            |dec     eax
00592083  |.  3B42 FC       |cmp     eax, [edx-4]
00592086  |.  72 05         |jb      short 0059208D                  ;  未到达串尾,跳
00592088  |.  E8 1F0EE7FF   |call    00402EAC
0059208D  |>  40            |inc     eax
0059208E  |.  0FB64402 FF   |movzx   eax, byte ptr [edx+eax-1]
00592093  |.  6BC0 02       |imul    eax, eax, 2                     ;  取此串中每个字符的ASCII码乘2
00592096  |.  71 05         |jno     short 0059209D
00592098  |.  E8 170EE7FF   |call    00402EB4
0059209D  |>  8B55 F4       |mov     edx, [ebp-C]
005920A0  |.  B9 80215900   |mov     ecx, 00592180                   ;  ASCII "wilhelmusvanoranjenasau"
005920A5  |.  4A            |dec     edx
005920A6  |.  3B51 FC       |cmp     edx, [ecx-4]                    ;  前二行字串的长度为23
005920A9  |.  72 05         |jb      short 005920B0                  ;  未到达串尾,跳
005920AB  |.  E8 FC0DE7FF   |call    00402EAC
005920B0  |>  42            |inc     edx
005920B1  |.  0FB65411 FF   |movzx   edx, byte ptr [ecx+edx-1]
005920B6  |.  33C2          |xor     eax, edx                        ;  与给定字串相应字符ASCII码值异或
005920B8  |.  8B55 F4       |mov     edx, [ebp-C]
005920BB  |.  83C2 01       |add     edx, 1                          ;  下一个字符
005920BE  |.  71 05         |jno     short 005920C5
005920C0  |.  E8 EF0DE7FF   |call    00402EB4
005920C5  |>  B9 80215900   |mov     ecx, 00592180                   ;  ASCII "wilhelmusvanoranjenasau"
005920CA  |.  4A            |dec     edx
005920CB  |.  3B51 FC       |cmp     edx, [ecx-4]
005920CE  |.  72 05         |jb      short 005920D5
005920D0  |.  E8 D70DE7FF   |call    00402EAC
005920D5  |>  42            |inc     edx
005920D6  |.  0FB65411 FF   |movzx   edx, byte ptr [ecx+edx-1]
005920DB  |.  33C2          |xor     eax, edx                        ;  再异或
005920DD  |.  8B55 F4       |mov     edx, [ebp-C]
005920E0  |.  4A            |dec     edx
005920E1  |.  83FA 31       |cmp     edx, 31
005920E4  |.  76 05         |jbe     short 005920EB
005920E6  |.  E8 C10DE7FF   |call    00402EAC
005920EB  |>  42            |inc     edx
005920EC  |.  898495 28FFFF>|mov     [ebp+edx*4-D8], eax             ;  保存
005920F3  |.  FF45 F4       |inc     dword ptr [ebp-C]               ;  下一个
005920F6  |.  837D F4 09    |cmp     dword ptr [ebp-C], 9
005920FA  |.^ 75 80         \jnz     short 0059207C                  ;  当比较完8个字符后另行处理
005920FC  |.  8B45 F8       mov     eax, [ebp-8]
005920FF  |.  E8 841BE7FF   call    00403C88
00592104  |.  C745 F4 01000>mov     dword ptr [ebp-C], 1
0059210B  |>  8D8D 28FFFFFF /lea     ecx, [ebp-D8]
00592111  |.  8B45 F4       |mov     eax, [ebp-C]
00592114  |.  48            |dec     eax
00592115  |.  83F8 31       |cmp     eax, 31
00592118  |.  76 05         |jbe     short 0059211F
0059211A  |.  E8 8D0DE7FF   |call    00402EAC
0059211F  |>  40            |inc     eax
00592120  |.  8B8485 28FFFF>|mov     eax, [ebp+eax*4-D8]
00592127  |.  BA 02000000   |mov     edx, 2
0059212C  |.  E8 D36BE7FF   |call    00408D04
00592131  |.  8B95 28FFFFFF |mov     edx, [ebp-D8]                   ;  把上面保存的异或值转换为ASCII字串
00592137  |.  8B45 F8       |mov     eax, [ebp-8]
0059213A  |.  E8 CD1DE7FF   |call    00403F0C
0059213F  |.  8B45 F8       |mov     eax, [ebp-8]
00592142  |.  FF45 F4       |inc     dword ptr [ebp-C]
00592145  |.  837D F4 09    |cmp     dword ptr [ebp-C], 9
00592149  |.^ 75 C0         \jnz     short 0059210B
0059214B  |>  33C0          xor     eax, eax
0059214D  |.  5A            pop     edx
0059214E  |.  59            pop     ecx
0059214F  |.  59            pop     ecx
00592150  |.  64:8910       mov     fs:[eax], edx
00592153  |.  68 73215900   push    00592173
00592158  |>  8D85 28FFFFFF lea     eax, [ebp-D8]
0059215E  |.  E8 251BE7FF   call    00403C88
00592163  |.  8D45 FC       lea     eax, [ebp-4]
00592166  |.  E8 1D1BE7FF   call    00403C88
0059216B  \.  C3            retn
0059216C   .^ E9 B714E7FF   jmp     00403628
00592171   .^ EB E5         jmp     short 00592158
00592173   .  8BE5          mov     esp, ebp
00592175   .  5D            pop     ebp
00592176   .  C3            retn

///////////    以上是代码    /////////////

在0059216C等两行处又出现没有逻辑性的代码,不过不管它了。还有一个问题,为什
么访问一个字符串指针减4的dword存储单元就总能取到这个字符串的长度,如果这样
的话那么strlen之类的函数岂不是多此一举了吗?好了,废话不多说,现在把算法总
结如下:

c$ = 常字符串 "wilhelmusvanoranjenasau"
u$ = 读入用户名字符串

循环:当 (u$长度小于23) u$ = u$与自身连接

i = 1
p$ = 空字符串

循环:当( i < 9)
  { a = u$第i个字符ASCII码 * 2
    b = c$第i个字符ASCII码
    a = a xor b
    b = c$第i+1个字符ASCII码
    a = a xor b
    s$ = 将a的16进制表示转换为长度为2的ASCII字串
  p$ = p$ 连接 s$
    i = i + 1
  }

return p$——注册码

由此可见,实际上没有必要把u$自身连接到长度大于23。并且由于异或运算的结合
律,我们可以先将c$的相邻字符先两两异或,得到一组数据,然后直接引用这组数
据。

w (119) xor i (105) = 30
i (105) xor l (108) = 5
l (108) xor h (104) = 4
h (104) xor e (101) = 13
e (101) xor l (108) = 9
l (108) xor m (109) = 1
m (109) xor u (117) = 24
u (117) xor s (115) = 6


    (3)注册函数

char *szHexToAsc(unsigned char uValue)
/* 实现把一字节的16进制数值uValue转化为长度为2的字符串表示 */ 

{
     /* 代码略 */


char *szStrCat(char *szString1, char *szString2)
/* 将字符串szString2追加到去掉'\0'的szString1末尾 */
/* szStrCat返回追加后的szString1指针 */ 
{
     /* 代码略 */
}

unsigned int uStrLen(char *szString)
/* 返回字符串szString的长度 */

{
     /* 代码略 */
}

void KeyGen(char *szUserName, char *szUnlockCode)
/* 注册码计算程序 */ 
/* 入口参数:szUserName——用户名(应在主调程序中留出足够大数组空间) 
             szUnlockCode——用于存放注册码的缓冲区(初始化为0) 
   出口参数:szUnlockCode——注册码
*/ 

{
     unsigned char a, i = 0, xordata[] = {30, 5, 4, 13, 9, 1, 24, 6};
     if (uStrLen(szUserName) == 0) return;
     while(uStrLen(szUserName) < 8) 
        szUserName = szStrCat(szUserName, szUserName);
     while(i < 8)
        {
             a = (szUserName[i] << 1) ^ xordata[i];  
             szUnlockCode = szStrCat(szUnlockCode, szHexToAsc(a));
             i = i + 1;
        }     
     return;
}


2. 感言

  到目前为止鄙人所解的程序都没有壳保护,从某个层面上说颇有专捏软柿
子之嫌,其实我也碰到过不少有壳保护的软件,并且也很想学脱壳,但研究电脑
毕竟不是我的专业,并且我还正在做自己方向的论文,怕在这方面投入太多精力
会影响工作,另外我的个人电脑坏了,目前还没钱买新的,从一方面说也是没条
件。