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

TinyObfuscate - 一个微小的 C/C++ 字符串混淆器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (21投票s)

2017年10月12日

CPOL

7分钟阅读

viewsIcon

70235

downloadIcon

2585

TinyObfuscate 是一个简单的工具,当您需要混淆或隐藏程序中的字符串时可以使用它;它可以在您的可执行文件被检查时防止它们被 strings 或 hex 工具发现。

获取商业版本

本文获得二等奖:2017 年 10 月最佳 C++ 文章

引言

混淆器的目的是以一种使其难以理解的方式隐藏程序代码、流程和功能的部分。混淆将使逆向工程更难,并揭示“秘密酱汁”,如果您的程序使用了商业秘密算法。有时,您需要混淆程序中的字符串,并且不想使用昂贵而复杂的混淆工具(市面上有不少)。下面的源代码级字符串混淆器在这种情况下可能很有用。

背景

如果您使用任何十六进制编辑器、Strings,甚至记事本 :) 来查看典型的可执行文件,您可能会在二进制数据中找到许多字符串,这些字符串揭示了商业秘密、IP 地址或其他信息,所有这些都以字符串的形式呈现;您不想泄露。

TinyObfuscate 的目的是隐藏这些字符串。

在我们开始之前,请记住混淆不是加密的一种形式。请记住,任何锁都可以被打破,因为在某个时候,任何加密的东西都必须在运行时解密才能使用。如果您在运行时加密字符串然后解密它们,您将获得更强的安全性。这就是混淆的优势所在。当我们进行混淆时,我们不加密;我们是**“隐藏在视线之中”**。混淆是在草堆里藏针。通过混淆,找到“针”可能比解密一个容易找到的加密字符串需要更长的时间和更多的资源。

安全总是需要结合使用多种方法;如果一种失败(或被破解),其他方法仍将保持有效保护。混淆应在所有其他功能实现后再进行。在添加了加密层并彻底调试了程序后,就可以对其进行混淆了(请注意,混淆后的源代码难以维护和更新,因此建议维护未混淆的版本,并在部署新版本之前对其进行混淆)。

TinyObfuscate 工具的目的是进行混淆,而不是加密。混淆的优点是没有任何东西被加密,因此无需解密。数据保持原样,但变得晦涩难懂。
请注意,本文介绍的工具是一个有限的、基础的版本,仅用于学习目的。混淆系统售价高达 10,000 美元以上,而我的工具旨在让您对混淆有一个小小的了解。此外,本文仅描述了混淆的一个方面,即字符串混淆。在商业产品中进行混淆时,它包括混淆函数、API 调用等。

使用工具

我过去曾写过关于字符串混淆的文章,但本文的独特性在于一种简单的混淆源代码中字符串的方法。无需运行任何工具或扫描项目。相反,复制粘贴您的敏感字符串(例如,“my secret string”),并命名您打算使用的变量(默认情况下,为了支持 UNICODE,将是 wchar_t)。您将获得一段初始化源代码供使用。

然后,而不是使用这段代码

wchar_t m_Variable[] = L"my secret string"

您运行这个工具...

输入 `string` 和变量名,然后复制结果

// (15.08.2020 19:15:21) Obfuscated: 'My secret string'
wchar_t* s_2411641058()
{
  wchar_t* _2411641058 = new wchar_t[32];
  _2411641058[0x9] = L'g' - 0x47;
  _2411641058[0x3] = L'4' + 077;
  _2411641058[0x1e] = L'h' - 04;
  _2411641058[0x4] = 0101 + 0x24;
  _2411641058[0x1] = L'h' + 021;
  _2411641058[0xc] = L's' - 0x1;
  _2411641058[0x16] = 119;
  _2411641058[0xe] = L'8' + 066;
  _2411641058[0x12] = 0x7a - 023;
  _2411641058[0x10] = 0;
  _2411641058[0x11] = 0x7e - 011;
  _2411641058[0x1b] = L'G' + 033;
  _2411641058[0xb] = 0150 + 0xc;
  _2411641058[0x13] = L'6' + 067;
  _2411641058[0x6] = 114;
  _2411641058[0x1c] = L')' + 0110;
  _2411641058[0x7] = 0106 + 0x1f;
  _2411641058[0x19] = L'g' - 05;
  _2411641058[0xd] = 105;
  _2411641058[0x17] = 0116 + 0x16;
  _2411641058[0x8] = 116;
  _2411641058[0x1f] = L'x' - 0x7;
  _2411641058[0x1a] = 107;
  _2411641058[0x1d] = 076 + 0x2d;
  _2411641058[0x18] = 0141 + 0x1; 
  _2411641058[0x5] = 062 + 0x31;
  _2411641058[0x14] = 0137 + 0x1b;
  _2411641058[0x15] = 057 + 0x41;
  _2411641058[0xa] = L'w' - 04;
  _2411641058[0x0] = 77;
  _2411641058[0xf] = 0133 + 0xc;
  _2411641058[0x2] = 0x3b - 033;
  _2411641058[0x1f] = '\0';
  return _2411641058;
}

您可以通过构建一个可执行文件并搜索字符串“My secret string”(最好使用 Strings 配合 ‘Findstr’ 选项)来测试每个选项。当使用混淆版本时,将找不到该字符串。假设您的软件连接到一个远程服务器,您存储了正在使用的 IP 地址,并且不希望它被泄露。这样,您就可以掩盖和隐藏敏感数据。数据只会从可执行文件中隐藏。但是,一旦您与远程服务器通信,嗅探工具就会显示 IP 地址以及发送或接收的任何内容。

有一种方法可以隐藏来自嗅探工具(例如 Wireshark)的 IP 和数据。我们多年前为一家大型政府机构开发了一个这样的 POC

尽管我们可以隐藏我们的程序与服务器之间的任何通信,包括服务器的 IP 地址,但我们仍然需要开发一个端到端加密的通信协议,并混淆程序文件本身中的 IP 地址(以及其他敏感数据)。

大型公司在任何敏感软件中都使用混淆。例如,Microsoft Windows 的 Patch Guard 是完全混淆的,这使得逆向工程更加困难。用于混淆 Windows 敏感组件的方法远远超出了仅混淆字符串,还包括 混淆函数名、变量等。

提供的示例

我创建了一个名为 CodeProjectTest.exe 的小型控制台应用程序,它有一行代码

wchar_t m_Variable[] = L"my secret string;

然后我使用 `TinyObfuscate` 对其进行了混淆,并构建了一个名为 obf_CodeProjectTest.exe 的程序。

它们都可以 在这里 下载(它们都使用 EV 代码签名证书进行了代码签名)。

我使用 ‘findstr’ 选项的 Strings 检查了两者。

strings CodeProjectTest.exe | findstr /i "secret"

strings obf_CodeProjectTest.exe | findstr /i "secret"

结果显示在以下屏幕截图中

  1. 混淆之前,找到了字符串“my secret string”。

  2. 混淆之后,“my secret string”字符串未被找到。

源代码 - 构成模块

随机字符和数字

首先要说明的是,不推荐使用 `rand()`。有关原因,请参阅本文。相反,我使用了 Arvid Gerstmann这篇文章中的代码。

使用这段代码,我创建了一个简单的函数,该函数返回一个给定范围内的随机数。

int RandomIntFromRange(int From, int To)
{
    int result;
    std::random_device rd;
    pcg rand(rd);
    std::uniform_int_distribution<> u(From, To);
    result = u(rand);
    return result;
}

我们需要能够生成随机字符和随机数字。我已经创建了以下宏

#define RANDOM_DIGIT (RandomIntFromRange(1,9))
#define RANDOM_WCHAR (WCHAR)(RandomIntFromRange(L'a',L'z'))
#define RANDOM_INT_LARGER_THAN(n) (int)(RandomIntFromRange(n,122))
#define RANDOM_INT_SMALLER_THAN(n) (int)((n>48)?RandomIntFromRange(48, n):n)

处理转义字符

当您输入一个包含转义字符的 `string` 时,您需要以不同的方式处理它们,否则它们将无法正确编码。以下函数将替换转义字符,例如 '\n'(将被表示为“\\n”)为正确的值,即 0x0a

CString ProcessEscapeString(CString p_szOriginalStr)
{
    CString w_szProcessStr;
    wchar_t w_pESC_char[] = { L'\a', L'\b', L'\f', L'\n', 
                              L'\r', L'\t', L'\v', L'\\', L'\0'};
    wchar_t w_pESC_str[] = { L'a', L'b', L'f', L'n', 
                             L'r', L't', L'v', L'\\', L'0'};
    int i, j;
    int w_nLength = p_szOriginalStr.GetLength();

    // parse escape characters
    for (i = 0; i < w_nLength; i++)
    {
        if (p_szOriginalStr.GetAt(i) == L'\\')
        {
            for (j = 0; j < 9; j++)
            {
                if (p_szOriginalStr.GetAt(i + 1) == w_pESC_str[j])
                {
                    w_szProcessStr += w_pESC_char[j];
                    i++;
                    break;
                }
            }
            if (j >= 9)
            {
                w_szProcessStr += p_szOriginalStr.GetAt(i);
            }
        }
        else
        {
            w_szProcessStr += p_szOriginalStr.GetAt(i);
        }
    }
    
    return w_szProcessStr;
}

打乱元素

