Sealed with a KYSS: Inside an Android Banking RAT
How a simple lipstick icon hides a sophisticated banking trojan
After wrapping the analysis on my previous sample, another one came in. It called itself KYSS, and the launcher icon was a lipstick-stained kiss mark, the kind of lure which gets people's attention.
Tapping it, however, didn't open an app. Instead, it opened its page in the Android Settings, the one which shows the permissions, battery usage and other details. An open button sat on the top, expecting to be tapped by a confused user.
That's when the malware comes to life.
I started with MobSF for a static pass, examined with JADX, and then spun up a rooted AVD and instrumented it with Frida. The deeper I got, the more interesting it became.
Analysis revealed it to be an Android banking RAT. It had capabilities to perform overlay attacks against 19 targets across Japan and Latin America, and popular crypto apps, accessibility service-based automation that grants itself extensive permissions, silent gallery and contact exfiltration, and an interface for operators to issue commands. Pivoting from the C2 infrastructure helped me find what appears to be a C2 control panel on shared infrastructure.
This post is a walkthrough of my analysis, from static properties, activation flow, crypto hooks, C2 payload interception and finally the C2 panel discovery.
First Look
The APK comes in at a tiny 6.1 MB. MobSF shows that it was V3 signed with every field in the certificate set to Unknown. No legitimate app ships like this.
Subject: CN=Unknown, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=Unknown
MobSF failed to extract basic metadata, package name, main activity, version, and JADX showed no AndroidManifest.xml. The manifest appears to use non-standard binary encoding: valid enough for the Android runtime to install and launch it, but enough to confuse or defeat common static analysis tools.
Opening up the app in JADX, the filenames look randomly generated. Even the usual AndroidManifest.xml file seems to be obfuscated and buried amongst this pile of various files.
Due to heavy obfuscation, it was impractical to try and prod further with static analysis. The obvious next step was dynamic analysis.
Analysis Environment
For the dynamic analysis, I set up a rooted Android Studio AVD running Android 10 x86_64. Frida was installed for dynamic instrumentation.
Activation Flow and Instrumentation
Upon opening the app, a screen is presented to the user which requests them to grant accessibility permissions to the app. Before granting the permission in the emulator, I attached my networkHook.js script to it to identify the C2 endpoint.
Java.perform(function() {
var Socket = Java.use('java.net.Socket');
Socket.$init.overload('java.lang.String', 'int').implementation = function(host, port) {
console.log('[*] Socket connect: ' + host + ':' + port);
return this.$init(host, port);
};
var URL = Java.use('java.net.URL');
URL.openConnection.overload().implementation = function() {
console.log('[*] URL.openConnection: ' + this.toString());
return this.openConnection();
};
});
As it is seen in the screenshot, when hooking into the app while it "optimizes" the phone, it phones home at the /api/device_log endpoint, probably reporting back the device info.
For that, we will have to hook into the method and see the payload while it is constructed.
The network hooks also showed the app using the bot API to send an initial message informing the operator of a successful infection on another device, although throughout my analysis the Telegram Bot API consistently returned a 429 Too Many Requests error.
While it might seem intuitive to hook directly into the same method and dump the payload directly, we have to remember that it uses an HTTPS connection, meaning the payload is encrypted before being handed off.
So we go a level lower. The Crypto classes in Java use a doFinal() method once all parameters have been configured. Hooking at that step to dump the plaintext is a good way forward. Using the cryptoHooks.js script as follows:
Java.perform(function() {
var Cipher = Java.use('javax.crypto.Cipher');
Cipher.doFinal.overload('[B').implementation = function(bytes) {
console.log('[*] Cipher.doFinal input: ' +
Java.use('java.lang.String').$new(bytes));
var result = this.doFinal(bytes);
return result;
};
var OutputStream = Java.use('java.io.OutputStream');
OutputStream.write.overload('[B').implementation = function(bytes) {
var str = Java.use('java.lang.String').$new(bytes);
console.log('[*] OutputStream.write: ' + str);
return this.write(bytes);
};
});
And as suspected, the app is exfiltrating the device info such as the charging status, the amount of memory available in the device, and interestingly, if the device has granted battery optimization exemption to the app, if the accessibility service is granted, the SDK, the make and model of the device and an ID.
The garbled text which also shows up seems to be the encrypted payload being sent by the server to the device, and we will have to improve on our script to be able to find out what the app is doing with the accessibility service as well.
boss.js The all in-one script
After several iterations, I finally made a boss.js, which would hook into multiple methods of interest, such as the Crypto methods, the Accessibility Service, the SecretKeySpec (since the app encrypts its payloads before sending over the network). With a little help from Claude, this is the script I came up with, available on Gitlab Snippets
The combined hooks revealed substantially more about the malware's behavior.
As soon as the app boots up, it reads the following HTML page, which is the accessibility granting flow shown to the user and presents it on the screen.
[NET] BufferedReader.readLine: function applyLang(lang){
[NET] BufferedReader.readLine: var t=i18n[lang]||i18n.en;
[NET] BufferedReader.readLine: var appName="App";
[NET] BufferedReader.readLine: try{appName=Android.getAppAcc()||Android.getAppLabel()||"App";}catch(e){}
[NET] BufferedReader.readLine: document.getElementById("title").textContent=t.title;
[NET] BufferedReader.readLine: document.getElementById("subtitle").textContent=t.subtitle;
[NET] BufferedReader.readLine: document.getElementById("stepSettings").textContent=t.stepSettings;
[NET] BufferedReader.readLine: document.getElementById("stepAccessibility").textContent=t.stepAccessibility;
[NET] BufferedReader.readLine: document.getElementById("stepEnable").textContent=t.stepEnable;
[NET] BufferedReader.readLine: document.getElementById("infoText").textContent=t.infoText.replace("{app}",appName);
[NET] BufferedReader.readLine: document.getElementById("btnText").textContent=t.btnText;
[NET] BufferedReader.readLine: document.getElementById("appName").textContent=appName;
[NET] BufferedReader.readLine: if(rtlLangs.indexOf(lang)!==-1){
[NET] BufferedReader.readLine: document.documentElement.setAttribute("dir","rtl");
[NET] BufferedReader.readLine: }
This Javascript function in the page stood out. It checks the locale of the device and changes the contents of the page to the matching language. This is indicative of a multi-region malware.
After this, the hooks on the SecretKeySpec fired, revealing the encryption class instantiated by the class to encrypt its requests.
[KEY] algorithm: AES
[KEY] key (b64): MDYyM1UyNUtUVDNZTzhQOQ==
[KEY] key (hex): 303632335532354b545433594f385039
[KEY] algorithm: AES
[KEY] key (b64): MDYyM1UyNUtUVDNZTzhQOQ==
[CIPHER] getInstance: AES/ECB/PKCS5Padding
[CIPHER] init mode: DECRYPT
[CIPHER] key length: 128 bits
The attacker chose a static key ( this was verified by running the malware multiple times ), and AES-ECB padding. ECB mode is deterministic: identical plaintext always produces identical ciphertext. This could potentially be used later to easily be able to decrypt any encrypted data flowing through this channel.
The app was reporting back a lot of information quite often. While I had suspected the use of websockets from the beginning, this log confirmed it.
[CIPHER] outgoing plaintext: {"action":"diag","type":"enc","data":{"type":"conn","msg":"binary_ws_connected","ts":1783684247430,"deviceId":"c86ede892b00d6a6"}}
[CIPHER] ciphertext (b64): T3Wv5ySb5IJKNT6FOJF30mqEgk+OZMNb7KhC+YCzQjLnhCyxDuM+s2ogVRmHY9Zp+miHE4hZVDIS
0TuZMualaEmPMZ1y7TzoXoovR7YaJVlGrkaHQSov8iBlPe7fcA2m9Delt5vjFIpZOcKTjR3QpNym
kktmqg7hr7zIUCYxbWxY1+xpwuct8IDxJVsqi8KZ
The server was also sending down huge payloads, which were encrypted with the same key, and base64 encoded on top. It contained a list of various OEM-specific settings strings it would click on for granting itself device admin access.
This payload contained the strings for the following OEMs:
com.vivo.permissionmanager - vivo (Funtouch OS)
com.samsung.android.permissioncontroller - Samsung (One UI)
com.huawei.systemmanager - Huawei (EMUI)
com.miui.securitycenter - Xiaomi (MIUI/HyperOS)
com.miui.packageinstaller - Xiaomi (MIUI/HyperOS)
com.lbe.security.miui - Xiaomi (MIUI/HyperOS)
com.coloros.safecenter - OPPO (ColorOS)
com.oppo.safe - OPPO (ColorOS)
com.transsion.phonemaster - Transsion (TECNO/Infinix/itel)
com.google.android.packageinstaller - AOSP/Generic Android
com.android.packageinstaller - AOSP/Generic Android
com.android.settings - AOSP/Generic Android
com.samsung.android.settings - Samsung (One UI)
com.samsung.android.lool - Samsung (One UI)
com.samsung.android.sm - Samsung (One UI)
com.miui.powerkeeper - Xiaomi (MIUI/HyperOS)
com.coloros.oppoguardelf - OPPO (ColorOS)
com.heytap.safecenter - OPPO/OnePlus/realme (OPlus)
com.oplus.safecenter - OPPO/OnePlus/realme (OPlus)
com.oneplus.security - OnePlus (OxygenOS)
com.iqoo.secure - iQOO (Funtouch OS)
com.vivo.abe - vivo (Funtouch OS)
com.hihonor.systemmanager - HONOR (MagicOS)
com.asus.mobilemanager - ASUS (ZenUI)
com.motorola.batterycare - Motorola (My UX)
com.evenwell.powersaving.g3 - Nokia/FIH (Evenwell)
com.transsion.phonemanager - Transsion (TECNO/Infinix/itel)
com.zte.heartyservice - ZTE (MyOS)
The app abuses the Accessibility permission it gained to click through menus and grant itself call, SMS, gallery, camera, contacts and external storage permissions, and finally makes itself an admin app of the device, which can make it much more difficult to uninstall it.
The payment apps target
The server also sent down another interesting payload
{
"action":"replacePinTargets",
"fromAdmin":"s1783684248210_155355",
"keywords":[
"pay"
],
"packages":[
"jp.co.fukuibank.bankingappli",
"jp.co.resona_gr.ss.SmartApp",
"jp.co.rakuten.kc.rakutencardapp.android",
"jp.japanpost.jp_bank.bankbookapp",
"jp.co.surugabank.portal",
"mx.intelifin.android.albo",
"dif.tech.plata",
"com.google.android.apps.walletnfcrel",
"mx.klar.app",
"com.mercadopago.wallet",
"com.nu.production",
"mx.bancosantander.supermovil",
"ai.powerup.stori",
"ar.com.bancar.uala",
"com.grability.rappi",
"gob.bancodelbienestar.bcobienapp",
"com.gojek.gopa",
"mx.com.miapp",
"com.wallet.crypto.trustapp"
]
}
This is a list of payment apps used in Japan and Latin America (especially Mexico). It appears that the app would override these apps and likely display credential-stealing overlays.
Data Exfiltration, Interaction Logging and Uninstall Protection
The app receives commands from the C2 to exfiltrate the contents of the gallery and contacts from the victim's device.
{
"action": "readAlbumList",
"curpage": 1,
"fromAdmin": "s1783683987269_154416",
"pagesize": 100,
"silent": true
}
{
"action": "readContactList",
"curpage": 0,
"fromAdmin": "s1783683987269_154416",
"pagesize": 500,
"silent": true
}
The silent: true flag is the operator explicitly instructing the malware to perform exfiltration without triggering any visible notification or UI change on the victim's device. Since the analysis ran on an emulator with no saved contacts or images, the commands were observed being received and decrypted, confirming the capability exists at the operator level. Execution on a live device with real data is a separate matter.
The abuse of the Accessibility permission goes further than just granting itself permissions. The interaction logger built on top of it captures text input character by character, along with the package name, input field ID, and view class for every field the victim types in. Testing this by typing in the Messages app confirmed it immediately.
{
"k": "keylog_t_com.google.android.apps.messaging_com.google.android.apps.messaging:id/compose_message_text",
"pkg": "com.google.android.apps.messaging",
"cls": "android.widget.MultiAutoCompleteTextView",
"type": "keylog",
"text": [
"This is a test for exfiltration"
]
}
It doesn't stop at text fields. Click events across apps are logged the same way, with package context and view class attached -- keylog_c_* entries were firing from com.android.camera2 and com.nexuslauncher without any text input involved. Every interaction on the device is being reported back.
Finally, the Accessibility permission is also used to block the victim from reaching the app's own settings page. Two anti_uninstall intercepts were observed:
{
"type": "anti_uninstall",
"msg": "BLOCKED|path=content_settings|pkg=com.android.settings"
}
{
"type": "anti_uninstall",
"msg": "BLOCKED|path=app_detail|pkg=com.android.settings|cls=com.android.settings.applications.InstalledAppDetailsTop"
}
Navigating to Settings or directly to the app's detail page gets intercepted and blocked. Combined with Device Admin privileges, removing this app from an infected device without ADB access would be non-trivial for an average user.
With the on-device behavior fully mapped, I turned my attention to where this data was being sent.
Infrastructure
With the C2 domain identified from the network hooks, I ran it through VirusTotal.
The domain was registered on 5th May 2026 through GoDaddy, with privacy protection enabled. At the time of analysis, it was flagged by 15 out of 91 vendors, tagging it as phishing and fraud. The passive DNS records showed 3 distinct IPs the domain has resolved to since registration.
The domain seems to have flown completely clean on the first two IPs. The operation was probably started early in May and operated undetected for nearly 2 months before any detection.
The Panel
A reverse IP lookup of the current IP, along with the historical SSL certificates section in VT surfaced a new domain running on the same server. Browsing to it over HTTPS revealed what appears to be the operator's C2 panel, a Chinese language web application branded "V2 Panel" announcing it is built with React and Ant Design. A login page with username and password fields, nothing else is exposed.
The DMARC record of the co-hosted domain lists dmarc_rua@onsecureserver.net as the aggregate report address. The domain itself was registered on the 28th of June 2026.
The Chinese-language panel UI, combined with the English and Chinese social engineering page served to victims and the catAllViewSwitch payload covering Japanese and Latin American targets, points to a Chinese-speaking threat actor operating commercial-grade Android banking malware infrastructure on a global targeting mandate.
At the end of it all, none of the technical sophistication matters as much as the kiss mark icon. The entire chain: the overlays, the interaction logger, the accessibility automation navigating through OEM settings menu in more than 40 languages, all sit behind a single moment when someone sees an icon which catches their attention and taps on it. The malware doesn't have to be too clever if the lure is good enough. KYSS is a reminder that the defences are always only as strong as the weakest link.
Update: IOCs and Additional Infrastructure
Following publication, Rajhans P pointed to a FOFA fingerprint querying V2 Panel by icon hash and title, which returns 48 indexed instances. This confirms that V2 Panel is a commercial platform with multiple operators, not infrastructure purpose-built for this campaign alone.
Since the C2 infrastructure is now publicly indexed, full IOCs are being published below.
| Type | Value |
|---|---|
| SHA-256 | 4af310165416dffbfa5468bfc2a4ab4d050e32aca3f239c1284c46cd829a16e8 |
| MD5 | e231cfce40afb884935035bd4c17faf9 |
| SHA-1 | e13904d0b76b1c388428895ca570c11ad245096d |
| Package Name | com.cmn0n5.qhtgr.b7d8.cj75 |
| AES Key | 0623U25KTT3YM8P9 |
| C2 Domain | datouyu86[.]com |
| C2 Panel Domain | cctvv2[.]com |
| C2 IP (current) | 103[.]106[.]229[.]158 |
| Historical IP | 13[.]127[.]80[.]127 |
| Historical IP | 43[.]210[.]157[.]151 |
| DMARC Report Address | dmarc_rua@onsecureserver.net |
Historical SSL Thumbprints (SHA-1):
| First Seen | Subject | Thumbprint |
|---|---|---|
| 2026-07-04 | cctv2.com |
0ad58517ce0cf14faca8d1bbcf2c8052d4145589 |
| 2026-06-07 | www.datouyu86.com |
cb90cecce535013654b431384ad5f1947a342de7 |
| 2026-05-17 | www.datouyu86.com |
c857aa15bc916e6a7f765861b03aadeb59f3dd4b |
| 2026-05-06 | www.datouyu86.com |
a8f98e5508d8039ac56b792916c8fb9c28c90b79 |
| 2026-05-06 | www.datouyu86.com |
c470ec938c523a36ec90e2cff4e468e3465840f3 |
Detection count has also increased to 17/91 since publication.