在C++中混淆敏感字符串的技巧

105

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

std::string myKey = "mysupersupersecretpasswordthatyouwillneverguess";
然而,通过strings进程(或任何其他从二进制应用程序提取字符串的进程)运行应用程序将会显示上述字符串。有哪些技术可以用来模糊这种敏感数据? 编辑:好的,几乎所有人都说过“您的可执行文件可以被反向工程化”——当然!这是我讨厌的事情之一,所以我要在这里发泄一下:
为什么在这个网站上99%(好吧,也许我有点夸张)与安全相关的问题都被回答成:“没有可能创建一个完全安全的程序”这不是一个有用的答案! 安全是在完美的可用性和无安全性的一端和完美的安全但没有可用性的另一端之间的滑动比例尺。
关键是,你根据你想要做什么和你的软件将在其中运行的环境来选择你在那个滑动比例尺上的位置。 我不是为军用装置编写应用程序,我正在为家庭PC编写应用程序。 我需要使用预先已知的加密密钥加密跨不受信任的网络的数据。 在这些情况下,“安全通过模糊”可能已经足够了! 当然,有足够的时间,精力和技能的人可能会反向工程二进制文件并找到密码,但是猜猜? 我不在乎:
实施顶尖安全系统的时间比由于破解版本而失去的销售量更昂贵(虽然我实际上并没有出售这个软件,但你理解我的意思)。 在新程序员中,“让我们以最好的方式来做”的蓝天趋势至少是愚蠢的。
感谢您抽出时间回答这个问题-它们非常有帮助。 不幸的是,我只能接受一个答案,但我已经投票支持了所有有用的答案。

2
也许如果您描述一下使用加密密钥所要实现的目标,我们可以提供如何避免需要这样做的建议。 - joshperry
3
如何在exe或dll中隐藏字符串 - Kirill V. Lyadvinsky
4
@Kirill:很难说这个问题完全与你提到的那个问题相同。的确,思路是一样的,但问题并不一样。 - xtofl
2
@xtofl,你可以选择不投票。对我来说,这两个问题看起来是完全相同的。 - Kirill V. Lyadvinsky
2
(与抱怨无关)“私钥”的定义是公共/私有密钥对中不公开的一半。在这种情况下,私钥是您保存在服务器上的那个;客户端应用程序中的那个是公共的。 - MSalters
显示剩余5条评论
14个回答

47

基本上,任何具备您程序访问权限和调试器的人都可以并且会在需要的情况下在应用程序中找到密钥。

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

使用异或来混淆密钥

例如,您可以使用异或将密钥分成两个字节数组:

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等程序中进行调试的功能。
还有一个更大的列表,可能有些过时,列出了保护二进制文件的工具。其中一些是免费的。
如果您需要存储密钥以将其与某种用户提交的密码进行匹配,可以使用单向哈希函数,最好是通过结合用户名、密码和盐。但问题在于,您的应用程序必须知道盐才能进行单向哈希并比较生成的哈希值。因此,您仍然需要在应用程序中存储盐。但是,正如@Edward在下面的评论中指出的那样,这将有效地防止使用彩虹表等字典攻击。
最后,您可以结合上述所有技术使用。

如果是用户必须输入的密码,请存储密码的哈希值+盐。请参见下面的答案。 - user172783
1
@hapalibashi:在您的应用程序中如何安全地存储盐?我认为OP不需要单向密码匹配系统,只需要一种存储静态密钥的通用方法。 - csl
2
在查看反汇编程序时,我发现通常很少有XOR操作,因此如果你希望使用XOR来隐藏某些内容,请记住它们会引起注意。 - kb.
2
@kb - 这是一个有趣的观点。我猜你会看到比特位与和或发生得比异或要多得多。a ^ b == (a & b) || (a & b) - Jeremy Powell
4
了解盐值通常不会给对手带来优势 - 盐的作用是避免“字典攻击”,这种攻击者预先计算了许多可能输入的哈希值。使用盐强制攻击者基于盐重新计算他们的字典表。如果每次都只使用一次盐,那么字典攻击就变得完全无效。 - Edward Dixon
它几乎不需要是加密级别的伪随机数生成器; 对于使用strings进行静态分析,简单的LFSR或xorshift+就足够了; 如果“攻击者”使用调试器找到此XOR代码,则可以在之后查看结果。您可能希望强制每个字节的高位设置为(tmp | = 0x80),以便XOR使ASCII数据不可打印,并混淆UTF-8多字节字符。或者也许最好不要有这样的模式,让它成为可打印和不可打印的混合体。 - Peter Cordes

12

有一个非常轻量级的仅头文件的项目obfuscate,由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");

