Инъекция для андроида. Внедряем код в чужие приложения с помощью Frida

Инъекция для андроида. Внедряем код в чужие приложения с помощью Frida

DeepWeb

Когда мы говорим о взломе и модификации чужих приложений, то чаще всего подразумеваем использование декомпилятора, дизассемблера и отладчика. Но есть инструмент, который работает совершенно иначе. Это Frida, тулкит, позволяющий внедриться в процесс и переписать его части на языке JavaScript.

 

Немного словоблудия

Представь, что тебе в руки попал семпл малвари. Ты запускаешь его в эмуляторе и пытаешься проанализировать поведение. Но оказывается, что в эмуляторе он работает совсем не так, как на реальном устройстве, и никакой подозрительной активности не проявляет: малварь умеет определять, что находится в эмулируемой среде.

Ты об этом догадываешься и поэтому решаешь запустить малварь под дебаггером (предварительно распаковав зловред и добавив строчку 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.

Итак, что тебе нужно:

  1. Машина под управлением Linux. Можно и Windows, но, когда занимаешься пентестом приложений для Android, лучше использовать Linux.
  2. Установленный adb. В Ubuntu/Debian/Mint устанавливается командой sudo apt-get install adb.
  3. Рутованный смартфон или эмулятор на базе Android 4.2 и выше. Frida умеет работать и на нерутованном, но для этого тебе придется модифицировать APK подопытного приложения. Это просто неудобно.

Для начала установим Frida:

$ sudo pip install frida

Далее скачаем сервер Frida, который необходимо установить на смартфон. Сервер можно найти на GitHub, его версия должна точно совпадать с версией Frida, которую мы установили на комп. На момент написания статьи это была 10.6.55. Скачиваем:

$ 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) и закидываем сервер на смартфон:

$ adb push frida-server-10.6.55-android-arm /data/local/tmp/frida-server

Теперь заходим на смартфон с помощью adb shell, выставляем нужные права на сервер и запускаем его:

$ adb shell
> su
> cd /data/local/tmp
> chmod 755 frida-server
> ./frida-server

 

Первые шаги

Ок, Frida установлена на комп, сервер запущен на смартфоне (не закрывай терминал с запущенным сервером). Теперь надо проверить, все ли работает как надо. Для этого воспользуемся командой frida-ps:

$ frida-ps -U

Команда должна вывести все процессы, запущенные на смартфоне (флаг -U означает USB, без него Frida выведет список процессов на локальной машине). Если ты видишь этот список, значит, все хорошо и можно приступать к более интересным вещам.

Для начала попробуем выполнить трассировку системных вызовов. Frida позволяет отследить обращения к любым нативным функциям, в том числе системные вызовы ядра Linux. Для примера возьмем системный вызов open(), который используется для открытия файлов на чтение и/или запись. Запустим трассировку Телеграма:

$ frida-trace -i "open" -U org.telegram.messenger

Возьми телефон и немного потыкай интерфейс Телеграма. На экран должны посыпаться сообщения примерно следующего содержания:

open(pathname="/data/user/0/org.telegram.messenger/shared_prefs/userconfing.xml", flags=0x241)

Эта строка означает, что Телеграм открыл файл userconfig.xml внутри каталога shared_prefs в своем приватном каталоге. Каталог shared_prefs в Android используется для хранения настроек, поэтому нетрудно догадаться, что файл userconfig.xml содержит настройки приложения. Еще одна строка:

open(pathname="/storage/emulated/0/Android/data/org.telegram.messenger/cache/223023676_121163.jpg", flags=0x0)

Здесь все еще проще. Телеграм агрессивно кеширует загруженные данные, поэтому для отображения картинки он взял ее из кеша.

open(pathname="/data/user/0/org.telegram.messenger/shared_prefs/stats.xml", flags=0x241)

Еще один файл в каталоге shared_prefs. Судя по всему, какая-то статистика использования.

open(pathname="/dev/ashmem", flags=0x2)

Выглядит странно, не так ли? На самом деле все просто. Файл /dev/ashmem виртуальный, он используется для обмена данными между процессами и системой с помощью IPC-механизма Binder. Проще говоря, эта строка означает, что Телеграм обратился к Android, чтобы выполнить какую-то системную функцию или получить информацию. Такие строки можно смело пропускать.

 

Пишем код

Мы можем перехватывать обращения к любым другим системным вызовам, например connect(), который используется для подключения к удаленным хостам:

$ frida-trace -i "connect" -U com.yandex.browser

Но вывод в данном случае будет не особо информативным:

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, то наверняка заметил примерно такую строку:

connect: Auto-generated handler at "/home/j1m/__handlers__/libc.so/connect.js"

Это автоматически сгенерированный код хука, который Frida выполняет, когда подопытное приложение обращается к указанной функции. Именно он ответственен за вывод тех малоинформативных строк, которые мы увидели. По умолчанию код выглядит так:

onEnter: function (log, args, state) {
    log("connect(" +
        "sockfd=" + args[0] +
        ", addr=" + args[1] +
        ", addrlen=" + args[2] +
    ")");
},

Видно, что хук просто выводит второй аргумент как есть. Но мы знаем, что второй аргумент системного вызова connect() — это указатель на структуру sockaddr, то есть просто адрес в памяти. Сама структура sockaddr имеет следующий вид:

struct sockaddr {
    unsigned short    sa_family;    // address family, AF_xxx
    char              sa_data[14];  // 14 bytes of protocol address
};

А в случае с сокетами типа AF_INET, которые нам и нужны, такой:

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-адрес с точками. Сделаем это, заменив изначальный хук таким:

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:

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) и добавь в него следующие строки:

Java.perform(function() {
    Java.enumerateLoadedClasses({
        onMatch: function(className) {
            console.log(className);
        },
        onComplete: function() {}
    });
});

Это очень простой код. Сначала мы вызываем метод Java.perform(), означающий, что мы хотим подключиться к виртуальной машине Java (или Dalvik/ART в случае Android). Далее мы вызываем метод Java.enumerateLoadedClasses() и передаем ему два колбэка: onMatch() будет выполнен при «обнаружении» класса, onComplete() — в самом конце (как видно, нам этот колбэк не нужен, и мы оставляем его пустым).

Запускаем:

$ frida -U -l enumerate.js org.telegram.messenger

И видим на экране длинный, кажущийся бесконечным список классов, некоторые из них — часть самого приложения, но подавляющее большинство — стандартные классы фреймворка Android (Android загружает весь фреймворк в каждый процесс в режиме copy-on-write).

На самом деле нам этот список не особо интересен. Намного интереснее то, что в любой из этих классов можно внедрить свой код, а если быть точным — переписать тело любого метода любого из этих классов. Для примера возьмем такой код:

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).

Те, кто знаком с разработкой приложений для Android, должны знать, что класс Activity предназначен для создания «экранов» приложения. Он имеет множество методов, один из которых называется onResume(). На самом деле это колбэк, который вызывается во время создания экрана, а также при возврате на него.

Если ты загрузишь данный скрипт во Frida, запустишь Телеграм, затем выйдешь из него, затем снова откроешь, то заметишь, что при каждом возврате в Телеграм в терминале будет появляться сообщение «onResume() got called!».

Точно таким же образом мы можем перехватывать нажатия на кнопки:

Java.perform(function () {
    MainActivity.onClick.implementation = function (v) {
        consle.log('onClick');
        this.onClick(v);
    }
});

А вот пример логирования всех URL, к которым обращается приложение:

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.

Любой из этих скриптов можно запустить без предварительного скачивания с помощью такой команды:

$ frida --codeshare dzonerzy/fridantiroot -U -f com.example.vulnapp

 

Ломаем CrackMe

А теперь давай попробуем взломать что-то реальное. На просторах интернета можно найти множество разных CrackMe. Возьмем первый попавшийся. Точнее, первый из пяти опубликованных в данном репозитории. Crackme-one.apk записывает файл в свой приватный каталог, а наша задача — вытащить содержимое этого файла. Сразу скажу, что существует масса способов сделать это за двадцать секунд, но в то же время это хороший пример, чтобы понять, как работать с Frida.

Итак, скачиваем и устанавливаем приложение:

$ wget https://www.dropbox.com/s/mrjnme2xiv45j4g/crackme-one.apk
$ adb install crackme-one.apk

Нам предлагают нажать кнопку для записи файла либо ввести ответ для проверки. Очевидно, чтобы взломать этот CrackMe, мы должны перехватить управление в момент записи файла. Но как это сделать? На самом деле очень просто. Большинство приложений для Android используют для записи данных либо класс java.io.OutputStream, либо класс java.io.OutputStreamWriter. У каждого из них есть метод write(), который и отвечает за запись файла. Нам необходимо лишь подменить его на свою реализацию и вывести на экран первый аргумент, который содержит либо массив байтов, либо строку:

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);
    };
});

Запускаем:

$ frida -U -f com.reoky.crackme.challengeone -l outputstream_write.js --no-pause

Вуаля, на экране появляется строка

poorly-protected-secret

Отмечу три момента:

  1. В этот раз мы использовали метод overload(), так как класс OutputStreamWriter реализует сразу три метода write() с разным набором аргументов.
  2. Мы использовали опцию --no-pause, которая нужна, если мы хотим выполнить холодный старт приложения и при этом не хотим, чтобы Frida остановила приложение в самом начале.
  3. На самом деле взломать этот CraсkMe можно было бы, просто перейдя в его приватный каталог и прочитав файл (это возможно, так как у нас рутованный смартфон) либо путем декомпиляции приложения (текст лежит в открытом виде). Здесь, однако, есть нюанс: если бы CrackMe хранил строку в зашифрованном виде и расшифровывал ее только перед записью, декомпиляция была бы бесполезна (ну, по крайней мере до тех пор, пока ты не извлек бы ключ шифрования и не написал скрипт расшифровки).
Извлечь строку можно было и с помощью декомпилятора

Выводы

Frida — очень мощный инструмент, с помощью которого можно сделать с подопытным приложением практически все, что угодно. Но это инструмент не для всех, он требует знания JavaScript, понимания принципов работы Android и приложений для него. Так что, если ты рядовой скрипт-кидди, тебе остается довольствоваться автоматизированными инструментами, созданными на основе Frida, например appmon.