В этой статье я хочу поделиться своим опытом обхода проверки на рутованость своего устройства. Статья может рассматриваться не только как самостоятельный материал, но и как прямое продолжения моей работы из предыдущей статьи.

Дисклеймер

Сразу предупрежу, что люблю писать подобные статьи довольно подробно, не ради объема и многобукав, а ради максимального погружения в проблему и способ ее решения. Обратите внимание, что я работаю на macOS, поэтому все команды в терминале будут ориентированы под данную ОС.

Как до этого дошло?

Мне выслали .apk файл приложения. Где-то внутри программы есть баг, который мне нужно воспроизвести. Но пройти вглубь приложения у меня не получается. Мое устройство не проходит проверку на рутованность.

Экран, который меня не пропускает
Экран, который меня не пропускает

Так как я уже потратил немало усилий, чтобы все же запустить это приложение (о каких именно усилиях идет речь можно узнать в прошлой статье), то и здесь я решил не сдаваться и попробовать обойти проверку на рутованность устройства.

Что будем делать?

Первое, что пришло мне в голову, так это попробовать выполнить декомпиляцию и поправить кое-где код, а потом собрать все обратно. Основное сомнение по поводу такого подхода вызывало то, что у меня не нативное android-приложение, а flutter-приложение. То есть как такового основного кода приложения в виде байт-кода в .apk найти не получится. Или получится? И нужен ли мне код самого приложения?

Небольшое отступление о том, как во флаттере запускается нативный код, то есть родной код для Android или iOS платформы. Например, перед flutter-разработчиком стоит задача: не давать возможность работать с приложением на рутованном устройстве. Аналогом андроид root-устройства является jailbreak для iOS. Для проверки на рутованность или jailbreak необходимо воспользоваться нативными средствами обеих платформ. Для этого разработчик пишет плагин, который вызывает соответствующие нативные методы в зависимости от платформы на которой запущено приложение. 

Таким образом, я сделал вывод, что в имеющемся .apk файле есть как минимум 2 интересующих меня места в виде байт-кода. Первое место - это нативная часть приложения, которая слушает команды из flutter части приложения, чтобы вызвать соответствующие нативные методы. И второе место - это собственно те самые нативные методы, а в данном случае - библиотека для проверки на рутованность устройства.

Я решил, что буду менять байт-код во втором месте. Честно говоря, сначала я работал с первым местом, то есть с плагином. Все потому, что кода там намного меньше и запутаться было сложнее. Всего лишь метод, который вызывает другой метод - это пара строк кода. Но в таком случае статья была бы еще сильнее завязана под конкретный .apk, и не было б такой абстрактности. Поэтому будем работать напрямую с кодом библиотеки.

Смотрим байт-код .apk

Итак, посмотрим на внутренности .apk. Для этого можно воспользоваться инструментом, предоставляемый Android Studio. В меню выбираем Build -> Analyze APK… В окне выбора файла находим интересующий нас .apk.

Весь скомпилированный байт-код хранится в файле classes.dex. Выберем его и взглянем на содержимое. В первую очередь, нас интересуют не файлы java или androidx, а именно файлы приложения. Поэтому выбираем первый package в структуре файлов android проекта, в моем случае это папка com. Из любопытства к содержимому каждой из папок я затригерился на слово rootbeer. Погуглив в гугле оказалось, что это весьма популярная библиотека для проверки устройства на предмет рутованности. 

Исследовав документацию и исходники библиотеки стало ясно о том, как пользоваться библиотекой и где располагается ключевой метод isRooted().

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

Судя по исходникам библиотеки, нужный мне файл должен находится рядом с классом RootBeerNative, и как мы видим тут таких, два a и b. Эмпирическим методом я понял, что это файл b. Правый клик по файлу b и в выпадающем меню выбираем Show Bytecode.

К сожалению, имена всех методов обфусцированы. Снова обращаясь к исходному файлу на github становится ясно, что метод isRooted() находится на строке 42. Побегав по файлу с байт-кодом мне удалось найти этот метод. Только теперь он называется a() и почему-то начинает работу с .line 43. Вот этот метод я и буду редактировать. Но не так быстро. К сожалению здесь только Read-only доступ.

Редактируем содержимое classes.dex

Для этого придется декомпилировать весь .apk при помощи известного инструмента apktool. Работать с ним довольно просто. После его установки нам понадобятся всего 2 команды:

apktool d appname.apk, которая декомпилирует указанный .apk и apktool b directory_with_app, которая соберет .apk обратно (в указанной directory_with_app должен располагаться файл apktool.yml).

Итак, выполняю декомпиляцию при помощи команды в терминале: apktool d app.apk

После выполнения команды появляется папка с файлами приложения. Нас интересует папка smali. Smali - это язык которым описан байт-код (вот классная статья по основам smali). Именно эта папка и есть содержимое файла classes.dex. К только что выполненной команде можно добавить параметр --skip-sources или просто -s тогда вместо папки smali мы увидим тот самый файл classes.dex.

Далее, в папке smali находим интересующий и уже известный нам файл b.smali:

Открываем его любым текстовым редактором и переходим к месту, где располагается метод isRooted():

