A technical analysis of the SALTWATER backdoor used in Barracuda 0-day vulnerability (CVE-2023-2868) exploitation

Summary

SALTWATER is a backdoor that has been used in the exploitation of the Barracuda 0-day vulnerability CVE-2023-2868. It is a module for the Barracuda SMTP daemon called bsmtpd. The malware hooked the recv, send, and close functions using an open-source hooking library called funchook. The following functionalities are implemented: execute arbitrary commands, download and upload files, proxy functionality, and tunneling functionality.

Technical analysis

SHA256: 1c6cad0ed66cf8fd438974e1eac0bc6dd9119f84892930cb71cb56a5e985f0a4

The malware implements hooks on the recv, send, and close functions in a method called cc_init, as shown below:

Figure 1

It obtains the address of the original functions using the dlsym method. The function called get_symbol_by_name returns the address (see Figure 2).

Figure 2

As Mandiant mentioned in their report, the binary uses the funchook hooking library to construct the hooks. The following functions are called: funchook_create, funchook_prepare, and funchook_install.

Figure 3
Figure 4

We will now describe the hooking functions my_recv, my_send, and my_close.

Firstly, my_recv calls the original recv function, as highlighted in Figure 5:

Figure 5

The value stored at the gIsRecvAlready_ptr address should be 0. The process allocates a new memory area that will store the buffer to be received:

Figure 6

The server response is copied to the gConnectedData_ptr address and is overwritten with the “quit\r\n” string. The value stored at gIsRecvAlready_ptr is set to 1, which means that a successful receive operation occurred:

Figure 7

The malware retrieves the IP address of the peer connected to the socket passed as a parameter to the send function using getpeername:

Figure 8

The original send function is called if any error occurred, as shown below:

Figure 9

The process implements a function called CheckRemoteIp, which converts the IP addresses from binary to text form using inet_ntop and then compares them with the value found at the gConnectedAddr_ptr address. It’s worth mentioning that the socket family is also expected to be 0xA (AF_CCITT), which corresponds to CCITT protocols:

Figure 10
Figure 11

In the my_close function, the binary searches for the socket descriptor passed as a parameter in a socket descriptors list:

Figure 12

The malware compares the socket descriptor with the value stored at gConnectedfd_ptr and expects them to be equal; otherwise, it calls the original close method (Figure 13).

Figure 13

A new thread that executes the cc_worker or Connected2Vps functions is created using the pthread_create method:

Figure 14

In the Connected2Vps function, the binary implements a method called OpenConnection, which takes two parameters (IP adddress and port number) that are supposed to indicate the threat actor’s VPS infrastructure:

Figure 15

The process obtains a structure of type hostent for the IP address using gethostbyname and the addrinfo structure using the getaddrinfo method:

Figure 16

The binary creates a new socket and connects to the C2 server using the connect method:

Figure 17

The malware sends the buffer found at the gConnectedData_ptr address to the server via a function call to write (see Figure 18).

Figure 18

The select method is utilized to monitor multiple socket file descriptors, waiting for the read file descriptor to be ready for reading:

Figure 19

Finally, the process reads the server response using the read method:

Figure 20

It creates a new SSL structure and sets the socket descriptor as the input/output for network connections:

Figure 21

The malware expects a 21-byte structure that contains the command to be executed. SSL_read is utilized to read data from the connections:

Figure 22
Figure 23

The first byte extracted from the response corresponds to the backdoor functionalities named “Channels”:

  • 0 (ShellChannel)
  • 1 (DownloadChannel)
  • 2 (UploadChannel)
  • 3 (ProxyChannel)
  • 4 (TunnelArgs)
Figure 24

ShellChannel

The remaining structure received from the C2 server called SHELL has 20 bytes.

The server can specify a command that will be executed on the infected device:

Figure 25

The received command is compared with “exit” and “exit\n”, which means that the process just exits. In any other case, the command is passed to the run_cmd function, as highlighted below:

Figure 26
Figure 27

