krkr2加解密插件编写方法
2008 10 9 09:02 AM 5341次查看
关于为什么要加密,以及加密的方法就不详述了。前者,每人都有自己的理由,比如保护自己的版权;后者请翻翻文档。
插件的编写需要会用VC++6.0,能用C/C++写程序(并不需要会写WIN32程序);否则可以54该文了。以上。
二、
要加/解密XP3文件,还是先来研究下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
};
这些信息中,某些可以用于加/解密。
三、
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条评论 你不来一发么↓