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代码中: