【问题标题】:Techniques for obscuring sensitive strings in C++在 C++ 中隐藏敏感字符串的技术
【发布时间】:2009-10-30 08:31:17
【问题描述】:

我需要在我的 C++ 应用程序中存储敏感信息(我想保密的对称加密密钥)。简单的方法是这样做:

std::string myKey = "mysupersupersecretpasswordthatyouwillneverguess";

但是,通过strings 进程(或从二进制应用程序中提取字符串的任何其他进程)运行应用程序将显示上述字符串。

应该使用什么技术来隐藏这些敏感数据?

编辑:

好的,几乎所有人都说过“你的可执行文件可以被逆向工程” - 当然!这是我的一个小烦恼,所以我要在这里咆哮一下:

为什么这个网站上所有与安全相关的问题中有 99%(好吧,也许我夸大了一点)都以“不可能创建一个完全安全的程序”的洪流来回答——也就是说没有帮助的答案!安全性是一个介于完美可用性和没有安全性,以及完美安全性但另一方面没有可用性之间的滑动标尺。

关键是您可以根据您要执行的操作以及您的软件运行的环境来选择您在该滑动刻度上的位置。 我不是为军事设施编写应用程序,而是为家用 PC 编写应用程序。我需要使用预先知道的加密密钥在不受信任的网络上加密数据。在这些情况下,“通过默默无闻的安全”可能就足够了!当然,有足够时间、精力和技能的人可以对二进制文件进行逆向工程并找到密码,但你猜怎么着?我不在乎:

我实施一流的安全系统所花费的时间比由于破解版本而导致的销售损失更昂贵(并不是说我实际上是在卖这个,但你明白我的意思)。至少可以说,在新程序员中,这种“让我们以绝对最好的方式来做”的趋势是愚蠢的。

感谢您抽出宝贵时间回答这个问题 - 他们非常有帮助。不幸的是,我只能接受一个答案,但我对所有有用的答案都投了赞成票。

【问题讨论】:

  • 也许如果您描述了您尝试使用加密密钥完成的任务,我们可以就如何避免这样做提供建议。
  • @Kirill:很难将这个问题称为与您提到的问题完全相同的问题。的确,想法是一样的。问题不是。
  • @xtofl,你可以不投票。在我看来,这似乎是两个相同的问题。
  • (与咆哮无关)“私钥”的定义是您不提供的公钥/私钥对的一半。在这种情况下,私钥也是您保存在服务器上的那个;您的客户端应用程序中的那个是公开的。

标签: c++ security obfuscation defensive-programming


【解决方案1】:

基本上,任何有权访问您的程序和调试器的人可以并且在他们想要的情况下在应用程序中找到密钥。

但是,如果您只是想确保在二进制文件上运行 strings 时不会显示密钥,例如,您可以确保密钥不在可打印范围内。

用 XOR 遮挡键

例如,您可以使用 XOR 将键拆分为两个字节数组:

key = key1 XOR key2

如果您创建与key 具有相同字节长度的key1,您可以使用(完全)随机字节值,然后计算key2

key1[n] = crypto_grade_random_number(0..255)
key2[n] = key[n] XOR key1[n]

您可以在您的构建环境中执行此操作,然后只在您的应用程序中存储key1key2

保护您的二进制文件

另一种方法是使用工具来保护您的二进制文件。例如,有几种安全工具可以确保您的二进制文件被混淆并启动它运行的虚拟机。这使得调试变得更加困难,并且也是保护许多商业级安全应用程序(唉,恶意软件)的常规方式。

Themida 是主要工具之一,它在保护您的二进制文件方面做得非常出色。众所周知的程序(例如 Spotify)经常使用它来防止逆向工程。它具有防止在 OllyDbg 和 Ida Pro 等程序中调试的功能。

还有一个更大的列表,可能有些过时,tools to protect your binary
其中一些是免费的。

密码匹配

这里有人讨论过哈希密码+盐。

如果您需要存储密钥以将其与某种用户提交的密码进行匹配,您应该使用单向散列函数,最好将用户名、密码和盐结合起来。但是,这样做的问题是您的应用程序必须知道盐才能进行单向操作并比较生成的哈希值。因此,您仍然需要将盐存储在应用程序的某个位置。但是,正如@Edward 在下面的 cmets 中指出的那样,这将有效地防止使用例如彩虹表的字典攻击。

最后,您可以结合使用上述所有技术。

【讨论】:

  • 如果是用户必须输入的密码,存储密码的hash+salt。请参阅下面的答案。
  • @hapalibashi:如何将盐安全地存储在应用程序中?我认为 OP 不需要单向密码匹配系统,只是一种存储静态密钥的通用方式。
  • 我在查看反汇编程序时发现通常没有很多 XOR,因此如果您希望使用 XOR 来掩盖某些东西,请记住它们会引起人们的注意。跨度>
  • @kb - 这是一个有趣的观点。我猜你会看到按位与和或发生的比异或要多得多。 a ^ b == (a & ~b) || (~a & b)
  • 知道盐值通常不会给对手带来优势——盐的目的是避免“字典攻击”,即攻击者已经预先计算了许多可能输入的哈希值。使用盐会迫使他们使用基于盐的新字典预先计算字典。如果每种盐只使用一次,那么字典攻击就完全没用了。
【解决方案2】:

首先,要意识到你无法阻止一个足够坚定的黑客,而且周围有很多这样的人。周围所有游戏和主机的保护最终都会被破解,所以这只是一个临时修复。

你可以做 4 件事来增加你隐藏一段时间的机会。

1) 以某种方式隐藏字符串的元素——很明显,像 xoring(^ 运算符)字符串与另一个字符串将足以使字符串无法搜索。

2) 将字符串拆分成小块——拆分您的字符串并将其中的一部分弹出到奇怪模块中的奇怪命名方法中。不要轻易搜索并找到其中包含字符串的方法。当然,有些方法必须调用所有这些位,但这仍然会使它有点困难。

3) 永远不要在内存中构建字符串——大多数黑客使用的工具可以让他们在对字符串进行编码后查看内存中的字符串。如果可能,请避免这种情况。例如,如果您将密钥发送到服务器,则逐个字符地发送它,因此整个字符串永远不会出现。当然,如果您是通过 RSA 编码之类的方式使用它,那么这就比较棘手了。

4) 做一个 ad-hoc 算法——在所有这些之上,添加一个或两个独特的扭曲。也许只需将 1 添加到您生产的所有内容中,或者进行两次加密,或者添加糖。这只会让已经知道在有人使用时寻找什么的黑客更加困难,例如,普通 md5 散列或 RSA 加密。

最重要的是,确保您的密钥何时被发现(如果您的应用程序变得足够流行,那将是)您的密钥不是太重要!

【讨论】:

    【解决方案3】:

    我过去使用的一个策略是创建一个看似随机的字符数组。您最初插入,然后使用代数过程定位您的特定字符,其中从 0 到 N 的每一步都将产生一个数字

    例子:

    给定一个字符数组(数字和破折号仅供参考)

    0123456789
    ----------
    ALFHNFELKD
    LKFKFLEHGT
    FLKRKLFRFK
    FJFJJFJ!JL
    

    还有一个方程,其前六个结果是:3、6、7、10、21、47

    会产生单词“HELLO!”从上面的数组。

    【讨论】:

    • 好主意 - 我想你可以通过在数组中使用非打印字符来进一步改进它......
    【解决方案4】:

    adamyaxley 制作了一个(非常轻量级的)纯标题项目obfuscate,效果很好。它基于 lambda 函数和宏,并在编译时使用 XOR 密码对字符串进行加密。如果需要,我们可以更改每个字符串的种子。

    以下代码不会在编译后的二进制文件中存储字符串“hello world”。

    #include "obfuscate.h"
    
    int main()
    {
      std::cout << AY_OBFUSCATE("Hello World") << std::endl;
      return 0;
    }
    

    我已使用 c++17 和 Visual Studio 2019 进行了测试,并通过 IDA 检查并确认字符串已隐藏。与ADVobfuscator 相比,一个宝贵的优势是它可以转换为 std::string(同时仍隐藏在编译后的二进制文件中):

    std::string var = AY_OBFUSCATE("string");
    

    【讨论】:

    • 在 ARM 上可以通过简单地修补 MOV R0、1 + BX LR 轻松解决混淆
    • @SouravBanerjee 这很有趣,你从哪里可以得到这些信息?我正在尝试查找混淆的库的评论,这很方便但使用不是那么广泛。
    • @Antonio Obfuscate 基本上是单字节异或,很容易被打败。使用带有符号的混淆构建任何二进制文件,您可以轻松找到在执行去混淆操作之前加载 XOR 密钥的例程。如果您使用单字节 XOR 来混淆私钥或 api 密钥等重要内容,请避免使用单字节异或。
    • @SouravBanerjee 感谢您的洞察力!这里提出的解决方案更好吗? stackoverflow.com/a/32287802/2436175
    • @AdamYaxley 可以理解,但是,我只是要求 OP 使用符号进行构建,以便他们可以将反汇编与剥离的二进制文件相关联,以更快地找到反混淆例程。我喜欢混淆,并成功地将它用于 API 端点等简单字符串,但是,对于 API 密钥和私钥等真正敏感的数据,不推荐这种机制。
    【解决方案5】:

    我同意@Checkers,您的可执行文件可以进行逆向工程。

    更好的方法是动态创建,例如:

    std::string myKey = part1() + part2() + ... + partN();
    

    【讨论】:

    • True,这样可以避免在搜索二进制文件时显示字符串。但是,您的字符串仍然驻留在内存中。不过,对于我正在做的事情,您的解决方案可能已经足够好了。
    • @Thomi,您当然可以在完成后立即销毁它。但是,它仍然不是处理敏感字符串的最佳方式。
    • ...因为销毁它实际上并不能保证内存会被立即重新使用。
    【解决方案6】:

    当然,将私人数据存储在交付给用户的软件中始终存在风险。任何受过充分教育(和敬业)的工程师都可以对数据进行逆向工程。

    话虽如此,您通常可以通过提高人们在披露您的私人数据时需要克服的障碍来使事情变得足够安全。这通常是一个很好的妥协。

    在您的情况下,您可以用不可打印的数据使字符串混乱,然后在运行时使用简单的辅助函数对其进行解码,如下所示:

    void unscramble( char *s )
    {
        for ( char *str = s + 1; *str != 0; str += 2 ) {
            *s++ = *str;
        }
        *s = '\0';
    }
    
    void f()
    {
        char privateStr[] = "\001H\002e\003l\004l\005o";
        unscramble( privateStr ); // privateStr is 'Hello' now.
    
        string s = privateStr;
        // ...
    }
    

    【讨论】:

      【解决方案7】:

      我为字符串创建了一个简单的加密工具,它可以自动生成加密字符串,并且有一些额外的选项可以做到这一点,几个例子:

      字符串作为全局变量:

      // myKey = "mysupersupersecretpasswordthatyouwillneverguess";
      unsigned char myKey[48] = { 0xCF, 0x34, 0xF8, 0x5F, 0x5C, 0x3D, 0x22, 0x13, 0xB4, 0xF3, 0x63, 0x7E, 0x6B, 0x34, 0x01, 0xB7, 0xDB, 0x89, 0x9A, 0xB5, 0x1B, 0x22, 0xD4, 0x29, 0xE6, 0x7C, 0x43, 0x0B, 0x27, 0x00, 0x91, 0x5F, 0x14, 0x39, 0xED, 0x74, 0x7D, 0x4B, 0x22, 0x04, 0x48, 0x49, 0xF1, 0x88, 0xBE, 0x29, 0x1F, 0x27 };
      
      myKey[30] -= 0x18;
      myKey[39] -= 0x8E;
      myKey[3] += 0x16;
      myKey[1] += 0x45;
      myKey[0] ^= 0xA2;
      myKey[24] += 0x8C;
      myKey[44] ^= 0xDB;
      myKey[15] ^= 0xC5;
      myKey[7] += 0x60;
      myKey[27] ^= 0x63;
      myKey[37] += 0x23;
      myKey[2] ^= 0x8B;
      myKey[25] ^= 0x18;
      myKey[12] ^= 0x18;
      myKey[14] ^= 0x62;
      myKey[11] ^= 0x0C;
      myKey[13] += 0x31;
      myKey[6] -= 0xB0;
      myKey[22] ^= 0xA3;
      myKey[43] += 0xED;
      myKey[29] -= 0x8C;
      myKey[38] ^= 0x47;
      myKey[19] -= 0x54;
      myKey[33] -= 0xC2;
      myKey[40] += 0x1D;
      myKey[20] -= 0xA8;
      myKey[34] ^= 0x84;
      myKey[8] += 0xC1;
      myKey[28] -= 0xC6;
      myKey[18] -= 0x2A;
      myKey[17] -= 0x15;
      myKey[4] ^= 0x2C;
      myKey[9] -= 0x83;
      myKey[26] += 0x31;
      myKey[10] ^= 0x06;
      myKey[16] += 0x8A;
      myKey[42] += 0x76;
      myKey[5] ^= 0x58;
      myKey[23] ^= 0x46;
      myKey[32] += 0x61;
      myKey[41] ^= 0x3B;
      myKey[31] ^= 0x30;
      myKey[46] ^= 0x6C;
      myKey[35] -= 0x08;
      myKey[36] ^= 0x11;
      myKey[45] -= 0xB6;
      myKey[21] += 0x51;
      myKey[47] += 0xD9;
      

      作为带解密循环的 unicode 字符串:

      // myKey = "mysupersupersecretpasswordthatyouwillneverguess";
      wchar_t myKey[48];
      
      myKey[21] = 0x00A6;
      myKey[10] = 0x00B0;
      myKey[29] = 0x00A1;
      myKey[22] = 0x00A2;
      myKey[19] = 0x00B4;
      myKey[33] = 0x00A2;
      myKey[0] = 0x00B8;
      myKey[32] = 0x00A0;
      myKey[16] = 0x00B0;
      myKey[40] = 0x00B0;
      myKey[4] = 0x00A5;
      myKey[26] = 0x00A1;
      myKey[18] = 0x00A5;
      myKey[17] = 0x00A1;
      myKey[8] = 0x00A0;
      myKey[36] = 0x00B9;
      myKey[34] = 0x00BC;
      myKey[44] = 0x00B0;
      myKey[30] = 0x00AC;
      myKey[23] = 0x00BA;
      myKey[35] = 0x00B9;
      myKey[25] = 0x00B1;
      myKey[6] = 0x00A7;
      myKey[27] = 0x00BD;
      myKey[45] = 0x00A6;
      myKey[3] = 0x00A0;
      myKey[28] = 0x00B4;
      myKey[14] = 0x00B6;
      myKey[7] = 0x00A6;
      myKey[11] = 0x00A7;
      myKey[13] = 0x00B0;
      myKey[39] = 0x00A3;
      myKey[9] = 0x00A5;
      myKey[2] = 0x00A6;
      myKey[24] = 0x00A7;
      myKey[46] = 0x00A6;
      myKey[43] = 0x00A0;
      myKey[37] = 0x00BB;
      myKey[41] = 0x00A7;
      myKey[15] = 0x00A7;
      myKey[31] = 0x00BA;
      myKey[1] = 0x00AC;
      myKey[47] = 0x00D5;
      myKey[20] = 0x00A6;
      myKey[5] = 0x00B0;
      myKey[38] = 0x00B0;
      myKey[42] = 0x00B2;
      myKey[12] = 0x00A6;
      
      for (unsigned int fngdouk = 0; fngdouk < 48; fngdouk++) myKey[fngdouk] ^= 0x00D5;
      

      字符串作为全局变量:

      // myKey = "mysupersupersecretpasswordthatyouwillneverguess";
      unsigned char myKey[48] = { 0xAF, 0xBB, 0xB5, 0xB7, 0xB2, 0xA7, 0xB4, 0xB5, 0xB7, 0xB2, 0xA7, 0xB4, 0xB5, 0xA7, 0xA5, 0xB4, 0xA7, 0xB6, 0xB2, 0xA3, 0xB5, 0xB5, 0xB9, 0xB1, 0xB4, 0xA6, 0xB6, 0xAA, 0xA3, 0xB6, 0xBB, 0xB1, 0xB7, 0xB9, 0xAB, 0xAE, 0xAE, 0xB0, 0xA7, 0xB8, 0xA7, 0xB4, 0xA9, 0xB7, 0xA7, 0xB5, 0xB5, 0x42 };
      
      for (unsigned int dzxykdo = 0; dzxykdo < 48; dzxykdo++) myKey[dzxykdo] -= 0x42;
      

      【讨论】:

      【解决方案8】:

      正如 joshperry 指出的那样,在某种程度上取决于您要保护的内容。 根据经验,我会说,如果它是保护您的软件的某些许可计划的一部分,那么请不要打扰。他们最终会对它进行逆向工程。只需使用像 ROT-13 这样的简单密码来保护它免受简单攻击(在其上运行字符串)。 如果是为了保护用户的敏感数据,我会质疑使用本地存储的私钥保护这些数据是否是明智之举。再次归结为您要保护的内容。

      编辑:如果你打算这样做,那么 Chris 指出的技术组合将比 rot13 好得多。

      【讨论】:

        【解决方案9】:

        如前所述,没有办法完全保护您的字符串。但是有一些方法可以以合理的安全性保护它。

        当我不得不这样做时,我确实在代码中放入了一些看似无害的字符串(例如,版权声明,或一些伪造的用户提示或其他任何不会被修复无关代码的人更改的内容),加密将自己用作密钥,对其进行哈希处理(添加一些盐),并将结果用作密钥来加密我真正想要加密的内容。

        当然这可能会被黑客入侵,但确实需要有决心的黑客才能这样做。

        【讨论】:

        • 好主意 - 另一种形式的模糊性是使用仍然相当强的字符串(长,标点符号和所有爵士乐),但看起来不像密码。
        • 我在想,一种攻击方法是修改(以编程方式)在可执行文件中找到的所有字符串,并通过修改解密失败的字符串来查看。使用分而治之的算法,可以很快找出哪个是根字符串。
        • @Antonio 正如投票率最高的答案中的第一句话所说:“基本上,任何有权访问您的程序和调试器的人都可以并且会在应用程序中找到密钥,如果他们愿意的话。”我们在这里谈论威慑的阴影。
        【解决方案10】:

        如果您使用的是 Windows 用户 DPAPI,http://msdn.microsoft.com/en-us/library/ms995355.aspx

        正如之前的帖子所说,如果您使用的是 Mac,请使用钥匙串。

        基本上,从安全角度来看,所有这些关于如何将私钥存储在二进制文件中的可爱想法都非常糟糕,你不应该这样做。任何人获得您的私钥都是一件大事,不要将其保存在您的程序中。根据应用程序的导入方式,您可以将私钥保存在智能卡上、代码与之通信的远程计算机上,或者您可以做大多数人所做的事情并将其保存在本地计算机上非常安全的地方(“密钥store”,有点像一个奇怪的安全注册表),受权限和操作系统的所有强度保护。

        这是一个已解决的问题,答案是不要将密钥保留在程序中 :)

        【讨论】:

          【解决方案11】:

          试试this。源代码解释了如何动态加密和解密给定 Visual Studio c++ 项目中的所有字符串。

          【讨论】:

            【解决方案12】:

            我最近尝试的一种方法是:

            1. 获取私有数据的哈希 (SHA256) 并将其填充到代码中为 part1
            2. 对私有数据及其哈希值进行异或运算并将其填充为part2
            3. 填充数据:不要将其存储为 char str[],而是使用赋值指令在运行时填充(如下面的宏所示)
            4. 现在,通过对part1part2 进行异或运算,生成运行时的私有数据
            5. 附加步骤:计算生成数据的哈希值并与part1进行比较。它将验证私有数据的完整性。

            宏来填充数据:

            假设,私有数据是 4 个字节。我们为它定义了一个宏,它以某种随机顺序保存带有赋值指令的数据。

            #define POPULATE_DATA(str, i0, i1, i2, i3)\
            {\
                char *p = str;\
                p[3] = i3;\
                p[2] = i2;\
                p[0] = i0;\
                p[1] = i1;\
            }
            

            现在在需要保存part1part2的代码中使用这个宏,如下:

            char part1[4] = {0};
            char part2[4] = {0};
            POPULATE_DATA(part1, 1, 2, 3, 4); 
            POPULATE_DATA(part2, 5, 6, 7, 8);
            

            【讨论】:

              【解决方案13】:

              您可能希望向用户请求并通过外部password manager 存储它,而不是将私钥存储在您的可执行文件中,类似于 Mac OS X 钥匙串访问。

              【讨论】:

              • 好吧,是的,不是..通常我会同意你的看法,但在这种情况下,我试图对软件用户隐藏这一点,因此将其存储在外部系统中不是一个好主意(许多钥匙串系统可以将密码作为纯文本公开给获得适当授权的用户)。钥匙串软件非常适合用户密码,但不适用于应用程序加密密钥。
              • 如果感觉不安全(尽管考虑到您的说明可能就足够了),您可以将钥匙串和硬编码的东西结合起来:)
              【解决方案14】:

              依赖于上下文,但您可以只存储键的 hash 加上一个 salt(常量字符串,容易混淆)。

              然后当(如果)用户输入密钥时,您添加 salt,计算 hash 并进行比较。

              salt 在这种情况下可能是不必要的,如果哈希可以被隔离,它会阻止暴力字典攻击(谷歌搜索也知道可以工作)。

              黑客仍然只需要在某处插入一条 jmp 指令即可绕过整个过程,但这比简单的文本搜索要复杂得多。

              【讨论】:

              • 这是一个加密密钥,而不是密码哈希。我需要实际的密钥来编码和解码数据。用户永远不会看到密钥,它不会存储在二进制文件之外。
              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 2021-10-31
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2019-08-22
              • 2020-07-15
              • 2019-12-10
              相关资源
              最近更新 更多