在ARM上,通过简单地修补为MOV R0,1 + BX LR就可以轻松地击败混淆。 - Sourav Banerjee
@SouravBanerjee 这非常有趣,你从哪里得到这个信息的?我正在寻找obfuscate库的评论,这个库非常方便,但使用并不是很广泛。 - Antonio
@Antonio,混淆基本上是单字节异或,很容易被打败。使用混淆构建任何二进制文件,并在其中包含符号,几乎不费吹灰之力就可以找到在执行去混淆操作之前加载异或密钥的例程。如果您正在使用单字节异或来混淆重要内容(如私钥或API密钥),请避免使用它。 - Sourav Banerjee
@SouravBanerjee 谢谢你的见解!这里提出的解决方案更好吗?https://dev59.com/mlwY5IYBdhLWcg3wuZym#32287802 - Antonio
1
@AdamYaxley 我理解你的意思,但我只是建议OP使用符号来构建代码,这样他们可以将反汇编与裸二进制文件进行对比,以更快地找到去混淆程序。我喜欢使用混淆技术,也成功地将其用于简单字符串(如API端点),但不建议将此机制用于真正敏感的数据,例如API密钥和私钥。 - Sourav Banerjee
显示剩余2条评论

10
首先要认识到,没有什么措施能够阻止一个足够决心的黑客,并且这样的黑客有很多。所有游戏和控制台上的保护措施最终都会被破解,所以这只是一个暂时的解决方案。
有四件事情可以增加你在一段时间内保持隐藏的机会:
1)以某种方式隐藏字符串元素——像用另一个字符串异或(^ 运算符)该字符串就足以使其无法搜索。
2)将字符串分成几部分——将字符串分开并将其中的一些部分放入奇怪模块的奇怪方法中。不要让它易于搜索并找到具有该字符串的方法。当然,某个方法必须调用所有这些部分,但这仍然使它稍微难一些。
3)永远不要在内存中构建字符串——大多数黑客使用工具,在你编码后能看到内存中的字符串。如果可能的话,避免这样做。例如,如果你正在将密钥发送到服务器,则逐个字符地发送它,这样整个字符串就不会存在。当然,如果你要从 RSA 编码等东西中使用它,那么这就比较棘手了。
4)进行即席算法——除此之外,再添加一两个独特的小技巧,也许只需将每个产生的值加 1,或者对任何加密做两次,或者添加一个“糖果”。这样只是让已经知道当有人使用例如纯粹 md5 哈希或 RSA 加密时该寻找什么的黑客感到稍微困难一些。
最重要的是,确保在你的密钥被发现时它并不是非常重要的事情(而且如果你的应用程序足够受欢迎,它肯定会被发现)。

7

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

例如:

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

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

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

从上面的数组中,这个方程会产生单词“HELLO!”。


好主意 - 我想你可以通过在数组中使用非打印字符来进一步改进它... - Thomi
哪里是 O? - vesperto

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;

3
我已使用http://www.stringencrypt.com网站来完成工作。它提供了C/C++的示例http://www.stringencrypt.com/c-cpp-encryption/,你可以考虑使用它来自动加密简单字符串。 - Bartosz Wójcik

4

我同意@Checkers的观点,你的可执行文件可以被反向工程。

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

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

没错,这样可以避免在搜索二进制文件时暴露字符串。然而,你的字符串仍然存在于内存中。不过,对于我正在做的事情来说,你的解决方案可能已经足够好了。 - Thomi
@Thomi,当然你可以在使用完之后销毁它。但是,这并不是处理敏感字符串的最佳方式。 - Nick Dandoulakis
由于销毁它并不能保证内存会立即被重用。 - Thomi

2

正如之前所说,没有完全保护字符串的方法。但是有一些方式可以使其具有合理的安全性。

当我需要这样做时,我会将一些看似无害的字符串放入代码中(例如版权声明,或者一些虚假的用户提示信息或其他任何不会被更改的内容),使用本身作为密钥对其进行加密,对其进行哈希处理(添加一些盐),并使用结果作为密钥来加密我实际想要加密的内容。

当然,这种方法也可能被黑客攻破,但这需要一个有决心的黑客才能做到。


好主意 - 另一种模糊的形式是使用一个仍然相当强大的字符串(长,标点符号等等),但不明显看起来像一个密码。 - Thomi
我在思考,攻击这个问题的一种方法是通过编程修改可执行文件中找到的所有字符串,并查看通过修改哪个字符串解密失败。使用分治算法可以快速找出根字符串是哪个。 - Antonio
@Antonio 正如最受欢迎答案中的第一句话所说:“基本上,任何有权访问您的程序和调试器的人都可以并且会在应用程序中找到密钥,如果他们想要的话。” 我们正在谈论威慑的不同层次。 - sbi

2
有些取决于您试图保护什么,正如joshperry所指出的那样,与此相关的是经验。如果它是某种许可方案的一部分以保护您的软件,那就不要费心了。他们最终会对其进行逆向工程。只需使用简单的密码,如ROT-13,以使其免受简单攻击(行运行字符串)。如果要保护用户敏感数据,我会质疑使用本地存储的私钥来保护该数据是否明智。再次,这取决于您试图保护什么。

编辑:如果您要这样做,则Chris指出的技术组合将比rot13好得多。


1
我最近尝试的一种方法是:
  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);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接