R0 CREW

Взлом Adnroid приложений с помощью Frida (Часть 1, Введение)

frida
android
crackme
reverse
ru
#1

Оригинал: codemetrix.net

В прошлом году я посетил RadareCon, там я узнал о Frida, это динамический фреймворк для взаимодействия с бинарными приложениями. Сначала инструмент просто показался мне интересным, чуть позже я понял насколько это веселая вещь. Помните режим бога в играх? Frida позволяет делать тоже самое, только уже с нативными приложениями. В этом посте я расскажу о использовании Frida для игры с Android приложениями. И раз уж вы здесь, мы дополнительно решим несложный Android CrackMe во второй части.

Что такое динамическая бинарная инструментация?

Динамическая бинарная инструментация (Dynamic binary instrumentation) позволяет внедрять сторонний код в уже запущенное приложение, для того чтобы оно делало то, что не делало раньше. Это не эксплуатация инъекции кода через ранее найденные уязвимости. Это не отладка приложения, ведь на самом деле вы не подключаетесь отладчиком к приложению, но это позволяет делать в общем-то похожие вещи. Какие возможности открывает динамическая бинарная инструментация:

  • Доступ к памяти процесса
  • Переопределение функций во время исполнения приложения
  • Вызов функций из импортированных классов
  • Поиск экземпляров объектов в куче и взаимодействие с ними
  • Перехват и трассировка функций

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

FRIDA

Frida “позволяет внедрять JavaScript сниппеты или вашу собственную библиотеку в нативное приложение на Windows, MacOS, Linux, iOS, Android или QNX”. Первоначально инструмент работал на движке Google V8 JavaScript Runtime, но начиная с 9 версии Frida использует Ducktape, при этом у вас есть возможность перейти обратно к V8, если вам это понадобиться. Frida определяет несколько режимов работы для взаимодействия с приложениями (включая возможность использования инструментария даже на устройстве без прав супер-пользователя), но сейчас мы рассмотрим самые частые примеры использования, не особо затрагивая методов их работы.

