В Android приложения с поддержкой root имеют особый статус. Во-первых, они будут работать только на рутованных смартфонах, то есть у довольно узкого круга пользователей. Во-вторых, возможности root-приложений безграничны. Они могут удалять или устанавливать системные приложения, изменять системные конфиги, прошивать в смартфон кастомные ядра или прошивки, да и вообще без проблем превратят смартфон в кирпич. Думаешь, реализовать все это сложно? Отнюдь!

Написание приложений с поддержкой прав root сильно отличается от традиционного программирования для Android. И не потому, что нам придется задействовать низкоуровневые системные API (хотя это тоже возможно), а потому, что, по сути, мы будем иметь дело с консолью и ее командами. То есть в буквальном смысле нажатия кнопочек на экране будут приводить к исполнению консольных команд. Поэтому первое, что мы должны сделать, — это научиться запускать команды без прав root.

 

Командуем

Запуск внешних команд в Android выполняется точно так же, как и в Java, а именно с помощью такой строки:

Runtime.getRuntime().exec("команда");

Правда, в коде придется обрамить ее try/catch:

try {
    Runtime.getRuntime().exec("команда");
} catch (IOException e) {
      throw new RuntimeException(e);
}

Метод exec() запускает шелл и команду в нем в отдельном потоке. После завершения команды поток дестроится. Все просто и понятно, но вот толку нам от этого мало — вывода команды мы все равно не видим. Чтобы решить эту проблему, мы должны прочитать стандартный выходной поток, для чего понадобится примерно такая функция:

