• XSS.stack #1 – первый литературный журнал от юзеров форума

Fuzzing Фаззим исполняемые файлы при помощи AFL++ с санитайзерами

tabac

CPU register
Пользователь
Регистрация
30.09.2018
Сообщения
1 610
Решения
1
Реакции
3 332
Фаззинг, как ты, наверное, знаешь, — это техника, которая позволяет автоматизировать поиск уязвимостей в софте. Бывает фаззинг методом «черного ящика», когда у нас нет исходников программы, а бывает основанный на покрытии — то есть применяемый к исходникам. В этой статье сосредоточимся на втором виде и на примере AFL++ разберем, какую роль здесь играют санитайзеры и как их применять.

Суть фаззинга заключается в том, чтобы автоматически подавать программе неправильные или испорченные данные и отлавливать потенциальные уязвимости.

На свете существует множество фаззеров: AFL++, libfuzz, WinAFL, CI Fuzz, PeachTech Peach Fuzzer, FuzzDB, go-fuzz и прочие, в том числе самописные. Фаззеры применимы не только для программ, но и для сайтов. Например, wfuzz — это веб‑фаззер, который нужен, чтобы вводить любые данные в любое поле HTTP-запроса.

В этой статье идет речь о фаззинге программ. Нашим инструментом будет AFL++ — фаззер, ориентированный на покрытие. Это значит, что он собирает информацию о покрытии для каждого измененного входа, чтобы обнаружить новые пути выполнения и потенциальные ошибки. Если у тебя есть исходник программы, AFL может вставлять вызовы функций в начало каждого базового блока, например в функции и циклы.

О том, как при помощи AFL тестировать блекбоксы, написано множество статей, в том числе о том, как его распараллеливать. Но этот фаззер умеет компилировать программы и делать свои вставки в код, что облегчает фаззинг. Такие вставки называются санитайзерами.

ЧТО ТАКОЕ САНИТАЙЗЕРЫ?​

Санитайзеры — это утилиты, которые обычно поставляются вместе с компиляторами. Они помогают искать ошибки в коде во время исполнения. Если обобщить, можно выделить три разновидности санитайзеров:
  • address — ищет ошибки работы с памятью;
  • thread — ищет проблемы в многопоточных вычислениях (состояние гонки);
  • undefined — ищет неопределенное поведение в программе.
Использование санитайзеров вместе с AFL++ — это хороший тон при фаззинге, потому что с ними фаззер будет лучше знать, какого рода ошибки ему нужно искать. С санитайзерами фаззинг всегда будет проходить успешнее, чем без них.

В AFL++ доступны разные санитайзеры, которые помогают обнаруживать ошибки в коде. Вот краткое описание каждого из них.
  1. AddressSanitizer (ASAN) предназначен для обнаружения ошибок чтения или записи в память, таких как переполнение буфера или использование освобожденной памяти. Он обеспечивает детальную информацию об ошибках и помогает в их отладке.
  2. MemorySanitizer (MSAN) призван обнаружить неинициализированную память. Он помогает выявить ошибки, связанные с использованием неинициализированных данных и способные привести к непредсказуемому поведению программы.
  3. ThreadSanitizer (TSAN) предназначен для поиска гонок данных в многопоточных программах. Он помогает выявить ситуации, когда несколько потоков одновременно обращаются к одним и тем же данным, что иногда приводит к непредсказуемым результатам
  4. UndefinedBehaviorSanitizer (UBSAN) помогает обнаружить неопределенное поведение в программе, такое как деление на ноль, переполнение целочисленных типов или неправильное использование указателей. Он предупреждает о таких проблемах и помогает предотвратить возможные ошибки.
  5. Control Flow Integrity (CFI) обеспечивает защиту от атак, связанных с изменением потока управления программы. Он проверяет целостность вызовов функций и предотвращает нежелательные изменения потока выполнения.
  6. Safe-stack предназначен для защиты от переполнения стека. Он обеспечивает дополнительные механизмы безопасности, чтобы предотвратить возможные атаки, связанные с переполнением стека.
В разных случаях могут пригодиться разные санитайзеры, в том числе это зависит от особенностей и требований проекта.

Например, санитайзер ASAN может выявить следующие проблемы:
  • Use-After-Free — использование чанка кучи после его освобождения;
  • HeapOverflow — переполнение одного чанка и добавление данных в другой чанк;
  • StackOverflow — классическое переполнение буфера.
Подробнее о возможностях ASAN читай в документации на GitHub.

ТЕНЕВАЯ ПАМЯТЬ​

Одна из техник, которую используют санитайзеры, — это теневая память. По сути, они поддерживают параллельную структуру данных наряду с фактической памятью программы, при этом каждый бит в теневой памяти соответствует 8 битам в фактической памяти. Такая схема позволяет хранить метаданные о каждом выделении памяти и обнаруживать нарушения при записи в нее.

