Abolishing ENV variables in favor of obfuscated strings?

Hello! This is my first post in the Forum, so I would like to start saying that after 10 years of Ruby I’ve completely moved my personal projects to Crystal in the last 2 years - amazed by the accomplishment of a compiled language on top of LLVM with the Ruby grammar.
Thank you guys, great work!


Since now I usually deploy my binaries direct into my servers - a basic script that builds and pushes it over ssh, like,

# deploy.sh
crystal build source.cr --Dpreview_mt --release -o bin/app;
scp bin/app server_host:~/bin/;
# executable saves an app.pid and restarts automatically
server_host < .~/app/bin;

So, I started to -fearless- leave some string passwords on my local code (APIs keys and database), abolishing the usage of the Linux ENV. And executing the final binary on the server.

How dangerous is this approach? I mean, we know ENV still has its flaws - despite 12factors recommendations… but since it is not an interpreted language, nobody can read the source-code on the server.

But, If someone could -hypothetically- access my server and download the app/bin, could they de-compile the binary and decode my passwords? Any way to obfuscate it?

Would keep it at the shell ENV still be a safer practice?

I appreciate any concerns on this topic, thank you again.

1 Like

I do some research.

# 1.cr

x = "hello world"
y = x
 ╰─ $ cr build 1.cr

 ╰─ $ strings ./1 |grep 'hello world'
hello world
 ╰─ $ cr build 1.cr -o 2 --release

 ╰─ $ strings ./2 |grep 'hello world'
hello world
le 
 ╰─ $ strip 1 2

 ╰─ $ file 1 2
1: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=af16c9cb227e471ece6e3daf01e63fbac884c17f, for GNU/Linux 4.4.0, stripped
2: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=150d19178d228368db44ef1a29493feae6db3d37, for GNU/Linux 4.4.0, stripped

 ╰─ $ strings 1 |grep 'hello world'
hello world

 ╰─ $ strings 2 |grep 'hello world'
hello world

so, i guess it not safe.

Hope others can give more professional advice.

1 Like

well, if someone manages to get into the server, even the shell ENVs are not safe, anyhow.

I did a little research and found this post, so now I do a
$ upx --best app/bin
and have the bin compacted in 35% rate and the strings get messed inside.

though, a “upx -d app/bin” decompress the original file.

I could also have a “class Secret” where the passwords are base64.inverse or something, but I guess a good step-by-step decompilers could find it.

Ideally crystal could include some of,

and a

$ crystal build --release --obfuscate

could do the job. Or any tips on how could I integrate that!?

TL;DR:

  1. " If someone could -hypothetically - access my server and download the app/bin, could they de-compile the binary and decode my passwords?"
    Yes sometimes, depends on the level and determination of the attacker.

  2. “Any way to obfuscate it?”
    If you really want to hardcode credentials: encrypt it(not encode), then decrypt it at runtime. The solution with the lowest time cost.

  3. “Would keep it at the shell ENV still be a safer practice?”
    Not necessarily, it is hard to say which is safer, using environment variables or hard-coding. In contrast encrypted hard-coded credentials are more secure. In a production environment, you may consider using a solution such as HashiCorp Vault.

Hard-coding credentials in executables has never been a best practice. As an example, there are many IoT device vulnerabilities due to the compilation of credential strings into the firmware.

As for the UPX, Base64 (not even secret, if you mean encryption) and LLVM obfuscator you mentioned, they are all “obfuscation”. They do protect your code to a certain extent and increase the cost to the attacker, but don’t expect to be spared in the face of a patient and skilled enough attacker. Against pack tools like UPX there is always a way to get the full memory dump at runtime, which is likely to contain your plaintext credentials; similarly, LLVM obfuscators (which actually target code, not data) and Base64 encoding do not prevent memory reads, after all you always have to decode it at runtime.

My advice is to consider security boundaries. If you trust the security boundaries (users/groups) provided by the OS, then there is nothing wrong with using environment variables, which many PaaS platforms also use to pass plaintext secrets, with encrypted forms kept separately. In this case, your task is to protect the server from being compromised.

1 Like

Hey @rafapolo welcome to the club =)

Obfuscation is not a solution. I wouldn’t even try it.
The program still needs to be able to access the secrets that are embedded into the binary. Whoever has access to the binary will be able to figure that out. They can also just sit and observe how the program deobfuscates and read the cleartext from memory :person_shrugging: Of course it might make it a bit more complicated for an adversary, but it’s hardly an actual barrier.

If you want secrets embedded so they can’t be extracted from the binary, you need to encrypt them. And that then requires a mechanism to provide the decryption key when the program needs it. If that’s only at startup you may do that interactively. It’s definitely more hassle, though.

I’m wondering what kind of thread model you’re actually planning for.
Unless you’re doing a high-risk operation, I don’t think there is much need for such intricate process-level measures. An adversary shouldn’t be able to get on your server and be able to access the binary. Make sure that this doesn’t happen. If they’re on the server, they can do a lot, regardless of what you do with your secrets.

2 Likes

I would argue that embedding secrets directly into a binary is never a good idea, though. Regardless of whether that’s in cleartext, obfuscated or encrypted.

That makes it hard to change in case they got compromised. For updating a secret you need to rebuild the program. That can be a serious risk if for some reason you can’t do that exactly at the point you need to (maybe you have lost access to the source code, it doesn’t compiler for whatever reason, or you don’t have a compiler at hand).
If you store secrets outside the binary, it’s much easier to change them.

2 Likes

And another argument in favour of storing sensitive data in environment variables vs. embedding them in the binary:
If they’re embedded in the binary, the secrets are available in more locations. Anywhere the binary goes. Even if you just build it, copy it to the server (securely) and delete it immediately: The secrets need to be available on your build system whenever you build the program.

Instead, if you store them in environment variables, they only need to be available on the server. You build system doesn’t need to know them! That’s one less vector of attack.

Of course, instead of environment variables, you could also use a file or anything else on the server. The point is, embedding it in the binary likely has more risk.

It might be more convenient for your deployment process. And it’s fine to do it that way if the risk of your build system is acceptable for you. Just be aware of the risks and compromises.

5 Likes

Off topic, not many security researchers have paid much attention to Crystal so far, and no one has looked at how to reverse engineer the binaries generated by Crystal. In contrast there is more reverse engineering of languages like Golang, as there is a lot of notorious malware developed with them (lower development and learning costs than C++). I bet when Crystal’s Windows support becomes more mature, there will be families of Crystal-developed malware and perhaps more obfuscation and anti-obfuscation research will emerge.

3 Likes