
Это ты на фото? SMS-RAT. Методы обфускации
Привет, Хабр! После короткого перерыва на связи снова команда uFactor и я, Иван Князев.
Угрозы для устройств на базе Android хорошо изучены, но вместе с тем вариантов их реализации становится всё больше. Если ранее злоумышленники делали акцент на сложный функционал и полный контроль над устройством, то сегодня они всё чаще выбирают облегчённые версии, которые проще распространять и сложнее детектировать.
В данный момент наибольший масштаб на территории РФ приобрели следующие классы ВПО под Android:
NFCGate
Mamont
NFCGate — реализация атаки типа Relay Attack, где устройство жертвы выступает ретранслятором NFC‑сигнала. Первоначально проект был создан в исследовательских целях (ссылка).
В большинстве случаев ВПО распространяется под видом «приложения ЦБ для перевода средств на безопасный счёт». Выделяют два вектора атаки:
Примечание: приложение должно быть выбрано как платёжное по умолчанию либо пользователь должен вручную выбрать его при поднесении устройства к терминалу.
Прямой: Жертву убеждают приложить карту к телефону (а также ввести пин‑код в приложении) — в результате данные считываются через NFC и передаются злоумышленнику для операций в его интересах.
Цель: завладеть картой жертвы.

Обратный: Жертву убеждают приложить телефон к банкомату и внести свои наличные средства на указанный счет (к примеру, «безопасный счет»). В данном случае в приложении присутствует информация о карте злоумышленника (так называемого дропа). То есть происходит эмуляция карты злоумышленника.
Цель: пополнение подконтрольного счета с помощью наличных.
В отличие от NFCGate, который реализует атаку на платёжный интерфейс, семейство Mamont представляет собой классический Remote Access Trojan (RAT), нацеленный на контроль над устройством жертвы.
Классификация реализаций Mamont
В рамках данного семейства были выделены две актуальные ветви Android RAT, различающиеся по требуемым привилегиям и, в зависимости от них, — по сложности установки и устойчивости к детектированию:
Параметр |
Полноценный RAT |
SMS‑RAT |
Примеры |
CraxsRAT, BTMOB, Anubis, ClayRat |
Облегчённые модификации Mamont RAT |
Функционал |
Полный контроль: экран, камера, микрофон, файловая система, захват клавиатуры, в том числе доступ к телеметрии
|
Сфокусирован на телеметрии: SMS, звонки, USSD, базовая информация об устройстве
|
В данной статье сосредоточимся на SMS‑RAT — облегчённой, но наиболее распространённой реализации семейства Mamont. В настоящее время такие APK массово распространяются через спам‑рассылки и фишинговые кампании, а минимальный набор требуемых разрешений существенно упрощает установку приложения на устройство.
Базовые (минимальные) разрешения для SMS‑RAT:
1. SMS‑разрешения:
RECEIVE_SMS — получение SMS
READ_SMS — чтение SMS
SEND_SMS — отправка SMS
2. Разрешения к телефону:
READ_PHONE_STATE — чтение состояния телефона (IMEI, сеть, вызовы)
READ_PHONE_NUMBERS — чтение номеров телефонов
3. Системные разрешения:
RECEIVE_BOOT_COMPLETED — автозапуск после загрузки
POST_NOTIFICATIONS — отправка уведомлений
4. Сетевые разрешения:
INTERNET — доступ к интернету
ACCESS_NETWORK_STATE — информация о сетевом подключении
5. Фоновые службы:
FOREGROUND_SERVICE — запуск фоновых служб
FOREGROUND_SERVICE_DATA_SYNC — синхронизация данных в фоновом режиме
В некоторых реализациях SMS‑RAT запрашиваются также READ_CALL_LOG (история звонков) + QUERY_ALL_PACKAGES (список всех установленных приложений), CALL_PHONE (совершение звонков и отправка USSD‑команд).
Архитектура вредоносного приложения строится вокруг двух ключевых модулей и двух дополнительных, каждый из которых реализует конкретный функционал по сбору данных или управлению устройством:
Модуль |
Назначение |
Как используется злоумышленником |
|
Сбор информации о приложениях
|
Анализ списка установленных на устройстве программ
|
Выявление банковских клиентов, криптокошельков и антивирусов для определения «ценности» жертвы и адаптации дальнейших атак
|
|
Перехват и управление SMS
|
Чтение входящих сообщений, отправка исходящих, блокировка уведомлений
|
Получение одноразовых кодов подтверждения (OTP) для доступа к банковским счетам, подписка на платные услуги |
|
Сбор журнала вызовов (не во всех эта реализация присутствует)
|
Копирование истории вызовов
|
Подготовка целевых фишинговых атак на контакты, в том числе с помощью отправки SMS с телефона жертвы
|
|
Выполнение USSD‑команд (не во всех эта реализация присутствует) |
Автоматический запуск сервисных запросов оператора
|
Проверка баланса, подключение платных опций, перевод средств через мобильный счёт без подтверждения пользователем или настройка переадресации вызовов
|
Как SMS-RAT попадает на устройство?
Часто пользователи могут увидеть APK в домовых чатах, в комментариях под постами (к примеру, к сервисам для обхода блокировок, а также к мобильным играм наподобие Standoff 2).

Часто веерная рассылка может осуществляться по контактам, в случае если аккаунт в мессенджере был скомпрометирован.


В отдельных кампаниях злоумышленники настраивают таргетированную рекламу на фишинговые лендинги, имитирующие официальные страницы сервисов. Переходя по рекламе в поиске, пользователь попадает на сайт с призывом скачать «необходимое» приложение.
Что происходит после установки SMS‑RAT?
После установки и запуска ВПО пользователь не наблюдает явных признаков компрометации.
Для маскировки вредоносной активности злоумышленники не усложняют реализацию и интегрируют стандартный компонент WebView, встраиваемый в SMS‑RAT. Фактически приложение функционирует как браузерная оболочка с жёстко зашитым URL. Ключевой принцип здесь — контекстное соответствие: содержимое страницы подбирается в зависимости от того, под каким предлогом приложение было установлено. Это позволяет легитимизировать присутствие APK на устройстве в глазах пользователя.
Сценарий «Новость/Сенсация»: Если приложение распространялось под видом «видео с места ДТП», внутри WebView открывается ссылка на новостную статью или видеохостинг с соответствующим контентом. Пользователь видит то, что ожидал.
Сценарий «Утилита/Загрузка»: В более универсальных случаях используется имитация бесконечного процесса загрузки. Например, часто (ссылка) встречается адрес photricity[.]com/flw/ajax/, который отображает бесконечную загрузку.
Домен
photricity[.]comпринадлежит легитимной студии веб‑дизайна (США). Однако путь/flw/ajax/(также/flw/ni/) ведёт на страницу с PRANK‑контентом — «No Internet Prank ∞ Forever Loading Website» photricity.com. Это легальный веб‑ресурс, созданный для розыгрышей, который эмулирует зависшую загрузку данных.
После установки и первого запуска SMS‑RAT инициирует связь с командным сервером (или альтернативным каналом — например, Telegram‑ботом) и передаёт первичный пакет телеметрии: список установленных приложений, информацию о SIM‑картах, архив SMS‑сообщений и журнал вызовов. Дополнительно может отправляться содержимое буфера обмена, которое часто содержит скопированные пароли, адреса кошельков или другую чувствительную информацию.
После регистрации на командном сервере устройство переходит в режим постоянного мониторинга: поступления новых SMS и звонков, перехватывая их в реальном времени. Злоумышленник получает возможность не только пассивно собирать данные, но и активно управлять устройством — отправлять SMS от имени жертвы (в том числе на короткие номера), выполнять USSD‑команды для проверки баланса.

Mamont as a Service
Количество вариаций SMS-RAT на чёрном рынке растёт семимильными шагами. Но в большинстве случаев они отличаются только UI (или панелями управлений, которых в результате мониторинга было обнаружено больше 10), а также методами обфускации исходного кода или методом доставки полученной информации до злоумышленника.



Почему так много различных реализаций?
Многообразие реализаций SMS-RAT на теневом рынке обусловлено в первую очередь спецификой монетизации и распределением ролей злоумышленников. На основе анализа инфраструктуры и коммуникаций злоумышленников мы выделили следующую модель, используя терминологию, принятую в самой среде:

Crypter распространяет свой сервис по подписке, и в его зону ответственности входит обновление механизмов обфускации и антидетекта RAT.

Coder поддерживает инфраструктуру и пишет базовый функционал RAT (обычно меняется лишь транспорт до контрольного сервера), также предоставляет веб‑панель или Telegram‑бота. Доступ к ресурсам продаётся по подписке, а также за процент, полученный с выгоды. Примеры того, как выглядят веб‑панели, можно посмотреть выше.

Team — это набор Worker’ов (о них ниже). Напрямую доступ к панели не предоставляется, в связи с этим Worker должен вступить в какую‑то команду, которая непосредственно купила подписку на SMS‑RAT. Владельцы команд берут процент с полученной выгоды.

Worker — конечное звено, который непосредственно распространяет APK.
Worker’ы подразделяются на два вида — Вбивер и Траффер. Траффер с помощью веб‑панели или Telegram‑бота осуществляет настройку ВПО под свою фишинговую компанию (добавляет название, меняет картинку, вставляет URL) и распространяет через имеющиеся каналы. Вбивер агрегирует и реализует информацию, полученную из логов, с целью дальнейшей монетизации.

Препятствия на пути исследования SMS‑RAT: статический разбор APK
Статический анализ Android SMS-RAT обычно начинается с извлечения содержимого APK: AndroidManifest, ресурсов и classes.dex, — с последующей декомпиляцией (например, с помощью JADX (ссылка)).
В исследованных образцах встречаются как примитивные реализации, где важные данные (адреса C2, токены, ключи) хранятся в открытом виде — в ресурсах или непосредственно в коде (константы в classes.dex), — так и более продвинутые варианты. В последних наблюдается переход к обфускации, сокрытию строк и техникам, намеренно усложняющим анализ APK.
Как отмечалось ранее, реализации SMS‑RAT могут существенно различаться в зависимости от подходов разработчиков и применяемых методов сокрытия. Чтобы избежать прямой привязки к конкретным авторам и упростить ход повествования, далее условно разделим рассмотренные образцы на два класса: SMS‑RAT-CLASS‑1 и SMS‑RAT-CLASS‑2.
В данной главе акцент сделан на практическом исследовании — от корректной обработки APK до анализа кода и извлечения параметров C2.
ZIP poisoning в APK
Поскольку APK формально представляет собой ZIP‑архив, на первом этапе чаще всего достаточно воспользоваться apktool или одним из стандартных ПО для распаковки архивов. Однако на практике этот процесс может быть намеренно усложнён. В рассматриваемых образцах ВПО SMS‑RAT содержатся намеренно модифицированные ZIP‑заголовки (например, установка флага шифрования, что не связано с реальным шифрованием, а используется как обфускация).
Такие модификации приводят к некорректной работе стандартных инструментов, которые в первую очередь ориентируются на метаданные архива и ожидают согласованную структуру. В то же время Android в ряде случаев продолжает корректно работать с такими APK. Это связано с тем, что система не выполняет «полную распаковку» архива, а обращается к отдельным записям по мере необходимости, используя собственную реализацию парсинга (libziparchive). В результате один и тот же APK может корректно устанавливаться и запускаться на устройстве, но при этом в рамках статического анализа извлечение или декомпиляция могут производиться с ошибками.
Иногда такие APK удаётся открыть напрямую с использованием JADX, однако это не гарантирует корректного извлечения всех артефактов (например, часть файлов может быть повреждена или пропущена). Поэтому для анализа необходимо учитывать особенности структуры архива и применять более контролируемые методы извлечения.
Простой ZIP poisoning
В большинстве случаев ZIP poisoning реализуется через установку бита шифрования (0x0001) в поле general purpose flag для отдельных файлов (например, AndroidManifest.xml, classes.dex).
Так, в SMS‑RAT-CLASS‑1 в ZIP‑архиве существует намеренная модификация флага шифрования. Согласно спецификации (ссылка) формата ZIP, метаинформация о каждом файле присутствует в двух структурах:
-
Local File Header (
PK 03 04)50 4B 03 04 — сигнатура (PK\x03\x04)
14 00 — version needed
03 00 — general purpose flag

