通常,我们会遇到项目中的一些数据、模型、资源文件保护问题,以防被人挪用,或者泄露流重要、敏感信息。本文即讨论这种外部资源文件的保护,以Windows环境下的模型文件的保护为例:(如果是linux,方法一不可行,只能将方法一用字节数组加载,参考https://blog.csdn.net/flyingleo1981/article/details/8193964)
主要考虑两种方法,一种是将外部资源文件嵌入二进制文件中(exe、dll、lib),然后将二进制文件加壳保护,这种方法开发量小,仅需要将资源文件嵌入并在运行时加载。
二是将外部文件加密,加密方法可以自定义,在加载模型文件前解密,考虑到安全性,防止解密后的模型文件暴露于内存被轻易dump,考虑使用流式加密的方法进行加解密,由此相对安全一点。
由于项目加载模型文件采用的是c++的文件流进行,而加载模型文件的方法封装成了库,所以这两种问题中都涉及一个问题是继承实现一个输入流,无论是从内存中加载还是解密后加载。其中嵌入资源文件的方法有个简单但安全性不高效率略低的做法可以避开继承输入流,即直接将嵌入资源重新加载到内存后将内存中的数据先写入某个流再从中读取。
继承iostream实现自定义都流输入输出,重点在于实现自定义的streambuf管理用于输入输出的缓冲区,因为根据继承关系,fstream、stringstream都是从源(file、string)处读取/写入至streambuf中,当streambuf已全部读取/写满溢出,则完成缓冲区的更新/序列化,因而关键是实现streambuf中的buf管理。有几篇博文讲的比较清楚,这里不再复述:
最全面的资料当然是官方流的说明文档:
http://www.cplusplus.com/reference/streambuf/streambuf/
https://izualzhy.cn/stream-buffer 这篇代码相对规范,更规范的参考南加大的例子:http://ilab.usc.edu/rjpeters/groovx/stdiobuf_8h-source.html
http://kaiyuan.me/2017/06/22/custom-streambuf/ 这篇讲的通俗易懂
https://wizmann.tk/phxrpc-1.html 这篇稍微深入点,并可以对照实现 putback 功能
所以,当采用资源嵌入的方式的时候,在windows下:
1 2 3 4 5 6 |
#include "resource.h" #include <Windows.h>
HRSRC hRes = ::FindResourceA(NULL, MAKEINTRESOURCEA(IDR_MODEL_CRY), "MODEL"); HGLOBAL hMem = ::LoadResource(NULL, hRes); DWORD dwSize = ::SizeofResource(NULL, hRes); |
然后将hMem所指向的内存作为只读缓冲区,用于streambuf的输入流缓冲区,dwSize为缓冲区大小,则可以直接从该内存区域使用流的操作进行读取,从而避免了内容在内存中的反复拷贝。
参考实现为 https://wizmann.tk/phxrpc-1.html 的 char_array_buffer
第二中实现的加解密流,重点是在继承实现的streambuf中完成对加解密流和文件指针的管理。文件的读取对应文件读指针fin_,输入缓冲区gbuf_,当输入缓冲区空的时候调用underflow,此时从文件读取并解密填充输入缓冲区,而文件写入对应写文件指针fout_, 以及输出缓冲区pbuf_,当输出缓冲区满时调用overflow,其内部调用sync将缓冲区内容加密并写入文件。
这里加解密算法选择的aes-ofb,其实是有讲究的,也可以选择aes-cfb,但是aes-ecb和aes-cbc(aes-pcbc)是块加密方式,涉及填充,稍微复杂些,应用于流式读写的时候需要设计合理的填充方式,当追加写入或者非整块刷入磁盘再次写入的时候要解除填充,不能随时刷入磁盘,随时再从磁盘读取并立即写入。其余的aes-cfb、aes-ofb、aes-ctr(aes-icm/aes-sic)均为流加解密模式,其中aes-ofb为流密钥异或明/密文模式,相对简单,aes-cfb在aes-ofb基础上引入和前述密文的关联,所以相对安全一些(密文部分串改将影响后续所有解密过程),而aes-ctr模式则更巧妙些,支持随机访问模式,因而可以更灵活的完成加解密功能,但是由于我们此处是对流进行操作,所以并不需要如此。关于aes加解密内容,参考如下链接:
https://blog.csdn.net/jerry81333/article/details/78336616
具体的代码:
加密的输出流实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
int CryptoStreamBuf::sync() { int encrypted = 0; int toEncrypt = pptr() - pbase(); // data that can be flushed char buf[MAX_BUFF_SIZE]; AesOfbXor(&aesOutCtx_,pbase(),buf,toEncrypt);
int ret = fwrite(buf,1,toEncrypt,fout_); if (ret == toEncrypt){ fflush(fout_); } else { return -1; } setp(pbase(), pbase() + bufSize_); // reset the buffer pbump(0); // reset pptr to buffer head return 0; }
int CryptoStreamBuf::overflow(int c) { if (-1 == sync()) { return traits_type::eof(); } else { // put c into buffer after successful sync if (!traits_type::eq_int_type(c, traits_type::eof())) { sputc(traits_type::to_char_type(c)); }
// return eq_int_type(c, eof()) ? eof():c; return traits_type::not_eof(c); } } |
解密的输入流实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
int CryptoStreamBuf::underflow() { char buf[MAX_BUFF_SIZE]; int ret = fread(buf,1,bufSize_, fin_); if (ret > 0) { char bufDecrypt[MAX_BUFF_SIZE]; AesOfbXor(&aesInCtx_,buf,bufDecrypt,ret); memcpy(eback(),bufDecrypt,ret); setg(eback(), eback(), eback() + ret); return traits_type::to_int_type(*gptr()); } else { return traits_type::eof(); } } |
此外,实际应用还应该注意的是,加解密的密钥的保护,不要静态编译于代码中,而应该通过一定的运算生成,否则很容易反编译获得,同时应该加入一定的反调试手段,防止直接调试获取到密钥,简单一些的方法直接用工具软件加壳保护等等。
源码下载:
https://download.csdn.net/download/atp1992/10963458
原文参见:
http://www.straka.cn/blog/general-resources-file-protect-encrypted-file-stream/