Привет, дорогой читатель, уже достаточно давно я изучаю мобильные приложения. Большинство приложений не пытаются как-то скрыть от меня свой «секретный» функционал. А я в это время радуюсь, ведь мне не приходится изучать чей-то обфусцированный код.
В этой статье я хотел бы поделиться своим видением обфускации, а также рассказать про интересный метод сокрытия бизнес-логики в приложениях с NDK, который нашел относительно недавно. Так что если вас интересуют живые примеры обфусцированного кода в Android — прошу под кат.
Под обфускацией в рамках этой статьи будем понимать приведение исполняемого кода Android-приложения к трудному для анализа виду. Существует несколько причин затруднять анализ кода:
- Ни один бизнес не хочет, чтобы в его «внутренностях» ковырялись.
- Даже если у вас приложение-пустышка, интересное там можно найти всегда (пример с инстаграмом).
Многие разработчики решают проблему простым форком конфига ProGuard. Это не лучший способ защиты данных (если вы первый раз слышите об этом, то см. вики).
Хочу привести показательный пример, почему предполагаемая “защита” с помощью ProGuard не работает. Возьмем любой простенький пример из Google Samples.
Подключив к нему ProGuard со стандартным конфигом, получим декомпилированный код:
«Ооо, ничего непонятно» – скажем мы и успокоимся. Но через пару минут переключения между файлами найдём подобные кусочки кода:
В этом примере код приложения выглядит затрудненным довольно слабо (логирование данных, создание видео захвата), поэтому некоторые методы, использованные в оригинальном коде, легко понятны и после обработки конфигом ProGuard.
Дальше больше, взглянем на data-классы в Kotlin. Data-класс по умолчанию создает метод “toString”, который содержит в себе названия переменных экземпляра и название самого класса.
Исходный data-класс:
Он может превратиться в лакомый кусочек для реверсера:
(автогенерация метода toString в Kotlin)
Выясняется, что ProGuard прячет далеко не весь исходный код проекта.
Если я все еще не убедил вас в нецелесообразности защиты кода таким способом, то давайте попробуем оставить в нашем проекте атрибут “.source”.
-keepattributes SourceFile
Эта строчка есть во многих opensource проектах. Она позволяет просматривать StackTrace при падении приложения. Однако, вытащив “.source” из smali-кода, мы получим всю иерархию проекта с полными названиями классов.
По определению, обфускация – это “приведение исходного кода в нечитаемый вид для того, чтобы противодействовать разным видам ресерча”. Однако, ProGuard (при использовании со стандартным конфигом) не делает код нечитаемым – он работает как минификатор, сжимающий названия и выкидывающий лишние классы из проекта.
Такое использование ProGuard – это легкое, но не совсем подходящее для хорошей обфускации решение на ”авось”. Хорошему разработчику нужно заставить ресерчера (или злоумышленника) испугаться “китайских символов”, которые трудно деобфусцировать.
Если вам интересно подробнее узнать про ProGuard, то предлагаю следующую познавательную статью.
Что прячем
Теперь давайте посмотрим, что обычно прячут в приложениях.
- Ключи шифрования:
- Специфическую логику приложения:
В коде часто может быть спрятано что-то более неожиданное (наблюдения из личного опыта), например:
- Имена разработчиков проекта
- Полный путь к проекту
- “client_secret” для протокола Oauth2
- PDF-книга “Как разрабатывать под Android” (наверное, чтобы всегда была под рукой)
Теперь мы знаем, что может прятаться в Android-приложениях и можем переходить к главному, а именно к способам сокрытия этих данных.
Способы сокрытия данных
Вариант 1: Ничего не скрывать, оставить все на виду
В таком случае я просто покажу вам эту картинку :)
“Помогите Даше найти бизнес-логику”
Это нетрудозатратное и совершенно бесплатное решение подойдет для:
- Простых приложений, которые не взаимодействуют с сетью и не хранят чувствительную пользовательскую информацию;
- Приложений, которые используют только публичное API.
Вариант 2: Использовать ProGuard с правильными настройками
Это решение все-таки имеет право на жизнь, потому что, в первую очередь, оно является простым и бесплатным. Несмотря на вышеупомянутые минусы, у него есть весомый плюс: при правильной настройке ProGuard-правил приложение может действительно стать обфусцированным.
Однако, нужно понимать, что такое решение после каждой сборки требует от разработчика декомпиляции и проверки, все ли нормально. Потратив несколько минут на изучение APK файла, разработчик (и его компания) могут стать увереннее в безопасности своего продукта.
Проверить приложение на наличие обфускации достаточно просто.
Для того, чтобы достать APK-файл из проекта существует несколько путей:
- взять из директории проекта (в Android Studio обычно название папки “build”);
- установить приложение на смартфон и достать APK с помощью приложения “Apk Extractor”.
После этого, пользуясь утилитой Apktool, получаем Smali-код (инструкция по получению здесь https://ibotpeaches.github.io/Apktool/documentation) и пытаемся найти что-нибудь подозрительно читаемое в строках проекта. Кстати, для поиска читаемых кодов можно запастись уже заранее готовыми bash-командами.
Это решение подойдет для:
- Приложений игрушек, приложений интернет-магазинов и т.п.;
- Приложений, которые действительно являются тонкими клиентами, и все данные прилетают исключительно с серверной стороны;
- Приложений, которые не пишут на всех своих баннерах “Безопасное приложение №1”.
Вариант 3: Использовать Open Source Obfuscator
К сожалению, реально хороших бесплатных обфускаторов для мобильных приложений я не знаю. А обфускаторы, которые можно найти в сети могут принести вам много головной боли, поскольку собрать такой проект под новые версии API будет слишком сложно.
Исторически сложилось, что существующие крутые обфускаторы сделаны под машинный код (для C/C++). Хорошие примеры:
- Obfuscator-LLVM, https://github.com/obfuscator-llvm/obfuscator
- Movfuscator, https://github.com/xoreaxeaxeax/movfuscator
Например, Movfuscator заменяет все opcodes mov-ами, делает код линейным, убирая все ветвления. Однако, крайне не рекомендуется использовать такой способ обфускации в боевом проекте, потому что тогда код рискует стать очень медленным и тяжелым.
Это решение подойдет для приложений, у которых основная часть кода — NDK.
Вариант 4: Использовать проприетарное решение
Это самый грамотный выбор для серьезных приложений, так как проприетарное ПО:
а) поддерживается;
б) всегда будет актуально.
Пример обфусцированного кода при использовании таких решений:
В этом фрагменте кода можно увидеть:
- Максимально непонятные названия переменных (с наличием русских букв);
- Китайские символы в строчках, не дающие понять, что реально происходит в проекте;
- Очень много добавленных в проект ловушек (“switch”, “goto”), которые сильно меняют codeflow приложения.
Это решение подойдет для:
- Банков;
- Страховых компаний;
- Мобильных операторов, приложений для хранения паролей и т. д.
Вариант 5: Использовать React-Native
Я решил выделить этот пункт, так как написание кроссплатформенных приложений сейчас стало действительно популярным занятием.
Кроме очень большого community, JS имеет очень большое количество открытых обфускаторов. Например, они могут превратить ваше приложение в смайлики:
Мне бы очень хотелось посоветовать вам данное решение, но тогда ваш проект будет работать самую малость быстрее черепахи.
Зато, уменьшив требование к обфускации кода, мы можем создать действительно хорошо защищенный проект. Так что гуглим “js obfuscator” и обфусцируем наш выходной bundle-файл.
Это решение подойдет для тех, кто готов писать кроссплатформенное приложение на React Native.
Было бы очень интересно узнать про обфускаторы на Xamarin, если у вас есть опыт их использования – расскажите, пожалуйста, о нем в комментариях.
Вариант 6: Использовать NDK
Мне самому часто приходилось использовать NDK в своем коде. И я знаю, что некоторые разработчики считают, что использование NDK спасает их приложение от реверсеров. Это не совсем так. Для начала нужно понять, как именно работает сокрытие с помощью NDK.
Оказывается, очень просто. В коде есть некоторая JNI-договоренность, что при вызове C/C++ кода в проекте он будет преобразовываться следующим образом.
Нативный класс NativeSummator:
Реализация нативного метода sum:
Реализация нативного статичного метода sum:
Cтановится понятно, что для вызова нативного метода используется поиск функции Java_<package name>_<Static?><class>_<method>
в динамической библиотеке.
Если заглянуть в Dalvik/ART код, то мы найдём следующие строки:
(источник)
Сначала сгенерируем из Java-объекта следующую строку Java_<package name>_<class>_<method>
, а затем попытаемся разрезолвить метод в динамической библиотеке с помощью вызова “dlsym”, который попытается найти нужную нам функцию в NDK.
Так работает JNI. Его основная проблема в том, что, декомпилировав динамическую библиотеку, мы увидим все методы, как на ладони:
Значит, нам нужно придумать такое решение, чтобы адрес функции был обфусцирован.
Сначала я пытался записать данные напрямую в нашу JNI-таблицу, но, понял, что механизмы ASLR и разные версии Android просто-напросто не позволят мне сделать этот способ работающим на всех устройствах. Тогда я решил узнать, какие методы NDK предоставляет разработчикам.
И, о чудо, нашелся метод “RegisterNatives”, который делает ровно то, что нам нужно (вызывает внутреннюю функцию dvmRegisterJNIMethod).
Определяем массив, описывающий наш нативный метод:
И регистрируем наш объявленный метод в функции JNI_OnLoad (метод вызывается после инициализации динамической библиотеки, тыц) :
Ура, мы самостоятельно спрятали функцию “hideFunc”. Теперь применим наш любимый llvm-обфускатор и порадуемся безопасности кода в конечном виде.
Это решение подойдет для приложений, которые уже используют NDK (подключение NDK несет в проект большое количество сложностей, поэтому для не-NDK приложений это решение не так актуально).
Вывод
На самом деле, в приложении не должно храниться никаких чувствительных данных, либо они должны быть доступны только после аутентификации пользователя. Однако, бывает, что бизнес-логика принуждает разработчиков к хранению токенов, ключей и специфических элементов логики кода внутри приложения. Надеюсь, эта статья поможет вам, если вы не хотите делиться такими чувствительными данными и быть “открытой книгой” для ресерчеров.
Я считаю, что обфускация – важная структурная часть любого современного приложения.
Обдуманно подходите к вопросам сокрытия кода и не ищите простых путей! :)
Кстати, спасибо пользователю miproblema за помощь в некоторых вопросах. Подписывайтесь на ее телеграм-канал, там интересно.
А так же огромное спасибо пользователям sverkunchik и SCaptainCAP за помощь в редактировании статьи.
Комментарии (14)
Revertis
21.01.2019 15:02Но ведь последний ход с помощью RegisterNatives() тоже быстро найдётся ресёрчером даже в обфусцированном коде, и ничего в итоге не даст. Изменилось ведь только название метода.
inwady Автор
21.01.2019 15:27Полностью согласен с вами.
Но в данном кейсе больше маневров для антиреверса. Плюс такой способ спрятать менее очевидный.
lostpassword
21.01.2019 16:48Код приложений на Android смотрел мало, но вот за обфускацию-минификацию JS-скриптов в вебе очень хочется поотрывать руки. Бывает, упрёшься в какой-нибудь упакованный-минифицированный JS-файл — и час ковыряешься, чтобы понять, что это за зверь, что он делает и на кой чёрт он там нужен.
Думаю, если бы занимался Андроидом, то хотелось бы поотрывать руки и здешним обфускаторам.
Если у вас есть ОМГ ВАЖНАЯ СЕКРЕТНАЯ УТЮТЮТЮТЮ логика — так и вычисляйте её на бэкенде, нечего её на клиентскую часть пихать. А на клиенте оставьте прозрачный и простой код.
Однако, бывает, что бизнес-логика принуждает разработчиков к хранению токенов, ключей и специфических элементов логики кода внутри приложения. Надеюсь, эта статья поможет вам, если вы не хотите делиться такими чувствительными данными и быть “открытой книгой” для ресерчеров.
Так после обфускации все «токены, ключи и специфические элементы логики» по-прежнему остаются на клиенте. Просто вместо 15 минут нужно, скажем, 15 часов. Ну или 15 дней. Это как ключ от двери не рядом на гвоздик повесить, а положить под коврик.inwady Автор
21.01.2019 17:26Про JS все верно. В статье я говорю про Android-приложения, потому что в техническом плане они могут много всего интересного сделать (SSL, потоки, NFC, WiFi и т.д.), чем обычные Web-страницы. Поэтому тут и появляется важная авторская логика кода (хотя и далеко не всегда) :)
Про 15 минут, часов, дней. Выше писал, что «уровень защищенности приложения должен быть всегда выше, чем ценность взлома самого приложения». Многие просто не станут тратить 15 дней своего времени на ваше приложение. Это и есть одна из целей обфускации.
А так да, все достается.lostpassword
21.01.2019 18:13Грустно, что становится всё сложнее определить, что это за непонятный кусок обфусцированного кода внутри приложения «фонарик» — то ли это разработчики очень пекутся о суперэффективных алгоритмах включения лампочки, то ли это злоумышленники бэкдор встроили или майнер. Опять же, у исследователей безопасности уходит куча времени на анализ этой фигни вместо разбора кода.
И всё ради сомнительно-призрачной возможности «а вдруг это нам поможет...».inwady Автор
21.01.2019 18:37Да, очень много времени тратится на анализ лишнего (суровая правда).
И, кстати, если приложение «фонарик» обфусцировано, то это как минимум подозрительно.NetBUG
23.01.2019 01:19А если приложение «Пофигбанк» обфусцировано (и разработчик контрактный, а не сам банк)? Кому доверять?
inwady Автор
23.01.2019 16:24+1Тут обычно никаких проблем нет. Банк в любом случае заключает договор с разработчиками приложения, которые обязаны не встраивать бэкдор в банковское ПО. В добавок к этому, банк может заказать дополнительный аудит у сторонней компании. Она может выявить потенциальные проблемы.
Если я неправильно понял ваш вопрос, то поправьте меня.
androidovshchik
21.01.2019 18:41> Как изучать APK-файл
Вот это не понял. Зачем устанавливать апк, а потом его вытаскивать? Не проще взять исходный апк
Результат аналогичный, но быстрее и удобнее
И насчет smali тоже непонятно, зачем это ковырять, если проще посмотреть на декомпилированные исходники в том же jadx-gui
А так статья в целом интересная, спасибо, есть моменты, которые подчерпнул
Что думаете насчет R8 от гугл?inwady Автор
21.01.2019 19:01Спасибо, рад, что статья понравилась.
Я там предложил два варианта, один из них как раз взять сгенерированный APK-файл и изучить его. Как это делать, мне кажется, не имеет значения. У каждого свой способ. Я, например, использую утилиту apkx (которая все переводит в Java из коробки). Пример с APKTOOL был взят, потому что он каноничный + там могут быть атрибуты, которые из Java (после jadx) могут быть не видны.
Про R8 не читал, как я понял, это быстрый ProGuard. Мне кажется, в мире обфускации он погоды не сделает, как минимум сейчас.
axifive
В конечном итоге любая обфуcкация упирается в мотивированность реверсера, что неоднократно доказали взломы сложнейших защит, таких как Denuvo.
Да и если приложение не offline, то болшую часть информации можно получить сниффингом трафика.
inwady Автор
Идея в том, что затраченные усилия должны соответствовать результату. Или перефразируя: уровень защищенности приложения должен быть всегда выше, чем ценность взлома самого приложения. Поэтому, с одной стороны, приложение взломать всегда можно (в случае с Denuvo), с другой стороны — нельзя ни для кого быть «открытой книгой для изучения». Это и есть цель статьи.
В случае со сниффингом трафика, можно внедрить pinning-сертификата или генерацию подписи тела. Вы отсеите значительную часть реверсеров, которые будут пытаться прослушивать запросы.