如何在二进制代码中隐藏字符串?

84
有时候,从二进制(可执行)文件中隐藏一个字符串是很有用的。例如,将加密密钥从二进制文件中隐藏起来是很有意义的。
当我说“隐藏”时,是指让字符串在编译后的二进制文件中更难被找到。
例如,这段代码:
const char* encryptionKey = "My strong encryption key";
// Using the key

编译后,会生成一个可执行文件,在其数据部分中包含以下内容:

4D 79 20 73 74 72 6F 6E-67 20 65 6E 63 72 79 70   |My strong encryp|
74 69 6F 6E 20 6B 65 79                           |tion key        |

你可以看到我们的秘密字符串很容易被发现和/或修改。

我可以隐藏这个字符串...

char encryptionKey[30];
int n = 0;
encryptionKey[n++] = 'M';
encryptionKey[n++] = 'y';
encryptionKey[n++] = ' ';
encryptionKey[n++] = 's';
encryptionKey[n++] = 't';
encryptionKey[n++] = 'r';
encryptionKey[n++] = 'o';
encryptionKey[n++] = 'n';
encryptionKey[n++] = 'g';
encryptionKey[n++] = ' ';
encryptionKey[n++] = 'e';
encryptionKey[n++] = 'n';
encryptionKey[n++] = 'c';
encryptionKey[n++] = 'r';
encryptionKey[n++] = 'y';
encryptionKey[n++] = 'p';
encryptionKey[n++] = 't';
encryptionKey[n++] = 'i';
encryptionKey[n++] = 'o';
encryptionKey[n++] = 'n';
encryptionKey[n++] = ' ';
encryptionKey[n++] = 'k';
encryptionKey[n++] = 'e';
encryptionKey[n++] = 'y';

......但这不是一个好方法。有更好的想法吗?

顺便说一句:我知道仅仅隐藏秘密并不能对抗决心坚定的攻击者,但这总比没有强……

另外,我知道非对称加密,但在这种情况下不可接受。我正在重构一个现有的应用程序,该应用程序使用Blowfish加密并将加密数据传递到服务器(服务器使用相同的密钥解密数据)。

无法 更改加密算法,因为我需要提供向后兼容性。 我甚至无法更改加密密钥。


13
有些公钥加密系统无需隐藏密钥即可使用。 - AProgrammer
5
我知道密钥对,但在这种情况下不可接受。我正在重构使用Blowfish加密的现有应用程序。加密数据传递到服务器并由服务器解密数据。我不能更改加密算法,因为我必须提供向后兼容性。 - Dmitriy
7
将钥匙隐藏在可执行文件中几乎总是不明智的做法。 - Evan Teran
重复的问题:C++中混淆敏感字符串的技巧(虽然已经有12年的活跃工作和有意义的答案,但不会关闭)。 - Alex Cohn
23个回答

56

非常抱歉回答得有些晚。

你的回答是完全正确的,但问题是如何优雅地隐藏字符串。我是这样做的:

#include "HideString.h"

DEFINE_HIDDEN_STRING(EncryptionKey, 0x7f, ('M')('y')(' ')('s')('t')('r')('o')('n')('g')(' ')('e')('n')('c')('r')('y')('p')('t')('i')('o')('n')(' ')('k')('e')('y'))
DEFINE_HIDDEN_STRING(EncryptionKey2, 0x27, ('T')('e')('s')('t'))

int main()
{
    std::cout << GetEncryptionKey() << std::endl;
    std::cout << GetEncryptionKey2() << std::endl;

    return 0;
}

HideString.h:

#include <boost/preprocessor/cat.hpp>
#include <boost/preprocessor/seq/for_each_i.hpp>
#include <boost/preprocessor/seq/enum.hpp>

#define CRYPT_MACRO(r, d, i, elem) ( elem ^ ( d - i ) )

#define DEFINE_HIDDEN_STRING(NAME, SEED, SEQ)\
static const char* BOOST_PP_CAT(Get, NAME)()\
{\
    static char data[] = {\
        BOOST_PP_SEQ_ENUM(BOOST_PP_SEQ_FOR_EACH_I(CRYPT_MACRO, SEED, SEQ)),\
        '\0'\
    };\
\
    static bool isEncrypted = true;\
    if ( isEncrypted )\
    {\
        for (unsigned i = 0; i < ( sizeof(data) / sizeof(data[0]) ) - 1; ++i)\
        {\
            data[i] = CRYPT_MACRO(_, SEED, i, data[i]);\
        }\
\
        isEncrypted = false;\
    }\
\
    return data;\
}

在HideString.h中最棘手的一行是:
BOOST_PP_SEQ_ENUM(BOOST_PP_SEQ_FOR_EACH_I(CRYPT_MACRO, SEED, SEQ))

让我解释一下这行代码。代码如下:
DEFINE_HIDDEN_STRING(EncryptionKey2, 0x27, ('T')('e')('s')('t'))

BOOST_PP_SEQ_FOR_EACH_I(CRYPT_MACRO, SEED, SEQ)
生成序列:

( 'T'  ^ ( 0x27 - 0 ) ) ( 'e'  ^ ( 0x27 - 1 ) ) ( 's'  ^ ( 0x27 - 2 ) ) ( 't'  ^ ( 0x27 - 3 ) )

BOOST_PP_SEQ_ENUM(BOOST_PP_SEQ_FOR_EACH_I(CRYPT_MACRO, SEED, SEQ))
的生成结果为:

'T' ^ ( 0x27 - 0 ), 'e' ^ ( 0x27 - 1 ), 's' ^ ( 0x27 - 2 ), 't' ^ ( 0x27 - 3 )

and finally,

DEFINE_HIDDEN_STRING(EncryptionKey2, 0x27, ('T')('e')('s')('t'))
generate:

static const char* GetEncryptionKey2()
{
    static char data[] = {
        'T' ^ ( 0x27 - 0 ), 'e' ^ ( 0x27 - 1 ), 's' ^ ( 0x27 - 2 ), 't' ^ ( 0x27 - 3 ),
        '\0'
    };
    static bool isEncrypted = true;
    if ( isEncrypted )
    {
        for (unsigned i = 0; i < ( sizeof(data) / sizeof(data[0]) ) - 1; ++i)
        {
            data[i] = ( data[i] ^ ( 0x27 - i ) );
        }
        isEncrypted = false;
    }
    return data;
}

"我的强加密密钥"的数据如下:

0x00B0200C  32 07 5d 0f 0f 08 16 16 10 56 10 1a 10 00 08  2.]......V.....
0x00B0201B  00 1b 07 02 02 4b 01 0c 11 00 00 00 00 00 00  .....K.........

非常感谢您的回答!

2
感谢分享您的解决方案!我需要隐藏字符串以防止被十六进制编辑器和基本反编译器检测到。 - Nikolay Spassov
@Dmitriy 你可以分享这个给使用C语言的人吗?也许在Github链接上。 - Florida
1
@Florida - 抱歉,但该解决方案使用 Boost 库,而 Boost 库(通常)不支持 C。 - Dmitriy
@Dmitriy 没问题,我已经找到另一个解决方案了,我现在不记得了,似乎是逐字节字符串代码。 - Florida
@NikolaySpassov - 对于了解机器码的人怎么办?或者知道一个朋友/可以雇佣懂机器码的人怎么办? - Stephen C
这部分 Boost 库只使用预处理器,因此在 C 语言中应该可以正常工作。 - Nick Brown

48

正如在 pavium 的answer中所指出的,你有两个选择:

  • 保护密钥
  • 保护解密算法

不幸的是,如果你必须将密钥和算法都嵌入代码中,那么它们都不是真正的秘密,因此你只能采用(较弱的)security through obscurity替代方案。换句话说,正如你提到的,你需要巧妙地隐藏其中一个或两个内容在可执行文件中。

这里有一些选项,但你需要记住,根据任何密码学最佳实践,这些选项都不是真正安全的,每种方法都有其缺点:

  1. 将密钥伪装成代码中通常出现的字符串。一个例子是printf()语句的格式字符串,它往往包含数字、字母和标点符号。
  2. 在启动时对代码或数据段进行哈希处理,并将其用作密钥。(你需要有些聪明才能确保密钥不会意外更改!)这有一个可能令人满意的副作用,即每次运行时验证代码的哈希部分。
  3. 从某些唯一的系统信息中(如网络适配器的MAC地址)在运行时生成密钥。
  4. 通过选择其他数据中的字节来创建密钥。如果您有静态或全局数据,无论类型(int, char,等),在变量初始化后(当然是到非零值)和变量发生变化之前,从每个变量的某个位置取一个字节。

请告诉我们您是如何解决这个问题的!

编辑: 你说你正在重构现有的代码,所以我假设你不能自己选择密钥。在这种情况下,遵循一个两步过程:使用上述方法之一加密密钥本身,然后使用密钥解密用户的数据。


22
  1. 将其发布为一个代码高尔夫问题
  2. 等待使用 J 语言编写的解决方案
  3. 在您的应用程序中嵌入 J 解释器

10

针对C语言,请查看以下链接:https://github.com/mafonya/c_hide_strings

针对C++语言,请参考以下内容:

class Alpha : public std::string
{
public:
    Alpha(string str)
    {
        std::string phrase(str.c_str(), str.length());
        this->assign(phrase);
    }
    Alpha c(char c) {
        std::string phrase(this->c_str(), this->length());
        phrase += c;
        this->assign(phrase);

        return *this;
    }
};

为了使用这个功能,只需包含Alpha和以下内容:
Alpha str("");
string myStr = str.c('T').c('e').c('s').c('t');

所以现在mystr是"Test",并且该字符串在二进制的字符串表中被隐藏。

9

你的示例根本没有隐藏字符串;该字符串仍然作为一系列字符呈现在输出中。

有多种方法可以混淆字符串。有简单的替换密码,或者您可以对每个字符执行数学运算(例如异或),其中结果输入到下一个字符的操作中,等等。

目标是得到看起来不像字符串的数据,因此例如,如果您使用大多数西方语言,大多数字符值将在32-127范围内 - 因此,您的目标应该是让操作将它们大部分移出该范围,以便它们不引起注意。


我可以使用类似 encryptionKey[n++] = 'M' ^ 0x79; 的方法,但这仍然是“不好的方法”。 - Dmitriy
1
你对“好看”的定义是什么? - T.J. Crowder
3
我认为这种“隐藏”方式是你能够做到的最好的。我之前也出于和你相同的原因做过类似的事情(除了我在字符串长度上使用了4个不同的异或常量进行轮换)。如果黑客有能力找到并解码我的字符串,那么他们也可以直接编辑程序返回值等内容。我希望我知道更好的答案。 - Peter M
4
“将字符旋转4个位置”将破解时间从大约30秒增加到大约120秒-这是使用纸和笔的情况下。 - MSalters

9
在代码中隐藏密码是一种安全性靠模糊性保护的做法。这种做法有害,因为会让你觉得已经有了某种程度的保护,实际上却非常脆弱。如果某些东西值得保护,那就要采用适当的方式进行保护。
PS:我知道这并不能对付真正的黑客,但总比没有好...
实际上,在很多情况下,没有任何安全措施比使用弱安全措施更好。至少你知道自己的立场。不需要成为“真正的黑客”就能规避嵌入式密码...
编辑:回应这条评论:
我知道密钥对,但在这种情况下不可接受。我正在重构现有的应用程序,该应用程序使用Blowfish加密。加密数据传递给服务器,服务器解密数据。我无法更改加密算法,因为我必须提供向后兼容性。
如果您真的关心安全,维护向后兼容性是让自己暴露于嵌入式密码攻击中的非常糟糕的理由。打破具有不安全安全方案的向后兼容性是一件好事。
这就像街头混混发现你把前门钥匙留在门垫下,但你继续这么做是因为爷爷希望在那里找到它一样。

