Недавно я усиленно разрабатывал свое приложение под Android, и в процессе защиты платной версии понял, что довольно сложно обезопасить приложение от взлома. Ради спортивного интереса решил попробовать убрать рекламу из одного бесплатного приложения, в котором баннер предлагается скрыть, если заплатить денежку через In-App Purchase.
В этой статье я опишу, как мне удалось убрать рекламу бесплатно и в конце — несколько слов о том, как усложнить задачу взломщикам.
Шаг 1. Получаем «читаемый» код приложения.
Чтобы добыть APK приложения из телефона, нужны root права. Вытягиваем приложение из телефона с помощью adb (пусть, для конспирации, у нас будет приложение greatapp.apk):
adb pull /data/app/greatapp.apk
APK — это ZIP архив, достаем оттуда интересующий нас файл classes.dex
со скомпилированным кодом.
Будем использовать ассемблер/дизассемблер smali/baksmali для наших грязных дел.
java -jar baksmali-1.3.2.jar classes.dex
На выходе получаем директорию out с кучей файлов *.smali
. Каждый из них соответствует файлу .class
. Естественно, все обфусцированно по самое не хочу, выглядит эта директория вот так:
Попытаемся понять, где в этой обфусцированной куче «говорится» о рекламе. Сначала я просто сделал поиск с текстом "AdView
" (View, отображающий рекламу из AdMob SDK) по всем файлам. Нашелся сам AdView.smali
, R$id.smali
и некий d.smali
. AdView.smali
смотреть не очень интересно, R.$id
я как-то сначала проигнорировал, и пошел сразу в таинственный d.smali
.
Шаг 2. Пойти по неверному пути.
Вот и метод a()
в файле d.smali
с первым упоминанием AdView
(я решил, скриншотом лучше, а то без форматирования это очень уныло читать):
Метод ничего не возвращает, поэтому я, недолго думая, решил просто вставить поближе к началу return-void
. Когда я все собрал и запустил, приложение радостно крэшнулось. Лог из adb logcat
:
E/AndroidRuntime(14262): java.lang.RuntimeException: Unable to start activity ComponentInfo{com.greatapp/com.greatapp.GreatApp}: android.view.InflateException: Binary XML file line #22: Error inflating class com.google.ads.AdView
Понятно, что наш AdView
в результате манипуляций должным образом не создался. Забудем пока про d.smali
.
Шаг 3. Откатываем назад все изменения и смотрим на пропущенный ранее R$id
. Вот и строчка с AdView
:
# static fields
.field public static final adView:I = 0x7f080006
Похоже, это идентификатор View с рекламой. Поищем, где он используется, сделав поиск по значению 0x7f080006
. Получаем всего два результата: тот же R$id
и GreatApp.smali
. В GreatApp.smali текст уже гораздо интереснее (комментарии мои):
Видно, что этот идентификатор используется для поиска View
(строка 588) и буквально сразу же AdView удаляется с экрана (строка 595). Видимо, удаляется, если пользователь заплатил за отсутствие рекламы? Если посмотреть немного выше, то взгляд цепляется за строчку 558 с «ключевыми словами»:
invoke-static {v7, v8}, Lnet/robotmedia/billing/BillingController;->isPurchased(Landroid/content/Context;Ljava/lang/String;)Z
robotmedia — сторонняя (open source) библиотека, призванная упростить работу с in-app billing-ом в андроиде. Почему же она не была полностью обфусцирована? Ну да ладно, повезло.
Видно, что метод isPurchased()
возвращает строку, которая с помощью Boolean.valueOf()
преобразуется в объект Boolean и, наконец, в обычный boolean
через booleanValue()
.
И тут самое интересное, в строке 572 мы переходим в некий :cond_32
, если значение результата == false
. А иначе начинается уже просмотренный код поиска и удаления AdView
.
Шаг 4. Минимальное изменение, собрать и запустить.
Что ж, дело за малым — удаляем эту ключевую строку, собираем приложение и сразу инсталлируем на телефон:
java -jar ..smalismali-1.3.2.jar ..smaliout -o classes.dex
apkbuilder C:develgreatappgreatapp_cracked.apk -u -z C:develgreatappgreatapp_noclasses.apk -f C:develgreatappclasses.dex
jarsigner -verbose -keystore my-release-key.keystore -storepass testtest -keypass testtest greatapp_cracked.apk alias_name
adb install greatapp_cracked.apk
(greatapp_noclasses.apk
— это оригинальный APK приложения, из которого удален classes.dex, сертификаты создаются с помощью Android SDK).
И ура, запускаем приложение, никакой рекламы!
Теперь о том, как усложнить задачу любителям халявы (это лишь то, что я запомнил из видео про пиратство с Google IO 2011, ссылка ниже):
- Не осуществлять проверку оплаты или лицензирования в классах Activity и особенно методах onCreate() и ему подобных. Эти «точки входа» запускаются всегда в известное время и не обфусцируются, их всегда можно посмотреть и понять, что происходит с различными элементами UI
- Лучше всего проводить проверку не в основном потоке и в случайные моменты времени
- Проверять CRC файла
classes.dex
, причем хранить его зашифрованным - Хранить код проверки лицензии или покупки скомпилированным и зашифрованным как ресурс приложения, динамически его загружать и запускать через reflection
Надеюсь, было интересно. В заключение, несколько полезных ссылок по теме:
- Отличное видео с Google IO 2011 о том, как защитить приложение от пиратов.
- Небольшая статья с блога Android Developers с краткой подборкой техник защиты приложения от взлома, много повторяет предыдущее видео
- Статья на Хабре о реверс-инжиниринге будильника
- Dalvik VM bytecodes
- http://androidcracking.blogspot.com/, отличный блог, посвященный взлому приложений
Автор: memkill