Друг порекомендовал приложение, но купить его не получилось с территории России. Статья о том, как поисследовать приложение до той степени, чтобы покупка потеряла свою актуальность. Может быть полезно почитать и разработчикам, чтобы понимать, что полную версию приложение включить достаточно легко.

Теоретическое описание процесса включения полной версии

Процесс исследования состоит из 4 шагов:

  1. Объединение split-apk в один apk (с появлением app bundles некоторые apk состоят из нескольких).

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

  3. Разархивировние apk и изменение android-байткода (smali) функций, подсморенных на этапе "1". Всегда проще собрать код до декомпиляции (подредактировать байткод).

  4. Сборка измененного байткода.

  5. Ужатие и подписание нового 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, статьи:

Можно посмотреть задачки с решениями, списки популярных инструментов с примерами их использования.

Вместо заключения

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

Комментарии (8)


  1. Mishkun
    04.01.2024 16:21
    +1

    Как бы ты защитил приложение от подобной атаки? Ведь по сути любая функциональность "за флажком" может быть включена


    1. arturdumchev Автор
      04.01.2024 16:21
      +4

      Наверняка защититься от подмены функциональности "за флажком" нельзя, можно лишь посмотреть в сторону усложнения реверс-инжениринга.

      Вынесение текста вроде "You reached the maximum number of allowed free Zones" в ресурсы не поможет, т.к. легко соотнести id ресурса с текстом.

      Вынесение функции в натив не особо усложнит задачу, т.к. будет обертка над нативными вызовами.

      Дописать код с проверкой подписи — его так же найдет реверс-инженер. Запустит приложение и посмотрит по `adb logcat | grep 'package.name'`, что падает при запуске.

      Обфускация кода — немного усложняет чтение, но как видно по статье — не проблема.

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

      Применение всего из вышеперечисленного, наверное, замедлит получение платной версии с получаса до трех-четырех часов, но не более.

      Можно представить менее реалистичные подходы, которые усложнят разработку и повлияют на производительность приложения.

      Шифровать и дефишровать ресурсы (тексты), чтобы ничего нельзя было прочитать. Но даже в этом случае реверс-инженер докапается до кода дешифровки и применит его к ресурсам.

      Можно написать свою виртуальную машину и писать код приложения для нее — тогда появится дополнительный уровень, с которым будет очень муторно возиться. Но сложно представить, чтобы кто-то этим занимался.

      Даже со всем вышеперечисленным (и тем, что я забыл упомянуть) можно работать. У злоумышленника может быть рутованный девайс, на котором он легко подключит дебаггер (или magisk, frida) и погуляет по коду, чтобы выяснить все, что ему нужно.


      1. badryuner
        04.01.2024 16:21
        +3

        Что вы думаете насчёт стандартной обфускации от Гугла? Их чудо-программа оборачивает все уязвимые места в VM.Invoke, а код переводится в псевдо vm инструкции, которые сохраняются в отдельные файлы в assets внутри apk. Виртуальная машина компилируется в нативную либу libpairipcore.so, которая обфусцирована ещё сильнее. Также у нее есть встроенная защита от: frida, пересборки apk, изменения *.so файлов (последние два пункта решаются по гайду на 4pda)


        1. arturdumchev Автор
          04.01.2024 16:21
          +1

          Вы имеете в виду что-то вроде?

          minifyEnabled true
          shrinkResources true
          proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'

          Я смотрел код open source приложения NewPipe с подобной конфигурацией (только shrinkResources было false), — сложилось впечатление, что обертки над boolean-флагами у меня найти бы получилось.

          Может быть, у вас есть опыт или были проблемы?


    1. fire64
      04.01.2024 16:21
      +3

      Наверное самый надёжный способ, реализация части функционала на сервере разработчика.


      1. redfox0
        04.01.2024 16:21

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


        1. arturdumchev Автор
          04.01.2024 16:21
          +1

          А я же смогу найти этот код и убрать проверку лицензии?


    1. semenovs
      04.01.2024 16:21

      Подключить firebase сервисы. И все :)