Анализ уязвимости CVE-2015-0235 (Glibc Ghost Vulnerability)

Анализ уязвимости CVE-2015-0235 (Glibc Ghost Vulnerability)

Переполнение буфера в куче в функции __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

Ученые доказали: чтение нашего канала продлевает жизнь!

Ладно, не доказали. Но мы работаем над этим

Поучаствуйте в эксперименте — подпишитесь