Любой читатель нашего журнала как минимум что-то слышал о возможности перехвата вызовов в различных операционных системах. А скорее всего, активно этим пользовался — по крайней мере в винде. Если вкратце, ты можешь перехватить вызов какой-то функции и как-нибудь этим распорядиться: использовать параметры по своему усмотрению, выполнить свой код вместо того, который должен был выполниться. Сегодня мы будем делать это в ОС 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 дает огромный простор для творчества. До встречи!

Оставить мнение