Взлом одного Android приложения

memkill 6 апреля 2012 в 10:19 67,2k
Недавно я усиленно разрабатывал свое приложение под Android, и в процессе защиты платной версии понял, что довольно сложно обезопасить приложение от взлома. Ради спортивного интереса решил попробовать убрать рекламу из одного бесплатного приложения, в котором баннер предлагается скрыть, если заплатить денежку через In-App Purchase.


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

Шаг 1. Получаем «читаемый» код приложения.
Чтобы добыть APK приложения из телефона, нужны root права. Вытягиваем приложение из телефона с помощью adb (пусть, для конспирации, у нас будет приложение greatapp.apk):
adb pull /data/app/greatapp.apk

Хабраюзер overmove подсказал мне, что root необязателен, можно с помощью Astro сделать бэкап любого приложения, и оно будет скопировано в /mnt/sdcard.
Хабраюзер MegaDiablo подсказал мне, что и Astro необязателен. Список установленных приложений и их файлы apk можно узнать через утилиту pm в шелле, а когда уже известно имя файла, его можно стянуть через adb pull /data/app/app.filename.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 ..\smali\smali-1.3.2.jar ..\smali\out -o classes.dex
apkbuilder C:\devel\greatapp\greatapp_cracked.apk -u -z C:\devel\greatapp\greatapp_noclasses.apk -f C:\devel\greatapp\classes.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


Надеюсь, было интересно. В заключение, несколько полезных ссылок по теме:
Сохранить: