Фаззинг сокетов: 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 <sanitizer/asan_interface.h>.

После этого можно применять макросы 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-запросов. В случае с PUTMOVE и 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 с применением методов, описанных в этой и в предыдущей статьях. А так как это будет последний материал моей серии статей «Фаззинг сокетов», я расскажу там о том, самом важном, что узнал, занимаясь фаззингом, и познакомлю читателей с темой моего следующего исследования.

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

О, а приходите к нам работать?
Источник: https://habr.com/ru/company/wunderfund/blog/650815/


Интересные статьи

Интересные статьи

В этой статье вы познакомитесь с основами работы искусственных нейронов. В последующих статьях мы изучим основы работы нейронных сетей и напишем простейшую нейронную сеть...
ZX Spectrum 128 и его многочисленные клоны имели встроенный звукогенератор AY-3-8912, благодаря чему как зарубежные музыканты, так и наши соотечественники успели написать...
Материал статьи взят с моего дзен-канала. Создаем тональный генератор В предыдущей статье мы выполнили установку библиотеки медиастримера, инструментов разработки и проверили их функционировани...
Сегодня быть онлайн — это привычное состояние для многих людей. Все мы покупаем, общаемся, читаем статьи, ищем информацию на разные темы. Сеть соединяет нас со всем миром, но прежде всего, она ...
Привет, Хабр! Сегодня мы построим систему, которая будет при помощи Spark Streaming обрабатывать потоки сообщений Apache Kafka и записывать результат обработки в облачную базу данных AWS RDS. ...