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

Эта статья, третья и последняя, посвящена результатам моих исследований. Я расскажу тут об обнаруженных мной уязвимостях Apache.
Разыменование NULL в session_identity_decode
Эту ошибку можно вызвать, поместив в Cookie пару ключ/значение, оба элемента которой равны NULL.

В этом примере можно заметить, что первую позицию в Cookie занимает ключ session и значение choko. Во второй позиции ключом является admin-user, а значением — число 2. А вот третья позиция представлена пустыми ключом и значением.
Что здесь за проблема? Если посмотреть на следующий фрагмент кода, там можно заметить два вызова apr_strtok, направленных на извлечение первой и второй строки (ключа и значения):
const char *psep = "="; char *key = apr_strtok(pair, psep, &plast); char *val = apr_strtok(NULL, psep, &plast);
А вот что происходит в функции apr_strtok в том случае, если первым её аргументом является NULL:
APR_DECLARE(char *) apr_strtok(char *str, const char *sep, char **last) { char *token; if (!str) str = *last; while (*str && strchr(sep, *str)) ++str;
Тут можно видеть, что в цикле while делается попытка разыменовать первый аргумент функции (указатель str). Если этот аргумент представлен значением NULL — это приведёт к ошибке разыменования NULL. Кроме того, именно это происходит в инструкции char *val = apr_strtok(NULL, psep, &plast);, когда предыдущий ключ тоже представлен NULL.
Воспользоваться этой ошибкой можно при включённом модуле mod_session. Эта уязвимость может привести к отказу в обслуживании на уровне дочернего потока процесса, повлияв на другие его потоки.
Ошибка неучтённой единицы (воздействующая на стек) в check_nonce
Для того чтобы воспользоваться этой ошибкой — нужно, чтобы был включён модуль mod_auth_digest, и чтобы приложение использовало бы метод аутентификации DIGEST.
Для вызова ошибки нужно назначить полю nonce специфический набор значений:
GET http://127.0.0.1/i?proxy=yes HTTP/1.1 Host: foo.example Accept: / Authorization: Digest username="2", realm="private area", nonce="d2hhdGFzdXJwcmlzZXhkeGR4ZHhkeGR4ZHhkeGR4ZHhkeGR4ZA==", uri="http://127.0.0.1:80/i?proxy=yes", qop=auth, nc=00000001, cnonce="0a4f113b", response="53849ce65ba787cd0a07a272ece3bba6", opaque="5ccc069c403ebaf9f0171e9517f40e41"
Как видите, поле nonce содержит значение в кодировке BASE64. Для декодирования этого значения функция check_nonce выполняет следующий вызов:
apr_base64_decode_binary(nonce_time.arr, resp->nonce)
Здесь nonce_time.arr — это локальный массив размером 8 байтов. Посмотрим на код функции apr_base64_decode_binary:
APR_DECLARE(int) apr_base64_decode_binary(unsigned char *bufplain, const char *bufcoded) { int nbytesdecoded; register const unsigned char *bufin; register unsigned char *bufout; register apr_size_t nprbytes; bufin = (const unsigned char *) bufcoded; while (pr2six[*(bufin++)] <= 63); nprbytes = (bufin - (const unsigned char *) bufcoded) - 1; nbytesdecoded = (((int)nprbytes +3) / 4) * 3; bufout = (unsigned char *) bufplain; bufin = (const unsigned char *) bufcoded; while (nprbytes > 4) { *(bufout++) = (unsigned char) (pr2six[*bufin] << 2 | pr2six[bufin[1]] >> 4); *(bufout++) = (unsigned char) (pr2six[bufin[1]] << 4 | pr2six[bufin[2]] >> 2); *(bufout++) = (unsigned char) (pr2six[bufin[2]] << 6 | pr2six[bufin[3]]); bufin += 4; nprbytes -= 4; } if (nprbytes > 1) { *(bufout++) = (unsigned char) (pr2six[*bufin] << 2 | pr2six[bufin[1]] >> 4); } if (nprbytes > 2) { *(bufout++) = (unsigned char) (pr2six[bufin[1]] << 4 | pr2six[bufin[2]] >> 2); } if (nprbytes > 3) { *(bufout++) = (unsigned char) (pr2six[bufin[2]] << 6 | pr2six[bufin[3]]); }
В обычных обстоятельствах в переменной nprbytes окажется значение 11, а цикл while будет выполнен два раза, записав всего 8 байтов в массив bufplain (6 + 2). Но если дата имеет неправильный формат, при вычислении значения переменной nprbytes может получиться число 12. В результате в этих случаях цикл while будет выполнен три раза, в массив bufplain будет записано 9 байтов. Вследствие этого программа запишет 1 байт за пределами локального массива nonce_time.arr, перезаписав 1 байт стека программы (так и происходит ошибка неучтённой единицы).
Ошибка в cleanup_tables, связанная с использованием памяти после её освобождения
Тут у нас имеется ошибка, связанная с использованием памяти после её освобождения (Use After Free, UAF) в функции cleanup_tables. Посмотрим на код этой функции:
static apr_status_t cleanup_tables(void *not_used) { ap_log_error(APLOG_MARK, APLOG_INFO, 0, NULL, APLOGNO(01756) "cleaning up shared memory"); if (client_rmm) { apr_rmm_destroy(client_rmm); client_rmm = NULL; } if (client_shm) { apr_shm_destroy(client_shm); client_shm = NULL; }
Она вызывает функцию apr_rmm_destroy для освобождения блока памяти client_rmm. Но тут есть одна проблема: в определённых обстоятельствах этот блок памяти уже может быть освобождён функцией apr_allocator_destroy (её код тут не показан).
В результате программа пытается обратиться к недействительному адресу памяти. Это приводит к появлению уязвимости UAF. Тут важно заметить, что эта уязвимость может быть активирована лишь в режиме ONE_PROCESS.
Ошибка записи данных за пределами допустимого диапазона (воздействующая на кучу) в ap_escape_quotes
В данном случае перед нами ошибка, связанная с записью данных за пределами допустимого диапазона в куче, воздействующая на функцию ap_escape_quotes. Эта функция экранирует кавычки в предоставленной ей строке. Источник данной ошибки — несовпадение длины входной строки и буфера outstring, память под который «выделена» с помощью malloc.
В следующем фрагменте кода показано вычисление длины входной строки:
while (*inchr != '\0'){ newlen++; if (*inchr == '"') { newlen++; } if ((*inchr == '\\') && (inchr[1] != '\0')) { inchr++; newlen++; } inchr++; } outstring = apr_palloc(p, newlen + 1);
А вот — вычисление размера outstring:
while (*inchr != '\0') { if ((*inchr == '\\') && (inchr[1] != '\0')) { *outchr++ = *inchr++; *outchr++ = *inchr++; } if (*inchr == '"') { *outchr++ = '\\'; } if (*inchr != '\0') { *outchr++ = *inchr++; } } *outchr = '\0'; return outstring;
Как видите, при вычислении размеров этих сущностей используется различная логика. В результате, если функции ap_escape_quotes предоставить особые входные данные, возникает возможность записи данных за пределами массива outchr.
Об этой ошибке, за несколько дней до того, как я её обнаружил, сообщили исследователи из проекта Google OSS-Fuzz.
Состояние гонок, ведущее к UAF
Теперь хочу рассказать кое о чём совершенно отличном от того, о чём уже рассказывал. В данном случае ошибка представлена состоянием гонок, которое ведёт к UAF и воздействует на Apache Core.
В ходе моих фаззинг-исследований я столкнулся с множеством невоспроизводимых UAF-падений программы. Глубже проанализировав ситуацию, я обнаружил нечто вроде состояния гонок между apr_allocator_destroy и allocator_alloc. Всё указывало на то, что эти функции в конкурентных сценариях могут не отличаться потокобезопасностью. Это может привести к повреждениям в некоторых узлах памяти и, иногда, к тому, что программа пытается освободить память, которая уже находится в пуле free. Этот баг чем-то cхож с багом ProFTPd, о котором я сообщал год назад (CVE-2020-9273).
Ниже представлен пример соответствующего стек-трейса ASAN:
==106820==ERROR: AddressSanitizer: heap-use-after-free on address 0x625000091100 at pc 0x7ffff7d2ff4d bp 0x7fffffffd800 sp 0x7fffffffd7f8 READ of size 8 at 0x625000091100 thread T0 #0 0x7ffff7d2ff4c in apr_allocator_destroy /home/antonio/Downloads/httpd-trunk/srclib/apr/memory/unix/apr_pools.c:197:26 #1 0x7ffff7d3306c in apr_pool_terminate /home/antonio/Downloads/httpd-trunk/srclib/apr/memory/unix/apr_pools.c:756:5 #2 0x7ffff77aeba6 in __run_exit_handlers /build/glibc-5mDdLG/glibc-2.30/stdlib/exit.c:108:8 #3 0x7ffff77aed5f in exit /build/glibc-5mDdLG/glibc-2.30/stdlib/exit.c:139:3 #4 0x5b1ae8 in clean_child_exit /home/antonio/Downloads/httpd-trunk/server/mpm/event/event.c:777:5 #5 0x5b19a5 in child_main /home/antonio/Downloads/httpd-trunk/server/mpm/event/event.c:2957:5 #6 0x5afa7b in make_child /home/antonio/Downloads/httpd-trunk/server/mpm/event/event.c:2981:9 #7 0x5af005 in startup_children /home/antonio/Downloads/httpd-trunk/server/mpm/event/event.c:3046:13 #8 0x5a74c1 in event_run /home/antonio/Downloads/httpd-trunk/server/mpm/event/event.c:3407:9 #9 0x6212b1 in ap_run_mpm /home/antonio/Downloads/httpd-trunk/server/mpm_common.c:100:1 #10 0x5e67e6 in main /home/antonio/Downloads/httpd-trunk/server/main.c:891:14 #11 0x7ffff778c1e2 in __libc_start_main /build/glibc-5mDdLG/glibc-2.30/csu/../csu/libc-start.c:308:16 #12 0x44da7d in _start ??:0:0
Эта проблема не нова. О похожих ошибках сообщал в 2018 году Ханно Бок (hanno). Тут можно найти его отчёты.
Небольшие ошибки
Я, занимаясь фаззингом, обнаружил ещё кое-какие небольшие баги. Об одном из них я сейчас расскажу. Это — переполнение целочисленной переменной в функции Session_Identity_Decode. Этот баг не относится к разряду опасных, но я полагаю, что интересно будет показать пример того, как легко его вызвать.
Мы отправляем WebDav-запрос LOCK, нацеленный на MOD_DAV, передавая очень большое значение Timeout (Second-41000000004100000000):
LOCK /dav/c HTTP/1.1 Host: 127.0.0.1 Timeout: Second-41000000004100000000 Content-Type: text/xml; charset="utf-8" Content-Length: XXX Authorization: Basic Mjoz <?xml version="1.0" encoding="utf-8" ?> <d:lockinfo xmlns:d="DAV:">
В следующем фрагменте кода можно видеть такую конструкцию:
return now + expires;
Здесь выполняется сложение двух 32-битных целочисленных значений, результат операции оказывается в переменной того же типа. Если складываемые значения достаточно велики — при возврате результата операции произойдёт переполнение.
while ((val = ap_getword_white(r->pool, &timeout)) if (!strncmp(val, "Infinite", 8)) { return DAV_TIMEOUT_INFINITE; } if (!strncmp(val, "Second-", 7)) { val += 7; expires = atol(val); now = time(NULL); return now + expires; } }
Так как этот баг вызывается при выполнении запроса LOCK — для того, чтобы он проявился, должен быть включён модуль MOD_DAV.
Итоги
Хотя безопасность Apache HTTP Server уже очень хорошо изучена, учитывая недавно обнаруженные уязвимости, связанные с обходом путей и раскрытием файлов (CVE-2021-41773 и CVE-2021-42013), ясно, что в этой программе ещё можно обнаружить новые критические уязвимости.
Проводя это исследование, я хотел сделать собственный вклад в улучшение безопасности Apache HTTP Server, и показать, что фаззинг можно применять для поиска уязвимостей в одном из самых популярных опенсорсных проектов современности. Я, в то же время, надеюсь, что у меня получилось поделиться знаниями, приобретёнными в ходе этой работы, с моими читателями.
Что дальше?
Эта статья закрывает цикл «Фаззинг сокетов». В следующем материале я собираюсь рассказать о фаззинге JavaScript-движков. До новых встреч!
О, а приходите к нам работать? ?
Мы в wunderfund.io занимаемся высокочастотной алготорговлей с 2014 года. Высокочастотная торговля — это непрерывное соревнование лучших программистов и математиков всего мира. Присоединившись к нам, вы станете частью этой увлекательной схватки.
Мы предлагаем интересные и сложные задачи по анализу данных и low latency разработке для увлеченных исследователей и программистов. Гибкий график и никакой бюрократии, решения быстро принимаются и воплощаются в жизнь.
Сейчас мы ищем плюсовиков, питонистов, дата-инженеров и мл-рисерчеров.
ссылка на оригинал статьи https://habr.com/ru/company/wunderfund/blog/651559/
Добавить комментарий