Не удалось прогрузить медиа
Всё что ниже, я перепроверил на живом семпле перед публикацией, хэш сходится, инфраструктура частично ещё дышит. Так что это не пересказ чужого протухшего отчёта, а актуальный срез прям на дату статьи.
Не удалось прогрузить медиа
Не удалось прогрузить медиа
Начнём с того, что видно снаружи, без всякого реверса. APK весит 3.2 мб, targetSdk 36 (Android 16), minSdk 24 (Android 7). Автор жадный, хочет покрыть максимально широкий парк, от древней семёрки до свежего шестнадцатого андроида, чтоб ни одна жертва не ушла. Имя приложения, VID_061125_27829. Классический паттерн "видосика", жертва думает что это видеозапись от 11 июня 2025 года, а на .apk расширение либо не смотрит, либо ей на голубом глазу впаривают что "надо доустановить кодек для просмотра". И вишенка, после установки приложение реально открывает ссылку на rutube (https://rutube.ru/video/30b351d2c7e3e122e352e448a5cee6ae/), чтоб жертва увидела какое-то видео и не почуяла подвох. Смотри, мол, всё честно. А в фоне в это время работает совсем другое кино.
Не удалось прогрузить медиа
Первое что бросается в глаза при разборе манифеста, это пермишены. Вот полный список того, что просит у вас невинный "видеофайл":
- READ_SMS, RECEIVE_SMS, SEND_SMS - полный контроль над SMS
- BROADCAST_SMS, BROADCAST_WAP_PUSH - системные бродкасты SMS/MMS (нужны как раз приложению-обработчику SMS)
- SEND_RESPOND_VIA_MESSAGE - "ответить сообщением" на звонок, тоже атрибут SMS-приложения
- READ_CALL_LOG, READ_PHONE_STATE, READ_PHONE_NUMBERS - звонки и номера
- RECEIVE_MMS - перехват MMS
- RECEIVE_BOOT_COMPLETED - автозапуск после перезагрузки
- INTERNET, ACCESS_NETWORK_STATE - сеть
- FOREGROUND_SERVICE, FOREGROUND_SERVICE_SPECIAL_USE - фоновая работа
- POST_NOTIFICATIONS - уведомления
- QUERY_ALL_PACKAGES - список установленных приложений
- WAKE_LOCK - не даёт телефону засыпать
- DUMP, BIND_JOB_SERVICE - служебные (WorkManager/диагностика)
Видеоплееру не нужен доступ к SMS. Ну вот вообще ни разу. Но жертва это всё не читает, она на автомате жмёт "Разрешить" по всему списку, лишь бы видео показали. И связка BROADCAST_SMS + BROADCAST_WAP_PUSH + SEND_RESPOND_VIA_MESSAGE это ровно тот комплект, который Android требует от приложения, претендующего на роль "приложения по умолчанию для SMS". Запомните этот момент, мы к нему ещё вернёмся, он тут ключевой.
Не удалось прогрузить медиа
Лезем глубже. И вот тут главное, что я хочу поправить относительно того как такие штуки обычно описывают в пабликах. Это НЕ "пассивный перехватчик SMS", как любят писать. Это приложение, которое делает себя приложением по умолчанию для SMS (default SMS app). Разница огромная, сейчас поймёте. Main Activity при запуске (метод ko09zE6UAgwkV в декомпиляции) делает вот что: на Android 10+ дёргает RoleManager.createRequestRoleIntent("android.app.role.SMS"), на более старых, Intent "android.provider.Telephony.ACTION_CHANGE_DEFAULT". То есть жертве вылетает системный диалог "Сделать VID_061125_27829 приложением для SMS по умолчанию?". И многие жмут "Да", думая что это нужно для просмотра "видео". А как только приложение стало дефолтным SMS-приложением, оно получает полный, тихий и абсолютно легальный с точки зрения системы доступ ко всем SMS, на чтение, отправку, и удаление из входящих. Никаких хитрых обходов ограничений Android, никаких рут-эксплойтов, ничего. Жертва сама, своими ручками отдала роль.
Именно поэтому в манифесте зарегистрированы такие компоненты:
- SmsReceiver - на action android.provider.Telephony.SMS_DELIVER (это бродкаст, который получает ТОЛЬКО приложение по умолчанию, в отличие от обычного SMS_RECEIVED)
- MmsReceiver - на WAP_PUSH_DELIVER (MMS для дефолтного приложения)
- CallReceiver - на android.intent.action.PHONE_STATE (состояние звонка)
- HeadlessSmsSendService - на RESPOND_VIA_MESSAGE, отправка SMS без UI
Эти четыре компонента и есть обязательный набор для роли SMS-приложения. То есть приложение может читать всё входящее, рассылать SMS от имени жертвы и она даже не увидит их в своих отправленных, а при желании и стирать сообщения банка из инбокса. Красота, правда? Для автора, не для жертвы.
Не удалось прогрузить медиа
Декомпиляция через jadx показала неплохую обфускацию. Сборка прогнана через R8, все классы и методы переименованы в рандомные строки типа VSncX8ksA0tswc2TMhzhOMdd, ej6unYlOWMDF, klTFaDtrPxx8 (да, глаза ломаются). Плюс ровно 90 фейковых Activity (по 3 на каждый из 30 "модулей", 30x3=90), которые не делают ничего полезного, чистый мусор чтоб аналитик захлебнулся. Названия модулей тоже рандомные: loxrvbb27, gwhuycb28, ajdavdb29, zpprdc2 и так далее.
Есть и трюк против ленивого анализа прямо на уровне самого APK-архива, и вот это уже интересно. У ключевых записей внутри zip (classes.dex, resources.arsc, AndroidManifest.xml) выставлен бит "шифрования" в заголовке. Файл на самом деле не зашифрован, но обычный unzip упирается, пишет "unable to get password" и пропускает их. Расчёт на то, что аналитик-новичок распакует APK, не увидит DEX и решит что он битый, руки опустит. А Android-загрузчик этот флаг игнорирует, jadx тоже читает спокойно, так что на реальную работу или на нормальный анализ это никак не влияет, чистая помеха для распаковки "в лоб".
Но обфускация обфускацией, а строки в DEX и ресурсах никуда не денешь. Грепаем и находим самое вкусное.
Не удалось прогрузить медиа
Первое, C2-инфраструктура. И вот тут всё оказалось интересней чем я ждал. В коде захардкожен JSON-массив с четырьмя IP:
95.215.108.110
2.59.161.117
185.214.74.200
147.45.219.172
Пробуем ткнуть каждый. На момент написания статьи два из четырёх мёртвы (95.215.108.110 и 147.45.219.172 не отвечают вообще, таймаут). Два живых (2.59.161.117 и 185.214.74.200) на любой запрос возвращают HTTP 407 Proxy Authentication Required с заголовком - Proxy-Authenticate: Basic realm="sing-box" charset="UTF-8"
sing-box, это популярный прокси-инструмент (форк v2ray/xray).
То есть эти четыре IP, это не C2 напрямую. Это прокси-ноды, через которые малварь подключается к реальному бэкенду. Фронт, который прикрывает настоящий сервер. И вот тут самое вкусное. 407 нас сначала остановил, но потом я подумал, а чё я туплю, у меня же есть сам бот, полез смотреть КАК он ходит через эти прокси. Сетевой слой написан на OkHttp, и в классе VSncX8ksA0tswc2TMhzhOMdd видно, что перед каждым запросом малварь поднимает прокси руками: new Proxy(Proxy.Type.SOCKS, new InetSocketAddress(ip, 80)). То есть я стучался не туда, это не HTTP-прокси, а SOCKS на 80-м порту (sing-box на mixed-inbound принимает и то и другое). А креды для авторизации лежат прямо в коде, в отдельном классе-Authenticator (KeEAqltnBZY), автор даже не прятал:
- логин: mgasogij#fhmodf#4u98yGJSDNE79235
- пароль: aSGKJFIGEIUGJNBEbdNI)EGBJH)G08U7*#50u(#UJGVNEI*BIOBNWRBHBJNIObinjr0woWEB
Зашиты через Authenticator.setDefault, общие на все ноды. Окей, берём эти креды и стучимся по правильному пути, тем же SOCKS5 что и бот. Auth проходит (407 больше нет, нода нас пускает), но дальше нода отказывается форвардить соединение куда-либо, даже на сторонний безобидный хост ответ пустой, коннект рвётся (SOCKS5 reply 1, general failure / CONNECT aborted). То есть ноды настроены как закрытый релай строго под бот-трафик, по чужому подключению egress зарезан. Так что да, креды у нас есть, путь восстановлен полностью.
Не удалось прогрузить медиа
А настоящий C2 нашёлся в ресурсах APK. В файле resources.arsc лежат строки, которые малварь подтягивает через getResources().getString(). Автор даже не стал прятать их за безликими именами, строки лежат с говорящими названиями прямо в values/strings.xml, бери да копируй:
- HOST: https://194.41.113.39:8081 - реальный адрес C2-сервера, порт 8081, HTTPS
- BRANCH: desomorphine - кодовое имя кампании (ботнета)
- INITIAL_MESSAGES: {+79830130598:retr} - номер телефона оператора с тегом "retr", думаю просто где-то в РФ стоит симбокс, а ребята с Днепра им рулят)
- URL: https://rutube.ru/video/30b351d2c7e3e122e352e448a5cee6ae/ - видео-прикрытие
- WORKER: брехня уже не маленький сука - строка автора на русском (без комментариев).
Итого архитектура: малварь -> sing-box прокси-ноды (4 штуки) -> настоящий C2 на 194.41.113.39:8081. Прокси-слой нужен чтоб при блокировке одного IP бот переключился на другой, а настоящий сервер оставался нетронутым. Классика с резервированием, всё как у взрослых.
Попытка ткнуть реальный C2 (194.41.113.39:8081) напрямую тоже ничего не дала. Порт 8081 открыт и принимает TCP-подключение, но на TLS handshake сервер не отдаёт никакого сертификата и рвёт соединение (unexpected eof). Перебор SNI (desomorphine, localhost и пр.) ничего не меняет. И через сами прокси-ноды (даже с правильными кредами) C2 наружу тоже не выкатывается, нода обрывает forward. То есть бэкенд доступен только по тому пути и тем рукопожатием, что знает сам бот, снаружи к панели не подойти ни напрямую, ни через релей. Важно что порт живой и две из четырёх нод ещё авторизуют подключения, то есть инфраструктура на момент анализа ещё поднята и обслуживается.
Не удалось прогрузить медиа
Не удалось прогрузить медиа
API-эндпоинты на C2:
/register - регистрация нового бота. Отправляет UID устройства, список установленных банковских приложений, номер телефона, и branch (desomorphine)
/sms - отправка перехваченных SMS на сервер
/call - отправка информации о входящих звонках
/sms_command - получение команд от оператора
Разберём как работает перехват SMS. SmsReceiver ловит бродкаст (SMS_DELIVER как дефолтное приложение, и для совместимости в коде обрабатывает и SMS_RECEIVED), выдёргивает номер отправителя и текст сообщения, и сразу шлёт на C2 через HTTP POST на эндпоинт /sms (в пайлоад идёт UID устройства, номер, текст и branch). Но тут есть хитрость, и она похуже самого перехвата. Малварь также парсит входящие SMS на наличие ключевых слов "Номер: " и "Текст: ". Если оба найдены, она извлекает номер и текст из тела SMS и ЧЕРЕЗ SmsManager.sendTextMessage ОТПРАВЛЯЕТ SMS на этот номер с этим текстом. То есть оператор может слать команды через SMS на заражённый телефон: "Номер: +79991234567 Текст: Привет", и телефон жертвы сам отправит это сообщение. Рассылка спама и фишинга с реальных номеров реальных людей, а крайним окажется владелец телефона. И самое приятное, канал SMS-команд работает даже если интернет на телефоне отвалился, это резервный канал управления.
Не удалось прогрузить медиа
CallReceiver работает проще, при входящем звонке (состояние RINGING) перехватывает номер звонящего и отправляет его на C2 через /call. Зачем? Чтоб оператор знал кто звонит жертве. Если звонит банк ("Мы заметили подозрительную активность на вашем счёте.."), оператор может среагировать. Или перезвонить жертве первым с подменного номера и отыграть того самого "сотрудника банка". Удобно, чё.
Не удалось прогрузить медиа
Отдельно интересен список целевых приложений, зашитый прямо в коде. Малварь проверяет какие из них у тебя стоят (через QUERY_ALL_PACKAGES) и сливает список при регистрации на C2:
Банки:
- ru.sberbankmobile (Сбер)
- com.idamob.tinkoff.android (Тинькофф)
- ru.vtb24.mobilebanking.android (ВТБ)
- ru.alfabank.mobile.android (Альфа-Банк)
- ru.mts.bank (МТС Банк)
- ru.gazprombank.android.mobilebank.app (Газпромбанк)
- ru.otpbank.mobile (ОТП Банк)
- ru.bankuralsib.mb.android (УралСиб)
- ru.letobank.Prometheus (Лето Банк)
Маркетплейсы и другое:
- com.wildberries.ru (Wildberries)
- ru.ozon.fintech.finance (Ozon Финансы)
- com.valvesoftware.android.steam.community (Steam)
- ru.tsk.ftc.bender.qpay (QPay)
Оператор получает полный профиль жертвы, какие банки, какие магазины, есть ли Steam. Дальше под конкретного человека подбирается атака. Наличие Steam говорит о том что авторы не брезгуют и игровыми аккаунтами, не только банковскими. Гребут всё что плохо лежит.
Не удалось прогрузить медиа
Не удалось прогрузить медиа
Не удалось прогрузить медиа
Не удалось прогрузить медиа
👍30
❤️4
😁4
🤔2
😢8
LIMB