linux 内核版本:Linux/x86 6.0.0-rc1(提交 37887783b3fef877bf34b8992c9199864da4afcb)
此漏洞允许攻击者通过满足函数 can_follow_write_pte(该函数检查 FOLL_FORCE、FOLL_COW 和 pte_dirty)的条件,使用 UFFDIO_CONTINUE 向只读共享内存页面写入任意内容。
/*
* FOLL_FORCE 可以写入甚至不可写的 pte,但
* 仅在我们经过 COW 周期且 pte 为脏之后。
*/
static inline bool can_follow_write_pte(pte_t pte, unsigned int flags)
{
return pte_write(pte) ||
((flags & FOLL_FORCE) && (flags & FOLL_COW) && pte_dirty(pte));
}
上述函数决定了一个 pte 是否可写。
如果满足以下条件之一,则该 pte 被视为可写:
为了满足条件 2,需要设置以下三个标志:
FOLL_FORCE
static ssize_t mem_rw(struct file *file, char __user *buf,
size_t count, loff_t *ppos, int write)
{
...
flags = FOLL_FORCE | (write ? FOLL_WRITE : 0);
while (count > 0) {
size_t this_len = min_t(size_t, count, PAGE_SIZE);
if (write && copy_from_user(page, buf, this_len)) {
copied = -EFAULT;
break;
}
this_len = access_remote_vm(mm, addr, page, this_len, flags); // 使用 FOLL_FORCE 调用 __get_user_pages
if (!this_len) {
if (!copied)
copied = -EIO;
break;
}
...
}
上述函数对 /proc/<pid>/mem 执行读/写操作。
通过使用此函数,可以在启用 FOLL_FORCE 标志的情况下到达 __get_user_pages 函数。
尽管 FOLL_FORCE 本应用于 ptrace,但其必要性已在以下提交中得到证明:https://github.com/torvalds/linux/commit/f511c0b17b081562dca8ac5061dfa86db4c66cc2
FOLL_COW
/*
* 进入时必须持有 mmap_lock。如果 @locked != NULL 且 *@flags
* 不包含 FOLL_NOWAIT,则可能会释放 mmap_lock。如果释放,
* 则 *@locked 将被设置为 0 并返回 -EBUSY。
*/
static int faultin_page(struct vm_area_struct *vma,
unsigned long address, unsigned int *flags, bool unshare,
int *locked)
{
...
/*
* VM_FAULT_WRITE 位告诉我们 do_wp_page 在必要时已经破坏了 COW,
* 即使 maybe_mkwrite 决定不设置 pte_write。因此
* 我们可以安全地将后续的页面查找视为读取。
* 但只应在循环查找 pte_write 无效时这样做:在某些情况下
* 用户空间可能也想要写入获取的用户页,
* 而这里的读故障可能会阻止这一点(只读页可能会被用户空间写入重新 COW)。
*/
if ((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE))
*flags |= FOLL_COW;
return 0;
}
可以在 faultin_page 函数中使用与 FOLL_COW 的 OR 操作,这是 Dirty COW 补丁的一部分。
static long __get_user_pages(struct mm_struct *mm,
unsigned long start, unsigned long nr_pages,
unsigned int gup_flags, struct page **pages,
struct vm_area_struct **vmas, int *locked)
{
...
retry:
/*
* 如果我们有待处理的 SIGKILL,不要继续故障处理页面并
* 可能分配内存。
*/
if (fatal_signal_pending(current)) {
ret = -EINTR;
goto out;
}
cond_resched();
page = follow_page_mask(vma, start, foll_flags, &ctx); // can_follow_write_pte
if (!page || PTR_ERR(page) == -EMLINK) {
ret = faultin_page(vma, start, &foll_flags,
PTR_ERR(page) == -EMLINK, locked); // flags |= FOLL_COW
switch (ret) {
case 0:
goto retry; // 再次尝试追踪页面
case -EBUSY:
case -EAGAIN:
ret = 0;
fallthrough;
case -EFAULT:
case -ENOMEM:
case -EHWPOISON:
goto out;
}
BUG();
} else if (PTR_ERR(page) == -EEXIST) {
...
}
pte_dirty
由于以下提交,可以满足此条件:https://github.com/torvalds/linux/commit/9ae0f87d009ca6c4aab2882641ddfc319727e3db
/*
* 安装 PTEs,将 dst_addr(在 dst_vma 内)映射到页面。
*
* 此函数为 shmem 和 anon 处理 MCOPY_ATOMIC_NORMAL 和 _CONTINUE,
* 并且为共享和私有 VMA 处理。
*/
int mfill_atomic_install_pte(struct mm_struct *dst_mm, pmd_t *dst_pmd,
struct vm_area_struct *dst_vma,
unsigned long dst_addr, struct page *page,
bool newly_allocated, bool wp_copy)
{
...
_dst_pte = mk_pte(page, dst_vma->vm_page_prot);
_dst_pte = pte_mkdirty(_dst_pte); // 无条件设置 pte 为脏
if (page_in_cache && !vm_shared)
writable = false;
...
}
由于上述补丁,可以为只读共享内存页面安装一个处于脏状态的 pte。
如果这三个标志都满足,can_follow_write_pte 函数将返回 true。
static struct page *follow_page_pte(struct vm_area_struct *vma,
unsigned long address, pmd_t *pmd, unsigned int flags,
struct dev_pagemap **pgmap)
{
...
// true && !true == false
if ((flags & FOLL_WRITE) && !can_follow_write_pte(pte, flags)) {
pte_unmap_unlock(ptep, ptl);
return NULL;
}
page = vm_normal_page(vma, address, pte); // 获取只读共享内存页面
...
out:
pte_unmap_unlock(ptep, ptl);
return page;
no_page:
pte_unmap_unlock(ptep, ptl);
if (!pte_none(pte))
return NULL;
return no_page_table(vma, flags);
}
函数 follow_page_pte 返回只读共享内存页面。
以下是由 PoC 证明的竞争场景,稍后将介绍。
madvise 和 read 例程执行两次;第一次执行诱导 __get_user_pages 的 retry,第二次执行引导 follow_page_pte 返回由 mfill_atomic_install_pte 设置为脏的 pte 所指向的页面。
https://www.openwall.com/lists/oss-security/2022/08/15/1
您可以在上面的链接中查看 David Hildenbrand 的重现器。
然而,由于该重现器使得识别确切的竞争场景变得困难,我修改了重现器以偏向更线性的执行,并相应地修改了 Linux 内核源代码。
.config:
...
CONFIG_USERFAULTFD=y
CONFIG_HAVE_ARCH_USERFAULTFD_WP=y
CONFIG_HAVE_ARCH_USERFAULTFD_MINOR=y
...
PoC:(poc.c)
补丁:(poc_deayzl.patch)
为了测试,必须执行以下预备步骤。(david 的重现器说明中文件路径是 /tmp/foo。但当前版本中 tmpfs 不是共享内存,因此 userfaultfd 注册会失败)
sudo -s
echo "asdf" > /dev/shm/foo
chmod 0404 /dev/shm/foo
exit
运行 PoC 后,您将看到以下屏幕。

提交:https://github.com/torvalds/linux/commit/5535be3099717646781ce1540cf725965d680e7b
lore.kernel 补丁 v1:https://lore.kernel.org/linux-mm/[email protected]/#r
lore.kernel 补丁 v2:https://lore.kernel.org/all/[email protected]/T/#u
openwall:https://www.openwall.com/lists/oss-security/2022/08/08/1
openwall2:https://lists.openwall.net/linux-kernel/2022/08/08/418
为了在启用 FOLL_COW 标志的情况下到达 can_follow_write_pte 函数,需要在 __get_user_pages 函数内调用 faultin_page 函数,重定向到 retry 标签,并再次调用 faultin_page 函数。
| madvise 和 read | UFFDIO_CONTINUE ioctl | pwrite |
|---|
| madvise // 清除页面 | ||
| shmem_fault // 读故障 | ||
| handle_userfault | ||
| userfaultfd_continue | ||
| mcontinue_atomic_pte | ||
| ret = shmem_getpage(inode, pgoff, &page, SGP_NOALLOC); // 获取页面 | ||
| mfill_atomic_install_pte | ||
| _dst_pte = pte_mkdirty(_dst_pte); // 将 pte 设置为脏 | ||
| set_pte_at(dst_mm, dst_addr, dst_pte, _dst_pte); // 安装 pte | mem_rw | |
| access_remote_vm // 带有 FOLL_FORCE | ||
| __get_user_pages | ||
| can_follow_write_pte // 没有 FOLL_COW,返回 0 | ||
| faultin_page | ||
| flags | ||
| retry: | ||
| follow_page_pte | ||
| madvise // 清除页面 | ||
| shmem_fault // 读故障 | ||
| handle_userfault | ||
| userfaultfd_continue | ||
| mcontinue_atomic_pte | ||
| ret = shmem_getpage(inode, pgoff, &page, SGP_NOALLOC); // 获取页面 | ||
| mfill_atomic_install_pte | ||
| _dst_pte = pte_mkdirty(_dst_pte); // 将 pte 设置为脏 | ||
| set_pte_at(dst_mm, dst_addr, dst_pte, _dst_pte); // 安装 pte | ||
| can_follow_write_pte // 返回 1 | ||
| copy_to_user(buf, page, this_len) // 向只读页面写入内容 |