一段时间以来,我一直想知道某些软件如何以无法轻易发现的方式隐藏密钥。仅举几个例子:
- DVD 播放器软件隐藏 CSS 键
- 带有序列号/注册码的软件隐藏了用于验证序列号的密钥/哈希
显然,这些程序所做的不仅仅是将密钥放在 byte[] 中,因为这样可以很容易地窃取他们的密钥并生成您自己的序列号等。
使用什么样的策略来隐藏这些键,使它们不容易被发现?
一段时间以来,我一直想知道某些软件如何以无法轻易发现的方式隐藏密钥。仅举几个例子:
显然,这些程序所做的不仅仅是将密钥放在 byte[] 中,因为这样可以很容易地窃取他们的密钥并生成您自己的序列号等。
使用什么样的策略来隐藏这些键,使它们不容易被发现?
这些密钥如此容易被发现的原因是它们隐藏在软件中。
不惜一切代价避免在软件中隐藏秘密——混淆只会让你走这么远。问问自己这个问题:对于完全访问反汇编、用户模式和内核模式调试器但没有日常工作的人,我能在多大程度上隐藏软件中的密钥?它被破解只是时间问题。
您只需将密钥隐藏在某处,并在需要时对其进行解密。“安全地”使用密钥是复杂的部分。破解者可能会在您使用解密密钥的位置设置断点并将其转储。他们可能会扫描您的代码以查找表明您正在使用已知加密算法的模式(大多数算法都有预先计算的表)。等等等等
这就是为什么您需要使整个软件的可执行文件难以分析。为此,您使用可执行打包程序、在虚拟机中运行代码、完整性检查等。所有这些都是为了减慢调试和修改代码的速度。
正如这里的大多数人指出的那样,您无法阻止任何人,只能让他们慢下来。我会去一个破解者论坛并在那里询问有关关键隐藏问题的建议。如果您问得好,它们很可能会有所帮助。
附言。公钥加密不会更好地隐藏密钥,但如果您正在执行许可方案,它可能会使密钥生成器更难(或理论上不可能)。
底线是,你不能。有关原因,请参阅此处的任何其他评论。甚至像 PGP/GPG 这样的加密软件也将密钥存储在一个文件中,然后强烈要求将这些文件保存在安全的闪存驱动器或其他安全的地方。将发现作为可执行代码的一部分存储的密钥。
事实上,如果你试图在客户端机器上加密任何将被客户端解密作为正常操作的一部分的东西,那也是愚蠢的差事。客户端机器本质上是不安全的,您无法控制它们将能够对您的数据做什么。
相反,如果您尝试进行身份验证,请查看基于 Internet 的身份验证以及登录到服务器,或某种生成的用于验证软件的 KeyCode。
作为公钥-私钥对的一部分的密钥应该保存在可以保护的数据文件中。对称密钥应作为会话密钥即时生成,然后丢弃。始终假设在其计算机上拥有 Secret 或 Session 密钥的任何人都能够发现它,并违背您的意图使用它。
阅读Bruce Schneier 的“应用密码学”以获取更多信息。
你不能永远隐藏一把钥匙。但你可以肯定很难找到。一些方法是在内存中加密密钥,保留多个副本(可能以不同方式加密)相互检查,留下可供访问的虚拟副本,以某种奇怪的格式存储密钥等。如果有人真的这样做,它们都不会起作用想要你的钥匙,但你至少可以劝阻一个随便/没有经验的攻击者。
您并不总是需要密钥来验证许可证。
但忽略这一事实,您的密钥也可能是另一个函数的结果。您实际上并没有存储特定的键值,而是有一个动态生成键的函数(总是相同的结果)。尽管并非不可能,但由于您不再寻找值,因此要找到它要困难得多,但是您必须弄清楚它是一个方程式。
当我们开始开发我们的软件时,我们已经创建了一个注明日期的许可证文件。然后,我们意识到,甚至没有太多人有兴趣购买我们的软件。然后,我们决定免费赠送。更多的人开始关心至少尝试我们的杰作。最后,我们开源了我们的软件。更多的用户开始使用它。现在我们只是希望这些用户中的一小部分可能会变成付费客户(即购买产品支持或要求定制)。
底线是,如果有人想破解您的软件,他/她无论如何都会这样做。浪费时间试图用这个隐藏的密钥保护它真的值得吗?
如果您负担得起,最好将私钥存储在加密 USB 令牌中。关键是只写,即您可以设置它但不能读取它。令牌在其硬件内部进行加密操作。检索密钥变得非常复杂(如果令牌没有已知的漏洞,那么旧的情况就不是这样)。
您可以使用https://godbolt.org/查看程序集并查看,您可以采取一些措施使其变得更加困难。首先是隐藏符号(所以checkSecret()
不那么明显),其次是使用函数来生成密钥/密码。这个想法是找到将导致绕过任何安全性的代码部分需要更长的时间,因此也不从main
函数中调用它可能是一个好主意。
看两种方法:
这是第一个示例:
#include <string>
#include <iostream>
using namespace std;
string secretKey = "fdasfdasfydsafhidljj3r32R#@f";
bool securityCheck(string key){
if(key==secretKey) cout << "Success!" << endl;
return key==secretKey;
}
int main(){
securityCheck(secretKey);
}
正如您在https://godbolt.org/上更清楚地看到的那样,这将创建以下程序集(使用 gcc 11.2):
secretKey[abi:cxx11]:
.zero 32
.LC0:
.string "Success!"
securityCheck(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >):
push rbp
mov rbp, rsp
sub rsp, 16
mov QWORD PTR [rbp-8], rdi
mov rax, QWORD PTR [rbp-8]
mov esi, OFFSET FLAT:secretKey[abi:cxx11]
mov rdi, rax
call __gnu_cxx::__enable_if<std::__is_char<char>::__value, bool>::__type std::operator==<char>(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)
test al, al
je .L16
mov esi, OFFSET FLAT:.LC0
mov edi, OFFSET FLAT:_ZSt4cout
call std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
mov esi, OFFSET FLAT:_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_
mov rdi, rax
call std::basic_ostream<char, std::char_traits<char> >::operator<<(std::basic_ostream<char, std::char_traits<char> >& (*)(std::basic_ostream<char, std::char_traits<char> >&))
.L16:
mov rax, QWORD PTR [rbp-8]
mov esi, OFFSET FLAT:secretKey[abi:cxx11]
mov rdi, rax
call __gnu_cxx::__enable_if<std::__is_char<char>::__value, bool>::__type std::operator==<char>(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)
leave
ret
main:
push rbp
mov rbp, rsp
push rbx
sub rsp, 40
lea rax, [rbp-48]
mov esi, OFFSET FLAT:secretKey[abi:cxx11]
mov rdi, rax
call std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) [complete object constructor]
lea rax, [rbp-48]
mov rdi, rax
call securityCheck(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)
lea rax, [rbp-48]
mov rdi, rax
call std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string() [complete object destructor]
mov eax, 0
jmp .L22
mov rbx, rax
lea rax, [rbp-48]
mov rdi, rax
call std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string() [complete object destructor]
mov rax, rbx
mov rdi, rax
call _Unwind_Resume
.L22:
mov rbx, QWORD PTR [rbp-8]
leave
ret
.LC1:
.string "basic_string::_M_construct null not valid"
.LC2:
.string "fdasfdasfydsafhidljj3r32R#@f"
__static_initialization_and_destruction_0(int, int):
push rbp
mov rbp, rsp
push rbx
sub rsp, 40
mov DWORD PTR [rbp-36], edi
mov DWORD PTR [rbp-40], esi
cmp DWORD PTR [rbp-36], 1
jne .L58
cmp DWORD PTR [rbp-40], 65535
jne .L58
mov edi, OFFSET FLAT:_ZStL8__ioinit
call std::ios_base::Init::Init() [complete object constructor]
mov edx, OFFSET FLAT:__dso_handle
mov esi, OFFSET FLAT:_ZStL8__ioinit
mov edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev
call __cxa_atexit
lea rax, [rbp-17]
mov rdi, rax
call std::allocator<char>::allocator() [complete object constructor]
lea rax, [rbp-17]
mov rdx, rax
mov esi, OFFSET FLAT:.LC2
mov edi, OFFSET FLAT:secretKey[abi:cxx11]
call std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string<std::allocator<char> >(char const*, std::allocator<char> const&)
lea rax, [rbp-17]
mov rdi, rax
call std::allocator<char>::~allocator() [complete object destructor]
mov edx, OFFSET FLAT:__dso_handle
mov esi, OFFSET FLAT:secretKey[abi:cxx11]
mov edi, OFFSET FLAT:std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string() [complete object destructor]
call __cxa_atexit
jmp .L58
mov rbx, rax
lea rax, [rbp-17]
mov rdi, rax
call std::allocator<char>::~allocator() [complete object destructor]
mov rax, rbx
mov rdi, rax
call _Unwind_Resume
.L58:
mov rbx, QWORD PTR [rbp-8]
leave
ret
_GLOBAL__sub_I_secretKey[abi:cxx11]:
push rbp
mov rbp, rsp
mov esi, 65535
mov edi, 1
call __static_initialization_and_destruction_0(int, int)
pop rbp
ret
您可以看到secretKey
是清晰可见的,函数名称也是如此。
这是第二个例子:
#include <string>
#include <iostream>
using namespace std;
string getSecretKey(){
srand(100);
string chars = "qwertyuioplkjhgfdsazxcvbnm123456789";
string result = "";
for(int i = 0; i < 100; ++i){
result += chars[rand()%chars.size()];
}
return result;
}
string secretKey = getSecretKey();
bool securityCheck(string key){
if(key==secretKey) cout << "Success!" << endl;
return key==secretKey;
}
int main(){
securityCheck(secretKey);
}
这将产生以下程序集:
.LC0:
.string "qwertyuioplkjhgfdsazxcvbnm123456789"
.LC1:
.string ""
getSecretKey[abi:cxx11]():
push rbp
mov rbp, rsp
push rbx
sub rsp, 72
mov QWORD PTR [rbp-72], rdi
mov edi, 100
call srand
lea rax, [rbp-22]
mov rdi, rax
call std::allocator<char>::allocator() [complete object constructor]
lea rdx, [rbp-22]
lea rax, [rbp-64]
mov esi, OFFSET FLAT:.LC0
mov rdi, rax
call std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string<std::allocator<char> >(char const*, std::allocator<char> const&)
lea rax, [rbp-22]
mov rdi, rax
call std::allocator<char>::~allocator() [complete object destructor]
lea rax, [rbp-21]
mov rdi, rax
call std::allocator<char>::allocator() [complete object constructor]
lea rdx, [rbp-21]
mov rax, QWORD PTR [rbp-72]
mov esi, OFFSET FLAT:.LC1
mov rdi, rax
call std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string<std::allocator<char> >(char const*, std::allocator<char> const&)
lea rax, [rbp-21]
mov rdi, rax
call std::allocator<char>::~allocator() [complete object destructor]
mov DWORD PTR [rbp-20], 0
jmp .L16
.L17:
call rand
movsx rbx, eax
lea rax, [rbp-64]
mov rdi, rax
call std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::size() const
mov rcx, rax
mov rax, rbx
mov edx, 0
div rcx
mov rcx, rdx
mov rdx, rcx
lea rax, [rbp-64]
mov rsi, rdx
mov rdi, rax
call std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::operator[](unsigned long)
movzx eax, BYTE PTR [rax]
movsx edx, al
mov rax, QWORD PTR [rbp-72]
mov esi, edx
mov rdi, rax
call std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::operator+=(char)
add DWORD PTR [rbp-20], 1
.L16:
cmp DWORD PTR [rbp-20], 99
jle .L17
nop
lea rax, [rbp-64]
mov rdi, rax
call std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string() [complete object destructor]
jmp .L26
mov rbx, rax
lea rax, [rbp-22]
mov rdi, rax
call std::allocator<char>::~allocator() [complete object destructor]
mov rax, rbx
mov rdi, rax
call _Unwind_Resume
mov rbx, rax
lea rax, [rbp-21]
mov rdi, rax
call std::allocator<char>::~allocator() [complete object destructor]
jmp .L21
mov rbx, rax
mov rax, QWORD PTR [rbp-72]
mov rdi, rax
call std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string() [complete object destructor]
.L21:
lea rax, [rbp-64]
mov rdi, rax
call std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string() [complete object destructor]
mov rax, rbx
mov rdi, rax
call _Unwind_Resume
.L26:
mov rax, QWORD PTR [rbp-72]
mov rbx, QWORD PTR [rbp-8]
leave
ret
secretKey[abi:cxx11]:
.zero 32
.LC2:
.string "Success!"
securityCheck(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >):
push rbp
mov rbp, rsp
sub rsp, 16
mov QWORD PTR [rbp-8], rdi
mov rax, QWORD PTR [rbp-8]
mov esi, OFFSET FLAT:secretKey[abi:cxx11]
mov rdi, rax
call __gnu_cxx::__enable_if<std::__is_char<char>::__value, bool>::__type std::operator==<char>(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)
test al, al
je .L28
mov esi, OFFSET FLAT:.LC2
mov edi, OFFSET FLAT:_ZSt4cout
call std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
mov esi, OFFSET FLAT:_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_
mov rdi, rax
call std::basic_ostream<char, std::char_traits<char> >::operator<<(std::basic_ostream<char, std::char_traits<char> >& (*)(std::basic_ostream<char, std::char_traits<char> >&))
.L28:
mov rax, QWORD PTR [rbp-8]
mov esi, OFFSET FLAT:secretKey[abi:cxx11]
mov rdi, rax
call __gnu_cxx::__enable_if<std::__is_char<char>::__value, bool>::__type std::operator==<char>(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)
leave
ret
main:
push rbp
mov rbp, rsp
push rbx
sub rsp, 40
lea rax, [rbp-48]
mov esi, OFFSET FLAT:secretKey[abi:cxx11]
mov rdi, rax
call std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) [complete object constructor]
lea rax, [rbp-48]
mov rdi, rax
call securityCheck(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)
lea rax, [rbp-48]
mov rdi, rax
call std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string() [complete object destructor]
mov eax, 0
jmp .L34
mov rbx, rax
lea rax, [rbp-48]
mov rdi, rax
call std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string() [complete object destructor]
mov rax, rbx
mov rdi, rax
call _Unwind_Resume
.L34:
mov rbx, QWORD PTR [rbp-8]
leave
ret
.LC3:
.string "basic_string::_M_construct null not valid"
__static_initialization_and_destruction_0(int, int):
push rbp
mov rbp, rsp
sub rsp, 16
mov DWORD PTR [rbp-4], edi
mov DWORD PTR [rbp-8], esi
cmp DWORD PTR [rbp-4], 1
jne .L72
cmp DWORD PTR [rbp-8], 65535
jne .L72
mov edi, OFFSET FLAT:_ZStL8__ioinit
call std::ios_base::Init::Init() [complete object constructor]
mov edx, OFFSET FLAT:__dso_handle
mov esi, OFFSET FLAT:_ZStL8__ioinit
mov edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev
call __cxa_atexit
mov eax, OFFSET FLAT:secretKey[abi:cxx11]
mov rdi, rax
call getSecretKey[abi:cxx11]()
mov edx, OFFSET FLAT:__dso_handle
mov esi, OFFSET FLAT:secretKey[abi:cxx11]
mov edi, OFFSET FLAT:std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string() [complete object destructor]
call __cxa_atexit
.L72:
nop
leave
ret
_GLOBAL__sub_I_getSecretKey[abi:cxx11]():
push rbp
mov rbp, rsp
mov esi, 65535
mov edi, 1
call __static_initialization_and_destruction_0(int, int)
pop rbp
ret
您绝对可以看到您不再天真地扫描代码以查找有趣的字符串,但涉及的内容更多。您必须设置断点并检查不同的值,并尝试从逻辑上查看它以解决它。尽管如此,在此示例中,符号是可见的,这会泄露有关应用程序正在做什么的大量敏感信息。从您所看到的情况来看,隐藏符号会使绕过安全性变得更加困难。
在代码中隐藏密钥不会真正安全。您可能已经注意到 DVD 和大多数软件序列号注册每天都会被黑客入侵。如果你真的想保护你需要使用公钥加密的东西。
你不能,这是不可能的。任何有权访问目标机器的攻击者都可以反汇编您的代码以找到它,或者在目标机器上找到密钥文件等。
确保加密密钥安全的唯一方法是在需要时由用户手动输入。
我认为这是 DVD 和 BluRay 如此迅速破解的最大原因之一。我认为他们能够真正阻止普通人以数字方式复制家庭电影的唯一方法是,如果他们创建了一种未经许可在计算机上使用且只能在经过认证的播放器上使用的媒体。将切断想要在他们的计算机和笔记本电脑上观看电影的市场部分,但可能会在更长时间内停止拥有完美的数字撕裂,并且会阻止普通人能够做到这一点。