How do you hide secret keys in code?
EncryptionObfuscationEncryption Problem Overview
I've wondered for some time how some software hides secret keys in such a way that they can't be trivially discovered. Just a few examples:
- DVD Player Software hides CSS keys
- Software with serial numbers/registration codes hides keys/hashes used to validate the serial numbers
Obviously, these programs do something more than just have the key in a byte[], as that would make it easy to steal their keys and generate your own serial numbers, etc.
What sorts of strategies are used to hide these keys so that they can't be found easily?
Encryption Solutions
Solution 1 - Encryption
The reasons those secret keys were so easily discovered is because they were hidden in software.
Avoid hiding secrets in software at all cost - obfuscation will only get you so far. Ask yourself this: How well can I hide a key in software from someone with full access to the disassembly, user mode and kernel mode debuggers, and no day job? It's only a matter of time before it gets cracked.
Solution 2 - Encryption
You just hide the key somewhere, and decrypt it when you need it. Using the key "securely" is the complicated part. Crackers might set a breakpoint to the place where you use the decrypted key and dump it. They might scan your code for patterns which show that you are using a known crypto algorithm (most algorithms have precalculated tables). etc etc.
That's why you need to make the whole software executable hard to analyze. For this you use executable packers, running code in a virtual machine, integrity checks etc. All this is to slow down debugging and modifying your code.
As most people here point out you can't stop anyone, just slow them down. I'd go to a cracker forum and ask there for suggestions about key hiding problematics. They are most likely helpful if you ask nicely.
ps. Public key crypto won't hide the key any better, but it might make it harder (or theoretically impossible) to make a key generator, if you're doing a licensing scheme.
Solution 3 - Encryption
The bottom line is, you can't. See any other comment here for the reasons why. Even encryption software like PGP/GPG stores the keys in a file, and then stridently urges those files to be kept on a flash drive in a safe, or something else secure. Keys stored as part of executable code will be discovered.
In fact, if you're trying to encrypt anything on a client machine that will be decrypted by the client as part of normal operations, that is also a fool's errand. The client machines are inherently insecure, and you can't control what they're going to be able to do to your data.
If you're trying to authenticate, instead, look at Internet based authentication with logins to a server, or some kind of generated KeyCode that is used to validate the software.
Secret keys as part of a Public-Private Keypair should be kept in data files that can be secured. Symmetric keys should be generated on the fly as Session Keys, then discarded. Always assume that anyone who has a Secret or Session key on their computer will be able to discover it, and use it against your intentions.
Read "Applied Cryptography" by Bruce Schneier for more information.
Solution 4 - Encryption
You can't hide a key forever. But you can sure make it hard to find. Some approaches are to encrypt the key in memory, keep multiple copies (perhaps encrypted differently) that are checked against each other, leave dummy copies to be accessed, store the key in some bizarre format, etc. None of them will work if somebody really wants your key, but you can at least dissuade a casual/inexperienced attacker.
Solution 5 - Encryption
You don't always need a key to validate a license.
But ignoring that fact, your key can also be the result of another function. You don't actually store a specific key value, instead you have a function that generates the key on the fly (always the same result). Although not impossible, it's much harder to find since you're no longer looking for a value, but you have to figure out it's an equation.
Solution 6 - Encryption
When we started developing our software, we've created a dated license file. Then, we realized, that not too many people are even interested in buying our software. Then, we decided to give it away for free. A lot more people started to care at least trying our masterpiece. Finally, we've open sourced our software. A lot more users started using it. Now we just hope that a small number of these users might turn into paying customers (i.e. buying prod. support or asking for customization).
The bottom line is, if someone wants to crack your software, he/she'll do it anyway. Is it really worth it to waste your time trying to protect it with this hidden secret key?
Solution 7 - Encryption
If you can afford it, the best is to store the private key in a cryptographic USB token. The key is write only, ie you can set it but not read it. The token does the cryptographic operations internally, in its hardware. It becomes very complicated to retrieve the key (if the token has no known vulnerability, which is not the case with older ones).
Solution 8 - Encryption
You can use https://godbolt.org/ to look at the assembly and see, there are a couple things you can do to make it more difficult. First is to hide the symbols (so checkSecret()
isn't so obvious), and the second to use a function to generate the key/password. The idea is that it will take longer to find the part of the code that will lead to bypassing any security, so also not calling it from the main
function is probably a good idea.
Looking at two approaches:
- an explicitly declared password
- a function that generates the password
Here's an example of the first:
#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);
}
As you can see more clearly on https://godbolt.org/, this creates the following assembly (with 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
You can see that the secretKey
is plainly visible, and the function names are as well.
Here's a second example:
#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);
}
This produces the following assembly:
.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
You can definitely see that you're no longer to naively scan the code for interesting strings, but there's more involved. You'll have to set breakpoints and check different values, and try to look through it logically to solve it. Still, in this example, the symbols are visible, which gives away a great deal of sensitive information about what the application is doing. Hiding the symbols makes bypassing the security more difficult, from what you can see.
Solution 9 - Encryption
Hiding secret keys in code is not going to be really secure. As you may have noticed DVDs and most software serial number registrations get hacked on a daily basis. If you really want to secure something you need to use public key encryption.
Solution 10 - Encryption
You cannot, it is impossible. Any attacker who has access to the target machine would be able to disassemble your code to find it, or find the key file on the target machine, etc.
The ONLY way to ensure that the encryption key is secure, is to have it typed in manually by the user when it is needed.
Solution 11 - Encryption
I think this is one of the biggest reasons that DVD and BluRay were cracked so quickly. I think the only way that they could really stop the average person from being able to digitally copy home movies is if they created a medium that wasn't licensed for use on computers, and could only be used on certified players. Would cut out the part of the market that wanted to watch movies on their computers and laptops, but would probably stop from having perfect digital rips for a little longer, and would stop the average person from being able to do it.