В этой статье я хочу поделиться своим опытом обхода проверки на рутованость своего устройства. Статья может рассматриваться не только как самостоятельный материал, но и как прямое продолжения моей работы из предыдущей статьи.
Дисклеймер
Сразу предупрежу, что люблю писать подобные статьи довольно подробно, не ради объема и многобукав, а ради максимального погружения в проблему и способ ее решения. Обратите внимание, что я работаю на 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. И считаю это маленькой победой. Данной статьей я стремился написать не просто инструкцию о том, как расковырять приложение. И не про свои способности по типу "смотрите как могу". Этой статьей я хочу сказать (и напомнить тем, кто стал забывать), что большинство проблем решаемы, нужно только копать, копать и еще раз копать, не опускать руки и результат обязательно будет положительным. Даже если, в конце концов, баг я так и не обнаружил, я очень рад, что расширил свой кругозор и в дальнейшем смогу взяться за более сложные задачи! Спасибо за внимание, обязательно буду писать еще про подобные танцы с бубном!
androidovshchik
Хочу поделиться наработкой по этому вопросу, сделал свой шаблон для автоматизации разборки/сборки подобных проектов. Правда он написан на bash, доступен через linux среду.
Например, так декомпилируются (в smali и java) все apk в корне шаблона и перемещаются в подпапку
$ bash dm.sh -ds -dj
Так компилируется последний апк и запускается на устройстве
$ bash br.sh
Код здесь. Кстати, кажется удобнее генерировать smali код через плагин в android studio, потом уже вставлять в проект, чем возиться в этой ассемблеровской каше