Прим. Wunder Fund: наш СТО Эмиль по совместительству является известным white-hat хакером и специалистом по информационной безопасности, и эту статью он предложил как хорошее знакомство с фаззером afl и вообще с фаззингом как таковым.
В первой статье из этой серии я рассказал о том, с чего стоит начать тому, кто хочет заняться фаззингом Apache HTTP Server. Там мы обсудили разработку кастомных мутаторов в AFL++, поговорили о том, как создать собственный вариант грамматики HTTP.
![](https://habrastorage.org/getpro/habr/upload_files/1e0/176/3be/1e01763be0a1d55dcef6c93822121ac0.png)
Сегодня я уделю внимание написанию перехватчиков ASAN, которые позволяют «ловить» баги в кастомных пулах памяти. Здесь пойдёт речь и о том, как перехватывать системные вызовы, нацеленные на файловую систему. Это позволяет выявлять логические ошибки в исследуемом приложении.
«Отравляем» память
Сначала по-быстрому разберёмся с некоторыми механизмами ASAN (Address Sanitizer), средства, направленного на выявление ошибок, возникающих при работе с памятью. В частности, речь идёт о теневой памяти (shadow memory) и об «отравлении» памяти (memory poisoning).
ASAN применяет теневую память, с помощью которой организовано наблюдение за всей реальной памятью, используемой приложением. Система способна определить возможность адресации такой памяти. Последовательности байтов в областях памяти, обращение к которым недопустимо, называются красными зонами, или «отравленной» памятью.
В результате, при компиляции программы с использованием Address Sanitizer, этот инструмент оснащает каждую операцию доступа к памяти проверкой. Затем ASAN наблюдает за работой программы. Если программа попытается записать данные в неправильную область памяти, выполнение программы будет остановлено, сформируется диагностический отчёт. Если же программа не делает ничего ненормального, она сможет продолжать работу в обычном режиме. Это позволяет программисту выявлять самые разные проблемы, касающиеся некорректного доступа к памяти и неправильного управления памятью.
![Теневая память и память процесса Теневая память и память процесса](https://habrastorage.org/getpro/habr/upload_files/168/670/0a8/1686700a8166d4323c02674a83687e0d.png)
В некоторых случаях возможность контролировать «отравленную» память может очень пригодиться программистам и исследователям информационной безопасности. Например, представьте себе особенную функцию, в которой управление памятью организовано так, что 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](https://habrastorage.org/getpro/habr/upload_files/c93/1a0/032/c931a00326330d5a94959431a398a893.png)
В данном случае значением второго аргумента является 126 (in_size = 126
), или, другими словами, наша цель заключается в том, чтобы выделить 126 байтов в пуле памяти g->apr_pool
. Запрошенное значение будет округлено до 128 байтов из-за требований по выравниванию памяти. Это вполне очевидно.
Если вы посмотрите один из предыдущих материалов этой серии, посвящённый, кроме прочего, ProFTPd, то найдёте сведения о внутренней реализации пулов памяти в ProFTPd. В частности, она основана на реализации подобных механизмов в Apache HTTP. В результате в данном случае эти реализации практически идентичны. Пулы памяти Apache HTTP представляют собой связные списки узлов памяти. Пример такого списка показан ниже.
![Связный список узлов памяти Связный список узлов памяти](https://habrastorage.org/getpro/habr/upload_files/406/c18/50c/406c1850cd264eac7568b3ba636ea231.png)
Программа добавляет в этот список новые узлы по мере возникновения необходимости в новой памяти. Когда свободного пространства узла недостаточно для удовлетворения нужд apr_palloc
— вызывается функция allocator_alloc
. Эта функция ответственна за создание нового узла и за добавление его в связный список. Но, что можно видеть на следующей иллюстрации, размер выделяемой памяти всегда округляется до MIN_ALLOC
байтов. В результате каждый из узлов списка будет располагать, как минимум, MIN_ALLOC
свободной памяти.
![Выделение памяти Выделение памяти](https://habrastorage.org/getpro/habr/upload_files/eaa/e0d/1d8/eaae0d1d86de1c7d0fa9aeff77cbdd6c.png)
Позже внутри этой функции выполняется вызов malloc
, цель которого — выделение новой памяти для создаваемого узла. На следующем рисунке видно, как выполняется вызов malloc
с size=8192
.
![Анализ вызова malloc Анализ вызова malloc](https://habrastorage.org/getpro/habr/upload_files/2cb/a83/abd/2cba83abdd08a8bb013d42b4e50f5575.png)
Мы находимся в ситуации, когда вызываем apr_palloc
с size = 126
.
![Нам надо выделить 126 байт памяти Нам надо выделить 126 байт памяти](https://habrastorage.org/getpro/habr/upload_files/96e/eb6/559/96eeb6559593b958572a455671c31190.png)
Но библиотека ASAN «отравила» область памяти размером в 8192 байта.
![«Отравленная» область памяти размером 8192 байта «Отравленная» область памяти размером 8192 байта](https://habrastorage.org/getpro/habr/upload_files/cb0/9c7/5c3/cb09c75c32fb3639ebff43bfbeca6c5b.png)
В итоге оказывается, что 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](https://habrastorage.org/getpro/habr/upload_files/3bd/3bc/ce0/3bd3bcce0846b96a05c9ede1e9966f10.png)
Как видите, тут, по умолчанию, имеется по одному вызову 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](https://habrastorage.org/getpro/habr/upload_files/9f1/8a5/55e/9f18a555e093a2e9ca3e0a407bf01c3e.png)
Позже адрес этой функции сохраняется в указателе ptr_to_real
.
![Запись адреса функции в указатель Запись адреса функции в указатель](https://habrastorage.org/getpro/habr/upload_files/d83/d2f/f7a/d83d2ff7ac24850a3cca5cc5415eef6c.png)
В результате, если подвести краткие итоги, получается, что для создания собственного перехватчика 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, …) Код реализации INTERCEPTOR(void, apr_palloc, …)](https://habrastorage.org/getpro/habr/upload_files/4d1/e6f/8d7/4d1e6f8d73e1d4bf0abc8624b7f22771.png)
Макрос 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](https://habrastorage.org/getpro/habr/upload_files/3e5/366/bdb/3e5366bdb448ae1e5c55a8782d600fe1.png)
После возврата из функции 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](https://habrastorage.org/getpro/habr/upload_files/f22/70b/f33/f2270bf331bc72b13ddaac9c10b21c68.png)
Я воспользовался именно этим подходом из-за того, что он прост, и что его особенности легко объяснять. Но он довольно-таки неэффективен, так как подразумевает просмотр всего массива addr
. Лучше было бы сохранять адреса узлов в unique(vector)
или в std::set
и делать так, чтобы каждый из них указывал бы на связный список адресов, выделенных с помощью asan_malloc
.
![Более эффективный подход к хранению сведений о выделенной памяти Более эффективный подход к хранению сведений о выделенной памяти](https://habrastorage.org/getpro/habr/upload_files/3bd/93b/b82/3bd93bb82086a7c745efb30960ccc243.png)
Файловые мониторы
Когда фаззят файловый сервер, вроде 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](https://habrastorage.org/getpro/habr/upload_files/ef0/6f1/58b/ef06f158b4badc7b2ea2dd1a62a6f905.png)
Аналогично, в конец функций можно добавить ENABLE_LOG = 0;
. Вот код функции log_high
.
![Функция log_high Функция log_high](https://habrastorage.org/getpro/habr/upload_files/5d2/b6b/8d1/5d2b6b8d169473bd76327d550fe51245.png)
Теперь, так же, как мы уже делали в предыдущем разделе, воспользуемся механизмом перехватчиков ASAN для перехвата системных вызовов, направленных на файловую систему. В случае с Apache я перехватываю следующие системные вызовы:
open
rename
unlink
![Перехватчик Перехватчик](https://habrastorage.org/getpro/habr/upload_files/78a/1c7/ce4/78a1c7ce4d72f6fb984509e978499d93.png)
Вот — пример выходного файла.
![Выходной файл Выходной файл](https://habrastorage.org/getpro/habr/upload_files/79a/149/e16/79a149e1647ee77371ddb0e08d2ee8b9.png)
После того, как в нашем распоряжении оказывается выходной файл, нам надо его проанализировать. Я для этого воспользовался Elasticsearch и провёл анализ файла, выполняемый после его формирования. Описание методики такого анализа выходит за рамки этого материала. Про анализ логов API с помощью Elasticsearch я планирую рассказать в другой статье. Ещё я расскажу о том, как, используя AFL++, можно провести анализ подобных данных в реальном времени.
Продолжение следует
В последнем материале из этой серии я расскажу об уязвимостях, обнаруженных мной в Apache HTTP Server с применением методов, описанных в этой и в предыдущей статьях. А так как это будет последний материал моей серии статей «Фаззинг сокетов», я расскажу там о том, самом важном, что узнал, занимаясь фаззингом, и познакомлю читателей с темой моего следующего исследования.
До встречи в третьей части!
О, а приходите к нам работать? ????
Мы в wunderfund.io занимаемся высокочастотной алготорговлей с 2014 года. Высокочастотная торговля — это непрерывное соревнование лучших программистов и математиков всего мира. Присоединившись к нам, вы станете частью этой увлекательной схватки.
Мы предлагаем интересные и сложные задачи по анализу данных и low latency разработке для увлеченных исследователей и программистов. Гибкий график и никакой бюрократии, решения быстро принимаются и воплощаются в жизнь.
Сейчас мы ищем плюсовиков, питонистов, дата-инженеров и мл-рисерчеров.