[Перевод] Фаззинг сокетов: Apache HTTP Server. Часть 2: кастомные перехватчики

Прим. Wunder Fund:  наш СТО Эмиль по совместительству является известным white-hat хакером и специалистом по информационной безопасности, и эту статью он предложил как хорошее знакомство с фаззером afl и вообще с фаззингом как таковым.

В первой статье из этой серии я рассказал о том, с чего стоит начать тому, кто хочет заняться фаззингом Apache HTTP Server. Там мы обсудили разработку кастомных мутаторов в AFL++, поговорили о том, как создать собственный вариант грамматики HTTP.

image-loader.svg

Сегодня я уделю внимание написанию перехватчиков ASAN, которые позволяют «ловить» баги в кастомных пулах памяти. Здесь пойдёт речь и о том, как перехватывать системные вызовы, нацеленные на файловую систему. Это позволяет выявлять логические ошибки в исследуемом приложении.

«Отравляем» память

Сначала по-быстрому разберёмся с некоторыми механизмами ASAN (Address Sanitizer), средства, направленного на выявление ошибок, возникающих при работе с памятью. В частности, речь идёт о теневой памяти (shadow memory) и об «отравлении» памяти (memory poisoning).

ASAN применяет теневую память, с помощью которой организовано наблюдение за всей реальной памятью, используемой приложением. Система способна определить возможность адресации такой памяти. Последовательности байтов в областях памяти, обращение к которым недопустимо, называются красными зонами, или «отравленной» памятью.

В результате, при компиляции программы с использованием Address Sanitizer, этот инструмент оснащает каждую операцию доступа к памяти проверкой. Затем ASAN наблюдает за работой программы. Если программа попытается записать данные в неправильную область памяти, выполнение программы будет остановлено, сформируется диагностический отчёт. Если же программа не делает ничего ненормального, она сможет продолжать работу в обычном режиме. Это позволяет программисту выявлять самые разные проблемы, касающиеся некорректного доступа к памяти и неправильного управления памятью.

Теневая память и память процессаТеневая память и память процесса

В некоторых случаях возможность контролировать «отравленную» память может очень пригодиться программистам и исследователям информационной безопасности. Например, представьте себе особенную функцию, в которой управление памятью организовано так, что ASAN не может его проанализировать. Именно для таких случаев в ASAN имеется внешний API для ручного «отравления» памяти. С помощью этого API программист может делать участки памяти «отравленными» и «неотравленными».

Воспользоваться этими возможностями через внешний интерфейс библиотеки ASAN можно, включив её в состав своего кода:

#include .

После этого можно применять макросы ASAN_POISON_MEMORY_REGION и ASAN_UNPOISON_MEMORY_REGION при вызове, соответственно,  malloc и free. Обычно этими инструментами пользуются так: сначала «отравляют» целую область памяти, а затем делают «неотравленными» выделяемые фрагменты памяти, оставляя между ними «отравленные» красные зоны.

Этот подход сравнительно прост, его несложно реализовать. Но он, каждый раз, когда его применяют к новой программе, являет собой пример «изобретения колеса». Хорошо было бы иметь возможность просто перехватывать вызовы определённой группы функций, действуя так же, как действует сама библиотека ASAN.

Именно по этой причине я хочу рассказать о другом подходе к решению этой задачи, о кастомных перехватчиках.

Кастомные перехватчики

Зачем это нужно?

В начале этого материала я говорил о кастомных перехватчиках и о собственных реализациях пулов памяти, как в Apache HTTP. Поэтому вопрос, который мы должны себе задать, звучит так: «Почему может понадобиться реализовывать кастомные перехватчики?».

Для того чтобы лучше в этом разобраться — рассмотрим пример.

Пусть есть фрагмент кода, где, для выделения памяти, вызывают apr_palloc:

Код, в котором используется apr_pallocКод, в котором используется apr_palloc

В данном случае значением второго аргумента является 126 (in_size = 126), или, другими словами, наша цель заключается в том, чтобы выделить 126 байтов в пуле памяти g->apr_pool. Запрошенное значение будет округлено до 128 байтов из-за требований по выравниванию памяти. Это вполне очевидно.

Если вы посмотрите один из предыдущих материалов этой серии, посвящённый, кроме прочего,  ProFTPd, то найдёте сведения о внутренней реализации пулов памяти в ProFTPd. В частности, она основана на реализации подобных механизмов в Apache HTTP. В результате в данном случае эти реализации практически идентичны. Пулы памяти Apache HTTP представляют собой связные списки узлов памяти. Пример такого списка показан ниже.

Связный список узлов памятиСвязный список узлов памяти

Программа добавляет в этот список новые узлы по мере возникновения необходимости в новой памяти. Когда свободного пространства узла недостаточно для удовлетворения нужд apr_palloc — вызывается функция allocator_alloc. Эта функция ответственна за создание нового узла и за добавление его в связный список. Но, что можно видеть на следующей иллюстрации, размер выделяемой памяти всегда округляется до MIN_ALLOC байтов. В результате каждый из узлов списка будет располагать, как минимум, MIN_ALLOC свободной памяти.

Выделение памятиВыделение памяти

Позже внутри этой функции выполняется вызов malloc, цель которого — выделение новой памяти для создаваемого узла. На следующем рисунке видно, как выполняется вызов malloc с size=8192.

Анализ вызова mallocАнализ вызова malloc

Мы находимся в ситуации, когда вызываем apr_palloc с size = 126.

Нам надо выделить 126 байт памятиНам надо выделить 126 байт памяти

Но библиотека ASAN «отравила» область памяти размером в 8192 байта.

«Отравленная» область памяти размером 8192 байта«Отравленная» область памяти размером 8192 байта

В итоге оказывается, что ASAN помечает 8192-126 = 8066 байтов как те, в которые можно осуществлять запись данных. А, на самом деле, это не память, уже выделенная под какие-то конкретные данные, а лишь свободная память, принадлежащая узлу связного списка. В результате следующий вызов memcpy(np, source, 5000) приведёт к выполнению операции записи, выходящей за пределы допустимого диапазона, к перезаписи оставшейся памяти узла. Но ASAN при этом не сообщит нам о том, что что-то пошло не так. Это приведёт к тому, что мы, даже при включении ASAN, упустим ошибки, связанные с повреждением памяти.

Подобные ошибки могут приводить к уязвимостям, вроде CVE-2020–9273 в ProFTPd, о которой я сообщил год назад.

Подготовительные шаги

Расскажу о том, как собирать LLVM-санитайзеры из исходного кода. Это нужно для того чтобы добавить в библиотеку ASAN наши кастомные перехватчики.

Для начала надо знать о том, что исполняемый код LLVM-санитайзера является частью того, что известно как библиотеки времени выполнения compiler-rt. В моём случае был загружен исходный код compiler-rt версии 9.0.0, так как это была именно та версия, которую я до этого установил в моём дистрибутиве Linux. Загрузить этот код можно отсюда.

Собирают compiler-rt так:

cd compiler-rt-9.0.0.src
mkdir build-compiler-rt
cd build-compiler-rt
cmake ../
make

После завершения процесса сборки надо добавить в систему следующие переменные окружения, нужные для выполнения процесса сборки Apache:

LD_LIBRARY_PATH= /Downloads/compiler-rt-9.0.0.src/build-compiler-rt/lib/linux
CFLAGS="-I/Downloads/compiler-rt-9.0.0.src/lib -shared-libasan"

В итоге нужно установить переменную окружения LD_LIBRARY_PATH в следующее значение:

LD_LIBRARY_PATH=/Downloads/compiler-rt-9.0.0.src/build-compiler-rt/lib/linux

Внутренние механизмы перехватчиков ASAN

Как мы уже знаем, ASAN, для организации наблюдения за использованием памяти, необходимо перехватывать вызовы malloc и free. Для этого нужно, чтобы соответствующие механизмы времени выполнения были бы загружены до библиотеки, экспортирующей эти функции. Поэтому, когда мы применяем флаг линковщика -fsanitize=address, компилятор ставит библиотеку libasan первой в системе поиска символов.

Если исследовать код, можно обнаружить, что функция входа называется __asan_init. Эта функция, в свою очередь, вызывает функции AsanActivate и AsanInternal. Во второй из этих функций решается большая часть задач по инициализации системы и вызывается InitializeAsanInterceptors. Именно эта функция представляет для нас наибольший интерес.

Функция InitializeAsanInterceptorsФункция InitializeAsanInterceptors

