幽灵GHOST漏洞解析.docx
《幽灵GHOST漏洞解析.docx》由会员分享,可在线阅读,更多相关《幽灵GHOST漏洞解析.docx(34页珍藏版)》请在冰豆网上搜索。
![幽灵GHOST漏洞解析.docx](https://file1.bdocx.com/fileroot1/2023-2/3/d5f72dcb-9dbf-496d-ba7a-34b4df6aec1c/d5f72dcb-9dbf-496d-ba7a-34b4df6aec1c1.gif)
幽灵GHOST漏洞解析
CVE-2015-0235:
幽灵(GHOST)漏洞解析
0x01摘要
Qualys公司在进行内部代码审核时,发现了一个在GNUC库(glibc)中存在的__nss_hostname_digits_dots函数导致的缓冲区溢出漏洞。
这个bug可达可以通过gethostbyname*()函数来触发,本地和远程均可行。
鉴于它的影响,我们决定仔细分析它。
分析完成后,我们也决定以“幽灵”(GHOST)命名此漏洞。
我们的分析过程中得出的主要结论是:
-通过gethostbyname()函数或gethostbyname2()函数,将可能产生一个堆上的缓冲区溢出。
经由gethostbyname_r()或gethostbyname2_r(),则会触发调用者提供的缓冲区溢出(理论上说,调用者提供的缓冲区可位于堆,栈,.data节和.bss节等。
但是,我们实际操作时还没有看到这样的情况)。
-漏洞产生时至多sizeof(char*)个字节可被覆盖(注意是char*指针的大小,即32位系统上为4个字节,64位系统为8个字节)。
但是payload中只有数字('0'...'9'),点(“.”),和一个终止空字符('\0')可用。
-尽管有这些限制,我们依然可以执行任意的代码。
我们开发了一套完整的针对Exim邮件服务器的攻击PoC,测试中发现可以绕过所有现有保护(ASLR,PIE和NX)。
且可以通杀32位和64位的机器。
而且,在不久的将来,我们还会发布一个Metasploit的模块。
-据悉,GNUC库的第一个易受攻击版本是glibc-2.2,发布于2000年11月10日,相当有年头了。
-据了解,是有一些方法可以减轻影响的。
事实上,这个漏洞其实在2013年5月21日就已经被修复了(在glibc-2.17和glibc-2.18的发行版之间)。
不幸的是,当时它并没有被认为是一个安全威胁。
其结果是,大多数稳定版和长期支持版本现在依然暴露在漏洞影响下,比如:
Debian7(wheezy),红帽企业版Linux6和7,CentOS6和7,Ubuntu12.04。
0x02分析
存在漏洞的函数__nss_hostname_digits_dots()由glibc的非重入版本的文件:
nss/getXXbyYY.c,以及重入版本:
nss/getXXbyYY_r.c提供。
然而,这个函数的调用是由#ifdefHANDLE_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(),并且可能触发它的缓冲区溢出。
该函数的作用是:
“如果主机名是IPv4/IPv6地址,就跳过费时的DNS查找”。
glibc-2.17的代码如下:
1.int
2. __nss_hostname_digits_dots (const char *name, struct hostent *resbuf,
3. char **buffer, size_t *buffer_size,
4. size_t buflen, struct hostent **result,
5. enum nss_status *status, int af, int *h_errnop)
6. {
7.
8. if (isdigit (name[0]) || isxdigit (name[0]) || name[0] == ':
')
9. {
10. const char *cp;
11. char *hostname;
12. typedef unsigned char host_addr_t[16];
13. host_addr_t *host_addr;
14. typedef char *host_addr_list_t[2];
15. host_addr_list_t *h_addr_ptrs;
16. char **h_alias_ptr;
17. size_t size_needed;
18.
19. size_needed = (sizeof (*host_addr)
20. + sizeof (*h_addr_ptrs) + strlen (name) + 1);
21.
22. if (buffer_size == NULL)
23. {
24. if (buflen < size_needed)
25. {
26.
27. goto done;
28. }
29. }
30. else if (buffer_size !
= NULL && *buffer_size < size_needed)
31. {
32. char *new_buf;
33. *buffer_size = size_needed;
34. new_buf = (char *) realloc (*buffer, *buffer_size);
35.
36. if (new_buf == NULL)
37. {
38. goto done;
39. }
40. *buffer = new_buf;
41. }
42.
43. host_addr = (host_addr_t *) *buffer;
44. h_addr_ptrs = (host_addr_list_t *)
45. ((char *) host_addr + sizeof (*host_addr));
46. h_alias_ptr = (char **) ((char *) h_addr_ptrs + sizeof (*h_addr_ptrs));
47. hostname = (char *) h_alias_ptr + sizeof (*h_alias_ptr);
48.
49. if (isdigit (name[0]))
50. {
51. for (cp = name;; ++cp)
52. {
53. if (*cp == '\0')
54. {
55. int ok;
56.
57. if (*--cp == '.')
58. break;
59.
60. if (af == AF_INET)
61. ok = __inet_aton (name, (struct in_addr *) host_addr);
62. else
63. {
64. assert (af == AF_INET6);
65. ok = inet_pton (af, name, host_addr) > 0;
66. }
67. if (!
ok)
68. {
69.
70. goto done;
71. }
72.
73. resbuf->h_name = strcpy (hostname, name);
74.
75. goto done;
76. }
77.
78. if (!
isdigit (*cp) && *cp !
= '.')
79. break;
80. }
81. }
Ln85-86计算所需的缓冲区大小size_needed来存储三个不同的实体:
HOST_ADDR,h_addr_ptrs和name(hostname)。
Ln88-117确保缓冲区足够大:
Ln88-97对应于函数重入的情况,Ln98-117为非重入的情况。
Ln121-125处理存储四个不同实体的指针地址,HOST_ADDR,h_addr_ptrs,h_alias_ptr,和hostname。
计算size_needed时,漏掉了一个sizeof(*h_alias_ptr)-也即一个char指针的大小。
因此,strcpy的()所在的Ln157应该可以让我们写过缓冲区的末尾,至多(取决于函数strlen(name)和对齐)4个字节(32位),或8个字节(64位)。
有一个类似的strcpy()在Ln200,但是这里没有缓冲区溢出:
1.size_needed = (sizeof (*host_addr)
2. + sizeof (*h_addr_ptrs) + strlen (name) + 1);
3.
4. host_addr = (host_addr_t *) *buffer;
5. h_addr_ptrs = (host_addr_list_t *)
6. ((char *) host_addr + sizeof (*host_addr));
7. hostname = (char *) h_addr_ptrs + sizeof (*h_addr_ptrs);
8.
9. resbuf->h_name = strcpy (hostname, name);
为了在行157触发溢出,主机名参数必须符合下列要求:
-它的第一个字符必须是数字(Ln127)。
-它的最后一个字符不能是点“.”(Ln135)。
-它必须只包含数字和点(Ln197)(我们称之为“数字和点”的要求)。
-它必须足够长以溢出缓冲区。
例如,非重入的gethostbyname*()函数最开始就会通过调用malloc(1024)来分配自己的缓冲区(申请“1KB”)。
-地址必须成功地解析为IPv4地址。
该解析由INET_ATON()(Ln143)完成,或作为inet_ptonIPv6地址()(Ln147)
-经过仔细分析这两个函数,我们可以进一步完善这一“inet-aton”的要求:
inet_pton()中冒号":
"是被禁止的,而且我们不可能将带数字和点的地址解析成IPv6地址。
因此,它是不可能达到的溢出地点的,也即以参数为AF_INET6调用gethostbyname2()或gethostbyname2_r()函数族。
结论:
inet_aton()是唯一的选择,并且主机名必须具有下列形式之一:
“a.b.c.d”,“a.b.c”,“a.b”,或“a”,其中a,b,c,d,必须是无符号整数,最多0xfffffffful,可以由strtoul()成功转换为十进制或者八进制(即没有整数溢出)(但不能是十六进制,因为'x'和'X'是被禁止的)。
0x03减轻影响的因素
这个bug的影响现在显著减少了,原因是:
◆补丁已经存在(因为2013年5月21日),并在2013年8月12日发布的glibc-2.18中被应用和测试:
[BZ#15014](更新日志)
*nss/getXXbyYY_r.c(INTERNAL(REENTRANT_NAME))[HANDLE_DIGITS_DOTS]:
当数字-点解析成功时设置any_service。
*nss/digits_dots.c(__nss_hostname_digits_dots):
为IPv6地址解析时,删除多余的变量声明和缓冲的重新分配。
可重入函数调用时,总是需要设置NSS状态。
当缓冲区太小时使用NETDB_INTERNAL而不是TRY_AGAIN。
正确计算了所需大小。
*nss/Makefile(tests):
加入test-digits-dots。
*nss/test-digits-dots.c:
新测试文件。
◆gethostbyname*()函数是过时的;随着IPv6的到来,新的应用程序应该使用getaddrinfo()来代替。
◆许多程序,特别是SUID文件可本地访问时,当且仅当之前调用inet_aton()失败时,会使用gethostbyname()。
但是,就算到这里了,后续其他调用也必须成功,这样才能走到溢出的地方(“inet-aton”规定):
但是这是不可能的,所以用这样的方案的程序是安全的。
◆大多数其他的程序,尤其是可远程访问的服务器,会使用gethostbyname()来执行反查DNS(FCrDNS,也被称为full-circlereverseDNS)。
这些程序通常是安全的,因为传递到的gethostbyname()的主机名通常都已经被DNS软件预先检查了:
(RFC1123)“每个label最多有63个8位数字,由点分隔,最多总计有255个八进制数字”。
这使得它不可能满足“1KB”要求。
事实上,glibc的的DNS解析器可以产生高达(最多)1025字符的主机名(如bit-string标签,特殊的或非打印的字符)。
但这会引入反斜杠('\'),这样也会使得它不可能满足“只有数字和点”的要求。
0x04案例分析
在本节中,我们将分析真实的调用gethostbyname*()函数的例子,但我们首先介绍一个小测试程序,检查系统是否脆弱:
1.[user@...ora-19 ~]$ cat > GHOST.c << EOF
2.#include
3.#include
4.#include
5.#include
6.#include
7.
8.#define CANARY "in_the_coal_mine"
9.
10.struct {
11. char buffer[1024];
12. char canary[sizeof(CANARY)];
13.} temp = { "buffer", CANARY };
14.
15.int main(void) {
16. struct hostent resbuf;
17. struct hostent *result;
18. int herrno;
19. int retval;
20.
21. /*** strlen (name) = size_needed - sizeof (*host_addr) - sizeof (*h_addr_ptrs) - 1; ***/
22. size_t len = sizeof(temp.buffer) - 16*sizeof(unsigned char) - 2*sizeof(char *) - 1;
23. char name[sizeof(temp.buffer)];
24. memset(name, '0', len);
25. name[len] = '\0';
26.
27. retval = gethostbyname_r(name, &resbuf, temp.buffer, sizeof(temp.buffer), &result, &herrno);
28.
29. if (strcmp(temp.canary, CANARY) !
= 0) {
30. puts("vulnerable");
31. exit(EXIT_SUCCESS);
32. }
33. if (retval == ERANGE) {
34. puts("not vulnerable");
35. exit(EXIT_SUCCESS);
36. }
37. puts("should not happen");
38. exit(EXIT_FAILURE);
39.}
40.EOF
41.
42.[user@...ora-19 ~]$ gcc GHOST.c -o GHOST
43.
44.On Fedora 19 (glibc-2.17):
45.
46.[user@...ora-19 ~]$ ./GHOST
47.vulnerable
48.
49.On Fedora 20 (glibc-2.18):
50.
51.[user@...ora-20 ~]$ ./GHOST
52.not vulnerable
4.1
glibc的本身包含了几个调用gethostbyname*()的函数。
特别是,仅当第一次调用inet_aton()失败时,getaddrinfo()会调用gethostbyname2_r():
按照“inet-aton”要求,这些内部调用是安全的。
例如,
eglibc-2.13/sysdeps/posix/getaddrinfo.c:
1.at->family = AF_UNSPEC;
2. ...
3. if (__inet_aton (name, (struct in_addr *) at->addr) !
= 0)
4. {
5. if (req->ai_family == AF_UNSPEC || req->ai_family == AF_INET)
6. at->family = AF_INET;
7. else if (req->ai_family == AF_INET6 && (req->ai_flags & AI_V4MAPPED))
8. {
9. ...
10. at->family = AF_INET6;
11. }
12. else
13. return -EAI_ADDRFAMILY;
14. ...
15. }
16. ...
17. if (at->family == AF_UNSPEC && (req->ai_flags & AI_NUMERICHOST) == 0)
18. {
19. ...
20. size_t tmpbuflen = 512;
21. char *tmpbuf = alloca (tmpbuflen);
22. ...
23. rc = __gethostbyname2_r (name, family, &th, tmpbuf,
24. tmpbuflen, &h, &herrno);
25. ...
26. }
4.2-mount.nfs
类似的,mount.nfs也没有漏洞:
1.if (inet_aton(hostname, &addr->sin_addr))
2. return 0;
3. if ((hp = gethostbyname(hostname)) == NULL) {
4. nfs_error(_("%s:
can't get address for %s\n"),
5. progname, hostname);
6. return -1;
7. }
4.3-mtr
mtr也没有漏洞,因为它调用了getaddrinfo()而不是gethostbyname*()。
1.#ifdef ENABLE_IPV6
2. /* gethostbyname2() is deprecated so we'll use getaddrinfo() instead. */
3. ...
4. error = getaddrinfo( Hostname, NULL, &hints, &res );
5. if ( error ) {
6. if (error == EAI_SYSTEM)
7. perror ("Failed to resolve host");
8. else
9. fprintf (stderr, "Fail