Overview
Static strings in a binary can make the life easier for reverse engineers, be those reverse engineers looking at malware samples, or researchers looking at your binary to understand how it works, for better or worse depending on their intentions. String obfuscation is a common approach used to increase the complexity of reverse engineering efforts and while it is a “security through obscurity” approach, anything that increases the barrier to entry is not a bad start, like for example, not disclosing server versions running on a public facing server. This isn’t a valid way to handle secrets per se, but it does increase the efforts of reverse engineering as strings are often used as landmarks to cross-reference and more easily track down the code that is related to that string.
While string obfuscation isn’t a new concept, the Rust programming language makes it very easy to implement via a crate called litcrypt. This crate is short for literal encryption and makes use of XOR to obfuscate data. While this can be done in most languages such as C, C++, or assembly, Rust makes it really simple with litcrypt where the entire thing is simply a crate and macro. Today, we will create a simple project in Rust with some strings and build it without litcrypt and examine the resulting binary. Then we will implement litcrypt into the project, after which we will examine the resulting binary.
Example Project Without litcrypt
Our example project will be a simple hello world project with another “secret” string that we don’t want to be so easy to discover. Our first build of this project will omit the use of litcrypt and will build the template program for both examples and show how easy this “secret” string is to find in the binary using simple command-line tools such as strings and grep.
Creating the Project
The first thing we need to do is create a project for our example. This is a pretty straightforward process using the Cargo system Rust provides. We will call our example litcrypt_example. To create the project we will run the following command:
cargo new litcrypt_example |
Once that finishes it will create a new directory called litcrypt_example which will contain the Cargo.toml file and a src/ directory containing the main.rs file. If all goes well, we will see output similar to the screenshot below.
At this point we will want to open it in some sort of IDE. In my case, I want to use Visual Studio Code, which I’ve already installed on this Kali VM. We can use the terminal to open it by running the following command from the root of the project folder.
code . |
This should open Visual Studio Code with the current directory opened as the project. If we expand the src directory on the left side Explorer pane, we can open the main.rs file to make it ready for editing. This file will already contain some hello world boilerplate code as shown in the screenshot below.
Adding Our Example Code
While the hello world example is near perfect for the example, we will leave it and add in another line under it with a message we’d like to obfuscate. For this purpose, let’s add the following line under the hello world println code.
println!("This is a secret string we'd rather not make easy to find."); |
The final code should look like the following screenshot.
Building a Release Binary
From within Visual studio Code, we can open a terminal by going to Terminal > New Terminal in the menu bar at the top.
This will open a terminal at the bottom of the IDE. This terminal should already be in the project root directory for its current working directory. To build a release version of the binary, issue the following command:
cargo build –release |
The screenshot below shows what the console output should look like.
After that completes, you should notice that the Explorer pane on the left will now have a new folder called target added in the project directory as shown in the screenshot below. This is where the compiler put the binary it built.
Running Our Release Binary
Now that we have built the binary, let’s give it a test run with the following commands in our IDE terminal in Visual Studio Code:
cd target/release; ./litcrypt_example |
The program behaves as expected, It prints out the hello world string and our secret string to the console as expected.
Examining the Binary with Strings
Now that we have our binary and have given it a test run, let’s examine it with the strings command. Since the output of strings alone would generate a lot of noise, we will use grep to focus on the string we want hidden by using the following command:
strings ./litcrypt_example | grep “secret” |
As shown in the screenshot below, this string appears clear-text in our binary and is really easy to discover with minimal effort.
Example Project With litcrypt
Now that we have a base project that has a string we’d like to obscure, let’s add and implement litcrypt into it. This is a fairly straightforward process and can be done on our existing project with minimal effort.
Adding the litcrypt Crate
The first thing we need to do is add the litcrypt crate to our Cargo.toml file. This file acts as a manifest and litcrypt is a crate (think module if you are from a Python background or package if NodeJS is your background). In our IDE we need to edit the Cargo.toml file and add the following line under the Dependencies header.
litcrypt = "0.3"
The screenshot below shows our Cargo.toml file after making this edit with a red arrow calling out the added line. The [dependencies} header was already present.
By adding this line to the Cargo.toml file, Cargo now knows that this external crate is needed to build the project. Next time we go to compile this code, Cargo will automatically download this crate and link it in the project for us.
Implementing litcrypt in Our Code
Now that we added the dependency for litcrypt to the Cargo.toml file, we can add it into our code. We first need to add the following code to the top of the main.rs file, above the main() function:
#[macro_use] extern crate litcrypt; use_litcrypt!(); |
This imports the litcrypt crate and the call to the use_litcrypt!() macro will initialize the litcrypt crate for us. At this moment, your code should look like the following screenshot.
Next, we need to apply litcrypt to the string we want to obfuscate. This is simply accomplished by wrapping the string in the lc!() macro. All we need to do is modify the following line of code
println!("This is a secret string we'd rather not make easy to find."); |
The modification we will make will be to wrap the string in the lc!() macro. However, we will need to also modify this to be a format string so that it can be dynamic. This is due to the first argument of println!() requiring a string literal, which the litcrypt string will not be. It will be run through XOR at runtime. If we tried to do this like below:
// Don’t do this, it will error out! println!(lc!("This is a secret string we'd rather not make easy to find.")); |
We would get a compiler error like shown below.
However, error messages in Rust are amazing and it tells you exactly how to fix it by making it a formatted string and even shows the line you should use to get the desired result by using a blank literal with a placeholder (“{}”) as the first parameter passed to println!() which will use the second dynamic value (the litcrypt string after it’s been decoded). So we will use the following line of code instead.
println!("{}", lc!("This is a secret string we'd rather not make easy to find."));
At the end our code should look like the following:
As you may have noticed, we only did this to one of the strings. This was intentional. The hello world string is not wrapped in the litcrypt macro and as we will see when we examine this binary later, this string will remain clear-text.
Specifying the Key for litcrypt
Now that we added this to our code. We need to specify an encryption key. If we don’t we will get an error message when we try to build the project like the screenshot below.
Again, Rust is helpful with its error messages and provides help that explains that the LITCRYPT_ENCRYPT_KEY environment variable is not set. This environment variable is where the litcrypt encryption key is stored and must be set for the build process to use. This key can be any string of your choice. This can be accomplished by using the export command in the build terminal before issuing the build command as shown below:
export LITCRYPT_ENCRYPT_KEY="This is an litcrypt example key"; |
The value of the key can be changed to anything you want it to be. Once it is set we should be able to build the project like normal. Be aware that this time Cargo will fetch the litcrypt crate, so the first build will take a little longer since it has to fetch this from the internet. The screenshot below shows the LITCRYPT_ENCRYPT_KEY environment variable being set and the release build running successfully.
Running Our Release litcrypt Binary
Now that we have our litcrypt version of the project, let’s run it with the following commands:
cd target/release; ./litcrypt_example |
This should produce the output we see in the screenshot below, showing our binary is running the same way as before showing the string is functioning like we want it to within the program.
However, the devil is in the details, as we shall see in the next section!
Examining the litcrypt Binary
We built this project with litcrypt, let’s see how that is working out for us. As we saw in the test run, it worked just fine within the program at runtime, we saw it output the hidden message to the console. But is this string still in the binary in clear-text? We can use the same command as before when we built this project without litcrypt
strings ./litcrypt_example | grep “secret”
As shown in the screenshot below. This string is no longer found clear-text in our binary!
Now, if you recall, we didn’t use litcrypt on the hello world string, which can be easily found in the binary using strings and grep as shown in the screenshot below.
And just as a test, some obfuscation methods for strings will flag as a virus to malware scanners. We can upload our binary to VirusTotal and see if Litcrypt’s presence alone will annoy an AV scanner. However, the results show that litcrypt’s decoder function doesn’t bother them at all.
This makes sense as it isn’t inherently malicious, but decoders written in other languages that have been used in malware tend to raise flags with AV on the presence of that function alone, likely due to being heavily reused in by several malware authors and this decoder function is usually easy to craft a signature for once discovered. Malware will always be a game of cat and mouse.
Conclusion
Hopefully this demonstrates how easy it is to implement string obfuscation in Rust. This sure beats having to write your own XOR string decryptor function and encrypting the strings outside the source code and pasting encrypted hex-encoded strings in the source code or trying to build your own hacky compiler macros. It’s not hard to see where this could be useful in a project if you want to hide strings.
For example, string obfuscation is commonly used by malware authors to make it harder to find the C2 server it intends to connect to or various command or API paths. Another example might be trying to obscure strings related to interesting functionality of your code to raise the barrier to entry on reverse engineering efforts. Either way, it’s nice to see that this is easy to implement in Rust, which has large binaries that are already difficult to reverse engineer as it is. This litcrypt_example binary clocks in at 3.7 MB in size, which is pretty massive for just printing out two lines to the console.
If you’re interested in security fundamentals, we have a Professionally Evil Fundamentals (PEF) channel that covers a variety of technology topics. We also answer general basic questions in our Knowledge Center. Finally, if you’re looking for a penetration test, professional training for your organization, or just have general security questions please Contact Us.