-
Central Directory Header (
PK 01 02)50 4B 01 02 — сигнатура (PK\x01\x02)
1E 03 — version made by
14 00 — version needed
03 00 — general purpose flag

В рассматриваемом случае значение поля general purpose flag равно 0x0003, что является битовой маской и включает, в частности, бит шифрования (0x0001), а также 0x0002 (deflate flag).
Именно наличие этого бита шифрования приводит к тому, что инструменты анализа интерпретируют запись как зашифрованную.
Пример сброса бита шифрования в ZIP для всех файлов в архиве
import sys data = bytearray(open(sys.argv[1], "rb").read()) i = 0 while i < len(data) - 10: if data[i:i+4] == b'PK\x03\x04': flag = int.from_bytes(data[i+6:i+8], "little") flag &= ~0x0001 data[i+6:i+8] = flag.to_bytes(2, "little") if data[i:i+4] == b'PK\x01\x02': flag = int.from_bytes(data[i+8:i+10], "little") flag &= ~0x0001 data[i+8:i+10] = flag.to_bytes(2, "little") i += 1 open("fixed.apk", "wb").write(data)
Для обхода достаточно сбросить только бит 0x0001 в заголовках ZIP (как в local header, так и в central directory), не затрагивая остальные флаги: после этого архив корректно обрабатывается стандартными инструментами и его можно распаковать обычным способом, например:
Команды для распаковки APK
unzip fixed.apk (bash) apktool d -s fixed.apk -o out
Таким образом, в базовом варианте ZIP poisoning задача сводится к исправлению метаданных архива, после чего дальнейший анализ выполняется стандартными средствами.
Комплексный ZIP poisoning
В более сложных вариантах ZIP poisoning злоумышленники не ограничиваются установкой бита шифрования (0x0001). Архив намеренно формируется таким образом, чтобы разные инструменты интерпретировали его по‑разному. Для этого используются нестандартные имена и пути файлов, добавляются дублирующие записи одного и того же файла (например, classes.dex и /classes.dex), модифицируется поле general purpose flag, а также допускаются различия между данными в local header и central directory.
Такие конструкции приводят к так называемым parser differentials. В результате набор извлечённых файлов становится недетерминированным: часть данных теряется, часть перезаписывается, либо архив не извлекается вовсе. Более подробно с этим можно ознакомиться в «My ZIP isn’t your ZIP: Identifying and Exploiting Semantic Gaps Between ZIP Parsers» (ссылка).
В SMS‑RAT-CLASS‑2 в исследуемых образцах архив содержит большое количество записей с аномальными именами, например:

Подобные записи формально допустимы в ZIP, однако их интерпретация зависит от конкретной реализации парсера. При распаковке «в директорию» это может приводить к:
перезаписи файлов с одинаковыми именами;
извлечению некорректных данных вместо реальных артефактов.
Дополнительно к описанному выше также используется модификация поля general purpose flag. В отличие от базового случая, здесь встречаются другие значения:
Local File Header:

Central Directory Header:

В данном примере general purpose flag 0x0809 = 0x0001 (encrypted) + 0x0008 (data descriptor) + 0x0800 (UTF-8).
Практический подход к анализу APK с ZIP poisoning
С учётом описанных особенностей структуры архива анализ таких APK целесообразно выполнять в два этапа:
1. На первом этапе необходимо устранить искусственное ограничение доступа к данным — корректно снять бит шифрования (
0x0001) в заголовках ZIP.На втором этапе не следует распаковывать APK целиком. Более надёжный подход заключается в адресном извлечении ключевых артефактов (AndroidManifest.xml, classes.dex, файлы из assets) и их последующем анализе по отдельности.
Например, извлечение DEX может быть выполнено напрямую:
unzip -p fixed.apk classes.dex > classes.dex
Такой подход позволяет избежать подмены и перезаписи файлов при распаковке.
В ряде случаев можно обойтись без корректировки ZIP‑структуры, используя сигнатурный анализ. Например, ПО binwalk (ссылка) позволяют обнаружить и извлечь вложенные DEX и другие бинарные артефакты непосредственно из APK, игнорируя некорректные заголовки:
binwalk -Me sample.apk
Однако в сложных вариантах ZIP poisoning этого также может быть недостаточно. При наличии нестандартных и конфликтующих путей (например,
\\,/., дублирующихся имён и псевдорасширений) извлечённая структура может оказаться повреждённой: файлы получают некорректные имена, смешиваются между собой или раскладываются по вложенным каталогам с артефактами. На практике это выглядит как появление большого количества мусорных файлов и директорий (например,classes.dex/,\\\\.xml,.9.pngи т.д.), что затрудняет автоматический анализ и делает результат извлечения непригодным без дополнительной обработки.
Аналогично часть данных можно получить без полной распаковки архива. Например, AndroidManifest.xml и ресурсы могут быть извлечены с помощью aapt2 (ссылка), который использует нативную реализацию разбора APK:
aapt2 dump xmltree --file AndroidManifest.xml sample.apk
Это особенно полезно в случаях, когда стандартные инструменты не могут корректно обработать архив.
Динамическая загрузка DEX
После устранения проблем с распаковкой APK переходим к следующему этапу — анализу его содержимого. На этом этапе основные трудности связаны уже не со структурой архива, а с обфускацией кода и сокрытием ключевых артефактов (payload, строки, C2).
Рассмотрим это на примере SMS‑RAT-CLASS‑1.
После извлечения содержимого APK переходим к анализу AndroidManifest.xml, так как он задаёт точку входа приложения и часто содержит косвенные указания на дальнейшую логику.
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8" standalone="no"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" android:compileSdkVersion="34" android:compileSdkVersionCodename="14" package="com.xiaomdi.kjdgzsc" platformBuildVersionCode="34" platformBuildVersionName="14"> <uses-feature android:name="android.hardware.telephony" android:required="false"/> <!-- Базовые разрешения --> <uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <uses-permission android:name="android.permission.WAKE_LOCK"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/> <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/> <!-- SMS разрешения --> <uses-permission android:name="android.permission.RECEIVE_SMS"/> <uses-permission android:name="android.permission.READ_SMS"/> <uses-permission android:name="android.permission.SEND_SMS"/> <!-- Телефония --> <uses-permission android:name="android.permission.READ_PHONE_STATE"/> <uses-permission android:name="android.permission.READ_PHONE_NUMBERS"/> <permission android:name="com.xiaomdi.kjdgzsc.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION" android:protectionLevel="signature"/> <uses-permission android:name="com.xiaomdi.kjdgzsc.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"/> <application android:allowBackup="false" android:appComponentFactory="androidx.core.app.CoreComponentFactory" android:extractNativeLibs="false" android:icon="@mipmap/ic_launcher" android:label="@string/str_ttru21" android:name="Ta1d2758YioY.Td300f62YioY.T38d8813YioY.T829464eYioY" android:supportsRtl="true" android:theme="@style/Theme.Defender" android:usesCleartextTraffic="true"> <activity android:name="com.xiaomdi.kjdgzsc.MainActivity" android:excludeFromRecents="true" android:exported="true" android:launchMode="singleTask" android:theme="@style/Theme.Defender"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.INFO"/> </intent-filter> </activity> <activity android:name="com.xiaomdi.kjdgzsc.SmsActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.SEND"/> <action android:name="android.intent.action.SENDTO"/> <category android:name="android.intent.category.DEFAULT"/> <category android:name="android.intent.category.BROWSABLE"/> <data android:scheme="sms"/> <data android:scheme="smsto"/> <data android:scheme="mms"/> <data android:scheme="mmsto"/> </intent-filter> </activity> <service android:name="com.xiaomdi.kjdgzsc.BootstrapService" android:exported="false"/> <service android:name="com.xiaomdi.kjdgzsc.SmsService" android:exported="true" android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE"> <intent-filter> <action android:name="android.intent.action.RESPOND_VIA_MESSAGE"/> <category android:name="android.intent.category.DEFAULT"/> <data android:scheme="sms"/> <data android:scheme="smsto"/> <data android:scheme="mms"/> <data android:scheme="mmsto"/> </intent-filter> </service> <service android:name="com.xiaomdi.kjdgzsc.CoreService" android:enabled="true" android:exported="false" android:foregroundServiceType="dataSync"/> <service android:name="com.xiaomdi.kjdgzsc.RegistrationService" android:enabled="true" android:exported="false"/> <receiver android:name="com.xiaomdi.kjdgzsc.PXl9RTViW8u16" android:exported="true" android:permission="android.permission.BROADCAST_SMS"> <intent-filter android:priority="2147483647"> <action android:name="android.provider.Telephony.SMS_RECEIVED"/> <action android:name="android.provider.Telephony.SMS_DELIVER"/> </intent-filter> </receiver> <receiver android:name="com.xiaomdi.kjdgzsc.PXl9RTViW8u12" android:exported="true" android:permission="android.permission.BROADCAST_WAP_PUSH"> <intent-filter> <action android:name="android.provider.Telephony.WAP_PUSH_DELIVER"/> <data android:mimeType="application/vnd.wap.mms-message"/> </intent-filter> </receiver> <receiver android:name="com.xiaomdi.kjdgzsc.PXl9RTViW8u1" android:enabled="true" android:exported="true"> <intent-filter> <action android:name="android.intent.action.BOOT_COMPLETED"/> <action android:name="android.intent.action.MY_PACKAGE_REPLACED"/> <category android:name="android.intent.category.DEFAULT"/> </intent-filter> </receiver> <receiver android:name="com.xiaomdi.kjdgzsc.AlarmReceiver" android:enabled="true" android:exported="false"/> <provider android:name="androidx.startup.InitializationProvider" android:authorities="com.xiaomdi.kjdgzsc.androidx-startup" android:exported="false"> <meta-data android:name="androidx.emoji2.text.EmojiCompatInitializer" android:value="androidx.startup"/> <meta-data android:name="androidx.lifecycle.ProcessLifecycleInitializer" android:value="androidx.startup"/> </provider> <activity android:name="com.truecaller.messaging.conversation.ConversationActivity" android:allowEmbedded="true" android:exported="false" android:parentActivityName="com.truecaller.ui.TruecallerInit" android:screenOrientation="portrait"> <intent-filter> <action android:name="com.truecaller.OPEN_CONVERSATION"/> <category android:name="android.intent.category.DEFAULT"/> </intent-filter> </activity> <activity android:name="edodpwmfjeji.efxosm.dkjskemd" android:enabled="false" android:screenOrientation="portrait"/> <activity android:name="oqzfksq.pqlqf.mrky" android:enabled="false" android:screenOrientation="portrait" android:windowSoftInputMode="adjustResize"/> <receiver android:name="rfjdjdk.djfifjekkd.ciciidkdkf" android:enabled="false"/> <receiver android:name="rfjfidikdf.jfifid.fjfid.fjer" android:enabled="false" android:exported="false"> <intent-filter> <action android:name="android.intent.action.BOOT_COMPLETED"/> <action android:name="android.intent.action.QUICKBOOT_POWERON"/> <action android:name="com.htc.intent.action.QUICKBOOT_POWERON"/> </intent-filter> </receiver> <receiver android:name="ofpfpdlekdjxj.wjdjd.ejdjf.ididr" android:enabled="false"/> <receiver android:name="cfidkd.djjdj.dkdkeiver" android:enabled="false" android:exported="true"> <intent-filter> <action android:name="android.intent.action.PACKAGE_ADDED"/> <data android:scheme="package"/> </intent-filter> </receiver> <receiver android:name="anfjfjfidkd.fkkfkdmde.ivfjfjfer" android:directBootAware="false" android:enabled="false" android:exported="true" android:permission="android.permission.DUMP"> <intent-filter> <action android:name="androidx.work.diagnostics.REQUEST_DIAGNOSTICS"/> </intent-filter> </receiver> <!-- Meta-data --> <meta-data android:name="android.app.shortcuts" android:value="Maloy"/> <meta-data android:name="com.kaspersky.security.KsConnectService" android:value="jFH.ffwv"/> <meta-data android:name="com.kaspersky.security.NewKsConnectService" android:value=".ffwv"/> </application> </manifest>
В AndroidManifest.xml сразу смотрим блок <application> — здесь указана точка входа приложения.
android:name="Ta1d2758YioY.Td300f62YioY.T38d8813YioY.T829464eYioY"
Дополнительно обращаем внимание на <meta‑data>:
<meta-data android:name="com.kaspersky.security.KsConnectService" android:value="jFH.ffwv"/> <meta-data android:name="com.kaspersky.security.NewKsConnectService" android:value=".ffwv"/>
Значения из <meta‑data> доступны приложению через ApplicationInfo.metaData.
Анализ classes.dex
Переходим к анализу кода приложения. Хотя в общем случае приложение может содержать несколько DEX-файлов (multidex), в данном примере присутствует только один — classes.dex.
jadx -d jadx_out classes.dex
Для этого декомпилируем classes.dex с помощью JADX, чтобы проверить, используется ли информация из блока <meta‑data>.

