Android-троян Mamont — теперь на Kotlin

Android-троян Mamont — теперь на Kotlin

24 февраля 2026 года в 16 часов по Хабаровскому времени в мессенджере MAX от аккаунта папы приходит сообщения вида "Посмотри, это ты на фото" и следующим сообщением приложен файл "Фото(3).apk". Я сразу же позвонил отцу - интернет отключили, симку вытащили, а на следующий день он сходил в МФЦ и поменял пароль. Файл с вирусом скачать я не смог - через полчаса после этого аккаунт отца удалили за спам, плюс само сообщение я удалил. Но пока файл ещё был, я попросил брата переслать его мне, но скачать я его уже не мог - из-за удаления аккаунта.

Работу пояснительную хоть и проводили, но "был без очков, что-то тыкнул" и установил - когда у тебя телефон от Huawei без гугл сервисов, то все приложения плюс-минус так и ставились. Прошло время - аккаунт через месяц папе дали вновь зарегистрировать, телефон тот мы отложили от греха подальше, выдал свой старый Samsung A50 и про случай забыли. Но одним вечером, когда я лежал в кровати я подумал - "Стоп, если аккаунт восстановили, то и файл я могу скачать?" Зашел в чат с братом, долистал до пересланного сообщения и решил скачать файл вновь. И что вы думаете - я его скачал! Б - Безопасность. А раз файл скачан, то надо его проанализировать - о чём и будет статья.

Ссылки на материалы

Прежде чем начать оставлю список из статей на Хабре, которые освещают данную тему:

https://habr.com/ru/companies/k2tech/articles/879412/ - тут вирус не шифрован, очень повезло

https://habr.com/ru/companies/k2tech/articles/879412/ - тут вирус не шифрован, очень повезло

https://habr.com/ru/companies/usergate/articles/1028474/ - очень хорошая статья, я сошлюсь на неё позже.

https://habr.com/ru/companies/usergate/articles/1028474/ - очень хорошая статья, я сошлюсь на неё позже.

Первичный осмотр

Первым делом отправляем файл на VirusTotal: https://www.virustotal.com/gui/file/f52786e3662ddf388cf8099e156da186ba6e77f76bf707c4d2d20b4e0f4ae2e8/detection

Когда я закидывал файл, то его распознало всего 18 штук, но уже на момент написания статьи 23 антивируса распознало данный файл. Ключевые моменты, что он encrypted и obfuscated. Это меня и тормознуло на небольшой срок. Во вкладке Behavior нас будет интересовать IP Traffic, а конкретно строка TCP 176.124.222.81:80 . Запомним эту строку - она понадобится нам далее. В целом, больше ничего интересного нам нет - большую часть информации мы и так узнаем, когда будем смотреть внутренности APK.

Для работы нам понадобится:

APKTool: https://apktool.org/docs/install ( https://github.com/iBotPeaches/Apktool ).

APKTool: https://apktool.org/docs/install ( https://github.com/iBotPeaches/Apktool ).

JADX-GUI: https://github.com/skylot/jadx/releases (я делал под Windows всё, тем более есть комплект сразу с JRE - очень удобно).

JADX-GUI: https://github.com/skylot/jadx/releases (я делал под Windows всё, тем более есть комплект сразу с JRE - очень удобно).

OpenCode - будем через ИИ писать деобсуфикатор.

OpenCode - будем через ИИ писать деобсуфикатор.

Качаем утилиты и погнали. Сам файл apk представляет zip архив, но при просмотре в архиваторе AndroidManifest.xml стоит параметр, что он под паролем. Как верно говорилось в статье - это ZIP poisoning (установка бита шифрования ( 0x0001 ) в поле general purpose flag для отдельных файлов). APKToolу меня без проблем извлек файлы, начинаем смотреть содержимое.

Из запрашиваемых разрешений нам сразу понятно, что это

SMS Stealer (RECEIVE_SMS/READ_SMS/SEND_SMS);

SMS Stealer (RECEIVE_SMS/READ_SMS/SEND_SMS);

работает в автозапуске (RECEIVE_BOOT_COMPLETED);

работает в автозапуске (RECEIVE_BOOT_COMPLETED);

читает не только СМС, но ещё и контакты (READ_PHONE_STATE/READ_CONTACTS/CALL_PHONE) - для того, чтобы потом рассылать вредоносную ссылку с вирусом;

читает не только СМС, но ещё и контакты (READ_PHONE_STATE/READ_CONTACTS/CALL_PHONE) - для того, чтобы потом рассылать вредоносную ссылку с вирусом;