Например, в программе определяется какой‑то указатель:
Код:
*address = ...;  // or: ... = *address;
Санитайзер меняет его использование на такое:
Код:
if (IsPoisoned(address)) {
  ReportError(address, kAccessSize, kIsWrite);
}
*address = ...;  // or: ... = *address;
Санитайзер помечает любую высвобождаемую память как «отравленную», а затем следит за доступом к ней. Если произошло переполнение буфера, который выделялся через malloc, то получается, что кто‑то пишет в отравленную область, а в эту область писать нельзя. Так фаззер обнаруживает баг.

Подобная же ситуация и с переполнением буфера на стеке. Предположим, у нас есть такая программа:
Код:
void foo() {
  char a[8];
  ...
  return;
}
Санитайзер ее изменит на вот такую:
Код:
void foo() {
  char redzone1[32];  // 32-byte aligned
  char a[8];          // 32-byte aligned
  char redzone2[24];
  char redzone3[32];  // 32-byte aligned
  int  *shadow_base = MemToShadow(redzone1);
  shadow_base[0] = 0xffffffff;  // poison redzone1
  shadow_base[1] = 0xffffff00;  // poison redzone2, unpoison 'a'
  shadow_base[2] = 0xffffffff;  // poison redzone3
  ...
  shadow_base[0] = shadow_base[1] = shadow_base[2] = 0; // unpoison all
  return;
}
Тут искусственно добавляется отравленная область, и, если произойдет запись в нее, фаззер сработает и сообщит о переполнении.

КОМПИЛИРУЕМ ТЕСТОВУЮ ПРОГРАММУ С AFL-CLANG-FAST​

В качестве подопытного для испытаний используем вот такую незамысловатую программу:
Код:
#include <stdio.h>
int main()
{
    char buf[256] = {0};
    char *a = 0;
    unsigned int size = read(0, buf, 256);
    if(size > 0 && buf[0] == 'H'){
        if(size > 1 && buf[1] == 'I'){
            if(size > 2 && buf[2] == '!'){
                a = 1;
            }
        }
    }
    if(a == 1){
        printf("g00d pwn\n");
    }
    return 0;
}

Для тестирования два раза скомпилируем программу. Первый раз обычным GCC, второй раз компилятором afl-clang-fast.

Компиляция при помощи afl-clang-fast выглядит точно так же, как и с GCC:
Код:
$ afl-clang-fast  main.c -o san.elf
Если скомпилировать не получается, стоит пробовать варианты afl-clang или afl-gcc.
В результате мы получаем инструментированный бинарный файл. При компиляции ты увидишь сообщение о том, что AFL нашел определенное количество места для инструментации и вставил свое логирование в места ветвления.

20240213182834.png


Для проверки работы скомпилированного таким образом бинарного файла можно просто его запустить. Если программа падает, значит, фаззер будет врать:

Код:
$ ./san.elf
HI!
g00d pwn

Теперь загружу два бинарных файла в IDA Pro, чтобы показать, как это все выглядит внутри.

ИЩЕМ ОТЛИЧИЯ БИНАРНЫХ ФАЙЛОВ С ПОМОЩЬЮ IDA PRO​

Слева загружена оригинальная программа, скомпилированная с помощью GCC. Справа — программа, скомпиленная при помощи afl-clang-fast.


20240213183536.png


Разница очевидна: AFL добавил функцию afl_maybe_log() для логирования сообщения. Эта функция принимает два аргумента:
  • type — тип сообщения. Может принимать значения ERROR, WARNING, INFO или DEBUG;
  • msg — сообщение, которое нужно залогировать.
Функция afl_maybe_log() будет выводить сообщение в стандартный вывод только в том случае, если уровень логирования установлен на значение не ниже типа сообщения. Например, если уровень логирования установлен на INFO, то сообщения с типом ERROR и WARNING будут выведены в стандартный вывод, а сообщения с типом INFO и DEBUG — нет.

Функция afl_maybe_log() используется для логирования событий, происходящих во время работы AFL++: ошибок, предупреждений, информации о покрытии кода и так далее.

Вот пример ее использования:
Код:
afl_maybe_log(ERROR, "An error occurred!");
afl_maybe_log(WARNING, "A warning occurred!");
afl_maybe_log(INFO, "Some information");
afl_maybe_log(DEBUG, "Some debug information");
Здесь сообщение об ошибке будет выведено в стандартный вывод, WARNING уйдет туда же, а INFO — нет, потому что уровень логирования не установлен на значение, которое не ниже типа этих сообщений.

Точно так же можно увидеть те самые семь инструментариев, которые вставил компилятор.


20240213184053.png


ФАЗЗИНГ​