При открытии класса T829464eYioY, указанного в AndroidManifest.xml, видно, что код обфусцирован. В таких случаях анализ целесообразно строить не от структуры классов, а от характерных API‑вызовов, связанных с взаимодействием с <meta‑data>, а также работой с файлами в assets.
В частности, обращения к getApplicationInfo(...).metaData указывают на использование параметров из манифеста, а вызовы getAssets().open(...) — на загрузку встроенных файлов (и в том числе — потенциального payload). Отслеживание этих вызовов и их аргументов позволяет проследить цепочку: получение имени ресурса → чтение файла → его последующая загрузка и выполнение. В упрощённом виде эта последовательность выглядит следующим образом:
Чтение <meta‑data> из AndroidManifest.xml:
\\ на участке кода значение переменной Tc910371YioY.O6d606848jpAw фиксировано и \\ равно 322, в результате 322 ^ 450 = 128, что соответствует константе \\ PackageManager.GET_META_DATA bundle = context.getPackageManager() .getApplicationInfo(context.getPackageName(), Tc910371YioY.O6d606848jpAw ^ 450) .metaData;
2. Получение значения по ключу (после декодирования строки):

Od5d81f30jpAw = bundle.getString("com.kaspersky.security.KsConnectService");
3. Чтение файла из assets:
inputStreamOpen = context.getAssets().open(Od5d81f30jpAw);
4. Запись во временный файл в code_cache и подготовка к загрузке.
5. Динамическая загрузка через BaseDexClassLoader:
File payloadFile = new File(codeCacheDir, Od5d81f30jpAw); String dexPath = payloadFile.getAbsolutePath(); baseDexClassLoader = new BaseDexClassLoader(dexPath, codeCacheDir, null, classLoader);
Примечание:
При копировании в code_cache содержимое файла jFH.ffwv обрабатывается в памяти (служебные байты/префиксы удаляются), и уже результат этой обработки записывается во временный файл. После всех преобразований валидный DEX передаётся в BaseDexClassLoader.
Как видно из псевдокода выше, classes.dex выполняет роль загрузчика: читает имя payload из <meta‑data>, открывает файл из assets, копирует его в code_cache и подключает через BaseDexClassLoader (механизм динамической загрузки DEX в Android).
Деобфускация на примере получения строки из Manifest
В качестве примера разберём, каким именно образом получилась строка "com.kaspersky.KsConnectService".
Как было показано ранее, для доступа к данным в assets используется переменная Od5d81f30jpAw. Анализ кода показывает, что она присваивается в единственном месте, что делает её удобной точкой для дальнейшего разбора (см. Рисунок 20). В нашем случае эта переменная устанавливается только в единственном месте.
Дополнительно видно, что в вычислении параметров участвует переменная Tc910371YioY.O6d606848jpAw, значение которой на данном участке кода фиксировано (раннее описано, что значение = 322).

Функция U218e9580bBfl выполняет побайтовое смещение строки из массива с использованием XOR, то есть строка изначально зашита в коде в обфусцированном виде. В качестве источника данных используется статический массив (в данном случае это массив short[] Oaed46797jpAw).

Функция берёт из него подмассив (по смещению и длине) и восстанавливает строку с использованием заданного ключа.
То есть параметры в нашем случае такие:
sArr = Oaed46797jpAw
i = 21 (смещение / start)
i2 = 39 (длина строки) 322 ^ 357 = 39
i3 = 2011 (XOR-ключ)
В результате выполнения получается строка "com.kaspersky.security.KsConnectService", что является названием переменной в <meta-data>. Значение данной переменной равно jFH.ffwv (см. AndroidManifest.xml).
Анализ jFH.ffwv
По результатам анализа мы видим, что в нашем classes.dex подгружается файл из assets с названием jFH.ffwv.

При просмотре содержимого файла видно, что после первых двух байт следует сигнатура DEX (64 65 78 0a, строка "dex\n035"), что указывает на наличие DEX-файла со смещением:

Поскольку сигнатура DEX находится со смещением, для корректного анализа необходимо удалить начальный префикс и привести файл к валидному виду. Это можно сделать, отбросив первые 2 байта (789C):
$ dd if=assets/jFH.ffwv of=payload_classes.dex bs=1 skip=2
После этого получается DEX-файл, который можно декомпилировать с помощью JADX.
Результат декомпиляции показывает, что несмотря на обфускацию имён классов и пакетов, ключевые строки (URL, endpoint’ы, параметры запросов) не скрыты и доступны в явном виде, что существенно упрощает анализ. В частности, адреса C2 и логика взаимодействия с ним читаются напрямую из кода без необходимости дополнительного исследования.

