CVE-2015-7547溢出漏洞的简单分析与调试 glibc getaddrinfo
2016-04-27 11:07
190 查看
bug:说明https://www.seebug.org/vuldb/ssvid-90749
漏洞编号:SSV-90749
披露/发现时间:2015-07-13
提交时间:2016-02-17
漏洞等级:
漏洞类别:缓冲区溢出
CVE-ID:CVE-2015-7547
CNNVD-ID:补充
CNVD-ID:补充
漏洞作者:未知
提交者:Root
ZoomEye Dork:补充
影响组件:Glibc (> 2.9)
Glibc是GNU发布的LIBC库的C运行库,Glibc是Linux系统中最底层的API,基本其它任何运行库都会依赖于Glibc。Glibc除了封装Linux操作系统所提供的系统服务外,还提供了其它的必要服务的实现。由于 Glibc 几乎包含所有的 UNIX 通行的标准,可以说是操作系统重要支撑库。
Google 的安全研究团队近日披露了glibc getaddrinfo 溢出漏洞,所有Debian 系列、Red Hat 系列的Linux 发行版,只要 glibc 版本大于 2.9 就会受到影响。
贡献者 k0Sh1 共获得 0.1KB
Glibc是GNU发布的LIBC库的C运行库,Glibc是Linux系统中最底层的API,基本其它任何运行库都会依赖于Glibc。Glibc除了封装Linux操作系统所提供的系统服务外,还提供了其它的必要服务的实现。由于 Glibc 几乎包含所有的 UNIX 通行的标准,可以说是操作系统重要支撑库。
Glibc中的 DNS 解析器中存在基于栈的缓冲区溢出漏洞,当在程序中调用
该漏洞影响Glibc 2.9以后的所有版本,虽然可以进行远程执行攻击,攻击者还需要解决绕过ASLR系统安全机制。
Google提供的POC由两部分组成:
执行
执行编译好的
实测其它调用Glibc的程序也会因查询域名导致崩溃。伪造DNS服务器发出的POC数据,在TCP DNS数据中包含了大量字符“B”,如下 :
使用IDA远程调试 Debian 系统上的CVE-2015-7547-CLIENT,在调用Glibc的
由于产生溢出覆盖,
栈空间中被覆盖的数据如下:
Glibc中导致此漏洞的函数调用顺序如下:
存在溢出漏洞的缓冲区是在_nss_dns_gethostbyname4_r函数中申请的。
可以看到在_nss_dns_gethostbyname4_r函数中,使用alloca函数申请了2048字节的内存空间。alloca函数的功能是动态开辟栈地址空间,但如果参数是个固定大小的值,汇编代码就生成为把ESP减去固定值。调试分析栈的布局可以发现,host_buffer等局部变量是处在栈的高地址,alloca分配的内存是处在栈的低地址,这2048字节被溢出之后会覆盖掉host_buffer等变量。
从以上两图可以看出,进入_nss_dns_gethostbyname4_r函数时,返回地址所在栈中的位置是0xBFFFF560。而当完成溢出覆盖导致访问异常时,此返回地址处的值已经被改写为0x42424242。
_nss_dns_gethostbyname4_r函数中调用了__libc_res_nsearch函数进行实际域名查询,把局部变量host_buffer的栈地址作为参数传递进去,用于保存DNS服务器数据的实际存储地址。最终会调用到send_vc函数,在接收大于2048字节的数据之前,本应该在判断缓冲区大小不够时去分配更大的堆内存,但由于存在一段不太成熟的测试代码结果造成了逻辑错误,使得判断缓冲区过小的条件永远不成立,这样就不会去分配大内存,导致数据保存到alloca分配的栈内存中,造成缓冲区溢出。在最新发布的glibc
2.23版补丁中,这段不成熟的代码已被删掉,解决了此漏洞。
POC导致程序崩溃的原因,是由于出现缓冲区溢出后,在__libc_res_nquery函数中会访问host_buffer指针所指向的地址,但此值已经被覆盖为0x42424242,是不可访问的地址,需要把这个值覆盖为一个可访问地址。
为了实现漏洞利用,要覆盖_nss_dns_gethostbyname4_r函数的返回地址。但是在此函数返回之前,还要进行一次free的操作。会判断host_buffer指针是否还是alloca分配的栈地址,如果被改变了,就说明又重新分配了堆内存,需要进行内存释放。但如果此变量被溢出覆盖成其它值了,就会导致释放这个非堆内存地址时,出现程序异常,不能继续加载返回地址。所以解决的办法是,在溢出覆盖后要么不改变这个指针的栈地址值,要么修改为一个有效的堆块起始地址。Glibc模块在函数代码中没有进行栈溢出检查,之后即可在函数返回时控制程序流程。
但是在开启地址随机化的情况下,如果没有办法泄露内存地址布局,单独靠这一漏洞是无法成功利用的。
在回溯过程中,我们需要着重观察的是,究竟是何时栈中被畸形字符串覆盖,又是在何处,导致畸形字符串的读取。
首先我们就从离崩溃现场已知最远端入手,进行分析。根据bt回溯的信息,我们可以看到nss_dns_gethostbyname4_r是nss_dns/dns-host.c中的函数,这个.c文件对应的动态链接库是libnss_dns.so.2,那么我们需要在加载动态链接库后对这个函数下断点,我们使用gdb中的catch load libnss_dns.so.2对动态链接库加载进行跟踪。
程序中断后,说明动态链接库已经被加载,这时,我们就可以给_nss_dns_gethostbyname4_r下断点了。
顺利在入口处断了下来,这时我们继续按c,进行continue操作发现直接到达漏洞现场,这个过程就不展示了,可以在跟踪调试时进行,这说明进入此函数是漏洞触发前唯一一次调用到_nss_dns_gethostbyname4_r函数的位置,我们通过bt来观察一下。
整个过程调用非常清晰,#3位置在主函数里,紧接着#2调用了我们的漏洞函数getaddrinfo,调用后某个位置我们调用了nss_dns_gethostbyname4_r函数,在到达此函数时,我们在poc端进行观察,发现poc并没有发送畸形字符串,在此函数入口,我们通过参数观察,也没有看到有畸形字符串加载进来。
这一点说明在getaddrinfo函数到nss_dns_gethostbyname之间没有涉及到畸形字符串获取,也就是说和漏洞无关,那么我们可以跳过这段调试,直接从_nss_gethostbyname4_r入手继续寻找。
接下来,我们通过最开始的bt堆栈调用,对后面几个函数进行分析,如果想在之后的调用位置下断点,需要继续对libresolv.so.2的加载进行跟踪,那么接下来,为了能够快速定位,我们就利用最开始回溯堆栈调用给予的信息,对#0,#1,#2三处下断点,首先利用catch load libresolv.so.2对动态链接库下断点,中断后,我们首先来到第一个#2位置。
可以看到,此函数调用时,还是我们程序对应的地址内容,那么接下来,到达#1位置。
可以看到,此时还是正常,接下来来到#0位置。
可以看到此时依然正常,这说明漏洞位置就出现在libc_res_nquery函数中,那么我们接下来,在对此函数进行跟踪分析之前,我们来通过源码来总结一下之前的调用过程。
可以看到这里为host_buffer作为querybuf开辟了2048字节的缓冲区,这也是后面漏洞在res_nquery形成的关键点。我将几次函数调用写在一起,省略了部分过程(毕竟不重要),这里我们还观察一下libc_res_nsearch调用的第五个参数,也就是2048空间对应的地址位置,接下来。
还记得刚才我们提到的第五个参数吗,就是现在的*answerp,紧接着继续调用到最后的处理函数。
还是answer变量值得关注,接下来的分析中会提到这一点,这个answer函数对应的位置就是已经分配的2048空间,而在函数进行read操作时,并没有对DNS返回的字符串畸形检查,而直接拷贝字符串了到数组空间了!
那么进入到res_nquery之后,我们需要对这个函数进行单步跟踪分析,因为一直到这个函数前,PoC端都没有反应,可见此时还是在本机进行了一些读取操作,后面查询操作时,才涉及到和DNS交互。单步跟踪,在某函数位置发现了问题。
我们进入到一处res_nsend函数,在进入前一切还正常,我们直接通过finish来执行到函数返回位置。
在代码区,我们可以看到现在所处的位置是0xb7df191b的位置,而在这个位置上面的地址,执行了call __libc_res_nsend函数,当函数返回后,我们发现在栈中bfffeb6c的位置,出现了我们的畸形字符串B,而PoC端此时也执行了发送操作。我们来看一下bfffeb6c此时的值。
已经覆盖了大量的42424242,那么我们可以定位出现问题的地方在__libc_res_nsend中。在res_query.c中,我们可以看到res_nquery函数对res_nsend的调用。而且也只有这一处调用了res_nsend。
接下来,我们要着重关注一下libc_res_nsend函数,首先我们跟踪调试时发现程序会进入一处if语句判断,进入send_vc和send_dg函数,在send_vc函数中发现了socket和connect连接语句,在连接语句执行结束时,poc端提示connect 127.0.0.1,也就是执行了连接操作。
连接后,我们继续单步跟进,poc端收到了tcp的请求,同时,glibc接收到了畸形字符串,通过read函数读取,我们可以来观察一下读取前后的情况,在此之前,我们通过bt观察一下某个之前提到的重点变量,就是保存了2048缓冲区的重点变量。
这里我们要好好分析一下,首先是#4处的answer,地址是0xbfffe340,之前我们提到过,这里时开辟的2048长度地址的缓冲区,后面的anslen=0x800也是长度,2048,接下来在#3中,answer地址没有变化继续传递,接下来在res_nquery中,依然没有变化,最后到达关键函数send_vc的时候,我们可以看到ansp=0xbfffd65c,这个地址非常有意思,首先在函数入口处,我们可以看一下这个地址的中存放的值。
还是0xbfffe340,那么这个地址很有可能是地址指针的指针,也就是类似于**ansp这样的形式!接下来这个值是如何传递的呢,我们可以分析一下。请注意我单行的注释。
看到这里,我们基本可以分析出来为什么PoC要发送两次,而在第二次中,加上了2300个'B',也就是说在第二次接收时,2048缓冲区对应的变量会赋值给即将接收字符串的缓冲区,而此时,没有对这个缓冲区要接收内容的长度进行处理,从而导致了超长串覆盖,函数返回后,某个地址被覆盖导致dns请求崩溃。
接下来我们可以看一下read前后,缓冲区的变化。
最后我们可以来看一下补丁后的对比
可以看到,在官方修复的2.23版本说明中,这里将不再采用静态缓冲区2048,而是会根据用户申请缓冲区的大小来重新分配缓冲区。
由于glibc 2.9 是在2008年发行的,所以大量Linux 系统都会受到该漏洞影响。若一旦绕过内存防护技术,则该漏洞可以成为一大杀器。被劫持的DNS server进行中间人攻击,可直接批量获取大量主机权限。
利用ldd 命令查看C 库函数版本如下:
有趣的是,早在去年的7月份,就有研究人员公布了有关这一漏洞的信息,但当时 此漏洞并没有得到重视。
根据目前的调查情况我们认为此漏洞的级别该视为高危漏洞,Glibc应用于众多Linux发行版本中,所以此类漏洞影响范围十分广泛。该漏洞影响Glibc 2.9以后的所有版本。
建议广大用户尽快给操作系统打补丁,该漏洞存在于resolv/res_send.c文件中,当getaddrinfo()函数被调用时会触发该漏洞。技术人员可以限制TCP DNS响应包字节的大小,并丢弃超过512字节的UDP DNS数据包来缓解该问题。
有趣的是,早在去年的7月份,就有研究人员公布了有关这一漏洞的信息,但当时 此漏洞并没有得到重视。根据目前的调查情况我们认为此漏洞的级别该视为高危漏洞,glibc应用于众多Linux发行版本中,所以此类漏洞影响范围十分广泛。该漏洞影响glibc 2.9到2.22的所有版本。
CVE-2015-7547:
glibc getaddrinfo stack-based buffer overflow
CVE-2015-7547 补丁
紧急!Linux
底层函数库“glibc”再现重大安全漏洞!多个 Linux 发行版受影响
Linux Glibc 函数库漏洞分析(CVE-2015-7547)
共 1
兑换了
贡献者 Root 共获得 0KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#!/usr/bin/python
#
# Copyright 2016 Google Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0 #
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Authors:
# Fermin J. Serna <fjserna@google.com>
# Gynvael Coldwind <gynvael@google.com>
# Thomas Garnier <thgarnie@google.com>
import
socket
import
time
import
struct
import
threading
IP
=
'127.0.0.1'
# Insert your ip for bind() here...
ANSWERS1
=
184
terminate
=
False
last_reply
=
None
reply_now
=
threading.Event()
def
dw(x):
return
struct.pack('>H',
x)
共 0 兑换
临时解决方案
该漏洞存在于
DNS响应包字节的大小,并丢弃超过512字节的UDP DNS数据包来缓解该问题。
官方解决方案
官方在 2016年2月16日发布了补丁,建议广大用户尽快给操作系统打补丁
点这里查看 CVE-2015-7547 补丁信息
防护方案
暂无防护方案
漏洞编号:SSV-90749
披露/发现时间:2015-07-13
提交时间:2016-02-17
漏洞等级:
漏洞类别:缓冲区溢出
CVE-ID:CVE-2015-7547
CNNVD-ID:补充
CNVD-ID:补充
漏洞作者:未知
提交者:Root
ZoomEye Dork:补充
影响组件:Glibc (> 2.9)
概要
Glibc是GNU发布的LIBC库的C运行库,Glibc是Linux系统中最底层的API,基本其它任何运行库都会依赖于Glibc。Glibc除了封装Linux操作系统所提供的系统服务外,还提供了其它的必要服务的实现。由于 Glibc 几乎包含所有的 UNIX 通行的标准,可以说是操作系统重要支撑库。Google 的安全研究团队近日披露了glibc getaddrinfo 溢出漏洞,所有Debian 系列、Red Hat 系列的Linux 发行版,只要 glibc 版本大于 2.9 就会受到影响。
漏洞详情
贡献者 k0Sh1 共获得 0.1KB
1. 漏洞概要
Glibc是GNU发布的LIBC库的C运行库,Glibc是Linux系统中最底层的API,基本其它任何运行库都会依赖于Glibc。Glibc除了封装Linux操作系统所提供的系统服务外,还提供了其它的必要服务的实现。由于 Glibc 几乎包含所有的 UNIX 通行的标准,可以说是操作系统重要支撑库。Glibc中的 DNS 解析器中存在基于栈的缓冲区溢出漏洞,当在程序中调用
Getaddrinfo函数时,攻击者自定义域名或是通过中间人攻击利用该漏洞控制用户系统。比如攻击者向用户发送带有指向恶意域名的链接的邮件,一旦用户点击该链接,攻击者构造合法的DNS请求时、以过大的DNS数据回应便会形成堆栈缓存区溢出并执行远程代码,达到完全控制用户操作系统。
该漏洞影响Glibc 2.9以后的所有版本,虽然可以进行远程执行攻击,攻击者还需要解决绕过ASLR系统安全机制。
2. 漏洞复现
Google提供的POC由两部分组成:执行
CVE-2015-7547-POC.py作为一个伪造的DNS服务器,会向DNS客户端发送构造的验证数据,包含超长字符串。
执行编译好的
CVE-2015-7547-CLIENT.c作为客户端,向此DNS服务器进行查询,会在收到数据后导致程序崩溃。实测其它调用Glibc的程序也会因查询域名导致崩溃。
实测其它调用Glibc的程序也会因查询域名导致崩溃。伪造DNS服务器发出的POC数据,在TCP DNS数据中包含了大量字符“B”,如下 :
使用IDA远程调试 Debian 系统上的CVE-2015-7547-CLIENT,在调用Glibc的
Getaddrinfo函数时出现崩溃,崩溃现场的状态如下:
由于产生溢出覆盖,
EDX寄存器的值被控制为
0x42424242,处在未使用的地址段,导致在对
[EDX+3]进行寻址访问时造成异常。此时函数调用栈如下:
栈空间中被覆盖的数据如下:
3.漏洞原因和利用
Glibc中导致此漏洞的函数调用顺序如下:getaddrinfo (getaddrinfo.c) -> _nss_dns_gethostbyname4_r (dns-host.c) -> __libc_res_nsearch (res_query.c) -> __libc_res_nquery (res_query.c) -> __libc_res_nsend (res_send.c) -> send_vc (res_send.c)
存在溢出漏洞的缓冲区是在_nss_dns_gethostbyname4_r函数中申请的。
可以看到在_nss_dns_gethostbyname4_r函数中,使用alloca函数申请了2048字节的内存空间。alloca函数的功能是动态开辟栈地址空间,但如果参数是个固定大小的值,汇编代码就生成为把ESP减去固定值。调试分析栈的布局可以发现,host_buffer等局部变量是处在栈的高地址,alloca分配的内存是处在栈的低地址,这2048字节被溢出之后会覆盖掉host_buffer等变量。
从以上两图可以看出,进入_nss_dns_gethostbyname4_r函数时,返回地址所在栈中的位置是0xBFFFF560。而当完成溢出覆盖导致访问异常时,此返回地址处的值已经被改写为0x42424242。
_nss_dns_gethostbyname4_r函数中调用了__libc_res_nsearch函数进行实际域名查询,把局部变量host_buffer的栈地址作为参数传递进去,用于保存DNS服务器数据的实际存储地址。最终会调用到send_vc函数,在接收大于2048字节的数据之前,本应该在判断缓冲区大小不够时去分配更大的堆内存,但由于存在一段不太成熟的测试代码结果造成了逻辑错误,使得判断缓冲区过小的条件永远不成立,这样就不会去分配大内存,导致数据保存到alloca分配的栈内存中,造成缓冲区溢出。在最新发布的glibc
2.23版补丁中,这段不成熟的代码已被删掉,解决了此漏洞。
POC导致程序崩溃的原因,是由于出现缓冲区溢出后,在__libc_res_nquery函数中会访问host_buffer指针所指向的地址,但此值已经被覆盖为0x42424242,是不可访问的地址,需要把这个值覆盖为一个可访问地址。
为了实现漏洞利用,要覆盖_nss_dns_gethostbyname4_r函数的返回地址。但是在此函数返回之前,还要进行一次free的操作。会判断host_buffer指针是否还是alloca分配的栈地址,如果被改变了,就说明又重新分配了堆内存,需要进行内存释放。但如果此变量被溢出覆盖成其它值了,就会导致释放这个非堆内存地址时,出现程序异常,不能继续加载返回地址。所以解决的办法是,在溢出覆盖后要么不改变这个指针的栈地址值,要么修改为一个有效的堆块起始地址。Glibc模块在函数代码中没有进行栈溢出检查,之后即可在函数返回时控制程序流程。
但是在开启地址随机化的情况下,如果没有办法泄露内存地址布局,单独靠这一漏洞是无法成功利用的。
4. 漏洞分析(该部分内容来自用户k0sh1)
在回溯过程中,我们需要着重观察的是,究竟是何时栈中被畸形字符串覆盖,又是在何处,导致畸形字符串的读取。首先我们就从离崩溃现场已知最远端入手,进行分析。根据bt回溯的信息,我们可以看到nss_dns_gethostbyname4_r是nss_dns/dns-host.c中的函数,这个.c文件对应的动态链接库是libnss_dns.so.2,那么我们需要在加载动态链接库后对这个函数下断点,我们使用gdb中的catch load libnss_dns.so.2对动态链接库加载进行跟踪。
gdb-peda$ catch load libnss_dns.so.2 Catchpoint 1 (load) gdb-peda$ run Starting program: /root/Desktop/CVE-2015-7547-master/CVE-2015-7547-master/gclient [----------------------------------registers-----------------------------------] EAX: 0xbfffe98c --> 0xbfffeb50 ("libnss_dns.so.2") EBX: 0xb7fff000 --> 0x22f0c ECX: 0x4 EDX: 0x9 ('\t') ESI: 0x0 EDI: 0x4 EBP: 0xbfffe868 --> 0xbfffe9c8 --> 0xbfffeb88 --> 0xbfffebb8 --> 0xbffff0e8 --> 0xbffff218 --> 0xbffff268 --> 0x0 ESP: 0xbfffe800 --> 0x804bff0 --> 0xb7e04000 --> 0x464c457f EIP: 0xb7fef15a (<dl_open_worker+970>: nop) EFLAGS: 0x202 (carry parity adjust zero sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0xb7fef153 <dl_open_worker+963>: test eax,eax 0xb7fef155 <dl_open_worker+965>: je 0xb7fef15b <dl_open_worker+971> 0xb7fef157 <dl_open_worker+967>: mov eax,DWORD PTR [ebp+0x8] => 0xb7fef15a <dl_open_worker+970>: nop 0xb7fef15b <dl_open_worker+971>: mov eax,DWORD PTR [ebp+0x8] 0xb7fef15e <dl_open_worker+974>: sub esp,0xc 0xb7fef161 <dl_open_worker+977>: mov ecx,DWORD PTR [eax+0x1c] 0xb7fef164 <dl_open_worker+980>: mov edx,DWORD PTR [eax+0x18] [------------------------------------stack-------------------------------------] [------------------------------------------------------------------------------] Legend: code, data, rodata, value Catchpoint 1 Inferior loaded /lib/i386-linux-gnu/libnss_dns.so.2 /lib/i386-linux-gnu/libresolv.so.2 0xb7fef15a in dl_open_worker (a=0xbfffe98c) at dl-open.c:572 572 dl-open.c: No such file or directory.
程序中断后,说明动态链接库已经被加载,这时,我们就可以给_nss_dns_gethostbyname4_r下断点了。
gdb-peda$ delete gdb-peda$ b _nss_dns_gethostbyname4_r Breakpoint 2 at 0xb7e064d0: file nss_dns/dns-host.c, line 284. gdb-peda$ run Starting program: /root/Desktop/CVE-2015-7547-master/CVE-2015-7547-master/gclient [----------------------------------registers-----------------------------------] EAX: 0xbffff0c4 --> 0x0 EBX: 0xb7fd3000 --> 0x19cd64 ECX: 0xbfffeb27 --> 0x0 EDX: 0xb7e064d0 (<_nss_dns_gethostbyname4_r>: push ebp) ESI: 0xb7e064d0 (<_nss_dns_gethostbyname4_r>: push ebp) EDI: 0x420 EBP: 0xbffff0e8 --> 0xbffff218 --> 0xbffff268 --> 0x0 ESP: 0xbfffebac --> 0xb7efddbc (<gaih_inet+3495>: add esp,0x20) EIP: 0xb7e064d0 (<_nss_dns_gethostbyname4_r>: push ebp) EFLAGS: 0x286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0xb7e064c8 <_nss_dns_gethostbyname_r+136>: pop ebx 0xb7e064c9 <_nss_dns_gethostbyname_r+137>: ret 0xb7e064ca: lea esi,[esi+0x0] => 0xb7e064d0 <_nss_dns_gethostbyname4_r>: push ebp 0xb7e064d1 <_nss_dns_gethostbyname4_r+1>: mov ebp,esp 0xb7e064d3 <_nss_dns_gethostbyname4_r+3>: push edi 0xb7e064d4 <_nss_dns_gethostbyname4_r+4>: push esi 0xb7e064d5 <_nss_dns_gethostbyname4_r+5>: push ebx [------------------------------------stack-------------------------------------] [------------------------------------------------------------------------------] Legend: code, data, rodata, value Breakpoint 2, _nss_dns_gethostbyname4_r (name=0x8048653 "foo.bar.google.com", pat=0xbffff0c8, buffer=0xbfffebd0 "\377\002", buflen=0x420, errnop=0xbffff0c4, herrnop=0xbffff0b0, ttlp=0x0) at nss_dns/dns-host.c:284 284 nss_dns/dns-host.c: No such file or directory.
顺利在入口处断了下来,这时我们继续按c,进行continue操作发现直接到达漏洞现场,这个过程就不展示了,可以在跟踪调试时进行,这说明进入此函数是漏洞触发前唯一一次调用到_nss_dns_gethostbyname4_r函数的位置,我们通过bt来观察一下。
gdb-peda$ bt #0 _nss_dns_gethostbyname4_r (name=0x8048653 "foo.bar.google.com", pat=0xbffff0c8, buffer=0xbfffebd0 "\377\002", buflen=0x420, errnop=0xbffff0c4, herrnop=0xbffff0b0, ttlp=0x0) at nss_dns/dns-host.c:284 #1 0xb7efddbc in gaih_inet (name=<optimized out>, name@entry=0x8048653 "foo.bar.google.com", service=<optimized out>, req=0xbffff23c, pai=0xbffff1fc, naddrs=0xbffff1c4) at ../sysdeps/posix/getaddrinfo.c:862 #2 0xb7f0023e in __GI_getaddrinfo (name=<optimized out>, service=0x8048650 "22", hints=0xbffff23c, pai=0xbffff234) at ../sysdeps/posix/getaddrinfo.c:2417 #3 0x08048588 in main () #4 0xb7e4d5cb in __libc_start_main (main=0x804853b <main>, argc=0x1, argv=0xbffff314, init=0x80485d0 <__libc_csu_init>, fini=0x8048630 <__libc_csu_fini>, rtld_fini=0xb7feb210 <_dl_fini>, stack_end=0xbffff30c) at libc-start.c:289 #5 0x08048461 in _start ()
整个过程调用非常清晰,#3位置在主函数里,紧接着#2调用了我们的漏洞函数getaddrinfo,调用后某个位置我们调用了nss_dns_gethostbyname4_r函数,在到达此函数时,我们在poc端进行观察,发现poc并没有发送畸形字符串,在此函数入口,我们通过参数观察,也没有看到有畸形字符串加载进来。
这一点说明在getaddrinfo函数到nss_dns_gethostbyname之间没有涉及到畸形字符串获取,也就是说和漏洞无关,那么我们可以跳过这段调试,直接从_nss_gethostbyname4_r入手继续寻找。
接下来,我们通过最开始的bt堆栈调用,对后面几个函数进行分析,如果想在之后的调用位置下断点,需要继续对libresolv.so.2的加载进行跟踪,那么接下来,为了能够快速定位,我们就利用最开始回溯堆栈调用给予的信息,对#0,#1,#2三处下断点,首先利用catch load libresolv.so.2对动态链接库下断点,中断后,我们首先来到第一个#2位置。
gdb-peda$ b __libc_res_nsearch Breakpoint 4 at 0xb7df5240: file res_query.c, line 342. gdb-peda$ run Starting program: /opt/gclient [----------------------------------registers-----------------------------------] EAX: 0xffffffb8 EBX: 0xb7e0d000 --> 0x5ec8 ECX: 0xbfffe200 --> 0x0 EDX: 0x0 ESI: 0xb7e35940 (0xb7e35940) EDI: 0x8048653 ("foo.bar.google.com") EBP: 0xbfffea68 --> 0xbfffefa8 --> 0xbffff0d8 --> 0xbffff128 --> 0x0 ESP: 0xbfffe1cc --> 0xb7e09590 (<_nss_dns_gethostbyname4_r+192>: add esp,0x30) EIP: 0xb7df5240 (<__GI___libc_res_nsearch>: push ebp) EFLAGS: 0x292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0xb7df5238 <__GI___res_hostalias+440>: ret 0xb7df5239 <__GI___res_hostalias+441>: call 0xb7dfca50 <__stack_chk_fail_local> 0xb7df523e: xchg ax,ax => 0xb7df5240 <__GI___libc_res_nsearch>: push ebp 0xb7df5241 <__GI___libc_res_nsearch+1>: push edi 0xb7df5242 <__GI___libc_res_nsearch+2>: push esi 0xb7df5243 <__GI___libc_res_nsearch+3>: push ebx 0xb7df5244 <__GI___libc_res_nsearch+4>: call 0xb7df06e0 <__x86.get_pc_thunk.bx> [------------------------------------------------------------------------------] Legend: code, data, rodata, value Breakpoint 4, __GI___libc_res_nsearch (statp=0xb7fd6340 <_res@GLIBC_2.0>, name=0x8048653 "foo.bar.google.com", class=0x1, type=0xf371, answer=0xbfffe200 "", anslen=0x800, answerp=0xbfffea2c, answerp2=0xbfffea30, nanswerp2=0xbfffea34, resplen2=0xbfffea38, answerp2_malloced=0xbfffea3c) at res_query.c:342 342 res_query.c: No such file or directory.
可以看到,此函数调用时,还是我们程序对应的地址内容,那么接下来,到达#1位置。
gdb-peda$ b __libc_res_nquerydomain Breakpoint 5 at 0xb7df4eb0: file res_query.c, line 563. gdb-peda$ run Starting program: /opt/gclient [----------------------------------registers-----------------------------------] EAX: 0xb7fd6340 --> 0x5 EBX: 0xb7e04000 --> 0x14ed4 ECX: 0xbfffea2c --> 0xbfffe200 --> 0x0 EDX: 0x8048653 ("foo.bar.google.com") ESI: 0x3 EDI: 0xb7fd6340 --> 0x5 EBP: 0xbfffea30 --> 0x0 ESP: 0xbfffdd2c --> 0xb7df54cb (<__GI___libc_res_nsearch+651>: add esp,0x30) EIP: 0xb7df4eb0 (<__libc_res_nquerydomain>: push ebp) EFLAGS: 0x292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0xb7df4ea4 <__GI___libc_res_nquery+1716>: push eax 0xb7df4ea5 <__GI___libc_res_nquery+1717>: call 0xb7df0680 <__assert_fail@plt> 0xb7df4eaa: lea esi,[esi+0x0] => 0xb7df4eb0 <__libc_res_nquerydomain>: push ebp 0xb7df4eb1 <__libc_res_nquerydomain+1>: push edi 0xb7df4eb2 <__libc_res_nquerydomain+2>: mov edi,eax 0xb7df4eb4 <__libc_res_nquerydomain+4>: push esi 0xb7df4eb5 <__libc_res_nquerydomain+5>: push ebx [------------------------------------------------------------------------------] Legend: code, data, rodata, value Breakpoint 5, __libc_res_nquerydomain ( statp=statp@entry=0xb7fd6340 <_res@GLIBC_2.0>, name=name@entry=0x8048653 "foo.bar.google.com", domain=0x0, class=0x1, type=0xf371, answer=0xbfffe200 "", anslen=0x800, answerp=0xbfffea2c, answerp2=0xbfffea30, nanswerp2=0xbfffea34, resplen2=0xbfffea38, answerp2_malloced=0xbfffea3c) at res_query.c:563 563 res_query.c: No such file or directory.
可以看到,此时还是正常,接下来来到#0位置。
gdb-peda$ b __libc_res_nquery Breakpoint 6 at 0xb7df47f0: file res_query.c, line 124. gdb-peda$ run Starting program: /opt/gclient [----------------------------------registers-----------------------------------] EAX: 0x11 EBX: 0xb7e04000 --> 0x14ed4 ECX: 0x13 EDX: 0x5 ESI: 0x8048653 ("foo.bar.google.com") EDI: 0xb7fd6340 --> 0x5 EBP: 0x0 ESP: 0xbfffd8ac --> 0xb7df4fa1 (<__libc_res_nquerydomain+241>: add esp,0x30) EIP: 0xb7df47f0 (<__GI___libc_res_nquery>: push ebp) EFLAGS: 0x292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0xb7df47eb: xchg ax,ax 0xb7df47ed: xchg ax,ax 0xb7df47ef: nop => 0xb7df47f0 <__GI___libc_res_nquery>: push ebp 0xb7df47f1 <__GI___libc_res_nquery+1>: mov edx,0x220 0xb7df47f6 <__GI___libc_res_nquery+6>: mov ebp,esp 0xb7df47f8 <__GI___libc_res_nquery+8>: push edi 0xb7df47f9 <__GI___libc_res_nquery+9>: push esi [------------------------------------------------------------------------------] Legend: code, data, rodata, value Breakpoint 6, __GI___libc_res_nquery (statp=0xb7fd6340 <_res@GLIBC_2.0>, name=0x8048653 "foo.bar.google.com", class=0x1, type=0xf371, answer=0xbfffe200 "", anslen=0x800, answerp=0xbfffea2c, answerp2=0xbfffea30, nanswerp2=0xbfffea34, resplen2=0xbfffea38, answerp2_malloced=0xbfffea3c) at res_query.c:124 124 res_query.c: No such file or directory.
可以看到此时依然正常,这说明漏洞位置就出现在libc_res_nquery函数中,那么我们接下来,在对此函数进行跟踪分析之前,我们来通过源码来总结一下之前的调用过程。
_nss_dns_gethostbyname4_r (const char *name, struct gaih_addrtuple **pat, char *buffer, size_t buflen, int *errnop, int *herrnop, int32_t *ttlp) { …… //省略过程 …… host_buffer.buf = orig_host_buffer = (querybuf *) alloca (2048);//开辟2048空间,重要! u_char *ans2p = NULL; int nans2p = 0; int resplen2 = 0; int ans2p_malloced = 0; int olderr = errno; enum nss_status status; //调用__libc_res_nsearch int n = __libc_res_nsearch (&_res, name, C_IN, T_UNSPEC, host_buffer.buf->buf, 2048, &host_buffer.ptr, &ans2p, &nans2p, &resplen2, &ans2p_malloced);
可以看到这里为host_buffer作为querybuf开辟了2048字节的缓冲区,这也是后面漏洞在res_nquery形成的关键点。我将几次函数调用写在一起,省略了部分过程(毕竟不重要),这里我们还观察一下libc_res_nsearch调用的第五个参数,也就是2048空间对应的地址位置,接下来。
int __libc_res_nsearch(res_state statp, const char *name, /* domain name */ int class, int type, /* class and type of query */ u_char *answer, /* buffer to put answer */ int anslen, /* size of answer */ u_char **answerp, u_char **answerp2, int *nanswerp2, int *resplen2, int *answerp2_malloced) { …… 省略过程 …… //调用_libc_res_nquerydomain ret = __libc_res_nquerydomain(statp, name, NULL, class, type, answer, anslen, answerp, answerp2, nanswerp2, resplen2, answerp2_malloced);
还记得刚才我们提到的第五个参数吗,就是现在的*answerp,紧接着继续调用到最后的处理函数。
static int __libc_res_nquerydomain(res_state statp, const char *name, const char *domain, int class, int type, /* class and type of query */ u_char *answer, /* buffer to put answer */ int anslen, /* size of answer */ u_char **answerp, u_char **answerp2, int *nanswerp2, int *resplen2, int *answerp2_malloced) { …… 省略过程 …… //调用libc_res_nquery return (__libc_res_nquery(statp, longname, class, type, answer, anslen, answerp, answerp2, nanswerp2, resplen2, answerp2_malloced)); }
还是answer变量值得关注,接下来的分析中会提到这一点,这个answer函数对应的位置就是已经分配的2048空间,而在函数进行read操作时,并没有对DNS返回的字符串畸形检查,而直接拷贝字符串了到数组空间了!
那么进入到res_nquery之后,我们需要对这个函数进行单步跟踪分析,因为一直到这个函数前,PoC端都没有反应,可见此时还是在本机进行了一些读取操作,后面查询操作时,才涉及到和DNS交互。单步跟踪,在某函数位置发现了问题。
gdb-peda$ run Starting program: /root/Desktop/CVE-2015-7547-master/CVE-2015-7547-master/gclient [----------------------------------registers-----------------------------------] EAX: 0x804c728 --> 0x35000002 EBX: 0xb7e01000 --> 0x14ed4 ECX: 0x0 EDX: 0xb7fd6340 --> 0x5 ESI: 0x0 EDI: 0xb7fd6514 --> 0xffffffff EBP: 0xb7fd6340 --> 0x5 ESP: 0xbfffd5d0 --> 0xbfffd764 --> 0x1006d EIP: 0xb7df3702 (<__libc_res_nsend+354>: mov eax,DWORD PTR [esp+0x158]) EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0xb7df36f6 <__libc_res_nsend+342>: mov esi,DWORD PTR [esp+0x1c] 0xb7df36fa <__libc_res_nsend+346>: test esi,esi 0xb7df36fc <__libc_res_nsend+348>: jne 0xb7df4145 <__libc_res_nsend+2981> => 0xb7df3702 <__libc_res_nsend+354>: mov eax,DWORD PTR [esp+0x158] 0xb7df3709 <__libc_res_nsend+361>: mov esi,DWORD PTR [ebp+0x0] 0xb7df370c <__libc_res_nsend+364>: mov DWORD PTR [esp+0x9c],0x0 0xb7df3717 <__libc_res_nsend+375>: mov DWORD PTR [esp+0x74],eax 0xb7df371b <__libc_res_nsend+379>: mov eax,DWORD PTR [esp+0x4] [------------------------------------------------------------------------------] Legend: code, data, rodata, value Breakpoint 2, __libc_res_nsend (statp=0xb7fd6340 <_res@GLIBC_2.0>, buf=0xbfffd740 "\362 \001", buflen=0x24, buf2=0xbfffd764 "m", buflen2=0x24, ans=0xbfffe340 "", anssiz=0x800, ansp=0xbfffeb6c, ansp2=0xbfffeb70, nansp2=0xbfffeb74, resplen2=0xbfffeb78, ansp2_malloced=0xbfffeb7c) at res_send.c:564 564 res_send.c: No such file or directory.
我们进入到一处res_nsend函数,在进入前一切还正常,我们直接通过finish来执行到函数返回位置。
gdb-peda$ finish Run till exit from #0 __libc_res_nsend (statp=0xb7fd6340 <_res@GLIBC_2.0>, buf=0xbfffd740 "\362 \001", buflen=0x24, buf2=0xbfffd764 "m", buflen2=0x24, ans=0xbfffe340 "", anssiz=0x800, ansp=0xbfffeb6c, ansp2=0xbfffeb70, nansp2=0xbfffeb74, resplen2=0xbfffeb78, ansp2_malloced=0xbfffeb7c) at res_send.c:564 [----------------------------------registers-----------------------------------] EAX: 0xbcc EBX: 0xb7e01000 --> 0x14ed4 ECX: 0x1 EDX: 0xffffffff ESI: 0xb7fd6340 --> 0x5 EDI: 0xbfffe340 --> 0x4242006d ('m') EBP: 0xbfffd9e8 --> 0x0 ESP: 0xbfffd710 --> 0xb7fd6340 --> 0x5 EIP: 0xb7df191b (<__GI___libc_res_nquery+299>: mov DWORD PTR [ebp-0x30],eax) EFLAGS: 0x286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0xb7df1912 <__GI___libc_res_nquery+290>: push DWORD PTR [ebp-0x30] 0xb7df1915 <__GI___libc_res_nquery+293>: push esi 0xb7df1916 <__GI___libc_res_nquery+294>: call 0xb7df35a0 <__libc_res_nsend> => 0xb7df191b <__GI___libc_res_nquery+299>: mov DWORD PTR [ebp-0x30],eax 0xb7df191e <__GI___libc_res_nquery+302>: mov eax,DWORD PTR [ebp-0x40] 0xb7df1921 <__GI___libc_res_nquery+305>: add esp,0x30 0xb7df1924 <__GI___libc_res_nquery+308>: test eax,eax 0xb7df1926 <__GI___libc_res_nquery+310>: jne 0xb7df1b50 <__GI___libc_res_nquery+864> [------------------------------------stack-------------------------------------] 0000| 0xbfffd710 --> 0xb7fd6340 --> 0x5 0004| 0xbfffd714 --> 0xbfffd740 --> 0x120f2 0008| 0xbfffd718 --> 0x24 ('$') 0012| 0xbfffd71c --> 0xbfffd764 --> 0x1006d 0016| 0xbfffd720 --> 0x24 ('$') 0020| 0xbfffd724 --> 0xbfffe340 --> 0x4242006d ('m') 0024| 0xbfffd728 --> 0x10000 0028| 0xbfffd72c --> 0xbfffeb6c ('B' <repeats 200 times>...) [------------------------------------------------------------------------------] Legend: code, data, rodata, value 0xb7df191b in __GI___libc_res_nquery (statp=0xb7fd6340 <_res@GLIBC_2.0>, name=0x8048653 "foo.bar.google.com", class=0x1, type=0xf371, answer=0xbfffe340 "m", anslen=0x800, answerp=0xbfffeb6c, answerp2=0xbfffeb70, nanswerp2=0xbfffeb74, resplen2=0xbfffeb78, answerp2_malloced=0xbfffeb7c) at res_query.c:227 227 res_query.c: No such file or directory.
在代码区,我们可以看到现在所处的位置是0xb7df191b的位置,而在这个位置上面的地址,执行了call __libc_res_nsend函数,当函数返回后,我们发现在栈中bfffeb6c的位置,出现了我们的畸形字符串B,而PoC端此时也执行了发送操作。我们来看一下bfffeb6c此时的值。
0xbfffeb9c: 0x42 0x42 0x42 0x42 0x42 0x42 0x42 0x42 0xbfffeba4: 0x42 0x42 0x42 0x42 0x42 0x42 0x42 0x42 0xbfffebac: 0x42 0x42 0x42 0x42 0x42 0x42 0x42 0x42 0xbfffebb4: 0x42 0x42 0x42 0x42 0x42 0x42 0x42 0x42 0xbfffebbc: 0x42 0x42 0x42 0x42 0x42 0x42 0x42 0x42 0xbfffebc4: 0x42 0x42 0x42 0x42 0x42 0x42 0x42 0x42 0xbfffebcc: 0x42 0x42 0x42 0x42
已经覆盖了大量的42424242,那么我们可以定位出现问题的地方在__libc_res_nsend中。在res_query.c中,我们可以看到res_nquery函数对res_nsend的调用。而且也只有这一处调用了res_nsend。
int __libc_res_nquery(res_state statp, const char *name, /* domain name */ int class, int type, /* class and type of query */ u_char *answer, /* buffer to put answer */ int anslen, /* size of answer buffer */ u_char **answerp, /* if buffer needs to be enlarged */ u_char **answerp2, int *nanswerp2, int *resplen2, int *answerp2_malloced) { HEADER *hp = (HEADER *) answer; HEADER *hp2; int n, use_malloc = 0; u_int oflags = statp->_flags; …… 省略过程 …… assert (answerp == NULL || (void *) *answerp == (void *) answer); //漏洞触发函数 n = __libc_res_nsend(statp, query1, nquery1, query2, nquery2, answer, anslen, answerp, answerp2, nanswerp2, resplen2, answerp2_malloced); if (use_malloc) free (buf);
接下来,我们要着重关注一下libc_res_nsend函数,首先我们跟踪调试时发现程序会进入一处if语句判断,进入send_vc和send_dg函数,在send_vc函数中发现了socket和connect连接语句,在连接语句执行结束时,poc端提示connect 127.0.0.1,也就是执行了连接操作。
gdb-peda$ n [----------------------------------registers-----------------------------------] EAX: 0x3 EBX: 0xb7e01000 --> 0x14ed4 ECX: 0xbfffd2e0 --> 0x2 EDX: 0xb7e01000 --> 0x14ed4 ESI: 0xbfffeb78 --> 0x0 EDI: 0xb7fd6514 --> 0xffffffff EBP: 0xb7fd6340 --> 0x5 ESP: 0xbfffd2e0 --> 0x2 EIP: 0xb7df2b64 (<send_vc+244>: add esp,0x10) EFLAGS: 0x203 (CARRY parity adjust zero sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0xb7df2b5b <send_vc+235>: movzx eax,WORD PTR [eax] 0xb7df2b5e <send_vc+238>: push eax 0xb7df2b5f <send_vc+239>: call 0xb7ded620 <socket@plt> => 0xb7df2b64 <send_vc+244>: add esp,0x10 0xb7df2b67 <send_vc+247>: test eax,eax 0xb7df2b69 <send_vc+249>: mov DWORD PTR [ebp+0x1c4],eax 0xb7df2b6f <send_vc+255>: js 0xb7df312a <send_vc+1722> 0xb7df2b75 <send_vc+261>: mov edi,DWORD PTR [esp+0x48] [------------------------------------stack-------------------------------------] 0000| 0xbfffd2e0 --> 0x2 0004| 0xbfffd2e4 --> 0x1 0008| 0xbfffd2e8 --> 0x0 0012| 0xbfffd2ec --> 0xb7e433e8 --> 0x72647800 ('') 0016| 0xbfffd2f0 --> 0xb7fd8900 --> 0xb7e36000 --> 0x464c457f 0020| 0xbfffd2f4 --> 0xbfffeb74 --> 0x0 0024| 0xbfffd2f8 --> 0xbfffeb6c --> 0x804c748 --> 0x8083ab32 0028| 0xbfffd2fc --> 0xbfffd728 --> 0x10000 [------------------------------------------------------------------------------] Legend: code, data, rodata, value 725 in res_send.c Breakpoint 2, send_vc (statp=0xb7fd6340 <_res@GLIBC_2.0>, buf=0xbfffd740 "B6\001", buflen=0x24, buf2=0xbfffd764 "\t\374\001", buflen2=0x24, ansp=0xbfffd65c, anssizp=0xbfffd728, terrno=0xbfffd668, ns=0x0, anscp=0xbfffeb6c, ansp2=0xbfffeb70, anssizp2=0xbfffeb74, resplen2=0xbfffeb78, ansp2_malloced=0xbfffeb7c) at res_send.c:669 669 res_send.c: No such file or directory.
连接后,我们继续单步跟进,poc端收到了tcp的请求,同时,glibc接收到了畸形字符串,通过read函数读取,我们可以来观察一下读取前后的情况,在此之前,我们通过bt观察一下某个之前提到的重点变量,就是保存了2048缓冲区的重点变量。
gdb-peda$ bt #0 send_vc (statp=0xb7fd6340 <_res@GLIBC_2.0>, buf=0xbfffd740 "\274\206\001", buflen=0x24, buf2=0xbfffd764 "\264\316\001", buflen2=0x24, ansp=0xbfffd65c, anssizp=0xbfffd728, terrno=0xbfffd668, ns=0x0, anscp=0xbfffeb6c, ansp2=0xbfffeb70, anssizp2=0xbfffeb74, resplen2=0xbfffeb78, ansp2_malloced=0xbfffeb7c) at res_send.c:669 #1 0xb7df3c4e in __libc_res_nsend (statp=0xb7fd6340 <_res@GLIBC_2.0>, buf=0xbfffd740 "\274\206\001", buflen=0x24, buf2=0xbfffd764 "\264\316\001", buflen2=0x24, ans=0xbfffe340 "", anssiz=0x10000, ansp=0xbfffeb6c, ansp2=0xbfffeb70, nansp2=0xbfffeb74, resplen2=0xbfffeb78, ansp2_malloced=0xbfffeb7c) at res_send.c:554 #2 0xb7df191b in __GI___libc_res_nquery (statp=0xb7fd6340 <_res@GLIBC_2.0>, name=0x8048653 "foo.bar.google.com", class=0x1, type=0xf371, answer=0xbfffe340 "", anslen=0x800, answerp=0xbfffeb6c, answerp2=0xbfffeb70, nanswerp2=0xbfffeb74, resplen2=0xbfffeb78, answerp2_malloced=0xbfffeb7c) at res_query.c:227 #3 0xb7df1fa1 in __libc_res_nquerydomain ( statp=statp@entry=0xb7fd6340 <_res@GLIBC_2.0>, name=name@entry=0x8048653 "foo.bar.google.com", domain=0x0, class=0x1, type=0xf371, answer=0xbfffe340 "", anslen=0x800, answerp=0xbfffeb6c, answerp2=0xbfffeb70, nanswerp2=0xbfffeb74, resplen2=0xbfffeb78, answerp2_malloced=0xbfffeb7c) at res_query.c:594 #4 0xb7df24cb in __GI___libc_res_nsearch (statp=0xb7fd6340 <_res@GLIBC_2.0>, name=0x8048653 "foo.bar.google.com", class=0x1, type=0xf371, answer=0xbfffe340 "", anslen=0x800, answerp=0xbfffeb6c, answerp2=0xbfffeb70, nanswerp2=0xbfffeb74, resplen2=0xbfffeb78, answerp2_malloced=0xbfffeb7c) at res_query.c:381
这里我们要好好分析一下,首先是#4处的answer,地址是0xbfffe340,之前我们提到过,这里时开辟的2048长度地址的缓冲区,后面的anslen=0x800也是长度,2048,接下来在#3中,answer地址没有变化继续传递,接下来在res_nquery中,依然没有变化,最后到达关键函数send_vc的时候,我们可以看到ansp=0xbfffd65c,这个地址非常有意思,首先在函数入口处,我们可以看一下这个地址的中存放的值。
gdb-peda$ x/10x 0xbfffd65c 0xbfffd65c: 0xbfffe340 0xbfffd764 0xbfffd770 0x0000006e 0xbfffd66c: 0x000009e8 0x56cd507c 0x1d20b5f8 0x00000003 0xbfffd67c: 0x00010001 0xbfffd740
还是0xbfffe340,那么这个地址很有可能是地址指针的指针,也就是类似于**ansp这样的形式!接下来这个值是如何传递的呢,我们可以分析一下。请注意我单行的注释。
static int send_vc(res_state statp, const u_char *buf, int buflen, const u_char *buf2, int buflen2, u_char **ansp, int *anssizp,//ansp是2048缓冲区对应地址 int *terrno, int ns, u_char **anscp, u_char **ansp2, int *anssizp2, int *resplen2, int *ansp2_malloced) { const HEADER *hp = (HEADER *) buf; const HEADER *hp2 = (HEADER *) buf2; u_char *ans = *ansp;//对应地址的传递 int orig_anssizp = *anssizp; // XXX REMOVE // int anssiz = *anssizp; HEADER *anhp = (HEADER *) ans; …… …… if (statp->_vcsock < 0 || (statp->_flags & RES_F_VC) == 0) { if (statp->_vcsock >= 0) __res_iclose(statp, false); //这里建立socket连接 statp->_vcsock = socket(nsap->sin6_family, SOCK_STREAM, 0); if (statp->_vcsock < 0) { *terrno = errno; Perror(statp, stderr, "socket(vc)", errno); return (-1); } __set_errno (0); //connect操作,客户端会提示connect 127.0.0.1 if (connect(statp->_vcsock, (struct sockaddr *)nsap, nsap->sin6_family == AF_INET ? sizeof (struct sockaddr_in) : sizeof (struct sockaddr_in6)) < 0) { *terrno = errno; Aerror(statp, stderr, "connect/vc", errno, (struct sockaddr *) nsap); __res_iclose(statp, false); return (0); } statp->_flags |= RES_F_VC; } /*发送部分,无关紧要 * Send length & message */ …… /*接收部分 * Receive length & response */ int recvresp1 = 0; int recvresp2 = buf2 == NULL; uint16_t rlen16; read_len: cp = (u_char *)&rlen16; len = sizeof(rlen16); while ((n = TEMP_FAILURE_RETRY (read(statp->_vcsock, cp, (int)len))) > 0) { cp += n; if ((len -= n) <= 0) break; } if (n <= 0) { *terrno = errno; Perror(statp, stderr, "read failed", errno); __res_iclose(statp, false); /* * A long running process might get its TCP * connection reset if the remote server was * restarted. Requery the server instead of * trying a new one. When there is only one * server, this means that a query might work * instead of failing. We only allow one reset * per query to prevent looping. */ if (*terrno == ECONNRESET && !connreset) { connreset = 1; goto same_ns; } return (0); } int rlen = ntohs (rlen16); int *thisanssizp; u_char **thisansp; int *thisresplenp; if ((recvresp1 | recvresp2) == 0 || buf2 == NULL) { ……//第一次收到,无关紧要,第二次收到将进入下面的else部分 } else { if (*anssizp != MAXPACKET) { …… } else { /* The first reply did not fit into the user-provided buffer. Maybe the second answer will. */ *anssizp2 = orig_anssizp; *ansp2 = *ansp; } thisanssizp = anssizp2; thisansp = ansp2; //此时ansp2会赋值给thisansp,而此时thisansp的值是ansp thisresplenp = resplen2; } …… //此时cp的地址是bfffe340,也就是2048字节缓冲区 cp = *thisansp; 接着read参数会读取这个接收到的参数,第二次接收到时,是长度为超长的字符串,而此时,没有对这个字符串长度进行任何判断! while (len != 0 && (n = read(statp->_vcsock, (char *)cp, (int)len)) > 0){ cp += n; len -= n; }
看到这里,我们基本可以分析出来为什么PoC要发送两次,而在第二次中,加上了2300个'B',也就是说在第二次接收时,2048缓冲区对应的变量会赋值给即将接收字符串的缓冲区,而此时,没有对这个缓冲区要接收内容的长度进行处理,从而导致了超长串覆盖,函数返回后,某个地址被覆盖导致dns请求崩溃。
接下来我们可以看一下read前后,缓冲区的变化。
gdb-peda$ n [----------------------------------registers-----------------------------------] EAX: 0x8fe EBX: 0xb7e01000 --> 0x14ed4 ECX: 0xbfffd65c --> 0xbfffe340 --> 0x0 EDX: 0x10000 ESI: 0xbfffe340 --> 0x0 EDI: 0xbfffeb70 --> 0xbfffe340 --> 0x0 EBP: 0xb7fd6340 --> 0x5 ESP: 0xbfffd2f0 --> 0xb7fd8900 --> 0xb7e36000 --> 0x464c457f EIP: 0xb7df2eba (<send_vc+1098>: mov edi,DWORD PTR [edi]) EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0xb7df2eab <send_vc+1083>: mov WORD PTR [esp+0x5e],ax 0xb7df2eb0 <send_vc+1088>: cmp ax,0xb 0xb7df2eb4 <send_vc+1092>: jbe 0xb7df2fbd <send_vc+1357> => 0xb7df2eba <send_vc+1098>: mov edi,DWORD PTR [edi] 0xb7df2ebc <send_vc+1100>: jmp 0xb7df2ed6 <send_vc+1126> 0xb7df2ebe <send_vc+1102>: xchg ax,ax 0xb7df2ec0 <send_vc+1104>: movzx edx,WORD PTR [esp+0x5e] 0xb7df2ec5 <send_vc+1109>: add edi,eax [------------------------------------stack-------------------------------------] 0000| 0xbfffd2f0 --> 0xb7fd8900 --> 0xb7e36000 --> 0x464c457f 0004| 0xbfffd2f4 --> 0xbfffeb74 --> 0x10000 0008| 0xbfffd2f8 --> 0xbfffeb6c --> 0x804c748 --> 0x80818bf5 0012| 0xbfffd2fc --> 0xbfffd728 --> 0x10000 0016| 0xbfffd300 --> 0xbfffeb70 --> 0xbfffe340 --> 0x0 0020| 0xbfffd304 --> 0x0 0024| 0xbfffd308 --> 0xbfffeb74 --> 0x10000 0028| 0xbfffd30c --> 0x1 [------------------------------------------------------------------------------] Legend: code, data, rodata, value 883 in res_send.c gdb-peda$ n [----------------------------------registers-----------------------------------] EAX: 0x8fe EBX: 0xb7e01000 --> 0x14ed4 ECX: 0xbfffe340 --> 0x4242bb5e EDX: 0x8fe ESI: 0xbfffe340 --> 0x4242bb5e EDI: 0xbfffe340 --> 0x4242bb5e EBP: 0xb7fd6340 --> 0x5 ESP: 0xbfffd2f0 --> 0xb7fd8900 --> 0xb7e36000 --> 0x464c457f EIP: 0xb7df2ec0 (<send_vc+1104>: movzx edx,WORD PTR [esp+0x5e]) EFLAGS: 0x202 (carry parity adjust zero sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0xb7df2eba <send_vc+1098>: mov edi,DWORD PTR [edi] 0xb7df2ebc <send_vc+1100>: jmp 0xb7df2ed6 <send_vc+1126> 0xb7df2ebe <send_vc+1102>: xchg ax,ax => 0xb7df2ec0 <send_vc+1104>: movzx edx,WORD PTR [esp+0x5e] 0xb7df2ec5 <send_vc+1109>: add edi,eax 0xb7df2ec7 <send_vc+1111>: sub edx,eax 0xb7df2ec9 <send_vc+1113>: movzx eax,dx 0xb7df2ecc <send_vc+1116>: test ax,ax [------------------------------------stack-------------------------------------] 0000| 0xbfffd2f0 --> 0xb7fd8900 --> 0xb7e36000 --> 0x464c457f 0004| 0xbfffd2f4 --> 0xbfffeb74 ('B' <repeats 200 times>...) 0008| 0xbfffd2f8 --> 0xbfffeb6c ('B' <repeats 200 times>...) 0012| 0xbfffd2fc --> 0xbfffd728 --> 0x10000 0016| 0xbfffd300 --> 0xbfffeb70 ('B' <repeats 200 times>...) 0020| 0xbfffd304 --> 0x0 0024| 0xbfffd308 --> 0xbfffeb74 ('B' <repeats 200 times>...) 0028| 0xbfffd30c --> 0x1 [------------------------------------------------------------------------------] Legend: code, data, rodata, value 886 in res_send.c
最后我们可以来看一下补丁后的对比
*thisresplenp = rlen; /* Is the answer buffer too small? */ if (*thisanssizp < rlen) { /* If the current buffer is not the the static user-supplied buffer then we can reallocate it. */ if (thisansp != NULL && thisansp != ansp) { /* Always allocate MAXPACKET, callers expect this specific size. */ u_char *newp = malloc (MAXPACKET); if (newp == NULL) { *terrno = ENOMEM; __res_iclose(statp, false); return (0); } *thisanssizp = MAXPACKET; *thisansp = newp; if (thisansp == ansp2) *ansp2_malloced = 1;
可以看到,在官方修复的2.23版本说明中,这里将不再采用静态缓冲区2048,而是会根据用户申请缓冲区的大小来重新分配缓冲区。
5. 漏洞检测
由于glibc 2.9 是在2008年发行的,所以大量Linux 系统都会受到该漏洞影响。若一旦绕过内存防护技术,则该漏洞可以成为一大杀器。被劫持的DNS server进行中间人攻击,可直接批量获取大量主机权限。利用ldd 命令查看C 库函数版本如下:
有趣的是,早在去年的7月份,就有研究人员公布了有关这一漏洞的信息,但当时 此漏洞并没有得到重视。
根据目前的调查情况我们认为此漏洞的级别该视为高危漏洞,Glibc应用于众多Linux发行版本中,所以此类漏洞影响范围十分广泛。该漏洞影响Glibc 2.9以后的所有版本。
6. 漏洞修复
建议广大用户尽快给操作系统打补丁,该漏洞存在于resolv/res_send.c文件中,当getaddrinfo()函数被调用时会触发该漏洞。技术人员可以限制TCP DNS响应包字节的大小,并丢弃超过512字节的UDP DNS数据包来缓解该问题。有趣的是,早在去年的7月份,就有研究人员公布了有关这一漏洞的信息,但当时 此漏洞并没有得到重视。根据目前的调查情况我们认为此漏洞的级别该视为高危漏洞,glibc应用于众多Linux发行版本中,所以此类漏洞影响范围十分广泛。该漏洞影响glibc 2.9到2.22的所有版本。
7. 相关链接
CVE-2015-7547:glibc getaddrinfo stack-based buffer overflow
CVE-2015-7547 补丁
紧急!Linux
底层函数库“glibc”再现重大安全漏洞!多个 Linux 发行版受影响
Linux Glibc 函数库漏洞分析(CVE-2015-7547)
共 1
兑换了
PoC
贡献者 Root 共获得 0KB1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#!/usr/bin/python
#
# Copyright 2016 Google Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0 #
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Authors:
# Fermin J. Serna <fjserna@google.com>
# Gynvael Coldwind <gynvael@google.com>
# Thomas Garnier <thgarnie@google.com>
import
socket
import
time
import
struct
import
threading
IP
=
'127.0.0.1'
# Insert your ip for bind() here...
ANSWERS1
=
184
terminate
=
False
last_reply
=
None
reply_now
=
threading.Event()
def
dw(x):
return
struct.pack('>H',
x)
共 0 兑换
解决方案
临时解决方案该漏洞存在于
Resolv/res_send.c文件中,当
Getaddrinfo()函数被调用时会触发该漏洞。技术人员可以限制TCP
DNS响应包字节的大小,并丢弃超过512字节的UDP DNS数据包来缓解该问题。
官方解决方案
官方在 2016年2月16日发布了补丁,建议广大用户尽快给操作系统打补丁
点这里查看 CVE-2015-7547 补丁信息
防护方案
暂无防护方案
相关文章推荐
- 机器学习真的可以起作用吗?(1)
- 20159318 《网络攻防实践》第9周学习总结
- 自定义广播增加权限控制
- Android 开源项目分类汇总(上)
- 使用shape自定义button状态
- LVM原理及配置
- MySQL启动参数(三) —— table_open_cache
- wdcp_v3.0.2版本发布
- socket判断网络连接状态
- Linux CentOS 7 防火墙之具有命令行的FirewallD的基本操作
- Node.js Mac版安装教程
- iOS 8创建交互式通知
- 网页链接在 什么时候 进行跳转到哪里?
- redis基础教程
- 移除navbar下横线 和 tabbar上横线 颜色转图片
- Spring scope属性详解
- 自定义Toast
- [spoj11414] combat on a tree 解题报告
- 快速排序
- 字符串匹配KMP算法