
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 - для исследования так сказать. Берегите себя и своих близких)