лезет в интернет (INTERNET/ACCESS_NETWORK_STATE);

лезет в интернет (INTERNET/ACCESS_NETWORK_STATE);

работает в фоне и имеет защиту от убийства процесса (FOREGROUND_SERVICE/WAKE_LOCK/SCHEDULE_EXACT_ALARM).

работает в фоне и имеет защиту от убийства процесса (FOREGROUND_SERVICE/WAKE_LOCK/SCHEDULE_EXACT_ALARM).

Понятное дело, что при установке всё это написано, но вот в такой ситуации папа внимание на всё это не обратил.

А мы обращаем внимание на следующие строчки:

priority=2147483647 - это максимально возможный приоритет в Android, то есть этот ресивер получит SMS раньше любого легитимного приложения .

DycyX.mb — это упакованный DEX-файл, спрятанный в assets/. Kaspersky-совместимость нужна, чтобы избегать детекта (причем касперский этот вирус не видел пару недель назад, если судить по virustotal).

Здесь пытается "косить" под Truecaller (Определитель номера). В целом, бросающиеся в глаза параметры из XML отмечены, поэтому перейдём к файлам.

Как видно, функции все обсуфицированны, но часть информации можно достать.

Файл: app/src/main/java/L16a8154QFmO/L17abc0dQFmO/Lf2dbcccQFmO/Laed1011QFmO.java

Класс Laed1011QFmO — это Application-класс стаба-загрузчика (в манифесте это строка android:name="L16a8154QFmO.L17abc0dQFmO.Lf2dbcccQFmO.Laed1011QFmO" ). В указанном выше case 1749728 BaseDexClassLoader используется для динамической загрузки DEX во время выполнения - это по сути лоадер. А вот в case 1753604 вызывается метод attachBaseContext() .

Когда идёт переопределение attachBaseContext в каком-то классе (например, в активности или в своём подклассе Application), обычно создаётся обёртка вокруг переданного Context через вспомогательный класс и затем вызывается super.attachBaseContext() с этой обёрткой (в нашем случае с переданным context ). Внутри метода система проверяет: если базовый контекст уже был задан, выбрасывается исключение IllegalStateException - после этого все вызовы к текущему контексту делегируются обёрнутому объекту. Если сказать ещё проще - одно приложение вызывает внутри себя другое приложение.

Метод attachBaseContext() делает следующее:

Читает индикатор версии из файла в code cache:

Читает индикатор версии из файла в code cache:

Копирует DycyX.mb из assets:

Копирует DycyX.mb из assets:

Загружает DEX через BaseDexClassLoader

Загружает DEX через BaseDexClassLoader

Подменяет class loader через рефлексивный доступ к ClassLoader.pathList.dexElements

Подменяет class loader через рефлексивный доступ к ClassLoader.pathList.dexElements

Если посмотреть на данный класс, то можно выделить 3 техники обфускации кода

Место в коде

Исходный код разбивается на базовые блоки, находящиеся на одном уровне вложенности. Каждый блок получает уникальный номер или идентификатор. Это можно увидеть в следующей структуре — while(true) { switch(hash) }

Все методы в Laed1011QFmO.java. Вот этот большой case и реализует данную технику.

Строки закодированы в short[] массивах, а ключ передаётся в вызове.

Массивы Febab3b5dTlgx и F48fce10cTlgx

Рефлексивные вызовы

API-вызовы обёрнуты в методы-прокладки с проверками

Классы L0b4c3f2QFmO , L9e63ba2QFmO , L51a4aa6QFmO

Для примера Control Flow Flattening посмотрим ещё раз начало файла:

Вызов метода Q985f7048zACV(Febab3b5dTlgx, 0, 3, 1234) декодирует 3-символьную строку из массива Febab3b5dTlgx (оффсет 0, длина 3) с ключом 1234 . Затем Q968726fazACV() берёт hashCode() декодированной строки, и этот хеш становится номером case в switch. Из-за этого невозможно предсказать порядок выполнения без эмуляции.

А где же тогда расшифровываются эти строки? А всё просто - в файле app/src/main/java/L16a8154QFmO/L17abc0dQFmO/Lf2dbcccQFmO/L0b4c3f2QFmO.java есть следующий код:

А в app/src/main/java/L28d0fa2QFmO/L74cd619QFmO/L51a4aa6QFmO.java есть похожий метод по своей структуре:

В этих методах используется общая формула - (char)(short_array[offset + i] ^ key). Все похожие методы делают одно и то же (XOR), но находятся в разных классах.

