代码之家  ›  专栏  ›  技术社区  ›  Thomi

C++中隐藏敏感字符串的技巧

  •  81
  • Thomi  · 技术社区  · 15 年前

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

    std::string myKey = "mysupersupersecretpasswordthatyouwillneverguess";

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

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

    编辑:

    好吧,你们几乎都说过 “可执行文件可以进行反向工程” -当然!这是我的一个宠物皮,所以我要在这里大喊大叫:

    为什么这个网站上所有与安全相关的问题中,99%(好吧,也许我有点夸张)的答案都是“没有办法创建一个完全安全的程序”——这不是一个有用的答案!安全性是在完美的可用性和一端没有安全性,以及完美的安全性和另一端没有可用性之间的一个滑动尺度。

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

    我实施一流的安全系统所花的时间要比由于版本破裂而导致的销售损失更为昂贵(不是说我真的在卖这个,但你明白我的意思)。在新程序员中,这种“让我们尽可能做到最好”的编程趋势至少是愚蠢的。

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

    13 回复  |  直到 7 年前
        1
  •  40
  •   csl    11 年前

    基本上,任何有权访问您的程序和调试器的人 可以 如果他们愿意,可以在应用程序中找到密钥。

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

    用XOR隐藏键

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

    key = key1 XOR key2
    

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

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

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

    保护二进制文件

    另一种方法是使用工具来保护二进制文件。例如,有几个安全工具可以确保您的二进制文件是模糊的,并启动运行它的虚拟机。这使得调试非常困难,同时也是许多商业级安全应用程序(也就是恶意软件)受到保护的方便方式。

    首要工具之一是 Themida 在保护二进制文件方面做得非常好。它经常被著名的程序使用,例如spotify,以防止逆向工程。它具有防止在Ollydbg和IDA Pro等程序中进行调试的功能。

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

    密码匹配

    这里有人讨论了哈希密码+salt。

    如果需要存储密钥以将其与某种用户提交的密码相匹配,则应使用单向散列函数,最好是将用户名、密码和salt组合在一起。不过,问题在于应用程序必须知道salt才能进行单向操作并比较得到的哈希值。因此,您仍然需要将盐存储在应用程序的某个地方。但是,正如@edward在下面的评论中指出的那样,这将有效地防止使用彩虹表等字典攻击。

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

        2
  •  7
  •   Chris Jefferson    15 年前

    首先,要意识到,没有什么可以阻止一个足够坚定的黑客,而且周围有很多这样的黑客。每一个游戏和控制台的保护最终都被破解了,所以这只是暂时的修复。

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

    1)以某种方式隐藏字符串的元素——像xoring(操作符^这样明显的东西——使用另一个字符串的字符串足够好,使字符串无法搜索。

    2)将字符串拆分为多个部分——将字符串拆分,并将其中的一些位弹出到奇怪模块中的同名方法中。不要使搜索和查找包含字符串的方法变得容易。当然,有些方法必须调用所有这些位,但它仍然使调用变得更困难。

    3)永远不要在内存中构建字符串——大多数黑客使用的工具可以让他们在编码后看到内存中的字符串。如果可能,请避免这样做。例如,如果要将密钥发送到服务器,请逐个发送,这样整个字符串就永远不会出现。当然,如果您使用的是类似于RSA编码的代码,那么这就更加棘手了。

    4)做一个特别的算法——最重要的是,添加一两个独特的扭曲。可能只需在你生产的每件产品中添加1,或者进行两次加密,或者添加一个糖。这只会让那些已经知道当有人在使用什么的黑客更加困难,例如普通的MD5哈希或RSA加密。

    最重要的是,确保在发现您的密钥的时候(如果您的应用程序足够流行的话,这一点也不会太重要)!

        3
  •  5
  •   Paul Sasik    15 年前

    我过去使用的一种策略是创建一个看似随机的字符数组。首先插入,然后使用代数过程定位特定字符,从0到n的每一步都将生成数组的数字<大小,该数组包含模糊字符串中的下一个字符。(这个答案现在感觉模糊了!)

    例子:

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

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

    前六个结果是:3,6,7,10,21,47的方程

    会说出“你好!”从上面的数组。

        4
  •  4
  •   Nick Dandoulakis    15 年前

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

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

    std::string myKey = part1() + part2() + ... + partN();
    
        5
  •  4
  •   Bill the Lizard Alexis MP    11 年前

    我已经为字符串创建了一个简单的加密工具,它可以自动生成加密的字符串,并有一些额外的选项,例如:

    作为全局变量的字符串:

    // 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;
    
        6
  •  3
  •   Frerich Raabe    15 年前

    当然,将私有数据存储在发送给用户的软件中总是一个风险。任何受过足够教育(并且有奉献精神)的工程师都可以对数据进行逆向工程。

    也就是说,你可以通过提高人们披露你的私人数据所需要克服的障碍,使事情足够安全。这通常是一个很好的妥协。

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

    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
  •  2
  •   Nic Strong    15 年前

    有点依赖于你想要保护的东西,如乔什佩里指出的。 根据经验,我想说,如果它是某个许可计划的一部分,以保护您的软件,那么不要麻烦。他们最终会对其进行逆向工程。只需使用像rot-13这样的简单密码就可以保护它不受简单攻击(在它上面运行字符串)。 如果要保护用户的敏感数据,我会质疑使用本地存储的私钥保护该数据是否明智。这又归结到你想要保护的东西。

    编辑:如果你打算这样做,那么克里斯指出的技术组合将远远优于ROT13。

        8
  •  2
  •   sbi    15 年前

    如前所述,没有办法完全保护你的绳子。但有一些方法可以保护它,以确保合理的安全。

    当我必须这样做的时候,我确实在代码中放了一些看起来无辜的字符串(例如,版权声明,或者一些伪造的用户提示,或者其他任何不会被修改无关代码的人更改的内容),加密了它,将它本身作为一个密钥,散列了它(添加了一些salt),并将结果作为一个密钥来加密我真正想要加密的内容。Rypt.

    当然,这是可以被黑客攻击的,但要做到这一点需要一个坚定的黑客。

        9
  •  1
  •   mouviciel    15 年前

    您可能不想在可执行文件中存储私钥,而是希望从用户那里请求并通过外部方式存储它。 password manager 类似于Mac OS X钥匙链访问。

        10
  •  1
  •   Collin    15 年前

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

    如前一篇文章所说,如果你在Mac上,使用钥匙链。

    基本上,从安全性的角度来看,所有关于如何将您的私钥存储在二进制文件中的可爱想法都非常糟糕,您不应该这样做。任何人拿到你的私人钥匙都是一件大事,不要把它藏在你的程序里。根据导入应用程序的方式,您可以将您的私人密钥保存在智能卡上、远程计算机上,或者您可以执行大多数人所做的操作,并将其保存在本地计算机上非常安全的位置(“密钥存储”类似于一个奇怪的安全注册表),该位置受权限和操作系统的所有功能保护。

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

        11
  •  1
  •   Michael Haephrati    9 年前

    尝试 this . 源代码解释了如何在给定的VisualStudioC++项目中对所有字符串进行加密和解密。

        12
  •  1
  •   Zeeshan    8 年前

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

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

    用于填充数据的宏:

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

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

    现在在需要保存的代码中使用这个宏 第一部分 第二部分 如下:

    char part1[4] = {0};
    char part2[4] = {0};
    POPULATE_DATA(part1, 1, 2, 3, 4); 
    POPULATE_DATA(part2, 5, 6, 7, 8);
    
        13
  •  0
  •   hplbsh    15 年前

    上下文相关,但您可以只存储 搞砸 钥匙加上一个 (常串,易模糊)。

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

    这个 在这种情况下,它可能是不必要的,如果散列可以被隔离,它就停止了野蛮的字典攻击(谷歌搜索也被知道可以工作)。

    黑客仍然只需要在某个地方插入JMP指令就可以绕过整个过程,但这比简单的文本搜索要复杂得多。