tags: note
Заметка по Frida
Это не полноценная статья! Это мои заметки по изучению фриды. Когда нужно быстро вспомнить какой-то аспект фриды, можно будет к ним быстро вернуться.
Frida General
Frida - это Open Source Dynamic Code Instrumentation Framework. Instrumentation (инструментация) - это модификация программы для сбора данных о ее поведении, производительности или других характеристиках. Frida работает с процессом в рантайме, вмешиваясь в его работу. Возможности Frida:
- API tracing
- Логгировать действия уже запущенного приложения, не останавливая его работы
- Расшифровка TLS трафика
- Фаззинг функций изнутри процесса (в отличии от классических фазеров)
- Дамп памяти процесса
- Создание протектора, антидебаггера (!)
- etc…
Фриду можно воспринимать как заскриптованный дебаггер. Она также как и дебагер подключается к процессу, но позволяет выполнять некоторые действия быстрее дебагера. Принцип работы Фриды заключается в инжекте в процесс QuickJS. QuickJS - маленький, самодостаточный интерпретатор Javascript. Вы пишите Frida скрипт на JS, а интерпретатор внутри процесса его исполняет. Если нет рута (ios, android), то фриду можно добавить непосредственно в исследуемое приложение следующими способами:
- Модификация исходников
- Патчинг бинарника
- Динамическая подгрузка (LD_PRELOAD)
Frida mini examples
Папка frida_mini_examples
содержит примеры возможностей фриды.
hello.c
:
#include <stdio.h>
#include <unistd.h>
void f (int n)
{
printf ("Number: %d\n", n);
}
int main (int argc, char * argv[])
{
int i = 0;
printf ("f() is at %p\n", f);
while (1)
{
f (i++);
sleep (1);
}
}
hook.py 0xaddr
- перехватывает функцию f()
и выводит аргумент
import frida
import sys
# intercept func at addr from arg
session = frida.attach("hello")
script = session.create_script("""
Interceptor.attach(ptr("%s"), {
onEnter(args) {
send(args[0].toInt32()); // print func args
}
});
""" % int(sys.argv[1], 16))
def on_message(message, data):
print(message)
script.on('message', on_message)
script.load()
sys.stdin.read()
modify.py 0xaddr
- модифицирует первый аргумент, меняет его на 1337
import frida
import sys
session = frida.attach("hello")
script = session.create_script("""
Interceptor.attach(ptr("%s"), {
onEnter(args) {
args[0] = ptr("1337");
}
});
""" % int(sys.argv[1], 16))
script.load()
sys.stdin.read()
call_funcs.py 0xaddr
- вызов внутренней функции f()
import frida
import sys
session = frida.attach("hello")
script = session.create_script("""
const f = new NativeFunction(ptr("%s"), 'void', ['int']);
f(1911);
f(1911);
f(1911);
""" % int(sys.argv[1], 16))
script.load()
inject_string
- создает строку “TESTMEPLZ!” в адресном пространстве процесса hi и вызывает функцию f() с этой строкой в качестве аргумента
hi.c
:
#include <stdio.h>
#include <unistd.h>
int f (const char * s)
{
printf ("String: %s\n", s);
return 0;
}
int main (int argc, char * argv[])
{
const char * s = "Testing!";
printf ("f() is at %p\n", f);
printf ("s is at %p\n", s);
while (1)
{
f (s);
sleep (1);
}
}
import frida
import sys
session = frida.attach("hi")
script = session.create_script("""
const st = Memory.allocUtf8String("TESTMEPLZ!");
const f = new NativeFunction(ptr("%s"), 'int', ['pointer']);
f(st);
""" % int(sys.argv[1], 16))
def on_message(message, data):
print(message)
script.on('message', on_message)
script.load()
inject_struct
- client коннектится к 127.0.0.1 по порту 5000, после нажатия Enter. Скрипт подменяет структуру sockaddr на собственную
nc -lp 5000
nc -lp 5001
./client 127.0.0.1
client.c
#include <arpa/inet.h>
#include <errno.h>
#include <netdb.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
int main (int argc, char * argv[])
{
int sock_fd, i, n;
struct sockaddr_in serv_addr;
unsigned char * b;
const char * message;
char recv_buf[1024];
if (argc != 2)
{
fprintf (stderr, "Usage: %s <ip of server>\n", argv[0]);
return 1;
}
printf ("connect() is at: %p\n", connect);
if ((sock_fd = socket (AF_INET, SOCK_STREAM, 0)) < 0)
{
perror ("Unable to create socket");
return 1;
}
bzero (&serv_addr, sizeof (serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons (5000);
if (inet_pton (AF_INET, argv[1], &serv_addr.sin_addr) <= 0)
{
fprintf (stderr, "Unable to parse IP address\n");
return 1;
}
printf ("\nHere's the serv_addr buffer:\n");
b = (unsigned char *) &serv_addr;
for (i = 0; i != sizeof (serv_addr); i++)
printf ("%s%02x", (i != 0) ? " " : "", b[i]);
printf ("\n\nPress ENTER key to Continue\n");
while (getchar () == EOF && ferror (stdin) && errno == EINTR)
;
if (connect (sock_fd, (struct sockaddr *) &serv_addr, sizeof (serv_addr)) < 0)
{
perror ("Unable to connect");
return 1;
}
message = "Hello there!";
if (send (sock_fd, message, strlen (message), 0) < 0)
{
perror ("Unable to send");
return 1;
}
while (1)
{
n = recv (sock_fd, recv_buf, sizeof (recv_buf) - 1, 0);
if (n == -1 && errno == EINTR)
continue;
else if (n <= 0)
break;
recv_buf[n] = 0;
fputs (recv_buf, stdout);
}
if (n < 0)
{
perror ("Unable to read");
}
return 0;
}
inject_struct.py
import frida
import sys
session = frida.attach("client")
script = session.create_script("""
send('Allocating memory and writing bytes...');
const st = Memory.alloc(16);
st.writeByteArray([0x02, 0x00, 0x13, 0x89, 0x7F, 0x00, 0x00, 0x01, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30]);
// 0x1389 = 5001d
Interceptor.attach(Module.getExportByName(null, 'connect'), {
onEnter(args) {
send('Injecting malicious byte array:');
args[1] = st;
}
//, onLeave(retval) {
// retval.replace(0); // Use this to manipulate the return value
//}
});
""")
def on_message(message, data):
if message['type'] == 'error':
print("[!] " + message['stack'])
elif message['type'] == 'send':
print("[i] " + message['payload'])
else:
print(message)
script.on('message', on_message)
script.load()
sys.stdin.read()
Frida windows
Пример 1
В папке samples_for_demonstration
лежат образцы классического plugx.
Запускать:
frida -l winapi_hooks.py -f sample.exe
winapi_hooks.py
- Скрипт хукает функции VirtualProtect, VirtualAlloc, CreateFileW, CreateFileA и выводит их аргументы. Делает дамп памяти, к которой применяется VirtualProtect.
var vp = Module.getExportByName(null, "VirtualProtect");
Interceptor.attach(vp, {
onEnter: function(args)
{
var vpAddress = args[0];
var vpAddr = args[0];
var vpSize = args[1].toInt32();
console.log("VirtualProtect called.\n Address:" + vpAddr + " Size:" + vpSize);
console.log(hexdump(vpAddress));
var dump = vpAddress.readByteArray(vpSize);
var filename = "D:\\dumps\\" + vpAddress + "_dump.bin";
var file = new File(filename, "wb");
file.write(dump);
file.flush();
file.close();
}
});
var va = Module.getExportByName(null, "VirtualAlloc");
Interceptor.attach(va, {
onEnter: function(args)
{
var vaSize = args[1].toInt32();
var vaProtect = args[3];
console.log("VA called.\n Size:" + vaSize + " Protect:" + vaProtect);
},
onLeave: function(retVal)
{
console.log("VA ret:" + retVal);
}
});
var createFileWAddr = Module.getExportByName(null, "CreateFileW");
Interceptor.attach(createFileWAddr, {
onEnter: function(args) {
console.log('CreateFileW() call');
console.log('path:' + args[0].readUtf16String());
}
});
var createFileAAddr = Module.getExportByName(null, "CreateFileA");
Interceptor.attach(createFileAAddr, {
onEnter: function(args) {
console.log('CreateFileA() call');
console.log('path:' + args[0].readAnsiString());
}
});
Пример 2
Я скачал рандомный образец (RemcosRAT) с malware bazaar:
Попробуем посмотреть, что делает малварь с файлами следующим скриптом фрида:
var createFileWAddr = Module.getExportByName(null, "CreateFileW");
Interceptor.attach(createFileWAddr, {
onEnter: function(args) {
console.log('path:' + args[0].readUtf16String());
}
});
В результате получим открываемые файлы малварью:
Среди путей видим интересный файл install.vbs
, который лежит в папке %TEMP%
. Откроем эту папку и видим, что созданный файл отсутствует.
Можно предположить, что он после использования удаляется. Как нам поступить? Мы можем перехватить записываемые данные в файл и сохранить их до удаления. Для этого напишем скрипт перехвата вызова WriteFile
, будем сохранять буфер в отдельный файл:
var writeFileAddr = Module.getExportByName(null, "WriteFile");
Interceptor.attach(writeFileAddr, {
onEnter: function(args) {
var buff = args[1];
var size = args[2].toInt32();
console.log("WriteFile | size = " + size)
console.log(hexdump(buff));
var dump = buff.readByteArray(size);
var filename = "D:\\dumps\\" + Math.random() + "_dump.bin";
var file = new File(filename, "wb");
file.write(dump);
file.flush();
file.close();
}
});
В результате мы сохранили целевой скрипт, который и вправду самоудаляется после выполнения:
Дамп памяти Android приложения
С помощью Frida можно дампнуть память любого процесса.
-
Скачиваем любую apk, в нашем случае это будет mcdonalds
-
Устанавливаем apk
adb install mcodnalds.apk
-
Копируем фрида сервер на эмулятор/телефон и запускаем его
adb push frida-server-12.7.26-android-x86_64 /data/local/tmp/frida
chmod +x /data/local/tmp/frida
/data/local/tmp/frida &
-
Устанавливаем fridump
git clone https://github.com/Nightbringer21/fridump.git
-
Открываем приложение mcdonalds. Вводим в поле пароль/телефон и нажимаем Войти!
-
Делаем дамп процесса. Дампы сохранятся в папку
dump
python fridump.py -U -v -s mcdonalds
-
Ищем в дампе наш пароль
grep -arni "pass123" dump/*
Frida Android example aka UnCrackable App for Android Level 2
Описание задания второго уровня
This app holds a secret inside. May include traces of native code. Objective: A secret string is hidden somewhere in this app. Find a way to extract it.
Описание уже содержит хорошую подсказку, но не будем забегать вперед. Скачиваем apk, устанавливаем на android эмулятор, запускаем. Видим, как и в первом задании, детектирование рута
Давайте обойдем эти проверки другим способом (не выпиливанием кода из apk), с помощью Frida. Это фреймворк, который позволяет вмешиваться в работу приложения, во время выполнения. Инструкция по установке проста:
pip install frida-tools
Далее нам необходимо скачать подходящий сервер frida отсюда. У меня эмулятор android x86, поэтому я скачал архив с названием frida-server-12.7.26-android-x86.xz. Распаковываем и закидываем на эмулятор
adb push frida-server-12.7.26-android-x86_64 /data/local/tmp/frida
Запускаем
chmod +x /data/local/tmp/frida
/data/local/tmp/frida &
После этого, мы должны командой frida-ps -U
получить список процессов на эмуляторе, чтобы убедиться, что Frida установлена правильно и сервер запущен
Теперь возвращаемся к приложению и детектированию рута. Оно по прежнему происходит при помощи трех функций, которые мы видим после invoke-static
invoke-static {}, Lsg/vantagepoint/a/b;->a()Z
move-result v0
if-nez v0, :cond_0
invoke-static {}, Lsg/vantagepoint/a/b;->b()Z
move-result v0
if-nez v0, :cond_0
invoke-static {}, Lsg/vantagepoint/a/b;->c()Z
move-result v0
С помощью Frida, мы можем заставить эти функции всегда возвращать false и обходить проверку. Для этого нам необходимо написать скрипт на javascript. Чтобы работать с java классами, мы должны использовать обертку, для нашего кода (подробнее о API к java, читайте в официальной документации)
Java.perform(function() {
//our code...
})
Чтобы получить доступ к классу sg.vantagepoint.a.b
(в котором находятся наши проверки), мы должны использовать метод Java.use(classname)
Java.perform(function() {
var antiRootClass = Java.use("sg.vantagepoint.a.b");
})
Теперь имея доступ к нужному классу, зная имя метода, мы можем переопределить его поведение на возврат false
antiRootClass.a.implementation = function() {
return false;
}
Повторим данный код, для всех трех методов и в итоге получим
console.log("Script started...");
Java.perform(function() {
var antiRootClass = Java.use("sg.vantagepoint.a.b");
antiRootClass.a.implementation = function() {
return false;
}
console.log("sg.vantagepoint.a.b.a modified");
antiRootClass.b.implementation = function() {
return false;
}
console.log("sg.vantagepoint.a.b.b modified");
antiRootClass.c.implementation = function() {
return false;
}
console.log("sg.vantagepoint.a.b.c modified");
})
Чтобы запустить этот скрипт, вам необходимо знать package name приложения. Его можно получить из манифеста или запустив приложение и посмотрев все той же командой frida-ps
.
Сохраняем код скрипта в файл с именем owasp2.js
и выполняем такую команду
frida -U -l owasp2.js --no-paus -f owasp.mstg.uncrackable2
которая сама стартует приложение и выполняет скрипт. В итоге мы не получаем окно, так как все наши проверки рута были отключены
Мы получили доступ к вводу секретной строки. Вводим что-нибудь
появилось окно. Ищем в исходниках место, где встречается эта строка и идет проверка. Таким местом оказался метод verify()
в классе MainActivity
. Его сокращенная версия
...
invoke-virtual {p1}, Landroid/widget/EditText;->getText()Landroid/text/Editable;
move-result-object p1
iget-object v1, p0, Lsg/vantagepoint/uncrackable2/MainActivity;->m:Lsg/vantagepoint/uncrackable2/CodeCheck;
invoke-virtual {v1, p1}, Lsg/vantagepoint/uncrackable2/CodeCheck;->a(Ljava/lang/String;)Z
move-result p1
if-eqz p1, :cond_0
const-string p1, "Success!"
Здесь мы видим, что введенный нами текст в EditText
, отправляется в функцию sg/vantagepoint/uncrackable2/CodeCheck;->a
и если все удачно, нам говорят “Success!”. Очевидно, что именно там идет проверка нашего ввода на правильность. Откроем класс sg.vantagepoint.uncrackable2.CodeCheck
.method private native bar([B)Z
.end method
# virtual methods
.method public a(Ljava/lang/String;)Z
.locals 0
invoke-virtual {p1}, Ljava/lang/String;->getBytes()[B
move-result-object p1
invoke-direct {p0, p1}, Lsg/vantagepoint/uncrackable2/CodeCheck;->bar([B)Z
move-result p1
return p1
.end method
Наш ввод, передается в функцию bar()
invoke-direct {p0, p1}, Lsg/vantagepoint/uncrackable2/CodeCheck;->bar([B)Z
Обратите внимание на модификатор native функции
.method private native bar([B)Z
Модификатор native означает, что реализация метода находится в библиотеках, написанных на других языках. Чтобы работать с такими библиотеками, используется механизм JNI (Java Native Interface). С помощью этого интерфейса, андроид приложение может вызывать код на С/С++. Значит, наше приложение использует внешнюю библиотеку. Найдем доказательство этому и сделаем поиск по исходникам, по слову loadLibrary. Находим следующий код в MainActivity
# direct methods
.method static constructor <clinit>()V
.locals 1
const-string v0, "foo"
invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V
return-void
.end method
Этот код говорит нам о том, что приложение загружает библиотеку с именем foo. Если мы распакуем наше приложение, то в папке lib мы видим библиотеку libfoo.so. Теперь пришло время изучить ее. Так как у нас нет исходных кодов библиотеки и написана она на С/С++, то нам необходимо воспользоваться дизассемблером, в нашем случае - Binary Ninja. Можно использовать бесплатную Веб версию.
Открываем файл нашей библиотеки и находим в списке нашу функцию bar
Открываем функцию. Углубляться в анализ ассемблерного кода мы сегодня не будем, возможно в другой жизни это сделаем. Просто обратите внимание на выделенный код и в особенности на hex значения
на адреса они не похожи, а похожи они на строки. Попробуем посмотреть опцией “отобразить, как символы”
Получилось вот так и это похоже на правду
Проделаем для всех значений
Эти части складываются в единную строку “Thanks for all the fish”. Далее по коду мы видим сравнение нашего ввода с этой строкой
Проверим строку в приложении
Это и есть наша секретная строка!
Frida TLS
Подробное обьяснение принципа работы
С помощью Frida можно расшифровывать schannel TLS трафик (IIS, RDP, IE, Outlook, Powershell, LDAP …). Для этого нам понадобится скрипт keylog.js, wireshark и для демонстрации будем использовать Windows 11.
SChannel a.k.a Secure Channel - это подсистема windows, используемая приложениями при работе с TLS (установка соединения, прием соединения от клиента).
keylog файл содержит значения, которые используются для генерации сессионых ключей TLS. Обьяснение данных в логе можно прочитать тут
Скрипт хукает функции библиотеки ncrypt.dll
. Например, функция SslHashHandshake
используется для генерации хэша во время SSL Handshake.
var shh = Module.findExportByName('ncrypt.dll', 'SslHashHandshake');
if(shh != null){
Interceptor.attach(shh, {
Дампает входящие аргументы функций связанных с TLS в кейлог файл, который затем добавляется в Wireshark.
- Чтобы фрида смогла приатачится к lsass.exe, в настройках
Exploit protection
выставляемhardware-enforced stack protection off
.
- В скрипте
keylog.js
указываем путь к логу. Узнаем PID процесса lsass.exe и аттачимся к нему командой
frida -p PID -l SCRIPT
-
Открываем Wireshark
-
Делаем какой либо HTTS запрос. Например:
Invoke-WebRequest -Uri "https://www.google.de" -Method GET -TimeoutSec 5
Убеждаемся, что видим только зашифрованный трафик.
-
Файл лога
keylog.log
должен заполниться значениями. -
В Wireshark, в настройках
Edit->Preferences->Protocols->TLS
вводим путь к логу в(Pre)-Master-Secret log filename
-
Трафик, который был у нас в Wireshark расшифруется.
Чистый запрос Invoke-WebRequest
Чистый ответ гугла
Frinet
Плагин для трейсинга с помощью фриды. Опция загрузки трейса становится доступна после загрузки бинарника.
Ссылки:
https://hex-rays.com/blog/plugin-focus-frinet/
https://blog.ret2.io/2021/04/20/tenet-trace-explorer/
Полезные ссылки
Для 2600 митапа я подготавливал презентацию по возможностям фриды.
Android greybox fuzzing with AFL++ Frida mode
https://8ksec.io/advanced-frida-usage-part-1-ios-encryption-libraries-8ksec-blogs/
Вверх