krkr2加解密插件编写方法

标签:吉里吉里

一、
关于为什么要加密,以及加密的方法就不详述了。前者,每人都有自己的理由,比如保护自己的版权;后者请翻翻文档。

插件的编写需要会用VC++6.0,能用C/C++写程序(并不需要会写WIN32程序);否则可以54该文了。以上。

二、
要加/解密XP3文件,还是先来研究下XP3文件的结构吧。(了解就行了,并不用理解)
以下部分摘自别人的资料:
文件开头是文件标志,为"XP3"
偏移11处是文件信息表的位置,uint64
从上面的值指示跳到文件信息表处,有以下一个结构:
struct    sXP3Info
{
    byte_t    zlib; // 文件信息表是否用zlib压缩过
    uint64    psize; // 文件信息表在包文件中的大小
#if zlib
    uint64    rsize; // 文件信息表解压后的大小
#endif
    byte_t    fileInfo[psize]; // 文件信息表数据
};
成功获取文件信息表数据后可以得到以下一组结构的数组,用于描述包中文件的信息:
struct    sXP3File
{
    uint32    tag1; // 标志1,"File" 0x656c6946
    uint64    fileSize; // 文件信息数据大小
    uint32    tag2; // 标志2,"info" 0x6f666e69
    uint64    infoSize; // 文件基本数据大小
    uint32    protect; // 估计是表示此文件是否加过密
    uint64    rsize; // 文件原始大小
    uint64    psize; // 文件包中大小
    uint16    nameLen; // 文件名长度(指的是UTF-16字符个数)
    wchar_t fileName[nameLen]; // 文件名(UTF-16LE编码,无0结尾)
    uint32    tag3; // 标志3,"segm" 0x6d676573
    uint64    segmSize; // 文件段数据大小
    uint32    compress; // 文件是否用zlib压缩过
    uint64    offset; // 文件开始的位置
    uint64    rsize;
    uint64    psize;
#if fileSize - infoSize - segmSize - 24 > 0
    uint32    tag4; // 标志4,"adlr" 0x726c6461
    uint64    adlrSize; // 文件附加数据大小,一般是4
    uint32    key; // 附加数据,用于加密
#endif
};
这些数据显示,XP3文件会有个文件信息表,存储了其中的文件信息。
这些信息中,某些可以用于加/解密。

三、
Miliardo様已经在《关于加密插件的编写》中翻译了一个例子,就从这开始编写加密插件吧。


运行VC++6.0,新建一个win32 dynamic-link library项目。
再新建一个C++ source file,文件名最好取为xp3enc.cpp(否则需把输出文件改名为xp3enc.dll),将《关于加密插件的编写》一文的第一段代码复制进去,保存。
再新建一个文本文件,把第2段代码复制进去,保存为xp3enc.def。
打开xp3enc.cpp,选择“build-build xp3enc.dll”,这样就应该生成好加密插件了。

用同样的方法新建个dll项目,将第3、4段代码分别保存成cpp和def文件。
然后下载吉里吉里外部C++接口库,解压后添加到该工程内。
最后build成dll文件,改后缀名为tpm文件(或选“project-settings-link”,把output file name改为tpm文件,当然也能改路径。)

再用上述2个文件测试下加密结果吧,方法就不说了,自己翻文档。
成功的话就可以继续了,否则我也没什么好说的了。

四、
下面开始分析这2个文件吧,先从加密开始。

对于我们来说,唯一要写的就是XP3ArchiveAttractFilter_v2这个函数,所以这里只介绍它。
extern "C" void __stdcall XP3ArchiveAttractFilter_v2(
        unsigned __int32 hash,
        unsigned __int64 offset, void * buffer, long bufferlen)
{
        // 接口版本 2 可以接受并使用以下的参数
        // hash      : 输入文件(解密后)内容的32位 Hash (散列)值
        // offset    : "Buffer" 成员所指向的数据偏离这个文件头部的偏移值
        //             (当文件被压缩时,将显示文件为压缩状态下的字节偏移
        //                                值)
        // buffer    : 指向要加密的数据的指针。当文件被压缩时,给出的是文
        //             件被压缩前的数据。
        //             ( 本函数不能更改压缩后的文件数据 )
        // bufferlen : "buffer" 参数给出的数据块的长度

        // 在这里作为范例,我们示范把数据和 hash 最后一字节进行 XOR 的方法。

        int i;
        for(i = 0; i < bufferlen; i++) ((unsigned char*)buffer)[i] ^= hash;

}
函数声明就不说了,必须这么写。
注释已经很清楚了,加密时可以用到4个值:
hash是解密后的文件hash值,这个编写解密插件时也能获取,所以我们可以用它来加密。
buffer是该块的指针。加密时,1个文件可能被分成多个块,每个块对应一个buffer。
offset是该块的块头与文件头的偏移量。对于该块的第i个字节,它对于文件头的偏移量应为offset+i字节。
bufferlen是该块的长度,因为解密时可能块长不是一样的,所以一般不会用这个来加密(否则没法解密了)。

再研究这句吧:
for(i = 0; i < bufferlen; i++) //在这个块中遍历
        ((unsigned char*)buffer)[i] ^= hash;//对于块中的每个字节(((unsigned char*)buffer)[i]),都与hash异或。因为只有一个字节,所以实际上是和hash的最后1个字节异或。

然后是解密。也只研究1个函数:TVPXP3ArchiveExtractionFilter。
void TVP_tTVPXP3ArchiveExtractionFilter_CONVENTION
        TVPXP3ArchiveExtractionFilter(tTVPXP3ExtractionFilterInfo *info)
{
        // TVPXP3ArchiveExtractionFilter 函数是会从吉里吉里本体调用的回调函数。
        // 能得到的参数只有一个,就是对 tTVPXP3ExtractionFilterInfo 结构体的指针。

        // TVPXP3ArchiveExtractionFilter 需要在后述的 V2Link 函数内使用
        // 吉里吉里本体的 TVPSetXP3ArchiveExtractionFilter 函数设定。

        // 这个样例只是纯粹的对 xp3enc.dll 样例代码加密的 XP3 文件包解密
        // 将所有的数据以 FileHash 的最后一个字节作异或(XOR)运算

        // 请注意,这个函数有可能在多个线程中被调用。

        /*
                tTVPXP3ExtractionFilterInfo 的成员如下
                * SizeOfSelf        : 结构体自身的大小
                * Offset            : "Buffer" 成员所指向的数据偏离这个文件头部的偏移值
                * Buffer            : 数据本体
                * BufferSize        : "Buffer" 成员所指向的数据的大小(字节单位)
                * FileHash          : 解密后文件内容的32位 Hash (散列)值
        */

        // 检查结构体的大小
        if(info->SizeOfSelf != sizeof(tTVPXP3ExtractionFilterInfo))
        {
                // 当结构体的大小错误,则最好投出异常
                TVPThrowExceptionMessage(TJS_W("Incompatible tTVPXP3ExtractionFilterInfo size"));
                        // TVPThrowExceptionMessage 是用于投出异常的函数
                        // 本函数不会返回。
        }

        // 復号
        tjs_uint i;
        for(i = 0; i < info->BufferSize; i++)
                ((unsigned char *)info->Buffer)[i] ^= info->FileHash;
}

注释中也能看到,我们可以获取5个参数。
FileHash等于加密时用到的hash变量。
Buffer是解密时的块。据我分析,解密时应该是XP3内包含多个文件,每个文件分成多个块。
Offset是该块的块头与XP3中对应文件的文件头的偏移量。
BufferSize是该块大小,可能和加密时的bufferlen不同,所以一般不用。
SizeOfSelf是结构体的大小,用于检查的。

再分析下函数的主体:
         tjs_uint i;//定义一个无符号整型变量
        for(i = 0; i < info->BufferSize; i++)
                ((unsigned char *)info->Buffer)[i] ^= info->FileHash;//((unsigned char *)info->Buffer)[i]就是块中的元素,照样还是和info->FileHash异或。

补充下异或的常识吧:
a ^ b ^ b == a。

因此就完成解密了。

五、
下面开始做我们自己的加解密插件吧。
先补充下解密的常识。解密函数必须是加密函数的反函数,即对于数据data,加密后为code(data),解密后为encode(code(data)),其值应等于data。

反函数有很多,比如异或和异或,加和减,循环左移和循环右移等。但有些反函数可能会丢失数据,如乘和除,乘方和对数,正切和反正切,左移和右移,所以这些一般是不能用的。

另外反函数是可以组合的。比如f2'(f1'(f1(f2(data)))) == data。('用于表示反函数,因为没法写上标的-1)。也就是只要按相反的顺序解密就行了。

下面来试着做吧。
1.先来最简单的,所有的数都加1。
         for(int i = 0; i < bufferlen; i++) ((unsigned char*)buffer)[i] += 1;
相应的解密插件为
        for(tjs_uint i = 0; i < info->BufferSize; i++)
                ((unsigned char *)info->Buffer)[i] -= 1;

忘说了,unsigned char类型的数据,值为[0, 255],处于2端时,0 - 1 == 255,255 + 1 == 0,所以这个是没问题的。

2.再改为与偏移量相加吧。
        for(int i = 0; i < bufferlen; i++) ((unsigned char*)buffer)[i] += (i + offset);
解密插件为
        for(tjs_uint i = 0; i < info->BufferSize; i++)
                ((unsigned char *)info->Buffer)[i] -= (i + info->Offset);
记住别忘了加offset,因为加解密时分块可能不一样。

3.接下来试试循环左/右移
unsigned char code, left, right;        //分别为加密后的1个字符,左移后的字符,右移后的字符

for (int index = 0; index < bufferlen; ++index, ++offset)        //此处offset实际是offset + index
{
        code = ((unsigned char*)buffer)[index];

        //循环左移(index % 8)位,不知道为何要转换成unsigned char,否则会警告。
        left = unsigned char(code << (offset % 8));
        right = unsigned char(code >> (8 - (offset % 8)));
        code = unsigned char(left | right);

        ((unsigned char*)buffer)[index] = code;
}
相应的加密插件,循环部分就不写了。
left = encode << (8 - (offset % 8));
right = encode >> (offset % 8);
encode = left | right;
当然,循环移位可以用汇编来实现,但我感觉汇编还没这段C代码快,所以就不给例子了。

4.然后再试试分段加密。
if (offset == 12)
{
        code ^= 34;
}
else if (offset == 56)
{
        code ^= 78;
}
else
{
        code ^= 90;
}
解密代码实际是一样的,所以不重复了。

5.接着来试试交换每个字节的第2和第8位。
struct BitCode
{
        unsigned b1 : 1;
        unsigned b2 : 1;
        unsigned b3to7 : 5;
        unsigned b8 : 1;
};

union ByteCode
{
        unsigned char code;
        BitCode bitCode;
}byteCode;

//交换第2、8位
byteCode.code = ((unsigned char*)buffer)[index];
BitCode tempCode = byteCode.bitCode;
tempCode.b8 = byteCode.bitCode.b2;
tempCode.b2 = byteCode.bitCode.b8;
byteCode.bitCode  = tempCode;
((unsigned char*)buffer)[index] = byteCode.code;
解密时仍然是相同的代码,不重复了。也可以试着用汇编,我是懒得用了。

6.最后试试怎么用伪随机数加密吧。
先补充下常识。C/C++库中的随机函数是伪随机数,使用前要先设定种子。对于相同的种子,生成的随机数列是相同的。
所以我们只要给加密与解密相同的种子,就能生成相同的随机数列了。而这个种子自然可以直接选hash。
另外,别忘了文件可能很大,如果加密1个1G的文件,就得计算10亿次随机数了。我们当然不会这么做,其实只要一段随机数就足够了。
下面来看看实际的代码。
#include <stdlib.h> //这个请放在文件开头,调用随机函数用的。

int const CODE_SIZE = 512;        //随机数列的大小
unsigned char codes[CODE_SIZE];

//生成随机数列
srand(hash);
for (int index = 0; index < CODE_SIZE; ++index)
{
        codes[index] = rand();
}

for (index = 0; index < bufferlen; ++index, ++offset)        //此处offset实际是offset + index
{
        //和随机数列异或
        ((unsigned char*)buffer)[index] ^= codes[offset % CODE_SIZE];
}
解密代码实际和加密非常相似,大家有兴趣自己试试吧。

六、
其实还有很多方法可以用,大家开动脑筋想想自己的算法吧,我只是提个思路而已。其实随意几个算法组合起来就已经够难破解了。
另外,解密时必须能够直接得到1个文件任意偏移量出的实际值。也就是说解密文件a的第100个字节时,不能依赖该文件其他字节的值,如第99个字节。
最后别忘了发布时设置为release模式,不然会带很多调试信息,使插件变大变慢,而且易破解。如果还想进一步保护的话,可以试试加壳,但千万别清除导出表(即def文件生成的信息),不然无法使用。如果不想发布时附带tpm文件的话,可以用一些能把dll和exe打包在一起的加壳程序。

0条评论 你不来一发么↓

    想说点什么呢?