На рисунке 25 видно, какие параметры отправляются на сервер для регистрации заражённого устройства.
Схема работы и взаимодействие с С2
SMS-RAT-CLASS-1. Схема работы и взаимодействие с C2
В упрощённом виде логика работы SMS‑RAT-CLASS‑1 сводится к следующему.
Сетевое взаимодействие реализовано через HTTP‑API с фиксированным набором endpoint’ов. На первом шаге приложение сначала обращается к BootstrapServer (серверу начальной инициализации 85.192.30[.]244:80), который возвращает список рабочих сетевых адресов C2 с приоритетами. Дальнейшее взаимодействие выполняется уже с выбранным сервером из этого списка (GET /api/v2/fenrir/ip).
Результат ответа на /api/v2/fenrir/ip:
{ "success": true, "servers": [ { "ip": "85.192.30[.]150", "port": 80, "priority": 1 }, { "ip": "109.120.152[.]219", "port": 80, "priority": 0 } ], "count": 2, "timestamp": 1771316010848 }
Дальше происходит получение конфига (GET /api/cfg/{buildId}), регистрация устройства (POST /api/v2/register), периодическое отправление heartbeat (POST /api/v2/ping), запрашивание команды (GET /api/v2/cmd/{device_id}) и передача перехваченных SMS (POST /api/v2/sms).
Исходные данные (JSON с параметрами) не отправляются напрямую — они предварительно шифруются и помещаются в обёртку вида: {"enc":"..."}. Дополнительно такие запросы помечаются HTTP‑заголовком (X‑Fenrir‑Enc: 1).
В новых версиях SMS‑RAT-CLASS‑1 логика осталась той же, но endpoint’ы были переименованы. Вместо /api/v2/... используются пути /cdn/*, /store/*, /media/*. Также, в отличие от предыдущего примера, адрес BootstrapServer отсутствует в виде жёстко заданной строки и формируется с помощью функции getBootstrapUrl() (см. ниже).
Функция формирования IP-адреса BootstrapServer:
public String getBootstrapUrl() { return u7.p() + u7.c() + ":80"; }
В фрагменте кода, представленном ниже, реализован механизм деобфускации строковых констант, предназначенный для скрытия сетевой инфраструктуры командных серверов (C2). В результате последовательного выполнения функций u7.p() и u7.c() из зашифрованных массивов данных формируется итоговый адрес сервера: 176.124.222[.]81:80.
Декомпилированный класс с алгоритмом расшифровки строк для получения адреса Bootstrap‑сервера
package a; import androidx.core.location.LocationRequestCompat; import okhttp3.HttpUrl; public final class u7 { private static final int aa = 23; private static final int ac = 40; private static final int ag = 74; private static final int ak = 108; private static final int e = 75; private static final int g = 92; private static final int i = 109; private static final int k = 126; private static final int o = 161; private static final int q = 178; /* renamed from: a, reason: collision with root package name */ private static final String[] f55a = new String[19]; private static final int[] b = {110, 61, 47, 7, 7, 231, 160, 245, 199, 192, 166, 190, 169, 133, 133, 125}; private static final int y = 246; private static final int m = 143; private static final int[] d = {125, 44, 24, 22, 244, y, m, 194, 200, 163, 177, 147, 193}; private static final int w = 229; private static final int[] f = {76, 3, 9, w, w, 193, 158, 221, 163, 189, 134, 153, 144, 121, 109}; private static final int ai = 91; private static final int[] h = {ai, 242, 250, 244, 218, 208, 237, 189, 185, m, 131, 109, 116}; private static final int[] j = {170, 255, 250, 200, 208, 167, 252, 149, 157, 150, LocationRequestCompat.QUALITY_LOW_POWER, 117, 69}; private static final int am = 119; private static final int[] l = {185, 206, 213, 217, 163, 182, 203, 131, 155, 123, am, 87, 70}; private static final int[] n = {135, 216, 167, 171, 181, 136, 217, 115, 98, 114, 73, 82, 55, 34}; private static final int[] p = {150, 165, 183, 142, 194, 155, 116, 103, 68, 90, 20}; private static final int[] r = {w, 180, 128, 159, 209, 101, am, 65, 87, 76}; private static final int s = 195; private static final int[] t = {234, 223, s, 44, 62, 46, 29, 24, 113, 98, 111, 68, 79, 181}; private static final int u = 212; private static final int[] v = {180, u, 64, 118, 78, 95, 83, 53, 121, 36, 0, 24}; private static final int[] x = {165, 39, 86, 116, 120, 19, 0, 61, 28}; private static final int c = 58; private static final int[] z = {122, 78, 78, 44, 49, c, 65, 16, 226}; private static final int[] ab = {108, 83, 39, 34, 6, 30, 9, 167, s, 221, 193, 219}; private static final int[] ad = {33, 61, 42, 11, 29, 226, 239, 239, 193, 218, 172, 224, 182, 154, 153, 109, 43, 61, 73, 95, 37, 35, 45, 14, 12, 184, 231, 235, 202, 148, 254}; private static final int ae = 57; private static final int[] af = {23, 16, ae, 73}; private static final int[] ah = {10, 27, 8, 249, 172, 140, 159}; private static final int[] aj = {66}; private static final int[] al = {31, 251, 232, 201, 219, 220, 173, 173, m, 156, 110, 34, 112, 84, ai, 47}; private static String a(int[] iArr, int i2) { int length = iArr.length; char[] cArr = new char[length]; int i3 = 0; while (true) { int i4 = 29364; while (true) { int i5 = (i4 ^ 29364) % 5; if (i5 == 0) { cArr[i3] = (char) (iArr[i3] ^ ((((i3 * 13) + i2) + 7) & 255)); } else if (i5 == 1) { i3++; if (i3 < length) { break; } i4 = 29366; } else { if (i5 == 2) { return new String(cArr); } if (i5 != 3) { if (i5 != 4) { } } } i4 = 29365; } } } private static String b(int i2) { String strA; String[] strArr = f55a; String str = strArr[i2]; if (str != null) { return str; } switch (((i2 * 10003) + 20113) % 41) { case 5: strA = a(al, am); break; case 6: strA = a(aj, 108); break; case 7: strA = a(ah, ai); break; case 8: strA = a(af, ag); break; case 9: strA = a(ad, ae); break; case 10: strA = a(ab, 40); break; case 11: strA = a(z, 23); break; case 12: strA = a(x, y); break; case 13: strA = a(v, w); break; case 14: strA = a(t, u); break; case 15: strA = a(r, s); break; case 16: strA = a(p, q); break; case 17: strA = a(n, o); break; case 18: strA = a(l, m); break; case 19: strA = a(j, 126); break; case 20: strA = a(h, 109); break; case 21: strA = a(f, g); break; case 22: strA = a(d, e); break; case 23: strA = a(b, c); break; default: strA = HttpUrl.FRAGMENT_ENCODE_SET; break; } strArr[i2] = strA; return strA; } public static String c() { return b(9); } public static String d() { return b(7); } public static String e() { return b(1); } public static String f() { return b(17); } public static String g() { return b(11); } public static String h() { return b(13); } public static String i() { return b(12); } public static String j() { return b(10); } public static String k() { return b(18); } public static String l() { return b(14); } public static String m() { return b(15); } public static String n() { return b(8); } public static String o() { return b(0); } public static String p() { return b(16); } public static String q() { return b(2); } public static String r() { return b(3); } public static String s() { return b(4); } public static String t() { return b(5); } public static String u() { return b(6); } }
В данном блоке кода реализована обфускация строк для сокрытия сетевой инфраструктуры. При вызове методов класса происходит посимвольная расшифровка данных из массивов с помощью функции a(). Функции u7.p() и u7.c() восстанавливают IP‑адрес из массива и получается 176.124.222.81.
Новые endpoint’ы:
GET /cdn/nodes — bootstrap (список серверов)

GET /cdn/asset/{buildId} — получение конфигурационного файла
POST /store/checkout — регистрация устройства на C2
POST /store/inventory — ping, проверка, онлайн ли устройство
GET /store/order/{device_id} — команды
POST /media/upload — отправка SMS
SMS-RAT-CLASS-2. Схема работы и взаимодействие с C2
При исследовании образцов, относящихся к SMS‑RAT-CLASS‑2, основные сложности были сосредоточены на этапе распаковки. После успешной распаковки дальнейший анализ выполнялся существенно проще: как и в первом случае, по декомпилированному коду удалось восстановить сетевую логику, набор endpoint’ов и общую схему взаимодействия с C2.

В отличие от SMS‑RAT-CLASS‑1, здесь отсутствует этап с получением списков IP‑адресов, а управление и часть взаимодействия с командным сервером перенесены на Firebase Cloud Messaging (FCM, ссылка). Сетевое взаимодействие строится вокруг набора POST‑запросов, а базовый C2‑адрес зашит в клиенте напрямую (45.131.214[.]10).
Краткая схема взаимодействия с C2:
POST /gettingData/gettingData — получение конфигурации (BuildInfo) по данным клиента (clientId, buildKeyValue, buildVersion)
POST /gettingData/event — регистрация/старт клиента (ClientStart)
POST /online/device_monitor/devices/ping — периодическая проверка, онлайн ли устройство
Команды для выполнения на устройстве (отправка SMS, получение номера и т.д.) поступают преимущественно через FCM (FIREBASE_COMMAND). Результат выполнения команд отправляется уже на C2 по HTTP
FCM-команды (online, ping, sms_send, send_all) обрабатываются в основном классе ChimeraWorker

