Содержание статьи
Любой читатель нашего журнала как минимум что-то слышал о возможности перехвата вызовов в различных операционных системах. А скорее всего, активно этим пользовался — по крайней мере в винде. Если вкратце, ты можешь перехватить вызов какой-то функции и как-нибудь этим распорядиться: использовать параметры по своему усмотрению, выполнить свой код вместо того, который должен был выполниться. Сегодня мы будем делать это в ОС Android!
Немного теории
Достаточно каноничным примером использования хуков являются так называемые соксификаторы — фактически они перехватывают функцию подключения к серверу, подсоединяются в этот момент к SOCKS-прокси и вместо оригинального дескриптора сокета возвращают тот дескриптор, который подключен к SOCKS-серверу.
Несколько лет назад появилась прекрасная вещь — Xposed Framework для Android. Что это такое? Это фреймворк, позволяющий перехватывать вызовы Java-методов в Android-приложениях. Он дает большой простор для модификации поведения приложений, установленных на устройство, — от простого калькулятора до SystemUI, отвечающего (да-да, Кэп) за работу системного интерфейса.
Для начала давай посмотрим, как оно работает.
Внутреннее устройство Xposed Framework
В Android есть процесс под названием Zygote, он отвечает за формирование среды исполнения для каждого Android-приложения (путем форка самого себя). За его запуск отвечает бинарник app_process, который стартует в момент инициализации Android.
Xposed при установке заменяет app_process своим модифицированным, а также добавляет файл XposedBridge.jar. Модифицированный app_process загружает XposedBridge, который и перехватывает вызовы заданных методов, заменяя их своими.
Как он понимает, что чем заменять? Xposed — вещь модульная. Сам по себе он почти ничего не заменяет. При инициализации он загружает установленные модули и уже из них берет информацию о том, что перехватывать и куда передавать управление. Сами по себе модули — это, по большому счету, обычные APK, только немного дополненные специфичной для Xposed информацией.
Конечно же, есть у него и минусы — такой подход не может не сказаться на производительности и стабильности системы. Доступ к различным частям системы внутри этого фреймворка никак не контролируется — любой модуль может перехватить любой вызов, будь то обычное приложение или системное. Если «хуки» вызываются часто и работают не слишком быстро, то система начнет тормозить и кушать батарейку (хоть это и стандартное для андроида поведение, хе-хе). Если модуль плохо протестирован — в лучшем случае что-то будет некорректно работать или падать. А может выйти и так, что система вообще перестанет грузиться (я пару раз случайно доводил ее до такого состояния в процессе разработки. Помогает только перепрошивка, потому что удалить модуль на этапе загрузки системы тоже нельзя, не имея кастомного рекавери).
Ладно, хватит теории, переходим к практике! Установи Xposed на свой девайс (думаю, с этим ты разберешься сам), и поехали.
Hello, world!
Давай начнем с того, что напишем своеобразный «Hello, world» с использованием Xposed Framework.
По сути, модуль Xposed — это обычный Android-проект с дополнительными файлами. Создадим новый проект в Android Studio. Графического интерфейса у нас не будет, поэтому Activity создавать не надо. Первым делом добавим в манифест (внутрь тега <application>
) параметры, которые нужны для отображения информации о нашем модуле, а также минимально требуемую версию фреймворка.
<meta-data
android:name="xposedmodule"
android:value="true" />
<meta-data
android:name="xposeddescription"
android:value="Hello, World!" />
<meta-data
android:name="xposedminversion"
android:value="30" />
Далее нужно добавить в проект библиотеку XposedBridgeApi-54.jar
— в ней, как нетрудно догадаться из названия, содержатся классы, необходимые для работы с Xposed Framework. Обрати внимание — эта библиотека должна быть помечена как provided, а не compile.
Теперь можно начинать кодить. Создаем класс HelloWorld
, реализующий интерфейс IXposedHookLoadPackage
. Нам нужно будет переопределить лишь один метод — handleLoadPackage
. Он вызывается при запуске какого-либо пакета (как ни странно), в нем мы и будем устанавливать хуки на необходимые нам методы. Пока что этот метод будет выглядеть так:
@Override
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable {
Log.i(TAG, "Package loaded: " + loadPackageParam.packageName);
}
Для того чтобы у нас получился минимально рабочий модуль, нужно совершить еще одно действие. Создаем файлик xposed_init
в директории assets нашего проекта. В нем указываем одну строчку — полное имя класса, который нужно будет загрузить. В моем случае это pro.labster.xposedhello.HelloWorld
, где pro.labster.xposedhello
— имя пакета, HelloWorld
— имя класса.
Теперь собираем и устанавливаем APK, в статусбаре устройства появится иконка Xposed Installer’а. Нажимаем на нее и ставим галочку напротив нашего модуля. Чтобы он заработал, остается лишь перезагрузить устройство. После перезагрузки в LogCat ты должен увидеть строчки вроде этих:
08-16 01:20:25.649 873-873/? I/XposedHello Package loaded: android
08-16 01:20:29.919 873-873/? I/XposedHello Package loaded: com.android.providers.settings
08-16 01:20:29.959 873-873/? I/XposedHello Package loaded: com.sec.android.providers.security
08-16 01:20:32.749 1087-1087/? I/XposedHello Package loaded: com.android.keyguard
08-16 01:20:33.229 1087-1087/? I/XposedHello Package loaded: com.android.systemui
Это те самые логи, которые ведутся нашим кодом, — поздравляю, оно работает!
Едем дальше. Для начала давай изменим текст в статусбаре нашего девайса — там, где отображаются часы. Статусбар находится в пакете com.android.systemui
. Нужный нам метод updateClock
объявлен в классе com.android.systemui.statusbar.policy.Clock и выглядит примерно так:
final void updateClock() {
mCalendar.setTimeInMillis(System.currentTimeMillis());
setText(getSmallTime());
}
Давай же его переопределим:
if ("com.android.systemui".equals(loadPackageParam.packageName)) {
XposedHelpers.findAndHookMethod(
"com.android.systemui.statusbar.policy.Clock",
loadPackageParam.classLoader,
"updateClock",
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
TextView textView = (TextView) param.thisObject;
String text = textView.getText().toString();
textView.setText(text + " :)");
textView.setTextColor(Color.RED);
}
}
);
}
Что мы делаем? Сразу, как загрузится пакет com.android.systemui
, находим в заданном классе нужный нам метод. Как только он вызывается, срабатывает XC_MethodHook
. В нем есть два метода — beforeHookedMethod
и afterHookedMethod
. Они вызываются до и после вызова оригинального метода. Нам нужен второй, поскольку мы хотим обновить текст уже после его обновления оригинальным методом.
Собери, переустанови наш модуль, перезагрузи устройство и смотри на часы :).
INFO
При разработке модулей обязательно ориентируйся на максимальную производительность. Это действительно важно.
Более интересный пример
А теперь давай сделаем кое-что посложнее и повеселее. Представим, что перед нами стоит задача перехватить вводимый в поля текст (email’а, да и вообще чего угодно). Естественно, только для сбора статистики, ничего криминального. Как известно, полем для ввода текста в Android служит класс EditText
, а текст из него получается методом getText()
. То есть нам нужно перехватить вызов этого метода, что делается следующим образом:
XposedHelpers.findAndHookMethod(
"android.widget.EditText",
getClass().getClassLoader(),
"getText",
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
Object result = param.getResult();
if (result != null && (result instanceof SpannableStringBuilder)) {
String text = result.toString();
Log.i(TAG, "Text: " + text);
}
}
}
);
В принципе, ничего сложного. Находим метод getText
в классе android.widget.EditText
, устанавливаем на него хук. При срабатывании хука проверяем, чтобы возвращаемое значение (param.getResult()
) было не нулем и инстансом класса SpannableStringBuilder
(на всякий случай — мало ли что там какой производитель нагородит), получаем и выводим в логи текст.
Заключение
С точки зрения разработки в использовании этого фреймворка нет ничего сложного. Трудности возникают при поиске того, что именно надо переопределить, — если это опенсорсное приложение, то потребуется немного покопаться в исходниках и найти нужные методы и их сигнатуры. Сложнее, если приложение проприетарное, — тогда придется его декомпилировать.
Xposed Framework — очень интересная и мощная вещь, при этом сама по себе не вносящая серьезных изменений в систему, что делает использование фреймворка достаточно безопасным. Он, судя по количеству постов на тематических форумах, весьма популярен. Однако использовать модули и с форумов, и из официального репозитория довольно рискованно — модули там особо не модерируются, они могут серьезно замедлить твое устройство, сделать работу с ним невозможной (если модуль по какой-то причине несовместим с твоей версией ОС) или устроить что-то еще более плохое (если ты понимаешь, о чем я).
В общем, экспериментируй — Xposed Framework дает огромный простор для творчества. До встречи!