Логотип ШифрыШифры
ГлавнаяЖурналЛотыУзел ИИВойти
ГлавнаяЖурналЛотыУзел ИИ
Войти
Логотип ШифрыШифры2К26Версияъ1
EmailMAX
Назад к логу

RatHat сам включает отладку на Android, выходит из песочницы без ПК и читает PIN по касаниям

23 сентября 2026 г.•Репа
#атаки#железо#ИИ

Обычному 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 с этим борется – отдельная история, и в ней замешан ИИ.

Касание в точке (540, 1624) – это цифра 5

Самый сильный из трёх кейлоггеров 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 – он сам знает, что добыл их из геометрии, а не из текста.

Схема восстановления PIN по касаниям: getevent читает сырые координаты из /dev/input, locateValues.json даёт раскладку клавиатуры, наложение возвращает цифру PIN, минуя FLAG_SECURE, кастомную клавиатуру и локскрин
Сырая координата касания плюс раскладка клавиатуры конкретного бренда дают цифру PIN – там, где скриншот и чтение текста уже заблокированы

Красота приёма в том, что он проходит сквозь защиты, поставленные ровно против других техник. 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 из песочницы Android: Accessibility → SystemHelperService включает Wireless Debugging → скрейп кода спаривания → libadb-android спаривается с локальным ADB-демоном → shell UID 2000 → Go-агент и frpc → C2
Приложение из песочницы дотягивается через Accessibility до ADB-демона собственного телефона и получает шелл вне песочницы – без эксплойта и без компьютера

ИИ вместо жёстко прошитых координат

Вся входная хореография – тапнуть по «Номеру сборки», найти кнопку включения отладки, вычитать код из диалога – спотыкается об один факт: у каждого вендора и каждой локали интерфейс свой. Жёстко прошитые координаты и селекторы здесь ломаются на первом же непредусмотренном телефоне. RatHat решает это неожиданно: сериализует живое дерево Accessibility в XML и отправляет его одному из самых популярных публичных ИИ-ассистентов, а тот возвращает, куда тапать.

Запросы формально безобидные: найти на экране центр названной цели и вернуть его координаты в JSON под синтетический тап; вернуть фактический экранный текст элемента из XML; подсказать команду навигации вроде SCROLL_DOWN. Модель не подозревает, что участвует в атаке, – она просто разбирает разметку интерфейса. Но именно эти промпты, по замечанию Zimperium, и указали на китайский след операторов.

Инженерно это сильный ход и одновременно дорогой. Сильный – потому что скриптовая автоматизация под каждый бренд не масштабируется, а ИИ-навигатор адаптируется к незнакомому интерфейсу на лету, чего жёсткая логика не умеет. Дорогой – потому что каждый шаг автоматизации теперь уходит наружу, к публичному ИИ-сервису: это и сетевой след, отличимый в трафике, и внешняя зависимость, без которой навигация встаёт, и – как выяснилось – канал утечки атрибуции. Классическая скриптовая логика молчала бы в сеть и не привязывала бы операцию ни к какому провайдеру; RatHat разменял эту тишину на гибкость. Для массового банкера, которому важнее заражать пёстрый парк устройств, чем прятаться, размен оправдан – но он же и подставил операторов.

Оверлеи под банки, WeChat и Alipay

Собранный доступ 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.

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

  • Порча контейнера. Часть файлов в ZIP объявлена директориями либо помечена битом шифрования. libziparchive в Android это игнорирует и ставит пакет штатно, а unzip, apktool и прочий инструментарий спотыкаются.
  • Манифест-бомба. AndroidManifest.xml раздут до 61 МБ, и 99% его – два чанка недокументированного типа 0x9999, вставленные между пулом строк и реальным потоком элементов. Нативный парсер Android неизвестный чанк просто пропускает, а декомпиляторы либо не разбирают нестандартную структуру, либо упираются в лимит памяти и падают.
  • Отравление байткода DEX. Изрядная часть кода несёт псевдоинструкции с невалидным 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.

C2 в три канала

Управление 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