На данном моменте я уже подключил ИИ - настроил OpenCode (с подключенным платным API Deepseek V4 Pro) и натравил на папку проекта с простым промтом вида "Есть обсуфицированный код, написанный на java - нужен скрипт для деобфускации" и уточнением, что нашёл в коде. Если бы я не воспользовался им, я бы писал статью дольше или вообще не дописал)

Пару минут размышлений и у меня есть уже готовый скрипт:

В начале каждого класса используются следующие статические поля:

Начальное значение

Класс:строка в коде

-486

L0b4c3f2QFmO.java:28

188

L9e63ba2QFmO.java:22

-437

Ldacae8bQFmO.java:28

467

L51a4aa6QFmO.java:24

Lfb61d19QFmO (в static-блоке Laed1011QFmO.java:43 - это уже ИИ нашёл)

Ключи вычисляются динамически, например L9e63ba2QFmO.F0b0cb632Tlgx ^ 180 в файле app/src/main/java/L16a8154QFmO/L17abc0dQFmO/Lf2dbcccQFmO/Laed1011QFmO.java :

Скрипт решает следующие задачи

Парсит JADX-декомпилированные Java файлы и ищет в них short[] массивы с вызовами *QFmO*() ;

Парсит JADX-декомпилированные Java файлы и ищет в них short[] массивы с вызовами *QFmO*() ;

Подставляет известные значения полей (из таблицы выше) в выражения ключей;

Подставляет известные значения полей (из таблицы выше) в выражения ключей;

Декодирует строку по известной нам уже формуле (char)(short_array[offset + i] ^ key) ;

Декодирует строку по известной нам уже формуле (char)(short_array[offset + i] ^ key) ;

Брутфорсом перебирает все 65536 ключей с проверкой читаемости результата (функция find_best_key).

Брутфорсом перебирает все 65536 ключей с проверкой читаемости результата (функция find_best_key).

Погонял я скрипт, но ничего интересного не извлек из этих java файлов: только пути для загрузки DEX и системные вызовы. Переходим к анализу дальше.

payload.dex

Сейчас посмотрим, что за этот dex. В папке app/src/main/assets/ видим следующие файлы:

DycyX.mb - это упакованный DEX, который и загружается указанным кодом выше.

cfg.dat и .Xj3X3sxItIypfDAA.txt - шифрованные файлы, какой-то набор байт - нас они не интересуют в данный момент.

Начинаем разбираться с нашим DEX файлом. Выше я уже говорил про ZIP poisoning - тут примерно тоже самое. Посмотрим начало файла DycyX.mb:

Первые 2 байта ( 78 9C ) имитируют zlib-заголовок: 78 — заголовок zlib (deflate, окно 32K), а 9C — контрольная сумма zlib. Далее мы видим сигнатуру DEX-файла (выделена на скриншоте выше). Это трюк для обхода автоматических сигнатурных сканеров: утилиты видят “сжатый” файл и не анализируют его как DEX.

Откидываем два байта простым скриптом:

С полученным dex файлом начинаем проделывать то же самое - закидываем в JADX-GUI и смотрим, что получилось.

Сразу видно уже знакомые нам из манифеста имена сервисов приложения:

Строка 57: android:name="com.reawlme.kcupeyue.POIX1UVS23" Строка 71: android:name="com.reawlme.kcupeyue.POIX1UVS20" Строка 74: android:name="com.reawlme.kcupeyue.POIX1UVS22" Строка 87: android:name="com.reawlme.kcupeyue.POIX1UVS21" Строка 92: android:name="com.reawlme.kcupeyue.POIX1UVS19" Строка 96: android:name="com.reawlme.kcupeyue.POIX1UVS16" Строка 105: android:name="com.reawlme.kcupeyue.POIX1UVS12" Строка 114: android:name="com.reawlme.kcupeyue.POIX1UVS1" Строка 124: android:name="com.reawlme.kcupeyue.POIX1UVS18"

Извлечённых файлов получилось много, поэтому на полученную папку натравливаю вновь OpenCode с API Deepseek и прошу разобрать структуру. Получилось следующее:

MainActivity.java

Точка входа, WebView для фишинга

Http.java

HTTP-клиент с шифрованием (о нем будет ниже)

C0715a.java

Менеджер конфигурации

C0717c.java

Менеджер серверов (bootstrap + ротация)

POIX1UVS16.java

POIX1UVS19.java

Сбор данных и отправка на сервер

POIX1UVS21.java

POIX1UVS1.java

В пакете p000a 324 файла, в которых ИИ увидел обфусцированные R8/ProGuard вспомогательные классы. Сделал этот вывод он по формату переменных:

Классы переименованы: p000a.C0000a , p000a.C0058bk , p000a.C0290u7

Классы переименованы: p000a.C0000a , p000a.C0058bk , p000a.C0290u7

Методы: m117a() , m560b() , m604d()

Методы: m117a() , m560b() , m604d()

Поля: f521a , f580f , f599c

Поля: f521a , f580f , f599c

Честно скажу - я ни разу не сталкивался с данным обфускатором (я с Java не работаю в профессиональном плане), но благо в интернете есть уже готовые проекты, например - https://github.com/LXGaming/Reconstruct . Как сказано в ответе stackoverflow ProGuard больше минимайзер - заменяет названия классов, методов и переменных на максимально возможно короткие (что мы увидели уже). Но строковые константы закодированы другим алгоритмом.

Цепочка вызовов идёт следующим образом:

Незамысловатая функция с говорящим названием getBootstrapUrl вызывается в файле payload_jadx/sources/com/reawlme/kcupeyue/C0717c.java:

А строковые константы шифруются следующей функцией (файл payload_jadx/sources/ p000a/C0290u7.java ):

Формула почти напоминает формулу выше, но чутка другая - на внешний вид это позиционно-зависимый XOR . ИИ подсказал, что таблица диспетчеризации находится в этом же файле дальше этой функции в строках 159-230.

Метод m560b(index) использует хеш ((index * 10003) + 20113) % 41 для выбора массива и ключа. Прошу ИИ написать мне скрипт для декодирования - вот полученный результат:

Полученные значения после работы скрипта:

m560b(index)

Декодированная строка

m574p()

http://

m561c()

76.124.222.81 - вот наш IP C2-сервера

m572n()

/cdn/nodes

m573o()

/store/inventory

m563e()

/store/order/

m575q()

/store/checkout

m576r()

/store/refund

m577s()

/media/uplaad (это не опечатка - реально такая строка)

m578t()

/media/report

m579u()

/media/process

m562d()

/cdn/asset/

m568j()

X-Fenrir-Enc (запомним это имя)

m565g()

m567i()

device-id

m566h()

m570l()

application/json; charset=utf-8

m571m()

FNR1 (опять этот Фенрир)

m564f()

m569k()

application/json

Bootstrap сервер жестко задан: http://176.124.222.81:80/cdn/nodes Данный IP адрес мы уже видели в отчёте VirusTotal. К сожалению (точнее к счастью), сервер уже не доступен. А так бы можно было получить JSON следующего формата (ИИ востановил структуру из файла C0717.java )

Сервера сортируются по priority , индекс текущего сохраняется в SharedPreferences. При ошибке — ротация на следующий сервер (строки 234-245 в C0717c.java ).

Исходя из кода, ИИ выделил следующие точки API:

Где вызывается

/store/inventory

не определил

Регистрация устройства

/store/checkout

POIX1UVS19.java:362-38

Полный дамп: device_id, worker, device_model, android_version, app_name, phone_number, sim_count, found_apps, sms_archive

/store/order/

Заказ - скорее всего получение команд что нужно украсть

/store/refund

POIX1UVS19.java:433

Проверка retry_phone

/media/uplaad

POIX1UVS16.java:77

Перехваченное SMS : device_id, sender, text, sim_slot

/media/report

/media/process

/cdn/asset/

/cdn/nodes

C0717c.java:162-166

Список серверов

В POIX1UVS19 функция C0706a содержит перечень приложений, данные которых перехватывает данный троян. Банки, Госуслуги, Госключ, Wildberries/Ozon, WhatsApp и зачем-то Яндекс (именно через плагин поиска searchplugin ). Как это работает. m886g() пробегается по списку установленных приложений и отправляет его в JSON-поле found_apps.

А кто этот ваш Fenrir?

Везде упоминается данная строка, но что же это? Возьму из статьи изображения, но судя по всему - это VaaS (Virus-as-a-Service), "вирус как сервис". Нехорошие ребята продают и обход Касперского, и консоль для веб-управления (на скриншоте видно тот самый Fenrir). "Бизнес есть бизнес", даже если в нём страдают твои же соотечественники.

FenrirCrypto служит для кастомного шифрования трафика. Взглянем на файл payload_jadx/sources/p000a/C0293v0.java

Разработки даже не скрывают свой "почерк":

