kernel heap 2: 内核堆利用技巧综述

2025-11-07

1. kernel heap使用范式

1.1 自定义kmem_cache

cachep = (kmem_cache *)kmem_cache_create("kheap_obj", 472LL, 0LL, 84156416LL, 0LL);//大小为472
v6 = kmem_cache_alloc(cachep, 0x400CC0LL);//从这个kmem_cache中分配一个slot
kmem_cache_free(cachep, filp->private_data);//释放右侧chunk,归入cahchep中。
kmem_cache_destroy(cachep); //摧毁kmem_cache

1.2 内核接口

void *kmalloc(size_t size, gfp_t flags)//分配地址位于物理映射区域,且在物理地址上连续
void kfree(const void *objp);

void *vmalloc(unsigned long size);//分配一段虚拟地址连续的区域,但在物理地址上不一定连续
void vfree(const void *addr);

2. 内核保护机制查询

linux kernel集成了许多安全机制,查看了解当前kernel的安全机制非常重要,这告诉我们要绕过哪些防御。
cat /proc/cmdline命令能够查看内核启动时是否有设置一些安全机制:

cat /proc/cmdline 
rw rootfstype=9p rootflags=trans=virtio console=ttyS0 init=/opt/pwn.college/vm/init PATH=/run/challenge/bin:/run/dojo/bin:/root/.cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

cat /proc/cpuinfo可以查看cpu有没有开启smep/smap

cat /proc/cpuinfo 
processor       : 0
vendor_id       : AuthenticAMD
cpu family      : 6
model           : 6
model name      : QEMU Virtual CPU version 2.5+
stepping        : 3
cpu MHz         : 2500.037
cache size      : 512 KB
physical id     : 0
siblings        : 1
core id         : 0
cpu cores       : 1
apicid          : 0
initial apicid  : 0
fpu             : yes
fpu_exception   : yes
cpuid level     : 13
wp              : yes
flags           : fpu de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 syscall nx lm nopl cpuid pni cx16 hypervisor lahf_lm svm 3dnowprefetch vmmcall smep smap
bugs            : fxsave_leak sysret_ss_attrs null_seg spectre_v1 spectre_v2 spec_store_bypass
bogomips        : 5000.07
TLB size        : 1024 4K pages
clflush size    : 64
cache_alignment : 64
address sizes   : 40 bits physical, 48 bits virtual
power management:

使用dmesg | grep -iE 'kaslr|kpti|pti|isolation|smep|smap|Kernel Offset'查看内核启动过程中是否添加了保护。

3. 内核堆漏洞

通常,内核堆漏洞分为如下三种:

3.1 oob(out of boundry):堆溢出

顾名思义,其实就是在一个slot中填充多于其大小的内容,覆盖下一个slot中的其他值。
只有这个漏洞通常能够泄露下一个slot中的信息(如果有机密信息的话)

3.2 UAF

uaf无需多说了。

3.3 overlapping allocation

同理。

4. 泄露内核地址

4.1 Oops泄露kernel地址绕过kaslr

触发Oops后的情景如下:
R10的值ffffffff82a58c20为内核地址段,R12的值0xffff8880043a7000为物理映射区域,但是实际上,它会指向kernel的heap基址!

5. 内核堆利用技巧

通常情况,找到了漏洞是以远远不够的。如何利用漏洞达成目的才是重点。为了达成目的,我们需要学习漏洞的利用手段.

5.1 Heap Spraying —— anit-freelist_randomization

heap spraying 是一个常见的内核堆利用技术,中文名堆喷射。在kernel heap当中,堆的布局往往是不可知的,最主要的原因是开启了freelist_randomization选项(默认开启)
比如下图是一个slab中常见的object布局:
对于攻击者而言,无法确认下一个分配到的object位于哪个位置。(除非你的堆溢出能够溢出非常多,且覆盖正常object不会对kernel运行造成影响)。
此时可以考虑利用heap spraying技术,我们可以申请多个object,让kernel耗尽当前kmem_cache_cpu中的slab,并让其重新申请一个slab:
此时再申请受害object后,堆溢出就容易许多了。

应用场景:

你有一个堆溢出读 / 写,但是堆布局对你而言是不可知的(比如说开启了 SLAB_FREELIST_RANDOM(默认开启)),你可以预先喷射大量特定结构体,从而保证对其中某个结构体的溢出。

5.1.1 OOB+Heap Spraying破解freelist_randomization

前提:关闭kaslr,通过利用OOB漏洞,执行一次任意地址函数调用,且rdi寄存器是一个指针(不可改变),指向的内存区域可控。
办法:申请多个slot,每个slot都覆盖相邻slot存放的函数地址,执行commit_creds(rdi)。rdi指向的内存区域,抄袭init_cred的内容
事实证明,拷贝init_cred的内容后,commit_creds(rdi)仍然成立,另外内核中所谓的原子变量,其实只指导如何操作这些变量,实际存储时和正常数据没有区别
拷贝init_cred才有意义,如果开启了kaslr,这个方法就无法使用了,关键在于cred中的成员变量security的地址无法确认

5.2 modprobe_path —— 以内核权限执行任意文件

modprobe是最初由Rusty Russell编写的Linux程序,用于在Linux内核中添加可加载的内核模块。实际上,当我们在Linux内核中安装或卸载新模块时,就会执行这个程序。它的路径是一个内核全局变量,默认为/sbin/modprobe,我们可以通过运行以下命令来检查它:
modprobe_path可以利用的原因如下:
1. 当我们执行具有未知文件类型的文件时,内核将执行存储在modprobe_path路径的程序。更准确地说,如果我们针对系统未知文件签名(魔术头)的文件调用execve(),则会产生以下调用,最终调用到modprobe:

(1)do_execve()
(2)do_execveat_common()
(3)bprm_execve()
(4)exec_binprm()
(5)search_binary_handler()
(6)request_module()
(7)call_modprobe()

2. modprobe的路径(默认情况下为/sbin/modprobe)存储在内核本身的modprobe_path符号以及可写页面中。
我们可以通过读取/proc/kallsyms来获取其地址(由于KASLR机制,因此这个地址各不相同):

换句话说,如果我们有任意地址写的原语,那么可以将modprobe_path修改为我们想要执行的文件的路径,随后执行一下未知文件头的文件,即可完成以内核身份执行modprobe_path所指向的文件

5.2.1 Oops泄露内核地址+UAF修改next_ptr实现任意地址分配修改modprobe_path

前提: 开启kaslr,存在UAF漏洞。kheap中存在函数地址,可执行一次该地址的调用,且rdi寄存器是一个指针(不可改变),指向的内存区域可控。
办法:

  1. 首先利用UAF泄露free_list ptr得到内核堆地址;
  2. 然后利用UAF漏洞修改object中的free_list ptr,导致分配的内存能够操纵函数地址;
  3. 将函数地址改为0,造成kernel Oops泄露地址信息
  4. 重新利用UAF漏洞修改object中的free_list ptr,使其分配到modprobe_path附近,将modprobe_path改为我们想要的路径,比如/tmp/exp
  5. 创建/tmp/exp文件,在其中写入我们想要执行的命令

modprobe_path也是内核常用的利用手段。
modprobe_path有使用细节需要注意:

void environ_set(void){
    puts("[*] Returned to userland, setting up for fake modprobe");
    
    system("mkdir /tmp");
    system("echo '#!/bin/sh\ncp /flag /tmp/flag\nchmod 777 /tmp/flag' > /tmp/exp");//细节1:运行bash脚本前必须添加上 #!/bin/sh 否则触发了也不执行。
    system("chmod +x /tmp/exp");

    system("printf '\xff\xff\xff\xff'  > /tmp/dummy");
    system("chmod +x /tmp/dummy");
    //exit(0);
}


void get_flag(void){
    puts("[*] Run unknown file");
    system("/tmp/dummy");//细节2: 运行未知文件要写全局路径,最好不要用./

    puts("[*] Hopefully flag is readable");
    system("cat /tmp/flag");// 细节3:内核态执行/tmp/x文件时,不能直接cat /flag,因为没有文件描述符,所以看不到内容。
    exit(0);
}

5.x 堆布局构造

在kernel heap场景当中,堆布局是非常困难的。kmalloc函数会从 通用的kmalloc_kmem_cache中返回对象。然而:通用cache可以保存许多大小相似的不同对象类型,这意味着:所有进程都会通过kmalloc分配通用slot(syscall也非常经常需要分配内存)
在内核的堆当中,并不是所有slot都是平等的,slot也分三六九等!
我们需要的内核slot对象应该有如下的特性:

1. object size可控
2. 避免copy_to_user 和 copy_from_user的使用(避免先前提到的Hardened Usercopy检测)
3. 存放ojbect ptr/function ptr(覆盖后就可以任意地址执行,十分方便)

这里提到两个非常有用的结构体:msg_msgpipe_buffer
详情请看下一章节~