内容简介:初学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,等函数的具体实现方式则是由驱动来决定,简而言之,如下图所示
-
应用层函数与模块函数之间是如何调用的?从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)
为了得到完整流程,我们还需要了解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,因此整个流程为
尽管上述流程可能对于解决babydrive这道题意义不大,但是理解他们是如何怎么工作的总归是有好处的,就像丁佬说的:不要把ctf当成“应试教育”。
分析
再回到这个题目,M4x大佬也对exp写了注释,我就不在此班门弄斧,只是针对exp写写自己的理解
int fd1 = open("/dev/babydev", 2); int fd2 = open("/dev/babydev", 2);
ioctl(fd1, 0x10001, 0x8a);
close(fd1);//kfree以后的内存会加入缓存,受slab分配器分配; int pid = fork();
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
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。