public String runCommand(String cmd) {
      try {
          // Выполняем команду
        Process process = Runtime.getRuntime().exec(cmd);

          // Читаем вывод
        BufferedReader reader = new BufferedReader(
            new InputStreamReader(process.getInputStream()));
        int read;
        char[] buffer = new char[4096];
        StringBuffer output = new StringBuffer();
        while ((read = reader.read(buffer)) > 0) {
            output.append(buffer, 0, read);
        }
        reader.close();

          // Дожидаемся завершения команды и возвращаем результат
        process.waitFor();
        return output.toString();
    } catch (IOException e) {
        throw new RuntimeException(e);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
}

Теперь у нас есть возможность запускать команды и видеть результат их работы, но что это нам дает? Ну, как вариант — мы можем прочитать инфу о процессоре. Создаем простую формочку с одной кнопкой сверху и TextView ниже нее. Далее в теле метода onCreate пишем такой код:

// Наш TextView
final TextView textView = (TextView) findViewById(R.id.textView);
// Кнопка
final Button button = (Button) findViewById(R.id.button);
// При нажатии кнопки
button.setOnClickListener(new View.OnClickListener() {
    public void onClick(View v) {
          // Запускаем команду и размещаем вывод в TextView
        String cpuinfo = runCommand("cat /proc/cpuinfo");
        textView.setText(cpuinfo);
    }
});
Собственный CPU-Z в две строки
Собственный CPU-Z в две строки

Все, результат, как говорится, налицо. Вместо команды cat /proc/cpuinfo можно использовать и что-то поэкзотичнее, например uname -a, которая выводит версию ядра Linux, или cat /system/build.prop для просмотра файла системных настроек. Однако так мы далеко не уедем, система не даст нам сделать что-то серьезнее, чем чтение некоторых файлов или запуск простых команд. Хардкор начнется только при наличии прав суперпользователя.

 

И пришел root

Как я уже сказал, функциональность root-приложений построена на том, чтобы запускать команды от имени пользователя root, то есть суперпользователя. В UNIX-системах (а ею Android является на самом низком уровне) эта операция выполняется с помощью команды su. По умолчанию она просто открывает шелл с правами root, но с помощью флага -c правами root можно наделить любую команду. Например, чтобы выполнить команду id, с правами root достаточно такой строки:

runCommand("su -c id");

Команда id, кстати говоря, возвращает идентификатор текущего пользователя (0 = root), так что сразу можно проверить, как все работает. Однако при таком методе запуска возникают проблемы с парсингом. Если ты укажешь дополнительные аргументы (например, su -c uname -a), команда просто не отработает. Обойти ограничение можно, передав ее как массив строк. Для удобства немного модифицируем код метода runCommand, заменив в нем первую строку:

public String runSuCommand(String cmd) {
      try {
          Process process = Runtime.getRuntime().exec(new String[]{"su", "-c", cmd});
          ...

Все, теперь с его помощью можно запускать хоть несколько команд одновременно, все с правами root:

runSuCommand("id; uname -a; cat /proc/cpuinfo");

В сущности, это все, и можно переходить к примерам, но есть еще один нюанс: смартфон может быть не рутован. Этот момент необходимо обязательно учитывать и проверять наличие прав root. Наиболее простой и эффективный способ проверки — посмотреть, есть ли бинарник su в системе. Обычно он располагается в каталоге /system/bin/ или /system/xbin/ (в большинстве случаев), поэтому просто напишем такую функцию:

private boolean checkSu() {
    String[] places = {"/system/bin/", "/system/xbin/"};
    for (String where : places) {
        if (new File(where + "su").exists()) {
            return true;
        }
    }
    return false;
}

Объяснять тут особо нечего, есть su — true, нет — false.

И еще один, последний нюанс. Чтобы получить доступ на редактирование (удаление/перемещение) файлов в системном разделе (/system), необходимо перемонтировать его в режиме чтение/запись:

runSuCommand("mount -o remount,rw /system");

После этого можно спокойно работать с файлами системного раздела, если, конечно, на устройстве не используется блокировка доступа к /system (S-ON). По-хорошему, после окончания работы с файлами рекомендуется снова смонтировать /system с правами «только чтение»:

runSuCommand("mount -o remount,ro /system");

Однако это, скорее, правило хорошего тона, на работу системы смонтированный на запись раздел /system никак не влияет.

Стандартное окно запроса прав root в CyanogenMod
Стандартное окно запроса прав root в CyanogenMod
 

Несколько примеров

В маркете полно разнообразных root-приложений с, казалось бы, действительно крутой функциональностью. Это и софт для запуска ADB в сетевом режиме, и приложения для установки recovery и ядер, и софт для перезагрузки напрямую в recovery. Сейчас я покажу, насколько на самом деле сложны эти приложения. Итак, первый тип софта: bloatware cleaner, приложение для очистки Android от системных (неудаляемых) приложений. Реализуется с помощью трех (можно одной) строк:

String apk = "/system/app/Email.apk";
runSuCommand("mount -o remount,rw /system");
runSuCommand("rm " + apk);

Все! Мы удалили системное приложение Email. Добавь сюда интерфейс с возможностью выбора приложений (на основе списка, сформированного из содержимого /system/app/ и /system/priv-app/), и у тебя есть полноценный bloatware cleaner. Можно прикручивать рекламу и выкладывать в маркет. Только имей в виду, что, начиная с Android 5.0, приложения в каталоге /system/app/ располагаются в собственных подкаталогах, так что придется смотреть, какая версия ОС стоит, и при необходимости использовать уже другой код:

String app = "/system/app/Email";
runSuCommand("mount -o remount,rw /system");
runSuCommand("rm -rf " + app);
В Android 5.0 каждое приложение располагается в своем собственном подкаталоге
В Android 5.0 каждое приложение располагается в своем собственном подкаталоге

ОK, а как насчет приложения для перезагрузки в recovery? В кастомах это штатная функция, доступная в Power Menu, но в стоковых прошивках для этого приходится использовать специальный софт. Все сложно? Отнюдь:

runSuCommand("reboot recovery");

Да, это всего одна команда. То есть полноценное приложение с кнопкой «Перезагрузиться в recovery» уместится в 15–20 строк. Неплохо, не так ли? Ладно-ладно, возьмем более сложный пример — прошивку кастомной консоли восстановления прямо из Android. Звучит круто? Конечно, но в коде это выглядит так:

String recoveryImg = "/sdcard/recovery.img";
String recoveryPtn = "/dev/block/platform/msm_sdcc.1/by-name/recovery";
runSuCommand("dd if=" + recoveryImg + " of=" + recoveryPtn);

Стоит отметить, что это пример для чипов Qualcomm, в устройствах на базе других SoC путь до раздела recovery будет другим. Ну и в целом пример не вполне корректный, по-хорошему надо писать интерфейс выбора образа recovery (здесь это /sdcard/recovery.img), а дальше прописывать его в переменную recoveryImg. Но в любом случае исходник полноценного приложения вряд ли будет длиннее тридцати строк.

Кстати, проверить, какой чип используется в девайсе, можно с помощью все того же /proc/cpuinfo:

String cpuinfo = runCommand("cat /proc/cpuinfo");
if (cpuinfo.contains("Qualcomm")) {
      // Имеем дело с Qualcomm, отлично, продолжаем
} else {
      // Другой SoC
}

Идем дальше, на очереди приложения в стиле WiFi ADB (это реальное название). В маркете таких полно, и все они выглядят одинаково: экран с одной кнопкой для включения/выключения режима отладки по сети (ADB over WiFi). Функция очень удобная, а потому приложения пользуются популярностью. Как реализовать то же самое? Как всегда, очень просто:

runSuCommand("setprop service.adb.tcp.port 5555; stop adbd; start adbd");

Все, теперь сервер ADB на смартфоне работает в сетевом режиме и к нему можно подключиться с помощью команды «adb connect IP-адрес». Для отключения сетевого режима используем такой код:

runSuCommand("setprop service.adb.tcp.port -1; stop adbd; start adbd");

Ну и в завершение поговорим о настройщиках ядра. Таких в маркете достаточно много, один из наиболее популярных — TricksterMod. Он позволяет изменять алгоритм энергосбережения ядра, включать/выключать ADB over WiFi, настраивать подсистему виртуальной памяти и многое другое. Почти все эти операции TricksterMod (и другой схожий софт) выполняет, записывая определенные значения в синтетические файлы в каталогах /proc и /sys.

Реализовать функции TricksterMod не составит труда. К примеру, нам надо написать код, изменяющий алгоритм энергосбережения процессора (governor). Для выполнения этой операции следует записать имя нужного алгоритма в файл /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor, однако для начала следует выяснить, какие алгоритмы поддерживает ядро. Они перечислены в другом файле, поэтому мы напишем простую функцию для его чтения:

private String[] getGovs() {
    return runCommand("cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_governors").split(" ");
}

Имея список поддерживаемых алгоритмов, мы можем выбрать один из них, записав в файл scaling_governor. Для удобства будем использовать такую функцию:

private boolean changeGov(String gov) {
    runSuCommand("echo " + gov + " > /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor");
    String newgov = runCommand("cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor");
    if (newgov == gov) {
        return true;
    }
    return false;
}

Обрати внимание, что после записи значения мы вновь читаем файл, чтобы удостовериться, что ядро действительно переключилось на указанный алгоритм. Далее можно создать формочку с кнопочками и менюшками и повесить на них наши функции.

Есть и множество других файлов для тонкой настройки алгоритма энергосбережения
Есть и множество других файлов для тонкой настройки алгоритма энергосбережения
 

Вместо выводов

С помощью обычных консольных команд и прав root в Android можно сделать очень и очень многое. Это подтверждает огромное количество root-софта в маркете и приведенные мной примеры. Единственная проблема — это отсутствие документации, поэтому способы решения тех или иных проблем придется искать самостоятельно, читая исходники и терроризируя форумчан. Скромный список команд есть в моем wiki. Начать можно оттуда.

Сторонние библиотеки

Приведенный мной способ выполнения команд с правами root отлично работает, но имеет небольшой недостаток. Дело в том, что права суперпользователя будут запрашиваться во время каждого исполнения функции RunSuCommand(), а это значит, что, если юзер не поставит галочку «Больше не спрашивать» в окне запроса прав root, он быстро устанет от таких запросов.

Решить проблему можно один раз, открыв root-шелл, а затем выполняя все нужные команды уже в нем. В большинстве случаев такой метод запуска избыточен, так как обычно нам необходимо выполнить только одну-две команды, которые можно объединить в одну строку. Но он будет полезен при разработке сложных root-приложений вроде комплексных настройщиков ядра и просто комбайнов.

Реализация удобной в использовании обертки для запуска root-шелла доступна как минимум в двух проектах. Первый — это RootTools от Stricson, разработчика инсталлятора BusyBox для Android, второй — libsuperuser от легендарного Chainfire. Достоинство RootTools в том, что это комбайн, кроме шелла включающий в себя огромное количество функций, позволяющих копировать и перемещать файлы, менять права доступа, монтировать файловые системы и многое другое. Libsuperuser, с другой стороны, имеет шикарную документацию.

Евгений Зобнин

Евгений Зобнин

Редактор рубрики X-Mobile. По совместительству сисадмин. Большой фанат Linux, Plan 9, гаджетов и древних видеоигр.

Check Also

Android: взламываем Medium и защищаем данные приложения

Сегодня в выпуске: взламываем приложение Medium, защищаем данные приложения, знакомимся с …

2 комментария

  1. Аватар

    http://rusdelphi.com

    22.10.2015 в 13:08

    Вот хорошее приложение для чтения паролей от wi-fi сетей https://play.google.com/store/apps/details?id=com.rusdelphi.wifipassword
    Читает пароли при наличии root на устройстве

  2. Аватар

    eugenebox

    06.11.2015 в 21:19

    Как это можно осуществить?

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