Tersine mühendislik

Inside Symbiote: malware deep dive

Symbiosis is a close and long-term biological interaction between two different biological organisms, either mutual or parasitic. In this case, this userland-level rootkit is named “Symbiote”. Due to its parasitic behavior in the Linux kernel ~ infecting every active process ~ I could say that the name is a nice fit.

It was first discovered in June 2022 by Joakim Kennedy, Security Researcher at Intezer, and the BlackBerry Threat Research & Intelligence team. What makes rootkits interesting is how they behave stealthily when an administrator starts a packet-capturing tool. It’s essentially hooking pcap_loop() and pcap_stats() functions to have control over captured packets and modify the filtered packet count. Detailed explanation is present in the following sections.

Today, we are going to deep dive into one of the first samples:

Hash

File Name

ec67bbdf55d3679fca72d3c814186ff4646dd779a862999c82c6faa8e6615180

search.so

How does it infect in the first place?

Unlike other executable malware, this is a Linux shared library (Shared Object) that can be loaded into processes. `LD_PRELOAD` is a Linux environment variable where you can specify a shared library to be loaded before all others. Symbiote uses this variable to import itself into binaries before they even get executed and override existing functions. Exported functions are shown in the image below:

Table of exported functions from the malware's shared library, listing names like execve, pam_authenticate and pcap_loop.

Overview

This sample seems to be:

  • Hiding itself from the process, directory and the ldd list.
  • Filtering malicious network packets from specific ports and domains.
  • Resolving DNS and communicating with the C2 server.
  • Stealing passwords from the PAM authentication hook.
  • Allows PAM authentication with a specific password.
  • Downloading and executing a backdoor script from the C2 server.
  • Implementing a keylogger.

Linker Stealth

As shown below, the malware hooks the `execve` function and checks if the hijacked process has `LD_TRACE_LOADED_OBJECTS` environment enabled. If set, it causes the program to list its dynamic dependencies, as if run by ldd(1), instead of running normally. So Symbiote prevents this by providing a fake tracing function. The original `execve` function is preserved when this option is disabled.

Commonly, it uses the RC4 encryption algorithm to hide hard-coded strings.

Decompiler view of the hooked execve function, RC4-decrypting strings and checking for LD_TRACE_LOADED_OBJECTS.

Directory/Process List Stealth

As notated in the readdir() documentation, it is used to get the pointer to a `dirent` structure representing the *next* directory entry in the directory. It first checks if the directory entry is coming from a process or a file. Then iterate through entries until it’s not in the hidden list. Simply skipping over hidden entries and providing the next valid one instead.

Decompiler view of the hooked readdir function, filtering directory entries through hidden_proc and hidden_file checks.

Looking at the `hidden_proc` function, it inspects the file name if it is similar to a command line process file (`/proc/PID/cmdline`). This read-only file holds the complete command line for the process, unless the process is a zombie.

Decompiler view of hidden_proc opening /proc/PID/cmdline after RC4-decrypting the path template.

Reading its content, it looks for a string related to strace, a command to intercept and record system calls, which reveals the malicious library operations. Then, going through the list of blacklisted paths, effectively skipping over the process in this case.

Decompiler view of hidden_proc reading cmdline content and comparing it against RC4-decrypted strace markers.

Additionally, applies the same check with the `/proc/PID/status` file that holds memory usage and status information.

Decompiler view of hidden_proc checking the /proc/PID/status file against the blacklisted process list.

Blacklisted processes are:

  • certbotx64
  • certbotx86
  • javautils

Unlike `hidden_proc`, `hidden_file` is very straightforward. There is a total of 6 entries.

Decompiler view of the hidden_file function comparing a filename against six RC4-decrypted blacklisted entries.

  • apache2start
  • apache2stop
  • profiles.php
  • 404erro.php
  • javaserverx64
  • javaclientex64

PCAP Hook and Packet filtering

PCAP Loop is a special function that is called from packet capturing tools and as shown below, the malware hooks it by replacing it with its own loop function.

Decompiler view of the hooked pcap_loop function resolving the original symbol and calling it with a check_pkt filter.

If we take a look at the check packet function, it’s filtering packets based on some conditions, as shown below.