38
几乎所有的软件许可证密钥和序列号都是安全性通过隐秘性实现的例子,这是完全合法的使用情况。按照你和其他人的论点,你永远不应该将自行车锁在自行车架上,因为所有的自行车锁都可以很容易地被正确的工具破解。至少当你的自行车没有锁定时,“你知道自己处于什么位置”。 - Harvey
1
@Harvey - 这正是人们能够找到密钥并盗版软件的原因。许可证密钥只能保护您免受诚实或不想冒险被抓的人的侵害。 - Stephen C
8
大多数门锁只能保护你不受那些诚实的人或者不想冒被发现风险的人的侵害。很多人认为门锁是安全的,但实际上并不安全,然而我也不会让门敞开着离开家。 - dureuill
2
如果某物值得保护,那么就值得妥善保护。我给你点了踩,因为和其他人一样,你只是说了这种泛泛而谈的话,却没有提供任何真正解决问题的方案。你并没有帮助别人,反而在伤害他们。 - Krythic
2
我认为这里的批评非常公正。你有一些好观点,但你完全忽略了另一面。重复自行车比喻,即使是最糟糕的自行车锁也可以防止那些只是想偷辆自行车回家的醉汉偷走你的自行车。我还记得我大约14岁时进行了一些黑客攻击。由于我的知识不是很高,我所能做的就是编辑二进制字符串。我确实这样做了。例如,我将旧的dos程序“格式化”中的警告文本替换为某些你绝对想用“是”的答案来代替。 - klutt
显示剩余7条评论

8
这就像把自行车放在荷兰阿姆斯特丹中央火车站附近一样不安全(眨眼,车就没了!)。
如果你想增加应用程序的安全性,那么从一开始就注定会失败,因为任何保护方案都会失败。你能做的只是让黑客找到所需信息更加复杂。还有一些技巧:
*)确保字符串在二进制文件中以UTF-16形式存储。
*)将数字和特殊字符添加到字符串中。
*)使用32位整数数组而不是字符串!将每个整数转换为字符串并将它们连接起来。
*)使用GUID,将其存储为二进制,然后将其转换为要使用的字符串。
如果你真的需要一些预定义的文本,请加密它并将加密后的值存储在二进制文件中,在运行时解密,其中解密的密钥是我之前提到的选项之一。
请注意,黑客通常会以其他方式破解你的应用程序。即使是密码学专家也无法保证安全。总的来说,唯一保护你的是黑客从黑客你的代码中获得的利润与攻击成本的比较。(这些成本通常只是大量时间,但如果黑客攻击你的应用程序需要一周时间,而攻击其他东西只需要2天时间,那么其他东西更有可能受到攻击。)
回复评论:
UTF-16 每个字符使用两个字节,因此对于查看二进制转储的用户来说更难识别,因为每个字母之间有一个额外的字节。不过,你仍然可以看到单词。UTF-32 将会更好,因为它在字母之间添加了更多的空间。不过,你也可以通过改为每个字符6位的方案来压缩文本。这样,每4个字符就会压缩成三个数字。但这将限制你只能使用2x26个字母、10个数字以及可能的空格和点来获取64个字符。
使用GUID是实用的,如果您将GUID存储在其二进制格式中而不是文本格式中。 GUID长度为16字节,可以随机生成。因此,很难猜测用作密码的GUID。但是,如果您仍然需要发送纯文本,则可以将GUID转换为字符串表示形式,例如“3F2504E0-4F89-11D3-9A0C-0305E82C3301”。 (或Base64编码为“7QDBkvCA1 + B9K / U0vrQx1A ==”)。但用户在代码中看不到任何明文,只有一些表面上随机的数据。 但并非GUID中的所有字节都是随机的。 GUID中隐藏了版本号。但是,对于加密目的,使用GUID并不是最佳选择。它是基于您的MAC地址或伪随机数计算的,使其相对可预测。尽管如此,创建和存储,转换和使用仍然很容易。创建更长的内容并不会增加更多的价值,因为黑客只会尝试找到其他破解安全性的技巧。这只是一个关于他们愿意投入更多时间分析二进制文件的问题。
总的来说,保持应用程序安全的最重要的事情是对其感兴趣的人数。如果没有人关心您的应用程序,那么也没有人会费心去破解它。当您是拥有5亿用户的顶级产品时,您的应用程序将在一小时内被破解。

2
你的担忧都是合理的,但不幸的是商业世界并非完美无缺,问题必须在可接受的范围内解决。话虽如此,我对你的一些建议很感兴趣。UTF-16: 为什么?比特就是比特 :-) 它是否更好地允许_all_值,包括不可打印的值呢?GUID:为什么这比任何其他(可能更长的)字节序列更好? - Adam Liss
其他我读到的提示:在与许可证无关的应用程序中的多个位置找到秘密字符串。此外,如果您可以延迟使用秘密代码的运行时间,例如通过定时事件触发,这将使破解者更难以进行调试。 - Harvey
基于时间的安全性很容易被虚拟化破解,除非您有一些加密请求发送到某个许可服务器。因此,在有效地测试虚拟化代码之前,他们必须先破解数据(网络数据)。 - m3nda

5
你可以使用我开发的一个 C++库 来实现这个目的。另一篇文章 更简单易实现,曾获得2017年9月最佳C++文章奖。如果想要更简单的方式来隐藏字符串,请参见 TinyObfuscate

4
我曾经也处于类似的尴尬境地。我有一些需要以二进制形式而非明文形式存储的数据。我的解决方案是使用一个非常简单的加密方案,使其看起来像程序的其余部分。我编写了一个程序,将字符串转换为ASCII码(必要时用零填充以获得三位数字),然后在3位代码的开头和结尾添加一个随机数字来加密数据。因此,加密字符串中的每个字符由5个字符(都是数字)表示。我将该字符串作为常量粘贴到应用程序中,然后在需要使用该字符串时,解密并将结果存储在变量中,仅在需要时使用。
因此,以您的示例为例,“My strong encryption key”变成了“207719121310329211541116181145111157110071030703283101101109309926114151216611289116161056811109110470321510787101511213”。然后,当您需要使用加密密钥时,通过撤销这个过程对其进行解码即可。
这当然不是绝对安全的,但我并没有追求这个。

9
我刚刚破解了你的应用程序。谢谢提供所需信息以便完成这个过程。 - Thomas Eding
12
你刚刚破解了哪个应用程序? - Corin
1
对于聪明的“破解高手”来说,这当然只是一个例子,你可以添加任何微小的变化并获得大量的组合来尝试:现在开始暴力破解吧。 - Zac

3

加密技术足够强大,可以保护重要数据而无需将其隐藏在二进制文件中。

或者,你的想法是使用二进制文件来掩盖某些东西被隐藏了吗?

那就叫做隐写术


2
@pavium:无论加密有多强,都必须有一个密钥和解密数据的算法。虽然算法可以公开,但密钥必须保密。我认为问题是如何嵌入密钥,使其不会(轻易地)被揭示。 - Adam Liss
@Adam Liss:请查看我对这个问题的评论,了解我的情况。 - Dmitriy
@Adam Liss:我认为问题是关于通过将消息隐藏在二进制文件中来伪装消息的,而且我认为这并不是一个简单的误解,即问题中使用了“密钥”而不是“明文”这个词。也许我得出了错误的结论。希望我们能找到答案。 - pavium
链接失效。 - Pang

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