In this post, BlueHalo Software Engineer Adnan Khan takes you through how he developed the malware for Halo U’s Malware Reversing Challenge III, hosted on April 8, 2021.
The malware created for the Malware Reversing Challenge III event on April 8, 2021 was a malicious Linux application masquerading as a useful developer tool. Software developers are an interesting target because they are often granted local administrator accounts to carry out their tasks. This may be on the machine itself or in a virtualized environment. Many developers are also very efficiency-motivated, so a fancy new tool published on GitHub that improves efficiency could be an easy way to establish a social engineering premise to get a developer to run malicious software. Another scenario could be a supply chain attack on a tool used by developers.
While the example malware in this case does not do much in the way of providing legitimate functionality, the core method of attack for initial infection can be built into a tool that does what it is supposed to do.
To quickly iterate while developing the malware, a simple Python build script which wrapped calls to gcc and performed string operations on source code was used.
For the XOR strings, the build script replaces strings which are #defined as plaintext with XOR encoded byte arrays. This allows quickly updating the plaintext values without having to manually re-encode the strings and add them back into the malware’s source code.
Anti-Reverse Engineering Techniques
Ultratool uses several techniques to make it harder to analyze the malware statically and dynamically.
Using the gcc -s flag we can compile a program that has symbols stripped from it. This prevents function names from being viewed in an RE tool. In addition, the strip command can be used to remove individual sections from the binary. For Ultratool the. comment, .note.gnu.property, .note.gnu.build-id, and .note.ABI-tag are stripped out.
XOR Encrypted Strings
First, most strings are encoded with a single byte XOR encryption. Each string has a unique key which also happens to be the least significant byte of a 32-bit integer representing the length of the string (XOR encryption also encrypts the null terminator, so we need to know where the string ends). Using the same value allowed the author to be slightly lazy at the expense of using a fairly weak single byte key.
A simple function reads the value and places the decrypted string in a shared buffer allocated in global memory.
If Ultratool were run in a debugger or using ptrace/strace, then the program will immediately exit. This is done by calling the function to attach a debugger to the program. If a debugger is already attached, then the call returns with an error, and we can detect this in the code. Defeating this is straightforward by hooking the function called within the program or simply modifying the binary to skip those instructions. In addition to the ptrace detection, the locations of functions are checked for the presence of the 0xCC byte. This instruction instructs the program to break, and is added by debuggers. It is easy to bypass because the breakpoint can just be added manually to the first instruction within a function, but it is one more hurdle that a RE would have to get around.
Encrypted Second Stage
The malware’s second stage is encrypted using AES 128-bit encryption. During the Malware Reversing Challenge, the reverse engineer (aka, “The Captain”) was quickly able to identify the presence of likely encrypted content using Ghidra’s entropy view. This works because AES encrypted data typically presents as random data.
A library known as TinyAES was used for encryption/decryption. That library can be found here: https://github.com/kokke/tiny-AES-c.
What is unique about keying for the malware is that the key is not saved within the binary at all. It is in fact read from a known file in the environment. In the case of Ultratool, it is the file “/usr/lib/os-release”. The first 16 characters are read from the file and used as a key. This is not ideal (and a more polished malware would ideally hash the bytes of interest to the necessary key size), however for the purpose of demonstrating keying based on a known value in the environment this will suffice. It also was something that could be pointed out by the reverse engineer during the challenge.
The benefit of using an environment derived key from an attacker’s perspective is that if the malware is intercepted prior to reaching its target, it will make it much harder for analysts to find the key needed to decrypt the second stage, as it is not present in the binary itself. Another strategy is for the first stage to reach out to a command-and-control server and receive the decryption key. This would ensure that a foothold has been established prior to decrypting the second stage.
The malware uses an initial infection vector targeting users that are running bash or zsh as their command line shell. While Ultratool does not include checks to ensure that it only executed its first stage if it is running under bash or zsh, this would be recommended for a more polished version of the initial attack vector.
In this case, what is happening is that individual characters are being written to the standard input of the character device representing the TTY the program is running under. This has the effect of sending characters into the shell as if the user typed the characters themselves. For Ultratool to hide the initial actions the local history is cleared, and the terminal is also cleared. For anyone that has used bash, recently entered commands can be viewed by pressing the up-arrow key. For each shell session this history is maintained until the shell is exited, at which point it is written out to the bash history file. The history -c command will clear the local history, so the user will not be able to see historical commands that were entered.
In addition to being written out to the current terminal, the command is added to the user’s .bashrc or .zshrc file. This covers the case where the user does not immediately use the sudo command, but then exits the terminal. The next terminal that is opened on the desktop will contain the aliases. The command that is sent is aliasing the sudo command. Alias is a specific bash statement (it is also in zsh) which allows replacing one command with any other string (which will be interpreted as a command). This does not require any permissions on the executable itself, so aliases are simply telling the interpreter to swap a command with another. One common alias that is often set as default in some Linux distributions is aliasing ll to ls -la. Using alias, the actual sudo binary is left untouched. This means if a user runs which sudo then the output shows the correct sudo binary on the system.
In this case, sudo is run on a user-controlled binary (which is placed in /tmp/ by the malware) and then sudo is run as normal. This means that when the user runs sudo with a command they intended, the moment they enter their password the malicious executable will be run as root followed by the user’s intended command. At this point the malware has root control over the system and any root persistence techniques can be carried out.
After running as root the malware will also attempt to clean the .bashrc and .zshrc files containing the alias line. The cleanup is a bit buggy as running the malware multiple times will cause multiple lines to be added, but it does attempt to cover its tracks somewhat.
To persist malware between reboots of the system, Ultratool copies itself to another temporary directory which is typically persisted between reboots and this directory is /var/tmp. Copying the second stage to /tmp/ would not work as that is typically tmpfs backed and will be cleared upon reboot. To run the malware, Ultratool adds a root cron job that runs every 5 minutes to execute the second stage. Note that 5 minutes is a short interval, and this was chosen due to the 1-hour timeframe of the Malware Reversing Challenge.
The line added to the root crontab located at /etc/crontab was:
*/5 * * * * root /var/tmp/.entry-3tps-93f8u-rprt
With root access a more sophisticated malware could install a kernel module to truly wreak havoc on the system.
The second stage is a bit simpler than the first. The main task of the second stage is to show a proof of the system being hacked and the malware running as root. Upon starting up, it performs some checks to make sure it is running as root and from the correct directory (if not, it will exit and not attempt to perform any actions). In addition, it creates a bind shell running on port 1337. This shellcode was created using msfvenom on Kali Linux. The “proof of hacking” is done by writing a file hacked HACKED.txt containing the contents of /etc/shadow to the Desktop folders of all users on the system.
For anyone curious, the source code of Ultratool is made available on my GitHub account. Note that this code is provided as-is.
If you would like to build it yourself, simply ensure that gcc is installed on an ubuntu system and run the build.py script! There are no external dependencies beyond having the build tools on the system.