Как видите, тут, по умолчанию, имеется по одному вызову ASAN_INTERCEPT_FUNC для каждого из перехватчиков ASAN. ASAN_INTERCEPT_FUNC — это макрос, транслируемый в Linux-системах в INTERCEPT_FUNCTION_LINUX_OR_FREEBSD. Этот макрос, в итоге, вызывает функцию InterceptFunction, ответственную за реализацию логики перехвата.

#define ASAN_INTERCEPT_FUNC(name) do { \
      if (!INTERCEPT_FUNCTION(name) && flags()->verbosity > 0) \
        Report("AddressSanitizer: failed to intercept '" #name "'\n"); \
    } while (0)

# define INTERCEPT_FUNCTION(func) INTERCEPT_FUNCTION_LINUX_OR_FREEBSD(func)
#define INTERCEPT_FUNCTION_LINUX_OR_FREEBSD(func) \
  ::__interception::InterceptFunction(            \
      #func,                                      \
      (::__interception::uptr *) & REAL(func),    \
      (::__interception::uptr) & (func),          \
      (::__interception::uptr) & WRAP(func))

В этой функции осуществляется вызов функции GetFuncAddr, а она, в свою очередь, вызывает dlsym()Dlsym позволяет программе получить адрес, по которому в память загружен символ (перехваченная функция).

Вызов dlsymВызов dlsym

Позже адрес этой функции сохраняется в указателе ptr_to_real.

Запись адреса функции в указательЗапись адреса функции в указатель

В результате, если подвести краткие итоги, получается, что для создания собственного перехватчика ASAN нужно выполнить следующие шаги:

  1. Определить INTERCEPTOR(int, foo, const char bar, double baz) { ... }, где foo — имя функции, которую мы хотим перехватить.

  2. Вызвать ASAN_INTERCEPT_FUNC (foo) до первого вызова функции foo (обычно — из функции InitializeAsanInterceptors).

Теперь я покажу реальный примере перехвата функции в библиотеке APR (Apache Portable Runtime).

Пример перехвата apr_palloc

Apache, как уже было сказано, использует кастомный пул памяти ради улучшения управления динамической памятью приложения. Именно поэтому, если мы хотим выделить память в пуле, нам нужно обращаться не к malloc, а к apr_palloc.

Для начала покажу код моей реализации INTERCEPTOR(void, apr_palloc, …).

Код реализации INTERCEPTOR(void, apr_palloc, …)Код реализации INTERCEPTOR (void, apr_palloc, …)

Макрос ENSURE_ASAN_INITED(), проверяет, прежде чем продолжить выполнение кода, была ли уже инициализирована библиотека ASAN.

Макрос GET_STACK_TRACE_MALLOC получает текущий стек-трейс. В результате, если произойдёт перехват, выводится отчёт ASAN. Мы будем придерживаться одного общего правила, которое заключается в том, чтобы выводить в перехватчиках данные о трассировке стека как можно раньше, так как нам не нужно, чтобы в эти данные попали бы сведения о внутренних функциях ASAN.

Затем мы, пользуясь REAL(apr_palloc), вызываем исходную функцию apr_palloc. Это нужно для создания внутренних структур пула памяти. Сама же функция apr_palloc вызывает функцию allocator_alloc, которая ответственна за выделение памяти тогда, когда это нужно. Но мы заменяем вызов malloc (который перехватывается ASAN) на __libc_malloc. Это позволяет избежать того, что вся память узла делается «неотравленной».

Функция allocator_allocФункция allocator_alloc

После возврата из функции apr_palloc мы выполняем выравнивание целочисленного значения in_size, поступая так же, как APR. Это приведёт к тому, что оба размера будут одинаковыми. Сразу после этого мы вызовем asan_malloc для выделения нового блока памяти размера in_size. Эта новая выделенная память будет находиться под контролем ASAN.

И наконец — мы сохраним адреса libc_malloc и asan_malloc в массиве. Это позволит освободить блок памяти, выделенный ASAN, после того, как будет уничтожен узел памяти.

