Переполнение буфера в куче в функции __nss_hostname_digits_dots в glibc 2.2 (и других сборках 2.x, выпущенных до версии 2.18) позволяет злоумышленникам, в зависимости от ситуации, выполнить произвольный код при помощи функций gethostbyname или gethostbyname2.
Автор: Rajivarnan.R
Описание уязвимости
Переполнение буфера в куче в функции __nss_hostname_digits_dots в glibc 2.2 (и других сборках 2.x, выпущенных до версии 2.18) позволяет злоумышленникам, в зависимости от ситуации, выполнить произвольный код при помощи функций gethostbyname или gethostbyname2.
Уязвимость, известная под именем «GHOST» (CVE-2015-0235), позволяет злоумышленнику удаленно получить полный контроль над системой жертвы без использования каких-либо предварительных сведений об учетных записях системы.
Брешь обнаружили специалисты компании Qualys. В результате плотной совместной работы с поставщиками линуксовых дистрибутивов, выпущены рекомендации по защите и патчи.
Что такое glibc?
Glibs (GNU C Library) представляет собой реализацию стандартной библиотеки языка СИ и является одним из ключевых компонентов, без которого система на базе Линукс работать не будет.
В чем суть уязвимости?
Во время исследования кода специалисты компании Qualys обнаружили брешь, связанную с переполнением буфера, в функции __nss_hostname_digits_dots() из библиотеки glibc. Уязвимость можно эксплуатировать как локально, так и удаленно через функции типа gethostbyname*(). Функции из семейства gethostbyname*() используются в приложениях для доступа к DNS resolver’у и для преобразования имени хоста в IP-адрес.
После обнаружения брешь получила кодовое название «GHOST». Кроме того, наши специалисты решили провести детальный анализ того, насколько серьезна эта угроза.
Главные выводы следующие:
· Переполняемый буфер, используемый в функциях gethostbyname() и gethostbyname2() находится в куче и доставляется вызывающей функцией (то есть, теоретически буфер может находиться в куче, стеке, .data, .bss и т. д.; хотя на практике мы не обнаружили подобные вызовы).
· Количество байтов, которое может быть перезаписано, не превышает размера (char *) (то есть, может быть перезаписано не более 4-х байт на 32-битных машинах, и 8-ми байт на 64-битных машинах). Байты могут быть перезаписаны только цифрами ('0'...'9'), точками ('.') и завершающим пустым символом ('\0').
· Даже несмотря на вышеуказанные ограничения, можно запустить произвольный код. В качестве доказательства был создан законченный эксплоит, удаленно эксплуатирующий уязвимость на почтовом сервере на базе Exim, с обходом всех существующих защит (ASLR, PIE и NX) на 32-битных и 64-битных машинах. В будущем эксплоит будет опубликован в виде модуля Metasploit.
· Первая уязвимая версия GNU C Library - glibc-2.2, выпущенная 10 ноября 2000 года.
· Мы нашли ряд факторов, снижающих угрозу использования уязвимости. В частности, мы обнаружили, что брешь была исправлена в мае 2013 года (между выпусками glibc-2.17 и glibc-2.18). К сожалению, в тот момент уязвимость была недооценена, вследствие чего многие дистрибутивы остались (и продолжают оставаться) незащищенными (например, Debian 7 Wheezy, Red Hat Enterprise Linux 6 & 7, CentOS 6 & 7,Ubuntu 12.04).
Анализ уязвимости
Уязвимая функция, __nss_hostname_digits_dots(), вызывается внутри glibc в файле nss/getXXbyYY.c (нереентерабельная версия) и в файле nss/getXXbyYY_r.c (реентерабельная версия). Вызовы окружены конструкцией #ifdef HANDLE_DIGITS_DOTS, но макрос объявлен только в следующих файлах:
· inet/gethstbynm.c
· inet/gethstbynm2.c
· inet/gethstbynm_r.c
· inet/gethstbynm2_r.c
· nscd/gethstbynm3_r.c
В этих файлах реализовано семейство функций gethostbyname*(). Следовательно, только через эти функции можно добраться до _nss_hostname_digits_dots() и вызвать переполнение буфера. Функция __nss_hostname_digits_dots() используется с целью избежать ресурсозатратных запросов к DNS в случае, если аргумент hostname представляет собой IPv4- или IPv6-адрес.
Код, показанный ниже, находится в glibc-2.17:
35 int
36 __nss_hostname_digits_dots (const char *name, struct hostent *resbuf,
37 char **buffer, size_t *buffer_size,
38 size_t buflen, struct hostent **result,
39 enum nss_status *status, int af, int *h_errnop)
40 {
..
57 if (isdigit (name[0]) || isxdigit (name[0]) || name[0] == ':')
58 {
59 const char *cp;
60 char *hostname;
61 typedef unsigned char host_addr_t[16];
62 host_addr_t *host_addr;
63 typedef char *host_addr_list_t[2];
64 host_addr_list_t *h_addr_ptrs;
65 char **h_alias_ptr;
66 size_t size_needed;
..
85 size_needed = (sizeof (*host_addr)
86 + sizeof (*h_addr_ptrs) + strlen (name) + 1);
87
88 if (buffer_size == NULL)
89 {
90 if (buflen < size_needed)
91 {
..
95 goto done;
96 }
97 }
98 else if (buffer_size != NULL && *buffer_size < size_needed)
99 {
100 char *new_buf;
101 *buffer_size = size_needed;
102 new_buf = (char *) realloc (*buffer, *buffer_size);
103
104 if (new_buf == NULL)
105 {
...
114 goto done;
115 }
116 *buffer = new_buf;
117 }
...
121 host_addr = (host_addr_t *) *buffer;
122 h_addr_ptrs = (host_addr_list_t *)
123 ((char *) host_addr + sizeof (*host_addr));
124 h_alias_ptr = (char **) ((char *) h_addr_ptrs + sizeof (*h_addr_ptrs));
125 hostname = (char *) h_alias_ptr + sizeof (*h_alias_ptr);
126
127 if (isdigit (name[0]))
128 {
129 for (cp = name;; ++cp)
130 {
131 if (*cp == '\0')
132 {
133 int ok;
134
135 if (*--cp == '.')
136 break;
...
142 if (af == AF_INET)
143 ok = __inet_aton (name, (struct in_addr *) host_addr);
144 else
145 {
146 assert (af == AF_INET6);
147 ok = inet_pton (af, name, host_addr) > 0;
148 }
149 if (! ok)
150 {
...
154 goto done;
155 }
156
157 resbuf->h_name = strcpy (hostname, name);
...
194 goto done;
195 }
196
197 if (!isdigit (*cp) && *cp != '.')
198 break;
199 }
200 }
...
В строках 85-86 в переменной size_needed вычисляется размер, необходимый для хранения трех (3) элементов буфера: host_addr, h_addr_ptrs, и имени хоста (hostname). В строках 88-117 происходит проверка, достаточного ли размера буфер: строки 88-97 относятся к реентерабельному случаю, строки 98-117 – к нереентерабельному.
В строках 121-125 происходит подготовка указателей для хранения четырех (4) элементов буфера: host_addr, h_addr_ptrs, h_alias_ptr, и hostname. Размер *h_alias_ptr (символьный указатель) не используется в формуле для вычисления размера, необходимого для буфера (переменная size_needed).
Таким образом, функция strcpy() в строке 157 позволяет нам записать данные вне пределов буфера. Размер записываемой информации не более (в зависимости от результата strlen(name) и выравнивания) 4-х байт на 32-битных машинах или 8-ми байт на 64-битных машинах. После строки 200 есть еще один вызов strcpy(), но без переполнения:
236 size_needed = (sizeof (*host_addr)
237 + sizeof (*h_addr_ptrs) + strlen (name) + 1);
...
267 host_addr = (host_addr_t *) *buffer;
268 h_addr_ptrs = (host_addr_list_t *)
269 ((char *) host_addr + sizeof (*host_addr));
270 hostname = (char *) h_addr_ptrs + sizeof (*h_addr_ptrs);
...
289 resbuf->h_name = strcpy (hostname, name);
Чтобы добиться переполнения в строке 157, аргумент hostname должен удовлетворять следующим требованиям:
· Первый символ аргумента должен быть цифрой (строка 127).
· Последний символ аргумента не должен быть точкой (строка 135).
· Аргумент должен содержать только цифры и точки (строка 197).
· Размер аргумента должен быть достаточным, чтобы переполнить буфер. Например, в нереентерабельном семействе функций gethostbyname*() первоначально происходит выделение буфера при помощи вызова malloc(1024).
· Аргумент должен быть успешно обработан функцией inet_aton() как IPv4-адрес (строка 143), или функцией inet_pton() как IPv6-адрес.
· Невозможно сделать так, чтобы аргумент, состоящий только из цифр и точек, был успешно обработан функцией inet_pton() как IPv6-адрес (поскольку символ «:» запрещен). Таким образом, невозможно добиться переполнения буфера через функцию gethostbyname2() или gethostbyname2_r(), если аргумент, связанный с семейством адресов, равен AF_INET6.
· Вывод: единственный способ добиться переполнения – использовать функцию inet_aton(). Аргумент hostname должен быть в одной из следующих форм: "a.b.c.d", "a.b.c", "a.b" или "a", где a, b, c, d – беззнаковые целые числа не более 0xfffffffful, успешно сконвертированные (без целочисленного переполнения) функцией strtoul() в десятичную или восьмеричную систему счисления (но не шестнадцатеричную систему, поскольку символы ‘x’ и ‘X’ запрещены).
Уровень угрозы
Степень влияния уязвимости значительно снижена по следующим причинам:
· Уже существует патч (с 21 мая 2013 года), который был протестирован в версии glibс-2.18, выпущенной 12 августа 2013 года.
[BZ #15014]
o nss/getXXbyYY_r.c (INTERNAL (REENTRANT_NAME))
[HANDLE_DIGITS_DOTS]: установка переменной any_service, когда обработка цифро-точек завершена успешно
o nss/digits_dots.c (__nss_hostname_digits_dots): удалены лишние объявления переменных и перераспределение буфера при обработке аргумента как IPv6-адреса. Всегда устанавливается NSS-статус (Name Service Switch, Диспетчер Службы Имен) при вызове из реентерабельных функций. Если буфер слишком маленький, используется NETDB_INTERNAL вместо TRY_AGAIN. Корректное вычисление нужного размера буфера.
o nss/Makefile (проверки): добавлена проверка цифр и точек.
o nss/test-digits-dots.c: новая проверка.
· Семейство функций gethostbyname*() устарело; с появлением IPv6 в современных приложениях используется функция getaddrinfo().
· Многие программы, особенно бинарные, доступные локально, файлы SUID, используют функцию gethostbyname() только если предварительный вызов inet_aton() завершился неудачно. Однако, для того чтобы выполнить переполнение, последующий вызов должен завершиться удачно (см. выше условия на аргумент hostname). Подобный вариант развития событий невозможен, и такие программы безопасны.
· Большинство других программ, особенно серверные приложения доступные удаленно, используют функцию gethostbyname() для выполнения проверок с использованием обратной зоны (Forward-Confirmed Reverse DNS, FCrDNS). Подобные программы в целом безопасны, поскольку параметр hostname, передаваемый в функцию gethostbyname() уже обработан программным обеспечением DNS сервера:
o «строка из текстовых меток, каждая из которых содержит до 63 8-битных октетов, разделенных точками. Суммарная строка содержит не более 255 октетов». Подобный расклад делает невозможным удовлетворение условия «не более 1 КБ» (см. выше рассуждения об условиях аргумента, где рассматривается вызов malloc(1024))
o На самом деле, DNS resolver библиотеки glibc может формировать имена хостов до (почти) 1025 знаков (если используются битовые строки и специальные непечатаемые символы). Но в этом случае имя хоста будет содержать обратные слеши ('\\'), а для успешной эксплуатации уязвимости имя хоста должно состоять только из символов и точек.
Анализ конкретных примеров
В этом разделе мы проанализируем реальные приложения, использующие семейство функций gethostbyname*(). Однако вначале будет приведена тестовая программа, проверяющая систему на предмет присутствия бреши:
[user () fedora-19 ~]$ cat > GHOST.c << EOF
#include
#include
#include
#include
#include
#define CANARY "in_the_coal_mine"
struct {
char buffer[1024];
char canary[sizeof(CANARY)];
} temp = { "buffer", CANARY };
int main(void) {
struct hostent resbuf;
struct hostent *result;
int herrno;
int retval;
/*** strlen (name) = size_needed - sizeof (*host_addr) - sizeof (*h_addr_ptrs) - 1; ***/
size_t len = sizeof(temp.buffer) - 16*sizeof(unsigned char) - 2*sizeof(char *) - 1;
char name[sizeof(temp.buffer)];
memset(name, '0', len);
name[len] = '\0';
retval = gethostbyname_r(name, &resbuf, temp.buffer, sizeof(temp.buffer), &result, &herrno);
if (strcmp(temp.canary, CANARY) != 0) {
puts("vulnerable");
exit(EXIT_SUCCESS);
}
if (retval == ERANGE) {
puts("not vulnerable");
exit(EXIT_SUCCESS);
}
puts("should not happen");
exit(EXIT_FAILURE);
}
EOF
[user () fedora-19 ~]$ gcc GHOST.c -o GHOST
On Fedora 19 (glibc-2.17):
[user () fedora-19 ~]$ ./GHOST
Vulnerable
On Fedora 20 (glibc-2.18):
[user () fedora-20 ~]$ ./GHOST
not vulnerable
Какие операционные системы содержат уязвимость?
Первая уязвимая версия GNU C Library - glibc-2.2, выпущенная 10 ноября 2000 года. Мы нашли ряд факторов, снижающих уровень угрозы данной бреши. В частности, мы обнаружили, что уязвимость была исправлена 21 мая 2013 года (между выпусками glibc-2.17 и glibc-2.18). К сожалению, в тот момент уязвимость была недооценена, вследствие чего многие дистрибутивы остались незащищенными, включая Debian 7 Wheezy, Red Hat Enterprise Linux 6 & 7, CentOS 6 & 7,Ubuntu 12.04.
Потенциально уязвимые службы
· Procmail
· Exim
· Pppd
· Clockdiff
Получить список сервисов, использующих библиотеки GNU C, можно при помощи следующей команды:
lsof | grep libc | awk '{print $1}' | sort | uni
Исправление уязвимости в Centos/RHEL/Fedora 5,6,7:
yum update glibc
sudo restart
Исправление уязвимости в Ubuntu:
sudo apt-get update
sudo apt-get dist-upgrade
sudo restart
В Матрице безопасности выбор очевиден