Препарируем Viber. Мини-гид по анализу приложений для Android. Часть 1
https://t.me/xakep_1saruman9
Как‑то раз при устройстве на работу мне дали весьма интересное задание — проанализировать Android-приложение Viber. В нем следовало найти уязвимости с последующей эксплуатацией. На этом примере расскажу о подходе к анализу реального приложения с получением конкретных результатов за короткий срок. Если пройти все эти этапы, то вполне возможно, что тебе удастся найти в Viber 0-day
ЦЕЛЬ
Искать ошибки в приложении для Android можно по‑разному, так же как и выбирать места для этого самого поиска. В рамках этой статьи мы выберем цель пониже уровнем — разделяемые библиотеки, то есть ориентироваться будем на баги memory corruption. Код на Java рассмотрим только в случае, когда необходимо выяснить его связь с разделяемыми библиотеками.
Для анализа APK использовались стандартные инструменты из любого публичного awesome list, поэтому я не буду заострять внимание на их названии, если того не требует контекст.
Целевой APK был получен с одного из mirror-сайтов. К выбору источника APK стоит относиться серьезно, поскольку нередко сайт может хранить:
- уже устаревшие версии;
- только версии для ненужных платформ и архитектур;
- модифицированные приложения (возможно, с содержанием малвари).
На данном этапе имеет смысл провести рекогносцировку цели: изучить CVE, а затем провести binary diffing анализ 1-day-уязвимостей.
РАЗДЕЛЯЕМЫЕ БИБЛИОТЕКИ
После распаковки APK в директории lib
можно обнаружить файлы библиотек: они не запакованы, не зашифрованы, не обфусцированы, не накрыты протектором, что облегчает нашу задачу в несколько раз. В защиту Viber могу сказать, что обычно мессенджеры не пытаются применять пассивные меры защиты от анализа библиотек, у WhatsApp лишь используется кастомный упаковщик, но и то — не ради защиты. Для анализа я выбрал версии библиотек для архитектуры x86_64 по следующим причинам:
- большее количество инструментов для этой архитектуры;
- лучше декомпиляция (это, конечно, спорно, так как многое зависит от выбора инструмента);
- возможность эмуляции на более высоких скоростях (моя хост‑машина имеет архитектуру x86_64);
- возможность частичного анализа на хост‑машине в обход эмулятора;
- на данном этапе нет задачи писать конкретный эксплоит для покрытия большего числа целей, соответственно, ARM-архитектуры можно отбросить, если это потребуется.
Поначалу для поверхностного анализа использовались такие инструменты, как IDA Pro, Binary Ninja и rizin (Ghidra не взял, потому что задачу следовало решить быстро): загружаешь библиотеку, смотришь экспортированные символы, находишь строки, немного читаешь код. Но затем я перешел к oneline-команде — по сути, большего мне и не требовалось: readelf -W --demangle --symbols $(LIBRARY_SO) | tail -n +4 | sort -k 7 | less
.
После идентификации JNI-функций из библиотек прохожусь rg
/grep
по smali-коду и нахожу файлы, где содержится объявление native-функций:
$ readelf -W --demangle --symbols libnativehttp.so | tail -n +4 | sort -k 7 | rg "FUNC.Java_." | less
33: 0000000000001bb5 55 FUNC GLOBAL DEFAULT 13 Java_com_viber_libnativehttp_HttpEngine_nativeCreateHttp
34: 0000000000001bec 15 FUNC GLOBAL DEFAULT 13 Java_com_viber_libnativehttp_HttpEngine_nativeDelete
38: 0000000000001bfb 622 FUNC GLOBAL DEFAULT 13 Java_com_viber_libnativehttp_HttpEngine_nativeTest
44: 00000000000018c3 109 FUNC GLOBAL DEFAULT 13 Java_com_viber_libnativehttp_NativeDownloader_nativeOnConnected
39: 00000000000015e8 366 FUNC GLOBAL DEFAULT 13 Java_com_viber_libnativehttp_NativeDownloader_nativeOnData
35: 0000000000001b0c 40 FUNC GLOBAL DEFAULT 13 Java_com_viber_libnativehttp_NativeDownloader_nativeOnDisconnected
40: 0000000000001930 476 FUNC GLOBAL DEFAULT 13 Java_com_viber_libnativehttp_NativeDownloader_nativeOnHead
$ rg "native.*nativeCreateHttp"
app/src/main/java/com/viber/libnativehttp/HttpEngine.java
9: public static native long nativeCreateHttp();
Дальне нужен поверхностный анализ, чтобы выявить, во‑первых, библиотеку‑цель, во‑вторых, компоненты open source, исследование которых предстоит сделать позднее. Анализировать можно с помощью того же readelf или в Rizin либо Binary Ninja: гуглим имя экспортированного символа и проводим поверхностный реверс‑инжиниринг, чтобы воссоздать общую картину функций библиотеки.
Предварительные результаты анализа
Ниже представлен список разделяемых библиотек с кратким описанием функций или ссылкой на проект с открытыми исходниками.
libc++_shared.so
— C++ standard library.libcrashlytics-common.so
,libcrashlytics-handler.so
,libcrashlytics-trampoline.so
,libcrashlytics.so
— Firebase Crashlytics.libreactnativeblob.so
,libreactnativejni.so
,libglog_init.so
,libjscexecutor.so
,libjsijniprofiler.so
,libjsinspector.so
— React Native.libfb.so
,libfbjni.so
— fbjni.libfolly_futures.so
— Folly: Facebook Open-source Library.libfolly_json.so
— Folly: Facebook Open-source Library, double-conversion.libglog.so
— Google Logging Library.libhermes-executor-release.so
,libhermes.so
— Hermes JS Engine.libicuBinder.so
— ICU extension for SQLite.libimage_processing_util_jni.so
— androidx.camera.core.libimagepipeline.so
,libnative-filters.so
,libnative-imagetranscoder.so
— Fresco.libgifimage.so
— The GIFLIB project, Fresco.libjingle_peerconnection_so.so
— старый компонент (libjingle
) из WebRTC.libmux.so
— FFmpeg из составаfftools
.libpl_droidsonroids_gif.so
— android-gif-drawable.librenderscript-toolkit.so
— RenderScript.libsigner.so
— Adjust SDK for Android.libspeexjni.so
— Speex.libsqliteX.so
— SQLite for Android.libtensorflowlite_gpu_jni.so
,libtensorflowlite_jni.so
— TensorFlow.libyoga.so
— Yoga.libCrossUnblocker.so
,libFlatBuffersParser.so
,liblinkparser.so
,libnativehttp.so
,libsvg.so
,libViberRTC.so
,libvideoconvert.so
,libVoipEngineNative.so
— самописные библиотеки c использованием open source кода.
В итоге у нас появляется список интересных библиотек. Составлялся он исходя всего из одного условия: как можно больше самописного кода, меньше компонентов open source. Вот этот список:
libCrossUnblocker.so
;libFlatBuffersParser.so
;liblinkparser.so
;libnativehttp.so
;libsvg.so
;libViberRTC.so
;libvideoconvert.so
;libVoipEngineNative.so
.
ФУНКЦИИ
Прежде чем анализировать какую‑либо функцию, сначала нужно выяснить, может ли атакующий до нее добраться. Для этого отсортируем по приоритету все библиотеки и функции, а затем проверим, откуда вызываются последние. Для анализа Java-кода я буду использовать связку jadx (декомпиляция) + Android Studio (рефакторинг) + Understand (анализ графов связей переменных, функций и данных).
Дополнительно проверим в каждой библиотеке наличие JNI-функций. Может быть, есть те, что используются с помощью RegisterNatives
.
libFlatBuffersParser.so
При более детальном анализе выясняется, что это open source библиотека FlatBuffers. Оставляем ее анализ на потом.
libsvg.so
Пользуемся утилитой strings и Ghidra, чтобы получить строки из бинарного файла. По ним мы понимаем, что код написан на C++:
[...]
_ZTVN10__cxxabiv121__vmi_class_type_infoE
_ZNSt6__ndk119__shared_weak_countD2Ev
_ZTINSt6__ndk119__shared_weak_countE
__android_log_print
_ZNKSt6__ndk16locale9has_facetERNS0_2idE
_ZNKSt6__ndk16locale9use_facetERNS0_2idE
[...]
Проверим еще и наличие RTTI-информации — в нашем случае удача благоволит нам, таковая имеется. С помощью плагина для Ghidra Ghidra C++ Class and Run Time Type Information Analyzer восстановим структуру классов кода C++.
На первый взгляд, здесь используется собственная SVG-библиотека, что хорошо. В ходе дальнейшего анализа Java-кода (анализ дерева вызовов) выясняется, что функции библиотеки задействованы в основном для загрузки ассетов приложения (директория ./assets/svg
в APK-файле), а это нам не подходит. Однако же цепочка вызовов нативной функции nativeParseFd
→ parseFile
используется для парсинга стикеров. А вот это уже интересно! Но я решил оставить эту функцию на потом, поскольку моя интуиция подсказала: бессмысленно писать парсер SVG с нуля. Соответственно, используется что‑то из open source.
libnativehttp.so
В ходе анализа возникает вопрос, с какой целью создавалась эта библиотека, ведь кажется, что ее возможности не очень широки: обертки над функциями обработки сетевых данных. Обработчики при возникновении событий вызывают все тот же Java-код, а не что‑то нативное. Может быть, здесь когда‑то были какие‑то функциональные возможности, ну или предполагались? Подобное я уже видел в мессенджере Telegram: legacy-кода хоть отбавляй, от такого большого attack surface поначалу чешутся руки, но после анализа все встает на свои места. Поэтому, уважаемый читатель, принимай во внимание, что иногда могут встречаться не только dead code, но и dead libraries, на анализ которых можно впустую потратить кучу ценного времени.
Здесь же со временем (читай — с более глубоким анализом) все становится понятно. Эта библиотека используется другими библиотеками, то есть функции, отвечающие за обработку сетевых данных, должны быть реализованы в другом месте. По своей сути это interceptor, который может использоваться, например, для счетчика сетевого трафика или для сбора аналитики.
Другие библиотеки
Ниже описаны мои действия, связанные всего с одной библиотекой. Я нашел ее интересной с точки зрения поиска low-hanging fruits, поэтому оставил на потом анализ остальных библиотек:
libCrossUnblocker.so
;libvideoconvert.so
;libViberRTC.so
;libVoipEngineNative.so
.
Но стоит признать, что именно в этих библиотеках могут быть найдены интересные уязвимости, поскольку в них заключен набор функций VoIP, в которых достаточно мест для ошибки.
АНАЛИЗ ЦЕЛИ
В качестве подопытной я выбрал библиотеку liblinkparser.so
по причине, описанной выше.
Анализ этой библиотеки начался с детального реверс‑инжиниринга. Настолько детального, что я потратил на это два дня и забыл проверить на практике, используются ли вообще функции из этой библиотеки и могут ли быть вызваны атакующим с его входными данными. Я затянул с реверс‑инжинирингом, потому что функциональные возможности показались мне весьма интересными с точки зрения поиска уязвимостей.
В качестве основного инструмента для реверс‑инжиниринга я выбрал Binary Ninja, предварительно сравнив все имеющиеся инструменты. Давным‑давно я делал сравнение с IDA Pro, по большей части оно до сих пор актуально, только у BN стало гораздо больше плюсов. Я рекомендую так поступать каждый раз, потому что с использованием разных инструментов удобнее реверсить разные таргеты. Этот вопрос стоит особенно остро, если время на анализ ограниченно.
Но местами я использовал IDA Pro, когда появлялась потребность проводить отладку библиотек: делать это с помощью одного GDB/LLDB оказалось, мягко скажем, неудобно (у BN отладчик еще сырой, а плагины‑связки не всегда хорошо работают). Будь это реальный случай, а не тестовый, скорее всего, я бы выбрал другой инструмент в качестве основного. В дальнейшем мне потребуется автоматизация процессов реверс‑инжиниринга, удобное написание эксплоита, перенос уже имеющихся данных между версиями библиотек, отладка сразу нескольких модулей и тому подобное. Все это делать я больше привык в Ghidra, но сейчас, как я уже сказал, важна скорость.
В ходе реверса я выяснил, за что отвечает интересующая меня библиотека: парсинг URL-адреса и метаданных сайта, а также генерация preview для ссылки, которая была отправлена в личных сообщениях пользователю или самому себе. Теперь же мне нужно было доказать, что конкретная функция отвечает за это действие в приложении.
Досягаемость функции
В качестве стенда я выбрал эмулятор на QEMU (Android Virtual Device) архитектуры x86_64. После тестового запуска Viber на эмуляторе стало понятно, что какие‑либо механизмы защиты отсутствуют, и это не могло не радовать. Загрузил туда сервер Frida, JS-скрипты брал прямо из jadx, что сыграло в итоге со мной злую шутку (об этом ниже).
Первой функцией для анализа (спойлер: и последней) я выбрал nativeGeneratePreview
:
let LinkParser = Java.use("com.viber.liblinkparser.LinkParser");
LinkParser["nativeGeneratePreview"].implementation = function (url, http) {
console.log(`LinkParser.nativeGeneratePreview is called: url=${url}, http=${http}`);
let result = this["nativeGeneratePreview"](url, http);
console.log(`LinkParser.nativeGeneratePreview result=${result}`);
return result;
};
Скрипт, к сожалению, не обрабатывался: Frida утверждала, что данный класс не найден. Я пробовал изменить конфигурации запуска Frida, произвел трассировку нативных функций с помощью frida-trace — все работало. Здесь можно было бы и остановиться, поскольку факт того, что функция вызывается, доказан, но мне стало интересно, почему не работает Frida, видимых на то причин я не находил. Я проверил наличие загруженных классов:
Java.enumerateLoadedClasses({
onMatch: function(className) {
console.log(className);
},
});
Класс не был загружен (что, в принципе, логично, потому что функции приложения я еще не вызывал). Тогда я решил попробовать вручную загрузить класс (может, внутри работает кастомный загрузчик?):
Java.enumerateClassLoaders({
onMatch: function(loader){
Java.classFactory.loader = loader;
var LinkParserClass;
try{
LinkParserClass = Java.use("com.viber.liblinkparser.LinkParser");
LinkParserClass.nativeGeneratePreview.implementation = function(){
console.log("[+] Inside nativeGeneratePreview method");
}
}catch(error){
if(error.message.includes("ClassNotFoundException")){
console.log("Class not found");
}
}
}
});
Это не дало никакого результата, ни положительного, ни отрицательного. И тут меня осенило! Я забыл Java.perform
, без него код не работает. В прошлом я использовал Frida не для Android, поэтому совсем упустил из вида столь важную деталь, а jadx мне о ней не напомнил. В итоге все предыдущие скрипты успешно отрабатывали.