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


87

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

std::string myKey = "mysupersupersecretpasswordthatyouwillneverguess";

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

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

编辑:

好的,几乎所有人都说过“您的可执行文件可以进行反向工程” -当然!这是我的宠儿,所以在这里我要大声疾呼:

为什么在该站点上所有与安全相关的问题中有99%(好的,所以我可能会夸张地说)用“没有可能创建完美安全的程序的方法”的洪流回答-这无济于事回答!安全性是介于完美可用性和一端没有安全性,而另一端是完美安全性却没有可用性之间的滑动标度。

关键是要根据您要执行的操作以及软件运行的环境,在此滑动标尺上选择位置。我不是在为军事安装编写应用程序,而是在为家用PC编写应用程序。我需要使用已知的加密密钥在不受信任的网络上加密数据。在这些情况下,“通过隐蔽性实现安全”可能就足够了!当然,有足够时间,精力和技能的人可以对二进制文件进行反向工程并找到密码,但是您猜怎么着?我不在乎:

实施一流的安全系统所花的时间要比由于破解版本而导致的销售损失要昂贵得多(不是我实际上是在出售此产品,但我明白了)。至少可以这样说,在新程序员中,这种蓝天式的“让它以绝对最佳的方式做到这一点”的趋势是愚蠢的。

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


2
也许如果您描述了要使用加密密钥完成的工作,我们可以提供有关如何避免这样做的建议。
joshperry


4
@Kirill:很难将此问题与您提到的问题完全相同。确实,想法是相同的。问题不是。
xtofl,2009年

1
@xtofl,您可以自由投票。对我来说,这似乎是两个相同的问题。
09年

2
(与rant无关)“私钥”的确切定义是您不提供的公钥/私钥对的一半。在这种情况下,私钥也是您保存在服务器上的私钥。您的客户端应用中的一个是公开的。
MSalters

Answers:


42

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

但是,如果您只想确保在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]

您可以在构建环境做到这一点,然后只存储key1key2应用程序中。

保护您的二进制文件

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

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

还有很多保护二进制文件工具,也许有些过时了。
其中一些是免费的。

密码匹配

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

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

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


如果它是用户必须输入的密码,请存储密码的hash + salt。请参阅下面的答案。

1
@hapalibashi:如何将盐安全地存储在应用程序中?我认为OP不需要单向密码匹配系统,而只是存储静态密钥的通用方法。
csl

2
我发现在查看反汇编程序时,通常没有太多的XOR,因此,如果您希望使用XOR来掩盖某些内容,请记住它们会引起人们的注意。
kb。

2
@kb-这很有趣。我猜您会发现按位与和比发生于xor的事情要多得多。a ^ b ==(a&〜b)|| (〜a和b)
杰里米·鲍威尔,

4
知道盐值通常不会给对手带来好处-盐的目的是避免“字典式攻击”,即攻击者已经为许多可能的输入预先计算了哈希值。使用盐会迫使他们使用基于盐的新字典来预先计算字典。如果每种盐仅使用一次,则字典攻击将变得完全无用。
爱德华·迪克森

8

首先,请意识到您无能为力,无法阻止足够坚决的黑客,并且周围有很多这样的黑客。最终,对每个游戏和主机的保护都将被破解,因此这只是一个临时解决方案。

您可以执行4件事,这将增加您隐藏一段时间的机会。

1)以某种方式隐藏字符串的元素-很明显,例如将字符串与另一个字符串进行异或(^运算符)将足以使该字符串无法搜索。

2)将字符串拆分为多个部分-将您的字符串拆分并弹出,并将其放入奇怪的模块中的名称奇怪的方法中。不要轻易搜索并找到带有字符串的方法。当然,某些方法将必须调用所有这些位,但是它仍然使它变得有点困难。

3)永远不要在内存中构建字符串-大多数黑客使用的工具可以让您在对字符串进行编码后看到内存中的字符串。如果可能,请避免这种情况。例如,如果要将密钥发送给服务器,请逐个字符地将其发送,这样整个字符串就永远不会存在。当然,如果您通过类似RSA编码的方式来使用它,那将更加棘手。

4)执行即席算法-在所有这些之上,添加一个或两个独特的转折。也许只需将1加到您生产的所有产品上,或将任何加密两次,或添加糖。这对于已经知道要使用某人(例如,香草md5哈希或RSA加密)的用户寻找内容的黑客来说,要增加一点难度。

最重要的是,确保发现密钥的时间不是太重要(并且当您的应用程序变得足够流行时才是)。


5

我过去使用的策略是创建一个看似随机的字符数组。您首先插入,然后通过代数过程定位您的特定字符,其中从0到N的每一步将产生数字<数组的大小,该数组包含混淆字符串中的下一个字符。(这个答案现在变得令人困惑!)

例:

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

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

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

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


好主意-我想您可以通过在数组中使用非打印字符来进一步改善它……
Thomi

4

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

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

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

是的,这避免了在搜索二进制文件时显示字符串。但是,您的字符串仍然驻留在内存中。.对于我在做什么,您的解决方案可能足够好。
托米

@Thomi,您当然可以在完成使用后立即销毁它。但是,这并不是处理敏感字符串的最佳方法。
尼克·丹杜拉基斯

...由于销毁它实际上并不能保证立即可以重新使用该内存。
2009年

4

当然,将私有数据存储在交付给用户的软件中总是有风险的。任何受过充分教育(专门)的工程师都可以对数据进行逆向工程。

话虽这么说,您通常可以通过提高人们披露个人数据所需克服的障碍来使事情变得足够安全。通常这是一个很好的折衷方案。

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

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

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;

不,我使用过stringencrypt.com网站来完成这项工作。它具有C / C ++ stringencrypt.com/c-cpp-encryption的示例,您可能会考虑使用它来自动执行简单的字符串加密。
BartoszWójcik'13

2

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

编辑:如果您要这样做,那么克里斯指出的技术组合将比rot13更好。


2

如前所述,无法完全保护您的琴弦。但是有一些保护它的方法具有合理的安全性。

当我必须这样做时,我确实在代码中放入了一些无害的字符串(例如,版权声明,伪造的用户提示或其他任何由不相关代码修复的人都不会更改的东西),并使用自身对其进行了加密。作为密钥,对其进行哈希处理(添加一些盐),然后将结果用作对我实际想要加密的内容进行加密的密钥。

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


好主意-另一种晦涩的用法是使用仍相当强壮的字符串(长,标点符号和所有爵士乐),但显然不像密码。
Thomi

1

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

如前一篇文章所述,如果您使用的是Mac,请使用钥匙串。

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

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



1

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

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

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;\
}

现在,在需要保存part1和的代码中使用此宏part2,如下所示:

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

0

与其将私钥存储在可执行文件中,不如从用户那里请求私钥,并通过外部密码管理器(与Mac OS X钥匙串访问类似)来存储私钥。


是的,不是。通常,我会同意您的意见,但是在这种情况下,我试图将其隐藏在软件的用户面前,因此将其存储在外部系统中并不是一个好主意(许多钥匙串系统可以在获得适当授权后,将密码以纯文本格式显示给用户)。钥匙串软件非常适合用户密码,但不适用于应用程序加密密钥。
托米

如果感觉不安全(尽管您的澄清也许就足够了),则可以结合使用钥匙串和一些硬编码的方法:)

0

取决于上下文,但是您可以只存储键的哈希值加上一个(常量字符串,易于理解)。

然后在(如果)用户输入密钥的情况下,添加,计算出哈希值并进行比较。

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

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


这是一个加密密钥,而不是密码哈希。我需要实际的密钥来编码和解码数据。用户永远不会看到密钥,也永远不会将其存储在二进制文件之外。
2009年

0

由adamyaxley制作的(非常轻)仅头文件模糊处理的效果很好。它基于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");
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.