Фаззинг - это отправка произвольных данных в программу с целью вызвать непредсказуемое поведение.
Ещё недавно, как я начал изучать веб хакинг, я счёл интересным занятие исследовать Linux и Windows на предмет бинарных уязвимостей. Хотя легально заработать в одиночку хакером у нас в России я думаю можно только веб хакингом, я всё равно хочу изучать все интересующие аспекты атакующей и защищающей стороны. Кто знает, вдруг я когда-нибудь буду в red team. Ну а пока я просто грызу гранит науки.
Слегка поразмыслив над решением задачи, я понял что нужно делать. Я не знаю как другие проводят фаззинг библиотек, у которых нет исходных текстов, но додумался до одного варианта. Далее будут два примера для Linux и Windows.
Я выбрал ассемблер nasm. Этот ассемблер полюбился больше, чем fasm, хотя и fasm я использовал раньше. Nasm кроссплатформенный, и как вы убедитесь позже, он подошел и для Windows разработки.
Библиотека, которую нужно проверить на ошибки, код которой я написал от балды. Я не стал приводить исполняющую часть, только заголовок:
Нам нужно передавать в V8:arse_string строки кода и ждать ответа в виде правильного, неправильного или segfault.
Также привожу заголовок фаззера:
В данном случае библиотека при каждом вызове передаёт указатель на std::string. Удобней было передавать именно указать, который не несёт за собой ничего, кроме хранения указателя в памяти.
Следующим шагом было собрать библиотеки и посмотреть с помощью radare2 названия связующих функций. Ими стали:
Чтобы добыть имена функций, radare2 выполнил команду
За их зашифрованными символами скрывались их определения и только названия функций давали понимания, что это именно то, что я ищу.
Далее остается только написать программу, которая принимает очередную строку и отправляет её в класс другой библиотеки:
Мой любимый ассемблер. Его я люблю за то, что он не требует от нас проверять типы данных. Для строгого C++ будет очень трудно восстанавливать класс, чтобы его можно было использовать в чужой библиотеке. Ассемблерная программа же даёт нам преимущество. Если C++ класс в библиотеке занимает 120 байт, то мы просто либо в стеке выделяем 120 байт, либо держим 8 байт памяти для хранения указателя.
Остается только собрать это всё и вот как это выглядит:
В итоге мы получаем программу, которая может получать данные из одной библиотеки и отправлять другой.
DLL заголовки я не буду приводить в пример, но могу сказать, что там нет ничего необычного. Всего лишь объявляется по правилам Windows вместе с dllspec и dllexport. Собираем обычным способом и отправляем в папку с фаззером. Для фаззинг библиотеке можно копировать dll.lib файл, а dll, ошибку в которой мы должны найти, может быть без исходников и тут нужно произвести несколько операций.
Первым делом используем dumpbin:
Из этого файла мы можем увидеть наши функции с внутренним названием, которые могут использоваться в ассемблере. Из всего, что там было, я выделил только те функции, которые были найдены:
Вместо @1 к примеру были прописаны их реальные названия в C++ стиле (public __dllspec Code::Code (void))
Далее нужно использовать программу lib такой строкой:
Но тут возникала ошибка, когда было прописано не @1, а нормальное название функции. @1 решил эту проблему. Если мне не изменяет память, это указывает номер функции.
На выходе мы получаем файл, который будет участвовать для связывания ассемблерной программы вместе с dll. То-есть происходит только связка, а dll будет использоваться потом при каждом запуске.
Код сборки получился таким:
А программа с ассемблерным кодом была такая:
Здесь кода мало, но это показывает, что так всё работает, и можно продолжать совершенствовать программу.
Ещё раз повторюсь, мы не знаем об этом, так что мы начинаем писать новую программу и создаем класс с примерным размером:
Далее открываем нашу библиотеку:
С помощью radare2 узнаем внутреннее название конструктора класса и получаем его:
Но это всего лишь функция. Так как от реверса мы узнаем, что первым аргументом всегда идёт класс, то меняет функцию так как требуется для того, чтобы обработать класс:
Таким образом мы наш класс прогоняем через конструктор. Далее нам нужно выполнить метод print это класса. Для этого мы также получаем его с помощью dlsym и вызываем в стиле C:
Отличная работа. Теперь можно фаззить незнакомые библиотеки прямо из C++.
Вот как выглядит полный код:
Конструктор ничего не возвращает в данном случае, как и деструктор.
(Данная статья также размещена на Хабр от моего имени)
Ещё недавно, как я начал изучать веб хакинг, я счёл интересным занятие исследовать Linux и Windows на предмет бинарных уязвимостей. Хотя легально заработать в одиночку хакером у нас в России я думаю можно только веб хакингом, я всё равно хочу изучать все интересующие аспекты атакующей и защищающей стороны. Кто знает, вдруг я когда-нибудь буду в red team. Ну а пока я просто грызу гранит науки.
Слегка поразмыслив над решением задачи, я понял что нужно делать. Я не знаю как другие проводят фаззинг библиотек, у которых нет исходных текстов, но додумался до одного варианта. Далее будут два примера для Linux и Windows.
Linux
Первым делом я занялся разработкой заготовки для linux. Нужно было определить все пункты, с которыми мне нужно будет столкнуться. Эти пункты составляли такой список:- Библиотека не имеет исходных кодов
- На каком ассемблере писать код
- Как вызывать функции из динамической библиотеки
Я выбрал ассемблер nasm. Этот ассемблер полюбился больше, чем fasm, хотя и fasm я использовал раньше. Nasm кроссплатформенный, и как вы убедитесь позже, он подошел и для Windows разработки.
Библиотека, которую нужно проверить на ошибки, код которой я написал от балды. Я не стал приводить исполняющую часть, только заголовок:
C++:
#ifndef TE_H
#define TE_H
#include <cstdio>
#include <string>
class Handler {
public:
Handler ();
private:
FILE *fp = {nullptr};
};
class V8 {
public:
V8 ();
int parse_string (Handler& handle, std::string& code);
};
Handler *create_handler ();
V8 *create_js ();
#endif
Нам нужно передавать в V8:arse_string строки кода и ждать ответа в виде правильного, неправильного или segfault.
Также привожу заголовок фаззера:
C++:
#ifndef GETTER_H
#define GETTER_H
#include <string>
std::string *getter_string ();
#endif
В данном случае библиотека при каждом вызове передаёт указатель на std::string. Удобней было передавать именно указать, который не несёт за собой ничего, кроме хранения указателя в памяти.
Следующим шагом было собрать библиотеки и посмотреть с помощью radare2 названия связующих функций. Ими стали:
Код:
extern _Z14create_handlerv
extern _Z9create_jsv
extern _Z13getter_stringB5cxx11v
extern _ZN2V812parse_stringER7HandlerRNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE
Чтобы добыть имена функций, radare2 выполнил команду
is
.За их зашифрованными символами скрывались их определения и только названия функций давали понимания, что это именно то, что я ищу.
Далее остается только написать программу, которая принимает очередную строку и отправляет её в класс другой библиотеки:
Код:
section .text
extern _Z14create_handlerv
extern _Z9create_jsv
extern _Z13getter_stringB5cxx11v
extern _ZN2V812parse_stringER7HandlerRNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE
global main
main:
sub rsp, 8 + 8 + 8
call _Z13getter_stringB5cxx11v
mov [rsp + 16], rax
call _Z14create_handlerv
mov [rsp + 0], rax
call _Z9create_jsv
mov [rsp + 8], rax
mov rdi, [rsp + 8]
mov rsi, [rsp + 0];
mov rsi, [rsi]
lea rdx, [rsp + 16];
mov rdx, [rdx]
call _ZN2V812parse_stringER7HandlerRNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE
mov rax, 60
mov rbx, 0
syscall
Мой любимый ассемблер. Его я люблю за то, что он не требует от нас проверять типы данных. Для строгого C++ будет очень трудно восстанавливать класс, чтобы его можно было использовать в чужой библиотеке. Ассемблерная программа же даёт нам преимущество. Если C++ класс в библиотеке занимает 120 байт, то мы просто либо в стеке выделяем 120 байт, либо держим 8 байт памяти для хранения указателя.
Остается только собрать это всё и вот как это выглядит:
Makefile:
all:
nasm -felf64 main.asm -o main.o
gcc main.o -Wl,-rpath=libs -Llibs -lte -lgetter -o test
clean:
rm main.o
rm test
В итоге мы получаем программу, которая может получать данные из одной библиотеки и отправлять другой.
Windows
Для Windows оказалось чуточку сложнее. Над этим я провёл 2 часа решаю как это сделать. Чтобы собрать ассемблерную программу, нужно, чтобы у dll библиотеки была её связующая часть в виде dll.lib. Как я понял, она нужна, чтобы программа могла понять какие в dll библиотеке есть функции и встроить эти данные в нашу программу.DLL заголовки я не буду приводить в пример, но могу сказать, что там нет ничего необычного. Всего лишь объявляется по правилам Windows вместе с dllspec и dllexport. Собираем обычным способом и отправляем в папку с фаззером. Для фаззинг библиотеке можно копировать dll.lib файл, а dll, ошибку в которой мы должны найти, может быть без исходников и тут нужно произвести несколько операций.
Первым делом используем dumpbin:
Код:
dumpbin /nologo /exports Dllcrackme.dll > Dllcrackme.def
Из этого файла мы можем увидеть наши функции с внутренним названием, которые могут использоваться в ассемблере. Из всего, что там было, я выделил только те функции, которые были найдены:
Код:
EXPORTS
??0Code@@QEAA@XZ = ??0Code@@QEAA@XZ @1
?check_code@Code@@QEAAHAEAV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@Z = ?check_code@Code@@QEAAHAEAV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@Z @2
?print@Code@@QEAAXXZ = ?print@Code@@QEAAXXZ @3
Вместо @1 к примеру были прописаны их реальные названия в C++ стиле (public __dllspec Code::Code (void))
Далее нужно использовать программу lib такой строкой:
Код:
lib /nologo /def:Dllcrackme.def /MACHINE:x64 /out:Dllcrackme.lib
Но тут возникала ошибка, когда было прописано не @1, а нормальное название функции. @1 решил эту проблему. Если мне не изменяет память, это указывает номер функции.
На выходе мы получаем файл, который будет участвовать для связывания ассемблерной программы вместе с dll. То-есть происходит только связка, а dll будет использоваться потом при каждом запуске.
Код сборки получился таким:
Код:
nasm -f win64 main.asm -o main.o
link main.o Dllcrackme.lib /entry:main /out:fuzzer.exe
А программа с ассемблерным кодом была такая:
Код:
section .text
global main
extern ?print@Code@@QEAAXXZ
main:
call ?print@Code@@QEAAXXZ
ret
Здесь кода мало, но это показывает, что так всё работает, и можно продолжать совершенствовать программу.
C++
Так как мы рассмотрели как это делается изнутри, поговорим теперь как это делается на C++. Здесь уже больше походит на вызов сишных функций. Наверняка вы уже знаете такие функции как dlopen и dlsym. Их как раз таки мы и будем использовать для загрузки наших функций. Приведу пример кода с методом print из класса. Сам класс вот так выглядит, но по ходу дела мы не знаем об этом:
C++:
#ifndef FM_H
#define FM_H
#include <iostream>
#include <string>
class FM {
public:
FM ();
void print ();
void test ();
};
#endif
Ещё раз повторюсь, мы не знаем об этом, так что мы начинаем писать новую программу и создаем класс с примерным размером:
C++:
#include <dlfcn.h>
#include <cstdint>
#include <iostream>
class FM {
uint8_t data[123];
};
Далее открываем нашу библиотеку:
C++:
int main (int argc, char **argv)
{
void *handle = dlopen ("libfm.so", RTLD_NOW);
С помощью radare2 узнаем внутреннее название конструктора класса и получаем его:
C++:
void *constructor_fm = (void*) dlsym (handle, "_ZN2FMC2Ev");
Но это всего лишь функция. Так как от реверса мы узнаем, что первым аргументом всегда идёт класс, то меняет функцию так как требуется для того, чтобы обработать класс:
C++:
FM *fm = new FM();
((void (*)(FM *)) constructor_fm) (fm); // constructor
Таким образом мы наш класс прогоняем через конструктор. Далее нам нужно выполнить метод print это класса. Для этого мы также получаем его с помощью dlsym и вызываем в стиле C:
Код:
void (*print)(FM *) = (void (*)(FM *)) dlsym (handle, "_ZN2FM5printEv");
print (fm);
Отличная работа. Теперь можно фаззить незнакомые библиотеки прямо из C++.
Вот как выглядит полный код:
C++:
#include <dlfcn.h>
#include <cstdint>
#include <iostream>
#include <cstring>
class FM {
uint8_t data[128];
};
int main (int argc, char **argv)
{
void *handle = dlopen ("libfm.so", RTLD_NOW);
void *destructor_fm = (void*) dlsym (handle, "_ZN2FMD2Ev");
void *constructor_fm = (void*) dlsym (handle, "_ZN2FMC2Ev");
FM *fm = new FM();
((void (*)(FM *)) constructor_fm) (fm); // constructor
void (*print)(FM *) = (void (*)(FM *)) dlsym (handle, "_ZN2FM5printEv");
print (fm);
((void (*)(FM *)) destructor_fm) (fm);
delete fm;
dlclose (handle);
}
Конструктор ничего не возвращает в данном случае, как и деструктор.
(Данная статья также размещена на Хабр от моего имени)
Последнее редактирование модератором: