65.9K
CodeProject 正在变化。 阅读更多。
Home

字符串混淆系统

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.86/5 (66投票s)

2012年12月3日

CDDL

5分钟阅读

viewsIcon

197071

downloadIcon

3404

一个集成到 Visual Studio C++ 解决方案中的字符串混淆系统

引言

混淆器的目的通常是隐藏程序代码、流程和功能。如果您的程序使用一项商业秘密算法,混淆会增加逆向工程并揭露该商业秘密的难度。但是,隐藏可执行文件中的数据呢?有人说这几乎是不可能实现的。由于程序需要能够读取这些数据,因此数据必须存在,如果数据存在,则最终可以被揭露。在我看来,一个混淆良好的程序可以使猜测数据的存储位置(加密)变得几乎不可能,即使找到了数据,也极难用强加密来混淆它。

解决方案

本文将介绍的一套工具,其目的就是为应用程序中的字符串加密。

通常,即使不进行逆向工程,应用程序也能透露大量关于自身的信息。当您使用十六进制编辑器(或记事本作为文本编辑器)打开像 Calc.exe 这样的应用程序时,您会发现诸如

例如,如果密码不加密,将带来风险。如果您的软件连接到互联网服务、短信网关或 FTP 服务器并发送密码,那么任何使用文本编辑器打开您的应用程序可执行文件的任何人都可以看到该密码。

背景

我阅读了 Chris Losinger精彩文章,并希望通过创建更强的加密(AES-256)并支持更多变体和字符串类型(包括 UNICODE、双字节和单字节字符串)来将其提升到一个新的水平。

此工具的目的是专业使用,而不仅仅是概念验证。

源代码

加密/解密机制集成到两个单独的项目中,需要将它们添加到您的 Visual Studio 解决方案中。这两个项目位于主文件夹中

  1. obfisider 项目
  2. obfuscase 项目

Obfisider 和 Obfuscate 项目

Obfisider 项目包含 AES 加密部分。Obfuscate 项目包含扫描解决方案及其包含的每个项目并加密找到的字符串所需的组件。

扫描解决方案

解决方案使用 parseSolution 进行扫描,该函数调用另一个名为 parseProject 的函数。

static 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 文件。

/**
 * 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 对字符串进行加密

/* -------------------------------------------------------------------------- */
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 enough 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 函数

此函数处理将字符串解密回来

/* -------------------------------------------------------------------------- */
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__ )进行解码

/* -------------------------------------------------------------------------- */
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__

/* -------------------------------------------------------------------------- */
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' 首先构建,构建完成后,会执行一个 Post Build 事件。Post Build 事件会调用 obfuscate 并将整个解决方案文件作为其参数。

文件扫描

Obfuscate 扫描给定的解决方案并处理其中的每个相关文件。当前版本要求路径中不能包含空格,如果存在空格,调用 "obfuscate" 的正确方式应该是

"$(TargetPath)" "$(SolutionPath)"

"obfuscate" 会扫描解决方案中的所有项目文件,但会处理以下文件类型:.c.cpp.h.hpp

工作流程

对于每个文件,会进行以下检查

// this is a comment   

/*  this a another comment */  
Static char c[]="test string";  
  1. 已混淆的文件将被跳过
  2. "obfuscate" 不会处理它自身,因此它自己的文件会被跳过。
  3. 注释将被跳过。这包括
  4. #include#pragma 声明将被跳过。
  5. 已初始化的全局字符串将被忽略。如果您看下面的例子
  6. 对于所有其他字符串,"obfuscate" 会查找原始声明并将其替换为对解密函数的调用,并将加密字符串作为参数。

为了方便维护,原始行会保留并作为注释行保留在新行之上。

ASCII 与 Unicode

该系统区分 ASCII 和 Unicode 文本。每个类型使用两组不同的函数。

以下语句

wcscpy(mystring, "my text");  

wcscpy(mystring, _T("my text")); 

将被识别为 Unicode 类型并替换为对 __ODC__ 的调用,而类似的 ASCII 语句

strcpy(mystring, "my text"); 

将被识别为此类并替换为对 __ODA__ 的调用。

加密和编码方案

例如:值 0x3 是通过将字符 "A" 和 "D" (0x40+0x3==0x43) 进行移位而得到的。

例如:如果最后一个值是 'z'(代码 0x7A),编码值为 0xF,则新值将编码为字符 '+'(代码 0x2B == 0x7A + 0xF - (0x7E-0x20))

  1. 每个字符串都使用一个临时生成的加密密钥单独加密。
  2. 此密钥是随机生成的,随机数生成器的 SEED 值在应用程序开始时设置。
  3. 所有字符串都用 NULL 字符填充以使其长度达到加密块的整数倍,这是 AES-256 加密方案所必需的。
  4. 结果是二进制形式的,并使用以下算法表示为可打印字符集
    • 每个字节被分成两半。较高的一半先编码,然后是第二半分。例如:0xAF 被分成:0xA 和 0xF。
    • 编码值是前一个值与新值之间的差值(初始值为 "A")。
  5. 当移位值达到 0x7E 时,从该值中减去初始值 "A"。

示例

以下语句

wprintf(L"This is a test\n" );

将被替换为以下行

/*  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";   

当数组大小未设置时,无法加密预定义的内容,因为在编译时无法知道实际大小。

另一种系统无法加密的例子被称为“隐藏合并”

#define PRODUCT_NAME "MyProduct"
#define PRODUCT_FOLDER "MyFolder"
#define WORK_PATH PRODUCT_NAME "/" PRODUCT_FOLDER  

历史

  • 2013 年 2 月 16 日:初始版本

Michael HaephratiCodeProject MVP 2013

© . All rights reserved.