在C ++中模糊敏感字符串的技术

时间:2009-10-30 08:31:18

标签: c++ security obfuscation defensive-programming

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

std::string myKey = "mysupersupersecretpasswordthatyouwillneverguess";

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

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

修改

好的,几乎所有人都说“你的可执行文件可以进行逆向工程” - 当然!这是我的一个宠儿,所以我会在这里咆哮:

为什么99%(好的,或许我夸大一点)本网站上所有与安全相关的问题都会被“没有可能的方法来创建一个完全安全的程序”所回答 - 这就是不是一个有用的答案!安全性是完美可用性与一端无安全性之间的滑动尺度,完美的安全性,但另一方面没有可用性。

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

实施顶级安全系统所花费的时间比破解版本的销售损失更为昂贵(不是我实际上是在销售这个,但你明白我的观点)。在新程序员的编程中,这个蓝天“让我们尽可能做到绝对最好的方式”是至少可以说是愚蠢的。

感谢您抽出宝贵时间回答这个问题 - 他们最有帮助。不幸的是,我只能接受一个答案,但我已经投了所有有用的答案。

14 个答案:

答案 0 :(得分:41)

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

但是,如果您只想确保在二进制文件上运行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还有一个更大的列表,可能有些过时 其中一些是免费的。

密码匹配

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

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

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

答案 1 :(得分:8)

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

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

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

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

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

4)做一个ad-hoc算法 - 除此之外,添加一两个独特的扭曲。也许只需在您生产的所有产品中添加1,或者进行两次加密,或添加糖。这对于已经知道在某人使用时需要查找的内容的黑客来说有点困难,例如,vanilla md5哈希或RSA加密。

最重要的是,确保它不是太重要的时候(如果你的应用程序变得足够流行的时候会发生这种情况)你的密钥被发现了!

答案 2 :(得分:5)

我过去使用的策略是创建一组看似随机的字符。您最初插入,然后使用代数过程定位您的特定字符,其中从0到N的每个步骤将产生一个数字<包含混淆字符串中下一个字符的数组的大小。 (这个答案现在感觉很混乱!)

示例:

给定一组字符(数字和短划线仅供参考)

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

前六个结果的等式:3,6,7,10,21,47

会产生“你好!”这个词。来自上面的数组。

答案 3 :(得分:4)

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

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

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

答案 4 :(得分:4)

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

字符串作为全局变量:

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

答案 5 :(得分:3)

当然,将私人数据存储在运送给用户的软件中总是存在风险。任何受过良好教育(和专职)的工程师都可以对数据进行逆向工程。

话虽这么说,你可以通过提高人们需要克服的障碍来揭示你的私人数据,从而使事情足够安全。这通常是一个很好的妥协。

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

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;
    // ...
}

答案 6 :(得分:2)

有点依赖于你想要保护的东西,正如joshperry指出的那样。 根据经验,我会说,如果它是保护您的软件的一些许可方案的一部分,那么不要打扰。他们会对它进行逆向工程。只需使用像ROT-13这样的简单密码来保护它免受简单的攻击(在它上面运行字符串)。 如果要保护用户敏感数据,我会质疑用本地存储的私钥保护数据是否明智。再次归结为你想要保护的东西。

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

答案 7 :(得分:2)

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

当我不得不这样做时,我确实在代码中放了一些无​​辜的字符串(例如,版权声明,或者某些伪造的用户提示或其他任何不会被修复无关代码的人更改的内容),加密使用自身作为密钥,散列(添加一些盐),并使用结果作为密钥来加密我实际想要加密的内容。

当然这可能会被黑客入侵,但确实需要一个坚定的黑客这样做。

答案 8 :(得分:1)

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

答案 9 :(得分:1)

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

正如之前的帖子所说,如果你在mac上使用钥匙串。

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

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

答案 10 :(得分:1)

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

答案 11 :(得分:1)

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

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

    假设私有数据是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);
    

答案 12 :(得分:1)

有一个由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");

答案 13 :(得分:0)

依赖于上下文,但您可以只存储密钥的哈希加上 salt (常量字符串,容易模糊)。

然后当(if)用户输入密钥时,添加 salt ,计算哈希并进行比较。

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

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