Ключ шифрования хранится в методе m606f():

Результат: vX8#kP3!wM6@qN9$rT2&jL5*cF7%bH0e

Инициализация вектора реализована в методе m607g() (строки в файле 236-251):

Результат: a357c91e84f26b3de74915bc (12 байт)

Сам алгоритм шифрования - это метод m604d()

Строки в файле C0293v0

XOR каждого байта: out[i] = key[i%32] ^ data[i] ^ iv[i%12] ^ (i & 0xFF)

163-168

Блочная перестановка: каждые 8 байт — swap первых 4 со вторыми 4

170-175

Инвертирование битов: каждый 3-й байт ^ 0xFF начиная с индекса 2

177-179

Base64 (NO_WRAP)

180

Подстановочный шифр: замена символов Base64 (A↔Z, B↔Y, …)

182-192

Префикс: "FNR1" + результат

193

Ну а чтобы сдешифровать, нужно сделать всё наоборот (метод m603c() ):

Проверка и удаление префикса "FNR1"

110-112

Обратная подстановка символов

116-123

Base64 decode

125

Обратное инвертирование битов (симметрично)

126-128

Обратная блочная перестановка (симметрично)

129-139

Обратный XOR (симметричен)

142-145

Зашифрованное сообщение отправляется по HTTP. Формирование JSON идёт в файле Http.java (строки 110-144)

Получится что-то похоже на

Как обычно просим ИИ сделать нам дешифратор:

Как я говорил выше - сервер давно в ауте, поэтому ни ответа, ни привета - проверить мне не на чем.

Ну и быстро пробежимся по оставшимся частям кода, а то уже статья и так большая (в плане приложенного кода), а мне хотелось всего лишь понять, на сколько был опасен вирус.

MainActivity.java:132-151 — m869r() :

MainActivity.java:169-181 — m871t() : если пользователь отклоняет — показывается AlertDialog с требованием сделать приложение SMS-обработчиком.

POIX1UVS16.java:45-51 и POIX1UVS19.java:321-332 :

POIX1UVS16.java:110-150 — onReceive() :

POIX1UVS16.java:67-94 — m878e() :

Сервис сбора данных

POIX1UVS19.java:452-477 — onStartCommand() :

POIX1UVS19.java:98-244 — m885f() :

Форматирует результат в ASCII-таблицу с заголовком “SMS ARCHIVE” и датами в часовом поясе “Europe/Moscow” (строка 105).

Я уже это описывал: файл POIX1UVS19.java:247-265 — метод m886g()

POIX1UVS19.java:276-302 — getPhoneNumbers() :

POIX1UVS19.java:353-386 — m889j() :

URL: panel_url + "/store/checkout" (строка 362)

Версия трояна ( C0058bk.java:23 ):

Вот тут конечно молодцы - можно понять, какую версию тебе выдали)

POIX1UVS19.java:336-349 — m888i() : повторяет отправку до 10 раз с паузой 5 секунд.

Foreground-сервис и C2-команды

POIX1UVS21.java:410-423 — onCreate() :

POIX1UVS21.java:147-158 — m907h() : создаёт notification channel с выключенными значками, вибрацией и звуком, с низкой важностью ( IMPORTANCE_LOW ).

POIX1UVS21.java:470-495 — sendPing() :

POIX1UVS21.java:381-402 — checkCommands() :

POIX1UVS21.java:211-227 — m912m() :

POIX1UVS21.java:277-303 — m916q() :

POIX1UVS21.java:331-344 и POIX1UVS21.java:199 :

Ну и в конце попросим ИИ создать диаграмму с выполнением:

Что хотелось бы сказать в конце.

Прошивать Huawei на чистую систему я не буду, потому что есть вероятность убить загрузчик (на 4pda есть инструкции, но если сделать не правильно, то телефон не запустится из-за "гибридной структуры" памяти телефона), хотя телефон папе этот больше нравится - батарейку лучше держит (логично - Google сервисов нет).

Использование ИИ очень помогает в иследовании - я оформлял статью больше по времени, чем мне OpenCode генерировал скрипты. Я выявил всё, что хотел - сервер, функции - этого мне было достаточно.

Про пароли я упомянул в самом начале, так же не забудьте поставь родственникам антивирус на смартфоны под управлением Android (хотя как мы видим, не всегда это может помочь). Надеюсь вам было интересно и если кому-то нужно, то могу выложить все файлы на Github - для исследования так сказать. Берегите себя и своих близких)

← Cybersecurity