一、 漏洞概述
CVE漏洞链接:http://www.cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2015-7547
披露/发现时间:2015-07-13 提交时间:2016-02-17
漏洞等级:高 漏洞类别 缓冲区溢出
漏洞概述:该漏洞是Glibc中的DNS解析器中存在基于栈的缓冲区溢出漏洞,glibc是GNU发布的libc库,即c运行库。glibc是linux系统中最底层的api,几乎其它任何运行库都会依赖于glibc。glibc除了封装linux操作系统所提供的系统服务外,它本身也提供了许多其它一些必要功能服务的实现。当在程序中调用Getaddrinfo函数时,攻击者自定义域名或是通过中间人攻击利用该漏洞控制用户系统。比如攻击者向用户发送带有指向恶意域名的链接的邮件,一旦用户点击该链接,攻击者构造合法的DNS请求时、以过大的DNS数据回应便会形成堆栈缓存区溢出并执行远程代码,达到完全控制用户操作系统。通过该漏洞直接批量获取大量主机权限。
影响范围:该漏洞影响Glibc 2.9以后的所有版本,Debian、Red Hat以及更多其它Linux发行版,只要glibc版本大于2.9就会受到该溢出漏洞影响。不过虽然可以进行远程执行攻击,攻击者还需要解决绕过ASLR系统安全机制。
二、 POC环境配置
为了深入分析该漏洞首先对poc的源码的执行环境进行配置,主要是可以触发的该漏洞glibc库的编译,以及对应的服务器配置和POC源码编译与运行
Poc:在github上:https://github.com/fjserna/CVE-2015-7547
1.gilbc下载与编译
由于一般主机的glibc的库是已经修复过该漏洞的,而且系统自带的glibc是发行版的,所以在编译的是时候选用了优化参数 -O2,所以在调试的过程中会出现变量被优化无法读取以及代码运行的时候与源码的行数对不上的情况。所以需要自行下载一个glibc库进行测试。这里我组下载了2.20版本。
首先为了可以使用gdb对glibc的跟踪调试,需要执行下面两个安装指令如下:
sudo apt-get install libc6-dbg sudo apt-get source libc6-dev
然后下载glibc2.9以后的一个版本的glibc就行,这里我下载版本是2.20版本。
Glibc的下载链接:http://ftp.gnu.org/gnu/glibc/
然后编译的时候要特别注意首先要加载到/usr/local/目录下新建的目录才能安装而且要使用O1进行编译否则默认O2会进行优化无法对应调试源码。执行如下命令进行配置:
/opt/glibc220/(下载解压缩的glibc的目录)configure --prefix=/usr/local/glibc220/ --enable-debugCFLAGS="-g -O1" CPPFLAGS = "-g -O1"
不过第一次会报错,百度以后发现必须要到/usr/local/glibc220/下去执行才可以。
但继续安装又报了如下的错,按提示再安装gawk,容易配置成功。在configure执行完成之后在直接make&&make install安装就好了。
2.poc程序的编译与环境配置
该漏洞是由于接受的DNS数据缓存溢出导致的,所以POC分为两个部分一个是模拟的恶意DNS服务器py文件,用于发送过长的字符串,另一个client客户端c文件负责调用Getaddrinfo函数接受数据触发溢出。由于服务器程序编写绑定是127.0.0.1,所以需要修改本地DNS配置文件/etc/resolv.conf。将域名服务器nameserver改为127.0.0.1。需要补充一点的是Ubuntu系统定时会重置这个文件所以每次运行之前需要检查这个文件是不是127.0.0.1。
然后执行如下命令指定我自己下载存在该漏洞的glibc2.20库去编译客户端源文件。
需要特别注意的是红字的部分还需要去指定动态库加载器,否则调试的时候会有问题无法对应到源码进行逐步调试。
gcc -O0 CVE-2015-7547-client.c -o client2-g -Wl,-rpath=/usr/local/glibc-220/lib -Wl,--dynamic-linker=/usr/local/glibc-220/lib/ld-linux.so.2
编译完成后可以通过ldd命令进行检查是否用的库我自己编译的2.20版本的库
三、 漏洞成因分析
1. 寻找漏洞溢出点
为了分析漏洞产生的原因首先就要定位到漏洞最后真正溢出点,我并没有去查看网上的博客去直接获得触发对应的缓冲区变量和函数,而是尝试自己去寻找追踪函数的调用过程发现触发点的。
首先我再开启服务器端py文件不断去发送超长的字符串。然后第一次gdb先直接运行客户端程序,如下图所示,产生的段溢出的错误。并且发现问题发生在res_query函数res_query.c 264代码上。
这时我进一步想既然是缓冲区溢出的段错误肯定是由于该缓冲区溢出覆盖了原有的返回地址,所以如果这时栈上最后一条函数地址就应该是溢出缓冲区申请的函数,因为栈上函数地址的存储顺序是按调用顺序进行,所以由于栈的是向上生长的溢出的缓冲区只能覆盖栈上自己的函数地址和调用该函数之前的函数的地址。这时我调用一个bt看一下栈如上图最后的红圈所示就可以推测出该溢出的缓冲区是在_nss_dns_gethostbyname4_r申请的。后续会进行验证之前的函数地址都被覆盖0x42也就是B。
这时看一下抓包的wireshark果然服务器发了一个很长字符串的B。
通过我上一步分析可以知道程序停在res_query函数上于是重新运行在resquery上打一个断点再去查看栈就可以获取程序崩溃前完整的调用程序流如下图所示:
但实际到底为什么发生了溢出真实的溢出点是不是程序崩溃的位置呢,需要进一步分析这里我先不追踪溢出的缓冲区,只是从刚才的崩溃来分析再下一部分再验证,通过gdb告诉我程序崩溃是res_query.c文件的264行于是对应找到该源码。如下图最后的红圈所示,发现是验证hp和hp2的时候发生了错误,推测应该是溢出覆盖了这两个变量的值于是需要继续追踪分析找到lib_res_nesend函数的参数影响了hp和hp1。由于函数参数在栈上是连续的所以去追踪该函数。
于是我继续追踪libc_res_nesend函数的参数发现send_dg和send_vc会影响这个函数的所有参数如下图所示:
然后我再单步调试nsend函数发现是先执行send_dg再执行了send_vc。这样我就先得到了完整的溢出触发的流程整体如下图所示:
2.成因具体分析
2.1 缓冲区变量跟踪
通过上面的分析只是推测出了溢出发生的函数触发流程,本节要通过之前的函数分析流程还具体分析到底是哪个变量指向缓冲区发生了溢出,为什么会发生溢出,同时这里也可以验证我之前的推测的触发过程是不是正确的。
通过上一节一开始的分析推测出这个栈空间应该是在_nss_dns_gethostbyname4_r函数发生的于是我断点在这个函数单步跟踪一下。如下图所示,果然发现了host_buffer.buf申请了2048字节的栈空间记录其栈的地址,而且发现ans2p这个变量(之前分析了这个变量赋值给hp2指针最终导致了崩溃)的地址只和栈底差了0xbfffea58-(0xbfffe210+2048)只有72个字节,也从侧面说明了很可能就是这个缓冲区溢出导致了覆盖了这个变量的地址。
于是逐步追踪这个host_buffer.buf地址,对应nsearch函数的answer变量再到nquery函数的answer变量再到nsend函数的ans变量,长度变量最后对应nsend函数的anssizp变量。
2.2 send_vc和send_dg逻辑分析
然后需要注意的是send_vc和send_dg传入的是对应变量的地址从如下的nsend函数定义和对应send_dg参数使用可以看出如下表所示
定义:int__libc_res_nsend(res_state statp, const u_char *buf, int buflen, const u_char *buf2, int buflen2, u_char *ans, int anssiz, u_char **ansp, u_char **ansp2, int *nansp2, int *resplen2, int *ansp2_malloced) 调用:n = send_dg(statp, buf, buflen, buf2, buflen2, &ans, &anssiz, &terrno, ns, &v_circuit, &gotsomewhere, ansp, ansp2, nansp2, resplen2, ansp2_malloced); 定义:send_dg(res_state statp, const u_char *buf, int buflen, const u_char *buf2, int buflen2, u_char **ansp, ,//指针的指针int *anssizp int *terrno, int ns, int *v_circuit, int *gotsomewhere, u_char **anscp, u_char **ansp2, int *anssizp2, int *resplen2, int *ansp2_malloced) |
于是需要继续追踪send_dg和send_vc函数的ansp(2048字节的缓冲区)和anssizp(2048字节的长度)这些变量内部的赋值与传递关系寻找最终的溢出触发位置。
由于DNS协议TCP和UDP都用到了。其中大数据包和域名系统间的消息传递用TCP即调用send_vc函数,一般的域名查询服务调用UDP即send_dg函数。
在POC中首先服务器发一个2500UDP包,所以客户端会先调用send_dg函数。去除调一些与关心变量无关的程序,通过gdb单步跟踪发现其核心的实际执行的关键代码和逻辑如下图所示。
可以看到实际中第一次运行的时候在第一个判断的满足情况下,thisansp为空所以会赋值anscp指向空间也就是这两个指针的地址是一样的,注意此时anscp指向的空间是NULL。同时thisansp的值赋值为ansszip根据之前的分析该值为缓冲区长度2048。所以其在第二个判断的时候该长度小于MAXPACKET(65536)同时anscp是有地址的。所以满足条件下执行下面的代码块。或重新分配一个65536的堆空间给thisansp,注意到此时thisansp和anscp地址是一致的,所以anscp被指向到了65536的一个堆空间。如下图所示:
最后该函数会调用recvfrom在65536的实际堆缓冲区但标识其长度为2048,去接受服务器的数据所以此时并不会触发溢出。但是注意到anscp指向65536的堆缓冲区而ansp还是指向2048字节的栈缓冲区。
然后这3个变量又继续传递给了send_vc函数。我继续单步根据去分析其执行逻辑,发现其利用goto语句循环执行两遍的接受客户端数据的程序,但是会根据条件判断执行不同的程序路径,最终导致了溢出。关键的代码和执行路径我也画了一个图如下所示:
第一次会满足判断条件执行中间的路径,cp=thisansp=anscp指向65536的堆空间如下图所示所以第一次读取数据不会发生溢出。
但是接受程序会循环等待第二次数据,这时会执行右边的路径分支。使thisansp最终等于ansp的栈空间2048个字节,而该程序以为缓冲区长度为65536的大小,所以允许接受数据包的长度可以大于2048没有改变如下图所示。这里POC服务发送的2302个字节大小的数据最终导致了溢出。
可以看到对应地址的缓冲区都被覆盖成了0x42(B)。这也最终验证了我在第一部分推测的漏洞触发流程,通过跟踪对应的缓冲区最终发现了该缓冲区溢出,如下图所示红色标识的变量为溢出的缓冲区指针在调用一层层函数中对应的变量,可以很清楚的看出触发流程。
四、 漏洞可利用分析
由于程序有ASLR和DEP保护,最终并没有实际进行ROP等实际利用的攻击实验。该部分主要分析栈的结构和程序中的一些验证如何绕过的分析和测试。
栈溢出漏洞的关键就是覆盖函数返回地址使其执行攻击者想执行的代码的地址。于是我去查看申请溢出缓冲区的函数nss_dns_gethostbyname4_r的地址。在刚进入该函数后查看esp对应的位置开始的4个字节就是刚刚压入堆栈的该函数的返回地址,如下图所示红圈旁边的4个字节的返回地址是0xb7f14c68存储在红圈地址0xbfffea7c的位置。
可以看出该返回的地址和我申请缓冲区的地址0xbfffe210相差了2156个字节如下图所示,但是我经过前面的分析知道程序在_libc_res_nquery函数返回后验证hp和hp1的时候发生了段错误,说明在覆盖缓冲区的时候要绕过这些参数检查才能真正的利用成功。
于是我改变POC程序,让服务器发生小于等于2048个字节的数据,这时再跟踪程序到验证的地方找到hp和hp2的内容。如下图所示这时再查看缓冲区下界到返回地址之间的栈空间可以找到对应的hp和hp2如下图红圈所示。这时就可以知道hp和hp2的位置在0xbfffea58和5c的位置。
进一步跟踪程序发现在libc_res_nsearch的函数返回后会对answerp2_malloc检查是否为空。非空的话会释放ans2p仍然出错。
所以获取ans2p的地址如下图所示填充成0,就可以跳过检查。
但还是出错了所以说明hp后面的参数还是有用的,重新画一下栈的空间修如下所示:
所以重新设置服务器端发送的数据如下图所示,把原来的参数原样填进去。
终于不再报错,说明可以进去覆盖返回地址的攻击了。
五、 漏洞补丁分析
我进一步分析了该漏洞补丁程序其中有许多的补丁版本这里只说明一种可以查到源码的。其中关键就是对send_dg和send_vc的修改对send_vc修改如下图所示:
注释掉了ansp=ansp,同时当thisansp为空的条件下增加了上图绿框中的代码,重新开辟65536的堆空间给CP,这样就不会发生溢出。
同时补丁对send_dg修改如下,使thisanssizp的长度与thisansp的大小对应。