Для начала вам понадобиться:

  • Frida (В этом руководстве я использую версию 9.1.16)
  • frida-server, исполняемый файл скаченный со страницы релизов (на момент написания это frida-server-9.1.16-android-arm.xz. Версия frida-server должна совпадать с версией Frida.
  • Android Emulator или устройство с root. Frida была разработана для Android 4.4 ARM, но работает и с более поздними версиями. Я успешно использовал Android 7.1.1 ARM для этого руководства. Для CrackMe во второй части статьи вам в любом случае понадобиться, что-то современнее, чем Android 4.4.

Я так же предполагаю использование в качестве хоста операционную систему основанную на Linux, если же вы работает на Windows или MacOS вам, скорее всего, потребуется изменить несколько команд, что бы все точно заработало.

Если вы планируете повторить решение OWASP Unbreakable Crackme Level 1, который решается во второй части статьи, вам так же следует загрузить:

Frida предоставляет несколько APIs и способов для начала анализа. Вы можете использовать командную строку или инструмент наподобие frida-trace для отслеживания низкоуровневых функций (таких как вызов “open” в libc.so) и максимально быстрого начала. Вы можете использовать биндинги на C, NodeJS или Python для нетривиальных задач. Под капотом Frida работает на JavaScript и в большинстве случаев вам потребуется писать код на этом языке. Поэтому, если вы как и я немного недолюбливаете JavaScript (помимо его возможностей XSS), Frida еще одна причина что бы разобраться с ним получше.

Установите Frida, если еще не сделали этого (посмотрите README для других способов установки):

pip install frida
npm install frida

Запустите ваш эмулятор или присоединитесь к вашему устройству, убедитесь, что устройство доступно в adb:

michael@sixtyseven:~$ adb devices
List of devices attached
emulator-5556 device

После, установите frida-server. Распакуйте архив и разместите исполняемый файл на устройстве:

adb push /home/michael/Downloads/frida-server-9.1.16-android-arm /data/local/tmp/frida-server

С помощью adb откройте shell на устройстве, перейдите в root и запустите frida:

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

(Примечание 1: Если frida-server не стартовал убедитесь, что у вас есть root права и файл находиться на устройстве. Я наблюдал несколько странных ошибок с поврежденным файлом при перемещении. Примечание 2: Если вы хотите запустить frida-server в фоновом режиме выполните: ./frida-server &)

В другом терминале, на хостовой системе проверьте, что Frida запустилась и вы можете посмотреть список процессов на Android:

frida-ps -U

аргумент -U означает использование USB и позволяет Frida проверить подключенные USB-устройства, но это также работает с эмулятором. Вы должны получить примерно следующий список процессов:

michael@sixtyseven:~$ frida-ps -U
 PID  Name
----  --------------------------------------------------
 696  adbd
5828  android.ext.services
6188  android.process.acore
5210  audioserver
5211  cameraserver
8334  com.android.calendar
6685  com.android.chrome
6245  com.android.deskclock
5528  com.android.inputmethod.latin
6120  com.android.phone
6485  com.android.printspooler
8355  com.android.providers.calendar
5844  com.android.systemui
7944  com.google.android.apps.nexuslauncher
6416  com.google.android.gms
[...]

Вы можете увидеть ID процесса (PID) и сам запущенный процесс (Name). С помощью Frida вы можете внедрится в любой из этих процессов и начать взаимодействие.

Например, вы можете отслеживать нужные вам вызовы функций в Chrome (запустите Chrome на эмуляторе, если он еще не запущен):

frida-trace -i "open" -U com.android.chrome 

Вы увидите следующее:

michael@sixtyseven:~$ frida-trace -i open -U -f com.android.chrome
Instrumenting functions...                                              
open: Loaded handler at "/home/michael/__handlers__/libc.so/open.js"
Started tracing 1 function. Press Ctrl+C to stop.                       
           /* TID 0x2740 */
   282 ms  open(pathname=0xa843ffc9, flags=0x80002)
           /* TID 0x2755 */
   299 ms  open(pathname=0xa80d0c44, flags=0x2)
           /* TID 0x2756 */
   309 ms  open(pathname=0xa80d0c44, flags=0x2)
           /* TID 0x2740 */
   341 ms  open(pathname=0xa80d06f7, flags=0x2)
   592 ms  open(pathname=0xa77dd3bc, flags=0x0)
   596 ms  open(pathname=0xa80d06f7, flags=0x2)
   699 ms  open(pathname=0xa80d105e, flags=0x80000)
   717 ms  open(pathname=0x9aff0d70, flags=0x42)
   742 ms  open(pathname=0x9ceffda0, flags=0x0)
   758 ms  open(pathname=0xa63b04c0, flags=0x0)

Команда frida-trace генерирует небольшой файл JavaScript, который внедряется в процесс и отслеживает вызовы функций. Посмотрим, как устроен скрипт open.js в handlers/libc.so/open.js. В нем происходит перехват вызовов функции open из libc.so и вывод ее аргументов. Собственно, сам код для Frida:

[...]
onEnter: function (log, args, state) {
    log("open(" + "pathname=" + args[0] + ", flags=" + args[1] + ")");
},
[...]

Обратите внимание как Frida предоставляет доступ к аргументам функции open во время ее вызова (args[0], args[1] и т.д.) внутри приложения Chrome. Давайте немного изменим скрипт. Было бы неплохо, если бы мы получали действительный путь до открытого файла, вместо адреса памяти где храниться данный путь. К счастью, мы можем напрямую обращаться к памяти с помощью Frida. Посмотрите на Frida API и объект Memory. Мы можем изменить наш скрипт, что бы он выводил содержимое по адресу памяти, где размещена UTF8 строка и мы получали более понятный вывод. После изменения код будет выглядеть примерно так:

onEnter: function (log, args, state) {
    log("open(" + "pathname=" + Memory.readUtf8String(args[0])+ ", flags=" + args[1] + ")");
},

(мы просто добавили функцию Memory.readUtf8String) и преобразили вывод:

michael@sixtyseven:~$ frida-trace -i open -U -f com.android.chrome
Instrumenting functions...                                              
open: Loaded handler at "/home/michael/__handlers__/libc.so/open.js"
Started tracing 1 function. Press Ctrl+C to stop.                       
           /* TID 0x29bf */
   240 ms  open(pathname=/dev/binder, flags=0x80002)
           /* TID 0x29d3 */
   259 ms  open(pathname=/dev/ashmem, flags=0x2)
           /* TID 0x29d4 */
   269 ms  open(pathname=/dev/ashmem, flags=0x2)
           /* TID 0x29bf */
   291 ms  open(pathname=/sys/qemu_trace/process_name, flags=0x2)
   453 ms  open(pathname=/dev/alarm, flags=0x0)
   456 ms  open(pathname=/sys/qemu_trace/process_name, flags=0x2)
   562 ms  open(pathname=/proc/self/cmdline, flags=0x80000)
   576 ms  open(pathname=/data/dalvik-cache/arm/system@app@Chrome@Chrome.apk@classes.dex.flock, flags=0x42)

Frida выводит пути до файлов. Легко, не правда ли?

Еще одна вещь о которой стоит рассказать, вы можете либо запустить приложение перед внедрением Frida, либо передать аргумент -f для Frida, чтобы приложение было запущено автоматически.

Теперь используем интерфейс командной строки Frida, frida-cli:

frida -U -f com.android.chrome 

Этой командой вы запустите приложение Chrome. Однако это не запускает основной процесс приложения. Это нужно для того что бы вы могли внедрить код во Frida, еще перед началом исполнения приложения. К сожалению, в моем случае приложение всегда закрывалось по истечению двух секунд. Это не то, что нам нужно. За эти две секунды вы можете набрать %resume%, как предлагает это cli и приложение сможет запустить свой основной процесс. Или вы можете запускать приложения без его остановки, для этого передайте аргумент –no-pause, в этом случае приложение все еще будет запускается посредством Frida.

В обоих случаях вы получаете shell (который не закрывается автоматически), куда вы можете вводить команды Frida следуя Frida JavaScript API. Нажмите TAB для просмотра доступных команд. Оболочка так же поддерживает автодополнение команд.

Большинство возможностей хорошо документированы. Для Android особенно внимательно посмотрите на секцию Java в JavaScript API (Я буду говорить о “Java API”, хотя технически это просто обертка на JavaScript для доступа к Java объектам). Мы сосредоточимся на Java API, поскольку это наиболее удобный способ взаимодействия с Android приложениями. Вместо перехвата функций из libc мы будем работать напрямую с функциями и объектами из Java. (Примечание: Если вы заинтересованы, что вы можете делать с помощью Frida за пределами Java API, перехватывая более низкоуровневые C функции на Android, как мы делали с помощью frida-trace, посмотрите раздел документации посвященный функциям. Я не рассматриваю это здесь.)

Начиная работу с Java API просто получим версию Android на устройстве:

[USB::Android Emulator 5556::['com.android.chrome']]-> Java.androidVersion
"7.1.1"

Или список загруженных классов (Предупреждение: Команда может иметь довольно длинный вывод. Дальше я объясню выполняемый код.)

[USB::Android Emulator 5556::['com.android.chrome']]-> Java.perform(function(){Java.enumerateLoadedClasses({"onMatch":function(className){ console.log(className) },"onComplete":function(){}})})

org.apache.http.HttpEntityEnclosingRequest
org.apache.http.ProtocolVersion
org.apache.http.HttpResponse
org.apache.http.impl.cookie.DateParseException
org.apache.http.HeaderIterator

Мы ввели достаточно длинную команду, с некоторой вложенной функцией. Прежде всего обратим внимание, что код обернут в Java.perform(function(){ … }), что является требованием Java API.

Это тело функции, находящейся внутри обертки Java.perform:

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

Код достаточно прост: Мы перечисляем все загруженные классы используя Java.enumerateLoadedClasses из Frida API, и выводим каждый элемент в консоль с помощью console.log. Такой тип Callback является шаблоном часто встречающимся во Frida. Это некоторое представление Callback объекта:

{
  "onMatch":function(arg1, ...){ ... },
  "onComplete":function(){ ... },
}

onMatch вызывается с одним и более аргументом, когда Frida находит совпадение с вашим запросом, а onComlete, когда Frida заканчивает итерирование возможных совпадений.

Сейчас мы углубимся в магию Frida и перезапишем функцию. Кроме того мы будем загрузим код из внешнего скрипта, вместо того что бы набирать его в консоли, это намного удобнее. Сохраните следующий код, например, как chrome.js:

Java.perform(function () {
    var Activity = Java.use("android.app.Activity");
    Activity.onResume.implementation = function () {
        console.log("[*] onResume() got called!");
        this.onResume();
    };
});

Этот код перезаписывает функцию onResume в классе android.app.Activity. Происходит вызов Java.use для получения доступа к реализации функции и последующего переопределения функции onResume в классе. В новой реализации функция вызывает оригинальную функцию, через this.onResume(), так что приложение продолжит работать нормально.

Открой ваш эмулятор, откройте Chrome и выполните внедрение кода передав -l.

frida -U -l chrome.js com.android.chrome

Теперь, когда происходит вызов onResume, например, перейдите в другое приложение и вернитесь обратно в Chrome, вы увидите:

[*] onResume() got called!

Приятно, не так ли? Мы фактически перезаписали функцию из приложения. Это дает нам много возможностей для контроля поведения нашего выбранного приложения. Но мы можем больше: мы также можем искать объекты в куче с помощью Java.choose.

Прежде чем мы продолжим, предостережение: Когда наша эмуляция достаточно медленная Frida может останавливается по тайм-ауту. Для предотвращения этого оберните свои скрипты в функцию setImmediate или экспортируйте их как RPC. По умолчанию RPC во Frida не имеют тайм-аутов (спасибо @oleavr за эти советы). setImmediate автоматически перезапускает ваши скрипты во Frida, после того как вы изменили исходный файл, поэтому это еще и удобно. Также это запускает ваши скрипты в фоновом режиме. Это означает, что сразу получите cli, хотя Frida все еще обрабатывает ваши скрипты. Просто подождите и не покидайте cli, пока Frida не показала вам вывод ваших скриптов. Еще раз отредактируем chrome.js:

setImmediate(function() {
    console.log("[*] Starting script");
    Java.perform(function () {

        Java.choose("android.view.View", { 
             
             "onMatch":function(instance){
                  console.log("[*] Instance found");
             },

             "onComplete":function() {
                  console.log("[*] Finished heap search")
             }
        });

    });
});

Запустим это через команду frida -U -l chrome.js com.android.chrome, получив следующий вывод:

[*] Starting script

[*] Instance found

[*] Instance found

[*] Instance found

[*] Instance found

[*] Finished heap search

Итак, мы обнаружили 4 экземпляра класса android.view.View в куче. Давайте посмотрим, что мы сможем с ними сделать. Может быть мы можем вызвать их методы. Добавим instance.toString() в наш вывод console.log (поскольку мы использовали setImmediate мы можем изменить скрипт и он перезагрузится во Frida автоматически)

setImmediate(function() {
    console.log("[*] Starting script");
    Java.perform(function () {

        Java.choose("android.view.View", { 
             
             "onMatch":function(instance){
                  console.log("[*] Instance found: " + instance.toString());
             },

             "onComplete":function() {
                  console.log("[*] Finished heap search")
             }
        });

    });
});

Это вернет:

[*] Starting script

[*] Instance found: android.view.View{7ccea78 G.ED..... ......ID 0,0-0,0 #7f0c01fc app:id/action_bar_black_background}

[*] Instance found: android.view.View{2809551 V.ED..... ........ 0,1731-0,1731 #7f0c01ff app:id/menu_anchor_stub}

[*] Instance found: android.view.View{be471b6 G.ED..... ......I. 0,0-0,0 #7f0c01f5 app:id/location_bar_verbose_status_separator}

[*] Instance found: android.view.View{3ae0eb7 V.ED..... ........ 0,0-1080,63 #102002f android:id/statusBarBackground}

[*] Finished heap search

Frida просто вызывает метод toString экземпляра класса android.view.View. Здорово. Таким образом с помощью Frida мы можем читать память процесса, изменять функции, находить инстансы классов в куче и все это в несколько строчек кода.

Теперь, вы должны иметь общее понимание Frida и вы сможете погрузится глубже в документацию и API для собственного использования. В завершении поста я хотел бы коснуться еще двух вещей, это Frida’s bindings и r2frida. Но сначала небольшое предупреждение.

Предостережения

Когда вы экспериментируете с Frida вы заметите, что есть некоторые нестабильности. Во-первых внедрение стороннего когда в другой процесс может вызвать сбои, потому приложение будет работать не так, как планировалось. Во-вторых, сама Frida является экспериментальной. Она работает, но часто вы должны попробовать несколько способов для получения желаемого результата. Например, Frida всегда крашится, когда я пытаюсь загрузить скрипт и запустить процесс в одну команду. Вместо этого я сначала запускаю процесс, а затем внедряю скрипт. Поэтому я и показывал вам разные способы избежания тайм-аута. Возможно, вам будет нужно какой из них работает в вашем случае.

Python bindings

Если вы хотите автоматизировать вашу работу с Frida еще больше, вы должны посмотреть на биндинги для Python, C и NodeJS, их очень просто использовать как только вы разобрались как работать с Frida. Например, инъекция вашего скрипта chrome.js из Python, вы можете использовать Python bindings и создать

chrome.py

:

#!/usr/bin/python
import frida

# put your javascript-code here
jscode= """
console.log("[*] Starting script");

Java.perform(function() {
   
   var Activity = Java.use("android.app.Activity");
    Activity.onResume.implementation = function () {
        console.log("[*] onResume() got called!");
        this.onResume();
    };

});

"""

# startup frida and attach to com.android.chrome process on a usb device
session = frida.get_usb_device().attach("com.android.chrome")

# create a script for frida of jsccode
script = session.create_script(jscode)

# and load the script
script.load()

Если вы хотите хотите завершить сессию Frida и скрипты внедренные в этой сессии вызовите session.detach().

Для других примеров, как обычно, смотрите документацию Frida.

Frida and Radare2: r2frida

Было бы неплохо, если бы мы могли использовать фрэймворк дизассеблирования, например radare2 для изучения памяти приложения. Здесь появляется r2frida. Вы можете использовать r2frida для подключения radare2 к Frida и проводить статический анализ и дизассеблировать память процесса. Я не буду вдаваться в подробности r2froda здесь, потому что он предполагает знаний Radare2 (на что совершенно точно следует посмотреть, если вы еще не сделали этого), но тем не менее, я хочу дать вам представление об его использовании.

Вы можете установит r2frida используя Radare2 packet management (кончено, если radare2 у вас уже установлен):

r2pm install r2frida

Вернитесь назад на пример с frida-trace, удалите или переименуйте наш измененный скрипт, теперь вывод будет такой же каким он был изначально:

michael@sixtyseven:~$ frida-trace -i open -U -f com.android.chrome
Instrumenting functions...                                              
open: Loaded handler at "/home/michael/__handlers__/libc.so/open.js"
Started tracing 1 function. Press Ctrl+C to stop.                       
           /* TID 0x2740 */
   282 ms  open(pathname=0xa843ffc9, flags=0x80002)
           /* TID 0x2755 */
   [...]

В r2frida вы можете легко вы можете посмотреть что лежит по выводимым адресам и прочитать путь, в данном случае /dev/binder.

root@sixtyseven:~# r2 frida://emulator-5556/com.android.chrome
 -- Enhance your graphs by increasing the size of the block and graph.depth eval variable.
[0x00000000]> s 0xa843ffc9
[0xa843ffc9]> px
- offset -   0 1  2 3  4 5  6 7  8 9  A B  C D  E F  0123456789ABCDEF
0xa843ffc9  2f64 6576 2f62 696e 6465 7200 4269 6e64  /dev/binder.Bind
0xa843ffd9  6572 2069 6f63 746c 2074 6f20 6f62 7461  er ioctl to obta
0xa843ffe9  696e 2076 6572 7369 6f6e 2066 6169 6c65  in version faile
0xa843fff9  643a 2025 7300 4269 6e64 6572 2064 7269  d: %s.Binder dri
[...]

Синтаксис доступа к процессу и подключения r2frida:

r2 frida://DEVICE-ID/PROCESS

Также проверьте доступные команды r2frida, они доступны с помощью префикса =!, который помимо прочего позволяет вам быстро находить области памяти с требуемым содержимым или записывать по требуемым адресам.

[0x00000000]> =!?
r2frida commands available via =!
?                          Show this help
?V                         Show target Frida version
/[x][j] <string|hexpairs>  Search hex/string pattern in memory ranges (see search.in=?)
/w[j] string               Search wide string
[...]

Полезные материалы:

Во второй части этого руководства, мы будем использовать Frida для красивого решения небольшого CrackMe.

© Translated by norver special for r0 Crew
Уроки взлома Adnroid приложений с помощью Frida