集成到 Visual Studio C++ 项目中的字符串混淆系统
Michael Haephrati, CodeProject MVP 2013
下载源代码 - 16 KB
简介
混淆器的一般目的是隐藏程序代码、流程和功能。如果您的程序使用某个作为商业秘密的算法,混淆将使其更难被反向工程并揭示该商业秘密。但是隐藏可执行文件中的数据呢?有人说这几乎是不可能实现的。由于问题需要能够读取此数据,因此数据必须存在,如果存在,则最终可以被揭示。在我看来,一个精心混淆的程序可以使其几乎不可能猜到数据存储(加密)的位置,即使找到数据,也很难使用强加密对其进行混淆。
解决方案
本文将介绍的工具集,是为应用程序中的字符串加密目的而开发的。
通常,一个应用程序即使无需反向工程,也能透露大量关于自身的信息。当您使用十六进制编辑器(或记事本作为文本编辑器)打开 Calc.exe 这样的应用程序时,您可以找到类似这样的字符串
![]()
如果不加密地存储,会带来风险的字符串示例是密码。如果您的软件连接到 Internet 服务、短信网关或 FTP 服务器并发送密码,则任何使用文本编辑器打开您应用程序可执行文件的人都可以看到此密码。
背景
我阅读了 Chris Losinger 的 出色文章,并希望通过创建更强的加密(AES-256)并支持更多变体和字符串类型,包括 UNICODE、双字节和单字节字符串,将其提升到一个新的水平。
此工具的目的是专业用途,而不仅仅是概念验证。
源代码
加密/解密机制集成在两个单独的项目中,需要将它们添加到您的 Visual Studio 项目中。这两个项目位于主文件夹中
a. obfisider 项目。
b. obfuscase 项目。
Obfisider 和 Obfuscate 项目
Obfisider 项目包含 AES 加密部分。Obfuscate 项目包含扫描解决方案及其包含的每个项目的必要部分,并加密找到的字符串。
扫描解决方案解决方案通过 parseSolution 调用另一个名为 parseProject 的函数进行扫描。
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
33
34
35
36
37
38
39
40
41
42
43
44
45static strList parseSolution( const char * solName ) { strList result; static char drive[_MAX_DRIVE]; static char somepath[_MAX_PATH]; static char buffer[_MAX_PATH]; static char path[_MAX_PATH]; static char ext[_MAX_EXT]; _splitpath( solName, drive, somepath, buffer, ext ); FILE * f = fopen( solName, "r" ); if( NULL == f ) { printf("ERROR: Solution %s is missing or unavailable.\n", solName ); exit(1); } while( !feof(f) ) { char * res = fgets( buffer, sizeof(buffer), f ); if( NULL == res ) continue; if( NULL != strstr(buffer, "Project(") ) { char * ptrName = strchr( buffer, '=' ); char * ptrFile = strchr( ptrName, ',' ); *ptrFile++ = 0; char * ptrEnd = strchr( ptrFile, ',' ); *ptrEnd++ = 0; while( ('=' == *ptrName) ||(' ' == *ptrName) ||('"' == *ptrName) ) ptrName++; if( '"' == ptrName[strlen(ptrName)-1] ) ptrName[strlen(ptrName)-1] = 0; while( (' ' == *ptrFile) ||('"' == *ptrFile) ) ptrFile++; if( '"' == ptrFile[strlen(ptrFile)-1] ) ptrFile[strlen(ptrFile)-1] = 0; _makepath( path, drive, somepath, ptrFile, NULL ); result.push_back( std::string(path) ); } } fclose(f); return result; }
parseProject 函数从给定项目中提取相关文件。相关文件表示:.c、.cpp、.h 和 .hpp 文件。
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
33
34
35
36
37
38
39
40
41
42
43/** * Parse project and extract fullpath source filename from project. */ static strList parseProject( const char * projName ) { strList result; static char drive[_MAX_DRIVE]; static char somepath[_MAX_PATH]; static char buffer[_MAX_PATH]; static char path[_MAX_PATH]; static char ext[_MAX_EXT]; _splitpath( projName, drive, somepath, buffer, ext ); FILE * f = fopen( projName, "r" ); if( NULL == f ) { printf("ERROR: Project %s is missing or unavailable.\n", projName ); exit(1); } while( !feof(f) ) { char * res = fgets( buffer, sizeof(buffer), f ); if( NULL == res ) continue; if( (NULL != strstr(buffer, "<ClInclude Include=")) ||(NULL != strstr(buffer, "<ClCompile Include=")) ) { char * ptrName = strchr( buffer, '=' ); char * ptrName1 = strstr( buffer, "/>" ); if( NULL != ptrName1 ) *ptrName1 = 0; while( ('=' == *ptrName) ||(' ' == *ptrName) ||('"' == *ptrName) ) ptrName++; while( ('"' == ptrName[strlen(ptrName)-1]) ||(' ' == ptrName[strlen(ptrName)-1]) ||('\n' == ptrName[strlen(ptrName)-1])) ptrName[strlen(ptrName)-1] = 0; _makepath( path, drive, somepath, ptrName, NULL ); result.push_back( std::string(path) ); } } fclose(f); return result; }
AES_Encode 函数
此函数处理使用 AES-256 加密字符串
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
33
34
35
36/* -------------------------------------------------------------------------- */ int AES_encode_a( unsigned int key_start, const wchar_t * plainString, unsigned char * outbuf, unsigned outlen ) { unsigned char key[32]; aes_key key_context = {0}; int i; unsigned char offset; /** Calculate required size */ int retval = (wcslen(plainString) + 1); /** Round to 16 byte over */ retval = ((retval + 15)&(~0xF)) + 4; /** Memory request */ if( NULL == outbuf ) return -retval; /** Not enought memory */ if( outlen < retval ) return 0; /** Prepare output buffer */ memset( outbuf, 0, retval ); // wcscpy( (char*)(outbuf+4), plainString ); WideCharToMultiByte( CP_ACP, 0, plainString, -1, (outbuf+4), retval-sizeof(unsigned),NULL, NULL); *((unsigned*)outbuf) = key_start; /** Prepare key */ srand(key_start); for( i = 0; i < sizeof(key); i++ ) key[i] = rand(); aes_prepare( &key_context, key ); memset( key, 0, sizeof(key) ); for( i = 4; i < retval; i += 16 ) { aes_encrypt_block( &key_context, &outbuf[i] ); } memset( &key_context, 0, sizeof(key_context) ); return retval; } /* -------------------------------------------------------------------------- */
AES_Decode 函数
此函数处理将字符串解密回来
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/* -------------------------------------------------------------------------- */ static int AES_decode( const unsigned char * inbuf, unsigned inlen, void *plainString, unsigned stringSize ) { unsigned char key[32]; aes_key key_context = {0}; int i; BYTE * outbuf = (BYTE*)plainString; if( NULL == plainString ) return -inlen; if( stringSize < inlen ) return 0; /** Prepare output buffer */ memcpy( outbuf, inbuf, inlen ); /** Prepare key */ for( i = 0; i < sizeof(key); i++ ) key[i] = rand(); aes_prepare( &key_context, key ); memset( key, 0, sizeof(key) ); for( i = 0; i < inlen; i += 16 ) { aes_decrypt_block( &key_context, &outbuf[i] ); } memset( &key_context, 0, sizeof(key_context) ); return inlen; }
将字符串解密回来
ASCII 字符串使用以下函数( __ODA__ )解密
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24/* -------------------------------------------------------------------------- */ char* __ODA__( const char * enc_str ) { int i, size = strlen( enc_str )/2; unsigned char * inBuff = NULL; unsigned key = 0; PDECODED_LIST ptr = &charList; char * result = a_text_err; while( NULL != ptr->next ) { if( ptr->org_str == enc_str ) return ((char*)ptr+sizeof(DECODED_LIST)); ptr = ptr->next; } if( NULL == (inBuff = (unsigned char*)malloc( size )) ) return result; // a_text_error if( NULL == (ptr->next = (PDECODED_LIST)malloc( size + sizeof(DECODED_LIST) )) ) { free( inBuff ); return result; // a_text_error } ptr = ptr->next; ptr->
当字符串是 UNICODE 时,使用以下函数 (__ODC__)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24/* -------------------------------------------------------------------------- */ wchar_t* __ODC__( const char * enc_str ) { int i, size = strlen( enc_str )/2; unsigned char * inBuff = NULL; unsigned key = 0; PDECODED_LIST ptr = &wcharList; wchar_t * result = w_text_err; while( NULL != ptr->next ) { if( ptr->org_str == enc_str ) return (wchar_t*) ((char*)ptr+sizeof(DECODED_LIST)); ptr = ptr->next; } if( NULL == (inBuff = (unsigned char*)malloc( size )) ) return result; // w_text_error if( NULL == (ptr->next = (PDECODED_LIST)malloc( size + sizeof(DECODED_LIST) )) ) { free( inBuff ); return result; // w_text_error } ptr = ptr->next; ptr->
如何使用
设置项目依赖关系,以便您的解决方案生成的主要可执行文件或 DLL 依赖于“obfinsider”项目。
‘obfinsider’项目必须依赖于另一个项目——‘obfuscate’项目。这将自动包含 obfinsider.lib,但如果您进行了可能破坏此依赖关系的更改,请手动添加 obfisider.lib。
工作原理
过程
首先构建 'obfuscate',构建完成后,将执行一个构建后事件。构建后事件会调用 obfuscate 并将整个解决方案文件作为其参数。
文件扫描Obfuscate 扫描给定解决方案并处理其中的每个相关文件。当前版本要求路径中不存在空格,如果存在空格,调用 "obfuscate" 的正确方式应该是
"$(TargetPath)" "$(SolutionPath)"
"obfuscate" 扫描解决方案中的所有项目文件,但处理以下文件类型:.c、.cpp、.h 和 .hpp。
工作流程
对于每个文件,会进行以下检查
a. 跳过已混淆的文件
b. "obfuscate" 不处理自身,因此会跳过其自身的文件。
c. 跳过注释。这包括
// 这是注释
和
/* 这是另一个注释 */
d. #include 和 #pragma 声明将被跳过。
e. 初始化全局字符串将被忽略。如果您查看以下示例
Static char c[]="test string";
f. 对于所有其他字符串,“obfuscate”会查找原始声明,并用对解密函数的调用替换它,并将加密的字符串作为参数。
为了方便维护,原始行将作为注释行保留在新行的上方。
ASCII 与 Unicode
系统区分 ASCII 和 Unicode 文本。为每种类型使用两组单独的函数。
以下语句
wcscpy(mystring, "my text");
或
wcscpy(mystring, _T("my text"));
将被识别为 Unicode 类型,并替换为对 __ODC__ 的调用,而类似的 ASCII 语句
strcpy(mystring, "my text");
将被识别为这样,并替换为对 __ODA__ 的调用。
加密和编码方案
1. 每个字符串都使用一个临时生成的加密密钥单独加密。2. 此密钥是随机生成的,而用于随机数生成的 SEED 值在应用程序开始时设置。
3. 所有字符串都用 NULL 字符填充以使其长度匹配 AES-256 加密方案所需的完整加密块数量。
4. 结果是二进制形式,并使用以下算法表示为可打印字符集
- 每个字节都分成两半。先编码高位半字节,然后是第二半字节。例如:0xAF 分成:0xA 和 0xF。
- 编码值是前一个值和新值之间的差值(初始值为“A”)。
例如:值 0x3 是通过从字符“A”和“D”(0x40+0x3==0x43)移位而得到的。
5. 当移位值达到 0x7E 时,将从该值中减去初始值“A”。
例如:如果最后一个字符是'z'(代码 0x7A),编码值为 0xF,那么新值将编码为字符'+'(代码 0x2B == 0x7A + 0xF - (0x7E-0x20))。示例
以下语句
wprintf(L"This is a test\n" );
将被替换为以下行
1
2/* wprintf( L"This is a test\n" );*/ wprintf( __ODC__("IPPXXXXXbmm|\"$%.=KXfgpx#-;DPZiw}$$*0=APR[\\epy##$.27EKXXdhq}#/00>DEOVVW]";
局限性
不可能涵盖 C/C++ 项目中字符串出现的所有变体,尽管我已经尽力涵盖了大多数。因为人们可以通过指定来初始化一维字符数组
例如:用大括号括起来的逗号分隔的常量列表,每个常量都可以包含在一个字符中 字符串常量(常量周围的大括号是可选的)
static char a[]="some_string";
当数组大小未设置时,无法加密预定义的内容,因为在编译时不知道实际大小。
另一个此类系统无法加密的例子是所谓的“隐藏合并”
1
2
3#define PRODUCT_NAME "MyProduct" #define PRODUCT_FOLDER "MyFolder" #define WORK_PATH PRODUCT_NAME "/" PRODUCT_FOLDER
许可证
本文以及任何相关的源代码和文件,均根据 CDDL(Common Development and Distribution License) 获得许可。
关于作者
Michael N. Haephrati 是一位企业家、发明家和音乐家。Haephrati 参与了许多项目,从 HarmonySoft 开始,设计了 Rashumon,这是 Amiga 计算机的第一个图形多语种文字处理器。在 1995-1996 年期间,他在加州库比蒂诺担任 Apple 的合同工。在一家研究所工作,迈出了在以色列开发信用评分领域的第一步。他创立了 Target Scoring,并基于地理统计数据开发了一个名为 ThiS 的信用评分系统,参与了 VISA CAL、Isracard、Leumi 银行和 Discount 银行(Target Scoring,作为一家大型以色列机构的业务发展副总裁)。
2000 年,他创立了 Target Eye,并开发了第一个名为 Target Eye 的远程 PC 监控系统。
其他项目包括:数据清理(作为 DataTune 系统的一部分,该系统已在许多组织中实施)。
关注 @haephrati
关注 Twitter、Google、LinkedIn
文章顶部
附件:[SourceCode.zip]