Так же, как мы поступили с malloc, мы можем поступить и с вызовами free(), имеющими отношение к освобождению памяти узлов. В нашем случае планируется модифицировать функции allocator_free и apr_allocator_destroy. Мы, кроме того, должны освободить память, выделенную до этого с помощью asan_malloc. С этой целью я просмотрел массив addr, где были сохранены адреса, и освободил все блоки памяти, связанные с данным узлом. В конце я применил непосредственный вызов функции free() с использованием инструкции __libc_free(node).

Функция allocator_freeФункция allocator_free

Я воспользовался именно этим подходом из-за того, что он прост, и что его особенности легко объяснять. Но он довольно-таки неэффективен, так как подразумевает просмотр всего массива addr. Лучше было бы сохранять адреса узлов в unique(vector) или в std::set и делать так, чтобы каждый из них указывал бы на связный список адресов, выделенных с помощью asan_malloc.

Более эффективный подход к хранению сведений о выделенной памятиБолее эффективный подход к хранению сведений о выделенной памяти

Файловые мониторы

Когда фаззят файловый сервер, вроде FTP- или HTTP-сервера, ему отправляют множество запросов, которые преобразуются в системные вызовы, направленные на файловую систему удалённого сервера (open(),  write(),  read() и так далее). В ходе этого процесса могут проявляться логические уязвимости, относящиеся к разрешениям на доступ к файлам, такие, как обход проверки доступа (access bypass), обход бизнес-логики (business flow bypass) и прочие подобные.

В большинстве случаев, правда, выявление подобных уязвимостей с помощью фаззера, вроде AFL, может оказаться весьма сложной задачей. Дело в том, что подобные фаззеры, в основном, нацелены на выявление уязвимостей, связанных с управлением памятью (переполнение стека, переполнение кучи и так далее). Именно поэтому нам, для отлова «файловых» уязвимостей, надо реализовать новый метод детектирования ошибок.

Метод поиска ошибок, о котором я расскажу, представляет собой базовый механизм, основанный на перехвате и сохранении системных вызовов, нацеленных на работу с файлами. Сохранённые данные позже можно будет проанализировать. Основная идея этого подхода заключается в том, чтобы сравнивать низкоуровневые обращения к файловой системе с их высокоуровневыми «коллегами». В ходе сравнения проверяют то, являются ли выполненные системные вызовы тем, чем они должны быть, выясняют, в правильном ли порядке они выполнены, а так же то, правильные ли аргументы им переданы.

Для того чтобы показать пример применения этого подхода, поработаем с тремя запросами WebDav:

  • PUT

  • MOVE

  • DELETE

Загрузить файл с кодом этих запросов можно отсюда.

Для начала я собираюсь идентифицировать высокоуровневые функции, вовлечённые в обработку соответствующих HTTP-запросов. В случае с PUT,  MOVE и DELETE — это, соответственно, следующие функции:

static int dav_method_put(request_rec *r)
static int dav_method_copymove(request_rec *r, int is_move)
static int dav_method_delete(request_rec *r)

Затем я добавляю в начало каждой функции вызов функции log_high.

Вызов log_highВызов log_high

Аналогично, в конец функций можно добавить ENABLE_LOG = 0;. Вот код функции log_high.

Функция log_highФункция log_high

Теперь, так же, как мы уже делали в предыдущем разделе, воспользуемся механизмом перехватчиков ASAN для перехвата системных вызовов, направленных на файловую систему. В случае с Apache я перехватываю следующие системные вызовы:

  • open

  • rename

  • unlink

ПерехватчикПерехватчик

Вот — пример выходного файла.

Выходной файлВыходной файл

После того, как в нашем распоряжении оказывается выходной файл, нам надо его проанализировать. Я для этого воспользовался Elasticsearch и провёл анализ файла, выполняемый после его формирования. Описание методики такого анализа выходит за рамки этого материала. Про анализ логов API с помощью Elasticsearch я планирую рассказать в другой статье. Ещё я расскажу о том, как, используя AFL++, можно провести анализ подобных данных в реальном времени.

Продолжение следует

В последнем материале из этой серии я расскажу об уязвимостях, обнаруженных мной в Apache HTTP Server с применением методов, описанных в этой и в предыдущей статьях. А так как это будет последний материал моей серии статей «Фаззинг сокетов», я расскажу там о том, самом важном, что узнал, занимаясь фаззингом, и познакомлю читателей с темой моего следующего исследования.

До встречи в третьей части!

О, а приходите к нам работать?

© Habrahabr.ru