Недавно скачал приложение-клиент на Android для нашего с вами любимого ресурса — Хабрахабра. Официальный мне не понравился, так как от браузера он мало чем отличался, а второй, кто был в списке поиска по Google Play, был HabraCitizen. На мою попытку авторизоваться, приложение ответило просьбой приобрести полную версию за $1.5, так же приобретение даёт возможность скачивать посты для оффлайн просмотра.
Так как нажатие на Нет отменяло авторизацию, нажал Да и увидел окошко Lucky Patcher'a с предложением получить эту покупку бесплатно. Посчитав такой подход к проблеме скучным и, оправдываясь перед совестью тем, что я небогатый студент, закрыл окошко и открыл Apktool. Разобрав HabraCitizen буквально по байтам принялся искать, где прячется вызов просьбы уменьшить мой электронный кошелёк.
1. Узнаём id текста с просьбой оплаты в файлах /res/values/strings.xml и /res/values/public.xml — 0x7f0c00d8
2. Ввёл id через поиск по всем .smali файлам с помощью модифицированного мной же приложения Search Thru (оригинальное пропускало «мимо ушей» файлы форматов кроме .txt, в том числе и .smali).
Итак, R$string и библиотека меня ничуть не интересуют, поэтому моя цель находится по адресу /smali/com/allesad/HabraCitizen/utils/i.smali. Открываем и видим:
Формирование и отображение окошка происходит в отдельном методе, который может быть вызван из любой точки программы. Так разработчики делают не всегда, но придётся искать дальше.
3. В Smali вызов метода выглядит так:
"invoke-static {p0, v0}, Lcom/allesad/HabraCitizen/utils/i;->a(Landroid/content/Context;Landroid/content/DialogInterface$OnClickListener;)V"
Где "p0, v0" — это параметры(если есть), "com/allesad/HabraCitizen/utils/i" — адрес класса, в котором вызываемый метод, "a" — имя вызываемого метода, "(Landroid/content/Context;Landroid/content/DialogInterface$OnClickListener;)V" — это что-то, связанное с каждым параметром(видно, что 2 параметра и два раза встречается ";"). Важно заметить, что имена методов могут быть одинаковыми, при условии, если разное количество параметров или разные типы параметров(а).
Нам нужно найти, где вызывается метод, создающий диалоговое окно:
по запросу "utils/i;->a" находим вызовы метода "а" с двумя параметрами, то есть с
"(Landroid/content/Context;Landroid/content/DialogInterface$OnClickListener;)V"
DawnloadsActivity.smali и LoginActivity.smali — как раз то, за что надо платить $1.5.
4. В обоих случаях перед вызовом метода с окошком происходит какая-то проверка:
А именно "if-nez v0, :cond_1", что означает "if not equals zero", в переводе "если не равно нулю, прыгнуть на :cond_1". После замены "if-nez v0, :cond_1" на "if-eq v0, v0, :cond_1", что значит "если v0 = v0, то перепрыгнуть вывод окошка" (и, разумеется, после компиляции приложения и установки с удалением оригинала), приложение не просило пожертвовать. Иногда этого достаточно и приложения начинают радовать платными(уже бесплатными) функциями, но сразу после авторизации HabraCitizen вылетал с ошибкой, значит придётся ещё попотеть.
5. На скриншотах в [4] можно заметить ещё кое-что общее: то, откуда программа получает результат "v0" для сравнения "if-nez": "invoke-vertual {v0}, Ljava/lang/Boolean;->booleanValue()Z", который в свою очередь берёт результат из метода
"invoke-vertual {0}, Lcom/allesad/HabraCitizen/utils/e;->p()Ljava/lang/Boolean;"
Где "com/allesad/HabraCitizen/utils/e" — класс, а "p" — имя метода.
6. Идём в /com/allesad/HabraCitizen/utils/e.smali и ищем метод "p()":
И видим, что в методе "p()" в одном месте встречаются "SharedPreferences" и "premium_account_key", в конце которого происходит возврат результата(считывания значения "premium_account_key", в данном случае там «false») действий метода туда, где он был вызван. А задав в поиск по файлу "premium" находим другой метод, который ничего не возвращает, но редактирует значение "premium_account_key" в "SharedPreferences". Но чтобы давать ему что-то новое, это что-то нужно получить, в данном случае это "p0". Кстати, переменные с префиксом "p" — параметры, получаемые при вызове данного метода. Значит, где-то происходит проверка лицензии и её результат отправляется в "SharedPreferences", что нам как раз надо подделать.
7. Ищем вызов метода "a(Ljava/lang/Boolean;)V" (который редактирует "SharedPreferences"):
И смотрим в удобном для понимания порядке — с третьего результата поиска по первый:
Здесь мы видим, что будет, если мы имеем лицензию — отправка значения «true». То, что в зелёной рамочке, нужно скопировать, оно нам понадобится.
Далее второй результат:
В нём происходит что-то, что явно не делает нам хорошо — заменяем то, что в красной рамочке тем, что скопировали ранее.
И наконец, первый результат:
Здесь я заметил «неприятное событие» — программа удаляет Coockie, а перед этим ряд похожих друг на друга действий, в том числе и присваивает к "premium_account_key" неведомое значение "v1"(в самом деле false), как во втором результате. Скажу сразу, что подмена "if-eqz v3, :cond_2" и "if-nez v0, :cond_7", как в случае с выводом окошка, приводит к вылету приложения сразу после его запуска. Чтобы всё работало, кроме замены того, что в красной рамочке на то, что было скопировано из третьего результата, я удалил те «похожие действия», вызов метода "removeAllCoockie" и всё аж вплоть до ":goto_3"(в данном случае это можно расценивать как логическое завершение мысли в русском языке). Вот как теперь стало:
После очередной компиляции и переустановки приложения, разумеется, всё заработало без намёка на донат или вылет.
В заключение хочу сказать, что рад за проявленное вами терпение и упорство, необходимые для того, чтобы прочитать эту статью. Ещё хочу заметить, что все вышеперечисленные действия были совершены на смартфоне.
Несколько ссылок, непосредственно относящихся к этой теме:
- Apktool — приложение для декомпиляции и компиляции приложений на Android устройстве
- Search Thru — приложение для поиска текста среди текстовых файлов
Приветствую! Есть возможность таким же образом изменить другое приложение? Могу заплатить если возьметесь сделать.
Моя почта shmagen@yandex.ru