RSS Feed
更好更安全的互联网

CVE-2017-16943 Exim UAF漏洞分析

2017-12-01

作者:Hcamael@知道创宇404实验室

感恩节那天,meh在Bugzilla上提交了一个exim的uaf漏洞:https://bugs.exim.org/show_bug.cgi?id=2199,这周我对该漏洞进行应急复现,却发现,貌似利用meh提供的PoC并不能成功利用UAF漏洞造成crash

漏洞复现

首先进行漏洞复现

环境搭建

复现环境:ubuntu 16.04 server

然后再修改下配置文件/etc/exim/configure文件的第364行,把
accept hosts = : 修改成 accept hosts = *

PoC测试

https://bugs.exim.org/attachment.cgi?id=1050获取到meh的debug信息,得知启动参数:

PoC有两个:

  1. https://bugs.exim.org/attachment.cgi?id=1049
  2. https://bugs.exim.org/attachment.cgi?id=1052

需要先安装下pwntools,直接用pip装就好了,两个PoC的区别其实就是padding的长度不同而已

然后就使用PoC进行测试,发现几个问题:

  1. 我的debug信息在最后一部分和meh提供的不一样
  2. 虽然触发了crash,但是并不是UAF导致的crash

debug信息不同点比较:

发现的确是抛异常了,但是跟meh的debug信息在最后却不一样,然后使用gdb进行调试,发现:

根本就不是meh描述的利用UAF造成的crash,继续研究,发现如果把debug all的选项-d+all换成只显示简单的debug信息的选项-dd,则就不会抛异常了

又仔细读了一遍meh在Bugzilla上的描述,看到这句,所以猜测有没有可能是因为padding大小的原因,才导致crash失败的?所以写了代码对padding进行爆破,长度从0-0x4000,爆破了一遍,并没有发现能成功造成crash的长度。

This PoC is affected by the block layout(yield_length), so this line: r.sendline('a'*0x1250+'\x7f') should be adjusted according to the program state.

所以可以排除是因为padding长度的原因导致PoC测试失败。

而且在漏洞描述页,我还发现Exim的作者也尝试对漏洞进行测试,不过同样测试失败了,还贴出了他的debug信息,和他的debug信息进行对比,和我的信息几乎一样。(并不知道exim的作者在得到meh的Makefile和log后有没有测试成功)。

所以,本来一次简单的漏洞应急,变为了对该漏洞的深入研究

浅入研究

UAF全称是use after free,所以我在free之前,patch了一个printf:

重新编译跑一遍,发现竟然成功触发了uaf漏洞:

然后gdb调试的信息也证明成功利用uaf漏洞造成了crash:

PS: 这里说明下./build-Linux-x86_64/exim这个binary是没有patch printf的代码,/usr/exim/bin/exim是patch了printf的binary

到这里就很奇怪了,加了个printf就能成功触发漏洞,删了就不能,之后用putswrite代替了printf进行测试,发现puts也能成功触发漏洞,但是write不能。大概能猜到应该是stdio的缓冲区机制的问题,然后继续深入研究。

深入研究

来看看meh在Bugzilla上对于该漏洞的所有描述:

在这里先提一下,在Exim中,自己封装实现了一套简单的堆管理,在src/store.c中

UAF漏洞所涉及的关键函数:

  • store_get_3 堆分配
  • store_extend_3 堆扩展
  • store_release_3 堆释放

还有4个重要的全局变量:

  • chainbase
  • next_yield
  • current_block
  • yield_length
第一步

发送一堆未知的命令去调整yield_length的值,使其小于0x100。

yield_length表示的是堆还剩余的长度,每次命令的处理使用的是src/receive.c代码中的receive_msg函数

在该函数处理用户输入的命令时,使用next->text来储存用户输入,在1709行进行的初始化:

在执行1709行代码的时候,如果0x100 > yield_length则会执行到newblock = store_malloc(mlength);,使用glibc的malloc申请一块内存,为了便于之后的描述,这块内存我们称为heap1。

根据store_get_3中的代码,这个时候:

  • current_block->next = heap1 (因为之前current_block==chainbase,所以这相当于是chainbase->next = heap1)
  • current_block = heap1
  • yield_length = 0x2000
  • next_yield = heap1+0x10
  • return next_yield
  • next_yield = next_yield+0x100 = heap1+0x110
  • yield_length = yield_length - 0x100 = 0x1f00
第二步

发送BDAT 1,进入receive_msg函数,并且让receive_getc变为bdat_getc

第三步

发送BDAT \x7f

相关代码在src/smtp_in.c中的bdat_getc函数:

BDAT命令进入下面这个分支:

因为\x7F 所以sscanf获取长度失败,进入synprot_error函数,该函数同样是位于smtp_in.c文件中:

然后在synprot_error函数中有一个string_printing函数,位于src/string.c代码中:

string_printing2函数中,用到store_get, 长度为length + nonprintcount * 3 + 1,比如BDAT \x7F这句命令,就是6+1*3+1 => 0x0a,我们继续跟踪store中的全局变量,因为0xa < yield_length,所以直接使用的Exim的堆分配,不会用到malloc,只有当上一次malloc 0x2000的内存用完或不够用时,才会再进行malloc

  • 0xa 对齐-> 0x10
  • return next_yield = heap1+0x110
  • next_yield = heap1+0x120
  • yield_length = 0x1f00 - 0x10 = 0x1ef0

最后一步,就是PoC中的发送大量数据去触发UAF:

再回到receive.c文件中,读取用户输入的是1788行的循环,然后根据meh所说,UAF的触发点是下面这几行代码:

当输入的数据大于等于0x100-4时,会触发store_extend函数,next->text的值上面提了,是heap1+0x10oldsize=0x100, header_size = 0x100*2 = 0x200

然后在store_extend中,有这几行判断代码:

其中next_yield = heap1+0x120, ptr + 0x100 = heap1+0x110

因为判断的条件为true,所以store_extend返回False

这是因为在之前string_printing函数中中分配了一段内存,所以在receive_msg中导致堆不平衡了,

随后进入分支会修补这种不平衡,执行store_get(0x200)

  • return next_yield = heap1+0x120
  • next_yield = heap1+0x320
  • yield_length = 0x1ef0 - 0x200 = 0x1cf0

然后把用户输入的数据复制到新的堆中

随后执行store_release函数,问题就在这里了,之前申请的0x2000的堆还剩0x1cf0,并没有用完,但是却对其执行glibc的free操作,但是之后这个free后的堆却仍然可以使用,这就是我们所知的UAF, 释放后重用漏洞

其中,bb = chainbase->next = heap1, 而且next->text == bb + 0x10

所以能成功执行free(bb)

因为输入了大量的数据,所以随后还会执行:

  • store_extend(next->text, 0x200, 0x400)
  • store_extend(next->text, 0x400, 0x800)
  • store_extend(next->text, 0x800, 0x1000)

但是这些都不能满足判断:if (CS ptr + rounded_oldsize != CS (next_yield[store_pool]) || inc > yield_length[store_pool] + rounded_oldsize - oldsize)

所以都是返回true,不会进入到下面分支

但是到store_extend(next->text, 0x1000, 0x2000)的时候,因为满足了第二个判断0x2000-0x1000 > yield_length[store_pool], 所以又一次返回了False

所以再一次进入分支,调用store_get(0x2000)

因为0x2000 > yield_length所以进入该分支:

这里就是该漏洞的关键利用点

首先:newblock = current_block = heap1

然后:newblock = newblock->next

我猜测的meh的情况和我加了printf进行测试的情况是一样的,在printf中需要malloc一块堆用来当做缓冲区,所以在heap1下面又多了一块堆,在free了heap1后,heap1被放入了unsortbin,fd和bk指向了arena

所以这个时候,heap1->next = fd = arena_top

之后的流程就是:

  • current_block = arena_top
  • next_yield = arena_top+0x10
  • return next_yield = arena_top+0x10
  • next_yield = arena_top+0x2010

在执行完store_get后就是执行memcpy:

上面的newtext就是store_get返回的值arena_top+0x10

把用户输入的数据copy到了arena中,最后达到了控制RIP=0xdeadbeef造成crash的效果

但是实际情况就不一样了,因为没有printf,所以heap1是最后一块堆,再free之后,就会合并到top_chunk中,fd和bk字段不会被修改,在释放前,这两个字段也是用来储存storeblock结构体的next和length,所以也是没法控制的

总结

CVE-2017-16943的确是一个UAF漏洞,但是在我的研究中却发现没法利用meh提供的PoC造成crash的效果

之后我也尝试其他利用方法,但是却没找到合适的利用链

发现由于Exim自己实现了一个堆管理,所以在heap1之后利用store_get再malloc一块堆是不行的因为current_block也会被修改为指向最新的堆块,所以必须要能在不使用store_get的情况下,malloc一块堆,才能成功利用控制RIP,因为exim自己实现了堆管理,所以都是使用store_get来获取内存,这样就只能找printf这种有自己使用malloc的函数,但是我找到的这些函数再调用后都会退出receive_msg函数的循环,所以没办法构造成一个利用链

引用

  1. Exim源码
  2. Bugzilla-2199
作者:dawu | Categories:安全研究技术分享 | Tags:

发表评论