POST /panel/ping/info — ответ на команду ping (статус устройства)
POST /panel/sms_send/info — статус/результат отправки SMS по команде
POST /panel/send_all/info — итог массовой рассылки (send_all)
POST /sms/event — эксфильтрация перехваченных SMS
POST /archive/event — эксфильтрация SMS‑архива
POST /push_and_failedSms/event — push‑уведомления, неуспешные SMS
POST /error/error_handle — обработка ошибок
В некоторых версиях SMS‑RAT-CLASS‑2 разработчики также решили отойти от хранения информации о сетевой инфраструктуре в открытом виде. Для получения адреса C2 использовался отличный от SMS‑RAT-CLASS‑2 подход — конфигурация не хранилась непосредственно в APK, а загружалась c GitHub.
На первом этапе приложение выполняет запрос к GitHub для получения конфигурации: https://github.com/acabthvichm-boop/remote-host-configuration/blob/main/config.json

В декомпилированном коде после получения конфигурационного файла с GitHub для извлечения адреса командного сервера помимо base64 используется также расшифровка с использованием AES‑CTR. При этом ключ и вектор инициализации в APK фиксированы.
Схема выглядит следующим образом:
Загрузка JSON с GitHub.

2. Парсинг полученного JSON.

3. base64-декодирование значений и расшифрование (AES).

4. Формирование итогового C2‑адреса (178.17.62[.]15:80).
Для автоматизации анализа и извлечения IOC был реализован вспомогательный скрипт:
python3 decode_c2_config.py https://raw.githubusercontent.com/acabthvichm-boop/remote-host-configuration/refs/heads/main/config.json -o decoded_config.json
Получение расшифрованного адреса C2
#!/usr/bin/env python3 import argparse import base64 import json import sys import urllib.request from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes def decode_and_save(url: str, out_path: str = "decoded_config.json") -> dict: with urllib.request.urlopen(url, timeout=25) as resp: raw = json.loads(resp.read().decode("utf-8")) key = b"6a209b01592808f7c582c49fa82cbd7b" iv = b"6a209b01592808f7" def dec(v: str) -> str: data = base64.b64decode(v) c = Cipher(algorithms.AES(key), modes.CTR(iv)).decryptor() return (c.update(data) + c.finalize()).decode("utf-8").strip() domain = dec(raw["domain"]) port = dec(raw["port"]) result = { "source_url": url, "raw": {"domain": raw["domain"], "port": raw["port"]}, "decoded": {"domain": domain, "port": port}, "c2_url": f"http://{domain}:{port}/%s", } with open(out_path, "w", encoding="utf-8") as f: json.dump(result, f, ensure_ascii=False, indent=2) f.write("\n") return result if __name__ == "__main__": p = argparse.ArgumentParser() p.add_argument("url") p.add_argument("-o", "--out", default="decoded_config.json") a = p.parse_args() try: r = decode_and_save(a.url, a.out) except Exception as e: print(f"[!] {e}", file=sys.stderr) raise SystemExit(1) print(f"C2 data: {r['c2_url']}") print(f"Saved res: {a.out}")
Результаты анализа
На практике большинство SMS‑RAT кластеризуется в некоторое количество классов (мы насчитываем 4), часть из них были описаны как CLASS‑1 и CLASS‑2.
Существуют версии SMS‑RAT, в которых, помимо раннее описанного функционала, встречается удалённое управление устройством (условный «VNC»). В большинстве случаев для такого канала используют WebSocket, потому что нужен постоянный двусторонний обмен. Однако в данном случае требуется включение специальных возможностей (Accessibility Service). Активация сервиса требует от жертвы прохождения многоэтапной и нетривиальной последовательности действий в системных настройках, что сопровождается критическими предупреждениями безопасности от Android. Формально такие образцы уже ближе не к «чистому» SMS‑RAT, а к полноценному Android RAT.
На самом деле кластеризовать данное семейство можно по транспорту, то есть по каким каналам заражённое устройство взаимодействует с C2:
Firebase Cloud Messaging. Через механизм доставки пушей
Взаимодействие через Rest Api
Telegram Api (в некоторых реализация эксфильтрация происходит сразу напрямую в мессенджер)
WebSocket

Примечание
Злоумышленники так же, как и обычные пользователи, испытывают проблемы в работе при активации режима «Белых списков». Поэтому здесь злоумышленники пришли к тому, что стоит добавить ещё один резервный транспорт — это SMS‑канал. На практике наблюдается следующее: на мобильный телефон дропа, который имеет постоянный доступ в интернет и в который вставлена сим‑карта, ставится приложение — «Ретранслятор». В качестве резервного канала при сборке приложения SMS‑RAT указывается номер этого ретранслятора. В условиях недоступности C2 все сообщения, полученные на устройство жертвы, пересылаются на ретранслятор, после чего данные эксфильтруются по раннее описанным каналам.

Рекомендации для пользователей
1. Одна из основных рекомендаций для конечных пользователей — скачивать ПО только с официальных источников:
2. На устройстве важно запретить установку из неизвестных источников.
3. На устройство рекомендуется установить антивирус, но важно не забывать проверять сработки, так как он лишь предупреждает, но принудительно не удаляет приложения.
Даже если близкий человек отправляет вам неизвестный файл, и вы видите, что вам предлагается установка, обязательно проверьте, точно ли аккаунт не был взломан, перезвоните.
Всегда лучше убедиться наверняка!