内容简介:初学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
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
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》 这本书的介绍吧!