Decompiler view of check_pkt filtering UDP DNS packets by TTL, IPv4 header and a hardcoded domain string.

If all these conditions are met, check the domain `bancodobrasil.dev` in the packet content bytes. If so, effectively dropping the packet and not letting the original loop handle it. This way, the malware succeeds to communicate with the C2 server without any detections.

Along with the `pcap_loop`, it hooks the `pcap_stats` function to hide the *packet count* as well. After calling the original function, it subtracts the filtered packet count and returns the valid value.

Decompiler view of the hooked pcap_stats function subtracting the filtered packet count from the real total.

PAM Authenticate Hook

The malware stealthily hooks the pam_authenticate() function that is used for authentication across important services like SSH (Secure Shell). If the password is `suporte42cbb32`, allowing the service to authenticate successfully as shown below. This gives a hidden access to the malicious actor.

Decompiler view of the hooked pam_authenticate function returning success for a hardcoded RC4-decrypted password.

If the password is something else, the malware simply saves passwords to the file `/usr/include/certbot.h` and sends it to the C2 server through DNS.

Decompiler view of pam_authenticate logging captured credentials to /usr/include/certbot.h before sending them over DNS.

Bash Script Backdoor

From the previous screenshot, you can see the `check_backdoor()` function. Let’s have a closer look at it.

Firstly, it’s forking the process (see fork()). And if there is a currently forked process, it just waits for it to finish with `waitpid(pid, 0LL, 0)`. Otherwise, create a new fork again. We can also see that it is pointing all available file descriptors to `/dev/null`. This is to prevent exposing the command output to the user ~ Main file descriptors are `STDIN`, `STDOUT` and `STDERR`.

Decompiler view of check_backdoor double-forking and redirecting file descriptors to /dev/null.

After setting everything up, it executes the script from the server and terminates.

Decompiler view of check_backdoor decrypting the C2 domain dev21.bancodobrasil.dev and calling execute_dns_code.

While downloading the script, it uses `ID[.]dev21[.]bancodobrasil[.]dev` format as the domain and again `suporte42bb32` for the RC4 decryption key.

Decompiler view of execute_dns_code building an ID-based subdomain and requesting a signature before downloading the script.

For every packet chunk, it modifies the subdomain with such a format `NUMBER[.]ID[.]dev21[.]bancodobrasil[.]dev`.

Decompiler view of download_script fetching base64 chunks over DNS TXT records and verifying them with ed25519_verify.

After getting the text and decoding it with Base64, it validates the content using the public key ~ determining whether it is coming from a valid C2 server or not.

Finally, forking the process again and closing file descriptors, followed by the bash shell spawn. It interacts with the shell through the `STDIN` file descriptor, afterwards, directly writing the command into the shell. Then terminates itself.

Decompiler view of prepare_pipe forking a process and executing bash to run the downloaded backdoor script.

Keylogger

To collect keys, it seems to hook the read() function and only call the `keylogger()` function when the hook mode is `2`.

Decompiler view of the hooked read function calling check_rw_hook and invoking the keylogger when the hook mode is 2.

As shown below, if the read() request comes from services like SCP (Secure Copy) or SSH (Secure Shell), it returns 2, which triggers the keylogger function.

Decompiler view of check_rw_hook reading /proc/self/exe and matching it against ssh, scp and sh binaries.

In the keylogger function, it first checks if the current process has a terminal interface with the isatty() function. And then copying the buffer from the original read() input.

Decompiler view of the keylogger function checking isatty and copying the read buffer into its own log buffer.

Finally, setting up the log format and storing the input buffer along with the command line logs to the same file `/usr/include/certbot.h`. As usual, the updated log file is sent through the DNS again.

Decompiler view of the keylogger formatting captured keystrokes, saving them to certbot.h and sending them over DNS.

When it is done storing credentials, it cleans up leftover buffers and disables the hook to avoid duplicate data.

Decompiler view of the keylogger clearing its buffers and disabling the hook after the second call.

Conclusion

Symbiote is one of the most evasive Linux malware. It’s best to act on improving security and the detection of such malware before it infects critical infrastructures.

References