[Перевод] Фаззинг сокетов: Apache HTTP Server. Часть 2: кастомные перехватчики
Прим. Wunder Fund: наш СТО Эмиль по совместительству является известным white-hat хакером и специалистом по информационной безопасности, и эту статью он предложил как хорошее знакомство с фаззером afl и вообще с фаззингом как таковым.
В первой статье из этой серии я рассказал о том, с чего стоит начать тому, кто хочет заняться фаззингом Apache HTTP Server. Там мы обсудили разработку кастомных мутаторов в AFL++, поговорили о том, как создать собственный вариант грамматики HTTP.
Сегодня я уделю внимание написанию перехватчиков 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
В данном случае значением второго аргумента является 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
Мы находимся в ситуации, когда вызываем apr_palloc
с size = 126
.
Нам надо выделить 126 байт памяти
Но библиотека ASAN «отравила» область памяти размером в 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
Как видите, тут, по умолчанию, имеется по одному вызову 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
Позже адрес этой функции сохраняется в указателе ptr_to_real
.
Запись адреса функции в указатель
В результате, если подвести краткие итоги, получается, что для создания собственного перехватчика ASAN нужно выполнить следующие шаги:
Определить
INTERCEPTOR(int, foo, const char bar, double baz) { ... }
, гдеfoo
— имя функции, которую мы хотим перехватить.Вызвать
ASAN_INTERCEPT_FUNC (foo)
до первого вызова функцииfoo
(обычно — из функцииInitializeAsanInterceptors
).
Теперь я покажу реальный примере перехвата функции в библиотеке APR (Apache Portable Runtime).
Пример перехвата apr_palloc
Apache, как уже было сказано, использует кастомный пул памяти ради улучшения управления динамической памятью приложения. Именно поэтому, если мы хотим выделить память в пуле, нам нужно обращаться не к malloc
, а к 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
После возврата из функции 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
Я воспользовался именно этим подходом из-за того, что он прост, и что его особенности легко объяснять. Но он довольно-таки неэффективен, так как подразумевает просмотр всего массива 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
Аналогично, в конец функций можно добавить ENABLE_LOG = 0;
. Вот код функции log_high
.
Функция log_high
Теперь, так же, как мы уже делали в предыдущем разделе, воспользуемся механизмом перехватчиков ASAN для перехвата системных вызовов, направленных на файловую систему. В случае с Apache я перехватываю следующие системные вызовы:
open
rename
unlink
Перехватчик
Вот — пример выходного файла.
Выходной файл
После того, как в нашем распоряжении оказывается выходной файл, нам надо его проанализировать. Я для этого воспользовался Elasticsearch и провёл анализ файла, выполняемый после его формирования. Описание методики такого анализа выходит за рамки этого материала. Про анализ логов API с помощью Elasticsearch я планирую рассказать в другой статье. Ещё я расскажу о том, как, используя AFL++, можно провести анализ подобных данных в реальном времени.
Продолжение следует
В последнем материале из этой серии я расскажу об уязвимостях, обнаруженных мной в Apache HTTP Server с применением методов, описанных в этой и в предыдущей статьях. А так как это будет последний материал моей серии статей «Фаззинг сокетов», я расскажу там о том, самом важном, что узнал, занимаясь фаззингом, и познакомлю читателей с темой моего следующего исследования.
До встречи в третьей части!
О, а приходите к нам работать?