Как и в прошлый раз, нам нужно создать две директории: /in с тесткейсами и /out — для вывода крашей:
Код:
mkdir in; mkdir out; cd in; ragg2 -P 10 -r > 1.txt; cd ..
Ragg2 — это утилита из набора Radare2, с помощью которой можно генерировать мусорные строки. Например, это удобно, когда нужно вычислять данные для переполнения буфера.

Запускаем фаззер:
Код:
afl-fuzz -m none -i $PWD/in -o $PWD/out -- ./san.elf
Давай посмотрим, что делает каждый из параметров:

  • -- — берется содержимое файлов, которые находятся в директории /in;
  • -m none означает, что необходимо убрать ограничение памяти для каждого запускаемого семпла. Это ускоряет фаззинг;
  • -i $PWD/in задает, какие тесткейсы берем для фаззинга;
  • -o $PWD/out задает, куда будут вылетать мутированные данные.
После запуска наблюдаем такую картину.

20240213185814.png


Внизу можно увидеть состояние фаззера, в правом верхнем углу — количество циклов и количество крашей. Через пять минут поле state из состояния started перейдет в состояние finished. Значит, фаззинг закончился!

20240213185707.png


Все результаты мутирования входного файла находятся по пути $PWD/out/default/queue. Давай посмотрим последний файл.

20240213190630.png


Можно увидеть разницу между входным файлом и тем, что выдал фаззер.

result-san.png


Удобно, что фаззер сам сообщает, когда все закончилось.

ВТОРОЙ ПРИМЕР САНИТАЙЗЕРА​

Допустим, у нас будет вот такая программа:
Код:
#include <stdio.h>
#include <string.h>
int main(int argc, const char *argv[])
{
    char *buffer = (char *)malloc(256);
    char *a = 0;
    unsigned int size = read(0, buf, 256);
    if(size > 0 && buf[0] == 'H'){
        if(size > 1 && buf[1] == 'I'){
            if(size > 2 && buf[2] == '!'){
                a = 1;
                free(buffer);
                strcpy(buffer, argv[1]);
                puts(buffer);
            }
        }
    }
    if(a == 1){
        printf("g00d pwn\n");
    }
    return 0;
}

Здесь в явном виде есть уязвимость Use-After-Free, но программа не падает и ошибки никакой не выдает. Наша задача — продемонстрировать работу санитайзера AFL++. Он должен обнаружить эту уязвимость, в то время как при обычном запуске фаззера, без санитайзеров, ничего произойти не должно.

Компилировать будем с флагом -fsanitize и в явном виде указываем, какой тип ошибки интересует:
Код:
afl-clang-fast -fsanitize=address main.c -o binary.elf

Далее повторяем те же шаги, что и раньше:
Код:
mkdir in; mkdir out; cd in; ragg2 -P 10 -r > 1.txt; cd ..

И запускаем фаззер:
Код:
afl-fuzz -i $PWD/in -o $PWD/out ./binary.elf @@ $PWD/in

20240215202324.png


Результат говорит о том, что была обнаружена уязвимость Use-After-Free.

Теперь проделаем такой же фокус, только без санитайзеров:
Код:
gcc main.c -o clear.elf

И запустим фаззер:
Код:
afl-fuzz -Q -D -i $PWD/in -o $PWD/out ./clear.elf @@ $PWD/in

20240215204310.png


Как видно, без санитайзера фаззер работает намного дольше.

ВЫВОДЫ​

Мы убедились, что санитайзеры — это удобная вещь, которую стоит использовать при фаззинге, если есть такая возможность. Можно указать самому фаззеру, на какие дефекты ему стоит обращать внимание.

Пример, который мы рассмотрели, показал достоинства санитайзера: фаззер адаптировался под код и заранее узнал, как ему надо двигаться, чтобы дойти до конца программы. Благодаря санитайзерам AFL++ смог грамотно подобрать тесткейс.

Автор Life-Hack
Источник xakep.ru
 
Последнее редактирование модератором:
Пожалуйста, обратите внимание, что пользователь заблокирован
  • Control Flow Integrity (CFI) обеспечивает защиту от атак, связанных с изменением потока управления программы. Он проверяет целостность вызовов функций и предотвращает нежелательные изменения потока выполнения.
  • Safe-stack предназначен для защиты от переполнения стека. Он обеспечивает дополнительные механизмы безопасности, чтобы предотвратить возможные атаки, связанные с переполнением стека.
5 - 6 это не санитайзеры, как на хакере такое вообще пропустили. митигейшен != санитайзер.
Санитайзер меняет его использование на такое:
И да не меняет, а вставляет всяческого рода проверки типа if else (это называется инструментированием кода)

В целом обычная вводная простенькая статья, и да тема на самом деле не раскрыта будь я редактором журнала хакер я бы отправил автора на доработку, пусть доделывает статью, а так я рекомендую этот челендж /threads/61626/
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх