1. Overview

Code obfuscation is the process of transforming source code into a form that is difficult for humans to read, understand, or modify. We can use obfuscation for a variety of purposes, including the following:

  • Protecting intellectual property or hiding our algorithms
  • Hiding sensitive information, such as passwords, keys, or URLs
  • Reducing code size, in the case of minification

In this tutorial, we’ll look at a different way to obfuscate a Bash script.

2. Precautions

Obfuscation has some drawbacks and limitations. We need to pay attention to some important issues:

  • Increased complexity → Obfuscated code is hard to debug
  • Side effects → Obfuscation can introduce bugs that are painful to find and fix
  • Reduced maintainability → Obfuscated code has no documentation and is difficult to edit
  • No security benefit → A skilled attacker can easily de-obfuscate the code

It’s important to weigh the pros and cons. In general, if we can avoid obfuscation, it’s better.

3. Example Bash Script

In the upcoming examples, we’ll use the following sample Bash script from the official Bash Reference Manual. Let’s save it as animal.sh:

#!/usr/bin/env bash

# Taken from the GNU Bash Reference Manual
echo -n "Enter the name of an animal: "
echo -n "The $ANIMAL has "
case $ANIMAL in
    horse | dog | cat) echo -n "four";;
    man | kangaroo ) echo -n "two";;
    *) echo -n "an unknown number of";; # ‘*’ defines the default case
echo " legs."

Compared to the original example, we’ve added two comments to check whether they’ll be obfuscated.

4. Minifying Scripts

Reducing code size is the goal of minification. In general, minification tools remove comments, whitespace, and line breaks. They also use shorter variables, function names, and operators. As a result, the code is harder to read and modify. It’s not actual obfuscation, but it’s similar. So let’s look at two utilities that can minify our Bash scripts.

4.1. minifier.py

minifier.py is a Python script that attempts to remove all comments and as many spaces, tabs, and line breaks as it can. Let’s note that the script is only compatible with Python 2.x:

$ wget -O 'minifier.py' 'https://raw.githubusercontent.com/precious/bash_minifier/master/minifier.py'
$ python2 minifier.py ./animal.sh > ./animal_min.sh

The result is a single-line script that is smaller than the original one:

$ wc -c animal.sh 
315 animal.sh
$ wc -c animal_min.sh 
205 animal_min.sh
$ cat animal_min.sh 
echo -n "Enter the name of an animal: ";read ANIMAL;echo -n "The $ANIMAL has ";case $ANIMAL in horse|dog|cat)echo [...]

There is no doubt that the code is harder to read.

In this and the following examples, it’s crucial to verify that the obfuscated code works as expected. We’ve always done this check, though we don’t always report it for the sake of brevity.

4.2. bash_obfus.pl

bash_obfus.pl is another Bash minification tool written in Perl. It shows the available options with the -h flag:

$ wget -O 'bash_obfus.pl' https://raw.githubusercontent.com/Aralhach/bashobfus/master/bash_obfus.pl
$ chmod +x ./bash_obfus.pl
$ ./bash_obfus.pl -h
    ./bash_obfus.pl -i <input_file> -o <output_file> [-V <new_var_string>] -C -F
        <input_file>	is the shell script you want to obfuscate
        <output_file>	is where you want to save the obfuscated script
        <new_var_string>	is an optional argument that defines what all variables will be changed to.
            The default is 'a', which means all variables will be changed to a0,a1,a2,a3,...
        -C	is an option to clean out full line comments and blank lines.
        -F	is an option to flatten out the code (remove indentations)

Let’s try all the options:

$ ./bash_obfus.pl -i animal.sh -o animal_min2.sh -V x -C -F

The resulting minification is less effective than the previous one, as it’s not on a single line and only removed comments – except the shebang – and indentation:

$ cat animal_min2.sh 
#!/usr/bin/env bash
echo -n "Enter the name of an animal: "
echo -n "The $ANIMAL has "

Also, the $ANIMAL variable was not replaced with $x0, as required by the -V x option, because it’s all uppercase. In fact, variable substitution only works with lowercase ones to avoid replacing system variables like $PATH.

5. Obfuscating Scripts

We’ll now look at two ways other than minification to make code hard to read. In these cases, it’s true obfuscation.

5.1. base64

The base64 utility performs Base64 encoding, which is a method of converting binary data into a sequence of ASCII characters. It’s often used to encode images, documents, and other types of binary data that aren’t natively supported by text-based formats.

We can also use Base64 encoding as a code obfuscation technique:

$ echo '#!/usr/bin/env bash' > animal_b64.sh
$ echo "bash <(echo '$(base64 animal.sh)' | base64 -d)" >> animal_b64.sh

This way, animal_b64.sh contains the code from animal.sh in a hidden form:

$ cat ./animal_b64.sh 
#!/usr/bin/env bash
bash <(echo 'IyEvdXNyL2Jpbi9lbnYgYmFzaAoKIyBUYWtlbiBmcm9tIHRoZSBHTlUgTWFudWFsCmVjaG8gLW4g
ZmF1bHQgY2FzZQplc2FjCmVjaG8gIiBsZWdzLiIK' | base64 -d)

However, anyone who wants to analyze or modify the code can simply decode the Base64 string and get the original source code.

5.2. makeself

makeself creates a Bash script containing a compressed, encrypted or encoded self-extracting archive. So, we can include our animal.sh in a self-extracting archive, which will be automatically extracted to a temporary directory, executed, and then removed after execution.

The default options are sufficient to make the original code unreadable by inspecting the self-extracting archive:

$ mkdir obfuscated
$ cp animal.sh ./obfuscated
$ makeself ./obfuscated animal_obf.sh "My obfuscated script" ./animal.sh
Adding files to archive named "animal_obf.sh"...
Self-extractable archive "animal_obf.sh" successfully created.
$ file animal_obf.sh
animal_obf.sh: POSIX shell script executable (binary data)
$ wc -l animal_obf.sh
714 animal_obf.sh

Let’s take a look at animal_obf.sh, reporting only the beginning since it’s 714 lines long:

$ cat animal_obf.sh 
# This script was generated using Makeself 2.4.5
# The license covering this archive and its contents, if any, is wholly independent of the Makeself license (GPL)
ARCHIVE_DIR=`dirname "$0"`
label="My obfuscated script"

At the end of animal_obf.sh, there are unreadable characters which are the binary content of animal.sh, compressed with gzip by default:

$ xxd animal_obf.sh
000049a0: bb5f cbdd 735b 35b4 1bd1 5796 b3c3 1e01  ._..s[5...W.....
000049b0: 556a 617b 81ef 0100 0000 0000 0000 0000  Uja{............
000049c0: 0000 0000 0000 0000 fca5 778e 48f2 3200  ..........w.H.2.
000049d0: 2800 00                                  (..

The following is what happens when we start animal_obf.sh:

$ ./animal_obf.sh 
Verifying archive integrity...  100%   MD5 checksums are OK. All good.
Uncompressing My obfuscated script  100%  
Enter the name of an animal:

There are many other options, such as changing the type of compression or encoding, or encrypting the script with a password.

6. Compiling Scripts

Compiling a Bash script means converting it into a binary executable. We can do this with the following two tools, which first translate a Bash script into C code and then compile it using a C compiler. As a result, the original source code is hidden.

6.1. shc

shc creates executable binaries from shell scripts. It doesn’t support the animal.sh‘s shebang, which is #!/usr/bin/env bash. So we created a new animal2.sh file that starts with #!/bin/bash.

If we want to distribute our compiled script, we’ll need to use the -r option:

$ shc -r -o animal.bin -f animal2.sh
$ file animal.bin 
animal.bin: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter [...]
$ ./animal.bin 
Enter the name of an animal:

Out of curiosity, we can take a look at the animal2.sh.x.c file generated by shc. It contains the C code into which animal2.sh was translated.

6.2. obash

obash is an alternative to shc. In the README.txt, the author explains why obash should be better than shc, but we won’t go into that here. Unlike all the other tools we’ve seen so far, which were very easy to install via the package manager or to download, in this case we have to compile obash:

$ mkdir obash
$ cd obash/
$ wget -O 'obash.zip' 'https://github.com/louigi600/obash/archive/refs/heads/master.zip'
$ unzip -j obash.zip
$ make clean
$ make
$ ./obash -h
./obash [-c] [-h] [-o <value>] [-r] [-s]  <input filename>
-c          : Cleanup intermediate c files on success.
-h          : How this help message.
-o <value>  : Output filename.
-r          : Create a reusable binary.
-s          : Create a statc binary.

Like shc, obash needs the -r option to build redistributable binaries:

$ cd
$ ./obash/obash -o animal.run -r animal.sh
Output filename will be: animal.run
input filename: animal.sh
Creating reusable intermadiate c file
Created animal.sh.c

[... compilation produces some warnings that we can ignore ...]

Compiling animal.sh.c ... done
Output filename: animal.run

$ file animal.run 
animal.run: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter [...]
$ ./animal.run 
Enter the name of an animal:

If we are curious, we can inspect the C code in animal.sh.c, which is substantially different from animal2.sh.x.c, showing that obash and shc use two different approaches.

7. Conclusion

In this article, we looked at different approaches to obfuscating the code of a Bash script, distinguishing three main categories of tools:

  • minifiers → minifier.py & bash_obfus.pl
  • obfuscators → base64 & makeself
  • compilers → shc & obash

However, obfuscation can cause more problems than benefits. Let’s keep in mind that it provides no real protection for the scripts, since their original code can be easily discovered by a skilled person.

Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.