Пример исследовательского реверс-инжиниринга приложения Zone Launcher
Друг порекомендовал приложение, но купить его не получилось с территории России. Статья о том, как поисследовать приложение до той степени, чтобы покупка потеряла свою актуальность. Может быть полезно почитать и разработчикам, чтобы понимать, что полную версию приложение включить достаточно легко.
Теоретическое описание процесса включения полной версии
Процесс исследования состоит из 4 шагов:
Объединение split-apk в один apk (с появлением app bundles некоторые apk состоят из нескольких).
Декомпиляция кода apk, потому что мы хотим посмотреть на код и понять, что отвечает за включение платных функций.
Разархивировние apk и изменение android-байткода (smali) функций, подсморенных на этапе "1". Всегда проще собрать код до декомпиляции (подредактировать байткод).
Сборка измененного байткода.
Ужатие и подписание нового apk своей подписью (без подписи не встанет).
Необходимый софт
jadx для декомпиляции ("1" из раздела теории);
apktool для разархивирования и архивирования ("2" и "3");
android sdk для ужатия apk, создания ключей и подписания apk.
apk extractor — андроид-приложение, позволяющее вытаскивать apk после установки приложения с google play, даже если приложение — split apk (app bundles).
APKEditor для упаковки нескольких split-apk в один apk, чтобы работать с ними через apktool, который это не поддерживает.
java для работы с APKEditor.
Пример: включение полной версии в ZoneLauncher 0.4.29
Я использую OSX, но все то же самое будет применимо и для Linux, и для Windows, изменится только пакетный менеджер (brew).
Скачиваем apk отсюда и идем по шагам из теоретического описания.
Декомпиляция
Устанавливаем jadx:
brew install jadx
Открываем UI-версию программы:
jadx-gui
Открываем скачанный apk: file -> open files -> <выбираем apk>.
Теперь можно сохранить проект (save as gradle project) и открыть его в другом текстовом редакторе, либо продожить пользоваться jadx-gui.
В большинстве случаев для разблокирования платной версии приложения достаточно найти функцию, отвечающую за возврат boolean-флага. Часто эта функция содержит слова "full", "paid", "free". Если приложение заобфусцировано, бывает полезен другой подход — пользоваться некупленной версией и искать текстовые подсказки. Например, при некоторых действиях Zone Launcher пишет "You reached the maximum number of allowed free Zones. Get the full version to unlock UNLIMITED number".
Нажимаем на иконку поиска в Jadx-gui или в своем редакторе, ищем что-то, что может навести на след. Я начал искать "full ver", и сразу же нашлось кое-что интересное:
@Override // android.view.View.OnClickListener
public void onClick(View view) {
EditorActivity editorActivity = this.f5811k;
if (editorActivity.f2394o.getChildCount() < 3 || App.m.f2323k == 1) {
editorActivity.startActivity(new Intent(editorActivity, AddCategoryActivity.class));
} else {
editorActivity.j("You reached the maximum number of allowed free Zones. Get the full version to unlock UNLIMITED number.");
}
Очевидно, что нужно посмотреть в App.m.f2323k
. Почему такие странные имена переменных и функций? Разработчики обфусцировали код, чтобы усложнить жизнь исследователям вроде нас. Стало ли сложнее? Совершенно нет, если все сводится к одной функции.
Ищем поле f2323k
. В jadx-gui в поиске можно поставить галогчку на field
. Находим его в классе app/src/main/java/com/bialy/zonelauncher/App.java
.
По коду видно только одно место, где f2323k
проставляется в 1:
public class b implements g {
public b(App app) {
}
@Override // m1.g
public void c(e eVar, List<Purchase> list) {
if (eVar.f4970a != 0 || list == null) {
return;
}
for (Purchase purchase : list) {
if (purchase.a() == 1) {
App.m.f2323k = 1;
}
}
}
}
Обратите внимание на комментарий самого поля f2323k
:
/* renamed from: k reason: collision with root package name */
public int f2323k = 0;
Это jadx переименовал его при декомпиляции, нам будет нужно поле k
.
По идее, достаточно проставить 1 по дефолту, но просто изменить тут код и собрать проект не получится, поэтому запоминаем путь и переходим к следующему шагу.
Разархивирование apk и изменение байткода
Устанавливаем apktool:
brew install apktool
Разархивируем apk:
apktool d com-bialy-zonelauncher-70.apk
Появится папка com-bialy-zonelauncher-70
, в ней нам надо найти тот файл app/src/main/java/com/bialy/zonelauncher/App.java
и убедиться, чтобы поле k
(f2323k
) было всегда равным 1. Изменится расширение — будет не java
, а smali
.
Открываем файл /smali/com/bialy/zonelauncher/App.smali
и видим в самом начале:
# static fields
.field public static m:Lcom/bialy/zonelauncher/App;
# instance fields
.field public k:I
.field public l:Lcom/android/billingclient/api/a;
Наше поле k
— instance filed типа Integer, все сходится. Давайте найдем код инициализации этого поля в конструкторе и заменим 0 на 1.
Инициализация происходит в констукторе, причем сперва мы объявляем константу v0
равной нулю (0x0
), а затем присваиваем v0
к полю k
:
# direct methods
.method public constructor <init>()V
.locals 1
invoke-direct {p0}, Lz0/b;-><init>()V
const/4 v0, 0x0
iput v0, p0, Lcom/bialy/zonelauncher/App;->k:I
return-void
.end method
Все что нам нужно сделать — заменить 0x0
на 0x1
и сохранить файл.
Сборка измененного байткода
Тут все просто, одна команда:
apktool b com-bialy-zonelauncher-70 -o zonelauncher-unsigned-unaligned.apk
Я добавил суффиксы unsigned
и unaligned
, чтобы не запутаться в именах файлов на следующем этапе.
Ужатие и подписание
Первое, что нам нужно сделать, — скачать android sdk и прописать его в PATH. Теоретически достаточно команды:
brew install --cask android-sdk
Но я не проверял. SDK у меня уже были установлены вместе с Android Studio, и я лишь указал их в PATH. В ~/.zprofile
(или в ~/.zshrc
, или в ~/.bashrc
):
export ANDROID_HOME=/Users/m1/Library/Android/sdk
export PATH=$ANDROID_HOME/build-tools/32.0.0:$PATH
Теперь мы можем ужать собранный apk
с помощью zipalign
(подробнее читать тут).
zipalign -v -p 4 zonelauncher-unsigned-unaligned.apk zonelauncher-unsigned.apk
Далее создаем ключи и сохраняем их в надежном месте, потому что когда выйдет новая версия, нам нужно будет подписать приложение теми же ключами, чтобы обновить имеющееся приложение, а не удалять и ставить новое.
Пример создания ключей (keytool
— утилита из android sdk):
keytool -genkey -v -keystore my-release-key.keystore -alias zone -keyalg RSA -keysize 2048 -validity 10000
Рекомендую так же рядом с ключами положить текстовый файл с подсказой по паролю, и там же записать alias приложения (об этом ниже).
Пример создания alias и подписания apk (apksigner
— утилита из android sdk):
apksigner sign --ks my-release-key.keystore --min-sdk-version 24 --ks-key-alias zone --out zonelauncher.apk zonelauncher-unsigned.apk
Теперь можно переслать apk на телефон любым удобным способом и установить, предварительно удалив предыдущую версию (если она с другой подписью).
Пример: включение полной версии в ZoneLauncher 0.4.6, split-apk
Версия есть на apkpure в формате .xapk
(можно переименовать в .zip
), но можно извлечь ее с собственного телефона, предварительно установив с Google play. Такой подход мне нравится больше: можно быть уверенным, что никто с apk предварительно не поработал.
Извлекаются apk с помощью ApkExtractors, но не все поддерживают split apk. Я пользуюсь этим.
Выбираем в приложении установленный Zone Launcher, жмем на него, "Do you want to extract?" — "yes". Несколько apk будут сохранены в папку ~/Downloads/AppExtractor/ZoneLauncher_com.bialy.zonelauncher
, которую можно будет найти любым полноценным файловым менеджером.
После передачи apk-файлов на компьютер, нужно будет объединить их в один apk с помощью загруженного jar-файла APKEditor:
brew install java
java -jar APKEditor-1.3.3.jar m -i path/to/dir/with/apks
Все шаги далее — те же самые, что и в первом примере, только работаем с apk, получившимся на выходе APKEditor.
Поиск "full ver" приводит нас в файл с таким кодом:
if (editorActivity.f2518w.getChildCount() >= 3 && !q7.E(editorActivity.getApplicationContext())) {
editorActivity.m("You reached the maximum number of allowed free Zones. Get the full version to unlock UNLIMITED number.");
return;
} else {
editorActivity.startActivity(new Intent(editorActivity, AddCategoryActivity.class));
return;
}
В этом же файле есть импорт с подсказкой:
import u3.q7;
И вот нужный нам код в u3.q7
:
public static boolean E(Context context) {
return context.getSharedPreferences("settings", 0).getBoolean("full", false);
}
Видно, что по умолчанию возвращается false
: getBoolean("full", false)
.
Байткод (smali) будет выглядеть так:
.method public static E(Landroid/content/Context;)Z
.locals 2
.line 1
const-string v0, "settings"
const/4 v1, 0x0
invoke-virtual {p0, v0, v1}, Landroid/content/Context;->getSharedPreferences(Ljava/lang/String;I)Landroid/content/SharedPreferences;
move-result-object p0
const-string v0, "full"
invoke-interface {p0, v0, v1}, Landroid/content/SharedPreferences;->getBoolean(Ljava/lang/String;Z)Z
move-result p0
return p0
.end method
По этому коду понятно, что в качестве дефолтного значения возвращается const v1
, которому присваивается 0x0
. Заменим 0x0
на 0x1
, и теперь у нас — full version.
Разархивирование, сборка и подписание — точно такие же, как в первом примере.
Материалы
Статья 2016-го года, но во многом по-прежнему актуальная, удаленная с habra по решению суда, но к огромному счастью исследователей сохранившаяся на просторах интернета (и даже на хабре c VPN или за пределами России).
Иногда задача сложнее, чем просто возвращение boolean-флага или единицы в поле класса. Тогда может иметь смысл запуск и дебаггинг приложения. Вот статья на тему, правда, старенькая, 2017.
Кладезь полезной информации — сайт owasm, статьи:
Можно посмотреть задачки с решениями, списки популярных инструментов с примерами их использования.
Вместо заключения
В современных реалиях получить платную версию приложения может быть невозможно с территории некоторых стран. В качестве решения в статье был предложен простой способ обойти проблему, но делать этого, разумеется, не стоит, разве что в исследовательских целях.