Обычному Android-приложению не дают прочитать /dev/input – файл, в который ядро пишет каждое касание экрана. Это и есть граница песочницы: пароль, набранный пальцем, приложению не виден, даже если оно смотрит во все глаза. RatHat эту границу переходит – и без единого эксплойта. Он уговаривает саму систему включить беспроводную отладку, спаривается с ADB-демоном телефона – с тем самым, к которому обычно цепляются с компьютера, – и поднимает шелл прямо на устройстве. С правами этого шелла /dev/input открыт, и касание в точке (540, 1624) превращается в цифру «5».
Этот шелл – мастер-ключ ко всему остальному. Из него растут аппаратный кейлоггер, который читает PIN мимо всех защит экрана, привилегированные демоны, переживающие удаление приложения, и генеративный ИИ, которому доверили водить телефон за пользователя. Ни root, ни компьютера, ни дыры в ядре – только штатный инструмент разработчика, повёрнутый против владельца.
Разобрала семейство команда Zimperium zLabs: назвала его RatHat и с осторожной формулировкой «похоже, работают из Китая» отнесла к китайским операторам. Цели – банковские и платёжные приложения по всему миру; доставка – смишинг, вредоносная реклама и сторонние витрины с APK.
Всё начинается с единственного разрешения, которое жертва даёт руками, – Accessibility. RatHat выпрашивает его локализованной HTML-страницей (svc_config.html под управлением pageStyleConfig): в одних странах – «доступ нужен из-за сетевых ограничений», в других – с финансовой приманкой. Само приложение к этому моменту уже мимикрирует под что-то безобидное: server_config.json в одной сборке выдаёт его за известный стриминговый сервис, а иконку оператор может позже переклеить, включив другой activity-алиас, – например, с подписью «Chrome».
Дальше разрешение Accessibility превращается в рычаг, которым RatHat двигает всю систему. Служба SystemHelperService синтетическими тапами открывает «О телефоне», семь раз жмёт по «Номеру сборки» – стандартный способ разблокировать «Для разработчиков», – и включает там беспроводную отладку. В этот момент Android показывает диалог спаривания с шестизначным кодом и динамическим портом. Человеку этот код нужен, чтобы ввести его на компьютере; RatHat считывает его прямо с экрана той же службой Accessibility, вытаскивая из диалога и код, и порт (а если не находит по resource-id – сканирует все TextView подряд).
Код и порт есть – остаётся спарить. Шестизначный код беспроводной отладки Android заводит именно затем, чтобы устройство, к которому подключаются, подтвердило: рядом живой человек, видящий экран. RatHat это подтверждение подделывает сам, вычитав код с того же экрана. Приложение несёт встроенную библиотеку libadb-android, проводит ею криптографический handshake ADB с демоном собственного телефона по петле 127.0.0.1 и получает то, ради чего вся пляска затевалась, – шелл в контексте /data/local/tmp с UID 2000. Компьютер в схеме не участвовал ни секунды: телефон спарился сам с собой.
Спаривание не одноразовое. Handshake оставляет доверенную пару ключей, и её RatHat не выбрасывает, а выгружает на сервер – cert.pem и приватный ключ устройства уходят на C2 отдельным эндпоинтом. Операторский доступ к ADB тем самым закрепляется криптографически: даже если конкретная сессия оборвётся, ключ, которому демон уже доверяет, остаётся на руках у атакующего.
Ровно этот трюк – беспроводной ADB как вектор без единого клика по установке – мы уже разбирали у малвари RedHook. RatHat доводит его до логического конца: там ADB открывал дверь, здесь он становится несущей осью всей операции, и почти каждая следующая возможность упирается в наличие этого шелла.
Шелл ADB – это не root. UID 2000 не даёт писать в системный раздел, не снимает SELinux и не открывает чужие приложения впрямую. Соблазн был бы взять именно root – через эксплойт ядра, дающий полную власть. RatHat идёт другим путём, и в этом выборе видна холодная инженерия: эксплойт ядра стоит рабочего 0-day или 1-day, живёт до ближайшего патча, ломается на смене прошивки и почти всегда завязан на конкретную модель. Самоспаривание с ADB не стоит ни одной уязвимости, работает на непропатченном и патченном телефоне одинаково и переносится между брендами. Оператор разменял избыточную мощь рута на дешевизну и живучесть – и не прогадал, потому что shell UID 2000 закрывает ровно те задачи, которые ему нужны.
А нужно ему немного, но точно. Из шелла Go-агент раздаёт приложению разрешения командой pm grant, включая WRITE_SECURE_SETTINGS – ключ к системным настройкам, который обычному приложению не выдают никогда. Из шелла же открывается /dev/input: чтение сырого потока касаний – привилегия, к которой у UID 2000 доступ есть, а у любого приложения нет. Цена, которую оператор платит за отказ от рута, – хрупкость входной хореографии: вся цепочка с тапами по «Номеру сборки» и вычиткой кода из диалога держится на конкретной вёрстке настроек, а она у каждого вендора и каждой локали своя. Как RatHat с этим борется – отдельная история, и в ней замешан ИИ.
Самый сильный из трёх кейлоггеров RatHat не читает ни текста, ни имён полей – он читает пальцы. Работает он в Go-агенте, в шелл-контексте, и опирается на штатный отладочный инструмент getevent, который выводит сырой поток из /dev/input:
/dev/input/event2: EV_ABS ABS_MT_POSITION_X 0000021c
/dev/input/event2: EV_ABS ABS_MT_POSITION_Y 00000658
/dev/input/event2: EV_SYN SYN_REPORT 00000000
Здесь нет ни текста, ни названия приложения, ни намёка на то, что было на экране, – только X, Y и момент времени. EV_ABS с ABS_MT_POSITION_X/Y – это отчёты мультитач-драйвера о положении пальца, а SYN_REPORT закрывает один пакет событий; 0x021c – это 540, 0x0658 – 1624: палец коснулся точки (540, 1624), и метка времени ставит это касание в очередь среди прочих. Сама по себе координата бесполезна, и в этом весь фокус защиты: обычному приложению /dev/input недоступен, читать его может только шелл UID 2000 – тот самый, что RatHat добыл через самоспаривание. Ради этой возможности, среди прочих, вся ADB-пляска и затевалась.
Вторая половина приёма – файл locateValues.json, где лежат раскладки экранных клавиатур под каждый крупный бренд: где на PIN-паде Samsung сидит «0», где «5», какова геометрия сетки графического ключа. Координату накладывают на раскладку – и касание (540, 1624) на «пин-паде Samsung» становится цифрой «5». Графический ключ восстанавливают так же: последовательность точек сопоставляют с сеткой 3×3, этим занимаются классы Point и DotAlign в пакете cipher. Восстановленные так учётки RatHat помечает отдельным типом PASSWORD_QUALITY_TOUCH_POINTS – он сам знает, что добыл их из геометрии, а не из текста.
Красота приёма в том, что он проходит сквозь защиты, поставленные ровно против других техник. FLAG_SECURE на окне блокирует скриншоты и чтение через Accessibility – но не трогает драйвер ввода. Кастомная клавиатура не отдаёт системе набранный текст – но пальцы всё равно бьют по экрану. Локскрин намеренно прячет цифры PIN от Accessibility – а координаты касаний это не задевает. Все три рубежа обороны Android оперируют на уровне текста и картинки; RatHat спускается под них, к железу.
Аппаратный сбор – козырь, но RatHat держит и два более традиционных механизма поверх той же Accessibility. UniversalInputMonitor слушает события изменения текста и восстанавливает набранное, отслеживая по каждому событию beforeText, eventText, nodeText, hintText, fieldId и пакет. Поля с секретами он классифицирует по спискам PASSWORD_KEYWORDS, PIN_KEYWORDS и ENTER_PASSWORD_KEYWORDS, а с маскированным вводом работает отдельно – собирает содержимое поля, показывающего одни точки, по посимвольным записям char_.
Второй, KeystrokeCapture, специализируется на адресных строках браузеров и читает их напрямую по resource-id: url_bar у Chrome и Edge, url_field у Opera, omnibarTextInput у DuckDuckGo, location_bar_edit_text у браузера Samsung – плюс AOSP и ColorOS. То, что человек вбивает в адресную строку, часто ценнее пароля: это карта его интересов, сервисов и рабочих порталов.
Реальную власть RatHat носит не в APK, а в двух Go-бинарях, замаскированных под нативные библиотеки. Первый, liblocal-service.so, приложение роняет в /data/local/tmp и запускает через добытый шелл – это командный мозг, работающий в контексте ADB, а не приложения. Именно он выдаёт системные привилегии и вырывает приложение из-под контроля питания; показательно, что этих команд нет в DEX приложения – они существуют только строками внутри Go-бинаря:
dumpsys deviceidle whitelist +<pkg> # вывести из-под Doze
am set-standby-bucket <pkg> active # держать в приоритете фона
pm disable-user --user 0 <pkg> # выключить чужой пакет
pm uninstall -k --user 0 <pkg> # снести пакет (напр. антивирус)
Разделение неслучайно: то, за что Play Protect и статический анализ цепляются в первую очередь, вынесено из проверяемого DEX в непрозрачный нативный бинарь, который к тому же исполняется не как приложение. Приложение общается с этим агентом по локальной петле – HTTP-сервер поднят на 127.0.0.1:7910, и через неё же, уже из своего привилегированного контекста, агент раздаёт приложению разрешения и включает исключения по питанию. Порядок в операционном потоке жёсткий: сперва Accessibility и синтетические тапы поднимают отладку, затем из UI вычитываются код и порт, libadb-android спаривается с демоном, в /data/local/tmp стартуют оба Go-бинаря, и только после этого агент по петле выдаёт pm grant, WRITE_SECURE_SETTINGS и вытягивает конфиг туннеля. Умеет агент и обновлять себя: проверяет свою версию на сервере (/api/v2/dev/agentVersion) и подкладывает новую сборку как /data/local/tmp/local-service.update.
Второй бинарь, libmedia_codec.so, – это frpc, клиент обратного прокси из проекта fatedier/frp, положенный в /data/local/tmp/frpc. Его единственная работа – поднять постоянный обратный туннель к серверу оператора, взяв адрес, порт и токен из конфига, который Go-агент тянет с C2 (main.fetchFrpcConfigFromServer). Через этот туннель ADB-демон устройства выставляется в интернет мимо NAT и файрвола: получается не набор функций малвари, а общая дорога внутрь телефона, по которой оператор проводит что угодно, независимо от того, что умеет сам RatHat.
Вся входная хореография – тапнуть по «Номеру сборки», найти кнопку включения отладки, вычитать код из диалога – спотыкается об один факт: у каждого вендора и каждой локали интерфейс свой. Жёстко прошитые координаты и селекторы здесь ломаются на первом же непредусмотренном телефоне. RatHat решает это неожиданно: сериализует живое дерево Accessibility в XML и отправляет его одному из самых популярных публичных ИИ-ассистентов, а тот возвращает, куда тапать.
Запросы формально безобидные: найти на экране центр названной цели и вернуть его координаты в JSON под синтетический тап; вернуть фактический экранный текст элемента из XML; подсказать команду навигации вроде SCROLL_DOWN. Модель не подозревает, что участвует в атаке, – она просто разбирает разметку интерфейса. Но именно эти промпты, по замечанию Zimperium, и указали на китайский след операторов.
Инженерно это сильный ход и одновременно дорогой. Сильный – потому что скриптовая автоматизация под каждый бренд не масштабируется, а ИИ-навигатор адаптируется к незнакомому интерфейсу на лету, чего жёсткая логика не умеет. Дорогой – потому что каждый шаг автоматизации теперь уходит наружу, к публичному ИИ-сервису: это и сетевой след, отличимый в трафике, и внешняя зависимость, без которой навигация встаёт, и – как выяснилось – канал утечки атрибуции. Классическая скриптовая логика молчала бы в сеть и не привязывала бы операцию ни к какому провайдеру; RatHat разменял эту тишину на гибкость. Для массового банкера, которому важнее заражать пёстрый парк устройств, чем прятаться, размен оправдан – но он же и подставил операторов.
Собранный доступ RatHat монетизирует привычным для банкеров способом – оверлеями, но с двумя разными движками. Для банковских приложений работает инъекционный движок на WebView: малварь следит за активным приложением и, увидев целевой пакет на переднем плане, накрывает его фальшивым HTML-экраном, который прислал оператор под конкретный банк. Пользователь видит знакомую форму логина и вводит учётку прямо в руки атакующему.
Отдельно RatHat целит в платёжные приложения – WeChat и Alipay: под них оверлеи не тянутся с сервера, а вшиты прямо в код, и заточены под кражу платёжного PIN. Выбор говорящий: банковские шаблоны меняются и раздаются гибко, а WeChat с Alipay настолько приоритетны и стабильны, что их обманки зафиксировали в бинаре намертво.
Украденной учётки для банка мало – её прикрывает одноразовый код, и RatHat снимает его прямо в момент прихода. Работают в паре два компонента: SMS-приёмник читает тело входящего сообщения, а слушатель уведомлений перехватывает то, что прилетает пушем. Логин добывает оверлей, второй фактор – эта связка, и на выходе у оператора оба элемента, нужные для полного захвата счёта. Именно ради такого комплекта банкер и городит весь остальной огород: пароль без OTP сегодня почти бесполезен, поэтому перехват второго фактора для RatHat не побочная функция, а половина смысла.
Смотреть на экран жертвы RatHat умеет двумя разными путями, и разница между ними – ровно в том, спрашивают ли согласие. Первый, штатный, – MediaProjection: приложение стримит экран, но Android на это показывает системный диалог-подтверждение, который жертва должна нажать. Второй путь согласия не спрашивает вовсе. Через тот же шелл Go-агент ведёт мониторинг экрана на уровне ADB (/api/v2/dev/data/screen-monitor), независимо от MediaProjection и его диалога, а инструмент захвата minicap подтягивается с сервера под конкретную архитектуру, версию SDK и бренд устройства (/api/bin/.../minicap.so). Видео уходит на C2 чанками, отдельным конвейером загрузки.
Разнесение и тут не случайно. MediaProjection – это разрешённый, «легальный» способ, который заметен пользователю в момент включения; shell-level мониторинг ту же картинку снимает тихо, ценой того, что держится он на добытом ADB-доступе, а не на API, и падает вместе с шеллом. Оператор оставил себе оба варианта: громкий, но универсальный, и тихий, но завязанный на выход из песочницы.
Поверх картинки RatHat умеет и файловый обмен в обе стороны: с устройства утянуть нужное, на устройство подложить своё. Для банкера это не главная функция, но полезная – так с телефона выгребают документы, фото и всё, что жертва хранит рядом с деньгами.
Первый рубеж персистентности RatHat – наглый и простой. Перехватив системный диалог удаления, малварь рисует поверх него фальшивый экран «сбоя Google Play» и отменяет деинсталляцию: пользователь думает, что удаление не прошло по технической причине.
Но настоящая живучесть глубже, и держится она на том же шелле. local-service запущен вне жизненного цикла пакета – как отдельный процесс в /data/local/tmp, а не как компонент приложения. Поэтому удаление APK его не трогает: приложение снесли, а шелл на устройстве остался. Сервис проверяет, на месте ли приложение, и если нет – переустанавливает его сам, без единого касания пользователя, разом возвращая и все разрешения, и Accessibility:
pm install -r -g -i com.android.packageinstaller /data/local/tmp/app.apk
settings put secure enabled_accessibility_services '<component>'
settings put secure accessibility_enabled 1
Пока же приложение живо, отдельный heartbeat переразворачивает и перезапускает local-service, если тот перестал отвечать, пересканирует лестницу ADB-портов и при нужде заново включает отладочные настройки. Инженерно это самая крепкая часть операции: обычный пользователь на вопрос «как убрать заразу» отвечает «удалить приложение», а здесь именно это действие ничего не решает – корень операции лежит вне пакета. Цена у решения тоже есть: процессы в /data/local/tmp видны любому, у кого есть ADB, и не переживают перезагрузку сами по себе – их держит на плаву вся та же хрупкая пляска с переспариванием, которую heartbeat вынужден повторять.
Ко всему прочему приложение может запросить права Device Admin, а с ними – возможность стереть устройство при попытке удаления. Угроза, работающая уже на нервах жертвы, а не на технике.
Всё это упаковано так, чтобы аналитику было больно с первого касания. Устанавливает малварь дроппер, несущий полезную нагрузку в двух зашифрованных ассетах: трансформ тривиальный – пропустить 24-байтный заголовок, прочитать длину big-endian, прогнать каждый байт через ((b ^ 0x4A) - 0xD2) ^ 0xF1 и распаковать gunzip; вторая стадия – DEX, поднятый в память рефлексией. Ставится всё через нативные API SessionInstaller в обход ограниченных настроек и защит Accessibility.
Поверх лежат четыре слоя, каждый бьёт по своему инструменту анализа:
libziparchive в Android это игнорирует и ставит пакет штатно, а unzip, apktool и прочий инструментарий спотыкаются.AndroidManifest.xml раздут до 61 МБ, и 99% его – два чанка недокументированного типа 0x9999, вставленные между пулом строк и реальным потоком элементов. Нативный парсер Android неизвестный чанк просто пропускает, а декомпиляторы либо не разбирают нестандартную структуру, либо упираются в лимит памяти и падают.element_width. ART их и не думает исполнять, а вот дизассемблер честно пытается вычислить длину такой нагрузки – и ломается на разборе.StringFog (base64, XOR с побайтовым ключом) малварь накидывает схему StringCrypto: base64, своп соседних байт-пар и XOR 16-байтным ключом.Отдельно – шесть проверок на отладку: JDWP через Debug.isDebuggerConnected, чтение TracerPid из /proc/self/status против нативных отладчиков, флаг FLAG_DEBUGGABLE (ловит переупаковку APK аналитиком), getprop на ro.debuggable/ro.secure, поиск Frida (TCP-проба порта 27042, ps с грепом по «frida», скан /proc/self/maps на libfrida-agent.so) и связка на Xposed, root и эмулятор – два десятка путей и пакетов, свойство persist.sys.xposed.enable, отпечаток сборки и узлы устройств QEMU.
Управление RatHat держит не в одном канале, а в трёх, и это осознанное разнесение. Приложение регистрируется по HTTP, затем держит открытым WebSocket с heartbeat; запросы подписаны HMAC-SHA256 по ключу устройства, выведенному из deviceKeySalt. Go-агент поднимает собственный HTTP-сервер на 127.0.0.1:7910 и ведёт свой командный канал (/api/v2/dev/tasks), не зависящий от WebSocket приложения, – если приложение снесут или заглушат, у агента остаётся своя линия связи, а офлайн-очередь докладывает накопленные за простой команды на переподключении.
Несколько эндпоинтов стоит назвать отдельно, потому что они выдают замысел. При заражении устройство регистрируется и снимает свой отпечаток (/api/node/register), заодно отдавая инвентарь установленных приложений – оператор сразу видит, есть ли на телефоне целевой банк, и стоит ли вообще тратить на эту жертву шаблоны оверлеев. /api/data/cipher синхронизирует добытые PIN, паттерн и пароль через агента (main.syncCipherToServer), и синхронизирует их именно агент – поэтому кража доезжает до сервера, даже когда приложение уже снесли. А /api/sapp подтягивает список «чувствительных» пакетов, на которых Accessibility-обработку надо приглушить, – маскировка, которую можно менять с сервера, не пересобирая билд; в пару к нему идёт /api/bapp со списком блокируемых. Вместе с уже упомянутой выгрузкой ключей ADB эти каналы показывают инженерный почерк: доступ, добыча и скрытность – каждое со своим отдельным механизмом и своим отдельным эндпоинтом, а не свалено в один канал.
RatHat собран из знакомых кирпичей – оверлеи, перехват SMS, кража учёток, – но держится он на другом фундаменте. Обычный Android-банкер живёт внутри песочницы и упирается в её потолок: чего Accessibility и разрешения не дают, того он не может. RatHat потолок пробивает снизу – самоспариванием с ADB он выносит свою рабочую часть в шелл-контекст, куда защиты приложения не дотягиваются, а корень операции держит вне жизненного цикла пакета, где его не достаёт даже удаление. Аппаратный кейлоггер, привилегированные демоны, обратный туннель, переустановка без пользователя – всё это следствия одного решения: не ломать песочницу эксплойтом, а выйти из неё через собственные инструменты разработчика телефона.
Дороже всего операторам обошёлся ИИ-навигатор: гибкость, которую он дал заражению пёстрого парка устройств, оплачена наблюдаемым каналом наружу и утечкой собственной атрибуции. Zimperium держится осторожной формулировки о китайском следе, и это правильный регистр: промпты и цели вроде WeChat и Alipay указывают в одну сторону, но указывают, а не доказывают. Куда важнее географии другое – RatHat показывает, куда движется мобильная угроза: не громкий эксплойт, а тихий выход из песочницы штатными средствами, адаптивная автоматика на живой модели и корень, который переживает то единственное действие, которым пользователь надеется от него избавиться.
Разбор семейства, реверс Go-агента и механизма касание-в-PIN, а также осторожную атрибуцию – Zimperium zLabs.
Автор: hacker@shifry.local