Frida - универсальный динамический фреймворк, позволяющий внедрять сторонний код в приложения БЕЗ изменения самих приложений.
Сегодня мы разберём работу с Frida на примере Android приложений. Я не буду рассказывать тут про frida-trace, frida-discover, etc.
Тут я расскажу про написание своих скриптов под Frida. В первом примере мы будем внедряться в наше приложение,
а в других - будем работать на практике (обходить кастомный SSL Pinning, решать crackme и не только)
1. Frida
2. Эмулятор Android Genymotion
3. Python 3
4. Декомпилятор Jadx
5. Genymotion ARM Translation Kit
Дисклеймер:
Я не несу ответственности за ваши действия. Вы все делаете на свой страх и риск
Установка:
(я же не символы тут набиваю)
Первое приложение - калькулятор
Итак, начнём мы с внедрения в простое приложение, которое 1 раз в секунду выводит произведение двух чисел
Java:
package xss.pro.app;
import android.app.Activity;
import android.os.Bundle;
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
while (true) {
try {Thread.sleep(250);} catch (InterruptedException ignored) {}
System.out.println("Total=" + multiply(10, 123));
}
}
int multiply(int x , int y){
System.out.println("X=" + x);
System.out.println("Y=" + y);
return x * y;
}
}
Запустив, смотрим вывод. X=10, Y=123. Теперь мы будем получать эти значения из Frida
Создаем .js файл и пишем основу для внедрения в Java:
JavaScript:
Java.perform(() => {
});
В нашем примере это xss.pro.app.MainActivity, следовательно, пишем Java.use("xss.pro.app.MainActivity").
Чтобы переназначить функцию, мы будем использовать функцию implementation: Java.use("xss.pro.app.MainActivity").multiply.implementation = function (x, y) {} и добавим туда простой console.log.
В итоге:
JavaScript:
Java.perform(() => {
Java.use("xss.pro.app.MainActivity").multiply.implementation = function (x, y) {
console.log("X=", x, "Y=", y);
};
});
Теперь запускаем: frida -U -f xss.pro.app -l script.js --no-pause
Немного подробнее об аргументах:
-U - подключения к frida-server на удаленном устройстве (в нашем случае на эмуляторе)
-f xss.pro.app - имя пакета приложения
-l script.js - скрипт для внедрения
--no-pause - сразу запускает приложение
Запустив, увидим что мы смогли получить X и Y:
Но у нас есть ошибка: мы ничего не возвращаем. Предлагаю исправить это - добавим return.
Мы имеем доступ ко всем функциям в данном классе, и можем использовать this.multiply: var ret = this.multiply(x,y); и return ret;
Сохраняем код (Frida автоматически обновит его при изменении файла) и видим что ошибка ушла (я добавил ещё один console.log):
Теперь предлагаю заменить вывод. Как Вы все поняли (надеюсь), для этого достаточно заменить переменную в return: return 1337;
Смотрим в logcat:
Аналогично мы можем подменять и начальные переменные (x, y):
Второе приложение - Мегафон Банк
Итак, это конечно все хорошо, но это очень просто. Предлагаю начать с обхода SSL Pinning в приложении Мегафон Банк (ТОЛЬКО В ОЗНАКОМИТЕЛЬНЫХ ЦЕЛЯХ).
Запускаем сниффер и видим печальную картину:
Из ошибки сниффера точно становится ясно, что это SSL Pinning
Итак, загружаем наш APK в Jadx, включаем деобфускацию и начинаем поиск. Искать сертификаты стоит по BEGIN и sha256/
Смотрим по BEGIN и видим класс com.megafon.bank.utils.SslUtils
В нем есть переменная TRUSTED_CERTS, в которой и содержаться все сертификаты. Мы могли бы просто пересобрать приложение, но кто знает какие там ещё есть защиты (ведь при модификации мы теряем подпись разработчика и проверку Safetynet).
Поэтому и будем использовать Frida. Смотрим немного вниз и видим где используется наша строка с сертификатами
Нажимаем Ctrl и нажимаем на нашу функцию .mo27468a (она у Вас будет называться по другому, это всё после деобфускатора).
Откроется сама функция
Обязательно запоминаем /* renamed from: a */ потому что нам надо использовать оригинальные названия функций, а не из деобфускатора
Смотрим в начало класса и тоже запоминаем оригинальное название:
JavaScript:
Java.use("h0.j").a.implementation = function (x, y) {
console.log(x,y)
return this.a(x,y);
};
В нашей функции типы переменных String и Charset, следовательно, выбираем .overload('java.lang.String', 'java.nio.charset.Charset') и добавляем перед .implementation.
Далее подменяем первый аргумент (String) на наш сертификат (сниффера) в формате PEM. Делаем запрос на авторизацию и смотрим:
Мы обошли SSL Pinning!
JavaScript:
Java.perform(() => {
Java.use("h0.j").a.overload('java.lang.String', 'java.nio.charset.Charset').implementation = function (x, y) {
x = "-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----\n"
return this.a(x,y);
};
});
Работа с Python (или почти безграничные возможности)
Конечно, для базовых скриптов, какие мы рассмотрели, спокойно и хватает возможностей V8, но мы не будем на этом останавливаться.
Нам понадобится начальный код для запуска скрипта:
Python:
import time, frida
device = frida.get_usb_device() # получаем девайс с Frida
pid = device.spawn(["owasp.mstg.uncrackable1"]) # запускаем приложение
device.resume(pid) # убираем приложение с паузы
time.sleep(1) # ждём загрузки
session = device.attach(pid) # подключаем Frida к приложению
with open("script.js") as f:
script = session.create_script(f.read()) # читаем скрипт из файла
script.load() # загружаем скрипт
input() # вечно ждём
Python:
def on_message(message, *args):
print(message)
Python:
script.on("message", on_message)
Python:
script.post({"x": 1337, "y": 1234})
JavaScript:
send({"x": x, "y": y})
Теперь будем принимать в самом скрипте значения из Python. Для начала обозначим переменные: let new_x, new_y;
Дальше с функцией recv будем принимать наши значения:
JavaScript:
recv(function (update) {
new_x = update.x;
new_y = update.y;
}).wait();
Смотрим в logcat:
Третье приложение - Crackme
Вот теперь мы можем начать делать серьезные вещи. Для примера я возьму OWASP MSTG Android Crackme Level 1
Из условий у нас - This app holds a secret inside. Can you find it?.
Предлагаю в начале установить приложение. Запускаем и видим проверку на Root:
Начинаем прямо с неё. Загружаем APK в Jadx и смотрим MainActivity:
Тут видно что ещё есть проверка на дебаггер, но она нас не интересует. Смотрим функции m2a, m3b, m4c:
Они возвращают false, если Root не найден. Меняем все выводы на false:
JavaScript:
Java.use("sg.vantagepoint.a.c").a.implementation = function () {return false};
Java.use("sg.vantagepoint.a.c").b.implementation = function () {return false};
Java.use("sg.vantagepoint.a.c").c.implementation = function () {return false};
Смотрим чуть ниже и видим где идёт сама проверка. А именно функция m6a:
Смотрим её код и видим что она сверяет результат дешифрования AES и строку пользователя:
AES у нас дешифрует функция m0a, предлагаю вытащить её вывод:
JavaScript:
Java.use("sg.vantagepoint.a.a").a.implementation = function (bArr, bArr2) {
let ret = this.a(bArr, bArr2);
console.log(ret)
return ret;
}
Предлагаю преобразовать его в текст:
JavaScript:
let decoded = '';
for (let i = 0; i < ret.length; i++) {
decoded += String.fromCodePoint(ret[i]);
}
console.log(decoded)
Вводим эту строку и проверяем что мы правильно решили Crackme!
JavaScript:
Java.perform(() => {
Java.use("sg.vantagepoint.a.c").a.implementation = function () {return false};
Java.use("sg.vantagepoint.a.c").b.implementation = function () {return false};
Java.use("sg.vantagepoint.a.c").c.implementation = function () {return false};
Java.use("sg.vantagepoint.a.a").a.implementation = function (bArr, bArr2) {
let ret = this.a(bArr, bArr2);
let decoded = '';
for (let i = 0; i < ret.length; i++) {
decoded += String.fromCodePoint(ret[i]);
}
console.log(decoded)
return ret;
}
});
Итоги
Конечно, хотелось бы рассказать про работу с нативными библиотеками, но уже никак бы не успел написать это на этот конкурс. (может на следующий и напишу, но это не точно)
Мы рассмотрели меньше 10% возможностей Frida и это одна из must have утилит для работы с Android (и не только) приложениями.
Возможно я немного не так понял условия конкурса, но если бы не попробовал участвовать, жалел бы ещё потом