Когда речь идет о взломе и модификации чужого приложения, то на ум приходит использование декомпилятора, дизассемблера и отладчика. Но есть софтина, которая работает по другому. Знакомьтесь — Frida, тулкит, который позволяет внедриться в какой-нибудь процесс и переписать его части на языке JavaScript.
Публикация данной статьи на портале www.spy-soft.net несет исключительно образовательный характер. Ни редакция сайта, ни автор статьи не несут ответственности за ненадлежащее использование полученной из статьи информации!
Еще по теме: Перехват вызовов функций WinAPI
Внедрение кода в чужое приложение
Представьте, что вам в руки попал семпл какого-нибудь вредоноса. Вы запускаете его в эмуляторе и пытаетесь проанализировать поведение. Но оказывается, что в эмуляторе он работает совсем не так, как на реальном устройстве, и никакой подозрительной активности не проявляет: вредонос умеет определять, что находится в эмулируемой среде.
Вы об этом догадываетесь и поэтому решаете запустить вредонос под дебаггером (предварительно распаковав малварь и добавив строчку android:debuggable=»true» в AndroidManifest.xml), чтобы определить, как именно вредонос производит проверку на эмулятор. И снова проблема: она умеет определять, что работает под отладчиком, и просто не запускается при запуске. Следующий шаг: статический анализ кода с помощью декомпилятора и дизассемблера, правка с целью вырезать куски, проверяющие наличие отладчика и эмулируемой среды, снова правка кода по причине ошибки и все в таком духе.
А теперь представьте, что у вас есть инструмент, позволяющий прямо во время работы приложения отключить все эти проверки, просто переписав проверочные функции на JavaScript. Никаких дизассемблерных листингов smali, никаких правок низкоуровневого кода, никаких пересборок приложения; вы просто подключаетесь к работающему приложению, находите нужную функцию и переписываете ее тело. Не плохо звучит, не так ли?
Внедрение кода в чужое приложение с помощью Frida
Frida — это так называемый Dinamic Instrumentation Toolkit, то есть набор инструментов, позволяющих на лету внедрять собственный код в другие приложения. Ближайшие аналоги Frida — это знаменитый Cydia Substrate для iOS и Xposed Framework для Android, те самые фреймворки, благодаря которым появились твики. Frida отличается от них тем, что нацелена на быструю правку кода в режиме реального времени. Отсюда и язык JavaScript вместо Objective-C или Java, и отсутствие необходимости упаковывать «твики» в настоящие приложения. Вы просто подключаетесь к процессу и меняете его поведение, используя интерактивную JS-консоль (ну или отдаете команду на загрузку ранее написанного скрипта).
Frida умеет работать с приложениями, написанными для всех популярных ОС, включая Windows, Linux, macOS, iOS и даже QNX. Мы же будем использовать ее для модификации приложений под Android.
Итак, что нам потребуется:
- Машина под управлением Linux. Можно и Windows, но, когда занимаешься пентестом приложений для Android, лучше использовать Linux.
- Установленный adb. В Ubuntu/Debian/Mint устанавливается командой sudo apt-get install adb.
- Рутованный смартфон или эмулятор на базе Android 4.2 и выше. Frida умеет работать и на нерутованном, но для этого вам придется модифицировать APK подопытного приложения. Это просто неудобно.
Для начала мы установим Frida:
1 |
$ sudo pip install frida |
Далее скачаем сервер Frida, который необходимо установить на смартфон. Сервер можно найти на GitHub, его версия должна точно совпадать с версией Frida, которую мы установили на компьютер. На момент написания статьи это была 10.6.55. Скачиваем:
1 2 3 |
$ cd ~/Downloads $ wget https://github.com/frida/frida/releases/download/10.6.55/frida-server-10.6.55-android-arm.xz $ unxz frida-server-10.6.55-android-arm.xz |
Подключаем смартфон к компу, включаем отладку по USB (Настройки → Для разработчиков → Отладка по USB) и закидываем сервер на смартфон:
1 |
$ adb push frida-server-10.6.55-android-arm /data/local/tmp/frida-server |
Теперь необходимо зайти на смартфон с помощью adb shell, выставить нужные права на сервер и запустить его:
1 2 3 4 5 |
$ adb shell > su > cd /data/local/tmp > chmod 755 frida-server > ./frida-server |
Использование Frida
Итак, Frida установлена на машину, сервер запущен на смартфоне (не закрывайте терминал с запущенным сервером). Теперь надо проверить, все ли работает как надо. Для этого воспользуемся командой frida-ps:
1 |
$ frida-ps -U |
Команда должна вывести все процессы, запущенные на смартфоне (флаг -U означает USB, без него Frida выведет список процессов на локальной машине). Если вы видите этот список, значит, все хорошо и можно приступать к более интересным вещам.
Для начала попробуем выполнить трассировку системных вызовов. Frida позволяет отследить обращения к любым нативным функциям, в том числе системные вызовы ядра Linux. Для примера возьмем системный вызов open(), который используется для открытия файлов на чтение и/или запись. Запустим трассировку Телеграма:
1 |
$ frida-trace -i "open" -U org.telegram.messenger |
Возьмите телефон и немного потыкайте интерфейс Телеграма. На экран должны посыпаться сообщения примерно следующего содержания:
1 |
open(pathname="/data/user/0/org.telegram.messenger/shared_prefs/userconfing.xml", flags=0x241) |
Эта строка означает, что Telegram открыл файл userconfig.xml внутри каталога shared_prefs в своем приватном каталоге. Каталог shared_prefs в Android используется для хранения настроек, поэтому нетрудно догадаться, что файл userconfig.xml содержит настройки приложения. Еще одна строка:
1 |
open(pathname="/storage/emulated/0/Android/data/org.telegram.messenger/cache/223023676_121163.jpg", flags=0x0) |
Здесь все еще проще. Telegram агрессивно кеширует загруженные данные, поэтому для отображения картинки он взял ее из кеша.
1 |
open(pathname="/data/user/0/org.telegram.messenger/shared_prefs/stats.xml", flags=0x241) |
Еще один файл в каталоге shared_prefs. Судя по всему, какая-то статистика использования.
1 |
open(pathname="/dev/ashmem", flags=0x2) |
Выглядит странно, не так ли? На самом деле все просто. Файл /dev/ashmem виртуальный, он используется для обмена данными между процессами и системой с помощью IPC-механизма Binder. Проще говоря, эта строка означает, что Телеграм обратился к Андроид, чтобы выполнить какую-то системную функцию или получить информацию. Такие строки можно смело пропускать.
Пишем код
Мы можем перехватывать обращения к любым другим системным вызовам, например connect(), который используется для подключения к удаленным хостам:
1 |
$ frida-trace -i "connect" -U com.yandex.browser |
Но вывод в данном случае будет не особо информативным:
1 2 |
2028 ms connect(sockfd=0x90, addr=0x94e86374, addrlen=0x6e) 2034 ms connect(sockfd=0x90, addr=0x94e86374, addrlen=0x6e) |
Причина в том, что второй аргумент системного вызова connect() — это указатель на структуру sockaddr. Frida не умеет ее парсить и поэтому выводит адрес участка памяти, в которой хранится эта структура. Но! Мы можем изменить код, который выполняет Frida при перехвате системного вызова или функции. А это значит, что мы можем пропарсить sockaddr сами!
Когда вы запускали команду frida-trace, то наверняка заметили примерно такую строку:
1 |
connect: Auto-generated handler at "/home/j1m/__handlers__/libc.so/connect.js" |
Это автоматически сгенерированный код хука, который Frida выполняет, когда подопытное приложение обращается к указанной функции. Именно он ответственен за вывод тех малоинформативных строк, которые мы увидели. По умолчанию код выглядит так:
1 2 3 4 5 6 7 |
onEnter: function (log, args, state) { log("connect(" + "sockfd=" + args[0] + ", addr=" + args[1] + ", addrlen=" + args[2] + ")"); }, |
Видно, что хук просто выводит второй аргумент как есть. Но мы знаем, что второй аргумент системного вызова connect() — это указатель на структуру sockaddr, то есть просто адрес в памяти. Сама структура sockaddr имеет следующий вид:
1 2 3 4 |
struct sockaddr { unsigned short sa_family; // address family, AF_xxx char sa_data[14]; // 14 bytes of protocol address }; |
А в случае с сокетами типа AF_INET, которые нам и нужны, такой:
1 2 3 4 5 6 7 8 9 10 |
struct sockaddr_in { short sin_family; // e.g. AF_INET, AF_INET6 unsigned short sin_port; // e.g. htons(3490) struct in_addr sin_addr; // see struct in_addr, below char sin_zero[8]; // zero this if you want to }; struct in_addr { unsigned long s_addr; // load with inet_pton() }; |
То есть сам IP-адрес находится в этой структуре по смещению 4 байта (short sin_family + unsigned short sin_port) и занимает 8 байт (unsigned long). Это значит, что нам нужно добавить к исходному адресу 4, затем прочитать 8 байт по полученному адресу и пропарсить их, чтобы получить текстовый IP-адрес с точками. Сделаем это, заменив изначальный хук таким:
1 2 3 4 5 6 7 8 9 10 11 |
onEnter: function (log, args, state) { var addr = args[1].add("4") var ip = Memory.readULong(addr) var ipString = [ip & 0xFF, ip >>> 8 & 0xFF, ip >>> 16 & 0xFF, ip >>> 24].join('.') log("connect(" + "sockfd=" + args[0] + ", addr=" + ipString + ", addrlen=" + args[2] + ")"); }, |
Обратите внимание, что мы парсим адрес, начиная с конца, то есть разворачиваем его. Это необходимо, так как все современные процессоры ARM используют little-endian порядок байтов. Также обратите внимание на класс Memory и метод add(), это части API Frida.
Сохраняем файл и вновь запускаем frida-trace:
1 2 |
connect(sockfd=0xbb, addr=173.194.222.139, addrlen=0x10) connect(sockfd=0xba, addr=74.125.205.94, addrlen=0x10) |
Вуаля. Правда, есть один нюанс. Так как наш код не умеет различать сокеты типа AF_UNIX, AF_INET и AF_INET6 и все их интерпретирует как AF_INET, иногда он будет выводить несуществующие адреса. То есть он будет пытаться парсить имя файла сокета AF_UNIX и выводить его как IP (или пытаться вывести IPv6-адрес как адрес IPv4). Отбраковать такие адреса очень легко, обычно они идут подряд и часто повторяются. В моем случае это был адрес 101.118.47.115.
Внедряемся
Конечно же, возможности Frida гораздо шире, чем перехват обращений к нативным функциям и системным вызовам. Если мы взглянем на упоминавшийся API Frida, то увидим, что в нем есть объект Java. С его помощью мы можем перехватывать обращения к любым Java-объектам и методам, а значит, изменить практически любой аспект поведения любого приложения для Android (в том числе написанного на Kotlin).
Начнем с простого — попробуем узнать обо всех загруженных в приложение классах. Создайте новый файл (пусть он называется enumerate.js) и добавьте в него следующие строки:
1 2 3 4 5 6 7 8 |
Java.perform(function() { Java.enumerateLoadedClasses({ onMatch: function(className) { console.log(className); }, onComplete: function() {} }); }); |
Это очень простой код. Сначала мы вызываем метод Java.perform(), означающий, что мы хотим подключиться к виртуальной машине Java (или Dalvik/ART в случае Android). Далее мы вызываем метод Java.enumerateLoadedClasses() и передаем ему два колбэка: onMatch() будет выполнен при «обнаружении» класса, onComplete() — в самом конце (как видно, нам этот колбэк не нужен, и мы оставляем его пустым).
Запускаем:
1 |
$ frida -U -l enumerate.js org.telegram.messenger |
И видим на экране длинный, кажущийся бесконечным список классов, некоторые из них — часть самого приложения, но подавляющее большинство — стандартные классы фреймворка Android (Android загружает весь фреймворк в каждый процесс в режиме copy-on-write).
На самом деле нам этот список не особо интересен. Намного интереснее то, что в любой из этих классов можно внедрить свой код, а если быть точным — переписать тело любого метода любого из этих классов. Для примера возьмем такой код:
1 2 3 4 5 6 7 |
Java.perform(function () { var Activity = Java.use("android.app.Activity"); Activity.onResume.implementation = function () { console.log("onResume() got called!"); this.onResume(); }; }); |
Сначала мы используем Java.use(), чтобы получить объект-обертку для работы с классом android.app.Activity. Затем мы переписываем его метод onResume(), вызывая в конце оригинальный метод (this.onResume).
Те, кто знаком с разработкой приложений для Андроид, должны знать, что класс Activity предназначен для создания «экранов» приложения. Он имеет множество методов, один из которых называется onResume(). На самом деле это колбэк, который вызывается во время создания экрана, а также при возврате на него.
Если вы загрузите данный скрипт во Frida, запустите Телеграмм, затем выйдете из него, затем снова откроете, то заметите, что при каждом возврате в Телеграмм в терминале будет появляться сообщение «onResume() got called!».
Точно таким же образом мы можем перехватывать нажатия на кнопки:
1 2 3 4 5 6 |
Java.perform(function () { MainActivity.onClick.implementation = function (v) { consle.log('onClick'); this.onClick(v); } }); |
А вот пример логирования всех URL, к которым обращается приложение:
1 2 3 4 5 6 7 8 |
Java.perform(function() { var httpclient = Java.use("com.squareup.okhttp.v_1_5_1.OkHttpClient"); httpclient.open.overload("java.net.URL").implementation = function(url) { console.log("request url:"); console.log(url.toString()); return this.open(url); } }); |
В данном случае мы внедряемся в очень популярную библиотеку OkHttp и переписываем ее метод okHttpClient.open(). Остальное должно быть ясно.
Frida CodeShare
У Frida есть официальный репозиторий скриптов, в котором можно найти такие полезности, как fridantiroot — комплексный скрипт, позволяющий отключить проверки на root, Universal Android SSL Pinning Bypass — обход SSL Pinning, Alert On MainActivity — пример кода, который реализует полноценное диалоговое окно Android на JavaScript.
Любой из этих скриптов можно запустить без предварительного скачивания с помощью такой команды:
1 $ frida --codeshare dzonerzy/fridantiroot -U -f com.example.vulnapp
Ломаем CrackMe
А теперь давайте попробуем взломать что-то реальное. На просторах интернета можно найти множество разных CrackMe. Возьмем первый попавшийся. Точнее, первый из пяти опубликованных в данном репозитории. Crackme-one.apk записывает файл в свой приватный каталог, а наша задача — вытащить содержимое этого файла. Сразу скажу, что существует масса способов сделать это за двадцать секунд, но в то же время это хороший пример, чтобы понять, как работать с Frida.
Итак, скачиваем и устанавливаем приложение:
1 2 |
$ wget https://www.dropbox.com/s/mrjnme2xiv45j4g/crackme-one.apk $ adb install crackme-one.apk |
Нам предлагают нажать кнопку для записи файла либо ввести ответ для проверки. Очевидно, чтобы взломать этот CrackMe, мы должны перехватить управление в момент записи файла. Но как это сделать?
На самом деле очень просто. Большинство приложений для Android используют для записи данных либо класс java.io.OutputStream, либо класс java.io.OutputStreamWriter. У каждого из них есть методwrite()`, который и отвечает за запись файла. Нам необходимо лишь подменить его на свою реализацию и вывести на экран первый аргумент, который содержит либо массив байтов, либо строку:
1 2 3 4 5 6 7 |
Java.perform(function () { var os = Java.use("java.io.OutputStreamWriter"); os.write.overload('java.lang.String', 'int', 'int').implementation = function (string, off, len) { console.log(string) this.write(string, off, len); }; }); |
Запускаем:
1 |
$ frida -U -f com.reoky.crackme.challengeone -l outputstream_write.js --no-pause |
Вуаля, на экране появляется строка
1 |
poorly-protected-secret |
Отмечу три момента:
- В этот раз мы использовали метод overload(), так как класс OutputStreamWriter реализует сразу три метода write() с разным набором аргументов.
- Мы использовали опцию —no-pause, которая нужна, если мы хотим выполнить холодный старт приложения и при этом не хотим, чтобы Frida остановила приложение в самом начале.
- На самом деле взломать этот CraсkMe можно было бы, просто перейдя в его приватный каталог и прочитав файл (это возможно, так как у нас рутованный смартфон) либо путем декомпиляции приложения (текст лежит в открытом виде). Здесь, однако, есть нюанс: если бы CrackMe хранил строку в зашифрованном виде и расшифровывал ее только перед записью, декомпиляция была бы бесполезна (ну, по крайней мере до тех пор, пока вы не извлекли бы ключ шифрования и не написали скрипт расшифровки).
Выводы
Frida — очень мощный инструмент, с помощью которого можно сделать с подопытным приложением практически все, что угодно. Но это инструмент не для всех, он требует знания JavaScript, понимания принципов работы Андроид и приложений для него. Так что, если вы рядовой скрипт-кидди, вам остается довольствоваться автоматизированными инструментами, созданными на основе Frida, например appmon.
Еще по теме: Защита приложения от отладки