
Dirty COW مقيد بـ shmem في نواة لينكس
إصدار نواة Linux: Linux/x86 6.0.0-rc1 (commit 37887783b3fef877bf34b8992c9199864da4afcb)
تسمح هذه الثغرة للمهاجم بكتابة محتوى عشوائي إلى صفحة ذاكرة مشتركة للقراءة فقط عن طريق استيفاء شروط الدالة can_follow_write_pte، والتي تتحقق من FOLL_FORCE وFOLL_COW وpte_dirty، باستخدام UFFDIO_CONTINUE.
/*
* FOLL_FORCE can write to even unwritable pte's, but only
* after we've gone through a COW cycle and they are dirty.
*/
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 قابلة للكتابة إذا تحقق أحد الشروط التالية:
لاستيفاء الشرط الثاني، يجب تعيين الأعلام الثلاثة التالية:
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); // __get_user_pages with FOLL_FORCE on
if (!this_len) {
if (!copied)
copied = -EIO;
break;
}
...
}
تنفّذ الدالة أعلاه عمليات القراءة/الكتابة على /proc/<pid>/mem.
باستخدام هذه الدالة، يمكنك الوصول إلى الدالة __get_user_pages مع تفعيل علم FOLL_FORCE.
على الرغم من أن FOLL_FORCE مخصص للاستخدام في ptrace، فقد ثبتت ضرورته في الالتزام (commit) التالي: https://github.com/torvalds/linux/commit/f511c0b17b081562dca8ac5061dfa86db4c66cc2
FOLL_COW
/*
* mmap_lock must be held on entry. If @locked != NULL and *@flags
* does not include FOLL_NOWAIT, the mmap_lock may be released. If it
* is, *@locked will be set to 0 and -EBUSY returned.
*/
static int faultin_page(struct vm_area_struct *vma,
unsigned long address, unsigned int *flags, bool unshare,
int *locked)
{
...
/*
* The VM_FAULT_WRITE bit tells us that do_wp_page has broken COW when
* necessary, even if maybe_mkwrite decided not to set pte_write. We
* can thus safely do subsequent page lookups as if they were reads.
* But only do so when looping for pte_write is futile: in some cases
* userspace may also be wanting to write to the gotten user page,
* which a read fault here might prevent (a readonly page might get
* reCOWed by userspace write).
*/
if ((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE))
*flags |= FOLL_COW;
return 0;
}
يمكن استخدام عملية OR مع FOLL_COW في الدالة faultin_page، وهي جزء من التصحيح الخاص بـ 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:
/*
* If we have a pending SIGKILL, don't keep faulting pages and
* potentially allocating memory.
*/
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; // try follow page again
case -EBUSY:
case -EAGAIN:
ret = 0;
fallthrough;
case -EFAULT:
case -ENOMEM:
case -EHWPOISON:
goto out;
}
BUG();
} else if (PTR_ERR(page) == -EEXIST) {
...
}
pte_dirty
يمكن استيفاء هذا الشرط بفضل الالتزام (commit) التالي: https://github.com/torvalds/linux/commit/9ae0f87d009ca6c4aab2882641ddfc319727e3db
/*
* Install PTEs, to map dst_addr (within dst_vma) to page.
*
* This function handles both MCOPY_ATOMIC_NORMAL and _CONTINUE for both shmem
* and anon, and for both shared and private VMAs.
*/
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); // set pte dirty unconditionally
if (page_in_cache && !vm_shared)
writable = false;
...
}
بفضل التصحيح أعلاه، يمكن تثبيت pte لصفحة الذاكرة المشتركة للقراءة فقط في حالة قذرة (dirty) دون قيد أو شرط.
إذا تم استيفاء هذه الأعلام الثلاثة، فستُرجع الدالة 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); // get read-only shared memory page
...
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، والتي سيتم تقديمها لاحقًا.
https://www.openwall.com/lists/oss-security/2022/08/15/1
يمكنك الاطلاع على مُعيد الإنتاج (reproducer) الخاص بـ 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، سترى الشاشة التالية.

commit: 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
من أجل الوصول إلى الدالة can_follow_write_pte مع تفعيل علم FOLL_COW، تحتاج إلى استدعاء الدالة faultin_page داخل الدالة __get_user_pages، ثم الانتقال إلى وسم retry، واستدعاء الدالة faultin_page مرة أخرى.
| madvise وread | UFFDIO_CONTINUE ioctl | pwrite |
|---|
| madvise // zap the page | ||
| shmem_fault // read fault | ||
| handle_userfault | ||
| userfaultfd_continue | ||
| mcontinue_atomic_pte | ||
| ret = shmem_getpage(inode, pgoff, &page, SGP_NOALLOC); // get page | ||
| mfill_atomic_install_pte | ||
| _dst_pte = pte_mkdirty(_dst_pte); // make pte dirty | ||
| set_pte_at(dst_mm, dst_addr, dst_pte, _dst_pte); // install pte | mem_rw | |
| access_remote_vm // with FOLL_FORCE | ||
| __get_user_pages | ||
| can_follow_write_pte // no FOLL_COW, return 0 | ||
| faultin_page | ||
| flags | ||
| retry: | ||
| follow_page_pte | ||
| madvise // zap the page | ||
| shmem_fault // read fault | ||
| handle_userfault | ||
| userfaultfd_continue | ||
| mcontinue_atomic_pte | ||
| ret = shmem_getpage(inode, pgoff, &page, SGP_NOALLOC); // get page | ||
| mfill_atomic_install_pte | ||
| _dst_pte = pte_mkdirty(_dst_pte); // make pte dirty | ||
| set_pte_at(dst_mm, dst_addr, dst_pte, _dst_pte); // install pte | ||
| can_follow_write_pte // return 1 | ||
| copy_to_user(buf, page, this_len) // write content to read-only page |