Содержимое файла b.smali
.method public a()Z
    .locals 1

    .line 43
    invoke-virtual {p0}, Lcom/scottyab/rootbeer/b;->c()Z

    move-result v0

    if-nez v0, :cond_1

    invoke-virtual {p0}, Lcom/scottyab/rootbeer/b;->d()Z

    move-result v0

    if-nez v0, :cond_1

    const-string v0, "su"

    invoke-virtual {p0, v0}, Lcom/scottyab/rootbeer/b;->a(Ljava/lang/String;)Z

    move-result v0

    if-nez v0, :cond_1

    const-string v0, "busybox"

    .line 44
    invoke-virtual {p0, v0}, Lcom/scottyab/rootbeer/b;->a(Ljava/lang/String;)Z

    move-result v0

    if-nez v0, :cond_1

    invoke-virtual {p0}, Lcom/scottyab/rootbeer/b;->f()Z

    move-result v0

    if-nez v0, :cond_1

    invoke-virtual {p0}, Lcom/scottyab/rootbeer/b;->g()Z

    move-result v0

    if-nez v0, :cond_1

    .line 45
    invoke-virtual {p0}, Lcom/scottyab/rootbeer/b;->b()Z

    move-result v0

    if-nez v0, :cond_1

    invoke-virtual {p0}, Lcom/scottyab/rootbeer/b;->h()Z

    move-result v0

    if-nez v0, :cond_1

    invoke-virtual {p0}, Lcom/scottyab/rootbeer/b;->j()Z

    move-result v0

    if-nez v0, :cond_1

    invoke-virtual {p0}, Lcom/scottyab/rootbeer/b;->e()Z

    move-result v0

    if-eqz v0, :cond_0

    goto :goto_0

    :cond_0
    const/4 v0, 0x0

    goto :goto_1

    :cond_1
    :goto_0
    const/4 v0, 0x1

    :goto_1
    return v0
.end method

Можем сравнить его с неповторимым оригиналом:

Очень похожи. Итак, нас интересует return, то есть самый конец метода. Видим, что метод возвращает некоторое константу v0, значение которой определяется на основе выполнивших в методе условий. Нам это не подходит. Нам всегда нужно возвращать false или же, как это будет в smali, 0x0. Для этого добавим свою констант v1 со значением 0x0. Сделаем это прямо над return

:goto_1
const/4 v1, 0x0
return v1

И конечно же заменим в return v0 на v1. Поднимемся в самое начало метода и изменим значение .locals с 1 на 2, потому что так надо. Сохраняем изменения в файле и закрываем редактор.

Собираем .apk обратно

Для этого воспользуемся командой apktool b app, где app - папка с приложением, в котором мы редактировали smali файл. После завершения команды, в папке app появится директория dist. Здесь и расположен наш заново собранный .apk. Однако, при попытке установить его, мы получим следующую ошибку:

The APK failed to install.
Error: INSTALLPARSEFAILEDNOCERTIFICATES: Failed collecting certificates for /data/app/vmdl164530405.tmp/base.apk: Failed to collect certificates from /data/app/vmdl164530405.tmp/base.apk: Attempt to get length of null array

Это из-за того, что после пересборки нужно еще и переподписать приложение. Для этого нам понадобиться любой keystore файл. Или можно создать новый при помощи команды:

keytool -genkey -v -keystore my-release-key.keystore -alias alias_name -keyalg RSA -keysize 2048 -validity 10000

Подписать ключом приложение можно при помощи одного из двух инструментов: apksigner или jarsigner.

Я выбрал jarsigner используя следующую команду:

jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore keystore app.apk cp_key

В этой команде мы указываем путь к keystore, путь к .apk, который хотим подписать и имя ключа (alias) из указанного keystore. После, вводим пароль от keystore и приложение успешно подписано.

Однако и это еще не все. Теперь, при попытке установить .apk, мы будем получать другую ошибку:

The APK failed to install.
Error: INSTALL_FAILED_INVALID_APK: Failed to extract native libraries, res=-2

Это потому, что после подписи приложения нужно выполнить оптимизацию .apk файла при помощи инструмента zipalign.

Для выполнения оптимизации нужно ввести следующую команду: ~/Library/Android/sdk/build-tools/30.0.3/zipalign -v -f -p 4 app.apk rdy.apk

30.0.3 - я выбрал самую последнюю версию на момент написания статьи. После завершения команды на выходе получаем файл rdy.apk, который можно успешно установить! Проверим, получилось ли обойти проверку на рутованность устройства:

И да, это успех!

Заключение

Вызов принят - вот, что я подумал получив этот .apk. И считаю это маленькой победой. Данной статьей я стремился написать не просто инструкцию о том, как расковырять приложение. И не про свои способности по типу "смотрите как могу". Этой статьей я хочу сказать (и напомнить тем, кто стал забывать), что большинство проблем решаемы, нужно только копать, копать и еще раз копать, не опускать руки и результат обязательно будет положительным. Даже если, в конце концов, баг я так и не обнаружил, я очень рад, что расширил свой кругозор и в дальнейшем смогу взяться за более сложные задачи! Спасибо за внимание, обязательно буду писать еще про подобные танцы с бубном!