Tipper
Member
- Joined
- Feb 11, 2018
- Messages
- 625
- Reaction score
- 244
- Age
- 32
Frida, это динамический фреймворк для взаимодействия с бинарными приложениями. Сначала инструмент просто показался мне интересным, чуть позже я понял насколько это веселая вещь. Помните режим бога в играх? Frida позволяет делать тоже самое, только уже с нативными приложениями. В этом посте я расскажу о использовании Frida для игры с Android приложениями. И раз уж вы здесь, мы дополнительно решим несложный Android CrackMe во второй части.
Что такое динамическая бинарная инструментация?
Динамическая бинарная инструментация (Dynamic binary instrumentation) позволяет внедрять сторонний код в уже запущенное приложение, для того чтобы оно делало то, что не делало раньше. Это не эксплуатация инъекции кода через ранее найденные уязвимости. Это не отладка приложения, ведь на самом деле вы не подключаетесь отладчиком к приложению, но это позволяет делать в общем-то похожие вещи. Какие возможности открывает динамическая бинарная инструментация:
- Доступ к памяти процесса
- Переопределение функций во время исполнения приложения
- Вызов функций из импортированных классов
- Поиск экземпляров объектов в куче и взаимодействие с ними
- Перехват и трассировка функций
FRIDA
Frida "позволяет внедрять JavaScript сниппеты или вашу собственную библиотеку в нативное приложение на Windows, MacOS, Linux, iOS, Android или QNX". Первоначально инструмент работал на движке Google V8 JavaScript Runtime, но начиная с 9 версии Frida использует Ducktape, при этом у вас есть возможность перейти обратно к V8, если вам это понадобиться. Frida определяет несколько режимов работы для взаимодействия с приложениями (включая возможность использования инструментария даже на устройстве без прав супер-пользователя), но сейчас мы рассмотрим самые частые примеры использования, не особо затрагивая методов их работы.
Для начала вам понадобиться:
- Frida
- 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.
Если вы планируете повторить решение 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@[email protected]@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 не имеют тайм-аутов. 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 вы заметите, что есть некоторые нестабильности. Во-первых внедрение стороннего когда в другой процесс может вызвать сбои, потому приложение будет работать не так, как планировалось. Во-вторых, сама 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 используя Frida. После того что мы узнали о Frida это должно быть очень легко, ну теоретически. Если вы хотите повторять за мной, скачайте:
Кончено, я предполагаю, что вы уже установили Frida (версии 9.1.16 или позднее) на свой компьютере и запустили соответствующий frida-server на устройстве с правами супер-пользователя. Для этого руководства я буду использовать образ Android 7.1.1 ARM запущенный в эмуляторе.
Установите Uncrackable Crackme Level 1 на ваше устройство:
adb install sg.vantagepoint.uncrackable1.apk
Подождите пока он установится, затем запустите его (оранжевая иконка в правом нижнем углу):
После запуска приложение сообщит, что не оно не работает на устройстве с правами супер-пользователя:
Если вы нажмете “ОК” приложение сразу же закроется. Хм. Нехорошо. Похоже мы не сможем решить CrackMe таким способом. А разве мы ожидали другого? Давайте разберемся что происходит, разберем как работает приложение внутри.
Сконвертируем APK в jar c помощью dex2jar:
michael@sixtyseven:/opt/dex2jar/dex2jar-2.0$ ./d2j-dex2jar.sh -o /home/michael/UnCrackable-Level1.jar /home/michael/UnCrackable-Level1.apk
dex2jar /home/michael/UnCrackable-Level1.apk -> /home/michael/UnCrackable-Level1.jar
Теперь загрузим файл а BytecodeViewer (или любой другой дизассемблер на ваш выбор, главное что бы он поддерживал Java). Вы также можете попробовать загрузить APK напрямую в BytecodeViewer или открыть его classes.dex, но у меня это не работало, поэтому я предварительно сконвертировал в jar с помощью dex2jar.
В BytecodeViewer выберете View->Panel 1->CFR->Java для использования CFR Decompiler. Вы можете включить отображение smali кода во втором окне (Panel 2) если вы хотите сравнивать вывод декомпилятора с дизассемблированным листингом (который зачастую точнее декомпилированного кода).
Вывод CFR Decompiler для MainActivity:
package sg.vantagepoint.uncrackable1;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.text.Editable;
import android.view.View;
import android.widget.EditText;
import sg.vantagepoint.uncrackable1.a;
import sg.vantagepoint.uncrackable1.b;
import sg.vantagepoint.uncrackable1.c;
public class MainActivity
extends Activity {
private void a(String string) {
AlertDialog alertDialog = new AlertDialog.Builder((Context)this).create();
alertDialog.setTitle((CharSequence)string);
alertDialog.setMessage((CharSequence)"This in unacceptable. The app is now going to exit.");
alertDialog.setButton(-3, (CharSequence)"OK", (DialogInterface.OnClickListener)new b(this));
alertDialog.show();
}
protected void onCreate(Bundle bundle) {
if (sg.vantagepoint.a.c.a() || sg.vantagepoint.a.c.b() || sg.vantagepoint.a.c.c()) {
this.a("Root detected!"); //This is the message we are looking for
}
if (sg.vantagepoint.a.b.a((Context)this.getApplicationContext())) {
this.a("App is debuggable!");
}
super.onCreate(bundle);
this.setContentView(2130903040);
}
public void verify(View object) {
object = ((EditText)this.findViewById(2131230720)).getText().toString();
AlertDialog alertDialog = new AlertDialog.Builder((Context)this).create();
if (a.a((String)object)) {
alertDialog.setTitle((CharSequence)"Success!");
alertDialog.setMessage((CharSequence)"This is the correct secret.");
} else {
alertDialog.setTitle((CharSequence)"Nope...");
alertDialog.setMessage((CharSequence)"That's not it. Try again.");
}
alertDialog.setButton(-3, (CharSequence)"OK", (DialogInterface.OnClickListener)new c(this));
alertDialog.show();
}
}
Взглянув на остальные декомпилированные файлы, можно заметить, что приложение достаточно небольшое, и возможно, мы могли бы решить CrackMe просто разобрав алгоритм расшифровки и преобразования строк. Однако, после знакомства с Frida разве мы не можем сделать проще?
Сперва посмотрим как приложение проверяет наличие root. Прямо над сообщением "Root detected" можно увидеть код:
if (sg.vantagepoint.a.c.a() || sg.vantagepoint.a.c.b() || sg.vantagepoint.a.c.c())
Если вы перейдете в класс sg.vantagepoint.a.c вы увидите различные проверки на root:
public static boolean a()
{
String[] a = System.getenv("PATH").split(":");
int i = a.length;
int i0 = 0;
while(true)
{
boolean b = false;
if (i0 >= i)
{
b = false;
}
else
{
if (!new java.io.File(a[i0], "su").exists())
{
i0 = i0 + 1;
continue;
}
b = true;
}
return b;
}
}
public static boolean b()
{
String s = android.os.Build.TAGS;
if (s != null && s.contains((CharSequence)(Object)"test-keys"))
{
return true;
}
return false;
}
public static boolean c()
{
String[] a = new String[7];
a[0] = "/system/app/Superuser.apk";
a[1] = "/system/xbin/daemonsu";
a[2] = "/system/etc/init.d/99SuperSUDaemon";
a[3] = "/system/bin/.ext/.su";
a[4] = "/system/etc/.has_su_daemon";
a[5] = "/system/etc/.installed_su_daemon";
a[6] = "/dev/com.koushikdutta.superuser.daemon/";
int i = a.length;
int i0 = 0;
while(i0 < i)
{
if (new java.io.File(a[i0]).exists())
{
return true;
}
i0 = i0 + 1;
}
return false;
}
Используя Frida мы можем сделать так, что бы все эти методы возвращали false, перезаписав их как мы делали еще в первой части руководства. Но что происходит, когда функция возвращает true, потому что обнаруживает root. Как мы видели в функции a, находящейся в MainActivity происходит открытие диалога. Так же устанавливается обработчик onClickListener на нажатие кнопки "OK".
alertDialog.setButton(-3, (CharSequence)"OK", (DialogInterface.OnClickListener)new b(this));
Реализация onClickListener довольно небольшая:
package sg.vantagepoint.uncrackable1;
class b implements android.content.DialogInterface$OnClickListener {
final sg.vantagepoint.uncrackable1.MainActivity a;
b(sg.vantagepoint.uncrackable1.MainActivity a0)
{
this.a = a0;
super();
}
public void onClick(android.content.DialogInterface a0, int i)
{
System.exit(0);
}
}
После события нажатия приложение закрывается, вызывая System.exit(0). Таким образов все, что нам нужно сделать это избежать закрытия приложения, переписав метод onClick в Frida. Создайте файл uncrackable1.js и разместите там следующий код:
setImmediate(function() { //prevent timeout
console.log("[*] Starting script");
Java.perform(function() {
bClass = Java.use("sg.vantagepoint.uncrackable1.b");
bClass.onClick.implementation = function(v) {
console.log("[*] onClick called");
}
console.log("[*] onClick handler modified")
})
})
Теперь скрипт должен быть понятен: мы оборачиваем наш код функцией setImmediate для предотвращения тайм-аутов (возможно вам это и не понадобится), а затем вызываем, Java.perform чтобы использовать функции Frida для работы с Java. Затем начинается настоящая магия: мы получаем класс который реализует интерфейс OnClickListener и перезаписываем его метод onClick. В нашей версии эта функция просто пишет какой-то консольный вывод. В отличие от оригинала, выход из приложения не выполняется. Поскольку оригинал функции onClickHandler подменяется на нашу внедренную функцию с помощью Frida, она никогда не выполнится и приложение больше не закроется, когда мы нажмем кнопку "OK" в диалоге. Давайте проверим это.
Откройте приложение (пусть оно снова отобразит диалоговое окно «Root detected»)
Внедрите скрипт:
frida -U -l uncrackable1.js sg.vantagepoint.uncrackable1
Дайте Frida несколько секунд для применения изменений кода, пока вы не увидите сообщение “onClick handler modified” (возможно вы получите shell раньше, из-за оборачивания в setImmediate функция выполняется в фоне):
Затем нажмите кнопку "ОК" в приложении. Если все прошло хорошо приложение не закроется.
Отлично, диалог исчезает, и мы можем ввести пароль. Давайте введем что-нибудь и посмотрим, что происходит:
Неверный код, как, собственно, и следовало ожидать. Но мы знаем, что нам нужно искать: какие-либо функции шифрования, расшифровки и сравнения нашего ввода.
Снова смотрим в MainActivity и видим функцию:
public void verify(View object) {
Она вызывает метод a из класса sg.vantagepoint.uncrackable1.a
if (a.a((String)object)) {
Это декомпилированный код класса sg.vantagepoint.uncrackable1.a:
package sg.vantagepoint.uncrackable1;
import android.util.Base64;
import android.util.Log;
/*
* Exception performing whole class analysis ignored.
*/
public class a {
public static boolean a(String string) {
byte[] arrby = Base64.decode((String)"5UJiFctbmgbDoLXmpL12mkno8HT4Lv8dlat8FxR2GOc=", (int)0);
byte[] arrby2 = new byte[]{};
try {
arrby2 = arrby = sg.vantagepoint.a.a.a((byte[])a.b((String)"8d127684cbc37c17616d806cf50473cc"), (byte[])arrby);
}
catch (Exception var2_2) {
Log.d((String)"CodeCheck", (String)("AES error:" + var2_2.getMessage()));
}
if (!string.equals(new String(arrby2))) return false;
return true;
}
public static byte[] b(String string) {
int n = string.length();
byte[] arrby = new byte[n / 2];
int n2 = 0;
while (n2 < n) {
arrby[n2 / 2] = (byte)((Character.digit(string.charAt(n2), 16) << 4) + Character.digit(string.charAt(n2 + 1), 16));
n2 += 2;
}
return arrby;
}
}
Обратите внимание на вызов string.equals для сравнения строк в конце метода a и создание строки arrby2 в блоке try выше. arrby2 это значение возвращаемое из функции sg.vantagepoint.a.a.a. string.equals сравнивает наш ввод с arrby2. Итак, нам нужно получить значение возвращаемое из sg.vantagepoint.a.a.a.
Мы можем начать реверс-инжинерить преобразование строк и функций расшифровки и работать с оригинальным зашифрованными строками, которые содержаться в вышеприведенном коде. Или мы можем оставить все эти действия приложению, не заботясь о них и просто перехватить возвращаемое значение функции sg.vantagepoint.a.a.a. Возвращаемое значение является уже расшифрованной строкой (в виде массива байтов) с которой сравнивается наш ввод. Следующий скрипт нам в этом поможет:
aaClass = Java.use("sg.vantagepoint.a.a");
aaClass.a.implementation = function(arg1, arg2) {
retval = this.a(arg1, arg2);
password = ''
for(i = 0; i < retval.length; i++) {
password += String.fromCharCode(retval);
}
console.log("[*] Decrypted: " + password);
return retval;
}
console.log("[*] sg.vantagepoint.a.a.a modified");
Мы перезаписываем функцию sg.vantagepoint.a.a.a, перехватываем ее возвращаемое значение, а после преобразовываем в читаемую строку. Это расшифрованная строка, которую мы ищем, поэтому вы выводим ее в консоли и надеемся, что получили решение.
Соединяем два скрипта в один полный:
setImmediate(function() {
console.log("[*] Starting script");
Java.perform(function() {
bClass = Java.use("sg.vantagepoint.uncrackable1.b");
bClass.onClick.implementation = function(v) {
console.log("[*] onClick called.");
}
console.log("[*] onClick handler modified")
aaClass = Java.use("sg.vantagepoint.a.a");
aaClass.a.implementation = function(arg1, arg2) {
retval = this.a(arg1, arg2);
password = ''
for(i = 0; i < retval.length; i++) {
password += String.fromCharCode(retval);
}
console.log("[*] Decrypted: " + password);
return retval;
}
console.log("[*] sg.vantagepoint.a.a.a modified");
});
});
Давайте запустим этот скрипт. Как и прежде сохраните его как uncrackable1.js и выполните (если Frida не сделает это автоматически):
frida -U -l uncrackable1.js sg.vantagepoint.uncrackable1
Дождитесь сообщения о модификации sg.vantagepoint.a.a.a, после чего нажмите "ОК" в диалоге Root detected введите что-нибудь в поле секретного кода и нажмите проверить. Проверку мы не прошли, но посмотрим на вывод во Frida:
michael@sixtyseven:~/Development/frida$ frida -U -l uncrackable1.js sg.vantagepoint.uncrackable1
____
/ _ | Frida 9.1.16 - A world-class dynamic instrumentation framework
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at http://www.frida.re/docs/home/
[*] Starting script
[USB::Android Emulator 5554::sg.vantagepoint.uncrackable1]->[*] onClick handler modified
[*] sg.vantagepoint.a.a.a modified
[*] onClick called.
[*] Decrypted: I want to believe
Хорошо. Теперь мы имеем расшифрованную строку I want to believe. Вот и все, давайте проверим, что она действительно подходит:
Заключение
Я надеюсь, что к этому моменту вы, по крайне мере, немного впечатлены тем, что можете делать с Frida и какие возможности открывает динамическая бинарная иструментация. Если вам понравилась эта статья и вы ждете дальнейших материалов по Frida, то мы рады обрадовать вас - будет еще одна статья.