Articles

小青蛙3D折纸软件序列号算法分析

小青蛙3D折纸软件是一款以三维立体的方式全方位、多视角指导用户制作模型的软件。 对于一个模型无论是从正面、反面、或侧面,每一个细节,都可以看得清清楚楚。 软件提供了90个折纸模型,包括飞禽走兽、水底游、日常物品等多种分类。 软件还配有丰富的模型贴图,使用软件打印出彩图可以折出漂亮的模型。 用户还可以使用软件输出的折纸线框图制作出个性化的模型。

由于涉及到的代码过长,所以本文只介绍一下具体算法,并且这个算法只是该软件中所使用的算法的一部分, 另一部分我还没有分析出来,因为这一部分验证失败后,没有任何提示,只是用GDI+画出了一个需要注册的图片 (这个图片应该也属于折纸模型的一部分,不太容易设断点跟踪)。

第一步

首先,在用户输入用户名和序列号点击注册按钮之后,该软件先判断输入的序列号是不是12位 (除去其中的0x20(‘ ‘)与0x2D(‘-‘)),如果不是12位,直接弹出窗口说 “不正确的注册码。”。 如果序列号是12位,那么转第二步。

第二部

下面假定输入的序列号是: BD5A-QXXW-QK3L(为了方便区分,每4位用-分开) 该软件用一个固定的字符串来计算序列号中每个字母出现的位置,字符串如下:

Section Virtual Addr. 0123456789ABCDEF0123456789ABCDEF
------- ------------- --------------------------------
.rdata 006B7C60 4BNPG8JK5AH6MVWULFCX79ED013Q2RTY

这个字符串是{0-9,A-Z} – {I,O,S,Z}的一个排列,总共0x20字节。计算出来的序列号中每个字符具体位置如下:

B(01)D(17)5(08)A(09) Q(1B)X(13)X(13)W(0E) Q(1B)K(07)3(1A)L(10)

因为序列号中总共有0x20(32)个可能字符,每个字符用5bit表示就足够了,将对应位置表示成二进制,就像下面这样:

字符 十六进制表示 二进制表示
---- ------------ -----------------------
BD5A 01 17 08 09 00001 10111 01000 01001
QXXW 1B 13 13 0E 11011 10011 10011 01110
QK3L 1B 07 1A 10 11011 00111 11010 10000

将序列号的二进制表示从序列号的最后一个字符写到第一字符:

L     3     K     Q     W     X     X     Q     A     5     D     B
10000 11010 00111 11011 01110 10011 10011 11011 01001 01000 10111 00001

这样就组成了一个60bit 的数字,计算机中每一个十六进制数可用 4 bit表示,对上面60bit的串重新分割,每4bit一组:

1000 0110 1000 1111 1011 0111 0100 1110 0111 1011 0100 1010 0010 1110 0001 = 0x0868FB74E7B4A2E1 = SUM (前面填一个0,刚好用一个64bit数表示)

然后验证SUM 的每一位数字异或是否为零:

0 ^ 8 ^ 6 ^ 8 ^ F ^ B ^ 7 ^ 4 ^ E ^ 7 ^ B ^ 4 ^ A ^ 2 ^ E ^ 1 = 0

如果结果不为零,那么弹出序列好错误的窗口,否则继续下一步。

第三步

将SUM 做一简单变形,记:

SUM' = SUM xor 0x0000000000434E00 = 0x0868FB74E7F7ECE1

将SUM’两位一组,分成8 组,08 68 FB 74 E7 F7 EC E1,取后7 组进行计算。运算过程如下:

DWORD eax, edx;
DWORD data[7] = {0x68, 0xFB, 0x74, 0xE7, 0xF7, 0xEC, 0xE1};
eax = 0;
for(int i = 6; i > 0; i--)
{
    edx = data[i] << 8;
    eax ^= edx;
    for(j = 1; j <= 6; j++)
    {
        if(eax & 0x8000)
        {
            eax <<= 1;
            eax ^= 0x1021;
        }
        else
        {
            eax <<= 1;
        }
    }
    edx = eax << 1;
    if(eax & 0x8000)
    {
        edx ^= 0x1021;
    }

    eax = edx << 1;
    if(edx & 0x8000)
    {
        eax ^= 0x1021;
    }
}
eax &= 0xFF;
eax ^= 0xFF;

此时,如果eax == data[0],那么验证成功,弹出:“谢谢您的支持,请重新启动本软件。”的窗口,否则弹出序列号错误的对话框。如果验证成功,点击确定后,继续下一步。

第四步

对输入的用户名进行计算,下面假定输入的用户名是lionel。

UCHAR *szUserName = "lionel";
UCHAR *ptr = NULL;
DWORD eax, edx;
edx = 0;
eax = (DWORD)szUserName[0];
for(ptr = szUserName; eax != 0;)
{
    edx <<= 5;
    edx += eax;
    edx <<= 2;
    edx ^= eax;
    edx &= 0xFFFF;
    ptr++;
    eax = (DWORD)ptr[0];
}

经过以上计算,edx = 0x7A5C = NameCode,这就是用户名计算出来的数字。

第五步

将SUM’用NameCode加密,写入注册表,具体过程如下。

SUM' = 0x0868 FB74 E7F7 ECE1

将SUM’的后48bit分别与16bit的NameCode异或:

FB74 xor 7A5C = 8128
E7F7 xor 7A5C = 9DAB
ECE1 xor 7A5C = 96BD

将SUM’最高16位于0x00FF求与:

0868 & 00FF = 0068

再将0068 8128 9DAB 96BD xor 0006 0504 0302 0100 = 0x006E842C9EA997BD 将其按字节倒过来,并且转换成字符,记:

strRegCode = "bd97a99e2c846e"

这就是序列号加密后写在注册表中的字符串。

[HKEY_LOCAL_MACHINE\SOFTWARE\PF3DCN]
"User"="lionel"
"Serial Number"="bd97a99e2c846e"

以上,该软件重新启动之前对序列号的验证过程分析完毕。

第六步

软件重新启动后,读取注册表中的用户名与序列号,然后对序列号进行反向运算, 重新还原出SUM’,并且继续对SUM’加以新的验证。

SUM'' = SUM' xor 0x00000000434E0000 = 0x0068FB74A4B9ECE1

同样将低48bit每个字节都反过来写:E1 EC B9 A4 74 FB。用如下代码对其进行变换:

DWORD eax, ecx;
DWORD data[6] = {0xE1, 0xEC, 0xB9, 0xA4, 0x74, 0xFB};
ecx = data[0];
for(int i = 1; i < 6; i++)
{
    ecx *= 0x343FD; ecx += 0x269EC3; eax = ecx >> 16;
    eax &= 0xFF;
    data[i] ^= eax;
}

变换之后,产生的新48bit为:E1 E9 47 40 9E D9,然后用如下代码对其进行验证:

DWORD eax, ecx, edx;
DWORD data[6] = {0xE1, 0xE9, 0x47, 0x40, 0x9E, 0xD9};
eax = data[5];
for(ecx = 4; ecx != 0; ecx--)
{
    edx = data[5 - ecx];
    edx &lt;&lt;= 8;
    eax ^= edx;
    for(j = 0; j &lt; 6; j++)
    {
        if(eax &amp; 0x8000)
        {
            eax &lt;&lt;= 1;
            eax ^= 0x1021;
        }
        else
        {
            eax &lt;&lt;= 1;
        }
    }
    if(eax &amp; 0x8000)
    {
        eax &lt;&lt;= 1;
        edx = eax;
        edx ^= 0x1021;
    }
    else
    {
        edx = eax &lt;&lt; 1;
    }
    eax = edx &lt;&lt; 1;
    if(edx &amp; 0x8000)
    {
        eax ^= 0x1021;
    }
}
eax &amp;= 0xFFFF;
ecx = data[0];
eax = eax + ecx + 1;
eax &amp;= 0xFF;

此时,如果eax == 0,则验证成功,以后再点击注册按钮的时候,原来的:“本软件使用权属于:未注册”变成”本软件使用权属于:lionel”。能够通过以上所有检验的序列号,看似正确的序列号,但是在软件使用过程中,仍然会出现请注册软件字样的图片,所以肯定还存在另外一处代码,对序列号进行了如果大家感兴趣,可以继续对这个软件序列号的算法进行分析,并补充本文,也希望有兴趣的朋友,找出最后这一处判断的朋友给我写信,告诉我破解的方法。