The popen function is used to run the desired command on the device, and the output is read using the fgets method (Figure 28).

Figure 28

The malware implements a function called MyWriteAll, which calls the SSL_write method. It sends 4 NULL bytes to the C2 server:

Figure 29
Figure 30

DownloadChannel

The remaining structure received from the C2 server called TRANSFILE has 20 bytes.

The process allocates 5MB of memory and expects that TRANSFILE[0:4], which is the file name’s length that will be created, to be <= 1022:

Figure 31

The file name to be created is read using the MyReadAll function, as shown in Figure 32.

Figure 32

The open64 routine is used to create the file on the device:

Figure 33

The file is populated with content received from the C2 server using the lseek64 and write functions:

Figure 34

UploadChannel

The remaining structure received from the C2 server called TRANSFILE has 20 bytes.

The binary expects that TRANSFILE[4:8], which is the file’s length to be exfiltrated, to be <= 5MB. It calls MyReadAll in order to read the file path that will be exfiltrated to the C2 server:

Figure 35

The file’s length is obtained using a function called GetFileSize, which uses the stat64 method to retrieve it. The malware opens the target file via a function call to open64:

Figure 36
Figure 37

The MyWriteAll function is used again to send 21 bytes to the C2 server, as shown in the figure below.

Figure 38

Finally, the target file content is read using the lseek64 and read routines, and then sent to the C2 server:

Figure 39
Figure 40

ProxyChannel

The remaining structure received from the C2 server called PROXY has 20 bytes.

The process allocates 6MB of memory and expects PROXY[12:16] to be 0, as shown below:

Figure 41

The first 4 bytes (PROXY[0:4]) are used to construct the Proxy IP address. The Proxy port number is derived from PROXY[10:12]:

Figure 42

The OpenConnection function presented above uses the recv method to receive data until a new line is received from the socket (see Figure 43).

Figure 43

The binary creates a new SSL structure, connects it with the socket descriptor initially passed as parameter to the my_close function, and initiates the TLS/SSL handshake with the Proxy server:

Figure 44

The process requests a 21-byte structure from the C2 server and sends it to the Proxy server using the MyReadAll and MyWriteAll functions:

Figure 45

As we’ve already mentioned, the first byte determines the command to be executed. A special case occurs if the byte is equal to 2, which leads to a call to the DownloadByProxyChannel routine:

Figure 46

In the DownloadByProxyChannel function, the binary reads a buffer from the C2 server and transmits it to the Proxy server:

Figure 47

Moving forward, the malware can read a 21-byte structure corresponding to a command from the Proxy server and sent it to the C2 server (Figure 48).

Figure 48

TunnelArgs

The remaining structure received from the C2 server called TUNNEL has 20 bytes.

The value TUNNEL[16:20] is expected to be different than 1 and TUNNEL[12:16] could be 0, 1, or 2:

Figure 49

In the first case, the bytes extracted from TUNNEL[0:4] and TUNNEL[8:12] are used to construct two IP addresses, and TUNNEL[4:8] is converted to a port number. The resulting values are stored at the gRemoteAddr_ptr, gRemotePort_ptr, and gConnectedAddr_ptr addresses (Figure 50).

Figure 50

The process sends the command ID (4) and a 20-byte buffer that contains NULL bytes to the C2 server:

Figure 51

In the second case, the binary extracts the IP addresses found at gRemoteAddr_ptr and gConnectedAddr_ptr, and the port number found at gRemotePort_ptr and converts them to integer using the atoi function. The 20-byte buffer that is exfiltrated to the C2 server contains the converted values:

Figure 52
Figure 53

In the third case, the values stored at gRemoteAddr_ptr, gRemotePort_ptr, and gConnectedAddr_ptr are overwritten with NULL bytes (Figure 54).

Figure 54

References

https://www.mandiant.com/resources/blog/barracuda-esg-exploited-globally

https://twitter.com/cyb3rops/status/1666726658806521857

https://linux.die.net/