初探Linux内核UAF
初接触内核漏洞利用的学习,记录的会比较详细。这里用的是2017年全国大学生信息安全竞赛的babydriver,这是一个很简单的Linux内核UAF漏洞。这是开启了SMEP保护的,本文中我用了常规的UAF修改cred结构体来完成提权,因此并不需要绕过SMEP。
参考资料: https://ctf-wiki.github.io/ctf-wiki/pwn/linux/kernel/kernel_uaf-zh/
Linux内核的一些基础:ctf-wiki、My blog - 初探LinuxLinux VFS虚拟文件系统、My blog - Linux内核-进程、线程初探
题目
解压缩,看看有啥:
1 | mkdir babydriver |
解压出三个文件:
- boot.sh:QEMU启动脚本
- bzImage: bzImage是vmlinuz经过gzip压缩后的文件,适用于大内核,启动现代Linux系统时,实际运行的即为bzImage kernel文件。
- rootfs.cpio:文件系统镜像
解压rootfs.cpio
看看文件系统里有什么文件:
1 | mkdir fs |
有一个init
文件,用于启动内核后初始化,查看一下:
1 |
|
可以看到,insmod
将babydriver.ko
安装进了内核,这个就是存在漏洞的LKMs了,稍后要分析的就是它,先将它拿出来,稍后分析。
最后一行有一个poweroff
,有的题目会用这个命令设置定时关闭,将它删掉,重新打包后,起系统后就不会定时关掉了。这里的话没有定时关闭,可以不管他。
重新打包:
1 | find . | cpio -o --format=newc > ./rootfs.cpio |
驱动分析
将babydriber.ko
从/lib/modules/4.4.72/
拿出来。
没有开启PIE和canary,并且带有符号表。
1、babyrelease
释放掉babydev_struct
中的buf,并且没有将结构体中存储的指针和size置零。
babydev_struct
的类型定义如下,第一个字段是buf,第二个字段是buf的大小。
2、babyopen
其中,kmem_cache_alloc_trace()
在源码中的定义如下:
它在kmalloc()
中被最终调用,很可能是编译时进行了优化
因此,babyopen的功能就是申请一块0x40大小的内存,然后给babydev_struct
结构体的buf和len赋相应的值。
3、babyioctl
定义了一个命令0x10001
,释放掉babydev_struct.device_buf
中存储的buf,然后根据用户提供的size重新申请,并给babydev_struct.device_buf
重新赋值。
4、babywrite
往babydev_struct
的buf中写东西,即将用户空间的数据写进内核空间
5、babyread
从babydev_struct
的buf中读取内容到用户空间的buffer
6、abydriver_init
对设备进行了初始化工作,查一下函数的用法理解即可。
alloc_chrdev_region、cdev_init、cdev_add、_class_create、device_create
漏洞点
在babyrelease()
中,由于释放后没有将结构体指针置零,而且对设备的访问并没有互斥操作,存在UAF漏洞。
例如,如果同时打开两次设备,由于放有内存指针的babydec_struct
是全局变量,第二次打开babydev的时候,会覆盖掉第一次打开的babydev的babydev_struct
。那么如果在第一个中release掉,而在第二个中仍能这块已释放的内存其进行操作,即形成了UAF释放后重用。
UAF exploit
通过UAF,修改进程的cred结构体以提升到root权限
- 打开两次babydev设备,并通过
ioctl
将buf的大小修改为0xA8(cred结构体的大小) - release掉其中一个,fork一个新进程,则这个新进程的
cred
就会在刚才释放掉的buf中 - 同时,通过另外一个打开的babydev的write方法对这个buf进行写操作,将cred结构体的
uid
、gid
改为0,即可实现提权到root
确定cred结构体的大小
要确定cred结构体的大小,可以通过源码计算得出,注意要用题目同版本内核的源码,cred结构体的定义:
1 | // linux-4.4.27/include/linux/cred.h |
编写驱动打印内核结构体size
也可以写一个简单驱动,在module_init
的时候,用sizof()
和printk()
将大小打印出来就可以了,可以看到,一个cred结构体的大小是168字节(0xA8)
代码如下:
1 | //mydriver.c |
Makefile:
1 | # at first type on ur terminal that $(uname -r) then u will get the version.. |
注意这里的KDIR是对应版本的源码路径,直接从https://mirrors.edge.kernel.org/pub/ 下载的源码现在头文件什么的还不全,需要先在源码目录下执行:
1 | make menuconfig && make prepare && make scripts |
然后再回到mydriver的目录下,make编译即可:
但是这里还有个小问题,用题目自带的bzImage的话,我这里编译出来的ko好像加载不了,虽然内核版本是一样的都是4.4.72,报错insmod: can't insert './mydriver.ko': invalid module format
最后我自己从源码编译了一个4.4.72版本的内核,然后就可以加载了。
1 | Kernel源码目录下: |
编译完成后在arch/x86/boot/bzImage
拿到编译出来的bzImage,用这个替换掉题目给的bzImage,运行,装载mydriver:
EXP
1 | //CISCN2017-babydriver |
gcc -static -o exploit exploit.c
编译,这里用的是静态编译,因为系统环境中没有libc
将编译出的exploit重新放进文件系统中打包find . | cpio -o --format=newc > ./rootfs.cpio
可以看到,运行exploit后,从普通用户变成了root用户,提权成功。
感谢阅读 ♪(^∇^*)
- 本文链接:http://www.sunxiaokong.xyz/2020-02-11/lzx-ciscn2017-babydriver/
- 版权声明:本站文章除特别声明外,均为本站原创或翻译,采用 知识共享署名 CC BY 4.0 国际协议进行许可,转载前请务必署名及注明出处。