当我们把 **string** 转换成一个数组时,我们想打乱它,使其顺序(几乎)随机,从而增加分析难度。解码混淆数据的方法之一是检查预期的逻辑顺序。打乱顺序使猜测混淆数据变得更加困难。

void shuffle(int array[], const int size)
{
    const int n_size = size;
    int temp[1028];
    std::vector<int> indices;

    for (int i(0); i < size; ++i)
        temp[i] = array[i];

    int index = rand() % size;
    indices.push_back(index);

    for (int i = 0; i < size; ++i)
    {
        if (i == 0)
            array[i] = temp[index];
        else
        {
            while (find(indices, index))
                index = rand() % size;

            indices.push_back(index);
            array[i] = temp[index];
        }
    }
}

添加垃圾数据

隐藏内容的另一种方法是在实际数据之间添加随机的垃圾数据。由于结果是一个以 `NULL` 结尾的数组,这很容易做到。您将 `NULL` 放在 `string` 的末尾,然后将垃圾数据放在 `NULL` 之后,但由于我们稍后会将每个值转换为一个公式(例如,不是“72”,而是“100 - 28”),因此这种方法对我们的目的来说已经足够好了。

    TextWithJunk += (CString)L" ";
    for (i = Length + 1; i < Length * 2; i++)
    {
        WCHAR result = RANDOM_WCHAR;
        TextWithJunk += (CString)(result);
    }

用公式替换值

然后我们随机地将值替换为不同类型的公式,例如 `x=z-y` 或 `z=y+z` 等。

因此,当公式为 `x=z-y` 时,我们需要 z 是随机的但大于 y。这就是为什么我们使用 `RANDOM_INT_LARGER()`。

        switch (choice)
        {
            case 10:
            case 1:
                // x = z - y
                // Calculate Z
                z = RANDOM_INT_LARGER_THAN(x);
                // Calculate the difference
                d = z-x;
                Formula.Format(L"%d - %d",z,d);
                break;
            case 2:
            case 3:
                // x = z + y
                // Calculate Z
                z = RANDOM_INT_SMALLER_THAN(x);
                // Calculate the difference
                d = x - z;
                Formula.Format(L"%d + %d", z, d);
                break;
            case 4:
            case 5:
                // x = 'z' - y
                // Calculate Z
                z = RANDOM_INT_LARGER_THAN(x);
                // Calculate the difference
                d = z - x;
                Formula.Format(L"L'%c' - %d", z, d);
                break;
            case 6:
            case 7:
                // x = 'z' + y
                // Calculate Z
                z = RANDOM_INT_SMALLER_THAN(x);
                // Calculate the difference
                d = x - z;
                Formula.Format(L"L'%c' + %d", z, d);
                break;
            case 8:
            case 9:
                // x = 'z'
                Formula.Format(L"%d",x);
                break;
        }

Tiny Obfuscate - 高级版本

自本文初次发布以来,我们不断改进项目,TinyObfuscate 的最新版本已用于我们日常开发中,包括在多个商业产品中。

最新版本有两种模式

  • 项目模式
  • 即时模式

即时模式类似于本文提到的原始版本,但增加了额外功能和改进。

  • 您可以选择字符串类型(UNICODE 或宽字符,const)。
  • 混淆后的代码被包装在一个新生成函数中。
  • 可选:函数代码和原型被插入到给定的 .cpp 和 .h 文件中,前提是先检查是否已经存在一个混淆给定字符串的函数。
  • 函数调用被复制到剪贴板(无论是新生成的函数还是之前混淆过的给定字符串的现有函数),以便用户可以直接粘贴以替换给定的字符串。
  • 生成的函数会自动进行测试,以验证它是否将返回给定的字符串。
  • 各种控制和转义字符都得到了处理。这些包括:\n, \t 等。%s, %d 等。
  • 自动添加注释,以跟踪原始混淆字符串以及何时进行了混淆。

混淆示例

这是一个混淆给定字符串和包含格式字符(“%d”)的字符串的示例。

此示例中使用的代码行是

wprintf(L"The result is %d", result);

我们必须混淆实际字符串,以便在对 wprintf 的调用中用生成的函数替换它。该字符串被放在“要混淆的字符串”字段中,用户按下 ENTER 键。

结果是,发生以下情况

1. 出现一个“气球提示”

2. 以下代码将出现(并插入到项目的源文件和头文件中)。

现在,您可以将该函数粘贴到源代码行中,该行将如下所示

wprintf(s_1111865989(),result);

关注点

该项目使用 Visual Studio 2019 Ultimate 和 MFC 创建。

附带的可执行文件已使用我们的扩展验证 (EV) 代码签名证书进行了代码签名。

历史

  • 2017 年 10 月 12 日:初始版本
© . All rights reserved.