skip to content
z10 blog

File UAF & dirty pageflags

/ 6 min read

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占用对象
  • 往 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_WRITE
if ((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 P
before: 0 0 1 0 0 1 0 1 <- R/W=0, P=1
after: 0 0 1 0 0 1 1 1 <- R/W=1, P 仍为 1